Repository navigation
§06 F16 说补录动作「档案岗或法务可用」,但实现只能给档案岗——权限是 AND,没有两边都持有的能力 - #177
Merged
Merged
Conversation
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
Contributor
Author
席位核对 — ACCEPT · head
|
| 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
zhuangjianguo
marked this pull request as ready for review
October 27, 2026 11:32
This was referenced Oct 27, 2026
Draft
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 #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
objectstack.config.tsbackfill_contract(org scope).execute_contract's description no longer claims the backfill.src/profiles/{records,legal,admin}.profile.tsbackfill_contract. The docblocks now name it (admin: "all seven action gates").src/actions/contract-backfill.actions.tsrequiredPermissions: ['backfill_contract'](wasexecute_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.tstype: 'autolaunched'becomestype: 'screen'.runAs: 'system'is kept. New flow-levelrequiredPermissions: ['backfill_contract']. New inputowner_id, written oncreate_contractasref('owner_id'). The header explains the start gate and the elevation.src/translations/{en,zh-CN}/objects.contract.tsexecuted_upload.params.owner_id.helpText.Why
type: 'screen', and whyrunAs: 'system'staysOn 18.2.0 every door that starts a flow by name asks
flowStartRefusal(objectstack#25643, commitf1ddace) two questions, in this order: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-levelrequiredPermissionson the oldautolaunchedflow would therefore have changed nothing. Records Manager was still refused.requiredPermissions? If not, the answer is403 PERMISSION_DENIEDand 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 itscreen. 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 useapi, 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:active(contract_type_stampadmits that only for a system write, §13 Q8);is_backfilled,signed_at,executed_at,activated_at);clm_recordshasallowCreate: falseonclm_contract).The caller's identity still rides through, so
created_bynames 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 ownrequiredPermissions, as #25643's acceptance notes say. Without the repeat every member would see a button that answers 403.Evidence:
pnpm demo:zhon 18.2.0, port 3151, throwaway DB each runorigin/main0159171; after =f1e7027. Each run started from a fresh.objectstackanddist.POST /api/v1/auth/admin/set-user-managerfor requester1/2/3 → Executive, which answered 200 ×3 in both runs./opt/pw-browsers/chromium, signed in through the Console form as each persona, localezh-CNunless markeden.WARN/⚠lines are identical before and after once timestamps are stripped (diffempty). 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, 业务承办人.0159171f1e7027POST /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'ssubflownode."; the dialog shows it inline; 0 contracts writtenDPA-2026-0011status=active,is_backfilled=true,signed_at = executed_at = activated_at = 2025-06-01T12:00:00Z,version_count=1; version 1kind=final_signed,is_current=true, fileexecuted.pdf;created_by= Records Manager,owner_id= Business Requester 2 (as picked)DPA-2026-0012active, backfilled, same stamps, final_signed v1;created_by= Legal Counsel 1,owner_id= Business Requester 1DPA-2026-0013active, backfilled, final_signed v1;created_by= CLM Administrator,owner_id= Business Requester 3In 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_contractas Business Requester 2 returns it withowner_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).
POST /api/v1/actions/clm_contract/executed_uploadPERMISSION_DENIED"Action 'executed_upload' on 'clm_contract' requires capability [backfill_contract] — caller is missing [backfill_contract]"POST /api/v1/automation/executed_upload/triggerPERMISSION_DENIED"Starting this flow requires capability [backfill_contract] — caller is missing [backfill_contract]" (the flow-level gate)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.refuse_date_missingsigned_dateomitted or""(the action door refuses first: 400Action param "signed_date" is required)refuse_date_invalidnot-a-date(the action door refuses first: 400expected YYYY-MM-DD)refuse_date_futurerefuse_partyno-such-partyFor 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,failed0.Reverse check. I changed the flow's list to
backfill_contrakt, andpnpm validateprinted⚠ flow "executed_upload": requiredPermissions names capability "backfill_contrakt", which no built-in, defineCapability or systemPermissions grant provides, so this gate fails closed(rulecapability-reference-unknownatflows[11].requiredPermissions; a warning, exit 0). I restored the file withgit checkout HEAD --. Afterwardsgit diff HEADwas empty and the blob hash matched HEAD.Gates: on
f1e7027, each exit code captured before any pipeGoverned 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):」, addbackfill_contractafterexecute_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
screenflow 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 validateandlintraise nothing about it.owner_idis read throughref(). A headless caller on the trigger route that omits it gets the platform's stamp (the acting user). The action door requires it.legal_owneris still not asked (§03 keeps the two fields separate). The seat's note on this card (13:00Z, from 34 backfilled contracts carry alegal_owner, andis_backfilledsays on the same row that no review ever happened #53) leaves an optionallegal_owneron the form open, so it is not built here.🤖 Generated with Claude Code
https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn