@agenticprimitives/contracts 0.1.0-alpha.2 → 1.0.0-alpha.10
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 +324 -47
- package/README.md +45 -10
- package/deployments-base-sepolia.json +36 -1
- package/deployments-registries-base-sepolia.json +1 -0
- package/dist/abi/AgentAccount.json +1670 -0
- package/dist/abi/AgentAccountFactory.json +477 -0
- package/dist/abi/AgentNameRegistry.json +51 -0
- package/dist/abi/AgenticGovernance.json +32 -0
- package/dist/abi/AgreementRegistry.json +427 -0
- package/dist/abi/AttestationRegistry.json +436 -0
- package/dist/abi/CallDataHashEnforcer.json +119 -0
- package/dist/abi/CustodyPolicy.json +1122 -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/IPaymentReceiptRegistry.json +50 -0
- package/dist/abi/MockUSDC.json +621 -0
- package/dist/abi/PaymentEnforcer.json +407 -0
- package/dist/abi/PaymentEscrow.json +299 -0
- package/dist/abi/PaymentReceiptRegistry.json +182 -0
- package/dist/abi/PermissionlessSubregistry.json +5 -0
- package/dist/abi/QuorumEnforcer.json +311 -0
- package/dist/abi/SignatureSlotRecovery.json +88 -0
- package/dist/abi/SkillDefinitionRegistry.json +491 -0
- package/dist/abi/SmartAgentPaymaster.json +873 -0
- package/dist/abi/UniversalSignatureValidator.json +65 -0
- package/dist/abi/index.d.ts +28 -0
- package/dist/abi/index.js +28 -0
- package/dist/deployments/base-sepolia.d.ts +41 -0
- package/dist/deployments/base-sepolia.js +39 -0
- package/dist/deployments/index.d.ts +9 -0
- package/dist/deployments/index.js +16 -0
- package/dist/deployments/registries-base-sepolia.d.ts +9 -0
- package/dist/deployments/registries-base-sepolia.js +7 -0
- package/package.json +28 -7
- package/src/AgentAccount.sol +191 -11
- package/src/AgentAccountFactory.sol +66 -12
- package/src/SmartAgentPaymaster.sol +186 -20
- package/src/UniversalSignatureValidator.sol +3 -0
- package/src/agency/DelegationManager.sol +170 -0
- package/src/agency/IDelegationManager.sol +38 -0
- package/src/agreement/AgreementRegistry.sol +331 -0
- package/src/attestation/AttestationRegistry.sol +384 -0
- package/src/custody/CustodyPolicy.sol +138 -8
- package/src/enforcers/AllowedMethodsEnforcer.sol +6 -0
- package/src/enforcers/AllowedTargetsEnforcer.sol +6 -0
- package/src/enforcers/CallDataHashEnforcer.sol +61 -0
- package/src/enforcers/PaymentEnforcer.sol +194 -0
- package/src/enforcers/QuorumEnforcer.sol +20 -0
- package/src/enforcers/TimestampEnforcer.sol +7 -0
- package/src/enforcers/ValueEnforcer.sol +6 -0
- package/src/geo/GeoFeatureRegistry.sol +197 -0
- package/src/governance/AgenticGovernance.sol +26 -4
- package/src/libraries/SignatureSlotRecovery.sol +8 -3
- package/src/libraries/WebAuthnLib.sol +13 -0
- package/src/mocks/MockUSDC.sol +152 -0
- package/src/naming/AgentNameRegistry.sol +52 -15
- package/src/naming/AgentNameUniversalResolver.sol +1 -1
- package/src/naming/PermissionlessSubregistry.sol +19 -2
- package/src/payments/PaymentEscrow.sol +137 -0
- package/src/payments/PaymentReceiptRegistry.sol +67 -0
- package/src/relationships/AgentRelationship.sol +8 -0
- package/src/skills/SkillDefinitionRegistry.sol +187 -0
- package/deployments-anvil.json +0 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "../agency/ICaveatEnforcer.sol";
|
|
5
|
+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
6
|
+
|
|
7
|
+
interface IPaymentReceiptRegistry {
|
|
8
|
+
function record(
|
|
9
|
+
bytes32 mandateId,
|
|
10
|
+
address payer,
|
|
11
|
+
address payee,
|
|
12
|
+
address asset,
|
|
13
|
+
uint256 amount,
|
|
14
|
+
bytes32 resourceHash,
|
|
15
|
+
bytes32 delegationHash,
|
|
16
|
+
bytes32 nonce
|
|
17
|
+
) external;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @title PaymentEnforcer
|
|
22
|
+
* @notice x402 pay-per-use caveat enforcer (spec 272, PAY-CON-1). Constrains a redeemed PAYMENT
|
|
23
|
+
* delegation to EXACTLY a capped `IERC20.transfer(treasury, amount)`, with a per-delegation
|
|
24
|
+
* aggregate spend cap, a windowed frequency cap, and a single-use nonce (on-chain replay
|
|
25
|
+
* guard). Fuses smart-agent's AllocationLimit (spend) + RateLimit (window) into one
|
|
26
|
+
* `beforeHook` so spend, frequency, and binding are checked ATOMICALLY — a charge that passes
|
|
27
|
+
* the rate limit but busts the budget reverts the whole redemption as a unit (spec §2 divergence).
|
|
28
|
+
*
|
|
29
|
+
* @dev terms = abi.encode(
|
|
30
|
+
* address treasury, address asset, uint256 maxAmountPerCharge,
|
|
31
|
+
* uint256 maxAggregate, uint32 maxRedemptionsPerWindow, uint32 windowSeconds) // 192 bytes
|
|
32
|
+
* args = abi.encode(bytes32 mandateId, bytes32 nonce, bytes32 resourceHash)
|
|
33
|
+
*
|
|
34
|
+
* State is keyed [delegator][delegationHash] so sibling delegations never cross-interfere
|
|
35
|
+
* (a redeemer can't bypass the cap by re-anchoring under a different delegation).
|
|
36
|
+
*
|
|
37
|
+
* Fail-closed (spec §9): wrong/zero asset or treasury, non-`transfer` calldata, wrong
|
|
38
|
+
* recipient/amount, ANY cap breach, or a reused nonce reverts and blocks the whole
|
|
39
|
+
* `DelegationManager.redeemDelegation`. The state SSTOREs are atomic with the underlying
|
|
40
|
+
* transfer — if Phase-2 execution reverts, the budget/nonce writes revert with it.
|
|
41
|
+
*
|
|
42
|
+
* `transfer`-only by construction (spec §163): `target == asset` rejects multicall/router
|
|
43
|
+
* targets; the 68-byte length + selector check rejects `transferFrom`/`approve`/extra-arg
|
|
44
|
+
* smuggling; `value == 0` rejects ETH; the DelegationManager's single (target,value,data)
|
|
45
|
+
* redemption shape rejects batch/delegatecall.
|
|
46
|
+
*/
|
|
47
|
+
contract PaymentEnforcer is ICaveatEnforcer {
|
|
48
|
+
bytes4 private constant TRANSFER_SELECTOR = IERC20.transfer.selector; // 0xa9059cbb
|
|
49
|
+
|
|
50
|
+
struct Budget {
|
|
51
|
+
uint256 spent; // cumulative against maxAggregate
|
|
52
|
+
uint64 windowStart; // start of the current frequency window
|
|
53
|
+
uint32 callsInWindow; // redemptions so far in the current window
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// delegator => delegationHash => budget/window state
|
|
57
|
+
mapping(address => mapping(bytes32 => Budget)) private _budgets;
|
|
58
|
+
/// delegator => delegationHash => nonce => consumed (on-chain replay guard)
|
|
59
|
+
mapping(address => mapping(bytes32 => mapping(bytes32 => bool))) private _nonceUsed;
|
|
60
|
+
|
|
61
|
+
/// @notice Trustless receipt sink (PAY-CON-4). Immutable; `address(0)` disables registry receipts
|
|
62
|
+
/// (the `PaymentCharged` event below is always the primary on-chain receipt).
|
|
63
|
+
IPaymentReceiptRegistry public immutable receiptRegistry;
|
|
64
|
+
|
|
65
|
+
event PaymentCharged(
|
|
66
|
+
address indexed delegator,
|
|
67
|
+
bytes32 indexed delegationHash,
|
|
68
|
+
bytes32 indexed mandateId,
|
|
69
|
+
address treasury,
|
|
70
|
+
address asset,
|
|
71
|
+
uint256 amount,
|
|
72
|
+
bytes32 nonce,
|
|
73
|
+
bytes32 resourceHash,
|
|
74
|
+
uint256 totalSpent,
|
|
75
|
+
uint32 callsInWindow
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
error InvalidTerms();
|
|
79
|
+
error ValueNotZero();
|
|
80
|
+
error AssetMismatch(address expectedAsset, address target);
|
|
81
|
+
error NotTransferCall();
|
|
82
|
+
error WrongRecipient(address treasury, address to);
|
|
83
|
+
error ChargeExceedsMax(uint256 amount, uint256 maxAmountPerCharge);
|
|
84
|
+
error AggregateExceeded(uint256 spent, uint256 maxAggregate, uint256 amount);
|
|
85
|
+
error FrequencyExceeded(uint32 callsInWindow, uint32 maxRedemptionsPerWindow);
|
|
86
|
+
error NonceReused(bytes32 nonce);
|
|
87
|
+
|
|
88
|
+
constructor(address receiptRegistry_) {
|
|
89
|
+
receiptRegistry = IPaymentReceiptRegistry(receiptRegistry_);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function beforeHook(
|
|
93
|
+
bytes calldata terms,
|
|
94
|
+
bytes calldata args,
|
|
95
|
+
bytes32 delegationHash,
|
|
96
|
+
address delegator,
|
|
97
|
+
address, /*redeemer*/
|
|
98
|
+
address target,
|
|
99
|
+
uint256 value,
|
|
100
|
+
bytes calldata callData
|
|
101
|
+
) external override {
|
|
102
|
+
// ── terms (immutable, set at delegation creation) ──
|
|
103
|
+
if (terms.length != 192) revert InvalidTerms();
|
|
104
|
+
(
|
|
105
|
+
address treasury,
|
|
106
|
+
address asset,
|
|
107
|
+
uint256 maxPerCharge,
|
|
108
|
+
uint256 maxAggregate,
|
|
109
|
+
uint32 maxPerWindow,
|
|
110
|
+
uint32 windowSeconds
|
|
111
|
+
) = abi.decode(terms, (address, address, uint256, uint256, uint32, uint32));
|
|
112
|
+
if (
|
|
113
|
+
treasury == address(0) || asset == address(0) || maxPerCharge == 0 || maxAggregate < maxPerCharge
|
|
114
|
+
|| maxPerWindow == 0 || windowSeconds == 0
|
|
115
|
+
) {
|
|
116
|
+
revert InvalidTerms();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── calldata MUST be exactly IERC20.transfer(treasury, amount): single call, no ETH ──
|
|
120
|
+
if (value != 0) revert ValueNotZero();
|
|
121
|
+
if (target != asset) revert AssetMismatch(asset, target);
|
|
122
|
+
if (callData.length != 68) revert NotTransferCall(); // 4 + 32 + 32; rejects transferFrom/approve/extra args
|
|
123
|
+
if (bytes4(callData[0:4]) != TRANSFER_SELECTOR) revert NotTransferCall();
|
|
124
|
+
(address to, uint256 amount) = abi.decode(callData[4:], (address, uint256));
|
|
125
|
+
if (to != treasury) revert WrongRecipient(treasury, to);
|
|
126
|
+
if (amount > maxPerCharge) revert ChargeExceedsMax(amount, maxPerCharge);
|
|
127
|
+
|
|
128
|
+
// ── replay: single-use nonce per delegation (on-chain idempotency, PAY-RAIL-4) ──
|
|
129
|
+
(bytes32 mandateId, bytes32 nonce, bytes32 resourceHash) = abi.decode(args, (bytes32, bytes32, bytes32));
|
|
130
|
+
if (_nonceUsed[delegator][delegationHash][nonce]) revert NonceReused(nonce);
|
|
131
|
+
_nonceUsed[delegator][delegationHash][nonce] = true;
|
|
132
|
+
|
|
133
|
+
Budget storage b = _budgets[delegator][delegationHash];
|
|
134
|
+
|
|
135
|
+
// ── aggregate spend cap (port AllocationLimitEnforcer) ──
|
|
136
|
+
uint256 nextSpent = b.spent + amount;
|
|
137
|
+
if (nextSpent > maxAggregate) revert AggregateExceeded(b.spent, maxAggregate, amount);
|
|
138
|
+
|
|
139
|
+
// ── windowed frequency cap; roll on expiry (port RateLimitEnforcer) ──
|
|
140
|
+
uint64 nowTs = uint64(block.timestamp);
|
|
141
|
+
if (b.windowStart == 0 || nowTs >= b.windowStart + windowSeconds) {
|
|
142
|
+
b.windowStart = nowTs;
|
|
143
|
+
b.callsInWindow = 0;
|
|
144
|
+
}
|
|
145
|
+
uint32 nextCalls = b.callsInWindow + 1;
|
|
146
|
+
if (nextCalls > maxPerWindow) revert FrequencyExceeded(b.callsInWindow, maxPerWindow);
|
|
147
|
+
|
|
148
|
+
b.spent = nextSpent;
|
|
149
|
+
b.callsInWindow = nextCalls;
|
|
150
|
+
|
|
151
|
+
emit PaymentCharged(
|
|
152
|
+
delegator, delegationHash, mandateId, treasury, asset, amount, nonce, resourceHash, nextSpent, nextCalls
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Trustless settlement receipt (PAY-CON-4). Reverting here reverts the whole redemption, so a
|
|
156
|
+
// settled charge always has a receipt and an unsettled one never does (atomic).
|
|
157
|
+
if (address(receiptRegistry) != address(0)) {
|
|
158
|
+
receiptRegistry.record(mandateId, delegator, treasury, asset, amount, resourceHash, delegationHash, nonce);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function afterHook(bytes calldata, bytes calldata, bytes32, address, address, address, uint256, bytes calldata)
|
|
163
|
+
external
|
|
164
|
+
pure
|
|
165
|
+
override
|
|
166
|
+
{}
|
|
167
|
+
|
|
168
|
+
// ── views (off-chain budget display + idempotency checks) ──
|
|
169
|
+
|
|
170
|
+
function getBudget(address delegator, bytes32 delegationHash)
|
|
171
|
+
external
|
|
172
|
+
view
|
|
173
|
+
returns (uint256 spent, uint64 windowStart, uint32 callsInWindow)
|
|
174
|
+
{
|
|
175
|
+
Budget storage b = _budgets[delegator][delegationHash];
|
|
176
|
+
return (b.spent, b.windowStart, b.callsInWindow);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isNonceUsed(address delegator, bytes32 delegationHash, bytes32 nonce) external view returns (bool) {
|
|
180
|
+
return _nonceUsed[delegator][delegationHash][nonce];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// @notice Encode the immutable caveat terms (mirrors `delegation.buildPaymentMandateCaveats`).
|
|
184
|
+
function encodeTerms(
|
|
185
|
+
address treasury,
|
|
186
|
+
address asset,
|
|
187
|
+
uint256 maxAmountPerCharge,
|
|
188
|
+
uint256 maxAggregate,
|
|
189
|
+
uint32 maxRedemptionsPerWindow,
|
|
190
|
+
uint32 windowSeconds
|
|
191
|
+
) external pure returns (bytes memory) {
|
|
192
|
+
return abi.encode(treasury, asset, maxAmountPerCharge, maxAggregate, maxRedemptionsPerWindow, windowSeconds);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -68,12 +68,23 @@ 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);
|
|
74
82
|
error UnauthorizedSigner(address signer);
|
|
75
83
|
error DuplicateOrUnsortedSigner(address signer);
|
|
76
84
|
error PayloadHashMismatch(bytes32 expected, bytes32 supplied);
|
|
85
|
+
/// @notice EN-11: a degenerate quorum caveat (threshold 0, or a threshold the
|
|
86
|
+
/// signer set can never satisfy) must fail closed, not pass with zero sigs.
|
|
87
|
+
error InvalidThreshold(uint8 threshold, uint256 signerSetSize);
|
|
77
88
|
|
|
78
89
|
/// @notice Typehash for the quorum-action binding (contract audit C-4).
|
|
79
90
|
/// @dev Bound to (chainId, enforcer, delegationHash, delegator, redeemer,
|
|
@@ -127,6 +138,15 @@ contract QuorumEnforcer is ICaveatEnforcer {
|
|
|
127
138
|
abi.decode(terms, (address[], uint8, address));
|
|
128
139
|
(bytes32 suppliedPayloadHash, bytes memory signatures) = abi.decode(args, (bytes32, bytes));
|
|
129
140
|
|
|
141
|
+
// EN-11: fail closed on a degenerate quorum. threshold == 0 would skip the
|
|
142
|
+
// verification loop entirely AND make `signatures.length < 0` unreachable, so
|
|
143
|
+
// the caveat would pass with ZERO signatures. A threshold larger than the
|
|
144
|
+
// signer set is unsatisfiable; reject it up front rather than running out of
|
|
145
|
+
// slots mid-loop.
|
|
146
|
+
if (threshold == 0 || threshold > signerSet.length) {
|
|
147
|
+
revert InvalidThreshold(threshold, signerSet.length);
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
// Contract audit C-4: bind the signed payload to the actual
|
|
131
151
|
// execution context. Previously the redeemer chose payloadHash
|
|
132
152
|
// freely, so a quorum signed off-chain to "approve a transfer
|
|
@@ -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,197 @@
|
|
|
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
|
+
latestVersion[p.featureId] = version;
|
|
125
|
+
if (prevVer == 0) _allFeatures.push(p.featureId);
|
|
126
|
+
// The heavy record write + event live in a helper so `publish`'s own stack stays shallow.
|
|
127
|
+
_storeAndEmit(p, version);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// @dev Writes the record FIELD-BY-FIELD to storage (no in-memory struct literal) and emits.
|
|
131
|
+
/// Behaviour is identical to the prior `_records[...] = FeatureRecord({...})` literal; the
|
|
132
|
+
/// split exists only to keep `publish` within solc's stack under `forge coverage --ir-minimum`
|
|
133
|
+
/// instrumentation (the 18-field memory literal + a dynamic `string` overflowed it by one slot).
|
|
134
|
+
function _storeAndEmit(PublishInput calldata p, uint64 version) internal {
|
|
135
|
+
FeatureRecord storage r = _records[p.featureId][version];
|
|
136
|
+
r.featureId = p.featureId;
|
|
137
|
+
r.version = version;
|
|
138
|
+
r.stewardAccount = p.stewardAccount;
|
|
139
|
+
r.featureKind = p.featureKind;
|
|
140
|
+
r.geometryHash = p.geometryHash;
|
|
141
|
+
r.coverageRoot = p.coverageRoot;
|
|
142
|
+
r.sourceSetRoot = p.sourceSetRoot;
|
|
143
|
+
r.metadataURI = p.metadataURI;
|
|
144
|
+
r.centroidLat = p.centroidLat;
|
|
145
|
+
r.centroidLon = p.centroidLon;
|
|
146
|
+
r.bboxMinLat = p.bboxMinLat;
|
|
147
|
+
r.bboxMinLon = p.bboxMinLon;
|
|
148
|
+
r.bboxMaxLat = p.bboxMaxLat;
|
|
149
|
+
r.bboxMaxLon = p.bboxMaxLon;
|
|
150
|
+
r.validAfter = p.validAfter;
|
|
151
|
+
r.validUntil = p.validUntil;
|
|
152
|
+
r.active = true;
|
|
153
|
+
r.registeredAt = uint64(block.timestamp);
|
|
154
|
+
emit FeaturePublished(p.featureId, version, p.stewardAccount, p.featureKind, p.geometryHash, p.coverageRoot, p.sourceSetRoot, p.metadataURI);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function deactivate(bytes32 featureId) external {
|
|
158
|
+
uint64 v = latestVersion[featureId];
|
|
159
|
+
if (v == 0) revert FeatureNotFound();
|
|
160
|
+
FeatureRecord storage r = _records[featureId][v];
|
|
161
|
+
if (msg.sender != r.stewardAccount) revert NotSteward();
|
|
162
|
+
r.active = false;
|
|
163
|
+
emit FeatureDeactivated(featureId, v);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function setValidity(bytes32 featureId, uint64 version, uint64 validAfter, uint64 validUntil) external {
|
|
167
|
+
FeatureRecord storage r = _records[featureId][version];
|
|
168
|
+
if (r.version == 0) revert FeatureNotFound();
|
|
169
|
+
if (msg.sender != r.stewardAccount) revert NotSteward();
|
|
170
|
+
r.validAfter = validAfter;
|
|
171
|
+
r.validUntil = validUntil;
|
|
172
|
+
emit FeatureValidityChanged(featureId, version, validAfter, validUntil);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Views ──────────────────────────────────────────────────────────
|
|
176
|
+
function getFeature(bytes32 featureId, uint64 version) external view returns (FeatureRecord memory) {
|
|
177
|
+
FeatureRecord memory r = _records[featureId][version];
|
|
178
|
+
if (r.version == 0) revert FeatureNotFound();
|
|
179
|
+
return r;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getLatest(bytes32 featureId) external view returns (FeatureRecord memory) {
|
|
183
|
+
uint64 v = latestVersion[featureId];
|
|
184
|
+
if (v == 0) revert FeatureNotFound();
|
|
185
|
+
return _records[featureId][v];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// @notice True if `version` of `featureId` exists (used by off-chain geo claim credentials to
|
|
189
|
+
/// confirm the pinned definition version they reference).
|
|
190
|
+
function exists(bytes32 featureId, uint64 version) external view returns (bool) {
|
|
191
|
+
return _records[featureId][version].version != 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function allFeatures() external view returns (bytes32[] memory) {
|
|
195
|
+
return _allFeatures;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -42,15 +42,21 @@ contract AgenticGovernance is IGovernanceView {
|
|
|
42
42
|
/// construction; rotate by redeploying.
|
|
43
43
|
address public immutable timelock;
|
|
44
44
|
|
|
45
|
-
/// @notice Fast-path emergency-pauser role. Can pause without delay
|
|
46
|
-
/// but CANNOT unpause (unpause requires the timelock).
|
|
47
|
-
address public immutable guardian;
|
|
48
|
-
|
|
49
45
|
// ─── State ────────────────────────────────────────────────────
|
|
50
46
|
|
|
51
47
|
bool private _paused;
|
|
52
48
|
mapping(address => bool) private _signers;
|
|
53
49
|
|
|
50
|
+
/// @notice Fast-path emergency-pauser role. Can pause without delay but
|
|
51
|
+
/// CANNOT unpause (unpause requires the timelock). GOV-1: this is a
|
|
52
|
+
/// STORAGE var (not immutable) so the timelock can ROTATE it — a
|
|
53
|
+
/// guardian whose key is compromised and grief-pauses the system can
|
|
54
|
+
/// be replaced WITHOUT a full governance redeploy, bounding the
|
|
55
|
+
/// re-pause DoS to one timelock window instead of "permanent". Set
|
|
56
|
+
/// (`setGuardian`) only by the timelock. Declared after the existing
|
|
57
|
+
/// storage vars so their slot assignments don't shift.
|
|
58
|
+
address public guardian;
|
|
59
|
+
|
|
54
60
|
// ─── Errors ───────────────────────────────────────────────────
|
|
55
61
|
|
|
56
62
|
error NotTimelock(address caller);
|
|
@@ -64,6 +70,8 @@ contract AgenticGovernance is IGovernanceView {
|
|
|
64
70
|
event Unpaused(address indexed by);
|
|
65
71
|
event SignerSet(address indexed who, bool isSigner);
|
|
66
72
|
event Executed(address indexed target, uint256 value, bytes data);
|
|
73
|
+
/// @notice GOV-1 — the guardian (fast-pause) role was rotated by the timelock.
|
|
74
|
+
event GuardianRotated(address indexed oldGuardian, address indexed newGuardian);
|
|
67
75
|
|
|
68
76
|
// ─── Constructor ──────────────────────────────────────────────
|
|
69
77
|
|
|
@@ -134,6 +142,20 @@ contract AgenticGovernance is IGovernanceView {
|
|
|
134
142
|
emit SignerSet(who, sig);
|
|
135
143
|
}
|
|
136
144
|
|
|
145
|
+
/// @notice GOV-1 — rotate the guardian (fast-pause) role. Timelock-only, so a
|
|
146
|
+
/// compromised guardian that is perpetually re-pausing the system can
|
|
147
|
+
/// be replaced after one timelock window WITHOUT a governance redeploy.
|
|
148
|
+
/// To retire the fast-pause capability entirely, rotate to a burn
|
|
149
|
+
/// address. Cannot be the zero address (would make `pause()` an
|
|
150
|
+
/// accidental no-op while looking live).
|
|
151
|
+
function setGuardian(address newGuardian) external {
|
|
152
|
+
if (msg.sender != timelock) revert NotTimelock(msg.sender);
|
|
153
|
+
if (newGuardian == address(0)) revert ZeroAddress();
|
|
154
|
+
address old = guardian;
|
|
155
|
+
guardian = newGuardian;
|
|
156
|
+
emit GuardianRotated(old, newGuardian);
|
|
157
|
+
}
|
|
158
|
+
|
|
137
159
|
// ─── Receive ETH ──────────────────────────────────────────────
|
|
138
160
|
|
|
139
161
|
receive() external payable {}
|
|
@@ -173,9 +173,14 @@ library SignatureSlotRecovery {
|
|
|
173
173
|
abi.decode(dyn, (uint256, uint256, bytes32, WebAuthnLib.Assertion));
|
|
174
174
|
address derived = address(uint160(uint256(keccak256(abi.encode(pubX, pubY)))));
|
|
175
175
|
if (derived != signer) revert PasskeyPubKeyMismatch(signer, derived);
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
176
|
+
// WA-2: require UV at the slot level. This path verifies custody-COUNCIL
|
|
177
|
+
// passkey signatures (CustodyPolicy quorum: trustees/custodians authorizing
|
|
178
|
+
// a recovery or admin change) — the highest-assurance signatures in the
|
|
179
|
+
// system. Consistent with the native ERC-1271 path (AgentAccount, R8.2):
|
|
180
|
+
// a UP-only assertion is rejected; browser ceremonies for custody signers
|
|
181
|
+
// MUST use `userVerification: 'required'`. A lower-assurance flow that needs
|
|
182
|
+
// UP-only must install a separate validator module that relaxes this.
|
|
183
|
+
if (!WebAuthnLib.verify(assertion, payloadHash, pubX, pubY, rpIdHash, true)) {
|
|
179
184
|
revert PasskeySigInvalid(signer);
|
|
180
185
|
}
|
|
181
186
|
} else if (v == 27 || v == 28) {
|
|
@@ -36,6 +36,13 @@ import "./P256Verifier.sol";
|
|
|
36
36
|
* PasskeyValidator (for external 1271-style verification).
|
|
37
37
|
*/
|
|
38
38
|
library WebAuthnLib {
|
|
39
|
+
/// @dev secp256r1 (P-256) curve order n ÷ 2 — the low-s malleability bound (WA-1).
|
|
40
|
+
/// RIP-7212 accepts BOTH (r, s) and (r, n−s) for the same message, so without
|
|
41
|
+
/// this bound a second valid signature always exists. Any path that treats a
|
|
42
|
+
/// signature as unique (quorum slot recovery, dedup) would be malleable.
|
|
43
|
+
uint256 internal constant P256_N_DIV_2 =
|
|
44
|
+
0x7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8;
|
|
45
|
+
|
|
39
46
|
/// @notice A WebAuthn-wrapped P-256 signature bundle.
|
|
40
47
|
struct Assertion {
|
|
41
48
|
bytes authenticatorData;
|
|
@@ -74,6 +81,12 @@ library WebAuthnLib {
|
|
|
74
81
|
if (!_checkClientData(assertion.clientDataJSON, assertion.typeIndex, assertion.challengeIndex, expectedChallengeHash)) {
|
|
75
82
|
return false;
|
|
76
83
|
}
|
|
84
|
+
// WA-1: enforce low-s. RIP-7212 verifies both (r, s) and (r, n−s); rejecting
|
|
85
|
+
// the high-s half makes the assertion signature canonical, closing P-256
|
|
86
|
+
// malleability for every caller (native ERC-1271 + custody-quorum slot recovery).
|
|
87
|
+
if (assertion.s > P256_N_DIV_2) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
77
90
|
bytes32 cdjHash = sha256(bytes(assertion.clientDataJSON));
|
|
78
91
|
bytes32 signingHash = sha256(abi.encodePacked(assertion.authenticatorData, cdjHash));
|
|
79
92
|
return P256Verifier.verify(signingHash, assertion.r, assertion.s, pubX, pubY);
|