aere-contracts/contracts/agentic/AereAgentMemoryVault.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

205 lines
8.3 KiB
Solidity

// 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);
}
}