# Memgate **Access control, conflict resolution, and audit for shared agent memory.** Memgate sits in front of the memory your AI agents share. It decides who can read and write what, resolves conflicting writes, and records every operation — so a multi-agent system's memory stays trustworthy as it grows. It speaks [MCP](https://modelcontextprotocol.io), so agents connect with one config entry and no code changes. > Status: early but working. The core (policy, conflict handling, audit, MCP tools) runs and is tested end-to-end with real agents. Adapters for external memory backends and a hosted dashboard are on the roadmap, not built yet. Feedback and issues welcome. --- ## Demo [![asciicast](https://asciinema.org/a/gV89C4wOep9phIfV.svg)](https://asciinema.org/a/gV89C4wOep9phIfV) --- ## The problem A single agent's memory is easy: keep the conversation in context, done. But when several agents share one memory pool — one handling support, one writing code, one doing billing — that pool degrades: - **Conflicting writes.** One agent records "customer is happy," another records "customer is angry." Nobody knows which is current. - **No access boundaries.** The billing agent can read customer PII; the support agent can overwrite financial records. Nothing stops it. - **No accountability.** When something goes wrong, there's no record of which agent wrote what, or when. Memory stores like pgvector, Mem0, or Zep *hold* memory well. They don't *govern* it. Memgate is the governance layer that sits on top. ## The idea Think of the shared memory as a ledger, and Memgate as the gatekeeper standing in front of it. Agents never touch the ledger directly — every request goes through the gate, which does three things: 1. **Policy** — checks whether this agent's role may read or write this namespace. Default is deny. 2. **Conflict resolution** — when a record already exists for an entity, applies the namespace's strategy (`last-write-wins`, `versioned`, or `require-review`) instead of silently clobbering it. 3. **Audit** — appends every operation, allowed or denied, to an append-only log. Identity is bound to the API key, not to anything the agent says. An agent cannot escalate its own privileges by claiming a different role. ## How it works ``` Agent A ─┐ Agent B ─┼── MCP ──▶ [ Memgate ] ──▶ Postgres + pgvector Agent C ─┘ policy · conflict · audit ``` Agents call three tools through MCP: | Tool | What it does | |------|--------------| | `memory_write(namespace, entity_key, content)` | Policy check → conflict resolution → write → audit | | `memory_read(namespace, entity_key)` | Policy check → return the active record → audit | | `memory_search(namespace, query)` | Policy check → semantic search over authorized namespaces → audit | --- ## Quickstart Requires Docker. ```bash git clone https://github.com/denizayhan04/memgate cd memgate docker compose up --build ``` This starts Postgres (with pgvector), runs migrations, and serves the MCP endpoint on **:8088**. Create an agent and get its API key: ```bash docker compose exec memgate memgate agent create --name support-bot --role support # → API key (shown once): mg_live_... ``` Connect an MCP client (Claude Code, Cursor, or anything that speaks MCP). Add to your project's `.mcp.json`: ```json { "mcpServers": { "memgate": { "type": "http", "url": "http://localhost:8088/mcp", "headers": { "Authorization": "Bearer mg_live_YOUR_KEY" } } } } ``` Now the agent has `memory_write`, `memory_read`, and `memory_search` tools, scoped to whatever its role allows. Inspect what happened: ```bash docker compose exec memgate memgate audit --entity "customer:acme" ``` ``` ID AT AGENT ACTION NAMESPACE ENTITY_KEY RESULT 14 2026-07-08T15:43:30Z test-eng write customer-data customer:acme denied 13 2026-07-08T15:43:14Z test-eng read customer-data customer:acme allowed 12 2026-07-08T15:41:05Z test-support read customer-data customer:acme allowed 11 2026-07-08T15:41:02Z test-support write customer-data customer:acme allowed ``` Two agents, one shared record, different permissions — the support agent writes and reads; the engineer reads but cannot write. Every attempt, allowed or denied, is on record. --- ## Policies Policies live in a YAML file (`config/policies.example.yaml` by default), not in the database — so they're versioned in git and reviewed like any other config. The rule is **default deny**: anything not explicitly granted is closed. ```yaml namespaces: customer-data: conflict_strategy: versioned # old record superseded, new active; history queryable tech-context: conflict_strategy: last-write-wins # new record overwrites old billing: conflict_strategy: require-review # write parked as pending until a human approves roles: support: customer-data: [read, write] tech-context: [read] engineer: tech-context: [read, write] customer-data: [read] finance-bot: billing: [read, write] customer-data: [read] auditor: "*": [read] ``` The running server reloads the policy file without a restart, on `SIGHUP` or via `memgate reload`. ### Conflict strategies When a write targets an entity that already has an active record: - **`last-write-wins`** — the new record becomes active, the old one is marked superseded. - **`versioned`** — same, but history is preserved and queryable (what did we know, and when). - **`require-review`** — the new write is parked as `pending_review`; the existing record stays active until a human approves it. Reviewing parked writes: ```bash memgate review list # list memories awaiting review memgate review approve # make a pending memory active memgate review reject # reject it ``` --- ## CLI ``` memgate serve Start the MCP (Streamable HTTP) server memgate migrate Apply SQL migrations memgate agent create --name N --role R Create an agent and print its API key memgate review list List memories awaiting review memgate review approve Approve a pending memory memgate review reject Reject a pending memory memgate reload Reload the policy YAML in a running server memgate audit [--entity K] [--namespace N] [--agent A] [--limit N] Show audit log, most recent first ``` ### Environment | Variable | Purpose | |----------|---------| | `MEMGATE_DATABASE_URL` | Postgres DSN (required) | | `MEMGATE_POLICY_FILE` | Policy YAML path (default `config/policies.example.yaml`) | | `MEMGATE_LISTEN_ADDR` | Listen address for `serve` (default `:8080`) | | `MEMGATE_MIGRATIONS_DIR` | Migrations directory (default `migrations`) | | `MEMGATE_PID_FILE` | PID file written by `serve`, read by `reload` | | `MEMGATE_OPENAI_API_KEY` | Optional. Set for real embeddings; unset falls back to a deterministic offline embedder, so search works without a key. | --- ## Local development ```bash make build # compile to bin/memgate make test # run tests make migrate # apply migrations (needs MEMGATE_DATABASE_URL) make run # run the MCP server locally make up # docker compose up --build make down # stop and remove services ``` Running from source without Docker: ```bash export MEMGATE_DATABASE_URL="postgres://memgate:memgate@localhost:5432/memgate?sslmode=disable" go run ./cmd/memgate agent create --name support-bot --role support ``` --- ## Design notes - **Backend-agnostic by design.** The memory store is behind an interface. Today the only implementation is Postgres + pgvector; adapters for external stores (Mem0, Zep) can be added without touching the policy, conflict, or audit layers. - **Identity is the key, not the claim.** An agent's role is resolved from its API key on every request. What the agent *says* about its role is irrelevant — there is no way to self-escalate. - **Audit is append-only.** The audit log rejects updates and deletes at the database level, not just in application code. ## Roadmap - Backend adapters (Mem0, Zep) - REST endpoints alongside MCP, for clients that don't speak MCP - A read-only audit dashboard - Team policy management and SSO (hosted) ## Tech Go · Postgres + pgvector · [mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) · MCP Streamable HTTP transport. ## License Apache 2.0. See [LICENSE](LICENSE).