@agenticprimitives/contracts 1.0.0-alpha.12 → 1.0.0-alpha.14
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/deployments-base-sepolia.json +33 -32
- package/dist/abi/AgentAccount.json +10 -0
- package/dist/abi/AgentAccountFactory.json +11 -0
- package/dist/abi/AgentRegistryBase.json +588 -0
- package/dist/abi/CustodyPolicy.json +11 -0
- package/dist/abi/IAgentAccountFactoryView.json +13 -0
- package/dist/abi/IRegistryMembershipPolicy.json +70 -0
- package/dist/abi/PaymentEnforcer.json +11 -6
- package/dist/abi/index.d.ts +2 -0
- package/dist/abi/index.js +2 -0
- package/dist/deployments/base-sepolia.d.ts +5 -4
- package/dist/deployments/base-sepolia.js +33 -32
- package/package.json +1 -1
- package/src/AgentAccount.sol +146 -7
- package/src/AgentAccountFactory.sol +16 -0
- package/src/SmartAgentPaymaster.sol +11 -6
- package/src/custody/CustodyPolicy.sol +10 -0
- package/src/enforcers/PaymentEnforcer.sol +49 -8
- package/src/identity/AgentProfilePredicates.sol +3 -0
- package/src/naming/AgentNamePredicates.sol +21 -0
- package/src/naming/AgentNameRegistry.sol +13 -3
- package/src/registry/AgentRegistryBase.sol +281 -0
- package/src/registry/IRegistryMembershipPolicy.sol +30 -0
|
@@ -260,16 +260,21 @@ contract SmartAgentPaymaster is BasePaymaster {
|
|
|
260
260
|
return _pausedMirror;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
/// @dev Read the canonical governance pause flag. Used ONLY by the
|
|
264
|
-
///
|
|
265
|
-
///
|
|
266
|
-
///
|
|
263
|
+
/// @dev Read the canonical governance pause flag. Used ONLY by the out-of-band sync path, never
|
|
264
|
+
/// during validation (ERC-7562 scope). L1 (seam audit): distinguish "no governance configured"
|
|
265
|
+
/// (legacy / test deploy — not paused, by design) from "governance IS configured but the read
|
|
266
|
+
/// faulted / returned malformed data" (a governance outage or non-conforming contract), which now
|
|
267
|
+
/// FAILS CLOSED — treat as paused so a sync during a governance fault halts sponsorship rather
|
|
268
|
+
/// than fail-open sponsoring. Only a clean, decodable `false` permits sponsorship.
|
|
267
269
|
function _governanceIsPaused() internal view returns (bool) {
|
|
268
|
-
|
|
270
|
+
// Unconfigured governance (unset, or an EOA with no code) → not paused (legacy/test path).
|
|
271
|
+
if (governance == address(0) || governance.code.length == 0) return false;
|
|
269
272
|
(bool ok, bytes memory data) = governance.staticcall(
|
|
270
273
|
abi.encodeWithSelector(IGovernanceView.isPaused.selector)
|
|
271
274
|
);
|
|
272
|
-
|
|
275
|
+
// Configured but faulted (call failed or undecodable) → fail closed (paused).
|
|
276
|
+
if (!ok || data.length < 32) return true;
|
|
277
|
+
return abi.decode(data, (bool));
|
|
273
278
|
}
|
|
274
279
|
|
|
275
280
|
// ─── PM-2: governance-timelocked deposit withdrawal ────────────────
|
|
@@ -309,6 +309,11 @@ contract CustodyPolicy {
|
|
|
309
309
|
error AdminInsufficientQuorum(uint256 supplied, uint8 required);
|
|
310
310
|
error AdminDuplicateOrUnsortedSigner(address signer);
|
|
311
311
|
error AdminUnauthorizedSigner(address signer);
|
|
312
|
+
/// @notice ACCT-C2 (spec 300) — the account itself was presented as a quorum
|
|
313
|
+
/// signer (via `isCustodian(address(this))==true` or the `v=0`
|
|
314
|
+
/// self-referential ERC-1271 slot). A self-signature must never count
|
|
315
|
+
/// toward the quorum, else one custodian forges an extra member.
|
|
316
|
+
error AdminSelfSignerNotAllowed(address account);
|
|
312
317
|
error TrusteeAlreadyExists(address guardian);
|
|
313
318
|
error TrusteeDoesNotExist(address guardian);
|
|
314
319
|
error InvalidMode(uint8 mode_);
|
|
@@ -748,6 +753,11 @@ contract CustodyPolicy {
|
|
|
748
753
|
);
|
|
749
754
|
if (signer <= prev) revert AdminDuplicateOrUnsortedSigner(signer);
|
|
750
755
|
prev = signer;
|
|
756
|
+
// ACCT-C2 (spec 300): the account is never its own quorum signer. Both
|
|
757
|
+
// exploit shapes — `isCustodian(address(this))==true` and the `v=0`
|
|
758
|
+
// self-referential ERC-1271 slot that recovers `signer==account` — land
|
|
759
|
+
// here as `signer == account`, so one guard closes both.
|
|
760
|
+
if (signer == account) revert AdminSelfSignerNotAllowed(account);
|
|
751
761
|
if (vargs.guardianMode) {
|
|
752
762
|
if (!c.trustees[signer]) revert UnauthorizedTrustee(signer);
|
|
753
763
|
} else {
|
|
@@ -58,6 +58,13 @@ contract PaymentEnforcer is ICaveatEnforcer {
|
|
|
58
58
|
/// delegator => delegationHash => nonce => consumed (on-chain replay guard)
|
|
59
59
|
mapping(address => mapping(bytes32 => mapping(bytes32 => bool))) private _nonceUsed;
|
|
60
60
|
|
|
61
|
+
/// @notice M1 (seam audit): keccak256(delegator, delegationHash, nonce) => (treasury balanceBefore + 1).
|
|
62
|
+
/// Snapshotted in `beforeHook`; consumed in `afterHook` to verify the transfer ACTUALLY moved
|
|
63
|
+
/// `amount` to the treasury before the settlement receipt is written. `+1` sentinel so a set
|
|
64
|
+
/// entry is distinguishable from an unset slot even when the treasury balance is 0. Set and
|
|
65
|
+
/// deleted within a single non-reentrant `redeemDelegation`, so it never persists across txs.
|
|
66
|
+
mapping(bytes32 => uint256) private _settleSnap;
|
|
67
|
+
|
|
61
68
|
/// @notice The ONLY authorized caller of the hooks (EXT-PMT-1 / C-NEW-1). Unlike the stateless
|
|
62
69
|
/// enforcers (timestamp/value/targets/methods), this enforcer MUTATES state (budget, nonce)
|
|
63
70
|
/// and writes a settlement receipt — so it is NOT safe to call externally. A direct caller
|
|
@@ -93,6 +100,10 @@ contract PaymentEnforcer is ICaveatEnforcer {
|
|
|
93
100
|
error FrequencyExceeded(uint32 callsInWindow, uint32 maxRedemptionsPerWindow);
|
|
94
101
|
error NonceReused(bytes32 nonce);
|
|
95
102
|
error NotDelegationManager(address caller);
|
|
103
|
+
/// @notice M1 (seam audit): the transfer did not move `amount` to the treasury (a token that returns
|
|
104
|
+
/// false without reverting, a fee-on-transfer skim, or a missing beforeHook snapshot) — so no
|
|
105
|
+
/// settlement receipt is written and the whole redemption reverts.
|
|
106
|
+
error SettlementNotObserved();
|
|
96
107
|
|
|
97
108
|
/// @dev The hooks are state-mutating + receipt-emitting, so ONLY the DelegationManager may invoke
|
|
98
109
|
/// them (EXT-PMT-1). `_runBeforeHooks`/`_runAfterHooks` call the enforcer directly, so during a
|
|
@@ -173,19 +184,49 @@ contract PaymentEnforcer is ICaveatEnforcer {
|
|
|
173
184
|
delegator, delegationHash, mandateId, treasury, asset, amount, nonce, resourceHash, nextSpent, nextCalls
|
|
174
185
|
);
|
|
175
186
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
187
|
+
// M1 (seam audit): snapshot the treasury balance BEFORE the transfer. The trustless settlement
|
|
188
|
+
// receipt (PAY-CON-4) is written in `afterHook` ONLY once the transfer is observed to have moved
|
|
189
|
+
// `amount` to the treasury — a token that returns false without reverting (or a fee-on-transfer
|
|
190
|
+
// skim) then records NO receipt and reverts the whole redemption, so "a settled charge always has
|
|
191
|
+
// a receipt and an unsettled one never does" holds against the value that ACTUALLY moved.
|
|
192
|
+
_settleSnap[_settleKey(delegator, delegationHash, nonce)] = IERC20(asset).balanceOf(treasury) + 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function afterHook(
|
|
196
|
+
bytes calldata terms,
|
|
197
|
+
bytes calldata args,
|
|
198
|
+
bytes32 delegationHash,
|
|
199
|
+
address delegator,
|
|
200
|
+
address, /*redeemer*/
|
|
201
|
+
address, /*target*/
|
|
202
|
+
uint256, /*value*/
|
|
203
|
+
bytes calldata callData
|
|
204
|
+
) external override onlyDelegationManager {
|
|
205
|
+
// Only this enforcer's own 192-byte-terms caveat carries a snapshot (beforeHook validated the
|
|
206
|
+
// shape); ignore anything else defensively so a malformed sibling caveat can't revert settlement.
|
|
207
|
+
if (terms.length != 192) return;
|
|
208
|
+
(address treasury, address asset,,,,) = abi.decode(terms, (address, address, uint256, uint256, uint32, uint32));
|
|
209
|
+
(bytes32 mandateId, bytes32 nonce, bytes32 resourceHash) = abi.decode(args, (bytes32, bytes32, bytes32));
|
|
210
|
+
(, uint256 amount) = abi.decode(callData[4:], (address, uint256));
|
|
211
|
+
|
|
212
|
+
bytes32 key = _settleKey(delegator, delegationHash, nonce);
|
|
213
|
+
uint256 snap = _settleSnap[key];
|
|
214
|
+
if (snap == 0) revert SettlementNotObserved(); // no beforeHook snapshot (defensive)
|
|
215
|
+
delete _settleSnap[key];
|
|
216
|
+
|
|
217
|
+
// The treasury must have received AT LEAST `amount` since the snapshot. A silent transfer failure
|
|
218
|
+
// (balance unchanged) or a fee-on-transfer short-delivery fails this and reverts the redemption.
|
|
219
|
+
if (IERC20(asset).balanceOf(treasury) < (snap - 1) + amount) revert SettlementNotObserved();
|
|
220
|
+
|
|
178
221
|
if (address(receiptRegistry) != address(0)) {
|
|
179
222
|
receiptRegistry.record(mandateId, delegator, treasury, asset, amount, resourceHash, delegationHash, nonce);
|
|
180
223
|
}
|
|
181
224
|
}
|
|
182
225
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
onlyDelegationManager
|
|
188
|
-
{}
|
|
226
|
+
/// @dev Unique per redemption — the nonce is single-use per [delegator][delegationHash].
|
|
227
|
+
function _settleKey(address delegator, bytes32 delegationHash, bytes32 nonce) private pure returns (bytes32) {
|
|
228
|
+
return keccak256(abi.encode(delegator, delegationHash, nonce));
|
|
229
|
+
}
|
|
189
230
|
|
|
190
231
|
// ── views (off-chain budget display + idempotency checks) ──
|
|
191
232
|
|
|
@@ -28,6 +28,9 @@ library AgentProfilePredicates {
|
|
|
28
28
|
// ─── Identity-only ──────────────────────────────────────────────
|
|
29
29
|
|
|
30
30
|
bytes32 internal constant ATL_DESCRIPTION = keccak256("atl:description");
|
|
31
|
+
/// spec 282 — publicly-asserted skill labels (string; the indexer projects to approf:skills, the
|
|
32
|
+
/// discovery matcher ranks on it). The agent's full private skill claims live in agent-skills VCs.
|
|
33
|
+
bytes32 internal constant ATL_SKILLS = keccak256("atl:skills");
|
|
31
34
|
bytes32 internal constant ATL_HOMEPAGE = keccak256("atl:homepage");
|
|
32
35
|
bytes32 internal constant ATL_AVATAR = keccak256("atl:avatar");
|
|
33
36
|
bytes32 internal constant ATL_PROFILE_SCHEMA_URI = keccak256("atl:profileSchemaURI");
|
|
@@ -55,6 +55,17 @@ library AgentNamePredicates {
|
|
|
55
55
|
/// (per ADR-0008). `string`.
|
|
56
56
|
bytes32 internal constant ATL_NATIVE_ID = keccak256("atl:nativeId");
|
|
57
57
|
|
|
58
|
+
/// `atl:connectionKind` — connection-bootstrap credential KIND (spec 280).
|
|
59
|
+
/// Stored as `bytes32` (hashed enum). Bound to CONNECTION_KIND_ENUM. OPT-IN,
|
|
60
|
+
/// owner-set, PUBLIC. Advertises how a returning person connects by name.
|
|
61
|
+
bytes32 internal constant ATL_CONNECTION_KIND = keccak256("atl:connectionKind");
|
|
62
|
+
|
|
63
|
+
/// `atl:connectionAddress` — connection-bootstrap credential ADDRESS (spec 280).
|
|
64
|
+
/// The custodian EOA (wallet) or social KMS C_sub. `address`. OPT-IN, owner-set,
|
|
65
|
+
/// PUBLIC pre-select hint only (NOT required to connect). Deliberate, documented
|
|
66
|
+
/// name→credential index (ADR-0040 amendment). Never set automatically.
|
|
67
|
+
bytes32 internal constant ATL_CONNECTION_ADDRESS = keccak256("atl:connectionAddress");
|
|
68
|
+
|
|
58
69
|
// ─── Shape + enum-set ids ───────────────────────────────────────
|
|
59
70
|
|
|
60
71
|
/// `atl:AgentName` — the class id for ShapeRegistry validation.
|
|
@@ -71,4 +82,14 @@ library AgentNamePredicates {
|
|
|
71
82
|
bytes32 internal constant AGENT_KIND_PERSON = keccak256("person");
|
|
72
83
|
bytes32 internal constant AGENT_KIND_ORG = keccak256("org");
|
|
73
84
|
bytes32 internal constant AGENT_KIND_SERVICE = keccak256("service");
|
|
85
|
+
|
|
86
|
+
/// Enum set bound to `atl:connectionKind` (spec 280). Contains the
|
|
87
|
+
/// five hashed member ids below.
|
|
88
|
+
bytes32 internal constant CONNECTION_KIND_ENUM = keccak256("atl:ConnectionKindEnum");
|
|
89
|
+
|
|
90
|
+
bytes32 internal constant CONNECTION_KIND_WALLET = keccak256("wallet");
|
|
91
|
+
bytes32 internal constant CONNECTION_KIND_GOOGLE = keccak256("google");
|
|
92
|
+
bytes32 internal constant CONNECTION_KIND_YOUVERSION = keccak256("youversion");
|
|
93
|
+
bytes32 internal constant CONNECTION_KIND_PASSKEY = keccak256("passkey");
|
|
94
|
+
bytes32 internal constant CONNECTION_KIND_MULTI = keccak256("multi");
|
|
74
95
|
}
|
|
@@ -261,7 +261,10 @@ contract AgentNameRegistry is GovernanceManaged {
|
|
|
261
261
|
|
|
262
262
|
bytes32 lh = keccak256(bytes(label));
|
|
263
263
|
childNode = keccak256(abi.encodePacked(parentNode, lh));
|
|
264
|
-
|
|
264
|
+
// M2 (seam audit): an EXPIRED name is reclaimable (parent-authorized re-register). Only a LIVE
|
|
265
|
+
// record blocks re-registration — previously expiry was inert and a name could never be reclaimed.
|
|
266
|
+
bool reclaiming = _records[childNode].registeredAt != 0;
|
|
267
|
+
if (reclaiming && !isExpired(childNode)) revert NodeAlreadyExists();
|
|
265
268
|
|
|
266
269
|
_records[childNode] = NameRecord({
|
|
267
270
|
owner: newOwner,
|
|
@@ -274,7 +277,8 @@ contract AgentNameRegistry is GovernanceManaged {
|
|
|
274
277
|
});
|
|
275
278
|
|
|
276
279
|
_children[parentNode][lh] = childNode;
|
|
277
|
-
_childLabels
|
|
280
|
+
// Only index a brand-new child; a reclaim reuses the existing _childLabels entry (no duplicate).
|
|
281
|
+
if (!reclaiming) _childLabels[parentNode].push(lh);
|
|
278
282
|
_label[childNode] = label;
|
|
279
283
|
|
|
280
284
|
emit NameRegistered(childNode, parentNode, label, newOwner, resolverContract, expiry);
|
|
@@ -352,7 +356,13 @@ contract AgentNameRegistry is GovernanceManaged {
|
|
|
352
356
|
function parent(bytes32 node) external view returns (bytes32) { return _records[node].parent; }
|
|
353
357
|
function labelhash(bytes32 node) external view returns (bytes32) { return _records[node].labelhash; }
|
|
354
358
|
function expiry(bytes32 node) external view returns (uint64) { return _records[node].expiry; }
|
|
355
|
-
|
|
359
|
+
/// @dev M2 (seam audit): an EXPIRED record does NOT exist for read purposes — so the resolver
|
|
360
|
+
/// (resolveName/resolveString, which gate on recordExists) and any other reader stop resolving
|
|
361
|
+
/// an expired name. Expiry now provides real revocation, not a decorative field. Registry
|
|
362
|
+
/// mutators gate on the raw `registeredAt`/`isExpired`, not this, so re-registration is unaffected.
|
|
363
|
+
function recordExists(bytes32 node) external view returns (bool) {
|
|
364
|
+
return _records[node].registeredAt != 0 && !isExpired(node);
|
|
365
|
+
}
|
|
356
366
|
function registeredAt(bytes32 node) external view returns (uint64) { return _records[node].registeredAt; }
|
|
357
367
|
|
|
358
368
|
function childNode(bytes32 parentNode, bytes32 lh) external view returns (bytes32) {
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import {IRegistryMembershipPolicy} from "./IRegistryMembershipPolicy.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title AgentRegistryBase
|
|
8
|
+
* @notice The generic, SA-anchored registry base that vertical registries are built FROM (spec 279 §4,
|
|
9
|
+
* ADR-0038 "many-registries"). It is a MEMBERSHIP LEDGER of facets — entries that point AT a
|
|
10
|
+
* Smart Agent address — not an account. It holds no funds, signs nothing, executes nothing, and
|
|
11
|
+
* grants NO authority to act: an entry is evidence ("this SA is a member of registry R, here is
|
|
12
|
+
* its card hash + binding-proof hash + claims"), never a delegation. Authority lives in the
|
|
13
|
+
* AgentAccount + DelegationManager; this contract only records and gates membership.
|
|
14
|
+
*
|
|
15
|
+
* ─── How it differs from AgentAccount ───
|
|
16
|
+
* AgentAccount IS the agent (ERC-4337/7579 custody + execution + ERC-1271). AgentRegistryBase is
|
|
17
|
+
* a directory of MANY entries pointing at MANY agents, one instance per registry. The subject SA
|
|
18
|
+
* controls its own entry because it must be the caller (RB-01); the registry's controller / policy
|
|
19
|
+
* hook gates admission + ejection (RB-02/03). The binding proof referenced by `bindingProofHash`
|
|
20
|
+
* is verified OFF-CHAIN by a verifier (registry-kit `verifyEntryBindingProof`) against the
|
|
21
|
+
* subject SA's ERC-1271 — this contract stores the hash as a pointer, it does not re-verify it.
|
|
22
|
+
*
|
|
23
|
+
* ─── Key invariants (RB-*) ───
|
|
24
|
+
* RB-01 entry control follows the subject SA custody policy — register / renew / self-revoke /
|
|
25
|
+
* self-update require `msg.sender == subjectAgent` (so the SA's own custody produced the
|
|
26
|
+
* call); the contract never re-implements custody.
|
|
27
|
+
* RB-02 admission is gated by the registry's pluggable membership hook (or open if unset); a
|
|
28
|
+
* policy returning false OR reverting fails closed (ADR-0013) — no second path.
|
|
29
|
+
* RB-03 suspension / revocation / claim-update by a non-subject require the registry controller
|
|
30
|
+
* OR a policy-authorized actor (`canAdminister`); never an arbitrary caller.
|
|
31
|
+
* RB-04 revocation is terminal; a revoked entry cannot be renewed, re-registered, suspended, or
|
|
32
|
+
* claim-updated.
|
|
33
|
+
* RB-05 NO external-registry-protocol fields in storage (no ENS/8004/HCS/OASF) — those are
|
|
34
|
+
* external projections (ADR-0037), not AP state.
|
|
35
|
+
* RB-06 reads are storage views only — discovery never scans chain logs (ADR-0012); the indexer
|
|
36
|
+
* consumes the events below.
|
|
37
|
+
* RB-07 epoch-bucket registration timestamps (raw block.timestamp never stored); `expiresAt` is a
|
|
38
|
+
* caller-supplied absolute unix-seconds bound compared against block.timestamp for liveness.
|
|
39
|
+
*
|
|
40
|
+
* Spec 279 §4 + ADR-0010 / 0012 / 0013 / 0037 / 0038.
|
|
41
|
+
*/
|
|
42
|
+
contract AgentRegistryBase {
|
|
43
|
+
// ─── Constants ──────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/// @dev RB-07 — registration time is bucketed (provenance, not a precise clock). Mirrors AttestationRegistry.
|
|
46
|
+
uint64 public constant EPOCH_SECONDS = 3600;
|
|
47
|
+
|
|
48
|
+
// ─── Types ──────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/// @dev Stored status. `Expired` is NOT stored (no keeper to flip it) — it is DERIVED from `expiresAt`
|
|
51
|
+
/// by {isActive}; storage only tracks the explicit lifecycle transitions.
|
|
52
|
+
enum EntryStatus {
|
|
53
|
+
None,
|
|
54
|
+
Active,
|
|
55
|
+
Suspended,
|
|
56
|
+
Revoked
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
struct Registry {
|
|
60
|
+
address controller; // the governing SA (set on create); address(0) iff !exists
|
|
61
|
+
address policy; // pluggable membership hook; address(0) = open registry (subject self-service only)
|
|
62
|
+
bool exists;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
struct Entry {
|
|
66
|
+
address subjectAgent; // the canonical SA this entry is a facet of
|
|
67
|
+
bytes32 cardHash; // sha256 of the agent-profile signed card bundle
|
|
68
|
+
bytes32 bindingProofHash; // sha256 of the off-chain binding proof (verified off-chain, not here)
|
|
69
|
+
bytes32 claimsRoot; // keccak of the registration-time claimHashes[] (the set the binding proof signed)
|
|
70
|
+
EntryStatus status;
|
|
71
|
+
uint64 registeredAtBucket; // block.timestamp / EPOCH_SECONDS at registration
|
|
72
|
+
uint64 expiresAt; // absolute unix seconds; 0 = non-expiring
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Storage ────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
mapping(bytes32 => Registry) private _registries; // registryId => Registry
|
|
78
|
+
mapping(bytes32 => Entry) private _entries; // entryKey => Entry
|
|
79
|
+
mapping(bytes32 => mapping(bytes32 => bytes32)) private _claims; // entryKey => claimSlotId => claimHash
|
|
80
|
+
|
|
81
|
+
// ─── Errors ─────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
error RegistryExists();
|
|
84
|
+
error RegistryNotFound();
|
|
85
|
+
error NotController();
|
|
86
|
+
error EntryExists();
|
|
87
|
+
error EntryNotFound();
|
|
88
|
+
error ZeroSubject();
|
|
89
|
+
error SubjectMismatch(); // msg.sender is not the entry's subject SA (RB-01)
|
|
90
|
+
error NotAuthorized(); // msg.sender is neither controller nor a policy-authorized admin (RB-03)
|
|
91
|
+
error NotActive();
|
|
92
|
+
error AlreadyRevoked();
|
|
93
|
+
error InvalidExpiry();
|
|
94
|
+
error MembershipDenied(); // RB-02 fail-closed admission denial
|
|
95
|
+
|
|
96
|
+
// ─── Events ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
// Registry provisioning (not part of the registry-kit lifecycle ABI).
|
|
99
|
+
event RegistryCreated(bytes32 indexed registryId, address indexed controller, address policy);
|
|
100
|
+
event RegistryPolicyUpdated(bytes32 indexed registryId, address policy);
|
|
101
|
+
|
|
102
|
+
// Entry lifecycle — MUST match registry-kit `AGENT_REGISTRY_BASE_ABI` + spec 279 §4.1 EXACTLY
|
|
103
|
+
// (the SDK encodes calls against / decodes logs from these signatures).
|
|
104
|
+
event RegistryEntryRegistered(
|
|
105
|
+
bytes32 indexed registryId,
|
|
106
|
+
bytes32 indexed entryId,
|
|
107
|
+
address indexed subjectAgent,
|
|
108
|
+
bytes32 cardHash,
|
|
109
|
+
bytes32 bindingProofHash
|
|
110
|
+
);
|
|
111
|
+
event RegistryEntryRenewed(bytes32 indexed registryId, bytes32 indexed entryId, uint64 expiresAt);
|
|
112
|
+
event RegistryEntrySuspended(bytes32 indexed registryId, bytes32 indexed entryId, bytes32 reasonHash);
|
|
113
|
+
event RegistryEntryRevoked(bytes32 indexed registryId, bytes32 indexed entryId, bytes32 reasonHash);
|
|
114
|
+
event RegistryClaimUpdated(
|
|
115
|
+
bytes32 indexed registryId, bytes32 indexed entryId, bytes32 indexed claimSlotId, bytes32 claimHash
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// ─── Registry provisioning ──────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/// @notice Create a registry. Permissionless + first-create-wins: `msg.sender` becomes the controller.
|
|
121
|
+
/// @dev `registryId` is `keccak256(urn:ap:registry:…)` off-chain; squatting on an id is a PROVISIONING
|
|
122
|
+
/// concern (create the registry in the same flow that mints its urn), out of scope for this base.
|
|
123
|
+
/// `policy == address(0)` ⇒ open registry: any SA may self-register, no admission gate.
|
|
124
|
+
function createRegistry(bytes32 registryId, address policy) external {
|
|
125
|
+
if (_registries[registryId].exists) revert RegistryExists();
|
|
126
|
+
_registries[registryId] = Registry({controller: msg.sender, policy: policy, exists: true});
|
|
127
|
+
emit RegistryCreated(registryId, msg.sender, policy);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// @notice Swap the pluggable membership hook. Controller only.
|
|
131
|
+
function setRegistryPolicy(bytes32 registryId, address policy) external {
|
|
132
|
+
Registry storage r = _registries[registryId];
|
|
133
|
+
if (!r.exists) revert RegistryNotFound();
|
|
134
|
+
if (msg.sender != r.controller) revert NotController();
|
|
135
|
+
r.policy = policy;
|
|
136
|
+
emit RegistryPolicyUpdated(registryId, policy);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Entry lifecycle ────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/// @notice Register a facet of `subjectAgent`. RB-01: the subject SA must be the caller. RB-02: the
|
|
142
|
+
/// registry's membership hook (if any) must admit. Signature ABI matches registry-kit.
|
|
143
|
+
function registerEntry(
|
|
144
|
+
bytes32 registryId,
|
|
145
|
+
bytes32 entryId,
|
|
146
|
+
address subjectAgent,
|
|
147
|
+
bytes32 cardHash,
|
|
148
|
+
bytes32 bindingProofHash,
|
|
149
|
+
bytes32[] calldata claimHashes,
|
|
150
|
+
uint64 expiresAt
|
|
151
|
+
) external {
|
|
152
|
+
Registry storage r = _registries[registryId];
|
|
153
|
+
if (!r.exists) revert RegistryNotFound();
|
|
154
|
+
if (subjectAgent == address(0)) revert ZeroSubject();
|
|
155
|
+
if (msg.sender != subjectAgent) revert SubjectMismatch(); // RB-01
|
|
156
|
+
if (expiresAt != 0 && expiresAt <= block.timestamp) revert InvalidExpiry();
|
|
157
|
+
|
|
158
|
+
bytes32 key = _entryKey(registryId, entryId);
|
|
159
|
+
if (_entries[key].status != EntryStatus.None) revert EntryExists();
|
|
160
|
+
|
|
161
|
+
// RB-02 — admission gate, fail-closed: a false return OR a revert blocks registration.
|
|
162
|
+
address policy = r.policy;
|
|
163
|
+
if (policy != address(0)) {
|
|
164
|
+
try IRegistryMembershipPolicy(policy)
|
|
165
|
+
.canRegister(registryId, entryId, subjectAgent, cardHash, claimHashes) returns (
|
|
166
|
+
bool ok
|
|
167
|
+
) {
|
|
168
|
+
if (!ok) revert MembershipDenied();
|
|
169
|
+
} catch {
|
|
170
|
+
revert MembershipDenied();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_entries[key] = Entry({
|
|
175
|
+
subjectAgent: subjectAgent,
|
|
176
|
+
cardHash: cardHash,
|
|
177
|
+
bindingProofHash: bindingProofHash,
|
|
178
|
+
claimsRoot: keccak256(abi.encode(claimHashes)),
|
|
179
|
+
status: EntryStatus.Active,
|
|
180
|
+
registeredAtBucket: uint64(block.timestamp) / EPOCH_SECONDS,
|
|
181
|
+
expiresAt: expiresAt
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
emit RegistryEntryRegistered(registryId, entryId, subjectAgent, cardHash, bindingProofHash);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// @notice Extend an active entry's expiry. RB-01: subject SA only. RB-04: a revoked/suspended entry
|
|
188
|
+
/// cannot be renewed.
|
|
189
|
+
function renewEntry(bytes32 registryId, bytes32 entryId, uint64 expiresAt) external {
|
|
190
|
+
Entry storage e = _entries[_entryKey(registryId, entryId)];
|
|
191
|
+
if (e.status == EntryStatus.None) revert EntryNotFound();
|
|
192
|
+
if (msg.sender != e.subjectAgent) revert SubjectMismatch(); // RB-01
|
|
193
|
+
if (e.status != EntryStatus.Active) revert NotActive(); // RB-04 (Suspended/Revoked)
|
|
194
|
+
if (expiresAt != 0 && expiresAt <= block.timestamp) revert InvalidExpiry();
|
|
195
|
+
e.expiresAt = expiresAt;
|
|
196
|
+
emit RegistryEntryRenewed(registryId, entryId, expiresAt);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// @notice Suspend an active entry. RB-03: admin action (controller or policy-authorized) — NOT subject.
|
|
200
|
+
function suspendEntry(bytes32 registryId, bytes32 entryId, bytes32 reasonHash) external {
|
|
201
|
+
Entry storage e = _entries[_entryKey(registryId, entryId)];
|
|
202
|
+
if (e.status == EntryStatus.None) revert EntryNotFound();
|
|
203
|
+
_requireAdmin(registryId, entryId); // RB-03
|
|
204
|
+
if (e.status != EntryStatus.Active) revert NotActive();
|
|
205
|
+
e.status = EntryStatus.Suspended;
|
|
206
|
+
emit RegistryEntrySuspended(registryId, entryId, reasonHash);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// @notice Revoke an entry (terminal). The subject SA may leave; the controller / policy-admin may eject.
|
|
210
|
+
function revokeEntry(bytes32 registryId, bytes32 entryId, bytes32 reasonHash) external {
|
|
211
|
+
Entry storage e = _entries[_entryKey(registryId, entryId)];
|
|
212
|
+
if (e.status == EntryStatus.None) revert EntryNotFound();
|
|
213
|
+
if (msg.sender != e.subjectAgent) _requireAdmin(registryId, entryId); // subject (leave) or admin (eject)
|
|
214
|
+
if (e.status == EntryStatus.Revoked) revert AlreadyRevoked(); // RB-04
|
|
215
|
+
e.status = EntryStatus.Revoked;
|
|
216
|
+
emit RegistryEntryRevoked(registryId, entryId, reasonHash);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// @notice Set a per-slot claim hash. Subject self-service OR admin (issuer/registry-managed claims).
|
|
220
|
+
/// @dev Distinct from `claimsRoot` (the immutable snapshot the binding proof signed at registration):
|
|
221
|
+
/// this is the mutable, slot-keyed projection the indexer reads for discovery.
|
|
222
|
+
function updateClaim(bytes32 registryId, bytes32 entryId, bytes32 claimSlotId, bytes32 claimHash) external {
|
|
223
|
+
bytes32 key = _entryKey(registryId, entryId);
|
|
224
|
+
Entry storage e = _entries[key];
|
|
225
|
+
if (e.status == EntryStatus.None) revert EntryNotFound();
|
|
226
|
+
if (e.status == EntryStatus.Revoked) revert AlreadyRevoked(); // RB-04
|
|
227
|
+
if (msg.sender != e.subjectAgent) _requireAdmin(registryId, entryId);
|
|
228
|
+
_claims[key][claimSlotId] = claimHash;
|
|
229
|
+
emit RegistryClaimUpdated(registryId, entryId, claimSlotId, claimHash);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Views (RB-06: discovery reads use these, never log scans) ──────
|
|
233
|
+
|
|
234
|
+
function getRegistry(bytes32 registryId) external view returns (Registry memory) {
|
|
235
|
+
Registry memory r = _registries[registryId];
|
|
236
|
+
if (!r.exists) revert RegistryNotFound();
|
|
237
|
+
return r;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getEntry(bytes32 registryId, bytes32 entryId) external view returns (Entry memory) {
|
|
241
|
+
Entry memory e = _entries[_entryKey(registryId, entryId)];
|
|
242
|
+
if (e.status == EntryStatus.None) revert EntryNotFound();
|
|
243
|
+
return e;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// @notice Effective liveness: stored-Active AND not past `expiresAt`. A missing entry returns false
|
|
247
|
+
/// (empty is an answer, ADR-0013 — not a trigger for a fallback lookup).
|
|
248
|
+
function isActive(bytes32 registryId, bytes32 entryId) external view returns (bool) {
|
|
249
|
+
Entry memory e = _entries[_entryKey(registryId, entryId)];
|
|
250
|
+
if (e.status != EntryStatus.Active) return false;
|
|
251
|
+
if (e.expiresAt != 0 && block.timestamp > e.expiresAt) return false;
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// @notice The current hash for a claim slot (0 if unset). Slot semantics are defined by the registry.
|
|
256
|
+
function getClaim(bytes32 registryId, bytes32 entryId, bytes32 claimSlotId) external view returns (bytes32) {
|
|
257
|
+
return _claims[_entryKey(registryId, entryId)][claimSlotId];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── Internal ───────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
function _entryKey(bytes32 registryId, bytes32 entryId) internal pure returns (bytes32) {
|
|
263
|
+
return keccak256(abi.encodePacked(registryId, entryId));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// @dev RB-03 — controller passes; otherwise the pluggable policy may authorize the actor. Fail-closed:
|
|
267
|
+
/// no policy, a false return, or a revert all deny.
|
|
268
|
+
function _requireAdmin(bytes32 registryId, bytes32 entryId) internal view {
|
|
269
|
+
Registry storage r = _registries[registryId];
|
|
270
|
+
if (msg.sender == r.controller) return;
|
|
271
|
+
address policy = r.policy;
|
|
272
|
+
if (policy != address(0)) {
|
|
273
|
+
try IRegistryMembershipPolicy(policy).canAdminister(registryId, entryId, msg.sender) returns (bool ok) {
|
|
274
|
+
if (ok) return;
|
|
275
|
+
} catch {
|
|
276
|
+
// fail-closed: treat a reverting policy as denial
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
revert NotAuthorized();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title IRegistryMembershipPolicy
|
|
6
|
+
* @notice Pluggable membership/validation hook for {AgentRegistryBase} (spec 279 §4.2). It answers two
|
|
7
|
+
* yes/no questions and NOTHING else:
|
|
8
|
+
*
|
|
9
|
+
* - `canRegister` — may `subjectAgent` create `entryId` in `registryId` with these claims?
|
|
10
|
+
* - `canAdminister` — may a NON-subject `actor` suspend / revoke / update `entryId`?
|
|
11
|
+
*
|
|
12
|
+
* Implementations live OUTSIDE Ring 0 (issuer-attested, stake/bond, quorum-approved,
|
|
13
|
+
* domain-control-proof, skill-claim-proof, …) — they MUST NOT push vertical business logic into
|
|
14
|
+
* the registry base. Both methods are `view`, so {AgentRegistryBase} reaches them via STATICCALL
|
|
15
|
+
* (no state mutation, no reentrancy). A policy that returns `false` OR reverts is treated as a
|
|
16
|
+
* denial: the base fails closed (ADR-0013) — there is no second, weaker authorization path.
|
|
17
|
+
*/
|
|
18
|
+
interface IRegistryMembershipPolicy {
|
|
19
|
+
/// @return ok whether `subjectAgent` may register `entryId` in `registryId` with `cardHash`/`claimHashes`.
|
|
20
|
+
function canRegister(
|
|
21
|
+
bytes32 registryId,
|
|
22
|
+
bytes32 entryId,
|
|
23
|
+
address subjectAgent,
|
|
24
|
+
bytes32 cardHash,
|
|
25
|
+
bytes32[] calldata claimHashes
|
|
26
|
+
) external view returns (bool ok);
|
|
27
|
+
|
|
28
|
+
/// @return ok whether `actor` (not the entry's subject) may administer (suspend / revoke / update) the entry.
|
|
29
|
+
function canAdminister(bytes32 registryId, bytes32 entryId, address actor) external view returns (bool ok);
|
|
30
|
+
}
|