You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
controlled_by_parent: per-object opt-in so read on the master is enough to insert and update the detail under the detail's own grants (ownerMayEdit covers owner update only; HotCRM Order to Cash blocked) #33250
A per-object opt-in under sharing.controlledByParent (name is the platform's call, e.g. masterReadSuffices: true) that changes the detail write gate from "EDIT on the master" to "READ on the master". The insert or update is then judged by the detail object's own grants (object CRUD, FLS, its row-level security), exactly as an unrelated object would be. Default stays as today (ADR-0055 §3: master EDIT required), so no existing app changes behaviour.
HotCRM Order to Cash (hotcrm#2315, draft PR hotcrm#2418) hangs detail objects off a contract: a collection plan with plan steps, invoices, payments. The people who write those rows do not edit the contract, by design:
sales_rep reads the contract and must add collection-plan steps: insert answers 403 (master not editable).
ar_accountant reads the contract and must record an invoice: insert answers 403, even with Modify All on the invoice object.
sharing.controlledByParent.ownerMayEdit (ADR-0055 §3 amendment) does not cover this. It lets the owner of an existing detail row update it. It does not admit an insert, and it does not admit a non-owner who holds the detail grant.
The workarounds are each worse: dropping controlled_by_parent loses read inheritance from the contract; granting reps and finance EDIT on contracts hands them the commercial terms.
Acceptance
Spec: the opt-in parses on an object that declares controlled_by_parent; lint refuses it elsewhere (as ownerMayEdit does).
With the opt-in, a principal who reads the master and holds create on the detail inserts a detail row; one who holds update on the detail updates it. Without read on the master, both still answer 403.
Without the opt-in, behaviour is byte-identical to 18.4.0 (existing controlled_by_parent tests unchanged).
The master check member on the security service answers the opt-in the same way the write path does.
Docs and the authoring skills describe when to use it (detail rows written by people who only read the master: plans, invoices, payments, activity-like rows).
Ruling
Irene, 2026-11-14T14:45Z, O2C phase 1a Q1 = A: the platform takes this, priority p1; hotcrm changes nothing and hotcrm#2418 is Blocked-by this card.
Area: A2 for the server half (the detail write gate in plugin-security), leased to domain:server#5 in the pilot
Grade p1: ruled on the card (Irene, 14:45Z, O2C phase 1a Q1 = A: the platform takes this, p1). HotCRM's Order to Cash is blocked: reps and AR cannot insert detail rows they must own (hotcrm#2418 is Blocked-by this card).
Lane: a new authorable key under sharing.controlledByParent is a spec contract widening (Clause-②: yes), plus its lint, so domain:spec leads. The write-gate change is plugin-security (server, pilot area A2). Either one PR carries both, written by spec and checked by A2, or spec lands the key and A2 lands the gate right after. domain:spec#1 and domain:server#5 agree which.
Permission boundary: this is an opt-in the app author declares. With it, a principal who only reads the master can insert or update detail rows under the detail's own grants (CRUD, FLS, RLS). No outside or lower-privileged actor gains a grant the author did not declare, so it is graded as a feature, not a security finding. Acceptance 2 and 3 (no master read → 403; no opt-in → byte-identical) are the pins that keep it so.
Seat: not dispatched — the ask re-opens a recorded maintainer ruling → needs-user-decision · 2026-11-14T15:54Z
⛔ Not a claim. The lane takes this card as soon as the door is ruled.
What the card asks: a per-object opt-in under sharing.controlledByParent with which read on the master suffices and the detail's own grants (CRUD, FLS, RLS) decide insert and update for everyone.
So: this card is that rejected option 2 (as an opt-in), plus the insert widening the ADR says must be ruled on its own evidence. The ruling quoted in the body (O2C phase 1a Q1 = A, 14:45Z) decides that the platform takes HotCRM's need at p1; it does not cite or lift 6398566164. The evidence here is new (inserts by a rep who does not own the contract; an AR accountant who is neither owner nor master editor), so the question is legitimately open again, but it is a write-gate (permission boundary) door and an ADR-0055 amendment, both the maintainer's.
Options to rule (the seat recommends after the evaluation; four-axis analysis follows on this card):
A — as filed: the opt-in admits insert and update by the detail's own grants with master read (option 2 by opt-in). Covers both measured roles.
B — narrower: extend the existing ownerMayEdit leg to an insert whose owner_id is the caller (with master read), keeping update owner-only. Covers the rep's plan steps and the accountant's own invoices; does not admit a non-owner update (e.g. Modify All on the invoice).
C — no platform change: HotCRM models these rows outside controlled_by_parent (lookup + its own RLS).
State:pm:queue → needs-user-decision. hotcrm#2418 stays Blocked-by this card. Whichever door is ruled, the spec seat dispatches one PR carrying the key, its parse and lint refusal, and the gate leg (D5: a key lands with its first reader), with domain:server#5 (pilot area A2) checking the plugin-security half, then the ADR-0055 amendment as its own domain:skills card.
③ 防 AI 写错。 B 留下一类很隐蔽的错:AI 写的应用用单个用户测试时全部通过(创建者就是所有者),到真实团队里同事一改就 403,而且权限集上写着的 Modify All 不兑现。A 不含删除时,AI 声明了删除授权加开关,删除仍被拒,同样是「声明了、运行时不兑现」。A+ 的规则是一句话:开了开关,主记录只问读,其余全看子对象自己的授权。出错时看到的是响亮的 403,不是静默放行。A+ 的风险是 AI 为了让 403 消失随手打开开关;约束在于它是显式声明、键是闭合的、放错对象解析即拒,写权限仍以子对象上逐权限集声明的增改删授权为上限。
写门:对声明了开关的子对象,凡是今天问「主记录可编辑」的地方(按 id 新增修改删除、批量新增、按条件写、链上每一跳、把行挪到另一个主记录下)统一改问「主记录可读」,读的答案取自读门本身。子对象自己的对象级授权、字段权限、行级策略照旧先判。未声明的对象逐字节不变。安全服务的 checkControlledByParentWrite 给出同样答案。
What it rules: a per-object opt-in under sharing.controlledByParent; with it, the master question in the write gate is read instead of edit for insert, update and delete; the detail's own object grants, FLS and RLS decide as before; an object that does not declare it is byte-identical. For opted-in objects it lifts the 2026-10-26 amendment's "Option 2 … was not chosen" line of ruling 6398566164.
Left to the seat (not ruled by the tap): the key name; the paths the swap covers (batch insert, predicate writes, chain hops, re-point); its relation to ownerMayEdit; the lint arm. The dispatch order carries them as seat calls under the veto window.
Next: the spec seat dispatches one PR (key + parse refusal + gate + security-service member + docs, D5), contract review before enqueue; the ADR-0055 amendment and the skills/** teaching are domain:skills carriers filed at landing. domain:server#5 (pilot area A2) is told and checks the plugin-security half.
Claim: PM loop round 1 · 2026-11-14T16:25Z
Session: session_01FEkQ5mswnWm181XgYcCUag (seat); card thread session_01FEkQ5mmtu9Aih9i3fVmmve is the dev
Account: os-project-manager
Branch: claude/project-thread-gkpv0o (the card thread's designated branch; the platform lets a thread push only that branch, so not claude/issue-33250-*)
Worktree: objectstack-issue-33250
Domain: domain:spec
Seat: domain:spec#1
File surface: packages/spec/src/data/object.zod.ts (+ test, object.form.ts, regenerated schema/references and packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts), packages/spec/src/contracts/security-service.ts; packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts, security-plugin-cbp-relation.ts, platform-ownership-policies.ts, explain-engine.ts and the wiring that implements checkControlledByParentWrite, with tests; packages/lint/src/validate-security-posture.ts, rule-explanations.ts and tests; packages/metadata-core/src/object-schema-fls-references.ts only if it enumerates the controlledByParent keys; hand-written docs of the controlled_by_parent write rule under content/docs/**; ui/packages/app-shell/src/views/metadata-admin/metadata-form-zh-cn.ts only if the form needs the label; root .changeset/. ⛔ docs/adr/** and skills/** are carriers (stop on breach; explain in the report)
Container & model: L, mode:thread, model: default tier (Clause-② widening built at the judgment tier; contract review at CONTRACT_REVIEW_TIER before enqueue, MANDATORY path: Clause-② yes), effort: high
Clause-②: yes
Metadata-protocol: added
Responsibility: n/a — not a defect card (a capability the maintainer ruled; the gate's own code in plugin-security produces today's 403, objectstack)
Thread-read: 6880588685
Premise: holds at origin/main24f2f401: sharing.controlledByParent declares only ownerMayEdit (object.zod.ts), and the gate asks master edit for insert and delete. Ruling-ref: the maintainer (Leehom) chose A+ 「读主记录即可增改删」 at 2026-11-14T16:21:29Z (record 6880588685). Order: /mnt/project-files/pm-orders/spec/order-33250.md (project share). Seat call under the veto window: key writeRequiresMasterRead (precedent: Salesforce's master-detail attribute of the same name and meaning).
Serial constraints cleared: open PRs touching plugin-security are #33303 and #33290 (security-plugin-authz-middleware.ts, security-plugin.test.ts) and #33245 (security-plugin-start-hooks.ts), none on the cbp gate, relation or ownership files; no open PR touches object.zod.ts, security-service.ts or validate-security-posture.ts. If the dev must edit security-plugin.test.ts, it merges main once after #33303 lands. Server pilot area A2 (domain:server#5) checks the plugin-security half on the PR.
domain:spec#1 · os-project-manager · session_01FEkQ5mmtu9Aih9i3fVmmve
os-dev-report
{
"issue": 33250,
"status": "done",
"branch": "claude/project-thread-gkpv0o",
"pr": "#33369",
"session": "session_01FEkQ5mmtu9Aih9i3fVmmve",
"premise_still_valid": true,
"summary": "Added sharing.controlledByParent.writeRequiresMasterRead (ruling A+). With it the ADR-0055 §3 write gate asks READ on the master instead of EDIT for insert (single and batch), by-id and predicate update/delete, re-point and emptying, per chain hop (an opted hop asks read and ends the walk; a non-opted hop keeps edit); the detail's own grants, RLS and FLS then decide. checkControlledByParentWrite and os explain agree with the gate; the lint owner-policy arm stands down on an opted detail; ObjectSchema refuses the key off a controlled_by_parent detail and beside ownerMayEdit:true (fix: delete ownerMayEdit). Undeclared objects are unchanged (full plugin-security suite green). Deviation from the order's wording: a by-id update/delete by someone who cannot read the master answers 404 RECORD_NOT_FOUND (read door first, #7505 hidden-row rule), not 403; insert and re-point answer 403; checkControlledByParentWrite gives deny legs for all.",
"tests": "Red on main (b4 gate change): plugin-security controlled-by-parent-owner-may-edit.test.ts 21 failed / 22 passed, delete by a master reader = 403 'requires edit access to its master record (no edit permission on master omd_contract)'. Green on b012326: that file 43/43; explain-controlled-by-parent-write.test.ts 87; spec controlled-by-parent-owner-may-edit.test.ts 26; lint validate-security-posture-owner-may-edit.test.ts 40. Full suites: spec 840 files/26844 tests exit 0; plugin-security 267/6641 (45 skipped) exit 0; lint 208/8890 exit 0; platform-objects 122/1957 exit 0 (two echo-decision leaf counts bumped 113->115, 737->738); app-shell metadata-form-i18n.test.ts 22 exit 0. Typecheck exit 0: spec (check:test-typecheck at --max-old-space-size=8192), plugin-security, lint, platform-objects; app-shell tsc not measured (1821 pre-existing errors from unbuilt ui deps, none in metadata-form-zh-cn.ts). Ablations via scripts/ablation-replace.mjs, each 'mutation landed' + 'restored: blob == HEAD': gate read branch (if (false && hopRel.writeRequiresMasterRead)) -> 21 red incl. both checkControlledByParentWrite pins; chainAsksMasterRead -> false -> 3 explain red; lint 'if (detail.masterReadSuffices) continue;' deleted -> 2 red; off-detail parse refusal off -> 4 red; pair refusal off -> 1 red.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands: 123 families, each run with exit captured before any pipe; --ran reconciliation exit 0: 123 derived, 122 run exit 0, 1 NOT MEASURED (pnpm check:dual-build-cjs-loads exit 3, needs every package dist; CI runs it). Three first-pass prerequisite refusals re-run green: check-plugin-teardown-shape --self-test (exit 3 shallow clone -> fetched pinned fixture 621a487 -> exit 0), spec check:skill-examples (exit 3 -> built client-react -> exit 0), spec check:generated (exit 1 on test-typecheck OOM 'failed without diagnostics' -> heap 8192 -> exit 0). check-i18n-bundles exit 0. CI check-runs on #33369: not yet read (seat holds PR events).",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "5 — issue_read get, issue_read get_comments x2, pull_request_read get_files (precedent #26005), pull_request_read get (#33369 read-back); no write tools",
"api_writes": "3 — git push (3 pushes to claude/project-thread-gkpv0o), relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 43622253252 success), this os-dev-report comment via post-stamped",
"deviations": [
"PR body Verification section was mangled at pr_create (shell expanded backticks in my body script): the 'On head' line, package names and gate names are blank and the i18n gate output was pasted in. Everything above ## Verification is intact (read back). Corrected full body: /mnt/project-files/pm-orders/spec/33369-pr-body-corrected.md; os-dev does not PATCH bodies, so the seat rewrites it per seat-post-protocol.md.",
"Order said refusals 'each answer 403': by-id update/delete without master read answer 404 RECORD_NOT_FOUND (read door precedes the gate; unchanged platform rule). Pinned as such."
],
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: c · reach: exception: maintainer · ADR-0055 §3 needs an amendment recording writeRequiresMasterRead (ruling A+ lifts ruling 6398566164's 'Option 2 not chosen' line for opted-in objects); docs/adr/** is out of this card · carrier: seat",
"class: c · reach: exception: maintainer · skills/** controlled_by_parent authoring guidance should teach writeRequiresMasterRead (PR body carries Skills-docs: follow-up owed) · carrier: seat",
"class: c · reach: contract review · denial legs reused (object_permission / row_level_security) for the read question to avoid widening the closed ControlledByParentWriteDenialLeg union; the seat's contract review should confirm against D10 and the delegation rule implemented (agent and delegator must each read the master) · carrier: seat"
]
}
Activity
objectstack-fleet commentedon Nov 14, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Path: none
Triage: first touch →
domain:spec(lead) · p1 ·pm:queue· 2026-11-14T15:52Z⛔ Not a claim.
Area: A2 for the server half (the detail write gate in plugin-security), leased to
domain:server#5in the pilotBlocked-bythis card).sharing.controlledByParentis a spec contract widening (Clause-②: yes), plus its lint, sodomain:specleads. The write-gate change isplugin-security(server, pilot area A2). Either one PR carries both, written by spec and checked by A2, or spec lands the key and A2 lands the gate right after.domain:spec#1anddomain:server#5agree which.objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag
Seat: not dispatched — the ask re-opens a recorded maintainer ruling →
needs-user-decision· 2026-11-14T15:54Z⛔ Not a claim. The lane takes this card as soon as the door is ruled.
sharing.controlledByParentwith which read on the master suffices and the detail's own grants (CRUD, FLS, RLS) decide insert and update for everyone.docs/adr/0055-master-detail-controlled-by-parent.md, Amendment 2026-10-26 (controlled_by_parent: the owner of a detail row cannot update their own row when they only read the master — no equivalent of the "activity owner always has full access" rule (hotclm obligations today, HotCRM tasks next) #25760), maintainer ruling of record6398566164(「同意」 2026-10-25), verbatim: "Option 2 (writeRequires: 'read', the detail's own object permission and RLS deciding the write for everyone) was not chosen: it opens the write to everyone the detail's own grants admit, a wider door than the need." Same amendment, "What is out of scope": "So the leg does not reach: an insert … Each is a widening of its own, to be ruled on its own evidence; none is implied by this one."6398566164. The evidence here is new (inserts by a rep who does not own the contract; an AR accountant who is neither owner nor master editor), so the question is legitimately open again, but it is a write-gate (permission boundary) door and an ADR-0055 amendment, both the maintainer's.ownerMayEditleg to an insert whoseowner_idis the caller (with master read), keeping update owner-only. Covers the rep's plan steps and the accountant's own invoices; does not admit a non-owner update (e.g. Modify All on the invoice).controlled_by_parent(lookup + its own RLS).pm:queue→needs-user-decision. hotcrm#2418 staysBlocked-bythis card. Whichever door is ruled, the spec seat dispatches one PR carrying the key, its parse and lint refusal, and the gate leg (D5: a key lands with its first reader), withdomain:server#5(pilot area A2) checking theplugin-securityhalf, then the ADR-0055 amendment as its owndomain:skillscard.objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag
#33250 决策分析:主从子记录的写权限开关开多大
写于 2026-11-14,Fable 评估线程。只读评估,未改任何仓库。读数基线:objectstack
main47a3b95,hotcrm#2418 头 024d1ff(@objectstack/*18.4.0)。一句话问题
销售和应收会计能看合同、按设计不能改合同,但他们要在合同下面建回款节点、录发票和回款。今天平台一律回答 403,HotCRM「订单与回款」模块因此卡住(hotcrm#2418 Blocked-by 本卡,Irene 2026-11-14 14:45Z 定为 p1)。
Governing text
docs/adr/0055-master-detail-controlled-by-parent.md§3:写子记录要求对主记录有编辑权。controlled_by_parent: the owner of a detail row cannot update their own row when they only read the master — no equivalent of the "activity owner always has full access" rule (hotclm obligations today, HotCRM tasks next) #25760,裁决6398566164「同意」):只加了ownerMayEdit(子记录所有者凭主记录读权限按 id 修改自己的行)。原文:「Option 2 (writeRequires: 'read'…) was not chosen: it opens the write to everyone the detail's own grants admit, a wider door than the need.」以及「So the leg does not reach: an insert … Each is a widening of its own, to be ruled on its own evidence; none is implied by this one.」decision-analysis.md〈应用仓推荐序特例〉):阻塞源是平台缺口时不荐应用绕行。是否改协议:是。
sharing.controlledByParent下新增一个可声明的键(Clause-②: yes,widening)。前提与读数(可复核)
na.rep建回款节点 403;fin.ar录发票 403,子对象上的 Modify All 不起作用sales_rep与ar_accountant对crm_contract都是allowEdit: falsegit show pr-2418:src/sales/profiles/sales-rep.profile.ts(第 67 行)、ar-accountant.profile.tssales_rep对crm_billing_milestone是增改删全开;ar_accountant对发票、回款是增改删加 Modify AllmasterGateCoversOperation覆盖 update 与 deletepackages/plugins/plugin-security/src/platform-ownership-policies.tssecurity-plugin-cbp-write-gate.ts第 593 至 598 行的注释writeRequires:2 条(本卡与 #25760);搜ownerMayEdit:19 条,无一条要求放开新增或非所有者修改gh api -X GET search/issues -f q='is:issue repo:objectstack-ai/objectstack writeRequires'未实测、属推断的两点:删除被拒(hotcrm#2418 只实测了新增;由写门代码与 ADR §3 可确定);方案 B 下非所有者修改被拒(由写门代码推断)。
选项、代价与工作量
ownerMayEdit,+1432/−18,34 个文件,2026-10-26 当天开当天合)。另有 ADR-0055 修订与文档技能卡ownerMayEdit延伸到「所有者是自己的新增」业务含义直译
四轴论证
① 项目长远合理性(领起)。 两年后这块能力在平台上应该是两个成熟平台都有的两件东西:一是关系级的档位「写子记录至少要对主记录有读权限还是编辑权限」(Salesforce Master-Detail 字段的 Sharing Setting: Read Only / Read/Write,覆盖增改删);二是「所有者始终能改自己的行」(Salesforce 活动所有者规则,已由
ownerMayEdit落地)。A+ 就是第一件,写门里只是把一个问题换了答案。B 在主流平台上没有对应物,它在写门里再开第三条特例分支(所有者修改、所有者新增、空主记录所有者规则),而且下一个同形模块 PSA(hotcrm#2316,工时与费用挂在项目下)一碰到非所有者修改就得再修订一次 ADR。A 把删除留在外面有一个长期代价:以后再把删除并进同一个键,等于悄悄放宽所有已声明该键的应用;不并就得再加一个键。所以动词范围应当这次一次定清。另外 A+ 的表达力严格大于 B:想要「只有创建者能改」的应用,打开开关再写一条所有者行级策略即可(策略是「与」上去的,会照写的生效);反过来 B 表达不了团队共管。② 实际业务拉动。 拉动是实测的:18.4.0 上两个角色两次 403,一个 p1 模块整体卡住。10-26 那次说方案 2「比需求宽」,当时的需求确实只有所有者修改。今天的需求变了:新增的人不是主记录编辑者,修改的人也不一定是创建者(财务团队、导入、接手)。HotCRM 的权限集本来就给了销售和财务删除权,这也是拉动,只是没有实测 403。
③ 防 AI 写错。 B 留下一类很隐蔽的错:AI 写的应用用单个用户测试时全部通过(创建者就是所有者),到真实团队里同事一改就 403,而且权限集上写着的 Modify All 不兑现。A 不含删除时,AI 声明了删除授权加开关,删除仍被拒,同样是「声明了、运行时不兑现」。A+ 的规则是一句话:开了开关,主记录只问读,其余全看子对象自己的授权。出错时看到的是响亮的 403,不是静默放行。A+ 的风险是 AI 为了让 403 消失随手打开开关;约束在于它是显式声明、键是闭合的、放错对象解析即拒,写权限仍以子对象上逐权限集声明的增改删授权为上限。
④ 创业阶段不扩散。 A+、A、B 都是新增一个键或一处语义。B 若复用
ownerMayEdit会悄悄改变 hotclm 已声明对象的行为,若另起键则以后大概率还要为非所有者修改再加一个。A 以后要补删除也要再动一次。A+ 一个键付清。不新增 lint 门禁,只沿用「放错对象解析即拒」。自检:只看①选 A+;②③④ 是否翻转:否。
推荐
推荐 A+。不取 A:把删除留到以后,等于将来悄悄放宽一个已发布的开关,或者再加一个键。不取 B:它只盖住今天实测的两次新增,盖不住财务团队共管和接手场景,且没有主流对应物。不取 C:是应用绕行平台缺口,并丢掉读继承。
置信缺口,如实列出:
裁后执行(若裁 A+)
sharing.controlledByParent下,闭合形状,非controlled_by_parent对象上声明即解析拒绝。checkControlledByParentWrite给出同样答案。ownerMayEdit时解析拒绝并提示删掉后者(新开关已包含它);现有 lint 提示把新开关也认作让所有者策略生效的声明。不新增 lint 规则。domain:skills卡。四棱块
os-decision-facets
① 项目长远合理性:A+ 缩小特例,一个问题换一个答案,对齐主流平台的关系级档位;B 扩大特例,再开一条所有者分支且下一个模块还要修订。
② 实际业务拉动:今天撞上的是 HotCRM 订单与回款(18.4.0 实测销售与财务新增各 403,p1);删除与非所有者修改由设计授权与写门代码推断,未实测。
③ 防 AI 犯错:A+ 是闭合键加解析拒绝,规则一句话,出错是响亮 403;B 与不含删除的 A 都会留下「声明了授权、运行时不兑现」的形状。
④ 创业阶段不扩散:A+ 一个键付清;A 与 B 都预留了下一次加键或悄悄放宽。不新增门禁。
Prior rulings read: controlled_by_parent / writeRequires / ownerMayEdit → 38 / 2 / 19 hits; ADR-0055 Amendment 2026-10-26(裁决 6398566164,经 ADR 原文与 #25760 正文读到,裁决评论本身未单独打开); thread: none
objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag
Ruled: A+ ·
needs-user-decision→pm:queue· 2026-11-14T16:23Zsharing.controlledByParent; with it, the master question in the write gate is read instead of edit for insert, update and delete; the detail's own object grants, FLS and RLS decide as before; an object that does not declare it is byte-identical. For opted-in objects it lifts the 2026-10-26 amendment's "Option 2 … was not chosen" line of ruling6398566164.ownerMayEdit; the lint arm. The dispatch order carries them as seat calls under the veto window.skills/**teaching aredomain:skillscarriers filed at landing.domain:server#5(pilot area A2) is told and checks theplugin-securityhalf.objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag
Claim: PM loop round 1 · 2026-11-14T16:25Z
Session:
session_01FEkQ5mswnWm181XgYcCUag(seat); card threadsession_01FEkQ5mmtu9Aih9i3fVmmveis the devAccount:
os-project-managerBranch:
claude/project-thread-gkpv0o(the card thread's designated branch; the platform lets a thread push only that branch, so notclaude/issue-33250-*)Worktree:
objectstack-issue-33250Domain:
domain:specSeat:
domain:spec#1File surface:
packages/spec/src/data/object.zod.ts(+ test,object.form.ts, regenerated schema/references andpackages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts),packages/spec/src/contracts/security-service.ts;packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts,security-plugin-cbp-relation.ts,platform-ownership-policies.ts,explain-engine.tsand the wiring that implementscheckControlledByParentWrite, with tests;packages/lint/src/validate-security-posture.ts,rule-explanations.tsand tests;packages/metadata-core/src/object-schema-fls-references.tsonly if it enumerates thecontrolledByParentkeys; hand-written docs of the controlled_by_parent write rule undercontent/docs/**;ui/packages/app-shell/src/views/metadata-admin/metadata-form-zh-cn.tsonly if the form needs the label; root.changeset/. ⛔docs/adr/**andskills/**are carriers (stop on breach; explain in the report)Container & model:
L,mode:thread,model: default tier(Clause-② widening built at the judgment tier; contract review at CONTRACT_REVIEW_TIER before enqueue, MANDATORY path: Clause-② yes),effort: highClause-②: yes
Metadata-protocol: added
Responsibility: n/a — not a defect card (a capability the maintainer ruled; the gate's own code in
plugin-securityproduces today's 403, objectstack)Thread-read: 6880588685
Premise: holds at
origin/main24f2f401:sharing.controlledByParentdeclares onlyownerMayEdit(object.zod.ts), and the gate asks master edit for insert and delete. Ruling-ref: the maintainer (Leehom) chose A+ 「读主记录即可增改删」 at 2026-11-14T16:21:29Z (record 6880588685). Order:/mnt/project-files/pm-orders/spec/order-33250.md(project share). Seat call under the veto window: keywriteRequiresMasterRead(precedent: Salesforce's master-detail attribute of the same name and meaning).Serial constraints cleared: open PRs touching
plugin-securityare #33303 and #33290 (security-plugin-authz-middleware.ts,security-plugin.test.ts) and #33245 (security-plugin-start-hooks.ts), none on the cbp gate, relation or ownership files; no open PR touchesobject.zod.ts,security-service.tsorvalidate-security-posture.ts. If the dev must editsecurity-plugin.test.ts, it merges main once after #33303 lands. Server pilot area A2 (domain:server#5) checks theplugin-securityhalf on the PR.objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mmtu9Aih9i3fVmmve
os-dev-report
{
"issue": 33250,
"status": "done",
"branch": "claude/project-thread-gkpv0o",
"pr": "#33369",
"session": "session_01FEkQ5mmtu9Aih9i3fVmmve",
"premise_still_valid": true,
"summary": "Added sharing.controlledByParent.writeRequiresMasterRead (ruling A+). With it the ADR-0055 §3 write gate asks READ on the master instead of EDIT for insert (single and batch), by-id and predicate update/delete, re-point and emptying, per chain hop (an opted hop asks read and ends the walk; a non-opted hop keeps edit); the detail's own grants, RLS and FLS then decide. checkControlledByParentWrite and os explain agree with the gate; the lint owner-policy arm stands down on an opted detail; ObjectSchema refuses the key off a controlled_by_parent detail and beside ownerMayEdit:true (fix: delete ownerMayEdit). Undeclared objects are unchanged (full plugin-security suite green). Deviation from the order's wording: a by-id update/delete by someone who cannot read the master answers 404 RECORD_NOT_FOUND (read door first, #7505 hidden-row rule), not 403; insert and re-point answer 403; checkControlledByParentWrite gives deny legs for all.",
"tests": "Red on main (b4 gate change): plugin-security controlled-by-parent-owner-may-edit.test.ts 21 failed / 22 passed, delete by a master reader = 403 'requires edit access to its master record (no edit permission on master omd_contract)'. Green on b012326: that file 43/43; explain-controlled-by-parent-write.test.ts 87; spec controlled-by-parent-owner-may-edit.test.ts 26; lint validate-security-posture-owner-may-edit.test.ts 40. Full suites: spec 840 files/26844 tests exit 0; plugin-security 267/6641 (45 skipped) exit 0; lint 208/8890 exit 0; platform-objects 122/1957 exit 0 (two echo-decision leaf counts bumped 113->115, 737->738); app-shell metadata-form-i18n.test.ts 22 exit 0. Typecheck exit 0: spec (check:test-typecheck at --max-old-space-size=8192), plugin-security, lint, platform-objects; app-shell tsc not measured (1821 pre-existing errors from unbuilt ui deps, none in metadata-form-zh-cn.ts). Ablations via scripts/ablation-replace.mjs, each 'mutation landed' + 'restored: blob == HEAD': gate read branch (if (false && hopRel.writeRequiresMasterRead)) -> 21 red incl. both checkControlledByParentWrite pins; chainAsksMasterRead -> false -> 3 explain red; lint 'if (detail.masterReadSuffices) continue;' deleted -> 2 red; off-detail parse refusal off -> 4 red; pair refusal off -> 1 red.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands: 123 families, each run with exit captured before any pipe; --ran reconciliation exit 0: 123 derived, 122 run exit 0, 1 NOT MEASURED (pnpm check:dual-build-cjs-loads exit 3, needs every package dist; CI runs it). Three first-pass prerequisite refusals re-run green: check-plugin-teardown-shape --self-test (exit 3 shallow clone -> fetched pinned fixture 621a487 -> exit 0), spec check:skill-examples (exit 3 -> built client-react -> exit 0), spec check:generated (exit 1 on test-typecheck OOM 'failed without diagnostics' -> heap 8192 -> exit 0). check-i18n-bundles exit 0. CI check-runs on #33369: not yet read (seat holds PR events).",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "5 — issue_read get, issue_read get_comments x2, pull_request_read get_files (precedent #26005), pull_request_read get (#33369 read-back); no write tools",
"api_writes": "3 — git push (3 pushes to claude/project-thread-gkpv0o), relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 43622253252 success), this os-dev-report comment via post-stamped",
"deviations": [
"PR body Verification section was mangled at pr_create (shell expanded backticks in my body script): the 'On head' line, package names and gate names are blank and the i18n gate output was pasted in. Everything above ## Verification is intact (read back). Corrected full body: /mnt/project-files/pm-orders/spec/33369-pr-body-corrected.md; os-dev does not PATCH bodies, so the seat rewrites it per seat-post-protocol.md.",
"Order said refusals 'each answer 403': by-id update/delete without master read answer 404 RECORD_NOT_FOUND (read door precedes the gate; unchanged platform rule). Pinned as such."
],
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: c · reach: exception: maintainer · ADR-0055 §3 needs an amendment recording writeRequiresMasterRead (ruling A+ lifts ruling 6398566164's 'Option 2 not chosen' line for opted-in objects); docs/adr/** is out of this card · carrier: seat",
"class: c · reach: exception: maintainer · skills/** controlled_by_parent authoring guidance should teach writeRequiresMasterRead (PR body carries Skills-docs: follow-up owed) · carrier: seat",
"class: c · reach: contract review · denial legs reused (object_permission / row_level_security) for the read question to avoid widening the closed ControlledByParentWriteDenialLeg union; the seat's contract review should confirm against D10 and the delegation rule implemented (agent and delegator must each read the master) · carrier: seat"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag
ACCEPT → PR #33369 comment 6882030929 · 2026-11-14T18:13Z · ready + auto-merge armed; card closes on merge. Carriers: #33373 (ADR-0055 amendment), #33374 (skills guidance), #33381 (pins and comments).