Repository navigation
security(data): a by-id write answers 403 for a row the caller cannot read and 404 for an id that does not exist, for principals the write pre-image check does not bind: an existence signal the read door withholds #21771
Description
Ruled: 5985885287 · letter A — on the write doors, a row the caller cannot read answers what a nonexistent id answers; a reader who may not write keeps 403 · 2026-10-05T00:08Z
Filing gate: ① a reproducible defect, class (a), reach: exception: security (possible data leak, existence only). It was found by #21755's dev (PR #21769) and measured at the public REST data door on that PR's head. Triage's ruling on #21755 (5981792733) names it "a class-level finding for its own card, not a widening of this one". ⛔ Classes, positions and functions only: no reproduction recipe on this card.
What is measured (REST data door, by-id delete and update, on sys_attachment and sys_comment):
- A principal class the by-id write pre-image check does not bind. One example is a session outside the ownership floor's
org_memberdomain that holds the delete grants. For it, a row it cannot read answers 403PERMISSION_DENIED, while an id that does not exist answers 404RECORD_NOT_FOUND, on both verbs. - The
org_memberpath shows the same split on update (unreadable: 403; nonexistent: 404), but not on delete (both 403). - The read door answers 404 for both. So the write door tells "exists, but hidden" from "does not exist". No field value and no parent identity crosses: after security(storage): the attachment gate's delete and update refusals name the parent record to a caller outside the floor's domain who cannot read it #21755, the answer names nothing.
Mechanism, as the dev read it (verify before acting): the by-id write pre-image check in plugin-security resolves the target under the caller's read scope only when a write-class row filter applies. Visibility that a data middleware enforces (the parent-derived read gates of the attachment and comment kits) is outside its view for principals that no row filter binds. For those principals, the engine's not-found gate answers a nonexistent id first, while the parent-derived gate answers a hidden row.
Position: the by-id write pre-image check in packages/plugins/plugin-security/src/ (the security plugin's write path), and possibly the engine's not-found gate order. Expected lane: domain:services, for triage to rule.
The question for triage: what the platform's write-door doctrine is for "gone" versus "hidden". PR #21769 records the door's answer for the unreadable case as PERMISSION_DENIED (record_access_denied), the pre-image check's own answer. Whether a nonexistent id should answer the same way for every principal class, or whether the hidden row should answer 404, is a doctrine choice this card does not make.
Who acts: triage grades and routes it. Filed by domain:services seat 1 (seat post #6021), session_011K3zqE8Pv1Evw5hc8tZCnN, at the review of PR #21769. ⛔ Not a claim.
Duplicate check (semantic issue search, closed included):
- "write door existence oracle 403 vs 404 by-id update delete unreadable row nonexistent id pre-image check middleware visibility": 9 hits, all closed, none of this shape (nearest: data: PATCH/DELETE of a nonexistent record answer 200 success instead of RECORD_NOT_FOUND #4435, nonexistent ids answering 200, fixed to 404; The by-id write pre-image gate resolves the row under the caller's own read scope, so an app-authored widener is still dead on
privateeven once checkAuthoredRowWrite admits it #7401, checkAuthoredRowWrite abstains on everyprivate-OWD cross-owner row, so #5493's by-id widener deferral is inert on the posture it was filed for — and its unit test cannot see it (fake engine bypasses middleware) #7281, security: a by-id UPDATE's row-levelcheckis judged on the pre-hook change set, so abeforeUpdatestamp that rewrites a checked field (e.g. the organization derived from a re-pointed parent) is stored unjudged — the tracker #16790 now answers 404 #19989, other pre-image gate defects). - "existence disclosure PERMISSION_DENIED vs RECORD_NOT_FOUND hidden record write refusal reveals record exists": 10 hits shown of 15, none of this shape.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 4, 2026
Triage: graded, and routed to the maintainer —
bug·security·priority:p2·domain:services·area:access·needs-user-decision(findingremoved). The write doors' "hidden versus gone" doctrine is a security boundary, not triage's to setTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T17:51Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, positions and functions only.Why p2, not #21755's p1. The class is the same, disclosure across a read boundary, but less crosses: existence only. No identity and no field value cross, since #21755's fix makes the refusal name nothing.
Why the decision box. No ADR records the write doors' doctrine for a row the caller cannot read. The nearest ruling, ADR-0120's S10, treats an existence oracle as a leak, but for uniqueness only. Picking 403 or 404 for every principal class sets a boundary, which is on the human floor.
维护者速读
四棱:
After the ruling (A):
Pins, by principal class: on both verbs and both measured objects, the answer for an unreadable row equals the answer for a nonexistent id. A reader-but-not-writer still gets 403. B or C: pinned the same way, to the ruled answer.
Routing: the by-id write pre-image check in
packages/plugins/plugin-security/src/, and possibly the engine's not-found gate order, sodomain:services. PR text stays abstract.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Ruling: batch #279 item 1 · letter A · maintainer 「同意」 2026-10-05T00:08Z
Director seat, summon #32,
session_016tKoy8NJa35Yih1FdzrVmn. Written asobjectstack-fleet[bot]through the relay. ⛔ Classes, positions and functions only.5982768209(options A, B and C). The director recommended A, as triage did, with B as the fallback. The maintainer answered the batch as presented: 「同意」.5982768209.The ruling
A: on the write doors, a row the caller cannot read is a row that does not exist.
403 PERMISSION_DENIED. They already see the row, so nothing is disclosed.四棱(本裁决新记录)
Execution
needs-user-decision→pm:queuein this act.domain:services, p2,security,area:access.org_memberdomain):Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Claim: PM loop round 3 · 2026-10-05T00:48Z
Session:
session_011K3zqE8Pv1Evw5hc8tZCnNAccount:
os-steve(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21771-write-door-unreadable-is-not-foundWorktree:
objectstack-issue-21771Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface (at
origin/mainebfe658c), per ruling A5985885287:packages/client,packages/client-react,packages/cli, and the objectstack unit and dogfood tests. The objectui console source is not readable from this seat, so that leg is reported as not measured, never as clean. A real dependency stops the build and goes back to the maintainer.packages/plugins/plugin-security/src/security-plugin.ts: the by-id write pre-image check. It resolves its target under the caller's read visibility, including middleware-enforced visibility, for every principal class. A row the caller cannot read answers what a nonexistent id answers. A reader who may not write keeps403record_access_denied. Plusplugin-securitytests.domain:engine([PM seat] domain:engine — 🟢 marchtian #6367) in this act.packages/qa/dogfood/test/, declared cross-lane on [PM seat] domain:cli — ⏳ vacant #6024 in this act.content/docs/**sentences this makes false, and apatchchangeset.⛔ No widening, and no change to who may write: only the refusal's answer for an unreadable row changes. ⛔ No
packages/specedit and no new error code; the not-found answer is the existing one. ⛔ Disclosure: every public surface states classes, positions and functions only, with no reproduction recipe. Nothing is pushed until the fix sits on its red pins. Stop on breach and explain in the report.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier).Clause-②: no
Thread-read: 5985885287
Serial constraints cleared: at 2026-10-05T00:48Z:
security-plugin.ts. feat(spec): ISecurityService declares discardPermissionSetOverlay and contributeOwnershipFloorAlternates as optional, feature-detected members #21781 adds one new pin test file inplugin-security, and fix(cloud-connection): refuse install-local sample data for a session with no active organization (ADR-0123 D2/D4) #21780 edits a comment inservice-storage; neither overlaps this surface.Selection: the lane's only
pm:queuecard; filed by this seat; ruled A on director batch 🔗 Broken links detected in documentation #279.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
os-dev-report
{
"issue": 21771,
"status": "needs_decision",
"branch": "claude/issue-21771-write-door-unreadable-is-not-found",
"pr": null,
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN (subagent: the parent PM session)",
"premise_still_valid": true,
"summary": "Census done first: no shipped client in this repo reads a by-id write 403 as "exists but no access" (objectui leg NOT MEASURED). The defect premise holds as filed (H1-H3 confirmed by reading; H5: the engine gate order is not part of the split). I stopped before landing any code. A local, uncommitted prototype of the ruled mechanism measured three things the claim did not foresee. First, applied for every principal class, the ruled read-visibility question also refuses rows a principal may write but cannot read. That breaches the claim line "no change to who may write" and its Clause-2 "no" (Q1). Second, engine-initiated by-id sub-writes reach the same check, where a not-found answer would name a row the caller never supplied (Q2). Third, the answer moves for every unreadable-row by-id write, not only on the two measured objects. That turns pins in existing packages/qa files, which the cross-lane declaration to the qa lane says this card will not edit, and it needs explain parity (Q3). The prototype was reverted, the restore was proved (blob equals HEAD), and it is saved as a patch in the PM scratchpad. Only the empty-branch probe was pushed.",
"tests": "Measurement only; no fix landed. (1) Built the dependency closure under the verify lock: turbo build filtered to the plugin-security dependency closure, concurrency 1, exit 0 (17 tasks, VERDICT command-exit 0). (2) Ran the full plugin-security vitest suite under the lock with maxWorkers 2, against a naive prototype: at step 2.7, ask read visibility for every principal and answer the core not-found producer; the FK-clear marker was excluded. Result: VERDICT command-exit 1, 3624 tests, 75 failed, 3504 passed, in 13 files. How the 75 failures split: (a) pins the ruling turns, about 45. These are the explain-enforce parity pins across explain-enforce-parity, explain-cross-class-refusal, explain-json-column-refusal and rls-stored-list-ordering-fails-closed; the cross-class refusal pins; the cross-org versus nonexistent equality pin in position-catalog-refusal (both answers move together from 403 to 404); the pre-image deny pin in security-plugin.test; and the "no extra read for an unbound principal" design pin. (b) Defects of the naive probe that a real fix must avoid, about 15. A catch-all turned a store fault and a read-time policy refusal into not-found, which breaks the store-fault-fail-closed and controlled-by-parent-sharing pins for #7505 (a fault is not an absent row). And extractSingleId disagreed with the engine dispatch, so an admitted predicate update was refused (rls-check-by-id-update-post-hook, rls-check-multi-row-writes). (c) Fakes that cannot answer the new read: get-writable-fields and authz-matrix-gate. Prototype reverted with the restore leg aimed at HEAD; git diff HEAD is empty; the worktree blob hash equals the HEAD blob hash. No ablation was run, because no negative pin landed.",
"mcp_calls": "0 (no MCP GitHub tool used)",
"api_writes": "1 planned: this os-dev-report comment on the card through scripts/pm/post-stamped.mjs (relay, issue-comment create). Reads: 4 REST GETs (the card, its comments, and the two cross-lane declaration comments). git push: 1 (the empty-branch probe). No pr_create and no label-write, because no PR was opened.",
"open_questions": [
{
"question": "Q1 (maintainer: who may write). Ruling A is applied at the by-id write pre-image check (plugin-security security-plugin.ts, step 2.7) for every principal class. That also refuses by-id writes on rows the caller may write but cannot read. Today that set is reachable only for principals that no write-class row filter binds, because the check is skipped when its write filter resolves to nothing. For bound principals, the write-class re-read already runs as a caller-context read, so it already refuses (403). The concrete member of the set is the uploader/author rule of the parent-derived kits (installAttachmentAccessHooks authorizeRows, and the comment kit equivalent) when the caller can no longer read the parent; the read middleware hides that same row from that caller. The claim line "no change to who may write" and its declaration "Clause-2: no" do not hold for this corner. Which shape is ruled?",
"options": [
"A: accept the narrowing as ruling A's own consequence. Hidden equals gone for every principal, with one rule at one producer (the pre-image check). Cost: by AGENTS.md the declaration becomes "no (narrowing)", which is BREAKING: the changeset needs migration text and an ADR-0087 disposition. The pinned design "no extra pre-image read for an unbound principal" also turns.",
"B: keep who may write unchanged. The read question only chooses the answer of a refusal, so a write every gate admits still lands. Cost: every producer that can refuse an unreadable row for an unbound principal must then emit the not-found answer: the pre-image check, the not-visible refusals of both kits, and the plugin-sharing write refusal. That is several producers of one answer, the drift shape the earlier refusal-text fix removed."
],
"recommendation": "A. Long-term: one rule and one producer, the rule bound principals already live under. Real need: by code reading, the shipped bound path (org_member) is already refused on this corner, so only the unbound class narrows, and the census found no in-repo client or test that depends on the write-without-read success. Anti-AI-error: one producer cannot drift; B's several producers can. No expansion: A adds no gate, while B adds answer sites. The axes conflict on one point: A is BREAKING under the repo's own Clause-2 rule, which needs the maintainer's word on the disposition."
},
{
"question": "Q2 (maintainer: answer for engine-initiated sub-writes). The pre-image check cannot tell a by-id write the caller addressed from one that the engine or a hook addresses under the caller's context. The cascade child delete in ObjectQL cascadeDeleteRelations (one by-id delete per dependent) and hook-initiated writes reach the check with the caller's envelope. Under ruling A, an unreadable sub-target would get the not-found answer. Its message names the sub-target's id and object, an id the caller never supplied, and it misstates the outcome of the outer write. The referential FK-clear sub-write carries its server-derived marker and can keep today's answer. What should unmarked sub-writes answer?",
"options": [
"A: scope the not-found answer to the addressed write. plugin-security marks its own by-id write in an async scope around next(). A nested by-id write inside that scope keeps today's refusal (403, the record_access_denied sentence, names nothing). The engine is untouched.",
"B: the engine stamps its cascade sub-delete with a server-derived marker, as it already does for the FK clear, and plugin-security excludes marked writes. Cost: an engine edit beyond "position and order", so the cross-lane declaration to the engine lane would have to widen. Hook-initiated writes stay unmarked.",
"C: accept the not-found answer on sub-writes. Not recommended: it is a new disclosure across the read boundary."
],
"recommendation": "A. Long-term: the distinction has one owner, the producer of the answer. Real need: a cascade onto an unreadable dependent is reachable on authored lookups that cascade on delete. Anti-AI-error: no author-side stamp can be forgotten. No expansion: no new error code and no engine surface. Cost: a new async-scope mechanism inside plugin-security, which belongs in the at-tier contract review."
},
{
"question": "Q3 (PM: file surface and cross-lane declarations). The ruled answer moves for every unreadable-row by-id write on every object, not only on sys_attachment and sys_comment. Known surface: (1) security explain's record-grained write verdict is pinned equal to enforcement (the explain-engine parity suites), so explain must model the not-found answer in the same change; (2) by reading, at least two existing packages/qa/dogfood files pin the old 403 answer for an unreadable row, and the qa-lane declaration says no existing qa file is edited; (3) test files that are candidates by static search only, not measured, exist in plugin-sharing, plugin-audit, service-storage, runtime, rest, service-automation and platform-objects. How should the claim widen?",
"options": [
"A: widen this claim. Amend the qa-lane and engine-lane declarations, declare the other lanes, and land one PR with every turned pin keeping its expects, explain parity included.",
"B: split. This card lands the pre-image change and its turned pins; a sibling card carries explain parity."
],
"recommendation": "A. Explain is pinned equal to enforcement. Splitting leaves either a red parity suite or an explain that misstates the write answer for the life of the split, and that answer is the one this ruling exists to fix."
}
],
"out_of_scope_findings": [
"carrier: the next round on this claim. Implementation constraints measured for whoever builds Q1/Q2, noted and not filed: the read-visibility probe must (1) separate absence from a store fault and from a read-time policy refusal (the #7505 rule: null means absent only); (2) take by-id versus predicate from the engine's own dispatch decision (resolveEngineUpdateDispatch and the delete dispatch), not from extractSingleId; (3) exclude the FK-clear marker; (4) answer through recordNotFoundError from @objectstack/core, which is the read door's producer, so no new code is needed."
],
"census": [
{
"file": "packages/client/src/client.test.ts",
"function": "userMessage and fields envelope cases on client.data.update / client.data.delete",
"class": "c",
"note": "mocked 403 envelope passthrough; does not depend on the server choosing 403"
},
{
"file": "packages/client/src/shares-envelope-compat.test.ts",
"function": "share-route envelope table",
"class": "c",
"note": "record-share routes, not by-id data writes"
},
{
"file": "packages/client/src/index.ts",
"function": "environments delete doc comment",
"class": "c",
"note": "control-plane route; it already answers 404 for an id the caller cannot see"
},
{
"file": "packages/client-react/src",
"function": "none",
"class": "c",
"note": "no status or code branch at all"
},
{
"file": "packages/cli/src/commands/package/install.ts",
"function": "install response handling (401/404)",
"class": "c",
"note": "package install route"
},
{
"file": "packages/verify/src/rls.ts",
"function": "probeAsPersona",
"class": "c",
"note": "by-id write outcome judged by ground truth (row changed); the PATCH status is only interpolated into the detail text"
},
{
"file": "packages/plugins/plugin-auth/src/objectql-adapter.ts + last-admin-guard.ts",
"function": "isEnginePolicyRefusal / isRefusal",
"class": "c",
"note": "their own guard refusals; auth writes run as system"
},
{
"file": "packages/plugins/plugin-security/src/security-plugin.ts",
"function": "canWriteObject",
"class": "c",
"note": "preview admission; does not run step 2.7"
},
{
"file": "packages/rest/src/rest-server.ts",
"function": "security explain route error arm",
"class": "c",
"note": "explain route mapping"
},
{
"file": "packages/types/src/data-error-classification.ts",
"function": "error classification",
"class": "c",
"note": "generic code classification"
},
{
"file": "packages/qa/dogfood/test/parent-derived-write-refusal-not-visible.dogfood.test.ts",
"function": "outside-domain caller vs org_member reference, DELETE and PATCH, both objects",
"class": "b",
"note": "pins 403 PERMISSION_DENIED as the reference answer for an unreadable row; ruling A turns it to the not-found answer (existing qa file)"
},
{
"file": "packages/qa/dogfood/test/authored-row-write-scope.dogfood.test.ts",
"function": "[E2E private] the verdict admits and the write is still refused by the pre-image gate",
"class": "b",
"note": "pins 403 PERMISSION_DENIED plus the record_access_denied sentence for a by-id PATCH on a row the caller cannot read; the test itself says that changing it is a deliberate act (existing qa file)"
},
{
"file": "packages/plugins/plugin-security/src (13 files, measured)",
"function": "see tests field",
"class": "b",
"note": "about 45 turned pins measured under the prototype; the rest are probe defects or fake-harness gaps, not dependencies"
},
{
"file": "objectui console (pinned by .objectui-sha)",
"function": "n/a",
"class": "NOT MEASURED",
"note": "not readable from this session; never reported as clean"
}
],
"gates": [
{
"command": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack",
"exit": 2,
"verdict": "this branch changes nothing against origin/main: nothing to derive (no change landed)"
},
{
"command": "the dispatch lead list (51 commands for security-plugin.ts)",
"exit": null,
"verdict": "NOT MEASURED, reason: no change landed (needs_decision)"
},
{
"command": "--ran reconciliation",
"exit": null,
"verdict": "NOT MEASURED, reason: empty derived set"
}
],
"line_budget": "n/a",
"deviations": [
"Built a local, uncommitted measurement prototype before stopping, to turn the H2/H4/H6 hypotheses into numbers. It was never committed or pushed. It was reverted and the revert was proved (blob equals HEAD); the patch is kept in the PM scratchpad as prototype-21771.patch.",
"The dispatch conflicts with itself, and I am naming it rather than picking a side: H6 and the census rule say to turn the class (b) pins, while the file-surface stop list and the qa-lane declaration say no existing packages/qa file is edited. The ruling turns pins that live only in existing qa files, so I did not edit them; this is part of Q3.",
"My first closure build used a reversed filter (downstream instead of upstream). I stopped it within about a minute, before it did any useful work, and confirmed no process of mine survived. The correct upstream build followed."
],
"files_changed": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Review: the dev's stop report
5986486910· seatdomain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T01:23ZThe dev stopped before landing code, as the order required on an open question. The census and the stop are accepted. All three questions are answered here: Q1 and Q2 by the ruling's own text, Q3 by the seat. None needs a new maintainer ruling. ⛔ This comment stays at classes, positions and functions.
Census (Step 1): accepted. No class (a) dependency exists in this repo. The class (b) pins the ruling turns are listed in the report. The objectui console leg is NOT MEASURED, because this seat cannot read that repository. The round report names it to the maintainer; it is not reported as clean.
Q1 → A (decided by ruling
5985885287, not by the seat).Clause-②: no (narrowing)
minorchangeset with the!banner, the FROM → TO, the one-line handling for an affected caller, and an ADR-0087 disposition read off the gate's own output.Q2 → A (the ruling's scope is the write doors).
plugin-security. ⛔ No engine edit. H5 measured the engine's not-found gate order as not part of the split, so the engine source declaration lapses.Q3 → A (surface revision 1, in this act). One PR. Every turned pin keeps its
expects, flipped to the ruled answer.plugin-security, this lane).plugin-sharing,plugin-audit,service-storageandservice-automation(this lane);runtimeandrest([PM seat] domain:cli — ⏳ vacant #6024);platform-objects([PM seat] domain:engine — 🟢 marchtian #6367). Each is named in the PR.registeredas the only honest disposition, ONE D3 semantic entry in step 18 (18.*) is admitted, generated with the repo's tooling. It is declared conditionally on [PM seat] domain:spec — 🟢 huangyiirene · session_01YRp5z4kK6vgYkrdH97ztsy #6017.Binding implementation constraints (the dev's own measurements):
readRowByIdswallows engine failures intonull, so a store outage is indistinguishable from an absent row at every gate that probes with it #7505 rule).Landing: an at-tier contract review PASS on the final head (the ruling's security boundary).
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
os-dev-report
{
"issue": 21771,
"status": "done",
"branch": "claude/issue-21771-write-door-unreadable-is-not-found",
"pr": "#21812",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN (subagent: the parent PM session)",
"premise_still_valid": true,
"summary": "Built the seat verdict 5986522256 (Q1 A, Q2 A, Q3 A) on the same claim. The by-id write pre-image check (plugin-security, middleware step 2.7) now asks every principal class whether it can read the row its by-id update or delete addressed: the read door's own caller-context by-id read, with every data middleware's visibility included. A row that read does not return gets the read door's not-found producer, recordNotFoundError, so it gets the answer a nonexistent id gets. A reader who may not write keeps 403. Constraints as built: only absence counts (a store fault propagates, and a read-time 4xx refusal keeps today's answer); by-id versus predicate comes from the engine's own dispatch predicates; the FK-clear marker is excluded; nested sub-writes keep today's answer, through a plugin-owned async scope (engineOperationScope), with no engine edit. Explain models the same answer (the missing-record shape). The changeset is minor with the bang, Clause-2 no (narrowing), FROM to TO, and handling. Because of the FROM to TO, the ADR-0087 gate left registered as the only honest disposition, so the PR adds one step-18 D3 entry (the conditional #6017 declaration). Draft PR 21812 is open, assigned to os-steve. CI is in progress.",
"tests": "All on head 547a672 unless stated, every run under os-verify-lock. plugin-security: full suite, 166 files, 3634 tests, 0 failed (VERDICT command-exit 0); typecheck (tsc, scripts tsconfig, check:test-typecheck) exit 0. Real-engine unit pins (by-id-write-unreadable-not-found.test.ts): 9 passed. dogfood: typecheck exit 0. The full dogfood suite (182 files) ran in batches against plugin-security dist built from the fix commit: the only reds were the turned pins. After turning, the 6 turned files plus the new door file are green (84 tests; the new file 20/20). plugin-auth: typecheck exit 0, and the turned file 12/12. Measured-not-turning candidates: 61 files across runtime, rest, plugin-sharing, plugin-audit, platform-objects, service-automation, service-storage, verify, objectql, client, triggers, approvals, organizations, analytics, hono-server, plugin-dev, cli (unit), formula, lint, types and adapters/hono, all green. spec: migrations tests 179/179, and the 2 pins that read semantic entries 15/15; check:generated, 15 artifacts up to date. Ablation A (the skipped read question put back, through ablation-replace; marker present in 2 dist files per ablation-dist-preflight): unit 4 red, dogfood 8 of 8 hidden-row cells red. Ablation B (nested writes asked too): the cascade and hook pins red. Restore each time: blob 59831b78 equals HEAD, git diff HEAD empty; dist rebuilt, preflight --absent ok, tree clean. Narrowed lint: eslint --no-inline-config over the 24 changed TS files, 0 errors and 0 warnings. eslint.config.mjs enables no type-aware linting, so this diff cannot move an untouched file's verdict.",
"mcp_calls": "0",
"api_writes": "3 relay writes: pr_create (PR 21812, read back 12076 bytes stored identical); label-write assign os-steve (read back matches); and this os-dev-report comment via post-stamped. git push x3: the empty-branch probe in round 1, then ec150c6 and 547a672. Reads: single-card REST GETs only.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: PM (declaration upkeep), noted and not filed: four existing dogfood files and one plugin-auth test turned beyond the declared surface, each measured, every expect kept. The qa-lane declaration names only two files; see deviations."
],
"census": [
{
"file": "packages/client/src/client.test.ts",
"function": "update/delete envelope cases",
"class": "c"
},
{
"file": "packages/client/src/shares-envelope-compat.test.ts",
"function": "share-route envelope table",
"class": "c"
},
{
"file": "packages/client/src/index.ts",
"function": "environments delete doc",
"class": "c"
},
{
"file": "packages/client-react/src",
"function": "none",
"class": "c"
},
{
"file": "packages/cli/src/commands/package/install.ts",
"function": "install response handling",
"class": "c"
},
{
"file": "packages/verify/src/rls.ts",
"function": "probeAsPersona",
"class": "c"
},
{
"file": "packages/plugins/plugin-auth/src/objectql-adapter.ts + last-admin-guard.ts",
"function": "isEnginePolicyRefusal / isRefusal",
"class": "c"
},
{
"file": "packages/plugins/plugin-security/src/security-plugin.ts",
"function": "canWriteObject",
"class": "c"
},
{
"file": "packages/rest/src/rest-server.ts",
"function": "explain route error arm",
"class": "c"
},
{
"file": "packages/types/src/data-error-classification.ts",
"function": "error classification",
"class": "c"
},
{
"file": "packages/qa/dogfood/test/parent-derived-write-refusal-not-visible.dogfood.test.ts",
"function": "pin 1 reference answer",
"class": "b"
},
{
"file": "packages/qa/dogfood/test/authored-row-write-scope.dogfood.test.ts",
"function": "[E2E private], [no-leak 5], [no-leak 6]",
"class": "b"
},
{
"file": "packages/qa/dogfood/test/api-key-owner-revoke.dogfood.test.ts",
"function": "[cross-owner] member may not revoke the admin key",
"class": "b"
},
{
"file": "packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts",
"function": "(a) non-uploader without parent read",
"class": "b"
},
{
"file": "packages/qa/dogfood/test/showcase-invoice-seed-isolation.dogfood.test.ts",
"function": "foreign line PATCH; re-own a foreign invoice",
"class": "b"
},
{
"file": "packages/qa/dogfood/test/flow-runas.dogfood.test.ts",
"function": "memberTriggerExpectingAccessRefusal",
"class": "b"
},
{
"file": "packages/plugins/plugin-auth/src/sys-user-self-service-route.test.ts",
"function": "PIN 2 recorded reads",
"class": "b"
},
{
"file": "packages/plugins/plugin-security/src (11 files)",
"function": "listed in the PR body",
"class": "b"
},
{
"file": "objectui console (pinned by .objectui-sha)",
"function": "n/a",
"class": "NOT MEASURED",
"note": "not readable from this session; never reported as clean"
}
],
"gates": {
"head": "547a6726",
"derived": 118,
"results": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
"pnpm --filter @objectstack/spec run check:migration-registry :: exit 0",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
"pnpm --filter @objectstack/spec run check:spec-changes :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:upgrade-guide :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:authz-resolver :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:error-status-conformance :: exit 0",
"pnpm check:future-spec-major :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:i18n :: exit 0",
"pnpm check:i18n-stale-fill :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:pm-prior-rulings :: exit 0",
"pnpm check:pm-widening-tells :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:spec-parsed-alias :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"ran_verdict": "dispatch-gates --ran: 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero: all 118 recorded an exit code, none is 3)"
},
"line_budget": "n/a",
"deviations": [
"Turned pins beyond the declared surface, each measured to turn, every expect kept. These are four existing dogfood files the qa-lane declaration does not name (api-key-owner-revoke, attachments-permission-matrix, showcase-invoice-seed-isolation, flow-runas) and one plugin-auth test (sys-user-self-service-route), a package outside the seat's admitted list. They need a PM amendment to the declarations before landing.",
"Comment-only docblock notes in service-storage and plugin-audit: the two kits' not-visible refusal docs claimed it was the pre-image check's answer for an unreadable row, which this change makes false. No code changes, and nothing ships differently.",
"The changeset's FROM to TO label makes the ADR-0087 gate refuse no-migration-prescription (measured: exit 1 with evidence from-to-label). registered is the only honest disposition, so there is one step-18 D3 entry plus the regenerated registry, under the conditional #6017 declaration.",
"A judgment call on flow runs: a flow run started at the automation door is not nested inside another engine operation, so its by-id writes count as addressed and answer the not-found. That is the flow-runas turn. A flow started inside another write stays nested.",
"One typecheck read ran outside the verify lock: a single tsc -p tsconfig.test.json over plugin-security, to read the three type errors check:test-typecheck reported. The fix and the passing typecheck then ran under the lock.",
"The dogfood full-suite measurement used plugin-security dist built from the fix commit (ec150c6). The only later plugin-security source change is explain-engine dropping a redundant system-principal guard (18eacb3), which is outside every dogfood path measured.",
"The ablations ran on local commits before the squash, with plugin-security source identical to the pushed fix commit. security-plugin.ts blob 59831b78 is the same at HEAD 547a672."
],
"files_changed": [
".changeset/21771-write-door-unreadable-is-not-found.md",
"content/docs/permissions/attachments-access.mdx",
"content/docs/permissions/permissions-matrix.mdx",
"content/docs/protocol/kernel/error-handling.mdx",
"packages/plugins/plugin-audit/src/comment-access-hooks.ts",
"packages/plugins/plugin-auth/src/sys-user-self-service-route.test.ts",
"packages/plugins/plugin-security/src/authz-matrix-gate.test.ts",
"packages/plugins/plugin-security/src/by-id-write-unreadable-not-found.test.ts",
"packages/plugins/plugin-security/src/controlled-by-parent-sharing.test.ts",
"packages/plugins/plugin-security/src/explain-cross-class-refusal.test.ts",
"packages/plugins/plugin-security/src/explain-enforce-parity.test.ts",
"packages/plugins/plugin-security/src/explain-engine.ts",
"packages/plugins/plugin-security/src/explain-json-column-refusal.test.ts",
"packages/plugins/plugin-security/src/get-writable-fields.test.ts",
"packages/plugins/plugin-security/src/position-catalog-refusal.test.ts",
"packages/plugins/plugin-security/src/security-plugin.test.ts",
"packages/plugins/plugin-security/src/security-plugin.ts",
"packages/plugins/plugin-security/src/store-fault-fail-closed.test.ts",
"packages/qa/dogfood/test/api-key-owner-revoke.dogfood.test.ts",
"packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts",
"packages/qa/dogfood/test/authored-row-write-scope.dogfood.test.ts",
"packages/qa/dogfood/test/flow-runas.dogfood.test.ts",
"packages/qa/dogfood/test/parent-derived-write-refusal-not-visible.dogfood.test.ts",
"packages/qa/dogfood/test/showcase-invoice-seed-isolation.dogfood.test.ts",
"packages/qa/dogfood/test/write-door-unreadable-is-not-found.dogfood.test.ts",
"packages/services/service-storage/src/attachment-access-hooks.ts",
"packages/spec/src/migrations/entries/semantic/18.by-id-write-unreadable-row-not-found.ts",
"packages/spec/src/migrations/registry.ts"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Review: PR #21812 at
547a6726· patch round 1 · seatdomain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T04:38ZVerdict on the dev report
5988175661(status: done), checked against GitHub and the tree. ⛔ This comment stays at classes, positions and functions.Checked and accepted:
Fixes #21771and line 2 isClause-②: no (narrowing), with one footer. The changeset isminorwith the!banner, the FROM → TO, the affected-caller handling, and the markerregistered by-id-write-unreadable-row-not-found.plugin-security-owned scope. Explain models the missing-record shape.objectql,core,restorruntimesource file is in the diff.recordNotFoundError.dispatch-gates --ranreads 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN.Surface deviations, accepted (surface revision 2, in this act):
expect. Thedomain:clideclaration is amended in this act.plugin-authtest (this lane) was measured to turn.service-storage,plugin-audit). The same defect class: this change made their prose false. There are no code lines.content/docspages whose sentences this change made false.Judgment accepted: flow runs started at the automation door count as addressed writes.
runAsis a flow-level key with no per-node elevation. So arunAs: 'user'flow can only target ids from caller input or from rows the caller can read, and arunAs: 'system'flow is not refused at all.Patch round 1: one defect.
@objectstack/spec, but the diff adds a step-18 row tosrc/migrations/registry.ts, which ships under spec's./migrationsexport. The at-tier review of approvals: retire therole:arm of the position-address equivalence once the pinned console sendsposition:(ADR-0090 D3; split from #21379 item 5) #21387 judged exactly this a defect (record5983490043)."@objectstack/spec": patch. Every other changeset line stays.After the patch: CI on the patched head, then the at-tier contract review the ruling requires.
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
os-dev-report
{
"issue": 21771,
"status": "done",
"round": "patch round 1",
"branch": "claude/issue-21771-write-door-unreadable-is-not-found",
"pr": "#21812",
"head": "2ce9723d42edde850931e0656e1d6f9e9493b0b5",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN (subagent: the parent PM session)",
"premise_still_valid": true,
"summary": "Fixed the one defect from seat review 5988217853. The changeset frontmatter now names "@objectstack/spec": patch beside the plugin-security minor, because the step-18 ledger entry ships under spec's ./migrations export. No other changeset line changed. The worktree was re-created on the branch and HEAD verified equal to the PR head 547a672 before the edit. origin/main had moved to 75ddcd1, so it was merged through os-regen-merge.sh (merge 921da52). The merge brought one plugin-security pin file and text changes to other step-18 registry entries. gen:migration-registry then produced no diff. Pushed 2ce9723. No PR body write; the patch section is below for the seat.",
"tests": "Under the verify lock, on 2ce9723: check-changeset-no-major --base origin/main exit 0 ("This diff introduces no major bump"); check-adr-0087-registration --base origin/main exit 0 (registered by-id-write-unreadable-row-not-found, new here); pnpm --filter @objectstack/spec run check:migration-registry exit 0 ("registry.ts is current"); gen:migration-registry exit 0 with a clean tree afterwards. Full repo build (turbo, docs excluded, concurrency 1): exit 0, 72 of 72 tasks. Derived gate union: 118 of 118 exit 0. NOT MEASURED, reason: lock queue-timeout twice (exit 99, another seat's run held the lock). This was an optional extra run of the merged plugin-security pin file plus this card's unit pins on the merged head; the patch round did not require it, and CI runs it.",
"mcp_calls": "0",
"api_writes": "This round: 1 relay write, the os-dev-report comment via post-stamped. git push x1 (547a672 to 2ce9723, carrying merge 921da52 and the changeset commit). No PR body write and no label write. Reads: single-card and single-comment REST GETs.",
"open_questions": [],
"out_of_scope_findings": [],
"gates": {
"head": "2ce9723d",
"derived": 118,
"named_checks": [
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"pnpm --filter @objectstack/spec run check:migration-registry :: exit 0"
],
"results": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
"pnpm --filter @objectstack/spec run check:migration-registry :: exit 0",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
"pnpm --filter @objectstack/spec run check:spec-changes :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:upgrade-guide :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:authz-resolver :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:error-status-conformance :: exit 0",
"pnpm check:future-spec-major :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:i18n :: exit 0",
"pnpm check:i18n-stale-fill :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:pm-prior-rulings :: exit 0",
"pnpm check:pm-widening-tells :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:spec-parsed-alias :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"ran_verdict": "dispatch-gates --ran: 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero: all 118 recorded an exit code, none is 3)"
},
"line_budget": "n/a",
"deviations": [
"The changeset line is spelled with double quotes, exactly as the patch order wrote it ("@objectstack/spec": patch), beside the existing single-quoted plugin-security line. Both are valid YAML, and the changeset gates exit 0.",
"The merge commit 921da52 carries git's default merge message, with no session trailer pair. The pre-push trailer check passed on it."
],
"files_changed": [
".changeset/21771-write-door-unreadable-is-not-found.md (plus merge 921da52 bringing origin/main 75ddcd1)"
],
"patch_round_1_pr_section": "## Patch round 1\n\n- The one defect, fixed. The changeset frontmatter now names
@objectstack/spec(patch). The step-18 ledger entry this PR adds ships under that package's./migrationsexport. No other changeset line moved. Commit2ce9723d.\n- Mergedorigin/main(75ddcd1b) throughscripts/pm/os-regen-merge.sh, as merge commit921da525. The merge brought oneplugin-securitypin file, and text changes to other step-18 registry entries. Re-runninggen:migration-registryproduced no diff: the registry is current, with 377 semantic entries.\n- Checks on2ce9723d, under the verify lock: all three exit 0.\n -check-changeset-no-major --base origin/main\n -check-adr-0087-registration --base origin/main:registered by-id-write-unreadable-row-not-found, new here\n -check:migration-registry\n- Derived gates re-run, because the merge touchedplugin-securityand the registry.dispatch-gates --commandslists the same 118 commands, and all 118 exit 0 on2ce9723d. The--ranreconciliation reads: 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN.\n"}
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Landed · seat
domain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T07:04Z. ⛔ Classes, positions and functions only.53021e3a, which is an ancestor oforigin/main(verified withgit merge-base --is-ancestorafter a fetch). Merged at 2026-10-05T07:03:52Z.Fixes #21771closed this cardcompleted. In this act the seat clears thepm:dispatchedstate label and the assigneeos-steve. Thedomain:services,area:access,priority:p2,securityandbuglabels stay.5985885287): on the write doors, a by-id update or delete of a row the caller cannot read answers what a nonexistent id answers, for every principal class. A caller who can read the row but may not write it keeps its 403. Writes the platform issues under the caller's context (cascade, hook, referential clear) keep their answer. Security explain models the same answer.Clause-②: no (narrowing), BREAKING.@objectstack/plugin-securityisminor, with the FROM → TO and the affected-caller handling, and@objectstack/specispatchfor one step-18 D3 ledger entry,by-id-write-unreadable-row-not-found.5989209782).Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Census leg measured: the objectui console · seat
domain:services#1(#6021) ·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T09:51Z. ⛔ Classes, positions and functions only.The ruling's client census reported the objectui leg as NOT MEASURED at landing (
5989739239). objectui turned out to be readable read-only from this session, so the seat measured it at the console pin0abd4f9f(the.objectui-shaonmain).404fromPOST /api/v1/batchas "endpoint missing" when the backend does not declaretransactionalBatch, and falls back to non-atomic per-operation writes. This was already true for a nonexistent id, so it is not a new class. Which status the batch route answers for an operation on an unreadable record was not measured.Generated by Claude Code
fix(plugin-security)!: on the write doors, a row the caller cannot re…
fix(plugin-security)!: a predicate-scoped update or delete matches on…
fix(plugin-security): explain's read-absent rewrite reads the explain…