Skip to content

feat(security): ADR-0090 D10 — activate agent principal (OAuth → scope ceiling)#2843

Merged
os-zhuang merged 1 commit into
mainfrom
feat/d10-producer-agent-principal
Jul 11, 2026
Merged

feat(security): ADR-0090 D10 — activate agent principal (OAuth → scope ceiling)#2843
os-zhuang merged 1 commit into
mainfrom
feat/d10-producer-agent-principal

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What

Wires the producer side of the ADR-0090 D10 agent intersection that shipped in #2838, so it stops being dormant. An MCP request authenticated with an OAuth access token is now resolved as an AI agent acting on behalf of the human sub, and its effective permission is the intersection of a scope-derived capability ceiling and the user's own grants.

Before this, principalKind:'agent' / onBehalfOf were a P1 context shape that nothing ever produced — the D10 enforcement engine had no input. #2698 already captured the OAuth clientId (azp) but discarded it; this translates it.

How

  • resolve-execution-context (producer) — when a verified MCP OAuth token names an authorized client (azp), the request resolves to principalKind:'agent' with onBehalfOf:{ userId } (the human). The agent's OWN grants are replaced by the scope-derived ceiling: data:read → read-only, data:write → full CRUD, neither → no data access. userId stays the human so owner-stamping and current_user.* RLS resolve to them; the user-derived systemPermissions are cleared so a cap-gated action can't ride the user's capabilities. A token without a client stays a human principal (not every bearer is an agent).
  • plugin-security — three built-in ceiling sets (mcp_agent_data_read / mcp_agent_data_write / mcp_agent_restricted): pure CRUD bits, no row-level security (all row/owner/tenant narrowing comes from the delegating user on the other side of the intersection). An agent principal skips the additive human baseline (member_default) — its grants are exactly its ceiling — and its fallback is the restricted (no-object-access) set, so a mis-resolved agent fails CLOSED, never open.
  • specMCP_AGENT_PERMISSION_SET_* + scopesToAgentPermissionSets(), single-sourced beside the OAuth scope constants; api-surface.json regenerated.

Behaviour change (a security tightening)

Previously an MCP OAuth request executed with the full authority of the logged-in user, and scopes narrowed only the tool surface (which tools are exposed). Now the scope is also a real data-layer ceiling: a data:read token can never write ANY record — even via a crafted call — no matter what the user could do. This is strictly consistent with the existing contract that "a scope can never grant more than the user could do" (the intersection only ever narrows) and closes the gap where a compromised or confused agent could act with the user's full reach.

Tests

  • Producer mapping — unit-tested in @objectstack/runtime (resolve-execution-context): data:read/data:write/actions:execute → correct ceiling + principalKind:'agent' + onBehalfOf; a token with no client stays human. Full runtime suite 491 green (incl. the existing MCP-OAuth dispatcher tests).
  • Enforcement — dogfooded against the served engine (showcase-agent-scope-ceiling, real SQLite + RLS + private-OWD): a data:read agent acting for a member who owns a record can read it but cannot edit or create; a data:write agent for the same user can edit; the human control can do both.
  • No regression: plugin-security 298, plugin-sharing 76, and the D10/OWD/permission-zoo dogfood suites unchanged. check:api-surface + tsc clean.

Scope / follow-ups

  • One ceiling per token today, derived from the coarse data:read/data:write scopes. Per-client custom grants (binding permission sets to a specific OAuth application) remain a follow-up — the sys_oauth_application record is better-auth-managed and has no grant linkage yet.
  • Action capabilities: an agent's systemPermissions are empty, so cap-gated actions are denied to agents by default (fail-safe). Giving agent ceilings specific action capabilities is a follow-up.
  • The agent grant-ceiling lint (D10 rule 2) is now less necessary — the ceiling sets are platform-owned and carry no high-privilege bits by construction.

🤖 Generated with Claude Code

…e ceiling)

Wires the PRODUCER side of the D10 intersection that shipped in #2838, so
it stops being dormant. An MCP request authenticated with an OAuth access
token is now resolved as an AI agent acting ON BEHALF OF the human `sub`,
and its effective permission is the intersection of a scope-derived
capability ceiling AND the user's own grants.

- resolve-execution-context (producer): a verified MCP OAuth token that
  names an authorized client (`azp`) → principalKind:'agent',
  onBehalfOf:{userId}, and the agent's OWN grants become the scope-derived
  ceiling (data:read→read-only, data:write→CRUD, neither→no data). userId
  stays the human (owner/RLS scope); user systemPermissions cleared so a
  cap-gated action can't ride the user's capabilities. No client → human.
- plugin-security: three built-in ceiling sets (mcp_agent_data_read /
  _write / _restricted) — pure CRUD bits, NO row-level security (all
  row/owner/tenant narrowing comes from the delegating user on the other
  side of the intersection). An agent skips the additive human baseline
  (member_default) and its fallback is the restricted (no-object-access)
  set, so a mis-resolved agent fails CLOSED, never open.
- spec: MCP_AGENT_PERMISSION_SET_* + scopesToAgentPermissionSets(),
  single-sourced beside the OAuth scope constants; api-surface regenerated.

Behaviour change (a security tightening): an MCP OAuth request previously
executed with the FULL authority of the logged-in user (scopes narrowed
only the tool surface). Now the scope is also a real data-layer ceiling —
a data:read token can never write ANY record, even via a crafted call,
regardless of what the user could do. Strictly consistent with "a scope
can never grant more than the user could do" (the intersection only
narrows), closing the confused/compromised-agent gap.

Tests: producer mapping unit-tested in runtime (resolve-execution-context,
491 green incl. mcp-oauth); enforcement dogfooded against the served
engine (showcase-agent-scope-ceiling — data:read reads but cannot
write/create; data:write for the same user can). plugin-security 298,
plugin-sharing 76, D10/OWD/permission dogfood unchanged. api-surface check
+ tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 11, 2026 2:23pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ai tooling size/m labels Jul 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/dogfood, @objectstack/plugin-security, @objectstack/runtime, @objectstack/spec.

108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/runtime, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/dogfood, packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/dogfood)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security, @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit d79ca07 into main Jul 11, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the feat/d10-producer-agent-principal branch July 11, 2026 14:52
os-zhuang added a commit that referenced this pull request Jul 12, 2026
)

The agent docs described MCP OAuth as "runs as you / scopes narrow tool
families" — inaccurate after the ADR-0090 D10 agent work shipped
(#2838/#2843/#2845). An OAuth client is now an agent acting ON BEHALF OF
the user, bounded by the INTERSECTION of the consent scopes and the user's
own permissions/RLS, and scopes are a real data-layer ceiling (a data:read
token can never write, even where the user could) — not just a tool-family
filter.

- packages/mcp/src/skill.ts (SKILL.md served to every connecting agent):
  fix "runs as you" → agent-on-behalf + scope-ceiling intersection.
- content/docs/ai/agents.mdx: same correction, user-facing.
- docs/design/permission-model.md: mark the overlap rule *enforced*
  (#2838/#2843/#2845); honestly mark the three still-PLANNED agent
  guardrails — the grant-ceiling *lint* (runtime intersection already caps
  it; the bind-time lint is #2849), destructive-action co-sign, and the
  double-signature audit provenance (writer stamps only the delegator today).
- docs/adr/0090: status note that D10 evaluation semantics landed.

mcp suite 66 green.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ai size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant