Skip to content

DESIGN.md §04 gives clm_requester update on its own obligations, and controlled_by_parent refuses it for all 200 — 我负责的履约 is read-only for the audience it is built for #64

Open

Description

@claude

Ruled: 6395825992 · letter D · 2026-10-25T09:14Z (D = platform card in objectstack; added by the director seat beyond A/B/C below)

Blocked-by: objectstack-ai/objectstack#26591 (ownerMayEdit compares owner_id; hotclm's accountable person is owner. #25760 shipped in 18.2.0 and the opt-in landed as PR #165, inert until this card)

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)

Activity

  1. zhuangjianguo commented on Sep 10, 2026

    @zhuangjianguo
    Collaborator

    PM triage — needs-user-decision, and the decision box goes to 9

    Labelled, ⛔ not dispatched. Every one of the three options either edits a governed surface (DESIGN.md §04/§05) or widens a row-level grant to satisfy a check it was not written for. That is not a developer's call, and the card is right to say so rather than picking one.

    Both load-bearing readings independently confirmed

    claim reading
    clm_obligation derives access from its parent src/objects/obligation.object.ts:34 — sharingModel: 'controlled_by_parent'
    a requester may edit a contract only in draft / submitted src/sharing/_lifecycle.ts:44 — REQUESTER_EDITABLE_STATUSES = subset(CONTRACT_STATUSES, ['draft', 'submitted'], …)

    So the disjointness is structural, exactly as the card argues: an obligation exists only under a post-signature contract, a requester holds edit only before signature, and controlled_by_parent routes the child's update through the parent's edit. There is no state in which the two overlap. §04's U(本人负责) on clm_obligation is unreachable for 100% of rows.

    Why this one is worth the maintainer's attention rather than a shrug

    The card names the cost precisely and I want it preserved for the decision batch:

    A read-only to-do list is a worse demo than an empty one, because it looks like it works.

    That is the distinguishing feature. This is not a missing capability — it is a declared capability that renders, gets reminded about by F10, and refuses the one action the reminder asks for. explain even answers allowed: true, because the refusal happens at the master-record check below it, so the platform's own introspection endpoint agrees with §04 right up until the write.

    And it has just become reachable in practice: until #52 (in flight), every obligation was owned by the dev admin, which holds no clm_* set and never opened 我负责的履约 at all. #52 does not cause this — it is the first change that makes a requester-owned obligation exist for anyone to be refused on.

    What I am NOT doing here

    ⛔ No adjudication and no four-prism analysis on this card yet. The tier fuse (CONTRACT_REVIEW_TIER = 'claude-fable-5-1', compared exactly) is not met by this seat, so the director's four duties stay parked — 不裁不清标. This card joins the box and gets its full analysis when the maintainer calls the next decision batch.

    One note for whoever writes that analysis: the card's own recommendation splits on a question the maintainer has effectively already answered elsewhere — is 我负责的履约 a status board or a work queue? §05 orders that grid by due_date ascending and filters to unfinished, and F10's message is "Record progress, or mark it done once it is performed." Both read as a work queue. That points at option B, and option B is the one that keeps §04 and §05 as written — but B widens a row-level grant, which is the kind of thing that wants an explicit ruling rather than an inference.

    Attribution

    Found and written by the dev on #52 while measuring, not by this seat. It measured a REST refusal it did not need to measure to finish its own card, chased it to two declarations that are each individually correct, and stopped at the point where a person has to choose. That is the right shape for a card an agent should not resolve.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    Contributor

    Evidence from the 17.7.0 browser pass · 2026-10-10T02:46Z — #87, finding 5 (screenshots 092–095 under qa/browser-test-17-7/screenshots/ on branch claude/issue-87-browser-test-17-7).

    Business Requester 1, My Obligations → "Provide the annual security attestation" → mark done:
    PATCH /api/v1/data/clm_obligation/-YpaMcuIEzJyRhnt {"status":"done"} → 403 "requires edit access to its master record (master 'clm_contract' not editable by this user (row-level security))"; the form says "You don't have permission to save this record".

    So this card's measurement holds on 17.7.0 in the browser, for an obligation the requester owns on an active contract: every requester obligation on an in-force contract is unreachable, as the card argued.


    Generated by Claude Code

  3. zhuangjianguo commented on Oct 25, 2026

    @zhuangjianguo
    Collaborator

    Ruling: batch #2 item 3 · letter D · maintainer 「同意」 2026-10-25T09:14:04Z

    裁定:D(新增项)—— 这是平台缺口,立 objectstack 卡,hotclm 等平台

    维护者在第 2 批决策箱回了「同意」(= 推荐项 D)。裁决文档:https://claude.ai/artifact/X4sZxhLrEYshLjkgYp4svr 。D 是本席在正文 A/B/C 之外按维护者 2026-10-25 的规则(「本项目是元数据应用,平台相关的功能应该在平台开发」)新增的选项。

    为什么是平台的事:正文说「不是平台 bug」是对的 —— ADR-0055 §3 在按设计工作(改子记录要求对上级有编辑权;子记录自己的 owner RLS 只 AND 收窄,永不授予)。但「一个有负责人的子记录,负责人能不能改自己那一行」是平台级语义:Salesforce 在 Controlled by Parent 的活动对象上有铁律「Assigned To 对自己那条记录永远有完整权限」;HotCRM 的任务 / 活动挂在客户下、分给只能读客户的销售,会撞上一模一样的墙。objectstack 去重:controlled_by_parent 相关卡 42 张,全是写入门的强制与回归,关于「子记录负责人」语义的为 0。

    ⛔ A(降为只读)削产品,与 §05、F10 的工作队列措辞相反;B 做不到正文写的形状(using 只按状态、FLS 按权限集不按状态,B 实际是让发起人能改自己生效合同的业务字段);C 为回退项,仅当平台答「请用 private」时启用。

    鲜度门:正文 2026-09-10T08:50Z;评论两条,最后一条 2026-10-10T02:47Z(17.7.0 浏览器复现)。main e4997a8 的 obligation.object.ts:34、_lifecycle.ts:44、requester.profile.ts 的两条 RLS 与正文一致。录裁前重读,无变化。

    裁后执行

    1. 本席立 objectstack 卡(类 ③ 平台缺口),回链见下一条评论;本卡 Blocked-by 它。
    2. 平台发布后 hotclm 两处:clm_obligation 加一行声明(形式按平台定),§04 加一句「履约由负责人更新,不要求上级合同可改」(受管面,随设计文档 PR)。
    3. 期间:「我负责的履约」对发起人保持只读,法务不受影响。⛔ 不做应用侧绕路(AGENTS.md → Platform gaps)。
    4. 若平台拒绝(答「请用 private」):回退 C,届时另起一轮呈维护者定 §04 的 OWD 句子。

    ⛔ 本席不动标签;lane 席位派发时转标签并带 Ruling-ref:。


    Generated by Claude Code

  4. zhuangjianguo commented on Oct 25, 2026

    @zhuangjianguo
    Collaborator

    Platform card filed per the D ruling above: objectstack-ai/objectstack#25760 — controlled_by_parent detail row's owner may update their own row with read on the master (opt-in per object). Class ③, unlabelled, awaiting triage there.

    Blocked-by: objectstack-ai/objectstack#25760


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    Contributor

    Out of the decision box — Ruling-ref: comment 6395825992 (batch #2 · maintainer 「同意」).

    • State: needs-user-decision → pm:blocked, Blocked-by: objectstack-ai/objectstack#25760 (controlled-by-parent detail owner may update own row).
    • Meanwhile 我负责的履约 stays read-only for requesters; no app-side workaround. If the platform answers "use private", fallback C goes back to the maintainer.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    Contributor

    Unblocked — 2026-10-27

    upstream state installable here
    objectstack-ai/objectstack#25760 closed fix commit 5e113b3 is an ancestor of tag @objectstack/spec@18.2.0

    main now pins @objectstack/* 18.2.0 (PR #155, merge 21f2319, push CI Validate success), so the fix is installable in this repo. The ruling recorded in the body stands; this card returns to the queue for dispatch. The dev re-measures the premise on main before writing code.

    Note: 18.2's lint already flags obligation_requester_own (security-controlled-by-parent-owner-policy-without-opt-in) on main. Opting in to sharing.controlledByParent.ownerMayEdit is this card's work.

    State: pm:blocked → pm:queue.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 17
    Ruling-ref: 6395825992
    Session: session_017nzWFRUo2U7evMkPdVniAn
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-64-obligation-owner-may-edit
    Worktree: hotclm-issue-64
    Domain: repo:hotclm (single-lane sister repo — no domain:*)
    Seat: repo:hotclm#1
    File surface: src/objects/obligation.object.ts (the controlledByParent.ownerMayEdit opt-in) · src/profiles/requester.profile.ts (only if the opt-in needs the policy touched) — §04 sentence rides the seat's design-doc PR #120 — stop on breach; explain in the report
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: this app's obligation sharing (an owner updates their own obligation) | objectstack-ai/objectstack#25760 shipped in 18.2.0 | every requester who performs an obligation
    Thread-read: 6437385561
    Premise: re-measured by the dev on origin/main 21f2319 (@objectstack 18.2.0) with README personas first
    Serial constraints cleared: Unblocked 2026-10-27 (18.2.0 on main); it also clears 18.2's new lint warning on obligation_requester_own; round 17 dispatches #134, #127, #64 and #159 together on disjoint files.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 64,
      "status": "needs_decision",
      "branch": "claude/issue-64-obligation-owner-may-edit",
      "pr": "https://github.com/objectstack-ai/hotclm/pull/165",
      "session": "session_017nzWFRUo2U7evMkPdVniAn",
      "premise_still_valid": false,
      "summary": "Ruled step 2 is done: clm_obligation now declares sharing.controlledByParent.ownerMayEdit: true. It is one key plus a comment in src/objects/obligation.object.ts, and lint drops from 1 warning to 0. The premise that this one line fixes the card is false. The 18.2.0 leg admits a caller whose userId equals the row's injected owner_id (security-plugin-cbp-write-gate.ts, ADR-0055 §3 amendment). HotCLM's accountable person is a separate column, clm_obligation.owner (Field.user). That column is what my_obligations filters on, what F10 reminds and what obligation_requester_own checks. All 200 demo obligations carry owner_id = Dev Admin, and F9 inserts with runAs system. Measured on 18.2.0 with README personas, Business Requester 3 is refused with the same 403 (requires edit access to its master record) before and after, over REST and in the browser. The drawer shows 负责人 = Business Requester 3 and 所有者 = Dev Admin, with no 编辑. PR #165 is Part of #64. The relay created it as a draft although the pr_create op carried no draft key. I did not touch that. Lining up owner and owner_id needs a decision (open question 1).",
      "tests": "REST, signed in as each persona, port 3143, throwaway DB per run, pnpm demo:zh plus README persona setup (create-user, change-password, sys_user_position clm_legal_counsel x2, set-user-manager x3, then a re-run to hand the rows over). BEFORE origin/main 21f2319, AFTER 7f5b38c. Business Requester 3 on 提交用量报表 under MSA-2026-0015 (active; owner and owner_id of the contract = BR3): PATCH status in_progress, notes, and status done each answered 403 'requires edit access to its master record (master clm_contract not editable by this user (row-level security))', before and after. explain clm_obligation update said allowed: true before and after. Not-own obligation (owner Legal Counsel 1) on BR3's contract: 403 'You do not have access to this record', before and after. Parent contract PATCH: 403, before and after. State machine (done to pending, pending to overdue) and re-point or reassign (owner to BR1, owner_id, contract to another active contract or to the own draft NDA-2026-0019): 403 at the master gate, before and after, so the hook is never reached for a requester. Legal Counsel 2 on an own obligation: PATCH in_progress 200 and in_progress to overdue 422 INVALID_STATE, before and after. Browser (/opt/pw-browsers/chromium, zh-CN): BR3 sees 18 rows in 我负责的履约 and the drawer for 提交用量报表 has no 编辑, with 更多操作 = [添加到收藏, 分享], before and after, and no write was sent. Legal Counsel 2: 编辑, 状态 进行中, 备注, 更新, PATCH clm_obligation 200, toast 履约义务“提供反商业贿赂合规声明”已更新, before and after. Console showed only two login-page 'Failed to load resource' lines (404 and 401), the same both runs. NOT MEASURED: a positive reading of the opt-in with owner_id = BR3. The dev admin's PATCH owner_id answered 403 because the dev admin holds only clm_requester, and I took no other route. Lint reverse leg: with the file restored to origin/main, pnpm lint gave '1 warning(s), 6 suggestion(s)' (security-controlled-by-parent-owner-policy-without-opt-in at permissions[4].rowLevelSecurity[1].using). Restored with git checkout HEAD --; the blob 2ea8e17 matches HEAD. No ablation: this is config/metadata, and no tested behaviour changed.",
      "gates": "head 7f5b38c, each exit code captured before any pipe: pnpm install 0. pnpm validate 0 (✓ Validation passed). pnpm lint 0 ('6 suggestion(s)', 0 warnings; main has 1). pnpm typecheck 0. pnpm lint:i18n-gate 0 (✓ i18n gate, 0 missing keys across 2 locales). CLM_DEMO_SEED=1 pnpm validate 0 (✓ Validation passed). PR #165 check-runs at report time: Validate completed/success (a second Validate run was in_progress).",
      "dogfood_battery": "n/a: sister repo, no ! in the title, no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 relay writes: pr_create objectstack-ai/hotclm#165 (fleet-write dispatch POST /repos/objectstack-ai/objectstack/dispatches, run 40479595675 success, read-back identical) and this os-dev-report comment (post-stamped.mjs). git push is not a REST write.",
      "open_questions": [
        {
          "question": "How should the obligation's accountable person become the platform owner that ownerMayEdit reads? The opt-in compares owner_id, while HotCLM records the person in clm_obligation.owner.",
          "options": [
            "A Platform card in objectstack: let the opt-in name the owner column (for example sharing.controlledByParent.ownerField: 'owner'), and narrow the lint arm to policies keyed on the column the gate reads. HotCLM's schema, views, F10 and seed stay as they are, and hotclm waits Blocked-by. Cost: a spec addition and a ruling upstream. The platform may answer 'one owner concept, use owner_id' (5e113b3's acceptance notes say exactly that).",
            "B App: make owner_id the accountable column and retire clm_obligation.owner. Touches obligation.object.ts, obligation.view.ts (my_obligations), obligation-due.flow.ts (F10 recipients), requester.profile.ts RLS, F9 in contract.hook.ts, post-signature.seed.ts, plan-children.ts, datasets, translations, and the DESIGN §03/§05 field wording. The largest change, and a public field rename on a stored column.",
            "C App: keep owner and also write owner_id to the same person. The seed names owner_id, F9 sets it, and a hook keeps owner_id equal to owner on reassignment, which needs a writer holding allowTransfer or a system write. Smallest diff, but two columns for one fact can drift, and it reads as reshaping metadata around the platform (AGENTS.md Platform gaps)."
          ],
          "recommendation": "A first, because the platform's own lint treats an owner == current_user.id policy as the owner shape and prescribes this opt-in for it, so the lint and the gate disagree upstream. If the platform rules 'use owner_id', fall back to B, not C. The 18.2 ADR settles that owner_id is the one owner concept, and B makes the product say the same thing once."
        },
        {
          "question": "Once a requester does pass the leg (owner_id = owner = them), which obligation fields should clm_requester be able to change?",
          "options": [
            "A In the same change that resolves question 1, add readOnly(Obligation, ['contract', 'owner', 'due_date']) to requester.profile.ts. contract: the leg only asks edit on the master the body names, and a requester holds edit on their own draft/submitted contracts, so they could re-point an obligation onto one. owner: a requester could hand 本人负责 to someone else, because obligation_requester_own has a using and no check. due_date: F10 marks arrears from it, so a requester could move it out and never go overdue. title and kind optional. owner_id needs no lock, because reassigning it needs allowTransfer, which clm_requester lacks (permissions-matrix.mdx).",
            "B Leave the fields open and trust the audit trail."
          ],
          "recommendation": "A, measured together with question 1. I added nothing now: today no requester reaches the leg, so any lock would be unmeasurable."
        },
        {
          "question": "Should the governed §04 sentence ride PR #120 now?",
          "options": [
            "A Carry it on #120 now: 「履约由负责人更新,不要求上级合同可改」",
            "B Hold it until question 1 lands, because today it is not true for any requester-owned obligation (measured)."
          ],
          "recommendation": "B. If the seat wants it on #120 now, write the owner precisely: 「履约由其负责人(平台所有者 owner_id)更新,只需能读上级合同,不要求上级合同可改」. As things stand, the opt-in admits nobody HotCLM calls 负责人."
        }
      ],
      "governed_text_proposal": "DESIGN.md §04, beside the five controlled_by_parent children: 「履约由负责人更新,不要求上级合同可改」. This is the ruling's sentence. See open question 3 on timing and on naming owner_id.",
      "out_of_scope_findings": [
        "class: c · reach: os lint (public door) on hotclm main 21f2319 printed security-controlled-by-parent-owner-policy-without-opt-in for obligation_requester_own (using: owner == current_user.id) with the fix 'Declare sharing: { controlledByParent: { ownerMayEdit: true } }'. After the fix, lint is silent, and the runtime still answers 403 'requires edit access to its master record' for the requester the policy names. Measured on 18.2.0, PATCH clm_obligation as Business Requester 3. · Seam: spec:sharing.controlledByParent.ownerMayEdit → runtime:plugin-security assertControlledByParentWrite (compares stored owner_id only) | lint:validate-security-posture owner-policy-without-opt-in arm (matches any FIELD == current_user.id) · repo: objectstack-ai/objectstack · evidence: before and after tables in PR #165 and this report · dedupe words: ownerMayEdit owner_id, owner-policy-without-opt-in, controlled_by_parent owner field, ownerMayEdit lint prescribes, detail owner column"
      ]
    }
  9. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    Contributor

    Ruling-ref: 6395825992

    Landed (part) — PR #165 · 2026-10-27, and re-blocked

    reading value
    PR #165 MERGED (squash on main) at its ACCEPT head 7f5b38c: ownerMayEdit: true on clm_obligation (ruled step 2). Lint is at 0 warnings. Part of #64
    authority maintainer, project thread, verbatim: 「你可以自己审核的就自己合并」
    premise not delivered yet. The 18.2.0 gate admits only the row's owner_id. hotclm's accountable person is the owner field, and the seeded and F9 obligations carry owner_id = the dev admin. Business Requester 3 is still refused (dev report 6438106376)

    Next: the platform rules on objectstack-ai/objectstack#26591, which offers two options. Either the opt-in names the owner column, or lint and gate agree on owner_id.

    • Under the first, the lane adds the column name and, measured in the same change, read-only contract · owner · due_date for clm_requester.
    • Under the second, moving the accountable column onto owner_id comes to the decision box.

    The §04 sentence waits until a requester can actually update. State: pm:dispatched → pm:blocked, assignee cleared; the body's Blocked-by: now names #26591.


    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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions