@agenticprimitives/contracts 0.1.0-alpha.3 → 1.0.0-alpha.5
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 +304 -47
- package/README.md +19 -8
- package/deployments-base-sepolia.json +1 -0
- package/dist/abi/AgentAccount.json +1647 -0
- package/dist/abi/AgentAccountFactory.json +472 -0
- package/dist/abi/AgentNameRegistry.json +46 -0
- package/dist/abi/AgreementRegistry.json +432 -0
- package/dist/abi/AttestationRegistry.json +436 -0
- package/dist/abi/CallDataHashEnforcer.json +119 -0
- package/dist/abi/CustodyPolicy.json +1111 -0
- package/dist/abi/DelegationManager.json +645 -0
- package/dist/abi/GeoFeatureRegistry.json +625 -0
- package/dist/abi/IAgentAccount.json +262 -0
- package/dist/abi/IAgentAccountFactoryAccessor.json +15 -0
- package/dist/abi/IAgentAccountFactoryView.json +41 -0
- package/dist/abi/IAgentAccountPauseView.json +15 -0
- package/dist/abi/IAgenticPrimitivesAgentAccount.json +15 -0
- package/dist/abi/IApprovedHashRegistryView.json +26 -0
- package/dist/abi/ICustodyPolicyFactoryView.json +15 -0
- package/dist/abi/ICustodyPolicyPauseView.json +15 -0
- package/dist/abi/IDelegationManager.json +171 -0
- package/dist/abi/IERC7579HookLike.json +44 -0
- package/dist/abi/IERC7579ModuleLike.json +28 -0
- package/dist/abi/PermissionlessSubregistry.json +5 -0
- package/dist/abi/QuorumEnforcer.json +295 -0
- package/dist/abi/SignatureSlotRecovery.json +88 -0
- package/dist/abi/SkillDefinitionRegistry.json +491 -0
- package/dist/abi/SmartAgentPaymaster.json +664 -0
- package/dist/abi/UniversalSignatureValidator.json +65 -0
- package/dist/abi/index.d.ts +23 -0
- package/dist/abi/index.js +23 -0
- package/dist/deployments/base-sepolia.d.ts +37 -0
- package/dist/deployments/base-sepolia.js +35 -0
- package/dist/deployments/index.d.ts +8 -0
- package/dist/deployments/index.js +13 -0
- package/package.json +20 -3
- package/src/AgentAccount.sol +130 -9
- package/src/AgentAccountFactory.sol +34 -5
- package/src/SmartAgentPaymaster.sol +37 -8
- package/src/UniversalSignatureValidator.sol +3 -0
- package/src/agency/DelegationManager.sol +155 -0
- package/src/agency/IDelegationManager.sol +35 -0
- package/src/agreement/AgreementRegistry.sol +306 -0
- package/src/attestation/AttestationRegistry.sol +320 -0
- package/src/custody/CustodyPolicy.sol +79 -2
- package/src/enforcers/AllowedMethodsEnforcer.sol +6 -0
- package/src/enforcers/AllowedTargetsEnforcer.sol +6 -0
- package/src/enforcers/CallDataHashEnforcer.sol +61 -0
- package/src/enforcers/QuorumEnforcer.sol +8 -0
- package/src/enforcers/TimestampEnforcer.sol +7 -0
- package/src/enforcers/ValueEnforcer.sol +6 -0
- package/src/geo/GeoFeatureRegistry.sol +190 -0
- package/src/naming/AgentNameRegistry.sol +25 -13
- package/src/naming/AgentNameUniversalResolver.sol +1 -1
- package/src/naming/PermissionlessSubregistry.sol +19 -2
- package/src/relationships/AgentRelationship.sol +8 -0
- package/src/skills/SkillDefinitionRegistry.sol +181 -0
|
@@ -4,6 +4,22 @@ pragma solidity ^0.8.28;
|
|
|
4
4
|
import {IAgentAccount, AgentAccountRecoveryArgs, AgentAccountRecoveryPasskeyAdd} from "../IAgentAccount.sol";
|
|
5
5
|
import {SignatureSlotRecovery} from "../libraries/SignatureSlotRecovery.sol";
|
|
6
6
|
|
|
7
|
+
/// @dev R6.6 — minimal interface for the `account → factory → governance.isPaused()`
|
|
8
|
+
/// staticcall chain. Mirrors the same patterns introduced by R6.5
|
|
9
|
+
/// in `AgentAccount` (see `IAgentAccountFactoryView` /
|
|
10
|
+
/// `IAgentAccountPauseView` in `AgentAccount.sol`).
|
|
11
|
+
interface IAgentAccountFactoryAccessor {
|
|
12
|
+
function factory() external view returns (address);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ICustodyPolicyFactoryView {
|
|
16
|
+
function governance() external view returns (address);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ICustodyPolicyPauseView {
|
|
20
|
+
function isPaused() external view returns (bool);
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
/**
|
|
8
24
|
* @title CustodyPolicy
|
|
9
25
|
* @notice ERC-7579 module that owns the schedule / apply / cancel
|
|
@@ -125,6 +141,64 @@ contract CustodyPolicy {
|
|
|
125
141
|
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator();
|
|
126
142
|
}
|
|
127
143
|
|
|
144
|
+
// ─── R6.6 / CON-CustodyPolicy-005 — system-pause modifier ──────
|
|
145
|
+
//
|
|
146
|
+
// `whenAccountNotPaused(account)` reverts when
|
|
147
|
+
// `AgenticGovernance.isPaused() == true`, where governance is
|
|
148
|
+
// resolved per-call via the account's factory:
|
|
149
|
+
//
|
|
150
|
+
// account.factory() → factory.governance() → governance.isPaused()
|
|
151
|
+
//
|
|
152
|
+
// 3 staticcalls. Custody schedule/apply are rare; the gas cost
|
|
153
|
+
// is acceptable. Non-conforming hops (EOA factory, EOA governance,
|
|
154
|
+
// missing fn) treat the system as "not paused" so legacy / test
|
|
155
|
+
// deploys keep working — mirrors the R6.5 pattern in AgentAccount
|
|
156
|
+
// and `GovernanceManaged._pausedSafe()`.
|
|
157
|
+
//
|
|
158
|
+
// Applied to: `scheduleCustodyChange`, `applyCustodyChange`.
|
|
159
|
+
//
|
|
160
|
+
// Deliberately NOT applied to (recovery primitives that must
|
|
161
|
+
// work during an incident):
|
|
162
|
+
// - `cancelScheduledChange` — defensive de-escalation
|
|
163
|
+
// - `onUninstall` — removing attack surface
|
|
164
|
+
// - `onInstall` — gated upstream by paused
|
|
165
|
+
// `installModule` on AgentAccount (R6.5)
|
|
166
|
+
// and paused `createAgentAccount`
|
|
167
|
+
// on AgentAccountFactory
|
|
168
|
+
modifier whenAccountNotPaused(address account) {
|
|
169
|
+
if (_systemPausedFor(account)) revert SystemPaused();
|
|
170
|
+
_;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// @notice R6.6 — thrown when a `whenAccountNotPaused`-guarded
|
|
174
|
+
/// entrypoint is called while the governance pause flag
|
|
175
|
+
/// is set.
|
|
176
|
+
error SystemPaused();
|
|
177
|
+
|
|
178
|
+
function _systemPausedFor(address account) internal view returns (bool) {
|
|
179
|
+
if (account == address(0) || account.code.length == 0) return false;
|
|
180
|
+
// Hop 1: account → factory
|
|
181
|
+
(bool okF, bytes memory dataF) = account.staticcall(
|
|
182
|
+
abi.encodeWithSelector(IAgentAccountFactoryAccessor.factory.selector)
|
|
183
|
+
);
|
|
184
|
+
if (!okF || dataF.length < 32) return false;
|
|
185
|
+
address fac = abi.decode(dataF, (address));
|
|
186
|
+
if (fac == address(0) || fac.code.length == 0) return false;
|
|
187
|
+
// Hop 2: factory → governance
|
|
188
|
+
(bool okG, bytes memory dataG) = fac.staticcall(
|
|
189
|
+
abi.encodeWithSelector(ICustodyPolicyFactoryView.governance.selector)
|
|
190
|
+
);
|
|
191
|
+
if (!okG || dataG.length < 32) return false;
|
|
192
|
+
address gov = abi.decode(dataG, (address));
|
|
193
|
+
if (gov == address(0) || gov.code.length == 0) return false;
|
|
194
|
+
// Hop 3: governance → isPaused
|
|
195
|
+
(bool okP, bytes memory dataP) = gov.staticcall(
|
|
196
|
+
abi.encodeWithSelector(ICustodyPolicyPauseView.isPaused.selector)
|
|
197
|
+
);
|
|
198
|
+
if (!okP || dataP.length < 32) return false;
|
|
199
|
+
return abi.decode(dataP, (bool));
|
|
200
|
+
}
|
|
201
|
+
|
|
128
202
|
function _buildDomainSeparator() internal view returns (bytes32) {
|
|
129
203
|
return keccak256(
|
|
130
204
|
abi.encode(
|
|
@@ -335,7 +409,7 @@ contract CustodyPolicy {
|
|
|
335
409
|
CustodyAction action,
|
|
336
410
|
bytes calldata args,
|
|
337
411
|
bytes calldata quorumSigs
|
|
338
|
-
) external returns (uint256 changeId) {
|
|
412
|
+
) external whenAccountNotPaused(account) returns (uint256 changeId) {
|
|
339
413
|
Config storage c = _configs[account];
|
|
340
414
|
if (!c.installed) revert NotInstalledOn(account);
|
|
341
415
|
|
|
@@ -394,7 +468,10 @@ contract CustodyPolicy {
|
|
|
394
468
|
emit CustodyChangeScheduled(account, changeId, action, eta, msg.sender);
|
|
395
469
|
}
|
|
396
470
|
|
|
397
|
-
function applyCustodyChange(address account, uint256 changeId, bytes calldata quorumSigs)
|
|
471
|
+
function applyCustodyChange(address account, uint256 changeId, bytes calldata quorumSigs)
|
|
472
|
+
external
|
|
473
|
+
whenAccountNotPaused(account)
|
|
474
|
+
{
|
|
398
475
|
Config storage c = _configs[account];
|
|
399
476
|
if (!c.installed) revert NotInstalledOn(account);
|
|
400
477
|
|
|
@@ -8,6 +8,12 @@ import "../agency/ICaveatEnforcer.sol";
|
|
|
8
8
|
* @notice Restricts delegated calls to specific function selectors.
|
|
9
9
|
* @dev terms = abi.encode(bytes4[] allowedSelectors)
|
|
10
10
|
* Follows ERC-7710 / MetaMask DeleGator beforeHook/afterHook pattern.
|
|
11
|
+
*
|
|
12
|
+
* R6.7 — Stateless validator; no `whenNotPaused` modifier needed
|
|
13
|
+
* (hooks are `pure`, contract has zero storage). The DM-side pause
|
|
14
|
+
* check in `DelegationManager.redeemDelegation` gates all
|
|
15
|
+
* reachable enforcer dispatch. See `docs/audits/r6-contracts-recon-2026-05-31.md`
|
|
16
|
+
* § 4.4 + `test/EnforcerPauseInvariantR67.t.sol`.
|
|
11
17
|
*/
|
|
12
18
|
contract AllowedMethodsEnforcer is ICaveatEnforcer {
|
|
13
19
|
error MethodNotAllowed();
|
|
@@ -8,6 +8,12 @@ import "../agency/ICaveatEnforcer.sol";
|
|
|
8
8
|
* @notice Restricts delegated calls to a specific set of target contracts.
|
|
9
9
|
* @dev terms = abi.encode(address[] allowedTargets)
|
|
10
10
|
* Follows ERC-7710 / MetaMask DeleGator beforeHook/afterHook pattern.
|
|
11
|
+
*
|
|
12
|
+
* R6.7 — Stateless validator; no `whenNotPaused` modifier needed
|
|
13
|
+
* (hooks are `pure`, contract has zero storage). The DM-side pause
|
|
14
|
+
* check in `DelegationManager.redeemDelegation` gates all
|
|
15
|
+
* reachable enforcer dispatch. See `docs/audits/r6-contracts-recon-2026-05-31.md`
|
|
16
|
+
* § 4.4 + `test/EnforcerPauseInvariantR67.t.sol`.
|
|
11
17
|
*/
|
|
12
18
|
contract AllowedTargetsEnforcer is ICaveatEnforcer {
|
|
13
19
|
error TargetNotAllowed();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "../agency/ICaveatEnforcer.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title CallDataHashEnforcer
|
|
8
|
+
* @notice Pins a delegation to EXACTLY one calldata payload — the exact-call
|
|
9
|
+
* binding ADR-0027 / spec 249 (RW1-4b) needs for sensitive ops.
|
|
10
|
+
* @dev terms = abi.encode(bytes32 expectedHash), where
|
|
11
|
+
* `expectedHash == keccak256(callData)` for the single call this
|
|
12
|
+
* delegation authorizes. `beforeHook` recomputes `keccak256(callData)`
|
|
13
|
+
* and reverts on mismatch.
|
|
14
|
+
*
|
|
15
|
+
* Ported from smart-agent's `CallDataHashEnforcer` (the "P4 — sensitive
|
|
16
|
+
* ops require exact-call sub-delegation" doctrine). Combined with
|
|
17
|
+
* `AllowedTargetsEnforcer` (target) + a tight `TimestampEnforcer`
|
|
18
|
+
* window + `ValueEnforcer`, a consent sub-delegation can pin the precise
|
|
19
|
+
* call — not merely its selector — so a registry that checks
|
|
20
|
+
* `DelegationManager.verifyAuthorizationForCall(...)` binds consent to the
|
|
21
|
+
* exact assertion being stored (RW1-1).
|
|
22
|
+
*
|
|
23
|
+
* R6.7 — Stateless validator: hooks are `pure`, the contract has zero
|
|
24
|
+
* storage, so no `whenNotPaused` is needed; the DM-side pause check in
|
|
25
|
+
* `DelegationManager.redeemDelegation` gates all reachable enforcer
|
|
26
|
+
* dispatch, and the read-only `verifyAuthorizationForCall` evaluates it
|
|
27
|
+
* via `staticcall`.
|
|
28
|
+
*/
|
|
29
|
+
contract CallDataHashEnforcer is ICaveatEnforcer {
|
|
30
|
+
error BadTermsLength();
|
|
31
|
+
error CallDataMismatch(bytes32 expected, bytes32 actual);
|
|
32
|
+
|
|
33
|
+
function beforeHook(
|
|
34
|
+
bytes calldata terms,
|
|
35
|
+
bytes calldata,
|
|
36
|
+
bytes32,
|
|
37
|
+
address,
|
|
38
|
+
address,
|
|
39
|
+
address,
|
|
40
|
+
uint256,
|
|
41
|
+
bytes calldata callData
|
|
42
|
+
) external pure override {
|
|
43
|
+
if (terms.length != 32) revert BadTermsLength();
|
|
44
|
+
bytes32 expected = abi.decode(terms, (bytes32));
|
|
45
|
+
bytes32 actual = keccak256(callData);
|
|
46
|
+
if (expected != actual) revert CallDataMismatch(expected, actual);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function afterHook(
|
|
50
|
+
bytes calldata,
|
|
51
|
+
bytes calldata,
|
|
52
|
+
bytes32,
|
|
53
|
+
address,
|
|
54
|
+
address,
|
|
55
|
+
address,
|
|
56
|
+
uint256,
|
|
57
|
+
bytes calldata
|
|
58
|
+
) external pure override {
|
|
59
|
+
// No post-execution check needed.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -68,6 +68,14 @@ import "../libraries/SignatureSlotRecovery.sol";
|
|
|
68
68
|
* Only the first `threshold` slots are checked. Excess entries
|
|
69
69
|
* beyond the threshold are ignored — callers SHOULD NOT pad blobs
|
|
70
70
|
* unnecessarily as calldata cost scales linearly.
|
|
71
|
+
*
|
|
72
|
+
* R6.7 — Stateless validator; no `whenNotPaused` modifier needed.
|
|
73
|
+
* `beforeHook` is `external view` (reads `block.chainid`), `afterHook`
|
|
74
|
+
* is `external pure` (empty body); contract has zero storage
|
|
75
|
+
* variables. The DM-side pause check in
|
|
76
|
+
* `DelegationManager.redeemDelegation` gates all reachable enforcer
|
|
77
|
+
* dispatch. See `docs/audits/r6-contracts-recon-2026-05-31.md`
|
|
78
|
+
* § 4.4 + `test/EnforcerPauseInvariantR67.t.sol`.
|
|
71
79
|
*/
|
|
72
80
|
contract QuorumEnforcer is ICaveatEnforcer {
|
|
73
81
|
error InsufficientQuorum(uint256 supplied, uint8 threshold);
|
|
@@ -8,6 +8,13 @@ import "../agency/ICaveatEnforcer.sol";
|
|
|
8
8
|
* @notice Enforces a time window — delegation is only valid between two timestamps.
|
|
9
9
|
* @dev terms = abi.encode(uint256 validAfter, uint256 validUntil)
|
|
10
10
|
* Follows ERC-7710 / MetaMask DeleGator beforeHook/afterHook pattern.
|
|
11
|
+
*
|
|
12
|
+
* R6.7 — Stateless validator; no `whenNotPaused` modifier needed.
|
|
13
|
+
* `beforeHook` is `external view` (reads `block.timestamp`); contract
|
|
14
|
+
* has zero storage. The DM-side pause check in
|
|
15
|
+
* `DelegationManager.redeemDelegation` gates all reachable enforcer
|
|
16
|
+
* dispatch. See `docs/audits/r6-contracts-recon-2026-05-31.md`
|
|
17
|
+
* § 4.4 + `test/EnforcerPauseInvariantR67.t.sol`.
|
|
11
18
|
*/
|
|
12
19
|
contract TimestampEnforcer is ICaveatEnforcer {
|
|
13
20
|
error TimestampNotYetValid();
|
|
@@ -8,6 +8,12 @@ import "../agency/ICaveatEnforcer.sol";
|
|
|
8
8
|
* @notice Enforces a maximum ETH value per call.
|
|
9
9
|
* @dev terms = abi.encode(uint256 maxValue)
|
|
10
10
|
* Follows ERC-7710 / MetaMask DeleGator beforeHook/afterHook pattern.
|
|
11
|
+
*
|
|
12
|
+
* R6.7 — Stateless validator; no `whenNotPaused` modifier needed
|
|
13
|
+
* (hooks are `pure`, contract has zero storage). The DM-side pause
|
|
14
|
+
* check in `DelegationManager.redeemDelegation` gates all
|
|
15
|
+
* reachable enforcer dispatch. See `docs/audits/r6-contracts-recon-2026-05-31.md`
|
|
16
|
+
* § 4.4 + `test/EnforcerPauseInvariantR67.t.sol`.
|
|
11
17
|
*/
|
|
12
18
|
contract ValueEnforcer is ICaveatEnforcer {
|
|
13
19
|
error ValueExceedsLimit();
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title GeoFeatureRegistry
|
|
6
|
+
* @notice Versioned PUBLIC geo features — the definition layer of the
|
|
7
|
+
* definition-vs-claim model (spec 251). Exact geometry stays OFF chain
|
|
8
|
+
* behind `metadataURI`; on chain we anchor a `geometryHash`, coverage +
|
|
9
|
+
* source merkle roots, and a COARSE centroid/bbox (UI only).
|
|
10
|
+
*
|
|
11
|
+
* ─── Doctrine ───
|
|
12
|
+
*
|
|
13
|
+
* GFR-01 Keyed by Smart Agent ADDRESS (the `stewardAccount`); names are never
|
|
14
|
+
* authority (ADR-0010).
|
|
15
|
+
* GFR-02 Authorization is `msg.sender == stewardAccount` — the steward SA
|
|
16
|
+
* publishes through its own `execute()` path. NO `isOwner` staticcall.
|
|
17
|
+
* GFR-03 Monotonic per-`featureId` versioning; a boundary refresh is a new
|
|
18
|
+
* version (claims pin a version, so they don't retroactively re-target).
|
|
19
|
+
* GFR-04 `featureKind` is a C-box-anchored codelist value (ADR-0009 lockstep).
|
|
20
|
+
* GFR-05 Geo CLAIMS are NOT on chain — they are verifiable credentials that
|
|
21
|
+
* point to a `(featureId, version)` here (spec 251). No GeoClaimRegistry.
|
|
22
|
+
* GFR-06 NO domain/faith vocabulary (reusable substrate).
|
|
23
|
+
* GFR-07 NEUTRAL public geography ONLY. A feature is a generic public place
|
|
24
|
+
* (planet/region/country/admin area). It is NEVER tagged with — and its
|
|
25
|
+
* `metadataURI` NEVER carries — domain or operational data that could
|
|
26
|
+
* imply a private presence (e.g. "where a worker operates", access
|
|
27
|
+
* sensitivity, ministry context). Sensitivity classification is an
|
|
28
|
+
* OFF-chain app/domain policy applied OVER neutral features, never an
|
|
29
|
+
* on-chain feature attribute. All metadata MUST be sanitized.
|
|
30
|
+
* GFR-08 NO on-chain skill↔geo mapping. This registry holds geo features only
|
|
31
|
+
* and has ZERO reference to SkillDefinitionRegistry (or any skill id).
|
|
32
|
+
* Any "serves skill X in region Y" fact is an off-chain claim/credential.
|
|
33
|
+
*
|
|
34
|
+
* Ported from smart-agent `GeoFeatureRegistry`; diverges per spec 251 (AP auth,
|
|
35
|
+
* C-box-anchored kinds, no name binding in v1).
|
|
36
|
+
*/
|
|
37
|
+
contract GeoFeatureRegistry {
|
|
38
|
+
// ─── C-box feature-kind codelist (keccak of the cbox URIs) ──────────
|
|
39
|
+
// Concept URIs are declared in packages/ontology cbox/geo-vocabulary.ttl
|
|
40
|
+
// (scheme apg:geoKind); lockstep-gated against the ontology (ADR-0009).
|
|
41
|
+
bytes32 public constant KIND_PLANET = keccak256(bytes("https://agenticprimitives.dev/ns/geo#Planet"));
|
|
42
|
+
bytes32 public constant KIND_REGION = keccak256(bytes("https://agenticprimitives.dev/ns/geo#Region"));
|
|
43
|
+
bytes32 public constant KIND_COUNTRY = keccak256(bytes("https://agenticprimitives.dev/ns/geo#Country"));
|
|
44
|
+
bytes32 public constant KIND_ADMIN = keccak256(bytes("https://agenticprimitives.dev/ns/geo#AdminArea"));
|
|
45
|
+
bytes32 public constant KIND_CUSTOM = keccak256(bytes("https://agenticprimitives.dev/ns/geo#Custom"));
|
|
46
|
+
int256 public constant COORD_SCALE = 1e7;
|
|
47
|
+
|
|
48
|
+
// ─── Errors ─────────────────────────────────────────────────────────
|
|
49
|
+
error NotSteward();
|
|
50
|
+
error FeatureNotFound();
|
|
51
|
+
error EmptyMetadata();
|
|
52
|
+
error EmptyId();
|
|
53
|
+
|
|
54
|
+
// ─── Storage ────────────────────────────────────────────────────────
|
|
55
|
+
struct FeatureRecord {
|
|
56
|
+
bytes32 featureId;
|
|
57
|
+
uint64 version;
|
|
58
|
+
address stewardAccount;
|
|
59
|
+
bytes32 featureKind;
|
|
60
|
+
bytes32 geometryHash; // keccak of canonical GeoJSON (off chain)
|
|
61
|
+
bytes32 coverageRoot; // merkle root over covering cells (e.g. H3)
|
|
62
|
+
bytes32 sourceSetRoot; // merkle root over the source dataset
|
|
63
|
+
string metadataURI;
|
|
64
|
+
int256 centroidLat; // degrees × 1e7, coarse — UI only
|
|
65
|
+
int256 centroidLon;
|
|
66
|
+
int256 bboxMinLat;
|
|
67
|
+
int256 bboxMinLon;
|
|
68
|
+
int256 bboxMaxLat;
|
|
69
|
+
int256 bboxMaxLon;
|
|
70
|
+
uint64 validAfter;
|
|
71
|
+
uint64 validUntil;
|
|
72
|
+
bool active;
|
|
73
|
+
uint64 registeredAt;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
mapping(bytes32 => mapping(uint64 => FeatureRecord)) private _records;
|
|
77
|
+
mapping(bytes32 => uint64) public latestVersion;
|
|
78
|
+
bytes32[] private _allFeatures;
|
|
79
|
+
|
|
80
|
+
// ─── Events ─────────────────────────────────────────────────────────
|
|
81
|
+
event FeaturePublished(
|
|
82
|
+
bytes32 indexed featureId,
|
|
83
|
+
uint64 indexed version,
|
|
84
|
+
address indexed steward,
|
|
85
|
+
bytes32 featureKind,
|
|
86
|
+
bytes32 geometryHash,
|
|
87
|
+
bytes32 coverageRoot,
|
|
88
|
+
bytes32 sourceSetRoot,
|
|
89
|
+
string metadataURI
|
|
90
|
+
);
|
|
91
|
+
event FeatureDeactivated(bytes32 indexed featureId, uint64 version);
|
|
92
|
+
event FeatureValidityChanged(bytes32 indexed featureId, uint64 version, uint64 validAfter, uint64 validUntil);
|
|
93
|
+
|
|
94
|
+
// ─── Publish ────────────────────────────────────────────────────────
|
|
95
|
+
struct PublishInput {
|
|
96
|
+
bytes32 featureId;
|
|
97
|
+
bytes32 featureKind;
|
|
98
|
+
address stewardAccount;
|
|
99
|
+
bytes32 geometryHash;
|
|
100
|
+
bytes32 coverageRoot;
|
|
101
|
+
bytes32 sourceSetRoot;
|
|
102
|
+
string metadataURI;
|
|
103
|
+
int256 centroidLat;
|
|
104
|
+
int256 centroidLon;
|
|
105
|
+
int256 bboxMinLat;
|
|
106
|
+
int256 bboxMinLon;
|
|
107
|
+
int256 bboxMaxLat;
|
|
108
|
+
int256 bboxMaxLon;
|
|
109
|
+
uint64 validAfter;
|
|
110
|
+
uint64 validUntil;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function publish(PublishInput calldata p) external returns (uint64 version) {
|
|
114
|
+
if (p.featureId == bytes32(0)) revert EmptyId();
|
|
115
|
+
if (bytes(p.metadataURI).length == 0) revert EmptyMetadata();
|
|
116
|
+
if (msg.sender != p.stewardAccount) revert NotSteward();
|
|
117
|
+
|
|
118
|
+
uint64 prevVer = latestVersion[p.featureId];
|
|
119
|
+
if (prevVer != 0 && _records[p.featureId][prevVer].stewardAccount != p.stewardAccount) {
|
|
120
|
+
revert NotSteward();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
version = prevVer + 1;
|
|
124
|
+
_records[p.featureId][version] = FeatureRecord({
|
|
125
|
+
featureId: p.featureId,
|
|
126
|
+
version: version,
|
|
127
|
+
stewardAccount: p.stewardAccount,
|
|
128
|
+
featureKind: p.featureKind,
|
|
129
|
+
geometryHash: p.geometryHash,
|
|
130
|
+
coverageRoot: p.coverageRoot,
|
|
131
|
+
sourceSetRoot: p.sourceSetRoot,
|
|
132
|
+
metadataURI: p.metadataURI,
|
|
133
|
+
centroidLat: p.centroidLat,
|
|
134
|
+
centroidLon: p.centroidLon,
|
|
135
|
+
bboxMinLat: p.bboxMinLat,
|
|
136
|
+
bboxMinLon: p.bboxMinLon,
|
|
137
|
+
bboxMaxLat: p.bboxMaxLat,
|
|
138
|
+
bboxMaxLon: p.bboxMaxLon,
|
|
139
|
+
validAfter: p.validAfter,
|
|
140
|
+
validUntil: p.validUntil,
|
|
141
|
+
active: true,
|
|
142
|
+
registeredAt: uint64(block.timestamp)
|
|
143
|
+
});
|
|
144
|
+
latestVersion[p.featureId] = version;
|
|
145
|
+
if (prevVer == 0) _allFeatures.push(p.featureId);
|
|
146
|
+
|
|
147
|
+
emit FeaturePublished(p.featureId, version, p.stewardAccount, p.featureKind, p.geometryHash, p.coverageRoot, p.sourceSetRoot, p.metadataURI);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function deactivate(bytes32 featureId) external {
|
|
151
|
+
uint64 v = latestVersion[featureId];
|
|
152
|
+
if (v == 0) revert FeatureNotFound();
|
|
153
|
+
FeatureRecord storage r = _records[featureId][v];
|
|
154
|
+
if (msg.sender != r.stewardAccount) revert NotSteward();
|
|
155
|
+
r.active = false;
|
|
156
|
+
emit FeatureDeactivated(featureId, v);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function setValidity(bytes32 featureId, uint64 version, uint64 validAfter, uint64 validUntil) external {
|
|
160
|
+
FeatureRecord storage r = _records[featureId][version];
|
|
161
|
+
if (r.version == 0) revert FeatureNotFound();
|
|
162
|
+
if (msg.sender != r.stewardAccount) revert NotSteward();
|
|
163
|
+
r.validAfter = validAfter;
|
|
164
|
+
r.validUntil = validUntil;
|
|
165
|
+
emit FeatureValidityChanged(featureId, version, validAfter, validUntil);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Views ──────────────────────────────────────────────────────────
|
|
169
|
+
function getFeature(bytes32 featureId, uint64 version) external view returns (FeatureRecord memory) {
|
|
170
|
+
FeatureRecord memory r = _records[featureId][version];
|
|
171
|
+
if (r.version == 0) revert FeatureNotFound();
|
|
172
|
+
return r;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getLatest(bytes32 featureId) external view returns (FeatureRecord memory) {
|
|
176
|
+
uint64 v = latestVersion[featureId];
|
|
177
|
+
if (v == 0) revert FeatureNotFound();
|
|
178
|
+
return _records[featureId][v];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// @notice True if `version` of `featureId` exists (used by off-chain geo claim credentials to
|
|
182
|
+
/// confirm the pinned definition version they reference).
|
|
183
|
+
function exists(bytes32 featureId, uint64 version) external view returns (bool) {
|
|
184
|
+
return _records[featureId][version].version != 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function allFeatures() external view returns (bytes32[] memory) {
|
|
188
|
+
return _allFeatures;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.28;
|
|
3
3
|
|
|
4
|
+
import "../governance/GovernanceManaged.sol";
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* @title AgentNameRegistry
|
|
6
8
|
* @notice Hierarchical multi-root name registry for Smart Agents.
|
|
7
9
|
*
|
|
8
|
-
* Names are keyed by `node` (the
|
|
10
|
+
* Names are keyed by `node` (the recursive namehash:
|
|
9
11
|
* `keccak256(parentNode || labelhash)`). Each node has an owner (a Smart
|
|
10
12
|
* Agent address), a resolver, an optional subregistry delegate that may
|
|
11
13
|
* issue children, a parent pointer, the labelhash, and an optional
|
|
@@ -36,7 +38,11 @@ pragma solidity ^0.8.28;
|
|
|
36
38
|
* discipline (a primary name only counts when the forward record
|
|
37
39
|
* agrees).
|
|
38
40
|
*/
|
|
39
|
-
|
|
41
|
+
/// @dev R6.8 — inherits `GovernanceManaged` to gain the
|
|
42
|
+
/// `whenNotPaused` modifier on its mutating surface. The registry
|
|
43
|
+
/// still owns its own per-node auth (`_requireParentAuth` /
|
|
44
|
+
/// `msg.sender == owner`); governance only owns the pause flag.
|
|
45
|
+
contract AgentNameRegistry is GovernanceManaged {
|
|
40
46
|
// ─── H7-C.4 / CON-NAMING-001 — root-initializer gate ───────────────
|
|
41
47
|
//
|
|
42
48
|
// `initializeRoot` was permissionless: any mempool observer could
|
|
@@ -129,10 +135,13 @@ contract AgentNameRegistry {
|
|
|
129
135
|
|
|
130
136
|
// ─── Constructor (H7-C.4) ───────────────────────────────────────
|
|
131
137
|
|
|
132
|
-
/// @notice Set the immutable root-initializer
|
|
133
|
-
/// deploys the registry + calls
|
|
134
|
-
/// transaction so the TLD
|
|
135
|
-
|
|
138
|
+
/// @notice Set the immutable root-initializer + governance pointer.
|
|
139
|
+
/// The deploy script deploys the registry + calls
|
|
140
|
+
/// `initializeRoot` in the same transaction so the TLD
|
|
141
|
+
/// can't be frontrun. R6.8 added `governance_` so the
|
|
142
|
+
/// registry can read the system pause flag through
|
|
143
|
+
/// `GovernanceManaged._pausedSafe()`.
|
|
144
|
+
constructor(address initializer_, address governance_) GovernanceManaged(governance_) {
|
|
136
145
|
if (initializer_ == address(0)) revert ZeroOwner();
|
|
137
146
|
initializer = initializer_;
|
|
138
147
|
}
|
|
@@ -219,7 +228,7 @@ contract AgentNameRegistry {
|
|
|
219
228
|
address newOwner,
|
|
220
229
|
address resolverContract,
|
|
221
230
|
uint64 expiry
|
|
222
|
-
) external returns (bytes32 childNode) {
|
|
231
|
+
) external whenNotPaused returns (bytes32 childNode) {
|
|
223
232
|
if (bytes(label).length == 0) revert EmptyLabel();
|
|
224
233
|
if (newOwner == address(0)) revert ZeroOwner();
|
|
225
234
|
_requireParentAuth(parentNode);
|
|
@@ -252,7 +261,7 @@ contract AgentNameRegistry {
|
|
|
252
261
|
* Authorized to the node's owner ONLY — labels can't be
|
|
253
262
|
* changed once set, preventing display spoofing.
|
|
254
263
|
*/
|
|
255
|
-
function backfillLabel(bytes32 node, string calldata label_) external {
|
|
264
|
+
function backfillLabel(bytes32 node, string calldata label_) external whenNotPaused {
|
|
256
265
|
_requireNodeAuth(node);
|
|
257
266
|
if (bytes(label_).length == 0) revert EmptyLabel();
|
|
258
267
|
if (keccak256(bytes(label_)) != _records[node].labelhash) revert NotAuthorized();
|
|
@@ -262,26 +271,26 @@ contract AgentNameRegistry {
|
|
|
262
271
|
|
|
263
272
|
// ─── Setters ────────────────────────────────────────────────────
|
|
264
273
|
|
|
265
|
-
function setOwner(bytes32 node, address newOwner) external {
|
|
274
|
+
function setOwner(bytes32 node, address newOwner) external whenNotPaused {
|
|
266
275
|
_requireNodeAuth(node);
|
|
267
276
|
if (newOwner == address(0)) revert ZeroOwner();
|
|
268
277
|
_records[node].owner = newOwner;
|
|
269
278
|
emit OwnerChanged(node, newOwner);
|
|
270
279
|
}
|
|
271
280
|
|
|
272
|
-
function setResolver(bytes32 node, address resolverContract) external {
|
|
281
|
+
function setResolver(bytes32 node, address resolverContract) external whenNotPaused {
|
|
273
282
|
_requireNodeAuth(node);
|
|
274
283
|
_records[node].resolver = resolverContract;
|
|
275
284
|
emit ResolverChanged(node, resolverContract);
|
|
276
285
|
}
|
|
277
286
|
|
|
278
|
-
function setSubregistry(bytes32 node, address subregistryContract) external {
|
|
287
|
+
function setSubregistry(bytes32 node, address subregistryContract) external whenNotPaused {
|
|
279
288
|
_requireNodeAuth(node);
|
|
280
289
|
_records[node].subregistry = subregistryContract;
|
|
281
290
|
emit SubregistryChanged(node, subregistryContract);
|
|
282
291
|
}
|
|
283
292
|
|
|
284
|
-
function renew(bytes32 node, uint64 newExpiry) external {
|
|
293
|
+
function renew(bytes32 node, uint64 newExpiry) external whenNotPaused {
|
|
285
294
|
_requireNodeAuth(node);
|
|
286
295
|
_records[node].expiry = newExpiry;
|
|
287
296
|
emit NameRenewed(node, newExpiry);
|
|
@@ -293,7 +302,10 @@ contract AgentNameRegistry {
|
|
|
293
302
|
* NOT verify the forward record points back here. The
|
|
294
303
|
* universal resolver enforces the round-trip on reads.
|
|
295
304
|
*/
|
|
296
|
-
function setPrimaryName(bytes32 node) external {
|
|
305
|
+
function setPrimaryName(bytes32 node) external whenNotPaused {
|
|
306
|
+
// R6.3: `registeredAt == 0` is a sentinel "record-never-created" check.
|
|
307
|
+
// Storage default IS exactly 0; not a numeric precision concern.
|
|
308
|
+
// slither-disable-next-line incorrect-equality
|
|
297
309
|
if (node != bytes32(0) && _records[node].registeredAt == 0) revert NodeNotFound();
|
|
298
310
|
_primaryName[msg.sender] = node;
|
|
299
311
|
if (node == bytes32(0)) emit PrimaryNameCleared(msg.sender);
|
|
@@ -122,7 +122,7 @@ contract AgentNameUniversalResolver {
|
|
|
122
122
|
/**
|
|
123
123
|
* @notice Reverse-resolve a Smart Agent address to its primary name
|
|
124
124
|
* STRING in a single external call. Per spec/222 this is
|
|
125
|
-
* the
|
|
125
|
+
* the on-chain reverse path — no log walks, no name
|
|
126
126
|
* reconstruction in the SDK, no event indexer required.
|
|
127
127
|
*
|
|
128
128
|
* Walks `parent(node)` up the registry, reading `label(node)`
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
pragma solidity ^0.8.28;
|
|
3
3
|
|
|
4
4
|
import "./AgentNameRegistry.sol";
|
|
5
|
+
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @title PermissionlessSubregistry
|
|
@@ -36,7 +37,15 @@ import "./AgentNameRegistry.sol";
|
|
|
36
37
|
* through whatever authority `newOwner` represents (EOA wallet,
|
|
37
38
|
* PSA CustodyPolicy quorum, etc.).
|
|
38
39
|
*/
|
|
39
|
-
|
|
40
|
+
/// @dev R6.2 — `ReentrancyGuard` closes the Slither finding
|
|
41
|
+
/// `reentrancy-no-eth` on `register()`: the prior-claim check
|
|
42
|
+
/// (`claimedBy[msg.sender] != 0`) was followed by an external
|
|
43
|
+
/// call to `REGISTRY.register(...)` BEFORE the corresponding
|
|
44
|
+
/// state write. A malicious resolver invoked by the registry
|
|
45
|
+
/// could re-enter `register()` and pass the check a second time.
|
|
46
|
+
/// The guard prevents any nested call into `register()`, closing
|
|
47
|
+
/// the window. Recon doc § 4.1 / `docs/audits/r6-contracts-recon-2026-05-31.md`.
|
|
48
|
+
contract PermissionlessSubregistry is ReentrancyGuard {
|
|
40
49
|
AgentNameRegistry public immutable REGISTRY;
|
|
41
50
|
bytes32 public immutable PARENT_NODE;
|
|
42
51
|
address public immutable DEFAULT_RESOLVER;
|
|
@@ -77,12 +86,20 @@ contract PermissionlessSubregistry {
|
|
|
77
86
|
* fee — fee gating belongs in a different subregistry
|
|
78
87
|
* shape if needed.
|
|
79
88
|
*/
|
|
80
|
-
function register(string calldata label, address newOwner) external returns (bytes32 childNode) {
|
|
89
|
+
function register(string calldata label, address newOwner) external nonReentrant returns (bytes32 childNode) {
|
|
81
90
|
if (bytes(label).length == 0) revert EmptyLabel();
|
|
82
91
|
if (bytes(label).length < MIN_LABEL_LENGTH) revert LabelTooShort();
|
|
83
92
|
if (newOwner == address(0)) revert ZeroNewOwner();
|
|
84
93
|
bytes32 prior = claimedBy[msg.sender];
|
|
85
94
|
if (prior != bytes32(0)) revert AlreadyClaimed(prior);
|
|
95
|
+
// R9 / docs/audits/r9-static-analysis-triage.md § Slither M-1:
|
|
96
|
+
// Slither flags this as cross-function reentrancy because state is
|
|
97
|
+
// written AFTER the external call. The function is `nonReentrant`
|
|
98
|
+
// (line 89) + REGISTRY is an immutable contract we control
|
|
99
|
+
// (`AgentNameRegistry`) that does not call back into this contract.
|
|
100
|
+
// The cross-function reader (`hasClaimed`) is view-only. No invariant
|
|
101
|
+
// is violated by the pre-write-window. Triaged false positive.
|
|
102
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
86
103
|
childNode = REGISTRY.register(PARENT_NODE, label, newOwner, DEFAULT_RESOLVER, 0);
|
|
87
104
|
claimedBy[msg.sender] = childNode;
|
|
88
105
|
unchecked {
|
|
@@ -34,6 +34,13 @@ pragma solidity ^0.8.28;
|
|
|
34
34
|
* Off-chain JSON published at `metadataURI` MUST hash to
|
|
35
35
|
* `metadataHash`; consumers reject mismatched fetches.
|
|
36
36
|
*/
|
|
37
|
+
// R6.3 — Every `e.createdAt == 0` comparison in this contract is a
|
|
38
|
+
// sentinel "edge-never-created" check. Storage default IS exactly
|
|
39
|
+
// 0 for unset records, so the equality is semantically a
|
|
40
|
+
// "does-not-exist" probe — not a numeric-precision concern.
|
|
41
|
+
// Disabling Slither's `incorrect-equality` at the contract scope
|
|
42
|
+
// keeps these inline reads idiomatic.
|
|
43
|
+
// slither-disable-start incorrect-equality
|
|
37
44
|
contract AgentRelationship {
|
|
38
45
|
enum EdgeStatus {
|
|
39
46
|
NONE, // 0 — never set (sentinel)
|
|
@@ -287,3 +294,4 @@ contract AgentRelationship {
|
|
|
287
294
|
}
|
|
288
295
|
}
|
|
289
296
|
}
|
|
297
|
+
// slither-disable-end incorrect-equality
|