You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
core: put the exact approvals act path on the ADR-0069 auth-gate allow-list, token-only as ADR-0043 sets it and as the self-hosted mount already serves it (segment 3 of ruling A on #22438) #22577
Filing gate ③: a task the maintainer directed. The maintainer ruled A + sweep on #22438 (director record 6079645593, verbatim 「A + 扫类」). The domain:services seat 1 asked triage to split A into four lane segments chained by Blocked-by: (6092059305). Filed by the triage seat (seat post #6015, session_01AavokzJ5DndAwitDXvKy4U). ⛔ Not a claim.
Segment 3 of 4. Parent: #22438. It is independent of segments 1, 2 and 4.
The change
packages/core/src/security/auth-gate.tsisAuthGateAllowlisted (about :196 on main96e4be4829) takes the approvals act path.
Why this is not a loosening: ADR-0043 authenticates the action page by the e-mail token alone, and the self-hosted raw-app mount already serves it without a session. This entry gives the dispatcher path the same authentication, nothing more.
The maintainer ruled it as part of A (6079645593).
⛔ The exact path only. No prefix or wildcard entry, and no other approvals path.
Anything wider than the self-hosted mount's authentication is a loosening of a security boundary. It stops and goes to the maintainer.
The PR states the boundary in one line: what the allow-list now admits, and that it matches the self-hosted mount.
Pins
The exact act path is allow-listed.
A sibling path (another /approvals/… path, or a prefix extension of the act path) is not.
Ablation: remove the entry and the first pin goes red.
Claim: PM loop round 2 · 2026-10-10T03:10Z
Session: session_01Bw3y2DWhT9RPnrmDsNqEVG
Account: os-tesla (the seat's linked user, as get_me answers it; the card's assignee)
Branch: claude/issue-22577-approvals-act-allowlist
Worktree: objectstack-issue-22577
Domain: domain:engine
Seat: domain:engine#2 (seat post #20966)
Provenance: segment 3 of ruling A on #22438 (the maintainer's 「A + 扫类」, director record 6079645593), split by triage. The card says it is independent of segments 1, 2 and 4.
File surface (read on origin/main99801d831f):
packages/core/src/security/auth-gate.ts: isAuthGateAllowlisted (:196) takes the exact approvals act path, the one plugin-approvals' self-hosted mount serves (ACT_PATH, approvals-plugin.ts:392). ⛔ Exact path only: no prefix, no wildcard, no other approvals path.
Tests beside it, and .changeset/22577-approvals-act-allowlist.md (@objectstack/core).
Stop on a breach and explain in the report. Anything wider than the self-hosted mount's authentication goes to the maintainer, as the card says.
Container & model: S, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate)
Clause-②: yes
Widening: one exact path passes the ADR-0069 gate without a session, as the maintainer ruled (A). It owes a contract review at tier before the queue.
Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared: at 2026-10-10T03:10Z, 10 open PRs read by filename. None touches packages/core/src/security/. PR fix(plugin-approvals): the generic data door serves sys_approval_request rows by the approvals door's visibility (#22559) #22587 edits plugin-approvals, in another lane and in other files. The other three segments have no open PR.
os-dev-report
{
"issue": 22577,
"status": "done",
"branch": "claude/issue-22577-approvals-act-allowlist",
"pr": "#22602",
"session": "session_01Bw3y2DWhT9RPnrmDsNqEVG (shared with the PM; this run's identity is the branch named in claim 6093168292)",
"premise_still_valid": true,
"summary": "Added one exact entry, ['approvals', 'act'], to ALLOW_ROUTES in packages/core/src/security/auth-gate.ts, so the ADR-0069 gate exempts the ADR-0043 action route at the mount bases and environment scope it already normalises, and nothing else. Zone 2 was measured, with no premise falsified. (1) isAuthGateAllowlisted has 3 production readers: REST enforceAuth, the dispatcher enforceAuthGate, and shouldDenyAnonymous. No shipped seam passes a path to shouldDenyAnonymous (13 of 13 call sites outside core), so the live effect is the dispatcher's gate for a session held by an authentication policy. (2) ACT_PATH = '/api/v1/approvals/act' at approvals-plugin.ts:392: rawApp GET renders (peekActionToken), rawApp POST redeems (redeemActionToken). Neither raw-app middleware (the use('') seams, the rate limiter) reads a session or the allow-list. (3) Spellings: listed in the tests field and in the PR body. (4) Measured, not assumed: on a dispatcher with no approvals domain, a held session at /approvals/act moves from 403 PASSWORD_EXPIRED to 404 ROUTE_NOT_FOUND, the answer every other caller already gets, and every other row is unchanged. That is harmless, and the PR body says so. One reviewer point is flagged in the PR body: the environment-scoped spelling is admitted by the gate's existing scope normalisation. It reaches the same token-only handler, so it adds no authentication beyond the self-hosted mount. No security-boundary stop was needed.",
"tests": "Head 340dfe9. (a) Pins, in a new describe block of packages/core/src/security/auth-gate.test.ts. PIN 1: 11 admitted spellings (/api/v1/approvals/act; /approvals/act; /api/approvals/act; ?token=abc on two of them; a trailing slash; a double trailing slash; //approvals//act; /environments/env_1/approvals/act; /api/v1/environments/env_1/approvals/act; /api/v1/projects/env_1/approvals/act). PIN 2: 29 refused spellings (extensions act/x, act/x/y, actx, act-x, acts; approvals base and requests routes; the route below data, meta, /api/v2 or /rest/v1; a scope with no id; case variants; %2F and %61 encodings). evaluateAuthGate pin: a held session passes the act route and is still held on requests/req_1/approve and act/x. BOUNDARY: about 122k-path corpus against the predicate transcribed from 99801d8; newly exempt is exactly the 63 base x scope act paths, newly gated is empty. (b) Ablation: node scripts/ablation-replace.mjs deleted the entry (anchor 1 → 0, blob 264818d54c29 → 13b642e7a462) around vitest run src/security/auth-gate.test.ts. Result: '3 failed | 23 passed (26)'. PIN 1 red at its first row ('/api/v1/approvals/act: expected false to be true'); the evaluateAuthGate pin and BOUNDARY also red; PIN 2 green. Restore proven: blob == HEAD 264818d54c29 and git diff HEAD is empty. No dist leg: the test imports ./auth-gate from src. (c) Dispatcher measurement (throwaway file, never committed, deleted): two legs, ablated vs fixed, in a gated HttpDispatcher fixture whose control row (/data/task) answered 403 PASSWORD_EXPIRED on both legs. Held session, GET and POST /approvals/act and the scoped GET: 403 → 404 ROUTE_NOT_FOUND. Held session, /approvals/requests and /approvals/act/x: 403 → 403. Anonymous: 404 → 404 on all 5 approvals paths. (d) pnpm --filter @objectstack/core test: 88 files, 2293 tests passed, VERDICT command-exit 0. test:repo: 3 files, 48 tests passed. typecheck: exit 0, including check:test-typecheck over tsconfig.test.json, which compiles auth-gate.test.ts (--listFilesOnly count 1; plain tsconfig.json count 0). (e) Gates: dispatch-gates --commands derived 63; all 63 exit 0; --ran: 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN. First readings of check:dts-closure (exit 1) and check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET) came from a partial tree: my JS-only measurement build, then a concurrent gate still building. Neither named core; both reran green on the settled tree. (f) eslint, a declared narrowing over 2 files: 0 errors, 0 warnings. Population from eslint's own config (--print-config resolves both files); the count is from --format json; invariance: no type-aware linting in eslint.config.mjs (:327). Repo-wide lint is CI's.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "3, each through the fleet relay (one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) POST /repos/objectstack-ai/objectstack/pulls, draft pr_create #22602 (request fw-20261010T034728Z-0092d4, run 38021801643; body read back 9152 = 9152 bytes, identical); (2) POST /repos//issues/22602/assignees, os-tesla via label-write.mjs (request fw-20261010T034813Z-30dfaa, run 38021843363; read-back matches; size/m was set by another actor and left alone); (3) POST /repos//issues/22577/comments, this os-dev-report via post-stamped.mjs. Not REST: 2 git pushes (the empty-branch probe and 340dfe9). No label written: the change publishes, so skip-changeset does not apply, and the dispatch named no label.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: segment 2's PR (the runtime lane that lands the dispatcher /approvals/act domain) · noted, not filed. The dispatcher membership gate (enforceProjectMembership) runs after the ADR-0069 gate, and its skip list has no /approvals entry. So with membership enforcement and a resolved environment, a signed-in non-member following a forwarded link would be refused before the token is read; the self-hosted mount has no such gate. Today nothing serves the route, so this is unreachable read-only inference. Recorded in PR Acceptance notes. Dedupe words: approvals act membership gate skip list dispatcher",
"carrier: none · noted, not filed. AnonymousDenyInput.path docstring (anonymous-deny.ts) lists the exempt paths in a parenthesis that already omitted me/apps and now also omits approvals/act; it defers to isAuthGateAllowlisted. Cosmetic, in PR Acceptance notes only.",
"carrier: none · noted, not filed. The #7898 ratchet's comment asks future allow-list changes to update both sides; its corpus cannot reach approvals/act, so its transcription stays exact over that corpus and was left verbatim. The new BOUNDARY test is this change's own ratchet. In PR Acceptance notes."
],
"head": "340dfe9dcc",
"files_changed": [
".changeset/22577-approvals-act-allowlist.md",
"packages/core/src/security/auth-gate.test.ts",
"packages/core/src/security/auth-gate.ts"
],
"gates": {
"node scripts/check-adr-0087-registration.mjs --base origin/main": 0,
"node scripts/check-adr-0087-registration.mjs --self-test": 0,
"node scripts/check-changeset-no-major.mjs --base origin/main": 0,
"node scripts/check-changeset-no-major.mjs --self-test": 0,
"node scripts/check-ci-filter-parity.mjs": 0,
"node scripts/check-closing-keyword-parity.mjs": 0,
"node scripts/check-closing-keyword-parity.mjs --self-test": 0,
"node scripts/check-comment-mask-adoption.mjs": 0,
"node scripts/check-comment-mask-adoption.mjs --self-test": 0,
"node scripts/check-comment-mask-corpus.mjs": 0,
"node scripts/check-dev-prereqs.mjs --self-test": 0,
"node scripts/check-dts-emitted.mjs --self-test": 0,
"node scripts/check-empty-changeset.mjs --base origin/main": 0,
"node scripts/check-empty-changeset.mjs --self-test": 0,
"node scripts/check-issue-citations.mjs": 0,
"node scripts/check-keyed-text-bounds.mjs": 0,
"node scripts/check-keyed-text-bounds.mjs --self-test": 0,
"node scripts/check-platform-object-tenancy-census.mjs": 0,
"node scripts/check-platform-object-tenancy-census.mjs --self-test": 0,
"node scripts/check-plugin-teardown-shape.mjs": 0,
"node scripts/check-plugin-teardown-shape.mjs --self-test": 0,
"node scripts/check-registry-log-declared.mjs": 0,
"node scripts/check-registry-log-declared.mjs --self-test": 0,
"node scripts/check-rest-log-spy-declared.mjs": 0,
"node scripts/check-rest-log-spy-declared.mjs --self-test": 0,
"node scripts/check-system-context-census.mjs": 0,
"node scripts/check-system-context-census.mjs --self-test": 0,
"node scripts/check-undeclared-dep-imports.mjs": 0,
"node scripts/check-undeclared-dep-imports.mjs --self-test": 0,
"node scripts/docs-audit/check-affected-docs.mjs": 0,
"node scripts/docs-audit/check-drift-comment.mjs": 0,
"node scripts/pm/release-rehearsal-clone.mjs --self-test": 0,
"node scripts/release-pending-publish.mjs --self-test": 0,
"pnpm --filter @objectstack/spec run check:duration-unit-keys": 0,
"pnpm check:changeset-gate-self-tests": 0,
"pnpm check:cross-package-test-inputs": 0,
"pnpm check:doc-authoring": 0,
"pnpm check:driver-memory-census": 0,
"pnpm check:dts-closure": 0,
"pnpm check:dual-build-cjs-loads": 0,
"pnpm check:engine-double-contract": 0,
"pnpm check:gitlink-declared": 0,
"pnpm check:issue-citations": 0,
"pnpm check:kernel-hook-pairs": 0,
"pnpm check:lean-entry-closure": 0,
"pnpm check:logger-receiver-detach": 0,
"pnpm check:nul-bytes": 0,
"pnpm check:objectql-double-limit": 0,
"pnpm check:objectui-changeset": 0,
"pnpm check:org-identifier": 0,
"pnpm check:page-declaration-shape": 0,
"pnpm check:pm-changeset-deadline-census": 0,
"pnpm check:published-files": 0,
"pnpm check:query-options-erasure": 0,
"pnpm check:refd-timer-probe": 0,
"pnpm check:slot-lookup": 0,
"pnpm check:sourcemap-no-sources-content": 0,
"pnpm check:test-source-alias": 0,
"pnpm check:tier-file-adoption": 0,
"pnpm check:type-check-coverage": 0,
"pnpm check:type-check-debt": 0,
"pnpm check:watch-hint-literal": 0,
"pnpm check:where-matcher": 0
},
"deviations": [
"Attribution: the commit carries AGENTS.md's model-free trailer pair (Claude-Session plus 'Co-authored-by: Claude'), not the harness reminder's model-named Co-Authored-By. The PR body ends with AGENTS.md's session-URL footer, not the reminder's two-line form. AGENTS.md takes precedence, and the pre-push hook refuses a model identifier.",
"Measurement outside the file surface: a throwaway test was placed in packages/runtime/src to measure Zone 2 item 4 on HttpDispatcher, then deleted; never committed. Running it required a JS-only (OS_SKIP_DTS=1) build of runtime's 27-package dependency closure; those dists were removed afterwards. That state caused the two first-reading non-zero gate exits named in tests (e).",
"Gate commands (check:) ran outside the verify lock, as os-dev's resource rules say. Builds and vitest runs used os-verify-lock.sh with slot issue-22577-dev, and never waited more than 11s.",
"Cleanup: node_modules and the worktree ../objectstack-issue-22577 are removed after this report is posted."
]
}
ACCEPT (seat review): PR #22602 at head 340dfe9dcc. The ADR-0069 gate admits the exact approvals act path, token-only, as the self-hosted mount does
domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG (os-tesla), claim 6093168292 · 2026-10-10T05:12Z. Read against GitHub and the branch, not the report (os-dev-report on this card).
Contract review at CONTRACT_REVIEW_TIER: 6093538082, PASS on this head. It is owed because of the Clause-②: yes line.
Shape. Draft, base main. Line 1 is Fixes #22577, line 2 is Clause-②: yes (matching the claim), and line 3 is Refs #22438. 3 files, +184/−2, all inside the claim's file surface. NOT governed (check-governed-merges: 186 changed lines). The head merges clean with origin/main (git merge-tree).
The change, as read in the diff
ALLOW_ROUTES (packages/core/src/security/auth-gate.ts) gains one exact entry, ['approvals', 'act'].
It matches through the gate's existing exact-length route matcher. So the newly admitted set is the act path under the three mount bases, with no scope or with one environment or project scope, and with the query, a trailing slash and empty segments stripped by the existing rules.
Nothing else is admitted: no prefix, no wildcard, no other approvals path. The BOUNDARY pin proves it over about 122,000 paths: newly exempt is exactly the 63 act spellings, and newly gated is empty.
The boundary, judged against the ruling and the card.
The environment-scoped spellings are admitted by the gate's uniform scope rule, which every entry shares. They reach the same token-only handler, so they authenticate exactly as the self-hosted mount does.
That does not trip the card's stop line ("anything wider than the self-hosted mount's authentication"). The record answers the dev's flag the same way (③).
Evidence read.
The dispatcher was measured in two legs. A held session at the act path moves from 403 to 404 ROUTE_NOT_FOUND, because segment 2's domain is not on main. That is harmless.
The sibling paths stay 403. Anonymous callers answer 404 on all five approvals paths in both legs.
The ablation (delete the entry) turns 3 pins red, and the restore is proven blob-equal.
Gates: 63 derived, 63 run, all exit 0.
Published text: the changeset (@objectstack/core minor). It states the boundary in one line and names no other path. It holds.
CI on 340dfe9dcc. 34 runs: 31 success, 3 skipped, 0 failure. All seven required contexts are success. The skips (Build Docs, Console Pin Gate, Packed-tarball smoke) are path or opt-in skips on the roster.
Activity
objectstack-fleet commentedon Oct 10, 2026
Claim: PM loop round 2 · 2026-10-10T03:10Z
Session:
session_01Bw3y2DWhT9RPnrmDsNqEVGAccount:
os-tesla(the seat's linked user, asget_meanswers it; the card's assignee)Branch:
claude/issue-22577-approvals-act-allowlistWorktree:
objectstack-issue-22577Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)Provenance: segment 3 of ruling A on #22438 (the maintainer's 「A + 扫类」, director record 6079645593), split by triage. The card says it is independent of segments 1, 2 and 4.
File surface (read on
origin/main99801d831f):packages/core/src/security/auth-gate.ts:isAuthGateAllowlisted(:196) takes the exact approvals act path, the oneplugin-approvals' self-hosted mount serves (ACT_PATH,approvals-plugin.ts:392). ⛔ Exact path only: no prefix, no wildcard, no other approvals path..changeset/22577-approvals-act-allowlist.md(@objectstack/core).Container & model:
S,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)Clause-②: yes
Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared: at 2026-10-10T03:10Z, 10 open PRs read by
filename. None touchespackages/core/src/security/. PR fix(plugin-approvals): the generic data door serves sys_approval_request rows by the approvals door's visibility (#22559) #22587 editsplugin-approvals, in another lane and in other files. The other three segments have no open PR.objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22577,
"status": "done",
"branch": "claude/issue-22577-approvals-act-allowlist",
"pr": "#22602",
"session": "session_01Bw3y2DWhT9RPnrmDsNqEVG (shared with the PM; this run's identity is the branch named in claim 6093168292)",
"premise_still_valid": true,
"summary": "Added one exact entry, ['approvals', 'act'], to ALLOW_ROUTES in packages/core/src/security/auth-gate.ts, so the ADR-0069 gate exempts the ADR-0043 action route at the mount bases and environment scope it already normalises, and nothing else. Zone 2 was measured, with no premise falsified. (1) isAuthGateAllowlisted has 3 production readers: REST enforceAuth, the dispatcher enforceAuthGate, and shouldDenyAnonymous. No shipped seam passes a path to shouldDenyAnonymous (13 of 13 call sites outside core), so the live effect is the dispatcher's gate for a session held by an authentication policy. (2) ACT_PATH = '/api/v1/approvals/act' at approvals-plugin.ts:392: rawApp GET renders (peekActionToken), rawApp POST redeems (redeemActionToken). Neither raw-app middleware (the use('') seams, the rate limiter) reads a session or the allow-list. (3) Spellings: listed in the tests field and in the PR body. (4) Measured, not assumed: on a dispatcher with no approvals domain, a held session at /approvals/act moves from 403 PASSWORD_EXPIRED to 404 ROUTE_NOT_FOUND, the answer every other caller already gets, and every other row is unchanged. That is harmless, and the PR body says so. One reviewer point is flagged in the PR body: the environment-scoped spelling is admitted by the gate's existing scope normalisation. It reaches the same token-only handler, so it adds no authentication beyond the self-hosted mount. No security-boundary stop was needed.",
"tests": "Head 340dfe9. (a) Pins, in a new describe block of packages/core/src/security/auth-gate.test.ts. PIN 1: 11 admitted spellings (/api/v1/approvals/act; /approvals/act; /api/approvals/act; ?token=abc on two of them; a trailing slash; a double trailing slash; //approvals//act; /environments/env_1/approvals/act; /api/v1/environments/env_1/approvals/act; /api/v1/projects/env_1/approvals/act). PIN 2: 29 refused spellings (extensions act/x, act/x/y, actx, act-x, acts; approvals base and requests routes; the route below data, meta, /api/v2 or /rest/v1; a scope with no id; case variants; %2F and %61 encodings). evaluateAuthGate pin: a held session passes the act route and is still held on requests/req_1/approve and act/x. BOUNDARY: about 122k-path corpus against the predicate transcribed from 99801d8; newly exempt is exactly the 63 base x scope act paths, newly gated is empty. (b) Ablation: node scripts/ablation-replace.mjs deleted the entry (anchor 1 → 0, blob 264818d54c29 → 13b642e7a462) around vitest run src/security/auth-gate.test.ts. Result: '3 failed | 23 passed (26)'. PIN 1 red at its first row ('/api/v1/approvals/act: expected false to be true'); the evaluateAuthGate pin and BOUNDARY also red; PIN 2 green. Restore proven: blob == HEAD 264818d54c29 and git diff HEAD is empty. No dist leg: the test imports ./auth-gate from src. (c) Dispatcher measurement (throwaway file, never committed, deleted): two legs, ablated vs fixed, in a gated HttpDispatcher fixture whose control row (/data/task) answered 403 PASSWORD_EXPIRED on both legs. Held session, GET and POST /approvals/act and the scoped GET: 403 → 404 ROUTE_NOT_FOUND. Held session, /approvals/requests and /approvals/act/x: 403 → 403. Anonymous: 404 → 404 on all 5 approvals paths. (d) pnpm --filter @objectstack/core test: 88 files, 2293 tests passed, VERDICT command-exit 0. test:repo: 3 files, 48 tests passed. typecheck: exit 0, including check:test-typecheck over tsconfig.test.json, which compiles auth-gate.test.ts (--listFilesOnly count 1; plain tsconfig.json count 0). (e) Gates: dispatch-gates --commands derived 63; all 63 exit 0; --ran: 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN. First readings of check:dts-closure (exit 1) and check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET) came from a partial tree: my JS-only measurement build, then a concurrent gate still building. Neither named core; both reran green on the settled tree. (f) eslint, a declared narrowing over 2 files: 0 errors, 0 warnings. Population from eslint's own config (--print-config resolves both files); the count is from --format json; invariance: no type-aware linting in eslint.config.mjs (:327). Repo-wide lint is CI's.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "3, each through the fleet relay (one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) POST /repos/objectstack-ai/objectstack/pulls, draft pr_create #22602 (request fw-20261010T034728Z-0092d4, run 38021801643; body read back 9152 = 9152 bytes, identical); (2) POST /repos//issues/22602/assignees, os-tesla via label-write.mjs (request fw-20261010T034813Z-30dfaa, run 38021843363; read-back matches; size/m was set by another actor and left alone); (3) POST /repos//issues/22577/comments, this os-dev-report via post-stamped.mjs. Not REST: 2 git pushes (the empty-branch probe and 340dfe9). No label written: the change publishes, so skip-changeset does not apply, and the dispatch named no label.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: segment 2's PR (the runtime lane that lands the dispatcher /approvals/act domain) · noted, not filed. The dispatcher membership gate (enforceProjectMembership) runs after the ADR-0069 gate, and its skip list has no /approvals entry. So with membership enforcement and a resolved environment, a signed-in non-member following a forwarded link would be refused before the token is read; the self-hosted mount has no such gate. Today nothing serves the route, so this is unreachable read-only inference. Recorded in PR Acceptance notes. Dedupe words: approvals act membership gate skip list dispatcher",
"carrier: none · noted, not filed. AnonymousDenyInput.path docstring (anonymous-deny.ts) lists the exempt paths in a parenthesis that already omitted me/apps and now also omits approvals/act; it defers to isAuthGateAllowlisted. Cosmetic, in PR Acceptance notes only.",
"carrier: none · noted, not filed. The #7898 ratchet's comment asks future allow-list changes to update both sides; its corpus cannot reach approvals/act, so its transcription stays exact over that corpus and was left verbatim. The new BOUNDARY test is this change's own ratchet. In PR Acceptance notes."
],
"head": "340dfe9dcc",
"files_changed": [
".changeset/22577-approvals-act-allowlist.md",
"packages/core/src/security/auth-gate.test.ts",
"packages/core/src/security/auth-gate.ts"
],
"gates": {
"node scripts/check-adr-0087-registration.mjs --base origin/main": 0,
"node scripts/check-adr-0087-registration.mjs --self-test": 0,
"node scripts/check-changeset-no-major.mjs --base origin/main": 0,
"node scripts/check-changeset-no-major.mjs --self-test": 0,
"node scripts/check-ci-filter-parity.mjs": 0,
"node scripts/check-closing-keyword-parity.mjs": 0,
"node scripts/check-closing-keyword-parity.mjs --self-test": 0,
"node scripts/check-comment-mask-adoption.mjs": 0,
"node scripts/check-comment-mask-adoption.mjs --self-test": 0,
"node scripts/check-comment-mask-corpus.mjs": 0,
"node scripts/check-dev-prereqs.mjs --self-test": 0,
"node scripts/check-dts-emitted.mjs --self-test": 0,
"node scripts/check-empty-changeset.mjs --base origin/main": 0,
"node scripts/check-empty-changeset.mjs --self-test": 0,
"node scripts/check-issue-citations.mjs": 0,
"node scripts/check-keyed-text-bounds.mjs": 0,
"node scripts/check-keyed-text-bounds.mjs --self-test": 0,
"node scripts/check-platform-object-tenancy-census.mjs": 0,
"node scripts/check-platform-object-tenancy-census.mjs --self-test": 0,
"node scripts/check-plugin-teardown-shape.mjs": 0,
"node scripts/check-plugin-teardown-shape.mjs --self-test": 0,
"node scripts/check-registry-log-declared.mjs": 0,
"node scripts/check-registry-log-declared.mjs --self-test": 0,
"node scripts/check-rest-log-spy-declared.mjs": 0,
"node scripts/check-rest-log-spy-declared.mjs --self-test": 0,
"node scripts/check-system-context-census.mjs": 0,
"node scripts/check-system-context-census.mjs --self-test": 0,
"node scripts/check-undeclared-dep-imports.mjs": 0,
"node scripts/check-undeclared-dep-imports.mjs --self-test": 0,
"node scripts/docs-audit/check-affected-docs.mjs": 0,
"node scripts/docs-audit/check-drift-comment.mjs": 0,
"node scripts/pm/release-rehearsal-clone.mjs --self-test": 0,
"node scripts/release-pending-publish.mjs --self-test": 0,
"pnpm --filter @objectstack/spec run check:duration-unit-keys": 0,
"pnpm check:changeset-gate-self-tests": 0,
"pnpm check:cross-package-test-inputs": 0,
"pnpm check:doc-authoring": 0,
"pnpm check:driver-memory-census": 0,
"pnpm check:dts-closure": 0,
"pnpm check:dual-build-cjs-loads": 0,
"pnpm check:engine-double-contract": 0,
"pnpm check:gitlink-declared": 0,
"pnpm check:issue-citations": 0,
"pnpm check:kernel-hook-pairs": 0,
"pnpm check:lean-entry-closure": 0,
"pnpm check:logger-receiver-detach": 0,
"pnpm check:nul-bytes": 0,
"pnpm check:objectql-double-limit": 0,
"pnpm check:objectui-changeset": 0,
"pnpm check:org-identifier": 0,
"pnpm check:page-declaration-shape": 0,
"pnpm check:pm-changeset-deadline-census": 0,
"pnpm check:published-files": 0,
"pnpm check:query-options-erasure": 0,
"pnpm check:refd-timer-probe": 0,
"pnpm check:slot-lookup": 0,
"pnpm check:sourcemap-no-sources-content": 0,
"pnpm check:test-source-alias": 0,
"pnpm check:tier-file-adoption": 0,
"pnpm check:type-check-coverage": 0,
"pnpm check:type-check-debt": 0,
"pnpm check:watch-hint-literal": 0,
"pnpm check:where-matcher": 0
},
"deviations": [
"Attribution: the commit carries AGENTS.md's model-free trailer pair (Claude-Session plus 'Co-authored-by: Claude'), not the harness reminder's model-named Co-Authored-By. The PR body ends with AGENTS.md's session-URL footer, not the reminder's two-line form. AGENTS.md takes precedence, and the pre-push hook refuses a model identifier.",
"Measurement outside the file surface: a throwaway test was placed in packages/runtime/src to measure Zone 2 item 4 on HttpDispatcher, then deleted; never committed. Running it required a JS-only (OS_SKIP_DTS=1) build of runtime's 27-package dependency closure; those dists were removed afterwards. That state caused the two first-reading non-zero gate exits named in tests (e).",
"Gate commands (check:) ran outside the verify lock, as os-dev's resource rules say. Builds and vitest runs used os-verify-lock.sh with slot issue-22577-dev, and never waited more than 11s.",
"Cleanup: node_modules and the worktree ../objectstack-issue-22577 are removed after this report is posted."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
ACCEPT (seat review): PR #22602 at head
340dfe9dcc. The ADR-0069 gate admits the exact approvals act path, token-only, as the self-hosted mount doesdomain:engine#2·session_01Bw3y2DWhT9RPnrmDsNqEVG(os-tesla), claim 6093168292 · 2026-10-10T05:12Z. Read against GitHub and the branch, not the report (os-dev-report on this card).Contract review at
CONTRACT_REVIEW_TIER: 6093538082, PASS on this head. It is owed because of theClause-②: yesline.Shape. Draft, base
main. Line 1 isFixes #22577, line 2 isClause-②: yes(matching the claim), and line 3 isRefs #22438. 3 files, +184/−2, all inside the claim's file surface. NOT governed (check-governed-merges: 186 changed lines). The head merges clean withorigin/main(git merge-tree).The change, as read in the diff
ALLOW_ROUTES(packages/core/src/security/auth-gate.ts) gains one exact entry,['approvals', 'act'].BOUNDARYpin proves it over about 122,000 paths: newly exempt is exactly the 63 act spellings, and newly gated is empty.The boundary, judged against the ruling and the card.
Evidence read.
403to404 ROUTE_NOT_FOUND, because segment 2's domain is not onmain. That is harmless.403. Anonymous callers answer404on all five approvals paths in both legs.Published text: the changeset (
@objectstack/coreminor). It states the boundary in one line and names no other path. It holds.CI on
340dfe9dcc. 34 runs: 31 success, 3 skipped, 0 failure. All seven required contexts are success. The skips (Build Docs, Console Pin Gate, Packed-tarball smoke) are path or opt-in skips on the roster.Out of scope, one line each
/approvalsskip entry. Once segment 2 adds the domain, a signed-in non-member would be refused before the token is read. That is relayed to segment 2's card, runtime + hono: an exact /approvals/act dispatcher domain that forwards to the approvals service member, and a catch-all that stops consuming non-JSON bodies (segment 2 of ruling A on #22438) #22576 (6093486456), for that lane to settle.AnonymousDenyInput.path's docstring lists exempt paths, and the list is incomplete. It defers toisAuthGateAllowlisted. Cosmetic; noted, not filed.Next: ready, then auto-merge, in this act. #22438 stays open,
pm:blockedon its other three segments.objectstack-fleet commentedon Oct 10, 2026
Landed: PR #22602 →
874a38d8d9through the merge queue.domain:engine#2·session_01Bw3y2DWhT9RPnrmDsNqEVG· 2026-10-10T05:39Z.origin/main:auth-gate.ts,auth-gate.test.tsand.changeset/22577-approvals-act-allowlist.mdare blob-identical to the reviewed head340dfe9dcc.['approvals', 'act']entry is inALLOW_ROUTES(1 hit).pm:blockedon segments 1, 2 and 4 (spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575, runtime + hono: an exact /approvals/act dispatcher domain that forwards to the approvals service member, and a catch-all that stops consuming non-JSON bodies (segment 2 of ruling A on #22438) #22576, plugin-approvals: implement the transport-neutral action-page member, keeping the self-hosted raw-app mount byte-unchanged (segment 4 of ruling A on #22438) #22578).Fixes #22577.pm:dispatchedis removed in this act; the domain, area, priority, security and type labels stay.