Skip to content

feat(spec,core,rest,runtime): declare ExecutionContext.authGate so the ADR-0069 gate joins the closed field set (#7280) - #7434

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7280-declare-auth-gate
Aug 10, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-7280-declare-auth-gate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #7280

Premise, re-measured on origin/main @ f188ed60e

All four legs hold; line numbers moved post-#7259, so this was re-anchored by content.

Claim Measurement
authGate undeclared in spec git grep -n authGate origin/main -- packages/spec/srczero hits
written behind as any rest-server.ts:2636-2643const execCtx = { ...base, ...(authGate ? { authGate } : {}), __kernel: kernel } as any;
read by enforceAuth rest-server.ts:2133const gate = context?.authGate;
the closed set exists to join packages/core/src/security/assemble-execution-context.ts on main (PR #7259, merged 05:41Z)

The dataflow question the card left open: entry-decided, or mid-request mutation?

Entry-decided, measured rather than assumed — which is what makes the card's "join the closed set" premise the right one:

  • Written exactly once, at rest-server.ts:2588-2594, inside computeExecCtx (the transport entry point), from the request's own session (getSession(headers)session.user.authGate), immediately before the assembleExecutionContext call.
  • git grep "authGate\s*=|authGate:" -- packages --include=*.ts (non-test) returns only auth-manager.ts:2621 (the upstream session enrichment) and the REST local. No handler writes ctx.authGate.
  • enforceAuth only reads it.

So it belongs in EntryExecutionContextField, not NonEntryExecutionContextField.

What changed

@objectstack/spec declares the field beside posture:

authGate: z.object({ code: z.string(), message: z.string() }).optional().describe(...)

Both inner keys required, matching the sole producer AuthManager.computeAuthGate (Promise< { code: string; message: string } | undefined >, both set on every return branch). The .describe() is deliberate: gen:docs renders .describe() and never TSDoc, so a bare key ships a blank description cell — the #6881 defect, avoided at the moment the row is added rather than after it ships.

@objectstack/coreauthGate joins ENTRY_EXECUTION_CONTEXT_FIELDS, and ExecutionContextAssemblyInput gains a required authGate input, on the accessToken template from #7259. A guest never carries one (no authenticated session for a policy gate to attach to). New export normalizeAuthGate completes a session user's loose gate into the declared shape at the one producer; evaluateAuthGate now calls it, so the two consumers cannot re-derive it differently. AuthGate is derived from the schema (NonNullable< ExecutionContext['authGate'] >) instead of being a second hand-written declaration.

@objectstack/rest passes the gate as an assembler input; the post-assembly spread is gone and the remaining as any covers __kernel alone.

@objectstack/runtime passes authGate: undefined, on the record.

Face census — every face the closed set forces a decision on

Face Decision Reason, stated at the call site
rest-server.ts computeExecCtx carries the gate its consumer is ten lines up — enforceAuth answers 403 { code, message } off the envelope
runtime/src/security/resolve-execution-context.ts (dispatcher / MCP) undefined, withheld on the record it enforces the same ADR-0069 gate at its own seam — HttpDispatcher.enforceAuthGate (http-dispatcher.ts:910-947) re-reads the session and calls evaluateAuthGate there — and never reads context.authGate. Carrying it would be a second copy no consumer reads. Converging the dispatcher onto the envelope is a real option but a behaviour-bearing change to a security seam, not a declaration's rider.
assemble-execution-context.ts entryFields emits it; guest ⇒ never the assembler itself
assemble-execution-context.test.ts 11 fixtures gained the explicit input runtime mirror of the closed set

No silent undefined anywhere: the input is required, so omission does not compile.

Reverse verification — directions predicted in writing before running

Probe A — the closure bites. Declared the field on the schema alone, left core untouched. Predicted RED with two independent errors; got exactly that:

assemble-execution-context.ts(148,7): error TS2322: Type 'true' is not assignable to type 'never'.
assemble-execution-context.ts(272,3): error TS2741: Property 'authGate' is missing in type
  '{ positions: string[]; ... }' but required in type 'ExecutionContextEntryFields'.

(148,7) is _ENTRY_FIELDS_EXHAUSTIVEMissingEntryField resolves to 'authGate', so the annotation becomes never. Green after the fix (tsc --noEmit -p packages/core/tsconfig.json, zero source-file errors).

Probe B — every face must decide. Deleted authGate: undefined from the runtime face. Predicted RED at compile; got it:

resolve-execution-context.ts(194,42): error TS2345: ... Property 'authGate' is missing in type
  '{ authz: ...; oauth: ...; localization: ...; requestLocale: ...; accessToken: ... }'
  but required in type 'ExecutionContextAssemblyInput'.

Probe C — the REST face is INVERTED, and this was predicted up front rather than discovered. Restoring the old post-assembly spread produces the identical envelope, so there is no before-green/after-red behavioural direction for that revert. Measured both halves:

  • behaviour: rest-auth-gate.test.ts4 passed with the old spread restored. Green, as predicted.
  • the closure: rest-server.ts(2619,51): error TS2345: ... Property 'authGate' is missing ... but required in type 'ExecutionContextAssemblyInput'.

Reporting that honestly rather than manufacturing a red: for this face the evidence is compile-time, by construction.

Probe D — the declaration does the work. Removed the declaration; the new spec rejection cases flip from success: false to success: true (an undeclared key is stripped, never rejected): 3 failed / 14 passed. Restored → 17 passed.

Probe E — the new REST pins bite. Withheld the gate at the REST face only (authGate: undefined): 3 failed — the envelope pin, the normalization pin and the new end-to-end 403. Restored → green. This is the genuine before-green/after-red direction, and it is on the pins this PR adds.

Every probe was taken out with git checkout / an in-place edit and a byte-identical restore verified against a saved git diff — never git stash.

Tests

New coverage, and one gap closed: nothing pinned the ADR-0069 seam end to end before — rest-auth-gate.test.ts hand-builds a context, so it proves only that enforceAuth reads the key, never that this face still puts it there.

  • packages/spec/.../execution-context.test.ts — accepts a well-formed gate; optional; rejects a code-without-message and a non-string code, asserted by issue path (authGate.message / authGate.code) and code: 'invalid_type', not by issue count; plus the [观察] ExecutionContextSchema.preserveAudit.describe() —— 生成的 reference 行描述为空(#6827 刻意划出的那半) #6881-style anti-vacuity pins on the published description.
  • packages/core/.../assemble-execution-context.test.ts — carried verbatim; no key when absent; guest never carries one; membership of the closed set.
  • packages/core/.../auth-gate.test.tsnormalizeAuthGate: null cases, verbatim pass-through, message completion, and that it drops any key the declaration does not name.
  • packages/rest/.../rest-exec-ctx-principal-kind.test.ts — on the wire through the real computeExecCtx: gate on the envelope, no key when ungated, message completed, and the end-to-end 403 on a protected data route with findData never called.
Command Result
pnpm --filter @objectstack/spec test 362 files / 9476 tests passed
pnpm --filter @objectstack/core test 29 files / 736 tests passed
pnpm --filter @objectstack/runtime test 119 files passed
pnpm --filter @objectstack/rest test 78 files passed
pnpm --filter @objectstack/{spec,rest,runtime} typecheck Done (all three)
tsc --noEmit -p packages/core/tsconfig.json zero source-file errors (core has no typecheck script; its test layer carries pre-existing TS2835 noise on main)
pnpm lint on the 10 changed files clean
node scripts/check-nul-bytes.mjs OK — 6740 files, no raw control bytes

18 further lint-job gates run green locally, including check:authz-resolver, check:adr-anchors, check:error-code-casing, check:route-envelope, check:engine-double-contract. Two could not run in a fresh worktree and say so themselves — check:i18n ("PREREQUISITE NOT MET — the workspace CLI is not built") and check:type-check-debt ("5 workspace dependenc(ies) ... have no built type entry point"); CI builds the closure before those steps.

Acceptance face

ExecutionContext is an authorable surface, so the four-step regen ran after a real spec build (buildcheck:generated--fix → re-check): all 11 generated artifacts up to date. Exactly three generated lines moved, all mechanical:

  • packages/spec/authorable-surface/kernel.json+ "kernel/ExecutionContext:authGate"
  • content/docs/references/kernel/execution-context.mdx — one row, with a real description
  • docs/audits/2026-07-unknown-key-strictness-ledger.counts.mdkernel/ 295 → 296 (census only; kernel/ is not in the triaged strictness campaign, and the nested object is plain z.object like its neighbour onBehalfOf)

api-surface/ and export-origins/ did not move — no new spec export — so the dual-snapshot rule does not apply here. Purely additive, so no ADR-0087 conversion/migration entry: no docs/adr/** file is touched by this PR.

Changeset level from the #6216 precedent (.changeset/execution-context-single-assembler.md: core minor, runtime/rest patch): spec minor (a new declared public key), core minor (new required input + new export), rest/runtime patch.

Out-of-scope finding filed

#7432enforceAuth passes req.path to isAuthGateAllowlisted unguarded, and isAuthGateAllowlisted(undefined) returns true, so a request with no path silently disables the gate. The sibling seam (anonymous-deny.ts:117-122) guards against exactly this and documents it. Observation-class, not live: the Hono adapter sets path at all three request-construction sites. Surfaced while writing the wire fixture here — it sat green through a gated session until path was spelled.


Generated by Claude Code

…e ADR-0069 gate joins the closed field set (#7280)

The authentication-policy gate rode the execution context undeclared: REST's
`computeExecCtx` spread it on with `...(authGate ? { authGate } : {})` behind an
`as any`, and `enforceAuth` read it back ten lines later. #6216's closed entry
field set is derived from `keyof ExecutionContext`, so a field living only
inside an `as any` is outside every closure gate by construction — the exact
blind spot that gate exists to remove.

Measured as ENTRY-decided, not a mid-request mutation: it is resolved from the
request's own session inside `computeExecCtx`, immediately before assembly, and
no handler writes it. So it joins the closed set rather than the non-entry
partition, and `ExecutionContextAssemblyInput` gains a REQUIRED `authGate` input
on the `accessToken` template — every face decides on the record. REST carries
it (its consumer reads it off the envelope); the runtime/MCP dispatcher passes
`undefined` because it enforces the same gate at its own seam
(`HttpDispatcher.enforceAuthGate`) and never reads `context.authGate`.

`normalizeAuthGate` completes a session user's loose gate into the declared
shape at the one producer, so a gate naming a code but no message no longer
renders a 403 body with `message: undefined`. `AuthGate` is now derived from the
schema instead of being a second hand-written declaration.

No runtime behaviour change: the assembler omits undefined-valued keys, so the
key is present exactly when it was before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 11:05am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/rest, @objectstack/runtime, @objectstack/spec.

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

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via packages/runtime, @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/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/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/core, @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 @objectstack/runtime, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/runtime, @objectstack/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/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.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/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/your-first-project.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime, @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/core, @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core, @objectstack/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 @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/core, @objectstack/rest, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/core, packages/runtime, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/rest, packages/runtime, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/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 @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

8 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/core, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v15.mdx (via @objectstack/core)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/core, @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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.

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 size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: context.authGate is read by REST's enforceAuth but is not declared on ExecutionContextSchema — an undeclared field outside every closure gate

2 participants