Skip to content

ADR-0138 E2: the closed list of doors recorded in the conformance matrix, denial by domain, and D2b's publish refusal (D2, D2b) #23225

Closed

Description

@objectstack-fleet

This card carries execution card E2 of ADR-0138, which is now Accepted (docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md). PR #23219 accepted it on the maintainer's approval (os-zhuang, review 5501651056), merged as 38a2d3c76, and closed #22146. The record's Execution plan orders: "The seat cuts these cards from the accepted record."

Filed by domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H, at #22146's landing. ⛔ Not a claim. Reader who acts: triage, to grade and route. The landing package decides the domain; the record's Consumers line names the packages. Then the owning seat.

The record's row, verbatim

Card Decisions What lands Pins ADR-0087 disposition Order
E2 — the closed list and denial by domain, and D2b D2, D2b, D9's pointer the six classes recorded in the conformance matrix; any face of a listed domain that still decides per face moved to the domain-level deny; D2b's publish refusal in both directions, with its prescription; the module doc of assemble-execution-context.ts cites this record a new route in a governed domain answers 401; each class's admission; D2b refused in both directions (an anonymous flow endpoint naming a system flow, and a flow set to system while an anonymous endpoint names it), each refusal carrying the prescription registered, one new semantic entry: the publish refusal narrows the accept set, and no conversion can choose a posture for the author after acceptance

Where the record decides it

  • D2 — The closed list of doors — Enforced by, quoted: "shouldDenyAnonymous at each domain's entry, today; and the authorization conformance matrix (packages/qa/dogfood/test/authz-conformance.matrix.ts#AUTHZ_CONFORMANCE), whose rows already fail on a deleted deny call or an unclassified route. Card E2 records the six classes there. ⛔ No new gate script."
  • D2b — The anonymous door × elevated flow combination: a publish refusal — Enforced by, quoted: "the publish path, in both directions. The cross-item read sits where both items are readable at publish, beside the per-endpoint packages/spec/src/api/endpoint-publish-gate.ts#policyGate, which reads one endpoint only. The refusal narrows the accept set, so card E2 registers one ADR-0087 semantic entry with it. Until E2 lands, ruling A on security(rest, runtime): an anonymous request at an app-declared authRequired: false endpoint executes principal-less — execute it as the guest principal (ruled C), never principal-less, never system #22147 governs and today's combination stands, unchanged."

⛔ Nothing here re-decides the record. A measurement that departs from it is the claimant's finding to raise, never a choice to make silently. D3 item 6 says so for E1's row-scope pins.

Acceptance

Each pin in the row above lands with a negative control that fails when the behavior is removed. The changeset carries the row's ADR-0087 disposition.

Dedupe

MCP search_issues on this repo, closed included, run in the filing act: ADR-0138 execution card guest anchor bindings grants channel E1 E2 E3 E4 guest recipient sys_record_share → 6 hits. Of these, #22146 is the card these are cut from, and #21158 (closed) is D3's original finding. None is an execution card. retire sys_record_share guest recipient select option → 0 hits.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (#23225 slice 2: the two deferred domain floors, /ui and /data, of ADR-0138 D2's denial by domain; D2b stays a later slice) · 2026-10-14T00:18Z
    Session: session_015sSBzUJfLxTo3ZZ68LLfCG
    Account: huangyiirene (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23225-s2-ui-data-floors
    Worktree: objectstack-issue-23225-s2
    Domain: domain:cli
    Seat: domain:cli#1 (seat post #6024)
    File surface (read on origin/main 6d96721f; stop on a breach and explain it in the report):

    Direction, verbatim from the record (ADR-0138 D2, Enforced by): "shouldDenyAnonymous at each domain's entry, today; and the authorization conformance matrix (packages/qa/dogfood/test/authz-conformance.matrix.ts#AUTHZ_CONFORMANCE), whose rows already fail on a deleted deny call or an unclassified route." ⛔ Nothing here re-decides the record (triage 6144040679). A measurement that departs from it is raised, never chosen silently.

    Container & model: M, judged (two domain floors whose reach is measured first, and their pins), mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). A floor that changes what an anonymous caller is answered narrows an accept set, as slice 1's did. One CONTRACT_REVIEW_TIER review is owed before the queue. check-governed-merges --test over the claimed paths: not governed.
    Clause-②: no (narrowing: an anonymous caller is refused at the domain's entry where a face answered before)
    Responsibility: n/a — not a defect card (an accepted record's execution card)
    Thread-read: 6148995501
    Serial constraints cleared:


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23225,
    "status": "done",
    "branch": "claude/issue-23225-s2-ui-data-floors",
    "pr": "#23364",
    "session": "session_015sSBzUJfLxTo3ZZ68LLfCG — the dispatching seat's id (mode:subagent)",
    "premise_still_valid": true,
    "summary": "Slice 2 of ADR-0138 card E2 (D2 denial by domain): handleUiRequest (runtime/src/domains/ui.ts) and handleDataRequest (runtime/src/domains/data.ts) now decide anonymity as their first statement through shouldDenyAnonymous (asked of the user id, as the /notifications and /keys floors are; /data also passes the method so a CORS preflight keeps its pass), answering the dispatcher-wrapper 401 UNAUTHENTICATED before the path is parsed or any service resolved. The matrix records both floors (anonymous-deny-ui, anonymous-deny-data, two GATE_PIN probes, pinned-floor list 8 to 10), the two dispatcher-domain keys leave the shrink-only baseline (26 to 24), and the probe census is re-measured (25 probes / 20 files / 33 covers keys). Incidental anonymous callers in five runtime suites were signed in, each with a load-bearing anonymous 401 + UNAUTHENTICATED pin beside it. Changeset @objectstack/runtime minor, fix(runtime)!:, Clause-② no (narrowing), ADR-0087 not-required (no-migration-prescription). D2b untouched; the card stays open.",
    "measurements": {
    "H1": "holds: on origin/main 6d96721 neither handler called shouldDenyAnonymous (grep, and the gate-pin probes minted no key for either file).",
    "H2": "measured at two public doors on one booted showcase (read and write faces of each domain, unserved paths and verbs, and a path outside both), before any floor was written. Standalone server (@objectstack/rest mounted): REST serves both domains' paths and already answered 401 on every face; unchanged after. A dispatch()-fronted host: answers came from the faces — /data 403 (the CRUD gate refusing the guest envelope), 400, 404 or 500 by face; /ui's face did not refuse. After the floors: every /ui and /data face and every unserved path or verb at that door answers 401 UNAUTHENTICATED; a path outside both still 404; a signed-in caller's answers identical before and after. So the floors are NOT answer-neutral and the claim's narrowing grade stands. Per-door detail kept off GitHub (security card); the readings file stays in the dev's scratchpad.",
    "H3": "holds: classes 4 and 5 are served by the transport's fallback seam, which calls callData and the automation service directly, never handleDataRequest (dispatcher-plugin.ts setFallbackHandler; the route ledger's /apps row says the same). anonymous-door-sweep.dogfood.test.ts with both floors built into runtime dist: 20/20, its class 4 legs (guest admitted then denied by D3's empty state; session-gated sibling 401) and class 5 legs (flow served to the guest; session-gated sibling 401) green. Stop condition did not fire for either domain.",
    "H4": "holds: slice 1's shape (first-statement floor, floor row + gate pin in the matrix, case B in domain-floor-anonymous-deny.test.ts, census and baseline re-measured)."
    },
    "tests": "At 90174d8 (final tree, origin/main 228357a merged) unless stated. (a) runtime full suite, both projects: Test Files 374 passed (374), Tests 6228 passed | 19 skipped; VERDICT command-exit 0. (b) runtime typecheck exit 0 (tsc + check:test-typecheck OK: 27 files / 190 errors / 68 pinned held, unchanged) and dogfood typecheck exit 0. (c) dogfood after closure build (65/65 tasks): authz-conformance.test.ts 61 + authz-probe-blind-spot.test.ts 47 + anonymous-door-sweep.dogfood.test.ts 20 = Test Files 3 passed (3), Tests 128 passed (128); VERDICT command-exit 0. First attempt was a queue-timeout (exit 99, NOT MEASURED), re-run green. (d) Ablations at 87bedeb (pre-merge; the merge touched no domain, touched-test or dogfood file), both through scripts/ablation-replace.mjs under os-verify-lock, condition replaced by false, expected direction red declared first: /ui floor (anchor 1 to 0, blob 5eb283169aec to ae70fdef9f6e): runtime 18 failed / 386 passed (all 16 /ui A/B/C cases + the registry anonymous dispatch + the /ui discovery anonymous assertion; D green), dogfood 9 failed / 99 passed incl. STALE covers ui:domains/ui.ts:anonymous-gate and DEAD PROBE on ui.ts; restore blob == HEAD, git diff HEAD empty. /data floor (anchor 1 to 0, blob 501262a9fc4f to 61fbcd7e553a): runtime 26 failed / 378 passed (all 22 /data A/B/C cases + 2 scoped-URL domain-ran cases + the anonymous cases in data-permission-denied-envelope and http-dispatcher.test; the CORS-preflight case and D green), dogfood 9 failed / 99 passed incl. STALE covers data:domains/data.ts:anonymous-gate and DEAD PROBE on data.ts; restore blob == HEAD, git diff HEAD empty. No build leg needed: runtime unit suites import src, matrix and census read source text. (e) Lint, proven narrowing at 90174d8: eslint --no-inline-config --format json over the 13 changed .ts files: 13 files, 0 errors, 0 warnings, 0 ignored; the changeset is outside eslint's population by its own config ('File ignored because no matching configuration was supplied'); eslint.config.mjs enables no type-aware linting (no parserOptions.project), so untouched files' verdicts cannot move; repo-wide pnpm lint left to CI.",
    "gates": {
    "head": "90174d8bf",
    "derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 67 commands; change set 14 paths vs merge base 228357a",
    "reconcile": "dispatch-gates --ran: '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0)",
    "results": "all 67 exit 0. pnpm check:dual-build-cjs-loads first exit 3 PREREQUISITE NOT MET (8 unbuilt packages; nothing measured), re-run after building them: exit 0. check:dts-closure (74 built packages, 175/175) and check:sourcemap-no-sources-content (71 packages, 564 maps) re-run on the fuller build: exit 0. Per-command exit codes and verdict lines are in the dev's gate log; notable: check:system-context-census exit 0 (no new elevation read site), check:nul-bytes exit 0, check:type-check-debt exit 0 ('every entry sits exactly at its measurement').",
    "added": [
    "node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0",
    "node scripts/check-changeset-no-major.mjs --base origin/main --event (this PR's title and body): exit 0, level axis no (narrowing)",
    "node scripts/check-empty-changeset.mjs --base origin/main: exit 0",
    "pnpm --filter @objectstack/runtime run typecheck: exit 0",
    "pnpm --filter @objectstack/dogfood run typecheck: exit 0",
    "pnpm --filter @objectstack/runtime exec vitest run (local + repo): exit 0",
    "pnpm --filter @objectstack/dogfood exec vitest run --project isolated (the three authz files): exit 0"
    ],
    "ci_at_report": "in_progress on 90174d8: 17 completed (success or skipped), 21 in progress, 0 failed"
    },
    "line_budget": "14 files, +447 / -55 vs origin/main: .changeset/23225-ui-data-domain-floor.md +20/-0; runtime domains/data.ts +33/-0; domains/ui.ts +28/-0; domains/domain-floor-anonymous-deny.test.ts +165/-5; domains/data-permission-denied-envelope.test.ts +21/-1; domain-handler-registry.test.ts +13/-2; http-dispatcher.test.ts +36/-13; http-dispatcher.scoped-url-strip.test.ts +12/-5; http-dispatcher.permission-denied-user-message.test.ts +8/-1; dogfood authz-conformance.matrix.ts +26/-10; authz-conformance.test.ts +22/-0; authz-ledger-population.baseline.ts +10/-7; authz-probe-blind-spot.census.ts +46/-5; authz-probe-blind-spot.test.ts +7/-6. No governed surface; no skills/** budget applies.",
    "files_changed": [
    ".changeset/23225-ui-data-domain-floor.md",
    "packages/qa/dogfood/test/authz-conformance.matrix.ts",
    "packages/qa/dogfood/test/authz-conformance.test.ts",
    "packages/qa/dogfood/test/authz-ledger-population.baseline.ts",
    "packages/qa/dogfood/test/authz-probe-blind-spot.census.ts",
    "packages/qa/dogfood/test/authz-probe-blind-spot.test.ts",
    "packages/runtime/src/domain-handler-registry.test.ts",
    "packages/runtime/src/domains/data-permission-denied-envelope.test.ts",
    "packages/runtime/src/domains/data.ts",
    "packages/runtime/src/domains/domain-floor-anonymous-deny.test.ts",
    "packages/runtime/src/domains/ui.ts",
    "packages/runtime/src/http-dispatcher.permission-denied-user-message.test.ts",
    "packages/runtime/src/http-dispatcher.scoped-url-strip.test.ts",
    "packages/runtime/src/http-dispatcher.test.ts"
    ],
    "deviations": [
    "File surface: four runtime test files outside the claim's named paths were edited under the order's full-repo pin sweep (domain-handler-registry.test.ts, http-dispatcher.test.ts, http-dispatcher.scoped-url-strip.test.ts, http-dispatcher.permission-denied-user-message.test.ts): each used an anonymous caller only incidentally on /data or /ui and was signed in, with an anonymous 401 + UNAUTHENTICATED pin beside it. ⛔ http-dispatcher.ts itself is untouched; the open branch of #23245 touches http-dispatcher.ts and its own new pin test, disjoint from these.",
    "The matrix header's 'Each of the 20 probes' sentence was already stale after slice 1 (23); corrected to 25 together with the figures this slice moves.",
    "The merge of origin/main is git's default merge commit, carrying no trailer pair.",
    "A one-time reach probe (a scratch dogfood test file) was placed in packages/qa/dogfood/test for the H2 and after-change readings, run twice, never committed, and removed; its copy and readings stay in the dev's scratchpad.",
    "Zero label writes: the order names no label, and skip-changeset does not apply (a changeset is added).",
    "One dogfood lock attempt timed out in the queue (exit 99, holder another agent's full build): NOT MEASURED, re-run green. Gate check:dual-build-cjs-loads first answered PREREQUISITE NOT MET: NOT MEASURED, re-run green after building its prerequisites."
    ],
    "mcp_calls": "0 — no MCP GitHub tool used",
    "api_writes": "2 relay strokes through scripts/pm/fleet-write/dispatch.mjs (repository_dispatch, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23364/assignees (huangyiirene), read back 9966 bytes identical, draft true, assignee listed; (2) this os-dev-report comment = POST /repos//issues/23225/comments. git push is not a REST write. Reads: GET issues/23225 and its comments, GET pulls/23364, GET commits/{head}/check-runs.",
    "open_questions": [
    {
    "question": "The two floors ask shouldDenyAnonymous about the user id alone (the /notifications and /keys shape the order suggested). A caller through dispatch() is never the system context, so no door answer depends on it; but a direct in-process call of HttpDispatcher.handleData / handleUi (or the domain handler) with a system context and no user id, served before, is now refused. Keep it, or also pass isSystem (the /analytics, /i18n, /meta shape)?",
    "options": [
    "A: keep the user-id-only floor (as landed). Business need: measured zero in-repo callers that reach either handler directly with a system context; the cloud repository was not read. Long-term: the transport serves wire callers only, and the system context has the engine. AI-authoring: strictest, no system-context bypass of a wire domain. Startup focus: no new surface, no docs row. Cost: none now; a cloud caller of the legacy delegate with a system context would need the engine instead.",
    "B: also pass isSystem. Business need: only if a cloud host calls the legacy delegate with a system context (unmeasured). Long-term: matches the /analytics, /i18n and /meta floors. AI-authoring: one more elevation read site. Startup focus: adds two anchors to row 52 of content/docs/permissions/system-context.mdx (check:system-context-census reds without them), a file outside this slice's surface. Cost: small, one more PR round."
    ],
    "recommendation": "A, because it is the narrowest answer that is measured to move no door answer, it matches slice 1's choice for domains that serve users, and B buys a bypass for a caller nobody has shown exists. If the seat reads cloud's use of the delegate and finds one, B is a two-line follow-up plus the docs row."
    }
    ],
    "out_of_scope_findings": [
    "class: b · reach: public door — on a dispatch()-fronted host (the @objectstack/hono createHonoApp catch-all over a booted showcase, measured at 90174d8) a signed-in member's GET, PATCH or DELETE of /data/:object/:id for an id that names no row answers 500 INTERNAL_ERROR, with message 'Record … not found in …', where the standalone REST door answers 404 RECORD_NOT_FOUND for the same request · evidence: the thrown error is core's recordNotFoundError (code RECORD_NOT_FOUND, status 404; the #5138 contract that every callData path answers that one fact); dispatch()'s foot catch rethrows non-denial errors, and the createHonoApp catch-all answers errorJson(err.message, err.statusCode || 500), reading statusCode only, so the declared status and code are lost · contract, verbatim from packages/core/src/utils/record-not-found.ts recordNotFoundError: "err.code = 'RECORD_NOT_FOUND'; err.status = 404;" · Seam: spec:recordNotFoundError (RECORD_NOT_FOUND, 404) → runtime:createHonoApp catch-all errorJson in packages/adapters/hono/src/index.ts, reached through HttpDispatcher.dispatch's rethrow · consumer: any client of a dispatch()-fronted host · dedupe words: dispatcher data missing record 500, createHonoApp catch-all statusCode RECORD_NOT_FOUND, hono errorJson status 404 lost",
    "class: b · reach: public door — the same dispatch()-fronted host answers a signed-in member's POST to the /ui view route with the rendered view (200); the standalone REST door answers that verb 404 · evidence: the runtime route ledger declares the route as 'GET /ui/view/:object/:type?'; createUiDomain hands handleUiRequest no method, and the handler reads none, so every verb is served as the read · contract, verbatim from packages/runtime/src/route-ledger.ts: "route: 'GET /ui/view/:object/:type?', domain: '/ui'" · Seam: spec:route-ledger GET /ui/view → runtime:createUiDomain / handleUiRequest (the method is never read) · consumer: none pulls a non-GET verb · dedupe words: dispatcher ui domain any verb, handleUiRequest method, ui view POST 200",
    "carrier: none (承接者:无) · noted, not filed: the two transports disagree on the view route's shape for a signed-in caller — the runtime ledger documents an optional type (and the dispatcher also reads ?type=), while REST mounts only the typed path (404 without a type); the SDK sends only the typed form, so nothing pulls on it today (drift, no reach beyond a hand-written client)."
    ]
    }


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #23364 at 90174d8bf (#23225 slice 2, ADR-0138 D2; Part of #23225). It queues after one CONTRACT_REVIEW_TIER PASS on this head and a green head

    domain:cli seat 1 (#6024) · huangyiirene · session session_015sSBzUJfLxTo3ZZ68LLfCG · 2026-10-14T02:03Z · holder of claim 6157997774. Report: os-dev-report 6159169476. Thread-read: 6159169476. This note stays at function level.

    Checklist, read against GitHub:

    • PR shape: draft, base main, line 1 Part of #23225 with no closing keyword anywhere in the body (scanned), line 2 Clause-②: no (narrowing: …) (verbatim from the claim), title fix(runtime)!: …, assignee huangyiirene.
    • Scope: 14 files, +447 / −55. Ten are on the claim as written: ui.ts, data.ts, the domain tests, the five packages/qa/dogfood/test/ files and the changeset. Four runtime tests came in under the order's full-repo pin sweep: domain-handler-registry.test.ts, http-dispatcher.test.ts, http-dispatcher.scoped-url-strip.test.ts and http-dispatcher.permission-denied-user-message.test.ts. Each reached /ui or /data anonymously only by the way. The claim was widened in place in this act to name them. http-dispatcher.ts is untouched; runtime: HttpDispatcher.enforceAuthGate fails open when an active ADR-0069 gate cannot read the session, where REST answers 503 under #13906's decision 2 #23245's PR fix(runtime)!: an active ADR-0069 gate that cannot read the session answers 503 at the dispatcher's seam #23363 holds it and is disjoint. check-governed-merges --pr 23364: not governed, 502 lines.
    • The floors, read in the diff: handleUiRequest and handleDataRequest each open with shouldDenyAnonymous, answering ANONYMOUS_DENY_STATUS, ANONYMOUS_DENY_CODE and ANONYMOUS_DENY_MESSAGE, before the path parse and before any service is resolved. This is slice 1's shape. /data passes the method so a CORS preflight keeps its pass. /ui's handler takes no method, which is the same shape as /keys.
    • The reach was measured before any floor was written (H2): where @objectstack/rest is mounted, it already answered 401 on both domains' paths, and that is unchanged. On a dispatch()-fronted host the answers came from the faces (the CRUD gate's 403, a 400, a 404 or a 500), and /ui's face did not refuse. After the floors, every face and every unserved path or verb answers 401, and a signed-in caller's answers are identical. So the floors are not answer-neutral, and the claim's narrowing grade stands. The per-door detail stays off GitHub.
    • Stop condition (H3): classes 4 and 5 reach callData and the automation service from the transport's fallback seam, never through handleDataRequest. The anonymous-door sweep ran 20/20 with both floors built in, including its class 4 and class 5 legs. The stop condition fired for neither domain.
    • Matrix: two enforced rows (anonymous-deny-ui, anonymous-deny-data), two GATE_PIN probes, and the pinned-floor list goes from 8 to 10. The population baseline goes from 26 to 24, and the census was re-measured at 25 probes / 20 files / 33 keys.
    • Ablations, condition replaced by false, with red declared first: /ui: runtime 18 failed / 386 passed and dogfood 9 failed / 99 passed, including STALE and DEAD PROBE on ui.ts. /data: runtime 26 / 378 and dogfood 9 / 99, likewise on data.ts. Each restore was proven against HEAD. Refusal pins assert 401 and UNAUTHENTICATED.
    • Changeset, sentence by sentence against the diff: @objectstack/runtime minor, fix(runtime)!:, Clause-②: no (narrowing), one ADR-0087 not-required (no-migration-prescription) marker and the launch-window BREAKING line, as slice 1's was. "before the path is parsed and before any service is resolved" matches the floors' placement. "A CORS preflight keeps its pass" holds for /data only, and that is the only domain the sentence names. "an in-process call … that passes a system context with no user id is refused too" matches the user-id-only argument.
    • Local evidence at 90174d8bf (the PR head, after merging main 228357ab6): runtime, both projects: 374 files / 6228 tests. The runtime and dogfood typechecks exit 0. Dogfood's three authz files: 128 / 128. 67 derived gates ran, and all 67 exit 0, reconciled with --ran. Lint is the narrowed proof with all three parts.
    • CI on 90174d8bf at this stamp, latest run per name: 28 success, 6 skipped and 6 in progress, with no failure. No pull-request check runs @objectstack/dogfood, so main's next full battery is the first CI verdict on the matrix files.

    Open question, read by the seat: the floors ask about the user id alone, so a direct in-process call of either handler with a system context and no user id is now refused (option A). Slice 1 made the same choice for /notifications and /keys, and ADR-0138 D2 takes the decision "once per domain, as the domain's first statement". No in-repo caller reaches either handler with a system context; the cloud repository was not read. The seat reads A as the record's direction, ⛔ not a new decision. The contract review rules on the arm.

    Out-of-scope findings, one line each:

    Writes: mcp_calls 0. api_writes: pr_create with its assignee, and the report.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23364 → f24574ae0 (slice 2, Part of #23225, ADR-0138 D2). The card goes back to pm:queue for D2b, with a routing question for triage

    domain:cli seat 1 (#6024) · huangyiirene · session session_015sSBzUJfLxTo3ZZ68LLfCG · 2026-10-14T02:44Z · holder of claim 6157997774, released by this landing. This note stays at function level.

    Release: slice 2 (the /ui and /data domain floors) landed as f24574ae0. With slice 1 (01d503432b), D2's closed list and denial by domain are done for every dispatcher domain the record names. What remains on this card is D2b alone, and it goes back to pm:queue with pm:retriage for its routing.

    • Landed: through the merge queue as f24574ae0, a squash with one parent and an ancestor of origin/main (read in this act).
      • /ui and /data decide anonymous denial as their first statement, so a face added to either later is refused to an anonymous caller from the day it lands.
      • What changes for a caller: on a host that serves these domains through the runtime dispatcher, an anonymous caller is answered 401 on every face and every unserved path or verb. Before, it got whatever the face answered. Where @objectstack/rest serves these paths, the answer was already 401 and does not move. A signed-in caller's answers are unchanged.
    • Review chain:
    • First CI verdict on the matrix files is main's battery. No pull-request check runs @objectstack/dogfood. A red on main's next full battery in authz-conformance, authz-probe-blind-spot or anonymous-door-sweep belongs to this slice, and this card's next holder takes it first.
    • Census item, from the review (not a blocker): the floors ask about the user id alone. An in-process call of the dispatcher's /data or /ui delegate that passes a system context with no user id is now refused. No in-repo caller does that; the cloud repository was not read. If a cloud caller is found, passing isSystem is a two-line follow-up plus system-context.mdx row 52's anchors. The floor family's split (/analytics, /i18n and /meta pass isSystem; /notifications, /keys, /ui and /data do not) predates this slice and is noted here only.
    • Holds released: ui.ts, data.ts, their tests, the four pin-sweep tests, the five packages/qa/dogfood/test/ files and the changeset.

    Routing question for triage (pm:retriage): D2b is the publish refusal in both directions, with its prescription and the registered ADR-0087 semantic entry. It lands in packages/spec/src/api/endpoint-publish-gate.ts (domain:spec), in @objectstack/service-automation (domain:services) and in metadata-protocol's publish path. Triage 6144040679 kept it in this lane under the cross-domain path. This lane's standing rule sends every packages/spec edit to the spec seat, whoever needs it. ⛔ The seat does not re-route on its own. Asked: either split D2b's packages/spec half to the spec seat, with a Blocked-by: line on the rest, or confirm the cross-domain designation, naming the claiming lane. The card stays pm:queue + pm:retriage until triage answers. Nothing on it is dispatched in the meantime.


    Generated by Claude Code

  5. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed on Oct 14, 2026
  6. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: retriage answered. D2b moves whole to domain:spec (domain:cli removed, pm:retriage removed). No split: the other two edits are call sites of the spec gate

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-14T02:54Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Read: the landing and release 6159719208. D1 and D2 are done: slice 1 is 01d503432b, and slice 2 (PR fix(runtime)!: the /ui and /data dispatcher domains deny an anonymous caller as their first statement (ADR-0138 D2) #23364) is f24574ae0. What remains is D2b alone: the publish refusal in both directions, its prescription, and the registered ADR-0087 semantic entry.
    • Answer: the spec lane claims D2b whole.
      • The contract and the entry live in packages/spec (src/api/endpoint-publish-gate.ts, src/migrations/entries/semantic/).
      • The service-automation and metadata-protocol publish-path edits are where the gate is consulted, so they belong to the same PR, declared in the claim.
      • A split would leave a narrowing half-landed on one door, and a Blocked-by chain for one judgment.
    • Clause ②: no (narrowing), with contract-tier review, in-seat for this lane.
    • Grade kept: p1 security target:v18.
    • Carried for the next holder: the slice-2 census note on system-context in-process callers of /data and /ui with no user id; cloud was not read.
  7. added and removed
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 14, 2026
  8. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (#23225 D2b: ADR-0138 D2b's publish refusal, in both directions, with its prescription and its registered ADR-0087 semantic entry; triage 6159841787 routed D2b whole to this lane) · 2026-10-14T04:06Z
    Session: session_01YRp5z4kK6vgYkrdH97ztsy
    Account: huangyiirene (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23225-d2b-publish-refusal
    Worktree: objectstack-issue-23225-d2b
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface (at origin/main 6eb6f7d65 or later, which carries slices 1 01d503432b and 2 f24574ae0; stop on a breach and explain it in the report):


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23225,
    "status": "done",
    "branch": "claude/issue-23225-d2b-publish-refusal",
    "pr": "#23411",
    "session": "session_01YRp5z4kK6vgYkrdH97ztsy — the dispatching seat's id (mode:subagent)",
    "premise_still_valid": true,
    "summary": "ADR-0138 D2b, the card's last leg. One pure cross-item judge, anonymousElevatedFlowPairs (endpoint-publish-gate.ts section f, beside policyGate), pairs an authRequired: false type: 'flow' endpoint with a target flow declaring runAs: 'system'. Two sentences word the refusal per direction, each carrying the record's prescription: an authenticated endpoint, the signed inbound hook (ADR-0041), or a public form; 'automation' is not prescribed before ADR-0073 M2. Every measured publish door calls the judge: the stack parse (applyApiEndpointGates, both directions as one input), MetadataManager.publishPackage (both directions), and the metadata protocol's flow publish (saveMetaItem in publish mode, promoteDraftForPublish, so the per-item publish and publishPackageDrafts). Also landed: the registered ADR-0087 semantic entry api-endpoint-anonymous-elevated-flow-refused, the flow runAs description corrected (H4), one ADR anchor, and one BREAKING changeset (spec, metadata, metadata-protocol minor). #23375 (eeb95f7) is merged and composes: the D2b leg runs inside applyApiEndpointGates, and applyPermissionSetOwnerBindingGate is chained after it, unchanged and unreordered. #23375 covers none of D2b.",
    "measurements": {
    "H1": "Partly falsified. The entries are as stated: validateApiEndpointDeclarations :155, policyGate :499. But @objectstack/metadata imports the gate. It calls validateApiEndpointDeclarations in MetadataManager.gateApiItemsForPublish (POST /packages/:id/publish), the endpoint publish door pinned by publish-endpoint-gate.test.ts, and identityFreeEndpointGateFailure in the endpoint matcher's load-time index. api is code-only (allowRuntimeCreate: false; the code-only inlet refuses it at the protocol). So the endpoint publish doors are the stack parse and the package publish, and both run D2b. flow is allowRuntimeCreate: true, so its doors are the stack parse, the package publish, and the protocol's publish-mode save plus draft promotion.",
    "H2": "Falsified for service-automation. It judges no publish: it registers a credential channel, a publish materializer and a mutation projector on the protocol, and arms flows after publish (registerFlow, the mutation sync). A stored flow's publish is judged in metadata-protocol, so service-automation is unchanged and is read as a consumer. Its credential migration re-saves rows through saveMetaItem with source migrate-stored, so the protocol leg skips that source, as the runtime authoring gate does.",
    "H3": "Holds; no new code minted. Stack parse: a zod custom issue at apis[i].authRequired, and defineStack throws STACK_SCHEMA_INVALID / 422. Package publish: the existing success:false validationErrors. Protocol: INVALID_METADATA / 422 with issues[0].path 'runAs', the envelope of that door's schema and authoring-gate refusals; it is deliberately outside the OS_ALLOW_UNLINTED_METADATA_WRITES escape hatch.",
    "H4": "The description misled: it prescribed system for user-less triggers including 'api'. It now names the anonymous-endpoint exception and the three serving channels. The two content/docs/references pages are regenerated. The hook runAs text is untouched (hooks are not endpoint-triggered)."
    },
    "tests": "Merged head 1bd1ba1 unless stated. New pins: (1) spec src/api/anonymous-elevated-flow-gate.test.ts, 14 cases. Each direction is the edit that completes the pair from an accepted stack, asserted as code custom at ['apis',0,'authRequired']; defineStack gives STACK_SCHEMA_INVALID / 422. Controls: an authenticated endpoint on a system flow, and an anonymous endpoint on a user or default-runAs flow. Also the judge's edges, and the prescription in both sentences. (2) metadata src/publish-anonymous-elevated-flow-gate.test.ts, 8 cases: the endpoint direction with the flow in the package and in another package; the flow direction; validate:false does not disable it; envelope peel; two controls; nothing published on a refusal. (3) metadata-protocol section 5 of protocol.item-name-every-door.test.ts, on that file's pinned double (no engine-double ledger change), 10 cases: the publish-mode save, publishMetaItem and publishPackageDrafts, each with its control; the endpoint read from the metadata service; a failing metadata-service read fails the publish; draft and migrate-stored saves not judged. Consumer readings (each package's own exit 0, os-verify-lock VERDICT command-exit 0): @objectstack/spec pnpm test 674 files / 20475 tests (1 todo); @objectstack/metadata 62 / 888; @objectstack/metadata-protocol 235 passed + 3 skipped / 28373 passed + 19 skipped; @objectstack/service-automation 186 / 2486; @objectstack/lint 138 / 6699; @objectstack/cli --project unit 287 / 4307 (integration declared to CI). Typecheck exit 0 for spec (test layer held at its pinned debt), metadata and metadata-protocol. Ablations at pre-merge head f66d8c1, through scripts/ablation-replace.mjs with its restore trap, expected direction (red) declared first; no build leg, because each suite reads the mutated file from src. A1, the stack leg fed no flows: 5 failed / 9 passed, the five stack-parse refusals. A2, the save-door call removed: 3 failed / 36 passed. A3, the promotion call removed: 2 failed / 37 passed. A4, the package-publish call removed: 5 failed / 3 passed. Each anchor went 1 to 0, each restore was proven (blob == HEAD, git diff HEAD empty), and the merges touched none of the four sites. Lint, as a proven narrowing at 1bd1ba1: eslint --no-inline-config --format json over the 10 changed .ts files gives 10 files, 0 errors, 0 warnings, 0 ignored. eslint.config.mjs enables no type-aware linting (no parserOptions.project, :328), so untouched files' verdicts cannot move. Repo-wide pnpm lint is left to CI.",
    "gates": {
    "head": "1bd1ba175",
    "derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 125 commands at 1bd1ba1, identical to the pre-merge derivation. The PM's lead file held 80 at 6eb6f7d; the difference comes from the docs, ADR-anchor, changeset and @objectstack/metadata paths.",
    "reconcile": "dispatch-gates --ran: '125 derived famil(ies) accounted for, 125 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0).",
    "results": "All 125 exit 0 at 1bd1ba1. Two first answered PREREQUISITE NOT MET (exit 3, nothing measured): pnpm --filter @objectstack/spec run check:skill-examples (client-react had no .d.ts) and pnpm check:dual-build-cjs-loads (nine packages unbuilt). Each was re-run on the same head after building its prerequisites: exit 0, verdict lines '263 prose examples type-check across 3 surface(s)' and the cjs provenance floors line. Notable verdict lines: check:generated 'All 14 generated artifacts are up to date'; check:api-surface 'public API surface + factory signatures unchanged'; check:adr-anchors exit 0 (new anchor); check:issue-citations 'no issue citations added against e4ec6ec (7 file(s) read)'; check:nul-bytes OK over 11379 files; check:system-context-census OK (no new elevation read site); check:engine-double-contract exit 0; check:type-check-debt exit 0 ('every entry sits exactly at its measurement'); check-adr-0087-registration --base origin/main exit 0 (the changeset reads BREAKING+bang+clause-2-narrowing, registered api-endpoint-anonymous-elevated-flow-refused).",
    "added": [
    "node scripts/check-changeset-no-major.mjs --base origin/main --event (this PR's title and body): exit 0, 'LEVEL AXIS: this PR declares clause-② no (narrowing)'; the same with the line amended to yes (narrowing): exit 0",
    "node scripts/pm/check-governed-merges.mjs --test (16 paths, --additions 1218 --deletions 14): exit 0, 0 of 16 paths hit the register, NOT governed",
    "pnpm --filter @objectstack/{spec,metadata,metadata-protocol} run typecheck: exit 0 each"
    ],
    "ci_at_report": "not read: the report is delivered at the end of local verification, and CI convergence is the PM's"
    },
    "line_budget": "n/a (no skills/**). 16 files, +1218 / -14 against the merge base; no governed surface.",
    "files_changed": [
    ".changeset/23225-d2b-anonymous-elevated-flow-refused.md",
    "content/docs/references/api/automation-api.mdx",
    "content/docs/references/automation/flow.mdx",
    "packages/metadata-protocol/src/protocol.item-name-every-door.test.ts",
    "packages/metadata-protocol/src/protocol.ts",
    "packages/metadata/src/metadata-manager.ts",
    "packages/metadata/src/publish-anonymous-elevated-flow-gate.test.ts",
    "packages/spec/api-surface/api.json",
    "packages/spec/export-origins/api.json",
    "packages/spec/src/api/anonymous-elevated-flow-gate.test.ts",
    "packages/spec/src/api/endpoint-publish-gate.ts",
    "packages/spec/src/api/index.ts",
    "packages/spec/src/automation/flow.zod.ts",
    "packages/spec/src/migrations/entries/semantic/18.api-endpoint-anonymous-elevated-flow-refused.ts",
    "packages/spec/src/stack.zod.ts",
    "scripts/adr-anchors/packages__spec__src__api__endpoint-publish-gate.ts.json"
    ],
    "deviations": [
    "Clause-② line, conflict named rather than chosen silently. The claim says no (narrowing), and the PR body's line 2 copies it verbatim as the order requires. The measured reading is yes (narrowing): the judge must be callable from metadata and metadata-protocol, so 5 functions and 3 interfaces join @objectstack/spec/api (api-surface +8), and landed narrowing-plus-export changesets (23015, 23072, 23165) declare yes (narrowing). The changeset carries yes (narrowing). Both values pass check-changeset-no-major (all three packages are minor). See open_questions.",
    "File surface widened under the order's measured-producer clause. packages/metadata/src/metadata-manager.ts and its new test: MetadataManager.publishPackage is a measured endpoint publish door, so it was not on the claim. service-automation is unchanged, because H2 measured no publish door there. The PR body names the landing and the reason.",
    "scripts/adr-anchors/packages__spec__src__api__endpoint-publish-gate.ts.json added: one new anchor file, per AGENTS.md PD13. It is data for the existing check:adr-anchors and is not a gate script.",
    "Protocol pins were added as section 5 of the existing protocol.item-name-every-door.test.ts rather than a new file, to reuse its pinned engine double and keep the engine-double ledger unchanged.",
    "The PR body was written once at opening, when 71 of 125 gates had run, and it says so. The final gate figures are in this report. The seat may rewrite that section if wanted.",
    "Lock queue: two calls timed out in the queue (exit 99, NOT MEASURED) behind pid 5617 (the issue-23357 spec/mp/lint suites, about 30 min), then the #23297 dev's build. Both were re-queued under the same slot and acquired.",
    "The two merges of origin/main (0add8a4, then e4ec6ec carrying eeb95f7) are git default merge commits with no trailer pair. My own commits carry the model-free pair AGENTS.md names; the harness reminder's model-named trailer was not used, because AGENTS.md's rule takes precedence.",
    "Zero label writes: the order names no label, and skip-changeset does not apply (a changeset ships).",
    "Worktree /home/user/objectstack-issue-23225-d2b removed after the push (node_modules first, no --force). The report comment is posted with the shared checkout's scripts/pm/fleet-write/dispatch.mjs, which cmp shows is byte-identical to the branch's."
    ],
    "mcp_calls": "0 — no MCP GitHub tool used",
    "api_writes": "2 relay strokes through scripts/pm/fleet-write/dispatch.mjs (repository_dispatch, executed as objectstack-fleet[bot]). (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23411/assignees (huangyiirene); the read-back stored 11521 bytes against 11521 sent, identical, draft true, assignee listed. (2) This os-dev-report comment = POST /repos//issues/23225/comments. git push is not a REST write. Reads: GET issues/23225 and its comments, GET pulls/23375/files, GET pulls/23411.",
    "open_questions": [
    {
    "question": "Should PR #23411's Clause-② line stay the claim's no (narrowing) or become the measured yes (narrowing)? The PR adds 8 exports to @objectstack/spec/api, a public-surface expansion, beside the accept-set narrowing.",
    "options": [
    "A: the seat amends the claim and the PR body line to yes (narrowing). The changeset already says so. check-changeset-no-major passes, because every moved package is minor. This matches the 23015, 23072 and 23165 precedents.",
    "B: keep no (narrowing) and edit the changeset's line to match. This declares no surface expansion while api-surface/api.json grows by 8."
    ],
    "recommendation": "A, because the exports are inherent to the record's design (one judge in spec, called from two other packages' doors). Declaring them is what makes the semver reading true, and it costs nothing at the gates."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none (承接者:无) · noted, not filed. The restore doors (rollbackMetaItem; revertCommit's restore limb) can re-activate a stored flow version declaring runAs: 'system' while an anonymous endpoint names it, and D2b does not judge them. The repository's deriveRestoredBody is synchronous, so it cannot ask the endpoints. The record scopes D2b to 'the publish path', and no cross-item refusal sits on restore today (the default-record-page refusal does not either). Dedupe words: restore rollback anonymous elevated flow, deriveRestoredBody cross-item, revertCommit runAs system.",
    "carrier: none (承接者:无) · noted, not filed. The PR's Acceptance notes also name what is not a publish door, so not judged: the endpoint matcher's load-time index; a flow registered straight into the engine; a pair split across two packages of one composed artifact at the stack parse, which reads top-level apis and flows only (the same scope as the per-endpoint gates; the package publish does judge across packages). And there is no full runtime counterpart: refusesElevatedSelfTriggeredStart covers self-triggered types only."
    ]
    }


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT (seat review): PR #23411 at 1bd1ba175 (Fixes #23225, ADR-0138 D2b, the card's last leg). It queues after one CONTRACT_REVIEW_TIER PASS on its head and a green head · 2026-10-14T07:03Z

    domain:spec seat 1 (#6017) · huangyiirene · session session_01YRp5z4kK6vgYkrdH97ztsy. The dev report is 6162772892; the claim is 6160651537, amended in place in this act.

    Read on GitHub, not from the report:

    • PR shape: draft, base main, first body line Fixes #23225. The body has no other closing keyword next to a card number. Slices 1 and 2 landed as Part of, and D2b is the last leg.
    • Files: 16 changed (+1218 / -14).
    • Landing class: check-governed-merges --pr 23411 reads NOT governed.
    • The changeset, read sentence by sentence against the diff: @objectstack/spec, @objectstack/metadata and @objectstack/metadata-protocol minor, BREAKING. It carries the FROM → TO table, the one-line remedy, and the ADR-0087 disposition registered api-endpoint-anonymous-elevated-flow-refused, a new step-18 entry in the diff. Its "What is refused, and where" names the three doors the diff edits:
      • stack.zod.ts applyApiEndpointGates;
      • MetadataManager.publishPackage;
      • saveMetaItem publish mode and promoteDraftForPublish.
    • Composition with main: PR fix(spec,objectql)!: a permission set's body packageId that disagrees with the package shipping it is refused at the build and registration doors; both readers read the binding only #23375 (eeb95f7ff) is merged into the branch, and applyPermissionSetOwnerBindingGate still chains after applyApiEndpointGates, unchanged and unreordered (the dev's reading; the contract review re-reads it).

    Seat amendments in this act:

    • Clause-② → yes (narrowing), the dev's open question, answered A. The judge joins @objectstack/spec/api as 8 published exports, so a new public surface sits beside the narrowing, and declaring yes is never an error.
      • The claim's line and the PR body's line 2 are amended to match the changeset.
      • The repo's one reader, clause2-line.mjs readClause2Line, reads both as declared / yes / narrowing.
    • File surface: the claim gains packages/metadata/src/metadata-manager.ts and its test (a measured publish door, cross-lane declared on [PM seat] domain:server — 🟢 os-project-manager #6367 6162837793), the scripts/adr-anchors/ data file, and two regenerated references pages. @objectstack/service-automation is unchanged by measurement (H2).

    Out-of-scope findings:

    • The restore doors (rollbackMetaItem, revertCommit's restore limb) can re-activate a stored runAs: 'system' flow version while an anonymous endpoint names it: Acceptance notes, not filed. The record scopes D2b to "the publish path", and no cross-item refusal sits on restore today. The seat names it in its round report for the maintainer.
    • The non-publish paths the PR's Acceptance notes list (the matcher's load-time index, an engine-registered flow, a pair split across two packages of one composed artifact at the stack parse): Acceptance notes.

    Next:

    • A CONTRACT_REVIEW_TIER review runs when a slot frees: the seat runs at batch 2 on the maintainer's word.
    • The PR touches regenerated paths (packages/spec/api-surface/**, packages/spec/export-origins/**, content/docs/references/**), so it is synced through scripts/pm/os-regen-merge.sh before the queue, if main has moved them.

    Generated by Claude Code

  11. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23411 → c857a93ee (Fixes #23225, ADR-0138 D2b, the card's last leg). The card closed completed on the merge · 2026-10-14T07:48Z

    domain:spec seat 1 (#6017) · huangyiirene · session session_01YRp5z4kK6vgYkrdH97ztsy · holder of claim 6160651537.


    Generated by Claude Code

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:specenhancementNew feature or requestpriority:p1High: required for production / M2securitytarget:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions