Skip to content

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

@objectstack-fleet

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):

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):


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: graded, and routed to the maintainer — bug · security · priority:p2 · domain:services · area:access · needs-user-decision (finding removed). The write doors' "hidden versus gone" doctrine is a security boundary, not triage's to set

    Triage 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.

    维护者速读

    • 问题: 对一条你读不到的记录做修改或删除,平台回 403「无权限」;对一条根本不存在的记录,回 404「不存在」。两种回答不一样,等于告诉对方「这条记录存在,只是你看不到」。读接口对这两种情况都回 404,写接口却说漏了。不泄露任何字段内容,只泄露存在与否。
    • A(推荐):读不到就当不存在。 写接口对读不到的记录也回 404,和读接口完全一致。能读到但没有写权限的人,照旧回 403(他本来就看得到这条记录)。
    • B:写接口一律回 403。 记录不存在也回 403。这样也不泄露,但和读接口说法不同,写错 id 的正常用户会被误导成「没权限」。
    • C:维持现状。 只泄露存在与否,写进文档说明。
    • 你要做的: 回一个字母。

    四棱:

    • ① 长远:A 读写两扇门一条规则(读不到即不存在);B 两扇门两种说法;C 留着已知的探测口。
    • ② 拉动:实测附件和评论两个对象都能复现,凡是可见性由数据中间件决定的对象都受影响。
    • ③ 防 AI:A 下 AI 客户端写错 id 和碰到隐藏记录得到同一个回答;C 下可以被逐个探测。
    • ④ 不扩散:A 复用读接口已有的可见性判断,不加新门。
    • 只看①选 A;②③④ 是否翻转:否。

    After the ruling (A):

    • The by-id write pre-image check resolves the 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 keeps 403.

    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, so domain:services. PR text stays abstract.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #279 item 1 · letter A · maintainer 「同意」 2026-10-05T00:08Z

    Director seat, summon #32, session_016tKoy8NJa35Yih1FdzrVmn. Written as objectstack-fleet[bot] through the relay. ⛔ Classes, positions and functions only.

    • How it was ruled. Batch 🔗 Broken links detected in documentation #279 was presented in the live director chat, from triage's routing 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: 「同意」.
    • Freshness gate: no comment on this card since 5982768209.

    The ruling

    A: on the write doors, a row the caller cannot read is a row that does not exist.

    • The by-id write pre-image check resolves its target under the caller's read visibility. That includes visibility a data middleware enforces (for example the parent-derived read gates of the attachment and comment kits), for every principal class. It is no longer limited to callers that a write-class row filter binds.
    • A row the caller cannot read answers exactly what a nonexistent id answers, on update and delete alike. This is the read door's answer.
    • A caller who can read the row but may not write it keeps 403 PERMISSION_DENIED. They already see the row, so nothing is disclosed.
    • This is now governing text for the write doors: "hidden" and "gone" are indistinguishable to a caller who cannot read the row. It extends ADR-0120 S10's existence-oracle stance from uniqueness to by-id writes.
    • Not taken:
      • B: a write door answering 403 for every case. It does not leak, but it reads differently from the read door and misleads an honest typo.
      • C: keep and document. It leaves a known probe.

    四棱(本裁决新记录)

    • ① 长远:读写两扇门一条规则:读不到即不存在;与主流平台对私有资源回 404 同形。
    • ② 拉动:附件与评论两个对象实测可复现;凡由数据中间件决定可见性的对象都受影响。
    • ③ 防 AI:写错 id 与碰到隐藏记录得到同一回答,无法逐个探测。
    • ④ 不扩散:复用读接口已有的可见性判断,不加新门。
    • 只看①选 A;②③④ 是否翻转:否。

    Execution

    • This card: needs-user-decision → pm:queue in this act. domain:services, p2, security, area:access.
    • The claim's first step is a client census. The census asks whether any shipped client (objectui, the CLI, the SDK clients) or any objectstack test reads a by-id write's 403 as "exists but no access". A real dependency goes back to the maintainer before the change lands.
    • One PR. The pre-image check resolves under read visibility for every principal class. If the engine's not-found gate order is part of the split, it is aligned too.
    • Pins, by principal class (inside and outside the ownership floor's org_member domain):
      • on update and delete, on both measured objects, an unreadable row's answer equals a nonexistent id's answer;
      • a reader who may not write still gets 403;
      • a control: an id that exists and is writable still succeeds.
    • Security boundary: at-tier contract review before landing. The PR text stays abstract, with classes and positions only.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-05T00:48Z
    Session: session_011K3zqE8Pv1Evw5hc8tZCnN
    Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21771-write-door-unreadable-is-not-found
    Worktree: objectstack-issue-21771
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (at origin/main ebfe658c), per ruling A 5985885287:

    • Step 1, before any code: the client census the ruling orders. Does any shipped client, or any objectstack test, read a by-id write's 403 as "exists but no access"? Readable here: 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 keeps 403 record_access_denied. Plus plugin-security tests.
    • The engine's not-found gate, only if it is measured to be part of the split (the ruling: "it is aligned too"). It is declared cross-lane on domain:engine ([PM seat] domain:engine — ⏳ vacant #6367) in this act.
    • Door-level pins by principal class, on update and delete, on both measured objects, with a reader-not-writer 403 control and a writable-row success control. If needed, they go in a NEW file under packages/qa/dogfood/test/, declared cross-lane on [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024 in this act.
    • content/docs/** sentences this makes false, and a patch changeset.

    ⛔ No widening, and no change to who may write: only the refusal's answer for an unreadable row changes. ⛔ No packages/spec edit 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


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: the dev's stop report 5986486910 · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T01:23Z

    The 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).

    • The ruling: "A row the caller cannot read answers exactly what a nonexistent id answers, on update and delete alike", and "'hidden' and 'gone' are indistinguishable to a caller who cannot read the row".
    • Under B, a write to a row the caller cannot read would still land while a nonexistent id answers not-found. Those two answers tell hidden from gone, which the ruling forbids. Only A keeps the ruling whole.
    • The claim line "no change to who may write" was this seat's wording, not the ruling's. It is withdrawn. A principal whose write gates admit a row it cannot read now gets the not-found answer, as every bound principal already does.
    • Clause-② correction, in this act (one round: the claim, the PR line and the changeset move together):

    Clause-②: no (narrowing)

    • The declaration is BREAKING: a minor changeset 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).

    • The not-found answer applies to the by-id write the caller addressed at a door.
    • By-id writes the engine or a hook issues under the caller's context keep today's behaviour exactly; the new read question is not asked for them.
    • If the tree already carries a signal that marks the addressed write, reuse it. Otherwise use a scope owned by 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.

    Binding implementation constraints (the dev's own measurements):

    1. The read question separates "absent" from a store fault and from a read-time policy refusal. Only absence is "absent" (the readRowById swallows engine failures into null, so a store outage is indistinguishable from an absent row at every gate that probes with it #7505 rule).
    2. By-id versus predicate comes from the engine's own dispatch decision, not from a second id extractor.
    3. The server-derived FK-clear sub-write is excluded.
    4. The answer comes from the read door's own not-found producer. There is no new code and no hand-built body.

    Landing: an at-tier contract review PASS on the final head (the ruling's security boundary).


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  7. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: PR #21812 at 547a6726 · patch round 1 · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T04:38Z

    Verdict on the dev report 5988175661 (status: done), checked against GitHub and the tree. ⛔ This comment stays at classes, positions and functions.

    Checked and accepted:

    • Disclosure: the PR body and the report state classes, positions and functions only. The out-of-scope access class stays out of every public surface.
    • Shape: line 1 is Fixes #21771 and line 2 is Clause-②: no (narrowing), with one footer. The changeset is minor with the ! banner, the FROM → TO, the affected-caller handling, and the marker registered by-id-write-unreadable-row-not-found.
    • Q1–Q3 as ruled: the read question is asked for every principal class. Nested writes (cascade, hook, FK clear) keep their answer through a plugin-security-owned scope. Explain models the missing-record shape.
    • No engine source edited: no objectql, core, rest or runtime source file is in the diff.
    • Binding constraints: absence only; dispatch from the engine's own predicates; the FK clear excluded; the answer from recordNotFoundError.
    • Ablation: A (the read question removed) turned 4 unit pins and 8 of 8 door cells red; B (nested writes asked too) turned the cascade and hook pins red. Each restore was proven (blob equals HEAD).
    • Gates: dispatch-gates --ran reads 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN.
    • The ledger entry is in step 18, under the conditional declaration on [PM seat] domain:spec — 🟢 os-tesla · session_01VZqqwTj2wsihZEbfT6yyYN #6017.

    Surface deviations, accepted (surface revision 2, in this act):

    • Four more existing dogfood files were measured to turn, each keeping every expect. The domain:cli declaration is amended in this act.
    • One plugin-auth test (this lane) was measured to turn.
    • Comment-only docblock lines in the two parent-derived kits (service-storage, plugin-audit). The same defect class: this change made their prose false. There are no code lines.
    • Three content/docs pages whose sentences this change made false.

    Judgment accepted: flow runs started at the automation door count as addressed writes.

    • A flow's runAs is a flow-level key with no per-node elevation. So a runAs: 'user' flow can only target ids from caller input or from rows the caller can read, and a runAs: 'system' flow is not refused at all.
    • The not-found answer therefore has no path to naming an id the caller could not already see. Treating door-started flows as addressed closes the same existence signal through the automation door, which keeps the ruling's governing text whole.

    Patch round 1: one defect.

    After the patch: CI on the patched head, then the at-tier contract review the ruling requires.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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 ./migrations export. No other changeset line moved. Commit 2ce9723d.\n- Merged origin/main (75ddcd1b) through scripts/pm/os-regen-merge.sh, as merge commit 921da525. The merge brought one plugin-security pin file, and text changes to other step-18 registry entries. Re-running gen:migration-registry produced no diff: the registry is current, with 377 semantic entries.\n- Checks on 2ce9723d, 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 touched plugin-security and the registry. dispatch-gates --commands lists the same 118 commands, and all 118 exit 0 on 2ce9723d. The --ran reconciliation reads: 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN.\n"
    }


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T07:04Z. ⛔ Classes, positions and functions only.

    • PR fix(plugin-security)!: on the write doors, a row the caller cannot read answers what a nonexistent id answers #21812 merged through the merge queue as 53021e3a, which is an ancestor of origin/main (verified with git merge-base --is-ancestor after a fetch). Merged at 2026-10-05T07:03:52Z.
    • Fixes #21771 closed this card completed. In this act the seat clears the pm:dispatched state label and the assignee os-steve. The domain:services, area:access, priority:p2, security and bug labels stay.
    • What shipped (ruling A 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.
    • Declaration: Clause-②: no (narrowing), BREAKING. @objectstack/plugin-security is minor, with the FROM → TO and the affected-caller handling, and @objectstack/spec is patch for one step-18 D3 ledger entry, by-id-write-unreadable-row-not-found.
    • Contract review: an at-tier PASS on the landed head (5989209782).
    • Carried, for the maintainer's round report (none blocks this landing):

    Generated by Claude Code

  10. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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 pin 0abd4f9f (the .objectui-sha on main).

    • Class (a), a client that reads a by-id write's 403 as "exists but no access" in a way the move to 404 would break: none. The console's by-id update and delete go through one adapter, which passes 403 and 404 through unchanged. No write handler builds behaviour on "the row exists".
    • Class (c): seven handlers choose different copy for 403 and 404. They are the record-form save, the calendar drag, the kanban move, the attachments panel delete, the shared permission-error predicate, the quiet HTTP logger and the auth-gate check.
    • Adjacent, flagged and not measured: the console's batch save reads a 404 from POST /api/v1/batch as "endpoint missing" when the backend does not declare transactionalBatch, 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

  11. added 2 commits that reference this issue on Oct 7, 2026
    53021e3
    cab6396
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p2Medium: important, M3security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions