Deployment, operations, and disaster recovery¶
Deployment and packaging¶
The server is distributed as a native Go binary (via install.sh, see §Client installation — client and server are the same binary). The Docker image only exists as a CI artifact for k8s deployment: it is not a local deployment mode (D73 — containers on macOS mean a slow virtualized filesystem for git/SQLite, and an always-on VM).
Topologies¶
| Topology | Description |
|---|---|
| Local | The binary runs as a native user service (cartographer service, launchd/systemd); the client on the same machine points to 127.0.0.1. |
| Shared server | HTTP with optional static bearer tokens for multiple agents, on a trusted network or behind a reverse proxy (for example Kubernetes). |
Provisioning artifact signing¶
An explicit kbs: entry may set artifact_signing_seed to a 32-byte hex
Ed25519 seed. Mount it from a secret manager or protected secret volume: never
commit it, expose it in health/MCP output, or place it in logs. The server logs
only the KB and derived key ID. Distribute and pin the corresponding public key
on each client before switching a signer; unsigned KBs remain supported through
the explicit client trust policy.
KB-provided MCP allow-list¶
Third-party MCP descriptors under mcp/*.json are denied by default, including
after an upgrade. To advertise one, add an exact per-KB mcp_allowlist entry
with its artifact name, http transport and normalized absolute target URL.
The server omits every unlisted descriptor from manifest and artifact_list
responses and logs the configuration action at startup; headers and environment
references never appear in that diagnostic. Stale allow-list entries are a
warning, so an endpoint can be staged before its descriptor is committed.
| Partitioned servers | Several instances mount different KBs. A client can connect to entries from more than one server. Do not use several active writer servers as replicas for the same KB; git conflict recovery is a safety net, not a write-scaling protocol. |
State and volumes¶
The server process is ephemeral (k8s pod or local service); what persists on volume/disk:
- The KBs' git working trees.
- SQLite indices (optional, rebuildable).
- Optional local runtime files under .cartographer/. Search indexes are
rebuildable; conflict registries are operational state and should survive a
restart until resolved.
When audit.log is set, MCP tool execution appends an attempt+completion event
pair per call, so the file is a complete operational request log (D119). See
§Audit log below and transport-auth §Operational audit.
Configuration: flags, env, YAML¶
cartographer serve resolves its configuration from three sources, in order of precedence (highest first):
CLI flag > environment variable > YAML file (--config/CARTOGRAPHER_CONFIG) > default
(internal/config, config.Default()). The YAML file is optional: without --config, only
defaults + env + flags apply, as in earlier versions.
Full annotated example: config.example.yaml (repo root).
Schema (internal/config.Config, YAML tags in parentheses):
http: ":39273" # (http) listen address; absent = stdio
init: true # (init) initialize missing KBs
auth:
mode: "on" # (auth.mode) on | off | auto (default auto: on if tokens are present)
tokens: [...] # (auth.tokens) prefer env CARTOGRAPHER_TOKENS instead (secret)
roles: [...] # (auth.roles) named fine-grained permission sets (D118), see below
data: /data # (data) directory with KB auto-discovery (one subfolder = one KB);
# paths already mounted explicitly (kbs[], including remote KBs cloned
# into this directory) are excluded from discovery — no double mount
kbs: # (kbs[]) explicit KBs, local path or remote git (bootstrap, see below)
- remote: ssh://git@host:2222/user/wiki-kb.git
- path: /data/kb-locale
- remote: https://gitea.example.com/team/shared-kb.git
name: shared-kb # (kbs[].name) explicit name (D53): overrides
# the name derived from remote/path — the HTTP
# endpoint, token scope, clone dir, and the
# token_dir/age_key_dir conventions below all use
# this name
author_name: "Team Bot" # (kbs[].author_name) commit author for this KB
author_email: team-bot@example.com # (kbs[].author_email)
committer_name: "Cartographer CI" # (kbs[].committer_name) optional, defaults to author
committer_email: cartographer-ci@example.com # (kbs[].committer_email)
allow_artifact_write: true # (kbs[].allow_artifact_write) enables artifact_write/
# artifact_delete on this KB (default false, D71):
# writing a skill means injecting instructions that
# clients will execute — the capability must be
# granted per-KB by the operator; an rw token alone
# does not imply it
tool_prefix: "team" # (kbs[].tool_prefix) opt-in per-KB tool-name prefix
# (default "", off): registers this KB's tools as
# `team__<tool>` instead of `<tool>`. Wins over
# `mcp.tool_prefix_mode` below. See §MCP tool-name prefix.
mcp:
tool_prefix_mode: "off" # (mcp.tool_prefix_mode) off (default) | kb-name: global default for
# every mounted KB that doesn't set its own kbs[].tool_prefix — kb-name
# derives the prefix from the KB's own name. See §MCP tool-name prefix.
git:
autocommit: true # (git.autocommit) commit after every write
sync: true # (git.sync) read/write fetch/pull-rebase + post-write push if the KB has a remote
ssh_key: /etc/kb-ssh/id_ed25519 # (git.ssh_key) default SSH identity for cloning kbs[] remotes
known_hosts: /etc/kb-ssh/known_hosts # (git.known_hosts) host verification for the same clone
author_name: "Team Bot" # (git.author_name) default author (set with author_email)
author_email: team-bot@example.com # explicit configuration wins over Git config
token_dir: /etc/kb-git-tokens # (git.token_dir) directory <token_dir>/<name>.token (D53, see below)
in_window: 30s # (git.in_window) SyncIn freshness window: within this duration
# since the last successful fetch+pull, subsequent reads and writes skip it
# (no-op). Default 30s, 0 = sync on every write (D76)
out_debounce: 3s # (git.out_debounce) debounce of the per-KB async push after the
# commit: N writes in quick succession = 1 push. Default 3s, 0 = push
# synchronously inline, no worker (rollback flag, D76)
# profile: server # (git.profile) local (default) | server (D117): the fields below
# base_branch: main # become required. Protected review target, never directly pushed.
# working_branch: cartographer/wiki # optional; defaults to cartographer/<kb-name>
# forge: github # only supported forge today
# github_owner: example-org # must match the origin remote path exactly (HTTPS or SSH)
# github_repository: wiki
# github_api_url: https://api.github.com
# github_token_env: CARTOGRAPHER_GITHUB_TOKEN # the value stays in this process environment only
search:
ollama_url: "" # (search.ollama_url) enables semantic search
ollama_model: nomic-embed-text
audit:
log: "" # (audit.log) path to the audit log's JSONL file
key_seed: "" # (audit.key_seed) Ed25519 hex seed for signing
mode: best_effort # (audit.mode) best_effort (default) | required
max_segment_bytes: 0 # (audit.max_segment_bytes) rotate past this size; 0 = package default
archive_dir: "" # (audit.archive_dir) rotated segments; empty = beside the log
retention_days: 0 # (audit.retention_days) delete checkpointed segments older than N days; 0 = keep
sops:
age_key_dir: /etc/kb-sops-keys # (sops.age_key_dir) directory <age_key_dir>/<name>.age (D53, see below)
tools:
profile: agent # (tools.profile) agent (default: tools/list exposes only the core
# set for the LLM agent) | full (all tools, including governance/plumbing).
# Hidden tools remain callable via tools/call (D65).
kbs[] (YAML) is additive to --kb/CARTOGRAPHER_KB and CARTOGRAPHER_KB_REMOTES (append, not
replace) — all listed KBs get mounted. Scalar fields (http, data, tokens, ...)
instead follow flag > env > YAML precedence: the last explicitly set level wins.
Bootstrapping a KB from a git remote (replaces the init container)¶
Every kbs: [{remote: ...}] entry (YAML) or CARTOGRAPHER_KB_REMOTES (env, CSV of URLs) is
cloned by cartographer serve at startup into <data>/<name> (name derived from the last segment
of the remote, without .git — or the explicit kbs[].name, if set, see D53 below), only if
the destination doesn't already exist — an existing clone is left untouched: the existing
git-autocommit/git-sync cycle handles fetch/push on subsequent writes
(cmd/cartographer/bootstrap.go). Requires --data/CARTOGRAPHER_DATA/data: as the destination
directory for clones.
GIT_SSH_COMMAND for the clone is built from git.ssh_key/git.known_hosts (ssh -i <key>
-o UserKnownHostsFile=<known_hosts> -o StrictHostKeyChecking=yes); if GIT_SSH_COMMAND is already
present in the environment, it wins and the YAML config is ignored. This removes the need for
a separate Kubernetes init container for the initial clone (see §K8s example).
Per-KB git identity (D46): each kbs[] entry can override, for that one KB, the SSH key
(ssh_key/known_hosts) and the author/committer identity (author_name/author_email,
committer_name/committer_email) used by serve for the initial clone and for every commit
(CommitOp, conflict resolution). Fallback cascade: the KB's value → global git.* → the
repository/global Git identity (including Git's native ambient resolution) → hard-coded
cartographer/cartographer@localhost only when Git has no identity. Identity names and emails
must be configured as pairs; the default committer follows the resolved author
(the KB's own or the global one) if committer_* is not set. The per-KB environment assembled
this way overrides the server's process environment (the reverse of the "environment wins"
rule that applies to the global git.ssh_key/GIT_SSH_COMMAND above) — see
internal/gitx.runGitEnv. With the identity set via kbs[].author_name/author_email/committer_*
(or via global git.author_name/author_email), any GIT_AUTHOR_*/GIT_COMMITTER_* in the k8s
Deployment's env: become redundant and can be removed.
If your forge enforces author push rules, set a complete author_name/author_email pair to a
forge account. service install writes the same commented identity block into its generated
configuration. Existing placeholder-authored commits must be rewritten manually before the first
accepted push, because the forge validates every commit in that push.
Explicit KB name, git token, and per-KB SOPS key by convention (D53): kbs[].name fixes the
KB's name (overrides the name derived from remote/path) — it's the name used everywhere: the HTTP
endpoint (/mcp/<name>, see below), the token scope (kb:<name>:r|rw), the clone directory (<data>/<name>), and the two
conventions below. With an http(s):// remote, if the file
<git.token_dir>/<name>.token exists (content: the token, trimmed), the server uses it as the HTTPS
credential for clone/fetch/pull/push of that KB, injected via credential.helper in the
per-process environment (GIT_CONFIG_COUNT/GIT_CONFIG_KEY_0/GIT_CONFIG_VALUE_0, D53): the token
never appears in argv, in the remote URL, or in .git/config — only the file's path is
embedded in the helper, which reads it (cat) whenever git invokes it. The existing SSH support
(ssh_key/known_hosts, global and per-KB, above) remains unchanged for ssh:// remotes.
Similarly, sops.age_key_dir fixes a directory with a per-KB age key
(<age_key_dir>/<name>.age); the cascade for the KB's SOPS key resolution is: kbs[].sops_age_key_file
(explicit override) → <sops.age_key_dir>/<name>.age if the file exists → global
sops.age_key_file.
MCP tool-name prefix (D102)¶
Every KB mounted on a multi-KB server exposes the same 20 tool names (concept_read, search,
...) on its own endpoint (?kb=<name> / /mcp/<name>). Clients that namespace MCP tools per
server — Claude Code, Codex, OpenCode — are unaffected by the collision. Kiro's CLI has a single,
flat tool namespace across every configured MCP server: mounting two KBs there means only one of
them keeps its tools reachable, silently.
The fix is an opt-in, default-off per-KB tool-name prefix: kbs[].tool_prefix (or the global
mcp.tool_prefix_mode: kb-name/CARTOGRAPHER_MCP_TOOL_PREFIX_MODE=kb-name, which derives the
prefix from the KB's own name for every KB that doesn't set its own tool_prefix) registers that
KB's tools as <prefix>__<tool> instead of <tool>. Precedence: kbs[].tool_prefix (explicit) >
mcp.tool_prefix_mode/env (global default) > off. It is off by default so that Claude
Code/Codex/OpenCode deployments, unaffected by the problem, never see a tool rename.
The raw prefix is sanitized before use: lowercased, every run of characters outside [a-z0-9_]
collapsed to a single _, leading/trailing _ stripped. The server fails fast at startup (not at
first tool call) if, after sanitisation, the result is empty, starts with a digit, or the resulting
<prefix>__<tool> name exceeds 48 characters for any tool the KB registers — the error names the
KB and the offending tool/prefix. Read/write classification (§Auth) and the agent/full tools
profile (D65) both match on the tool name after stripping the prefix, so scoped tokens and the
tools profile behave identically with or without a prefix. When 2+ KBs are mounted, serverInfo.name
also becomes cartographer:<kb> (a single-KB deployment keeps the bare cartographer it has
always reported).
cartographer connect/cartographer sync print a warning on stderr whenever the provider being
configured is kiro and 2+ MCP entries (i.e. 2+ KBs) are about to be written, regardless of
whether the server actually has prefixes configured (the client has no way to probe that): the
operator adds tool_prefix/tool_prefix_mode to the server config as the fix.
GET /health's kbs[] items carry the KB's effective prefix as tool_prefix (omitted when
unprefixed) — the exact sanitised value the server registered its tools under, whether it came
from an explicit kbs[].tool_prefix or a derived kb-name mode (D120). Client-owned direct
administrative tool calls (cartographer sync's sync_pull, cartographer reindex) discover
this value from a live /health snapshot before qualifying the tool name; they never re-derive
it from the server's own YAML. A stale client selection (a configured KB no longer among the
KBs the server currently advertises) is reported as an explicit error rather than guessed.
Environment variables¶
Every startup option has a corresponding environment variable (the CLI flag takes precedence):
| Variable | Equivalent flag | Description |
|---|---|---|
CARTOGRAPHER_CONFIG |
--config |
Path to a YAML config file (see §Configuration: flags, env, YAML) |
CARTOGRAPHER_KB |
--kb |
Path(s) to the KB(s), comma-separated |
CARTOGRAPHER_KB_REMOTES |
— | Git remotes to clone into --data at startup, comma-separated (see §Bootstrapping a KB) |
CARTOGRAPHER_DATA |
--data |
Directory whose direct subfolders are separate KBs (auto-discovery) |
CARTOGRAPHER_HTTP |
--http |
HTTP listen address, e.g. :39273 |
CARTOGRAPHER_TOKENS |
--tokens |
Bearer tokens, comma/whitespace-separated. Each entry is either a bare token (admin, full access to every KB) or token\|scope1;scope2 with per-KB scopes kb:<name>:r\|rw (scopes separated by ;, never by spaces/commas, to avoid colliding with the between-entry separator). E.g. admintok, readtok\|kb:wiki:r, writetok\|kb:wiki:rw;kb:notes:r (D44). |
CARTOGRAPHER_AUTH |
— | Explicit auth toggle (see §Auth) |
| — | — | Fine-grained roles have no env form: auth.roles is YAML-only, because a rule is a structured object (see §Fine-grained roles). |
CARTOGRAPHER_GIT_AUTOCOMMIT |
--git-autocommit |
Enables the git commit after every write. Default true; set to false or 0 to disable. |
CARTOGRAPHER_GIT_SYNC |
--git-sync |
If the KB has an origin remote, runs fetch+pull-rebase before reads and writes, then pushes after writes (git as inter-instance sync). Default true; false/0 to disable. Inert if the KB has no remote. Read-side sync is best-effort: a network failure serves the local replica; a rebase conflict is registered and the read also serves locally. |
CARTOGRAPHER_GIT_TOKEN_DIR |
— | Directory with one file per KB (<dir>/<name>.token) used as the HTTPS credential for that KB's git (D53, see §Bootstrapping a KB from a git remote). |
CARTOGRAPHER_SYNC_IN_WINDOW |
— | Go duration (e.g. 30s, 0) of the freshness window on SyncIn: within this window since the last successful fetch+pull, subsequent reads and writes skip it. Default 30s; 0 = sync on every read and write (D76, D93). |
CARTOGRAPHER_SYNC_OUT_DEBOUNCE |
— | Go duration (e.g. 3s, 0) of the per-KB async push debounce: N writes in quick succession = 1 push, performed this long after the last signal. Default 3s; 0 = push synchronously inline, no worker (rollback flag, D76). |
CARTOGRAPHER_SOPS_AGE_KEY_DIR |
— | Directory with a per-KB age key (<dir>/<name>.age), a fallback checked before CARTOGRAPHER_SOPS_AGE_KEY_FILE (D53). |
CARTOGRAPHER_TOOLS_PROFILE |
--tools-profile |
tools/list tool profile: agent (default, only the core set for the LLM agent) | full (all of them). Hidden tools remain callable via tools/call (D65, → control-plane.md §MCP API). |
CARTOGRAPHER_AUDIT_LOG |
— | Path to the audit log's JSONL file (e.g. /data/audit.log). If empty, audit is disabled. |
CARTOGRAPHER_AUDIT_KEY |
— | Ed25519 seed (hex, 64 chars) for signing entries. Requires CARTOGRAPHER_AUDIT_LOG. |
CARTOGRAPHER_SERVER_URL |
— | Client (not server): default server URL for cartographer connect on the client machine when no .cartographer.yaml exists yet. Precedence: existing yaml > env > http://localhost:39273/mcp (D64, internal/clientconfig.Default). |
CARTOGRAPHER_MCP_TOOL_PREFIX_MODE |
— | Global default for mcp.tool_prefix_mode: off (default) | kb-name. Overridden per KB by kbs[].tool_prefix (D102, see §MCP tool-name prefix). |
CARTOGRAPHER_AUTH — three modes:
| Value | Behavior |
|---|---|
false / 0 / no / off |
Auth disabled (e.g. a local service on loopback) |
true / 1 / yes / on |
Auth mandatory — fatal error at startup if no token is configured |
| unset | Auto: enabled if tokens are present, disabled otherwise |
Example: native local service (launchd/systemd, auth off, single client)¶
The local mode (D73) uses the binary already installed by install.sh as a user service:
cartographer service install # generates config + plist/unit, starts the server
cartographer service status # binary, config, installed/running/healthy
cartographer service start|stop|restart
cartographer service uninstall # removes the service; config and data remain
cartographer kb create <name> # scaffolds a KB in the service's data dir (D85)
cartographer kb clone <remote> # mounts an existing remote KB in that data dir (D97)
service install (idempotent: re-running it rewrites the plist/unit and restarts):
- generates ~/.config/cartographer/server.yaml only if it doesn't exist (--config for a different path; --data, default ~/cartographer-data, and --http, default 127.0.0.1:39273, are only used at generation time — if the config already exists they are ignored with a warning: edit the file and run service restart);
- creates the configured data dir if it doesn't exist yet (D83), so a fresh install never leaves serve pointed at a missing directory;
- macOS: LaunchAgent ~/Library/LaunchAgents/com.cartographer.serve.plist (KeepAlive, logging to ~/Library/Logs/cartographer/server.log). The plist's binary path prefers a stable Homebrew symlink (/opt/homebrew/bin/cartographer or /usr/local/bin/cartographer) over the versioned Caskroom path, so it survives brew upgrade without a re-install (D83);
- Linux: systemd user unit ~/.config/systemd/user/cartographer.service (log via journalctl --user -u cartographer; on a headless host, loginctl enable-linger <user> is needed for the service to survive logout).
Binds to loopback by default (127.0.0.1:39273) → auth stays in auto-off mode without exposing anything on the network. With an empty (or missing — D83: serve creates it and treats it as empty rather than failing) data dir the server starts with 0 KBs — /health is still up (liveness: status:"ok") but /ready reports 503 {"ready":false} (D84, §Observability): cartographer kb create <name> scaffolds a subfolder KB the same way serve --kb <path> --init would (D85), while cartographer kb clone <remote> mounts an existing OKF remote there (D97; service install itself prints a hint pointing at creation if it starts with 0 KBs mounted), or add kbs: entries to clone remotes (§Bootstrapping a KB) — either way, service restart (or kb create --restart / kb clone --restart, which do this for you and wait for the server to report healthy again) is what makes the new KB visible.
service status uses systemctl-like exit codes: 0 running, 3 installed but stopped, 4 not installed — this is what lets install.sh update automatically restart only a running service (see §Client installation).
Data/code separation: the cartographer repo contains no data. KBs live in the data dir (default ~/cartographer-data); every subdirectory of it is a separate KB. For multiple users on shared KBs, see the k8s topology below — the server configuration is identical, only where it runs changes; for multiple instances on the same KB, see §Topologies (Multi-server).
Client-side convenience: if cartographer connect targets a loopback URL and the probe fails because the service isn't running, it directly offers to install+start the service (in the interactive flow) or suggests cartographer service install (in the non-interactive one).
Example: K8s pod (untrusted network, auth on, multi-client)¶
No init container: bootstrapping the git remotes (§Bootstrapping a KB from a git remote) is done by
the serve process itself at startup, reading kbs: from the mounted ConfigMap.
ConfigMap (mounts config.yaml at /etc/cartographer):
apiVersion: v1
kind: ConfigMap
metadata:
name: cartographer-config
data:
config.yaml: |
http: ":39273"
auth:
mode: "on"
data: /data
kbs:
- remote: ssh://git@gitea.example.com:2222/user/wiki-kb.git
git:
autocommit: true
sync: true
ssh_key: /etc/kb-ssh/id_ed25519
known_hosts: /etc/kb-ssh/known_hosts
Deployment (excerpt — token and SSH key from a Secret, never in the ConfigMap):
containers:
- name: cartographer
image: ghcr.io/beppetemp/cartographer:<tag>
args: ["serve", "--config=/etc/cartographer/config.yaml"]
env:
- name: CARTOGRAPHER_TOKENS
valueFrom:
secretKeyRef: { name: cartographer-tokens, key: tokens }
volumeMounts:
- { name: config, mountPath: /etc/cartographer, readOnly: true }
- { name: kb-ssh, mountPath: /etc/kb-ssh, readOnly: true }
- { name: data, mountPath: /data }
volumes:
- name: config
configMap: { name: cartographer-config }
- name: kb-ssh
secret: { secretName: cartographer-kb-ssh, defaultMode: 0440 } # root-owned, private key + known_hosts
- name: data
persistentVolumeClaim: { claimName: cartographer-data }
This topology serves multiple clients (one or more agents connected via cartographer connect /
cartographer sync, all over HTTP): git remains the sync/backup layer between the server and the
remote KBs, not a requirement for the client.
HTTP routing (D84)¶
MultiKBServer.Handler (internal/mcpserver/httpserver.go) selects a KB three ways:
- bare /mcp — auto-routes when exactly one KB is mounted (single-KB convenience);
- /mcp?kb=<name> — explicit selection by query parameter;
- /mcp/<name> — explicit selection by path (the form implied by kbs[].name, §Configuration above).
If both /mcp/<name> and ?kb= are present and disagree, the request is rejected with 400
conflicting kb selection rather than silently picking one; an unknown <name> (either form) is
404 unknown kb.
For a client connected to a multi-KB server, cartographer connect and cartographer sync use
the query form deliberately: they create one provider MCP entry per mounted KB,
<server_name>-<kb> → /mcp?kb=<kb>. This makes the selected KB explicit to every current client
without requiring a client to understand path routing. A one-KB server remains a single bare
<server_name> → /mcp entry for backwards compatibility.
Runtime secrets¶
Git credentials, MCP tokens, and the age/SOPS key (SOPS_AGE_KEY_FILE) are
runtime secrets — never in the bundles. The same applies to the bootstrap SSH
key (git.ssh_key): mount or configure it outside committed YAML.
Cold start¶
clone/pull of the KBs (including bootstrapping the remotes in kbs:, §Bootstrapping a KB from a git remote) + rebuilding missing indices. Per-KB incremental startup (a KB is served as soon as it's ready); with many KBs, the first startup is not instant.
The persisted SQLite index (<kb>/.cartographer/index.db) is excluded from git via .git/info/exclude (D62, never versioned): after a fresh clone (e.g. pod restart) it starts empty even if the concepts are already on disk. At startup, for every mounted KB, the server reconciles content hashes from the .md files against the persisted index, adding new concepts, refreshing changed ones, and removing vanished ones — keyword/FTS5 only (no embeddings: Ollama may be unreachable at boot). The same reconciliation runs after a SyncIn pull that moves HEAD. Best-effort: an error doesn't block startup, it is logged to stderr.
Imports and manual filesystem edits no longer require a service restart: call the MCP reindex tool, or run cartographer reindex [--kb <name>]. The CLI calls a healthy configured server over HTTP so it never opens that server's SQLite database; only when the server is down does it reconcile the configured local KB databases directly.
Administration¶
Run cartographer help for the authoritative command list. Server lifecycle,
KB create/clone, client connection/sync, import, resolve and reindex have CLI
commands; concept writes, snapshots and contradiction resolution remain MCP
tool operations. There is no separate runtime token-registration command.
Fine-grained roles¶
auth.roles declares named permission sets that narrow a token below whole-KB
granularity (D118). Semantics, the resource classes and the enforcement
guarantees are in transport-auth §Roles and fine-grained
permissions; this is the operator view.
auth:
mode: "on"
roles:
- name: runbook-editor
rules:
- kb: homelab
access: rw # r | rw
maps: [infra] # empty = every map
types: [Runbook] # empty = every type
- kb: reference
access: r
tokens:
- token: ${CARTOGRAPHER_TOKEN}
id: ci # stable principal id for logs; derived from a
# token digest when omitted
roles: [runbook-editor]
Rollout notes:
- Nothing changes until you opt in. Tokens without
rolesbehave exactly as before: unscoped tokens stay admin,scopeskeep granting whole-KB access. - Migrate one token at a time.
rolesandscopeson the same token are unioned, so a token can gain a narrow role while keeping an existing scope. - Validation is fatal at startup. A duplicate role or principal ID, an
unknown role reference, an empty
kb, anaccessother thanr/rw, an empty or traversal selector, or a selector declared as both map and journal aborts the boot. Diagnostics name the role and rule index, never a token. - Selectors are not validated against KB content: a role may reference a map that does not exist yet, and grants nothing until it does.
- Verify a rollout with a real request rather than by reading config: a token
outside its perimeter receives a generic
not found, deliberately indistinguishable from a missing concept.
Audit log¶
audit.log turns on the compliance trail (D119). Semantics and the guarantees
are in transport-auth §Operational audit; this is the
operator view.
audit:
log: /var/lib/cartographer/audit.jsonl
key_seed: ${CARTOGRAPHER_AUDIT_SEED} # Ed25519 hex seed; enables signatures
mode: required # best_effort (default) | required
max_segment_bytes: 67108864
archive_dir: /var/lib/cartographer/audit-archive
retention_days: 400
Rollout notes:
- Start in
best_effort. It is the default and the pre-D119 behaviour: a broken sink never blocks a call. Move torequiredonly once the log's storage is as available as the service itself — inrequiredmode an unwritable audit log takes MCP calls down with it, which is the correct trade for a compliance deployment and the wrong one everywhere else. - Back up
archive_dirtogether with the checkpoint index. Retention only ever deletes a segment already covered by a signed checkpoint, so verification still succeeds afterwards, but only if the index survives. - Keep the public key. Without it
audit verifychecks the chain but not the signatures; it reports the unsigned count rather than silently passing. - Verify from the operator machine, not the server: both commands read files and never contact a running instance, so they work during an outage.
Observability¶
Cartographer emits structured operational messages and per-phase sync timing
to stderr. It does not expose Prometheus metrics or derive token/queue/search
SLIs from the audit file; the audit log is evidence for compliance review
(cartographer audit verify|export), not a metrics source.
Liveness vs readiness (D84): /health is liveness — status:"ok" unconditionally (a probe that
restarted the process on status != "ok" must never fire from a KB-mounting issue); it also carries
a ready: <bool> field (single-KB server: always true; MultiKB: len(kbs) > 0) for callers that
want both signals from one request. /ready is the dedicated readiness endpoint: 200
{"ready":true} once at least one KB is mounted, 503 {"ready":false,"kbs":0} otherwise (e.g. a
fresh local install with an empty data dir, §Cold start). Point k8s
livenessProbe at /health and readinessProbe at /ready.
Backup and disaster recovery¶
RPO/RTO¶
The git remote is the primary copy: the push debounce determines how many commits are lost if the host dies → fix the cadence and replicate the remote.
Lost SOPS key¶
A truly unrecoverable scenario. Prevent it with multi-recipient escrow + restore testing (→ skills-services-secrets.md §break-glass).
Working-tree crash recovery¶
The server does not repair arbitrary interrupted git state at boot. Before mounting, inspect and clean up an active merge/rebase, stale lock file or unexpected working-tree changes using normal git recovery procedures. Keep a backup before discarding any state.
External component failures¶
| Component | Behavior |
|---|---|
| Git remote unreachable | The local commit remains; asynchronous push logs the failure and a later sync retries |
| Invalid/expired git credentials | The git command fails and the error is surfaced; rotate the configured token/key |
| Disk full | Atomic writes fail cleanly |
| Corrupted SQLite index | Rebuilt from the .md files |
Upgrades, schema migration, and repo growth¶
- Server upgrade with mounted KBs: graceful HTTP shutdown flushes pending pushes before the process exits.
- Homebrew upgrade:
brew upgradereplaces the binary behind Homebrew's stable symlink, but an already-running native service keeps its old process untilcartographer service restart. Runcartographer statusafter upgrading: it reports client/server versions and warns about skew, including the restart hint for an installed local service. Do not restart from a cask hook: an explicit restart keeps the operator in control of the drain. For Kubernetes, bump the image tag; connected clients surface the same version skew while the rollout catches up. - KB format changes: follow the release notes and back up the git remote before a migration. There is no general automatic schema migration engine.
- Repo growth: keep large binary/source archives outside the KB and link them; Cartographer is optimized for Markdown knowledge repositories.
CI/CD: release + deploy pipeline¶
Three GitHub Actions workflows (.github/workflows/):
ci.yml— on every PR and push tomain: thetestjob (make vet && make test, Go fromgo.mod, git identity configured for git-backed tests) and thepr-titlejob (lints the PR title as a conventional commit: with squash-merge the title becomes the commit onmainthat release-please reads).testis the required status check onmain's ruleset.release-please.yml— on every push tomain: maintains the release PR (semver bump computed from conventional commits +CHANGELOG.md). On merge, it creates thevX.Y.Ztag and GitHub Release with the notes. Uses theRELEASE_PLEASE_TOKENPAT (PRs opened with the defaultGITHUB_TOKENdo not triggerpull_requestworkflows).release.yml— on thev*tag: GoReleaser (.goreleaser.yaml) builds thedarwin/linux × amd64/arm64binaries (-X main.version=v<semver>, read bycartographer version), attaches them to the existing release withsha256sums.txt(release.mode: keep-existing— the release and its notes belong to release-please) and publishes the Homebrew cask onBeppeTemp/homebrew-tap(HOMEBREW_TAP_TOKENPAT, macOS only); in parallel, buildx publishes the multi-arch imageghcr.io/beppetemp/cartographer:<tag>+:latest. After the image push, themcp-registryjob publishes the server's metadata to the official MCP Registry (io.github.beppetemp/cartographer, OCI package pointing at the ghcr image):mcp-publisher login github-oidc(no long-lived secret) +publishwithserver.json(repo root;versionand image tag are stamped by the job). Ownership is verified via theio.modelcontextprotocol.server.nameLABEL baked into the image.
Dependency updates (Go modules and action versions) come from Dependabot (.github/dependabot.yml, weekly, grouped PRs).
Deployment to the maintainer's infrastructure happens outside the pipeline: the cluster is GitOps, and the image tag bump in the manifests repo is reconciled by Flux (D68). CI does not run kubectl apply: it would be redundant, would conflict with Flux's drift control, and would require a long-lived kubeconfig in the repo's secrets.
Secrets required in the GitHub repo: RELEASE_PLEASE_TOKEN, HOMEBREW_TAP_TOKEN. GITHUB_TOKEN is provided automatically by Actions (for ghcr.io the packages: write permission declared in the workflow is enough).
The end-to-end operator procedure (release, rollout verification, local client update) lives outside the repo, in the maintainer's local tooling: it contains details of the private deployment infrastructure.
Client installation (install.sh)¶
Preferred path on macOS: brew install beppetemp/tap/cartographer (cask from the tap). Alternatively (and on Linux), install.sh (repo root) downloads the latest cartographer binary from the most recent GitHub Release for the current platform:
curl -fsSL https://raw.githubusercontent.com/BeppeTemp/cartographer/main/install.sh | sh
curl -fsSL .../install.sh | sh -s -- update # updates if not already on the latest release
curl -fsSL .../install.sh | sh -s -- uninstall
No package-manager dependency: pure POSIX sh, verifies the checksum via sha256sums.txt if present in the release, installs into /usr/local/bin (falling back to ~/.local/bin if not writable, overridable with CARTOGRAPHER_INSTALL_DIR). See docs/configurator.md §Installation.
After an update, if the local service (§Example: native local service) is found running (service status → exit 0) the script restarts it automatically, so the daemonized server switches to the new binary right away; a service stopped on purpose (exit 3) stays stopped.
The repo is public on GitHub: a plain
curlworks with no authentication. The script supports an optionalGITHUB_TOKENto avoid API rate limits on frequent runs (e.g. CI).