// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @title AereAgentMemoryVault — portable, user-owned AI agent memory * @notice Today every AI assistant rebuilds your context from scratch. * Your ChatGPT history, your Claude preferences, the tone your * trading bot has learned about you — none of it travels. Switch * provider, the memory dies. The vendor owns the relationship. * * AereAgentMemoryVault flips that ownership. The USER (not the * provider) is the namespace owner. Agents read and write to * per-user slots under EXPLICIT user permission. When the user * switches provider, the new agent gets read access to the same * history with one signature. Privacy is preserved because the * memory blobs themselves are ENCRYPTED off-chain — only * commitments + access-control live on chain. * * MODEL: * * 1. Each user has a private namespace (keyed by their address). * Within it, named "slots" hold versioned memory entries. * * 2. The user grants Permission to an agent address. A Permission * specifies (agent, slot, write?, validUntil). Multiple * permissions can coexist (read-only "memory consumer" agent * + read/write "memory builder" agent). * * 3. An authorised agent writes an entry: a content hash + an * opaque pointer (ipfs:// to the encrypted blob). The chain * stores ONLY the commitment. Encryption keys are off-chain * and exchanged out-of-band (e.g. via ECIES to the agent's * registered key, or via a passkey-bound user device). * * 4. The user can REVOKE any permission at any time. The * revocation is immediate; an agent's next write attempt * reverts. Past entries written under that permission stay * in history (the chain remembers; only the agent's ability * to read goes away — the user can rotate keys to actually * cut access). * * 5. PORTABILITY: when a user grants a fresh agent read access * to ALL their slots, that agent immediately sees the full * memory tree. The chain emits enough events for an indexer * to reconstruct the canonical slot → latest-entry map. * * WHAT THE CHAIN DOES NOT DO: * - Does NOT decrypt anything. Keys live with the user / agent. * - Does NOT enforce schema. Slots are just bytes32 names. * - Does NOT charge fees per write. (Gas is the only cost.) * * IMMUTABILITY: * - No admin, no Foundation override. The user is the only * authority over their own namespace. * - Permissions table is append-only via events; the latest * permission state per (user, agent, slot) is the source of * truth. */ contract AereAgentMemoryVault { /* ================================ types ================================ */ struct Permission { bool canRead; bool canWrite; uint64 validUntil; // 0 = no expiry bool exists; // true after first grant; never reset } struct Entry { bytes32 contentHash; // SHA256 of the encrypted blob string blobUri; // ipfs:// or https:// (encrypted) address writer; uint64 version; uint64 writtenAt; } /* ================================ storage ================================ */ /// @notice user → agent → slot → Permission. Per-slot grain; wildcard /// per (user, agent) NOT supported — agents must enumerate the /// exact slots they're given access to. mapping(address => mapping(address => mapping(bytes32 => Permission))) internal _perm; /// @notice user → slot → next version counter. Starts at 1. mapping(address => mapping(bytes32 => uint64)) public nextVersion; /// @notice user → slot → latest Entry. Indexers can reconstruct full /// history from events; the chain keeps only the head. mapping(address => mapping(bytes32 => Entry)) internal _head; /* ================================ events ================================ */ event PermissionSet( address indexed user, address indexed agent, bytes32 indexed slot, bool canRead, bool canWrite, uint64 validUntil ); event PermissionRevoked(address indexed user, address indexed agent, bytes32 indexed slot); event EntryWritten( address indexed user, bytes32 indexed slot, address indexed agent, uint64 version, bytes32 contentHash, string blobUri ); /* ================================ errors ================================ */ error NotWriter(); error PermissionExpired(); error NoSuchPermission(); error EmptyHash(); error InvalidValidity(); /* =========================== user-side permission ========================= */ /// @notice User grants (or updates) a per-slot permission to an agent. /// msg.sender == user. validUntil = 0 means no expiry. function grant( address agent, bytes32 slot, bool canRead, bool canWrite, uint64 validUntil ) external { if (validUntil != 0 && validUntil <= block.timestamp) revert InvalidValidity(); Permission storage p = _perm[msg.sender][agent][slot]; p.canRead = canRead; p.canWrite = canWrite; p.validUntil = validUntil; p.exists = true; emit PermissionSet(msg.sender, agent, slot, canRead, canWrite, validUntil); } /// @notice User revokes a permission. Immediate effect on writes. function revoke(address agent, bytes32 slot) external { Permission storage p = _perm[msg.sender][agent][slot]; if (!p.exists) revert NoSuchPermission(); p.canRead = false; p.canWrite = false; // We KEEP `exists` true so subsequent grants don't accidentally // look like brand-new attestations; just zero the rights. emit PermissionRevoked(msg.sender, agent, slot); } /* =============================== writes =============================== */ /// @notice Agent writes a memory entry for `user` at `slot`. msg.sender /// must have an unexpired canWrite permission. function write( address user, bytes32 slot, bytes32 contentHash, string calldata blobUri ) external returns (uint64 version) { if (contentHash == bytes32(0)) revert EmptyHash(); Permission storage p = _perm[user][msg.sender][slot]; if (!p.canWrite) revert NotWriter(); if (p.validUntil != 0 && block.timestamp > p.validUntil) revert PermissionExpired(); version = ++nextVersion[user][slot]; _head[user][slot] = Entry({ contentHash: contentHash, blobUri: blobUri, writer: msg.sender, version: version, writtenAt: uint64(block.timestamp) }); emit EntryWritten(user, slot, msg.sender, version, contentHash, blobUri); } /* ================================ views ================================ */ function permissionOf(address user, address agent, bytes32 slot) external view returns ( bool canRead, bool canWrite, uint64 validUntil, bool exists ) { Permission storage p = _perm[user][agent][slot]; return (p.canRead, p.canWrite, p.validUntil, p.exists); } /// @notice True if `agent` can currently read this slot (canRead + /// not expired). Used by off-chain clients before requesting /// the encrypted blob from the user's key-exchange service. function canReadNow(address user, address agent, bytes32 slot) external view returns (bool) { Permission storage p = _perm[user][agent][slot]; if (!p.canRead) return false; if (p.validUntil != 0 && block.timestamp > p.validUntil) return false; return true; } function headOf(address user, bytes32 slot) external view returns ( bytes32 contentHash, string memory blobUri, address writer, uint64 version, uint64 writtenAt ) { Entry storage e = _head[user][slot]; return (e.contentHash, e.blobUri, e.writer, e.version, e.writtenAt); } }