@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,362 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /**
5
+ * @title AgentNameRegistry
6
+ * @notice Hierarchical multi-root name registry for Smart Agents.
7
+ *
8
+ * Names are keyed by `node` (the ENS-style namehash:
9
+ * `keccak256(parentNode || labelhash)`). Each node has an owner (a Smart
10
+ * Agent address), a resolver, an optional subregistry delegate that may
11
+ * issue children, a parent pointer, the labelhash, and an optional
12
+ * expiry.
13
+ *
14
+ * Adapted from smart-agent (`packages/contracts/src/AgentNameRegistry.sol`,
15
+ * 359 LOC) with the following simplifications per spec 215 § Phase 3 +
16
+ * ADR-0006:
17
+ *
18
+ * - **No AgentRelationship dependency.** The smart-agent original
19
+ * created a `NAMESPACE_CONTAINS` edge on every register; ADR-0006
20
+ * rules that as parallel authority. The parent pointer in
21
+ * `_records[node].parent` IS the hierarchy.
22
+ *
23
+ * - **No OpenZeppelin AccessControl / TimelockController.** The owner
24
+ * Smart Agent's CustodyPolicy module IS the timelock + RBAC.
25
+ * Authorization here is purely `msg.sender == owner` (the Smart
26
+ * Agent executes through its CustodyPolicy gate, then calls into
27
+ * this contract).
28
+ *
29
+ * - **No multi-owner `isOwner(address)` fallback.** Our AgentAccount
30
+ * is ERC-7579 modular with no built-in owner registry; quorum
31
+ * belongs to the CustodyPolicy module. The legacy smart-agent path
32
+ * `staticcall isOwner(msg.sender)` does not apply.
33
+ *
34
+ * Reverse-records (primary names: address → node) ALSO live here
35
+ * for atomicity; the universal resolver enforces the round-trip
36
+ * discipline (a primary name only counts when the forward record
37
+ * agrees).
38
+ */
39
+ contract AgentNameRegistry {
40
+ // ─── H7-C.4 / CON-NAMING-001 — root-initializer gate ───────────────
41
+ //
42
+ // `initializeRoot` was permissionless: any mempool observer could
43
+ // front-run the deployer's first `initializeRoot("agent", ...)` and
44
+ // own `.agent` forever. The fix is to pin the initializer to an
45
+ // immutable address set at construction; the deploy script performs
46
+ // both steps in a single transaction so no front-run window exists.
47
+ //
48
+ // The role is INITIALIZATION-ONLY. Once a root is initialized, control
49
+ // belongs to its `rootOwner` (a smart-agent governance address) — the
50
+ // initializer cannot rotate it.
51
+ address public immutable initializer;
52
+
53
+ // ─── Types ──────────────────────────────────────────────────────
54
+
55
+ struct NameRecord {
56
+ address owner; // Smart Agent that controls this name
57
+ address resolver; // resolver contract for this node's records
58
+ address subregistry; // 0 = owner-only; non-zero = delegate may also register children
59
+ bytes32 parent; // parent namehash; bytes32(0) for roots
60
+ bytes32 labelhash; // keccak256(bytes(label))
61
+ uint64 expiry; // 0 = no expiry
62
+ uint64 registeredAt;
63
+ }
64
+
65
+ // ─── Errors ─────────────────────────────────────────────────────
66
+
67
+ error NotAuthorized();
68
+ error NodeAlreadyExists();
69
+ error NodeNotFound();
70
+ error ParentNotFound();
71
+ error NameExpired();
72
+ error RootAlreadyInitialized();
73
+ error EmptyLabel();
74
+ error ZeroOwner();
75
+ /// @notice H7-C.4 / CON-NAMING-001 — `initializeRoot` may only be called
76
+ /// by the immutable `initializer` set at construction. Closes the
77
+ /// permissionless-frontrun vector where any mempool observer
78
+ /// could claim a TLD before the deployer's first init landed.
79
+ error NotInitializer(address caller, address initializer);
80
+
81
+ // ─── Events ─────────────────────────────────────────────────────
82
+
83
+ event RootInitialized(bytes32 indexed rootNode, string label, address indexed owner, bytes32 kind);
84
+ event NameRegistered(
85
+ bytes32 indexed node,
86
+ bytes32 indexed parent,
87
+ string label,
88
+ address owner,
89
+ address resolver,
90
+ uint64 expiry
91
+ );
92
+ event OwnerChanged(bytes32 indexed node, address indexed newOwner);
93
+ event ResolverChanged(bytes32 indexed node, address indexed resolver);
94
+ event SubregistryChanged(bytes32 indexed node, address indexed subregistry);
95
+ event NameRenewed(bytes32 indexed node, uint64 newExpiry);
96
+ event PrimaryNameSet(address indexed agent, bytes32 indexed node);
97
+ event PrimaryNameCleared(address indexed agent);
98
+
99
+ // ─── Storage ────────────────────────────────────────────────────
100
+
101
+ mapping(bytes32 => NameRecord) private _records;
102
+ mapping(bytes32 => mapping(bytes32 => bytes32)) private _children; // parent => labelhash => childNode
103
+ mapping(bytes32 => bytes32[]) private _childLabels; // parent => labelhash[]
104
+ /// @notice Plain-text label per node — the string that was passed to
105
+ /// `initializeRoot` / `register` at registration time.
106
+ /// Per spec/222 this enables view-call reverse resolution
107
+ /// (universal resolver concatenates parents into the full
108
+ /// dotted name) WITHOUT any `eth_getLogs` scan.
109
+ mapping(bytes32 => string) private _label;
110
+
111
+ /// @notice Multi-root registry — `true` iff `node` was initialized via `initializeRoot`.
112
+ mapping(bytes32 => bool) public isRoot;
113
+ /// @notice Per-root opaque kind tag (e.g. `keccak256("namespace:Agent")`).
114
+ mapping(bytes32 => bytes32) public rootKind;
115
+ /// @notice Lookup root node by ASCII TLD label.
116
+ mapping(string => bytes32) private _rootByLabel;
117
+ /// @notice Enumeration of every initialized root.
118
+ bytes32[] private _allRoots;
119
+
120
+ /// @notice Reverse-record: Smart Agent address → primary name node.
121
+ /// @dev Forward agreement (resolver-addr == agent) is enforced by
122
+ /// the universal resolver, NOT here — keeping registry writes
123
+ /// simple. A primary-name claim with no forward agreement
124
+ /// returns null on reverseResolve via the resolver.
125
+ mapping(address => bytes32) private _primaryName;
126
+
127
+ /// @notice Default kind tags downstream callers may use.
128
+ bytes32 public constant KIND_AGENT = keccak256("namespace:Agent");
129
+
130
+ // ─── Constructor (H7-C.4) ───────────────────────────────────────
131
+
132
+ /// @notice Set the immutable root-initializer. The deploy script
133
+ /// deploys the registry + calls `initializeRoot` in the same
134
+ /// transaction so the TLD can't be frontrun.
135
+ constructor(address initializer_) {
136
+ if (initializer_ == address(0)) revert ZeroOwner();
137
+ initializer = initializer_;
138
+ }
139
+
140
+ // ─── Namehash Helpers ───────────────────────────────────────────
141
+
142
+ /// @notice Pure namehash for a top-level label (parent = bytes32(0)).
143
+ function namehashRoot(string memory label) public pure returns (bytes32) {
144
+ return keccak256(abi.encodePacked(bytes32(0), keccak256(bytes(label))));
145
+ }
146
+
147
+ /// @notice Backward-compat helper returning `namehash("agent")` — SDK
148
+ /// callers use this to derive `.agent` without re-implementing
149
+ /// the algorithm.
150
+ function AGENT_ROOT() public pure returns (bytes32) {
151
+ return keccak256(abi.encodePacked(bytes32(0), keccak256(bytes("agent"))));
152
+ }
153
+
154
+ // ─── Root Initialization (multi-root) ───────────────────────────
155
+
156
+ /**
157
+ * @notice Initialize a TLD root.
158
+ * @param label TLD label without leading dot (e.g. "agent").
159
+ * @param rootOwner Address that will own the root (Smart Agent or deployer EOA for bootstrap).
160
+ * @param resolverContract Default resolver for the root (0 = none).
161
+ * @param kind Opaque tag; SDK / downstream binders use it to dispatch.
162
+ */
163
+ function initializeRoot(
164
+ string calldata label,
165
+ address rootOwner,
166
+ address resolverContract,
167
+ bytes32 kind
168
+ ) external returns (bytes32 rootNode) {
169
+ // H7-C.4 / CON-NAMING-001: only the immutable initializer may claim a
170
+ // TLD. The deploy script bundles deploy+init in one transaction so no
171
+ // frontrun window exists.
172
+ if (msg.sender != initializer) revert NotInitializer(msg.sender, initializer);
173
+ if (bytes(label).length == 0) revert EmptyLabel();
174
+ if (rootOwner == address(0)) revert ZeroOwner();
175
+ rootNode = namehashRoot(label);
176
+ if (_records[rootNode].registeredAt != 0) revert RootAlreadyInitialized();
177
+
178
+ _records[rootNode] = NameRecord({
179
+ owner: rootOwner,
180
+ resolver: resolverContract,
181
+ subregistry: rootOwner,
182
+ parent: bytes32(0),
183
+ labelhash: keccak256(bytes(label)),
184
+ expiry: 0,
185
+ registeredAt: uint64(block.timestamp)
186
+ });
187
+
188
+ isRoot[rootNode] = true;
189
+ rootKind[rootNode] = kind;
190
+ _rootByLabel[label] = rootNode;
191
+ _allRoots.push(rootNode);
192
+ _label[rootNode] = label;
193
+
194
+ emit RootInitialized(rootNode, label, rootOwner, kind);
195
+ }
196
+
197
+ /// @notice Enumerate every initialized root.
198
+ function getRoots() external view returns (bytes32[] memory) {
199
+ return _allRoots;
200
+ }
201
+
202
+ /// @notice Look up a root by its TLD label. Returns `bytes32(0)` if not initialized.
203
+ function rootByLabel(string calldata label) external view returns (bytes32) {
204
+ return _rootByLabel[label];
205
+ }
206
+
207
+ // ─── Registration ───────────────────────────────────────────────
208
+
209
+ /**
210
+ * @notice Register a child name under a parent.
211
+ * @dev Caller must be parent's owner OR parent's subregistry delegate.
212
+ * Caller's Smart Agent is responsible for routing through its
213
+ * CustodyPolicy before this call lands; this function trusts
214
+ * `msg.sender` to be the gated entity.
215
+ */
216
+ function register(
217
+ bytes32 parentNode,
218
+ string calldata label,
219
+ address newOwner,
220
+ address resolverContract,
221
+ uint64 expiry
222
+ ) external returns (bytes32 childNode) {
223
+ if (bytes(label).length == 0) revert EmptyLabel();
224
+ if (newOwner == address(0)) revert ZeroOwner();
225
+ _requireParentAuth(parentNode);
226
+ _requireNotExpired(parentNode);
227
+
228
+ bytes32 lh = keccak256(bytes(label));
229
+ childNode = keccak256(abi.encodePacked(parentNode, lh));
230
+ if (_records[childNode].registeredAt != 0) revert NodeAlreadyExists();
231
+
232
+ _records[childNode] = NameRecord({
233
+ owner: newOwner,
234
+ resolver: resolverContract,
235
+ subregistry: address(0),
236
+ parent: parentNode,
237
+ labelhash: lh,
238
+ expiry: expiry,
239
+ registeredAt: uint64(block.timestamp)
240
+ });
241
+
242
+ _children[parentNode][lh] = childNode;
243
+ _childLabels[parentNode].push(lh);
244
+ _label[childNode] = label;
245
+
246
+ emit NameRegistered(childNode, parentNode, label, newOwner, resolverContract, expiry);
247
+ }
248
+
249
+ /**
250
+ * @notice Backfill a label for a node registered before per-node
251
+ * label storage shipped (pre-spec/222 deployments).
252
+ * Authorized to the node's owner ONLY — labels can't be
253
+ * changed once set, preventing display spoofing.
254
+ */
255
+ function backfillLabel(bytes32 node, string calldata label_) external {
256
+ _requireNodeAuth(node);
257
+ if (bytes(label_).length == 0) revert EmptyLabel();
258
+ if (keccak256(bytes(label_)) != _records[node].labelhash) revert NotAuthorized();
259
+ if (bytes(_label[node]).length != 0) revert NotAuthorized();
260
+ _label[node] = label_;
261
+ }
262
+
263
+ // ─── Setters ────────────────────────────────────────────────────
264
+
265
+ function setOwner(bytes32 node, address newOwner) external {
266
+ _requireNodeAuth(node);
267
+ if (newOwner == address(0)) revert ZeroOwner();
268
+ _records[node].owner = newOwner;
269
+ emit OwnerChanged(node, newOwner);
270
+ }
271
+
272
+ function setResolver(bytes32 node, address resolverContract) external {
273
+ _requireNodeAuth(node);
274
+ _records[node].resolver = resolverContract;
275
+ emit ResolverChanged(node, resolverContract);
276
+ }
277
+
278
+ function setSubregistry(bytes32 node, address subregistryContract) external {
279
+ _requireNodeAuth(node);
280
+ _records[node].subregistry = subregistryContract;
281
+ emit SubregistryChanged(node, subregistryContract);
282
+ }
283
+
284
+ function renew(bytes32 node, uint64 newExpiry) external {
285
+ _requireNodeAuth(node);
286
+ _records[node].expiry = newExpiry;
287
+ emit NameRenewed(node, newExpiry);
288
+ }
289
+
290
+ /**
291
+ * @notice Set the reverse-record (primary name) for `msg.sender`.
292
+ * @dev Anyone may set their own primary name; the registry does
293
+ * NOT verify the forward record points back here. The
294
+ * universal resolver enforces the round-trip on reads.
295
+ */
296
+ function setPrimaryName(bytes32 node) external {
297
+ if (node != bytes32(0) && _records[node].registeredAt == 0) revert NodeNotFound();
298
+ _primaryName[msg.sender] = node;
299
+ if (node == bytes32(0)) emit PrimaryNameCleared(msg.sender);
300
+ else emit PrimaryNameSet(msg.sender, node);
301
+ }
302
+
303
+ /// @notice Read the unverified primary-name node for `agent`. The
304
+ /// universal resolver MUST round-trip this against the
305
+ /// resolver's `addr(node)`.
306
+ function primaryName(address agent) external view returns (bytes32) {
307
+ return _primaryName[agent];
308
+ }
309
+
310
+ // ─── Queries ────────────────────────────────────────────────────
311
+
312
+ function owner(bytes32 node) external view returns (address) { return _records[node].owner; }
313
+ function resolver(bytes32 node) external view returns (address) { return _records[node].resolver; }
314
+ function subregistry(bytes32 node) external view returns (address) { return _records[node].subregistry; }
315
+ function parent(bytes32 node) external view returns (bytes32) { return _records[node].parent; }
316
+ function labelhash(bytes32 node) external view returns (bytes32) { return _records[node].labelhash; }
317
+ function expiry(bytes32 node) external view returns (uint64) { return _records[node].expiry; }
318
+ function recordExists(bytes32 node) external view returns (bool) { return _records[node].registeredAt != 0; }
319
+ function registeredAt(bytes32 node) external view returns (uint64) { return _records[node].registeredAt; }
320
+
321
+ function childNode(bytes32 parentNode, bytes32 lh) external view returns (bytes32) {
322
+ return _children[parentNode][lh];
323
+ }
324
+ function childCount(bytes32 parentNode) external view returns (uint256) {
325
+ return _childLabels[parentNode].length;
326
+ }
327
+ function childLabelhashes(bytes32 parentNode) external view returns (bytes32[] memory) {
328
+ return _childLabels[parentNode];
329
+ }
330
+ function isExpired(bytes32 node) public view returns (bool) {
331
+ uint64 exp = _records[node].expiry;
332
+ return exp != 0 && block.timestamp > exp;
333
+ }
334
+
335
+ /// @notice Plain-text label for `node` (the string that was passed
336
+ /// to register / initializeRoot). Empty for un-backfilled
337
+ /// pre-spec/222 records.
338
+ function label(bytes32 node) external view returns (string memory) {
339
+ return _label[node];
340
+ }
341
+
342
+ // ─── Auth ───────────────────────────────────────────────────────
343
+
344
+ function _requireNodeAuth(bytes32 node) internal view {
345
+ NameRecord storage r = _records[node];
346
+ if (r.registeredAt == 0) revert NodeNotFound();
347
+ if (msg.sender != r.owner) revert NotAuthorized();
348
+ }
349
+
350
+ function _requireParentAuth(bytes32 parentNode) internal view {
351
+ NameRecord storage r = _records[parentNode];
352
+ if (r.registeredAt == 0) revert ParentNotFound();
353
+ if (msg.sender == r.owner) return;
354
+ if (r.subregistry != address(0) && msg.sender == r.subregistry) return;
355
+ revert NotAuthorized();
356
+ }
357
+
358
+ function _requireNotExpired(bytes32 node) internal view {
359
+ if (isExpired(node)) revert NameExpired();
360
+ }
361
+ }
362
+
@@ -0,0 +1,210 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "./AgentNameRegistry.sol";
5
+ import "./AgentNameAttributeResolver.sol";
6
+ import "./AgentNamePredicates.sol";
7
+
8
+ /**
9
+ * @title AgentNameUniversalResolver
10
+ * @notice Read-only aggregator for the agent-naming registry +
11
+ * ontology-backed AttributeResolver.
12
+ *
13
+ * Combines the registry (who owns what node) with the per-node typed
14
+ * resolver (what records are stored) into a single, gas-efficient read
15
+ * surface. The SDK targets this contract for `resolveName`,
16
+ * `reverseResolve`, and typed multi-record reads.
17
+ *
18
+ * Round-trip discipline (security invariant from spec 215 § 10):
19
+ * `resolveName(reverseResolve(addr)) == addr` MUST hold. We enforce
20
+ * this on the read side; `AgentNameRegistry.setPrimaryName` stays
21
+ * permissive so callers can sequence forward + reverse writes
22
+ * independently.
23
+ */
24
+ contract AgentNameUniversalResolver {
25
+ AgentNameRegistry public immutable REGISTRY;
26
+
27
+ constructor(AgentNameRegistry registry) {
28
+ REGISTRY = registry;
29
+ }
30
+
31
+ // ─── Forward resolution ─────────────────────────────────────────
32
+
33
+ /**
34
+ * @notice Resolve `node` to its Smart Agent address.
35
+ *
36
+ * Lookup order:
37
+ * 1. resolver.getAddress(node, ATL_ADDR) — explicit forward record.
38
+ * 2. REGISTRY.owner(node) — fallback when ATL_ADDR is unset.
39
+ *
40
+ * Returns `address(0)` for unregistered nodes (does NOT revert) so
41
+ * a multi-call can probe many names without partial reverts.
42
+ */
43
+ function resolveName(bytes32 node) external view returns (address) {
44
+ if (!REGISTRY.recordExists(node)) return address(0);
45
+ address resolverAddr = REGISTRY.resolver(node);
46
+ if (resolverAddr != address(0)) {
47
+ try AgentNameAttributeResolver(resolverAddr).getAddress(node, AgentNamePredicates.ATL_ADDR) returns (address resolved) {
48
+ if (resolved != address(0)) return resolved;
49
+ } catch {}
50
+ }
51
+ return REGISTRY.owner(node);
52
+ }
53
+
54
+ /// @notice Resolve a single string-valued record by predicate id.
55
+ function resolveString(bytes32 node, bytes32 predicate) external view returns (string memory) {
56
+ address resolverAddr = REGISTRY.resolver(node);
57
+ if (resolverAddr == address(0)) return "";
58
+ try AgentNameAttributeResolver(resolverAddr).getString(node, predicate) returns (string memory value) {
59
+ return value;
60
+ } catch {
61
+ return "";
62
+ }
63
+ }
64
+
65
+ /// @notice Resolve a single bytes32-valued record by predicate id.
66
+ function resolveBytes32(bytes32 node, bytes32 predicate) external view returns (bytes32) {
67
+ address resolverAddr = REGISTRY.resolver(node);
68
+ if (resolverAddr == address(0)) return bytes32(0);
69
+ try AgentNameAttributeResolver(resolverAddr).getBytes32(node, predicate) returns (bytes32 value) {
70
+ return value;
71
+ } catch {
72
+ return bytes32(0);
73
+ }
74
+ }
75
+
76
+ /// @notice Resolve a single address-valued record by predicate id.
77
+ function resolveAddress(bytes32 node, bytes32 predicate) external view returns (address) {
78
+ address resolverAddr = REGISTRY.resolver(node);
79
+ if (resolverAddr == address(0)) return address(0);
80
+ try AgentNameAttributeResolver(resolverAddr).getAddress(node, predicate) returns (address value) {
81
+ return value;
82
+ } catch {
83
+ return address(0);
84
+ }
85
+ }
86
+
87
+ /// @notice Multi-read of N string records in a single static-call.
88
+ function resolveStringBatch(bytes32 node, bytes32[] calldata predicates)
89
+ external
90
+ view
91
+ returns (string[] memory values)
92
+ {
93
+ values = new string[](predicates.length);
94
+ address resolverAddr = REGISTRY.resolver(node);
95
+ if (resolverAddr == address(0)) return values;
96
+ for (uint256 i = 0; i < predicates.length; i++) {
97
+ try AgentNameAttributeResolver(resolverAddr).getString(node, predicates[i]) returns (string memory value) {
98
+ values[i] = value;
99
+ } catch {
100
+ values[i] = "";
101
+ }
102
+ }
103
+ }
104
+
105
+ // ─── Reverse resolution (round-trip enforced) ───────────────────
106
+
107
+ /**
108
+ * @notice Resolve a Smart Agent address back to its primary-name node.
109
+ * @return node The primary-name node, OR `bytes32(0)` when no primary
110
+ * name is set OR when the forward record does not point
111
+ * back to `agent` (round-trip fails — squat protection).
112
+ */
113
+ function reverseResolve(address agent) external view returns (bytes32 node) {
114
+ node = REGISTRY.primaryName(agent);
115
+ if (node == bytes32(0)) return bytes32(0);
116
+ if (!REGISTRY.recordExists(node)) return bytes32(0);
117
+ address forward = _resolveNameView(node);
118
+ if (forward != agent) return bytes32(0);
119
+ return node;
120
+ }
121
+
122
+ /**
123
+ * @notice Reverse-resolve a Smart Agent address to its primary name
124
+ * STRING in a single external call. Per spec/222 this is
125
+ * the ENS-aligned reverse path — no log walks, no name
126
+ * reconstruction in the SDK, no event indexer required.
127
+ *
128
+ * Walks `parent(node)` up the registry, reading `label(node)`
129
+ * at each level, joining with `.`. All view calls. Returns
130
+ * `""` when (a) no primary set, (b) round-trip fails (squat
131
+ * protection), or (c) any node along the parent chain has
132
+ * no on-chain label string (pre-spec/222 backfill not done).
133
+ */
134
+ function reverseResolveString(address agent) external view returns (string memory) {
135
+ bytes32 node = REGISTRY.primaryName(agent);
136
+ if (node == bytes32(0)) return "";
137
+ if (!REGISTRY.recordExists(node)) return "";
138
+ if (_resolveNameView(node) != agent) return "";
139
+ return _composeName(node);
140
+ }
141
+
142
+ /**
143
+ * @notice Compose the full dotted name string for `node` by walking
144
+ * the parent chain. Generalizes: works for ANY registered
145
+ * node, not just an agent's primary. Returns `""` if any
146
+ * label in the chain is missing on chain.
147
+ */
148
+ function nameOf(bytes32 node) external view returns (string memory) {
149
+ if (node == bytes32(0)) return "";
150
+ if (!REGISTRY.recordExists(node)) return "";
151
+ return _composeName(node);
152
+ }
153
+
154
+ function _composeName(bytes32 startNode) internal view returns (string memory) {
155
+ // Collect labels walking up to root. Bounded depth = 10 to
156
+ // match the SDK's previous _reconstructName cap (no demo path
157
+ // exceeds 4 — alice7.demo.agent is 3).
158
+ string[10] memory labels;
159
+ uint256 depth = 0;
160
+ bytes32 cur = startNode;
161
+ while (cur != bytes32(0) && depth < 10) {
162
+ string memory lbl = REGISTRY.label(cur);
163
+ if (bytes(lbl).length == 0) return "";
164
+ labels[depth] = lbl;
165
+ cur = REGISTRY.parent(cur);
166
+ unchecked { depth++; }
167
+ }
168
+ if (depth == 0) return "";
169
+
170
+ // Concatenate labels[0..depth-1] with '.' separators.
171
+ uint256 totalLen = depth - 1; // for the dots
172
+ for (uint256 i = 0; i < depth; i++) totalLen += bytes(labels[i]).length;
173
+ bytes memory out = new bytes(totalLen);
174
+ uint256 pos = 0;
175
+ for (uint256 i = 0; i < depth; i++) {
176
+ bytes memory lbl = bytes(labels[i]);
177
+ for (uint256 j = 0; j < lbl.length; j++) out[pos++] = lbl[j];
178
+ if (i + 1 < depth) out[pos++] = 0x2e; // '.'
179
+ }
180
+ return string(out);
181
+ }
182
+
183
+ function _resolveNameView(bytes32 node) internal view returns (address) {
184
+ address resolverAddr = REGISTRY.resolver(node);
185
+ if (resolverAddr != address(0)) {
186
+ try AgentNameAttributeResolver(resolverAddr).getAddress(node, AgentNamePredicates.ATL_ADDR) returns (address resolved) {
187
+ if (resolved != address(0)) return resolved;
188
+ } catch {}
189
+ }
190
+ return REGISTRY.owner(node);
191
+ }
192
+
193
+ // ─── Directory listing ──────────────────────────────────────────
194
+
195
+ /// @notice List a node's children and their resolved addresses.
196
+ function getChildren(bytes32 parentNode)
197
+ external
198
+ view
199
+ returns (bytes32[] memory childNodes, address[] memory owners)
200
+ {
201
+ bytes32[] memory labelhashes = REGISTRY.childLabelhashes(parentNode);
202
+ childNodes = new bytes32[](labelhashes.length);
203
+ owners = new address[](labelhashes.length);
204
+ for (uint256 i = 0; i < labelhashes.length; i++) {
205
+ bytes32 child = REGISTRY.childNode(parentNode, labelhashes[i]);
206
+ childNodes[i] = child;
207
+ owners[i] = REGISTRY.owner(child);
208
+ }
209
+ }
210
+ }
@@ -0,0 +1,98 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "./AgentNameRegistry.sol";
5
+
6
+ /**
7
+ * @title PermissionlessSubregistry
8
+ * @notice Anyone-can-register subregistry for a single parent name.
9
+ *
10
+ * Design:
11
+ * - At deploy: bound to a specific `PARENT_NODE` + a default
12
+ * resolver address. The parent's owner (typically the deployer
13
+ * during demo bootstrap) MUST call
14
+ * `AgentNameRegistry.setSubregistry(PARENT_NODE, address(this))`
15
+ * once after deploy to grant this contract authority to register
16
+ * children under `PARENT_NODE`.
17
+ *
18
+ * - Any caller (EOA or Smart Agent) may invoke `register(label,
19
+ * newOwner)` to claim `<label>.<parent>`. The registered child
20
+ * is owned by `newOwner` (caller decides — typically themselves
21
+ * OR a PSA they control).
22
+ *
23
+ * Spam prevention:
24
+ * - Minimum label length (`MIN_LABEL_LENGTH = 3`).
25
+ * - One name per caller `msg.sender` (`claimedBy[caller] != 0`
26
+ * reverts). Designed for demos / sybil-resistant rollups; a
27
+ * production subregistry would gate on a registration fee or a
28
+ * governance allowlist instead.
29
+ *
30
+ * Authority model:
31
+ * - The contract calls `REGISTRY.register(...)`; the registry sees
32
+ * `msg.sender == address(this)` AND `r.subregistry == this` and
33
+ * authorizes the registration. The new child's owner is the
34
+ * caller-supplied `newOwner` argument — NOT this contract.
35
+ * - Anyone wanting to update records on the new child does so
36
+ * through whatever authority `newOwner` represents (EOA wallet,
37
+ * PSA CustodyPolicy quorum, etc.).
38
+ */
39
+ contract PermissionlessSubregistry {
40
+ AgentNameRegistry public immutable REGISTRY;
41
+ bytes32 public immutable PARENT_NODE;
42
+ address public immutable DEFAULT_RESOLVER;
43
+
44
+ uint256 public constant MIN_LABEL_LENGTH = 3;
45
+
46
+ /// @notice msg.sender → the child node they've already claimed.
47
+ mapping(address => bytes32) public claimedBy;
48
+ /// @notice Total claims served by this subregistry instance.
49
+ uint256 public claimCount;
50
+
51
+ event NameClaimed(
52
+ address indexed caller,
53
+ bytes32 indexed childNode,
54
+ string label,
55
+ address newOwner
56
+ );
57
+
58
+ error AlreadyClaimed(bytes32 existingNode);
59
+ error LabelTooShort();
60
+ error EmptyLabel();
61
+ error ZeroNewOwner();
62
+
63
+ constructor(AgentNameRegistry registry, bytes32 parentNode, address defaultResolver) {
64
+ REGISTRY = registry;
65
+ PARENT_NODE = parentNode;
66
+ DEFAULT_RESOLVER = defaultResolver;
67
+ }
68
+
69
+ /**
70
+ * @notice Claim `<label>.<parent>` for `newOwner`. Reverts if the
71
+ * caller has already claimed a name through this
72
+ * subregistry, OR if the label fails the minimum-length
73
+ * guard, OR if the registry rejects the registration
74
+ * (e.g. label already taken).
75
+ *
76
+ * The caller pays gas. The contract does NOT collect a
77
+ * fee — fee gating belongs in a different subregistry
78
+ * shape if needed.
79
+ */
80
+ function register(string calldata label, address newOwner) external returns (bytes32 childNode) {
81
+ if (bytes(label).length == 0) revert EmptyLabel();
82
+ if (bytes(label).length < MIN_LABEL_LENGTH) revert LabelTooShort();
83
+ if (newOwner == address(0)) revert ZeroNewOwner();
84
+ bytes32 prior = claimedBy[msg.sender];
85
+ if (prior != bytes32(0)) revert AlreadyClaimed(prior);
86
+ childNode = REGISTRY.register(PARENT_NODE, label, newOwner, DEFAULT_RESOLVER, 0);
87
+ claimedBy[msg.sender] = childNode;
88
+ unchecked {
89
+ claimCount += 1;
90
+ }
91
+ emit NameClaimed(msg.sender, childNode, label, newOwner);
92
+ }
93
+
94
+ /// @notice Has `caller` already claimed a name? Convenience read.
95
+ function hasClaimed(address caller) external view returns (bool) {
96
+ return claimedBy[caller] != bytes32(0);
97
+ }
98
+ }