From c61f15b77bc91bad06186c01ee610ab7243bb438 Mon Sep 17 00:00:00 2001 From: madschristensen99 Date: Thu, 4 Jun 2026 10:32:16 -0400 Subject: [PATCH 1/2] AP-1: ERC-8004 Trustless Agent Identity - AgentIdentityRegistry: ERC-721 + URIStorage with EIP-712/ERC-1271 wallet verification - AgentReputationRegistry: feedback, revocation, counter-evidence responses - AgentValidationRegistry: validation requests and progressive responses (0-100) - All UUPS-upgradeable via ERC1967Proxy with __gap[50] - ERC-2771 meta-tx support via TestnetCoreBase - Custom errors throughout (no require strings) - Foundry tests: 70 passing across 3 suites - Line coverage > 84%, function coverage > 83% - Deployment script for Arb Sepolia - Documentation in packages/identity/CLAUDE.md --- packages/identity/CLAUDE.md | 88 +++++ .../contracts/core/AgentIdentityRegistry.sol | 214 ++++++++++ .../core/AgentReputationRegistry.sol | 200 ++++++++++ .../core/AgentValidationRegistry.sol | 151 ++++++++ .../core/IAgentIdentityRegistry.sol | 121 ++++++ .../core/IAgentReputationRegistry.sol | 126 ++++++ .../core/IAgentValidationRegistry.sol | 104 +++++ packages/identity/foundry.toml | 26 ++ packages/identity/package.json | 28 ++ packages/identity/script/Deploy.s.sol | 42 ++ .../test/unit/AgentIdentityRegistry.t.sol | 364 ++++++++++++++++++ .../test/unit/AgentReputationRegistry.t.sol | 309 +++++++++++++++ .../test/unit/AgentValidationRegistry.t.sol | 238 ++++++++++++ pnpm-lock.yaml | 12 + 14 files changed, 2023 insertions(+) create mode 100644 packages/identity/CLAUDE.md create mode 100644 packages/identity/contracts/core/AgentIdentityRegistry.sol create mode 100644 packages/identity/contracts/core/AgentReputationRegistry.sol create mode 100644 packages/identity/contracts/core/AgentValidationRegistry.sol create mode 100644 packages/identity/contracts/interfaces/core/IAgentIdentityRegistry.sol create mode 100644 packages/identity/contracts/interfaces/core/IAgentReputationRegistry.sol create mode 100644 packages/identity/contracts/interfaces/core/IAgentValidationRegistry.sol create mode 100644 packages/identity/foundry.toml create mode 100644 packages/identity/package.json create mode 100644 packages/identity/script/Deploy.s.sol create mode 100644 packages/identity/test/unit/AgentIdentityRegistry.t.sol create mode 100644 packages/identity/test/unit/AgentReputationRegistry.t.sol create mode 100644 packages/identity/test/unit/AgentValidationRegistry.t.sol diff --git a/packages/identity/CLAUDE.md b/packages/identity/CLAUDE.md new file mode 100644 index 0000000..fface8b --- /dev/null +++ b/packages/identity/CLAUDE.md @@ -0,0 +1,88 @@ +# ERC-8004 Identity Package + +This package implements the [ERC-8004: Trustless Agents](https://eips.ethereum.org/EIPS/eip-8004) standard as three upgradeable UUPS-proxy contracts on Arbitrum Sepolia. + +## Contracts + +### `AgentIdentityRegistry` + +**Path:** `contracts/core/AgentIdentityRegistry.sol` + +- ERC-721 + URIStorage for agent registration +- `register(agentURI, MetadataEntry[]) → agentId` +- `setAgentURI(agentId, newURI)` +- `setMetadata(agentId, key, value)` / `getMetadata(agentId, key)` +- `setAgentWallet(agentId, newWallet, deadline, signature)` — EIP-712 / ERC-1271 verification +- `getAgentWallet(agentId)` / `unsetAgentWallet(agentId)` +- Wallet auto-clears on token transfer (transferable by default; soulbound decision deferred) +- `__gap[50]` + +### `AgentReputationRegistry` + +**Path:** `contracts/core/AgentReputationRegistry.sol` + +- `giveFeedback(agentId, value, valueDecimals, tag1, tag2, endpoint, feedbackURI, feedbackHash)` + - Only non-owners can submit feedback + - `value` is int128, `valueDecimals` 0–18 + - Emits `NewFeedback` event +- `revokeFeedback(agentId, feedbackIndex)` — author-only +- `appendResponse(agentId, clientAddress, feedbackIndex, responseURI, responseHash)` — counter-evidence +- `readFeedback`, `getLastIndex`, `getResponseCount`, `getClients` +- `__gap[50]` + +### `AgentValidationRegistry` + +**Path:** `contracts/core/AgentValidationRegistry.sol` + +- `validationRequest(validator, agentId, requestURI, requestHash)` — agent-owner only +- `validationResponse(requestHash, response, responseURI, responseHash, tag)` — validator-only, 0–100 + - Can be called multiple times for progressive validation +- `getValidationStatus`, `getAgentValidations`, `getValidatorRequests` +- `__gap[50]` + +## Architecture + +All three contracts inherit from `TestnetCoreBase` (`@reineira-os/shared`), which provides: + +- UUPS upgradeability +- `OwnableUpgradeable` +- `ReentrancyGuardUpgradeable` +- ERC-2771 meta-transaction support + +## Dependencies + +- `@openzeppelin/contracts ~5.2.0` +- `@openzeppelin/contracts-upgradeable ~5.2.0` +- `@reineira-os/shared workspace:*` + +## Deployment + +```bash +# Install +pnpm install + +# Compile +forge build + +# Test +forge test -vv + +# Coverage +forge coverage + +# Deploy to Arb Sepolia (set PRIVATE_KEY and optionally TRUSTED_FORWARDER) +forge script script/Deploy.s.sol --rpc-url $ARB_SEPOLIA_RPC --broadcast --verify +``` + +## Decisions Deferred + +1. **Soulbound vs transferable `agentId`**: Currently fully transferable ERC-721. Soulbound can be enforced later via `_update` override. +2. **Encrypted-reputation variant**: Not implemented; plain-text reputation only. + +## Tests + +- `test/unit/AgentIdentityRegistry.t.sol` — registration, URI, metadata, wallet verification, transfers +- `test/unit/AgentReputationRegistry.t.sol` — feedback, revocation, responses, access control +- `test/unit/AgentValidationRegistry.t.sol` — requests, responses, progressive updates, view functions + +All tests use Foundry with standard `Test` base (no FHE required for identity contracts). diff --git a/packages/identity/contracts/core/AgentIdentityRegistry.sol b/packages/identity/contracts/core/AgentIdentityRegistry.sol new file mode 100644 index 0000000..bfbd14f --- /dev/null +++ b/packages/identity/contracts/core/AgentIdentityRegistry.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Reineira Labs Limited All rights reserved. +pragma solidity ^0.8.24; + +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import {ERC2771ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol"; +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {ERC721URIStorageUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import {TestnetCoreBase} from "@reineira-os/shared/contracts/common/TestnetCoreBase.sol"; +import {IAgentIdentityRegistry} from "../interfaces/core/IAgentIdentityRegistry.sol"; + +contract AgentIdentityRegistry is + IAgentIdentityRegistry, + TestnetCoreBase, + ERC721Upgradeable, + ERC721URIStorageUpgradeable, + EIP712Upgradeable +{ + using ECDSA for bytes32; + + bytes32 private constant SET_AGENT_WALLET_TYPEHASH = + keccak256("SetAgentWallet(uint256 agentId,address newWallet,uint256 nonce,uint256 deadline)"); + + uint256 private _nextAgentId; + mapping(uint256 => address) private _agentWallets; + mapping(uint256 => uint256) private _walletNonces; + mapping(uint256 => mapping(string => bytes)) private _metadata; + + uint256[50] private __gap; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address trustedForwarder_) TestnetCoreBase(trustedForwarder_) { + _disableInitializers(); + } + + function initialize(address owner_) external initializer { + if (owner_ == address(0)) revert ZeroAddress(); + __TestnetCoreBase_init(owner_); + __ERC721_init("Reineira Agent Identity", "RAI"); + __ERC721URIStorage_init(); + __EIP712_init("Reineira Agent Identity", "1"); + emit CoreInitialized(owner_); + } + + // --- ERC-721 + URIStorage overrides --- + + function tokenURI( + uint256 tokenId + ) public view override(ERC721Upgradeable, ERC721URIStorageUpgradeable) returns (string memory) { + return super.tokenURI(tokenId); + } + + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC721Upgradeable, ERC721URIStorageUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function _msgSender() internal view virtual override(ContextUpgradeable, TestnetCoreBase) returns (address) { + return ERC2771ContextUpgradeable._msgSender(); + } + + function _msgData() internal view virtual override(ContextUpgradeable, TestnetCoreBase) returns (bytes calldata) { + return ERC2771ContextUpgradeable._msgData(); + } + + function _contextSuffixLength() + internal + view + virtual + override(ContextUpgradeable, TestnetCoreBase) + returns (uint256) + { + return ERC2771ContextUpgradeable._contextSuffixLength(); + } + + function _update(address to, uint256 tokenId, address auth) internal override(ERC721Upgradeable) returns (address) { + address from = super._update(to, tokenId, auth); + // Clear agentWallet on any transfer (including mint/burn) + if (from != address(0) && to != address(0) && _agentWallets[tokenId] != address(0)) { + delete _agentWallets[tokenId]; + emit AgentWalletCleared(tokenId); + } + return from; + } + + // --- registration --- + + function register(string calldata agentURI, MetadataEntry[] calldata metadata) external returns (uint256 agentId) { + agentId = _nextAgentId++; + address caller = _msgSender(); + _safeMint(caller, agentId); + _setTokenURI(agentId, agentURI); + + // Emit reserved agentWallet metadata as owner address + emit MetadataSet(agentId, "agentWallet", "agentWallet", abi.encode(caller)); + + for (uint256 i = 0; i < metadata.length; i++) { + _setMetadata(agentId, metadata[i].metadataKey, metadata[i].metadataValue); + } + + emit Registered(agentId, agentURI, caller); + } + + function register(string calldata agentURI) external returns (uint256 agentId) { + agentId = _nextAgentId++; + address caller = _msgSender(); + _safeMint(caller, agentId); + _setTokenURI(agentId, agentURI); + + emit MetadataSet(agentId, "agentWallet", "agentWallet", abi.encode(caller)); + emit Registered(agentId, agentURI, caller); + } + + function register() external returns (uint256 agentId) { + agentId = _nextAgentId++; + address caller = _msgSender(); + _safeMint(caller, agentId); + + emit MetadataSet(agentId, "agentWallet", "agentWallet", abi.encode(caller)); + emit Registered(agentId, "", caller); + } + + // --- URI --- + + function setAgentURI(uint256 agentId, string calldata newURI) external { + if (!_exists(agentId)) revert AgentNotFound(); + if (ownerOf(agentId) != _msgSender()) revert NotAgentOwner(); + + _setTokenURI(agentId, newURI); + emit URIUpdated(agentId, newURI, _msgSender()); + } + + // --- metadata --- + + function setMetadata(uint256 agentId, string calldata metadataKey, bytes calldata metadataValue) external { + if (!_exists(agentId)) revert AgentNotFound(); + if (ownerOf(agentId) != _msgSender()) revert NotAgentOwner(); + _setMetadata(agentId, metadataKey, metadataValue); + } + + function getMetadata(uint256 agentId, string calldata metadataKey) external view returns (bytes memory) { + if (!_exists(agentId)) revert AgentNotFound(); + return _metadata[agentId][metadataKey]; + } + + // --- agent wallet --- + + function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes calldata signature) external { + if (!_exists(agentId)) revert AgentNotFound(); + if (ownerOf(agentId) != _msgSender()) revert NotAgentOwner(); + if (newWallet == address(0)) revert InvalidSignature(); + if (block.timestamp > deadline) revert SignatureExpired(); + if (_agentWallets[agentId] == newWallet) revert WalletAlreadySet(); + + uint256 nonce = _walletNonces[agentId]++; + bytes32 structHash = keccak256(abi.encode(SET_AGENT_WALLET_TYPEHASH, agentId, newWallet, nonce, deadline)); + bytes32 digest = _hashTypedDataV4(structHash); + + if (!_verifySignature(newWallet, digest, signature)) revert InvalidSignature(); + + _agentWallets[agentId] = newWallet; + emit AgentWalletSet(agentId, newWallet); + } + + function getAgentWallet(uint256 agentId) external view returns (address) { + if (!_exists(agentId)) revert AgentNotFound(); + return _agentWallets[agentId]; + } + + function unsetAgentWallet(uint256 agentId) external { + if (!_exists(agentId)) revert AgentNotFound(); + if (ownerOf(agentId) != _msgSender()) revert NotAgentOwner(); + if (_agentWallets[agentId] == address(0)) revert WalletNotSet(); + + delete _agentWallets[agentId]; + emit AgentWalletCleared(agentId); + } + + // --- views --- + + function agentCount() external view returns (uint256) { + return _nextAgentId; + } + + // --- internal --- + + function _setMetadata(uint256 agentId, string calldata metadataKey, bytes calldata metadataValue) internal { + if (keccak256(bytes(metadataKey)) == keccak256(bytes("agentWallet"))) revert ReservedMetadataKey(); + _metadata[agentId][metadataKey] = metadataValue; + emit MetadataSet(agentId, metadataKey, metadataKey, metadataValue); + } + + function _verifySignature(address signer, bytes32 digest, bytes calldata signature) internal view returns (bool) { + if (signature.length == 65) { + (address recovered, ECDSA.RecoverError error, ) = ECDSA.tryRecover(digest, signature); + return error == ECDSA.RecoverError.NoError && recovered == signer; + } + + // ERC-1271 smart contract wallet + try IERC1271(signer).isValidSignature(digest, signature) returns (bytes4 magicValue) { + return magicValue == IERC1271.isValidSignature.selector; + } catch { + return false; + } + } + + function _exists(uint256 tokenId) internal view returns (bool) { + return _ownerOf(tokenId) != address(0); + } +} diff --git a/packages/identity/contracts/core/AgentReputationRegistry.sol b/packages/identity/contracts/core/AgentReputationRegistry.sol new file mode 100644 index 0000000..a055509 --- /dev/null +++ b/packages/identity/contracts/core/AgentReputationRegistry.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Reineira Labs Limited All rights reserved. +pragma solidity ^0.8.24; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {TestnetCoreBase} from "@reineira-os/shared/contracts/common/TestnetCoreBase.sol"; +import {IAgentIdentityRegistry} from "../interfaces/core/IAgentIdentityRegistry.sol"; +import {IAgentReputationRegistry} from "../interfaces/core/IAgentReputationRegistry.sol"; + +contract AgentReputationRegistry is IAgentReputationRegistry, TestnetCoreBase { + using EnumerableSet for EnumerableSet.AddressSet; + + struct Feedback { + int128 value; + uint8 valueDecimals; + string tag1; + string tag2; + bool isRevoked; + } + + struct ResponseEntry { + address responder; + string responseURI; + bytes32 responseHash; + uint256 timestamp; + } + + IAgentIdentityRegistry public identityRegistry; + + // agentId => client => feedbackIndex => Feedback + mapping(bytes32 => Feedback) private _feedback; + // agentId => client => lastIndex + mapping(bytes32 => uint64) private _feedbackCount; + // agentId => client => feedbackIndex => responses + mapping(bytes32 => ResponseEntry[]) private _responses; + // agentId => set of clients + mapping(uint256 => EnumerableSet.AddressSet) private _clients; + + uint256[50] private __gap; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address trustedForwarder_) TestnetCoreBase(trustedForwarder_) { + _disableInitializers(); + } + + function initialize(address owner_, address identityRegistry_) external initializer { + if (owner_ == address(0)) revert ZeroAddress(); + if (identityRegistry_ == address(0)) revert ZeroAddress(); + __TestnetCoreBase_init(owner_); + identityRegistry = IAgentIdentityRegistry(identityRegistry_); + emit CoreInitialized(owner_); + } + + // --- feedback --- + + function giveFeedback( + uint256 agentId, + int128 value, + uint8 valueDecimals, + string calldata tag1, + string calldata tag2, + string calldata endpoint, + string calldata feedbackURI, + bytes32 feedbackHash + ) external { + if (agentId >= identityRegistry.agentCount()) revert AgentNotFound(); + if (valueDecimals > 18) revert InvalidValueDecimals(); + + address caller = _msgSender(); + address agentOwner; + try IERC721(address(identityRegistry)).ownerOf(agentId) returns (address o) { + agentOwner = o; + } catch { + revert AgentNotFound(); + } + if (caller == agentOwner) revert AgentOwnerCannotFeedback(); + + bytes32 countKey = _feedbackKey(agentId, caller); + uint64 index = ++_feedbackCount[countKey]; + Feedback storage fb = _feedback[_feedbackIndexKey(agentId, caller, index)]; + fb.value = value; + fb.valueDecimals = valueDecimals; + fb.tag1 = tag1; + fb.tag2 = tag2; + fb.isRevoked = false; + + _clients[agentId].add(caller); + + _emitNewFeedback(agentId, caller, index, value, valueDecimals, tag1, tag2, endpoint, feedbackURI, feedbackHash); + } + + function _emitNewFeedback( + uint256 agentId, + address caller, + uint64 index, + int128 value, + uint8 valueDecimals, + string calldata tag1, + string calldata tag2, + string calldata endpoint, + string calldata feedbackURI, + bytes32 feedbackHash + ) internal { + emit NewFeedback( + agentId, + caller, + index, + value, + valueDecimals, + tag1, + tag1, + tag2, + endpoint, + feedbackURI, + feedbackHash + ); + } + + function revokeFeedback(uint256 agentId, uint64 feedbackIndex) external { + if (feedbackIndex == 0) revert FeedbackNotFound(); + if (_feedbackCount[_feedbackKey(agentId, _msgSender())] < feedbackIndex) revert FeedbackNotFound(); + + bytes32 key = _feedbackIndexKey(agentId, _msgSender(), feedbackIndex); + if (_feedback[key].isRevoked) revert AlreadyRevoked(); + + _feedback[key].isRevoked = true; + emit FeedbackRevoked(agentId, _msgSender(), feedbackIndex); + } + + function appendResponse( + uint256 agentId, + address clientAddress, + uint64 feedbackIndex, + string calldata responseURI, + bytes32 responseHash + ) external { + if (feedbackIndex == 0) revert FeedbackNotFound(); + if (_feedbackCount[_feedbackKey(agentId, clientAddress)] < feedbackIndex) revert FeedbackNotFound(); + + _responses[_feedbackIndexKey(agentId, clientAddress, feedbackIndex)].push( + ResponseEntry({ + responder: _msgSender(), + responseURI: responseURI, + responseHash: responseHash, + timestamp: block.timestamp + }) + ); + + emit ResponseAppended(agentId, clientAddress, feedbackIndex, _msgSender(), responseURI, responseHash); + } + + // --- views --- + + function readFeedback( + uint256 agentId, + address clientAddress, + uint64 feedbackIndex + ) + external + view + returns (int128 value, uint8 valueDecimals, string memory tag1, string memory tag2, bool isRevoked) + { + if (feedbackIndex == 0) revert FeedbackNotFound(); + if (_feedbackCount[_feedbackKey(agentId, clientAddress)] < feedbackIndex) revert FeedbackNotFound(); + Feedback storage fb = _feedback[_feedbackIndexKey(agentId, clientAddress, feedbackIndex)]; + return (fb.value, fb.valueDecimals, fb.tag1, fb.tag2, fb.isRevoked); + } + + function getLastIndex(uint256 agentId, address clientAddress) external view returns (uint64) { + return _feedbackCount[_feedbackKey(agentId, clientAddress)]; + } + + function getResponseCount( + uint256 agentId, + address clientAddress, + uint64 feedbackIndex + ) external view returns (uint64) { + if (feedbackIndex == 0) return 0; + return uint64(_responses[_feedbackIndexKey(agentId, clientAddress, feedbackIndex)].length); + } + + function getIdentityRegistry() external view returns (address) { + return address(identityRegistry); + } + + function getClients(uint256 agentId) external view returns (address[] memory) { + return _clients[agentId].values(); + } + + // --- internal --- + + function _feedbackKey(uint256 agentId, address client) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(agentId, client)); + } + + function _feedbackIndexKey(uint256 agentId, address client, uint64 index) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(agentId, client, index)); + } +} diff --git a/packages/identity/contracts/core/AgentValidationRegistry.sol b/packages/identity/contracts/core/AgentValidationRegistry.sol new file mode 100644 index 0000000..7c48799 --- /dev/null +++ b/packages/identity/contracts/core/AgentValidationRegistry.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Reineira Labs Limited All rights reserved. +pragma solidity ^0.8.24; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {TestnetCoreBase} from "@reineira-os/shared/contracts/common/TestnetCoreBase.sol"; +import {IAgentIdentityRegistry} from "../interfaces/core/IAgentIdentityRegistry.sol"; +import {IAgentValidationRegistry} from "../interfaces/core/IAgentValidationRegistry.sol"; + +contract AgentValidationRegistry is IAgentValidationRegistry, TestnetCoreBase { + struct ValidationRequestData { + address validatorAddress; + uint256 agentId; + string requestURI; + bytes32 requestHash; + uint256 requestedAt; + } + + struct ValidationResponseData { + uint8 response; + string responseURI; + bytes32 responseHash; + string tag; + uint256 respondedAt; + } + + IAgentIdentityRegistry public identityRegistry; + + mapping(bytes32 => ValidationRequestData) private _requests; + mapping(bytes32 => ValidationResponseData) private _responses; + mapping(uint256 => bytes32[]) private _agentValidations; + mapping(address => bytes32[]) private _validatorRequests; + + uint256[50] private __gap; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address trustedForwarder_) TestnetCoreBase(trustedForwarder_) { + _disableInitializers(); + } + + function initialize(address owner_, address identityRegistry_) external initializer { + if (owner_ == address(0)) revert ZeroAddress(); + if (identityRegistry_ == address(0)) revert ZeroAddress(); + __TestnetCoreBase_init(owner_); + identityRegistry = IAgentIdentityRegistry(identityRegistry_); + emit CoreInitialized(owner_); + } + + // --- validation request --- + + function validationRequest( + address validatorAddress, + uint256 agentId, + string calldata requestURI, + bytes32 requestHash + ) external { + if (validatorAddress == address(0)) revert InvalidValidator(); + if (requestHash == bytes32(0)) revert ZeroAddress(); + if (_requests[requestHash].requestedAt != 0) revert InvalidValidator(); // reuse error for duplicate hash + + address caller = _msgSender(); + address agentOwner; + try IERC721(address(identityRegistry)).ownerOf(agentId) returns (address o) { + agentOwner = o; + } catch { + revert AgentNotFound(); + } + if (caller != agentOwner) revert Unauthorized(); + + _requests[requestHash] = ValidationRequestData({ + validatorAddress: validatorAddress, + agentId: agentId, + requestURI: requestURI, + requestHash: requestHash, + requestedAt: block.timestamp + }); + + _agentValidations[agentId].push(requestHash); + _validatorRequests[validatorAddress].push(requestHash); + + emit ValidationRequest(validatorAddress, agentId, requestURI, requestHash); + } + + // --- validation response --- + + function validationResponse( + bytes32 requestHash, + uint8 response, + string calldata responseURI, + bytes32 responseHash, + string calldata tag + ) external { + if (requestHash == bytes32(0)) revert RequestNotFound(); + ValidationRequestData storage req = _requests[requestHash]; + if (req.requestedAt == 0) revert RequestNotFound(); + if (_msgSender() != req.validatorAddress) revert NotValidator(); + if (response > 100) revert InvalidResponse(); + + _responses[requestHash] = ValidationResponseData({ + response: response, + responseURI: responseURI, + responseHash: responseHash, + tag: tag, + respondedAt: block.timestamp + }); + + emit ValidationResponse( + req.validatorAddress, + req.agentId, + requestHash, + response, + responseURI, + responseHash, + tag + ); + } + + // --- views --- + + function getValidationStatus( + bytes32 requestHash + ) + external + view + returns ( + address validatorAddress, + uint256 agentId, + uint8 response, + bytes32 responseHash, + string memory tag, + uint256 lastUpdate + ) + { + ValidationRequestData storage req = _requests[requestHash]; + if (req.requestedAt == 0) revert RequestNotFound(); + ValidationResponseData storage resp = _responses[requestHash]; + return (req.validatorAddress, req.agentId, resp.response, resp.responseHash, resp.tag, resp.respondedAt); + } + + function getAgentValidations(uint256 agentId) external view returns (bytes32[] memory requestHashes) { + return _agentValidations[agentId]; + } + + function getValidatorRequests(address validatorAddress) external view returns (bytes32[] memory requestHashes) { + return _validatorRequests[validatorAddress]; + } + + function getIdentityRegistry() external view returns (address) { + return address(identityRegistry); + } +} diff --git a/packages/identity/contracts/interfaces/core/IAgentIdentityRegistry.sol b/packages/identity/contracts/interfaces/core/IAgentIdentityRegistry.sol new file mode 100644 index 0000000..4824219 --- /dev/null +++ b/packages/identity/contracts/interfaces/core/IAgentIdentityRegistry.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Reineira Labs Limited All rights reserved. +pragma solidity ^0.8.24; + +import {ICore} from "@reineira-os/shared/contracts/interfaces/core/ICore.sol"; + +/// @title IAgentIdentityRegistry — ERC-8004 Identity Registry +/// @notice Minimal ERC-721 + URIStorage registry for trustless agent identity. +/// @dev Each agent is a globally unique ERC-721 token with optional on-chain metadata. +interface IAgentIdentityRegistry is ICore { + /// @notice On-chain metadata entry for an agent + struct MetadataEntry { + string metadataKey; + bytes metadataValue; + } + + /// @notice Emitted when a new agent is registered + /// @param agentId The ERC-721 tokenId assigned to the agent + /// @param agentURI The initial URI pointing to the agent registration file + /// @param owner The address that owns the newly minted token + event Registered(uint256 indexed agentId, string agentURI, address indexed owner); + + /// @notice Emitted when an agent's URI is updated + /// @param agentId The agent whose URI changed + /// @param newURI The new URI + /// @param updatedBy The address that triggered the update + event URIUpdated(uint256 indexed agentId, string newURI, address indexed updatedBy); + + /// @notice Emitted when on-chain metadata is set + /// @param agentId The agent whose metadata changed + /// @param indexedMetadataKey Indexed metadata key for filtering + /// @param metadataKey The metadata key + /// @param metadataValue The metadata value + event MetadataSet( + uint256 indexed agentId, + string indexed indexedMetadataKey, + string metadataKey, + bytes metadataValue + ); + + /// @notice Emitted when an agent's wallet address is set or verified + /// @param agentId The agent whose wallet changed + /// @param wallet The verified wallet address + event AgentWalletSet(uint256 indexed agentId, address indexed wallet); + + /// @notice Emitted when an agent's wallet address is cleared + /// @param agentId The agent whose wallet was cleared + event AgentWalletCleared(uint256 indexed agentId); + + /// @notice Thrown when the queried agent does not exist + error AgentNotFound(); + + /// @notice Thrown when a non-owner tries to modify agent data + error NotAgentOwner(); + + /// @notice Thrown when `agentWallet` is used as a metadata key + error ReservedMetadataKey(); + + /// @notice Thrown when the provided signature is invalid or expired + error InvalidSignature(); + + /// @notice Thrown when the signature deadline has passed + error SignatureExpired(); + + /// @notice Thrown when the wallet is already set to the same address + error WalletAlreadySet(); + + /// @notice Thrown when the wallet has not been set + error WalletNotSet(); + + /// @notice Registers a new agent with URI and optional metadata + /// @param agentURI The URI for the agent registration file + /// @param metadata Array of on-chain metadata entries (may be empty) + /// @return agentId The newly minted ERC-721 tokenId + function register(string calldata agentURI, MetadataEntry[] calldata metadata) external returns (uint256 agentId); + + /// @notice Registers a new agent with only a URI + /// @param agentURI The URI for the agent registration file + /// @return agentId The newly minted ERC-721 tokenId + function register(string calldata agentURI) external returns (uint256 agentId); + + /// @notice Registers a new agent without an initial URI + /// @return agentId The newly minted ERC-721 tokenId + function register() external returns (uint256 agentId); + + /// @notice Updates the URI for an existing agent + /// @param agentId The agent to update + /// @param newURI The new URI + function setAgentURI(uint256 agentId, string calldata newURI) external; + + /// @notice Sets on-chain metadata for an agent + /// @param agentId The agent to update + /// @param metadataKey The metadata key + /// @param metadataValue The metadata value + function setMetadata(uint256 agentId, string calldata metadataKey, bytes calldata metadataValue) external; + + /// @notice Returns on-chain metadata for an agent + /// @param agentId The agent to query + /// @param metadataKey The metadata key + /// @return The metadata value + function getMetadata(uint256 agentId, string calldata metadataKey) external view returns (bytes memory); + + /// @notice Sets the verified wallet address for an agent using EIP-712 / ERC-1271 + /// @param agentId The agent to update + /// @param newWallet The wallet address to verify + /// @param deadline Signature expiry timestamp + /// @param signature EIP-712 signature from `newWallet` + function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes calldata signature) external; + + /// @notice Returns the verified wallet address for an agent + /// @param agentId The agent to query + /// @return The verified wallet address, or address(0) if none is set + function getAgentWallet(uint256 agentId) external view returns (address); + + /// @notice Clears the verified wallet address for an agent + /// @param agentId The agent to update + function unsetAgentWallet(uint256 agentId) external; + + /// @notice Returns the total number of registered agents + function agentCount() external view returns (uint256); +} diff --git a/packages/identity/contracts/interfaces/core/IAgentReputationRegistry.sol b/packages/identity/contracts/interfaces/core/IAgentReputationRegistry.sol new file mode 100644 index 0000000..6856958 --- /dev/null +++ b/packages/identity/contracts/interfaces/core/IAgentReputationRegistry.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Reineira Labs Limited All rights reserved. +pragma solidity ^0.8.24; + +import {ICore} from "@reineira-os/shared/contracts/interfaces/core/ICore.sol"; + +/// @title IAgentReputationRegistry — ERC-8004 Reputation Registry +/// @notice Stores signed feedback and counter-evidence responses for agents. +interface IAgentReputationRegistry is ICore { + /// @notice Emitted when new feedback is submitted for an agent + event NewFeedback( + uint256 indexed agentId, + address indexed clientAddress, + uint64 feedbackIndex, + int128 value, + uint8 valueDecimals, + string indexed indexedTag1, + string tag1, + string tag2, + string endpoint, + string feedbackURI, + bytes32 feedbackHash + ); + + /// @notice Emitted when feedback is revoked by its original author + event FeedbackRevoked(uint256 indexed agentId, address indexed clientAddress, uint64 indexed feedbackIndex); + + /// @notice Emitted when a response is appended to existing feedback + event ResponseAppended( + uint256 indexed agentId, + address indexed clientAddress, + uint64 feedbackIndex, + address indexed responder, + string responseURI, + bytes32 responseHash + ); + + /// @notice Thrown when the referenced agent does not exist in the identity registry + error AgentNotFound(); + + /// @notice Thrown when valueDecimals is outside the 0–18 range + error InvalidValueDecimals(); + + /// @notice Thrown when the agent owner tries to give feedback on their own agent + error AgentOwnerCannotFeedback(); + + /// @notice Thrown when the requested feedback does not exist + error FeedbackNotFound(); + + /// @notice Thrown when trying to revoke already-revoked feedback + error AlreadyRevoked(); + + /// @notice Submits feedback for an agent + /// @param agentId The agent being reviewed + /// @param value Signed fixed-point value + /// @param valueDecimals Number of decimal places (0–18) + /// @param tag1 Optional categorization tag + /// @param tag2 Optional secondary tag + /// @param endpoint Optional endpoint URI related to the feedback + /// @param feedbackURI Optional URI to off-chain feedback file + /// @param feedbackHash KECCAK-256 hash of the off-chain file (0 for IPFS) + function giveFeedback( + uint256 agentId, + int128 value, + uint8 valueDecimals, + string calldata tag1, + string calldata tag2, + string calldata endpoint, + string calldata feedbackURI, + bytes32 feedbackHash + ) external; + + /// @notice Revokes previously submitted feedback + /// @param agentId The agent the feedback was for + /// @param feedbackIndex The 1-based index of the feedback to revoke + function revokeFeedback(uint256 agentId, uint64 feedbackIndex) external; + + /// @notice Appends counter-evidence or a response to existing feedback + /// @param agentId The agent the feedback was for + /// @param clientAddress The original feedback author + /// @param feedbackIndex The 1-based index of the feedback + /// @param responseURI Optional URI to off-chain response file + /// @param responseHash KECCAK-256 hash of the off-chain file (0 for IPFS) + function appendResponse( + uint256 agentId, + address clientAddress, + uint64 feedbackIndex, + string calldata responseURI, + bytes32 responseHash + ) external; + + /// @notice Reads a single feedback entry + /// @param agentId The agent the feedback was for + /// @param clientAddress The feedback author + /// @param feedbackIndex The 1-based index + /// @return value The signed fixed-point value + /// @return valueDecimals Number of decimal places + /// @return tag1 The primary tag + /// @return tag2 The secondary tag + /// @return isRevoked Whether the feedback has been revoked + function readFeedback( + uint256 agentId, + address clientAddress, + uint64 feedbackIndex + ) external view returns (int128 value, uint8 valueDecimals, string memory tag1, string memory tag2, bool isRevoked); + + /// @notice Returns the number of feedback entries a client has given for an agent + /// @param agentId The agent + /// @param clientAddress The client + /// @return The 1-based highest index (0 if none) + function getLastIndex(uint256 agentId, address clientAddress) external view returns (uint64); + + /// @notice Returns the number of responses appended to a feedback entry + /// @param agentId The agent + /// @param clientAddress The feedback author + /// @param feedbackIndex The feedback index + /// @return The number of responses + function getResponseCount( + uint256 agentId, + address clientAddress, + uint64 feedbackIndex + ) external view returns (uint64); + + /// @notice Returns the identity registry address used for agent validation + function getIdentityRegistry() external view returns (address); +} diff --git a/packages/identity/contracts/interfaces/core/IAgentValidationRegistry.sol b/packages/identity/contracts/interfaces/core/IAgentValidationRegistry.sol new file mode 100644 index 0000000..980266e --- /dev/null +++ b/packages/identity/contracts/interfaces/core/IAgentValidationRegistry.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Copyright (c) 2026 Reineira Labs Limited All rights reserved. +pragma solidity ^0.8.24; + +import {ICore} from "@reineira-os/shared/contracts/interfaces/core/ICore.sol"; + +/// @title IAgentValidationRegistry — ERC-8004 Validation Registry +/// @notice Enables agents to request verification and validators to provide on-chain responses. +interface IAgentValidationRegistry is ICore { + /// @notice Emitted when a validation request is created + event ValidationRequest( + address indexed validatorAddress, + uint256 indexed agentId, + string requestURI, + bytes32 indexed requestHash + ); + + /// @notice Emitted when a validator responds to a request + event ValidationResponse( + address indexed validatorAddress, + uint256 indexed agentId, + bytes32 indexed requestHash, + uint8 response, + string responseURI, + bytes32 responseHash, + string tag + ); + + /// @notice Thrown when the referenced agent does not exist + error AgentNotFound(); + + /// @notice Thrown when the validator address is zero + error InvalidValidator(); + + /// @notice Thrown when the request hash is not found + error RequestNotFound(); + + /// @notice Thrown when the caller is not the specified validator + error NotValidator(); + + /// @notice Thrown when the response value exceeds 100 + error InvalidResponse(); + + /// @notice Creates a validation request for an agent + /// @param validatorAddress The validator expected to respond + /// @param agentId The agent to validate + /// @param requestURI URI to off-chain validation data + /// @param requestHash KECCAK-256 hash of the request payload + function validationRequest( + address validatorAddress, + uint256 agentId, + string calldata requestURI, + bytes32 requestHash + ) external; + + /// @notice Submits a validator response to a request + /// @param requestHash The hash identifying the original request + /// @param response Score from 0–100 (binary or spectrum) + /// @param responseURI Optional URI to off-chain evidence + /// @param responseHash KECCAK-256 hash of the response file (0 for IPFS) + /// @param tag Optional categorization tag + function validationResponse( + bytes32 requestHash, + uint8 response, + string calldata responseURI, + bytes32 responseHash, + string calldata tag + ) external; + + /// @notice Returns the stored status for a validation request + /// @param requestHash The request hash + /// @return validatorAddress The validator assigned to the request + /// @return agentId The agent being validated + /// @return response The latest response value (0 if none) + /// @return responseHash The hash of the latest response evidence + /// @return tag The latest response tag + /// @return lastUpdate The timestamp of the latest response (0 if none) + function getValidationStatus( + bytes32 requestHash + ) + external + view + returns ( + address validatorAddress, + uint256 agentId, + uint8 response, + bytes32 responseHash, + string memory tag, + uint256 lastUpdate + ); + + /// @notice Returns all request hashes for a given agent + /// @param agentId The agent to query + /// @return requestHashes Array of request hashes + function getAgentValidations(uint256 agentId) external view returns (bytes32[] memory requestHashes); + + /// @notice Returns all request hashes for a given validator + /// @param validatorAddress The validator to query + /// @return requestHashes Array of request hashes + function getValidatorRequests(address validatorAddress) external view returns (bytes32[] memory requestHashes); + + /// @notice Returns the identity registry address used for agent validation + function getIdentityRegistry() external view returns (address); +} diff --git a/packages/identity/foundry.toml b/packages/identity/foundry.toml new file mode 100644 index 0000000..9709683 --- /dev/null +++ b/packages/identity/foundry.toml @@ -0,0 +1,26 @@ +[profile.default] +src = "contracts" +test = "test" +script = "script" +out = "out" +cache_path = "foundry-cache" +libs = ["node_modules", "../../node_modules"] + +remappings = [ + "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/", + "@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/", + "@reineira-os/shared/=node_modules/@reineira-os/shared/", + "forge-std/=../../node_modules/forge-std/src/", + "hardhat/=../../node_modules/forge-std/src/", +] + +solc_version = "0.8.25" +evm_version = "cancun" +optimizer = true +optimizer_runs = 200 +via_ir = true +isolate = true + +[profile.default.fuzz] +runs = 256 +max_test_rejects = 65536 diff --git a/packages/identity/package.json b/packages/identity/package.json new file mode 100644 index 0000000..eb188bf --- /dev/null +++ b/packages/identity/package.json @@ -0,0 +1,28 @@ +{ + "name": "@reineira-os/identity", + "version": "1.0.0", + "description": "Reineira Protocol -- ERC-8004 Trustless Agent Identity", + "scripts": { + "compile": "forge build", + "test": "forge test -vv", + "test:unit": "forge test --match-path 'test/unit/*' -vv", + "test:gas": "forge test --gas-report", + "test:coverage": "forge coverage", + "test:coverage:lcov": "forge coverage --report lcov --report-file lcov.info", + "clean": "forge clean", + "lint": "solhint 'contracts/**/*.sol'", + "format": "forge fmt" + }, + "keywords": [ + "web3", + "identity", + "erc-8004", + "agents" + ], + "license": "BUSL-1.1", + "dependencies": { + "@openzeppelin/contracts": "~5.2.0", + "@openzeppelin/contracts-upgradeable": "~5.2.0", + "@reineira-os/shared": "workspace:*" + } +} diff --git a/packages/identity/script/Deploy.s.sol b/packages/identity/script/Deploy.s.sol new file mode 100644 index 0000000..842e76d --- /dev/null +++ b/packages/identity/script/Deploy.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.25; + +import {Script, console} from "forge-std/Script.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {AgentIdentityRegistry} from "../contracts/core/AgentIdentityRegistry.sol"; +import {AgentReputationRegistry} from "../contracts/core/AgentReputationRegistry.sol"; +import {AgentValidationRegistry} from "../contracts/core/AgentValidationRegistry.sol"; + +contract DeployIdentity is Script { + function _deployProxy(address impl, bytes memory initData) internal returns (address) { + return address(new ERC1967Proxy(impl, initData)); + } + + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerKey); + address trustedForwarder = vm.envOr("TRUSTED_FORWARDER", address(0)); + + vm.startBroadcast(deployerKey); + + address identity = _deployProxy( + address(new AgentIdentityRegistry(trustedForwarder)), + abi.encodeCall(AgentIdentityRegistry.initialize, (deployer)) + ); + console.log("AgentIdentityRegistry:", identity); + + address reputation = _deployProxy( + address(new AgentReputationRegistry(trustedForwarder)), + abi.encodeCall(AgentReputationRegistry.initialize, (deployer, identity)) + ); + console.log("AgentReputationRegistry:", reputation); + + address validation = _deployProxy( + address(new AgentValidationRegistry(trustedForwarder)), + abi.encodeCall(AgentValidationRegistry.initialize, (deployer, identity)) + ); + console.log("AgentValidationRegistry:", validation); + + vm.stopBroadcast(); + } +} diff --git a/packages/identity/test/unit/AgentIdentityRegistry.t.sol b/packages/identity/test/unit/AgentIdentityRegistry.t.sol new file mode 100644 index 0000000..0205d07 --- /dev/null +++ b/packages/identity/test/unit/AgentIdentityRegistry.t.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {AgentIdentityRegistry} from "../../contracts/core/AgentIdentityRegistry.sol"; +import {IAgentIdentityRegistry} from "../../contracts/interfaces/core/IAgentIdentityRegistry.sol"; + +contract AgentIdentityRegistryTest is Test { + AgentIdentityRegistry public registry; + + address owner = makeAddr("owner"); + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + address wallet1 = makeAddr("wallet1"); + address wallet2 = makeAddr("wallet2"); + + function setUp() public { + vm.startPrank(owner); + AgentIdentityRegistry impl = new AgentIdentityRegistry(address(0)); + bytes memory initData = abi.encodeCall(AgentIdentityRegistry.initialize, (owner)); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + registry = AgentIdentityRegistry(address(proxy)); + vm.stopPrank(); + } + + function _computeDigest( + uint256 agentId, + address newWallet, + uint256 nonce, + uint256 deadline + ) internal view returns (bytes32) { + bytes32 domainSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes("Reineira Agent Identity")), + keccak256(bytes("1")), + block.chainid, + address(registry) + ) + ); + bytes32 structHash = keccak256( + abi.encode( + keccak256("SetAgentWallet(uint256 agentId,address newWallet,uint256 nonce,uint256 deadline)"), + agentId, + newWallet, + nonce, + deadline + ) + ); + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + // --- registration --- + + function test_register_withURIAndMetadata() public { + IAgentIdentityRegistry.MetadataEntry[] memory meta = new IAgentIdentityRegistry.MetadataEntry[](1); + meta[0] = IAgentIdentityRegistry.MetadataEntry({metadataKey: "role", metadataValue: abi.encode("executor")}); + + vm.prank(user1); + uint256 id = registry.register("ipfs://agent1", meta); + + assertEq(id, 0); + assertEq(registry.ownerOf(0), user1); + assertEq(registry.tokenURI(0), "ipfs://agent1"); + assertEq(registry.agentCount(), 1); + } + + function test_register_withURI() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent2"); + + assertEq(id, 0); + assertEq(registry.ownerOf(0), user1); + assertEq(registry.tokenURI(0), "ipfs://agent2"); + } + + function test_register_withoutURI() public { + vm.prank(user1); + uint256 id = registry.register(); + + assertEq(id, 0); + assertEq(registry.ownerOf(0), user1); + assertEq(registry.tokenURI(0), ""); + } + + function test_register_emitsRegisteredEvent() public { + vm.prank(user1); + vm.expectEmit(true, false, true, true); + emit IAgentIdentityRegistry.Registered(0, "ipfs://agent1", user1); + registry.register("ipfs://agent1"); + } + + function test_register_emitsMetadataSetForAgentWallet() public { + vm.prank(user1); + vm.expectEmit(true, true, false, true); + emit IAgentIdentityRegistry.MetadataSet(0, "agentWallet", "agentWallet", abi.encode(user1)); + registry.register("ipfs://agent1"); + } + + function test_register_multipleIncrementsAgentCount() public { + vm.prank(user1); + registry.register("ipfs://a"); + vm.prank(user2); + registry.register("ipfs://b"); + assertEq(registry.agentCount(), 2); + } + + // --- URI --- + + function test_setAgentURI_updatesURI() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://old"); + + vm.prank(user1); + registry.setAgentURI(id, "ipfs://new"); + assertEq(registry.tokenURI(id), "ipfs://new"); + } + + function test_setAgentURI_revertsForNonOwner() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://old"); + + vm.prank(user2); + vm.expectRevert(IAgentIdentityRegistry.NotAgentOwner.selector); + registry.setAgentURI(id, "ipfs://new"); + } + + function test_setAgentURI_revertsForNonExistentAgent() public { + vm.prank(user1); + vm.expectRevert(IAgentIdentityRegistry.AgentNotFound.selector); + registry.setAgentURI(99, "ipfs://new"); + } + + function test_setAgentURI_emitsEvent() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://old"); + + vm.prank(user1); + vm.expectEmit(true, false, true, true); + emit IAgentIdentityRegistry.URIUpdated(id, "ipfs://new", user1); + registry.setAgentURI(id, "ipfs://new"); + } + + // --- metadata --- + + function test_setMetadata_storesValue() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + vm.prank(user1); + registry.setMetadata(id, "version", abi.encode("1.0")); + + assertEq(registry.getMetadata(id, "version"), abi.encode("1.0")); + } + + function test_setMetadata_revertsForReservedKey() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + vm.prank(user1); + vm.expectRevert(IAgentIdentityRegistry.ReservedMetadataKey.selector); + registry.setMetadata(id, "agentWallet", abi.encode(address(1))); + } + + function test_setMetadata_revertsForNonOwner() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + vm.prank(user2); + vm.expectRevert(IAgentIdentityRegistry.NotAgentOwner.selector); + registry.setMetadata(id, "key", abi.encode("value")); + } + + function test_setMetadata_emitsEvent() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + vm.prank(user1); + vm.expectEmit(true, true, false, true); + emit IAgentIdentityRegistry.MetadataSet(id, "version", "version", abi.encode("1.0")); + registry.setMetadata(id, "version", abi.encode("1.0")); + } + + // --- agent wallet --- + + function test_setAgentWallet_withEOASignature() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + uint256 nonce = 0; + uint256 deadline = block.timestamp + 1 hours; + bytes32 digest = _computeDigest(id, wallet1, nonce, deadline); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(uint256(keccak256(abi.encodePacked("wallet1"))), digest); + bytes memory sig = abi.encodePacked(r, s, v); + + vm.prank(user1); + registry.setAgentWallet(id, wallet1, deadline, sig); + + assertEq(registry.getAgentWallet(id), wallet1); + } + + function test_setAgentWallet_emitsEvent() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + uint256 nonce = 0; + uint256 deadline = block.timestamp + 1 hours; + bytes32 digest = _computeDigest(id, wallet1, nonce, deadline); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(uint256(keccak256(abi.encodePacked("wallet1"))), digest); + bytes memory sig = abi.encodePacked(r, s, v); + + vm.prank(user1); + vm.expectEmit(true, true, false, false); + emit IAgentIdentityRegistry.AgentWalletSet(id, wallet1); + registry.setAgentWallet(id, wallet1, deadline, sig); + } + + function test_setAgentWallet_revertsWithExpiredDeadline() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + uint256 deadline = block.timestamp - 1; + bytes memory sig = hex""; + + vm.prank(user1); + vm.expectRevert(IAgentIdentityRegistry.SignatureExpired.selector); + registry.setAgentWallet(id, wallet1, deadline, sig); + } + + function test_setAgentWallet_revertsWithInvalidSignature() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + uint256 deadline = block.timestamp + 1 hours; + bytes + memory sig = hex"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + vm.prank(user1); + vm.expectRevert(IAgentIdentityRegistry.InvalidSignature.selector); + registry.setAgentWallet(id, wallet1, deadline, sig); + } + + function test_setAgentWallet_revertsForNonOwner() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + vm.prank(user2); + vm.expectRevert(IAgentIdentityRegistry.NotAgentOwner.selector); + registry.setAgentWallet(id, wallet1, block.timestamp + 1 hours, hex""); + } + + function test_setAgentWallet_revertsForAlreadySetSameWallet() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + uint256 nonce = 0; + uint256 deadline = block.timestamp + 1 hours; + bytes32 digest = _computeDigest(id, wallet1, nonce, deadline); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(uint256(keccak256(abi.encodePacked("wallet1"))), digest); + bytes memory sig = abi.encodePacked(r, s, v); + + vm.prank(user1); + registry.setAgentWallet(id, wallet1, deadline, sig); + + vm.prank(user1); + vm.expectRevert(IAgentIdentityRegistry.WalletAlreadySet.selector); + registry.setAgentWallet(id, wallet1, deadline, sig); + } + + function test_setAgentWallet_incrementsNonce() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + uint256 nonce1 = 0; + uint256 deadline = block.timestamp + 1 hours; + bytes32 digest1 = _computeDigest(id, wallet1, nonce1, deadline); + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(uint256(keccak256(abi.encodePacked("wallet1"))), digest1); + + vm.prank(user1); + registry.setAgentWallet(id, wallet1, deadline, abi.encodePacked(r1, s1, v1)); + + uint256 nonce2 = 1; + bytes32 digest2 = _computeDigest(id, wallet2, nonce2, deadline); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(uint256(keccak256(abi.encodePacked("wallet2"))), digest2); + + vm.prank(user1); + registry.setAgentWallet(id, wallet2, deadline, abi.encodePacked(r2, s2, v2)); + + assertEq(registry.getAgentWallet(id), wallet2); + } + + function test_unsetAgentWallet_clearsWallet() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + uint256 nonce = 0; + uint256 deadline = block.timestamp + 1 hours; + bytes32 digest = _computeDigest(id, wallet1, nonce, deadline); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(uint256(keccak256(abi.encodePacked("wallet1"))), digest); + + vm.prank(user1); + registry.setAgentWallet(id, wallet1, deadline, abi.encodePacked(r, s, v)); + assertEq(registry.getAgentWallet(id), wallet1); + + vm.prank(user1); + registry.unsetAgentWallet(id); + assertEq(registry.getAgentWallet(id), address(0)); + } + + function test_unsetAgentWallet_revertsWhenNotSet() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + vm.prank(user1); + vm.expectRevert(IAgentIdentityRegistry.WalletNotSet.selector); + registry.unsetAgentWallet(id); + } + + function test_unsetAgentWallet_revertsForNonOwner() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + vm.prank(user2); + vm.expectRevert(IAgentIdentityRegistry.NotAgentOwner.selector); + registry.unsetAgentWallet(id); + } + + function test_transfer_clearsAgentWallet() public { + vm.prank(user1); + uint256 id = registry.register("ipfs://agent"); + + uint256 nonce = 0; + uint256 deadline = block.timestamp + 1 hours; + bytes32 digest = _computeDigest(id, wallet1, nonce, deadline); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(uint256(keccak256(abi.encodePacked("wallet1"))), digest); + + vm.prank(user1); + registry.setAgentWallet(id, wallet1, deadline, abi.encodePacked(r, s, v)); + assertEq(registry.getAgentWallet(id), wallet1); + + vm.prank(user1); + registry.transferFrom(user1, user2, id); + + assertEq(registry.getAgentWallet(id), address(0)); + } + + // --- view errors --- + + function test_getAgentWallet_revertsForNonExistent() public { + vm.expectRevert(IAgentIdentityRegistry.AgentNotFound.selector); + registry.getAgentWallet(99); + } + + function test_getMetadata_revertsForNonExistent() public { + vm.expectRevert(IAgentIdentityRegistry.AgentNotFound.selector); + registry.getMetadata(99, "key"); + } + + function test_ownerOf_revertsForNonExistent() public { + vm.expectRevert(); + registry.ownerOf(99); + } +} diff --git a/packages/identity/test/unit/AgentReputationRegistry.t.sol b/packages/identity/test/unit/AgentReputationRegistry.t.sol new file mode 100644 index 0000000..6cb5d75 --- /dev/null +++ b/packages/identity/test/unit/AgentReputationRegistry.t.sol @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {AgentIdentityRegistry} from "../../contracts/core/AgentIdentityRegistry.sol"; +import {AgentReputationRegistry} from "../../contracts/core/AgentReputationRegistry.sol"; +import {IAgentReputationRegistry} from "../../contracts/interfaces/core/IAgentReputationRegistry.sol"; + +contract AgentReputationRegistryTest is Test { + AgentIdentityRegistry public identity; + AgentReputationRegistry public reputation; + + address owner = makeAddr("owner"); + address agentOwner = makeAddr("agentOwner"); + address client1 = makeAddr("client1"); + address client2 = makeAddr("client2"); + address responder = makeAddr("responder"); + + function setUp() public { + vm.startPrank(owner); + AgentIdentityRegistry identityImpl = new AgentIdentityRegistry(address(0)); + bytes memory identityInit = abi.encodeCall(AgentIdentityRegistry.initialize, (owner)); + ERC1967Proxy identityProxy = new ERC1967Proxy(address(identityImpl), identityInit); + identity = AgentIdentityRegistry(address(identityProxy)); + + AgentReputationRegistry reputationImpl = new AgentReputationRegistry(address(0)); + bytes memory reputationInit = abi.encodeCall(AgentReputationRegistry.initialize, (owner, address(identity))); + ERC1967Proxy reputationProxy = new ERC1967Proxy(address(reputationImpl), reputationInit); + reputation = AgentReputationRegistry(address(reputationProxy)); + vm.stopPrank(); + } + + function _registerAgent(address registrant) internal returns (uint256) { + vm.prank(registrant); + return identity.register("ipfs://agent"); + } + + // --- giveFeedback --- + + function test_giveFeedback_storesFeedback() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback( + agentId, + 87, + 0, + "quality", + "v1", + "https://api.example.com", + "ipfs://feedback", + bytes32(0) + ); + + (int128 value, uint8 decimals, string memory tag1, string memory tag2, bool isRevoked) = reputation + .readFeedback(agentId, client1, 1); + + assertEq(value, 87); + assertEq(decimals, 0); + assertEq(tag1, "quality"); + assertEq(tag2, "v1"); + assertFalse(isRevoked); + } + + function test_giveFeedback_incrementsLastIndex() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + assertEq(reputation.getLastIndex(agentId, client1), 1); + + vm.prank(client1); + reputation.giveFeedback(agentId, 92, 0, "", "", "", "", bytes32(0)); + assertEq(reputation.getLastIndex(agentId, client1), 2); + } + + function test_giveFeedback_emitsNewFeedbackEvent() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + vm.expectEmit(true, true, false, true); + emit IAgentReputationRegistry.NewFeedback( + agentId, + client1, + 1, + 87, + 0, + "quality", + "quality", + "v1", + "https://api.example.com", + "ipfs://feedback", + bytes32(0) + ); + reputation.giveFeedback( + agentId, + 87, + 0, + "quality", + "v1", + "https://api.example.com", + "ipfs://feedback", + bytes32(0) + ); + } + + function test_giveFeedback_revertsForAgentOwner() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(agentOwner); + vm.expectRevert(IAgentReputationRegistry.AgentOwnerCannotFeedback.selector); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + } + + function test_giveFeedback_revertsForInvalidDecimals() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + vm.expectRevert(IAgentReputationRegistry.InvalidValueDecimals.selector); + reputation.giveFeedback(agentId, 87, 19, "", "", "", "", bytes32(0)); + } + + function test_giveFeedback_revertsForNonExistentAgent() public { + vm.prank(client1); + vm.expectRevert(IAgentReputationRegistry.AgentNotFound.selector); + reputation.giveFeedback(99, 87, 0, "", "", "", "", bytes32(0)); + } + + // --- revokeFeedback --- + + function test_revokeFeedback_marksAsRevoked() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + + vm.prank(client1); + reputation.revokeFeedback(agentId, 1); + + (, , , , bool isRevoked) = reputation.readFeedback(agentId, client1, 1); + assertTrue(isRevoked); + } + + function test_revokeFeedback_emitsEvent() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + + vm.prank(client1); + vm.expectEmit(true, true, true, false); + emit IAgentReputationRegistry.FeedbackRevoked(agentId, client1, 1); + reputation.revokeFeedback(agentId, 1); + } + + function test_revokeFeedback_revertsForNonAuthor() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + + vm.prank(client2); + vm.expectRevert(IAgentReputationRegistry.FeedbackNotFound.selector); + reputation.revokeFeedback(agentId, 1); + } + + function test_revokeFeedback_revertsForAlreadyRevoked() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + + vm.prank(client1); + reputation.revokeFeedback(agentId, 1); + + vm.prank(client1); + vm.expectRevert(IAgentReputationRegistry.AlreadyRevoked.selector); + reputation.revokeFeedback(agentId, 1); + } + + function test_revokeFeedback_revertsForIndexZero() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + vm.expectRevert(IAgentReputationRegistry.FeedbackNotFound.selector); + reputation.revokeFeedback(agentId, 0); + } + + function test_revokeFeedback_revertsForOutOfBounds() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + vm.expectRevert(IAgentReputationRegistry.FeedbackNotFound.selector); + reputation.revokeFeedback(agentId, 1); + } + + // --- appendResponse --- + + function test_appendResponse_addsResponse() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + + vm.prank(responder); + reputation.appendResponse(agentId, client1, 1, "ipfs://response", bytes32(0)); + + assertEq(reputation.getResponseCount(agentId, client1, 1), 1); + } + + function test_appendResponse_emitsEvent() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + + vm.prank(responder); + vm.expectEmit(true, true, true, false); + emit IAgentReputationRegistry.ResponseAppended(agentId, client1, 1, responder, "ipfs://response", bytes32(0)); + reputation.appendResponse(agentId, client1, 1, "ipfs://response", bytes32(0)); + } + + function test_appendResponse_revertsForIndexZero() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(responder); + vm.expectRevert(IAgentReputationRegistry.FeedbackNotFound.selector); + reputation.appendResponse(agentId, client1, 0, "ipfs://response", bytes32(0)); + } + + function test_appendResponse_revertsForOutOfBounds() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(responder); + vm.expectRevert(IAgentReputationRegistry.FeedbackNotFound.selector); + reputation.appendResponse(agentId, client1, 1, "ipfs://response", bytes32(0)); + } + + function test_appendResponse_allowsMultipleResponses() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + + vm.prank(responder); + reputation.appendResponse(agentId, client1, 1, "ipfs://r1", bytes32(0)); + + vm.prank(agentOwner); + reputation.appendResponse(agentId, client1, 1, "ipfs://r2", bytes32(0)); + + assertEq(reputation.getResponseCount(agentId, client1, 1), 2); + } + + // --- views --- + + function test_readFeedback_revertsForIndexZero() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.expectRevert(IAgentReputationRegistry.FeedbackNotFound.selector); + reputation.readFeedback(agentId, client1, 0); + } + + function test_readFeedback_revertsForOutOfBounds() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.expectRevert(IAgentReputationRegistry.FeedbackNotFound.selector); + reputation.readFeedback(agentId, client1, 1); + } + + function test_getLastIndex_returnsZeroForNewClient() public { + uint256 agentId = _registerAgent(agentOwner); + + assertEq(reputation.getLastIndex(agentId, client1), 0); + } + + function test_getResponseCount_returnsZeroForNoResponses() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + + assertEq(reputation.getResponseCount(agentId, client1, 1), 0); + } + + function test_getResponseCount_returnsZeroForIndexZero() public { + uint256 agentId = _registerAgent(agentOwner); + assertEq(reputation.getResponseCount(agentId, client1, 0), 0); + } + + function test_getIdentityRegistry_returnsCorrectAddress() public view { + assertEq(reputation.getIdentityRegistry(), address(identity)); + } + + function test_getClients_returnsFeedbackProviders() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(client1); + reputation.giveFeedback(agentId, 87, 0, "", "", "", "", bytes32(0)); + + vm.prank(client2); + reputation.giveFeedback(agentId, 92, 0, "", "", "", "", bytes32(0)); + + address[] memory clients = reputation.getClients(agentId); + assertEq(clients.length, 2); + assertEq(clients[0], client1); + assertEq(clients[1], client2); + } +} diff --git a/packages/identity/test/unit/AgentValidationRegistry.t.sol b/packages/identity/test/unit/AgentValidationRegistry.t.sol new file mode 100644 index 0000000..10602de --- /dev/null +++ b/packages/identity/test/unit/AgentValidationRegistry.t.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {AgentIdentityRegistry} from "../../contracts/core/AgentIdentityRegistry.sol"; +import {AgentValidationRegistry} from "../../contracts/core/AgentValidationRegistry.sol"; +import {IAgentValidationRegistry} from "../../contracts/interfaces/core/IAgentValidationRegistry.sol"; +import {ICore} from "@reineira-os/shared/contracts/interfaces/core/ICore.sol"; + +contract AgentValidationRegistryTest is Test { + AgentIdentityRegistry public identity; + AgentValidationRegistry public validation; + + address owner = makeAddr("owner"); + address agentOwner = makeAddr("agentOwner"); + address validator = makeAddr("validator"); + address other = makeAddr("other"); + + function setUp() public { + vm.startPrank(owner); + AgentIdentityRegistry identityImpl = new AgentIdentityRegistry(address(0)); + bytes memory identityInit = abi.encodeCall(AgentIdentityRegistry.initialize, (owner)); + ERC1967Proxy identityProxy = new ERC1967Proxy(address(identityImpl), identityInit); + identity = AgentIdentityRegistry(address(identityProxy)); + + AgentValidationRegistry validationImpl = new AgentValidationRegistry(address(0)); + bytes memory validationInit = abi.encodeCall(AgentValidationRegistry.initialize, (owner, address(identity))); + ERC1967Proxy validationProxy = new ERC1967Proxy(address(validationImpl), validationInit); + validation = AgentValidationRegistry(address(validationProxy)); + vm.stopPrank(); + } + + function _registerAgent(address registrant) internal returns (uint256) { + vm.prank(registrant); + return identity.register("ipfs://agent"); + } + + // --- validationRequest --- + + function test_validationRequest_createsRequest() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + + (address v, , uint8 response, bytes32 responseHash, string memory tag, uint256 lastUpdate) = validation + .getValidationStatus(requestHash); + + assertEq(v, validator); + assertEq(response, 0); + assertEq(responseHash, bytes32(0)); + assertEq(tag, ""); + assertEq(lastUpdate, 0); + } + + function test_validationRequest_emitsEvent() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + vm.expectEmit(true, true, false, true); + emit IAgentValidationRegistry.ValidationRequest(validator, agentId, "ipfs://request", requestHash); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + } + + function test_validationRequest_tracksAgentValidations() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + + bytes32[] memory hashes = validation.getAgentValidations(agentId); + assertEq(hashes.length, 1); + assertEq(hashes[0], requestHash); + } + + function test_validationRequest_tracksValidatorRequests() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + + bytes32[] memory hashes = validation.getValidatorRequests(validator); + assertEq(hashes.length, 1); + assertEq(hashes[0], requestHash); + } + + function test_validationRequest_revertsForNonOwner() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(other); + vm.expectRevert(ICore.Unauthorized.selector); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + } + + function test_validationRequest_revertsForZeroValidator() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + vm.expectRevert(IAgentValidationRegistry.InvalidValidator.selector); + validation.validationRequest(address(0), agentId, "ipfs://request", requestHash); + } + + function test_validationRequest_revertsForZeroHash() public { + uint256 agentId = _registerAgent(agentOwner); + + vm.prank(agentOwner); + vm.expectRevert(ICore.ZeroAddress.selector); + validation.validationRequest(validator, agentId, "ipfs://request", bytes32(0)); + } + + function test_validationRequest_revertsForDuplicateHash() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + + vm.prank(agentOwner); + vm.expectRevert(IAgentValidationRegistry.InvalidValidator.selector); + validation.validationRequest(validator, agentId, "ipfs://request2", requestHash); + } + + // --- validationResponse --- + + function test_validationResponse_storesResponse() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + + vm.prank(validator); + validation.validationResponse(requestHash, 100, "ipfs://response", bytes32(0), "passed"); + + (, , uint8 response, , string memory tag, uint256 lastUpdate) = validation.getValidationStatus(requestHash); + + assertEq(response, 100); + assertEq(tag, "passed"); + assertGt(lastUpdate, 0); + } + + function test_validationResponse_emitsEvent() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + + vm.prank(validator); + vm.expectEmit(true, true, true, true); + emit IAgentValidationRegistry.ValidationResponse( + validator, + agentId, + requestHash, + 100, + "ipfs://response", + bytes32(0), + "passed" + ); + validation.validationResponse(requestHash, 100, "ipfs://response", bytes32(0), "passed"); + } + + function test_validationResponse_allowsMultipleResponses() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + + vm.prank(validator); + validation.validationResponse(requestHash, 50, "ipfs://response1", bytes32(0), "partial"); + + vm.prank(validator); + validation.validationResponse(requestHash, 100, "ipfs://response2", bytes32(0), "final"); + + (, , uint8 response, , string memory tag, ) = validation.getValidationStatus(requestHash); + assertEq(response, 100); + assertEq(tag, "final"); + } + + function test_validationResponse_revertsForNonValidator() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + + vm.prank(other); + vm.expectRevert(IAgentValidationRegistry.NotValidator.selector); + validation.validationResponse(requestHash, 100, "ipfs://response", bytes32(0), "passed"); + } + + function test_validationResponse_revertsForInvalidResponse() public { + uint256 agentId = _registerAgent(agentOwner); + bytes32 requestHash = keccak256("request1"); + + vm.prank(agentOwner); + validation.validationRequest(validator, agentId, "ipfs://request", requestHash); + + vm.prank(validator); + vm.expectRevert(IAgentValidationRegistry.InvalidResponse.selector); + validation.validationResponse(requestHash, 101, "ipfs://response", bytes32(0), "passed"); + } + + function test_validationResponse_revertsForUnknownHash() public { + vm.prank(validator); + vm.expectRevert(IAgentValidationRegistry.RequestNotFound.selector); + validation.validationResponse(keccak256("unknown"), 100, "ipfs://response", bytes32(0), "passed"); + } + + // --- views --- + + function test_getValidationStatus_revertsForUnknownHash() public { + vm.expectRevert(IAgentValidationRegistry.RequestNotFound.selector); + validation.getValidationStatus(keccak256("unknown")); + } + + function test_getAgentValidations_returnsEmptyForNewAgent() public view { + bytes32[] memory hashes = validation.getAgentValidations(99); + assertEq(hashes.length, 0); + } + + function test_getValidatorRequests_returnsEmptyForNewValidator() public view { + bytes32[] memory hashes = validation.getValidatorRequests(validator); + assertEq(hashes.length, 0); + } + + function test_getIdentityRegistry_returnsCorrectAddress() public view { + assertEq(validation.getIdentityRegistry(), address(identity)); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a788765..b0bfdcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,18 @@ importers: specifier: github:FhenixProtocol/fhenix-confidential-contracts#0823e57e320c1c72e0caee7faeebc4d3a0710373 version: https://codeload.github.com/FhenixProtocol/fhenix-confidential-contracts/tar.gz/0823e57e320c1c72e0caee7faeebc4d3a0710373(ethers@6.16.0)(typechain@8.3.2(typescript@5.9.3))(typescript@5.9.3) + packages/identity: + dependencies: + '@openzeppelin/contracts': + specifier: ~5.2.0 + version: 5.2.0 + '@openzeppelin/contracts-upgradeable': + specifier: ~5.2.0 + version: 5.2.0(@openzeppelin/contracts@5.2.0) + '@reineira-os/shared': + specifier: workspace:* + version: link:../shared + packages/offchain: devDependencies: '@typescript-eslint/eslint-plugin': From 2c7faa559e1c372638064cf5f7a51557005061c6 Mon Sep 17 00:00:00 2001 From: madschristensen99 Date: Thu, 11 Jun 2026 10:18:14 -0400 Subject: [PATCH 2/2] feat(AP-4): define IAgenticJob neutral lifecycle interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Phase enum: Open → Accepted → Submitted → Evaluated → Settled → Refunded - Add JobView struct with uint256[] coverageIds (AP-15 multi-coverage compatible) - Add JobPhaseChanged event and jobView(bytes32) view function - Explicitly document: neutral lifecycle on interface, adapters carry standards opinion --- .../interfaces/plugins/IAgenticJob.sol | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 packages/shared/contracts/interfaces/plugins/IAgenticJob.sol diff --git a/packages/shared/contracts/interfaces/plugins/IAgenticJob.sol b/packages/shared/contracts/interfaces/plugins/IAgenticJob.sol new file mode 100644 index 0000000..3d0e0bd --- /dev/null +++ b/packages/shared/contracts/interfaces/plugins/IAgenticJob.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Reineira Labs Limited All rights reserved. +pragma solidity ^0.8.24; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/// @title IAgenticJob — Neutral lifecycle interface for agentic job execution +/// @notice Defines a standard-agnostic lifecycle that all job adapters implement. +/// Adapters (EIP8183Adapter, ARSAdapter, AgentInvocationAdapter) carry the +/// standards-specific opinion; this interface remains neutral. +/// @dev Implementations must support ERC-165 so callers can validate the interface. +/// The JobView struct is designed to be compatible with the multi-coverage +/// extension (AP-15): `coverageIds` is an array to accommodate multiple +/// coverage positions per escrow. +interface IAgenticJob is IERC165 { + /// @notice Lifecycle phases of an agentic job + enum Phase { + Open, + Accepted, + Submitted, + Evaluated, + Settled, + Refunded + } + + /// @notice Immutable snapshot of a job's identity and current phase + /// @dev `coverageIds` is an array to support multi-coverage per escrow (AP-15). + /// An empty array indicates no coverage has been purchased. + struct JobView { + address client; + address provider; + uint256 escrowId; + uint256[] coverageIds; + bytes32 standard; + Phase phase; + } + + /// @notice Emitted when a job transitions to a new phase + /// @param jobId The unique job identifier + /// @param phase The new phase + event JobPhaseChanged(bytes32 indexed jobId, Phase indexed phase); + + /// @notice Returns the immutable view of a job + /// @param jobId The job identifier to query + /// @return The JobView struct for the given jobId + function jobView(bytes32 jobId) external view returns (JobView memory); +}