Repository navigation
fix(plugin-security)!: under a controlled_by_parent declaration whose one master reference is readonly or system, every write that sets it is refused (ADR-0055 §1) - #25256
Conversation
…olled_by_parent declaration whose one master reference is readonly or system (ADR-0055 §1) The parse refuses that declaration, so an object carries it only through a door that does not parse. There the write gate now refuses an insert that names a master, with the parse's own message (422 INVALID_METADATA), before any master is asked. Stored rows keep their master and their reads. Claude-Session: https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7af7fc8a34f75e5670fd8438f1f12bc5d514202d && git checkout 7af7fc8a34f75e5670fd8438f1f12bc5d514202d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 284e04810a309d5a7c8c3654b036f9ebf07a53d4 5aea100c1554460a0b97e78c3006f312f7b74981 && git checkout -B drift-repro 284e04810a309d5a7c8c3654b036f9ebf07a53d4 && git merge --no-ff 5aea100c1554460a0b97e78c3006f312f7b74981
node scripts/docs-audit/affected-docs.mjs --json 284e04810a309d5a7c8c3654b036f9ebf07a53d4
|
…arent master reference declared readonly or system (ADR-0055 §1) Ruling 6345869077 on #25095 (route B, refuse at the write gate). The relation still resolves for such a declaration, so reads, the cache and the unresolved report are unchanged and stored rows keep their master. CbpRelation.masterNotWritable is now read from the same field def the relation came from, judged by the parse's own predicate on that one def. The gate refuses, before any master is asked and before anything is written, every insert (single or batch, with or without a master in the body) and every update whose body carries the reference key (any value, null included), 422 INVALID_METADATA with the parse's message. A delete and an update that leaves the key alone take the path they took before. The same answer for every caller the gate judges; the system bypass is unchanged. Tests: the suite is renamed to controlled-by-parent-unwritable-master-write and now covers a member and a platform admin, ordinary and sys_-named objects, inserts with and without a master, batch inserts, updates naming a master or null, and the controls (stored rows read, an update without the key, a delete, a system create, the in-repo declaration shape). The [#8688] flagged-insert pin in controlled-by-parent-sharing and the unjudged-reference pins in controlled-by-parent-every-row-master move to the new answer; the stored-master-first ordering pin moves to the validated detail. The stale AUTHORABLE wording and the two stale freeze-note paragraphs are corrected. Claude-Session: https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB Co-authored-by: Claude <noreply@anthropic.com>
…-fi0g2a # Conflicts: # packages/plugins/plugin-security/src/controlled-by-parent-sharing.test.ts # packages/plugins/plugin-security/src/security-plugin.ts
…table-master suite after #25198 The detail read is now the engine's ReadableIds leaf over its master, so the rig fills the engine's security-source slot as a kernel does. The read-access sentence in the error docblock and the changeset names the security plugin, not the write gate. Claude-Session: https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB Co-authored-by: Claude <noreply@anthropic.com>
…answer INVALID_METADATA for a readonly or system master This PR changes what the two flagged declarations answer, so the two pages that published the old 422 MISSING_REQUIRED_FIELD rows are corrected here: the stored-null-master shape keeps MISSING_REQUIRED_FIELD (its example now shows that message), the flagged rows answer 422 INVALID_METADATA for every write that sets the reference, and the stale "authorable today" paragraphs, false since the parse refuses both declarations, are removed. Claude-Session: https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Face: Gate verdicts. On the head, every completed check is ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged against the ruling and the cited code at the head.
② Semver level
③ Boundary flagsThe dev's os-dev-report
Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT · 2026-10-22T18:58Z
⛔ Disclosure (#9387): abstract. The PR body, the report
Generated by Claude Code |
Requested by Sales · project thread
Fixes #25095
Clause-②: no (narrowing)
Metadata-protocol: none
⛔ Disclosure ruling
5323310017(#9387) applies: this PR states the rule and its prescription only. No probe, posture, caller, request or served value is recorded here or in any comment; the measurements are held privately.The rule
ADR-0055 §1 derives a
controlled_by_parentdetail's access from exactly one requiredmaster_detailthat every write of a detail supplies. Since #25092 the parse refuses that master when it is declaredreadonly: trueorsystem: true(controlledByParentMasterRefusal, shapemaster-not-writable). An object with that declaration still reaches the registry through a door that does not parse:registerObjectorregisterApp;defineStack(…, { strict: false });[metadata_spec_invalid]diagnostic (the registry registers off-spec rows for availability).On such an object the write gate let writes that set the master reference through, and a detail could end up outside the master its access derives from: readable by nobody, while the write reported success.
Now the write gate refuses every write that sets that reference,
422 INVALID_METADATA, before any master is asked and before anything is written:nullincluded (an upsert reaches the gate as one of the two).The message is the parse's own refusal of the declaration, prefixed with the write it refused, so it carries the parse's prescription: remove the flag and declare the master reference as an ordinary required field, or give the object its own
sharingModel. Both flags follow one rule, and every caller the gate judges gets the same answer.Premise checked first (ruling item 7): on all three doors above, the object registers,
resolveCbpRelationresolves its relation, and the flag is readable on the field def the relation came from.What changed
packages/plugins/plugin-security/src/security-plugin.tsCbpRelation.masterNotWritable: resolved inresolveCbpRelationfrom the same field def the relation came from, judged bycontrolledByParentMasterRefusalon that one def, so the flags and the sentence are spelled once, in the parse. The relation still resolves; the read leg, the cache andreportCbpMasterUnresolvedare untouched. No second registry reader, andomissionRefusedByValidationis not widened.assertControlledByParentWrite: right after the missing-relation leg, before any master is asked,writesMasterReference(insert always; update when the body carries the key) throwsMasterReferenceNotWritableError. A delete, and an update that leaves the key alone, take the path they took before.packages/plugins/plugin-security/src/errors.ts:MasterReferenceNotWritableErrorbesideMasterDetailRelationMissingError(INVALID_METADATA, 422; the error-code ledger already has this row for plugin-security). Not exported. TheMasterReferenceMissingErrordocblock is corrected..changeset/25095-cbp-unwritable-master-write.md:patch, BREAKING,Clause-②: no (narrowing),Metadata-protocol: none, ADR-0087not-required (already-registered cbp-master-relation-tiers-retired), aspnpm check:adr-0087-registrationjudges it.content/docs/api/error-catalog.mdxandcontent/docs/protocol/kernel/error-handling.mdx: corrected, because this PR makes them false. They said a write that leaves out areadonlyorsystemmaster reference answers422 MISSING_REQUIRED_FIELD; such a write now answers422 INVALID_METADATA(the two-item list above), and the422 MISSING_REQUIRED_FIELDexception is narrowed to a by-id update or delete of a detail whose stored master reference is null. The flat example body moves to that case.Unchanged, on purpose
controlled_by_parent.packages/spec,packages/objectqlor lint change, and no read-side change.checkControlledByParentWrite) asks a by-id update with no body, so it never reaches the new refusal, and its closed outcome vocabulary is untouched.Tests
packages/plugins/plugin-security/src/controlled-by-parent-unwritable-master-write.test.ts(renamed from…-insert.test.ts, 83 cases). Real stack:ObjectQLover better-sqlite3, the realSecurityPlugin, the real sharing service and middleware, a member context fromresolveAuthzContext+assembleExecutionContext, a platform admin's carried posture, and the engine's security source wired as a kernel wires it, so a detail read composes the engine'sReadableIdsleaf over its master. The flagged declarations are registered raw throughregisterApp, forreadonly,systemand both, on an ordinary object and on asys_-named one. For a member and for a platform admin:INVALID_METADATA/ 422 with exactly the parse's message, and nothing is stored; the master walk is never entered;null, answer the same, and the row keeps its master;controlled_by_parentobjects declare theirs (Field.masterDetail,required: true, through the parse) takes single and batch creates, a re-pointing update, a note update and a delete as before.A system create lands on every flagged declaration, and each flagged declaration is one the parse refuses at
fieldsasmaster-not-writable.Moved pins, by the ruling:
controlled-by-parent-sharing.test.ts's[#8688]flagged-insert cases now expectINVALID_METADATAwith the parse's message (and its stale "AUTHORABLE" wording is corrected);controlled-by-parent-every-row-master.test.ts's cases on itssystem-flagged detail now expectINVALID_METADATAfor every batch create and every emptying update, and its "the stored master is asked first" ordering pin moves to the validated detail, where it still holds.Ablation and the gate list are reported in the card's
os-dev-report.Notes
claude/project-thread-fi0g2ais the harness-pinned branch for this session and names no issue, so the Duplicate Fix Guard's branch-name hint will not match. The claim on plugin-security: the controlled_by_parent detail write gate may not hold one more value of a master reference to ADR-0055 §1 and §3 (reach not measured; detail withheld under the disclosure ruling) #25095 (6345497639) is the identity record.mainafter it. The read scope it moved to the engine'sReadableIdsleaf is not this gate, and the new suite fills the engine's security-source slot the way fix(plugin-security): a controlled_by_parent detail's read scope is the engine's ReadableIds leaf over its master (ADR-0139 D6, seventh consumer); Q2 A in service-analytics; the Turso remote nested leaf #25198's rigs do. Split card [maintainer] refactor(plugin-security): splitsrc/security-plugin.ts(10,746 lines, +2,803 in 30 days) along itsSecurityPluginseams, one move-only slice per PR #25223 waits on this card.packages/spec/src/conversions/refused-shapes.tsstill says a storedmaster-not-writableobject "loses the save door" only; a pointer is left for thedomain:specseat ([PM seat] domain:spec · seat 2 — 🟢 os-bill #18549), since spec is not this lane's.CONTRACT_REVIEW_TIERreview of record on the narrowing face is owed before the queue.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB