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 - #165

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-64-obligation-owner-may-edit
Oct 27, 2026

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #64

Ruling-ref: 6395825992 (letter D) · post-release execution step 2, the clm_obligation half. #64 stays open; see "Why this is Part of, not Fixes".

What changed

src/objects/obligation.object.ts declares the 18.2.0 opt-in (objectstack-ai/objectstack#25760, commit 5e113b3):

sharing: { controlledByParent: { ownerMayEdit: true } },

The comment above it says what the opt-in opens (a by-id update by the row's platform owner with read on the contract; create, delete and re-pointing to another contract still need edit on the contract), and what it does not do here yet: it keys on the injected owner_id, not on the obligation's own owner field.

One file, one key, plus its comment. requester.profile.ts is untouched.

Why this is Part of, not Fixes: the opt-in admits no requester on 18.2.0

The platform's leg (assertControlledByParentWrite, ADR-0055 §3 amendment) admits a caller whose userId equals the stored row's owner_id. HotCLM's accountable person is a different column: clm_obligation.owner (Field.user). That column is what 我负责的履约 filters on (my_obligations: owner == {current_user_id}), what F10 reminds, and what obligation_requester_own checks (owner == current_user.id). Nothing writes the person into owner_id:

  • The demo seed sets owner only. On the measured DB, all 200 obligations have owner_id = Dev Admin: 50, 53 and 40 of them have owner = Business Requester 1, 2 and 3, 29 and 27 have owner = Legal Counsel 1 and 2, and 1 has no owner.
  • F9 (contract_activate) inserts its renewal obligation with runAs: 'system', so owner_id is not stamped from the requester either. This is read from the code, not measured.

So the condition owner_id == caller never holds for the requester the obligation is assigned to. This was measured, and is below. Which way to line the two up is a decision, and it is in the hand-back on #64.

Measurement: README personas, pnpm demo:zh, port 3143, throwaway DB, zh-CN, @objectstack 18.2.0

The personas were set up the README way: create-user with mustChangePassword and then change-password as the user for Business Requester 1–3, Legal Counsel 1–2 and a Line Manager; sys_user_position clm_legal_counsel for both counsel; Set Manager for the three requesters; then pnpm demo:zh again to hand the rows over. Every reading below was taken signed in as the persona named.

Before = origin/main 21f2319. After = this branch.

# Business Requester 3, obligation 提交用量报表 (owner = me) under MSA-2026-0015 (active, owner_id = me) before after
1 PATCH {status: in_progress} 403 requires edit access to its master record 403, same
1b PATCH {notes} 403, same 403, same
1c PATCH {status: done} 403, same 403, same
2 PATCH an obligation on my own contract whose owner is Legal Counsel 1 403 You do not have access to this record 403, same
3 PATCH the parent contract (jurisdiction) 403 You do not have access to this record 403, same
5 state machine: done → pending, pending → overdue 403 at the master gate, before the hook is reached 403, same
6 reassign owner, write owner_id, re-point contract to another active contract or to my own draft 403 at the master gate 403, same
— POST /security/explain {clm_obligation, update} allowed: true allowed: true
# Legal Counsel 2, an obligation they own before after
4 PATCH {status: in_progress} 200 200
4b in_progress → overdue 422 INVALID_STATE (only the daily job marks overdue) 422, same

Browser (Chromium /opt/pw-browsers/chromium, zh-CN): 我的合同 › 我负责的履约 lists 18 rows for Business Requester 3, before and after. I opened 提交用量报表. The record drawer shows 负责人 = Business Requester 3 and 所有者 = Dev Admin. It offers no 编辑 button, and its 更多操作 menu holds only 添加到收藏 and 分享. This was the same before and after, and no write was sent. Control: Legal Counsel 2 opened 提供反商业贿赂合规声明, clicked 编辑, set 状态 = 进行中, filled 备注 and clicked 更新. The page sent PATCH /api/v1/data/clm_obligation/… and got 200, with the toast 履约义务“提供反商业贿赂合规声明”已更新. This was the same before and after. The console logged two Failed to load resource lines (one 404, one 401) on the login page, the same both times.

Not measured: a positive reading of the opt-in, meaning a requester row whose owner_id is the requester. The dev admin's PATCH {owner_id} on an obligation answered 403 You do not have access to this record, because the dev admin holds only clm_requester. Under the rules of this round, I took no other route.

Gates (head 7f5b38c)

pnpm install                 exit 0
pnpm validate                exit 0   ✓ Validation passed (780ms)
pnpm lint                    exit 0   6 suggestion(s) (773ms)          -- 0 warnings
pnpm typecheck               exit 0   tsc --noEmit
pnpm lint:i18n-gate          exit 0   ✓ i18n gate · COVERAGE : 0 missing keys across 2 locale(s)
CLM_DEMO_SEED=1 pnpm validate exit 0  ✓ Validation passed (783ms)

Reverse leg for the lint reading: with this file temporarily restored to origin/main, pnpm lint printed 1 warning(s), 6 suggestion(s). The warning was security-controlled-by-parent-owner-policy-without-opt-in at permissions[4].rowLevelSecurity[1].using. The file was then restored with git checkout HEAD --, and its blob hash matches HEAD 2ea8e17.

Acceptance notes

  • The lint warning is cleared by shape, and the gap it pointed at is still there. The 18.2 lint arm fires on any FIELD == current_user.id update policy and prescribes the opt-in. The gate the opt-in turns on compares only owner_id. obligation_requester_own keys on owner, so the lint's fix does not change what the runtime admits here. This is reported to the seat as a platform finding.
  • If a requester ever does pass the leg (owner_id = owner = them), these were read from the platform code and ADR-0055's amendment, not measured:

🤖 Generated with Claude Code

https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn

The ruling on #64 (letter D) waited for the platform to let a
controlled_by_parent detail's owner update its row with read on the
master. 18.2.0 ships it as sharing.controlledByParent.ownerMayEdit, and
18.2's lint already warns that obligation_requester_own can never admit
an owner without it. This declares the opt-in, which clears that warning.

The platform's owner is the injected owner_id, not the obligation's own
owner field that My Obligations, F10 and obligation_requester_own key on.
All 200 demo obligations carry owner_id = the dev admin, so measured on
18.2.0 the opt-in admits no requester yet; the comment says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT (Part of #64) · head 7f5b38c · 2026-10-27T09:21Z

check reading
shape draft → main, Part of #64; mergeable clean
CI on the head both Validate check-runs success on 7f5b38c = PR head.sha
scope 1 file: sharing: { controlledByParent: { ownerMayEdit: true } } on clm_obligation, with a comment that records the owner / owner_id gap. This is ruled step 2 of 6395825992 (D). It is within the claim's surface. Nothing governed. Lint drops from 1 warning to 0
behaviour unchanged, as measured: Business Requester 3 is still refused (403 master edit) before and after, legal is unchanged, and the state machine still holds. The opt-in is inert today, because the 18.2.0 gate compares owner_id and hotclm's accountable person is owner. Merged anyway: every route forward needs the opt-in, and the rollback is deleting one key
seat calls on the open questions Q1 → platform first (option A). Filed as objectstack-ai/objectstack#26591: the opt-in should be able to name the owner column, or lint and gate must agree on owner_id. #64 goes pm:blocked on it. If the platform rules "owner_id only", moving hotclm's accountable column becomes a decision-box item (option B; C stays out under the platform-gap rule). Q2 → the contract / owner / due_date read-only locks ride the change that makes the leg reachable, so they can be measured; this is recorded on #64. Q3 → hold the §04 sentence until requesters can actually update (option B)

Landing: under the maintainer's merge authorization (project thread: 「你可以自己审核的就自己合并」).


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants