Found while measuring #52, which deals clm_obligation.owner to real accounts for the first time. ⛔ Not introduced by that card — it applies to any requester-owned obligation, and #52 is simply the first change that makes one exist.
The contract
DESIGN.md §04, permission matrix, verbatim:
| 对象 |
clm_requester |
clm_legal |
clm_finance |
clm_records |
clm_admin |
| clm_obligation |
RU(本人负责) |
RCU |
R |
R |
RCUD |
U(本人负责) — a requester updates the obligations they are accountable for. §05 puts that list in the 我的合同 group every employee reaches (我负责的履约), and F10 reminds the owner to "Record progress, or mark it done once it is performed."
Measured: the U never arrives, for every row that exists
Clean database, pnpm demo on port 3152, README operator setup performed plus the clm_requester grant #11 is about, re-seeded so the upserts hand the rows over. Signed in as Business Requester 3 over REST:
obligation : Submit the quarterly service report | status pending | owner == me: true
its parent : NDA-2026-0009 | status active | owner_id == me: true
POST /api/v1/security/explain {object: clm_obligation, operation: update} -> allowed: true
PATCH /api/v1/data/clm_obligation/OBLIGATION_ID {"status":"in_progress"} -> 403
{"error":"[Security] Access denied: update on 'clm_obligation' requires edit access to
its master record (master 'clm_contract' not editable by this user (row-level security))",
"code":"PERMISSION_DENIED","object":"clm_obligation"}
The account owns the obligation and owns its parent contract, and the object-level explain says yes. The write is still refused.
Why it can never succeed
Two declarations that are each correct alone:
clm_obligation is sharingModel: 'controlled_by_parent' (§04 "五个子对象 controlled_by_parent", ADR-0055) — so update on a child requires edit on the master clm_contract row, whatever the child's own row scope says;
clm_requester's contract edit window is REQUESTER_EDITABLE_STATUSES = ['draft', 'submitted'] (§04 "本人发起,draft/submitted 可改", src/sharing/_lifecycle.ts).
An obligation is a post-signature commitment: plan-children.ts hangs every one of the 200 off a contract in active / expired / terminated, and the product itself only creates them at F9 (contract_activate). So the two windows are disjoint by construction — there is no state in which a requester holds edit on the parent of an obligation. §04's U(本人负责) on clm_obligation is unreachable for 100% of rows, today and by design.
clm_legal is unaffected and was measured working: Legal Counsel 2 PATCHed one of its own obligations to in_progress and got 200, because contract_legal_all grants edit on every contract.
What this costs
我负责的履约 renders for a requester (measured: 18 open rows for Business Requester 3, navigation groups My Contracts and Analytics), F10's reminder reaches them, and every action the reminder asks for is refused. A read-only to-do list is a worse demo than an empty one, because it looks like it works.
What it is not
⛔ Not a platform bug. controlled_by_parent is doing exactly what ADR-0055 says. The disagreement is between two things this repository declares about itself.
Options, none of which a developer agent should pick
- A. Change §04:
clm_obligation for clm_requester becomes R(本人负责), and the reminder wording, my_obligations and F10's message stop implying the owner can act. Cheapest; concedes that only legal, finance and records ever close an obligation, which contradicts §05's placement of 我负责的履约 in the 我的合同 group.
- B. Give
clm_requester a narrow contract-edit grant on executed contracts they own, scoped by FLS to nothing — enough to satisfy the master check. Keeps §04 and §05 as written; widens a row-level grant for a reason that reads as a workaround, and needs the FLS surface listed field by field.
- C. Change
clm_obligation off controlled_by_parent to private with its own sharing rules. Honours both §04 rows literally; contradicts §04's own OWD sentence and ADR-0055's reason for deriving child access from the parent, and every other child object would ask why not too.
Recommendation: A if 我负责的履约 is a status board, B if it is a work queue. §05's 履约 grid (due_date 升序, unfinished only) and F10's "mark it done once it is performed" both read as a work queue, so B is what the rest of the design already assumes — but the wording that would have to change is §04/§05, which is a governed surface.
Related
#52 (where this was measured) · #11 (the other half of why a requester holds nothing by default) · #28 · #39 / PR #42 (F10 itself, working as built)
Generated by Claude Code
Found while measuring #52, which deals
clm_obligation.ownerto real accounts for the first time. ⛔ Not introduced by that card — it applies to any requester-owned obligation, and #52 is simply the first change that makes one exist.The contract
DESIGN.md§04, permission matrix, verbatim:U(本人负责)— a requester updates the obligations they are accountable for. §05 puts that list in the 我的合同 group every employee reaches (我负责的履约), and F10 reminds the owner to "Record progress, or mark it done once it is performed."Measured: the U never arrives, for every row that exists
Clean database,
pnpm demoon port 3152, README operator setup performed plus theclm_requestergrant #11 is about, re-seeded so the upserts hand the rows over. Signed in asBusiness Requester 3over REST:The account owns the obligation and owns its parent contract, and the object-level explain says yes. The write is still refused.
Why it can never succeed
Two declarations that are each correct alone:
clm_obligationissharingModel: 'controlled_by_parent'(§04 "五个子对象controlled_by_parent", ADR-0055) — so update on a child requires edit on the masterclm_contractrow, whatever the child's own row scope says;clm_requester's contract edit window isREQUESTER_EDITABLE_STATUSES = ['draft', 'submitted'](§04 "本人发起,draft/submitted可改",src/sharing/_lifecycle.ts).An obligation is a post-signature commitment:
plan-children.tshangs every one of the 200 off a contract inactive/expired/terminated, and the product itself only creates them at F9 (contract_activate). So the two windows are disjoint by construction — there is no state in which a requester holds edit on the parent of an obligation. §04'sU(本人负责)onclm_obligationis unreachable for 100% of rows, today and by design.clm_legalis unaffected and was measured working:Legal Counsel 2PATCHed one of its own obligations toin_progressand got200, becausecontract_legal_allgrants edit on every contract.What this costs
我负责的履约 renders for a requester (measured: 18 open rows for
Business Requester 3, navigation groupsMy ContractsandAnalytics), F10's reminder reaches them, and every action the reminder asks for is refused. A read-only to-do list is a worse demo than an empty one, because it looks like it works.What it is not
⛔ Not a platform bug.
controlled_by_parentis doing exactly what ADR-0055 says. The disagreement is between two things this repository declares about itself.Options, none of which a developer agent should pick
clm_obligationforclm_requesterbecomesR(本人负责), and the reminder wording,my_obligationsand F10's message stop implying the owner can act. Cheapest; concedes that only legal, finance and records ever close an obligation, which contradicts §05's placement of 我负责的履约 in the 我的合同 group.clm_requestera narrow contract-edit grant on executed contracts they own, scoped by FLS to nothing — enough to satisfy the master check. Keeps §04 and §05 as written; widens a row-level grant for a reason that reads as a workaround, and needs the FLS surface listed field by field.clm_obligationoffcontrolled_by_parenttoprivatewith its own sharing rules. Honours both §04 rows literally; contradicts §04's own OWD sentence and ADR-0055's reason for deriving child access from the parent, and every other child object would ask why not too.Recommendation: A if 我负责的履约 is a status board, B if it is a work queue. §05's 履约 grid (
due_date升序, unfinished only) and F10's "mark it done once it is performed" both read as a work queue, so B is what the rest of the design already assumes — but the wording that would have to change is §04/§05, which is a governed surface.Related
#52 (where this was measured) · #11 (the other half of why a requester holds nothing by default) · #28 · #39 / PR #42 (F10 itself, working as built)
Generated by Claude Code