@agenticprimitives/contracts 0.1.0-alpha.2

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 (84) hide show
  1. package/AUDIT.md +67 -0
  2. package/CLAUDE.md +40 -0
  3. package/LICENSE +21 -0
  4. package/README.md +45 -0
  5. package/deployments-anvil.json +1 -0
  6. package/deployments-base-sepolia.json +1 -0
  7. package/dist/abi/AgentNameAttributeResolver.json +798 -0
  8. package/dist/abi/AgentNamePredicates.json +1 -0
  9. package/dist/abi/AgentNameRegistry.json +826 -0
  10. package/dist/abi/AgentNameUniversalResolver.json +222 -0
  11. package/dist/abi/AgentProfilePredicates.json +1 -0
  12. package/dist/abi/AgentProfileResolver.json +1044 -0
  13. package/dist/abi/AgentRelationship.json +583 -0
  14. package/dist/abi/AgentRelationshipPredicates.json +1 -0
  15. package/dist/abi/AgenticGovernance.json +259 -0
  16. package/dist/abi/AllowedMethodsEnforcer.json +108 -0
  17. package/dist/abi/AllowedTargetsEnforcer.json +103 -0
  18. package/dist/abi/ApprovedHashRegistry.json +114 -0
  19. package/dist/abi/AttributeStorage.json +557 -0
  20. package/dist/abi/CaveatEnforcerBase.json +130 -0
  21. package/dist/abi/GovernanceManaged.json +43 -0
  22. package/dist/abi/IAttributeReader.json +98 -0
  23. package/dist/abi/ICaveatEnforcer.json +98 -0
  24. package/dist/abi/IDelegationManager.json +211 -0
  25. package/dist/abi/IERC7579Module.json +34 -0
  26. package/dist/abi/IERC7579ModuleLifecycle.json +60 -0
  27. package/dist/abi/IGovernanceView.json +34 -0
  28. package/dist/abi/MultiSendCallOnly.json +29 -0
  29. package/dist/abi/MultiSendCallOnlyHarness.json +42 -0
  30. package/dist/abi/OntologyTermRegistry.json +397 -0
  31. package/dist/abi/P256Verifier.json +1 -0
  32. package/dist/abi/PermissionlessSubregistry.json +207 -0
  33. package/dist/abi/RelationshipTypeRegistry.json +455 -0
  34. package/dist/abi/ShapeRegistry.json +627 -0
  35. package/dist/abi/SmartAgentModuleTypes.json +1 -0
  36. package/dist/abi/TimestampEnforcer.json +108 -0
  37. package/dist/abi/ValueEnforcer.json +103 -0
  38. package/dist/abi/WebAuthnLib.json +1 -0
  39. package/dist/abi/index.d.ts +35 -0
  40. package/dist/abi/index.js +35 -0
  41. package/package.json +48 -0
  42. package/spec.md +52 -0
  43. package/src/AgentAccount.sol +1374 -0
  44. package/src/AgentAccountFactory.sol +274 -0
  45. package/src/ApprovedHashRegistry.sol +57 -0
  46. package/src/IAgentAccount.sol +138 -0
  47. package/src/SmartAgentPaymaster.sol +281 -0
  48. package/src/UniversalSignatureValidator.sol +136 -0
  49. package/src/agency/DelegationManager.sol +374 -0
  50. package/src/agency/ICaveatEnforcer.sol +62 -0
  51. package/src/agency/IDelegationManager.sol +69 -0
  52. package/src/custody/CustodyPolicy.sol +892 -0
  53. package/src/custody/IERC7579Module.sol +60 -0
  54. package/src/enforcers/AllowedMethodsEnforcer.AUDIT.md +51 -0
  55. package/src/enforcers/AllowedMethodsEnforcer.sol +48 -0
  56. package/src/enforcers/AllowedTargetsEnforcer.AUDIT.md +49 -0
  57. package/src/enforcers/AllowedTargetsEnforcer.sol +44 -0
  58. package/src/enforcers/CaveatEnforcerBase.sol +19 -0
  59. package/src/enforcers/QuorumEnforcer.AUDIT.md +71 -0
  60. package/src/enforcers/QuorumEnforcer.sol +191 -0
  61. package/src/enforcers/TimestampEnforcer.AUDIT.md +50 -0
  62. package/src/enforcers/TimestampEnforcer.sol +43 -0
  63. package/src/enforcers/ValueEnforcer.AUDIT.md +51 -0
  64. package/src/enforcers/ValueEnforcer.sol +41 -0
  65. package/src/governance/AgenticGovernance.sol +140 -0
  66. package/src/governance/GovernanceManaged.sol +75 -0
  67. package/src/governance/IGovernance.sol +15 -0
  68. package/src/identity/AgentProfilePredicates.sol +40 -0
  69. package/src/identity/AgentProfileResolver.sol +194 -0
  70. package/src/libraries/MultiSendCallOnly.sol +95 -0
  71. package/src/libraries/P256Verifier.sol +47 -0
  72. package/src/libraries/SignatureSlotRecovery.sol +196 -0
  73. package/src/libraries/WebAuthnLib.sol +164 -0
  74. package/src/naming/AgentNameAttributeResolver.sol +95 -0
  75. package/src/naming/AgentNamePredicates.sol +74 -0
  76. package/src/naming/AgentNameRegistry.sol +362 -0
  77. package/src/naming/AgentNameUniversalResolver.sol +210 -0
  78. package/src/naming/PermissionlessSubregistry.sol +98 -0
  79. package/src/ontology/AttributeStorage.sol +289 -0
  80. package/src/ontology/OntologyTermRegistry.sol +146 -0
  81. package/src/ontology/ShapeRegistry.sol +240 -0
  82. package/src/relationships/AgentRelationship.sol +289 -0
  83. package/src/relationships/AgentRelationshipPredicates.sol +44 -0
  84. package/src/relationships/RelationshipTypeRegistry.sol +143 -0
@@ -0,0 +1,51 @@
1
+ # `ValueEnforcer` — Security & Architecture Audit
2
+
3
+ **Status:** shipped
4
+ **Last refreshed:** 2026-05-21
5
+ **Owners:** delegation package CODEOWNERS
6
+ **Registry entry:** [`docs/architecture/enforcer-registry/enforcers.json`](../../../../docs/architecture/enforcer-registry/enforcers.json) (entry: `ValueEnforcer`)
7
+ **Live address (Base Sepolia):** `0x49F0B31bf5228B1964dED8DC0F357f104cA74523`
8
+
9
+ ## 1. Charter
10
+
11
+ Caps the native-token (`msg.value`) sent in a redemption to at most `maxValue` wei. Per-call, not cumulative. The redeemer cannot exceed — enforcer reverts.
12
+
13
+ Does NOT do: cumulative-across-redemptions value caps (DTK splits this into a separate enforcer; we don't have it yet — phase 8). Does not cover ERC-20 token amounts (those live in calldata; phase 6c.6 `ArgumentRuleEnforcer` covers them).
14
+
15
+ ## 2. Security invariants
16
+
17
+ - `maxValue` is set at delegation creation, immutable.
18
+ - The redemption's `value` parameter is checked exactly: `require(value <= maxValue)`.
19
+ - `maxValue = 0` is permitted (no native-token transfers allowed via this delegation).
20
+ - The check has no slack / decimal logic — bare wei comparison.
21
+
22
+ ## 3. Threat model
23
+
24
+ | Threat | Likelihood | Impact | Mitigation | Status |
25
+ | --- | --- | --- | --- | --- |
26
+ | Multiple redemptions add up to a value exceeding intent | High by design | High | Per-call only; the delegator MUST add a usage-counter caveat (planned RateLimitEnforcer) for cumulative caps | **Open: VE-1** — cumulative variant tracked as gap |
27
+ | Native-token forwarding from target to other recipients | Low | Medium | Out-of-scope here — the enforcer doesn't trace where the target sends the value. Use `AllowedTargetsEnforcer` to restrict who receives. | By design |
28
+ | `maxValue` set very high accidentally | Low | High | SDK lint should warn above 1 ETH; AgentAccount's `t3HighValueCeiling` provides a second backstop | **Open: VE-2** — lint pending |
29
+
30
+ ## 4. DTK / smart-agent parity
31
+
32
+ **DTK:** `ValueLteEnforcer` + `NativeTokenLimitEnforcer` split. Our single `ValueEnforcer` matches `ValueLteEnforcer` semantically. We lack the cumulative variant — phase 8.
33
+
34
+ **smart-agent:** `ValueEnforcer.sol` — same shape.
35
+
36
+ ## 5. Test posture
37
+
38
+ Forge tests: `packages/contracts/test/Enforcers.t.sol` covers value=maxValue / value<maxValue / value>maxValue / value=0 cases (4 tests).
39
+
40
+ ## 6. Open findings
41
+
42
+ | ID | Severity | Finding | Status |
43
+ | --- | --- | --- | --- |
44
+ | VE-1 | P2 | No cumulative-value enforcer yet; intent leakage if delegator doesn't pair with RateLimitEnforcer. | Open (phase 8) |
45
+ | VE-2 | P3 | SDK lint warning for high `maxValue` (> 1 ETH) absent. | Open |
46
+
47
+ ## 7. Cross-references
48
+
49
+ - [Registry entry](../../../../docs/architecture/enforcer-registry/enforcers.json)
50
+ - Spec 207 § 6 — T3 high-value gate that informs how T3 (value) delegations should always carry a ValueEnforcer.
51
+ - Phase 8 follow-up: cumulative-value variant.
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "../agency/ICaveatEnforcer.sol";
5
+
6
+ /**
7
+ * @title ValueEnforcer
8
+ * @notice Enforces a maximum ETH value per call.
9
+ * @dev terms = abi.encode(uint256 maxValue)
10
+ * Follows ERC-7710 / MetaMask DeleGator beforeHook/afterHook pattern.
11
+ */
12
+ contract ValueEnforcer is ICaveatEnforcer {
13
+ error ValueExceedsLimit();
14
+
15
+ function beforeHook(
16
+ bytes calldata terms,
17
+ bytes calldata,
18
+ bytes32,
19
+ address,
20
+ address,
21
+ address,
22
+ uint256 value,
23
+ bytes calldata
24
+ ) external pure override {
25
+ uint256 maxValue = abi.decode(terms, (uint256));
26
+ if (value > maxValue) revert ValueExceedsLimit();
27
+ }
28
+
29
+ function afterHook(
30
+ bytes calldata,
31
+ bytes calldata,
32
+ bytes32,
33
+ address,
34
+ address,
35
+ address,
36
+ uint256,
37
+ bytes calldata
38
+ ) external pure override {
39
+ // No post-execution check needed
40
+ }
41
+ }
@@ -0,0 +1,140 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "./IGovernance.sol";
5
+
6
+ /**
7
+ * @title AgenticGovernance
8
+ * @notice H7-C.9 / EXT3-009 closure — the governance surface every
9
+ * `GovernanceManaged` contract (`AgentAccountFactory`,
10
+ * `SmartAgentPaymaster`, the registries) sees as its
11
+ * `governance` immutable. Wraps:
12
+ *
13
+ * - **Pause / unpause** (the kill-switch behind every
14
+ * `whenNotPaused` modifier in `GovernanceManaged`).
15
+ * - **Forwarded execution** (`execute(target, data, value)`)
16
+ * so the slow-path timelock can deliver `onlyGovernance`
17
+ * calls that authenticate as `msg.sender == address(this)`.
18
+ * - **Signer / pauser registry** (`isSigner`) consumed by
19
+ * `IGovernanceView`.
20
+ *
21
+ * Two-role authority split:
22
+ * - `timelock` — the OZ `TimelockController(24h)` deployed
23
+ * alongside this contract. Owns slow / deliberate actions
24
+ * (admin role mutations, unpause, signer changes,
25
+ * forwarded calls). All actions go through 24h delay.
26
+ * - `guardian` — fast-path emergency pause. CAN pause (no
27
+ * delay; the incident-response lever) but CANNOT unpause —
28
+ * unpause requires the timelock so an attacker who steals
29
+ * guardian keys can't unpause a paused system.
30
+ *
31
+ * Post-deploy operator step (see deploy-runbook): replace the
32
+ * bootstrap `[deployer]` proposers/executors on the timelock
33
+ * with a long-lived multisig (an `AgentAccount` deployed by
34
+ * the factory whose `CustodyPolicy` requires M-of-N signers).
35
+ * Deployer then renounces the timelock admin role and is gone.
36
+ */
37
+ contract AgenticGovernance is IGovernanceView {
38
+ // ─── Immutables ───────────────────────────────────────────────
39
+
40
+ /// @notice The `TimelockController(24h)` that may execute slow-path
41
+ /// governance actions through this contract. Set once at
42
+ /// construction; rotate by redeploying.
43
+ address public immutable timelock;
44
+
45
+ /// @notice Fast-path emergency-pauser role. Can pause without delay
46
+ /// but CANNOT unpause (unpause requires the timelock).
47
+ address public immutable guardian;
48
+
49
+ // ─── State ────────────────────────────────────────────────────
50
+
51
+ bool private _paused;
52
+ mapping(address => bool) private _signers;
53
+
54
+ // ─── Errors ───────────────────────────────────────────────────
55
+
56
+ error NotTimelock(address caller);
57
+ error NotGuardian(address caller);
58
+ error ExecuteFailed(address target, bytes returndata);
59
+ error ZeroAddress();
60
+
61
+ // ─── Events ───────────────────────────────────────────────────
62
+
63
+ event Paused(address indexed by);
64
+ event Unpaused(address indexed by);
65
+ event SignerSet(address indexed who, bool isSigner);
66
+ event Executed(address indexed target, uint256 value, bytes data);
67
+
68
+ // ─── Constructor ──────────────────────────────────────────────
69
+
70
+ constructor(address timelock_, address guardian_, address[] memory initialSigners) {
71
+ if (timelock_ == address(0) || guardian_ == address(0)) revert ZeroAddress();
72
+ timelock = timelock_;
73
+ guardian = guardian_;
74
+ for (uint256 i; i < initialSigners.length; i++) {
75
+ address s = initialSigners[i];
76
+ if (s == address(0)) revert ZeroAddress();
77
+ _signers[s] = true;
78
+ emit SignerSet(s, true);
79
+ }
80
+ }
81
+
82
+ // ─── IGovernanceView ──────────────────────────────────────────
83
+
84
+ /// @inheritdoc IGovernanceView
85
+ function isPaused() external view returns (bool) {
86
+ return _paused;
87
+ }
88
+
89
+ /// @inheritdoc IGovernanceView
90
+ function isSigner(address who) external view returns (bool) {
91
+ return _signers[who];
92
+ }
93
+
94
+ // ─── Pause / Unpause ──────────────────────────────────────────
95
+
96
+ /// @notice Emergency pause. Callable by the guardian without delay.
97
+ function pause() external {
98
+ if (msg.sender != guardian) revert NotGuardian(msg.sender);
99
+ _paused = true;
100
+ emit Paused(msg.sender);
101
+ }
102
+
103
+ /// @notice Unpause. Requires the timelock (a deliberate 24h
104
+ /// decision); guardian alone cannot unpause.
105
+ function unpause() external {
106
+ if (msg.sender != timelock) revert NotTimelock(msg.sender);
107
+ _paused = false;
108
+ emit Unpaused(msg.sender);
109
+ }
110
+
111
+ // ─── Slow-path governance (timelock only) ─────────────────────
112
+
113
+ /// @notice Forward a call to a `GovernanceManaged` target. The
114
+ /// target sees `msg.sender == address(this)`, satisfying
115
+ /// its `onlyGovernance` modifier. Only callable by the
116
+ /// timelock (every action sees a 24h delay before execute).
117
+ function execute(address target, bytes calldata data, uint256 value)
118
+ external
119
+ payable
120
+ returns (bytes memory)
121
+ {
122
+ if (msg.sender != timelock) revert NotTimelock(msg.sender);
123
+ (bool ok, bytes memory result) = target.call{value: value}(data);
124
+ if (!ok) revert ExecuteFailed(target, result);
125
+ emit Executed(target, value, data);
126
+ return result;
127
+ }
128
+
129
+ /// @notice Add / remove a signer (consumed by `isSigner` reads from
130
+ /// downstream `GovernanceManaged` contracts).
131
+ function setSigner(address who, bool sig) external {
132
+ if (msg.sender != timelock) revert NotTimelock(msg.sender);
133
+ _signers[who] = sig;
134
+ emit SignerSet(who, sig);
135
+ }
136
+
137
+ // ─── Receive ETH ──────────────────────────────────────────────
138
+
139
+ receive() external payable {}
140
+ }
@@ -0,0 +1,75 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "./IGovernance.sol";
5
+
6
+ /**
7
+ * @title GovernanceManaged
8
+ * @notice Base contract that gates admin functions behind a single
9
+ * `Governance` multisig + timelock, and exposes a pause hook so
10
+ * downstream writes can be killed system-wide in an incident.
11
+ *
12
+ * Spec 007 Phase A.5 (SC4 § 4.2). The `governance` address is
13
+ * immutable per deploy — to "upgrade" governance for a contract,
14
+ * you redeploy the contract pointing at the new governance.
15
+ * Governance itself is non-upgradeable.
16
+ *
17
+ * Inheritors gain:
18
+ * - `onlyGovernance` modifier for setters / admin functions.
19
+ * - `whenNotPaused` modifier for write-surface functions that
20
+ * should freeze during an incident.
21
+ * - A pause-aware view (`paused()`) for off-chain tooling.
22
+ */
23
+ abstract contract GovernanceManaged {
24
+ /// @notice The Governance multisig + timelock contract that owns
25
+ /// admin authority over this contract.
26
+ address public immutable governance;
27
+
28
+ error NotGovernance();
29
+ error SystemPaused();
30
+ error ZeroGovernance();
31
+
32
+ constructor(address governance_) {
33
+ if (governance_ == address(0)) revert ZeroGovernance();
34
+ governance = governance_;
35
+ }
36
+
37
+ /// @dev Only the governance contract (executing a passed proposal)
38
+ /// can call functions protected by this modifier.
39
+ modifier onlyGovernance() {
40
+ if (msg.sender != governance) revert NotGovernance();
41
+ _;
42
+ }
43
+
44
+ /// @dev Reverts when the governance pause flag is set. Read-only
45
+ /// functions stay live; this is for write-surface protection
46
+ /// only.
47
+ ///
48
+ /// H7-C.10: skipped when `governance` is an EOA or a contract
49
+ /// that doesn't implement `IGovernanceView` (legacy / test
50
+ /// deploys). Production deploys MUST pass an
51
+ /// `AgenticGovernance` contract; the production-deploy
52
+ /// preflight (`check:production-deploy`) enforces that.
53
+ modifier whenNotPaused() {
54
+ if (_pausedSafe()) revert SystemPaused();
55
+ _;
56
+ }
57
+
58
+ /// @notice Read the global pause flag. Returns false when governance
59
+ /// is non-conforming.
60
+ function paused() external view returns (bool) {
61
+ return _pausedSafe();
62
+ }
63
+
64
+ /// @dev Calls `governance.isPaused()` via staticcall and treats any
65
+ /// failure (EOA, missing function, revert) as "not paused" so
66
+ /// legacy / test deploys with a non-conforming governance still
67
+ /// work. Production deploys ALWAYS use `AgenticGovernance` which
68
+ /// implements `IGovernanceView`.
69
+ function _pausedSafe() internal view returns (bool) {
70
+ if (governance.code.length == 0) return false;
71
+ (bool ok, bytes memory data) = governance.staticcall(abi.encodeWithSelector(IGovernanceView.isPaused.selector));
72
+ if (!ok || data.length < 32) return false;
73
+ return abi.decode(data, (bool));
74
+ }
75
+ }
@@ -0,0 +1,15 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /// @title IGovernanceView
5
+ /// @notice Minimal read surface every `GovernanceManaged` contract depends
6
+ /// on. The pause flag flows through here so we can short-circuit
7
+ /// writes without pulling in the whole governance type.
8
+ interface IGovernanceView {
9
+ /// @notice Global system-pause flag.
10
+ function isPaused() external view returns (bool);
11
+
12
+ /// @notice Whether `who` is currently authorised to call `emergencyPause`
13
+ /// on behalf of governance (i.e. they're an active signer).
14
+ function isSigner(address who) external view returns (bool);
15
+ }
@@ -0,0 +1,40 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /**
5
+ * @title AgentProfilePredicates
6
+ * @notice Canonical bytes32 ids for identity-profile predicates,
7
+ * registered in the shared OntologyTermRegistry alongside
8
+ * the AgentName predicates from NS Phase 3.
9
+ *
10
+ * Some predicates are SHARED with AgentName (`atl:displayName`,
11
+ * `atl:metadataURI`, `atl:metadataHash`, `atl:agentKind`) — same
12
+ * vocabulary across stores is the whole point of the central
13
+ * OntologyTermRegistry. The IDs here that overlap MUST equal the
14
+ * AgentName ones (they're both `keccak256("atl:displayName")` etc.).
15
+ *
16
+ * Identity-only predicates (`atl:description`, `atl:homepage`,
17
+ * `atl:avatar`, `atl:profileSchemaURI`, `atl:profileActive`,
18
+ * `atl:profileRegisteredAt`) are NEW and registered by Deploy.s.sol.
19
+ */
20
+ library AgentProfilePredicates {
21
+ // ─── Shared with AgentName (MUST equal AgentNamePredicates.*) ────
22
+
23
+ bytes32 internal constant ATL_DISPLAY_NAME = keccak256("atl:displayName");
24
+ bytes32 internal constant ATL_AGENT_KIND = keccak256("atl:agentKind");
25
+ bytes32 internal constant ATL_METADATA_URI = keccak256("atl:metadataURI");
26
+ bytes32 internal constant ATL_METADATA_HASH = keccak256("atl:metadataHash");
27
+
28
+ // ─── Identity-only ──────────────────────────────────────────────
29
+
30
+ bytes32 internal constant ATL_DESCRIPTION = keccak256("atl:description");
31
+ bytes32 internal constant ATL_HOMEPAGE = keccak256("atl:homepage");
32
+ bytes32 internal constant ATL_AVATAR = keccak256("atl:avatar");
33
+ bytes32 internal constant ATL_PROFILE_SCHEMA_URI = keccak256("atl:profileSchemaURI");
34
+ bytes32 internal constant ATL_PROFILE_ACTIVE = keccak256("atl:profileActive");
35
+ bytes32 internal constant ATL_PROFILE_REGISTERED_AT = keccak256("atl:profileRegisteredAt");
36
+
37
+ // ─── Class id ───────────────────────────────────────────────────
38
+
39
+ bytes32 internal constant CLASS_AGENT_PROFILE = keccak256("atl:AgentProfile");
40
+ }
@@ -0,0 +1,194 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "../ontology/AttributeStorage.sol";
5
+ import "./AgentProfilePredicates.sol";
6
+
7
+ /**
8
+ * @title AgentProfileResolver
9
+ * @notice Per-agent profile resolver. Inherits the shared
10
+ * AttributeStorage so writes are predicate-active-checked
11
+ * against the OntologyTermRegistry deployed in NS Phase 3.
12
+ *
13
+ * Subject encoding: `bytes32(uint256(uint160(agent)))`. This places
14
+ * the agent's address in the low 20 bytes of the bytes32 subject id
15
+ * — same convention smart-agent uses, lets the SDK convert
16
+ * mechanically between `address` and `subject` without lookup.
17
+ *
18
+ * Authorization: `msg.sender == agent`. The agent's Smart Agent
19
+ * executes through its CustodyPolicy gate, then calls into this
20
+ * contract — `msg.sender` here equals the agent. No isOwner fallback
21
+ * (our AgentAccount has no built-in owner registry; quorum belongs
22
+ * in the CustodyPolicy module).
23
+ *
24
+ * Adapted from smart-agent
25
+ * (`packages/contracts/src/AgentAccountResolver.sol`, 274 LOC) with
26
+ * the following simplifications per spec 217 § Phase 3 + ADR-0007:
27
+ *
28
+ * - Single auth path (`msg.sender == agent`) — no isOwner staticcall.
29
+ * - Profile shape (`atl:AgentProfile`) defined in ShapeRegistry,
30
+ * not via a hand-written `CoreRecord` struct here.
31
+ * - Off-chain `AgentCard` JSON is the source of truth; this
32
+ * resolver stores the content-hash anchor + typed metadata only.
33
+ */
34
+ contract AgentProfileResolver is AttributeStorage {
35
+ address[] private _agents;
36
+ mapping(address => bool) private _registered;
37
+
38
+ event AgentRegistered(address indexed agent, string displayName, bytes32 indexed agentKind);
39
+ event MetadataUpdated(address indexed agent, string metadataURI, bytes32 metadataHash);
40
+ event PropertySet(address indexed agent, bytes32 indexed predicate);
41
+
42
+ error NotAgentOwner();
43
+ error AlreadyRegistered();
44
+ error NotRegistered();
45
+
46
+ /// @notice `msg.sender == agent` (the canonical path). For any
47
+ /// other principal, callers must route through their own
48
+ /// CustodyPolicy / AgentAccount execute path.
49
+ modifier onlyAgent(address agent) {
50
+ if (msg.sender != agent) revert NotAgentOwner();
51
+ _;
52
+ }
53
+
54
+ modifier onlyRegistered(address agent) {
55
+ if (!_registered[agent]) revert NotRegistered();
56
+ _;
57
+ }
58
+
59
+ constructor(address ontologyRegistry) AttributeStorage(ontologyRegistry) {}
60
+
61
+ function _subject(address agent) internal pure returns (bytes32) {
62
+ return bytes32(uint256(uint160(agent)));
63
+ }
64
+
65
+ // ─── Registration ───────────────────────────────────────────────
66
+
67
+ /**
68
+ * @notice One-time profile registration for a Smart Agent. The
69
+ * agent calls this on itself (`msg.sender == agent`).
70
+ * Subsequent edits go through the typed setters below.
71
+ */
72
+ function register(
73
+ address agent,
74
+ string calldata displayName,
75
+ string calldata description,
76
+ bytes32 agentKind,
77
+ string calldata profileSchemaURI
78
+ ) external onlyAgent(agent) {
79
+ if (_registered[agent]) revert AlreadyRegistered();
80
+ bytes32 s = _subject(agent);
81
+ _setString(s, AgentProfilePredicates.ATL_DISPLAY_NAME, displayName);
82
+ if (bytes(description).length > 0) {
83
+ _setString(s, AgentProfilePredicates.ATL_DESCRIPTION, description);
84
+ }
85
+ if (agentKind != bytes32(0)) {
86
+ _setBytes32(s, AgentProfilePredicates.ATL_AGENT_KIND, agentKind);
87
+ }
88
+ if (bytes(profileSchemaURI).length > 0) {
89
+ _setString(s, AgentProfilePredicates.ATL_PROFILE_SCHEMA_URI, profileSchemaURI);
90
+ }
91
+ _setBool(s, AgentProfilePredicates.ATL_PROFILE_ACTIVE, true);
92
+ _setUint(s, AgentProfilePredicates.ATL_PROFILE_REGISTERED_AT, block.timestamp);
93
+ _registered[agent] = true;
94
+ _agents.push(agent);
95
+ emit AgentRegistered(agent, displayName, agentKind);
96
+ }
97
+
98
+ // ─── Typed setters ──────────────────────────────────────────────
99
+
100
+ function setMetadata(
101
+ address agent,
102
+ string calldata metadataURI,
103
+ bytes32 metadataHash
104
+ ) external onlyAgent(agent) onlyRegistered(agent) {
105
+ bytes32 s = _subject(agent);
106
+ _setString(s, AgentProfilePredicates.ATL_METADATA_URI, metadataURI);
107
+ _setBytes32(s, AgentProfilePredicates.ATL_METADATA_HASH, metadataHash);
108
+ emit MetadataUpdated(agent, metadataURI, metadataHash);
109
+ }
110
+
111
+ function setStringProperty(address agent, bytes32 predicate, string calldata value)
112
+ external onlyAgent(agent) onlyRegistered(agent)
113
+ {
114
+ _setString(_subject(agent), predicate, value);
115
+ emit PropertySet(agent, predicate);
116
+ }
117
+
118
+ function setAddressProperty(address agent, bytes32 predicate, address value)
119
+ external onlyAgent(agent) onlyRegistered(agent)
120
+ {
121
+ _setAddress(_subject(agent), predicate, value);
122
+ emit PropertySet(agent, predicate);
123
+ }
124
+
125
+ function setBoolProperty(address agent, bytes32 predicate, bool value)
126
+ external onlyAgent(agent) onlyRegistered(agent)
127
+ {
128
+ _setBool(_subject(agent), predicate, value);
129
+ emit PropertySet(agent, predicate);
130
+ }
131
+
132
+ function setBytes32Property(address agent, bytes32 predicate, bytes32 value)
133
+ external onlyAgent(agent) onlyRegistered(agent)
134
+ {
135
+ _setBytes32(_subject(agent), predicate, value);
136
+ emit PropertySet(agent, predicate);
137
+ }
138
+
139
+ function setUintProperty(address agent, bytes32 predicate, uint256 value)
140
+ external onlyAgent(agent) onlyRegistered(agent)
141
+ {
142
+ _setUint(_subject(agent), predicate, value);
143
+ emit PropertySet(agent, predicate);
144
+ }
145
+
146
+ function setActive(address agent, bool active)
147
+ external onlyAgent(agent) onlyRegistered(agent)
148
+ {
149
+ _setBool(_subject(agent), AgentProfilePredicates.ATL_PROFILE_ACTIVE, active);
150
+ emit PropertySet(agent, AgentProfilePredicates.ATL_PROFILE_ACTIVE);
151
+ }
152
+
153
+ // ─── Convenience readers ────────────────────────────────────────
154
+
155
+ function isRegistered(address agent) external view returns (bool) {
156
+ return _registered[agent];
157
+ }
158
+
159
+ function getStringProperty(address agent, bytes32 predicate) external view returns (string memory) {
160
+ return this.getString(_subject(agent), predicate);
161
+ }
162
+
163
+ function getAddressProperty(address agent, bytes32 predicate) external view returns (address) {
164
+ return this.getAddress(_subject(agent), predicate);
165
+ }
166
+
167
+ function getBoolProperty(address agent, bytes32 predicate) external view returns (bool) {
168
+ return this.getBool(_subject(agent), predicate);
169
+ }
170
+
171
+ function getBytes32Property(address agent, bytes32 predicate) external view returns (bytes32) {
172
+ return this.getBytes32(_subject(agent), predicate);
173
+ }
174
+
175
+ function getUintProperty(address agent, bytes32 predicate) external view returns (uint256) {
176
+ return this.getUint(_subject(agent), predicate);
177
+ }
178
+
179
+ function getPredicateKeys(address agent) external view returns (bytes32[] memory) {
180
+ return this.predicatesOf(_subject(agent));
181
+ }
182
+
183
+ function agentCount() external view returns (uint256) {
184
+ return _agents.length;
185
+ }
186
+
187
+ function getAllAgents() external view returns (address[] memory) {
188
+ return _agents;
189
+ }
190
+
191
+ function subjectFor(address agent) external pure returns (bytes32) {
192
+ return bytes32(uint256(uint160(agent)));
193
+ }
194
+ }
@@ -0,0 +1,95 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /**
5
+ * @title MultiSendCallOnly
6
+ * @notice Atomic batched-call library, ported from Safe's
7
+ * `MultiSendCallOnly` shape. Lets an `AgentAccount` execute
8
+ * multiple inner calls in one userOp without changing
9
+ * `execute`'s ABI — important for flows like
10
+ * `approveHash + redeem` (multi-sig pre-approval path) and the
11
+ * coming Treasury withdraw + emit-event combinations.
12
+ *
13
+ * @dev This is the **call-only** variant. The full Safe `MultiSend`
14
+ * supports `op = 1` (delegatecall) but we explicitly disallow it
15
+ * here because delegatecall from a smart account whose authority
16
+ * is gated by caveats is a footgun — a single malicious target
17
+ * can nullify every caveat we enforce. If you need
18
+ * delegatecall semantics inside a quorum-gated flow, build a
19
+ * dedicated module + caveat rather than reaching for this
20
+ * library.
21
+ *
22
+ * Packed format per entry (one slot):
23
+ * {1 byte op}{20 bytes target}{32 bytes value}{32 bytes dataLen}{dataLen bytes data}
24
+ * where `op` MUST be 0 (call). Total slot size is `0x55 + dataLen`.
25
+ *
26
+ * MUST be invoked via `delegatecall` from the caller's
27
+ * `AgentAccount` so each inner call's `msg.sender` is the account
28
+ * itself (not this library). This contract is stateless — safe
29
+ * to deploy once per chain and reuse from any account.
30
+ *
31
+ * Reverts on the first inner failure with the failing call index
32
+ * and the inner revert data so callers can decode which sub-call
33
+ * broke.
34
+ */
35
+ library MultiSendCallOnly {
36
+ error InvalidOperation(uint8 op);
37
+ error CallFailed(uint256 index, bytes returnData);
38
+
39
+ /**
40
+ * @notice Iterate the packed batch and invoke each call.
41
+ */
42
+ function multiSend(bytes memory transactions) internal {
43
+ uint256 i;
44
+ uint256 n = transactions.length;
45
+ uint256 callIndex;
46
+ while (i < n) {
47
+ uint8 operation;
48
+ address to;
49
+ uint256 value;
50
+ uint256 dataLength;
51
+ bytes memory data;
52
+
53
+ assembly {
54
+ let pos := add(transactions, add(0x20, i))
55
+ operation := shr(248, mload(pos)) // 1 byte
56
+ to := shr(96, mload(add(pos, 0x01))) // 20 bytes
57
+ value := mload(add(pos, 0x15)) // 32 bytes
58
+ dataLength := mload(add(pos, 0x35)) // 32 bytes
59
+ }
60
+
61
+ if (operation != 0) revert InvalidOperation(operation);
62
+
63
+ // Slice the data segment into a fresh `bytes`.
64
+ data = new bytes(dataLength);
65
+ assembly {
66
+ let pos := add(transactions, add(0x20, i))
67
+ let dataStart := add(pos, 0x55)
68
+ let dst := add(data, 0x20)
69
+ for { let j := 0 } lt(j, dataLength) { j := add(j, 0x20) } {
70
+ mstore(add(dst, j), mload(add(dataStart, j)))
71
+ }
72
+ }
73
+
74
+ (bool success, bytes memory ret) = to.call{ value: value }(data);
75
+ if (!success) revert CallFailed(callIndex, ret);
76
+
77
+ // Advance: 1 + 20 + 32 + 32 + dataLength
78
+ i += 0x55 + dataLength;
79
+ callIndex += 1;
80
+ }
81
+ }
82
+ }
83
+
84
+ /**
85
+ * @title MultiSendCallOnlyHarness
86
+ * @notice Test-only wrapper that exposes `multiSend` as an external
87
+ * entry point so Foundry can invoke it directly. Production
88
+ * usage `delegatecall`s the library from an `AgentAccount`.
89
+ */
90
+ contract MultiSendCallOnlyHarness {
91
+ function multiSend(bytes calldata transactions) external payable {
92
+ bytes memory copy = transactions;
93
+ MultiSendCallOnly.multiSend(copy);
94
+ }
95
+ }