Repository navigation
ADR-0138 E2: the closed list of doors recorded in the conformance matrix, denial by domain, and D2b's publish refusal (D2, D2b) #23225
Description
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: "
shouldDenyAnonymousat 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-declaredauthRequired: falseendpoint 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
objectstack-fleet commentedon Oct 13, 2026
Triage: first grade,
enhancement·security·priority:p1·target:v18·domain:cli·area:access, intopm:queue. ADR-0138 E2Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T10:58Z. ⛔ Not a claim, ⛔ not a dispatch.securitytarget:v18: it executes the accepted guest model (ADR-0138, design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146). E2 is the closed list of anonymous doors and denial by domain.domain:clipackages: the domain-level deny at each domain's entry (runtime,rest) and the conformance matrix (packages/qa).@objectstack/specandservice-automation) narrows a published accept set. So it isClause-②: noand stays in this lane, but it owes a contract-tier review before the queue (execution-duties.md). The claim declares the cross-lane files.objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 17 (#23225 slice 1: ADR-0138 D2, the closed list of six door classes recorded in the conformance matrix, and denial by domain; D2b is slice 2) · 2026-10-13T11:30Z
Session:
session_01VoSxBQujKLZKPwK2u5ehQ6Account:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23225-s1-closed-door-listWorktree:
objectstack-issue-23225-s1Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/main6f4c518b01; stop on a breach and explain it in the report):packages/qa/dogfood/test/authz-conformance.matrix.ts(AUTHZ_CONFORMANCE) records ADR-0138 D2's six door classes, each row naming its enforcing site as the record's table does. Its tests inpackages/qa/dogfood/test/carry the row's pins, each with a negative control:shouldDenyAnonymous), inpackages/runtime(the dispatcher's domains) orpackages/rest. The dev measures which faces those are first.createHonoApp's catch-all flattens a repeated query name to its last value, so every dispatcher arity rule is unreachable on a@objectstack/honohost #23214 slice 1 holds (packages/runtime/src/domains/{meta,ui,i18n,security,data,packages}.ts, claim6144390225) is NOT made here. It is reported and done after that slice lands.packages/core/src/security/assemble-execution-context.ts: the module doc cites ADR-0138 (the row's "D9's pointer"). Doc only, and cross-lane (domain:engine): declared on [PM seat] domain:engine — 🟢 sunhaolin #6367 in this act.packages/spec/src/api/endpoint-publish-gate.ts,@objectstack/service-automation, the ADR-0087 semantic entry andmetadata-protocol's publish path;Direction, verbatim from the record (ADR-0138 D2, Enforced by): "
shouldDenyAnonymousat 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." ⛔ Nothing here re-decides the record (triage6144040679). A measurement that departs from it is raised, never chosen silently.Container & model:
M, judged (six class rows with their pins, a measured set of domain-level moves),mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). The record measured that everything-else answers 401 throughout onmain(G2), so a per-face to domain-level move is answer-neutral, andClause-②: nowith no review owed. If the dev measures a move that changes an answer, the slice becomesClause-②: no (narrowing)and oneCONTRACT_REVIEW_TIERreview is owed before the queue.check-governed-merges --testover the claimed paths: not governed.Clause-②: no
Responsibility: n/a — not a defect card (an accepted record's execution card)
Thread-read: 6144040679
Serial constraints cleared:
authz-conformance.matrix.ts,assemble-execution-context.ts,anonymous-deny.ts,resolve-execution-context.ts, the runtime domains orrest-server.ts(file lists of all open PRs read in this act).createHonoApp's catch-all flattens a repeated query name to its last value, so every dispatcher arity rule is unreachable on a@objectstack/honohost #23214 slice 1 (this seat, in flight) holds six runtime domain files. They are excluded above.endpoint-policy.ts,dispatcher-plugin.ts, REST's direct mounts and cloud-connection, which are disjoint.packages/create-objectstack) is disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{
"issue": 23225,
"status": "done",
"branch": "claude/issue-23225-s1-closed-door-list",
"pr": "#23286",
"session": "session_01VoSxBQujKLZKPwK2u5ehQ6 — the dispatching seat's id (mode:subagent)",
"premise_still_valid": true,
"summary": "Slice 1 (D2) of ADR-0138 card E2. The six D2 door classes are recorded in AUTHZ_CONFORMANCE as enforced, HIGH_RISK rows naming D2's enforcing sites, each proven by the existing anonymous-door-sweep.dogfood.test.ts (which now claims exactly those six), with a new matrix block holding the list closed and negative controls for a seventh, a missing, an unproven or a renamed row. Every dispatcher domain's anonymous decision was censused: /notifications, /keys and /share-links decided it per face below another answer and now decide it as their first statement (share-links keeps the class 2 token faces), each with a floor row, a gate pin and its ledger key classified (baseline 30 to 26, probe census re-measured to 23 probes / 18 files / 29 keys). The module doc of assemble-execution-context.ts cites ADR-0138 D9 and the D2 list (doc only). D2b is untouched (slice 2).",
"six_rows": [
{
"row": "anonymous-door-public-form",
"class": 1,
"enforcement_names": [
"RestServer.registerFormEndpoints",
"publicFormGrant branch of SecurityPlugin"
],
"proof": "anonymous-door-sweep.dogfood.test.ts (class 1: resolve and submit of an anonymous-sharing form admitted; an undeclared slug refused)",
"covers": [
"rest-family:rest-route-ledger.ts:forms"
]
},
{
"row": "anonymous-door-share-link",
"class": 2,
"enforcement_names": [
"registerShareLinkRoutes (plugin-sharing)"
],
"proof": "same file (class 2: a minted token resolves its one record; revoked, refused)",
"covers": []
},
{
"row": "anonymous-door-public-book",
"class": 3,
"enforcement_names": [
"isPublicAudienceRead (rest meta-item-read-gate)"
],
"proof": "same file (class 3: public book, tree and claimed doc admitted; org book, org doc and a write refused 401)",
"covers": []
},
{
"row": "anonymous-door-endpoint-object-operation",
"class": 4,
"enforcement_names": [
"resolveExecutionContext (guest envelope, D9)",
"the CRUD gate"
],
"proof": "same file (class 4: guest envelope reaches the engine once and gets D3's empty-state refusal; the gated sibling 401)",
"covers": []
},
{
"row": "anonymous-door-endpoint-flow",
"class": 5,
"enforcement_names": [
"resolveExecutionContext",
"@objectstack/service-automation"
],
"proof": "same file (class 5: the flow runs once as the guest and serves its output only; the gated sibling 401)",
"covers": []
},
{
"row": "anonymous-door-public-read-file",
"class": 6,
"enforcement_names": [
"registerStorageRoutes",
"authorizeDownload"
],
"proof": "same file (class 6: a public_read file's bytes served at both doors; a private file 401 at both)",
"covers": []
}
],
"new_route_pin": "Through the matrix's own two mechanisms. (1) A new route FAMILY or dispatcher DOMAIN mints a ledger key and is UNCLASSIFIED: already enforced; negative controls are the existing population-block cases, re-measured by scratch ablation 3 (a new ledger domain: UNCLASSIFIED red). (2) A route INSIDE an existing domain mints no key (the matrix header's measured bound, re-measured by scratch ablation 4: zero UNCLASSIFIED); it is held by the domain floor being the first statement: the new negative control deletes each of the 8 gate-pinned floors in turn and asserts STALE, a second case asserts the minted gate pins are exactly those 8, and each moved domain's unit suite (runtime/src/domains/domain-floor-anonymous-deny.test.ts, case B) asserts a path or verb no face serves yet is refused 401 before route selection. No new gate script.",
"census": {
"domain_level_first_statement": [
"/actions",
"/automation (with the ADR-0069 allow-list row for the signed inbound hook)",
"/analytics",
"/packages (23214 file)",
"/i18n (23214 file)",
"/security (23214 file)",
"/meta (23214 file; class 3 carve-out through isPublicAudienceRead)"
],
"moved_here": [
"/notifications (decided below the inbox capability probe)",
"/keys (below the method check)",
"/share-links (inside the management faces, below the service probe; class 2 token faces kept as the one exception)"
],
"deferred_to_after_23214_slice_1": [
"/ui (runtime/src/domains/ui.ts: handleUiRequest makes no anonymous decision of its own)",
"/data (runtime/src/domains/data.ts: handleDataRequest makes no anonymous decision of its own; anonymity is left to the guest envelope at the CRUD gate)"
],
"not_moved_raised": [
"/ai (handleAIRequest decides once at entry but applies it per route: a registered route declaring auth false is admitted to an anonymous caller; see open_questions)",
"/webhooks/redeliver (decided by the plugin-webhooks member by design, byte parity with the self-hosted mount; a domain floor would re-decide that)",
"REST families (each handler opens with enforceAuth: per route by construction; only the metadata family has a family-wide guarded registrar; read as outside D2's dispatcher-domain sense, see open_questions)"
],
"outside_by_construction": [
"/mcp (D2's own outside family; its 401 follows the enabled and service probes)",
"/mcp/skill",
"/approvals/act",
"/auth",
"/health",
"/ready",
"/discovery",
"/.well-known/objectstack",
"the declared-endpoint fallback seam (classes 4 and 5 and authRequired true endpoints, decided per declaration by endpoint-policy)"
]
},
"changeset_decision": ".changeset/23225-domain-floor-anonymous-deny.md: '@objectstack/runtime': minor, Clause-②: no (narrowing), ADR-0087 not-required (no-migration-prescription). Measured: runtime's published code moves (three domain files); @objectstack/core's published dist/index.d.ts carries the new doc comment (3 ADR-0138 hits, all from this file) but a comment is fast-track no-release, so no core changeset; packages/qa/dogfood is private. Deciding gates: check:adr-0087-registration --base origin/main, check:changeset-no-major --base origin/main, check:empty-changeset --base origin/main, all exit 0 at 8f838b7; CI's Check Changeset counts the one added changeset. PR body line 2 is Clause-②: no (narrowing); one CONTRACT_REVIEW_TIER review is owed before the queue, per the claim's rule for a move that changes an answer.",
"departures_raised": [
"Grade: the three moves change what an anonymous caller is answered on compositions the record did not measure (501 to 401 with no inbox or no share-link service; 405 to 401 on the key mint). The claim's rule makes that the narrowing grade; execution-duties.md says runtime security behaviour is not Clause ② and Clause ② is untouched by pulling back to a declared contract. I followed the claim (narrowing, review owed) and name the tension for the seat.",
"The /ai route-level auth false opt-out is a door mechanism D2 does not list; not moved, raised as needs_decision in open_questions.",
"REST families decide per route; whether D2's 'domain' covers them is raised in open_questions; nothing moved there.",
"Two #23214-held domain files carry no domain-level decision at all; deferred, with the readings to the seat privately."
],
"tests": "All at the final tree 8f838b7 unless stated. (a) closure build pnpm turbo run build --filter=@objectstack/dogfood^... 64/64 tasks; @objectstack/dogfood typecheck green; vitest test/anonymous-door-sweep.dogfood.test.ts test/authz-conformance.test.ts test/authz-probe-blind-spot.test.ts: Test Files 3 passed (3), Tests 124 passed (124); os-verify-lock VERDICT command-exit 0. (b) runtime vitest over the 8 touched or ordering-sensitive files (domain-floor-anonymous-deny, domain-handler-registry, share-links-public-cache-headers, notification-schema-conformance, declared-capability-absence-warn-once, dispatcher-5xx-always-logged, http-dispatcher.keys, http-dispatcher): Test Files 8 passed (8), Tests 405 passed (405); runtime typecheck (tsc + check:test-typecheck OK: 27 files / 190 errors / 68 pinned held) and core typecheck green; VERDICT command-exit 0. (c) full runtime --project local at 2d84837: Test Files 368 passed (368), Tests 5269 passed, 19 skipped; VERDICT command-exit 0; the later merge touched no runtime, rest, core or dogfood file. Ablations at 2d84837, each through scripts/ablation-replace.mjs with a restore trap under os-verify-lock: (1) class 3 admission removed in isPublicAudienceRead (anchor 1 to 0, blob 41952c07b37a to 41c05ff602de, rest rebuilt, dist preflight marker in 2 built files): sweep 2 failed / 18 passed, exactly class 3's two admitted legs; restore: blob == HEAD, rebuilt, preflight --absent (0 of 6 files, tree clean), sweep 20/20. (2) /notifications floor condition replaced by false (marker 1, gate spelling 0): runtime unit 14 failed / 33 passed (every notifications A/B/C case, both anonymous shapes); matrix 9 failed / 95 passed incl. STALE covers notifications:domains/notifications.ts:anonymous-gate and DEAD PROBE; restore: blob == HEAD, 47/47 and 104/104. (3) scratch new ledger domain /anon-scratch: UNCLASSIFIED surface dispatcher-domain:route-ledger.ts:/anon-scratch (8 failed / 53). (4) scratch route inside /notifications: zero UNCLASSIFIED, only the docblock row-count pin (2 failed). First attempt of (3) and (4) was a no-op the tool refused (the replacement contained its own anchor: anchor count 1 to 1); re-run with a different anchor, readings above are the second run. Lint, proven narrowing at 8f838b7: eslint --no-inline-config --format json over the 16 changed .ts files: 16 files, 0 errors, 0 warnings, 0 reported ignored; no type-aware linting in eslint.config.mjs, so untouched files' verdicts cannot move; repo-wide pnpm lint left to CI.",
"gates": {
"head": "8f838b7460",
"derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 69 commands, identical to the derivation at af299c2",
"reconcile": "dispatch-gates --ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN",
"results": [
{
"command": "node scripts/check-adr-0087-registration.mjs --base origin/main",
"exit": 0,
"verdict_line": "::notice file=.changeset/23225-domain-floor-anonymous-deny.md::ADR-0087 exemption (no-migration-prescription): No metadata moves: no spec key, authorable spelling, object definitio"
},
{
"command": "node scripts/check-adr-0087-registration.mjs --self-test",
"exit": 0,
"verdict_line": "✓ check-adr-0087-registration --self-test: 548 assertions over real temp git repos (real scan()/assertInputs() path)"
},
{
"command": "node scripts/check-changeset-no-major.mjs --base origin/main",
"exit": 0,
"verdict_line": "✓ Protocol lockstep: PROTOCOL_VERSION major 18, @objectstack/spec@18.0.0-next.0 — not one major ahead of a stable version, so there is no pre-mode exception to judge."
},
{
"command": "node scripts/check-changeset-no-major.mjs --self-test",
"exit": 0,
"verdict_line": "✓ check-changeset-no-major --self-test: 401 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff "
},
{
"command": "node scripts/check-ci-filter-parity.mjs",
"exit": 0,
"verdict_line": "OK: all 21 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every
$TURBO_ROOT$input of a build Build Core runs) and all 11 script(s) th"},
{
"command": "node scripts/check-closing-keyword-parity.mjs",
"exit": 0,
"verdict_line": " • packages/spec/CHANGELOG.md -- 8701420 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files"
},
{
"command": "node scripts/check-closing-keyword-parity.mjs --self-test",
"exit": 0,
"verdict_line": "✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red."
},
{
"command": "node scripts/check-comment-mask-adoption.mjs",
"exit": 0,
"verdict_line": "OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/, all 14 recorded and every recorded row still reached (13 unconverted, 1 specimen)."
},
{
"command": "node scripts/check-comment-mask-adoption.mjs --self-test",
"exit": 0,
"verdict_line": "PASS check-comment-mask-adoption --self-test (0 failure(s))"
},
{
"command": "node scripts/check-comment-mask-corpus.mjs",
"exit": 0,
"verdict_line": "✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8976 files, 0 disagree, 0 unparseable, 140.0s (comparator self-test: 26 cases pass)."
},
{
"command": "node scripts/check-dev-prereqs.mjs --self-test",
"exit": 0,
"verdict_line": "✓ check:dev-prereqs --self-test — every verdict reachable, exclusions and freshness coverage pinned (19 batteries, 76 cases), plus the shared workspace enumerator."
},
{
"command": "node scripts/check-dts-emitted.mjs --self-test",
"exit": 0,
"verdict_line": "check-dts-emitted self-test: all assertions passed."
},
{
"command": "node scripts/check-empty-changeset.mjs --base origin/main",
"exit": 0,
"verdict_line": "✓ No changeset from the merge base modified or deleted by this diff (#17712)."
},
{
"command": "node scripts/check-empty-changeset.mjs --self-test",
"exit": 0,
"verdict_line": "✓ check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)"
},
{
"command": "node scripts/check-issue-citations.mjs",
"exit": 0,
"verdict_line": "✅ check-issue-citations: no issue citations added against 09ef77c (4 file(s) read)."
},
{
"command": "node scripts/check-keyed-text-bounds.mjs",
"exit": 0,
"verdict_line": " ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide. Reproduce the record: see this file's header."
},
{
"command": "node scripts/check-keyed-text-bounds.mjs --self-test",
"exit": 0,
"verdict_line": "PASS check-keyed-text-bounds --self-test (0 failure(s))"
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs",
"exit": 0,
"verdict_line": "✓ platform-object tenancy census matches the tree: 87 platform-namespace objects, 52 in the machinery's reach, 35 outside it, every exclusion explained by a declaration on its own "
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs --self-test",
"exit": 0,
"verdict_line": "✓ check-platform-object-tenancy-census self-test: all checks pass (87 objects, 35 outside the machinery)"
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs",
"exit": 0,
"verdict_line": "✓ check:plugin-teardown-shape: 77 Plugin implementation(s) across 8390 source(s) under packages/; every teardown-shaped method (stop / shutdown / close / dispose) sits beside a r"
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs --self-test",
"exit": 0,
"verdict_line": "✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, every roster name reds, eve"
},
{
"command": "node scripts/check-registry-log-declared.mjs",
"exit": 0,
"verdict_line": " payload, not read (a dir the package's own tsconfig.json excludes): packages/create-objectstack/src/templates"
},
{
"command": "node scripts/check-registry-log-declared.mjs --self-test",
"exit": 0,
"verdict_line": "self-test OK: 17 cases + level-vocabulary read + population declaration + real-tree selection floor."
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs",
"exit": 0,
"verdict_line": "OK: 32 of 284 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level."
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs --self-test",
"exit": 0,
"verdict_line": "check-rest-log-spy-declared self-test reached its verdict: 23 case(s), 0 failure(s)."
},
{
"command": "node scripts/check-system-context-census.mjs",
"exit": 0,
"verdict_line": "check-system-context-census: OK — 134 elevation read sites in 21 packages across 61 files, living in 114 symbol(s); the page cites 127 symbol(s) against 127 required, over 150 anch"
},
{
"command": "node scripts/check-system-context-census.mjs --self-test",
"exit": 0,
"verdict_line": "check-system-context-census --self-test: all cases passed"
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs",
"exit": 0,
"verdict_line": " ⚠ The delta is information, not a verdict — the floors are
>=and cannot see an upward drift at all, which is why it is PRINTED. Reproduce the record: see this file's header."},
{
"command": "node scripts/check-undeclared-dep-imports.mjs --self-test",
"exit": 0,
"verdict_line": "PASS check-undeclared-dep-imports --self-test (0 failure(s))"
},
{
"command": "node scripts/docs-audit/check-affected-docs.mjs",
"exit": 0,
"verdict_line": " → the unreachable rows themselves: this command with --json"
},
{
"command": "node scripts/docs-audit/check-drift-comment.mjs",
"exit": 0,
"verdict_line": "✓ check-drift-comment: 66 cases pass across 5 fixture diff(s)."
},
{
"command": "node scripts/pm/release-rehearsal-clone.mjs --self-test",
"exit": 0,
"verdict_line": "✓ self-test passed"
},
{
"command": "node scripts/release-pending-publish.mjs --self-test",
"exit": 0,
"verdict_line": "✓ release-pending-publish self-test: 92 cases across 22 batteries pass."
},
{
"command": "pnpm --filter @objectstack/spec run check:duration-unit-keys",
"exit": 0,
"verdict_line": "✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 3082 source file(s) all carry their unit in the key name (or in a sibling
unit, or under a declared exemptio"},
{
"command": "pnpm --filter @objectstack/spec run check:empty-state",
"exit": 0,
"verdict_line": "✓ all classified (2 closed, 2 open, 4 output, 9 scope)"
},
{
"command": "pnpm --filter @objectstack/spec run check:liveness",
"exit": 0,
"verdict_line": " (not a completeness claim about the 529 child key(s) under the declared blanket verdicts above — those are recorded, not classified.)"
},
{
"command": "pnpm --filter @objectstack/spec run check:strictness-ledger",
"exit": 0,
"verdict_line": " untriaged: 1212 object site(s) across 9 director(ies)"
},
{
"command": "pnpm --filter @objectstack/spec run check:variant-docs",
"exit": 0,
"verdict_line": "✓ variant/doc gate: 18 discriminated union(s) — 8 governed (every variant mentioned in a bound doc), 10 exempt."
},
{
"command": "pnpm check:changeset-gate-self-tests",
"exit": 0,
"verdict_line": "✓ check-changeset-no-major --self-test: 401 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff "
},
{
"command": "pnpm check:cross-package-test-inputs",
"exit": 0,
"verdict_line": "OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split "test:repo" task); 13 walked root(s) judged, 3 on ACCEPTED_"
},
{
"command": "pnpm check:dispatcher-error-vocabulary",
"exit": 0,
"verdict_line": " [#15723] the ARGUMENT POSITION of new APIError( … ) and APIError.from( … ) IS now in this gate's population — the
apierrorargshape.APIError.fromcopies the record'scode"},
{
"command": "pnpm check:doc-authoring",
"exit": 0,
"verdict_line": "✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 95099 string(s) read in 1325 parsed source(s) (seen floor 40000 strings / 60"
},
{
"command": "pnpm check:driver-memory-census",
"exit": 0,
"verdict_line": "check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states "#6664 census: 2 ruled consumers". This gate polices the cen"
},
{
"command": "pnpm check:dts-closure",
"exit": 0,
"verdict_line": "check-dts-closure: 73 built package(s) swept - 173/173 declared declaration file(s) present across 72 package(s); 1 built package(s) declare no declaration entry point and owe none"
},
{
"command": "pnpm check:dual-build-cjs-loads",
"exit": 0,
"verdict_line": " ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide. Reproduce the record: see this file's header."
},
{
"command": "pnpm check:engine-double-contract",
"exit": 0,
"verdict_line": " ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide."
},
{
"command": "pnpm check:gitlink-declared",
"exit": 0,
"verdict_line": "check-gitlink-declared: OK (11259 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare)."
},
{
"command": "pnpm check:issue-citations",
"exit": 0,
"verdict_line": "✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes kept apart, both board strategies agree, d"
},
{
"command": "pnpm check:kernel-hook-pairs",
"exit": 0,
"verdict_line": "✓ kernel hook pin pairing: 4 dispatched kernel:* hook(s), each pinned in both kernel.test.ts and lite-kernel.test.ts"
},
{
"command": "pnpm check:lean-entry-closure",
"exit": 0,
"verdict_line": " Admitted set held exactly (15 packages); 6 denied names absent."
},
{
"command": "pnpm check:logger-receiver-detach",
"exit": 0,
"verdict_line": " control corpus fired on all five detach shapes in this same run, and stayed silent on the measured
consoleand options-callback populations -- so the zero above is a reading."},
{
"command": "pnpm check:nul-bytes",
"exit": 0,
"verdict_line": "check-nul-bytes: OK (scanned 11250 text file(s) -- 11250 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes)."
},
{
"command": "pnpm check:objectql-double-limit",
"exit": 0,
"verdict_line": " baseline key set verified against 09ef77c: no files added."
},
{
"command": "pnpm check:objectui-changeset",
"exit": 0,
"verdict_line": "✓ objectui-range --self-test: all checks passed"
},
{
"command": "pnpm check:org-identifier",
"exit": 0,
"verdict_line": "check-org-identifier: OK (3401 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias)."
},
{
"command": "pnpm check:page-declaration-shape",
"exit": 0,
"verdict_line": " blind spot: 1 computed carrier(s) no source scan can enumerate — examples/app-crm/objectstack.config.ts:85."
},
{
"command": "pnpm check:pm-changeset-deadline-census",
"exit": 0,
"verdict_line": "✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, the verdict truth table includ"
},
{
"command": "pnpm check:published-files",
"exit": 0,
"verdict_line": "✓ check:published-files — 71 publishable package(s) of 82 workspace member(s) declare a
fileswhitelist that covers every entry point plus CHANGELOG.md and admits no test, test-h"},
{
"command": "pnpm check:query-options-erasure",
"exit": 0,
"verdict_line": " baseline key set verified against 09ef77c: no files added."
},
{
"command": "pnpm check:refd-timer-probe",
"exit": 0,
"verdict_line": " 1 code site(s), all inside the approved module, which is present and still reads it."
},
{
"command": "pnpm check:route-envelope",
"exit": 0,
"verdict_line": " read/write discriminator: 13 file(s) skipped as fetch readers (79 zero-argument
res.json()call(s), none swept in)"},
{
"command": "pnpm check:slot-lookup",
"exit": 0,
"verdict_line": " baseline key set verified against 09ef77c: no files added."
},
{
"command": "pnpm check:sourcemap-no-sources-content",
"exit": 0,
"verdict_line": "check-sourcemap-no-sources-content: 70 built package(s) swept - 554 map(s), none embed source text."
},
{
"command": "pnpm check:test-source-alias",
"exit": 0,
"verdict_line": "check-test-source-alias OK — 74 packages with tests scanned; 60 registered as still resolving a workspace dep through
dist/; 53 published subpath(s) resolved through every alias "},
{
"command": "pnpm check:tier-file-adoption",
"exit": 0,
"verdict_line": " @objectstack/cli — 82 file(s); imports readTierMode, selectTierFiles from scripts/nightly-tiers.mjs (via packages/cli/vitest-tiers.ts)"
},
{
"command": "pnpm check:type-check-coverage",
"exit": 0,
"verdict_line": " ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide."
},
{
"command": "pnpm check:watch-hint-literal",
"exit": 0,
"verdict_line": "✓ check-watch-hint-literal: 73 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 49, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -- every on"
},
{
"command": "pnpm check:where-matcher",
"exit": 0,
"verdict_line": " baseline key set verified against 09ef77c: no files added."
},
{
"command": "pnpm check:type-check-debt",
"exit": 0,
"verdict_line": "check-type-check-coverage --re-measure: OK — 1 ledger entr(ies) re-measured in 93.1s, 26 raw tsc error(s) total, none above its recorded number."
}
],
"added": [
"pnpm --filter @objectstack/dogfood exec vitest run test/authz-conformance.test.ts test/authz-probe-blind-spot.test.ts test/anonymous-door-sweep.dogfood.test.ts: exit 0",
"pnpm --filter @objectstack/dogfood run typecheck: exit 0",
"pnpm --filter @objectstack/runtime run typecheck: exit 0",
"pnpm --filter @objectstack/core run typecheck: exit 0",
"check:system-context-census: exit 0 in the derived set (it went red once mid-slice when the floors passed isSystem: three new elevation read sites; fixed by asking the floors about the user id alone)"
]
},
"line_budget": "17 files, +845 / -59 against origin/main (no generated file); no governed surface (no .claude, docs/adr, skills, AGENTS.md or CLAUDE.md path); no skills/** budget applies",
"files_changed": [
".changeset/23225-domain-floor-anonymous-deny.md",
"packages/core/src/security/assemble-execution-context.ts",
"packages/qa/dogfood/test/anonymous-door-sweep.dogfood.test.ts",
"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/declared-capability-absence-warn-once.test.ts",
"packages/runtime/src/dispatcher-5xx-always-logged.test.ts",
"packages/runtime/src/domain-handler-registry.test.ts",
"packages/runtime/src/domains/domain-floor-anonymous-deny.test.ts",
"packages/runtime/src/domains/keys.ts",
"packages/runtime/src/domains/notifications.ts",
"packages/runtime/src/domains/share-links-public-cache-headers.test.ts",
"packages/runtime/src/domains/share-links.ts",
"packages/runtime/src/notification-schema-conformance.test.ts"
],
"deviations": [
"A backgrounded '&&' chain assigned the scratch-dir variable inside the background subshell, so its pid file landed at the filesystem root: /build-closure.pid (6 bytes, a pid). The harness's safety check refused my rm of it; it needs deleting by the seat or a person.",
"Five existing runtime tests that used an anonymous caller only incidentally (a 405, a 501, a thrown-error propagation, a declared-absence 501 fixture) were moved to a signed-in caller beside an added anonymous-401 assertion, the pattern the /analytics move used; within the moved faces' tests.",
"The census file, its test and the population baseline in packages/qa/dogfood/test were edited as the matrix's companions (figures, rows, dated notes); within the dogfood test surface.",
"Zero label writes: the dispatch's write budget names no label, and skip-changeset does not apply (a changeset is added).",
"Lock queue timeouts (exit 99) on two runs were NOT MEASURED and re-run; a failed closure build left cloud-connection without declarations, which made check:type-check-debt read +1 and dogfood typecheck TS7016 transiently; after a clean rebuild both are green at the final tree."
],
"mcp_calls": "0 — no MCP GitHub tool used",
"api_writes": "2 relay strokes through scripts/pm (fleet-write repository_dispatch, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23286/assignees (os-steve), read back 11600 bytes identical; (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/23286.",
"open_questions": [
{
"question": "The /ai dispatcher domain (handleAIRequest) and the dispatcher-plugin mount (mountRouteOnServer) admit an anonymous caller on any registered AI route that declares auth false. ADR-0138 D2 lists six door classes and says a new door class is an amendment. Move /ai to the domain-level deny (retiring the route-level opt-out), or record the opt-out?",
"options": [
"A: retire the route-level auth false opt-out and move /ai to the domain-level deny. Business need: zero in-repo producers (measured: no non-test 'auth: false' route in this repository); the Cloud/EE service-ai route table was not read. Long-term: closes the last per-route anonymous opt-out on the dispatcher, matching D2. AI-authoring safety: strongest, no unlisted door can be declared. Startup focus: one domain edit plus the retirement of a runtime contract the tests pin today; needs the cloud consumer census first. Cost: small code, a cross-repo measurement.",
"B: keep the opt-out and amend ADR-0138 with a seventh class for declared public AI routes. Business need: only if cloud's service-ai declares one. Long-term: a second declaration-derived door. AI-authoring: weaker, a route can open itself. Startup focus: an ADR amendment for an unmeasured need.",
"C: leave as is, record the gap. Cheapest, but it is a declared exit from the closed list that nothing enforces."
],
"recommendation": "A, after a read of cloud's service-ai route table: no in-repo producer exists, the record's closed list and the startup axis both argue against a door kept for an unmeasured user, and A is the only option where what is declared is what is enforced. Seat or maintainer call; not decided here."
},
{
"question": "Do D2's 'domains' include the REST server's route families? They decide per route by construction (each handler opens with enforceAuth); only the metadata family has a family-wide guarded registrar, so a new REST route that forgets enforceAuth is caught only by the booted sweep.",
"options": [
"A: no — D2's 'domain' is the dispatcher domain the runtime face's guest envelope reaches (D2's own sentence about the envelope), and the REST seam stays per route, held by the sweep and the per-route census.",
"B: yes — give each REST family a guarded registrar (the registerMetadataEndpoints shape) in a separate card; a large rest-server.ts change and a re-measure of the probe census's enforceAuth counts."
],
"recommendation": "A for this card (it is the reading the record's text supports and nothing in it measured otherwise); B, if wanted, is its own card with its own measurement."
}
],
"out_of_scope_findings": [
"class: a · reach: exception: security (possible schema disclosure to an anonymous caller on a composition that routes to the dispatcher's /ui domain; not reproduced end to end) · evidence: runtime/src/domains/ui.ts handleUiRequest makes no anonymous decision and calls the protocol's getUiView, which reads the object's schema with no principal; an existing unit test dispatches the domain with an empty context and asserts getUiView is called; the standalone showcase never routes there; readings to the seat privately · fix lands as the deferred /ui move after 23214 slice 1 (that slice holds the file) · dedupe words: dispatcher ui domain anonymous getUiView, handleUiRequest shouldDenyAnonymous, ui view schema guest",
"class: b · reach: named producer: the runtime dispatcher's /data domain on a composition without @objectstack/rest · evidence: runtime/src/domains/data.ts handleDataRequest makes no anonymous decision, so an anonymous caller reaches the guest envelope's CRUD gate (a 403) instead of D2's 401; every /data ledger route is shadowed by REST where REST is mounted · Seam: spec:ADR-0138 D2 everything-else 401 → runtime:handleDataRequest · deferred /data move after 23214 slice 1 · dedupe words: dispatcher data domain anonymous 401, handleDataRequest floor",
"carrier: 23214 slice 1 (domain:cli seat) · the /ui and /data moves above land in that slice's files once it merges; noted, not filed"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
✅ ACCEPT: PR #23286 at
8f838b7460(#23225 slice 1, ADR-0138 D2;Part of #23225). It queues after oneCONTRACT_REVIEW_TIERPASS on this head and a green headdomain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-13T14:27Z · holder of claim6144558082. Report:os-dev-report6147723822. Thread-read: 6147723822. This note stays at function level.Checklist, read against GitHub:
PR shape: draft, base
main, line 1Part of #23225(no closing keyword), line 2Clause-②: no (narrowing), assigneeos-steve.feat(runtime,dogfood): …, and is nowfix(runtime)!: …, matching the changeset's narrowing grade. A squash takes the title as its subject.check-changeset-no-major --base origin/main --eventon the corrected title and body exits 0 (level axisno (narrowing), the launch-window arm).Scope: 17 files, +845 / −59, all inside the claim:
packages/qa/dogfood/test/(the matrix, its tests, the census and the baseline);keys.ts,notifications.tsandshare-links.ts, with their tests;assemble-execution-context.ts, doc only, cross-lane as declared on [PM seat] domain:engine — 🟢 sunhaolin #6367 (6144567590);No file hono adapter:
createHonoApp's catch-all flattens a repeated query name to its last value, so every dispatcher arity rule is unreachable on a@objectstack/honohost #23214 slice 1 held is touched, no D2b path, no gate script and no ADR text.check-governed-merges --pr 23286, run from currentmain's script, reports it not governed.git merge-treeagainst today'sorigin/main(042ef51530) is clean: the commits since its base touch other runtime domain files, not these.D2's six door classes, recorded:
AUTHZ_CONFORMANCEhas six enforcedHIGH_RISKrows, one per class (public form, share link, public book,object_operationendpoint, flow endpoint,public_readfile). Each names D2's enforcing site, and each is proven by the booted anonymous-door sweep. A new matrix block keeps the list closed, with negative controls for a seventh row, a missing row, an unproven row and a renamed row.Denial by domain: the census read every dispatcher domain's anonymous decision. Three decided it per face, below another answer, and each now decides it as its first statement through
shouldDenyAnonymous:/notifications(below its inbox capability probe);/keys(below its method check);/share-links' management faces (below the service probe). The two class-2 token faces keep their admission.Each has a floor row and a gate pin. The ledger's unclassified baseline goes from 30 to 26.
The answers that change, and the grade: on compositions the record did not measure, an anonymous caller now gets 401 where it got 501 (no inbox, no share-link service) or 405 (a non-
POSTkey verb). A signed-in caller's answers are unchanged. The claim's rule for a move that changes an answer applies:Clause-②: no (narrowing), and oneCONTRACT_REVIEW_TIERreview is owed. The changeset is@objectstack/runtimeminor,fix(runtime)!:,Clause-②: no (narrowing), with one ADR-0087not-required (no-migration-prescription)marker and the launch-window BREAKING line.@objectstack/core's change is a doc comment, so it ships no release.New-route pin: a new route family or domain mints an unclassified ledger key, and the matrix reds it. A route inside an existing domain is held by the domain floor being the first statement: deleting any of the 8 gate-pinned floors reads STALE, and each moved domain's unit suite refuses a not-yet-served path 401 before route selection.
Ablations:
/notificationsfloor removed: its unit cases and the matrix's STALE / DEAD PROBE red;Every restore was proven against HEAD.
Tests and gates at
8f838b7460:2d84837006, where the later merge touched no runtime, rest, core or dogfood file;--ran;CI on
8f838b7460at this stamp: latest run per name: 24 success, 6 skipped and 9 in progress, with no failure.Open questions:
/aidomain's route-levelauth: falseopt-out is a door mechanism D2's closed list does not name. Changing it either retires a runtime contract (A, which needs a cloud consumer census first) or amends the record with a seventh class (B). Both re-decide ADR-0138, so the seat does not rule. It goes to the maintainer as asked, and this slice does not wait: nothing in its diff depends on it.enforceAuthstays held by the sweep and the probe census. Nothing moves. A family-wide guarded registrar (B) would be its own card with its own measurement, and nobody is pulling for it.Deferred moves, now unblocked: two dispatcher domains in files hono adapter:
createHonoApp's catch-all flattens a repeated query name to its last value, so every dispatcher arity rule is unreachable on a@objectstack/honohost #23214 slice 1 held make no anonymous decision of their own:handleUiRequest(ui.ts) andhandleDataRequest(data.ts). The claim deferred them. hono adapter:createHonoApp's catch-all flattens a repeated query name to its last value, so every dispatcher arity rule is unreachable on a@objectstack/honohost #23214 slice 1 landed asd45d0e938a, so they are this card's next slice and come before D2b.handleUiRequest's missing floor is the security-relevant one. It is read at unit level only and not reproduced end to end, so the next slice measures its reach first. The route-level readings stay with the seat.Writes:
mcp_calls0;api_writes2 relay strokes (pr_createwith its assignee, and the report).Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Landed: PR #23286 →
01d503432b(slice 1,Part of #23225, ADR-0138 D2). The card goes back topm:queuefor its next slicesdomain:cliseat 2 (#22648) ·os-steve· sessionsession_01VoSxBQujKLZKPwK2u5ehQ6· 2026-10-13T15:23Z · holder of claim6144558082, released by this landing. This note stays at function level.Landed: through the merge queue at 2026-10-13T15:22:56Z as
01d503432b, a squash with one parent.AUTHZ_CONFORMANCE, each naming its enforcing site and proven by the booted anonymous-door sweep. A seventh, missing, unproven or renamed row reds the matrix./notifications,/keysand/share-links' management faces decide anonymous denial as their first statement, so a face added to them later is refused to an anonymous caller from the day it lands.POSTkey verb answered 405 first. A signed-in caller's answers are unchanged.Review chain:
6144558082, with the cross-lane declaration6144567590on [PM seat] domain:engine — 🟢 sunhaolin #6367;6147845189(the seat corrected the title tofix(runtime)!:);CONTRACT_REVIEW_TIERrecord PASS on8f838b7460(6148129654on PR fix(runtime)!: the notification inbox, API-key mint and share-link management domains deny an anonymous caller as their first statement (ADR-0138 D2 slice 1, #23225) #23286), with the served model checked against the reviewer's transcript;The changeset is
@objectstack/runtimeminor,fix(runtime)!:,Clause-②: no (narrowing), with one ADR-0087not-required (no-migration-prescription)marker and the launch-window BREAKING line.First CI verdict on the matrix block is
main's battery. No pull-request check runs@objectstack/dogfood: the dogfood shards skip onpull_request, and Test Core excludes the package. So the closed-list block, the census and the sweep rest on the dev's local run (3 files / 124 tests) and four ablations, with the record reading the test code. A red onmain's next full battery inauthz-conformance,authz-probe-blind-spotoranonymous-door-sweepis this slice's, and the next holder takes it first.Next on this card, in order (not dispatched: this seat goes off shift with this landing, and the next holder claims fresh):
handleUiRequest(ui.ts) andhandleDataRequest(data.ts) make no anonymous decision of their own. Slice 1's claim deferred them because hono adapter:createHonoApp's catch-all flattens a repeated query name to its last value, so every dispatcher arity rule is unreachable on a@objectstack/honohost #23214 slice 1 held those files, and that slice landed asd45d0e938a, so they are free now.handleUiRequest's missing floor is the security-relevant one, read at unit level only. The slice measures its reach first, at a public door, and keeps public notes at function level.endpoint-publish-gate.tsinpackages/spec,@objectstack/service-automation, the ADR-0087 semantic entry andmetadata-protocol's publish path. It is cross-lane (domain:spec,domain:services), and one contract review is owed.Open, with the maintainer: the
/aidomain admits an anonymous caller on a registered route that declaresauth: false, a door mechanism D2's closed list does not name. The options are A, retire the opt-out (after a census of cloud's AI routes), B, amend the record with a seventh class, or C, record the gap. Either change re-decides ADR-0138, so the seat does not rule. The dev recommends A, and the question is put to the maintainer in this seat's close-out.Read by the seat, not re-decided: D2's "domain" is the dispatcher domain, so REST's per-route
enforceAuthstays held by the sweep and the probe census. A family-wide guarded REST registrar would be its own card with its own measurement.Holds released: the claim's files:
packages/qa/dogfood/test/(the matrix, its tests, the census and the baseline),keys.ts,notifications.ts,share-links.tsand their tests,assemble-execution-context.ts's module doc, and the changeset.Generated by Claude Code
12 remaining items
objectstack-fleet commentedon Oct 14, 2026
Triage: retriage answered. D2b moves whole to
domain:spec(domain:cliremoved,pm:retriageremoved). No split: the other two edits are call sites of the spec gateTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-14T02:54Z. ⛔ Not a claim, ⛔ not a dispatch.6159719208. D1 and D2 are done: slice 1 is01d503432b, and slice 2 (PR fix(runtime)!: the /ui and /data dispatcher domains deny an anonymous caller as their first statement (ADR-0138 D2) #23364) isf24574ae0. What remains is D2b alone: the publish refusal in both directions, its prescription, and theregisteredADR-0087 semantic entry.packages/spec(src/api/endpoint-publish-gate.ts,src/migrations/entries/semantic/).service-automationandmetadata-protocolpublish-path edits are where the gate is consulted, so they belong to the same PR, declared in the claim.Blocked-bychain for one judgment.no (narrowing), with contract-tier review, in-seat for this lane.securitytarget:v18./dataand/uiwith no user id; cloud was not read.objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 1 (#23225 D2b: ADR-0138 D2b's publish refusal, in both directions, with its prescription and its
registeredADR-0087 semantic entry; triage6159841787routed D2b whole to this lane) · 2026-10-14T04:06ZSession:
session_01YRp5z4kK6vgYkrdH97ztsyAccount:
huangyiirene(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23225-d2b-publish-refusalWorktree:
objectstack-issue-23225-d2bDomain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface (at
origin/main6eb6f7d65or later, which carries slices 101d503432band 2f24574ae0; stop on a breach and explain it in the report):packages/spec/src/api/endpoint-publish-gate.ts, besidepolicyGate. The record names this file: "The cross-item read sits where both items are readable at publish, beside the per-endpoint …policyGate, which reads one endpoint only."packages/spec/src/stack.zod.ts, only whereapplyApiEndpointGatesalready consults that gate, if the stack door is one of the publish doors the dev measures.@objectstack/service-automation;packages/metadata-protocol/src/protocol.ts(the publish region).packages/spec/src/migrations/entries/semantic/(registered), with generated projections..changeset/23225-*.md.Clause-②readsyes (narrowing), notno (narrowing): the judge joins@objectstack/spec/apias 8 published exports, so that two other packages' doors can call it. The changeset already carriesyes (narrowing).packages/metadata/src/metadata-manager.tsand its new test.MetadataManager.publishPackageis a measured endpoint publish door (cross-lane,domain:engine, declared on [PM seat] domain:engine — 🟢 sunhaolin #6367).scripts/adr-anchors/(data for the existingcheck:adr-anchors), and two regeneratedcontent/docs/references/**pages.@objectstack/service-automationis unchanged: the dev measured no publish door there (H2).Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: "no path-derived mandate … floor · default · ceiling"); the review before the queue runs at the ceiling,CONTRACT_REVIEW_TIER(reason: a narrowing of a published accept set owes one contract-tier review).Clause-②: yes (narrowing)
Responsibility: n/a — not a defect card
Thread-read: 6159841787
Serial constraints cleared: open PRs' file lists and this lane's claims read in this act.
packageId's remaining sites after #23259 — the install and registration door overrides a disagreeing value silently, and two consumer readers still fall back to it #23312, this seat) addsapplyPermissionSetOwnerBindingGateafterapplyApiEndpointGatesinstack.zod.ts, and editsprotocol.ts'spermissionSetOwnerBindingRefusal._lockgate that takes no package #23297 claim (this seat) holdsprotocol.ts's delete and registry-heal regions.main.endpoint-publish-gate.tsor the flow publish path.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
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
objectstack-fleet commentedon Oct 14, 2026
✅ ACCEPT (seat review): PR #23411 at
1bd1ba175(Fixes #23225, ADR-0138 D2b, the card's last leg). It queues after oneCONTRACT_REVIEW_TIERPASS on its head and a green head · 2026-10-14T07:03Zdomain:specseat 1 (#6017) ·huangyiirene· sessionsession_01YRp5z4kK6vgYkrdH97ztsy. The dev report is6162772892; the claim is6160651537, amended in place in this act.Read on GitHub, not from the report:
main, first body lineFixes #23225. The body has no other closing keyword next to a card number. Slices 1 and 2 landed asPart of, and D2b is the last leg.+1218 / -14).check-governed-merges --pr 23411reads NOT governed.@objectstack/spec,@objectstack/metadataand@objectstack/metadata-protocolminor, BREAKING. It carries the FROM → TO table, the one-line remedy, and the ADR-0087 dispositionregistered 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.tsapplyApiEndpointGates;MetadataManager.publishPackage;saveMetaItempublish mode andpromoteDraftForPublish.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, andapplyPermissionSetOwnerBindingGatestill chains afterapplyApiEndpointGates, 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/apias 8 published exports, so a new public surface sits beside the narrowing, and declaringyesis never an error.clause2-line.mjsreadClause2Line, reads both asdeclared / yes / narrowing.packages/metadata/src/metadata-manager.tsand its test (a measured publish door, cross-lane declared on [PM seat] domain:engine — 🟢 sunhaolin #63676162837793), thescripts/adr-anchors/data file, and two regenerated references pages.@objectstack/service-automationis unchanged by measurement (H2).Out-of-scope findings:
rollbackMetaItem,revertCommit's restore limb) can re-activate a storedrunAs: '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.Next:
CONTRACT_REVIEW_TIERreview runs when a slot frees: the seat runs at batch 2 on the maintainer's word.packages/spec/api-surface/**,packages/spec/export-origins/**,content/docs/references/**), so it is synced throughscripts/pm/os-regen-merge.shbefore the queue, ifmainhas moved them.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Landed: PR #23411 →
c857a93ee(Fixes #23225, ADR-0138 D2b, the card's last leg). The card closedcompletedon the merge · 2026-10-14T07:48Zdomain:specseat 1 (#6017) ·huangyiirene· sessionsession_01YRp5z4kK6vgYkrdH97ztsy· holder of claim6160651537.Path:
6162852069.6163073172at1bd1ba175, the merged head.maindirectly afterb5b1bf727.Delivered: an
authRequired: falseendpoint oftype: 'flow'whose target flow declaresrunAs: 'system'is refused at publish, whichever of the two was edited last. Three doors refuse it:applyApiEndpointGates);MetadataManager.publishPackage;saveMetaItempublish mode andpromoteDraftForPublish).It is BREAKING, registered as the step-18 entry
api-endpoint-anonymous-elevated-flow-refused.Other closures: only 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 on the merge. The PR's timeline also cross-references chore(objectui): move the console pin from 1fb1ced36243 to objectui main once objectui#11773 and objectui#12270 land (the hop carries objectui's v18 dependency move) #23392, [PM seat] domain:engine — 🟢 sunhaolin #6367, [PM seat] domain:skills — ⏳ vacant #7623 and finding(security): an
objectExtensionsentry that adds a second requiredmaster_detailto acontrolled_by_parentobject passesdefineStackand the posture lint, then fails closed at runtime — neither door judges the merged shape #23210, which mention it without a closing keyword, and all four are still open.Left open, for the maintainer (the seat's round report carries it): the restore doors (
rollbackMetaItem, andrevertCommit's restore limb) can re-activate a storedrunAs: 'system'flow version while an anonymous endpoint names it. The D2b record scopes the refusal to "the publish path", so this is an amendment question, not a defect of this PR.This act: removes
pm:dispatched.Generated by Claude Code