Integrations
Every source and consumer, what it needs, and where credentials live. Principle (spec §10): local-first — no credential here grants a cloud service access to graph contents; auth only flows toward fetching your own data.
The mecha-graph source CLI
Integrations are managed through a registry at ~/.mecha-graph/config.toml (chmod 600):
mecha-graph source list # table: kind, enabled, auth state, last ok, items
mecha-graph source add ics --url '<secret-ical>' --me you@example.edu
mecha-graph source add slack --token xoxp-… # validated via auth.test before saving
mecha-graph source add imessage --db ~/.mecha-graph/chat.db --self-handles '+16035550123'
mecha-graph source add mbox --path ~/Takeout/mail.mbox --me you@example.edu
mecha-graph source test [name] # auth/connectivity, no writes
mecha-graph source sync [name] [--full] # ingest all enabled (cursored, idempotent)
mecha-graph source enable|disable|remove <name>
add runs the connectivity test before saving (--no-test to skip);
sync is what the nightly runs. bee and sessions self-register — they
need zero config.
Status at a glance
| Integration | Direction | Auth | Status |
|---|---|---|---|
| Bee | source | bee login (token in Bee CLI config) | ✅ authenticated, synced nightly |
| Calendar (ICS) | source | secret iCal URL (capability URL) | ⚠️ one mecha-graph source add ics --url … away |
| Hermes sessions | source | none (local file, read-only) | ✅ |
| Claude Code sessions | source | none (local files) | ✅ |
| Slack | source | user token xoxp-… (or bot xoxb-…) | ✅ built — mecha-graph source add slack --token … |
| SMS / iMessage | source | synced copy of chat.db (Mac: Full Disk Access) | ✅ built — mecha-graph source add imessage --db … |
| Email (mbox) | source | none — mbox export (Gmail Takeout etc.) | ✅ built — mecha-graph source add mbox --path … |
| llama-server (embed + extract) | infra | none (localhost) | ✅ shared with mecha; :8080 chat, :8081 embed |
| Hermes (agent) | consumer | none (local stdio MCP) | ✅ wired |
| Claude Code (agent) | consumer | none (local stdio MCP) | ✅ wired |
| DB encryption | infra | local keyfile (auto) | ✅ SQLCipher, enabled 2026-08-02 |
| Email (live OAuth) | source | OAuth — lives in FlowMail/macOS | ⏳ FlowMail-side, by design |
Sources
Bee (ambient conversations)
- Auth:
bee login(one-time browser flow). Check withbee status; re-auth withbee logout && bee login. Token is managed by the Bee CLI. - Auth needs a D-Bus session, which cron does not have. The CLI keeps
its token in the Secret Service keyring and reads it over the session
bus, so under cron it fails with
Cannot autolaunch D-Bus without X11 $DISPLAY— a message that names neither the keyring nor the real fix.nightly.shexportsDBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus, which persists across logout/reboot only because lingering is enabled (loginctl enable-linger $USER); the keyring must also have been unlocked by a login at least once. This silently broke bee ingestion until 2026-08-13. Second time cron's thinner environment has hit this source — the first wasPATHmissing~/.local/bin. Any source shelling out to a user-installed CLI needs its environment reconstructed explicitly, andmecha-graph statsstaleness is what catches it (that alert is the reason this was found). - Config:
mode = "stream"(current setup) — conversations + dailies are pulled straight from the Bee API (bee … --json) into the encrypted DB; the full JSON record is archived toepisode_raw. No plaintext mirror exists (the historical~/bee-syncmarkdown mirror was removed 2026-08-02 after 1:1 archive verification; mirror mode remains available by omittingmode). - Privacy: episodes land as sensitivity
private— excluded from default retrieval; opt in per query (--private/include_private: true).
Calendar — the identity bridge (§5.1); highest-value config left to do
- Auth: a secret iCal address — a capability URL; anyone holding it can
read your calendar, so it is treated as a credential.
- Google: Calendar → Settings → (your calendar) → Integrate calendar → Secret address in iCal format. If leaked: Reset on that same page.
- Outlook: Settings → Calendar → Shared calendars → Publish → ICS link.
- Config: put it in
~/.mecha-graph/nightly.env(chmod 600, never in the repo):(See docs/OPERATIONS.md — gitignored — for this machine's values.) The nightly fetches it toMECHA_GRAPH_ICS_URL=https://calendar.google.com/calendar/ical/.../basic.icsMECHA_GRAPH_SELF_EMAIL=you@example.edu~/.mecha-graph/calendar.icsand ingests. Manual alternative: drop any exported.icsat~/.mecha-graph/calendar.ics, or runmecha-graph ingest ics <file> --me <your-email>directly. - Multiple calendars: add more
mecha-graph ingest icslines in the nightly, or concatenate ICS files — events are idempotent by UID.
Agent sessions (Hermes + Claude Code)
- Auth: none.
~/.hermes/state.dbis opened read-only;~/.claude/projects/*/*.jsonlare plain files. No writes ever. - Config: paths overridable via
mecha-graph ingest sessions --hermes/--claude.
Infrastructure
llama-server (embeddings + Tier-7 extraction)
Replaced ollama on 2026-08-20. Not for tidiness: ollama runs its own
llama-server underneath, so the choice was never about the engine — only
about who sets the flags. Running both meant two copies of the same 35B
model resident at once (57.5 GB of a 121 GB unified pool), at a different
quantisation, under a chatml template override, with --context-shift on and
--reasoning-budget absent, so reasoning ran unbounded. What exactly caused
the 300 s extraction timeouts is not established — the measured
contributor is contention (two copies on one GPU put generation at 28 tok/s
against a 79.8 baseline); unbounded reasoning is plausible and was never
isolated.
- Auth: none — localhost.
- Two endpoints, because llama-server holds one model per process:
- chat/extraction —
MECHA_GRAPH_CHAT_URLor[llm] base_url, defaulthttp://127.0.0.1:8080 - embeddings —
MECHA_GRAPH_EMBED_URLor[llm] embed_url, defaulthttp://127.0.0.1:8081; on the owner's box that port is a systemd socket that starts the model on first connection and stops it after ten idle minutes (mecha'sscripts/llama/), so a first request can wait a cold start (~4 s) —Embedder::health_withinwaits it out, and a 503, reset or 5xx is "there, not ready", never "absent"
- chat/extraction —
- Shared by default, never duplicated.
Backend::resolveprobes{base_url}/health; if anything answers, it is used as-is (a 503 is a server loading a model, and is waited for, never spawned over) — as long as it is llama-server:connectrefuses a server that answers/healthbut not/props, because on a router what it serves cannot otherwise be told, so a bare OpenAI-compatible endpoint (vLLM, a proxy) is not supported as the chat endpoint. Installed beside mecha that is mecha's own server, holding the model once. The client deliberately has no code that looks for mecha or reads its config. mecha-graph-core knows nothing about any agent (lib.rs rule 1), and a user running their own llama-server gets the shared path for the same reason. The one place mecha's files are touched is the binary's opt-in holds directory (below), whichnightly.shnames only where mecha made it. - Behind a router, the client follows the loaded model. Every request names whatever model the router has loaded at that moment, not the one loaded when the run began. A request that lands mid-swap waits up to 15 s for the router to settle. So a long run goes on with the model the owner picked, instead of loading its own back, and each episode is recorded under the model that extracted it.
[llm] holds_dir: holding a shared router per episode. Withholds_dir = "~/.mecha/holds"in~/.mecha-graph/config.toml,extracttakes a hold there for each episode, in the file protocol a model switch waits on (mecha-graph/src/holds.rs). A switch then lets the episode in flight finish, and an episode doesn't start while one is pending.- It's a config key, not an environment variable, so every run honours it, not only the nightly (ARCHITECTURE.md's rule for an opt-in on mecha).
- Unset, nothing is held. Set, it never degrades: a directory that isn't there, or a config that can't be read, stops extraction with an error instead of running unheld.
- A switch still pending after 30 minutes, or a switch file that can't be read for 10, stops extraction for the night with an error. The episodes stay pending.
- Starting a server is opt-in, gated on
[llm] model_path. A machine that has not named a GGUF cannot start one, which is the whole safety property: probe-and-spawn on its own would answer a transient outage of mecha's server by loading a second 20 GB copy — silently, at 03:30, for the rest of the night. Nothing ever spawns at a URL that already answers, and a managed server is killed when the process exits. - The served model is discovered, not asserted. A single-model
llama-server ignores a request's
modelfield, soGET /props→model_aliasis what gets recorded inextract_state.model. A mismatch against an explicitly pinned[llm] modelwarns rather than refuses: a nightly that dies the first time you try a different model in the TUI would make the graph's health depend on remembering to edit a second config. - A llama-server router is the exception (mecha's :8080 since
2026-09-27). Its bare
/propsis a placeholder (role: "router",model_alias: "llama-server") and the request'smodelfield selects. So the served model is the one resident inGET /models(from a list whose every status is known); when that cannot be told, the configured model is used only if the router lists it, and otherwiseconnectrefuses before the first request. Reading the placeholder as the model cost 100 extractions and 30 summaries on 2026-09-27 — refused requests that extract marked attempted. - Embedding config:
[llm] embed_model,embed_dims,embed_max_chars.embed::ensure_vec_dimsreconciles thevec0tables toembed_dimsby rebuilding them — destructive by necessity, since vectors of a different width or model are not convertible — andembed_metarecords (model, dims, instruction) so a swap is detectable afterwards. Changing the embedder invalidatesprecheck's thresholds, which encode one model's cosine scale: measured on identical text, a same-claim pair scores 0.8650 on nomic and 0.6926 on Qwen3-Embedding-0.6B, against aSEMANTIC_DUP_THRESHOLDof 0.93. Recalibrate before re-enablingprecheck --auto-accept. - Serve an embedder with
llama-server -m <gguf> --port 8081 --embeddings --pooling last --embd-normalize 2.
mecha's docs/LLAMA-SERVER.md is the full operational reference: slot
geometry, KV arithmetic, the measured -np table, and the request contract.
Database & encryption
- DB:
~/.mecha-graph/graph.db(overrideMECHA_GRAPH_DBor--db). Dir is chmod 700. - SQLCipher-encrypted at rest. Key resolution on every open:
MECHA_GRAPH_DB_KEYenv →MECHA_GRAPH_DB_KEYFILE→ a local keyfile (0600) → plaintext; see docs/OPERATIONS.md (gitignored) for this machine's values.mecha-graph encryptmigrated the store in place with count verification;mecha-graph decrypt --out <path>writes an ephemeral plaintext snapshot for a tool that cannot read SQLCipher. - Back up the keyfile separately from the DB file (e.g. a password manager) — without it the graph is unrecoverable; with only it, an attacker still needs the DB file.
Retention & streaming — the lifecycle of raw data
Decision (2026-08-02, revised): stream where possible; capture-then-delete where files are unavoidable. Plaintext residue trends to zero.
Three retention modes per source (--retention on mecha-graph source add, or
retention = "…" in config.toml):
| Mode | What happens | When |
|---|---|---|
keep (default) | files untouched | while building trust in extraction |
capture | full raw archived to episode_raw inside the encrypted DB; files kept | transition |
capture_delete | archived, then the plaintext file is deleted — only after the archive row is verified present | end state |
Streaming beats all three when available — plaintext never exists:
- Bee:
mode = "stream"(current setup) pulls conversations + dailies straight from the Bee API (CLI--json); the full JSON record is always archived. - Calendar (URL): fetched and parsed in memory; no cache file.
- Slack: always streamed (API → DB).
- iMessage / mbox: inherently file-based (chat.db copy, Takeout export) —
use
--retention capture_delete; the transfer file is deleted after every episode's raw is archived, and the next sync re-creates it.
Re-processing after deletion is guaranteed: enrichment, embedding, and
Tier-7 extraction all read from the DB (episode_raw fallback where needed),
so prompt/schema improvements re-run against the archive — mecha-graph raw <uid>
shows exactly what's preserved. mecha-graph redact deletes the archive row along
with everything else.
At-rest architecture (final, 2026-08-02)
The design converged on stream-first + encrypted archive, which made a separate encrypted vault unnecessary (a gocryptfs vault was built, then removed before ever being used — see git history if it's ever wanted again):
- SQLCipher on
~/.mecha-graph/graph.dbis the at-rest layer. It holds the distilled graph AND the full raw archive (episode_raw) for every streamed/captured episode — the DB is the system of record. - No long-lived plaintext exists. Bee streams from its API; calendar
URLs parse in memory; Slack is API-native; iMessage/mbox transfer files
are
capture_delete(archived → verified → deleted). - The keyfile is the single secret. Back it up in your password manager, separate from any DB backup. Threat model, honestly stated: this protects against DB-file leaks (stray copies, backups); a thief who images the whole disk gets the keyfile too — the mitigation for that class is OS-level disk encryption (LUKS), a reinstall-level decision.
- Plaintext remnants can exist outside mecha-graph: agent session transcripts may quote graph content, and the distilled boot-context file (chmod 600) holds it by design. See docs/OPERATIONS.md (gitignored) for this machine's values.
Backups: copy graph.db (it's ciphertext at rest) + keep the keyfile in
the password manager. mecha-graph decrypt --out produces plaintext snapshots; treat
those as ephemeral.
Consumers (MCP)
The server is mecha-graph-mcp — stdio transport, no network listener, no auth
surface; access = ability to execute the binary as you.
- Hermes — an entry under
mcp_serversin~/.hermes/config.yamlwhosecommandismecha-graph-mcp; the key you give it is the name its tools appear under. Restart Hermes to pick it up. - Claude Code — wired at user scope:
claude mcp add --scope user graph -- mecha-graph-mcp. Verify withclaude mcp list; remove withclaude mcp remove graph. - Any other MCP client: point it at the same binary, stdio transport.
- After
cargo install mecha-graph-mcp, running servers keep the old binary until their host app restarts.
Remote access — MCP over SSH (laptop → graph host)
The graph lives on one host; other machines get live access with zero local
state by running mecha-graph-mcp through SSH (Tailscale authenticates). See
docs/OPERATIONS.md (gitignored) for this machine's values:
# on the laptop — verify the transport first:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | \
ssh -T -o LogLevel=ERROR examplehost \
'$HOME/.cargo/bin/mecha-graph-mcp'
# expect a single JSON line back ({"id":1,...serverInfo...})
# then register it:
claude mcp add --scope user graph -- ssh -T -o LogLevel=ERROR examplehost \
'$HOME/.cargo/bin/mecha-graph-mcp'
The single quotes matter: they defer $HOME to the graph host's shell, so a
laptop with a different home path still names the host's binary.
Notes: -T + LogLevel=ERROR keep stdio clean (any motd/banner corrupts
JSON-RPC); use the absolute binary path (non-login shell, no PATH); writes
(kg_upsert) work identically — fact writes land in the graph host's staging
queue, episode writes (kind='episode') land as source-owned evidence whose
extracted facts stage on the next nightly.
Multiple simultaneous clients are fine (SQLite WAL + busy_timeout).
If full offline replicas are ever wanted instead, the uid-based mecha-graph sync
design is queued — the schema already carries sync identities.
The credentialed sources in detail
Slack
- Get a token: create an app at api.slack.com/apps → OAuth & Permissions
→ add User Token Scopes:
channels:history,groups:history,im:history,mpim:history,channels:read,groups:read,im:read,mpim:read,users:read,users:read.email→ Install to Workspace → copy thexoxp-…token. (A botxoxb-…token also works but can't see your DMs or channels it isn't invited to.) mecha-graph source add slack --token xoxp-…— validated viaauth.testfirst.- What it does:
users.listseeds every workspace member as a person withslack_uidand email identifiers (merges with calendar/email people deterministically); messages land one episode per channel-day. DMs are sensitivityprivate; channelspersonal. - Tunables in config.toml:
max_channels(default 50),max_pages/channel. - Revoke: uninstall the app from the workspace, or rotate the token.
SMS / iMessage
- No API — a file: sync a copy of the Mac's
chat.dbover Tailscale:rsync mac:~/Library/Messages/chat.db ~/.mecha-graph/chat.db(grant the Mac-side terminal Full Disk Access once; add the rsync to the nightly or a Mac-side launchd job). The DB is only ever opened read-only. mecha-graph source add imessage --db ~/.mecha-graph/chat.db --self-handles '+1603…,you@x.com'- Identity:
handle.id(E.164 phone or email) → deterministicnode_identifier. Phone-only contacts get named after the number until a richer source supplies the real name — the identifier makes the merge automatic later. All episodesprivate. - v1 limitation: messages whose body lives only in
attributedBody(typedstream) rather thantextare skipped.
Email (mbox)
- No credentials: point at any mbox export — Gmail Takeout (takeout.google.com → Mail), Apple Mail export, mutt archives.
mecha-graph source add mbox --path ~/Takeout/mail.mbox --me you@example.edu- One episode per thread (References/In-Reply-To chains); bulk mail (List-Unsubscribe / List-Id / Precedence: bulk) is dropped at ingest (§5.3).
- Live sync remains FlowMail's job on macOS (it holds the Gmail/Outlook OAuth, spec §3); this path is for corpus backfill without new credentials.
Credential hygiene
~/.mecha-graph/nightly.envis chmod 600 and outside the repo; nothing secret is ever committed (.gitignorealso excludes*.db).- The ICS URL and the Bee token are the only two credentials in the pipeline
today; both are revocable at their source (Google reset /
bee logout). - MCP server binds nothing: stdio only (§10's "loopback only" satisfied by not opening a socket at all).