@agenticprimitives/contracts 1.0.0-alpha.13 → 1.0.0-alpha.14

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.
@@ -1,37 +1,37 @@
1
1
  {
2
- "agentAccountFactory": "0x3E68B72B45e7C9d35B210E4Ab06e5Cece85cEbE4",
3
- "agentAccountImplementation": "0x338318ABf9C8b28E7e77ed96a91857cCE1Db1328",
4
- "agentNameRegistry": "0x15F7ed064A230C011b0244A14fD9653f011d609B",
5
- "agentNameResolver": "0xcC9855A23C7a4D4592E68Fe47E195C2f9D832473",
6
- "agentNameUniversalResolver": "0x7d777d2d0bbc1806B9Cc779121C27fbaAaFDb60b",
7
- "agentProfileResolver": "0xc448EB159B2a35F07f6E1814CC8d125244d7384B",
8
- "agentRegistryBase": "0xe320947b18D3C71710DCB75D4bf200147ded18Df",
9
- "agentRelationship": "0xE6B8F6C7F1c9857013dA356DF08D0384334A9607",
10
- "agreementRegistry": "0x7f4d6A74eda1D6ce251D371566f1c6639F9B0DDa",
11
- "allowedMethodsEnforcer": "0xf77cf06aCd8b9e2f3204fe4bE16449B3594c9633",
12
- "allowedTargetsEnforcer": "0xe3909FdEfD8b74B7D006E045DCd678e97D469Bfd",
13
- "approvedHashRegistry": "0x90735B5281434D1746926E2AA722Cc6DbF3EFEe6",
14
- "attestationRegistry": "0xaE3861fd299c74D96F6FCA25f19c7ACf8b9020ed",
15
- "callDataHashEnforcer": "0x9F12932F1b85A1F96c1Cb1c5A0645e3F0Ab478Dd",
2
+ "agentAccountFactory": "0xfCFafCbA6D88351aAAA190a5E971F06CAb40437d",
3
+ "agentAccountImplementation": "0x28b7d8e4682EF36bE479330Ba6CFE78E9E724Bc4",
4
+ "agentNameRegistry": "0x419928546Be67ba83C2aEaFCb6AD3364D5659C8D",
5
+ "agentNameResolver": "0xf96A25DeBF92C514459DC904ec8b79CE52D265A5",
6
+ "agentNameUniversalResolver": "0x3aC8Cb8Df1A8F9fcA05a37cEC82426b4432A53D3",
7
+ "agentProfileResolver": "0xb988e75179e11E4d17665d61c6a9Ce0e737A808F",
8
+ "agentRegistryBase": "0x7fab087fc3d0e1793bdb1d4c5d253635131ee1d1",
9
+ "agentRelationship": "0xDc409D69aC002B4FC90D4B453cc6B6277B98CB46",
10
+ "agreementRegistry": "0x882371Dc898f4835335a01558aE5C2B306069FcF",
11
+ "allowedMethodsEnforcer": "0x1Df87222f53ad26F599C60364F7232a3c0393ad9",
12
+ "allowedTargetsEnforcer": "0x55A9fD6aF8688EE9c68D15CDAa84d9DBD027B80F",
13
+ "approvedHashRegistry": "0x878272cc4E0c5bA026CEc29C9722F41fC0515fE5",
14
+ "attestationRegistry": "0xC695939EA5fB1d7a88BCF536EAC3C13bfcc9E50d",
15
+ "callDataHashEnforcer": "0x4ab106b80488FE6A920D973cDb96052a933c7450",
16
16
  "chainId": 84532,
17
- "custodyPolicy": "0xDeD6542A55476745846370562d1c75D15DDf2572",
18
- "delegationManager": "0x3a8E2cE74564f699b135db6f266ccDb563979C05",
17
+ "custodyPolicy": "0xfAFDc79bc03CC7cE27f7AA12A3180E4E4C0BC3F6",
18
+ "delegationManager": "0x10DB7c0C5887910722354ef37eF874fBD124026C",
19
19
  "deployer": "0x31ed17fb99e82E02085Ab4B3cbdaB05489098b44",
20
- "entryPoint": "0x9B33De9d9597de82Eb56b5B26C478Dc0a6955388",
21
- "geoFeatureRegistry": "0x78FdCae0aC9B0CD9943996282b8F39D77bec5b1c",
22
- "ontologyTermRegistry": "0xf486Ba6e9FD2d687e68a1a7A77D0FA27bb1E66c3",
23
- "permissionlessSubregistry": "0x1B8ED8693738e1A9DD653FEE5430d49e00202Bb7",
24
- "permissionlessSubregistryDemoAgent": "0xc76b1cdB4340398f59fD9C5d6Cb65fCdC606B33b",
25
- "quorumEnforcer": "0x76fb494aE991d1fe1603a91384BA4c1F890442a8",
26
- "relationshipTypeRegistry": "0x526fBF5900b18B1D06201493bcE470b416FB71C3",
27
- "shapeRegistry": "0x03AbD67A910abB2c5026f1973A05502e0473e316",
28
- "skillDefinitionRegistry": "0x5ae94aba46D7daA1Ca9eaE0051043Ad05bcc392B",
29
- "smartAgentPaymaster": "0x8eF92B9D62826052D8F7e6dcaB630dC3890bF540",
30
- "timestampEnforcer": "0x7c452434314A5bf2048f9263aeCd0cC20Fa07965",
31
- "universalSignatureValidator": "0xe8F3e531429F44D77dC3A331C18032bfa1550D84",
32
- "valueEnforcer": "0x7c4533e0A9fB0B1cb5aa70327b222a80d72d52db",
33
- "paymentEnforcer": "0xCB5D2dbfc11e146d568764471516C9DB734D2c60",
34
- "paymentReceiptRegistry": "0x391Ed4D7538f80a221F910539005bB00b996Ff9F",
35
- "mockUsdc": "0x8fb56ff3C13347DFC4E1287aE83E88deE5a7211C",
36
- "paymentEscrow": "0xC85EB1831e9180F5e2cA7854f362f6C9a6D727EA"
20
+ "entryPoint": "0x86Dc419feDD30F58250B314F5947aDd8cf379436",
21
+ "geoFeatureRegistry": "0x1C3b16aF3C2649f2b1ac78aFD6AB20dA0c379121",
22
+ "mockUsdc": "0x027de10c034f4055551C32491BCe65711BFE5f69",
23
+ "ontologyTermRegistry": "0x95C149Be91c925FB348Fa231aD28440FBaD562cF",
24
+ "paymentEnforcer": "0xB09b95E7BcB5d1b3C934facFBEd5bc474b15897E",
25
+ "paymentEscrow": "0x053dA1bdD4f0A75b82A75A46F189f137a5267928",
26
+ "paymentReceiptRegistry": "0x29645211A44714b06B8c4C55c109Ef285699f56c",
27
+ "permissionlessSubregistry": "0x542915A4118A28f49D40DAE53Ee0dc5721Aa4868",
28
+ "permissionlessSubregistryDemoAgent": "0x00EB165eA2fd33448bbBd5634a8ba92D616AEc1D",
29
+ "quorumEnforcer": "0x093ff9790601F712B3730C3cAa34f9353dC93739",
30
+ "relationshipTypeRegistry": "0x0250274222baBB42d44e72e9992DFEB701399a56",
31
+ "shapeRegistry": "0x7d0a4e9A981775a3807282bbf3499e77f5E2bA53",
32
+ "skillDefinitionRegistry": "0x7be2c5615d83Ca95e02b9BDe24879eb220cb1780",
33
+ "smartAgentPaymaster": "0xe93f4ea2cEA8CFF898948c54D6d6144A8F14ca72",
34
+ "timestampEnforcer": "0x118A74d6F59ecD9D19BA4b720157a007b6f39894",
35
+ "universalSignatureValidator": "0x3Bc1Debf2ac2a59548aC5Ad09404A56bA21b5803",
36
+ "valueEnforcer": "0xB7952Ca4a5E10efD24C03c069a0Ea7e262ebE959"
37
37
  }
@@ -1267,6 +1267,11 @@
1267
1267
  }
1268
1268
  ]
1269
1269
  },
1270
+ {
1271
+ "type": "error",
1272
+ "name": "AdminRequiresCustodyQuorum",
1273
+ "inputs": []
1274
+ },
1270
1275
  {
1271
1276
  "type": "error",
1272
1277
  "name": "AgenticPrimitivesAgentNotAllowedAsCustodian",
@@ -1310,6 +1315,11 @@
1310
1315
  }
1311
1316
  ]
1312
1317
  },
1318
+ {
1319
+ "type": "error",
1320
+ "name": "DelegatedGovernanceForbidden",
1321
+ "inputs": []
1322
+ },
1313
1323
  {
1314
1324
  "type": "error",
1315
1325
  "name": "DirectUpgradeBlocked",
@@ -390,6 +390,17 @@
390
390
  ],
391
391
  "anonymous": false
392
392
  },
393
+ {
394
+ "type": "error",
395
+ "name": "InfeasibleOrgConfig",
396
+ "inputs": [
397
+ {
398
+ "name": "nSigners",
399
+ "type": "uint8",
400
+ "internalType": "uint8"
401
+ }
402
+ ]
403
+ },
393
404
  {
394
405
  "type": "error",
395
406
  "name": "InsufficientTrusteesForMode",
@@ -778,6 +778,17 @@
778
778
  }
779
779
  ]
780
780
  },
781
+ {
782
+ "type": "error",
783
+ "name": "AdminSelfSignerNotAllowed",
784
+ "inputs": [
785
+ {
786
+ "name": "account",
787
+ "type": "address",
788
+ "internalType": "address"
789
+ }
790
+ ]
791
+ },
781
792
  {
782
793
  "type": "error",
783
794
  "name": "AdminUnauthorizedSigner",
@@ -12,6 +12,19 @@
12
12
  ],
13
13
  "stateMutability": "view"
14
14
  },
15
+ {
16
+ "type": "function",
17
+ "name": "custodyPolicy",
18
+ "inputs": [],
19
+ "outputs": [
20
+ {
21
+ "name": "",
22
+ "type": "address",
23
+ "internalType": "address"
24
+ }
25
+ ],
26
+ "stateMutability": "view"
27
+ },
15
28
  {
16
29
  "type": "function",
17
30
  "name": "governance",
@@ -20,22 +20,22 @@
20
20
  "name": "afterHook",
21
21
  "inputs": [
22
22
  {
23
- "name": "",
23
+ "name": "terms",
24
24
  "type": "bytes",
25
25
  "internalType": "bytes"
26
26
  },
27
27
  {
28
- "name": "",
28
+ "name": "args",
29
29
  "type": "bytes",
30
30
  "internalType": "bytes"
31
31
  },
32
32
  {
33
- "name": "",
33
+ "name": "delegationHash",
34
34
  "type": "bytes32",
35
35
  "internalType": "bytes32"
36
36
  },
37
37
  {
38
- "name": "",
38
+ "name": "delegator",
39
39
  "type": "address",
40
40
  "internalType": "address"
41
41
  },
@@ -55,13 +55,13 @@
55
55
  "internalType": "uint256"
56
56
  },
57
57
  {
58
- "name": "",
58
+ "name": "callData",
59
59
  "type": "bytes",
60
60
  "internalType": "bytes"
61
61
  }
62
62
  ],
63
63
  "outputs": [],
64
- "stateMutability": "view"
64
+ "stateMutability": "nonpayable"
65
65
  },
66
66
  {
67
67
  "type": "function",
@@ -412,6 +412,11 @@
412
412
  "name": "NotTransferCall",
413
413
  "inputs": []
414
414
  },
415
+ {
416
+ "type": "error",
417
+ "name": "SettlementNotObserved",
418
+ "inputs": []
419
+ },
415
420
  {
416
421
  "type": "error",
417
422
  "name": "ValueNotZero",
@@ -21,7 +21,11 @@ export interface Deployments {
21
21
  readonly "deployer": Address;
22
22
  readonly "entryPoint": Address;
23
23
  readonly "geoFeatureRegistry": Address;
24
+ readonly "mockUsdc": Address;
24
25
  readonly "ontologyTermRegistry": Address;
26
+ readonly "paymentEnforcer": Address;
27
+ readonly "paymentEscrow": Address;
28
+ readonly "paymentReceiptRegistry": Address;
25
29
  readonly "permissionlessSubregistry": Address;
26
30
  readonly "permissionlessSubregistryDemoAgent": Address;
27
31
  readonly "quorumEnforcer": Address;
@@ -32,10 +36,6 @@ export interface Deployments {
32
36
  readonly "timestampEnforcer": Address;
33
37
  readonly "universalSignatureValidator": Address;
34
38
  readonly "valueEnforcer": Address;
35
- readonly "paymentEnforcer": Address;
36
- readonly "paymentReceiptRegistry": Address;
37
- readonly "mockUsdc": Address;
38
- readonly "paymentEscrow": Address;
39
39
  readonly "chainId": number;
40
40
  }
41
41
  export declare const CONTRACTS: Deployments;
@@ -1,40 +1,40 @@
1
1
  // Generated by scripts/build-deployments.mjs — do not edit by hand.
2
2
  // Source: deployments-base-sepolia.json
3
3
  export const CONTRACTS = {
4
- "agentAccountFactory": "0x3E68B72B45e7C9d35B210E4Ab06e5Cece85cEbE4",
5
- "agentAccountImplementation": "0x338318ABf9C8b28E7e77ed96a91857cCE1Db1328",
6
- "agentNameRegistry": "0x15F7ed064A230C011b0244A14fD9653f011d609B",
7
- "agentNameResolver": "0xcC9855A23C7a4D4592E68Fe47E195C2f9D832473",
8
- "agentNameUniversalResolver": "0x7d777d2d0bbc1806B9Cc779121C27fbaAaFDb60b",
9
- "agentProfileResolver": "0xc448EB159B2a35F07f6E1814CC8d125244d7384B",
10
- "agentRegistryBase": "0xe320947b18D3C71710DCB75D4bf200147ded18Df",
11
- "agentRelationship": "0xE6B8F6C7F1c9857013dA356DF08D0384334A9607",
12
- "agreementRegistry": "0x7f4d6A74eda1D6ce251D371566f1c6639F9B0DDa",
13
- "allowedMethodsEnforcer": "0xf77cf06aCd8b9e2f3204fe4bE16449B3594c9633",
14
- "allowedTargetsEnforcer": "0xe3909FdEfD8b74B7D006E045DCd678e97D469Bfd",
15
- "approvedHashRegistry": "0x90735B5281434D1746926E2AA722Cc6DbF3EFEe6",
16
- "attestationRegistry": "0xaE3861fd299c74D96F6FCA25f19c7ACf8b9020ed",
17
- "callDataHashEnforcer": "0x9F12932F1b85A1F96c1Cb1c5A0645e3F0Ab478Dd",
4
+ "agentAccountFactory": "0xfCFafCbA6D88351aAAA190a5E971F06CAb40437d",
5
+ "agentAccountImplementation": "0x28b7d8e4682EF36bE479330Ba6CFE78E9E724Bc4",
6
+ "agentNameRegistry": "0x419928546Be67ba83C2aEaFCb6AD3364D5659C8D",
7
+ "agentNameResolver": "0xf96A25DeBF92C514459DC904ec8b79CE52D265A5",
8
+ "agentNameUniversalResolver": "0x3aC8Cb8Df1A8F9fcA05a37cEC82426b4432A53D3",
9
+ "agentProfileResolver": "0xb988e75179e11E4d17665d61c6a9Ce0e737A808F",
10
+ "agentRegistryBase": "0x7fab087fc3d0e1793bdb1d4c5d253635131ee1d1",
11
+ "agentRelationship": "0xDc409D69aC002B4FC90D4B453cc6B6277B98CB46",
12
+ "agreementRegistry": "0x882371Dc898f4835335a01558aE5C2B306069FcF",
13
+ "allowedMethodsEnforcer": "0x1Df87222f53ad26F599C60364F7232a3c0393ad9",
14
+ "allowedTargetsEnforcer": "0x55A9fD6aF8688EE9c68D15CDAa84d9DBD027B80F",
15
+ "approvedHashRegistry": "0x878272cc4E0c5bA026CEc29C9722F41fC0515fE5",
16
+ "attestationRegistry": "0xC695939EA5fB1d7a88BCF536EAC3C13bfcc9E50d",
17
+ "callDataHashEnforcer": "0x4ab106b80488FE6A920D973cDb96052a933c7450",
18
18
  "chainId": 84532,
19
- "custodyPolicy": "0xDeD6542A55476745846370562d1c75D15DDf2572",
20
- "delegationManager": "0x3a8E2cE74564f699b135db6f266ccDb563979C05",
19
+ "custodyPolicy": "0xfAFDc79bc03CC7cE27f7AA12A3180E4E4C0BC3F6",
20
+ "delegationManager": "0x10DB7c0C5887910722354ef37eF874fBD124026C",
21
21
  "deployer": "0x31ed17fb99e82E02085Ab4B3cbdaB05489098b44",
22
- "entryPoint": "0x9B33De9d9597de82Eb56b5B26C478Dc0a6955388",
23
- "geoFeatureRegistry": "0x78FdCae0aC9B0CD9943996282b8F39D77bec5b1c",
24
- "ontologyTermRegistry": "0xf486Ba6e9FD2d687e68a1a7A77D0FA27bb1E66c3",
25
- "permissionlessSubregistry": "0x1B8ED8693738e1A9DD653FEE5430d49e00202Bb7",
26
- "permissionlessSubregistryDemoAgent": "0xc76b1cdB4340398f59fD9C5d6Cb65fCdC606B33b",
27
- "quorumEnforcer": "0x76fb494aE991d1fe1603a91384BA4c1F890442a8",
28
- "relationshipTypeRegistry": "0x526fBF5900b18B1D06201493bcE470b416FB71C3",
29
- "shapeRegistry": "0x03AbD67A910abB2c5026f1973A05502e0473e316",
30
- "skillDefinitionRegistry": "0x5ae94aba46D7daA1Ca9eaE0051043Ad05bcc392B",
31
- "smartAgentPaymaster": "0x8eF92B9D62826052D8F7e6dcaB630dC3890bF540",
32
- "timestampEnforcer": "0x7c452434314A5bf2048f9263aeCd0cC20Fa07965",
33
- "universalSignatureValidator": "0xe8F3e531429F44D77dC3A331C18032bfa1550D84",
34
- "valueEnforcer": "0x7c4533e0A9fB0B1cb5aa70327b222a80d72d52db",
35
- "paymentEnforcer": "0xCB5D2dbfc11e146d568764471516C9DB734D2c60",
36
- "paymentReceiptRegistry": "0x391Ed4D7538f80a221F910539005bB00b996Ff9F",
37
- "mockUsdc": "0x8fb56ff3C13347DFC4E1287aE83E88deE5a7211C",
38
- "paymentEscrow": "0xC85EB1831e9180F5e2cA7854f362f6C9a6D727EA"
22
+ "entryPoint": "0x86Dc419feDD30F58250B314F5947aDd8cf379436",
23
+ "geoFeatureRegistry": "0x1C3b16aF3C2649f2b1ac78aFD6AB20dA0c379121",
24
+ "mockUsdc": "0x027de10c034f4055551C32491BCe65711BFE5f69",
25
+ "ontologyTermRegistry": "0x95C149Be91c925FB348Fa231aD28440FBaD562cF",
26
+ "paymentEnforcer": "0xB09b95E7BcB5d1b3C934facFBEd5bc474b15897E",
27
+ "paymentEscrow": "0x053dA1bdD4f0A75b82A75A46F189f137a5267928",
28
+ "paymentReceiptRegistry": "0x29645211A44714b06B8c4C55c109Ef285699f56c",
29
+ "permissionlessSubregistry": "0x542915A4118A28f49D40DAE53Ee0dc5721Aa4868",
30
+ "permissionlessSubregistryDemoAgent": "0x00EB165eA2fd33448bbBd5634a8ba92D616AEc1D",
31
+ "quorumEnforcer": "0x093ff9790601F712B3730C3cAa34f9353dC93739",
32
+ "relationshipTypeRegistry": "0x0250274222baBB42d44e72e9992DFEB701399a56",
33
+ "shapeRegistry": "0x7d0a4e9A981775a3807282bbf3499e77f5E2bA53",
34
+ "skillDefinitionRegistry": "0x7be2c5615d83Ca95e02b9BDe24879eb220cb1780",
35
+ "smartAgentPaymaster": "0xe93f4ea2cEA8CFF898948c54D6d6144A8F14ca72",
36
+ "timestampEnforcer": "0x118A74d6F59ecD9D19BA4b720157a007b6f39894",
37
+ "universalSignatureValidator": "0x3Bc1Debf2ac2a59548aC5Ad09404A56bA21b5803",
38
+ "valueEnforcer": "0xB7952Ca4a5E10efD24C03c069a0Ea7e262ebE959"
39
39
  };
40
40
  export default CONTRACTS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticprimitives/contracts",
3
- "version": "1.0.0-alpha.13",
3
+ "version": "1.0.0-alpha.14",
4
4
  "description": "Solidity contracts + ABIs + flattened sources + per-network deployment addresses for the agenticprimitives stack.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,6 +28,11 @@ interface IAgentAccountFactoryView {
28
28
  /// this address to call `IGovernanceView.isPaused()` from
29
29
  /// `whenNotPaused`-guarded entrypoints.
30
30
  function governance() external view returns (address);
31
+ /// @dev Wave-1/ACCT-C1 — the canonical CustodyPolicy this factory installs
32
+ /// as the custody executor for `mode > 0` accounts. AgentAccount reads it
33
+ /// to decide whether IT is custody-governed (its CustodyPolicy is installed)
34
+ /// vs. merely hosting an unrelated utility executor.
35
+ function custodyPolicy() external view returns (address);
31
36
  }
32
37
 
33
38
  /// @dev R6.5 — minimal interface for the pause read. Mirrors the same
@@ -158,6 +163,27 @@ contract AgentAccount is
158
163
  /// `_authorizeUpgrade` frame) AND absent from the storage layout.
159
164
  bool private transient _upgradeAuthorizedCtx;
160
165
 
166
+ /// @dev Wave-1 / ACCT-C1 (spec 300) — transient "this call stack was entered
167
+ /// by the installed CustodyPolicy EXECUTOR via `executeFromModule`"
168
+ /// (custody-quorum authorized) flag. Set ONLY inside `executeFromModule`,
169
+ /// around the dispatched call, cleared after. Governance mutators require
170
+ /// it on a custody-governed account (`mode > 0`, a CustodyPolicy EXECUTOR
171
+ /// installed): a 1-of-N UserOp `execute(self, adminSelector)` self-call
172
+ /// leaves the flag unset and reverts, so the quorum+timelock can no longer
173
+ /// be bypassed by a single compromised key. Person accounts (`mode 0`, no
174
+ /// CustodyPolicy) self-govern and are exempt. Transient ⇒ tx-global +
175
+ /// absent from the storage layout.
176
+ bool private transient _custodyGovCtx;
177
+
178
+ /// @dev Wave-1 / DM-3 (spec 300) — transient "this execution stack originated
179
+ /// from the DelegationManager". Set at the outermost `execute`/`executeBatch`
180
+ /// frame whose `msg.sender == _delegationManager`, preserved across the
181
+ /// legitimate `DM → execute(self, executeBatch(...))` nest, cleared by the
182
+ /// frame that set it. While set, a self-targeted governance selector is
183
+ /// rejected — a delegation may never reach account governance (that is the
184
+ /// CustodyPolicy's job), regardless of custody mode.
185
+ bool private transient _delegationExecCtx;
186
+
161
187
  /// @dev Spec 007 Phase A.5 — maximum permitted upgrade timelock to
162
188
  /// protect against an owner accidentally setting an absurdly
163
189
  /// large delay that bricks future migrations.
@@ -223,6 +249,15 @@ contract AgentAccount is
223
249
  error LegacyUpgradePathDisabled();
224
250
  error ModuleOperationNotAllowed();
225
251
 
252
+ // Wave 1 — account authorization boundary (spec 300 / ACCT-C1 + DM-3).
253
+ /// @notice A governance mutator was reached on a custody-governed account
254
+ /// (`mode > 0`) outside the CustodyPolicy `executeFromModule` path —
255
+ /// i.e. a single-signer UserOp `execute(self, adminSelector)` self-call.
256
+ error AdminRequiresCustodyQuorum();
257
+ /// @notice A delegation-originated execution stack tried to self-call an
258
+ /// account-governance selector. Delegations may never reach governance.
259
+ error DelegatedGovernanceForbidden();
260
+
226
261
  /// @notice Emitted when `setDelegationManager` rotates the DM via a
227
262
  /// self-call (the only authorized path post-Wave-2A).
228
263
  event DelegationManagerRotated(address indexed newDelegationManager);
@@ -441,8 +476,14 @@ contract AgentAccount is
441
476
  // matured owner queue — set `_upgradeAuthorizedCtx` and pass through, so the
442
477
  // custody path is not double-delayed. A zero timelock keeps the
443
478
  // immediate-upgrade default (backward-compatible).
444
- if (_upgradeTimelock != 0 && !_upgradeAuthorizedCtx) {
445
- revert DirectUpgradeBlocked();
479
+ // Wave-1/ACCT-C1 + M-4 (spec 300): outside an authorized context, a
480
+ // custody-governed account (`mode>0`) may upgrade ONLY through the
481
+ // CustodyPolicy quorum — reject even when the per-account timelock is 0 (M-4
482
+ // floors it non-zero at the factory, but a quorum-set 0 must not re-open the
483
+ // direct path). A person account keeps the CA-1 behavior (timelock if set).
484
+ if (!_upgradeAuthorizedCtx) {
485
+ if (_hasCustodyGovernance()) revert AdminRequiresCustodyQuorum();
486
+ if (_upgradeTimelock != 0) revert DirectUpgradeBlocked();
446
487
  }
447
488
  }
448
489
 
@@ -478,6 +519,7 @@ contract AgentAccount is
478
519
  /// meaningful when a timelock is configured — with none set, upgrade
479
520
  /// directly. Overwrites any existing pending upgrade.
480
521
  function scheduleUpgrade(address newImplementation) external onlySelf {
522
+ _requireCustodyGoverned();
481
523
  if (newImplementation == address(0)) revert ZeroAddress();
482
524
  uint256 tl = _upgradeTimelock;
483
525
  if (tl == 0) revert UpgradeTimelockNotSet();
@@ -533,6 +575,7 @@ contract AgentAccount is
533
575
  /// (`onlySelf`). 0 == immediate upgrades (default,
534
576
  /// backward-compat).
535
577
  function setUpgradeTimelock(uint256 secs) external onlySelf {
578
+ _requireCustodyGoverned();
536
579
  if (secs > MAX_UPGRADE_TIMELOCK) {
537
580
  revert UpgradeTimelockTooLong(secs, MAX_UPGRADE_TIMELOCK);
538
581
  }
@@ -576,6 +619,7 @@ contract AgentAccount is
576
619
  * a self-call into this function.
577
620
  */
578
621
  function setDelegationManager(address dm) external onlySelf {
622
+ _requireCustodyGoverned();
579
623
  if (dm.code.length == 0) revert ValidatorRequired();
580
624
  _delegationManager = dm;
581
625
  emit DelegationManagerRotated(dm);
@@ -639,6 +683,7 @@ contract AgentAccount is
639
683
  * delegation the owner has authorized.
640
684
  */
641
685
  function acceptSessionDelegation(bytes32 sessionDelegationHash) external onlySelf {
686
+ _requireCustodyGoverned();
642
687
  if (_acceptedSessionDelegations[sessionDelegationHash]) {
643
688
  revert SessionDelegationAlreadyAccepted(sessionDelegationHash);
644
689
  }
@@ -823,6 +868,10 @@ contract AgentAccount is
823
868
  address module,
824
869
  bytes calldata initData
825
870
  ) external onlySelfOrFactoryInit whenNotPaused {
871
+ // Wave-1/ACCT-C1: on a custody-governed account, module installs must route
872
+ // through the CustodyPolicy quorum. Exempt by construction at factory-init
873
+ // (no EXECUTOR installed yet ⇒ `_hasCustodyGovernance()` is false).
874
+ _requireCustodyGoverned();
826
875
  if (module == address(0)) revert ZeroAddress();
827
876
  if (!_isSupportedModuleType(moduleTypeId)) revert UnsupportedModuleType(moduleTypeId);
828
877
 
@@ -872,6 +921,7 @@ contract AgentAccount is
872
921
  // at deploy time, and there's never a legitimate reason for
873
922
  // the factory to do that.
874
923
  if (msg.sender != address(this)) revert ModuleOperationNotAllowed();
924
+ _requireCustodyGoverned();
875
925
  if (!_isSupportedModuleType(moduleTypeId)) revert UnsupportedModuleType(moduleTypeId);
876
926
 
877
927
  ModulesStorage storage $ = _modulesStorage();
@@ -978,14 +1028,18 @@ contract AgentAccount is
978
1028
  revert NotInstalledExecutor(msg.sender);
979
1029
  }
980
1030
  emit ModuleExecuted(msg.sender, target, value);
981
- // CA-1: a module action is a custody-governed context (only an installed
982
- // EXECUTOR — the CustodyPolicy — reaches here, gated upstream by its own
983
- // T5 quorum + timelock). Authorize any upgrade it dispatches so the custody
984
- // path is exempt from the per-account `_upgradeTimelock` (no double delay).
985
- // Transient + nonReentrant, cleared after the call.
1031
+ // CA-1 + Wave-1/ACCT-C1 (spec 300): a module action is a custody-governed
1032
+ // context (only an installed EXECUTOR — the CustodyPolicy — reaches here,
1033
+ // gated upstream by its own T5 quorum + timelock). `_custodyGovCtx` authorizes
1034
+ // every governance mutator this call dispatches (the ONLY way past
1035
+ // `_requireCustodyGoverned` on a `mode>0` account); `_upgradeAuthorizedCtx`
1036
+ // additionally exempts a dispatched upgrade from the per-account timelock (no
1037
+ // double delay). Both transient, cleared after the call.
1038
+ _custodyGovCtx = true;
986
1039
  _upgradeAuthorizedCtx = true;
987
1040
  (bool ok, bytes memory ret) = target.call{value: value}(data);
988
1041
  _upgradeAuthorizedCtx = false;
1042
+ _custodyGovCtx = false;
989
1043
  if (!ok) {
990
1044
  assembly { revert(add(ret, 32), mload(ret)) }
991
1045
  }
@@ -1035,6 +1089,13 @@ contract AgentAccount is
1035
1089
  function execute(address target, uint256 value, bytes calldata data) external override nonReentrant whenNotPaused {
1036
1090
  _requireForExecute();
1037
1091
 
1092
+ // DM-3 (spec 300): mark a delegation-originated stack (set once at the
1093
+ // outermost DM frame; preserved across the legitimate DM → execute(self,
1094
+ // executeBatch(...)) nest) and forbid it from self-calling governance.
1095
+ bool setDelCtx = (msg.sender == _delegationManager && !_delegationExecCtx);
1096
+ if (setDelCtx) _delegationExecCtx = true;
1097
+ _rejectDelegatedGovernance(target, data);
1098
+
1038
1099
  ModulesStorage storage $ = _modulesStorage();
1039
1100
  address[] memory hooks = $.installedList[MODULE_TYPE_HOOK];
1040
1101
  bytes[] memory hookData = new bytes[](hooks.length);
@@ -1062,6 +1123,8 @@ contract AgentAccount is
1062
1123
  for (uint256 i = 0; i < hooks.length; i++) {
1063
1124
  IERC7579HookLike(hooks[i]).postCheck(hookData[i]);
1064
1125
  }
1126
+
1127
+ if (setDelCtx) _delegationExecCtx = false;
1065
1128
  }
1066
1129
 
1067
1130
  /// @notice Atomically execute multiple calls from this account.
@@ -1088,6 +1151,13 @@ contract AgentAccount is
1088
1151
  function executeBatch(Call[] calldata calls) external override whenNotPaused {
1089
1152
  _requireForExecute();
1090
1153
 
1154
+ // DM-3 (spec 300): same delegation-origin marking as `execute`. Preserved
1155
+ // across the DM → execute(self, executeBatch(...)) nest so a governance
1156
+ // self-call in ANY inner call is caught even though this frame's
1157
+ // msg.sender is address(this).
1158
+ bool setDelCtx = (msg.sender == _delegationManager && !_delegationExecCtx);
1159
+ if (setDelCtx) _delegationExecCtx = true;
1160
+
1091
1161
  ModulesStorage storage $ = _modulesStorage();
1092
1162
  address[] memory hooks = $.installedList[MODULE_TYPE_HOOK];
1093
1163
  bytes[] memory hookData = new bytes[](hooks.length);
@@ -1101,6 +1171,7 @@ contract AgentAccount is
1101
1171
  }
1102
1172
 
1103
1173
  for (uint256 i = 0; i < calls.length; i++) {
1174
+ _rejectDelegatedGovernance(calls[i].target, calls[i].data);
1104
1175
  (bool ok, bytes memory ret) = calls[i].target.call{ value: calls[i].value }(calls[i].data);
1105
1176
  if (!ok) {
1106
1177
  assembly {
@@ -1113,6 +1184,8 @@ contract AgentAccount is
1113
1184
  for (uint256 i = 0; i < hooks.length; i++) {
1114
1185
  IERC7579HookLike(hooks[i]).postCheck(hookData[i]);
1115
1186
  }
1187
+
1188
+ if (setDelCtx) _delegationExecCtx = false;
1116
1189
  }
1117
1190
 
1118
1191
  // ─── ERC-4337 ───────────────────────────────────────────────────
@@ -1146,6 +1219,68 @@ contract AgentAccount is
1146
1219
  }
1147
1220
  }
1148
1221
 
1222
+ // ─── Wave 1 — account authorization boundary (spec 300) ─────────
1223
+
1224
+ /// @dev True when this account is custody-governed — i.e. the canonical
1225
+ /// CustodyPolicy the factory installs for `mode > 0` accounts is installed
1226
+ /// as an EXECUTOR here. Governance mutators then require the
1227
+ /// `executeFromModule` (quorum+timelock) path. A `mode 0` person account
1228
+ /// (no CustodyPolicy) self-governs; installing an UNRELATED utility executor
1229
+ /// does NOT make it custody-governed (we check the specific policy address,
1230
+ /// not "any executor"). If the factory is unknown (legacy direct deploy),
1231
+ /// fall back to person semantics.
1232
+ function _hasCustodyGovernance() internal view returns (bool) {
1233
+ address f = _factory;
1234
+ if (f == address(0)) return false;
1235
+ address cp = IAgentAccountFactoryView(f).custodyPolicy();
1236
+ if (cp == address(0)) return false;
1237
+ return _modulesStorage().installed[MODULE_TYPE_EXECUTOR][cp];
1238
+ }
1239
+
1240
+ /// @dev ACCT-C1 gate: on a custody-governed account, a governance mutator is
1241
+ /// reachable only through `executeFromModule` (which sets `_custodyGovCtx`).
1242
+ /// A single-signer UserOp `execute(self, adminSelector)` self-call leaves the
1243
+ /// flag unset and reverts here, so the custody quorum can no longer be bypassed.
1244
+ function _requireCustodyGoverned() internal view {
1245
+ if (_hasCustodyGovernance() && !_custodyGovCtx) revert AdminRequiresCustodyQuorum();
1246
+ }
1247
+
1248
+ /// @dev DM-3 gate: a delegation-originated stack may never self-call an account
1249
+ /// governance selector. `_delegationExecCtx` is set at the outermost
1250
+ /// DM-driven `execute`/`executeBatch` and persists across the legitimate
1251
+ /// `DM → execute(self, executeBatch(...))` nest, so a nested governance
1252
+ /// self-call is caught regardless of mode.
1253
+ function _rejectDelegatedGovernance(address target, bytes calldata data) internal view {
1254
+ if (
1255
+ _delegationExecCtx &&
1256
+ target == address(this) &&
1257
+ data.length >= 4 &&
1258
+ _isGovernanceSelector(bytes4(data[0:4]))
1259
+ ) {
1260
+ revert DelegatedGovernanceForbidden();
1261
+ }
1262
+ }
1263
+
1264
+ /// @dev The account-governance selector set — mutators that change custody,
1265
+ /// the DelegationManager, installed modules, or the upgrade path. Kept in
1266
+ /// sync with the `_requireCustodyGoverned`-gated functions above.
1267
+ function _isGovernanceSelector(bytes4 s) internal pure returns (bool) {
1268
+ return
1269
+ s == this.setDelegationManager.selector ||
1270
+ s == UUPSUpgradeable.upgradeToAndCall.selector ||
1271
+ s == this.setUpgradeTimelock.selector ||
1272
+ s == this.scheduleUpgrade.selector ||
1273
+ s == this.executePendingUpgrade.selector ||
1274
+ s == this.cancelPendingUpgrade.selector ||
1275
+ s == this.installModule.selector ||
1276
+ s == this.uninstallModule.selector ||
1277
+ s == IAgentAccount.addCustodian.selector ||
1278
+ s == IAgentAccount.removeCustodian.selector ||
1279
+ s == this.addPasskey.selector ||
1280
+ s == this.removePasskey.selector ||
1281
+ s == this.acceptSessionDelegation.selector;
1282
+ }
1283
+
1149
1284
  // ─── ERC-1271 ───────────────────────────────────────────────────
1150
1285
 
1151
1286
  /// @dev 32-byte ERC-6492 magic suffix — `0x6492…6492` repeated.
@@ -1359,6 +1494,7 @@ contract AgentAccount is
1359
1494
  /// the ERC-165 marker. Passkey custodians are not added here;
1360
1495
  /// use `addPasskey` instead.
1361
1496
  function addCustodian(address owner) external override onlySelf whenNotPaused {
1497
+ _requireCustodyGoverned();
1362
1498
  if (owner == address(0)) revert ZeroAddress();
1363
1499
  if (_externalCustodians[owner]) revert CustodianAlreadyExists(owner);
1364
1500
  if (_isAgenticPrimitivesAgent(owner)) {
@@ -1402,6 +1538,7 @@ contract AgentAccount is
1402
1538
  /// owner if there are also no registered passkeys. A passkey-only
1403
1539
  /// account is allowed, but a zero-signer account is not.
1404
1540
  function removeCustodian(address owner) external override onlySelf {
1541
+ _requireCustodyGoverned();
1405
1542
  if (!_externalCustodians[owner]) revert CustodianDoesNotExist(owner);
1406
1543
  if (_externalCustodianCount == 1 && _passkeyStorage().count == 0) revert CannotRemoveLastCustodian();
1407
1544
  _externalCustodians[owner] = false;
@@ -1466,6 +1603,7 @@ contract AgentAccount is
1466
1603
  uint256 y,
1467
1604
  bytes32 rpIdHash
1468
1605
  ) external onlySelf {
1606
+ _requireCustodyGoverned();
1469
1607
  if (x == 0 || y == 0) revert InvalidPasskeyPublicKey();
1470
1608
  // Audit C-6: zero digest poisons piaToCredentialId mapping.
1471
1609
  if (credentialIdDigest == bytes32(0)) revert InvalidCredentialIdDigest();
@@ -1490,6 +1628,7 @@ contract AgentAccount is
1490
1628
  /// AND passkeys together. Also clears the credential's PIA
1491
1629
  /// entry so `isCustodian(pia)` flips back to false.
1492
1630
  function removePasskey(bytes32 credentialIdDigest) external onlySelf {
1631
+ _requireCustodyGoverned();
1493
1632
  PasskeyStorage storage $ = _passkeyStorage();
1494
1633
  if (!$.registered[credentialIdDigest]) revert PasskeyNotRegistered(credentialIdDigest);
1495
1634
  if (_externalCustodianCount + $.count == 1) revert CannotRemoveLastSigner();
@@ -77,6 +77,9 @@ contract AgentAccountFactory is GovernanceManaged {
77
77
  error NoInitialSigner();
78
78
  error InsufficientTrusteesForMode(uint8 mode, uint256 actual, uint256 required);
79
79
  error TrusteesRequiredForRecoverableMode(uint8 mode);
80
+ /// @notice ACCT-M1 (spec 300): a mode-3 (org) account needs ≥2 signers so its separation-of-duties
81
+ /// (disjoint propose + apply quorums) is satisfiable — a 1-signer org can never split duties.
82
+ error InfeasibleOrgConfig(uint8 nSigners);
80
83
  error ZeroAddress();
81
84
  /// @notice R9.6 / ATL-SEC-05 -- thrown when `params.custodians.length`
82
85
  /// exceeds `MAX_INITIAL_CUSTODIANS`. Without this cap,
@@ -308,6 +311,19 @@ contract AgentAccountFactory is GovernanceManaged {
308
311
  thresholds[t] = CustodyPolicy(custodyPolicy).defaultApprovals(uint8(nSigners), t);
309
312
  }
310
313
 
314
+ // ACCT-M1 (spec 300): mode-3 (org) separation-of-duties requires DISJOINT propose + apply quorums
315
+ // for the T4/T5 tiers, so a satisfiable config needs 2×threshold DISTINCT signers. The default
316
+ // matrix sets T4/T5 = N for small N (all custodians), which would demand 2N signers — making every
317
+ // such change permanently un-executable (the account silently locks). Cap the SoD-constrained
318
+ // thresholds at floor(N/2) so a disjoint quorum always exists, and require ≥2 signers (a 1-signer
319
+ // org can't split duties). Modes 0-2 have no SoD, so their thresholds are unchanged.
320
+ if (params.mode == 3) {
321
+ if (nSigners < 2) revert InfeasibleOrgConfig(uint8(nSigners));
322
+ uint8 sodCap = uint8(nSigners / 2); // ≥ 1 since nSigners ≥ 2
323
+ if (thresholds[4] > sodCap) thresholds[4] = sodCap;
324
+ if (thresholds[5] > sodCap) thresholds[5] = sodCap;
325
+ }
326
+
311
327
  uint32[7] memory timelocks;
312
328
  timelocks[4] = timelockOverrides[4] == 0 ? uint32(1 hours) : timelockOverrides[4];
313
329
  timelocks[5] = timelockOverrides[5] == 0 ? uint32(24 hours) : timelockOverrides[5];
@@ -260,16 +260,21 @@ contract SmartAgentPaymaster is BasePaymaster {
260
260
  return _pausedMirror;
261
261
  }
262
262
 
263
- /// @dev Read the canonical governance pause flag. Used ONLY by the
264
- /// out-of-band sync path, never during validation. Mirrors the prior
265
- /// defensive decode: a non-conforming governance (EOA / missing fn)
266
- /// reads as "not paused" so legacy / test deploys keep working.
263
+ /// @dev Read the canonical governance pause flag. Used ONLY by the out-of-band sync path, never
264
+ /// during validation (ERC-7562 scope). L1 (seam audit): distinguish "no governance configured"
265
+ /// (legacy / test deploy — not paused, by design) from "governance IS configured but the read
266
+ /// faulted / returned malformed data" (a governance outage or non-conforming contract), which now
267
+ /// FAILS CLOSED — treat as paused so a sync during a governance fault halts sponsorship rather
268
+ /// than fail-open sponsoring. Only a clean, decodable `false` permits sponsorship.
267
269
  function _governanceIsPaused() internal view returns (bool) {
268
- if (governance.code.length == 0) return false;
270
+ // Unconfigured governance (unset, or an EOA with no code) → not paused (legacy/test path).
271
+ if (governance == address(0) || governance.code.length == 0) return false;
269
272
  (bool ok, bytes memory data) = governance.staticcall(
270
273
  abi.encodeWithSelector(IGovernanceView.isPaused.selector)
271
274
  );
272
- return ok && data.length >= 32 && abi.decode(data, (bool));
275
+ // Configured but faulted (call failed or undecodable) → fail closed (paused).
276
+ if (!ok || data.length < 32) return true;
277
+ return abi.decode(data, (bool));
273
278
  }
274
279
 
275
280
  // ─── PM-2: governance-timelocked deposit withdrawal ────────────────
@@ -309,6 +309,11 @@ contract CustodyPolicy {
309
309
  error AdminInsufficientQuorum(uint256 supplied, uint8 required);
310
310
  error AdminDuplicateOrUnsortedSigner(address signer);
311
311
  error AdminUnauthorizedSigner(address signer);
312
+ /// @notice ACCT-C2 (spec 300) — the account itself was presented as a quorum
313
+ /// signer (via `isCustodian(address(this))==true` or the `v=0`
314
+ /// self-referential ERC-1271 slot). A self-signature must never count
315
+ /// toward the quorum, else one custodian forges an extra member.
316
+ error AdminSelfSignerNotAllowed(address account);
312
317
  error TrusteeAlreadyExists(address guardian);
313
318
  error TrusteeDoesNotExist(address guardian);
314
319
  error InvalidMode(uint8 mode_);
@@ -748,6 +753,11 @@ contract CustodyPolicy {
748
753
  );
749
754
  if (signer <= prev) revert AdminDuplicateOrUnsortedSigner(signer);
750
755
  prev = signer;
756
+ // ACCT-C2 (spec 300): the account is never its own quorum signer. Both
757
+ // exploit shapes — `isCustodian(address(this))==true` and the `v=0`
758
+ // self-referential ERC-1271 slot that recovers `signer==account` — land
759
+ // here as `signer == account`, so one guard closes both.
760
+ if (signer == account) revert AdminSelfSignerNotAllowed(account);
751
761
  if (vargs.guardianMode) {
752
762
  if (!c.trustees[signer]) revert UnauthorizedTrustee(signer);
753
763
  } else {
@@ -58,6 +58,13 @@ contract PaymentEnforcer is ICaveatEnforcer {
58
58
  /// delegator => delegationHash => nonce => consumed (on-chain replay guard)
59
59
  mapping(address => mapping(bytes32 => mapping(bytes32 => bool))) private _nonceUsed;
60
60
 
61
+ /// @notice M1 (seam audit): keccak256(delegator, delegationHash, nonce) => (treasury balanceBefore + 1).
62
+ /// Snapshotted in `beforeHook`; consumed in `afterHook` to verify the transfer ACTUALLY moved
63
+ /// `amount` to the treasury before the settlement receipt is written. `+1` sentinel so a set
64
+ /// entry is distinguishable from an unset slot even when the treasury balance is 0. Set and
65
+ /// deleted within a single non-reentrant `redeemDelegation`, so it never persists across txs.
66
+ mapping(bytes32 => uint256) private _settleSnap;
67
+
61
68
  /// @notice The ONLY authorized caller of the hooks (EXT-PMT-1 / C-NEW-1). Unlike the stateless
62
69
  /// enforcers (timestamp/value/targets/methods), this enforcer MUTATES state (budget, nonce)
63
70
  /// and writes a settlement receipt — so it is NOT safe to call externally. A direct caller
@@ -93,6 +100,10 @@ contract PaymentEnforcer is ICaveatEnforcer {
93
100
  error FrequencyExceeded(uint32 callsInWindow, uint32 maxRedemptionsPerWindow);
94
101
  error NonceReused(bytes32 nonce);
95
102
  error NotDelegationManager(address caller);
103
+ /// @notice M1 (seam audit): the transfer did not move `amount` to the treasury (a token that returns
104
+ /// false without reverting, a fee-on-transfer skim, or a missing beforeHook snapshot) — so no
105
+ /// settlement receipt is written and the whole redemption reverts.
106
+ error SettlementNotObserved();
96
107
 
97
108
  /// @dev The hooks are state-mutating + receipt-emitting, so ONLY the DelegationManager may invoke
98
109
  /// them (EXT-PMT-1). `_runBeforeHooks`/`_runAfterHooks` call the enforcer directly, so during a
@@ -173,19 +184,49 @@ contract PaymentEnforcer is ICaveatEnforcer {
173
184
  delegator, delegationHash, mandateId, treasury, asset, amount, nonce, resourceHash, nextSpent, nextCalls
174
185
  );
175
186
 
176
- // Trustless settlement receipt (PAY-CON-4). Reverting here reverts the whole redemption, so a
177
- // settled charge always has a receipt and an unsettled one never does (atomic).
187
+ // M1 (seam audit): snapshot the treasury balance BEFORE the transfer. The trustless settlement
188
+ // receipt (PAY-CON-4) is written in `afterHook` ONLY once the transfer is observed to have moved
189
+ // `amount` to the treasury — a token that returns false without reverting (or a fee-on-transfer
190
+ // skim) then records NO receipt and reverts the whole redemption, so "a settled charge always has
191
+ // a receipt and an unsettled one never does" holds against the value that ACTUALLY moved.
192
+ _settleSnap[_settleKey(delegator, delegationHash, nonce)] = IERC20(asset).balanceOf(treasury) + 1;
193
+ }
194
+
195
+ function afterHook(
196
+ bytes calldata terms,
197
+ bytes calldata args,
198
+ bytes32 delegationHash,
199
+ address delegator,
200
+ address, /*redeemer*/
201
+ address, /*target*/
202
+ uint256, /*value*/
203
+ bytes calldata callData
204
+ ) external override onlyDelegationManager {
205
+ // Only this enforcer's own 192-byte-terms caveat carries a snapshot (beforeHook validated the
206
+ // shape); ignore anything else defensively so a malformed sibling caveat can't revert settlement.
207
+ if (terms.length != 192) return;
208
+ (address treasury, address asset,,,,) = abi.decode(terms, (address, address, uint256, uint256, uint32, uint32));
209
+ (bytes32 mandateId, bytes32 nonce, bytes32 resourceHash) = abi.decode(args, (bytes32, bytes32, bytes32));
210
+ (, uint256 amount) = abi.decode(callData[4:], (address, uint256));
211
+
212
+ bytes32 key = _settleKey(delegator, delegationHash, nonce);
213
+ uint256 snap = _settleSnap[key];
214
+ if (snap == 0) revert SettlementNotObserved(); // no beforeHook snapshot (defensive)
215
+ delete _settleSnap[key];
216
+
217
+ // The treasury must have received AT LEAST `amount` since the snapshot. A silent transfer failure
218
+ // (balance unchanged) or a fee-on-transfer short-delivery fails this and reverts the redemption.
219
+ if (IERC20(asset).balanceOf(treasury) < (snap - 1) + amount) revert SettlementNotObserved();
220
+
178
221
  if (address(receiptRegistry) != address(0)) {
179
222
  receiptRegistry.record(mandateId, delegator, treasury, asset, amount, resourceHash, delegationHash, nonce);
180
223
  }
181
224
  }
182
225
 
183
- function afterHook(bytes calldata, bytes calldata, bytes32, address, address, address, uint256, bytes calldata)
184
- external
185
- view
186
- override
187
- onlyDelegationManager
188
- {}
226
+ /// @dev Unique per redemption — the nonce is single-use per [delegator][delegationHash].
227
+ function _settleKey(address delegator, bytes32 delegationHash, bytes32 nonce) private pure returns (bytes32) {
228
+ return keccak256(abi.encode(delegator, delegationHash, nonce));
229
+ }
189
230
 
190
231
  // ── views (off-chain budget display + idempotency checks) ──
191
232
 
@@ -28,6 +28,9 @@ library AgentProfilePredicates {
28
28
  // ─── Identity-only ──────────────────────────────────────────────
29
29
 
30
30
  bytes32 internal constant ATL_DESCRIPTION = keccak256("atl:description");
31
+ /// spec 282 — publicly-asserted skill labels (string; the indexer projects to approf:skills, the
32
+ /// discovery matcher ranks on it). The agent's full private skill claims live in agent-skills VCs.
33
+ bytes32 internal constant ATL_SKILLS = keccak256("atl:skills");
31
34
  bytes32 internal constant ATL_HOMEPAGE = keccak256("atl:homepage");
32
35
  bytes32 internal constant ATL_AVATAR = keccak256("atl:avatar");
33
36
  bytes32 internal constant ATL_PROFILE_SCHEMA_URI = keccak256("atl:profileSchemaURI");
@@ -55,6 +55,17 @@ library AgentNamePredicates {
55
55
  /// (per ADR-0008). `string`.
56
56
  bytes32 internal constant ATL_NATIVE_ID = keccak256("atl:nativeId");
57
57
 
58
+ /// `atl:connectionKind` — connection-bootstrap credential KIND (spec 280).
59
+ /// Stored as `bytes32` (hashed enum). Bound to CONNECTION_KIND_ENUM. OPT-IN,
60
+ /// owner-set, PUBLIC. Advertises how a returning person connects by name.
61
+ bytes32 internal constant ATL_CONNECTION_KIND = keccak256("atl:connectionKind");
62
+
63
+ /// `atl:connectionAddress` — connection-bootstrap credential ADDRESS (spec 280).
64
+ /// The custodian EOA (wallet) or social KMS C_sub. `address`. OPT-IN, owner-set,
65
+ /// PUBLIC pre-select hint only (NOT required to connect). Deliberate, documented
66
+ /// name→credential index (ADR-0040 amendment). Never set automatically.
67
+ bytes32 internal constant ATL_CONNECTION_ADDRESS = keccak256("atl:connectionAddress");
68
+
58
69
  // ─── Shape + enum-set ids ───────────────────────────────────────
59
70
 
60
71
  /// `atl:AgentName` — the class id for ShapeRegistry validation.
@@ -71,4 +82,14 @@ library AgentNamePredicates {
71
82
  bytes32 internal constant AGENT_KIND_PERSON = keccak256("person");
72
83
  bytes32 internal constant AGENT_KIND_ORG = keccak256("org");
73
84
  bytes32 internal constant AGENT_KIND_SERVICE = keccak256("service");
85
+
86
+ /// Enum set bound to `atl:connectionKind` (spec 280). Contains the
87
+ /// five hashed member ids below.
88
+ bytes32 internal constant CONNECTION_KIND_ENUM = keccak256("atl:ConnectionKindEnum");
89
+
90
+ bytes32 internal constant CONNECTION_KIND_WALLET = keccak256("wallet");
91
+ bytes32 internal constant CONNECTION_KIND_GOOGLE = keccak256("google");
92
+ bytes32 internal constant CONNECTION_KIND_YOUVERSION = keccak256("youversion");
93
+ bytes32 internal constant CONNECTION_KIND_PASSKEY = keccak256("passkey");
94
+ bytes32 internal constant CONNECTION_KIND_MULTI = keccak256("multi");
74
95
  }
@@ -261,7 +261,10 @@ contract AgentNameRegistry is GovernanceManaged {
261
261
 
262
262
  bytes32 lh = keccak256(bytes(label));
263
263
  childNode = keccak256(abi.encodePacked(parentNode, lh));
264
- if (_records[childNode].registeredAt != 0) revert NodeAlreadyExists();
264
+ // M2 (seam audit): an EXPIRED name is reclaimable (parent-authorized re-register). Only a LIVE
265
+ // record blocks re-registration — previously expiry was inert and a name could never be reclaimed.
266
+ bool reclaiming = _records[childNode].registeredAt != 0;
267
+ if (reclaiming && !isExpired(childNode)) revert NodeAlreadyExists();
265
268
 
266
269
  _records[childNode] = NameRecord({
267
270
  owner: newOwner,
@@ -274,7 +277,8 @@ contract AgentNameRegistry is GovernanceManaged {
274
277
  });
275
278
 
276
279
  _children[parentNode][lh] = childNode;
277
- _childLabels[parentNode].push(lh);
280
+ // Only index a brand-new child; a reclaim reuses the existing _childLabels entry (no duplicate).
281
+ if (!reclaiming) _childLabels[parentNode].push(lh);
278
282
  _label[childNode] = label;
279
283
 
280
284
  emit NameRegistered(childNode, parentNode, label, newOwner, resolverContract, expiry);
@@ -352,7 +356,13 @@ contract AgentNameRegistry is GovernanceManaged {
352
356
  function parent(bytes32 node) external view returns (bytes32) { return _records[node].parent; }
353
357
  function labelhash(bytes32 node) external view returns (bytes32) { return _records[node].labelhash; }
354
358
  function expiry(bytes32 node) external view returns (uint64) { return _records[node].expiry; }
355
- function recordExists(bytes32 node) external view returns (bool) { return _records[node].registeredAt != 0; }
359
+ /// @dev M2 (seam audit): an EXPIRED record does NOT exist for read purposes — so the resolver
360
+ /// (resolveName/resolveString, which gate on recordExists) and any other reader stop resolving
361
+ /// an expired name. Expiry now provides real revocation, not a decorative field. Registry
362
+ /// mutators gate on the raw `registeredAt`/`isExpired`, not this, so re-registration is unaffected.
363
+ function recordExists(bytes32 node) external view returns (bool) {
364
+ return _records[node].registeredAt != 0 && !isExpired(node);
365
+ }
356
366
  function registeredAt(bytes32 node) external view returns (uint64) { return _records[node].registeredAt; }
357
367
 
358
368
  function childNode(bytes32 parentNode, bytes32 lh) external view returns (bytes32) {