# Attestor

The memory layer for agent teams. Self-hosted, deterministic retrieval, zero LLM in the critical path.

PyPI: `attestor` -- Python import: `attestor`.

## Project Structure

```
attestor/
  core.py              -- AgentMemory class (main public API)
  client.py            -- MemoryClient (HTTP drop-in for remote Attestor)
  context.py           -- AgentContext (identity, role, namespace, token budget, provenance)
  models.py            -- Memory, RetrievalResult dataclasses
  cli.py               -- CLI entry point
  api.py               -- Starlette ASGI REST API
  store/
    base.py            -- DocumentStore / VectorStore / GraphStore interfaces
    registry.py        -- Backend selection; default = postgres + neo4j
    connection.py      -- Config layering / env resolution
    embeddings.py      -- Provider auto-detect (local / OpenAI / Bedrock / Vertex / Azure)
    postgres_backend.py-- Postgres (document role; pgvector embedding column kept in schema)
    pinecone_backend.py-- Pinecone (vector role; auto-detects Local Docker vs Cloud)
    neo4j_backend.py   -- Neo4j + GDS (graph role, PageRank / BFS)
    schema.sql
  graph/
    extractor.py       -- Entity/relation extraction (4-output GraphRAG)
  retrieval/
    orchestrator.py    -- 6-step semantic-first cascade (vector → BM25 → RRF → graph → MMR → fit)
    tag_matcher.py
    scorer.py          -- Temporal + entity + PageRank boosts, confidence decay, MMR
  temporal/
    manager.py         -- Contradiction detection, supersession, timeline, as_of replay
  extraction/          -- Rule-based + optional LLM memory extraction
  mcp/
    server.py          -- MCP server (tools + resources + prompts)
  hooks/
    session_start.py   -- Context injection at session start
    post_tool_use.py   -- Auto-capture from Write/Edit/Bash
    stop.py            -- Session summary on exit
  utils/
    config.py, tokens.py
  infra/local/         -- Docker Compose for local Postgres + Pinecone Local + Neo4j
tests/                 -- Unit tests (live integration tests env-gated)
```

## Prerequisites

Attestor supports a single canonical stack — one service per storage role:

- **PostgreSQL 16+** -- document role (source of truth for content, tags, entity, ts, provenance, confidence). The schema also carries a `vector(N)` embedding column (kept so writes are self-contained), but the canonical recall path goes through Pinecone.
- **Pinecone** -- vector role (dense embeddings, per-namespace isolation, cosine search). Pinecone Local (Docker, free) for development; Pinecone Cloud for production.
- **Neo4j 5+ with GDS** -- graph role (PageRank, multi-hop BFS, Leiden).

Local development can run all three via Docker (`attestor/infra/local/`). Cloud deployments can swap in managed Postgres (Neon, RDS, Cloud SQL, AlloyDB-as-PG, Cosmos PG flex), managed Pinecone (Standard plan starts at $50/mo; free Starter tier = 5M embedding tokens/month), and managed Neo4j (AuraDB) or self-host any of them.

Alternate backends (ArangoDB, AWS DynamoDB+OpenSearch+Neptune, Azure Cosmos+NetworkX, GCP AlloyDB+AGE+ScaNN, the legacy pgvector single-DB bundle) and the Apache AGE-on-Postgres graph path were removed from the codebase on 2026-05-02 in service of single-stack focus.

## Development

- Poetry manages dependencies -- `poetry run pytest`, `poetry run python`
- Unit tests: `.venv/bin/pytest tests/ -q` (no external services required for unit layer)
- Live integration tests (env-gated): Postgres/Neon, Neo4j/AuraDB, Azure, ArangoDB

## Architecture

**Three storage roles, every memory persisted across all three backends:**

| Role | Purpose | Backend |
|------|---------|---------|
| Document | Source of truth (content, tags, entity, ts, provenance, confidence) | Postgres |
| Vector | Dense embedding per memory | Pinecone (Local Docker / Cloud) |
| Graph | Entity nodes + typed edges (`uses`, `authored-by`, `supersedes`) | Neo4j (GDS) |

Default embedder is **Pinecone Inference `llama-text-embed-v2`** (NVIDIA-hosted, 1024-D) — matches the index dim and pairs naturally with the Pinecone vector store. Voyage / OpenAI / Ollama remain as opt-in providers via `attestor/store/embeddings.py`.

**6-step retrieval pipeline (deterministic, no LLM in the hot path):**
1. Vector top-K (Pinecone cosine; HyDE v2 lane optional)
2. BM25 lane (optional — Postgres FTS for v3, in-memory rank_bm25 for the experiment harness)
3. RRF blend (vector + BM25 → unified rank, k=60)
4. Graph narrow (Neo4j BFS depth=2 affinity bonus + synthetic triple injection)
5. MMR diversity (λ=0.7) + confidence decay
6. Token-budget pack (greedy fit to recall budget)

Implementation: `attestor/retrieval/orchestrator.py`. The pipeline is
semantic-first (vector → BM25 → RRF → graph → MMR → fit) as of 2026-04-19;
a separate tag-match utility lives at `attestor/retrieval/tag_matcher.py`
but is consumed by the entity extractor, not the recall pipeline.

**Multi-agent primitives:** 6 RBAC roles (ORCHESTRATOR, PLANNER, EXECUTOR, RESEARCHER, REVIEWER, MONITOR) — **enforced at the AgentContext layer** via `ROLE_PERMISSIONS` in `attestor/context.py`. Matrix: ORCHESTRATOR = READ+WRITE+FORGET; PLANNER/EXECUTOR/RESEARCHER = READ+WRITE; REVIEWER/MONITOR = READ only. `read_only=True` is an independent kill switch that strips WRITE+FORGET regardless of role. Direct `AgentMemory.add()` calls (without an AgentContext) bypass the matrix — RBAC is a context-layer guarantee, not a backend one. Namespace isolation is enforced across all three roles: row-level + RLS on Postgres (policies keyed to `attestor.current_user_id`), per-namespace on Pinecone, and a `namespace` property with a composite `(key, namespace)` constraint plus namespace-scoped BFS on Neo4j (`store/neo4j_backend.py`). One known caveat: the Neo4j PageRank GDS projection (`neo4j_backend.py:pagerank`) is **not** namespace-scoped — it computes entity importance over the global graph. This is a ranking *signal* only (it reorders results that are already namespace-filtered); it does not surface another project's memories. The Claude Code plugin keys each project (git root, else cwd) to its own RLS tenant (`external_id=cc-project:<sha256(root)>`, hashed so it fits `VARCHAR(256)` and avoids path-separator issues; the human path is kept in user metadata) via `attestor/_project.py` + `attestor/hooks/_tenant.py`, so memory never bleeds across projects. Provenance tracking, write quotas, and per-agent token budgets are wired.

**Temporal:** Contradiction detection auto-supersedes older facts; nothing is deleted. Every fact has a validity window; `recall(as_of=...)` replays the past.

## Runtime topologies

- **Mode A — Embedded library**: `AgentMemory(config)` in-process, talks directly to Postgres + Pinecone + Neo4j.
- **Mode B — Sidecar**: `attestor api` on `localhost:8080`, language-agnostic HTTP client shares the same Postgres + Pinecone + Neo4j.
- **Mode C — Shared service**: one Attestor service in front of an agent mesh (App Runner / Cloud Run / Container Apps) backed by managed Postgres + Pinecone + Neo4j.

Same API across all three. Only configuration changes.

## Key Conventions

- Postgres is the source of truth. Pinecone vectors and Neo4j graph are derived state, both rebuildable from Postgres.
- Non-fatal errors in vector/graph are caught and logged -- the document path never breaks.
- Degradation is explicit and tiered: vector down → tag+graph; graph down → tag+vector; the document store is the only hard dependency.
- PyPI name: `attestor`; import: `attestor`; single CLI entry point `attestor`

## Install for Claude Code

**The one install:** `pipx install attestor && attestor quickstart` — zero questions, one default profile. It writes `~/.attestor/{config.toml,attestor.yaml,.env}`, brings up the **three-role local stack** in Docker, uses a local **Ollama `bge-m3`** embedder (no cloud key), wires the Claude Code MCP server (`./.mcp.json`) + lifecycle hooks, and runs `attestor doctor`.

**Prerequisites:** Docker running + Ollama serving `bge-m3` (`ollama pull bge-m3`). `quickstart` runs a preflight that scans for these and reports what's missing — it never prompts.

**Reverse it:** `attestor teardown` (zero-question; keeps your data by default — `--purge` also wipes Docker volumes; `--dry-run` previews).

**Per-project isolation is automatic.** Each working directory (git root, else cwd) is its own hard-isolated memory tenant (`attestor/_project.py` + `attestor/hooks/_tenant.py`) — no namespace to configure, and memory never bleeds across projects. Other MCP clients (Cursor, etc.) and runtimes (library / sidecar / shared service) are covered in `docs/INSTALL.md`.

**Plugin path (optional, secondary):** `/plugin marketplace add bolnet/attestor` → `/plugin install attestor` (then ENABLE it in the `/plugin` menu) → run `/attestor:install-attestor` (namespaced!), which just runs `attestor quickstart`. Note the command is `/attestor:install-attestor`, NOT bare `/install-attestor`.

## Health Check

Run `attestor doctor` or call `mem.health()`. The MCP server exposes `memory_health` -- call it first when integrating.

Checks: Document Store (Postgres), Vector Store (Pinecone Local or Cloud), Graph Store (Neo4j), Retrieval Pipeline.
