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.
205 lines
8.3 KiB
Solidity
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);
|
|
}
|
|
}
|