Skip to content

docs(permissions): the "How enforcement works" callout names every transport seam that consults the ADR-0069 gate - #23333

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-23288-auth-gate-seams-callout
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-23288-auth-gate-seams-callout

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23288
Clause-②: no (docs)

What changed

One callout on one page: the "How enforcement works" box in content/docs/permissions/authentication.mdx (Enterprise Authentication Hardening, after the Network table). It said that only "the REST server + runtime dispatcher block gated sessions". That stopped being true when PR #23200 (08b38cb9d8), PR #23242 (d9d7eddd01) and PR #23285 (bae5841bce) landed. The new text says:

  • every transport seam that resolves a caller from the session consults the gate, through the one shared decision in @objectstack/core (evaluateAuthGate and its helpers);
  • those seams are the REST server, the runtime dispatcher (which also serves the MCP HTTP endpoint), and every route family that a plugin or service mounts directly on the host server and that resolves its own caller, at the family's own door;
  • a gated session gets 403 with the gate's own code (PASSWORD_EXPIRED or MFA_REQUIRED) at whichever of them serves the request;
  • the auth endpoints and a short allow-list kept with that decision still pass, so the user can remediate.

The page names functions and seams only. It lists no route and no family, as the card's acceptance asks. No other page is touched.

The disclosure rule this follows

The card asks for the text "at function level as the lane's disclosure rule asks". The card family it comes from (#23178, #23190) cites the domain:services lane rule. Verbatim, from .claude/skills/pm-dispatch/references/lanes/services.md (lines 19 and 20 at 4d4040ccb6):

  • 安全族卡的披露纪律:复现配方不落任何公开面(卡、PR、评论)。
  • 证据以抽象描述或私有通道承载。

In English: on a security-family card, a reproduction recipe never goes on a public surface (card, PR or comment), and evidence is carried by an abstract description or a private channel. The record that sets the same level for this gate's door classes is ADR-0138 (docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md, its Card paragraph): "Classes, positions and functions only: door-level readings that would work as an exploit recipe stay private". So the page names functions, the call-site list below is at file:line level only, and nothing here is a request.

Measured first (the PM's mechanism assumptions)

All readings are on origin/main at 4d4040ccb6, the base of this branch.

1. Where the gate is consulted (file:line; this list goes in the PR and the report, never on the page)

Seam Consults the gate at Refuses with
REST server packages/rest/src/rest-server.ts:2343 enforceAuth reads context.authGate (:2348), and computeExecCtx lifts it with normalizeAuthGate (:3272) 403 { error: { code, message } } (:2369)
Runtime dispatcher packages/runtime/src/http-dispatcher.ts:1362 enforceAuthGate calls evaluateAuthGate (:1396), and dispatch() runs it before any domain (:2746) 403 with the gate's code
MCP over HTTP served through dispatcher.dispatch (packages/runtime/src/dispatcher-plugin.ts:1272), so behind the dispatcher's gate above as above
Declared-endpoint fallback packages/runtime/src/endpoint-policy.ts:421 evaluateAuthGate (PR #23242) authGateDenial (:334), 403
REST direct mounts packages/rest/src/package-routes.ts:83 (refusePackageRequest) and packages/rest/src/external-datasource-routes.ts:333 (refuseFederationRequest), PR #23242 403 with the gate's code
Cloud connection packages/cloud-connection/src/cloud-connection-plugin.ts:234 (sessionFromAuthService), refused at seven doors (:332 through :688); packages/cloud-connection/src/marketplace-install-local-plugin.ts:3378 (resolveInstallPrincipal), refused at :566. PR #23242 403 with the gate's code
Datasource admin packages/services/service-datasource/src/admin-routes.ts:533 (requireDatasourceAdmin), PR #23200 403 with the gate's code
Share links packages/plugins/plugin-sharing/src/share-link-routes.ts:123 and :282 (the family door); posture lifted at packages/plugins/plugin-sharing/src/sharing-plugin.ts:970. PR #23200 403 with the gate's code
Webhook redeliver packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts:543 (serveRedeliver), PR #23200 403 with the gate's code
Storage packages/services/service-storage/src/storage-routes.ts:687 and :695 (the family door), PR #23200 403 with the gate's code
Settings packages/services/service-settings/src/settings-routes.ts:176 and :223 (the family door); posture lifted at packages/services/service-settings/src/settings-service-plugin.ts:334. PR #23285 403 with the gate's code

Each landing's pins were re-run on this tree, and all pass:

Package Pin files Result
@objectstack/rest rest-auth-gate.test.ts, direct-mount-auth-gate.pin.test.ts 2 files, 16 tests passed
@objectstack/runtime endpoint-policy.auth-gate.pin.test.ts, http-dispatcher.root-auth-gate.test.ts 2 files, 25 tests passed
@objectstack/cloud-connection cloud-connection-auth-gate.pin.test.ts 1 file, 12 tests passed
@objectstack/service-datasource admin-routes-auth-gate.pin.test.ts 1 file, 45 tests passed
@objectstack/plugin-sharing share-link-routes.auth-gate.pin.test.ts 1 file, 18 tests passed
@objectstack/plugin-webhooks webhook-redeliver-auth-gate.pin.test.ts 1 file, 8 tests passed
@objectstack/service-storage storage-routes.auth-gate.pin.test.ts 1 file, 42 tests passed
@objectstack/service-settings settings-routes.auth-gate.pin.test.ts 1 file, 22 tests passed

Each ran as pnpm --filter PKG exec vitest run --maxWorkers=2 FILE… under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0).

Where the card's list and the measurement differ.

2. The callout's other sentences, checked against the source

  • "Password expiry and enforced MFA share a single session-validation gate." True. AuthManager.computeAuthGate (packages/plugins/plugin-auth/src/auth-manager.ts:8061) returns PASSWORD_EXPIRED for an expired password and for an administrator-forced change (must_change_password, :8101), and MFA_REQUIRED once the grace window has elapsed (:8135). The page now names the forced change.
  • "customSession stamps the session with an authGate posture." True, with one correction: the posture goes on the session user. The return is user: { ...user, …, authGate } (auth-manager.ts:4119). The page now says "the session user".
  • "auth, health, and a small allowlist still pass so the user can remediate." True at the two model seams. isAuthGateAllowlisted exempts the auth namespace plus ALLOW_ROUTES (health, ready, discovery, the two current-user reads, the approval action page) and the POST-only inbound hook row (auth-gate.ts:126 and following). The raw-host doors pass an empty path, so they grant no exemption at all. The page keeps "a short allow-list" and gives "health and discovery among them" as examples, not as a full list.
  • "can't read data" is now "can't read or change data". The gate refuses writes as well (the pins above drive PUT and POST doors).

3. Left out on purpose

Verification (tree 14f73ed586)

  • Derived gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 43 commands from the one changed path. All 43 ran, each with its exit code recorded before any pipe. Reconcile: --ran printed 43 derived, 43 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. Every command exits 0. The named ones:
    • check:doc-anchors: "499 internal #fragment link(s) across 420 source file(s) all resolve to a real heading".
    • check:doc-authoring: "53 published skill files clean — no internal issue-id references".
    • check:corpus-claim-drift: "OK, no new claim sites beside a pinned spelling".
    • check:nul-bytes: "OK (scanned 11270 text file(s) … no raw ASCII control bytes)".
    • @objectstack/spec check:docs: "226 generated files in sync with packages/spec".
    • @objectstack/lint check:doc-security-posture: "28 ObjectSchema.create example(s) … carry an os validate-clean security posture".
    • @objectstack/spec check:skill-examples first answered PREREQUISITE NOT MET (exit 3: client-react was not built). That is not a measurement. After pnpm --filter '@objectstack/client-react...' --filter '@objectstack/client...' build it exits 0: "263 prose examples type-check across 3 surface(s)".
  • Link check, with the check-links.yml argv. lychee v0.24.2 (the workflow's pin) with --offline --root-dir content --fallback-extensions mdx,md --config lychee.toml 'content/**/*.md' 'content/**/*.mdx' README.md ARCHITECTURE.md reports "2460 Total … 2162 OK 🚫 0 Errors 👻 298 Excluded", exit 0. The diff adds no link.
  • MDX compile. @mdx-js/mdx as fumadocs-mdx 15.2.3 resolves it, with remark-gfm. Both the base and the edited page compile (exit 0). Negative control: an unclosed expression fails the same harness (exit 1).
  • What CI owns. The CI-only rows dispatch-gates names (the Build Docs job and the type-check lanes) are not run locally. A docs-only diff moves no TypeScript.

Changeset: none (skip-changeset)

The diff touches only content/docs/**. Measured: across the 71 non-private workspace packages, 4134 files under their files[], plus each package.json and README.md, were scanned. No files[] entry points outside its package directory.

  • "How enforcement works": 0 hits.
  • The new phrase "resolves a caller from the session": 0 hits.
  • Positive control, ExecutionContextSchema: 48 hits, among them packages/lint/dist/index.cjs.

No package build reads content/docs into its output; git grep over package manifests, tsup configs and package scripts finds only @objectstack/lint's doc checkers. The only packages built here are the closures above, and the scan says so.

Acceptance notes

  • A finding outside this card, raised in the report for the seat. Held sessions reach plugin-auth's platform-admin mounts. Those mounts sit under the auth namespace that isAuthGateAllowlisted exempts as a whole, and their judge (gateAdmin → judgePlatformAdmin) reads no posture. ADR-0069 D3 holds a session "out of all protected resources until enrollment + verification". The gate module's header lists the exemptions it means as "the auth endpoints (change-password, two-factor enrollment, sign-out)". Whether user administration under that namespace belongs inside that exemption is a security-boundary question. This was read from source at function level only. It is not measured, and nothing about it changes here.
  • The card's "what holds" list differs from the measurement in three entries and misses one group (section 1). The page is written to the measurement.

Generated by Claude Code

…ansport seam that consults the ADR-0069 gate

The callout said only the REST server and the runtime dispatcher block a
gated session. Every transport seam that resolves a caller from the session
now consults the gate through core's shared decision: REST, the dispatcher
(MCP's HTTP endpoint included), and the route families mounted directly on
the host server. Described at function level, with no route list.

Claude-Session: https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Oct 13, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 13, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 13, 2026 20:36
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 13, 2026 20:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 4185638 Oct 13, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23288-auth-gate-seams-callout branch October 13, 2026 21:17
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/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants