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
A finance controller cannot launch a contract — clm_finance's field locks also apply to the draft they create #127
Ruled: 6406413107 · letter A · 2026-10-26T01:47:10Z
Blocked-by: objectstack-ai/objectstack#25866 (field permission entry with a row condition; filed 2026-10-26, unlocks when a published release carrying it is installed here)
Filing class: ① product defect, needs a ruling (it touches §04 / §13 Q1) — reach: public door, measured on 18.1.0 by the #124 dev (os-dev-report on #124, PR #126 head 19e8dac). Reader: the maintainer, through the decision box.
Symptom
Every member holds clm_requester (PR #122), so every member is meant to be able to launch a contract (DESIGN.md §04 「所有员工默认」). A finance controller cannot: the launch wizard's last step answers
400 Node 'create_contract' failed: create_record(clm_contract) failed: [Security] Field write denied: not permitted to edit [confidentiality_term_months, governing_law, status, summary] on 'clm_contract'
The records manager is very likely in the same place (its set locks contract fields with editableOnly); not measured.
Why
clm_finance locks the legal fields and status on clm_contract (src/profiles/finance.profile.ts:48, §04 「FLS 锁法律字段」 + §13 Q1 「与 status」). Field-level security in this platform is per set and per object, not per row: it cannot say "locked on the contracts finance reaches through the approval sharing rule, open on the draft finance launches itself". And only sets that declare a field vote (src/profiles/_grants.ts), so clm_requester, which says nothing about those fields, does not lift the lock. The lock therefore also lands on the brand-new draft a finance controller creates. Until PR #126 this was hidden behind the business-unit 403 one step earlier.
What needs deciding
Which of §04's two promises gives way, or whether the platform should be asked for row-scoped field locks. The seat's reading of the options, for the director to set out:
A — ask the platform for field-level security that applies only on the rows a set's sharing rule delivers (or a per-row condition on a field lock); finance and records staff launch nothing until it ships.
C — accept it: finance and records staff do not launch contracts in the standard product; §04's wording changes (governed).
No code moves until the maintainer rules.
Dedupe: hotclm issues listed and grepped for finance, Field write denied, launch, FLS — #64 (finance detail rows, ruled, different), none of them this.
Filed by the repo:hotclm PM seat from the #124 dev report.
Ruling: batch #8 item 1 · letter A · maintainer 「A」 2026-10-26T01:47:10Z
A — ask the platform. Field-level security gets a per-row condition; hotclm keeps §04 and §13 Q1 as written and waits.
What the director checked before recording (objectstack main, hotclm main ae13f57):
FieldPermissionSchema carries only readable / editable (packages/spec/src/security/permission.zod.ts); no row predicate exists, and RLS cannot name a field. The gap is real, not a usage error.
getFieldPermissions (plugin-security permission-evaluator.ts) merges most-permissively, but only sets that declare a field vote. Opening the four fields on clm_requester would also lift finance's post-approval lock, so there is no app-side fix that keeps §13 Q1.
B would run the whole intake wizard as the system (the flow header records why it runs as the user), would still refuse a finance controller editing their own draft, and waits for a release carrying objectstack#25643. Not taken.
Execution after the ruling:
Director files the platform card at objectstack (field permission entry with an optional row condition, same expression language as RLS using); this issue gets a Blocked-by: line pointing at it.
PM seat labels pm:blocked; no hotclm code moves until the platform ships, is released, and hotclm upgrades.
Then the lane adds the condition to clm_finance (lock from approved onward) and clm_records (lock from signing onward), measures finance AND records launching a contract, and §04 stays unchanged.
State: needs-user-decision → pm:blocked on objectstack-ai/objectstack#25866 (letter A: ask the platform). No hotclm code until a release carrying it is installed here; then the lane adds the row condition to clm_finance (approved onward) and clm_records (signing onward) and measures a finance and a records persona launching a contract.
fix commit c8cb510 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.
Claim: PM loop round 17
Ruling-ref: 6406413107
Session: session_017nzWFRUo2U7evMkPdVniAn
Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-127-finance-launch-fls-when
Worktree: hotclm-issue-127
Domain: repo:hotclm (single-lane sister repo — no domain:*)
Seat: repo:hotclm#1
File surface: src/profiles/finance.profile.ts · src/profiles/records.profile.ts · src/profiles/_grants.ts (helper only, if needed) — the ruling's row condition on the finance lock (from approved) and the records lock (from signing) — 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 field permissions (a draft its own launcher can fill) | objectstack-ai/objectstack#25866 shipped in 18.2.0 | every finance controller and records manager who launches a contract
Thread-read: 6437373910
Premise: re-measured by the dev on origin/main21f2319 (@objectstack 18.2.0) with README personas first
Serial constraints cleared: Unblocked 2026-10-27 (18.2.0 on main); round 17 dispatches #134, #127, #64 and #159 together on disjoint files.
{
"issue": 127,
"status": "done",
"branch": "claude/issue-127-finance-launch-fls-when",
"pr": "#166",
"session": "session_017nzWFRUo2U7evMkPdVniAn",
"premise_still_valid": true,
"summary": "Premise re-measured on origin/main 21f2319 (@objectstack 18.2.0): Finance Controller 1 and Records Manager 1 both failed 发起合同 at create_contract with 400 FLOW_FAILED 'Field write denied'. Per ruling A, the platform's field permission row condition (when, objectstack#25866, c8cb510, in 18.2.0) now scopes clm_finance's legal-field+status+termination_reason lock to status in POST_APPROVAL_STATUSES and clm_records' editableOnly lock to status in EXECUTION_STATUSES, through a new _grants.ts helper onRowsWhere (it refuses an unreadable entry, as the spec does). On every other row a conditioned entry is absent, so clm_requester alone decides on the launcher's own draft. Its locks still hold there (stamps, risk_level, liability_cap, termination_reason). Finance or records can update a non-window row only through the requester window (draft/submitted, own_and_reports), so nothing opens for other roles or other rows. After the change both personas launch (submitted run lands in_review, unsubmitted run stays draft). §13 Q1 holds unchanged, and requester and legal readings match main. One platform residual remains: the Console record form's field projection is row-blind by the 18.2.0 spec, so finance and records see their own draft's legal fields disabled in the 编辑 dialog, although the wizard and API writes are admitted.",
"tests": "pnpm demo:zh, port 3142, fresh .objectstack each run: before 21f2319, after feec816. README personas: admin create-user, change-password, sys_user_position by name, set-user-manager to Executive 1, contract sharing rules re-evaluated. Browser: Playwright /opt/pw-browsers/chromium, zh-CN, signed in as each persona. LAUNCH (双向保密协议/极光系统/阿卡迪亚欧洲有限公司, filled title, 摘要, 适用法律, 保密期限, uploaded v1): before fc1 'POST …/contract_intake/runs/…/resume -> 400 FLOW_FAILED … Field write denied … [confidentiality_term_months, governing_law, status, summary]'; rm1 400, same with a long list (amount, auto_renew, contract_type …); br1 合同已发起。. After: fc1, rm1 and br1 all 合同已发起。; submitted runs land in_review, unsubmitted runs stay draft, with governing_law=德国法 and conf=24 stored. FILL (API as persona, current value written back): after, fc1 and rm1 PATCH own draft legal fields -> 200 and commercial fields -> 200 (before: no draft existed); fc1 own draft {status:draft} -> 200; {status:approved} -> 422 state machine; risk_level and submitted_at -> 403 for fc1, rm1 and br1 (requester locks); fc1 POST insert {status:approved, governing_law} -> 403 Field write denied [governing_law, status]; rm1 POST insert {status:signing} -> 403 [contract_type, governing_law, our_entity, party, title]. §13 Q1, identical before and after: fc1 approved governing_law, summary, confidentiality_term_months, status, termination_reason and jurisdiction -> 403 FLS x6; payment_terms+amount -> 200; signing governing_law+status -> 403, payment_terms -> 200; in_approval (finance-routed) -> 403 RLS. rm1 signing title, governing_law, amount and risk_level -> 403 FLS x4; signing status (same value) -> 200; expired archive_no -> 200; expired title -> 403; approved title -> 403 RLS. lc1 in_review governing_law, summary, risk_level and liability_cap -> 200; approved governing_law -> 200. br1 another owner's active -> 404. EXPLAIN (after, update): finance 10 and records 56 conditioned entries named 'locked when status in [...]'. FORM (browser, after): fc1 own draft 编辑 disables 摘要, 适用法律, 保密期限, 管辖, 风险等级 and 状态; saving 标题 -> POST /api/v1/batch 200, toast 已更新. rm1 own draft: only 档案号 enabled. br1 own draft (control): only 风险等级 disabled. Boot-phase WARN lines diff empty before vs after; 0 'row condition … applied as its lock' WARN lines after. REVERSE CHECK: when field changed to statuz -> pnpm validate exit 1 'field permission when field "statuz" is not a field on object "clm_contract"'; restored with git checkout HEAD --, git diff HEAD empty, hash matches HEAD. No ablation beyond that: metadata only, and no suite pins this behaviour.",
"gates": "On feec816, each exit captured before any pipe: pnpm install 0; pnpm validate 0 (8 ⚠, line-identical to origin/main 21f2319, measured on a detached main worktree); pnpm lint 0 ('1 warning(s), 6 suggestion(s)', the one warning #64's security-controlled-by-parent-owner-policy-without-opt-in, identical to main); pnpm typecheck 0; pnpm lint:i18n-gate 0 (0 missing keys across 2 locales); CLM_DEMO_SEED=1 pnpm validate 0. CI on PR #166: not awaited (in_progress at report time).",
"dogfood_battery": "n/a — hotclm, no ! in the title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 — relay fleet-write pr_create (#166, read-back identical, assignee zhuangjianguo); post-stamped os-dev-report comment on #127. Plus git push of the branch (not REST).",
"open_questions": [
{
"question": "After this PR, a finance controller or records manager launches a contract, but the Console 编辑 dialog on their own draft disables the row-conditioned fields. The 18.2.0 field projection is row-blind by declared contract ('Readers that hold no row … report a row-conditioned entry as its lock'). For finance that is the legal fields and status; for records it is every field but 档案号. The write gate admits those writes (wizard create, API PATCH 200). Raise a capability request upstream?",
"options": [
"A — ask objectstack for a row-aware field projection on the record form (the form judges when against the record it edits); hotclm waits, no app change",
"B — accept: the launch wizard is the door for these fields on a launcher's draft, and later edits go through legal"
],
"recommendation": "A, because the ruling's intent ('a draft its own launcher can fill') is met at launch but not in the form, and the gap is the platform's declared row-blind reader, which hotclm must not work around (AGENTS.md platform gaps)."
}
],
"out_of_scope_findings": [
"class: none (declared platform behaviour, capability request) · reach: Console 编辑 dialog as Finance Controller 1 on own draft UfYFt_Hz3epInWiq, 18.2.0 feec816: 摘要/适用法律/保密期限/管辖/状态 disabled while PATCH of the same fields answers 200 · evidence: PR #166 Acceptance notes, FieldPermissionSchema.when describe · dedupe words: row-conditioned field permission form projection, when editable form disabled, row-blind field readers · carrier: none, noted in PR Acceptance notes",
"carrier: #64 (requester.profile.ts, same round) · finance and records launchers' drafts now rely on clm_requester's contract locks (stamps, risk_level, liability_cap, termination_reason); #64's diff should keep them"
],
"governed_text_proposal": "none — ruling A keeps DESIGN.md §04 and §13 Q1 unchanged"
}
Release: a finance controller or records manager can launch a contract. Their field locks now apply only from approved (finance) or signing (records) onward, so §13 Q1 holds.
Residual, platform: the console's 编辑 dialog still shows those fields disabled on their own draft, although saving them is allowed. This is filed in objectstack and linked below. State: pm:dispatched removed, assignee cleared.
Activity
zhuangjianguo commentedon Oct 26, 2026
Ruling: batch #8 item 1 · letter A · maintainer 「A」 2026-10-26T01:47:10Z
A — ask the platform. Field-level security gets a per-row condition; hotclm keeps §04 and §13 Q1 as written and waits.
What the director checked before recording (objectstack main, hotclm main
ae13f57):FieldPermissionSchemacarries onlyreadable/editable(packages/spec/src/security/permission.zod.ts); no row predicate exists, and RLS cannot name a field. The gap is real, not a usage error.getFieldPermissions(plugin-securitypermission-evaluator.ts) merges most-permissively, but only sets that declare a field vote. Opening the four fields onclm_requesterwould also lift finance's post-approval lock, so there is no app-side fix that keeps §13 Q1.Execution after the ruling:
using); this issue gets aBlocked-by:line pointing at it.pm:blocked; no hotclm code moves until the platform ships, is released, and hotclm upgrades.clm_finance(lock fromapprovedonward) andclm_records(lock fromsigningonward), measures finance AND records launching a contract, and §04 stays unchanged.Doc: https://claude.ai/artifact/W7sEAgCjQ1XKQQ8UbebbPT
Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Ruling-ref: 6406413107
State:
needs-user-decision→pm:blockedon objectstack-ai/objectstack#25866 (letter A: ask the platform). No hotclm code until a release carrying it is installed here; then the lane adds the row condition toclm_finance(approved onward) andclm_records(signing onward) and measures a finance and a records persona launching a contract.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Unblocked — 2026-10-27
closedc8cb510is an ancestor of tag@objectstack/spec@18.2.0mainnow pins@objectstack/*18.2.0 (PR #155, merge21f2319, push CIValidatesuccess), 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 onmainbefore writing code.State:
pm:blocked→pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 17
Ruling-ref: 6406413107
Session:
session_017nzWFRUo2U7evMkPdVniAnAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-127-finance-launch-fls-whenWorktree:
hotclm-issue-127Domain:
repo:hotclm(single-lane sister repo — nodomain:*)Seat:
repo:hotclm#1File surface:
src/profiles/finance.profile.ts·src/profiles/records.profile.ts·src/profiles/_grants.ts(helper only, if needed) — the ruling's row condition on the finance lock (fromapproved) and the records lock (fromsigning) — stop on breach; explain in the reportContainer & model:
S,mode:subagent,model: opus(TIER_DEFAULT; hand-derived for a sister repo),effort: mediumClause-②: no
Metadata-protocol: none
Responsibility:
this app's field permissions (a draft its own launcher can fill) | objectstack-ai/objectstack#25866 shipped in 18.2.0 | every finance controller and records manager who launches a contractThread-read: 6437373910
Premise: re-measured by the dev on
origin/main21f2319(@objectstack 18.2.0) with README personas firstSerial constraints cleared: Unblocked 2026-10-27 (18.2.0 on
main); round 17 dispatches #134, #127, #64 and #159 together on disjoint files.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{
"issue": 127,
"status": "done",
"branch": "claude/issue-127-finance-launch-fls-when",
"pr": "#166",
"session": "session_017nzWFRUo2U7evMkPdVniAn",
"premise_still_valid": true,
"summary": "Premise re-measured on origin/main 21f2319 (@objectstack 18.2.0): Finance Controller 1 and Records Manager 1 both failed 发起合同 at create_contract with 400 FLOW_FAILED 'Field write denied'. Per ruling A, the platform's field permission row condition (
when, objectstack#25866, c8cb510, in 18.2.0) now scopes clm_finance's legal-field+status+termination_reason lock to status in POST_APPROVAL_STATUSES and clm_records' editableOnly lock to status in EXECUTION_STATUSES, through a new _grants.ts helper onRowsWhere (it refuses an unreadable entry, as the spec does). On every other row a conditioned entry is absent, so clm_requester alone decides on the launcher's own draft. Its locks still hold there (stamps, risk_level, liability_cap, termination_reason). Finance or records can update a non-window row only through the requester window (draft/submitted, own_and_reports), so nothing opens for other roles or other rows. After the change both personas launch (submitted run lands in_review, unsubmitted run stays draft). §13 Q1 holds unchanged, and requester and legal readings match main. One platform residual remains: the Console record form's field projection is row-blind by the 18.2.0 spec, so finance and records see their own draft's legal fields disabled in the 编辑 dialog, although the wizard and API writes are admitted.","tests": "pnpm demo:zh, port 3142, fresh .objectstack each run: before 21f2319, after feec816. README personas: admin create-user, change-password, sys_user_position by name, set-user-manager to Executive 1, contract sharing rules re-evaluated. Browser: Playwright /opt/pw-browsers/chromium, zh-CN, signed in as each persona. LAUNCH (双向保密协议/极光系统/阿卡迪亚欧洲有限公司, filled title, 摘要, 适用法律, 保密期限, uploaded v1): before fc1 'POST …/contract_intake/runs/…/resume -> 400 FLOW_FAILED … Field write denied … [confidentiality_term_months, governing_law, status, summary]'; rm1 400, same with a long list (amount, auto_renew, contract_type …); br1 合同已发起。. After: fc1, rm1 and br1 all 合同已发起。; submitted runs land in_review, unsubmitted runs stay draft, with governing_law=德国法 and conf=24 stored. FILL (API as persona, current value written back): after, fc1 and rm1 PATCH own draft legal fields -> 200 and commercial fields -> 200 (before: no draft existed); fc1 own draft {status:draft} -> 200; {status:approved} -> 422 state machine; risk_level and submitted_at -> 403 for fc1, rm1 and br1 (requester locks); fc1 POST insert {status:approved, governing_law} -> 403 Field write denied [governing_law, status]; rm1 POST insert {status:signing} -> 403 [contract_type, governing_law, our_entity, party, title]. §13 Q1, identical before and after: fc1 approved governing_law, summary, confidentiality_term_months, status, termination_reason and jurisdiction -> 403 FLS x6; payment_terms+amount -> 200; signing governing_law+status -> 403, payment_terms -> 200; in_approval (finance-routed) -> 403 RLS. rm1 signing title, governing_law, amount and risk_level -> 403 FLS x4; signing status (same value) -> 200; expired archive_no -> 200; expired title -> 403; approved title -> 403 RLS. lc1 in_review governing_law, summary, risk_level and liability_cap -> 200; approved governing_law -> 200. br1 another owner's active -> 404. EXPLAIN (after, update): finance 10 and records 56 conditioned entries named 'locked when status in [...]'. FORM (browser, after): fc1 own draft 编辑 disables 摘要, 适用法律, 保密期限, 管辖, 风险等级 and 状态; saving 标题 -> POST /api/v1/batch 200, toast 已更新. rm1 own draft: only 档案号 enabled. br1 own draft (control): only 风险等级 disabled. Boot-phase WARN lines diff empty before vs after; 0 'row condition … applied as its lock' WARN lines after. REVERSE CHECK: when field changed to statuz -> pnpm validate exit 1 'field permission when field "statuz" is not a field on object "clm_contract"'; restored with git checkout HEAD --, git diff HEAD empty, hash matches HEAD. No ablation beyond that: metadata only, and no suite pins this behaviour.",
"gates": "On feec816, each exit captured before any pipe: pnpm install 0; pnpm validate 0 (8 ⚠, line-identical to origin/main 21f2319, measured on a detached main worktree); pnpm lint 0 ('1 warning(s), 6 suggestion(s)', the one warning #64's security-controlled-by-parent-owner-policy-without-opt-in, identical to main); pnpm typecheck 0; pnpm lint:i18n-gate 0 (0 missing keys across 2 locales); CLM_DEMO_SEED=1 pnpm validate 0. CI on PR #166: not awaited (in_progress at report time).",
"dogfood_battery": "n/a — hotclm, no
!in the title and no (narrowing) arm","mcp_calls": "0",
"api_writes": "2 — relay fleet-write pr_create (#166, read-back identical, assignee zhuangjianguo); post-stamped os-dev-report comment on #127. Plus git push of the branch (not REST).",
"open_questions": [
{
"question": "After this PR, a finance controller or records manager launches a contract, but the Console 编辑 dialog on their own draft disables the row-conditioned fields. The 18.2.0 field projection is row-blind by declared contract ('Readers that hold no row … report a row-conditioned entry as its lock'). For finance that is the legal fields and status; for records it is every field but 档案号. The write gate admits those writes (wizard create, API PATCH 200). Raise a capability request upstream?",
"options": [
"A — ask objectstack for a row-aware field projection on the record form (the form judges
whenagainst the record it edits); hotclm waits, no app change","B — accept: the launch wizard is the door for these fields on a launcher's draft, and later edits go through legal"
],
"recommendation": "A, because the ruling's intent ('a draft its own launcher can fill') is met at launch but not in the form, and the gap is the platform's declared row-blind reader, which hotclm must not work around (AGENTS.md platform gaps)."
}
],
"out_of_scope_findings": [
"class: none (declared platform behaviour, capability request) · reach: Console 编辑 dialog as Finance Controller 1 on own draft UfYFt_Hz3epInWiq, 18.2.0 feec816: 摘要/适用法律/保密期限/管辖/状态 disabled while PATCH of the same fields answers 200 · evidence: PR #166 Acceptance notes, FieldPermissionSchema.when describe · dedupe words: row-conditioned field permission form projection, when editable form disabled, row-blind field readers · carrier: none, noted in PR Acceptance notes",
"carrier: #64 (requester.profile.ts, same round) · finance and records launchers' drafts now rely on clm_requester's contract locks (stamps, risk_level, liability_cap, termination_reason); #64's diff should keep them"
],
"governed_text_proposal": "none — ruling A keeps DESIGN.md §04 and §13 Q1 unchanged"
}
objectstack-fleet commentedon Oct 27, 2026
Ruling-ref: 6406413107
Landed — PR #166 · 2026-10-27
MERGED(squash onmain), merged at its ACCEPT headfeec816;Fixes #127closes this cardRelease: a finance controller or records manager can launch a contract. Their field locks now apply only from
approved(finance) orsigning(records) onward, so §13 Q1 holds.Residual, platform: the console's 编辑 dialog still shows those fields disabled on their own draft, although saving them is allowed. This is filed in objectstack and linked below. State:
pm:dispatchedremoved, assignee cleared.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Residual filed upstream: objectstack-ai/objectstack#26594 (the record form disables row-conditioned fields on every row).
Generated by Claude Code