@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.
Files changed (74) hide show
  1. package/AUDIT.md +324 -47
  2. package/README.md +45 -10
  3. package/deployments-base-sepolia.json +36 -1
  4. package/deployments-registries-base-sepolia.json +1 -0
  5. package/dist/abi/AgentAccount.json +1670 -0
  6. package/dist/abi/AgentAccountFactory.json +477 -0
  7. package/dist/abi/AgentNameRegistry.json +51 -0
  8. package/dist/abi/AgenticGovernance.json +32 -0
  9. package/dist/abi/AgreementRegistry.json +427 -0
  10. package/dist/abi/AttestationRegistry.json +436 -0
  11. package/dist/abi/CallDataHashEnforcer.json +119 -0
  12. package/dist/abi/CustodyPolicy.json +1122 -0
  13. package/dist/abi/DelegationManager.json +645 -0
  14. package/dist/abi/GeoFeatureRegistry.json +625 -0
  15. package/dist/abi/IAgentAccount.json +262 -0
  16. package/dist/abi/IAgentAccountFactoryAccessor.json +15 -0
  17. package/dist/abi/IAgentAccountFactoryView.json +41 -0
  18. package/dist/abi/IAgentAccountPauseView.json +15 -0
  19. package/dist/abi/IAgenticPrimitivesAgentAccount.json +15 -0
  20. package/dist/abi/IApprovedHashRegistryView.json +26 -0
  21. package/dist/abi/ICustodyPolicyFactoryView.json +15 -0
  22. package/dist/abi/ICustodyPolicyPauseView.json +15 -0
  23. package/dist/abi/IDelegationManager.json +171 -0
  24. package/dist/abi/IERC7579HookLike.json +44 -0
  25. package/dist/abi/IERC7579ModuleLike.json +28 -0
  26. package/dist/abi/IPaymentReceiptRegistry.json +50 -0
  27. package/dist/abi/MockUSDC.json +621 -0
  28. package/dist/abi/PaymentEnforcer.json +407 -0
  29. package/dist/abi/PaymentEscrow.json +299 -0
  30. package/dist/abi/PaymentReceiptRegistry.json +182 -0
  31. package/dist/abi/PermissionlessSubregistry.json +5 -0
  32. package/dist/abi/QuorumEnforcer.json +311 -0
  33. package/dist/abi/SignatureSlotRecovery.json +88 -0
  34. package/dist/abi/SkillDefinitionRegistry.json +491 -0
  35. package/dist/abi/SmartAgentPaymaster.json +873 -0
  36. package/dist/abi/UniversalSignatureValidator.json +65 -0
  37. package/dist/abi/index.d.ts +28 -0
  38. package/dist/abi/index.js +28 -0
  39. package/dist/deployments/base-sepolia.d.ts +41 -0
  40. package/dist/deployments/base-sepolia.js +39 -0
  41. package/dist/deployments/index.d.ts +9 -0
  42. package/dist/deployments/index.js +16 -0
  43. package/dist/deployments/registries-base-sepolia.d.ts +9 -0
  44. package/dist/deployments/registries-base-sepolia.js +7 -0
  45. package/package.json +28 -7
  46. package/src/AgentAccount.sol +191 -11
  47. package/src/AgentAccountFactory.sol +66 -12
  48. package/src/SmartAgentPaymaster.sol +186 -20
  49. package/src/UniversalSignatureValidator.sol +3 -0
  50. package/src/agency/DelegationManager.sol +170 -0
  51. package/src/agency/IDelegationManager.sol +38 -0
  52. package/src/agreement/AgreementRegistry.sol +331 -0
  53. package/src/attestation/AttestationRegistry.sol +384 -0
  54. package/src/custody/CustodyPolicy.sol +138 -8
  55. package/src/enforcers/AllowedMethodsEnforcer.sol +6 -0
  56. package/src/enforcers/AllowedTargetsEnforcer.sol +6 -0
  57. package/src/enforcers/CallDataHashEnforcer.sol +61 -0
  58. package/src/enforcers/PaymentEnforcer.sol +194 -0
  59. package/src/enforcers/QuorumEnforcer.sol +20 -0
  60. package/src/enforcers/TimestampEnforcer.sol +7 -0
  61. package/src/enforcers/ValueEnforcer.sol +6 -0
  62. package/src/geo/GeoFeatureRegistry.sol +197 -0
  63. package/src/governance/AgenticGovernance.sol +26 -4
  64. package/src/libraries/SignatureSlotRecovery.sol +8 -3
  65. package/src/libraries/WebAuthnLib.sol +13 -0
  66. package/src/mocks/MockUSDC.sol +152 -0
  67. package/src/naming/AgentNameRegistry.sol +52 -15
  68. package/src/naming/AgentNameUniversalResolver.sol +1 -1
  69. package/src/naming/PermissionlessSubregistry.sol +19 -2
  70. package/src/payments/PaymentEscrow.sol +137 -0
  71. package/src/payments/PaymentReceiptRegistry.sol +67 -0
  72. package/src/relationships/AgentRelationship.sol +8 -0
  73. package/src/skills/SkillDefinitionRegistry.sol +187 -0
  74. package/deployments-anvil.json +0 -1
@@ -0,0 +1,331 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "@openzeppelin/contracts/interfaces/IERC1271.sol";
5
+ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
6
+
7
+ /**
8
+ * @title AgreementRegistry
9
+ * @notice Commitment-only registry per spec 241 + ADR-0024 Decision 2.
10
+ *
11
+ * ─── Substrate role ───
12
+ *
13
+ * Layer 8 of the coordination spine — Agreement / Commitment. Stores the
14
+ * commitment HASH only (no agreement bodies, no party SAs). Bodies live in
15
+ * party Joint Vaults; party SAs surface only through joint assertions
16
+ * registered in the AttestationRegistry (which references our `commitmentHash`).
17
+ *
18
+ * ─── Key invariants (AR-*) ───
19
+ *
20
+ * AR-01 register() recomputes the commitment from supplied components
21
+ * AR-02 issuer EIP-712 attestation verified via ERC-1271
22
+ * AR-03 schema validated against (off-chain) ShapeRegistry
23
+ * AR-04 status state-machine: ACTIVE → COMPLETED | DISPUTED | REVOKED
24
+ * AR-05 bilateral signing required for ACTIVE→COMPLETED, ACTIVE→REVOKED
25
+ * AR-06 either-party for ACTIVE→DISPUTED
26
+ * AR-07 NO issuerRevoke / issuerStatusUpdate entrypoint — by static analysis
27
+ * AR-08 nullifier set prevents replay on status transitions
28
+ * AR-09 epoch-bucket timestamps; raw block.timestamp never stored
29
+ * AR-10 isAssertableCommitment view gateway for AttestationRegistry
30
+ * AR-11 party SA addresses NEVER appear in register() calldata
31
+ * AR-12 single on-chain row per agreement (D-37)
32
+ */
33
+ contract AgreementRegistry {
34
+ // ─── Constants ──────────────────────────────────────────────────────
35
+
36
+ uint64 public constant EPOCH_SECONDS = 3600;
37
+
38
+ uint8 public constant STATUS_NONE = 0;
39
+ uint8 public constant STATUS_ACTIVE = 1;
40
+ uint8 public constant STATUS_COMPLETED = 2;
41
+ uint8 public constant STATUS_DISPUTED = 3;
42
+ uint8 public constant STATUS_REVOKED = 4;
43
+
44
+ /// @dev RW1-3 (ADR-0027): the contract RECOMPUTES the transition digest the
45
+ /// parties sign from (agreementCommitment, toStatus, nullifier) — it no
46
+ /// longer trusts a caller-supplied `transitionStructHash`. One canonical
47
+ /// digest model; the TS side derives the same constant (cross-stack
48
+ /// typehash-equality gate). Plain struct hash (no domain separator),
49
+ /// matching the attestation registry's JOINT_CONSENT_TYPEHASH.
50
+ /// AGR-1 (audit 2026-06-10): also bind chainId + this contract so a transition signature
51
+ /// cannot be replayed on another chain or a redeployed registry.
52
+ bytes32 internal constant TRANSITION_TYPEHASH = keccak256(
53
+ "AgreementTransition(bytes32 agreementCommitment,uint8 toStatus,bytes32 nullifier,uint256 chainId,address verifyingContract)"
54
+ );
55
+
56
+ /// @dev SC-1 (audit 2026-06-09): the issuer attestation digest is RECOMPUTED on-chain from the
57
+ /// agreement's contents + chain + this contract — it is NOT a caller-supplied free-form hash.
58
+ /// Binds (agreementCommitment, schemaHash, issuer, chainId, verifyingContract) so a signature
59
+ /// lifted from any other context cannot back an attacker-chosen commitment. The TS side derives
60
+ /// the same constant (cross-stack typehash-equality gate).
61
+ bytes32 internal constant AGREEMENT_ISSUER_TYPEHASH = keccak256(
62
+ "AgreementIssuerAttestation(bytes32 agreementCommitment,bytes32 schemaHash,address issuer,uint256 chainId,address verifyingContract)"
63
+ );
64
+
65
+ // ─── Errors ─────────────────────────────────────────────────────────
66
+
67
+ error AlreadyRegistered();
68
+ error NotRegistered();
69
+ error InvalidStatus();
70
+ error InvalidIssuerSignature();
71
+ error InvalidTransitionSignature();
72
+ error NullifierUsed();
73
+ error CommitmentMismatch();
74
+ error SignerNotParty();
75
+
76
+ // ─── Storage ────────────────────────────────────────────────────────
77
+
78
+ struct AgreementRow {
79
+ bytes32 agreementCommitment; // canonical commitment hash
80
+ bytes32 schemaHash; // SHACL shape hash from ShapeRegistry
81
+ address issuer; // SA whose EIP-712 attestation backs the commitment
82
+ uint8 status;
83
+ uint64 createdEpochBucket;
84
+ uint64 lastTransitionEpochBucket;
85
+ }
86
+
87
+ /// @dev key = agreementCommitment hash
88
+ mapping(bytes32 => AgreementRow) private _rows;
89
+
90
+ /// @dev AR-08: nullifier set for replay protection on status transitions.
91
+ mapping(bytes32 => bool) private _nullifiers;
92
+
93
+ // ─── Requests ───────────────────────────────────────────────────────
94
+
95
+ /// @notice The components the contract recomputes the commitment from
96
+ /// (per spec 241 §3). Party SAs are intentionally NOT in this
97
+ /// payload — AR-11.
98
+ struct AgreementIssuancePayload {
99
+ bytes32 schemaHash;
100
+ address issuer;
101
+ /// @dev issuer's signature over the RECOMPUTED issuer-attestation digest (SC-1). The contract
102
+ /// derives the digest from the payload contents — no caller-supplied attestation hash.
103
+ bytes issuerSignature;
104
+ /// @dev The asserted canonical commitment hash; we verify recomputation.
105
+ bytes32 agreementCommitment;
106
+ /// @dev Component commitments for AR-01 recomputation:
107
+ /// keccak256(partySet, issuerCommitment, termsCommitment, scheduleCommitment, salt) == agreementCommitment
108
+ bytes32 partySetCommitment;
109
+ bytes32 issuerCommitment;
110
+ bytes32 termsCommitment;
111
+ bytes32 scheduleCommitment;
112
+ uint256 salt;
113
+ }
114
+
115
+ struct StatusUpdatePayload {
116
+ bytes32 agreementCommitment;
117
+ uint8 toStatus;
118
+ bytes32 nullifier;
119
+ /// @dev Signatures over the RECOMPUTED transition digest (RW1-3): 1 for
120
+ /// either-party (DISPUTED), 2 for bilateral (COMPLETED/REVOKED).
121
+ bytes signature1;
122
+ bytes signature2;
123
+ /// @dev Party SAs that signed. For DISPUTED: signer1 only. For COMPLETED/REVOKED: both.
124
+ address signer1;
125
+ address signer2;
126
+ /// @dev RW1-2 (ADR-0027): the agreement's parties + commitment components, REVEALED at
127
+ /// transition time so the contract can recompute the commitment and PROVE the signers
128
+ /// are the parties. (Registration stays commitment-only — AR-12; the transition is
129
+ /// already a public state change that names its signers.)
130
+ address party1;
131
+ address party2;
132
+ bytes32 issuerCommitment;
133
+ bytes32 termsCommitment;
134
+ bytes32 scheduleCommitment;
135
+ uint256 commitmentSalt;
136
+ }
137
+
138
+ // ─── Events ─────────────────────────────────────────────────────────
139
+
140
+ event AgreementRegistered(
141
+ bytes32 indexed agreementCommitment,
142
+ address indexed issuer,
143
+ bytes32 indexed schemaHash,
144
+ uint64 epochBucket
145
+ );
146
+
147
+ event StatusUpdated(
148
+ bytes32 indexed agreementCommitment,
149
+ uint8 indexed fromStatus,
150
+ uint8 indexed toStatus,
151
+ uint64 epochBucket
152
+ );
153
+
154
+ // ─── Public surface ─────────────────────────────────────────────────
155
+
156
+ /// @notice Register an agreement by its commitment hash.
157
+ function register(AgreementIssuancePayload calldata p) external returns (bytes32) {
158
+ // AR-01 — recompute commitment from components
159
+ bytes32 recomputed = keccak256(
160
+ abi.encode(p.partySetCommitment, p.issuerCommitment, p.termsCommitment, p.scheduleCommitment, p.salt)
161
+ );
162
+ if (recomputed != p.agreementCommitment) revert CommitmentMismatch();
163
+
164
+ if (_rows[p.agreementCommitment].agreementCommitment != bytes32(0)) {
165
+ revert AlreadyRegistered();
166
+ }
167
+
168
+ // AR-02 / SC-1 — RECOMPUTE the issuer-attestation digest from the agreement contents (+ chain +
169
+ // this contract) and verify the issuer signed THAT. A signature over an arbitrary digest from
170
+ // any other context can no longer back an attacker-chosen commitment.
171
+ bytes32 issuerDigest = keccak256(
172
+ abi.encode(
173
+ AGREEMENT_ISSUER_TYPEHASH,
174
+ p.agreementCommitment,
175
+ p.schemaHash,
176
+ p.issuer,
177
+ block.chainid,
178
+ address(this)
179
+ )
180
+ );
181
+ if (!_isValidSignatureBool(p.issuer, issuerDigest, p.issuerSignature)) {
182
+ revert InvalidIssuerSignature();
183
+ }
184
+
185
+ uint64 bucket = uint64(block.timestamp) / EPOCH_SECONDS;
186
+
187
+ _rows[p.agreementCommitment] = AgreementRow({
188
+ agreementCommitment: p.agreementCommitment,
189
+ schemaHash: p.schemaHash,
190
+ issuer: p.issuer,
191
+ status: STATUS_ACTIVE,
192
+ createdEpochBucket: bucket,
193
+ lastTransitionEpochBucket: bucket
194
+ });
195
+
196
+ emit AgreementRegistered(p.agreementCommitment, p.issuer, p.schemaHash, bucket);
197
+ return p.agreementCommitment;
198
+ }
199
+
200
+ /// @notice Update the agreement status. Signing requirements (AR-04..AR-06):
201
+ /// - DISPUTED: signer1 (either party) only
202
+ /// - COMPLETED / REVOKED: both signer1 + signer2 (bilateral)
203
+ /// RW1-2 (ADR-0027): the caller REVEALS the parties + commitment
204
+ /// components; the contract recomputes the commitment (register's
205
+ /// exact formula) and requires each signer to BE one of the two
206
+ /// parties. The on-chain row still stores NO party SAs (AR-11 covers
207
+ /// register() calldata only); the parties surface here because a
208
+ /// status transition is already a public state change that names its
209
+ /// signers. Replay protection comes from the nullifier. RW1-3: the
210
+ /// transition digest the parties sign is RECOMPUTED here from
211
+ /// (agreementCommitment, toStatus, nullifier) — not caller-supplied.
212
+ function updateStatus(StatusUpdatePayload calldata p) external {
213
+ AgreementRow storage row = _rows[p.agreementCommitment];
214
+ if (row.agreementCommitment == bytes32(0)) revert NotRegistered();
215
+
216
+ // Status transition validity (AR-04)
217
+ if (row.status != STATUS_ACTIVE && row.status != STATUS_DISPUTED) revert InvalidStatus();
218
+ if (
219
+ p.toStatus != STATUS_COMPLETED &&
220
+ p.toStatus != STATUS_DISPUTED &&
221
+ p.toStatus != STATUS_REVOKED
222
+ ) {
223
+ revert InvalidStatus();
224
+ }
225
+ // DISPUTED → COMPLETED / REVOKED needs bilateral.
226
+ // ACTIVE → DISPUTED: either-party allowed.
227
+ // ACTIVE → COMPLETED / REVOKED: bilateral.
228
+
229
+ // AR-08 — replay protection
230
+ if (_nullifiers[p.nullifier]) revert NullifierUsed();
231
+ _nullifiers[p.nullifier] = true;
232
+
233
+ // RW1-2 (ADR-0027): PROVE the signers are the agreement's parties. Recompute the agreement
234
+ // commitment from the REVEALED parties + components (register's exact formula) and require it
235
+ // to equal the row's commitment; then require each signer to BE a party. The caller-supplied
236
+ // signer set is no longer trusted (a nonzero commitment ref / arbitrary signer is not authority).
237
+ {
238
+ bytes32 partySet = keccak256(abi.encodePacked(p.party1, p.party2));
239
+ bytes32 recomputed = keccak256(
240
+ abi.encode(partySet, p.issuerCommitment, p.termsCommitment, p.scheduleCommitment, p.commitmentSalt)
241
+ );
242
+ if (recomputed != p.agreementCommitment) revert CommitmentMismatch();
243
+ }
244
+
245
+ // RW1-3 (ADR-0027): RECOMPUTE the transition digest the parties sign — do not trust a
246
+ // caller-supplied hash. The signed payload is canonically bound to (commitment, toStatus,
247
+ // nullifier) here, on chain.
248
+ bytes32 transitionDigest = keccak256(
249
+ abi.encode(TRANSITION_TYPEHASH, p.agreementCommitment, p.toStatus, p.nullifier, block.chainid, address(this))
250
+ );
251
+
252
+ bool bilateralRequired = (p.toStatus != STATUS_DISPUTED);
253
+
254
+ // signer1 must BE a party, and must have signed.
255
+ if (p.signer1 != p.party1 && p.signer1 != p.party2) revert SignerNotParty();
256
+ if (!_isValidSignatureBool(p.signer1, transitionDigest, p.signature1)) {
257
+ revert InvalidTransitionSignature();
258
+ }
259
+ if (bilateralRequired) {
260
+ // both DISTINCT parties must sign.
261
+ if (p.signer2 == address(0) || p.signer2 == p.signer1) revert InvalidTransitionSignature();
262
+ if (p.signer2 != p.party1 && p.signer2 != p.party2) revert SignerNotParty();
263
+ if (!_isValidSignatureBool(p.signer2, transitionDigest, p.signature2)) {
264
+ revert InvalidTransitionSignature();
265
+ }
266
+ }
267
+
268
+ uint8 fromStatus = row.status;
269
+ row.status = p.toStatus;
270
+ row.lastTransitionEpochBucket = uint64(block.timestamp) / EPOCH_SECONDS;
271
+
272
+ emit StatusUpdated(p.agreementCommitment, fromStatus, p.toStatus, row.lastTransitionEpochBucket);
273
+ }
274
+
275
+ /// @notice Gateway for spec 242 AttestationRegistry.assertJointAgreement.
276
+ /// Returns (true, "") if the commitment exists and is in a state
277
+ /// that permits public joint assertion (currently any non-NONE
278
+ /// status; revoked agreements can still be jointly disclosed as
279
+ /// historical record).
280
+ ///
281
+ /// @dev NOT a consent or validity oracle (audit 2026-06-10). A `true`
282
+ /// result means only that `agreementCommitment` is REGISTERED — it is
283
+ /// an opaque back-reference, NOT proof that any particular party
284
+ /// consented, that the agreement is currently valid, or that a given
285
+ /// credential is bound to it. Bilateral consent for a joint assertion
286
+ /// is established separately by `AttestationRegistry.assertJointAgreement`,
287
+ /// which recomputes both parties' consent signatures. Consumers MUST
288
+ /// NOT treat the boolean here as authorization.
289
+ function isAssertableCommitment(
290
+ bytes32 agreementCommitment,
291
+ address /* actor */
292
+ ) external view returns (bool ok, string memory reason) {
293
+ AgreementRow memory row = _rows[agreementCommitment];
294
+ if (row.agreementCommitment == bytes32(0)) return (false, "not-registered");
295
+ return (true, "");
296
+ }
297
+
298
+ /// @notice Get the on-chain row.
299
+ function getRecord(bytes32 agreementCommitment) external view returns (AgreementRow memory) {
300
+ AgreementRow memory row = _rows[agreementCommitment];
301
+ if (row.agreementCommitment == bytes32(0)) revert NotRegistered();
302
+ return row;
303
+ }
304
+
305
+ /// @notice Has a nullifier been consumed?
306
+ function isNullifierUsed(bytes32 n) external view returns (bool) {
307
+ return _nullifiers[n];
308
+ }
309
+
310
+ // ─── Internal ───────────────────────────────────────────────────────
311
+
312
+ function _isValidSignatureBool(
313
+ address signer,
314
+ bytes32 digest,
315
+ bytes memory signature
316
+ ) internal view returns (bool) {
317
+ if (signer.code.length > 0) {
318
+ try IERC1271(signer).isValidSignature(digest, signature) returns (bytes4 result) {
319
+ return result == IERC1271.isValidSignature.selector;
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+ // P2-2 (audit 2026-06-10): route EOA recovery through OpenZeppelin ECDSA.tryRecover instead of
325
+ // raw ecrecover — it rejects high-s (malleable) signatures and malformed lengths, matching the
326
+ // DelegationManager idiom so every signature-verifying contract shares one low-s-safe path.
327
+ bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest));
328
+ (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(ethHash, signature);
329
+ return err == ECDSA.RecoverError.NoError && recovered == signer;
330
+ }
331
+ }