@@ -61,15 +61,90 @@ The package has four gate states:
6161| ` escalated ` | Does not run until the host approval path resolves. | Signs the escalation decision and outcome. |
6262| ` policy_error ` | Does not run the action body. | Signs that the policy evaluator failed closed. |
6363
64- ` verifyActionGateRun() ` checks signatures, record hashes, decision-to-outcome
65- binding, action id consistency, and the rule that blocked, escalated, and
66- policy-error states did not execute.
64+ ` verifyActionGateRun() ` checks signatures, record hashes, canonical entry IDs,
65+ event types, tool names, argument and result commitments, host identity,
66+ context continuity, decision-to-outcome binding, action id consistency, and the
67+ rule that blocked, escalated, and policy-error states did not execute.
6768
6869` runGatedAction() ` returns both signed records and local sidecars. If ` onRecord `
6970throws while delivering a signed record to a mirror, log sink, or proof-packet
7071writer, the action result still returns a complete decision/outcome pair and
7172adds the callback failure to ` record_delivery_errors ` .
7273
74+ ## Split-phase and hash-only hosts
75+
76+ Some hosts decide first and receive an execution report later. They can build
77+ and sign the decision and outcome separately:
78+
79+ ``` ts
80+ import {
81+ buildActionGateDecisionEntry ,
82+ buildActionGateOutcomeEntry ,
83+ hashCanonical ,
84+ signActionGateDecision ,
85+ signActionGateOutcome ,
86+ type ActionGateActionEnvelope ,
87+ } from ' @atrib/action-gate'
88+
89+ const action = {
90+ run_id: ' refund-run-1042' ,
91+ action_id: ' refund-order' ,
92+ agent_id: ' support-agent' ,
93+ surface: ' support' ,
94+ tool_name: ' refund.order' ,
95+ args_digest: hashCanonical ({
96+ orderId: ' 1042' ,
97+ amount: ' 284.00' ,
98+ currency: ' USD' ,
99+ }),
100+ } satisfies ActionGateActionEnvelope
101+
102+ const decisionEntry = buildActionGateDecisionEntry ({
103+ action ,
104+ policy ,
105+ timestamp: new Date ().toISOString (),
106+ })
107+ const decision = await signActionGateDecision ({
108+ entry: decisionEntry ,
109+ action ,
110+ privateKey ,
111+ contextId ,
112+ timestampMs: Date .now (),
113+ })
114+
115+ const outcomeEntry = buildActionGateOutcomeEntry ({
116+ status: ' executed' ,
117+ run_id: action .run_id ,
118+ action_id: action .action_id ,
119+ decision_id: decision .entry .decision_id ,
120+ decision_record_hash: decision .record_hash ,
121+ executed: true ,
122+ result_digest: hashCanonical ({
123+ executionState: ' executed' ,
124+ resultHash: hashCanonical ({ status: ' accepted' }),
125+ outcomeHash: hashCanonical ({ refundId: ' re_1042' }),
126+ }),
127+ timestamp: new Date ().toISOString (),
128+ })
129+ const outcome = await signActionGateOutcome ({
130+ entry: outcomeEntry ,
131+ action ,
132+ privateKey ,
133+ contextId ,
134+ decisionRecordHash: decision .record_hash ,
135+ chainTailHex: decision .record_hash .slice (' sha256:' .length ),
136+ timestampMs: Date .now (),
137+ })
138+ ```
139+
140+ Use ` args_digest ` or ` result_digest ` when the signing process has the canonical
141+ SHA-256 commitment but must not receive the raw payload. Each value must use the
142+ ` sha256:<64 lowercase hex> ` form.
143+
144+ An allowed action that never ran can use ` status: 'not_executed' ` with
145+ ` executed: false ` . This records the difference between policy permission and
146+ runtime execution without treating the runtime decision as an error.
147+
73148## Trusted-transaction policy
74149
75150` requireTrustedTransaction() ` is a ready-made ` evaluate ` policy for transaction
0 commit comments