@agenticprimitives/contracts 1.0.0-alpha.24 → 1.0.0-alpha.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AUDIT.md CHANGED
@@ -52,6 +52,11 @@ This package ships the **on-chain enforcement layer** for the agenticprimitives
52
52
  | 2.12 | **CustodyPolicy reinstall is permanently forbidden.** | ✅ CLOSED audit C-11 | `CustodyPolicy.sol:onUninstall` + `onInstall` reinstall gate; tested in `CustodyPolicyWave2C.t.sol` |
53
53
  | 2.13 | **`SmartAgentPaymaster.getHash` binds `address(entryPoint)` + chainId.** | ✅ CLOSED H7-C.7 / CON-PAYMASTER-004 | `SmartAgentPaymaster.sol:206-222`; tested in `SmartAgentPaymasterValidateR610.t.sol` |
54
54
  | 2.14 | **`SmartAgentPaymaster` constructor takes EXPLICIT `devMode` (no implicit fail-open).** | ✅ CLOSED R5.7 / PKG-PAYMASTER-002 / external audit P0-2 | `SmartAgentPaymaster.sol:126-141`; tested in `SmartAgentPaymaster.t.sol::test_R5_7_*` |
55
+ | 2.15 | **The account's `execute` never reaches its own custody-tier contracts from below the custody tier.** `CustodyPolicy.onUninstall` reverts `StillInstalledOn` unless `uninstallModule` already dropped the module; a delegation stack (`_delegationExecCtx`) may not target `custodyPolicy` or `approvedHashRegistry` at all. | ✅ CLOSED R917-C-1 (spec 408 §1.1) | `CustodyPolicy.sol:onUninstall`, `AgentAccount.sol:_isAccountTrustedTarget`; `test/HardeningR917.t.sol` |
56
+ | 2.16 | **An approved hash dies with the custody set that approved it.** The `0x03` sentinel consults `isApproved(this, keccak256(hash ‖ custodyEpoch))`; `custodyEpoch` bumps on every credential retirement (`removeCustodian` / `removePasskey`); `ApprovedHashRegistry.revokeAll()` voids per signer. | ✅ CLOSED R917-C-2 (spec 408 §1.2) | `AgentAccount.sol:approvedHashKey`, `ApprovedHashRegistry.sol`; `test/HardeningR917.t.sol`, `test/ApprovedHashIsValidSignature.t.sol` |
57
+ | 2.17 | **A digest-bound redemption is single-use per step.** `DigestBindingEnforcer` is stateful + DM-only: `(delegator, delegationHash, kind, stepNonce)` consumed in `beforeHook`. | ✅ CLOSED R917-C-4 (spec 408 §1.4) | `enforcers/DigestBindingEnforcer.sol`; `test/DigestBindingEnforcer.t.sol` |
58
+ | 2.18 | **Network posture is a constructor fact, never a chain-id list.** `AgentAccountFactory.relaxedT4Floor` (immutable) is the only way to lift the 1 h T4 floor; recorded in the deployments JSON; refused by `check:production-deploy` off anvil. | ✅ CLOSED R917-C-5 (spec 408 §1.3) | `AgentAccountFactory.sol:_custodyInitData`; `test/AgentAccountFactoryMode.t.sol` |
59
+ | 2.19 | **Receipt anchors are keyed by who anchored.** `(anchoredBy, receiptDigest)` — no squat on a public mempool. | ✅ CLOSED R917-C-3 (spec 408 §1.5) | `provenance/ReceiptAnchorRegistry.sol`; `test/provenance/ReceiptAnchorRegistry.t.sol` |
55
60
 
56
61
  ## 3. Per-contract notes for the external review
57
62
 
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @agenticprimitives/contracts
2
2
 
3
+ > Part of **[Agentic Primitives](https://agenticprimitives.dev)** — the open-source trust substrate for agentic applications: identity that can sign, authority checked at act time, evidence the owner carries. [Developer kit](https://github.com/agentictrustlabs/agentic-primitives) · [All packages](https://agenticprimitives.dev/developers)
4
+
3
5
  **Contracts and SDK are one artifact.**
4
6
 
5
7
  This is the on-chain enforcement layer of the agenticprimitives substrate — 42 Solidity sources, all present and audited under `src/`, none of them stubs. ERC-4337 v0.7 Smart Agent core, ERC-7710-style delegation manager with caveat enforcers, an ERC-7579 modular CustodyPolicy, a three-mode paymaster, `.agent` TLD naming with hierarchical subregistries, ontology and profile resolvers, identity facets, and an ERC-6492-aware UniversalSignatureValidator. The package publishes ABI subpaths, per-network deployment JSON, flattened sources, and the Solidity itself — so a TypeScript consumer pulls addresses and ABIs from the same artifact the auditors read.
@@ -45,5 +45,6 @@
45
45
  "household": "0x9c3373E8F28739Df3aA478C18209fD7eeD769F5b"
46
46
  },
47
47
  "agentRegistryBase": "0x0Be172F1b5cfD1d084CEc2cF2Af0A0cE9023Deb1",
48
- "digestBindingEnforcer": "0xA3bb9BCC9b2F6F2419E1aBe5ED6Fd5399b9E68e1"
48
+ "digestBindingEnforcer": "0xA3bb9BCC9b2F6F2419E1aBe5ED6Fd5399b9E68e1",
49
+ "receiptAnchorRegistry": "0x0e339334B4438F7546FD53837FDC8943cB3Cf54C"
49
50
  }
@@ -99,6 +99,25 @@
99
99
  "outputs": [],
100
100
  "stateMutability": "nonpayable"
101
101
  },
102
+ {
103
+ "type": "function",
104
+ "name": "approvedHashKey",
105
+ "inputs": [
106
+ {
107
+ "name": "hash",
108
+ "type": "bytes32",
109
+ "internalType": "bytes32"
110
+ }
111
+ ],
112
+ "outputs": [
113
+ {
114
+ "name": "",
115
+ "type": "bytes32",
116
+ "internalType": "bytes32"
117
+ }
118
+ ],
119
+ "stateMutability": "view"
120
+ },
102
121
  {
103
122
  "type": "function",
104
123
  "name": "bundlerSigner",
@@ -138,6 +157,19 @@
138
157
  ],
139
158
  "stateMutability": "view"
140
159
  },
160
+ {
161
+ "type": "function",
162
+ "name": "custodyEpoch",
163
+ "inputs": [],
164
+ "outputs": [
165
+ {
166
+ "name": "",
167
+ "type": "uint64",
168
+ "internalType": "uint64"
169
+ }
170
+ ],
171
+ "stateMutability": "view"
172
+ },
141
173
  {
142
174
  "type": "function",
143
175
  "name": "decodeWebAuthnAssertion",
@@ -1071,6 +1103,19 @@
1071
1103
  ],
1072
1104
  "anonymous": false
1073
1105
  },
1106
+ {
1107
+ "type": "event",
1108
+ "name": "CustodyEpochBumped",
1109
+ "inputs": [
1110
+ {
1111
+ "name": "epoch",
1112
+ "type": "uint64",
1113
+ "indexed": false,
1114
+ "internalType": "uint64"
1115
+ }
1116
+ ],
1117
+ "anonymous": false
1118
+ },
1074
1119
  {
1075
1120
  "type": "event",
1076
1121
  "name": "DelegationManagerRotated",
@@ -36,6 +36,11 @@
36
36
  "name": "approvedHashRegistry_",
37
37
  "type": "address",
38
38
  "internalType": "address"
39
+ },
40
+ {
41
+ "name": "relaxedT4Floor_",
42
+ "type": "bool",
43
+ "internalType": "bool"
39
44
  }
40
45
  ],
41
46
  "stateMutability": "nonpayable"
@@ -289,6 +294,19 @@
289
294
  ],
290
295
  "stateMutability": "view"
291
296
  },
297
+ {
298
+ "type": "function",
299
+ "name": "relaxedT4Floor",
300
+ "inputs": [],
301
+ "outputs": [
302
+ {
303
+ "name": "",
304
+ "type": "bool",
305
+ "internalType": "bool"
306
+ }
307
+ ],
308
+ "stateMutability": "view"
309
+ },
292
310
  {
293
311
  "type": "function",
294
312
  "name": "sessionIssuer",
@@ -17,12 +17,12 @@
17
17
  "name": "approved",
18
18
  "inputs": [
19
19
  {
20
- "name": "",
20
+ "name": "signer",
21
21
  "type": "address",
22
22
  "internalType": "address"
23
23
  },
24
24
  {
25
- "name": "",
25
+ "name": "hash",
26
26
  "type": "bytes32",
27
27
  "internalType": "bytes32"
28
28
  }
@@ -36,6 +36,25 @@
36
36
  ],
37
37
  "stateMutability": "view"
38
38
  },
39
+ {
40
+ "type": "function",
41
+ "name": "epochOf",
42
+ "inputs": [
43
+ {
44
+ "name": "signer",
45
+ "type": "address",
46
+ "internalType": "address"
47
+ }
48
+ ],
49
+ "outputs": [
50
+ {
51
+ "name": "",
52
+ "type": "uint64",
53
+ "internalType": "uint64"
54
+ }
55
+ ],
56
+ "stateMutability": "view"
57
+ },
39
58
  {
40
59
  "type": "function",
41
60
  "name": "isApproved",
@@ -60,6 +79,13 @@
60
79
  ],
61
80
  "stateMutability": "view"
62
81
  },
82
+ {
83
+ "type": "function",
84
+ "name": "revokeAll",
85
+ "inputs": [],
86
+ "outputs": [],
87
+ "stateMutability": "nonpayable"
88
+ },
63
89
  {
64
90
  "type": "function",
65
91
  "name": "revokeHash",
@@ -73,6 +99,25 @@
73
99
  "outputs": [],
74
100
  "stateMutability": "nonpayable"
75
101
  },
102
+ {
103
+ "type": "event",
104
+ "name": "AllHashesRevoked",
105
+ "inputs": [
106
+ {
107
+ "name": "signer",
108
+ "type": "address",
109
+ "indexed": true,
110
+ "internalType": "address"
111
+ },
112
+ {
113
+ "name": "epoch",
114
+ "type": "uint64",
115
+ "indexed": false,
116
+ "internalType": "uint64"
117
+ }
118
+ ],
119
+ "anonymous": false
120
+ },
76
121
  {
77
122
  "type": "event",
78
123
  "name": "HashApproved",
@@ -1076,6 +1076,17 @@
1076
1076
  }
1077
1077
  ]
1078
1078
  },
1079
+ {
1080
+ "type": "error",
1081
+ "name": "StillInstalledOn",
1082
+ "inputs": [
1083
+ {
1084
+ "name": "account",
1085
+ "type": "address",
1086
+ "internalType": "address"
1087
+ }
1088
+ ]
1089
+ },
1079
1090
  {
1080
1091
  "type": "error",
1081
1092
  "name": "SystemPaused",
@@ -1,4 +1,15 @@
1
1
  [
2
+ {
3
+ "type": "constructor",
4
+ "inputs": [
5
+ {
6
+ "name": "delegationManager_",
7
+ "type": "address",
8
+ "internalType": "address"
9
+ }
10
+ ],
11
+ "stateMutability": "nonpayable"
12
+ },
2
13
  {
3
14
  "type": "function",
4
15
  "name": "afterHook",
@@ -62,12 +73,12 @@
62
73
  "internalType": "bytes"
63
74
  },
64
75
  {
65
- "name": "",
76
+ "name": "delegationHash",
66
77
  "type": "bytes32",
67
78
  "internalType": "bytes32"
68
79
  },
69
80
  {
70
- "name": "",
81
+ "name": "delegator",
71
82
  "type": "address",
72
83
  "internalType": "address"
73
84
  },
@@ -93,7 +104,91 @@
93
104
  }
94
105
  ],
95
106
  "outputs": [],
96
- "stateMutability": "pure"
107
+ "stateMutability": "nonpayable"
108
+ },
109
+ {
110
+ "type": "function",
111
+ "name": "delegationManager",
112
+ "inputs": [],
113
+ "outputs": [
114
+ {
115
+ "name": "",
116
+ "type": "address",
117
+ "internalType": "address"
118
+ }
119
+ ],
120
+ "stateMutability": "view"
121
+ },
122
+ {
123
+ "type": "function",
124
+ "name": "isUsed",
125
+ "inputs": [
126
+ {
127
+ "name": "delegator",
128
+ "type": "address",
129
+ "internalType": "address"
130
+ },
131
+ {
132
+ "name": "delegationHash",
133
+ "type": "bytes32",
134
+ "internalType": "bytes32"
135
+ },
136
+ {
137
+ "name": "kind",
138
+ "type": "bytes32",
139
+ "internalType": "bytes32"
140
+ },
141
+ {
142
+ "name": "stepNonce",
143
+ "type": "bytes32",
144
+ "internalType": "bytes32"
145
+ }
146
+ ],
147
+ "outputs": [
148
+ {
149
+ "name": "",
150
+ "type": "bool",
151
+ "internalType": "bool"
152
+ }
153
+ ],
154
+ "stateMutability": "view"
155
+ },
156
+ {
157
+ "type": "event",
158
+ "name": "DigestBindingRedeemed",
159
+ "inputs": [
160
+ {
161
+ "name": "delegator",
162
+ "type": "address",
163
+ "indexed": true,
164
+ "internalType": "address"
165
+ },
166
+ {
167
+ "name": "delegationHash",
168
+ "type": "bytes32",
169
+ "indexed": true,
170
+ "internalType": "bytes32"
171
+ },
172
+ {
173
+ "name": "kind",
174
+ "type": "bytes32",
175
+ "indexed": true,
176
+ "internalType": "bytes32"
177
+ },
178
+ {
179
+ "name": "presented",
180
+ "type": "bytes32",
181
+ "indexed": false,
182
+ "internalType": "bytes32"
183
+ },
184
+ {
185
+ "name": "stepNonce",
186
+ "type": "bytes32",
187
+ "indexed": false,
188
+ "internalType": "bytes32"
189
+ }
190
+ ],
191
+ "anonymous": false
97
192
  },
98
193
  {
99
194
  "type": "error",
@@ -125,5 +220,32 @@
125
220
  "internalType": "bytes32"
126
221
  }
127
222
  ]
223
+ },
224
+ {
225
+ "type": "error",
226
+ "name": "NotDelegationManager",
227
+ "inputs": [
228
+ {
229
+ "name": "caller",
230
+ "type": "address",
231
+ "internalType": "address"
232
+ }
233
+ ]
234
+ },
235
+ {
236
+ "type": "error",
237
+ "name": "StepNonceReused",
238
+ "inputs": [
239
+ {
240
+ "name": "kind",
241
+ "type": "bytes32",
242
+ "internalType": "bytes32"
243
+ },
244
+ {
245
+ "name": "stepNonce",
246
+ "type": "bytes32",
247
+ "internalType": "bytes32"
248
+ }
249
+ ]
128
250
  }
129
251
  ]
@@ -0,0 +1,31 @@
1
+ [
2
+ {
3
+ "type": "function",
4
+ "name": "isModuleInstalled",
5
+ "inputs": [
6
+ {
7
+ "name": "moduleTypeId",
8
+ "type": "uint256",
9
+ "internalType": "uint256"
10
+ },
11
+ {
12
+ "name": "module",
13
+ "type": "address",
14
+ "internalType": "address"
15
+ },
16
+ {
17
+ "name": "additionalContext",
18
+ "type": "bytes",
19
+ "internalType": "bytes"
20
+ }
21
+ ],
22
+ "outputs": [
23
+ {
24
+ "name": "",
25
+ "type": "bool",
26
+ "internalType": "bool"
27
+ }
28
+ ],
29
+ "stateMutability": "view"
30
+ }
31
+ ]
@@ -0,0 +1,153 @@
1
+ [
2
+ {
3
+ "type": "function",
4
+ "name": "anchor",
5
+ "inputs": [
6
+ {
7
+ "name": "receiptDigest",
8
+ "type": "bytes32",
9
+ "internalType": "bytes32"
10
+ },
11
+ {
12
+ "name": "intentDigest",
13
+ "type": "bytes32",
14
+ "internalType": "bytes32"
15
+ },
16
+ {
17
+ "name": "mandateRef",
18
+ "type": "bytes32",
19
+ "internalType": "bytes32"
20
+ }
21
+ ],
22
+ "outputs": [],
23
+ "stateMutability": "nonpayable"
24
+ },
25
+ {
26
+ "type": "function",
27
+ "name": "anchorOf",
28
+ "inputs": [
29
+ {
30
+ "name": "anchoredBy",
31
+ "type": "address",
32
+ "internalType": "address"
33
+ },
34
+ {
35
+ "name": "receiptDigest",
36
+ "type": "bytes32",
37
+ "internalType": "bytes32"
38
+ }
39
+ ],
40
+ "outputs": [
41
+ {
42
+ "name": "",
43
+ "type": "tuple",
44
+ "internalType": "struct ReceiptAnchorRegistry.Anchor",
45
+ "components": [
46
+ {
47
+ "name": "anchoredBy",
48
+ "type": "address",
49
+ "internalType": "address"
50
+ },
51
+ {
52
+ "name": "at",
53
+ "type": "uint64",
54
+ "internalType": "uint64"
55
+ },
56
+ {
57
+ "name": "intentDigest",
58
+ "type": "bytes32",
59
+ "internalType": "bytes32"
60
+ },
61
+ {
62
+ "name": "mandateRef",
63
+ "type": "bytes32",
64
+ "internalType": "bytes32"
65
+ }
66
+ ]
67
+ }
68
+ ],
69
+ "stateMutability": "view"
70
+ },
71
+ {
72
+ "type": "function",
73
+ "name": "isAnchored",
74
+ "inputs": [
75
+ {
76
+ "name": "anchoredBy",
77
+ "type": "address",
78
+ "internalType": "address"
79
+ },
80
+ {
81
+ "name": "receiptDigest",
82
+ "type": "bytes32",
83
+ "internalType": "bytes32"
84
+ }
85
+ ],
86
+ "outputs": [
87
+ {
88
+ "name": "",
89
+ "type": "bool",
90
+ "internalType": "bool"
91
+ }
92
+ ],
93
+ "stateMutability": "view"
94
+ },
95
+ {
96
+ "type": "event",
97
+ "name": "ReceiptAnchored",
98
+ "inputs": [
99
+ {
100
+ "name": "receiptDigest",
101
+ "type": "bytes32",
102
+ "indexed": true,
103
+ "internalType": "bytes32"
104
+ },
105
+ {
106
+ "name": "anchoredBy",
107
+ "type": "address",
108
+ "indexed": true,
109
+ "internalType": "address"
110
+ },
111
+ {
112
+ "name": "intentDigest",
113
+ "type": "bytes32",
114
+ "indexed": true,
115
+ "internalType": "bytes32"
116
+ },
117
+ {
118
+ "name": "mandateRef",
119
+ "type": "bytes32",
120
+ "indexed": false,
121
+ "internalType": "bytes32"
122
+ },
123
+ {
124
+ "name": "at",
125
+ "type": "uint64",
126
+ "indexed": false,
127
+ "internalType": "uint64"
128
+ }
129
+ ],
130
+ "anonymous": false
131
+ },
132
+ {
133
+ "type": "error",
134
+ "name": "AlreadyAnchored",
135
+ "inputs": [
136
+ {
137
+ "name": "receiptDigest",
138
+ "type": "bytes32",
139
+ "internalType": "bytes32"
140
+ },
141
+ {
142
+ "name": "by",
143
+ "type": "address",
144
+ "internalType": "address"
145
+ }
146
+ ]
147
+ },
148
+ {
149
+ "type": "error",
150
+ "name": "ZeroDigest",
151
+ "inputs": []
152
+ }
153
+ ]
@@ -34,6 +34,7 @@ export declare const IAgenticPrimitivesAgentAccount: Abi;
34
34
  export declare const IApprovedHashRegistryView: Abi;
35
35
  export declare const IAttributeReader: Abi;
36
36
  export declare const ICaveatEnforcer: Abi;
37
+ export declare const ICustodyPolicyAccountModuleView: Abi;
37
38
  export declare const ICustodyPolicyFactoryView: Abi;
38
39
  export declare const ICustodyPolicyPauseView: Abi;
39
40
  export declare const IDelegationManager: Abi;
@@ -54,6 +55,7 @@ export declare const PaymentEscrow: Abi;
54
55
  export declare const PaymentReceiptRegistry: Abi;
55
56
  export declare const PermissionlessSubregistry: Abi;
56
57
  export declare const QuorumEnforcer: Abi;
58
+ export declare const ReceiptAnchorRegistry: Abi;
57
59
  export declare const RelationshipTypeRegistry: Abi;
58
60
  export declare const ShapeRegistry: Abi;
59
61
  export declare const SignatureSlotRecovery: Abi;
package/dist/abi/index.js CHANGED
@@ -34,6 +34,7 @@ export { default as IAgenticPrimitivesAgentAccount } from './IAgenticPrimitivesA
34
34
  export { default as IApprovedHashRegistryView } from './IApprovedHashRegistryView.json' with { type: 'json' };
35
35
  export { default as IAttributeReader } from './IAttributeReader.json' with { type: 'json' };
36
36
  export { default as ICaveatEnforcer } from './ICaveatEnforcer.json' with { type: 'json' };
37
+ export { default as ICustodyPolicyAccountModuleView } from './ICustodyPolicyAccountModuleView.json' with { type: 'json' };
37
38
  export { default as ICustodyPolicyFactoryView } from './ICustodyPolicyFactoryView.json' with { type: 'json' };
38
39
  export { default as ICustodyPolicyPauseView } from './ICustodyPolicyPauseView.json' with { type: 'json' };
39
40
  export { default as IDelegationManager } from './IDelegationManager.json' with { type: 'json' };
@@ -54,6 +55,7 @@ export { default as PaymentEscrow } from './PaymentEscrow.json' with { type: 'js
54
55
  export { default as PaymentReceiptRegistry } from './PaymentReceiptRegistry.json' with { type: 'json' };
55
56
  export { default as PermissionlessSubregistry } from './PermissionlessSubregistry.json' with { type: 'json' };
56
57
  export { default as QuorumEnforcer } from './QuorumEnforcer.json' with { type: 'json' };
58
+ export { default as ReceiptAnchorRegistry } from './ReceiptAnchorRegistry.json' with { type: 'json' };
57
59
  export { default as RelationshipTypeRegistry } from './RelationshipTypeRegistry.json' with { type: 'json' };
58
60
  export { default as ShapeRegistry } from './ShapeRegistry.json' with { type: 'json' };
59
61
  export { default as SignatureSlotRecovery } from './SignatureSlotRecovery.json' with { type: 'json' };
@@ -36,6 +36,7 @@ export interface Deployments {
36
36
  readonly "valueEnforcer": Address;
37
37
  readonly "agentRegistryBase": Address;
38
38
  readonly "digestBindingEnforcer": Address;
39
+ readonly "receiptAnchorRegistry": Address;
39
40
  readonly "chainId": number;
40
41
  readonly "permissionlessSubregistries": string;
41
42
  readonly "deploymentEpoch": string;
@@ -48,6 +48,7 @@ export const CONTRACTS = {
48
48
  },
49
49
  "agentRegistryBase": "0x0Be172F1b5cfD1d084CEc2cF2Af0A0cE9023Deb1",
50
50
  "digestBindingEnforcer": "0xA3bb9BCC9b2F6F2419E1aBe5ED6Fd5399b9E68e1",
51
+ "receiptAnchorRegistry": "0x0e339334B4438F7546FD53837FDC8943cB3Cf54C",
51
52
  "deploymentEpoch": "ep_80afd31afaeacbf9"
52
53
  };
53
54
  export default CONTRACTS;
package/package.json CHANGED
@@ -1,18 +1,22 @@
1
1
  {
2
2
  "name": "@agenticprimitives/contracts",
3
- "version": "1.0.0-alpha.24",
3
+ "version": "1.0.0-alpha.26",
4
4
  "type": "module",
5
5
  "description": "Solidity contracts + ABIs + flattened sources + per-network deployment addresses for the agenticprimitives stack.",
6
+ "homepage": "https://agenticprimitives.dev",
7
+ "bugs": {
8
+ "url": "https://github.com/agentictrustlabs/agenticprimitives/issues"
9
+ },
10
+ "author": {
11
+ "name": "Agentic Trust Labs",
12
+ "url": "https://agenticprimitives.dev"
13
+ },
6
14
  "license": "MIT",
7
15
  "repository": {
8
16
  "type": "git",
9
17
  "url": "git+https://github.com/agentictrustlabs/agenticprimitives.git",
10
18
  "directory": "packages/contracts"
11
19
  },
12
- "homepage": "https://github.com/agentictrustlabs/agenticprimitives/tree/master/packages/contracts",
13
- "bugs": {
14
- "url": "https://github.com/agentictrustlabs/agenticprimitives/issues"
15
- },
16
20
  "exports": {
17
21
  "./abi": "./dist/abi/index.js",
18
22
  "./abi/*": "./dist/abi/*.json",
@@ -48,6 +52,13 @@
48
52
  "peerDependencies": {
49
53
  "viem": "^2.52.2"
50
54
  },
55
+ "keywords": [
56
+ "agentic-primitives",
57
+ "agenticprimitives",
58
+ "ai-agents",
59
+ "agent-authority",
60
+ "agentic-trust"
61
+ ],
51
62
  "scripts": {
52
63
  "setup": "bash setup.sh",
53
64
  "build": "forge build && pnpm run build:abi && pnpm run build:deployments",
@@ -189,12 +189,23 @@ contract AgentAccount is
189
189
  /// large delay that bricks future migrations.
190
190
  uint256 internal constant MAX_UPGRADE_TIMELOCK = 30 days;
191
191
 
192
+ /// @dev R917-C-2 (spec 408 §1.2) — THE CUSTODY EPOCH. Incremented by every path that RETIRES a
193
+ /// credential (`removeCustodian`, `removePasskey` — T6 recovery and a full rotation remove through
194
+ /// these). The `0x03` approved-hash sentinel keys every approval to the epoch it was made under
195
+ /// (`approvedHashKey`), so an approval planted by a custodian who is later removed is void the
196
+ /// moment the custody set no longer contains that custodian — an ECDSA signature dies with its key,
197
+ /// and an approved hash now dies with its custody set. Adding a credential does not bump it: an
198
+ /// added key cannot launder an approval a still-present key planted, and voiding a person's
199
+ /// standing wires every time she enrols a device would be the wrong invariant. Takes one slot
200
+ /// from the gap (49 remain).
201
+ uint64 private _custodyEpoch;
202
+
192
203
  /// @dev Spec 007 Phase A.5 (SC7 § 3.1) — storage gap reserves 50
193
204
  /// slots after the last linear-layout state variable. Future
194
205
  /// additive upgrades shrink the gap by however many slots they
195
206
  /// add. ERC-7201 namespaced state (passkeys, modules) lives at
196
207
  /// keccak-computed slots and is NOT in this linear layout.
197
- uint256[50] private __gap;
208
+ uint256[49] private __gap;
198
209
 
199
210
  // ─── Errors ─────────────────────────────────────────────────────
200
211
 
@@ -943,6 +954,13 @@ contract AgentAccount is
943
954
  revert ModuleNotInstalled(moduleTypeId, module);
944
955
  }
945
956
 
957
+ // R917-C-1: effects BEFORE the interaction. The module's `onUninstall` may verify that this account
958
+ // no longer lists it (CustodyPolicy does — that is how it tells `uninstallModule` apart from a
959
+ // custodian's `execute(custodyPolicy, onUninstall)`), so the flag is cleared first. A revert below
960
+ // still rolls the whole call back.
961
+ $.installed[moduleTypeId][module] = false;
962
+ _removeFromList($.installedList[moduleTypeId], module);
963
+
946
964
  // Loud uninstall — if the module reverts in onUninstall, we revert
947
965
  // too so the caller sees the failure. (Owner can force-uninstall
948
966
  // by passing deInitData the module can handle, or by re-deploying
@@ -953,9 +971,6 @@ contract AgentAccount is
953
971
  revert ModuleOnUninstallFailed(reason);
954
972
  }
955
973
 
956
- $.installed[moduleTypeId][module] = false;
957
- _removeFromList($.installedList[moduleTypeId], module);
958
-
959
974
  emit ModuleUninstalled(moduleTypeId, module);
960
975
  }
961
976
 
@@ -1297,14 +1312,32 @@ contract AgentAccount is
1297
1312
  /// `DM → execute(self, executeBatch(...))` nest, so a nested governance
1298
1313
  /// self-call is caught regardless of mode.
1299
1314
  function _rejectDelegatedGovernance(address target, bytes calldata data) internal view {
1315
+ if (!_delegationExecCtx) return;
1300
1316
  if (
1301
- _delegationExecCtx &&
1302
1317
  target == address(this) &&
1303
1318
  data.length >= 4 &&
1304
1319
  _isGovernanceSelector(bytes4(data[0:4]))
1305
1320
  ) {
1306
1321
  revert DelegatedGovernanceForbidden();
1307
1322
  }
1323
+ // R917-C-1/C-2 (spec 408 §1.1): a delegation stack may not speak AS THE ACCOUNT to the account's own
1324
+ // custody-tier contracts. `CustodyPolicy` and `ApprovedHashRegistry` treat `msg.sender == account` as
1325
+ // the principal — a delegate reaching them through `execute` would uninstall the custody policy
1326
+ // (bricking the account) or plant approved-hash root delegations that outlive the delegate's own
1327
+ // caveats. Any selector: there is no legitimate delegated call to either.
1328
+ if (_isAccountTrustedTarget(target)) revert DelegatedGovernanceForbidden();
1329
+ }
1330
+
1331
+ /// @dev The contracts that take THIS ACCOUNT as principal when it is `msg.sender` — reachable only from
1332
+ /// the account's own governance paths (`uninstallModule`, a custodian-signed userOp), never from a
1333
+ /// delegation. The custody policy is read from the factory (the canonical one; a legacy direct
1334
+ /// deploy with no factory has none); the registry is the immutable this account was built with.
1335
+ function _isAccountTrustedTarget(address target) internal view returns (bool) {
1336
+ if (target == _approvedHashRegistry && target != address(0)) return true;
1337
+ address f = _factory;
1338
+ if (f == address(0)) return false;
1339
+ address cp = IAgentAccountFactoryView(f).custodyPolicy();
1340
+ return cp != address(0) && target == cp;
1308
1341
  }
1309
1342
 
1310
1343
  /// @dev The account-governance selector set — mutators that change custody,
@@ -1349,9 +1382,11 @@ contract AgentAccount is
1349
1382
  // branch is unreachable from `_validateSignature`/`_validateSig`
1350
1383
  // (the userOp-auth path), where 0x03 is an unknown type → false.
1351
1384
  // Fails CLOSED if the registry is unset/not-a-contract (ADR-0013).
1385
+ // R917-C-2: the registry is consulted for the EPOCH-BOUND key, never the raw hash — an approval
1386
+ // made under a custody set that has since retired a credential is not this account's approval.
1352
1387
  if (signature.length == 1 && uint8(signature[0]) == SIG_TYPE_APPROVED_HASH) {
1353
1388
  if (_approvedHashRegistry.code.length == 0) return bytes4(0xffffffff);
1354
- return IApprovedHashRegistryView(_approvedHashRegistry).isApproved(address(this), hash)
1389
+ return IApprovedHashRegistryView(_approvedHashRegistry).isApproved(address(this), approvedHashKey(hash))
1355
1390
  ? ERC1271_MAGIC_VALUE
1356
1391
  : bytes4(0xffffffff);
1357
1392
  }
@@ -1611,6 +1646,7 @@ contract AgentAccount is
1611
1646
  _externalCustodians[owner] = false;
1612
1647
  _externalCustodianCount--;
1613
1648
  emit CustodianRemoved(owner);
1649
+ _bumpCustodyEpoch();
1614
1650
  }
1615
1651
 
1616
1652
  // ─── Passkey (WebAuthn P-256) management ──────────────────────
@@ -1659,6 +1695,8 @@ contract AgentAccount is
1659
1695
  /// (ADR-0012/0013; INV-302-3).
1660
1696
  event PasskeyAdded(bytes32 indexed credentialIdDigest);
1661
1697
  event PasskeyRemoved(bytes32 indexed credentialIdDigest);
1698
+ /// @dev R917-C-2 — a credential was retired; every approved hash keyed to the previous epoch is void.
1699
+ event CustodyEpochBumped(uint64 epoch);
1662
1700
 
1663
1701
  /// @notice Register a new WebAuthn credential bound to a specific RP.
1664
1702
  /// `onlySelf` — callable via a UserOp signed by any existing signer
@@ -1715,6 +1753,7 @@ contract AgentAccount is
1715
1753
  $.count -= 1;
1716
1754
  emit PasskeyRemoved(credentialIdDigest);
1717
1755
  emit CustodianRemoved(pia);
1756
+ _bumpCustodyEpoch();
1718
1757
  }
1719
1758
 
1720
1759
  /// @notice Whether a passkey is registered on this account.
@@ -1741,6 +1780,28 @@ contract AgentAccount is
1741
1780
  return _passkeyStorage().count;
1742
1781
  }
1743
1782
 
1783
+ // ─── Custody epoch (R917-C-2, spec 408 §1.2) ─────────────────────
1784
+
1785
+ /// @notice The current custody epoch — bumped whenever a credential is retired.
1786
+ function custodyEpoch() external view returns (uint64) {
1787
+ return _custodyEpoch;
1788
+ }
1789
+
1790
+ /// @notice THE KEY an SDK approves in the `ApprovedHashRegistry` for `hash` to validate through this
1791
+ /// account's `0x03` sentinel NOW: `keccak256(abi.encode(hash, custodyEpoch))`. A counterfactual
1792
+ /// account (not yet deployed) is at epoch 0, so its key is computable before deploy — which is
1793
+ /// what spec 253's batched deploy-and-approve relies on.
1794
+ function approvedHashKey(bytes32 hash) public view returns (bytes32) {
1795
+ return keccak256(abi.encode(hash, _custodyEpoch));
1796
+ }
1797
+
1798
+ /// @dev Retire-a-credential paths call this; see `_custodyEpoch`.
1799
+ function _bumpCustodyEpoch() internal {
1800
+ uint64 next = _custodyEpoch + 1;
1801
+ _custodyEpoch = next;
1802
+ emit CustodyEpochBumped(next);
1803
+ }
1804
+
1744
1805
  // ─── Custody policy + scheduled changes ───────────────────────────
1745
1806
  //
1746
1807
  // The schedule / apply / cancel custody-change surface, CustodyAction
@@ -55,6 +55,14 @@ contract AgentAccountFactory is GovernanceManaged {
55
55
  /// sentinel consults it; it is NOT a mutable factory-view.
56
56
  address public immutable approvedHashRegistry;
57
57
 
58
+ /// @notice R917-C-5 (spec 408 §1.3) — whether the T4 (admin) safety-delay floor is LIFTED to zero for
59
+ /// accounts this factory deploys. A DEPLOYMENT decision, stated by the deployer in the
60
+ /// constructor and recorded in the deployments JSON, never a `block.chainid` list in bytecode:
61
+ /// the previous carve-out named the estate's own chain (34348) and silently switched the
62
+ /// EXT-TL-FLOOR-1 invariant off where it mattered. `check:production-deploy` refuses a
63
+ /// production/testnet record with this `true`. T5 (24 h) / T6 (48 h) are never relaxed.
64
+ bool public immutable relaxedT4Floor;
65
+
58
66
  address public bundlerSigner;
59
67
  address public sessionIssuer;
60
68
 
@@ -127,11 +135,13 @@ contract AgentAccountFactory is GovernanceManaged {
127
135
  address bundlerSigner_,
128
136
  address sessionIssuer_,
129
137
  address governance_,
130
- address approvedHashRegistry_
138
+ address approvedHashRegistry_,
139
+ bool relaxedT4Floor_
131
140
  ) GovernanceManaged(governance_) {
132
141
  if (custodyPolicy_ == address(0)) revert ZeroAddress();
133
142
  accountImplementation = new AgentAccount(entryPoint_, approvedHashRegistry_);
134
143
  approvedHashRegistry = approvedHashRegistry_;
144
+ relaxedT4Floor = relaxedT4Floor_;
135
145
  delegationManager = delegationManager_;
136
146
  custodyPolicy = custodyPolicy_;
137
147
  bundlerSigner = bundlerSigner_;
@@ -357,15 +367,12 @@ contract AgentAccountFactory is GovernanceManaged {
357
367
  if (thresholds[5] > sodCap) thresholds[5] = sodCap;
358
368
  }
359
369
 
360
- // DEV/TEST-CHAIN CARVE-OUT (local demo only): on a local development chain the T4 floor is lifted
361
- // so a demo can drive an admin custody action's schedule→apply back-to-back without a real-time
362
- // wait, and a zero override yields a ZERO T4 safety delay instead of the 1-hour production default.
363
- // Gated strictly by `block.chainid` — anvil (31337/1337) and the local faithchain (34348). Every
364
- // PRODUCTION and public TESTNET chain id (Base Sepolia 84532, mainnet, …) keeps the audited
365
- // EXT-TL-FLOOR-1 invariant. T5 (24h) and T6 (recovery, 48h) are NOT relaxed even on dev — the
366
- // CustodyPolicy requires a non-zero timelock for those tiers, and the demo only exercises T4.
367
- bool devChain = block.chainid == 31337 || block.chainid == 1337 || block.chainid == 34348;
368
- uint32 floor4 = devChain ? 0 : uint32(1 hours);
370
+ // R917-C-5 (spec 408 §1.3): the T4 floor is lifted ONLY when the deployer said so in the constructor
371
+ // (`relaxedT4Floor`) — a local development posture so a demo can drive an admin custody action's
372
+ // schedule→apply back-to-back. It is not inferred from `block.chainid`: the estate's own chain was
373
+ // once on that list, and the audited EXT-TL-FLOOR-1 invariant was off where it mattered. T5 (24h)
374
+ // and T6 (recovery, 48h) are NOT relaxed under any posture.
375
+ uint32 floor4 = relaxedT4Floor ? 0 : uint32(1 hours);
369
376
 
370
377
  uint32[7] memory timelocks;
371
378
  timelocks[4] = timelockOverrides[4] == 0 ? floor4 : timelockOverrides[4];
@@ -26,13 +26,26 @@ pragma solidity ^0.8.28;
26
26
  * Pairs with `MultiSendCallOnly` (atomic batch) so a passkey
27
27
  * smart account can `approveHash` + perform the delegated action
28
28
  * in one userOp.
29
+ *
30
+ * R917-C-2 (spec 408 §1.2): an `AgentAccount`'s `0x03` sentinel does NOT approve the raw hash —
31
+ * it approves `AgentAccount.approvedHashKey(hash)` = `keccak256(abi.encode(hash, custodyEpoch))`,
32
+ * so an approval planted by a custodian who is later removed is void once the account's custody
33
+ * epoch moves. This registry is agnostic to that: it stores whatever 32 bytes the signer approves.
29
34
  */
30
35
  contract ApprovedHashRegistry {
31
- /// signer => hash => approved
32
- mapping(address => mapping(bytes32 => bool)) public approved;
36
+ /// @dev R917-C-2 (spec 408 §1.2): an approval is recorded with the signer's epoch at the time
37
+ /// (`epoch + 1`, so a set slot is never zero); `revokeAll()` bumps the epoch and voids every
38
+ /// approval the signer ever made in one call. `isApproved` is the ONE read: the `0x03` sentinel
39
+ /// and `QuorumEnforcer`'s `v == 1` slot both consult it.
40
+ /// signer => hash => (epoch at approval + 1), 0 = never approved
41
+ mapping(address => mapping(bytes32 => uint64)) private _approvedAt;
42
+ /// signer => current epoch
43
+ mapping(address => uint64) private _epoch;
33
44
 
34
45
  event HashApproved(address indexed signer, bytes32 indexed hash);
35
46
  event HashRevoked(address indexed signer, bytes32 indexed hash);
47
+ /// @dev R917-C-2 — every approval `signer` made before this event is void.
48
+ event AllHashesRevoked(address indexed signer, uint64 epoch);
36
49
 
37
50
  /**
38
51
  * @notice Pre-approve a hash. `msg.sender` is the signer; only
@@ -40,18 +53,40 @@ contract ApprovedHashRegistry {
40
53
  * for this hash.
41
54
  */
42
55
  function approveHash(bytes32 hash) external {
43
- approved[msg.sender][hash] = true;
56
+ _approvedAt[msg.sender][hash] = _epoch[msg.sender] + 1;
44
57
  emit HashApproved(msg.sender, hash);
45
58
  }
46
59
 
47
60
  /// @notice Revoke a previously-approved hash before redemption.
48
61
  function revokeHash(bytes32 hash) external {
49
- approved[msg.sender][hash] = false;
62
+ _approvedAt[msg.sender][hash] = 0;
50
63
  emit HashRevoked(msg.sender, hash);
51
64
  }
52
65
 
53
- /// @notice External view used by `QuorumEnforcer`'s `v == 1` path.
66
+ /// @notice R917-C-2 — revoke EVERY hash `msg.sender` ever approved, in one call. The per-hash
67
+ /// approvals are unenumerable on chain; this is how a signer that suspects a planted approval
68
+ /// voids all of them without knowing which.
69
+ function revokeAll() external {
70
+ uint64 next = _epoch[msg.sender] + 1;
71
+ _epoch[msg.sender] = next;
72
+ emit AllHashesRevoked(msg.sender, next);
73
+ }
74
+
75
+ /// @notice The signer's current epoch (informational; `isApproved` already accounts for it).
76
+ function epochOf(address signer) external view returns (uint64) {
77
+ return _epoch[signer];
78
+ }
79
+
80
+ /// @notice Whether `signer` approved `hash` under its CURRENT epoch. External view used by the
81
+ /// `0x03` sentinel and by `QuorumEnforcer`'s `v == 1` path.
54
82
  function isApproved(address signer, bytes32 hash) external view returns (bool) {
55
- return approved[signer][hash];
83
+ uint64 at = _approvedAt[signer][hash];
84
+ return at != 0 && at == _epoch[signer] + 1;
85
+ }
86
+
87
+ /// @notice Back-compat read shape of the former public mapping: true iff currently approved.
88
+ function approved(address signer, bytes32 hash) external view returns (bool) {
89
+ uint64 at = _approvedAt[signer][hash];
90
+ return at != 0 && at == _epoch[signer] + 1;
56
91
  }
57
92
  }
@@ -20,6 +20,13 @@ interface ICustodyPolicyPauseView {
20
20
  function isPaused() external view returns (bool);
21
21
  }
22
22
 
23
+ /// @dev R917-C-1 — the account's ERC-7579 module view. `onUninstall` refuses while the account still lists
24
+ /// this module as installed, so the only path to it is `AgentAccount.uninstallModule` (onlySelf +
25
+ /// custody-quorum), which clears the flag BEFORE calling out.
26
+ interface ICustodyPolicyAccountModuleView {
27
+ function isModuleInstalled(uint256 moduleTypeId, address module, bytes calldata additionalContext) external view returns (bool);
28
+ }
29
+
23
30
  /**
24
31
  * @title CustodyPolicy
25
32
  * @notice ERC-7579 module that owns the schedule / apply / cancel
@@ -346,6 +353,11 @@ contract CustodyPolicy {
346
353
  /// @dev Audit C-11: reinstall over a previously-uninstalled config
347
354
  /// is forbidden — stale mappings would compose unpredictably.
348
355
  error ReinstallForbidden(address account);
356
+ /// @dev R917-C-1: `onUninstall` was called while the account still has this module installed — i.e. by
357
+ /// `execute(custodyPolicy, onUninstall)` under a single custodian or a delegation, not by
358
+ /// `uninstallModule`. Accepting it would leave the account governed by a policy that refuses every
359
+ /// change (installed on the account, uninstalled here, reinstall forbidden) — bricked forever.
360
+ error StillInstalledOn(address account);
349
361
 
350
362
  // ─── ERC-7579 lifecycle ───────────────────────────────────────────
351
363
 
@@ -452,6 +464,14 @@ contract CustodyPolicy {
452
464
  address account = msg.sender;
453
465
  Config storage c = _configs[account];
454
466
  if (!c.installed) revert NotInstalledOn(account);
467
+ // R917-C-1: the account must have ALREADY dropped this module. `AgentAccount.uninstallModule` (onlySelf
468
+ // + `_requireCustodyGoverned`) clears its installed flag before this call; any other caller that is the
469
+ // account — a 1-of-N custodian userOp `execute(custodyPolicy, onUninstall)`, a delegation redeemed
470
+ // through the DM — still lists the module and is refused here. Fail closed on the read as well: a
471
+ // caller that cannot answer the view is not an account that uninstalled us.
472
+ if (ICustodyPolicyAccountModuleView(account).isModuleInstalled(MODULE_TYPE_EXECUTOR, address(this), "")) {
473
+ revert StillInstalledOn(account);
474
+ }
455
475
  c.installed = false;
456
476
  // Audit C-11: lock against ever re-installing on this account.
457
477
  // The mapping state (trustees, thresholds, proposals) is left
@@ -10,14 +10,15 @@ import "../agency/ICaveatEnforcer.sol";
10
10
  * §2): authority *for a reason*, not authority for any purpose until revoked.
11
11
  *
12
12
  * @dev terms = abi.encode(bytes32 kind, bytes32 digest)
13
- * args = abi.encode(bytes32 presented) — supplied by the redeemer at redemption
13
+ * args = abi.encode(bytes32 presented, bytes32 stepNonce) — supplied by the redeemer at redemption
14
14
  *
15
15
  * `kind` names WHICH artifact the digest is of, so an off-chain reader can tell an intent binding
16
16
  * from a projection binding without guessing:
17
17
  * keccak256("ap:intent") — the canonical intent (RFC 8785 JCS bytes)
18
18
  * keccak256("ap:projection") — what was disclosed to the grantee (spec 336 §4)
19
19
  * keccak256("ap:offer") — the accepted offer (spec 336 §5)
20
- * One enforcer serves all three: the mechanics are identical and the kind belongs in the terms,
20
+ * keccak256("ap:plan") — the admitted plan (spec 408 §2.3)
21
+ * One enforcer serves all four: the mechanics are identical and the kind belongs in the terms,
21
22
  * where it is signed, rather than in which contract address was chosen.
22
23
  *
23
24
  * WHAT THIS ENFORCES, HONESTLY. The chain cannot know what an intent *means*; it can only hold
@@ -30,33 +31,67 @@ import "../agency/ICaveatEnforcer.sol";
30
31
  * 2. the presented digest lands in the transaction, so the receipt's `intentDigest` and the
31
32
  * chain's record of what the redeemer claimed to be doing are the same bytes — evidence a
32
33
  * third party can check without trusting either side.
33
- * A redeemer that copies `terms` into `args` satisfies the check; that is the point — it has
34
+ * A redeemer that copies `terms` into `args` satisfies the digest check; that is the point — it has
34
35
  * now committed, on chain, to that intent, and a step whose real intent differs is caught by the
35
36
  * verifier that computed the digest from the step rather than from the caveat.
36
37
  *
37
- * R6.7 — Stateless validator: hooks are `pure`, zero storage; the DM-side pause check gates all
38
- * reachable enforcer dispatch.
38
+ * R917-C-4 (spec 408 §1.4) — SINGLE-USE PER STEP. A digest-bound redemption is consumed under
39
+ * `(delegator, delegationHash, kind, stepNonce)`: the harness derives `stepNonce` from the intent
40
+ * digest and the step's reference (the same derivation as `PaymentEnforcer`'s nonce), so ONE step
41
+ * redeems once and a replay of it reverts `StepNonceReused`. Which steps exist is bound off chain by
42
+ * the plan digest the mandate carries; the chain holds each of them to one redemption. The hooks
43
+ * therefore mutate state and are `onlyDelegationManager` (EXT-PMT-1's rule for stateful enforcers):
44
+ * a stranger cannot burn a step's nonce ahead of the legitimate redemption.
39
45
  */
40
46
  contract DigestBindingEnforcer is ICaveatEnforcer {
41
47
  error BadTermsLength();
42
48
  error BadArgsLength();
43
49
  error DigestMismatch(bytes32 kind, bytes32 bound, bytes32 presented);
50
+ error StepNonceReused(bytes32 kind, bytes32 stepNonce);
51
+ error NotDelegationManager(address caller);
52
+
53
+ /// @notice The ONLY authorized caller of `beforeHook` — it consumes a nonce (R917-C-4).
54
+ address public immutable delegationManager;
55
+
56
+ /// delegator => delegationHash => keccak256(kind, stepNonce) => consumed
57
+ mapping(address => mapping(bytes32 => mapping(bytes32 => bool))) private _used;
58
+
59
+ event DigestBindingRedeemed(address indexed delegator, bytes32 indexed delegationHash, bytes32 indexed kind, bytes32 presented, bytes32 stepNonce);
60
+
61
+ constructor(address delegationManager_) {
62
+ require(delegationManager_ != address(0), "DigestBindingEnforcer: zero DelegationManager");
63
+ delegationManager = delegationManager_;
64
+ }
65
+
66
+ modifier onlyDelegationManager() {
67
+ if (msg.sender != delegationManager) revert NotDelegationManager(msg.sender);
68
+ _;
69
+ }
70
+
71
+ /// @notice Whether `stepNonce` was already consumed for this binding (a reader's replay check).
72
+ function isUsed(address delegator, bytes32 delegationHash, bytes32 kind, bytes32 stepNonce) external view returns (bool) {
73
+ return _used[delegator][delegationHash][keccak256(abi.encode(kind, stepNonce))];
74
+ }
44
75
 
45
76
  function beforeHook(
46
77
  bytes calldata terms,
47
78
  bytes calldata args,
48
- bytes32,
49
- address,
79
+ bytes32 delegationHash,
80
+ address delegator,
50
81
  address,
51
82
  address,
52
83
  uint256,
53
84
  bytes calldata
54
- ) external pure override {
85
+ ) external override onlyDelegationManager {
55
86
  if (terms.length != 64) revert BadTermsLength();
56
- if (args.length != 32) revert BadArgsLength();
87
+ if (args.length != 64) revert BadArgsLength();
57
88
  (bytes32 kind, bytes32 bound) = abi.decode(terms, (bytes32, bytes32));
58
- bytes32 presented = abi.decode(args, (bytes32));
89
+ (bytes32 presented, bytes32 stepNonce) = abi.decode(args, (bytes32, bytes32));
59
90
  if (presented != bound) revert DigestMismatch(kind, bound, presented);
91
+ bytes32 slot = keccak256(abi.encode(kind, stepNonce));
92
+ if (_used[delegator][delegationHash][slot]) revert StepNonceReused(kind, stepNonce);
93
+ _used[delegator][delegationHash][slot] = true;
94
+ emit DigestBindingRedeemed(delegator, delegationHash, kind, presented, stepNonce);
60
95
  }
61
96
 
62
97
  function afterHook(
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /**
5
+ * @title ReceiptAnchorRegistry
6
+ * @notice The on-chain ANCHOR for a harness receipt (spec 351 P0.6, spec 406 W2): a run's record digest, bound
7
+ * to the intent digest the mandate was over and the mandate's reference, anchored once by the acting
8
+ * runtime's agent. A counterparty who holds the record recomputes its digest and reads the anchor here —
9
+ * no trust in the runtime's logs, no vendor, no vault: the chain says a runtime with THIS agent's key
10
+ * committed to THESE bytes at THIS block, and the intent digest says which ask it was under.
11
+ *
12
+ * @dev DIGESTS AND ADDRESSES ONLY — nothing of what the run was about reaches the chain (the S1 firewall of
13
+ * spec 395 holds by construction). Per-row storage, so a reader uses `readContract` (ADR-0012: no log
14
+ * walks in product paths); the event is for indexers. Anchoring is permissionless: WHO anchored is the
15
+ * evidence (`anchoredBy`), never a gate — a stranger anchoring random digests proves nothing about a
16
+ * run whose receipt names a different agent. A digest anchors ONCE PER ANCHORER (R917-C-3, spec 408
17
+ * §1.5): rows are keyed by `(anchoredBy, receiptDigest)`, so a front-runner on a public mempool cannot
18
+ * squat the digest ahead of the agent, and an anchor cannot be re-attributed after the fact.
19
+ */
20
+ contract ReceiptAnchorRegistry {
21
+ struct Anchor {
22
+ address anchoredBy;
23
+ uint64 at;
24
+ bytes32 intentDigest;
25
+ bytes32 mandateRef;
26
+ }
27
+
28
+ /// anchoredBy => receiptDigest => anchor (R917-C-3: keyed by WHO anchored, so a stranger's anchor of
29
+ /// the same digest is a different row — it can neither pre-empt the agent's nor be mistaken for it).
30
+ mapping(address => mapping(bytes32 => Anchor)) private _anchors;
31
+
32
+ event ReceiptAnchored(bytes32 indexed receiptDigest, address indexed anchoredBy, bytes32 indexed intentDigest, bytes32 mandateRef, uint64 at);
33
+
34
+ error ZeroDigest();
35
+ error AlreadyAnchored(bytes32 receiptDigest, address by);
36
+
37
+ /// @notice Anchor a receipt digest, once per anchorer. `intentDigest` and `mandateRef` may be zero for a run under no mandate (a read).
38
+ function anchor(bytes32 receiptDigest, bytes32 intentDigest, bytes32 mandateRef) external {
39
+ if (receiptDigest == bytes32(0)) revert ZeroDigest();
40
+ Anchor storage a = _anchors[msg.sender][receiptDigest];
41
+ if (a.anchoredBy != address(0)) revert AlreadyAnchored(receiptDigest, a.anchoredBy);
42
+ _anchors[msg.sender][receiptDigest] = Anchor({ anchoredBy: msg.sender, at: uint64(block.timestamp), intentDigest: intentDigest, mandateRef: mandateRef });
43
+ emit ReceiptAnchored(receiptDigest, msg.sender, intentDigest, mandateRef, uint64(block.timestamp));
44
+ }
45
+
46
+ /// @notice The anchor `anchoredBy` made of a digest, or a zero struct when none — the reader names the
47
+ /// agent from the receipt it holds, and decides what absence means.
48
+ function anchorOf(address anchoredBy, bytes32 receiptDigest) external view returns (Anchor memory) {
49
+ return _anchors[anchoredBy][receiptDigest];
50
+ }
51
+
52
+ function isAnchored(address anchoredBy, bytes32 receiptDigest) external view returns (bool) {
53
+ return _anchors[anchoredBy][receiptDigest].anchoredBy != address(0);
54
+ }
55
+ }