Skip to content

Commit 2a58186

Browse files
authored
fix: harden action-gate proof verification
1 parent 0884ae2 commit 2a58186

4 files changed

Lines changed: 1341 additions & 11 deletions

File tree

.changeset/bright-pears-check.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@atrib/action-gate': patch
3+
---
4+
5+
Reject contradictory action-gate entries during signing and verification. Add
6+
precomputed digests and truthful not-executed outcomes for split-phase hosts.

packages/action-gate/README.md

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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`
6970
throws while delivering a signed record to a mirror, log sink, or proof-packet
7071
writer, the action result still returns a complete decision/outcome pair and
7172
adds 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

Comments
 (0)