Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
64aa25f
Added Microsoft 365 Gatekeeper: SharePoint, Outlook, Calendar
jonesphillip Oct 27, 2026
aad9814
Fail closed on unresolved SharePoint links, recheck access and scope …
jonesphillip Oct 27, 2026
66019f4
Use toSorted in the two new access tests
jonesphillip Oct 27, 2026
3653323
Re-prove scope before SharePoint undos, keep version fences on follow…
jonesphillip Oct 27, 2026
6cab8a1
Keep a pending attendee removal from downgrading later actions, refus…
jonesphillip Oct 27, 2026
c2c1b5e
Re-prove scope before Outlook and Calendar undos, hold meeting follow…
jonesphillip Oct 27, 2026
bc5d626
Stop reused folder proofs renewing themselves, re-prove a listed fold…
jonesphillip Oct 27, 2026
ecd50a2
Prove a reply's or forward's source still covered before Outlook clon…
jonesphillip Oct 27, 2026
2a6db21
Refuse replacing a file renamed into another format, treat a lost lis…
jonesphillip Oct 27, 2026
5bda57f
Keep a private exception hidden while a series change is pending, pin…
jonesphillip Oct 27, 2026
863d2e8
Check every prefix Excel reads as a formula, keep pending cells withi…
jonesphillip Oct 27, 2026
251b092
Fail closed on unrecognized SharePoint access and sites settings, tol…
jonesphillip Oct 27, 2026
3b40ac7
Refuse copying a file renamed into another format since the copy was …
jonesphillip Oct 27, 2026
bbd95d6
Undo a move by where it landed the messages, hold a reply draft's sen…
jonesphillip Oct 27, 2026
b63b1e5
Keep SharePoint session helpers off the RPC surface, recheck a rename…
jonesphillip Oct 27, 2026
1b8f6d0
List every Graph permission the deploy wizard needs, put the calendar…
jonesphillip Oct 27, 2026
a171d63
Say that only the owner observes someone else's calendar, follow 303 …
jonesphillip Oct 27, 2026
9ce1f96
Restore a renamed file's old format only to the content it had when r…
jonesphillip Oct 27, 2026
be08073
Undo a cell edit by putting its range back, restore a replaced file's…
jonesphillip Oct 27, 2026
d01af2a
Fence a calendar change's undo on the version the change left, note t…
jonesphillip Oct 27, 2026
fe662e2
Recheck the connection's access before undoing a SharePoint, Outlook …
jonesphillip Oct 27, 2026
f5bf405
Undo an upload or copy only while the file is still what it created, …
jonesphillip Oct 27, 2026
3a25c7e
Move an idle folder's receive cursor forward, fence a created event's…
jonesphillip Oct 27, 2026
ad7e681
Fence a list item's undo on the version the change left, keep an abse…
jonesphillip Oct 27, 2026
f6a6c66
Prepare SharePoint writes one at a time, list the user's OneDrive fir…
jonesphillip Oct 27, 2026
51c690a
Test a workbook range read, and its refusals before anything is read
jonesphillip Oct 27, 2026
25f8af4
Keep pending cell changes inside a workbook range the agent asked for
jonesphillip Oct 27, 2026
0104c26
Write a list definition's nsid unsigned, copy the body's whole sectio…
jonesphillip Oct 27, 2026
ba9a34c
Show a link's destination in a deck edit's preview, keep a worksheet …
jonesphillip Oct 27, 2026
1c3979c
Refuse a formula with a control character or a refused function passe…
jonesphillip Oct 27, 2026
f2b0795
Trim and split whitespace in linear time, walk stylesheets without ba…
jonesphillip Oct 27, 2026
3cc6a3a
Keep narrowed Outlook bindings out of private subfolders, resume a cu…
jonesphillip Oct 27, 2026
b2acee2
Pin a file's unapproved major version for observers, join a page's UR…
jonesphillip Oct 27, 2026
367e34a
Check the live event before a personal change fenced on a later versi…
jonesphillip Oct 27, 2026
896b64b
Refuse to undo a draft edit or a SharePoint write whose version went …
jonesphillip Oct 27, 2026
ce3ef12
Treat a failed copy progress check as an unknown outcome once SharePo…
jonesphillip Oct 27, 2026
2189569
Resolve a move undo's origin folder, resume a cut-short mail scan by …
jonesphillip Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ reviewing or answering questions about code under its directory:
| `packages/gatekeeper-context/AGENTS.md` | Context Library collections and sharing |
| `packages/gatekeeper-cloudflare/AGENTS.md` | Cloudflare OAuth, billing, Workers Observability |
| `packages/gatekeeper-x/AGENTS.md` | X, whose reads are billed |
| `packages/gatekeeper-microsoft/AGENTS.md` | Microsoft 365: SharePoint, Outlook, Calendar |
| `packages/gatekeeper-websearch/AGENTS.md` | Web Search |
| `packages/mcp-shared/AGENTS.md` | MCP / MCP Server Portal gatekeepers |
| `packages/observability/AGENTS.md` | server logging, issue reporting, metrics |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ Each gatekeeper package contains instructions for how to set it up:
* [Home Assistant](packages/gatekeeper-homeassistant/README.md)
* [Slack API](packages/gatekeeper-slack/README.md)
* [Spotify](packages/gatekeeper-spotify/README.md)
* [Microsoft 365](packages/gatekeeper-microsoft/README.md)
* [X API](packages/gatekeeper-x/README.md)
* [ZoomInfo API](packages/gatekeeper-zoominfo/README.md)

Expand Down
3 changes: 3 additions & 0 deletions docs/observers.md
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,9 @@ its resource types.
| **context** | Context Library singleton | **C** | Track collections whose contents a read revealed (search results, a collection's listing, a document); verify each is in the observer's sharing domain and is public, is everyone-viewable, or grants the observer's Context account a viewer, editor, or owner role. Uses the kit's default observer cap to bound per-read verifier fanout. The top-level collection listing tracks nothing: its titles, descriptions and counts are discovery metadata the agent catalog already advertises unobserved. The denial never names the collection, which the refused collaborator cannot read. |
| **x** | Account | **C** | Admit any observer with a working X connection: public posts, profiles, public Lists and follow graphs need only an X account. Reads private to the account -- bookmarks, likes, mute state, private Lists, and any page holding a protected account's post -- authorize against one synthetic `owner` collection, which admits only an observer connected as the same X user. |
| **x** | Post / List | **C** | As Account, plus a check on every open that the observer's own X account can see the bound post or List (`canViewPost` / `canViewList`). Each check is a read billed to the observer's connection, so a passed check is trusted for an hour; a failed one never is. |
| **microsoft** | SharePoint (all, site, library, folder, file, list, page) | **C** | Track every object a read revealed as a unit (`sharepoint-observers.ts`), since SharePoint breaks inheritance at any level and one container's ACL never vouches for its contents; verify each by probing it with the observer's own token, twenty to a `$batch`, where 403 and 404 both mean no access. A file's content and its version history are their own units, since someone may see a file they can't open; a draft the owner read pins its version, and the probe passes only if the observer is shown the same one. |
| **microsoft** | Outlook Mailbox / Folder / Conversation / Sender | **C** | Track the mailbox, folders and messages each read revealed (`outlook-observers.ts`); verify each by a probe `GET` as the observer: the whole mailbox needs access to all of it (someone shared one folder gets 404), a folder or message needs that folder or message. |
| **microsoft** | Outlook Calendar / Availability | **C** | Track calendar units at the detail level each read revealed (`calendar-observers.ts`): a calendar unit admits an observer only when the owner's `calendarPermissions` give them a role that sees that level (`calendar-acl.ts`, where a group entry can only lower the organization default), which only the owner can read, so a unit of someone else's calendar admits that owner alone; an `availability:<address>` unit only when `getSchedule` as the observer answers for that address; the all-calendars unit admits nobody but the owner. |
| **x** | Profile | **C** | As Account: a public profile needs only an X account, and the `owner` collection covers a protected user's posts. Accepted over-permissiveness for every X binding: an author who has blocked the observer is still readable through the gadget, since checking each author per observer per read would cost a read each and what leaks is public. |

### 9.3 The "broad binding" lens
Expand Down
12 changes: 12 additions & 0 deletions packages/gatekeeper-microsoft/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Microsoft 365 gatekeeper

Microsoft 365 (Entra ID OAuth, Microsoft Graph). It has three resources. **SharePoint**'s binding is one of: all of SharePoint & OneDrive, a site, a library or OneDrive, a folder, a file, a list, or a page; its configurator starts at "all" and drills down. **Outlook**'s is one mailbox (own or shared), optionally narrowed to a folder, a conversation or senders, at `read`/`draft`/`send`. **Calendar**'s is every calendar (for chat), one calendar (own or shared) at `freeBusy`/`limited`/`full` detail (for a gadget shared with that calendar's sharees), or availability only, at `read`/`edit`/`schedule`; `CALENDAR_RESOURCE` (`…/calendar/*`) must stay ahead of `OUTLOOK_RESOURCE` (`…/*`) in `ALL_RESOURCES`, since the Workshop takes the first matching pattern. `plans/microsoft-365-gatekeeper.md`, `plans/microsoft-outlook.md` and `plans/microsoft-calendar.md` are the design records and `storage-schema.md` the storage layout.

- Agents act with the user's delegated permissions; the binding narrows further, and `ScopeGuard` (`sharepoint-scope.ts`) is the chokepoint. Every ID is checked before Graph is called (a folder by walking its ancestry), paths are measured from the binding's top, and a temporary `~N` ID is resolved at every entry point. `graph-api.ts` is the only code that holds a token: it refuses non-Graph hosts, and follows a download redirect once, only to a SharePoint or conversion host, without the `Authorization` header.
- A connection and its bindings are pinned to `(tenantId, objectId)` from the `id_token`, confirmed against `/me`. Tenant policy (`tenantPolicy` in `microsoft-env.ts`) refuses `common`/`consumers`, and refuses `organizations` without a GUID allowlist. Each resource's effective access is the lowest of the binding's own level (Outlook and Calendar), the deployment ceiling (`MICROSOFT_SHAREPOINT_ACCESS`, `MICROSOFT_OUTLOOK_ACCESS`, `MICROSOFT_CALENDAR_ACCESS`) and the grant's scopes, and a SharePoint file, folder or library binding drops to read when Graph's undocumented `shared.effectiveRoles` says the user can only read it (only ever lowering). `MICROSOFT_SHAREPOINT_SITES=selected` asks for `Sites.Selected` instead of the `.All` permissions, but a connection's behavior follows what it was granted (`grantedSharePoint`), never the setting; a read-only binding's session (`SharePointReader`, `OutlookReader`, `CalendarReader`) has no write methods at all.
- Observers use tracked collections of *access units* (site/drive/item/list/listItem/page), because SharePoint breaks inheritance at any level. One container's ACL never stands for its contents.
- Graph behaviours that return wrong-but-plausible data, verified live: `/sites?search=` can return nothing on a working tenant, so sites come from Microsoft Search (`/search/query`) plus followed sites; `/sites/{id}/lists` omits Site Pages, so pages are detected through the pages endpoint; search lags uploads by hours on a new tenant, so nothing may depend on finding a fresh file.
- Outlook's chokepoint is `OutlookContext.guarded` (`outlook-session.ts`): every message ID is read with its metadata and checked against the scope before anything else is read or changed, and a listing's Graph filter is only an optimization. 400, 403 and 404 all mean "not in this binding". Every request sends `Prefer: IdType="ImmutableId"`, so IDs survive moves and sending. Send is never auto-approvable and is exactly-once through a Message-ID fixed at proposal, found across the whole mailbox before `/send` (`submit` in `outlook-actions.ts` handles the half-second after a 202 when a second `/send` is 403). Inbox rules, permanent deletion, permissions and send-as are deliberately not offered.
- Exchange gives a mailbox four concurrent requests, so `$batch` entries can 429 individually and `GraphApi.batch` resends them; Sent Items' `from` is a legacy X.500 DN, replaced with the owner's address on read; Outlook web links use EWS IDs, converted in `parseOutlookUrl`. New-message hooks run through one `OutlookHookDriver` per connection and mailbox, which reads by cursor and treats Graph change notifications (when `BASE_URL` is public https) only as a prompt to read sooner.
- Calendar's chokepoint is `CalendarContext.guarded` (`calendar-session.ts`): an event's parent entry ID (`Binary 0x0E09`) must equal the bound calendar's (`Binary 0x0FFF`, pinned at binding) before anything is shown, and listings are re-confirmed the same way, because a calendar-scoped `calendarView` is not trusted. Redaction to the binding's detail level is the gatekeeper's (`eventInfo` in `calendar-events.ts`), not Graph's: the user's own token sees everything. Observers (`calendar-observers.ts`) admit someone to a calendar unit only when the owner's `calendarPermissions` give them a role that sees that level (`calendar-acl.ts`; a group entry can only lower the org default), to `availability:<address>` only when `getSchedule` as them answers, and to `calendars:<id>` never but the user. Anything that mails someone (`calendar.invitations`, `calendar.responses`) is never auto-approvable and is `claimBeforeApply`; changes are fenced with If-Match on the change key, creations with a `transactionId`.
- Tests: Node (`vitest.config.ts`, where the configurator frame suites drive the generated frames in Chromium through `__tests__/frame-host.ts`) and workerd against a fake Graph (`vitest.worker.config.ts`), both run by `pnpm --filter @gadgets/microsoft-gatekeeper test:run` after it builds the configurator frames; and a live-tenant contract suite (`vitest.contract.config.ts`), run by `test:contract`, which is skipped unless `.dev.vars` and a tokens file are present.
Loading
Loading