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

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": "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",
2
+ "agentAccountFactory": "0x1D3BF769B3991faA7847872cE05FA22beE2f07e5",
3
+ "agentAccountImplementation": "0xd96eA06208140763256899727B22A1DF7A09387d",
4
+ "agentNameRegistry": "0x4e5a6369C91B9093ad17bF0eAc8d344F1b41EdE0",
5
+ "agentNameResolver": "0x776a5bADb7DBE238048542E0c91f7a86933C8986",
6
+ "agentNameUniversalResolver": "0x8539fad5B148512775cD3712bb55F2fd5f1C85cB",
7
+ "agentProfileResolver": "0x82dBC51a77A92A51b177FFE97181cD40aa1c24ab",
8
+ "agentRegistryBase": "0x54f76a5faab995f194cc61c50eb5e272c8d85cf4",
9
+ "agentRelationship": "0x763c164d8275F76A806721B423aaB5ad6cb4fEA1",
10
+ "agreementRegistry": "0xaD300f03EdDf8F015D5583feC8D5F2D1BBC6f854",
11
+ "allowedMethodsEnforcer": "0xa149A31F32789cDBfd5f0eed1CF92E7BCb91b549",
12
+ "allowedTargetsEnforcer": "0xfb78589C8a9f464BD1e9A340BC809CEe7949b17c",
13
+ "approvedHashRegistry": "0x7152282bB71692E7f73ff31DdB43f4a50818845b",
14
+ "attestationRegistry": "0x9f9bCAF7b238b87c6d7897767c473b6f8C55C0a6",
15
+ "callDataHashEnforcer": "0x0fCE11eC016bd4Bc3a60951FbE4e9Fe7087955b1",
16
16
  "chainId": 84532,
17
- "custodyPolicy": "0xfAFDc79bc03CC7cE27f7AA12A3180E4E4C0BC3F6",
18
- "delegationManager": "0x10DB7c0C5887910722354ef37eF874fBD124026C",
17
+ "custodyPolicy": "0xF408e54dA113Fe6b785615750c6Bab6F9008A9d9",
18
+ "delegationManager": "0xaCdB7001dB8305c23703f6DB0EfB8B9BC1d75D62",
19
19
  "deployer": "0x31ed17fb99e82E02085Ab4B3cbdaB05489098b44",
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"
20
+ "entryPoint": "0x9Eb5F84E8323aACD4AD61b4b980aDB23B15c2903",
21
+ "geoFeatureRegistry": "0x74AEb56420a8630c611c36b3053780d90191f94d",
22
+ "mockUsdc": "0x07190E7586a468E808D6bd58dC3CEdEC0b25B8E3",
23
+ "ontologyTermRegistry": "0x69670d24AcE8C47a294231a1fdC4207642669a25",
24
+ "paymentEnforcer": "0xF08946A2c4eed77f366AfDc39AE150F2262E5d29",
25
+ "paymentEscrow": "0x30f0fd53dc1245062962b498be3a7e9641cc7c6c",
26
+ "paymentReceiptRegistry": "0x36f0cd94A286b011b8C6106E69E14567fb7A498e",
27
+ "permissionlessSubregistry": "0xE37E1da21Bcce70b515d2f44d84D757c358b96B6",
28
+ "permissionlessSubregistryDemoAgent": "0xeE2Db355624BdB4a7551604229EbE72683f8fD85",
29
+ "quorumEnforcer": "0xd030196bD6Ba6a654e6E8f7e3609aAd470122422",
30
+ "relationshipTypeRegistry": "0x8D5f597f2dE53CC8Ed1a948abF972F8C85301a17",
31
+ "shapeRegistry": "0x41273A14e77d7dA256c05276B95D8F2054C54930",
32
+ "skillDefinitionRegistry": "0x9bcd76e8E7e5E57863dca0631F44707974cBa58B",
33
+ "smartAgentPaymaster": "0xb48Ccecb9118D579204868850f62feb4653c1469",
34
+ "timestampEnforcer": "0x5ce38463DcE9c910F09a14e6D0650258e72eEcCA",
35
+ "universalSignatureValidator": "0x4BC3Ea8920c87638e0612007eb9de0d70ee4Ba8f",
36
+ "valueEnforcer": "0x45170540Ba4d90C7c27103FBF4B136c5EdA545BB"
37
37
  }
@@ -590,6 +590,25 @@
590
590
  ],
591
591
  "stateMutability": "view"
592
592
  },
593
+ {
594
+ "type": "function",
595
+ "name": "isExternalCustodianOrPasskey",
596
+ "inputs": [
597
+ {
598
+ "name": "account",
599
+ "type": "address",
600
+ "internalType": "address"
601
+ }
602
+ ],
603
+ "outputs": [
604
+ {
605
+ "name": "",
606
+ "type": "bool",
607
+ "internalType": "bool"
608
+ }
609
+ ],
610
+ "stateMutability": "view"
611
+ },
593
612
  {
594
613
  "type": "function",
595
614
  "name": "isModuleInstalled",
@@ -854,6 +854,17 @@
854
854
  "name": "EmptyOwnerSet",
855
855
  "inputs": []
856
856
  },
857
+ {
858
+ "type": "error",
859
+ "name": "InfeasibleOrgConfig",
860
+ "inputs": [
861
+ {
862
+ "name": "nSigners",
863
+ "type": "uint8",
864
+ "internalType": "uint8"
865
+ }
866
+ ]
867
+ },
857
868
  {
858
869
  "type": "error",
859
870
  "name": "InvalidCustodyAction",
@@ -622,6 +622,17 @@
622
622
  "name": "LegacyRevocationDisabled",
623
623
  "inputs": []
624
624
  },
625
+ {
626
+ "type": "error",
627
+ "name": "NoCodeEnforcer",
628
+ "inputs": [
629
+ {
630
+ "name": "enforcer",
631
+ "type": "address",
632
+ "internalType": "address"
633
+ }
634
+ ]
635
+ },
625
636
  {
626
637
  "type": "error",
627
638
  "name": "NotDelegatorOrDelegate",
@@ -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": "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",
4
+ "agentAccountFactory": "0x1D3BF769B3991faA7847872cE05FA22beE2f07e5",
5
+ "agentAccountImplementation": "0xd96eA06208140763256899727B22A1DF7A09387d",
6
+ "agentNameRegistry": "0x4e5a6369C91B9093ad17bF0eAc8d344F1b41EdE0",
7
+ "agentNameResolver": "0x776a5bADb7DBE238048542E0c91f7a86933C8986",
8
+ "agentNameUniversalResolver": "0x8539fad5B148512775cD3712bb55F2fd5f1C85cB",
9
+ "agentProfileResolver": "0x82dBC51a77A92A51b177FFE97181cD40aa1c24ab",
10
+ "agentRegistryBase": "0x54f76a5faab995f194cc61c50eb5e272c8d85cf4",
11
+ "agentRelationship": "0x763c164d8275F76A806721B423aaB5ad6cb4fEA1",
12
+ "agreementRegistry": "0xaD300f03EdDf8F015D5583feC8D5F2D1BBC6f854",
13
+ "allowedMethodsEnforcer": "0xa149A31F32789cDBfd5f0eed1CF92E7BCb91b549",
14
+ "allowedTargetsEnforcer": "0xfb78589C8a9f464BD1e9A340BC809CEe7949b17c",
15
+ "approvedHashRegistry": "0x7152282bB71692E7f73ff31DdB43f4a50818845b",
16
+ "attestationRegistry": "0x9f9bCAF7b238b87c6d7897767c473b6f8C55C0a6",
17
+ "callDataHashEnforcer": "0x0fCE11eC016bd4Bc3a60951FbE4e9Fe7087955b1",
18
18
  "chainId": 84532,
19
- "custodyPolicy": "0xfAFDc79bc03CC7cE27f7AA12A3180E4E4C0BC3F6",
20
- "delegationManager": "0x10DB7c0C5887910722354ef37eF874fBD124026C",
19
+ "custodyPolicy": "0xF408e54dA113Fe6b785615750c6Bab6F9008A9d9",
20
+ "delegationManager": "0xaCdB7001dB8305c23703f6DB0EfB8B9BC1d75D62",
21
21
  "deployer": "0x31ed17fb99e82E02085Ab4B3cbdaB05489098b44",
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"
22
+ "entryPoint": "0x9Eb5F84E8323aACD4AD61b4b980aDB23B15c2903",
23
+ "geoFeatureRegistry": "0x74AEb56420a8630c611c36b3053780d90191f94d",
24
+ "mockUsdc": "0x07190E7586a468E808D6bd58dC3CEdEC0b25B8E3",
25
+ "ontologyTermRegistry": "0x69670d24AcE8C47a294231a1fdC4207642669a25",
26
+ "paymentEnforcer": "0xF08946A2c4eed77f366AfDc39AE150F2262E5d29",
27
+ "paymentEscrow": "0x30f0fd53dc1245062962b498be3a7e9641cc7c6c",
28
+ "paymentReceiptRegistry": "0x36f0cd94A286b011b8C6106E69E14567fb7A498e",
29
+ "permissionlessSubregistry": "0xE37E1da21Bcce70b515d2f44d84D757c358b96B6",
30
+ "permissionlessSubregistryDemoAgent": "0xeE2Db355624BdB4a7551604229EbE72683f8fD85",
31
+ "quorumEnforcer": "0xd030196bD6Ba6a654e6E8f7e3609aAd470122422",
32
+ "relationshipTypeRegistry": "0x8D5f597f2dE53CC8Ed1a948abF972F8C85301a17",
33
+ "shapeRegistry": "0x41273A14e77d7dA256c05276B95D8F2054C54930",
34
+ "skillDefinitionRegistry": "0x9bcd76e8E7e5E57863dca0631F44707974cBa58B",
35
+ "smartAgentPaymaster": "0xb48Ccecb9118D579204868850f62feb4653c1469",
36
+ "timestampEnforcer": "0x5ce38463DcE9c910F09a14e6D0650258e72eEcCA",
37
+ "universalSignatureValidator": "0x4BC3Ea8920c87638e0612007eb9de0d70ee4Ba8f",
38
+ "valueEnforcer": "0x45170540Ba4d90C7c27103FBF4B136c5EdA545BB"
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.14",
3
+ "version": "1.0.0-alpha.15",
4
4
  "description": "Solidity contracts + ABIs + flattened sources + per-network deployment addresses for the agenticprimitives stack.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1028,18 +1028,30 @@ contract AgentAccount is
1028
1028
  revert NotInstalledExecutor(msg.sender);
1029
1029
  }
1030
1030
  emit ModuleExecuted(msg.sender, target, value);
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;
1039
- _upgradeAuthorizedCtx = true;
1031
+ // CA-1 + Wave-1/ACCT-C1 (spec 300): a module action from the CANONICAL CustodyPolicy is a
1032
+ // custody-governed context (gated upstream by its own T5 quorum + timelock). `_custodyGovCtx`
1033
+ // authorizes every governance mutator this call dispatches (the ONLY way past
1034
+ // `_requireCustodyGoverned` on a `mode>0` account); `_upgradeAuthorizedCtx` additionally exempts a
1035
+ // dispatched upgrade from the per-account timelock (no double delay). Both transient.
1036
+ //
1037
+ // NEW-EXEC-MODULE-1 (2026-07-05 audit, internal + external): grant that context ONLY to the
1038
+ // canonical CustodyPolicy the factory installs — NOT to any installed executor. Otherwise a second
1039
+ // (utility / automation) executor a quorum installs believing it unprivileged would silently inherit
1040
+ // full authority to mutate custody / DM / upgrade / modules, bypassing quorum+timelock. Matching the
1041
+ // specific policy address is the same check `_hasCustodyGovernance` uses. A non-canonical executor
1042
+ // runs WITHOUT the context, so `_requireCustodyGoverned` rejects any governance mutator it dispatches
1043
+ // (defense-in-depth: not reachable at v1 since the factory installs only CustodyPolicy).
1044
+ address f = _factory;
1045
+ bool custodyGoverned = f != address(0) && msg.sender == IAgentAccountFactoryView(f).custodyPolicy();
1046
+ if (custodyGoverned) {
1047
+ _custodyGovCtx = true;
1048
+ _upgradeAuthorizedCtx = true;
1049
+ }
1040
1050
  (bool ok, bytes memory ret) = target.call{value: value}(data);
1041
- _upgradeAuthorizedCtx = false;
1042
- _custodyGovCtx = false;
1051
+ if (custodyGoverned) {
1052
+ _upgradeAuthorizedCtx = false;
1053
+ _custodyGovCtx = false;
1054
+ }
1043
1055
  if (!ok) {
1044
1056
  assembly { revert(add(ret, 32), mload(ret)) }
1045
1057
  }
@@ -1465,6 +1477,19 @@ contract AgentAccount is
1465
1477
  return _passkeyStorage().piaToCredentialId[account] != bytes32(0);
1466
1478
  }
1467
1479
 
1480
+ /// @notice EXT-ISCUSTODIAN-1 (2026-07-05 audit): like `isCustodian` but EXCLUDES the account itself.
1481
+ /// @dev `isCustodian(address(this)) == true` is deliberate (account-actor compatibility), but that
1482
+ /// footgun can recreate the ACCT-C2 quorum-collapse class in a FUTURE consumer that reuses
1483
+ /// `isCustodian` as if it meant "external signer." Any quorum / authority consumer that must count
1484
+ /// only real external custodians / passkeys (never the account) should use THIS view. ACCT-C2 fixed
1485
+ /// the current CustodyPolicy quorum at the counting site (reject signer==account); this gives new
1486
+ /// consumers the safe primitive so the class can't reappear off-path.
1487
+ function isExternalCustodianOrPasskey(address account) external view returns (bool) {
1488
+ if (account == address(this)) return false;
1489
+ if (_externalCustodians[account]) return true;
1490
+ return _passkeyStorage().piaToCredentialId[account] != bytes32(0);
1491
+ }
1492
+
1468
1493
  /// @inheritdoc IAgentAccount
1469
1494
  /// @dev Counts EOA/contract custodians PLUS registered passkeys —
1470
1495
  /// each passkey contributes one PIA-custodian. Matches the
@@ -109,6 +109,10 @@ contract DelegationManager is IDelegationManager, ReentrancyGuard {
109
109
  // Phase A.5 errors.
110
110
  error NotDelegatorOrDelegate();
111
111
  error HashMismatch();
112
+ /// @dev EXT-NOCODE-HOOK-1: a caveat enforcer with no code — a call to it would
113
+ /// succeed vacuously (fail-OPEN), so the live before/after hook loops reject it,
114
+ /// matching the view path (`verifyAuthorizationForCall` → "no-code-enforcer").
115
+ error NoCodeEnforcer(address enforcer);
112
116
  /// @dev Thrown by the deprecated permissionless `revokeDelegation(bytes32)`
113
117
  /// path. Migrate callers to `revokeDelegationByOwner(Delegation)`.
114
118
  error LegacyRevocationDisabled();
@@ -454,6 +458,9 @@ contract DelegationManager is IDelegationManager, ReentrancyGuard {
454
458
  ) internal {
455
459
  bytes32 dHash = hashDelegation(d);
456
460
  for (uint256 j = 0; j < d.caveats.length; j++) {
461
+ // EXT-NOCODE-HOOK-1: fail closed on a no-code enforcer (a call would succeed vacuously),
462
+ // symmetric with the view path's "no-code-enforcer" reject — never rely on call semantics.
463
+ if (d.caveats[j].enforcer.code.length == 0) revert NoCodeEnforcer(d.caveats[j].enforcer);
457
464
  ICaveatEnforcer(d.caveats[j].enforcer).beforeHook(
458
465
  d.caveats[j].terms,
459
466
  d.caveats[j].args,
@@ -475,6 +482,8 @@ contract DelegationManager is IDelegationManager, ReentrancyGuard {
475
482
  ) internal {
476
483
  bytes32 dHash = hashDelegation(d);
477
484
  for (uint256 j = 0; j < d.caveats.length; j++) {
485
+ // EXT-NOCODE-HOOK-1: fail closed on a no-code enforcer, symmetric with the before-hook + view path.
486
+ if (d.caveats[j].enforcer.code.length == 0) revert NoCodeEnforcer(d.caveats[j].enforcer);
478
487
  ICaveatEnforcer(d.caveats[j].enforcer).afterHook(
479
488
  d.caveats[j].terms,
480
489
  d.caveats[j].args,
@@ -318,6 +318,10 @@ contract CustodyPolicy {
318
318
  error TrusteeDoesNotExist(address guardian);
319
319
  error InvalidMode(uint8 mode_);
320
320
  error InvalidThresholdValue(uint8 thr);
321
+ /// @dev EXT-CHANGEMODE-1: a runtime ChangeMode into org mode (3) with an infeasible signer
322
+ /// count (< 2, so separation-of-duties can't split proposers from appliers). Mirrors the
323
+ /// factory's install-time InfeasibleOrgConfig.
324
+ error InfeasibleOrgConfig(uint8 nSigners);
321
325
  /// @dev CP-1: a tier that must be configured (T4/T5 at install, T6 at read)
322
326
  /// was left unset — refuse rather than fall to the 1-of-n read fallback.
323
327
  error UnconfiguredTier(uint8 tier);
@@ -884,6 +888,23 @@ contract CustodyPolicy {
884
888
  if (newMode > 3) revert InvalidMode(newMode);
885
889
  uint8 oldMode = c.mode;
886
890
  if (newMode == 0 && c.trusteeCount > 0) revert CannotDowngradeWithTrustees();
891
+ // EXT-CHANGEMODE-1 (2026-07-05 audit): a runtime ChangeMode must re-run the factory's install-time
892
+ // mode-3 feasibility so the account can't enter an un-executable / liveness-hostile org mode.
893
+ // Org SoD needs ≥2 signers (distinct proposers + appliers) and T4/T5 thresholds within the
894
+ // floor(N/2) cap (else 2×threshold > N → no disjoint quorum → the tier silently locks). Require the
895
+ // signer floor; cap the SoD-constrained thresholds (mirrors the factory, avoids a locked mode).
896
+ if (newMode == 3) {
897
+ uint256 n = IAgentAccount(account).custodianCount();
898
+ if (n < 2) revert InfeasibleOrgConfig(uint8(n));
899
+ uint8 sodCap = uint8(n / 2); // ≥ 1 since n ≥ 2
900
+ for (uint8 tier = 4; tier <= 5; tier++) {
901
+ uint8 v = c.approvalsRequiredByTier[tier];
902
+ if (v > sodCap) {
903
+ c.approvalsRequiredByTier[tier] = sodCap;
904
+ emit ThresholdChanged(account, tier, v, sodCap);
905
+ }
906
+ }
907
+ }
887
908
  c.mode = newMode;
888
909
  emit ModeChanged(account, oldMode, newMode);
889
910
  }
@@ -968,6 +989,13 @@ contract CustodyPolicy {
968
989
  if (newCount == 0) revert InvalidThresholdValue(newCount);
969
990
  uint256 n = IAgentAccount(account).custodianCount();
970
991
  if (uint256(newCount) > n) revert InvalidThresholdValue(newCount);
992
+ // acct-mode3-sod-cap-runtime (2026-07-05 audit): the factory caps mode-3 (org) T4/T5 thresholds at
993
+ // floor(N/2) at install so a DISJOINT propose+apply quorum always exists (2×threshold ≤ N). A later
994
+ // ChangeApprovalsRequired must not raise them past that cap and silently lock the tier — reject an
995
+ // explicit over-cap request loud (the org can lower N-constraints or use a lower threshold).
996
+ if (c.mode == 3 && (tier == 4 || tier == 5) && uint256(newCount) > n / 2) {
997
+ revert InvalidThresholdValue(newCount);
998
+ }
971
999
  uint8 oldValue = c.approvalsRequiredByTier[tier];
972
1000
  if (oldValue == 0) oldValue = 1; // mirror _approvalsValue fallback
973
1001
  c.approvalsRequiredByTier[tier] = newCount;