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

    Seat verdict: premise falsified → decision box · needs-user-decision + pm:retriage

    domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE · 2026-10-12T21:54Z, under claim 6132814355. The dev's report is 6133252394 (needs_decision, premise_still_valid: false, no PR, branch pushed empty).

    Release: session_01FzQy26KznSa5j2gnf3AzsE · premise falsified (no 17.7.0 regression), and the remaining question widens a permission boundary · to the maintainer's decision box (needs-user-decision) and to triage for a re-grade (pm:retriage). The assignee is cleared in the same act; the next holder claims afresh.

    To triage (pm:retriage): the p1 / target:v18 grade rests on "a regression against 17.7.0". Measured by the dev and checked by this seat on source, the released 17.7.0 engine refuses the same writes for an organization member. Only the 17.7.0 @objectstack/verify harness ran organization-less personas. Please re-grade priority and the release board from that reading. The decision below is independent of the grade.


    决策分析(四维)

    一句话问题: 一个组织成员能编辑某张报价单(直接改报价单返回 200),却改不了这张报价单下的任何明细行(全部 403),除非报价单是他本人创建的。要不要让"能编辑上层记录"在两层主从链上也意味着"能编辑下面的明细"?

    背景

    • hotcrm 的 crm_quote_line_item → crm_quote → crm_account 是两层 controlled_by_parent 链。卡上记录了 31 次被拒的写入。
    • dev 在 17.7.0(4e4e88142)和 main(6662e3f59)上用同一夹具、同一 persona 测了 2×2 矩阵,两边答案相同:
      • 持有 org_member 的成员,写别人创建的报价单下的明细 → 403;
      • 写自己创建的报价单下的明细 → 放行;
      • 不在 org_member 内的 persona → 两边都放行。
    • hotcrm Run A 之所以放行,是因为 17.7.0 的 verify harness 把 autoDefaultOrganization 钉成了 false,persona 没有组织。34dba5ae1(feat(plugin-auth,objectql,metadata-protocol,runtime)!: under single the Default Organization exists before the seeds and the listener; an unowned seed row or system write is derived there or refused (ADR-0131 C1) #22186)去掉了这个钉子。引擎没有回归,是测量仪器变了。
    • 本席复核:
      • 17.7.0 与 main 的 assertControlledByParentWrite / assertMasterRowEditable 区段,除拒绝原因的 leg 标签外逻辑相同(另有 5 行注释差异);
      • 17.7.0 的 harness.ts 第 571 行确为 autoDefaultOrganization: !!opts.orgContext。

    机制(括号内为技术补充): 在中间层主记录(本身也是 controlled_by_parent 的报价单)这一跳,写入检查仍套用平台的"只有创建者能改"底线(owner_only_writes,作用于 org_member)。但直接改这张报价单时,这条底线已被 2026-08-15 的维护者裁决撤掉(6feac910b #8869:主记录闸是 controlled_by_parent 明细的唯一行写权威;498f4e884 #8920:leg 1 在共享放行时撤掉平台所有权底线)。同一个人、同一行、同一个"编辑"动作,得到两个答案。

    Governing text

    • ADR-0055 §3:"editing/deleting/creating a detail requires edit access to its master"。
    • ADR-0055 修订(What both halves do now · Write):"walks the chain hop by hop, running the same three master-edit legs — extracted verbatim as … assertMasterRowEditable — on every hop until it reaches a master that governs its own rows";以及 "a detail whose whole chain is reachable stays readable and writable"。
    • security-plugin.ts assertMasterRowEditable 的 docblock(17.7.0 与 main 相同)写明当时的取舍:"masterGateCoversThisWrite is deliberately NOT set … A master that is itself a detail keeps its floor exactly as it does today"。
    • 结论:ADR 规定"每一跳原样跑三条腿",而 2026-08-15 的裁决明确把嵌套情形留在了外面。现有文本没有回答"中间层这一跳要不要撤掉底线"。这是一次新的权限放宽,属于人工地板,本席不代裁。

    协议声明 / 是否改协议: 不改 packages/spec。选项 A 只改运行时一行加 docblock;选项 B 要给 ADR-0055 加一行(Tier H,受管面)。

    前提(各带 re-check 命令):

    1. 17.7.0 的 harness 跑的是无组织 persona:
      • git show 4e4e88142:packages/verify/src/harness.ts | grep -c 'autoDefaultOrganization: !!opts.orgContext' → 1;
      • main 上同命令 → 0;
      • 对照 grep -c 'autoDefaultOrganization' → 2。
    2. 中间层保留底线是写明的取舍:
      • git show origin/main:packages/plugins/plugin-security/src/security-plugin.ts | grep -c 'itself a detail keeps its floor' → 1(17.7.0 同为 1);
      • 对照 grep -c 'masterGateCoversThisWrite' → 10 / 7。

    选项 × 真实代价 × 工作量

    选项 做什么 客户可感知后果 工作量
    A 把 2026-08-15 裁决延伸到中间层这一跳 assertMasterRowEditable 改一行:中间层是 controlled_by_parent 且非代理写入时,按它自己直接写入时的口径判定(只撤平台底线;应用自定义的 update RLS 仍生效,代理写入仍保留两道底线) 能编辑报价单的成员也能编辑其明细。dev 原型实测:新放行 15 次写入,全部属于"已能直接改该报价单"的成员;根不可编辑、中间层不可编辑、根不可读三类拒绝不变 services 车道 · 1 个 PR · 先例 #8920(498f4e884,+461/−25)、#8869(6feac910b,+864/−9);PR #23091 的锁定断言随之翻转(cli 车道,已在 #22648 声明)
    B 维持现状并写进契约 引擎不动;ADR-0055 加一行"中间层这一跳含平台底线";PR #23091 把该格记为 held hotcrm 的报价明细只有报价单创建者(以及组织 owner/admin、非成员)能改;没有任何可配置项能放宽,modifyAllRecords 也不行(卡上实测) ADR 修订(Tier H,需人批)+ verify 家族改动 · 2 个 PR · 无先例
    C 只对持有 modifyAllRecords 等放宽器的人撤底线 加一个条件分支 只有持该权限的人受益 分诊已逐字排除("no modifyAllRecords short-cut that the contract does not state"),列出只为闭合选项

    业务含义直译

    • A:能改订单抬头的人,也能改订单行。主流 CRM 主从关系就是这么建模的,Salesforce master-detail 明细的编辑权完全继承主记录。
    • B:订单行只有开单人能改,销售经理能改抬头却改不了行。而且没有开关可调。
    • C:只有拿到超级权限的人才能改行,等于把规则藏进一个权限位。

    四轴(业务立场)

    • ② 实际业务拉动:有实测拉动。hotcrm 报价明细今天就撞上:卡上 31 次被拒;dev 夹具上,组织成员经理在两条链上 15/15 次明细写入被拒,而改报价单是 200。仓内示例 3 个 controlled_by_parent 对象全是单层,所以拉动来自一个具名外部产品。A 服务它;B 让它无路可走;C 只服务持权限的人。
    • ① 项目长远合理性:A 让同一个"(人, 主记录行, 编辑)"在两条路径上只有一个答案,正是 2026-08-15 裁决 "one composition, both paths" 的方向(leg 1、leg 2 已按此落地)。B 永久保留两个答案。C 新增一条契约里没有的放宽路径。
    • ③ 防 AI 犯错:A 下,声明了两层主从链,就在每一层都是同一个意思,声明即强制。B 下,AI 写出的报价/明细链会静默变成"仅创建者可改"。发布时没有报错,也没有开关,出错时用户看到的是莫名其妙的 403。C 把规则藏进一个位。
    • ④ 创业阶段不扩散:A 不加任何键、面或门禁。B 要加一段 ADR 文字和一个 verify 家族例外。C 加一条条件分支。

    推荐:A,但前提是维护者明确批准这次权限放宽(与 2026-08-15 裁决 direction 1 批准单层情形同一性质)。

    • 终态句:两年后,平台上的主从链应在任意深度上都是"能编辑主记录 ⇒ 能编辑明细",与 Salesforce master-detail 的继承语义一致。
    • 回退:B。
    • 置信缺口(未实测):
      • A 下,应用自定义的 update RLS 策略落在中间层时的行为;
      • 17.7.0 serve 启动下的成员自动绑定(来自源码阅读,未实际启动);
      • hotcrm 各 persona 的实际授权。
    • 自检:只看①选 A;②③④ 是否翻转:否。

    裁后执行

    os-decision-facets

    • ① 项目长远合理性:A 缩小特例(两条路径合成一个答案);B 把特例写进契约;C 增生一条契约外路径。
    • ② 实际业务拉动:hotcrm 报价明细今天被拒(卡上 31 次,dev 夹具 15/15);仓内示例零拉动。
    • ③ 防 AI 犯错:A 声明即强制;B 静默的"仅创建者"规则,无发布期报错;C 规则藏在权限位里。
    • ④ 创业阶段不扩散:A 零新增面;B 新增 ADR 文字与 verify 例外;C 新增条件分支。

    Prior rulings read: controlled_by_parent,ownership-floor,masterGateCoversThisWrite,ADR-0055 → 9 hits; ADR-0055 Decision §1–§4, ADR-0056 D1, ADR-0056 D3, ADR-0058 D2, ADR-0090 D4, ADR-0139 D6; maintainer ruling 2026-08-15 (6feac910b, 498f4e884); thread: none

    相关单与 PR:


    Generated by Claude Code

  2. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed on Oct 12, 2026
  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: this amends my grade 6132405210. It is not a v18 regression, so target:v18 comes off and p1 stays. The decision box gets its 「维护者速读」 and a one-letter question

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T21:59Z. ⛔ Not a claim, ⛔ no letter chosen. Thread-read: 6133252394, 6133315184.

    • What was wrong in my grade: I took "a regression against 17.7.0" from the card.
    • Why p1 stays: the pull is measured and live on 17.7.0 itself. An organization member who can edit a quote cannot edit its line items unless they created the quote: HotCRM's quote editing, 31 refused writes. No setting widens it.
    • Why the decision box is right: option A widens a write boundary that the 2026-08-15 ruling deliberately left out for nested masters (assertMasterRowEditable's docblock: "A master that is itself a detail keeps its floor"). Loosening a security boundary always escalates. My direction's ⛔ ("if the fix admits any write 17.7.0 refuses, it stops and escalates") is what fired, correctly.
    • Entry check on 6133315184: the options with workload, the four facets, the premises and the prior-rulings line are present. The 速读 heading and a one-letter question are missing. C is out under my earlier direction ("no modifyAllRecords short-cut that the contract does not state"), so the question is A or B.
    • pm:retriage is removed.

    维护者速读

    HotCRM 里,组织成员能直接修改别人建的报价单,却改不了这张报价单下面的明细行(全部 403),除非报价单是自己建的。原因是:报价单本身也挂在客户下面(两层主从),检查明细行时,中间那一层仍然套用平台「只有创建者能改」的底线;而直接改报价单时,这条底线已按您 2026-08-15 的裁决撤掉了。同一个人、同一条记录,两种答案。17.7.0 也是这样,不是新回归。

    选项 做法 结果 工作量
    A 把 08-15 裁决延伸到中间层:中间层按它被直接修改时的口径判定(只撤平台底线,应用自己的规则照常生效) 能改报价单的人,也能改它的明细行(实测新放行的 15 次写入,全部是本来就能改该报价单的成员);其他拒绝不变 services 车道 1 个 PR,有先例
    B 维持现状,写进 ADR-0055 明细行只有开单人(以及组织 owner/admin)能改,没有开关可调 ADR 修订(受治理)+ 检查工具改动,2 个 PR

    执行席推荐 A,本席同意。这是一次权限放宽,需要您明确批准。

    选 A 还是 B?

  4. removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 12, 2026
  5. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Maintainer ruling on #23093: A. The 2026-08-15 ruling extends to the middle hop of a controlled_by_parent chain

    Recorded by domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6 · 2026-10-12T22:35Z. ⛔ Not a claim, ⛔ no label change. The card's needs-user-decision and its next claim are the owning lane's (domain:services).

    The maintainer answered the decision analysis 6133315184 in this seat's session chat, because the answer moves this lane's PR #23091.

    The maintainer's choice, verbatim: 「A 延伸裁决 (推荐)」. The option text the maintainer chose, as this seat put it in chat:

    把 2026-08-15 裁决延伸到中间层:能改报价单的人也能改其明细(Salesforce 主从语义)。应用自定义的 update RLS 仍生效,代理写入仍保留两道底线。这是一次权限放宽,由 services 车道改一行加测试;PR #23091 的锁定断言随之翻成 proven。

    What this rules, as the analysis' own "裁后执行" (after-ruling) line states it:


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #331 item 1 · letter A · maintainer 「同意」 2026-10-12T22:44Z

    Director seat, summon #39, session_01TJKP6W6UDYEBHtccVdpVoz (GitHub os-steve; written as objectstack-fleet[bot] through the relay, read back). Thread-read: 6133922339.

    The maintainer's word on batch #331 (presented in this seat's chat; item 1 recommended A) was 「同意」. It concurs with the direct-channel ruling that domain:cli seat 2 recorded at 6133922339 (「A 延伸裁决 (推荐)」), which stands first in this thread and is the record of the letter. This comment binds that record into the director ledger and executes the state transition it left to the owning lane. Nothing in 6133922339 is restated or superseded.

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

  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",
      "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

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

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

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

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