Repository navigation
A finance controller cannot launch a contract — clm_finance's field locks also apply to the draft they create - #166
Merged
Conversation
…edit windows clm_finance locked the legal fields and status on every contract row, and clm_records locked everything but status and archive_no, so the locks also landed on the draft a finance controller or records manager launches as a requester: the intake wizard's create_contract was refused with 403 "Field write denied". Ruling A on #127: the platform's field permission row condition (`when`, @objectstack 18.2.0, objectstack#25866) now scopes finance's lock to approved onward and records' lock to signing onward. Elsewhere the entries are absent, so clm_requester alone decides on the launcher's own draft; §04 and §13 Q1 are unchanged. A new helper, onRowsWhere, adds the condition to a set of entries and refuses an unreadable one, as the spec does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn
Contributor
Author
席位核对 — ACCEPT · head
|
| check | reading |
|---|---|
| shape | draft → main, Fixes #127; mergeable clean |
| CI on the head | both Validate check-runs success on feec816 = PR head.sha |
| scope | 3 files (+56/−8): _grants.ts (onRowsWhere helper), finance.profile.ts (the lock is conditioned on POST_APPROVAL_STATUSES), records.profile.ts (the lock is conditioned on EXECUTION_STATUSES). Within the claim's surface. Nothing governed; lint has only main's one warning |
| the ruling | 6406413107 letter A — delivered on 18.2.0's field-permission when (objectstack#25866). Before: Finance Controller 1 and Records Manager 1 got 400 Field write denied at 发起合同. After: both launch, with the contract reaching draft or in_review. §04 and §13 Q1 are unchanged, as the ruling requires |
| no widening | on approved and later rows, finance is still refused the legal fields, status and termination_reason (403 ×6). On signing and later rows, records is still limited to status and archive_no. On a launcher's own draft, clm_requester's locks still hold (stamps, risk_level, liability_cap). Finance and records on rows outside their window are refused by RLS. Requester and legal readings are unchanged. Reverse check: a misspelt when field fails pnpm validate |
| helper | onRowsWhere refuses a hidden entry, as the spec refuses readable: false beside when |
| residual | on their own draft, the console 编辑 dialog still disables the conditioned fields for finance and records, while the write is admitted (wizard and API: 200). That is the 18.2.0 form projection, which ignores the row by declared contract. The seat files it upstream; hotclm waits, with no app change |
Landing: under the maintainer's merge authorization (project thread: 「你可以自己审核的就自己合并」).
Generated by Claude Code
zhuangjianguo
marked this pull request as ready for review
October 27, 2026 09:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #127
Ruling A (comment 6406413107): the platform's field permission row condition shipped in
@objectstack18.2.0 (objectstack-ai/objectstack#25866, commitc8cb510, keywhen), andmainruns on 18.2.0 since #155. This PR adds that condition to the two locks that also landed on a launcher's own draft. DESIGN.md §04 and §13 Q1 are unchanged.What changed
clm_financerisk_level,liability_cap,status,termination_reason(10 entries,readable: true, editable: false)when: status in ["approved", "signing", "active", "expired", "terminated"](POST_APPROVAL_STATUSES)clm_recordseditableOnly(Contract, ['status', 'archive_no'])(every other contract field locked)when: status in ["signing", "active", "expired", "terminated"](EXECUTION_STATUSES)src/profiles/_grants.ts: new helperonRowsWhere(security, condition). It addswhento each entry and refuses an unreadable entry, as the spec refusesreadable: falsebeside awhen.src/profiles/finance.profile.ts,src/profiles/records.profile.ts: the two locks above, with docblocks that say why.CONTRACT_STAMPED_FIELDS) stays unconditional, and so does every other set.How it combines with
clm_requester(_grants.tsopen). Under the spec, a conditioned entry votes on rows that match and is absent on every other row. The most-permissive merge and the rule that only declaring sets vote are unchanged. So on a draft, the finance or records lock has no vote, andclm_requesterdecides alone, as it does for every launcher. The requester's own locks still hold there: stamps,risk_level,liability_cap,termination_reason(measured below). Finance or records can only update a row outside their own edit window through the requester's window:draft/submitted,own_and_reports. So the lift reaches exactly the rows the holder edits as a requester. Rows they reach through a sharing rule before their window opens stay refused by RLS (in_approvalfor finance,approvedfor records, both measured). No other set carries awhen, so legal, admin, the executive and GM rungs, and plain requesters get the same field answer on every row as before.Evidence:
pnpm demo:zhon 18.2.0, throwaway DB, port 3142Both runs used a fresh
.objectstack: before =origin/main21f2319, after =feec816. Personas were set up the README way:POST /api/v1/auth/admin/create-user, thenchange-password(mustChangePassword), then asys_user_positionrow by name, thenset-user-managerfor Business Requester 1, Finance Controller 1, Records Manager 1 and Legal Counsel 1 (manager Executive 1), then every contract sharing rule re-evaluated. Browser: Playwright on/opt/pw-browsers/chromium, localezh-CN, signed in through the Console form as each persona. API readings were signed in as the persona. Every PATCH wrote the record's current value back, so a 200 changes no data and a 403 is FLS or RLS.Launch through 发起合同: 双向保密协议 · 极光系统 · 阿卡迪亚欧洲有限公司. The wizard filled title, 摘要, 适用法律 德国法 and 保密期限 24, uploaded a first version, and ticked 立即提交 or left it unticked.
21f2319feec816POST …/contract_intake/runs/…/resume → 400 FLOW_FAILED"Node 'create_contract' failed: … Field write denied: not permitted to edit [confidentiality_term_months, governing_law, status, summary]"; toast 您没有权限保存这条记录。in_review(F2 onward hop), unsubmitted run staysdraft;governing_law=德国法,confidentiality_term_months=24, summary setin_review/draft, same valuesin_review/draftFilling the refused fields afterwards (API, on the persona's own draft):
governing_law, summary, confidentiality_term_months, jurisdictiontitle, amount, payment_terms{status: 'draft'}{status: 'approved'}draftrow, the hook still guards)title, amount, payment_termsrisk_level·submitted_atPOST clm_contract {status:'approved', governing_law, …}POST clm_contract {status:'signing', …}§13 Q1 kept:
governing_law,summary,confidentiality_term_months,status,termination_reason,jurisdictionpayment_terms, amount(control)governing_law, status/payment_termsin_approvalcontract it reads viacontract_finance_routedtitle·governing_law·amount·risk_levelstatus(same value) · expiredarchive_notitle· approvedtitlegoverning_law, summary, risk_level, liability_cap· approvedgoverning_lawPOST /api/v1/security/explain(update, after) names the conditioned entries. Finance: "10 field permission(s) carry a row condition … governing_law (locked when status in ["approved", "signing", "active", "expired", "terminated"], set "clm_finance") …". Records: 56 entries,when status in ["signing", …].Record form (browser, after, 编辑 dialog):
POST /api/v1/batch 200, toast 合同…已更新The disabled inputs on fc1's and rm1's own drafts are the platform's declared behaviour, not this change: "Readers that hold no row (
/auth/me/permissions, the field projections, the write preview on an update) report a row-conditioned entry as its lock" (FieldPermissionSchema.when, 18.2.0). The launch wizard and API writes reach the write gate, which judges the row. The Console form does not. See Acceptance notes.Boot logs: boot-phase
WARN/⚠lines are identical before and after once timestamps are stripped (diffempty). The only[Security]lines are the same three RLS refusals my own probes caused in both runs. No "row condition … applied as its lock" WARN appeared in the after run (0 lines), so the wizard'ssubmit_contractupdate was judged on its row and not treated as unjudged. Seed summary:inserted 830, errored 404in both runs (README's missing-owner lines).Reverse check: changing the finance
whentostatuz in [...]turnedpnpm validatered (exit 1): "field permission when field "statuz" is not a field on object "clm_contract". Did you mean "status"?". I restored it withgit checkout HEAD --, andgit diff HEADwas empty with the hash matching HEAD.Gates: on
feec816, each exit code captured before any pipeAcceptance notes
clm_requester's locks (stamps,risk_level,liability_cap,termination_reason) are what still hold on a finance or records launcher's draft. DESIGN.md §04 givesclm_requesterupdate on its own obligations, andcontrolled_by_parentrefuses it for all 200 — 我负责的履约 is read-only for the audience it is built for #64 editsrequester.profile.tsthis round. Its diff should leave those entries in place.signing, as ruled. Onapproved(records' read-only window) its entries are absent, but RLS refuses the update (measured 403 above).🤖 Generated with Claude Code
https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn