Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions .changeset/24137-oauth-token-organization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/runtime': patch
---

fix(runtime): an OAuth MCP agent acts in the organization the user's consent bound, so the user's organization-scoped grants count for it

Clause-②: no

- **What was wrong.** On the `/mcp` door, `resolveExecutionContext` built a verified OAuth access token's principal with no organization. The agent's delegator (the user it acts for, ADR-0090 D10) was then resolved with no active organization and held only its global grants. Every grant an administrator assigns is scoped to an organization, so an ordinary user's agent was refused every object operation: `on-behalf-of principal may not 'find' … the delegator lacks that grant (ADR-0090 D10 intersection)`.
- **What changed.** The principal is resolved in the organization the consent bound: the `organizationId` that `IAuthService.verifyMcpAccessToken` returns from the token's `organization_id` claim. It is handed to the shared resolver where a session carries its active organization, so it is vetted on every request by the rule a browser session's organization gets (`vetOrganizationClaim`, `@objectstack/core`). The user's grants scoped to that organization count for the agent; grants scoped to the user's other organizations do not. It is never the union of the user's organizations, never the organization the user's browser session has selected now, and never a widened global grant.
- **A membership that is gone.** Under a walled tenancy posture (`isolated`, `group`), a token whose organization the user no longer belongs to resolves with no organization, so only global grants apply. Under `isolated`, a former member's agent gets an empty `200` read and a refused tenant write. The same holds for an organization the user never belonged to, whatever put it on the token. Under `single`, or with no `tenancy` service, the organization stands as claimed, exactly as a browser session's does there.
- **Unchanged.** A token with no `organization_id` (minted before consents bound one, or by a user with no active organization at consent) resolves with no organization, as before: global grants only, until the user re-confirms the consent once. A platform admin's global grant applies as before. The API-key and session tracks are untouched.
15 changes: 14 additions & 1 deletion content/docs/ai/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,20 @@ SKILL.md download (`GET /api/v1/mcp/skill`), and API-key minting.
private deployments work out of the box**. You authorize the client in the
browser; it then acts as an **agent on your behalf** (`principalKind: 'agent'`),
its **data access** bounded by the **intersection** of the consent scopes and
your own permissions/RLS — it can never exceed either. For object CRUD the
your own permissions/RLS — it can never exceed either. The agent acts in
**one organization**: the one you had selected when you consented, carried
in the token as its `organization_id` claim. Your grants scoped to that
organization count for it; grants scoped to your other organizations do not
(select the other organization and authorize the client again to work
there). Your membership is re-checked on every request by the rule a
browser session's organization gets: under an organization wall (`isolated`
or `group` tenancy) a token whose organization you no longer belong to falls
back to **no organization**, so only your global grants apply — under
`isolated`, a former member's agent gets an **empty `200`** on reads and a
refusal on writes to organization data. Under `single` (no wall) the
organization stands as consented, as a session's does. A token issued
before consents carried an organization holds your global grants only until
you re-confirm the consent once. For object CRUD the
consent scopes (`data:read`, `data:write`) are a real ceiling, not just a
tool-family filter: they narrow the exposed tools **and** cap what the agent
can do at the data layer — a `data:read` token can never write a record even
Expand Down
Loading
Loading