@cairnkeep/cli 2.1.0 → 2.2.1
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 +23 -0
- package/README.md +20 -0
- package/containers/compose.yaml +29 -0
- package/containers/entrypoint.sh +54 -0
- package/containers/healthcheck.mjs +46 -0
- package/containers/quadlet/cairnkeep-data.volume +2 -0
- package/containers/quadlet/cairnkeep.container +32 -0
- package/containers/workspace-entrypoint.sh +29 -0
- package/docs/containers.md +193 -0
- package/docs/operating.md +14 -0
- package/docs/overlay-distributions.md +8 -0
- package/docs/privacy-and-data-flow.md +6 -0
- package/docs/releasing.md +19 -4
- package/docs/storage.md +6 -0
- package/mcp-memory-server/dist/embeddings.js +7 -2
- package/package.json +4 -2
- package/scripts/cairn-container +225 -0
- package/scripts/doctor.sh +16 -3
- package/scripts/probe-container-memory.mjs +51 -0
- package/scripts/test-container-runner.sh +87 -0
- package/scripts/test-container.sh +214 -0
- package/scripts/test-doctor.sh +62 -2
- package/templates/env.example.template +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
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.1] - 2026-07-20
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Make default sandbox-volume names path-specific so same-named repositories
|
|
11
|
+
cannot share a workspace accidentally.
|
|
12
|
+
- Refuse to merge repository content into a non-empty unmarked workspace
|
|
13
|
+
volume.
|
|
14
|
+
- Document that sandbox copies include untracked files and must be treated as
|
|
15
|
+
sensitive persistent data.
|
|
16
|
+
|
|
17
|
+
## [2.2.0] - 2026-07-20
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Add unprivileged memory-server and workspace OCI images with rootless Podman
|
|
22
|
+
launchers, authenticated HTTP deployment examples, and persistent storage.
|
|
23
|
+
- Add isolated-copy and explicit shared-checkout workspace modes for private
|
|
24
|
+
derived distributions.
|
|
25
|
+
- Verify stdio startup, HTTP authentication, memory persistence across
|
|
26
|
+
replacement, and both workspace modes in container CI.
|
|
27
|
+
|
|
6
28
|
## [2.1.0] - 2026-07-16
|
|
7
29
|
|
|
8
30
|
### Added
|
|
@@ -89,6 +111,7 @@ stored memories.
|
|
|
89
111
|
- Preserve executable permissions and Bash 3.2 portability on macOS.
|
|
90
112
|
- Add backup-first uninstall and SQLite-safe memory export/import guidance.
|
|
91
113
|
|
|
114
|
+
[2.2.0]: https://github.com/cairnkeep/cairnkeep/compare/v2.1.0...v2.2.0
|
|
92
115
|
[2.1.0]: https://github.com/cairnkeep/cairnkeep/compare/v2.0.0...v2.1.0
|
|
93
116
|
[2.0.0]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.3...v2.0.0
|
|
94
117
|
[1.1.3]: https://github.com/cairnkeep/cairnkeep/compare/v1.1.2...v1.1.3
|
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,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,29 @@
|
|
|
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
|
+
if find /workspace -mindepth 1 -maxdepth 1 -print -quit | grep -q .; then
|
|
15
|
+
echo "cairnkeep: refusing to initialize a non-empty unmarked workspace volume" >&2
|
|
16
|
+
exit 65
|
|
17
|
+
fi
|
|
18
|
+
cp -a /source/. /workspace/
|
|
19
|
+
: > /workspace/.cairnkeep-sandbox-ready
|
|
20
|
+
fi
|
|
21
|
+
;;
|
|
22
|
+
*)
|
|
23
|
+
echo "cairnkeep: CAIRN_WORKSPACE_MODE must be shared or sandbox" >&2
|
|
24
|
+
exit 64
|
|
25
|
+
;;
|
|
26
|
+
esac
|
|
27
|
+
|
|
28
|
+
cd /workspace
|
|
29
|
+
exec /usr/local/bin/cairn-container-entrypoint "$@"
|
|
@@ -0,0 +1,193 @@
|
|
|
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 an explicit lifecycle is preferable. Default volume
|
|
158
|
+
names include both the repository basename and a checksum of its absolute path,
|
|
159
|
+
so same-named repositories do not share a sandbox accidentally.
|
|
160
|
+
|
|
161
|
+
The initial copy includes tracked and untracked files. Treat the workspace
|
|
162
|
+
volume as sensitive local data and remove it when it is no longer needed.
|
|
163
|
+
Cairnkeep refuses to initialize a non-empty volume that lacks its sandbox
|
|
164
|
+
marker rather than merging unrelated content.
|
|
165
|
+
|
|
166
|
+
`shared` mode is opt-in and bind-mounts the checkout read/write:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
cairn-container workspace --repo /path/to/repository --mode shared
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The container has normal outbound network access but receives no host home
|
|
173
|
+
directory, container-engine socket, or host credentials unless the operator
|
|
174
|
+
mounts them. For stricter execution, add network policy and resource limits in
|
|
175
|
+
the private distribution.
|
|
176
|
+
|
|
177
|
+
## Private derived images
|
|
178
|
+
|
|
179
|
+
A managed overlay should derive from a pinned workspace image:
|
|
180
|
+
|
|
181
|
+
```dockerfile
|
|
182
|
+
ARG CAIRNKEEP_VERSION
|
|
183
|
+
FROM ghcr.io/cairnkeep/cairnkeep-workspace:${CAIRNKEEP_VERSION}
|
|
184
|
+
|
|
185
|
+
USER root
|
|
186
|
+
# Install the approved harness package from the approved registry.
|
|
187
|
+
USER 10001:10001
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The derived image may contain non-secret organization policy and launchers.
|
|
191
|
+
Machine credentials and repository access remain runtime secrets. Build and
|
|
192
|
+
publish that image from the private overlay repository to its approved private
|
|
193
|
+
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`
|
|
23
|
-
already present
|
|
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
|
|
30
|
-
and `id-token: write`; the
|
|
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
|
|
@@ -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
|
-
|
|
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(
|
|
67
|
+
signal: AbortSignal.timeout(config.timeoutMs),
|
|
63
68
|
});
|
|
64
69
|
if (!response.ok) {
|
|
65
70
|
const text = await response.text();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cairnkeep/cli",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
|
-
"cairn": "bin/cairn"
|
|
11
|
+
"cairn": "bin/cairn",
|
|
12
|
+
"cairn-container": "scripts/cairn-container"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"bin/",
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
"opencode/",
|
|
19
20
|
"examples/anythingllm/",
|
|
20
21
|
"examples/overlay-distribution/",
|
|
22
|
+
"containers/",
|
|
21
23
|
"schemas/",
|
|
22
24
|
"docs/",
|
|
23
25
|
"CHANGELOG.md",
|