From 0843d788f9aa3a114b72baddfc2164bfd2a66fc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 17:42:07 +0000 Subject: [PATCH] feat(security): let the finance rung read the contracts routed to it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The finance controller co-signs at in_approval whenever the matrix stamps route_finance (F5), yet it reached contracts only from approved onward, so the approval request it acts on read "无法打开此记录". Add the contract_finance_routed criteria rule (route_finance == true, read, the clm_finance_controller position), the same shape as the executive and general-manager routed rules. The edit window is unchanged. README: the finance position's row says what it reads, and the rung-1 section records that on the open edition the direct manager decides from the request alone until objectstack-ai/objectstack#26180 ships. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn --- README.md | 14 +++++++++++++- src/sharing/contract.sharing.ts | 25 ++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5ebb0bb..a8ef843 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ requester a manager ([below](#rung-1-of-the-approval-ladder-is-a-manager)): |---|---|---| | `clm_legal_counsel` | 2 | Receives contracts on submission (round-robin by open load) and owns review. **Name these two exactly** — see below | | `clm_legal_head` | 1 | Rung 2 of the approval ladder whenever `route_legal_head` is stamped | -| `clm_finance_controller` | 1 | Rung 2/3, and the only position that may edit bank details | +| `clm_finance_controller` | 1 | Rung 2/3, reads every contract with `route_finance`, and the only position that may edit bank details | | `clm_executive` | 1 | Rung 4, and reads every contract with `route_executive` | | `clm_general_manager` | 1 | Rung 5, and reads every contract with `route_gm` | | `clm_records_manager` | 1 | Execution formalities, the executed copy and the archive number | @@ -190,6 +190,18 @@ waiting on them and **我的合同 › Waiting on Me** lists them. A reminder se a request does arrive as a message; the missing one on opening is the platform's, not this app's. +**On the open edition the rung-1 manager decides from the request alone.** +The approval request shows the contract's fields, but its record link reads +「无法打开此记录」: being the owner's manager grants no read. The read that +would (`own_and_reports`, DESIGN.md §04 `contract_manager_reports`) is an +enterprise-edition hierarchy scope that the open edition resolves to +owner-only, and a sharing rule cannot name "the owner's manager". Unless the +manager's own position already reaches the contract — the executive on a +contract stamped `route_executive`, say — its page, versions and reviews stay +closed to them until the platform lets an approver read the record a step +waits on (upstream `objectstack-ai/objectstack#26180`). The rungs after it are +positions, and each reads the contracts it approves. + ### Assigning a position from a script Clicking through Setup → Users needs nothing more than the table above. Automating it does: the diff --git a/src/sharing/contract.sharing.ts b/src/sharing/contract.sharing.ts index 71c7e34..df220e2 100644 --- a/src/sharing/contract.sharing.ts +++ b/src/sharing/contract.sharing.ts @@ -9,7 +9,8 @@ import { CLM_POSITION } from './positions.js'; * `private`; the five children are `controlled_by_parent` and follow it). * * §04 lists six rows. Five are criteria rules and live here (as seven rows — - * see the shapes below). The sixth, + * see the shapes below), and ruling A on #134 adds an eighth, + * `contract_finance_routed`. The sixth, * `contract_manager_reports`, is not a rule at all: it is the * `own_and_reports` DEPTH scope on the `clm_requester` grant * (`src/profiles/requester.profile.ts`), exactly as the §04 table spells it. @@ -106,6 +107,27 @@ export const ContractRecordsApprovedReadSharingRule = defineSharingRule({ sharedWith: { type: 'position', value: CLM_POSITION.recordsManager }, }); +/** + * Ruling A on #134: the finance controller is the `route_finance` rung of the + * approval ladder (F5's legal-plus-finance co-sign), so it reads the contracts + * the matrix routed to it, as the executive and general-manager rungs do below. + * Same shape as those two — no status window: the flag is stamped at + * submission and re-evaluated on every resubmission (F2), so it names exactly + * the contracts finance approves. Read only: finance's edit window stays + * `contract_finance_post_approval` above and the `clm_finance` update row + * policy, both from `approved` onward. + */ +export const ContractFinanceRoutedSharingRule = defineSharingRule({ + name: 'contract_finance_routed', + label: 'Contracts — Finance (routed)', + description: 'The finance controller reads the contracts the approval matrix routed to the finance rung.', + object: 'clm_contract', + type: 'criteria', + condition: P`record.route_finance == true`, + accessLevel: 'read', + sharedWith: { type: 'position', value: CLM_POSITION.financeController }, +}); + export const ContractExecutiveRoutedSharingRule = defineSharingRule({ name: 'contract_executive_routed', label: 'Contracts — Executive (routed)', @@ -135,6 +157,7 @@ export const ClmSharingRules = [ ContractFinancePostApprovalSharingRule, ContractRecordsExecutionSharingRule, ContractRecordsApprovedReadSharingRule, + ContractFinanceRoutedSharingRule, ContractExecutiveRoutedSharingRule, ContractGmRoutedSharingRule, ];