@cairnkeep/cli 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,37 @@
3
3
  All notable user-facing changes are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [2.2.0] - 2026-07-20
7
+
8
+ ### Added
9
+
10
+ - Add unprivileged memory-server and workspace OCI images with rootless Podman
11
+ launchers, authenticated HTTP deployment examples, and persistent storage.
12
+ - Add isolated-copy and explicit shared-checkout workspace modes for private
13
+ derived distributions.
14
+ - Verify stdio startup, HTTP authentication, memory persistence across
15
+ replacement, and both workspace modes in container CI.
16
+
17
+ ## [2.1.0] - 2026-07-16
18
+
19
+ ### Added
20
+
21
+ - Add `memory_apply_reviewed` for idempotent application of externally reviewed
22
+ memory revisions with durable provenance and collision-safe history.
23
+ - Add `memory_invalidate_reviewed` for revision-matched retraction and durable
24
+ tombstones that prevent delayed writes from resurrecting invalid memory.
25
+ - Add end-to-end checks for replay, supersession, manual changes, retraction,
26
+ tombstones, and reserved provenance records.
27
+
28
+ ### Changed
29
+
30
+ - Make history snapshot keys unique when multiple revisions occur in the same
31
+ millisecond.
32
+
33
+ The existing memory tools, database locations, and remote HTTP routing contract
34
+ are unchanged. Existing clients remain compatible and reviewed-memory records
35
+ are created only when an integration explicitly invokes the new tools.
36
+
6
37
  ## [2.0.0] - 2026-07-15
7
38
 
8
39
  ### Breaking
@@ -69,6 +100,8 @@ stored memories.
69
100
  - Preserve executable permissions and Bash 3.2 portability on macOS.
70
101
  - Add backup-first uninstall and SQLite-safe memory export/import guidance.
71
102
 
103
+ [2.2.0]: https://github.com/cairnkeep/cairnkeep/compare/v2.1.0...v2.2.0
104
+ [2.1.0]: https://github.com/cairnkeep/cairnkeep/compare/v2.0.0...v2.1.0
72
105
  [2.0.0]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.3...v2.0.0
73
106
  [1.1.3]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.2...v1.1.3
74
107
  [1.1.2]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.1...v1.1.2
package/README.md CHANGED
@@ -41,6 +41,24 @@ and 20 are end-of-life upstream.
41
41
  Linux, macOS, Bash 3.2 portability, and clean Node 22/24/26 runtime checks
42
42
  are exercised in CI.
43
43
 
44
+ ## Try with Podman
45
+
46
+ The minimal OCI image runs the memory server as an unprivileged user and keeps
47
+ all databases in a named volume:
48
+
49
+ ```bash
50
+ podman run --rm -i \
51
+ --userns=keep-id:uid=10001,gid=10001 \
52
+ --read-only --cap-drop=all --security-opt=no-new-privileges \
53
+ --tmpfs=/tmp:rw,noexec,nosuid,size=64m,mode=1777 \
54
+ --volume cairnkeep-data:/data:Z,U \
55
+ ghcr.io/cairnkeep/cairnkeep:latest stdio
56
+ ```
57
+
58
+ For persistent authenticated HTTP, rootless Quadlet, isolated workspace mode,
59
+ storage paths, secrets, and private derived images, see
60
+ **[Containers](docs/containers.md)**.
61
+
44
62
  ## Components
45
63
 
46
64
  - **`mcp-memory-server/`** — an MCP server exposing durable, scoped memory
@@ -166,6 +184,7 @@ search):
166
184
  | `CAIRN_LLM_EXTRACTION_MODEL` | Chat model used for memory extraction |
167
185
  | `CAIRN_MEMORY_EMBEDDING_URL` | Embeddings endpoint (falls back to `CAIRN_LLM_API_URL`) |
168
186
  | `CAIRN_MEMORY_EMBEDDING_MODEL` | Embedding model name (required for semantic search) |
187
+ | `CAIRN_MEMORY_EMBEDDING_TIMEOUT_MS` | Embedding request timeout before substring fallback (default `15000`) |
169
188
  | `CAIRN_AGENTFS_BASE_DIR` | Base dir for global memory scopes (default `~/.cairnkeep`) |
170
189
  | `CAIRN_GIT_PROVIDER` | Git host for collaboration commands: `github`\|`gitlab`\|`codeberg`\|`forgejo`\|`none` ([docs/git-providers.md](docs/git-providers.md)) |
171
190
  | `CAIRN_ROUTE_ENDPOINT` | Base URL of an already-running token-miser routing/tiering proxy (unset → `route_check` is inert) |
@@ -189,6 +208,7 @@ enabling them.
189
208
  - **Managed overlay distributions** (wrapper CLI, policy lock, private registry, rollback) — [docs/overlay-distributions.md](docs/overlay-distributions.md)
190
209
  - **Full operating guide** — [docs/operating.md](docs/operating.md)
191
210
  - **Memory storage and deployment** — [docs/storage.md](docs/storage.md)
211
+ - **Podman and OCI containers** — [docs/containers.md](docs/containers.md)
192
212
  - **Privacy and data flow** — [docs/privacy-and-data-flow.md](docs/privacy-and-data-flow.md)
193
213
  - **Git providers** — [docs/git-providers.md](docs/git-providers.md)
194
214
  - **Support** — [SUPPORT.md](SUPPORT.md)
@@ -0,0 +1,29 @@
1
+ services:
2
+ cairnkeep:
3
+ image: ghcr.io/cairnkeep/cairnkeep:${CAIRNKEEP_VERSION:-latest}
4
+ command: ["http"]
5
+ restart: unless-stopped
6
+ read_only: true
7
+ cap_drop: ["ALL"]
8
+ security_opt:
9
+ - no-new-privileges:true
10
+ environment:
11
+ MCP_HTTP_HOST: 0.0.0.0
12
+ MCP_HTTP_PORT: "7801"
13
+ CAIRN_MEMORY_HTTP_ALLOWED_HOSTS: ${CAIRN_MEMORY_HTTP_ALLOWED_HOSTS:-localhost:7801,127.0.0.1:7801}
14
+ CAIRN_MEMORY_HTTP_TOKEN_FILE: /run/secrets/http-token
15
+ ports:
16
+ - "127.0.0.1:${CAIRNKEEP_PORT:-7801}:7801"
17
+ volumes:
18
+ - cairnkeep-data:/data
19
+ secrets:
20
+ - http-token
21
+ tmpfs:
22
+ - /tmp:size=64m,mode=1777
23
+
24
+ volumes:
25
+ cairnkeep-data:
26
+
27
+ secrets:
28
+ http-token:
29
+ file: ${CAIRNKEEP_HTTP_TOKEN_FILE:-./secrets/http-token}
@@ -0,0 +1,54 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ load_secret() {
5
+ name=$1
6
+ eval "file=\${${name}_FILE:-}"
7
+ eval "value=\${${name}:-}"
8
+
9
+ if [ -n "$file" ] && [ -n "$value" ]; then
10
+ echo "cairnkeep: set either $name or ${name}_FILE, not both" >&2
11
+ exit 64
12
+ fi
13
+ if [ -n "$file" ]; then
14
+ if [ ! -f "$file" ] || [ ! -r "$file" ]; then
15
+ echo "cairnkeep: secret file for $name is not readable" >&2
16
+ exit 66
17
+ fi
18
+ value=$(cat "$file")
19
+ export "$name=$value"
20
+ fi
21
+ }
22
+
23
+ load_secret CAIRN_MEMORY_HTTP_TOKEN
24
+ load_secret CAIRN_LLM_API_KEY
25
+ load_secret ANYTHINGLLM_API_KEY
26
+
27
+ mode=${1:-stdio}
28
+ shift || true
29
+
30
+ case "$mode" in
31
+ stdio)
32
+ unset MCP_HTTP_HOST MCP_HTTP_PORT
33
+ cd "${CAIRN_SERVER_WORKDIR:-/data/project}"
34
+ exec cairn memory-server "$@"
35
+ ;;
36
+ http)
37
+ if [ -z "${CAIRN_MEMORY_HTTP_TOKEN:-}" ]; then
38
+ echo "cairnkeep: HTTP mode requires CAIRN_MEMORY_HTTP_TOKEN or CAIRN_MEMORY_HTTP_TOKEN_FILE" >&2
39
+ exit 64
40
+ fi
41
+ export MCP_HTTP_HOST=${MCP_HTTP_HOST:-0.0.0.0}
42
+ export MCP_HTTP_PORT=${MCP_HTTP_PORT:-7801}
43
+ allowed_hosts=${CAIRN_MEMORY_HTTP_ALLOWED_HOSTS:-}
44
+ export CAIRN_MEMORY_HTTP_ALLOWED_HOSTS="${allowed_hosts:+$allowed_hosts,}localhost:${MCP_HTTP_PORT},127.0.0.1:${MCP_HTTP_PORT}"
45
+ cd "${CAIRN_SERVER_WORKDIR:-/data/project}"
46
+ exec cairn memory-server "$@"
47
+ ;;
48
+ cairn)
49
+ exec cairn "$@"
50
+ ;;
51
+ *)
52
+ exec "$mode" "$@"
53
+ ;;
54
+ esac
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const port = process.env.MCP_HTTP_PORT?.trim();
4
+ if (!port) {
5
+ process.exit(0);
6
+ }
7
+
8
+ let token = process.env.CAIRN_MEMORY_HTTP_TOKEN?.trim();
9
+ const tokenFile = process.env.CAIRN_MEMORY_HTTP_TOKEN_FILE?.trim();
10
+ if (!token && tokenFile) {
11
+ try {
12
+ token = readFileSync(tokenFile, "utf8").trim();
13
+ } catch {
14
+ process.exit(1);
15
+ }
16
+ }
17
+ if (!token) {
18
+ process.exit(1);
19
+ }
20
+
21
+ const body = {
22
+ jsonrpc: "2.0",
23
+ id: 1,
24
+ method: "initialize",
25
+ params: {
26
+ protocolVersion: "2025-06-18",
27
+ capabilities: {},
28
+ clientInfo: { name: "cairnkeep-container-health", version: "1" },
29
+ },
30
+ };
31
+
32
+ try {
33
+ const response = await fetch(`http://127.0.0.1:${port}/mcp`, {
34
+ method: "POST",
35
+ headers: {
36
+ Authorization: `Bearer ${token}`,
37
+ "Content-Type": "application/json",
38
+ Accept: "application/json, text/event-stream",
39
+ },
40
+ body: JSON.stringify(body),
41
+ signal: AbortSignal.timeout(4000),
42
+ });
43
+ process.exit(response.ok ? 0 : 1);
44
+ } catch {
45
+ process.exit(1);
46
+ }
@@ -0,0 +1,2 @@
1
+ [Volume]
2
+ VolumeName=cairnkeep-data
@@ -0,0 +1,32 @@
1
+ [Unit]
2
+ Description=Cairnkeep memory server
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Container]
7
+ Image=ghcr.io/cairnkeep/cairnkeep:latest
8
+ ContainerName=cairnkeep
9
+ Exec=http
10
+ UserNS=keep-id:uid=10001,gid=10001
11
+ Volume=cairnkeep-data.volume:/data:Z
12
+ Secret=cairnkeep-http-token,type=mount,target=http-token
13
+ Environment=MCP_HTTP_HOST=0.0.0.0
14
+ Environment=MCP_HTTP_PORT=7801
15
+ Environment=CAIRN_MEMORY_HTTP_ALLOWED_HOSTS=localhost:7801,127.0.0.1:7801
16
+ Environment=CAIRN_MEMORY_HTTP_TOKEN_FILE=/run/secrets/http-token
17
+ PublishPort=127.0.0.1:7801:7801
18
+ ReadOnly=true
19
+ DropCapability=all
20
+ HealthCmd=node /usr/local/lib/cairnkeep/container-healthcheck.mjs
21
+ HealthInterval=30s
22
+ HealthTimeout=5s
23
+ HealthStartPeriod=10s
24
+ HealthRetries=3
25
+ PodmanArgs=--security-opt=no-new-privileges
26
+
27
+ [Service]
28
+ Restart=on-failure
29
+ RestartSec=5
30
+
31
+ [Install]
32
+ WantedBy=default.target
@@ -0,0 +1,25 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ mode=${CAIRN_WORKSPACE_MODE:-shared}
5
+ case "$mode" in
6
+ shared)
7
+ ;;
8
+ sandbox)
9
+ if [ ! -d /source ]; then
10
+ echo "cairnkeep: sandbox mode requires a read-only repository mounted at /source" >&2
11
+ exit 66
12
+ fi
13
+ if [ ! -e /workspace/.cairnkeep-sandbox-ready ]; then
14
+ cp -a /source/. /workspace/
15
+ : > /workspace/.cairnkeep-sandbox-ready
16
+ fi
17
+ ;;
18
+ *)
19
+ echo "cairnkeep: CAIRN_WORKSPACE_MODE must be shared or sandbox" >&2
20
+ exit 64
21
+ ;;
22
+ esac
23
+
24
+ cd /workspace
25
+ exec /usr/local/bin/cairn-container-entrypoint "$@"
@@ -0,0 +1,187 @@
1
+ # Containers
2
+
3
+ Cairnkeep publishes two OCI images for Podman and other OCI-compatible
4
+ engines:
5
+
6
+ | Image | Purpose |
7
+ |---|---|
8
+ | `ghcr.io/cairnkeep/cairnkeep:<version>` | Minimal MCP memory server, over stdio or authenticated HTTP |
9
+ | `ghcr.io/cairnkeep/cairnkeep-workspace:<version>` | Derivation base with Cairnkeep, Git, SSH, and ripgrep |
10
+
11
+ Both images run as the unprivileged user `10001:10001`. The workspace image is
12
+ not a preconfigured coding harness: a private distribution can derive from it
13
+ and install only its approved harnesses and policies.
14
+
15
+ ## Local stdio
16
+
17
+ After installing `@cairnkeep/cli`, register the container launcher rather than
18
+ the host memory server:
19
+
20
+ ```bash
21
+ claude mcp add cairn-memory -s user -- cairn-container stdio
22
+ ```
23
+
24
+ Each harness session starts an ephemeral server container. Memory survives in
25
+ the `cairnkeep-data` named volume. The container does not discover or contact a
26
+ remote Cairnkeep server.
27
+
28
+ The equivalent direct Podman command is:
29
+
30
+ ```bash
31
+ podman run --rm -i \
32
+ --userns=keep-id:uid=10001,gid=10001 \
33
+ --read-only --cap-drop=all --security-opt=no-new-privileges \
34
+ --tmpfs=/tmp:rw,noexec,nosuid,size=64m,mode=1777 \
35
+ --volume cairnkeep-data:/data:Z,U \
36
+ ghcr.io/cairnkeep/cairnkeep:latest stdio
37
+ ```
38
+
39
+ Pin a version instead of `latest` for managed installations.
40
+
41
+ ## Authenticated HTTP
42
+
43
+ Generate the bearer token into a private file and start a loopback-only
44
+ listener:
45
+
46
+ ```bash
47
+ install -m 700 -d ~/.config/cairnkeep/secrets
48
+ openssl rand -hex 32 > ~/.config/cairnkeep/secrets/http-token
49
+ chmod 600 ~/.config/cairnkeep/secrets/http-token
50
+
51
+ cairn-container http \
52
+ --token-file ~/.config/cairnkeep/secrets/http-token \
53
+ --port 7801
54
+ ```
55
+
56
+ The launcher refuses token files that are symlinks or have a mode other than
57
+ `0400` or `0600`. It mounts the token read-only instead of putting it in the
58
+ container environment or image.
59
+
60
+ The listener is deliberately published only on `127.0.0.1`. For access from
61
+ other machines, keep it on loopback behind a TLS reverse proxy or encrypted
62
+ private network. Add the externally visible `Host` header to the DNS-rebinding
63
+ allowlist:
64
+
65
+ ```bash
66
+ cairn-container http \
67
+ --token-file ~/.config/cairnkeep/secrets/http-token \
68
+ --allowed-hosts memory.example.com
69
+ ```
70
+
71
+ The container always adds its own loopback address for health checks.
72
+
73
+ One HTTP server and bearer token form one trust domain. They are not a
74
+ multi-tenant authorization boundary; see [Memory storage and
75
+ deployment](storage.md).
76
+
77
+ ## Persistent service
78
+
79
+ [`containers/compose.yaml`](../containers/compose.yaml) is a portable starting
80
+ point:
81
+
82
+ ```bash
83
+ mkdir -p containers/secrets
84
+ openssl rand -hex 32 > containers/secrets/http-token
85
+ chmod 600 containers/secrets/http-token
86
+ podman compose -f containers/compose.yaml up -d
87
+ ```
88
+
89
+ Set `CAIRNKEEP_VERSION` to pin the image and
90
+ `CAIRN_MEMORY_HTTP_ALLOWED_HOSTS` when a reverse proxy sends a different host
91
+ name. The named volume contains all memory state.
92
+
93
+ For rootless Podman managed by systemd, copy the files from
94
+ [`containers/quadlet`](../containers/quadlet) to
95
+ `~/.config/containers/systemd/`, create the referenced secret, and reload:
96
+
97
+ ```bash
98
+ printf '%s' "$(openssl rand -hex 32)" |
99
+ podman secret create cairnkeep-http-token -
100
+ systemctl --user daemon-reload
101
+ systemctl --user enable --now cairnkeep.service
102
+ ```
103
+
104
+ Review the image tag, published port, allowed hosts, and backup location before
105
+ using the sample as a long-running service.
106
+
107
+ ## Storage and backups
108
+
109
+ Inside the server image:
110
+
111
+ | Data | Container path |
112
+ |---|---|
113
+ | Local/legacy project database | `/data/project/.agentfs/project.db` |
114
+ | Named/global scopes | `/data/scopes/<scope>.db` |
115
+ | Header-routed HTTP projects | `/data/scopes/projects/<project-id>.db` |
116
+
117
+ All three paths are below `/data`, so replacing the container while retaining
118
+ the volume does not move or delete memory. Removing the volume does delete the
119
+ store. Back up the volume with the service stopped, or use SQLite online
120
+ backups for each database as described in [storage.md](storage.md).
121
+
122
+ ## Model and RAG endpoints
123
+
124
+ Embedding, extraction, and document-RAG services remain optional external
125
+ services. Supply non-secret settings with an env file:
126
+
127
+ ```dotenv
128
+ CAIRN_LLM_API_URL=https://models.example.com/v1
129
+ CAIRN_LLM_EXTRACTION_MODEL=example-chat-model
130
+ CAIRN_MEMORY_EMBEDDING_URL=https://models.example.com/v1
131
+ CAIRN_MEMORY_EMBEDDING_MODEL=example-embedding-model
132
+ ```
133
+
134
+ For workspace containers, pass keys as files:
135
+
136
+ ```bash
137
+ cairn-container workspace --repo . \
138
+ --env-file ~/.config/cairnkeep/container.env \
139
+ --secret CAIRN_LLM_API_KEY=~/.config/cairnkeep/secrets/llm-key
140
+ ```
141
+
142
+ The entrypoint reads `CAIRN_LLM_API_KEY_FILE`,
143
+ `CAIRN_MEMORY_HTTP_TOKEN_FILE`, and `ANYTHINGLLM_API_KEY_FILE`. Do not bake
144
+ credentials, cookies, SSH private keys, or a populated `.env` into an image.
145
+
146
+ ## Isolated workspaces
147
+
148
+ The default `sandbox` mode copies the selected repository into a persistent
149
+ named volume. Commands can change that copy but cannot write to the host
150
+ checkout:
151
+
152
+ ```bash
153
+ cairn-container workspace --repo /path/to/repository
154
+ ```
155
+
156
+ Rerunning the command with the same workspace volume resumes the copy. Use
157
+ `--workspace-volume` when repositories have the same basename or when an
158
+ explicit lifecycle is preferable.
159
+
160
+ `shared` mode is opt-in and bind-mounts the checkout read/write:
161
+
162
+ ```bash
163
+ cairn-container workspace --repo /path/to/repository --mode shared
164
+ ```
165
+
166
+ The container has normal outbound network access but receives no host home
167
+ directory, container-engine socket, or host credentials unless the operator
168
+ mounts them. For stricter execution, add network policy and resource limits in
169
+ the private distribution.
170
+
171
+ ## Private derived images
172
+
173
+ A managed overlay should derive from a pinned workspace image:
174
+
175
+ ```dockerfile
176
+ ARG CAIRNKEEP_VERSION
177
+ FROM ghcr.io/cairnkeep/cairnkeep-workspace:${CAIRNKEEP_VERSION}
178
+
179
+ USER root
180
+ # Install the approved harness package from the approved registry.
181
+ USER 10001:10001
182
+ ```
183
+
184
+ The derived image may contain non-secret organization policy and launchers.
185
+ Machine credentials and repository access remain runtime secrets. Build and
186
+ publish that image from the private overlay repository to its approved private
187
+ registry; do not add private configuration to the public Cairnkeep image.
package/docs/operating.md CHANGED
@@ -23,6 +23,8 @@ installs. This guide covers all three in order.
23
23
  - Optional: an OpenAI-compatible LLM endpoint for memory extraction and
24
24
  embedding-ranked search. Without it, memory search degrades to substring
25
25
  matching — everything else still works.
26
+ - Optional: rootless Podman for the containerized memory server and isolated
27
+ workspace base described in [Containers](containers.md).
26
28
 
27
29
  ## Setup order (Claude Code)
28
30
 
@@ -87,6 +89,17 @@ Step 3 installs into `~/.claude` (override with `CLAUDE_CONFIG_DIR` or
87
89
  Re-running `sync-claude-assets.sh --apply` is idempotent; use `--check` to see
88
90
  drift without writing. Run it again whenever you pull changes to `claude/`.
89
91
 
92
+ To keep the memory-server runtime in a container, install the npm package for
93
+ the launcher and replace the registration command with:
94
+
95
+ ```bash
96
+ claude mcp add cairn-memory -s user -- cairn-container stdio
97
+ ```
98
+
99
+ This still stores memory locally, in the `cairnkeep-data` Podman volume. It
100
+ does not configure remote HTTP storage or containerize the harness. See
101
+ [Containers](containers.md) for those separate deployment choices.
102
+
90
103
  ## Setup order (OpenCode)
91
104
 
92
105
  OpenCode is a secondary path. Steps 1, 4, and 5 are identical (use
@@ -146,6 +159,7 @@ vendor or host.
146
159
  | `CAIRN_LLM_EXTRACTION_MODEL` | Chat model for `memory-capture` extraction |
147
160
  | `CAIRN_MEMORY_EMBEDDING_URL` | Embeddings endpoint (falls back to `CAIRN_LLM_API_URL`) |
148
161
  | `CAIRN_MEMORY_EMBEDDING_MODEL` | Embedding model name (required for semantic search) |
162
+ | `CAIRN_MEMORY_EMBEDDING_TIMEOUT_MS` | Embedding request timeout before substring fallback (default `15000`) |
149
163
  | `CAIRN_AGENTFS_BASE_DIR` | Server-side base dir for named/global memory scopes (default `~/.cairnkeep`); it does not affect `project` scope |
150
164
  | `CAIRN_GIT_PROVIDER` | Git host for collaboration commands: `github`\|`gitlab`\|`codeberg`\|`forgejo`\|`none`. See [git-providers.md](git-providers.md) |
151
165
  | `CAIRN_ROUTE_ENDPOINT` | Base URL of an already-running token-miser routing/tiering proxy (unset → the `route_check` tool is inert) |
@@ -77,6 +77,14 @@ checkout. Avoid `postinstall` enrollment: installing a package must not silently
77
77
  replace machine policy. Provide an explicit enrollment command with a retained
78
78
  rollback target.
79
79
 
80
+ For container delivery, derive from an exact
81
+ `ghcr.io/cairnkeep/cairnkeep-workspace:<version>` tag and publish only to the
82
+ approved private registry. Install approved harnesses and non-secret policy in
83
+ the derived image, but inject credentials at runtime through the platform's
84
+ secret mechanism. The public image deliberately contains no organization
85
+ profile and never selects a remote memory service. See
86
+ [Containers](containers.md) for the base-image and workspace contracts.
87
+
80
88
  ## Release and fleet safety
81
89
 
82
90
  Every release should verify:
@@ -36,6 +36,12 @@ Project-scoped and named/global database locations are documented in
36
36
  the remote server host; changing a storage environment variable on the client
37
37
  does not relocate that server's databases.
38
38
 
39
+ The official container stores all databases below `/data`. A named volume
40
+ persists them after container replacement and remains sensitive data. Sandbox
41
+ workspace mode also retains a repository copy in its named volume. Neither
42
+ volume is encrypted by Cairnkeep; remove it explicitly when its retention
43
+ period ends. See [Containers](containers.md).
44
+
39
45
  ## Credentials and transport
40
46
 
41
47
  Keep API keys and bearer tokens out of repositories and command output. Load
package/docs/releasing.md CHANGED
@@ -14,20 +14,35 @@ from a workstation.
14
14
  tests, and publishes `@cairnkeep/cli` with npm provenance.
15
15
  6. The workflow attaches the npm tarball, a CycloneDX SBOM, and SHA-256 checksums
16
16
  to the GitHub Release.
17
+ 7. The workflow publishes versioned `linux/amd64` and `linux/arm64`
18
+ memory-server and workspace images to GHCR. It also updates `latest` for a
19
+ stable release or `next` for a prerelease.
20
+ 8. Verify both image digests, SBOM attestations, provenance attestations, and an
21
+ anonymous pull before announcing the release.
17
22
 
18
23
  Stable versions must use a stable GitHub Release and publish to npm's `latest`
19
24
  tag. SemVer prerelease versions must use a GitHub prerelease and publish to
20
25
  `next`. A mismatch fails before publication.
21
26
 
22
- The workflow is safe to rerun: it skips `npm publish` when that exact version is
23
- already present, then regenerates and replaces the release artifacts.
27
+ The workflow is safe to rerun: it skips `npm publish` and each versioned OCI
28
+ image when that exact version is already present. It regenerates release
29
+ artifacts, retries attestations against the resolved image digests, and moves
30
+ only the `latest` or `next` channel tag. A versioned image tag is never rebuilt
31
+ or moved.
24
32
 
25
33
  ## Repository configuration
26
34
 
27
35
  The repository must provide an `NPM_TOKEN` Actions secret authorized to publish
28
36
  `@cairnkeep/cli`. Keep that credential out of local files and rotate it according
29
- to the npm account's security policy. The workflow grants only `contents: write`
30
- and `id-token: write`; the latter is required for npm provenance.
37
+ to the npm account's security policy. The workflow grants `contents: write`,
38
+ `packages: write`, `attestations: write`, and `id-token: write`; the last two
39
+ are required for registry-backed build provenance.
40
+
41
+ GitHub creates each new GHCR package as private. After the first container
42
+ release, a package administrator must change both `cairnkeep` and
43
+ `cairnkeep-workspace` to Public in their Package settings. This is a one-time,
44
+ irreversible visibility change for each package; subsequent releases remain
45
+ automatic. Confirm an unauthenticated pull after changing visibility.
31
46
 
32
47
  Treat a published version as immutable. If a release is wrong, fix it in a new
33
48
  version rather than moving its tag or replacing the npm package.
package/docs/storage.md CHANGED
@@ -29,6 +29,12 @@ services used to process/search memory. They do not change where the SQLite
29
29
  databases are stored. Git-provider and routing configuration do not change
30
30
  storage either.
31
31
 
32
+ When the memory server runs in the official container, the same rule applies:
33
+ the process stores databases below `/data`, normally backed by the
34
+ `cairnkeep-data` named volume. Replacing the container preserves that volume;
35
+ removing the volume removes the databases. See [Containers](containers.md) for
36
+ the exact paths and backup boundary.
37
+
32
38
  ## Remote HTTP mode
33
39
 
34
40
  Remote storage is explicit. An operator starts Cairnkeep in HTTP mode on a
@@ -112,6 +118,31 @@ sqlite3 /path/to/project/.agentfs/project.db \
112
118
  Treat every database and export archive as sensitive. They may contain source
113
119
  paths, decisions, incident details, and other project context.
114
120
 
121
+ ## Reviewed-memory integration
122
+
123
+ External review systems should not compose `memory_write`, `memory_read`, and
124
+ `memory_delete` into their own promotion protocol. That sequence is not atomic
125
+ and can delete a newer manual revision during lifecycle invalidation.
126
+
127
+ Cairnkeep exposes two additive MCP tools for reviewed integrations:
128
+
129
+ - `memory_apply_reviewed` takes `scope`, `review_id`, `key`, and `value`. It is
130
+ idempotent for the same review and content, preserves a displaced live value
131
+ in memory history, and rejects reuse of the review ID with different content.
132
+ - `memory_invalidate_reviewed` takes `scope`, `review_id`, `key`, and an optional
133
+ `reason`. It removes the live value only when it still matches that reviewed
134
+ revision. If apply has not arrived yet, it records a tombstone so a delayed
135
+ apply cannot resurrect invalid memory.
136
+
137
+ Provenance and tombstones use a hidden reserved namespace in the same scoped
138
+ database. Generic memory writes and deletes cannot modify those records. Review
139
+ IDs must be stable, non-secret idempotency identifiers; evidence payloads and
140
+ credentials do not belong in them.
141
+
142
+ These tools are for an explicit, trusted integration. Cairnkeep itself does not
143
+ approve candidates, discover an evidence service, or promote memory
144
+ automatically. Existing clients and the original memory tools remain unchanged.
145
+
115
146
  ## Automating trusted personal clients
116
147
 
117
148
  Keep fleet-specific values in a private dotfiles repository or secret manager,
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
+ const DEFAULT_EMBEDDING_TIMEOUT_MS = 15000;
4
5
  // Resolve the embeddings endpoint from the environment. Returns null when the
5
6
  // API key, URL, or model name is not configured, so callers can fall back to
6
7
  // substring search. Point CAIRN_LLM_API_URL (or CAIRN_MEMORY_EMBEDDING_URL) at
@@ -21,7 +22,11 @@ export function getEmbeddingConfig() {
21
22
  if (!model) {
22
23
  return null;
23
24
  }
24
- return { apiUrl, apiKey, model };
25
+ const rawTimeout = Number(process.env.CAIRN_MEMORY_EMBEDDING_TIMEOUT_MS);
26
+ const timeoutMs = Number.isFinite(rawTimeout) && rawTimeout >= 100 && rawTimeout <= 120000
27
+ ? Math.floor(rawTimeout)
28
+ : DEFAULT_EMBEDDING_TIMEOUT_MS;
29
+ return { apiUrl, apiKey, model, timeoutMs };
25
30
  }
26
31
  export function hashText(text) {
27
32
  return createHash("sha1").update(text).digest("hex");
@@ -59,7 +64,7 @@ export async function embedTexts(config, texts, chunkSize = 64) {
59
64
  "Content-Type": "application/json",
60
65
  },
61
66
  body: JSON.stringify({ model: config.model, input: chunk }),
62
- signal: AbortSignal.timeout(120000),
67
+ signal: AbortSignal.timeout(config.timeoutMs),
63
68
  });
64
69
  if (!response.ok) {
65
70
  const text = await response.text();