@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.
- package/AUDIT.md +67 -0
- package/CLAUDE.md +40 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/deployments-anvil.json +1 -0
- package/deployments-base-sepolia.json +1 -0
- package/dist/abi/AgentNameAttributeResolver.json +798 -0
- package/dist/abi/AgentNamePredicates.json +1 -0
- package/dist/abi/AgentNameRegistry.json +826 -0
- package/dist/abi/AgentNameUniversalResolver.json +222 -0
- package/dist/abi/AgentProfilePredicates.json +1 -0
- package/dist/abi/AgentProfileResolver.json +1044 -0
- package/dist/abi/AgentRelationship.json +583 -0
- package/dist/abi/AgentRelationshipPredicates.json +1 -0
- package/dist/abi/AgenticGovernance.json +259 -0
- package/dist/abi/AllowedMethodsEnforcer.json +108 -0
- package/dist/abi/AllowedTargetsEnforcer.json +103 -0
- package/dist/abi/ApprovedHashRegistry.json +114 -0
- package/dist/abi/AttributeStorage.json +557 -0
- package/dist/abi/CaveatEnforcerBase.json +130 -0
- package/dist/abi/GovernanceManaged.json +43 -0
- package/dist/abi/IAttributeReader.json +98 -0
- package/dist/abi/ICaveatEnforcer.json +98 -0
- package/dist/abi/IDelegationManager.json +211 -0
- package/dist/abi/IERC7579Module.json +34 -0
- package/dist/abi/IERC7579ModuleLifecycle.json +60 -0
- package/dist/abi/IGovernanceView.json +34 -0
- package/dist/abi/MultiSendCallOnly.json +29 -0
- package/dist/abi/MultiSendCallOnlyHarness.json +42 -0
- package/dist/abi/OntologyTermRegistry.json +397 -0
- package/dist/abi/P256Verifier.json +1 -0
- package/dist/abi/PermissionlessSubregistry.json +207 -0
- package/dist/abi/RelationshipTypeRegistry.json +455 -0
- package/dist/abi/ShapeRegistry.json +627 -0
- package/dist/abi/SmartAgentModuleTypes.json +1 -0
- package/dist/abi/TimestampEnforcer.json +108 -0
- package/dist/abi/ValueEnforcer.json +103 -0
- package/dist/abi/WebAuthnLib.json +1 -0
- package/dist/abi/index.d.ts +35 -0
- package/dist/abi/index.js +35 -0
- package/package.json +48 -0
- package/spec.md +52 -0
- package/src/AgentAccount.sol +1374 -0
- package/src/AgentAccountFactory.sol +274 -0
- package/src/ApprovedHashRegistry.sol +57 -0
- package/src/IAgentAccount.sol +138 -0
- package/src/SmartAgentPaymaster.sol +281 -0
- package/src/UniversalSignatureValidator.sol +136 -0
- package/src/agency/DelegationManager.sol +374 -0
- package/src/agency/ICaveatEnforcer.sol +62 -0
- package/src/agency/IDelegationManager.sol +69 -0
- package/src/custody/CustodyPolicy.sol +892 -0
- package/src/custody/IERC7579Module.sol +60 -0
- package/src/enforcers/AllowedMethodsEnforcer.AUDIT.md +51 -0
- package/src/enforcers/AllowedMethodsEnforcer.sol +48 -0
- package/src/enforcers/AllowedTargetsEnforcer.AUDIT.md +49 -0
- package/src/enforcers/AllowedTargetsEnforcer.sol +44 -0
- package/src/enforcers/CaveatEnforcerBase.sol +19 -0
- package/src/enforcers/QuorumEnforcer.AUDIT.md +71 -0
- package/src/enforcers/QuorumEnforcer.sol +191 -0
- package/src/enforcers/TimestampEnforcer.AUDIT.md +50 -0
- package/src/enforcers/TimestampEnforcer.sol +43 -0
- package/src/enforcers/ValueEnforcer.AUDIT.md +51 -0
- package/src/enforcers/ValueEnforcer.sol +41 -0
- package/src/governance/AgenticGovernance.sol +140 -0
- package/src/governance/GovernanceManaged.sol +75 -0
- package/src/governance/IGovernance.sol +15 -0
- package/src/identity/AgentProfilePredicates.sol +40 -0
- package/src/identity/AgentProfileResolver.sol +194 -0
- package/src/libraries/MultiSendCallOnly.sol +95 -0
- package/src/libraries/P256Verifier.sol +47 -0
- package/src/libraries/SignatureSlotRecovery.sol +196 -0
- package/src/libraries/WebAuthnLib.sol +164 -0
- package/src/naming/AgentNameAttributeResolver.sol +95 -0
- package/src/naming/AgentNamePredicates.sol +74 -0
- package/src/naming/AgentNameRegistry.sol +362 -0
- package/src/naming/AgentNameUniversalResolver.sol +210 -0
- package/src/naming/PermissionlessSubregistry.sol +98 -0
- package/src/ontology/AttributeStorage.sol +289 -0
- package/src/ontology/OntologyTermRegistry.sol +146 -0
- package/src/ontology/ShapeRegistry.sol +240 -0
- package/src/relationships/AgentRelationship.sol +289 -0
- package/src/relationships/AgentRelationshipPredicates.sol +44 -0
- 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
|
+
}
|