Skip to content

§06 F16 说补录动作「档案岗或法务可用」,但实现只能给档案岗——权限是 AND,没有两边都持有的能力 - #177

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-43-backfill-capability
Oct 27, 2026
Merged

zhuangjianguo merged 1 commit into
mainfrom
claude/issue-43-backfill-capability

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #43
Part of #99 (item 7)

Ruling 6395823636, letter A: a new capability backfill_contract, held by the records manager, legal and the admin. F16 (补录已签合同) is now gated on it, at the action and at the flow's start door. #99 item 7 (ruling 6398273819, letter A) rides along: the backfill dialog asks for the business owner, and the flow writes it.

What changed

File Change
objectstack.config.ts declares backfill_contract (org scope). execute_contract's description no longer claims the backfill.
src/profiles/{records,legal,admin}.profile.ts each grants backfill_contract. The docblocks now name it (admin: "all seven action gates").
src/actions/contract-backfill.actions.ts requiredPermissions: ['backfill_contract'] (was execute_contract). New required param { field: 'owner_id' }, which inherits the user picker and the 业务承办人 label. The header's "half of it this platform cannot express" note is gone. The new header says why the list is declared twice.
src/flows/executed-upload.flow.ts type: 'autolaunched' becomes type: 'screen'. runAs: 'system' is kept. New flow-level requiredPermissions: ['backfill_contract']. New input owner_id, written on create_contract as ref('owner_id'). The header explains the start gate and the elevation.
src/translations/{en,zh-CN}/objects.contract.ts executed_upload.params.owner_id.helpText.

Why type: 'screen', and why runAs: 'system' stays

On 18.2.0 every door that starts a flow by name asks flowStartRefusal (objectstack#25643, commit f1ddace) two questions, in this order:

  1. Is the flow runAs: 'system' with a self-triggered type (autolaunched, record_change, schedule)? If so, only the system principal may start it, and no capability opens it. A flow-level requiredPermissions on the old autolaunched flow would therefore have changed nothing. Records Manager was still refused.
  2. Does the caller hold every capability in the flow's requiredPermissions? If not, the answer is 403 PERMISSION_DENIED and nothing runs.

The spec's own remedy for row 1 is "If members are meant to start it, declare it type: 'screen' (or 'api' for a signed inbound hook)" (content/docs/automation/flows.mdx, the 403 row). F16 is a member's button, so I declared it screen. The flow has no screen node, because the action's dialog collects every input, so a run goes straight from start to its end or to a refusing end. I did not use api, because that type is an HMAC-signed webhook and the engine refuses to arm one without a per-flow secret (trigger-api).

runAs: 'system' stays because the writes still need it:

  • the insert of a contract born active (contract_type_stamp admits that only for a system write, §13 Q8);
  • the fields §04 locks for every position (is_backfilled, signed_at, executed_at, activated_at);
  • the version filing for a caller whose own grants cannot do it (clm_records has allowCreate: false on clm_contract).

The caller's identity still rides through, so created_by names the backfiller (measured below).

The action repeats the flow's list on purpose. The 18.2.0 listing faces (the console action strip and MCP list_actions) read only the action's own requiredPermissions, as #25643's acceptance notes say. Without the repeat every member would see a button that answers 403.

Evidence: pnpm demo:zh on 18.2.0, port 3151, throwaway DB each run

  • Runs: before = origin/main 0159171; after = f1e7027. Each run started from a fresh .objectstack and dist.
  • Personas: the declared demo staff (PR pnpm demo creates its demo staff, so every persona opens 我的合同 out of the box #171), password admin123. The one manual step was POST /api/v1/auth/admin/set-user-manager for requester1/2/3 → Executive, which answered 200 ×3 in both runs.
  • Browser: Playwright on /opt/pw-browsers/chromium, signed in through the Console form as each persona, locale zh-CN unless marked en.
  • Boot: the boot-phase WARN/⚠ lines are identical before and after once timestamps are stripped (diff empty). Each run has 120 contracts seeded.

Backfill end to end through 补录已签合同. The dialog was filled with type, party, title, signing date 2025-06-01, executed.pdf, signing entity and, after the change, 业务承办人.

Persona before 0159171 after f1e7027
records@example.com (Records Manager) button shown; POST /api/v1/actions/clm_contract/executed_upload → 403 PERMISSION_DENIED "This caller may not start this flow. A flow that runs on its own trigger starts there, or as a sub-flow from a parent flow's subflow node."; the dialog shows it inline; 0 contracts written 200 completed; dialog closes; DPA-2026-0011 status=active, is_backfilled=true, signed_at = executed_at = activated_at = 2025-06-01T12:00:00Z, version_count=1; version 1 kind=final_signed, is_current=true, file executed.pdf; created_by = Records Manager, owner_id = Business Requester 2 (as picked)
counsel1@example.com (Legal Counsel 1) no button; API action door 403 "requires capability [execute_contract]" button shown; 200, toast 已签合同已登记。; DPA-2026-0012 active, backfilled, same stamps, final_signed v1; created_by = Legal Counsel 1, owner_id = Business Requester 1
clm.admin@example.com (CLM Administrator) button shown; 403 "This caller may not start this flow" 200; DPA-2026-0013 active, backfilled, final_signed v1; created_by = CLM Administrator, owner_id = Business Requester 3
requester1@example.com no button no button: not in the toolbar, and in neither the zh-CN nor the en "More actions" menu (en menu: Launch Contract, Add to favorites)

In en, counsel1 finds Backfill Executed Contract in the list's "More actions" menu (the English labels overflow the toolbar).

#99 item 7: the owner is the one named. Business Requester 2 signed in and opened 我的合同 → 我发起的 (/clm_contract/view/my_contracts). The view lists #43 after rm ok zh-CN. GET /api/v1/data/clm_contract as Business Requester 2 returns it with owner_id = their id. Before this change the dialog had no owner field and the door refused every caller, so the before side could not be measured.

A requester at the doors (API, signed in as the persona).

Persona Door before after
requester1, requester2, finance POST /api/v1/actions/clm_contract/executed_upload 403 "requires capability [execute_contract]" 403 PERMISSION_DENIED "Action 'executed_upload' on 'clm_contract' requires capability [backfill_contract] — caller is missing [backfill_contract]"
requester1, requester2, finance POST /api/v1/automation/executed_upload/trigger 403 "This caller may not start this flow…" 403 PERMISSION_DENIED "Starting this flow requires capability [backfill_contract] — caller is missing [backfill_contract]" (the flow-level gate)
counsel1 (future date) both doors 403 / 403 200 status: refused (admitted; the flow's own refusal)

0 contracts written by any door probe.

The four refusing ends from PR #170. Before, Records Manager reached none of them: every door answered 403. After, all four were read as Records Manager. Each answer is 200 { status: "refused", refusalMessage }, and the message is the app's sentence alone, with no engine chain.

End How reached zh-CN en
refuse_date_missing trigger route, signed_date omitted or "" (the action door refuses first: 400 Action param "signed_date" is required) 必须填写签署日期:补录的是已经签署的合同,签署日期正是要登记的事实。 A signing date is required: a backfilled contract is one that was already executed, and the date it was executed is the fact being recorded.
refuse_date_invalid trigger route, not-a-date (the action door refuses first: 400 expected YYYY-MM-DD) 签署日期「not-a-date」不是有效日期。 The signing date "not-a-date" is not a date.
refuse_date_future browser 2030-01-15, and both API doors notice 补录已签合同 · 签署日期在未来。补录登记的是已经签署的合同。 · 关闭 notice Backfill Executed Contract · The signing date is in the future. A backfill records a contract that has already been executed. · Close
refuse_party browser: a party Legal Counsel 1 flags blocked while the dialog is open; API: party id no-such-party notice 补录已签合同 · 这个相对方不能使用:它不存在,或已被标记为「禁止合作」。相对方禁止合作的合同不予登记,补录也不例外。 · 关闭 notice Backfill Executed Contract · That counterparty cannot be used: it is either missing or flagged blocked. A contract with a blocked counterparty is not recorded, even retrospectively. · Close

For the browser readings, the language is the Console's. For the API readings it comes from Accept-Language. No refused run wrote a contract or a version. Run history (GET /api/v1/automation/executed_upload/runs, after): refused ×21, completed ×3, failed 0.

Reverse check. I changed the flow's list to backfill_contrakt, and pnpm validate printed ⚠ flow "executed_upload": requiredPermissions names capability "backfill_contrakt", which no built-in, defineCapability or systemPermissions grant provides, so this gate fails closed (rule capability-reference-unknown at flows[11].requiredPermissions; a warning, exit 0). I restored the file with git checkout HEAD --. Afterwards git diff HEAD was empty and the blob hash matched HEAD.

Gates: on f1e7027, each exit code captured before any pipe

pnpm install                    exit 0
pnpm validate                   exit 0   ✓ Validation passed (7 ⚠: hierarchy-security edition + 6 approval-approvers-may-resolve-empty, the set main prints)
pnpm lint                       exit 0   6 suggestion(s), 0 warnings (origin/main 0159171: the same)
pnpm typecheck                  exit 0
pnpm lint:i18n-gate             exit 0   COVERAGE : 0 missing keys across 2 locale(s)
CLM_DEMO_SEED=1 pnpm validate   exit 0

Governed text (not in this diff: it rides the seat's design-doc PR #120)

  • DESIGN.md §04, the action-gate line. In the list after 「动作门控同时在 UI 与服务端生效(ADR-0066 D4):」, add backfill_contract after execute_contract, then add one sentence: 「backfill_contract(F16 补录已签合同)由 clm_records、clm_legal、clm_admin 持有,同时声明在动作与流程 executed_upload 的 requiredPermissions 上。」
  • DESIGN.md §06 F16, the row's last clause. Replace 「仅 clm_records.access 与 clm_legal.access 可用」 with 「持有 backfill_contract(档案、法务、管理员)者可用;对话框必填业务承办人 owner_id,legal_owner 仍由法务在记录上指定」.

Acceptance notes

  • A screen flow with no screen node is new in this app. The Console treats it like any other flow action: the params dialog, then the run's answer. pnpm validate and lint raise nothing about it.
  • The flow's input owner_id is read through ref(). A headless caller on the trigger route that omits it gets the platform's stamp (the acting user). The action door requires it.
  • legal_owner is still not asked (§03 keeps the two fields separate). The seat's note on this card (13:00Z, from 34 backfilled contracts carry a legal_owner, and is_backfilled says on the same row that no review ever happened #53) leaves an optional legal_owner on the form open, so it is not built here.
  • The console's action strip hides the button using the action's own list. Inheriting the flow's list would let the action declare nothing, but then every member would see a button that answers 403. That is a 18.2.0 listing behaviour recorded in objectstack#25643's acceptance notes, not a defect here.
  • No ablation: no test suite pins this behaviour, and the change is metadata. The reverse check above is the one-time proof.

🤖 Generated with Claude Code

https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn

DESIGN.md §06 F16 lets the records manager or legal backfill an executed
contract, but the action was gated on execute_contract, which legal does
not hold, and on 18.x the autolaunched + runAs system flow refused every
non-system caller at its start door. Ruling A on #43: a new capability,
backfill_contract, held by clm_records, clm_legal and clm_admin.

- objectstack.config.ts declares backfill_contract; execute_contract no
  longer claims the backfill.
- The three profiles grant it.
- executed_upload becomes a screen flow (still runAs system for the active
  birth and the locked stamps) with flow-level requiredPermissions, the
  18.2 start gate (objectstack#25643). The action repeats the list so the
  button hides for everyone else.
- The dialog asks for the business owner and the flow writes owner_id
  (#99 item 7), with help text in both bundles.

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 · head f1e7027 · 2026-10-27T11:32Z

check reading
shape draft → main, Fixes #43 + Part of #99 (item 7); mergeable clean
CI on the head both Validate check-runs success on f1e7027 = PR head.sha
scope 8 files (+102/−33): the backfill_contract capability in objectstack.config.ts (execute_contract no longer claims backfill); grants in the records, legal and admin profiles; the same list on the F16 action and on the executed_upload flow; an owner_id param with helpText in en and zh-CN. Nothing governed; lint 0 warnings
autolaunched → screen accepted. On 18.2.0 flowStartRefusal refuses every non-system caller of a runAs: 'system' self-triggered flow, and no capability opens that rule. A member-started entry is screen by the spec's own definition, and an elevated screen flow is the author's explicit choice (ADR-0073 D2). api would need an HMAC secret. runAs: 'system' stays, because the run writes §04-locked stamps and a contract born active
list declared twice accepted. The flow's list is the gate. The action's copy only hides the button, because the console strip and MCP list_actions read the action alone (objectstack#25643 notes this)
the defect measured in zh-CN with the declared staff. Before: records@ and clm.admin@ got 403 「This caller may not start this flow」, and counsel1@ had no button. After: all three backfill (DPA-2026-0011/12/13 are active, is_backfilled, with a final_signed current version). requester1 has no button, and the API refuses requester1, requester2 and finance at the action and at the trigger route, writing 0 rows. The four #170 refusals still answer in both locales
#99 item 7 owner_id is required and is written as given. records@ picked Business Requester 2, and BR2 sees the contract under 我的合同 → 我发起的
open question: legal_owner on the form (#53 seat note) seat call: not added. Ruling A and §03 keep owner_id and legal_owner separate, legal assigns the custodian lawyer on the record (help text since PR #115), and the form stays one business-side question. The maintainer can reopen this as a card
governed text the §04 backfill_contract row and the §06 F16 sentence go to the seat's design PR #120

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.

[Decision] §06 F16 说补录动作「档案岗**或**法务可用」,但实现只能给档案岗——权限是 AND,没有两边都持有的能力

2 participants