@agenticprimitives/contracts 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/AUDIT.md +67 -0
  2. package/CLAUDE.md +40 -0
  3. package/LICENSE +21 -0
  4. package/README.md +45 -0
  5. package/deployments-anvil.json +1 -0
  6. package/deployments-base-sepolia.json +1 -0
  7. package/dist/abi/AgentNameAttributeResolver.json +798 -0
  8. package/dist/abi/AgentNamePredicates.json +1 -0
  9. package/dist/abi/AgentNameRegistry.json +826 -0
  10. package/dist/abi/AgentNameUniversalResolver.json +222 -0
  11. package/dist/abi/AgentProfilePredicates.json +1 -0
  12. package/dist/abi/AgentProfileResolver.json +1044 -0
  13. package/dist/abi/AgentRelationship.json +583 -0
  14. package/dist/abi/AgentRelationshipPredicates.json +1 -0
  15. package/dist/abi/AgenticGovernance.json +259 -0
  16. package/dist/abi/AllowedMethodsEnforcer.json +108 -0
  17. package/dist/abi/AllowedTargetsEnforcer.json +103 -0
  18. package/dist/abi/ApprovedHashRegistry.json +114 -0
  19. package/dist/abi/AttributeStorage.json +557 -0
  20. package/dist/abi/CaveatEnforcerBase.json +130 -0
  21. package/dist/abi/GovernanceManaged.json +43 -0
  22. package/dist/abi/IAttributeReader.json +98 -0
  23. package/dist/abi/ICaveatEnforcer.json +98 -0
  24. package/dist/abi/IDelegationManager.json +211 -0
  25. package/dist/abi/IERC7579Module.json +34 -0
  26. package/dist/abi/IERC7579ModuleLifecycle.json +60 -0
  27. package/dist/abi/IGovernanceView.json +34 -0
  28. package/dist/abi/MultiSendCallOnly.json +29 -0
  29. package/dist/abi/MultiSendCallOnlyHarness.json +42 -0
  30. package/dist/abi/OntologyTermRegistry.json +397 -0
  31. package/dist/abi/P256Verifier.json +1 -0
  32. package/dist/abi/PermissionlessSubregistry.json +207 -0
  33. package/dist/abi/RelationshipTypeRegistry.json +455 -0
  34. package/dist/abi/ShapeRegistry.json +627 -0
  35. package/dist/abi/SmartAgentModuleTypes.json +1 -0
  36. package/dist/abi/TimestampEnforcer.json +108 -0
  37. package/dist/abi/ValueEnforcer.json +103 -0
  38. package/dist/abi/WebAuthnLib.json +1 -0
  39. package/dist/abi/index.d.ts +35 -0
  40. package/dist/abi/index.js +35 -0
  41. package/package.json +48 -0
  42. package/spec.md +52 -0
  43. package/src/AgentAccount.sol +1374 -0
  44. package/src/AgentAccountFactory.sol +274 -0
  45. package/src/ApprovedHashRegistry.sol +57 -0
  46. package/src/IAgentAccount.sol +138 -0
  47. package/src/SmartAgentPaymaster.sol +281 -0
  48. package/src/UniversalSignatureValidator.sol +136 -0
  49. package/src/agency/DelegationManager.sol +374 -0
  50. package/src/agency/ICaveatEnforcer.sol +62 -0
  51. package/src/agency/IDelegationManager.sol +69 -0
  52. package/src/custody/CustodyPolicy.sol +892 -0
  53. package/src/custody/IERC7579Module.sol +60 -0
  54. package/src/enforcers/AllowedMethodsEnforcer.AUDIT.md +51 -0
  55. package/src/enforcers/AllowedMethodsEnforcer.sol +48 -0
  56. package/src/enforcers/AllowedTargetsEnforcer.AUDIT.md +49 -0
  57. package/src/enforcers/AllowedTargetsEnforcer.sol +44 -0
  58. package/src/enforcers/CaveatEnforcerBase.sol +19 -0
  59. package/src/enforcers/QuorumEnforcer.AUDIT.md +71 -0
  60. package/src/enforcers/QuorumEnforcer.sol +191 -0
  61. package/src/enforcers/TimestampEnforcer.AUDIT.md +50 -0
  62. package/src/enforcers/TimestampEnforcer.sol +43 -0
  63. package/src/enforcers/ValueEnforcer.AUDIT.md +51 -0
  64. package/src/enforcers/ValueEnforcer.sol +41 -0
  65. package/src/governance/AgenticGovernance.sol +140 -0
  66. package/src/governance/GovernanceManaged.sol +75 -0
  67. package/src/governance/IGovernance.sol +15 -0
  68. package/src/identity/AgentProfilePredicates.sol +40 -0
  69. package/src/identity/AgentProfileResolver.sol +194 -0
  70. package/src/libraries/MultiSendCallOnly.sol +95 -0
  71. package/src/libraries/P256Verifier.sol +47 -0
  72. package/src/libraries/SignatureSlotRecovery.sol +196 -0
  73. package/src/libraries/WebAuthnLib.sol +164 -0
  74. package/src/naming/AgentNameAttributeResolver.sol +95 -0
  75. package/src/naming/AgentNamePredicates.sol +74 -0
  76. package/src/naming/AgentNameRegistry.sol +362 -0
  77. package/src/naming/AgentNameUniversalResolver.sol +210 -0
  78. package/src/naming/PermissionlessSubregistry.sol +98 -0
  79. package/src/ontology/AttributeStorage.sol +289 -0
  80. package/src/ontology/OntologyTermRegistry.sol +146 -0
  81. package/src/ontology/ShapeRegistry.sol +240 -0
  82. package/src/relationships/AgentRelationship.sol +289 -0
  83. package/src/relationships/AgentRelationshipPredicates.sol +44 -0
  84. package/src/relationships/RelationshipTypeRegistry.sol +143 -0
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /**
5
+ * @title P256Verifier
6
+ * @notice Dispatcher for P-256 (secp256r1) signature verification.
7
+ *
8
+ * **H7-C.2 / CON-P256-001 closure.** Previously this library
9
+ * silently fell through to the Daimo P256Verifier at the hardcoded
10
+ * address `0xc2b78104907F722DABAc4C69f826a522B2754De4` when the
11
+ * RIP-7212 precompile was absent. Two problems:
12
+ *
13
+ * 1. `try fast catch slow` pattern — direct ADR-0013 violation
14
+ * (one mechanism per security path; an attacker squatting that
15
+ * address on a fork made the on-chain verifier accept whatever
16
+ * the malicious contract returned).
17
+ * 2. Un-version-pinned third-party dependency — if Daimo's
18
+ * upgrade keys (or address) ever change, every account on
19
+ * every chain that fell back becomes compromised in lockstep.
20
+ *
21
+ * This library now uses **RIP-7212 only**. Chains without the
22
+ * precompile (e.g. pre-Pectra Ethereum mainnet) cannot use this
23
+ * verifier and MUST wire a separate, explicitly configured
24
+ * pure-Solidity P-256 verifier at the consumer layer. That config
25
+ * is intentionally NOT in this library — a hardcoded fallback in
26
+ * a security primitive is the exact pattern the audit rejected.
27
+ *
28
+ * Live deployments at the time of H7-C.2:
29
+ * - Base / Base Sepolia ✓ RIP-7212 native
30
+ * - Polygon zkEVM ✓
31
+ * - Optimism (post-Granite) ✓
32
+ * - Scroll, Linea ✓
33
+ * - Anvil (with --odyssey) ✓
34
+ * - Ethereum mainnet ✗ (until Pectra activates RIP-7212)
35
+ *
36
+ * Input layout: msgHash(32) || r(32) || s(32) || x(32) || y(32)
37
+ * Output: bool — true iff the signature verifies.
38
+ */
39
+ library P256Verifier {
40
+ address internal constant RIP7212_PRECOMPILE = address(0x100);
41
+
42
+ function verify(bytes32 hash, uint256 r, uint256 s, uint256 x, uint256 y) internal view returns (bool) {
43
+ bytes memory input = abi.encodePacked(hash, r, s, x, y);
44
+ (bool ok, bytes memory out) = RIP7212_PRECOMPILE.staticcall(input);
45
+ return ok && out.length >= 32 && uint256(bytes32(out)) == 1;
46
+ }
47
+ }
@@ -0,0 +1,196 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "../ApprovedHashRegistry.sol";
5
+ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
6
+ import {WebAuthnLib} from "./WebAuthnLib.sol";
7
+
8
+ /**
9
+ * @title SignatureSlotRecovery
10
+ * @notice Safe-compatible 65-byte signature-slot recovery, factored out
11
+ * of `QuorumEnforcer` so both the caveat path (via
12
+ * `QuorumEnforcer.beforeHook`) and the account-direct path
13
+ * (`AgentAccount.proposeAdmin` / `executeAdmin` / `cancelAdmin`)
14
+ * can verify quorum signatures without duplicating ~40 lines
15
+ * of ECDSA / ERC-1271 / approve-hash branching.
16
+ *
17
+ * @dev Per-slot layout (65 bytes):
18
+ * {32 r/data}{32 s/data}{1 v/type}
19
+ *
20
+ * v-byte type discrimination (Safe-compatible + AP extension):
21
+ * v == 27 || v == 28 → ECDSA over `payloadHash`
22
+ * v > 30 → eth_sign ECDSA (EIP-191 wrapped); v - 4 = recovery
23
+ * v == 1 → pre-approved hash via `approvedHashRegistry`;
24
+ * r holds signer (left-padded), s unused
25
+ * v == 0 → ERC-1271 contract sig; r holds signer
26
+ * (left-padded), s holds the byte offset into
27
+ * `signatures` to a length-prefixed sig tail
28
+ * v == 2 → WebAuthn passkey sig (agenticprimitives
29
+ * extension; NOT in Safe). r holds the
30
+ * Passkey-Identity-Address (PIA, derived
31
+ * from the P-256 pubkey via
32
+ * `address(uint160(uint256(keccak256(abi.encode(x, y)))))`).
33
+ * s holds the byte offset into `signatures`
34
+ * to a length-prefixed tail containing
35
+ * `abi.encode(uint256 x, uint256 y, bytes32 rpIdHash, WebAuthnLib.Assertion assertion)`.
36
+ * The `rpIdHash` MUST equal the rpIdHash this credential was
37
+ * registered against — verified by `WebAuthnLib.verify`. Added
38
+ * in H7-C.1 / CON-WEBAUTHN-001 closure.
39
+ * The library re-derives the PIA from (x, y)
40
+ * and asserts it matches r before running
41
+ * the WebAuthn verify against (x, y).
42
+ * Caller-side authorization is unchanged —
43
+ * CustodyPolicy still checks
44
+ * `account.isCustodian(pia)` after recovery.
45
+ *
46
+ * Library is `internal` so the bodies inline into each calling contract
47
+ * (no DELEGATECALL hop, no proxy address juggling, no extra storage
48
+ * lookup). Both callers pay the gas for one copy each but the
49
+ * maintenance burden of "two diverging recovers" is eliminated.
50
+ */
51
+ library SignatureSlotRecovery {
52
+ error InvalidSignature(uint8 v);
53
+ error ApprovedHashRequired(address signer);
54
+ error ContractSigInvalid(address signer);
55
+ error PasskeySigInvalid(address signer);
56
+ error PasskeyPubKeyMismatch(address claimed, address derived);
57
+ /// @notice H7-C.3 / CON-SIG-SLOT-001/-002 — a slot's tail (length + blob)
58
+ /// claims to extend past the end of the `signatures` array.
59
+ /// Without this check, the assembly load could read unallocated
60
+ /// memory and pass garbage to the downstream verifier.
61
+ error SigTailOutOfBounds(uint8 v, uint256 sigOffset, uint256 sigLen, uint256 totalLen);
62
+
63
+ bytes4 internal constant ERC1271_MAGIC = 0x1626ba7e;
64
+
65
+ /**
66
+ * @notice Recover the signer for the i-th 65-byte slot in
67
+ * `signatures`. Reverts with one of the library errors on
68
+ * malformed input or failed sub-checks; returns the signer
69
+ * address on success.
70
+ *
71
+ * @param payloadHash The hash the signer signed.
72
+ * @param signatures Packed sig blob (n * 65 bytes + optional
73
+ * ERC-1271 sig tails appended after).
74
+ * @param index Zero-based slot index to recover.
75
+ * @param approvedHashRegistry Address of the v=1 path's companion
76
+ * registry. Pass `address(0)` if the
77
+ * caller does not want to allow v=1 sigs
78
+ * (e.g. quorum-of-owners admin paths
79
+ * that don't accept pre-approved hashes).
80
+ */
81
+ function recoverFromSlot(
82
+ bytes32 payloadHash,
83
+ bytes memory signatures,
84
+ uint256 index,
85
+ address approvedHashRegistry
86
+ ) internal view returns (address signer) {
87
+ bytes32 r;
88
+ bytes32 s;
89
+ uint8 v;
90
+ uint256 offset = index * 65;
91
+ assembly {
92
+ let pos := add(signatures, add(0x20, offset))
93
+ r := mload(pos)
94
+ s := mload(add(pos, 0x20))
95
+ v := byte(0, mload(add(pos, 0x40)))
96
+ }
97
+
98
+ if (v == 0) {
99
+ // ERC-1271 contract signature. r holds the signer
100
+ // (left-padded); s holds the offset into `signatures` to a
101
+ // (length, blob) tail.
102
+ signer = address(uint160(uint256(r)));
103
+ uint256 sigOffset = uint256(s);
104
+ // H7-C.3 / CON-SIG-SLOT-001: bound the length-prefix read.
105
+ // sigOffset must leave room for the 32-byte length word.
106
+ if (sigOffset + 32 > signatures.length) {
107
+ revert SigTailOutOfBounds(v, sigOffset, 0, signatures.length);
108
+ }
109
+ uint256 sigLen;
110
+ assembly {
111
+ sigLen := mload(add(signatures, add(0x20, sigOffset)))
112
+ }
113
+ // H7-C.3: bound the tail-blob copy. The tail (length + blob) must
114
+ // fit fully within `signatures.length`; otherwise the assembly
115
+ // copy below reads past the buffer and feeds garbage to the
116
+ // verifier.
117
+ if (sigOffset + 32 + sigLen > signatures.length) {
118
+ revert SigTailOutOfBounds(v, sigOffset, sigLen, signatures.length);
119
+ }
120
+ bytes memory dyn = new bytes(sigLen);
121
+ assembly {
122
+ let src := add(signatures, add(0x40, sigOffset))
123
+ let dst := add(dyn, 0x20)
124
+ for { let j := 0 } lt(j, sigLen) { j := add(j, 0x20) } {
125
+ mstore(add(dst, j), mload(add(src, j)))
126
+ }
127
+ }
128
+ try IERC1271(signer).isValidSignature(payloadHash, dyn) returns (bytes4 magic) {
129
+ if (magic != ERC1271_MAGIC) revert ContractSigInvalid(signer);
130
+ } catch {
131
+ revert ContractSigInvalid(signer);
132
+ }
133
+ } else if (v == 1) {
134
+ // Pre-approved hash.
135
+ if (approvedHashRegistry == address(0)) revert InvalidSignature(v);
136
+ signer = address(uint160(uint256(r)));
137
+ if (!ApprovedHashRegistry(approvedHashRegistry).isApproved(signer, payloadHash)) {
138
+ revert ApprovedHashRequired(signer);
139
+ }
140
+ } else if (v == 2) {
141
+ // WebAuthn passkey slot. r holds the claimed PIA; tail
142
+ // holds (x, y, rpIdHash, Assertion). Verify the assertion
143
+ // against the supplied pubkey, then assert that the PIA
144
+ // derived from (x, y) matches the claim. Caller authorizes
145
+ // via `account.isCustodian(signer)`.
146
+ signer = address(uint160(uint256(r)));
147
+ uint256 sigOffset = uint256(s);
148
+ // H7-C.3 / CON-SIG-SLOT-002: bound the length-prefix read.
149
+ if (sigOffset + 32 > signatures.length) {
150
+ revert SigTailOutOfBounds(v, sigOffset, 0, signatures.length);
151
+ }
152
+ uint256 sigLen;
153
+ assembly {
154
+ sigLen := mload(add(signatures, add(0x20, sigOffset)))
155
+ }
156
+ // H7-C.3: bound the tail-blob copy.
157
+ if (sigOffset + 32 + sigLen > signatures.length) {
158
+ revert SigTailOutOfBounds(v, sigOffset, sigLen, signatures.length);
159
+ }
160
+ bytes memory dyn = new bytes(sigLen);
161
+ assembly {
162
+ let src := add(signatures, add(0x40, sigOffset))
163
+ let dst := add(dyn, 0x20)
164
+ for { let j := 0 } lt(j, sigLen) { j := add(j, 0x20) } {
165
+ mstore(add(dst, j), mload(add(src, j)))
166
+ }
167
+ }
168
+ // H7-C.1 / CON-WEBAUTHN-001: slot now carries rpIdHash so the
169
+ // verifier pins the RP the assertion was produced for.
170
+ // Slot encoding: (uint256 pubX, uint256 pubY, bytes32 rpIdHash,
171
+ // WebAuthnLib.Assertion assertion).
172
+ (uint256 pubX, uint256 pubY, bytes32 rpIdHash, WebAuthnLib.Assertion memory assertion) =
173
+ abi.decode(dyn, (uint256, uint256, bytes32, WebAuthnLib.Assertion));
174
+ address derived = address(uint160(uint256(keccak256(abi.encode(pubX, pubY)))));
175
+ if (derived != signer) revert PasskeyPubKeyMismatch(signer, derived);
176
+ // requireUv=false at the slot level; account policy can layer UV
177
+ // requirement via a future policy module.
178
+ if (!WebAuthnLib.verify(assertion, payloadHash, pubX, pubY, rpIdHash, false)) {
179
+ revert PasskeySigInvalid(signer);
180
+ }
181
+ } else if (v == 27 || v == 28) {
182
+ signer = ecrecover(payloadHash, v, r, s);
183
+ if (signer == address(0)) revert InvalidSignature(v);
184
+ } else if (v > 30) {
185
+ // eth_sign-wrapped: signer prefixed with the "Ethereum Signed Message"
186
+ // wrapper before signing. Subtract 4 from v to recover the original
187
+ // recovery byte.
188
+ bytes32 wrapped =
189
+ keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash));
190
+ signer = ecrecover(wrapped, v - 4, r, s);
191
+ if (signer == address(0)) revert InvalidSignature(v);
192
+ } else {
193
+ revert InvalidSignature(v);
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,164 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "./P256Verifier.sol";
5
+
6
+ /**
7
+ * @title WebAuthnLib
8
+ * @notice Pure-computation library for verifying a WebAuthn assertion against
9
+ * an expected challenge hash and a registered P-256 public key.
10
+ *
11
+ * Reconstructs the signing message:
12
+ * signingMessage = authenticatorData || sha256(clientDataJSON)
13
+ * signingHash = sha256(signingMessage)
14
+ *
15
+ * Checks (H7-C.1 / CON-WEBAUTHN-001 closure):
16
+ * - `authData[0..32]` (rpIdHash) EQUALS `expectedRpIdHash`
17
+ * — kills cross-RP signing-oracle attacks where an attacker controls
18
+ * a signing oracle at a different RP whose origin happens to be
19
+ * under their control. WITHOUT this check the on-chain verifier
20
+ * accepted any P-256 signature over `sha256(authData || sha256(cdj))`
21
+ * regardless of which RP produced it.
22
+ * - User-Present bit set (`authData[32] & 0x01 == 0x01`).
23
+ * - If `requireUv = true`, User-Verified bit set (`authData[32] & 0x04 == 0x04`).
24
+ * - `clientDataJSON` contains `"type":"webauthn.get"` at `typeIndex`.
25
+ * - `clientDataJSON` contains `"challenge":"<base64url(expectedHash)>"`
26
+ * at `challengeIndex`.
27
+ * - `P256Verifier.verify(signingHash, r, s, x, y)` returns true.
28
+ *
29
+ * The `clientDataJSON.origin` allowlist is NOT pinned here. Origin
30
+ * semantics depend on multi-frontend account policy and live in the
31
+ * account's stored policy if it adopts an allowlist — verifiers that
32
+ * want origin-pinning supply a pre-hashed origin via the consumer's
33
+ * own logic.
34
+ *
35
+ * Used by both AgentAccount (native passkey path) and the standalone
36
+ * PasskeyValidator (for external 1271-style verification).
37
+ */
38
+ library WebAuthnLib {
39
+ /// @notice A WebAuthn-wrapped P-256 signature bundle.
40
+ struct Assertion {
41
+ bytes authenticatorData;
42
+ string clientDataJSON;
43
+ uint256 challengeIndex;
44
+ uint256 typeIndex;
45
+ uint256 r;
46
+ uint256 s;
47
+ bytes32 credentialIdDigest;
48
+ }
49
+
50
+ /// @notice Verify that `assertion` is a valid WebAuthn signature over
51
+ /// `expectedChallengeHash` produced by `(pubX, pubY)`, AND was
52
+ /// produced for the RP whose ID hashes to `expectedRpIdHash`,
53
+ /// AND has the User-Present (and optionally User-Verified) bits set.
54
+ ///
55
+ /// @param assertion The decoded WebAuthn assertion bundle.
56
+ /// @param expectedChallengeHash The 32-byte challenge the assertion must sign.
57
+ /// @param pubX, pubY The registered P-256 public key (uncompressed).
58
+ /// @param expectedRpIdHash `sha256(rpId)` of the RP the credential was
59
+ /// registered against. Pinning this kills the
60
+ /// cross-RP signing-oracle vector.
61
+ /// @param requireUv Whether the User-Verified bit must be set
62
+ /// (the account's policy choice).
63
+ function verify(
64
+ Assertion memory assertion,
65
+ bytes32 expectedChallengeHash,
66
+ uint256 pubX,
67
+ uint256 pubY,
68
+ bytes32 expectedRpIdHash,
69
+ bool requireUv
70
+ ) internal view returns (bool) {
71
+ if (!_checkAuthData(assertion.authenticatorData, expectedRpIdHash, requireUv)) {
72
+ return false;
73
+ }
74
+ if (!_checkClientData(assertion.clientDataJSON, assertion.typeIndex, assertion.challengeIndex, expectedChallengeHash)) {
75
+ return false;
76
+ }
77
+ bytes32 cdjHash = sha256(bytes(assertion.clientDataJSON));
78
+ bytes32 signingHash = sha256(abi.encodePacked(assertion.authenticatorData, cdjHash));
79
+ return P256Verifier.verify(signingHash, assertion.r, assertion.s, pubX, pubY);
80
+ }
81
+
82
+ /// @dev Validates the authenticatorData header per WebAuthn spec §6.1:
83
+ /// bytes [0..32] = rpIdHash
84
+ /// byte [32] = flags (bit 0 UP, bit 2 UV, bit 6 AT, bit 7 ED)
85
+ /// bytes [33..36] = signCount (big-endian u32; informational, not checked here)
86
+ function _checkAuthData(
87
+ bytes memory authData,
88
+ bytes32 expectedRpIdHash,
89
+ bool requireUv
90
+ ) private pure returns (bool) {
91
+ if (authData.length < 37) return false;
92
+ // Compare 32-byte rpIdHash prefix.
93
+ bytes32 actualRpIdHash;
94
+ assembly {
95
+ // authData is `bytes`; first 32 bytes after the length word are the rpIdHash.
96
+ actualRpIdHash := mload(add(authData, 0x20))
97
+ }
98
+ if (actualRpIdHash != expectedRpIdHash) return false;
99
+ uint8 flags = uint8(authData[32]);
100
+ if (flags & 0x01 == 0) return false; // UP not set
101
+ if (requireUv && flags & 0x04 == 0) return false; // UV required but not set
102
+ return true;
103
+ }
104
+
105
+ /// @dev Validates the two structural invariants of clientDataJSON:
106
+ /// (1) starts with `"type":"webauthn.get"` at typeIndex
107
+ /// (2) has `"challenge":"<base64url(hash)>"` at challengeIndex.
108
+ function _checkClientData(
109
+ string memory cdj,
110
+ uint256 typeIndex,
111
+ uint256 challengeIndex,
112
+ bytes32 hash
113
+ ) private pure returns (bool) {
114
+ bytes memory buf = bytes(cdj);
115
+ bytes memory typeExpected = bytes('"type":"webauthn.get"');
116
+ if (typeIndex + typeExpected.length > buf.length) return false;
117
+ for (uint256 i; i < typeExpected.length; i++) {
118
+ if (buf[typeIndex + i] != typeExpected[i]) return false;
119
+ }
120
+ bytes memory challengePrefix = bytes('"challenge":"');
121
+ if (challengeIndex + challengePrefix.length + 43 + 1 > buf.length) return false;
122
+ for (uint256 i; i < challengePrefix.length; i++) {
123
+ if (buf[challengeIndex + i] != challengePrefix[i]) return false;
124
+ }
125
+ if (buf[challengeIndex + challengePrefix.length + 43] != bytes1('"')) return false;
126
+
127
+ // Decode the 43 base64url chars that follow the prefix and compare to `hash`.
128
+ return _base64UrlEqualsHash(buf, challengeIndex + challengePrefix.length, hash);
129
+ }
130
+
131
+ /// @dev Read 43 base64url chars starting at `offset` in `buf`, decode, and
132
+ /// assert the 32 decoded bytes equal `hash`.
133
+ function _base64UrlEqualsHash(bytes memory buf, uint256 offset, bytes32 hash) private pure returns (bool) {
134
+ uint256 acc;
135
+ uint256 bits;
136
+ uint256 outIdx;
137
+ bytes memory decoded = new bytes(32);
138
+ for (uint256 i; i < 43; i++) {
139
+ int256 v = _b64UrlCharVal(buf[offset + i]);
140
+ if (v < 0) return false;
141
+ acc = (acc << 6) | uint256(v);
142
+ bits += 6;
143
+ while (bits >= 8 && outIdx < 32) {
144
+ bits -= 8;
145
+ decoded[outIdx++] = bytes1(uint8((acc >> bits) & 0xff));
146
+ }
147
+ }
148
+ if (outIdx != 32) return false;
149
+ for (uint256 i; i < 32; i++) {
150
+ if (decoded[i] != hash[i]) return false;
151
+ }
152
+ return true;
153
+ }
154
+
155
+ function _b64UrlCharVal(bytes1 c) private pure returns (int256) {
156
+ uint8 b = uint8(c);
157
+ if (b >= 0x41 && b <= 0x5a) return int256(uint256(b - 0x41));
158
+ if (b >= 0x61 && b <= 0x7a) return int256(uint256(b - 0x61 + 26));
159
+ if (b >= 0x30 && b <= 0x39) return int256(uint256(b - 0x30 + 52));
160
+ if (b == 0x2d) return 62;
161
+ if (b == 0x5f) return 63;
162
+ return -1;
163
+ }
164
+ }
@@ -0,0 +1,95 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "./AgentNameRegistry.sol";
5
+ import "../ontology/AttributeStorage.sol";
6
+
7
+ /**
8
+ * @title AgentNameAttributeResolver
9
+ * @notice Per-node records resolver for the agent-naming registry.
10
+ *
11
+ * Inherits `AttributeStorage` so every (node, predicate) write is
12
+ * validated against the bound `OntologyTermRegistry` — predicates MUST
13
+ * be registered + active before they can be stored. The subject is the
14
+ * namehash node (already `bytes32`, no conversion). The on-chain
15
+ * `AgentName` shape (defined in `ShapeRegistry`) governs which
16
+ * predicates / datatypes / cardinalities are well-formed for a name.
17
+ *
18
+ * Authorization: `msg.sender == REGISTRY.owner(node)`. The owner Smart
19
+ * Agent's CustodyPolicy module gates the call upstream — this contract
20
+ * trusts `msg.sender` to be the gated entity per spec 215 § Phase 3.
21
+ *
22
+ * Per-spec simplifications kept from the pure key/value port:
23
+ * - No multi-coin (ENSIP-9) address records. Other-chain identifiers
24
+ * ride on the `atl:nativeId` predicate (a CAIP-10 string).
25
+ * - No aliases, versioning, or operator approvals. Rotation flows
26
+ * through the registry's `setOwner` (which changes who may write).
27
+ *
28
+ * SDK contract: `agenticprimitives/agent-naming/records` exposes
29
+ * the same predicate ids and routes encode / decode through the typed
30
+ * setters here. See `AgentNamePredicates.sol` for the canonical set.
31
+ */
32
+ contract AgentNameAttributeResolver is AttributeStorage {
33
+ AgentNameRegistry public immutable REGISTRY;
34
+
35
+ error NotAuthorized();
36
+ error NodeNotFound();
37
+
38
+ constructor(AgentNameRegistry registry, address ontology) AttributeStorage(ontology) {
39
+ REGISTRY = registry;
40
+ }
41
+
42
+ // ─── Typed setters (predicate-active checked + owner-only) ──────
43
+
44
+ function setStringAttribute(bytes32 node, bytes32 predicate, string calldata value) external {
45
+ _requireAuth(node);
46
+ _setString(node, predicate, value);
47
+ }
48
+
49
+ function setAddressAttribute(bytes32 node, bytes32 predicate, address value) external {
50
+ _requireAuth(node);
51
+ _setAddress(node, predicate, value);
52
+ }
53
+
54
+ function setBoolAttribute(bytes32 node, bytes32 predicate, bool value) external {
55
+ _requireAuth(node);
56
+ _setBool(node, predicate, value);
57
+ }
58
+
59
+ function setUintAttribute(bytes32 node, bytes32 predicate, uint256 value) external {
60
+ _requireAuth(node);
61
+ _setUint(node, predicate, value);
62
+ }
63
+
64
+ function setBytes32Attribute(bytes32 node, bytes32 predicate, bytes32 value) external {
65
+ _requireAuth(node);
66
+ _setBytes32(node, predicate, value);
67
+ }
68
+
69
+ function setStringArrayAttribute(bytes32 node, bytes32 predicate, string[] calldata values) external {
70
+ _requireAuth(node);
71
+ _setStringArr(node, predicate, values);
72
+ }
73
+
74
+ function setAddressArrayAttribute(bytes32 node, bytes32 predicate, address[] calldata values) external {
75
+ _requireAuth(node);
76
+ _setAddressArr(node, predicate, values);
77
+ }
78
+
79
+ function setBytes32ArrayAttribute(bytes32 node, bytes32 predicate, bytes32[] calldata values) external {
80
+ _requireAuth(node);
81
+ _setBytes32Arr(node, predicate, values);
82
+ }
83
+
84
+ function unsetAttribute(bytes32 node, bytes32 predicate) external {
85
+ _requireAuth(node);
86
+ _unset(node, predicate);
87
+ }
88
+
89
+ // ─── Auth ───────────────────────────────────────────────────────
90
+
91
+ function _requireAuth(bytes32 node) internal view {
92
+ if (!REGISTRY.recordExists(node)) revert NodeNotFound();
93
+ if (msg.sender != REGISTRY.owner(node)) revert NotAuthorized();
94
+ }
95
+ }
@@ -0,0 +1,74 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /**
5
+ * @title AgentNamePredicates
6
+ * @notice Canonical bytes32 predicate ids for the AgentName ontology
7
+ * (`atl:*` CURIEs). Single source of truth shared by:
8
+ * - Deploy.s.sol (registers these in OntologyTermRegistry)
9
+ * - AgentNameAttributeResolver (typed-attribute setters
10
+ * validate against these ids)
11
+ * - agenticprimitives/agent-naming SDK (off-chain encoders /
12
+ * decoders use the same ids)
13
+ *
14
+ * Naming convention: `ATL_<UPPER_SNAKE_NAME>` where the CURIE is
15
+ * `atl:<lowerCamelName>`. Use a library so the constants can be
16
+ * imported as `using AgentNamePredicates for *;` OR referenced
17
+ * directly as `AgentNamePredicates.ATL_DISPLAY_NAME`.
18
+ */
19
+ library AgentNamePredicates {
20
+ // ─── Resolver-record predicates (forward record + typed metadata) ─
21
+
22
+ /// `atl:addr` — the forward record. Stored as `address` datatype.
23
+ bytes32 internal constant ATL_ADDR = keccak256("atl:addr");
24
+
25
+ /// `atl:agentKind` — discriminator (`person`/`org`/`service`/`treasury`).
26
+ /// Stored as `bytes32` (hashed enum value). Bound to AGENT_KIND_ENUM in ShapeRegistry.
27
+ bytes32 internal constant ATL_AGENT_KIND = keccak256("atl:agentKind");
28
+
29
+ /// `atl:displayName` — human-friendly label. `string`.
30
+ bytes32 internal constant ATL_DISPLAY_NAME = keccak256("atl:displayName");
31
+
32
+ /// `atl:a2aEndpoint` — A2A service URL. `string`.
33
+ bytes32 internal constant ATL_A2A_ENDPOINT = keccak256("atl:a2aEndpoint");
34
+
35
+ /// `atl:mcpEndpoint` — MCP service URL. `string`.
36
+ bytes32 internal constant ATL_MCP_ENDPOINT = keccak256("atl:mcpEndpoint");
37
+
38
+ /// `atl:metadataURI` — off-chain JSON profile URL. `string`.
39
+ bytes32 internal constant ATL_METADATA_URI = keccak256("atl:metadataURI");
40
+
41
+ /// `atl:metadataHash` — keccak256 of the canonical-JSON profile
42
+ /// (matches `agenticprimitives/agent-identity.profileContentHash`).
43
+ /// `bytes32`.
44
+ bytes32 internal constant ATL_METADATA_HASH = keccak256("atl:metadataHash");
45
+
46
+ /// `atl:passkeyCredentialDigest` — keccak256 of the controlling
47
+ /// passkey credentialId (NEVER the raw credentialId). `bytes32`.
48
+ bytes32 internal constant ATL_PASSKEY_CREDENTIAL_DIGEST = keccak256("atl:passkeyCredentialDigest");
49
+
50
+ /// `atl:custodyPolicy` — address of the owner Smart Agent's
51
+ /// CustodyPolicy module. `address`.
52
+ bytes32 internal constant ATL_CUSTODY_POLICY = keccak256("atl:custodyPolicy");
53
+
54
+ /// `atl:nativeId` — CAIP-10 chain-agnostic account identifier
55
+ /// (per ADR-0008). `string`.
56
+ bytes32 internal constant ATL_NATIVE_ID = keccak256("atl:nativeId");
57
+
58
+ // ─── Shape + enum-set ids ───────────────────────────────────────
59
+
60
+ /// `atl:AgentName` — the class id for ShapeRegistry validation.
61
+ bytes32 internal constant CLASS_AGENT_NAME = keccak256("atl:AgentName");
62
+
63
+ /// Enum set bound to `atl:agentKind`. Contains the three hashed
64
+ /// member ids below.
65
+ bytes32 internal constant AGENT_KIND_ENUM = keccak256("atl:AgentKindEnum");
66
+
67
+ // agentKind is 3-valued (person/org/service). A treasury is a KIND OF
68
+ // SERVICE (agentKind=service), distinguished at the profile layer
69
+ // (ProfileType/serviceType='treasury'; specs 210/217/225 §6) — NOT its own
70
+ // agent kind. Do not re-add AGENT_KIND_TREASURY.
71
+ bytes32 internal constant AGENT_KIND_PERSON = keccak256("person");
72
+ bytes32 internal constant AGENT_KIND_ORG = keccak256("org");
73
+ bytes32 internal constant AGENT_KIND_SERVICE = keccak256("service");
74
+ }