aim-ops
Server lifecycle for ai-memory (engine by akitaonrails/ai-memory, packaged and
extended by an ops repo that builds the images and holds the runbooks). This skill
is the reproducible procedure; the ops repo holds the real compose, images and
instance-specific runbooks. Never write secrets, digests or hostnames of a real
instance into this skill — they belong in the operator's private runbooks/memory.
Client-side wiring (marker, MCP entry, hooks, local CLI) → aim-init.
Route intent
- "deploy / self-host / subir uma instância nova", "new environment" (PT: "instala o ai-memory num servidor", "sobe uma instância") → install.
- "upgrade / update the server", "saiu versão nova, qual o impacto?" (PT: "atualiza o engine", "faz upgrade") → upgrade (references/upgrade-playbook.md).
- "change the LLM / embeddings", "too expensive", "run a local model?" (PT: "troca o modelo", "desliga o Cloudflare") → providers (references/providers.md).
- "issue/revoke an API key for X", "hooks 401/403" → keys.
- "backup / restore / rollback" → backup-restore.
- "is it healthy?", "what's running?", "why is capture failing?" (PT: "como está o servidor?") → doctor (read-only).
Before any write: confirm which instance (there may be several), the host access path
(SSH user@host, compose dir), and that a backup exists or will be taken first. Use the
harness's structured-question tool for install-time choices (edge, providers, mirror).
Anatomy (what an instance is)
One Docker Compose project on a single host, loopback-only, fronted by a tunnel/reverse proxy.
Full description, env-by-service and invariants in
references/compose-stack.md; templates in templates/.
| Service |
Role |
Must know |
ai-memory (engine) |
MCP + /api/v1 + /admin + web UI; SQLite + wiki on ./data |
pinned by digest; AI_MEMORY_IN_CONTAINER=1; user: 1000:1000 |
mcp-auth |
forward-auth sidecar: validates consumer keys (amk_, keys-only mode) or OIDC JWT; injects X-Memory-Actor-*; serves /keys* |
proxy token ≠ root token; PASSTHROUGH_UNKNOWN_BEARER=0 |
caddy |
edge on 127.0.0.1:8080: public SPA + /auth*, /keys* → sidecar, everything else forward_auth → engine |
strips client-supplied actor headers; tunnel points here |
scope-guard |
blocking admission webhook: per-actor write ACL |
empty actor ⇒ needs HOOK_AUTH_USERNAME on mcp-auth |
contributors |
non-blocking webhook: stamps contributors: frontmatter |
— |
git-mirror |
non-blocking webhook: pushes every write to a git backup repo |
deploy key in ./git-ssh; loses near-simultaneous writes (re-write to resync) |
Data dir layout: data/db/memory.sqlite, data/wiki/<ws-uuid>/<proj-uuid>/…, data/backups/
(2.0+ automatic pre-migration archives), data/logs/.
install (new environment)
- Host prerequisites: Linux x86_64/arm64, Docker + Compose v2, 4+ vCPU, 8+ GiB RAM
(30 GiB comfortable if you plan local models), disk ≥ 5× the expected data dir (backups
are full copies). Outbound access to the image registry and providers. No inbound ports:
use a tunnel (Cloudflare Tunnel) or a TLS reverse proxy to
127.0.0.1:8080.
- Images: build via the ops repo
build-images.yml (workflow_dispatch) with immutable
refs — engine_ref=<release tag> and ui_ref=<40-char SHA> — never main/latest. The
engine image bundles the custom UI. Record the resulting candidate-<run>-<n> tag and pull
it on the host; verify before use:docker run --rm --entrypoint ai-memory <image> --version
docker inspect <image> --format '{{index .RepoDigests 0}}' # → pin this digest in compose
- Compose dir (e.g.
/opt/ai-memory): copy templates/compose.yml.tmpl,
templates/Caddyfile.tmpl, templates/.env.example.tmpl
→ compose.yml, Caddyfile, .env (chmod 600). Fill placeholders: hostname, root
username, providers, mirror repo. Generate secrets locally (openssl rand -hex 32):
AI_MEMORY_AUTH_TOKEN (root), ACTOR_PROXY_BEARER_TOKEN (distinct from root),
HOOK_AUTH_TOKEN, AI_MEMORY_AUTH__TOKEN_PEPPER (never rotate after first use — it seals
native aim_ credentials), AI_MEMORY_AUTH__RECOVERY_TOKEN.
AI_MEMORY_AUTH__TRUSTED_PROXY_CIDRS = the Caddy container IP/32 on the compose network
(read it after first up, then restart the engine).
- Providers: decide LLM (consolidation/lint/auto-improve/explore) and embeddings per
references/providers.md. Set the provider env explicitly —
on 2.0+ an unset embedding provider silently enables the bundled English-only local model.
Zero-LLM is valid (capture/search/handoff work); consolidation just won't run.
- First start:
docker compose up -d, then docker compose logs -f ai-memory until
ai-memory starting version=…, embedder enabled …, memory_consolidate … enabled (if LLM),
starting wiki watcher. Then docker compose ps → all healthy/Up.
- Root human bootstrap: set
AI_MEMORY_AUTH__INITIAL_ROOT_PASSWORD once, start, log in at
/login, change the password, then remove the variable and recreate the engine. Keep
the recovery token in a root-only file on the host, never in git.
- Consumer keys (mcp-auth keys-only): bootstrap the first
admin-scoped amk_ key with
SQL inside the sidecar container (see compose-stack.md → "Bootstrap admin key"), then
issue one key per consumer through the API (owner is derived from the caller):curl -s -X POST https://<instance>/keys -H "Authorization: Bearer <admin-amk>" \
-H 'Content-Type: application/json' \
-d '{"id":"claude-code","actor_user":"<username>","scopes":["read","write"]}'
# → plaintext key printed ONCE. Scopes: read | write | admin. Rotation = create + revoke (DELETE /keys/{id}).
/admin/* answers 403 to a key without admin scope — expected, not an engine error.
- Wire clients with
aim-init (install-mcp/install-hooks with each consumer's own
amk_ key). Hooks use HOOK_AUTH_TOKEN or a consumer key; HOOK_AUTH_USERNAME must
name an ACL-allowed user or scope-guard 403s every capture silently.
- Edge: point the tunnel/proxy hostname at
http://127.0.0.1:8080. Verify from outside:
/ and /login → 200 (public SPA), /mcp → 401 without bearer, /api/v1/workspaces → 200
with a read key, /keys/whoami with the admin key → can_issue:true.
- Backups: git-mirror pushing (
push ok in its logs after a test write) and a periodic
ai-memory backup --to /data/backup-<ts>.tar.gz copied off the volume. Record the
instance facts (host, compose dir, digests, providers, key ids — no secrets) in the
operator's runbook/memory.
upgrade
Full gated procedure and gotcha table: references/upgrade-playbook.md.
Summary of the gates — do not skip any:
- Impact read: release notes +
CHANGELOG between the running version and the target;
diff crates/ai-memory-store/migrations/ and the HTTP route set (custom UI compatibility);
check config keys you set still exist in config.rs. Note one-way migrations.
- Refs: the engine image builds from a fork; sync
fork/main from upstream and push
the release tag to the fork (the Dockerfile cache-busts against the fork's commit API).
Cancel the fork's redundant release.yml run if upstream already published binaries.
- Build + verify the candidate (
--version inside the image) and pin its digest.
- Backup off-volume (
ai-memory backup from inside the running engine, then move the
tarball out of data/). Check free disk ≥ 3× data dir.
- Rehearse on a clone: scripts/rehearse-upgrade.sh extracts the
backup to
scratch/, boots the candidate against it with --network none, providers off,
webhooks [], using the env rendered by docker compose config — and reports whether the
store opens, migrations apply, and the wiki migration completes. A refused store here is a
stop, not a prod retry.
- Cutover:
compose.yml → new digest (+ any new required env), docker compose up -d ai-memory, follow logs to starting wiki watcher. Expect minutes of downtime for large
stores (2.0's backup-gated OKF migration ≈ 4 min for ~3 GB).
- Validate: MCP
memory_status / memory_query (vector) / memory_explore (LLM) from a
client; /login, /api/v1/*; engine log WARN|ERROR count; git-mirror push ok after a
test write; container healthy.
- Clients: upgrade the local CLI to the same release and re-run
install-hooks --apply
per agent (aim-init refresh).
- Record: runbook §state (digest, workflow run, refs, date, deviations) + memory page.
Rollback = old digest in compose + restore the pre-upgrade tarball into data/ (post-2.0
data dirs are refused by 1.x binaries; the tarball is the only way back).
providers
See references/providers.md: env matrix, key resolution order,
cost model per consolidation, context requirements (≥64k input comfortable), reasoning
off, multilingual embeddings, when a local model is worth it, and auto-improve settings
(AI_MEMORY_AUTO_IMPROVE__SCHEDULER__ENABLED, …__REQUIRE_APPROVAL, ai-memory pending-writes).
Switching the embedding provider/model/dim invalidates existing vectors (hybrid search
ignores mismatched triples until ai-memory embed --force or the startup backfill re-embeds).
keys
- List:
GET /keys (admin key). Issue: POST /keys {id, actor_user, scopes[], expires_at?}.
Revoke: DELETE /keys/{id} (soft). No rotate endpoint: issue new, switch consumer, revoke old.
- Hook token last: rotate
HOOK_AUTH_TOKEN only after POST /hook with the current one
returns 202; a 401 there fills every client's spool.
- Rotating
ACTOR_PROXY_BEARER_TOKEN requires recreating engine and mcp-auth (same value
in both; the token-check script in compose-stack.md prints MATCH/DIFFER without values).
backup-restore
- Take:
docker exec <engine> ai-memory backup --to /data/backup-<ts>.tar.gz → move out of
data/ (and off-host periodically). 2.0+ also leaves data/backups/ai-memory-backup-okf-*.tar.gz
after its migration; the homepage shows a notice until you delete it.
- Restore:
docker compose stop ai-memory → empty or move data/ aside → extract the tarball
into data/ (or ai-memory restore --from … --force inside a one-off container) → fix
ownership 1000:1000 → start with the binary version that wrote the backup (or newer).
- git-mirror is a second copy of the wiki markdown (not the SQLite): useful for history and
diffing, not for a full restore.
doctor (read-only)
Report, without writing: running image digest ↔ --version inside the container; upstream latest
release; docker compose ps health; engine log WARN|ERROR in the last 15 min (watcher
"does not belong to workspace" = orphan wiki dirs of purged projects → move them out);
providers announced in the startup log (embedder model/dim, LLM model); auto-improve scheduler
state; PRAGMA foreign_key_check on a read-only DB copy; data/backups and off-volume tarballs
with dates; git-mirror last push ok; edge probes (/, /login, /mcp 401, /keys/whoami);
token-invariant check (proxy ≠ root, engine == sidecar); disk free vs data dir size; consumer key
list with last_used; hook spool health on clients (aim-init doctor).
1---2name: aim-ops3description: Operate an ai-memory SERVER end to end: install a new instance (Docker Compose stack: engine + mcp-auth consumer keys + Caddy edge + admission webhooks + git-mirror, behind a tunnel), upgrade the engine/UI image safely (immutable refs, digest pin, backup, rehearsal on a data clone, one-way migrations), choose/switch LLM and embedding providers, manage consumer keys and auto-improve, back up/restore, and diagnose. Use when the user asks to deploy, host, self-host, upgrade, update, migrate, restore, or troubleshoot an ai-memory instance, change its LLM/embeddings, or issue API keys for it. Client-side wiring (repo marker, MCP entry, hooks, local CLI) is aim-init, not this skill.4---56# aim-ops78Server lifecycle for **ai-memory** (engine by `akitaonrails/ai-memory`, packaged and9extended by an **ops repo** that builds the images and holds the runbooks). This skill10is the reproducible procedure; the ops repo holds the real compose, images and11instance-specific runbooks. **Never** write secrets, digests or hostnames of a real12instance into this skill — they belong in the operator's private runbooks/memory.1314Client-side wiring (marker, MCP entry, hooks, local CLI) → [aim-init](../aim-init/SKILL.md).1516## Route intent1718- "deploy / self-host / subir uma instância nova", "new environment" (PT: "instala o ai-memory num servidor", "sobe uma instância") → **install**.19- "upgrade / update the server", "saiu versão nova, qual o impacto?" (PT: "atualiza o engine", "faz upgrade") → **upgrade** ([references/upgrade-playbook.md](references/upgrade-playbook.md)).20- "change the LLM / embeddings", "too expensive", "run a local model?" (PT: "troca o modelo", "desliga o Cloudflare") → **providers** ([references/providers.md](references/providers.md)).21- "issue/revoke an API key for X", "hooks 401/403" → **keys**.22- "backup / restore / rollback" → **backup-restore**.23- "is it healthy?", "what's running?", "why is capture failing?" (PT: "como está o servidor?") → **doctor** (read-only).2425Before any write: confirm **which instance** (there may be several), the host access path26(SSH user@host, compose dir), and that a backup exists or will be taken first. Use the27harness's structured-question tool for install-time choices (edge, providers, mirror).2829## Anatomy (what an instance is)3031One Docker Compose project on a single host, loopback-only, fronted by a tunnel/reverse proxy.32Full description, env-by-service and invariants in33[references/compose-stack.md](references/compose-stack.md); templates in [templates/](templates/).3435| Service | Role | Must know |36|---|---|---|37| `ai-memory` (engine) | MCP + `/api/v1` + `/admin` + web UI; SQLite + wiki on `./data` | pinned **by digest**; `AI_MEMORY_IN_CONTAINER=1`; `user: 1000:1000` |38| `mcp-auth` | forward-auth sidecar: validates consumer keys (`amk_`, keys-only mode) or OIDC JWT; injects `X-Memory-Actor-*`; serves `/keys*` | proxy token **≠** root token; `PASSTHROUGH_UNKNOWN_BEARER=0` |39| `caddy` | edge on `127.0.0.1:8080`: public SPA + `/auth*`, `/keys*` → sidecar, everything else `forward_auth` → engine | strips client-supplied actor headers; tunnel points here |40| `scope-guard` | blocking admission webhook: per-actor write ACL | empty actor ⇒ needs `HOOK_AUTH_USERNAME` on mcp-auth |41| `contributors` | non-blocking webhook: stamps `contributors:` frontmatter | — |42| `git-mirror` | non-blocking webhook: pushes every write to a git backup repo | deploy key in `./git-ssh`; loses near-simultaneous writes (re-write to resync) |4344Data dir layout: `data/db/memory.sqlite`, `data/wiki/<ws-uuid>/<proj-uuid>/…`, `data/backups/`45(2.0+ automatic pre-migration archives), `data/logs/`.4647## install (new environment)48491. **Host prerequisites:** Linux x86_64/arm64, Docker + Compose v2, 4+ vCPU, 8+ GiB RAM50 (30 GiB comfortable if you plan local models), disk ≥ 5× the expected data dir (backups51 are full copies). Outbound access to the image registry and providers. No inbound ports:52 use a tunnel (Cloudflare Tunnel) or a TLS reverse proxy to `127.0.0.1:8080`.532. **Images:** build via the ops repo `build-images.yml` (workflow_dispatch) with **immutable**54 refs — `engine_ref=<release tag>` and `ui_ref=<40-char SHA>` — never `main`/`latest`. The55 engine image bundles the custom UI. Record the resulting `candidate-<run>-<n>` tag and pull56 it on the host; **verify** before use:57 ```bash58 docker run --rm --entrypoint ai-memory <image> --version59 docker inspect <image> --format '{{index .RepoDigests 0}}' # → pin this digest in compose60 ```613. **Compose dir** (e.g. `/opt/ai-memory`): copy [templates/compose.yml.tmpl](templates/compose.yml.tmpl),62 [templates/Caddyfile.tmpl](templates/Caddyfile.tmpl), [templates/.env.example.tmpl](templates/.env.example.tmpl)63 → `compose.yml`, `Caddyfile`, `.env` (chmod 600). Fill placeholders: hostname, root64 username, providers, mirror repo. Generate secrets locally (`openssl rand -hex 32`):65 `AI_MEMORY_AUTH_TOKEN` (root), `ACTOR_PROXY_BEARER_TOKEN` (**distinct** from root),66 `HOOK_AUTH_TOKEN`, `AI_MEMORY_AUTH__TOKEN_PEPPER` (never rotate after first use — it seals67 native `aim_` credentials), `AI_MEMORY_AUTH__RECOVERY_TOKEN`.68 `AI_MEMORY_AUTH__TRUSTED_PROXY_CIDRS` = the Caddy container IP/32 on the compose network69 (read it after first `up`, then restart the engine).704. **Providers:** decide LLM (consolidation/lint/auto-improve/explore) and embeddings per71 [references/providers.md](references/providers.md). Set the provider env **explicitly** —72 on 2.0+ an unset embedding provider silently enables the bundled English-only local model.73 Zero-LLM is valid (capture/search/handoff work); consolidation just won't run.745. **First start:** `docker compose up -d`, then `docker compose logs -f ai-memory` until75 `ai-memory starting version=…`, `embedder enabled …`, `memory_consolidate … enabled` (if LLM),76 `starting wiki watcher`. Then `docker compose ps` → all `healthy`/`Up`.776. **Root human bootstrap:** set `AI_MEMORY_AUTH__INITIAL_ROOT_PASSWORD` once, start, log in at78 `/login`, change the password, then **remove** the variable and recreate the engine. Keep79 the recovery token in a root-only file on the host, never in git.807. **Consumer keys (mcp-auth keys-only):** bootstrap the first `admin`-scoped `amk_` key with81 SQL **inside** the sidecar container (see compose-stack.md → "Bootstrap admin key"), then82 issue one key per consumer through the API (owner is derived from the caller):83 ```bash84 curl -s -X POST https://<instance>/keys -H "Authorization: Bearer <admin-amk>" \85 -H 'Content-Type: application/json' \86 -d '{"id":"claude-code","actor_user":"<username>","scopes":["read","write"]}'87 # → plaintext key printed ONCE. Scopes: read | write | admin. Rotation = create + revoke (DELETE /keys/{id}).88 ```89 `/admin/*` answers **403** to a key without `admin` scope — expected, not an engine error.908. **Wire clients** with `aim-init` (`install-mcp`/`install-hooks` with each consumer's own91 `amk_` key). Hooks use `HOOK_AUTH_TOKEN` **or** a consumer key; `HOOK_AUTH_USERNAME` must92 name an ACL-allowed user or scope-guard 403s every capture silently.939. **Edge:** point the tunnel/proxy hostname at `http://127.0.0.1:8080`. Verify from outside:94 `/` and `/login` → 200 (public SPA), `/mcp` → 401 without bearer, `/api/v1/workspaces` → 20095 with a read key, `/keys/whoami` with the admin key → `can_issue:true`.9610. **Backups:** git-mirror pushing (`push ok` in its logs after a test write) and a periodic97 `ai-memory backup --to /data/backup-<ts>.tar.gz` copied **off the volume**. Record the98 instance facts (host, compose dir, digests, providers, key ids — no secrets) in the99 operator's runbook/memory.100101## upgrade102103Full gated procedure and gotcha table: [references/upgrade-playbook.md](references/upgrade-playbook.md).104Summary of the gates — do not skip any:1051060. **Impact read:** release notes + `CHANGELOG` between the running version and the target;107 diff `crates/ai-memory-store/migrations/` and the HTTP route set (custom UI compatibility);108 check config keys you set still exist in `config.rs`. Note one-way migrations.1091. **Refs:** the engine image builds from a **fork**; sync `fork/main` from upstream and push110 the release **tag** to the fork (the Dockerfile cache-busts against the fork's commit API).111 Cancel the fork's redundant `release.yml` run if upstream already published binaries.1122. **Build + verify** the candidate (`--version` inside the image) and pin its digest.1133. **Backup off-volume** (`ai-memory backup` from inside the running engine, then move the114 tarball out of `data/`). Check free disk ≥ 3× data dir.1154. **Rehearse on a clone:** [scripts/rehearse-upgrade.sh](scripts/rehearse-upgrade.sh) extracts the116 backup to `scratch/`, boots the candidate against it with `--network none`, providers off,117 webhooks `[]`, using the env rendered by `docker compose config` — and reports whether the118 store opens, migrations apply, and the wiki migration completes. **A refused store here is a119 stop**, not a prod retry.1205. **Cutover:** `compose.yml` → new digest (+ any new required env), `docker compose up -d121 ai-memory`, follow logs to `starting wiki watcher`. Expect minutes of downtime for large122 stores (2.0's backup-gated OKF migration ≈ 4 min for ~3 GB).1236. **Validate:** MCP `memory_status` / `memory_query` (vector) / `memory_explore` (LLM) from a124 client; `/login`, `/api/v1/*`; engine log `WARN|ERROR` count; git-mirror `push ok` after a125 test write; container `healthy`.1267. **Clients:** upgrade the local CLI to the same release and re-run `install-hooks --apply`127 per agent (`aim-init refresh`).1288. **Record:** runbook §state (digest, workflow run, refs, date, deviations) + memory page.129130Rollback = old digest in compose + restore the pre-upgrade tarball into `data/` (post-2.0131data dirs are refused by 1.x binaries; the tarball is the only way back).132133## providers134135See [references/providers.md](references/providers.md): env matrix, key resolution order,136cost model per consolidation, context requirements (≥64k input comfortable), reasoning137off, multilingual embeddings, when a local model is worth it, and auto-improve settings138(`AI_MEMORY_AUTO_IMPROVE__SCHEDULER__ENABLED`, `…__REQUIRE_APPROVAL`, `ai-memory pending-writes`).139Switching the **embedding** provider/model/dim invalidates existing vectors (hybrid search140ignores mismatched triples until `ai-memory embed --force` or the startup backfill re-embeds).141142## keys143144- List: `GET /keys` (admin key). Issue: `POST /keys {id, actor_user, scopes[], expires_at?}`.145 Revoke: `DELETE /keys/{id}` (soft). No rotate endpoint: issue new, switch consumer, revoke old.146- Hook token last: rotate `HOOK_AUTH_TOKEN` only after `POST /hook` with the current one147 returns 202; a 401 there fills every client's spool.148- Rotating `ACTOR_PROXY_BEARER_TOKEN` requires recreating engine **and** mcp-auth (same value149 in both; the token-check script in compose-stack.md prints MATCH/DIFFER without values).150151## backup-restore152153- **Take:** `docker exec <engine> ai-memory backup --to /data/backup-<ts>.tar.gz` → move out of154 `data/` (and off-host periodically). 2.0+ also leaves `data/backups/ai-memory-backup-okf-*.tar.gz`155 after its migration; the homepage shows a notice until you delete it.156- **Restore:** `docker compose stop ai-memory` → empty or move `data/` aside → extract the tarball157 into `data/` (or `ai-memory restore --from … --force` inside a one-off container) → fix158 ownership `1000:1000` → start with the **binary version that wrote the backup** (or newer).159- **git-mirror** is a second copy of the wiki markdown (not the SQLite): useful for history and160 diffing, not for a full restore.161162## doctor (read-only)163164Report, without writing: running image digest ↔ `--version` inside the container; upstream latest165release; `docker compose ps` health; engine log `WARN|ERROR` in the last 15 min (watcher166"does not belong to workspace" = orphan wiki dirs of purged projects → move them out);167providers announced in the startup log (embedder model/dim, LLM model); auto-improve scheduler168state; `PRAGMA foreign_key_check` on a read-only DB copy; `data/backups` and off-volume tarballs169with dates; git-mirror last `push ok`; edge probes (`/`, `/login`, `/mcp` 401, `/keys/whoami`);170token-invariant check (proxy ≠ root, engine == sidecar); disk free vs data dir size; consumer key171list with `last_used`; hook spool health on clients (`aim-init doctor`).