@agenticprimitives/contracts 0.1.0-alpha.2 → 1.0.0-alpha.10

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.
Files changed (74) hide show
  1. package/AUDIT.md +324 -47
  2. package/README.md +45 -10
  3. package/deployments-base-sepolia.json +36 -1
  4. package/deployments-registries-base-sepolia.json +1 -0
  5. package/dist/abi/AgentAccount.json +1670 -0
  6. package/dist/abi/AgentAccountFactory.json +477 -0
  7. package/dist/abi/AgentNameRegistry.json +51 -0
  8. package/dist/abi/AgenticGovernance.json +32 -0
  9. package/dist/abi/AgreementRegistry.json +427 -0
  10. package/dist/abi/AttestationRegistry.json +436 -0
  11. package/dist/abi/CallDataHashEnforcer.json +119 -0
  12. package/dist/abi/CustodyPolicy.json +1122 -0
  13. package/dist/abi/DelegationManager.json +645 -0
  14. package/dist/abi/GeoFeatureRegistry.json +625 -0
  15. package/dist/abi/IAgentAccount.json +262 -0
  16. package/dist/abi/IAgentAccountFactoryAccessor.json +15 -0
  17. package/dist/abi/IAgentAccountFactoryView.json +41 -0
  18. package/dist/abi/IAgentAccountPauseView.json +15 -0
  19. package/dist/abi/IAgenticPrimitivesAgentAccount.json +15 -0
  20. package/dist/abi/IApprovedHashRegistryView.json +26 -0
  21. package/dist/abi/ICustodyPolicyFactoryView.json +15 -0
  22. package/dist/abi/ICustodyPolicyPauseView.json +15 -0
  23. package/dist/abi/IDelegationManager.json +171 -0
  24. package/dist/abi/IERC7579HookLike.json +44 -0
  25. package/dist/abi/IERC7579ModuleLike.json +28 -0
  26. package/dist/abi/IPaymentReceiptRegistry.json +50 -0
  27. package/dist/abi/MockUSDC.json +621 -0
  28. package/dist/abi/PaymentEnforcer.json +407 -0
  29. package/dist/abi/PaymentEscrow.json +299 -0
  30. package/dist/abi/PaymentReceiptRegistry.json +182 -0
  31. package/dist/abi/PermissionlessSubregistry.json +5 -0
  32. package/dist/abi/QuorumEnforcer.json +311 -0
  33. package/dist/abi/SignatureSlotRecovery.json +88 -0
  34. package/dist/abi/SkillDefinitionRegistry.json +491 -0
  35. package/dist/abi/SmartAgentPaymaster.json +873 -0
  36. package/dist/abi/UniversalSignatureValidator.json +65 -0
  37. package/dist/abi/index.d.ts +28 -0
  38. package/dist/abi/index.js +28 -0
  39. package/dist/deployments/base-sepolia.d.ts +41 -0
  40. package/dist/deployments/base-sepolia.js +39 -0
  41. package/dist/deployments/index.d.ts +9 -0
  42. package/dist/deployments/index.js +16 -0
  43. package/dist/deployments/registries-base-sepolia.d.ts +9 -0
  44. package/dist/deployments/registries-base-sepolia.js +7 -0
  45. package/package.json +28 -7
  46. package/src/AgentAccount.sol +191 -11
  47. package/src/AgentAccountFactory.sol +66 -12
  48. package/src/SmartAgentPaymaster.sol +186 -20
  49. package/src/UniversalSignatureValidator.sol +3 -0
  50. package/src/agency/DelegationManager.sol +170 -0
  51. package/src/agency/IDelegationManager.sol +38 -0
  52. package/src/agreement/AgreementRegistry.sol +331 -0
  53. package/src/attestation/AttestationRegistry.sol +384 -0
  54. package/src/custody/CustodyPolicy.sol +138 -8
  55. package/src/enforcers/AllowedMethodsEnforcer.sol +6 -0
  56. package/src/enforcers/AllowedTargetsEnforcer.sol +6 -0
  57. package/src/enforcers/CallDataHashEnforcer.sol +61 -0
  58. package/src/enforcers/PaymentEnforcer.sol +194 -0
  59. package/src/enforcers/QuorumEnforcer.sol +20 -0
  60. package/src/enforcers/TimestampEnforcer.sol +7 -0
  61. package/src/enforcers/ValueEnforcer.sol +6 -0
  62. package/src/geo/GeoFeatureRegistry.sol +197 -0
  63. package/src/governance/AgenticGovernance.sol +26 -4
  64. package/src/libraries/SignatureSlotRecovery.sol +8 -3
  65. package/src/libraries/WebAuthnLib.sol +13 -0
  66. package/src/mocks/MockUSDC.sol +152 -0
  67. package/src/naming/AgentNameRegistry.sol +52 -15
  68. package/src/naming/AgentNameUniversalResolver.sol +1 -1
  69. package/src/naming/PermissionlessSubregistry.sol +19 -2
  70. package/src/payments/PaymentEscrow.sol +137 -0
  71. package/src/payments/PaymentReceiptRegistry.sol +67 -0
  72. package/src/relationships/AgentRelationship.sol +8 -0
  73. package/src/skills/SkillDefinitionRegistry.sol +187 -0
  74. package/deployments-anvil.json +0 -1
@@ -22,6 +22,33 @@ import "./libraries/WebAuthnLib.sol";
22
22
  interface IAgentAccountFactoryView {
23
23
  function bundlerSigner() external view returns (address);
24
24
  function sessionIssuer() external view returns (address);
25
+ /// @dev R6.5 — the factory inherits `GovernanceManaged`; its
26
+ /// `governance()` getter returns the AgenticGovernance pointer
27
+ /// shared across every account it creates. AgentAccount reads
28
+ /// this address to call `IGovernanceView.isPaused()` from
29
+ /// `whenNotPaused`-guarded entrypoints.
30
+ function governance() external view returns (address);
31
+ }
32
+
33
+ /// @dev R6.5 — minimal interface for the pause read. Mirrors the same
34
+ /// selector `GovernanceManaged._pausedSafe()` uses so an EOA or
35
+ /// non-conforming governance contract is treated as "not paused"
36
+ /// (legacy / test deploys still work). Production deploys ALWAYS
37
+ /// use `AgenticGovernance` which implements this interface.
38
+ interface IAgentAccountPauseView {
39
+ function isPaused() external view returns (bool);
40
+ }
41
+
42
+ /// @dev Spec 253 — minimal read surface of `ApprovedHashRegistry`. The
43
+ /// account consults it ONLY from `isValidSignature` (the ERC-1271
44
+ /// entrypoint), under the `0x03` sentinel, so a hash the account
45
+ /// itself pre-approved (inside its own custody-gated execute/batch)
46
+ /// validates as an ERC-1271 signature. Address is bound IMMUTABLE in
47
+ /// the impl (see constructor) — never an owner-mutable view — so a
48
+ /// factory/governance key compromise cannot re-point it at an
49
+ /// attacker registry to forge signatures.
50
+ interface IApprovedHashRegistryView {
51
+ function isApproved(address signer, bytes32 hash) external view returns (bool);
25
52
  }
26
53
 
27
54
  /**
@@ -60,6 +87,14 @@ contract AgentAccount is
60
87
  /// @dev The ERC-4337 EntryPoint contract
61
88
  IEntryPoint private immutable _entryPoint;
62
89
 
90
+ /// @dev Spec 253 — the `ApprovedHashRegistry` consulted by the `0x03`
91
+ /// sentinel branch of `isValidSignature`. IMMUTABLE (baked into
92
+ /// impl bytecode, shared by every proxy, changeable only via a
93
+ /// custody-gated UUPS upgrade). May be `address(0)` on legacy/test
94
+ /// deploys that don't use the sentinel — in which case the
95
+ /// sentinel fails CLOSED (see `isValidSignature`).
96
+ address private immutable _approvedHashRegistry;
97
+
63
98
  /// @dev Authorized DelegationManager (ERC-7710 executor)
64
99
  address private _delegationManager;
65
100
 
@@ -108,9 +143,21 @@ contract AgentAccount is
108
143
  }
109
144
 
110
145
  /// @dev Spec 007 Phase A.5 — current pending upgrade (set by
111
- /// `upgradeToWithAuthorization` when `_upgradeTimelock > 0`).
146
+ /// `scheduleUpgrade` when `_upgradeTimelock > 0`).
112
147
  PendingUpgrade private _pendingUpgrade;
113
148
 
149
+ /// @dev CA-1 — transient "the current call stack is an authorized upgrade
150
+ /// context" flag. Set while (a) a module action executes
151
+ /// (custody-governed, e.g. `CustodyPolicy.ApplySystemUpdate`, which
152
+ /// carries its own T5 quorum + timelock) or (b) a matured owner-queued
153
+ /// upgrade fires (`executePendingUpgrade`). `_authorizeUpgrade` reads it
154
+ /// so that, once an account configures `_upgradeTimelock`, a DIRECT owner
155
+ /// `upgradeToAndCall` is refused (must `scheduleUpgrade` then
156
+ /// `executePendingUpgrade`), while the custody path keeps working with no
157
+ /// double delay. Transient ⇒ tx-global (visible in the nested
158
+ /// `_authorizeUpgrade` frame) AND absent from the storage layout.
159
+ bool private transient _upgradeAuthorizedCtx;
160
+
114
161
  /// @dev Spec 007 Phase A.5 — maximum permitted upgrade timelock to
115
162
  /// protect against an owner accidentally setting an absurdly
116
163
  /// large delay that bricks future migrations.
@@ -127,6 +174,9 @@ contract AgentAccount is
127
174
 
128
175
  error NotFromSelf();
129
176
  error NotCustodianOrSelf();
177
+ /// @notice R6.5 — thrown when a `whenNotPaused`-guarded entrypoint
178
+ /// is called while `AgenticGovernance.isPaused() == true`.
179
+ error SystemPaused();
130
180
  error CustodianAlreadyExists(address owner);
131
181
  error CustodianDoesNotExist(address owner);
132
182
  error CannotRemoveLastCustodian();
@@ -160,6 +210,13 @@ contract AgentAccount is
160
210
  error UpgradeNotReady(uint64 readyAt, uint256 nowTs);
161
211
  error PendingUpgradeMismatch(address pending, address attempted);
162
212
  error UpgradePending();
213
+ /// @notice CA-1 — a direct `upgradeToAndCall` was attempted while a per-account
214
+ /// upgrade timelock is set; route through `scheduleUpgrade` +
215
+ /// `executePendingUpgrade` (or the custody module path) instead.
216
+ error DirectUpgradeBlocked();
217
+ /// @notice CA-1 — `scheduleUpgrade` called with no upgrade timelock configured
218
+ /// (nothing to queue against — upgrade directly).
219
+ error UpgradeTimelockNotSet();
163
220
 
164
221
  // Wave 2A — on-chain authority closure (contract audit C-1..C-3).
165
222
  error ValidatorRequired();
@@ -200,10 +257,53 @@ contract AgentAccount is
200
257
  _;
201
258
  }
202
259
 
260
+ // ─── R6.5 / CON-AgentAccount-005 — system-wide pause modifier ──
261
+ //
262
+ // When `AgenticGovernance.isPaused() == true`, every mutating
263
+ // external entrypoint guarded by this modifier reverts with
264
+ // `SystemPaused`. Recovery-shaped entrypoints
265
+ // (`uninstallModule`, `cancelPendingUpgrade`, `removeCustodian`,
266
+ // the three `onlySelf` ceremonies) are deliberately NOT guarded
267
+ // — an operator must always be able to remove attack surface
268
+ // even during an incident.
269
+ //
270
+ // Reads `governance()` through the factory at each check so a
271
+ // factory-level governance rotation propagates to every account
272
+ // without per-account migration. Legacy / direct-deploy accounts
273
+ // (no factory) and EOA-governance / non-conforming-governance
274
+ // deploys are treated as "not paused" so existing test fixtures
275
+ // keep working — mirrors `GovernanceManaged._pausedSafe()`.
276
+ modifier whenNotPaused() {
277
+ if (_systemPaused()) revert SystemPaused();
278
+ _;
279
+ }
280
+
281
+ /// @dev R6.5 — `staticcall` chain: factory → governance.isPaused().
282
+ /// Returns `false` on any non-conforming hop (legacy
283
+ /// compatibility). Production deploys MUST pass a real
284
+ /// `AgenticGovernance` to the factory; the production-deploy
285
+ /// preflight enforces that invariant.
286
+ function _systemPaused() internal view returns (bool) {
287
+ address f = _factory;
288
+ if (f == address(0)) return false;
289
+ (bool okF, bytes memory dataF) = f.staticcall(
290
+ abi.encodeWithSelector(IAgentAccountFactoryView.governance.selector)
291
+ );
292
+ if (!okF || dataF.length < 32) return false;
293
+ address g = abi.decode(dataF, (address));
294
+ if (g == address(0) || g.code.length == 0) return false;
295
+ (bool okG, bytes memory dataG) = g.staticcall(
296
+ abi.encodeWithSelector(IAgentAccountPauseView.isPaused.selector)
297
+ );
298
+ if (!okG || dataG.length < 32) return false;
299
+ return abi.decode(dataG, (bool));
300
+ }
301
+
203
302
  // ─── Constructor / Initializer ──────────────────────────────────
204
303
 
205
- constructor(IEntryPoint entryPoint_) {
304
+ constructor(IEntryPoint entryPoint_, address approvedHashRegistry_) {
206
305
  _entryPoint = entryPoint_;
306
+ _approvedHashRegistry = approvedHashRegistry_;
207
307
  _disableInitializers();
208
308
  }
209
309
 
@@ -331,7 +431,20 @@ contract AgentAccount is
331
431
  * will revert here. Master / bundler / session-issuer
332
432
  * therefore cannot upgrade even by submitting the tx.
333
433
  */
334
- function _authorizeUpgrade(address) internal view override onlySelf {}
434
+ function _authorizeUpgrade(address) internal view override onlySelf {
435
+ // CA-1: the per-account upgrade timelock is now ENFORCED here (it was
436
+ // previously dead code — set via `setUpgradeTimelock` but never consulted,
437
+ // so a direct `upgradeToAndCall` fired immediately). When a timelock is
438
+ // configured, a direct owner upgrade is refused; the owner must
439
+ // `scheduleUpgrade` then `executePendingUpgrade` after the delay. Authorized
440
+ // contexts — a custody-module action (its own T5 quorum + timelock) or the
441
+ // matured owner queue — set `_upgradeAuthorizedCtx` and pass through, so the
442
+ // custody path is not double-delayed. A zero timelock keeps the
443
+ // immediate-upgrade default (backward-compatible).
444
+ if (_upgradeTimelock != 0 && !_upgradeAuthorizedCtx) {
445
+ revert DirectUpgradeBlocked();
446
+ }
447
+ }
335
448
 
336
449
  /**
337
450
  * @notice DEPRECATED — single-signature upgrade path (contract audit C-3).
@@ -358,10 +471,25 @@ contract AgentAccount is
358
471
  revert LegacyUpgradePathDisabled();
359
472
  }
360
473
 
474
+ /// @notice CA-1 — queue an implementation upgrade subject to the per-account
475
+ /// upgrade timelock. The owner signs a userOp that calls this
476
+ /// (`onlySelf`); the bound implementation can then be applied via
477
+ /// `executePendingUpgrade` once `_upgradeTimelock` has elapsed. Only
478
+ /// meaningful when a timelock is configured — with none set, upgrade
479
+ /// directly. Overwrites any existing pending upgrade.
480
+ function scheduleUpgrade(address newImplementation) external onlySelf {
481
+ if (newImplementation == address(0)) revert ZeroAddress();
482
+ uint256 tl = _upgradeTimelock;
483
+ if (tl == 0) revert UpgradeTimelockNotSet();
484
+ uint64 readyAt = uint64(block.timestamp + tl);
485
+ _pendingUpgrade = PendingUpgrade({newImplementation: newImplementation, readyAt: readyAt});
486
+ emit UpgradeProposed(newImplementation, readyAt);
487
+ }
488
+
361
489
  /// @notice Execute a previously-queued upgrade. Permissionless once
362
490
  /// the timelock has expired — anyone can pay the gas, but
363
491
  /// the implementation address was bound at queue time.
364
- function executePendingUpgrade() external {
492
+ function executePendingUpgrade() external whenNotPaused {
365
493
  PendingUpgrade memory p = _pendingUpgrade;
366
494
  if (p.readyAt == 0) revert NoPendingUpgrade();
367
495
  if (block.timestamp < p.readyAt) revert UpgradeNotReady(p.readyAt, block.timestamp);
@@ -369,7 +497,12 @@ contract AgentAccount is
369
497
  // impl can't replay this state.
370
498
  delete _pendingUpgrade;
371
499
  emit UpgradeAuthorized(p.newImplementation);
500
+ // CA-1: authorize the nested `_authorizeUpgrade` for this matured queue
501
+ // (transient, tx-global). Cleared after so a later same-tx direct upgrade
502
+ // isn't inadvertently authorized.
503
+ _upgradeAuthorizedCtx = true;
372
504
  this.upgradeToAndCall(p.newImplementation, "");
505
+ _upgradeAuthorizedCtx = false;
373
506
  }
374
507
 
375
508
  /// @notice Cancel a queued upgrade during the timelock window. Owner
@@ -689,7 +822,7 @@ contract AgentAccount is
689
822
  uint256 moduleTypeId,
690
823
  address module,
691
824
  bytes calldata initData
692
- ) external onlySelfOrFactoryInit {
825
+ ) external onlySelfOrFactoryInit whenNotPaused {
693
826
  if (module == address(0)) revert ZeroAddress();
694
827
  if (!_isSupportedModuleType(moduleTypeId)) revert UnsupportedModuleType(moduleTypeId);
695
828
 
@@ -837,6 +970,7 @@ contract AgentAccount is
837
970
  function executeFromModule(address target, uint256 value, bytes calldata data)
838
971
  external
839
972
  nonReentrant
973
+ whenNotPaused
840
974
  returns (bytes memory)
841
975
  {
842
976
  ModulesStorage storage $ = _modulesStorage();
@@ -844,7 +978,14 @@ contract AgentAccount is
844
978
  revert NotInstalledExecutor(msg.sender);
845
979
  }
846
980
  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.
986
+ _upgradeAuthorizedCtx = true;
847
987
  (bool ok, bytes memory ret) = target.call{value: value}(data);
988
+ _upgradeAuthorizedCtx = false;
848
989
  if (!ok) {
849
990
  assembly { revert(add(ret, 32), mload(ret)) }
850
991
  }
@@ -891,7 +1032,7 @@ contract AgentAccount is
891
1032
  /// EntryPoint / self / DelegationManager; the guard hardens
892
1033
  /// against a malicious target re-entering through one of those
893
1034
  /// callers.
894
- function execute(address target, uint256 value, bytes calldata data) external override nonReentrant {
1035
+ function execute(address target, uint256 value, bytes calldata data) external override nonReentrant whenNotPaused {
895
1036
  _requireForExecute();
896
1037
 
897
1038
  ModulesStorage storage $ = _modulesStorage();
@@ -944,7 +1085,7 @@ contract AgentAccount is
944
1085
  /// OUTER `execute` already holds the guard, so external re-entry
945
1086
  /// is blocked; `_requireForExecute` restricts entry to
946
1087
  /// EntryPoint / self / DM (DM has its own `nonReentrant`).
947
- function executeBatch(Call[] calldata calls) external override {
1088
+ function executeBatch(Call[] calldata calls) external override whenNotPaused {
948
1089
  _requireForExecute();
949
1090
 
950
1091
  ModulesStorage storage $ = _modulesStorage();
@@ -1018,6 +1159,21 @@ contract AgentAccount is
1018
1159
  bytes32 hash,
1019
1160
  bytes calldata signature
1020
1161
  ) external view override(IAgentAccount, IERC1271) returns (bytes4) {
1162
+ // Spec 253 — approved-hash sentinel (ERC-1271 ONLY). A bare 1-byte
1163
+ // 0x03 validates iff THIS account pre-approved `hash` in the
1164
+ // (immutable) ApprovedHashRegistry — letting an org batch its
1165
+ // outbound-delegation approvals into one deploy userOp. Checked on
1166
+ // the RAW signature before the ERC-6492 unwrap: a 1-byte sig can't
1167
+ // carry a 32-byte 6492 suffix, so there's no smuggling path. This
1168
+ // branch is unreachable from `_validateSignature`/`_validateSig`
1169
+ // (the userOp-auth path), where 0x03 is an unknown type → false.
1170
+ // Fails CLOSED if the registry is unset/not-a-contract (ADR-0013).
1171
+ if (signature.length == 1 && uint8(signature[0]) == SIG_TYPE_APPROVED_HASH) {
1172
+ if (_approvedHashRegistry.code.length == 0) return bytes4(0xffffffff);
1173
+ return IApprovedHashRegistryView(_approvedHashRegistry).isApproved(address(this), hash)
1174
+ ? ERC1271_MAGIC_VALUE
1175
+ : bytes4(0xffffffff);
1176
+ }
1021
1177
  bytes memory inner = signature;
1022
1178
  if (signature.length >= 32 && bytes32(signature[signature.length - 32:]) == ERC6492_MAGIC) {
1023
1179
  (, , bytes memory unwrapped) = abi.decode(
@@ -1033,6 +1189,12 @@ contract AgentAccount is
1033
1189
 
1034
1190
  uint8 internal constant SIG_TYPE_ECDSA = 0x00;
1035
1191
  uint8 internal constant SIG_TYPE_WEBAUTHN = 0x01;
1192
+ /// @dev Spec 253 — approved-hash sentinel. Honored ONLY by
1193
+ /// `isValidSignature` (ERC-1271), NEVER by `_validateSig` /
1194
+ /// `_validateSignature` (the userOp-auth path) — there `0x03` is an
1195
+ /// unknown type and returns false. A bare 1-byte `0x03` means
1196
+ /// "this hash is pre-approved in the ApprovedHashRegistry".
1197
+ uint8 internal constant SIG_TYPE_APPROVED_HASH = 0x03;
1036
1198
 
1037
1199
  /// @dev Internal dispatcher. Accepts plain 65-byte ECDSA sigs as legacy
1038
1200
  /// form AND type-prefixed sigs (first byte = SIG_TYPE_*).
@@ -1061,11 +1223,14 @@ contract AgentAccount is
1061
1223
 
1062
1224
  function _verifyEcdsa(bytes32 hash, bytes memory sig) internal view returns (bool) {
1063
1225
  // Try raw hash first — matches EntryPoint v0.8 (EIP-712 userOpHash signed directly).
1226
+ // R6.3: third return (sigVersion) intentionally discarded; we rely on `err` + custodian-set membership.
1227
+ // slither-disable-next-line unused-return
1064
1228
  (address recovered, ECDSA.RecoverError err,) = ECDSA.tryRecover(hash, sig);
1065
1229
  if (err == ECDSA.RecoverError.NoError && _externalCustodians[recovered]) return true;
1066
1230
  // Fall back to eth-signed-message wrap — matches v0.7 and legacy ERC-1271
1067
1231
  // callers that pre-prefix the digest.
1068
1232
  bytes32 ethSigned = hash.toEthSignedMessageHash();
1233
+ // slither-disable-next-line unused-return
1069
1234
  (recovered, err,) = ECDSA.tryRecover(ethSigned, sig);
1070
1235
  return err == ECDSA.RecoverError.NoError && _externalCustodians[recovered];
1071
1236
  }
@@ -1079,9 +1244,12 @@ contract AgentAccount is
1079
1244
  bytes memory sig,
1080
1245
  address expected
1081
1246
  ) internal pure returns (bool) {
1247
+ // R6.3: third return (sigVersion) intentionally discarded; the `err` discriminant + `recovered == expected` is the auth.
1248
+ // slither-disable-next-line unused-return
1082
1249
  (address recovered, ECDSA.RecoverError err,) = ECDSA.tryRecover(hash, sig);
1083
1250
  if (err == ECDSA.RecoverError.NoError && recovered == expected) return true;
1084
1251
  bytes32 ethSigned = MessageHashUtils.toEthSignedMessageHash(hash);
1252
+ // slither-disable-next-line unused-return
1085
1253
  (recovered, err,) = ECDSA.tryRecover(ethSigned, sig);
1086
1254
  return err == ECDSA.RecoverError.NoError && recovered == expected;
1087
1255
  }
@@ -1105,11 +1273,23 @@ contract AgentAccount is
1105
1273
  PasskeyEntry storage key = $.keys[a.credentialIdDigest];
1106
1274
  if (key.x == 0 && key.y == 0) return false;
1107
1275
  // H7-C.1 / CON-WEBAUTHN-001: pin per-credential rpIdHash + require UP.
1108
- // UV is not required at the library layer; account policy can layer
1109
- // UV requirement in via a future policy module if needed.
1276
+ // R8.2 / ATL-SEC-03: require UV (User Verification) at the library
1277
+ // layer. Wave H1 made the JS-side ceremony pass
1278
+ // `userVerification: 'required'` for custody-grade flows
1279
+ // (demo-sso, demo-sso-next, demo-org, demo-web-recovery); the
1280
+ // third-party audit (2026-05-31) correctly observed that the
1281
+ // contract still passed `requireUv: false` here. That meant a
1282
+ // permissive authenticator could produce a UP-only assertion
1283
+ // and we'd accept it. R8.2 makes UV strictly required on every
1284
+ // ERC-1271 verify. Browser ceremonies that pass
1285
+ // `userVerification: 'preferred'` will be REJECTED at the
1286
+ // signature step — those apps (demo-web, demo-web-pro) MUST
1287
+ // upgrade to `'required'` for production deployments. Tests
1288
+ // exercising lower-assurance flows can install a separate
1289
+ // passkey-validator module that relaxes this check.
1110
1290
  bytes32 rpIdHash = $.rpIdHashOf[a.credentialIdDigest];
1111
1291
  if (rpIdHash == bytes32(0)) return false;
1112
- return WebAuthnLib.verify(a, hash, key.x, key.y, rpIdHash, false);
1292
+ return WebAuthnLib.verify(a, hash, key.x, key.y, rpIdHash, /*requireUv*/ true);
1113
1293
  } catch {
1114
1294
  return false;
1115
1295
  }
@@ -1178,7 +1358,7 @@ contract AgentAccount is
1178
1358
  /// agenticprimitives AgentAccount is forbidden — enforced via
1179
1359
  /// the ERC-165 marker. Passkey custodians are not added here;
1180
1360
  /// use `addPasskey` instead.
1181
- function addCustodian(address owner) external override onlySelf {
1361
+ function addCustodian(address owner) external override onlySelf whenNotPaused {
1182
1362
  if (owner == address(0)) revert ZeroAddress();
1183
1363
  if (_externalCustodians[owner]) revert CustodianAlreadyExists(owner);
1184
1364
  if (_isAgenticPrimitivesAgent(owner)) {
@@ -10,7 +10,7 @@ import "./governance/GovernanceManaged.sol";
10
10
  /**
11
11
  * @title AgentAccountFactory
12
12
  * @notice Single-entry factory for AgentAccount proxies with
13
- * deterministic CREATE2 addresses. Phase 6f.5 — collapsed
13
+ * deterministic CREATE2 addresses. Phase 6f.5 -- collapsed
14
14
  * createPersonAgent + createMultiSigSmartAgent into one
15
15
  * entry point so every account on chain has the same shape;
16
16
  * the only axis is `mode` on the init params.
@@ -32,7 +32,7 @@ import "./governance/GovernanceManaged.sol";
32
32
  * marker check).
33
33
  *
34
34
  * The canonical CustodyPolicy address is wired at construction
35
- * (factory-immutable) — there is no per-call validator
35
+ * (factory-immutable) -- there is no per-call validator
36
36
  * override. Swapping CustodyPolicy versions means redeploying
37
37
  * the factory, which is the same blast radius as a CREATE2
38
38
  * salt bump.
@@ -41,7 +41,7 @@ import "./governance/GovernanceManaged.sol";
41
41
  * `bundlerSigner` and `sessionIssuer` are factory-level capability
42
42
  * addresses. They are NEVER custodians of any deployed account; each
43
43
  * account reads them off this factory on demand. Phase A.5 made both
44
- * mutable under governance — rotation flows through a governance
44
+ * mutable under governance -- rotation flows through a governance
45
45
  * proposal + 48h timelock and propagates automatically to every
46
46
  * existing AgentAccount.
47
47
  */
@@ -49,6 +49,11 @@ contract AgentAccountFactory is GovernanceManaged {
49
49
  AgentAccount public immutable accountImplementation;
50
50
  address public immutable delegationManager;
51
51
  address public immutable custodyPolicy;
52
+ /// @dev Spec 253 — the ApprovedHashRegistry baked (immutable) into the
53
+ /// AgentAccount impl this factory deploys; surfaced here for
54
+ /// deploy-time auditing. The account's `isValidSignature` 0x03
55
+ /// sentinel consults it; it is NOT a mutable factory-view.
56
+ address public immutable approvedHashRegistry;
52
57
 
53
58
  address public bundlerSigner;
54
59
  address public sessionIssuer;
@@ -73,6 +78,21 @@ contract AgentAccountFactory is GovernanceManaged {
73
78
  error InsufficientTrusteesForMode(uint8 mode, uint256 actual, uint256 required);
74
79
  error TrusteesRequiredForRecoverableMode(uint8 mode);
75
80
  error ZeroAddress();
81
+ /// @notice R9.6 / ATL-SEC-05 -- thrown when `params.custodians.length`
82
+ /// exceeds `MAX_INITIAL_CUSTODIANS`. Without this cap,
83
+ /// `_buildValidatorInitData` casts `nSigners` to `uint8`
84
+ /// and silently truncates at 256, computing a wrong default
85
+ /// threshold for a misconfigured account. A self-hurt
86
+ /// footgun, not a cross-account vulnerability -- capped so
87
+ /// the misconfiguration fails closed at construction.
88
+ error TooManyInitialCustodians(uint256 actual, uint256 max);
89
+
90
+ /// @notice R9.6 / ATL-SEC-05 -- upper bound on initial custodians
91
+ /// passed via `AgentAccountInitParams`. 32 is deliberately
92
+ /// generous (every smart-account substrate we surveyed
93
+ /// caps well below 16) but well under the `uint8` truncation
94
+ /// threshold at `_buildValidatorInitData:252`.
95
+ uint256 public constant MAX_INITIAL_CUSTODIANS = 32;
76
96
 
77
97
  constructor(
78
98
  IEntryPoint entryPoint_,
@@ -80,10 +100,12 @@ contract AgentAccountFactory is GovernanceManaged {
80
100
  address custodyPolicy_,
81
101
  address bundlerSigner_,
82
102
  address sessionIssuer_,
83
- address governance_
103
+ address governance_,
104
+ address approvedHashRegistry_
84
105
  ) GovernanceManaged(governance_) {
85
106
  if (custodyPolicy_ == address(0)) revert ZeroAddress();
86
- accountImplementation = new AgentAccount(entryPoint_);
107
+ accountImplementation = new AgentAccount(entryPoint_, approvedHashRegistry_);
108
+ approvedHashRegistry = approvedHashRegistry_;
87
109
  delegationManager = delegationManager_;
88
110
  custodyPolicy = custodyPolicy_;
89
111
  bundlerSigner = bundlerSigner_;
@@ -134,10 +156,13 @@ contract AgentAccountFactory is GovernanceManaged {
134
156
  // timelock (see AgenticGovernance).
135
157
  _validateInitParams(params);
136
158
 
137
- address addr = _getAddressForAgentAccount(params, salt);
159
+ // CA-F1 (audit 2026-06-10): derive the deploy salt from the FULL custody config so the
160
+ // counterfactual address commits to mode/trustees/timelocks too — not just custodians/passkey.
161
+ bytes32 deploySalt = _effectiveSalt(params, timelockOverrides, salt);
162
+ address addr = _getAddressFromSalt(params, deploySalt);
138
163
  if (addr.code.length > 0) return AgentAccount(payable(addr));
139
164
 
140
- ERC1967Proxy proxy = new ERC1967Proxy{salt: bytes32(salt)}(
165
+ ERC1967Proxy proxy = new ERC1967Proxy{salt: deploySalt}(
141
166
  address(accountImplementation),
142
167
  _initData(params)
143
168
  );
@@ -159,12 +184,16 @@ contract AgentAccountFactory is GovernanceManaged {
159
184
  }
160
185
 
161
186
  /// @notice Counterfactual address for `createAgentAccount`.
187
+ /// @dev CA-F1: takes `timelockOverrides` because the address now commits to it. Callers MUST
188
+ /// predict with the SAME (params, timelockOverrides, salt) they will deploy with, else the
189
+ /// predicted address won't match the deployed one.
162
190
  function getAddressForAgentAccount(
163
191
  AgentAccountInitParams calldata params,
192
+ uint32[7] calldata timelockOverrides,
164
193
  uint256 salt
165
194
  ) external view returns (address) {
166
195
  _validateInitParams(params);
167
- return _getAddressForAgentAccount(params, salt);
196
+ return _getAddressFromSalt(params, _effectiveSalt(params, timelockOverrides, salt));
168
197
  }
169
198
 
170
199
  // ─── Internals ──────────────────────────────────────────────────────
@@ -187,22 +216,40 @@ contract AgentAccountFactory is GovernanceManaged {
187
216
  );
188
217
  }
189
218
 
190
- function _getAddressForAgentAccount(
219
+ /// @dev CA-F1 (audit 2026-06-10) — the deploy salt commits to the FULL custody configuration:
220
+ /// mode, trustees, and per-tier timelock overrides. `_initData` already folds custodians +
221
+ /// the initial passkey into the proxy bytecode hash (and thus the address); those, plus the
222
+ /// mode/trustees/timelocks bound here, mean the canonical address IS its complete custody
223
+ /// config. Two `createAgentAccount` calls that differ in ANY of these resolve to DIFFERENT
224
+ /// addresses, so an attacker can no longer front-run a victim's counterfactual address with a
225
+ /// weaker mode, attacker-controlled recovery trustees, or shortened timelocks. The
226
+ /// occupied-address branch is consequently reachable only by an identical-config request —
227
+ /// which also closes CA-F2 (silent adoption of a mismatched pre-existing account) by
228
+ /// construction, with no separate config-equality assertion needed.
229
+ function _effectiveSalt(
191
230
  AgentAccountInitParams calldata params,
231
+ uint32[7] calldata timelockOverrides,
192
232
  uint256 salt
233
+ ) internal pure returns (bytes32) {
234
+ return keccak256(abi.encode(salt, params.mode, params.trustees, timelockOverrides));
235
+ }
236
+
237
+ function _getAddressFromSalt(
238
+ AgentAccountInitParams calldata params,
239
+ bytes32 deploySalt
193
240
  ) internal view returns (address) {
194
241
  bytes memory initData = _initData(params);
195
- return _create2Address(initData, salt);
242
+ return _create2Address(initData, deploySalt);
196
243
  }
197
244
 
198
- function _create2Address(bytes memory initData, uint256 salt) internal view returns (address) {
245
+ function _create2Address(bytes memory initData, bytes32 salt) internal view returns (address) {
199
246
  bytes memory proxyBytecode = abi.encodePacked(
200
247
  type(ERC1967Proxy).creationCode,
201
248
  abi.encode(address(accountImplementation), initData)
202
249
  );
203
250
  bytes32 bytecodeHash = keccak256(proxyBytecode);
204
251
  return address(uint160(uint256(keccak256(
205
- abi.encodePacked(bytes1(0xff), address(this), bytes32(salt), bytecodeHash)
252
+ abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash)
206
253
  ))));
207
254
  }
208
255
 
@@ -219,6 +266,13 @@ contract AgentAccountFactory is GovernanceManaged {
219
266
 
220
267
  bool hasPasskey = params.initialPasskeyX != 0 && params.initialPasskeyY != 0;
221
268
  if (params.custodians.length == 0 && !hasPasskey) revert NoInitialSigner();
269
+ // R9.6 / ATL-SEC-05: cap the initial custodian set so
270
+ // `_buildValidatorInitData`'s `uint8(nSigners)` cast can't
271
+ // silently truncate. 32 is well above any rational config and
272
+ // well below the uint8 boundary.
273
+ if (params.custodians.length > MAX_INITIAL_CUSTODIANS) {
274
+ revert TooManyInitialCustodians(params.custodians.length, MAX_INITIAL_CUSTODIANS);
275
+ }
222
276
 
223
277
  if (params.mode == 0) return;
224
278