@agenticprimitives/contracts 1.0.0-alpha.4 → 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.
Files changed (57) hide show
  1. package/AUDIT.md +304 -47
  2. package/README.md +19 -8
  3. package/deployments-base-sepolia.json +1 -0
  4. package/dist/abi/AgentAccount.json +1647 -0
  5. package/dist/abi/AgentAccountFactory.json +472 -0
  6. package/dist/abi/AgentNameRegistry.json +46 -0
  7. package/dist/abi/AgreementRegistry.json +432 -0
  8. package/dist/abi/AttestationRegistry.json +436 -0
  9. package/dist/abi/CallDataHashEnforcer.json +119 -0
  10. package/dist/abi/CustodyPolicy.json +1111 -0
  11. package/dist/abi/DelegationManager.json +645 -0
  12. package/dist/abi/GeoFeatureRegistry.json +625 -0
  13. package/dist/abi/IAgentAccount.json +262 -0
  14. package/dist/abi/IAgentAccountFactoryAccessor.json +15 -0
  15. package/dist/abi/IAgentAccountFactoryView.json +41 -0
  16. package/dist/abi/IAgentAccountPauseView.json +15 -0
  17. package/dist/abi/IAgenticPrimitivesAgentAccount.json +15 -0
  18. package/dist/abi/IApprovedHashRegistryView.json +26 -0
  19. package/dist/abi/ICustodyPolicyFactoryView.json +15 -0
  20. package/dist/abi/ICustodyPolicyPauseView.json +15 -0
  21. package/dist/abi/IDelegationManager.json +171 -0
  22. package/dist/abi/IERC7579HookLike.json +44 -0
  23. package/dist/abi/IERC7579ModuleLike.json +28 -0
  24. package/dist/abi/PermissionlessSubregistry.json +5 -0
  25. package/dist/abi/QuorumEnforcer.json +295 -0
  26. package/dist/abi/SignatureSlotRecovery.json +88 -0
  27. package/dist/abi/SkillDefinitionRegistry.json +491 -0
  28. package/dist/abi/SmartAgentPaymaster.json +664 -0
  29. package/dist/abi/UniversalSignatureValidator.json +65 -0
  30. package/dist/abi/index.d.ts +23 -0
  31. package/dist/abi/index.js +23 -0
  32. package/dist/deployments/base-sepolia.d.ts +37 -0
  33. package/dist/deployments/base-sepolia.js +35 -0
  34. package/dist/deployments/index.d.ts +8 -0
  35. package/dist/deployments/index.js +13 -0
  36. package/package.json +20 -3
  37. package/src/AgentAccount.sol +130 -9
  38. package/src/AgentAccountFactory.sol +34 -5
  39. package/src/SmartAgentPaymaster.sol +2 -0
  40. package/src/UniversalSignatureValidator.sol +3 -0
  41. package/src/agency/DelegationManager.sol +155 -0
  42. package/src/agency/IDelegationManager.sol +35 -0
  43. package/src/agreement/AgreementRegistry.sol +306 -0
  44. package/src/attestation/AttestationRegistry.sol +320 -0
  45. package/src/custody/CustodyPolicy.sol +79 -2
  46. package/src/enforcers/AllowedMethodsEnforcer.sol +6 -0
  47. package/src/enforcers/AllowedTargetsEnforcer.sol +6 -0
  48. package/src/enforcers/CallDataHashEnforcer.sol +61 -0
  49. package/src/enforcers/QuorumEnforcer.sol +8 -0
  50. package/src/enforcers/TimestampEnforcer.sol +7 -0
  51. package/src/enforcers/ValueEnforcer.sol +6 -0
  52. package/src/geo/GeoFeatureRegistry.sol +190 -0
  53. package/src/naming/AgentNameRegistry.sol +25 -13
  54. package/src/naming/AgentNameUniversalResolver.sol +1 -1
  55. package/src/naming/PermissionlessSubregistry.sol +19 -2
  56. package/src/relationships/AgentRelationship.sol +8 -0
  57. package/src/skills/SkillDefinitionRegistry.sol +181 -0
@@ -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 ENS-style namehash:
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
- contract AgentNameRegistry {
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. The deploy script
133
- /// deploys the registry + calls `initializeRoot` in the same
134
- /// transaction so the TLD can't be frontrun.
135
- constructor(address initializer_) {
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 ENS-aligned reverse path — no log walks, no name
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
- contract PermissionlessSubregistry {
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
@@ -0,0 +1,181 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /**
5
+ * @title SkillDefinitionRegistry
6
+ * @notice Versioned PUBLIC skill definitions — the definition layer of the
7
+ * definition-vs-claim model (spec 251). Small, stable, public, indexable
8
+ * anchors only: id, version, steward, kind, content hashes, metadata URI,
9
+ * validity. The full SKOS expansion + taxonomy live OFF chain behind
10
+ * `metadataURI` + `ontologyMerkleRoot`.
11
+ *
12
+ * ─── Doctrine ───
13
+ *
14
+ * SDR-01 Keyed by Smart Agent ADDRESS (the `stewardAccount`); names are never
15
+ * authority (ADR-0010).
16
+ * SDR-02 Authorization is `msg.sender == stewardAccount` — the steward SA
17
+ * publishes through its own `execute()` path. NO `isOwner(address)`
18
+ * staticcall fallback (AP diverges from smart-agent; see AgentRelationship).
19
+ * SDR-03 Monotonic per-`skillId` versioning; each version chains the prior
20
+ * version's `ontologyMerkleRoot` as `predecessorRoot` (auditable taxonomy
21
+ * refresh). A skill is never deleted; `deactivate` flips `active`.
22
+ * SDR-04 `skillKind` is a C-box-anchored codelist value: `keccak256(<cbox URI>)`,
23
+ * gate-checked against `packages/ontology` (ADR-0009 lockstep).
24
+ * SDR-05 NO domain/faith vocabulary. This is reusable substrate (spec 251
25
+ * "People groups are excluded").
26
+ * SDR-06 SANITIZED metadata only. `metadataURI` + `conceptHash` carry NEUTRAL,
27
+ * public, non-operational skill vocabulary — never anything that implies
28
+ * private operational data about who holds the skill or where it is used.
29
+ * The agent↔skill association is an OFF-chain vault credential (spec 251).
30
+ * SDR-07 NO on-chain skill↔geo mapping. This registry holds skills only and has
31
+ * ZERO reference to GeoFeatureRegistry (or any geo id). A "skill X in
32
+ * region Y" co-occurrence is an off-chain claim/credential, never an
33
+ * on-chain link between the two definition registries.
34
+ *
35
+ * Ported from smart-agent `SkillDefinitionRegistry`; diverges per spec 251 (AP
36
+ * auth model, C-box-anchored kinds, no `.skill` name binding in v1).
37
+ */
38
+ contract SkillDefinitionRegistry {
39
+ // ─── C-box skill-kind codelist (keccak of the cbox vocabulary URIs) ──
40
+ // Concept URIs are declared in packages/ontology cbox/skill-vocabulary.ttl
41
+ // (scheme aps:skillKind); the lockstep gate asserts these equal keccak of
42
+ // the declared skos:Concept URIs (ADR-0009).
43
+ /// @dev A leaf skill in a controlled taxonomy (e.g. an OASF leaf).
44
+ bytes32 public constant KIND_LEAF = keccak256(bytes("https://agenticprimitives.dev/ns/skill#Leaf"));
45
+ /// @dev A domain/grouping concept (a non-leaf category).
46
+ bytes32 public constant KIND_DOMAIN = keccak256(bytes("https://agenticprimitives.dev/ns/skill#Domain"));
47
+ /// @dev A steward-defined custom skill outside the controlled set.
48
+ bytes32 public constant KIND_CUSTOM = keccak256(bytes("https://agenticprimitives.dev/ns/skill#Custom"));
49
+
50
+ // ─── Errors ─────────────────────────────────────────────────────────
51
+ error NotSteward();
52
+ error SkillNotFound();
53
+ error EmptyMetadata();
54
+ error EmptyId();
55
+
56
+ // ─── Storage ────────────────────────────────────────────────────────
57
+ struct SkillRecord {
58
+ bytes32 skillId; // stable id across versions
59
+ uint64 version; // monotonic per skillId (1-based; 0 = absent)
60
+ address stewardAccount; // governing steward SA
61
+ bytes32 skillKind; // C-box kind codelist value
62
+ bytes32 conceptHash; // keccak of canonical SKOS prefLabel + ancestors
63
+ bytes32 ontologyMerkleRoot; // anchors the RDF/SKOS expansion (off chain)
64
+ bytes32 predecessorRoot; // prior version's ontologyMerkleRoot (0 for v1)
65
+ string metadataURI; // ipfs://… or https://… JSON-LD blob
66
+ uint64 validAfter; // 0 = since inception
67
+ uint64 validUntil; // 0 = indefinite
68
+ bool active;
69
+ uint64 registeredAt;
70
+ }
71
+
72
+ /// @dev skillId => version => record.
73
+ mapping(bytes32 => mapping(uint64 => SkillRecord)) private _records;
74
+ /// @dev skillId => latest version (0 = unknown).
75
+ mapping(bytes32 => uint64) public latestVersion;
76
+ bytes32[] private _allSkills;
77
+
78
+ // ─── Events ─────────────────────────────────────────────────────────
79
+ event SkillPublished(
80
+ bytes32 indexed skillId,
81
+ uint64 indexed version,
82
+ address indexed steward,
83
+ bytes32 skillKind,
84
+ bytes32 conceptHash,
85
+ bytes32 ontologyMerkleRoot,
86
+ bytes32 predecessorRoot,
87
+ string metadataURI
88
+ );
89
+ event SkillDeactivated(bytes32 indexed skillId, uint64 version);
90
+ event SkillValidityChanged(bytes32 indexed skillId, uint64 version, uint64 validAfter, uint64 validUntil);
91
+
92
+ // ─── Publish ────────────────────────────────────────────────────────
93
+ struct PublishInput {
94
+ bytes32 skillId;
95
+ bytes32 skillKind;
96
+ address stewardAccount;
97
+ bytes32 conceptHash;
98
+ bytes32 ontologyMerkleRoot;
99
+ string metadataURI;
100
+ uint64 validAfter;
101
+ uint64 validUntil;
102
+ }
103
+
104
+ /// @notice Publish a new skill definition, or a new version of an existing one.
105
+ function publish(PublishInput calldata p) external returns (uint64 version) {
106
+ if (p.skillId == bytes32(0)) revert EmptyId();
107
+ if (bytes(p.metadataURI).length == 0) revert EmptyMetadata();
108
+ // SDR-02 — only the steward SA, acting through its own execute() path.
109
+ if (msg.sender != p.stewardAccount) revert NotSteward();
110
+
111
+ uint64 prevVer = latestVersion[p.skillId];
112
+ if (prevVer != 0 && _records[p.skillId][prevVer].stewardAccount != p.stewardAccount) {
113
+ // SDR-03 — a new version must come from the SAME steward.
114
+ revert NotSteward();
115
+ }
116
+
117
+ version = prevVer + 1;
118
+ bytes32 predecessorRoot = prevVer == 0 ? bytes32(0) : _records[p.skillId][prevVer].ontologyMerkleRoot;
119
+
120
+ _records[p.skillId][version] = SkillRecord({
121
+ skillId: p.skillId,
122
+ version: version,
123
+ stewardAccount: p.stewardAccount,
124
+ skillKind: p.skillKind,
125
+ conceptHash: p.conceptHash,
126
+ ontologyMerkleRoot: p.ontologyMerkleRoot,
127
+ predecessorRoot: predecessorRoot,
128
+ metadataURI: p.metadataURI,
129
+ validAfter: p.validAfter,
130
+ validUntil: p.validUntil,
131
+ active: true,
132
+ registeredAt: uint64(block.timestamp)
133
+ });
134
+ latestVersion[p.skillId] = version;
135
+ if (prevVer == 0) _allSkills.push(p.skillId);
136
+
137
+ emit SkillPublished(p.skillId, version, p.stewardAccount, p.skillKind, p.conceptHash, p.ontologyMerkleRoot, predecessorRoot, p.metadataURI);
138
+ }
139
+
140
+ /// @notice Deactivate the latest version (history is preserved).
141
+ function deactivate(bytes32 skillId) external {
142
+ uint64 v = latestVersion[skillId];
143
+ if (v == 0) revert SkillNotFound();
144
+ SkillRecord storage r = _records[skillId][v];
145
+ if (msg.sender != r.stewardAccount) revert NotSteward();
146
+ r.active = false;
147
+ emit SkillDeactivated(skillId, v);
148
+ }
149
+
150
+ /// @notice Update the validity window of a published version.
151
+ function setValidity(bytes32 skillId, uint64 version, uint64 validAfter, uint64 validUntil) external {
152
+ SkillRecord storage r = _records[skillId][version];
153
+ if (r.version == 0) revert SkillNotFound();
154
+ if (msg.sender != r.stewardAccount) revert NotSteward();
155
+ r.validAfter = validAfter;
156
+ r.validUntil = validUntil;
157
+ emit SkillValidityChanged(skillId, version, validAfter, validUntil);
158
+ }
159
+
160
+ // ─── Views ──────────────────────────────────────────────────────────
161
+ function getSkill(bytes32 skillId, uint64 version) external view returns (SkillRecord memory) {
162
+ SkillRecord memory r = _records[skillId][version];
163
+ if (r.version == 0) revert SkillNotFound();
164
+ return r;
165
+ }
166
+
167
+ function getLatest(bytes32 skillId) external view returns (SkillRecord memory) {
168
+ uint64 v = latestVersion[skillId];
169
+ if (v == 0) revert SkillNotFound();
170
+ return _records[skillId][v];
171
+ }
172
+
173
+ /// @notice True if `version` of `skillId` exists (used by the claim registry to pin a version).
174
+ function exists(bytes32 skillId, uint64 version) external view returns (bool) {
175
+ return _records[skillId][version].version != 0;
176
+ }
177
+
178
+ function allSkills() external view returns (bytes32[] memory) {
179
+ return _allSkills;
180
+ }
181
+ }