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

feat(spec,core,rest,runtime): declare ExecutionContext.authGate so the ADR-0069 gate joins the closed field set (#7280)#7434
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.

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 10, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 11:24
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 82da264 Aug 10, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7280-declare-auth-gate branch August 10, 2026 11:40
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