diff --git a/README.md b/README.md index 6a7dd16..37d2bae 100644 --- a/README.md +++ b/README.md @@ -130,22 +130,37 @@ argued from taste. ## What it is -Two halves around one record. The **memory server** is what an agent harness reaches -over MCP: it serves the record by summaries first, holds the task ledger, and checks every -read and write against the policy. The **launcher** runs a harness confined, with no -network except the **egress**, which forwards its model calls to a provider or a gateway, -meters them, holds the credentials, enforces spend limits and records each call's -decision context. An agent starts empty, loads a small index called T0, works a task it -claimed, and writes a handoff when it stops; the next agent continues from the handoff, -not a transcript. Around them, the **librarian** curates the record on a budget, the -**perimeter** decides what may be read, written and admitted, and a person promotes what +Several parts around one record. No one of them is the safety boundary on its own. + +- The **memory server** is what an agent harness reaches over MCP. It serves the record + by summaries first, holds the task ledger, and checks every read and write against the + policy. In a launch, it gives the harness nothing above the **ceiling** of its model's + destination: the highest label that may be sent there. +- The **launcher** runs a harness in a **sandbox**, which has no network. The one thing + the harness can reach is the egress. +- The **egress** carries the memory server's MCP to the harness and forwards the + harness's model calls to a provider or a gateway. It holds each call to its + destination's ceiling. A call it forwards is also metered, held to its spend limit, + and recorded with its decision context. A call it tunnels, as for Copilot, for Codex's + ChatGPT login and for the Grok Build and Antigravity logins, is checked against the + ceiling when the tunnel opens and is not metered. The egress holds the credentials, + unless the harness runs on its own subscription login. +- The **tools**, fetch, search and the other tool taps, run outside the sandbox: in the + `amem` process, on a tool server called through the egress, or in the owner's browser. + Each decides every request against the session's label. +- The **perimeter** is the policy over `grants.toml` and the record's policy entries. The + memory server, the egress and the tools all enforce it. + +An agent starts empty, loads a small index called T0, works a task it claimed, and writes +a handoff when it stops; the next agent continues from the handoff, not a transcript. +Around them, the **librarian** curates the record on a budget, and a person promotes what the system may rely on. ``` person ──▶ amem: web launcher, CLI, profiles │ launch / work (a claimed task) ▼ - ┌─ confined harness, no network ───────┐ + ┌─ sandbox: harness, no network ───────┐ │ Claude Code, Codex, Gemini CLI, ... │──MCP──▶ memory server ──▶ record/ entries, transcripts, contexts, tapes └──────────────┬───────────────────────┘ │ policy: ├──▶ index SQLite FTS5, BM25 │ model calls │ Rego ├──▶ ledger SQLite, or PostgreSQL @@ -154,6 +169,9 @@ the system may rely on. metered, spend-limited, contexts recorded └── librarian: curation, checker, canaries, drift ``` +The MCP arrow is drawn straight to the memory server, but it too passes through the +egress, the harness's one way out. + Everything lives under one root, `~/memory` unless `--root` or `MEMORY_ROOT` says otherwise, and the process touches nothing outside it: @@ -216,7 +234,7 @@ pi, in native Linux namespaces from the harness image once `make unpack` has put place (every one but Antigravity is in it), under WSL containers, or, not yet run on a Mac, under Seatbelt on macOS; auth is `key`, `gateway`, or `subscription`, the harness's own login, which the egress passes -through and meters as plan quota. [local/README.md](local/README.md) is the full guide. +through and, where it forwards rather than tunnels, meters as plan quota. [local/README.md](local/README.md) is the full guide. Or point a harness at the memory server alone over stdio, unconfined: @@ -226,6 +244,11 @@ claude mcp add memory -- /path/to/amem --root ~/memory serve --author "agent:cla The agent gets `t0`, `search`, `get`, `write`, `list`, `recall`, `promote`, the ledger's tools and `transcript_turn`, and its writes land at raw trust under the author given. +Such a harness has no sandbox, and its model calls do not go through an egress. The +policy still decides what it reads and writes over MCP. But the harness keeps its own +network and credentials, nothing meters its model calls, and no model's ceiling caps what +it may read. `amem launch --unconfined` is the middle case, for Claude Code and Codex so +far: no sandbox, but the model calls still go through the egress. ## The parts, and their commands diff --git a/docs/reference-stack.md b/docs/reference-stack.md index e70058b..d65c366 100644 --- a/docs/reference-stack.md +++ b/docs/reference-stack.md @@ -19,13 +19,17 @@ Two things fall out of this for free. One materializer per domain, which the pap | Index | Apache Solr, shard key = domain | Lucene BM25 for ranked text, the compiled filter expressed as a query filter, dense vectors as a derived field in the same engine. Index-served searches in single-digit ms. Shard by domain so a session's filter hits one shard | One Solr container. OpenSearch is the non-Apache equivalent if operations prefer it | | Ledger and lease cache | PostgreSQL, one schema per domain | A claim is one statement with row locking and `SKIP LOCKED`: atomic, a few ms. Domains are independent, so partitions and replicas scale by domain. Rebuildable from the log | One container | | Policy engine | OPA as a sidecar, registry and grants as data | Partial evaluation compiles the session filter once; every later check is local. The filter is pushed into the Solr query, so denied entries are never scored | One sidecar per memory-server replica | -| Memory server | A stateless service: MCP and HTTP surface, compiled filter and T0 cached per session, ceiling check per outbound call | Replicas behind a load balancer. T0 is a prefix the harness caches, so orientation costs zero tool calls | One container, scale by count | +| Memory server | A stateless service: MCP and HTTP surface, compiled filter and T0 cached per session, the read and write checks, reads capped at the model's ceiling in a launch | Replicas behind a load balancer. T0 is a prefix the harness caches, so orientation costs zero tool calls | One container, scale by count | +| Launcher and sandbox | Ours: each harness confined with Linux namespaces, WSL containers, or Seatbelt on macOS (not yet run on a Mac), with no network but the egress. `--unconfined` drops the sandbox and keeps the egress, for a platform without a sandbox backend; it runs Claude Code and Codex so far | Runs where the harness runs, one per session, so it scales with the people and agents working rather than with the stack | The `amem` binary | +| Egress | Ours: the confined harness's only network. It serves the memory server's MCP and forwards model calls: ceiling check per call, credentials injected, spend limits, metering, decision contexts recorded. A tunnelled call (Copilot, and some subscription logins) is checked when the tunnel opens and not metered. It also carries the launcher's calls to hosted tool servers | One beside each confined session, forwarding to LiteLLM for models and to tool servers for hosted taps; a loopback relay and a unix socket between it and the harness | Part of `amem`; under WSL, its own container, which also runs the memory server | | Recall deputy, librarians, auditors | Agents on Gofannon, cheap tier through llmao | Already how the platform runs. Librarians are a consumer group on the log like any other | The existing Gofannon and LiteLLM containers | | Taps | MCP servers, one per tracker or list, each registered with a ceiling; as WASI components hosted by the memory server (section 4) | Contributed connectors run sandboxed with exactly the network and filesystem the grant allows | Containers, or components loaded by the server | | Telemetry and analytics | OpenTelemetry to Kafka; Apache Flink or Spark streaming into Apache Iceberg tables on the same object store; queried with Apache DataFusion or Trino | Columnar, append-heavy, cross-corpus. Iceberg snapshots give time travel over telemetry and over the entry metadata table. This is where cost per task, co-retrieval mining, decay sweeps, and lineage queries live | OTel collector writing Parquet; DuckDB or DataFusion to query. Flink when volume justifies it | | Interchange | Markdown bundle with a manifest; Apache Arrow for bulk metadata transport | Arrow is the columnar wire format the analytics side already speaks | Same | | Identity | Authentik for the institution; nobody for a single person | Lives in the serving layer, so the local stack runs with it off | Optional container | +No one row is the safety boundary. The sandbox gives the harness no network. Of the host it sees only what the launch mounts: the system's binaries and configuration, the harness, the memory binary and the record's projection, all read-only; a few device nodes; a private home and its working tree; and the egress's socket. The one thing it can reach is the egress. The egress carries the memory server's MCP to the harness, decides where each model call may go, and holds the credentials unless the harness runs on its own subscription login. The memory server checks what is read and written. The tools run outside the sandbox, in the launcher, on a tool server or in the owner's browser, and decide every request against the session's label. The memory server, the egress and the tools enforce the grants. A harness that reaches the memory server without the launcher keeps the memory server's checks, and the harness's own calls lose the sandbox and the egress. + ## 3. Latency on the hot path comes from what is never on it - **Orientation** is a cached prefix: T0 and the policy digest arrive with the session, not as tool calls. @@ -37,7 +41,7 @@ Two things fall out of this for free. One materializer per domain, which the pap ## 4. Language: Rust, one core library compiled three ways -The parts that are ours: the memory server, the materializer, the indexer, the ledger projection, taps, the personal server, the CLI, and a browser UI. The recommendation is Rust, with a single core library that compiles to native for Windows and Linux, to WebAssembly for the browser, and to WASI for sandboxed plugins. +The parts that are ours: the memory server, the launcher with its sandbox, the egress, the materializer, the indexer, the ledger projection, taps, the personal server, the CLI, and a browser UI. The recommendation is Rust, with a single core library that compiles to native for Windows and Linux, to WebAssembly for the browser, and to WASI for sandboxed plugins. **Why Rust fits this stack.** The Apache libraries involved are Rust-native: OpenDAL is written in Rust, so is DataFusion, Arrow has a first-class Rust implementation, and the Iceberg Rust library is maturing quickly. Tantivy gives embedded BM25 for the personal server, where Solr is too heavy; Solr itself is an HTTP client. SQLite with FTS5 through rusqlite, Postgres through sqlx, Kafka through the librdkafka bindings. There is an official Rust MCP SDK. One static binary per platform, no runtime to install, Windows a tier-one target. The cost is velocity: services take longer to write than in Go, and async Rust has a learning curve. @@ -59,7 +63,7 @@ So the browser is the person's window onto their own memory and the Foundation's ## 6. Two deployments from one codebase -**The Foundation's reference deployment** is the compose file above: MinIO, Kafka, Solr, Postgres, OPA, the memory server, Gofannon, LiteLLM, the OTel collector, optionally Authentik. Replicating it is adding brokers, shards, schemas, and replicas along the domain key. +**The Foundation's reference deployment** is the compose file above: MinIO, Kafka, Solr, Postgres, OPA, the memory server, Gofannon, LiteLLM, the OTel collector, optionally Authentik. The launcher and the egress are not in it: they run where each harness runs, and reach the stack as clients. Today the memory server runs in the same process as the egress: the launcher's, or under WSL the egress container's. The egress reads the session from it. Splitting the memory server from the egress will need the session's label and the grants carried across that boundary. Replicating it is adding brokers, shards, schemas, and replicas along the domain key. **The committer's laptop** is one process from the same crates: files or a local object store for the record, SQLite with FTS5 for index and ledger, regorus over a grants file, no broker, no identity. Same surface, same bundle format. It is the conformance check for the surface and the thing a person can run before their institution has built anything. @@ -76,7 +80,9 @@ One Rust workspace. | `ledger` | Two backends: SQLite, Postgres. Claims, leases, origin dedup | native | | `log` | Kafka producer and consumer groups; a file-backed log for the laptop | native | | `policy` | OPA sidecar client; regorus for the local grants file | native, wasm | -| `server` | MCP and HTTP surface, session cache, ceiling checks, T0 assembly | native | +| `server` | MCP and HTTP surface, session cache, the read filter and write checks, T0 assembly | native | +| `sandbox` | Confinement: the plan, namespaces on Linux, Seatbelt on macOS; the WSL container backend lives in `amem` (`crates/memory`) | native | +| `egress` | The confined harness's one way out: the memory server's MCP over HTTP, and model calls with ceilings, credentials, spend, metering | native | | `plugins` | WASI host for taps and skill bundles | native | | `ui` | The browser reader and curation page | wasm | | `memoryd`, `memory-local` | The two binaries | native, Windows and Linux |