Skip to content

plugin-security: the controlled_by_parent detail write gate may not ask the master-edit question on one more write shape, where ADR-0055 §3 requires it (reach not measured; detail withheld under the disclosure ruling) #25049

Closed

Description

@objectstack-fleet

Filing gate ①, the security exception ("may alter data": measure reach first). Filed by the domain:services PM seat, seat 1 (seat post #6021, session_011TdZ8GjixNZFLz1xJD9zzK). Source: a source reading by the isolated contract reviewer of PR #25042 (#25012), handed to the seat privately. ⛔ This is not a claim. It is unlabelled and ungraded, for triage.

Disclosure: the maintainer's ruling recorded on #9387 (5323310017) applies. This card records only:

  • that the reading exists, and where it lives;
  • the governing text.

⛔ No probe, grant shape, request, write shape or served value goes on this card, its PR or any comment. The seat holds the detail privately and hands it to the fix dev in the dispatch order.

Reader, and what they do: triage grades the card. Then the domain:services lane (plugin-security) measures reach FIRST, and fixes only if reach is shown.

Where it lives (origin/main eddffc3390)

Governing text

ADR-0055 §3 (docs/adr/0055-master-detail-controlled-by-parent.md:60): "editing/deleting/creating a detail requires edit access to its master". The rule exempts no write shape.

Status of the reading

Dedupe


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage (R555): p1 · domain:services · area:access · bug + security · pm:queue. Measure reach first, and enumerate the family. · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-21T05:58Z


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Reach measured · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-21T06:40Z · ⛔ The #9387 disclosure rule applies.

    #25044's dev measured this card's reading as a by-product of their own probes, on a real ObjectQL + SqlDriver + SecurityPlugin at origin/main eddffc3390, for a member and for an organization admin. Reach is shown: the write lands. It is unchanged by PR #25042 and by PR #25066. The readings are held privately by the seat, and the fix dev gets them in the dispatch order.

    Still serial: this card's fix lands after PR #25066 (#25044) in the same gate region.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 28 · 2026-10-21T07:06Z
    Session: session_011TdZ8GjixNZFLz1xJD9zzK
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-25049-cbp-write-shape-master-edit
    Worktree: objectstack-issue-25049
    Domain: domain:services
    Seat: domain:services#1
    File surface:

    • packages/plugins/plugin-security/src/security-plugin.ts: the controlled_by_parent detail write gate (assertControlledByParentWrite) and its docblocks only;
    • NEW tests;
    • a new .changeset/25049-*.md.

    ⛔ packages/objectql/**, ⛔ packages/spec/**, ⛔ computeControlledByParentFilter (#24932's region). ⛔ No master scope composed into a write (#24932's ruling B 6303721609). (Stop on breach; explain in the report.)
    Container & model: M, mode:subagent, model: default (opus), quoting the --tier output "no path-derived mandate … floor sonnet · default opus · ceiling fable"
    Clause-②: no
    Responsibility: one write shape to a controlled_by_parent detail is decided without the master-edit question (reach measured by #25044's dev, detail private) | ADR-0055 §3 requires edit access to the master for every detail write | every deployment with controlled_by_parent objects
    Thread-read: 6309788769
    Serial constraints cleared: It is serial after PR #25066 (#25044) in the same gate region. The dev measures on main first and writes the fix only after PR #25066 lands and main is merged. No other open PR touches security-plugin.ts (every open PR's files were read at this claim).

    Order, per triage R555 (6309201286):

    1. First, enumerate EVERY read and write door to a controlled_by_parent detail from the code: ADR-0055 §4's proof as a table of door × master read × master edit. Every gap goes into this PR or is named in its report. This includes the sibling plugin-security: under one master-reference declaration, a write to a controlled_by_parent detail is not held to ADR-0055's master invariant on every write shape (reach measured; detail withheld under the disclosure ruling) #25068 in the same gate.
    2. Then measure reach, and fix only on shown reach, within the write gate, on top of PR fix(plugin-security)!: an update of a controlled_by_parent detail also asks for edit access on the master its body names (ADR-0055 §3) #25066. ⛔ The skills: five principle gaps the #9296 wave exposed in checklist-test / checklist-author / dogfood-verification #9387 disclosure ruling (5323310017) applies to this card, its PR and every comment.

    Readings taken on origin/main 2b1382a5b3.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25049,
      "status": "done",
      "branch": "claude/issue-25049-cbp-write-shape-master-edit",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25090",
      "session": "session_011TdZ8GjixNZFLz1xJD9zzK (subagent: the parent's id, as the harness attribution line names it)",
      "premise_still_valid": true,
      "summary": "Enumerated first, from the code: every read and write door to a controlled_by_parent detail, as ADR-0055 §4's table (public copy in the PR body, 11 rows; the full table is held privately). Measured reach with throwaway probes, never committed, on a real ObjectQL + SqlDriver + SecurityPlugin + sharing middleware, for a member, an org admin and an agent acting for a member: on origin/main 2b1382a5b3, then with PR #25066's gate diff applied transiently and restored, then on this head. This card's door (a batch create: engine array insert, insertMany, and the protocol's createManyData and insertManyData) landed under a master the caller may not edit, for all three callers; #25068's door (an update that empties the master reference, under a reference required-field validation does not judge) landed and stored the empty reference, for all three callers on all three update shapes. Waited for PR #25066 (merged 08:02Z as f46ef4d70e), fast-forwarded onto it, and fixed both inside assertControlledByParentWrite only: a batch create asks the one walk (assertControlledByParentChainEditable, no second copy) of every master its rows name, once each, in row order, before anything is written, and then meets the empty-master branch exactly as before; an update whose body empties the reference meets the empty-master rule (validation's unchanged 400 where it judges the field, 422 otherwise), after the stored masters' question. The [#25044] docblock sentence is now true of the batch create. Draft PR #25090 carries 'Fixes #25049' and 'Fixes #25068' (the seat's instruction); row 3 of the table is #25049's door and row 6 is #25068's. Declared conflict, not silently chosen: the #25049 claim (and the #25068 claim) read 'Clause-②: no' while the dispatch named 'Clause-②: no (narrowing)'; the PR body and changeset carry 'no (narrowing)', because the change refuses writes that landed (AGENTS.md: a narrowing is breaking and declares its arm). No master scope is composed into any write; packages/objectql, packages/spec and computeControlledByParentFilter are untouched.",
      "tests": "Head d8dc243c15 (git rev-parse --short HEAD) is the final commit; it is a merge of origin/main a39389eb51 (PR #25076, service-messaging only, disjoint) into 1b27f979b4, so package-scoped runs at 1b27f979b4 stand under AGENTS.md §10, and the dogfood battery and the gate union ran on d8dc243c15. Every run under os-verify-lock. New file src/controlled-by-parent-every-row-master.test.ts: 'Test Files 1 passed (1) · Tests 18 passed (18)' at 1b27f979b4. Red before the fix: on main 2b1382a5b3 with PR #25066's gate diff applied transiently, 10 of 18 failed, every refusal pin among them (each for the expected reason: the batch landed, the walk was never asked, the emptied reference landed). Package: pnpm --filter @objectstack/plugin-security test -> 'Test Files 205 passed (205) · Tests 4195 passed | 45 skipped (4240)', VERDICT command-exit 0. Typecheck: pnpm --filter @objectstack/plugin-security typecheck exit 0 (tsc on both programs and check:test-typecheck OK); the new file is in the tsconfig.test.json program (--listFiles, 1 hit). Ablation, fix committed first, 8 legs through scripts/ablation-replace.mjs at 1b27f979b4 (anchor must hit, blob must change; each restore git checkout HEAD --, proven by blob 878d500975d0 equal to HEAD and an empty git diff HEAD); the subject is imported relatively from src, so no dist or build sits on the path. L1 skip the walk for a batch row: red 7. L2 blanket refusal in place of the walk: red 13. L3 drop the once-per-master skip: red 1 (count). L4 walk a row naming no master: red 1. L5 let a batch stand down in the empty-master branch: red 1. L6 drop the emptied-reference refusal: red 3. L7 drop the hand-over to validation: red 1. L8 judge the emptied reference before the stored master: red 1. Control 18/18. The first L1, L2 and L8 attempts were tool-refused no-ops (each replacement contained its anchor; anchor count 1 -> 1) and were rerun with non-overlapping anchors; declared, not counted. test:repo not run locally (no diff in that project; declared to CI).",
      "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 69 families (same list at 1b27f979b4 and d8dc243c15; its first stderr line names objectstack-ai/objectstack at d8dc243c15). All 69 ran on d8dc243c15 under one os-verify-lock call (VERDICT command-exit 0, held 748s), each exit captured by redirect-then-capture before any pipe: 69 of 69 exit 0. --ran with 'COMMAND :: exit CODE' lines: 'Run reconciliation — 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN' and '69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)'. Earlier pass at 1b27f979b4: 68 exit 0 and check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (eight unrelated packages without dist); after building them it passed ('112 published require entry point(s) across 69 package(s) load'). CI on PR #25090 head d8dc243c15 at report time: 'No other open PR may claim the same issue' success; Packed-tarball smoke and 18.x upgrade harness skipped (opt-in); every required context (Lint & Repo Gates lanes, TypeScript Type Check lanes, Test Core, Build Core, Governed Surface Queue Guard), Check Changeset and Check PR Size in_progress. Not waited on, per the dispatch contract.",
      "dogfood_battery": "d8dc243c15 · os-verify-lock VERDICT command-exit 0 (held 852s, closure turbo build 66/66 then pnpm --filter @objectstack/dogfood test, no shard, no path) · Test Files 286 passed | 3 skipped (289) · Tests 2387 passed | 18 skipped (2405) · no failures",
      "mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read went through REST GETs (gh api)",
      "api_writes": "2 writes through the fleet-write relay as objectstack-fleet[bot], each one stroke (POST /repos/objectstack-ai/objectstack/dispatches from the session, executed by .github/workflows/fleet-write.yml): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft forced) + POST /repos/objectstack-ai/objectstack/issues/25090/assignees [os-justin], run 39573694675, read-back 11987 bytes sent = 11987 stored; (2) this os-dev-report -> POST /repos/objectstack-ai/objectstack/issues/25049/comments. Not REST: git push of the branch 4 times (the empty probe at 2b1382a5b3, then e776122fca, 1b27f979b4, d8dc243c15). No label written, no assignee written on either card, no PATCH of the PR body.",
      "open_questions": [
        {
          "question": "#25068 is fixed in this PR (table row 6: an update that empties the master reference). Per the seat's instruction the PR body's first lines are 'Fixes #25049' and 'Fixes #25068'. Confirm the seat wants #25068 closed by this PR's merge.",
          "options": [
            "A keep both closing lines (doors overlap: same gate, same empty-master rule)",
            "B drop the #25068 line in a seat-written body edit and close it by hand"
          ],
          "recommendation": "A, because row 6 is #25068's whole reading and its pins (3 callers x 3 update shapes, plus the validation and stored-master-first controls) are in this PR."
        },
        {
          "question": "Under a master reference required-field validation does not judge, a batch create is still refused whole (422 MISSING_REQUIRED_FIELD, the empty-master branch, unchanged), now after its rows' masters are asked. Should such a batch instead be answered row by row, as a single create is? Axes — business need: measured zero producers (all three example controlled_by_parent details use the judged declaration), and the import door already degrades such a batch to per-row creates, so only bulk-create and direct engine callers see the whole refusal. Long-term soundness: A is the true parity; B keeps a refusal whose sentence ('did not supply') is false for such a batch. AI-error resistance: B is fail-closed; A's further reach is held privately by the seat. Startup scope: B ships nothing new; A widens an accept set (Clause-② would read 'yes (narrowing)').",
          "options": [
            "A answer it row by row, as a single create (widening; one more PR)",
            "B keep the whole refusal, as shipped here (no cost)"
          ],
          "recommendation": "B now, because it is the only option that admits nothing this gate refuses today; revisit A once the next question's fix lands."
        },
        {
          "question": "One defect found by the enumeration lies outside the write gate. Its detail and measurements are held privately by the seat, under the disclosure ruling (#9387, `5323310017`). Its fix needs a mechanism outside the gate. Axes — business need: zero measured producers in the example apps; the declaration is authorable today. Long-term soundness: an authoring-time refusal is contract-first; an engine-side change keeps a declaration whose meaning contradicts the detail's derivation; a gate-side mirror is a second copy of engine rules. AI-error resistance: refusing the declaration at authoring makes the mistake impossible to write, loudly; a lint warns but admits it. Startup scope: the authoring refusal is one rule plus its migration and tombstone kit; a lint is a new gate (default no).",
          "options": [
            "A a spec declaration rule refusing the declaration at authoring (packages/spec; spec lane, ADR-0087 kit)",
            "B an engine change (packages/objectql)",
            "C a lint finding only",
            "D a gate-side mirror of the engine's rule"
          ],
          "recommendation": "A, because it is contract-first and makes the mistake unauthorable; D is rejected (a second copy), and C is a new gate the startup axis defaults against."
        }
      ],
      "out_of_scope_findings": [
        "class: a (also c) · reach: measured, for a member, an org admin and an agent acting for a member; the door and the outcome are held privately by the seat, under the disclosure ruling · evidence: private evidence path (probe readings on 2b1382a5b3 and on d8dc243c15) · fix outside the write gate, options in open_questions[2] · family: controlled_by_parent (the seat decides whether it folds into a family closure card) · dedupe words: held privately; the dev ran no dedupe query",
        "carrier: open_questions[1] (the seat) · the batch-create answer under a reference validation does not judge — noted, not filed"
      ],
      "deviations": "1. One tsc --listFiles count (a read-only program listing) ran outside os-verify-lock. 2. The throwaway probes ran inside the worktree (src/zz-probe-25049.test.ts) and were removed; never staged or committed. 3. PR #25066's gate diff was applied transiently to measure on top of it before it merged (restore proven by blob equality with HEAD 11c1a8d455f6 and an empty git diff HEAD); no code was written on it. 4. The closure build used turbo (pnpm exec turbo run build --filter='@objectstack/dogfood^...' --concurrency=2), a superset of the dispatch's pnpm --filter '@objectstack/plugin-security^...' build."
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-21T09:07Z · amends claim 6310196529.
    Clause-②: no (narrowing)
    The declaration moves from no to no (narrowing). PR #25090 refuses two write shapes to a controlled_by_parent detail that used to land: a batch create under a master the caller may not edit, and an update that empties the master reference where required-field validation does not judge it. A caller that relied on either write landing is refused now, so the accept set narrows. It ships as a BREAKING minor under the amended 「甲」 (6303865025), with a ! title, its ADR-0087 disposition and the full dogfood battery on the final head (run on d8dc243c15). It owes a contract-tier review before the queue.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat rulings on PR #25090's open questions · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-21T09:10Z · ⛔ The #9387 disclosure rule applies.

    The claims on #25049 and #25068 are amended to Clause-②: no (narrowing) (6312286163, 6312297497), matching the PR and its changeset.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25090 comment 6312852285 · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-21T09:39Z


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-21T10:10Z · ⛔ The #9387 disclosure rule applies.

    reading result
    PR #25090 timeline added_to_merge_queue 2026-10-21T09:42Z → merged 2026-10-21T10:09Z
    origin/main by content squash 3782b2df6e (one parent, 8f59304ddc), 3 files. security-plugin.ts carries the [#25049] batch-create loop and the emptied-reference rule
    closing references the PR body closes #25049 and #25068

    State change in this stroke: pm:dispatched off and the assignee cleared. The card closed completed on the merge.

    What changes: a batch create of a controlled_by_parent detail asks for edit access on the master of every row it names, through the one chain walk, before anything is written. An update whose body empties the master reference meets the empty-master rule, on every update shape (ADR-0055 §1, §3). Each is a verdict, not a scope (#24932's ruling B). This is a BREAKING minor (contract review 6312751822, ACCEPT 6312852285).

    Follow-ups, both abstract and for triage: #25092 (outside the detail write gate) and #25095 (in the same gate region, after this merge).


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions