Skip to content

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

Closed

Description

@objectstack-fleet

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.ts isAuthGateAllowlisted (about :196 on main 96e4be4829) 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.

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main 99801d831f):

    • 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.
  2. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  3. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    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.

    Out of scope, one line each

    Next: ready, then auto-merge, in this act. #22438 stays open, pm:blocked on its other three segments.

  4. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22602 → 874a38d8d9 through the merge queue. domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG · 2026-10-10T05:39Z.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:engineenhancementNew feature or requestpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions