Skip to content

finding(security): every write to a controlled_by_parent detail TWO levels below its root master answers 403 "master … not editable" for a principal who edits that master — a regression against 17.7.0, measured on hotcrm's crm_quote_line_item #23093

Closed

Description

@objectstack-fleet

Ruled: 6134050293 · letter A · 2026-10-12T22:48Z
Filing gate ①, class (a): a reproducible defect, measured at a public door. It is a regression against the released engine.

reach: HTTP on this tree, and a named producer. The producer is hotcrm's crm_quote_line_item → crm_quote → crm_account. hotcrm's pinned 17.7.0 engine admits all 15 writes that this tree refuses.

Filed by domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6. It comes from #15953 slice 4's dev (os-dev-report 6131548194, out-of-scope finding 1). That slice's new family, @objectstack/verify's parent-derived write reach (PR #23091), found it. ⛔ Not a claim. Lane and grade are triage's.

Reader who acts: triage grades it. The named seam is packages/plugins/plugin-security/src/security-plugin.ts#assertMasterRowEditable, which reads as domain:services. This seat does not land there.

Dedupe: semantic issue search scoped to this repository, all states.

Measured at the door (this tree, 275ad4229a and 8b4a5802f0)

The fixture app is account (private) ← quote (controlled_by_parent) ← quote_line (controlled_by_parent). One signed-in principal holds:

  • account {read, edit, modifyAllRecords};
  • quote {read, create, edit};
  • quote_line {read, create, edit, delete}.

The answers:

  • PATCH /api/v1/data/pwr_quote/ID with an empty body answers 200.
  • PATCH /api/v1/data/pwr_quote_line/ID answers 403 PERMISSION_DENIED: [Security] Access denied: update on 'pwr_quote_line' requires edit access to its master record (master 'pwr_quote' not editable by this user (row-level security)).
  • Delete and insert answer the same.
  • The same holds for a principal who owns the whole chain, and with modifyAllRecords on both masters.

Against the published engine (hotcrm, read-only clone at 1d7148bf)

  • Run A, hotcrm's own pinned @objectstack/verify 17.7.0 over its own artifact: all 15 writes to crm_quote_line_item by sales_manager and sales_rep are admitted, consistently with their admitted crm_quote edits.
  • Runs B and C, this tree's engine over the same artifact: all of them are refused. That is 31 contradicted writes across the personas.
  • On this tree, single-level details (crm_contract, crm_quote, crm_opportunity_line_item, crm_contact) agree with ADR-0055 §3.

The contract

ADR-0055 §3: "editing/deleting/creating a detail requires edit access to its master". The 2026-09-07 amendment: the write gate "walks the chain hop by hop … until it reaches a master that governs its own rows", and "a detail whose whole chain is reachable stays readable and writable".

Here the middle master (quote) is itself controlled_by_parent, and the caller edits it (200). The hop still answers "not editable (row-level security)".

Where it seems to break (a lead, not a diagnosis)

The dev read assertMasterRowEditable: the hop's computeRlsFilter(master, 'update') plus ql.findOne under the caller's context answers no row for a controlled_by_parent middle master. It also notes that the @objectstack/plugin-security 17.3.0 chain walk (#11082, PR #11183) is what the amendment describes. When this regressed is NOT MEASURED.

Points for the dev

  1. Bisect first. Measure on the released 17.7.0 engine against this tree, using the fixture shape above. PR feat(verify): the parent-derived write reach family, runnable as os verify --parent-write-reach (#15953 slice 4) #23091's pinned test finds today's engine refusing a two-level detail whose master the persona edits (packages/verify/src/parent-write-reach.test.ts) is a ready reproduction.
  2. Fix the hop, so that a controlled_by_parent master's editability is its own write gate's answer, which composes the chain. ⛔ Do not open the gate. The cycle guard and the depth bound keep denying.
  3. Pins:
    • a two-level detail is writable exactly when its middle master is editable by the caller;
    • CONTROL: a single-level detail is unchanged;
    • CONTROL: a caller who cannot edit the middle master is still refused.
  4. When this lands, PR feat(verify): the parent-derived write reach family, runnable as os verify --parent-write-reach (#15953 slice 4) #23091's pinned expectation flips from contradicted to proven. That PR's README and cli.mdx say so.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · security · priority:p1 · target:v18 · domain:services · area:access, into pm:queue. A regression against 17.7.0: the chain walk is restored to ADR-0055 §3 as amended, and widened no further

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T20:51Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Why p1 and target:v18: a write gate refuses writes that the released 17.7.0 engine admits, on a real product. HotCRM's quote line items are refused for every persona that edits the quote: 31 contradicted writes. Shipping v18 with this breaks HotCRM's quote editing. It is fail-closed, so nothing is exposed; the cost is broken work.
    • Why security without escalation: it is an authorization gate. The fix restores the contract ADR-0055 §3 states (the 2026-09-07 amendment: the gate "walks the chain hop by hop … until it reaches a master that governs its own rows"). Restoring a ruled contract is not a loosening. ⛔ If the fix would admit any write the 17.7.0 engine refuses, it is a loosening, and it stops and escalates.
    • Direction:
      1. Bisect first, on the card's fixture shape, to name the commit that regressed.
      2. assertMasterRowEditable decides a controlled_by_parent middle master's editability by walking on to the master that governs its own rows, as the amendment says. ⛔ Not by reading the middle row under its own row-level filter.
      3. ⛔ No bypass for a named object, and ⛔ no modifyAllRecords short-cut that the contract does not state.
    • Triage: lands in packages/plugins/plugin-security (domain:services); rationale: the write gate's chain walk.
  2. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-12T21:20Z
    Session: session_01FzQy26KznSa5j2gnf3AzsE
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-23093-cbp-two-level-chain-walk
    Worktree: objectstack-issue-23093
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface: packages/plugins/plugin-security/src/security-plugin.ts (only the write gate's chain walk: assertMasterRowEditable and the hop loop of assertControlledByParentWrite) and that plugin's tests for this gate; one .changeset/23093-*.md; if PR #23091 has merged first, its pinned expectations in packages/verify/src/parent-write-reach.test.ts and packages/cli/test/verify-parent-write-reach.test.ts (expectation flips only, declared to domain:cli seat 2 in #22648) (stop on breach; explain in the report)
    Container & model: M (security write gate; bisect first, then a fix bounded by the released 17.7.0 engine's answers), mode:subagent, model: default — dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate … The tier stays the PM's per-card judgment call"
    Clause-②: no — a runtime authorization behaviour restored to ADR-0055 §3 as amended; no published accept set or public surface moves.
    Responsibility: plugin-security's write gate (assertMasterRowEditable, the platform's own code) refuses a two-level controlled_by_parent write that ADR-0055 §3 and the released 17.7.0 engine admit | none: every detail write passes this gate, and no other path admits it | hotcrm's crm_quote_line_item under crm_quote under crm_account, for every persona that edits the quote (31 contradicted writes, measured at the door on the card), and any app with a two-level controlled_by_parent chain
    Thread-read: 6132405210
    Serial constraints cleared: same file: PR #22900 (#15204 stage 6b-2, draft, needs-user-decision) edits security-plugin.ts only at its boot regions (hunks at about 4431 to 4751) and one doc comment near CbpRelation (about 588), disjoint from this region (about 9300 to 9550); declared to the epic in #15194. PR #23091 (domain:cli seat 2, draft) pins today's refusal in packages/verify; the flip order is declared in #22648. No other open PR touches security-plugin.ts (the open PRs' file lists, read at this claim). Recent same-file landings: 06cc45dc3 (#22895), 0253192f7 (#22863), d112087b8 (#22854); they are bisect candidates, not claims.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23093,
      "status": "needs_decision",
      "branch": "claude/issue-23093-cbp-two-level-chain-walk",
      "pr": null,
      "session": "session_01FzQy26KznSa5j2gnf3AzsE — the dispatching PM's session (mode:subagent); identity = the branch named in claim 6132814355",
      "premise_still_valid": false,
      "summary": "PREMISE FALSE: the engine did not regress. On the card's fixture shape (quote_line controlled_by_parent under quote controlled_by_parent under account private), driven through @objectstack/verify bootStack + hooks.run as each signed-in persona, the released 17.7.0 engine (4e4e88142, the plugin-security@17.7.0 tag commit) and main (6662e3f59) give the SAME answer once the persona is the same: a persona holding position org_member is refused every line write (update/insert/delete) under a quote it did not create — 403 PERMISSION_DENIED, \"[Security] Access denied: update on 'rp_quote_line' requires edit access to its master record (master 'rp_quote' not editable by this user (row-level security))\" — while its direct update of that same quote answers ok; a persona without org_member is admitted on both trees. What differs between hotcrm's Run A and Runs B/C is the INSTRUMENT, not the gate: 17.7.0's bootStack pinned AuthPlugin autoDefaultOrganization: false (org-less personas, positions ['everyone'], tenant none, zero sys_organization rows); commit 34dba5ae1 (#22186, ADR-0131 C1) removed that pin, so harness sign-ups became Default Organization members (positions ['org_member','everyone']). 17.7.0's own serve/dev composition already binds every sign-up as role member (plugin-auth reconcile-membership.ts, membershipPolicy auto) — read from source, not booted. Mechanism (H1 confirmed, located one step further): the hop for a controlled_by_parent MIDDLE master runs computeRlsFilter(master, 'update') with the platform ownership floor owner_only_writes (created_by == current_user.id, positions ['org_member']) standing, because assertMasterRowEditable deliberately does not set masterGateCoversThisWrite; the by-id write of that same master drops it (step 2.7, the 2026-08-15 ruling recorded in commit 6feac910b). Discriminator measured: the same org-member persona's chain where IT created the quote is admitted for all 5 writes on both trees. The gate region (assertControlledByParentWrite + assertMasterRowEditable) is unchanged 17.7.0 -> main except the leg labels. HARD STOP: any fix that admits these writes admits writes the 17.7.0 engine refuses for an org member — triage 6132405210's escalation condition. The candidate (A below) was prototyped locally, built, measured and restored; nothing pushed, no PR. The branch is pushed empty at 6662e3f59 as the claim's landing marker.",
      "tests": "Scratch reproduction (outside the repo, never committed; copied untracked into packages/verify/src of each tree, removed after): boots the fixture with securityDefaultPermissionSets plus four persona sets, seeds chains as system (owner_id other / own), plus a 'door' chain whose quote the persona inserts itself; each persona has a twin with org_member toggled (17.7.0: insert sys_member role member; main: delete the persona's sys_member rows). Builds (all under os-verify-lock, slot issue-23093): main closure `pnpm turbo run build --filter=@objectstack/verify^... --concurrency=2` VERDICT command-exit 0 (46 tasks); 17.7.0 closure same command in a detached worktree at 4e4e88142, VERDICT command-exit 0 (32 tasks). Runs (`pnpm exec vitest run --maxWorkers=2 src/zz-repro-23093.test.ts`, each VERDICT command-exit 0, Tests 1 passed): 2x2 per persona rp_manager (account read/edit/modifyAll, quote read/create/edit, line CRUD) — 17.7.0 no-org_member: other+own+door lines 15/15 ok; 17.7.0 org_member: other+own lines 10/10 refused 'row-level security', door 5/5 ok; main org_member: other+own 10/10 refused 'row-level security', door 5/5 ok; main no-org_member: 15/15 ok. Quote and contract direct updates ok in every cell. Controls identical on both trees: rp_rootro (account read-only) refused everywhere; rp_midro (quote read-only) lines refused 'no edit permission on master rp_quote'; rp_member (owner-only) other chain 404 RECORD_NOT_FOUND on by-id writes and insert refused. Prototype A (one line in assertMasterRowEditable: computeRlsFilter(..., { dropPlatformOwnershipFloor, masterGateCoversThisWrite: !delegatedWrite && this.declaresControlledByParent(rel.master) })): plugin-security build VERDICT command-exit 0, marker grep -c in dist/index.js = 1; matrix run VERDICT command-exit 0: every line answer equals the same principal's direct quote-update answer — org-member rp_manager other+own 10/10 ok, rp_member own 5/5 ok (these 15 are what 17.7.0 refuses for an org member); rp_rootro 10/10 still refused (now at the account hop, 'no edit permission on master rp_account', the same text 17.7.0 gives its no-org_member persona); rp_midro still refused; rp_member other insert still refused 'row-level security'. Restore: `git checkout HEAD -- packages/plugins/plugin-security/src/security-plugin.ts`; git diff HEAD = 0 lines; hash-object 0986060dcc5f = HEAD blob 0986060dcc5f; rebuild VERDICT command-exit 0; `node scripts/ablation-dist-preflight.mjs @objectstack/plugin-security MARKER --absent` -> marker absent from all 6 built files, tree clean. No package test/typecheck run: the branch has no diff (`dispatch-gates --commands --repo objectstack-ai/objectstack` exit 2: 'this branch changes nothing against origin/main (merge base 6662e3f59)'). NOT MEASURED: an app-authored update RLS policy on the middle master under prototype A; a 17.7.0 serve boot (the member auto-bind there is read from source).",
      "mcp_calls": "0",
      "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/23093/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Not REST: one git push of the empty branch.",
      "open_questions": [
        {
          "question": "Should the chain walk stop applying the platform ownership floor (owner_only_writes) on a controlled_by_parent MIDDLE master — i.e. extend the 2026-08-15 ruling recorded in commit 6feac910b ('the master gate is the sole row-write authority for a controlled_by_parent detail') from that object's own by-id write to the hop that decides edit access for its details? Measured today, on both 17.7.0 and main, one org-member principal gets 200 on PATCH quote and 403 on every line under it unless it created the quote; ADR-0055's amendment says a detail whose whole chain is reachable stays writable, while assertMasterRowEditable's docblock records the opposite choice ('A master that is itself a detail keeps its floor exactly as it does today'). Any answer that admits these writes admits writes the released 17.7.0 engine refuses for an org member, which triage 6132405210 sends to escalation.",
          "options": [
            "A — Extend the ruling to the hop. One line in assertMasterRowEditable: pass masterGateCoversThisWrite = !delegatedWrite && declaresControlledByParent(rel.master), so the RLS leg on a middle master is the same composition its own by-id write uses (only the PLATFORM floor drops; authored update RLS and the derive-from-select floor still run; the walk continues to its master as today; delegated writes keep both floors, mirroring step 2.7's !delegatorSets). Measured on the fixture: each line answer equals the same principal's direct quote-update answer; newly admitted = 15 writes for org members who can already update that quote; root-not-editable, middle-not-editable and unreadable-root refusals measured unchanged; the cycle and depth guards sit in the loop, outside the edited leg (code path untouched, not in the measured matrix). Widening: an org_member who can PATCH the middle master by id gains writes on its details, nobody else. Cost: S, one PR — the line, the docblock bullet rewritten, pins in controlled-by-parent-chain.test.ts (its fixture needs the floor and an org_member position), negative-pin ablations, patch changeset; PR #23091's pinned expectation flips (domain:cli). No ADR amendment needed: ADR-0055 already states the target.",
            "B — Keep the floor (status quo, equal to the 17.7.0 engine). Record it instead: an ADR-0055 line that a middle master's editability in the walk includes the platform floor (creator-only for rank-and-file members), and PR #23091 reports that cell held rather than contradicted. Cost: an ADR amendment (Tier H) plus the verify family change; no engine change. Consequence: hotcrm's quote lines stay editable only by the quote's creator and by principals outside the floor's domain (org owner/admin, non-members); no authorable lever widens it — modifyAllRecords on both masters included (card-measured: still refused) — the same undeclared creator-only rule that ruling removed for direct writes.",
            "C — Drop the floor in the hop only for principals holding modifyAllRecords (or another widener) on the middle master. Excluded verbatim by triage ('no modifyAllRecords short-cut that the contract does not state'); listed only to close it."
          ],
          "recommendation": "A, but only with the maintainer's explicit approval of the permission-boundary widening, as the 2026-08-15 ruling's direction 1 approved the single-level analogue. The four axes: (1) Real business need, measured: hotcrm's crm_quote_line_item -> crm_quote -> crm_account (31 contradicted writes on the card; here 15/15 line writes refused for an org-member manager on two chains while the quote update is 200). In-repo examples at 6662e3f59 hold 3 controlled_by_parent objects, all single-level, so the pull is one named external product. A serves it; B leaves it with no authorable fix; C serves only principals holding modifyAllRecords on the middle master (hotcrm's personas' grants not measured here). (2) Long-term soundness: A gives one composition for one (principal, master row, update), the 'one composition, both paths' direction already ruled for leg 1 (commit 498f4e884) and leg 2 (#8679). B keeps two answers for that triple. C adds a second widening path the contract never states. (3) Keeping AI authors from getting it wrong: under A a declared controlled_by_parent chain means the same thing at every level, so the declaration is the enforcement. Under B a natural quote/line chain an AI writes is silently creator-only, with no publish-time error and no lever. C hides the rule behind a bit. (4) Start-up scope discipline: A adds no surface, key or gate. B adds an ADR amendment and a verify-family exception. C adds a conditional path. Nobody proposes a new gate."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the PM / triage seat · noted, not filed — the card's title and its p1/target:v18 rationale ('a regression against 17.7.0', 'Shipping v18 with this breaks HotCRM's quote editing') rest on hotcrm Run A, whose 17.7.0 bootStack personas were org-less; measured here, the 17.7.0 engine refuses the same writes for an org member, so the break predates v18 for any deployment whose sign-ups are members. Re-grading is the triage seat's call.",
        "carrier: PR #23091 (domain:cli seat 2) · noted, not filed — its pinned test 'finds today's engine refusing a two-level detail whose master the persona edits' is a correct reading of today's engine; only its framing as a 17.7.0 regression is contradicted by this run. Under option A its expectation flips (declared in #22648); under B it becomes held.",
        "carrier: hotcrm (pins @objectstack/verify 17.7.0) · noted, not filed — 17.7.0's bootStack pins autoDefaultOrganization: false, so hotcrm's verify runs measure org-less personas (positions ['everyone']) that no serve deployment has; every floor-scoped answer there reads wider than production. Its upgrade past 34dba5ae1 is the carrier."
      ],
      "gates": "none owed — empty change set (dispatch-gates --commands --repo objectstack-ai/objectstack, tree 6662e3f59: exit 2, 'this branch changes nothing against origin/main'); the PM's 66-command reading was not run because no file changed.",
      "line_budget": "n/a — no skills/** or ruled-ledger file touched",
      "deviations": [
        "No draft PR and no changeset: the dispatch's hard stop applied (the only fix shape found admits writes the 17.7.0 engine refuses), so the order's PR/changeset/label steps were not reached; label-write not called.",
        "Bisect done as endpoint measurement plus source identification rather than a per-commit build bisect: the engine answer is identical at 17.7.0 and main once the persona shape matches (2x2 above), so there is no engine commit to bisect to; the instrument change is identified by source diff as 34dba5ae1 (harness pin removed), not by booting 34dba5ae1 and its parent.",
        "A prototype edit to security-plugin.ts was built and measured locally to give the decision exact data, then restored (hash and dist-absence proofs in tests); never committed or pushed.",
        "Fetching PR #23091's branch into refs/issue-23093/pr23091 also updated the shared remote-tracking ref origin/claude/issue-15953-s4-parent-write-reach (read-only side effect); the owned ref was deleted afterwards."
      ],
      "files_changed": []
    }

    Generated by Claude Code

  4. 11 remaining items

  5. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · 2026-10-12T23:17Z
    Session: session_01FzQy26KznSa5j2gnf3AzsE
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-23093-cbp-two-level-chain-walk (pushed empty at 6662e3f59 by the previous dispatch, an ancestor of main)
    Worktree: objectstack-issue-23093
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    Ruling-ref: 6134050293 (director binding of the maintainer's 「同意」, letter A) · 6133922339 (the direct-channel record, 「A 延伸裁决 (推荐)」)
    File surface: packages/plugins/plugin-security/src/security-plugin.ts (only assertMasterRowEditable's write-RLS leg and its docblock bullet) and that plugin's tests for this gate (controlled-by-parent-chain.test.ts and neighbours); one .changeset/23093-*.md; if PR #23091 has merged first, its pinned expectations in packages/verify/src/parent-write-reach.test.ts and packages/cli/test/verify-parent-write-reach.test.ts (expectation flips only, declared in 6132790096) (stop on breach; explain in the report)
    Container & model: M (ruled one-line composition change plus four pins, every refusal pin ablated), mode:subagent, model: default — dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate … The tier stays the PM's per-card judgment call"
    Clause-②: no — a runtime authorization behaviour under the maintainer's ruling A; no published accept set or public surface moves.
    Responsibility: plugin-security's write gate (assertMasterRowEditable, the platform's own code) keeps the platform ownership floor at a controlled_by_parent middle master that the same principal's direct write has dropped since the 2026-08-15 ruling | none: every detail write passes this gate | hotcrm's crm_quote_line_item under crm_quote for organization members who can update the quote (31 refused writes on the card; 15/15 in the dev's fixture), on 17.7.0 and main alike
    Thread-read: 6134050293
    Serial constraints cleared: same file: PR #22900 (#15204 stage 6b-2, draft) edits only the boot regions and one doc comment near CbpRelation, disjoint (declared in #15194). No commit touched security-plugin.ts between 6662e3f59 and main 8464380248. PR #23091 (domain:cli, draft, open) pins today's answer; the flip order is declared in 6132790096. No other open PR touches the file (open PRs' file lists, read at this claim).


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23093,
      "status": "done",
      "branch": "claude/issue-23093-cbp-two-level-chain-walk",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23114",
      "session": "session_01FzQy26KznSa5j2gnf3AzsE — the dispatching PM's session (mode:subagent); identity = the branch named in claim 6134458430",
      "premise_still_valid": true,
      "summary": "Ruling A implemented: assertMasterRowEditable's write-RLS leg now passes masterGateCoversThisWrite: !delegatedWrite && this.declaresControlledByParent(rel.master) to computeRlsFilter, so a controlled_by_parent middle master on a non-delegated write is judged by its own direct-write composition (only the platform floor yields; app update RLS and the derive-from-select floor still run; a delegated write keeps both floors). The docblock bullet ('A master that is itself a detail keeps its floor ...') is rewritten to state the ruling and why the vouch holds structurally (same predicate that continues the walk). H1 confirmed: the prior prototype line is the whole engine change. H2 confirmed: an app-authored update policy on the middle master still refuses the line write; measured consequence: before the fix the CREATOR of a middle master whose app policy refuses its direct update could still write its lines (floor OR policy), now refused, matching the direct update (the fork invariant's other direction; no recipe beyond the changeset's behaviour notice). H3 confirmed by pin 4 and ablation A4. No fork: zero cases at either layer where a line write's answer differs from the same principal's direct quote update. PR #23091 had MERGED before the PR opened, so origin/main (21c86444e) was merged in and its verify expectation flipped (pwr_quote_line proven); its cli test needs no flip (single-level fixture).",
      "tests": "HEAD 343e21e33 (final, after merging origin/main 21c86444e): `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2` -> 'Test Files 191 passed (191) / Tests 3966 passed | 45 skipped (4011)', VERDICT command-exit 0; `pnpm --filter @objectstack/plugin-security typecheck` VERDICT command-exit 0 ('check:test-typecheck: OK'); `pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2 src/parent-write-reach.test.ts` -> 'Tests 10 passed (10)', VERDICT command-exit 0 (pwr_quote_line proven, summary held 135 / contradictions 0 / hardFailures 0); `pnpm --filter @objectstack/verify typecheck` OK; closure `pnpm turbo run build --concurrency=2 --filter='@objectstack/verify^...' --filter=@objectstack/verify` 47/47, VERDICT command-exit 0; narrowed eslint `--no-inline-config --format json` on the 3 touched TS files: 3 files, 0 errors, 0 warnings, none ignored; eslint.config.mjs never enables type-aware linting (no parserOptions.project), so untouched files' verdicts cannot move; full `pnpm lint` is CI's. At c6a48bd1d (pre-merge): suite 193 files / 3981 passed / 45 skipped, VERDICT 0; the new test file is in tsconfig.test.json's program (--listFiles count 1). PRE-FIX BASELINE on main 846438024, new pin file: 'Tests 5 failed | 4 passed (9)' (pin 1 line update refused '(master cbc_quote not editable by this user (row-level security))'; pin-2 refusals at the quote hop, not the root; pin 3 creator case ADMITTED; pin 4 non-delegated control refused). POST-FIX: 'Tests 9 passed (9)'. ABLATIONS, each from committed c6a48bd1d via `node scripts/ablation-replace.mjs` in WRAP mode under os-verify-lock, inside a driver with `trap restore EXIT INT TERM` (git checkout HEAD -- absolute path, blob compared to HEAD blob, empty = failure); the test imports ./security-plugin.js from src, so no dist leg: A0 old leg (masterGateCoversThisWrite: false): anchor 1->0, blob 12a9565cd3b9->cb1cf13b368e, 'Tests 5 failed | 4 passed', pin 1 'line update: expected { kind: refused ... } to deeply equal { kind: admitted }'; A2 walk stops after the first hop: 2 failed (both pin-2 cases; root-unreadable insert passed the gate and met VALIDATION_FAILED downstream); A3a middle object-permission leg skipped for cbp: 1 failed (pin 3 no-grant: admitted); A3b middle write-RLS leg skipped for cbp (bare widening): 4 failed (pin 3 app-policy + creator, both pin 4): admitted; A4 '!delegatedWrite &&' removed: 2 failed (both pin-4 cases admitted while the delegated quote update is refused). Every leg: 'ok restored: blob == HEAD (12a9565cd3b9) and git diff HEAD is empty'; driver re-check: hash-object == HEAD blob 12a9565cd3b9d8e369cc1021f42e7de17077c631, git diff HEAD 0 lines; green re-run from the committed state: 5 cbp files, 71 passed, VERDICT 0. FULL STACK (scratch zz-repro-23093.test.ts, the previous dev's matrix plus an rp_locked H2 persona, copied untracked into packages/verify/src and removed after; plugin-security dist rebuilt from c6a48bd1d, marker grep -c 1 in dist/index.js and index.mjs): 5 personas x {org_member, toggled} x chains other/own/door x 3 line verbs, invariant violations [] ; manager 15/15 admitted both variants; rp_locked 30/30 line writes refused naming rp_quote (row-level security), door chain included; root-RO refused at the root; middle-RO refused naming rp_quote. Whole-repo sweep: 11 files carry the refusal text; none pins the two-level refusal under the floor (org_member fixtures are single-level); the full suite stayed green.",
      "mcp_calls": "0",
      "api_writes": "3 REST writes (each through the fleet-write relay as objectstack-fleet[bot], 2 dispatches): (1) pr_create, run 38325882287 = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/23114/assignees (zhuangjianguo), read back 10461 bytes identical; (2) POST /repos/objectstack-ai/objectstack/issues/23093/comments (this os-dev-report, scripts/pm/post-stamped.mjs). Not REST: 3 git pushes of claude/issue-23093-cbp-two-level-chain-walk (empty probe 6662e3f59..846438024, c6a48bd1d, 343e21e33). label-write: 0 calls (the dispatch named no label; a changeset is present so skip-changeset does not apply); the labels documentation / size/l / tests / tooling were attached by the labeler, not by this run.",
      "open_questions": [
        {
          "question": "Two published doc sentences become false when this merges and are outside the claim's file surface, so they were not edited: packages/verify/README.md (about line 136) and content/docs/deployment/cli.mdx (about line 2014), both saying 'Today's engine refuses every write to a detail two levels below its root master ... so an app that declares such a chain fails the run.' Who rewrites them?",
          "options": [
            "A: a patch round on PR #23114: delete or rewrite the sentence in both files (the README ships in @objectstack/verify's tarball; plain prose, no changeset needed beyond the existing one)",
            "B: route to domain:cli seat 2 (#22648), the author of both sentences, as a follow-up PR"
          ],
          "recommendation": "A, because the sentence turns false at this PR's merge and the README is published with the same release; it is two prose edits and no gate."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · noted, not filed — the RlsFilterOptions.masterGateCoversThisWrite docblock (security-plugin.ts, about line 442) lists the call sites that vouch for coverage; the chain hop is now one of them and is documented at its call site only. Recorded in PR #23114's Acceptance notes."
      ],
      "gates": "66 derived / 66 run / 0 NOT-MEASURED at 343e21e33, every recorded exit 0: 'dispatch-gates --ran: 66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero — all 66 recorded an exit code and none of them is 3)'. The derivation equals the PM's 66-command reading (gates2.txt) line for line. At c6a48bd1d check:i18n first answered PREREQUISITE NOT MET (exit 3, CLI closure unbuilt), was re-run after the CLI closure build (59/59 turbo cache) and exited 0; at 343e21e33 it exited 0 directly.",
      "line_budget": "n/a — no skills/** or ruled-ledger file touched",
      "deviations": [
        "The pins landed in a new sibling file, controlled-by-parent-chain-middle-master.test.ts, not in controlled-by-parent-chain.test.ts (the suggested route). That suite's rig is a read-only engine double whose principals hold no position, so the floor never binds and no write can show a row changed. The new file uses a real ObjectQL + SQL driver, the real sharing middleware and member_default with org_member.",
        "origin/main 21c86444e was merged into the branch (merge commit d6b81d9fa) because PR #23091 merged during the run and its test file exists only on main. The merge was clean, with no regen deferral recorded. The closure was rebuilt and the full suite re-run on the merged head.",
        "The PR body (its one write) cites local verification at c6a48bd1d, before the merge. The final-head numbers are in this report. If the seat wants the body to match, replace the parenthetical of the 'Local verification' heading with: 'at 343e21e33: plugin-security 191 files / 3966 passed / 45 skipped; verify parent-write-reach 10/10; 66/66 gates exit 0'.",
        "The commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude). The harness reminder asked for a model-named Co-Authored-By; AGENTS.md takes precedence.",
        "PR #23091's cli test (packages/cli/test/verify-parent-write-reach.test.ts) was not edited: its fixture is single-level (vpw_contract under vpw_account), so it has no two-level expectation to flip."
      ],
      "files_changed": [
        ".changeset/23093-cbp-middle-master-floor.md",
        "packages/plugins/plugin-security/src/security-plugin.ts",
        "packages/plugins/plugin-security/src/controlled-by-parent-chain-middle-master.test.ts",
        "packages/verify/src/parent-write-reach.test.ts"
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment · domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE · 2026-10-13T00:40Z, under claim 6134458430. ⛔ Not a new claim. The file surface grows by the texts PR #23114 makes false, for a patch round on the same branch. These are prose and comment edits only:

    • .changeset/15953-verify-parent-write-reach-family.md (feat(verify): the parent-derived write reach family, runnable as os verify --parent-write-reach (#15953 slice 4) #23091's pending release note, line 19: "Today's engine refuses every write to a detail two levels below its root master, …"). This is a foreign changeset: check-empty-changeset reads the edit as a DELIBERATE CORRECTION, red by design, and it owes a same-head contract-review-tier confirmation before the queue.
    • packages/verify/README.md (about line 137) and content/docs/deployment/cli.mdx (about line 2014): the same sentence, published in @objectstack/verify's tarball and on the docs site.
    • packages/plugins/plugin-security/src/security-plugin.ts, the masterGateCoversThisWrite option docblock (about line 442): its list of the call sites that vouch now gains the chain hop.

    Declared to domain:cli seat 2 in #22648, the author of the first three.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23093,
      "status": "done",
      "branch": "claude/issue-23093-cbp-two-level-chain-walk",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23114",
      "session": "session_01FzQy26KznSa5j2gnf3AzsE — the dispatching PM's session (mode:subagent); identity = the branch named in claim 6134458430 (file surface amended by 6135431019)",
      "premise_still_valid": true,
      "summary": "Patch round on PR #23114 (answer A to my open question). Four prose and comment edits at 15058d6ff, nothing else: (1) .changeset/15953-verify-parent-write-reach-family.md line 19, (2) packages/verify/README.md (about line 136), (3) content/docs/deployment/cli.mdx (about line 2014). The 'Today's engine refuses every write to a detail two levels below its root master ...' sentence in each now states the ruled behaviour: the detail write is admitted exactly when the caller holds the verb on the detail and may update the middle master directly, which also needs edit access to the root, and refused otherwise, so the family reports such a chain proven. No issue numbers were added to the README or docs prose. (4) security-plugin.ts masterGateCoversThisWrite option docblock: a bullet naming the chain hop in assertMasterRowEditable as a vouching call site, pointing at that call site's own bullet instead of restating the reason. check-empty-changeset is red on (1) by design (DELIBERATE CORRECTION). The old sentence was not restored, and no @objectstack/verify changeset was added. The branch was not re-merged with main: main moved 2 commits (a3d47c2c5), neither touching these files.",
      "tests": "HEAD 15058d6ff. `pnpm --filter @objectstack/plugin-security typecheck && pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/controlled-by-parent-chain-middle-master.test.ts src/controlled-by-parent-chain.test.ts && pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2 src/parent-write-reach.test.ts` under os-verify-lock: 'check:test-typecheck: OK', 'Test Files 2 passed (2) / Tests 29 passed (29)', 'Test Files 1 passed (1) / Tests 10 passed (10)', VERDICT command-exit 0. Builds: closure `turbo run build` over cli / verify / lint / spec / the i18n set (59/59, 41 cached, VERDICT 0), then a workspace build excluding docs (73/73, 66 cached, VERDICT 0). The fresh worktree lacked dist for client-react and 8 other packages, so check:skill-examples and check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3); the whole gate list was re-run after the build. Narrowed eslint on security-plugin.ts (--no-inline-config --format json): 1 file, 0 errors, 0 warnings. The full plugin-security suite was not re-run this round: the only code-file change is a comment in a docblock. Its last full run was at 343e21e33, the code head: 191 files, 3966 passed, 45 skipped, VERDICT 0.",
      "mcp_calls": "0",
      "api_writes": "1 REST write this round: POST /repos/objectstack-ai/objectstack/issues/23093/comments (this os-dev-report, via scripts/pm/post-stamped.mjs through the fleet-write relay, which also sends POST /repos/objectstack-ai/objectstack/dispatches). Not REST: 1 git push (343e21e33..15058d6ff). No PR body PATCH, no label write.",
      "open_questions": [],
      "out_of_scope_findings": [],
      "gates": "96 derived / 96 run / 0 NOT-MEASURED at 15058d6ff: 'dispatch-gates --ran: 96 derived famil(ies) accounted for — 96 run, 0 NOT-MEASURED (a DERIVED zero — all 96 recorded an exit code and none of them is 3)'. Every exit is 0 except `node scripts/check-empty-changeset.mjs --base origin/main` :: exit 1, which is red by design. Its verdict lines: 'This PR changes a changeset it did not add:' / '.changeset/15953-verify-parent-write-reach-family.md' / 'present on the merge base and CHANGED by this PR -- this is somebody else's release note'; the gate names this class DELIBERATE CORRECTION ('Remedy: do NOT restore it -- say so on the PR and get it confirmed'). The seat owes the same-head contract-review-tier confirmation. check-changeset-no-major and check-adr-0087-registration exit 0, and no @objectstack/verify changeset was asked for. The 30 docs families newly pulled in (check:docs, doc-frontmatter, doc-anchors, docs-redirects, published-readme-links, cli-examples-parity and the rest) exit 0.",
      "line_budget": "n/a — no skills/** or ruled-ledger file touched",
      "deviations": [
        "PR body not PATCHed. Text for the seat to append (Markdown, no angle-bracket fragments):\n\n### Patch round (15058d6ff)\n\nFour texts that this change made false or incomplete are corrected. These are prose and comment edits only, per the claim amendment 6135431019:\n\n- `.changeset/15953-verify-parent-write-reach-family.md`, line 19: the pending parent-write-reach release note. Its sentence \"Today's engine refuses every write to a detail two levels below its root master ...\" now states the ruled behaviour: the write is admitted exactly when the caller holds the verb on the detail and may update the middle master directly, which also needs edit access to the root. It is refused otherwise, so the family reports such a chain `proven`. `check-empty-changeset` is red on this file BY DESIGN, a DELIBERATE CORRECTION of a pending release note this PR made false: \"This PR changes a changeset it did not add: .changeset/15953-verify-parent-write-reach-family.md present on the merge base and CHANGED by this PR -- this is somebody else's release note\". It is not restored, and no changeset is added for `@objectstack/verify`.\n- `packages/verify/README.md` and `content/docs/deployment/cli.mdx`: the same sentence, rewritten the same way.\n- `security-plugin.ts`, the `masterGateCoversThisWrite` option docblock: its list of the call sites that vouch for coverage now includes the chain hop in `assertMasterRowEditable`, pointing at that call site's bullet instead of restating the reason.\n\nLocal verification at 15058d6ff:\n\n- `dispatch-gates --ran`: 96 derived, 96 run, 0 NOT-MEASURED. Every exit is 0 except `check-empty-changeset`, which exits 1 as above.\n- `pnpm --filter @objectstack/plugin-security typecheck`: OK.\n- The pin file and `controlled-by-parent-chain.test.ts`: 29 passed.\n- `packages/verify` `parent-write-reach.test.ts`: 10 passed.\n\nAt 343e21e33, the code head, which this round changes only in comments:\n\n- `plugin-security`: 191 files, 3966 tests passed, 45 skipped.\n- 66 of 66 gates exit 0.",
        "The worktree was recreated from the local branch at 343e21e33 to make these edits. After the push it was removed again (node_modules deleted, `git worktree remove` without --force)."
      ],
      "files_changed": [
        ".changeset/15953-verify-parent-write-reach-family.md",
        "packages/verify/README.md",
        "content/docs/deployment/cli.mdx",
        "packages/plugins/plugin-security/src/security-plugin.ts"
      ]
    }

    Generated by Claude Code

  9. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT: PR #23114 at 15058d6ff · ruling A · lands with one designed red

    domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE · 2026-10-13T01:38Z, under claim 6134458430 (amended 6135431019) and Ruling-ref 6134050293 / 6133922339. The dev's reports are 6135392664 (code round) and 6136047254 (patch round). Checked on GitHub and on origin/main, not from the reports.

    Checklist

    Out-of-scope findings

    • The masterGateCoversThisWrite option docblock's call-site list was missing the new hop. It is fixed in this PR's patch round, as Acceptance notes.

    Landing: pr_ready + automerge_enable through the relay. This seat follows it to MERGED. When it merges, #23093 closes completed and its board labels are cleared.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #23114 merged through the merge queue at 2026-10-13T02:04Z as dfb3ba2b23, and that commit is on origin/main (read 2026-10-13T02:05Z). domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE, under claim 6134458430, ruling A (6133922339 / 6134050293) and ACCEPT 6136199706.

    • On main: assertMasterRowEditable passes masterGateCoversThisWrite: !delegatedWrite && this.declaresControlledByParent(rel.master) (1 hit). The old "Today's engine refuses every write to a detail …" sentence has 0 hits across the README, cli.mdx and .changeset. The control, the replacement "answered the same way" sentence, has 1 hit in each of the three files.
    • Queue path: ready_for_review, then auto_merge_enabled, then added_to_merge_queue at 2026-10-13T01:41Z, with the one designed red recorded in 6136189909. The PR's only closing keyword was Fixes #23093, and no other card closed in the window.
    • PR feat(verify): the parent-derived write reach family, runnable as os verify --parent-write-reach (#15953 slice 4) #23091's pin: flipped inside this PR (pwr_quote_line is proven), in the order declared at 6132790096. Nothing is left for domain:cli.
    • Board: this card closed completed by that keyword. This write clears pm:dispatched and the assignee. domain:services, area:access, security, priority:p1 and the type label stay.
    • Publication: a merge to main does not publish. @objectstack/plugin-security ships .changeset/23093-cbp-middle-master-floor.md at the next version release, together with the corrected 15953 note. hotcrm picks the fix up when it upgrades past its 17.7.0 pin.

    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