Lethe remembers. Charon carries.
Charon is the MCP authorization and governance gateway for Lethe. It controls project scopes, proposals, review, protected merges, credentials, and audit behavior while keeping Lethe private. Charon requires a reachable Lethe instance; ChatGPT, Claude, and other MCP clients connect to Charon, never to Lethe directly.
Charon defaults to CHARON_MODE=memory-git, which registers only versioned
branch, changeset, context, review, and merge tools. The original mutable event
surface remains available only through the explicit CHARON_MODE=direct
compatibility setting.
ChatGPT / Claude / Generic MCP Client
|
| OAuth or Obol-authenticated MCP
v
Charon
authorization / policy / review
|
| private typed API
v
Lethe
canonical persistent memory
OpenClaw
|
| optional automatic context integration
v
Lethe OpenClaw Plugin
The OpenClaw plugin is optional and does not replace Charon: generic MCP clients use the full Memory Git workflow by calling Charon's tools explicitly. A typical deployment fronts Charon with Cloudflare Tunnel over HTTPS while Lethe Git (port 18485, isolated SQLite) stays on the private network; OpenClaw keeps using OpenLethe (legacy mode, port 18483) separately.
| Component | Release |
|---|---|
| Charon | v0.1.0-beta.1 |
| Lethe | v0.4.0-beta.1 |
| Memory Git schema | memory_git/v1 |
| Merge authorization | memory-git-merge/v2 |
| Lethe OpenClaw plugin | matching Lethe release |
Canonical default ports:
| Service | Default |
|---|---|
| Legacy/OpenClaw Lethe | 18483 |
| Charon MCP | 18484 |
| Lethe Git | 18485 |
| Charon reviewer | 18486 |
In memory-git mode only the thirteen Memory Git V1 tools below are
registered. The read/proposal/write/thread tools are the direct compatibility
surface and cannot be called through the versioned deployment.
memory_search— Search accepted memory across authorized projectsmemory_get— Retrieve a single memory event by IDmemory_recent— Retrieve recent accepted memorythread_list— List threads for an authorized sessionthread_get— Retrieve a thread from an authorized projectthread_events— Retrieve authorized events in a thread
memory_propose— Quarantine a proposed legacy memory entrymemory_checkpoint— Quarantine a structured conversation checkpointproposal_list— List pending memory proposals
Legacy proposal approval currently fails closed because Lethe's event-create API does not offer an idempotency key or caller-supplied event ID. Use Memory Git for reviewed durable writes; direct compatibility proposals may be inspected or rejected but are not falsely reported as materialized.
memory_write— Direct trusted write to Lethe (requiresmemory.writescope)
memory_repo_init— Idempotently create a project's synthetic root and protectedrefs/shared/main(exact-projectmemory.commitauthority required)memory_status,memory_log,memory_show,memory_diff,memory_ref_list— Inspect authorized project historymemory_context_at— Reconstruct an exact authorized ref/head; pin selected IDs only with exact-project commit/write authoritymemory_branch_create— Create an owned agent, session, or topic refmemory_changeset_create— CAS commit to an owned non-protected refmemory_merge_propose— Reuse Charon proposals as merge requests into protected refsmemory_merge_review— Record an explicit approve, request-changes, or reject verdict bound to the current proposal snapshotmemory_merge— Apply only a currently approved fast-forward, multi-parent, or selected-op mergememory_merge_cancel— Cancel a pending merge request without deleting its branch history
Prerequisite: a reachable Lethe instance. For Memory Git mode, run Lethe in
git mode on port 18485 (see the Lethe repository's docker-compose.git.yml)
and point CHARON_UPSTREAM at it.
Copy .env.example to .env and replace the placeholders, or write the file
from scratch — every value is operator-generated. A complete manual .env
looks like this:
# ── Required ──────────────────────────────────────────────
# Must match the key the upstream Lethe was started with.
LETHE_API_KEY=replace-with-upstream-lethe-api-key
# Three distinct signing keys. Generate each independently:
# openssl rand -hex 32
# Never reuse one value across purposes; leave legacy CHARON_HMAC_KEY unset.
CHARON_OBOL_HMAC_KEY=replace-with-random-64-hex
CHARON_OAUTH_HMAC_KEY=replace-with-random-64-hex
# Must ALSO be configured on Lethe with the identical value — it signs and
# verifies the memory-git-merge/v2 merge envelopes.
CHARON_MERGE_HMAC_KEY=replace-with-random-64-hex
# Public URL of this gateway — credential audience + OAuth metadata.
CHARON_PUBLIC_URL=https://charon.example.com
# OAuth client configuration (browser-flow clients such as ChatGPT).
CHARON_OAUTH_CLIENT_ID=chatgpt-mcp
CHARON_OAUTH_REDIRECT_URIS=https://chatgpt.com/connector/oauth/*
# Principal (ID or unique name) the OAuth flow issues tokens for. Leave
# blank until after `charon principal reconcile`, then fill it in.
CHARON_OAUTH_DEFAULT_USER=
# ── Defaults you can keep ─────────────────────────────────
CHARON_MODE=memory-git
CHARON_AUTH_MODE=oauth
# Plaintext HTTP to a non-loopback upstream is refused; the documented
# local Docker Desktop gateway is the one allowed exception.
# CHARON_UPSTREAM=http://host.docker.internal:18485
# CHARON_ALLOW_INSECURE_UPSTREAM=1
# ── Optional ──────────────────────────────────────────────
# CHARON_OAUTH_GENERATE_PAIRING_SECRET=true # fresh browser pairing key on every start (default in Compose)
# CHARON_OAUTH_ALLOW_AUTHOR_SCOPES=false # keep OAuth tokens read/propose-only
# CHARON_AUTO_USER=chatgpt # auto-bootstrap a principal + token on first start
# CHARON_AUTO_PERMISSION=readonly # readonly | propose | review | write
# CHARON_AUTO_PROJECTS=*
# CHARON_LEDGER_HMAC_KEY= # purpose-specific ledger signing key (recommended)
# CHARON_LEDGER_ANCHOR_PATH=./data/ledger-anchor.jsonl
# CHARON_MERGE_AUTH_TTL=2m # envelope lifetime (hard cap 15m)
# CHARON_RECOVERY_READONLY=false # set true after a restore until reconcile passesThe full annotated reference is .env.example. After editing:
chmod 600 .env and never commit it — .gitignore already covers it.
export CHARON_HTTP=":18484"
export CHARON_ADMIN_HTTP="127.0.0.1:18486"
export CHARON_MODE="memory-git"
export CHARON_UPSTREAM="http://localhost:18485"
export CHARON_PUBLIC_URL="https://charon.example.com"
export LETHE_API_KEY="your-lethe-api-key"
export CHARON_OBOL_HMAC_KEY="$(openssl rand -hex 32)"
export CHARON_OAUTH_HMAC_KEY="$(openssl rand -hex 32)"
export CHARON_MERGE_HMAC_KEY="$(openssl rand -hex 32)"
# Configure the same value on trusted Lethe as CHARON_MERGE_HMAC_KEY
# (CHARON_HMAC_KEY accepted as a legacy fallback) so protected merge
# signatures can be verified. Keep all three values distinct.
export CHARON_DATA_DIR="./data"The Compose stack pulls ghcr.io/openlethe/charon:latest — no local build:
docker compose up -d # primary OAuth gateway on 127.0.0.1:18484
# reviewer Obol gateway on 127.0.0.1:18486The charon-data named volume is created automatically on first start and
holds the policy database. To build from source instead:
make build
./bin/charon servebin/charon is a tracked development launcher, not a compiled artifact. It
executes the root ./charon binary produced by make build when present and
otherwise falls back to go run ./cmd/charon. Tagged release binaries are
created under dist/ and published by the release workflow.
# Trusted local author: exact project grant plus branch/commit/propose scopes.
./bin/charon principal reconcile "Local Memory Author" propose lethe-git
# Reconciled principal: principal_...
# Independent reviewer: exact project grant, review/merge authority, no direct write.
./bin/charon principal reconcile "Local Memory Reviewer" review lethe-git
# Read-only clients may use a wildcard project grant.
./bin/charon principal reconcile "Read Only Client" readonly '*'./bin/charon obol mint principal_...
# Obol minted:
# ID: ...
# Token: obol_..._...
# Exp: 2026-08-11...curl -H "Authorization: Bearer obol_..._..." \
http://localhost:18484/mcpMemory Git changes reach protected refs only through independent review:
- An author principal (exact project grant plus branch/commit scopes)
commits a changeset to an owned branch ref with
memory_changeset_createand opens a proposal withmemory_merge_propose. - A separate reviewer principal (review/merge authority, no direct write)
records an explicit approve, request-changes, or reject verdict with
memory_merge_review, bound to the exact proposal snapshot reviewed. - Only a currently approved proposal can be applied with
memory_merge; review findings against a stale or mutated proposal fail closed.
Author and reviewer principals are distinct credentials (see quick start step 3), so no single principal can both propose and approve the same protected merge.
| Command | Description |
|---|---|
charon version |
Show version, commit, build time, and Go runtime |
charon serve |
Start the MCP gateway server |
charon principal create <name> |
Create a new principal |
charon principal grant-project <id> <project> |
Grant project access |
charon principal grant-scope <id> <scope> |
Grant scope |
charon principal reconcile <name> <permission> <projects> |
Create/update exact grants |
charon principal disable <id> |
Immediately disable authentication for a principal |
charon principal enable <id> |
Re-enable a disabled principal |
charon obol mint <principal-id> |
Mint a new obol |
charon obol ls <principal-id> |
List obols |
charon obol revoke <obol-id> |
Revoke an obol |
charon proposal ls [limit] |
List pending proposals |
charon proposal approve <proposal-id> |
Fails closed for legacy event proposals until Lethe exposes idempotent materialization; Memory Git merges use memory_merge |
charon proposal reject <proposal-id> |
Reject a proposal |
charon ledger tail [limit] |
View recent ledger entries |
charon ledger verify |
Verify every audit hash and predecessor link |
The primary docker-compose.yml is a hardened local production reference. It
runs as an unprivileged user, binds the MCP port to loopback, drops Linux
capabilities, enables no-new-privileges, uses a read-only root filesystem,
limits processes and memory, and mounts only a named /data volume. Charon
never mounts or writes Lethe's SQLite database directly.
cp .env.example .env
# Edit .env so the upstream key, three purpose-specific HMAC keys,
# authentication mode, project grants, and public URL match the deployment.
docker compose up -d --buildFor the repository's isolated sibling-checkout E2E environment, use the separate test stack instead:
docker compose -f docker-compose.memory-git-test.yml up -dThat test stack intentionally expects ../lethe/.env.git, publishes Charon on
127.0.0.1:18487, stores its database in a dedicated named volume, and writes
only local E2E credential/expectation files under the ignored
./charon-memory-git-data directory.
OAuth authorization is restricted to CHARON_OAUTH_CLIENT_ID and the explicit
redirect allowlist. A trailing * permits ChatGPT's per-app callback path, but
the scheme, host, and path prefix are parsed and validated independently.
The browser authorization step displays Charon's no-store pairing page, including
the exact client ID, callback destination, and protected resource. The primary
Compose profile generates a fresh 32-byte URL-safe authorization key at every
Charon process start and prints it once in a dedicated first-output container
banner. The page accepts that exact key, tolerates accidental leading or trailing
whitespace, shows an immediate authorizing state after submission, and places
key-mismatch feedback at the top of the authorization card.
Set CHARON_OAUTH_GENERATE_PAIRING_SECRET=false only when a static 32+ character
CHARON_OAUTH_PAIRING_SECRET is intentionally managed outside the container;
static values are never logged. Treat container logs as sensitive while the
runtime key is active. Public clients must use S256 PKCE and state. Tokens are
capped to read/proposal scopes by default. In memory-git
mode, a trusted deployment may explicitly set
CHARON_OAUTH_ALLOW_AUTHOR_SCOPES=true to retain only memory.branch and
memory.commit when the principal already has those grants. Review, merge, and
direct-write scopes remain stripped from this OAuth flow.
The explicit review-verdict field changes the MCP tool schema. Existing ChatGPT
connectors must refresh or reconnect after upgrading so they retrieve the new
memory_merge_review contract, and OAuth clients must complete a new paired
authorization if their existing token is no longer accepted.
The Lethe OpenClaw plugin is optional. It automatically retrieves and injects accepted memory into OpenClaw conversations and records context metadata. Charon and Lethe work without it: ChatGPT, Claude, and other MCP clients use Charon's Memory Git tools directly. Install the plugin from ClawHub when an OpenClaw agent should receive accepted memory automatically instead of calling MCP tools explicitly.
make test # Run tests
make lint # Format and vet
make build # Build binary
make docker # Build container- Fail-closed: Missing secrets prevent startup
- Least-privilege: Scoped projects and scopes per principal
- No generic proxy: Strict Lethe adapter with typed methods only
- Mode isolation:
memory-gitmode does not register legacy read/write/proposal/thread tools - Quarantine: Canonical/protected memory changes go through proposal review; branch work remains isolated
- Audit: Serialized transactional metadata ledger with verifiable hashes; write failures are surfaced in logs
- Protected refs: Charon-signed merge CAS; generic Lethe writes cannot move protected refs
- Obols: High-entropy bearer credentials with purpose-specific HMAC verification
- Key separation: Distinct Obol, OAuth, and protected-merge keys with a legacy migration fallback
- Local admin: Admin interface binds to loopback or a Unix socket only
- Bounded HTTP: Request bodies, headers, reads, idle connections, upstream calls, and shutdown are capped
- Container isolation: Pinned base images, non-root UID, dropped capabilities, read-only root filesystem
Report security issues through SECURITY.md, not a public issue. See CONTRIBUTING.md for development and review requirements and docs/operations.md for health, backup, restoration, rotation, and incident-response procedures.
MIT