Skip to main content
Glama
README.md
# 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

<!--
  Record a short terminal session showing: agent creation, an agent writing
  and reading, a denied write, and the audit log. Then replace the line below.

    brew install asciinema        # or: pipx install asciinema
    asciinema rec memgate-demo.cast
    # run the flow, then Ctrl+D
    asciinema upload memgate-demo.cast

  Paste the badge asciinema gives you here, e.g.:
  [![asciicast](https://asciinema.org/a/XXXXXX.svg)](https://asciinema.org/a/XXXXXX)
-->

[![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 <id>      # make a pending memory active
memgate review reject <id>       # 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 <id>              Approve a pending memory
memgate review reject <id>               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).