@cairnkeep/cli 2.23.1 → 2.25.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 +51 -0
- package/docs/README.md +4 -0
- package/docs/agents.md +18 -0
- package/docs/containers.md +4 -0
- package/docs/design/team-mode.md +17 -9
- package/docs/harness-compatibility.md +17 -0
- package/docs/learning/CURRICULUM-MAP.md +3 -1
- package/docs/learning/FEATURE-GUIDE.md +34 -1
- package/docs/learning/README.md +5 -3
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L04-memory-fundamentals.md +12 -0
- package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +2 -2
- package/docs/learning/lessons/L23-guided-setup.md +1 -1
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +1 -1
- package/docs/learning/lessons/L26-context-intelligence.md +1 -1
- package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
- package/docs/learning/lessons/L28-security-assurance.md +2 -2
- package/docs/learning/lessons/L29-team-continuity.md +95 -0
- package/docs/learning/tracks/operator.md +2 -0
- package/docs/learning/video-scripts/V04-memory-retrieval-supplement.md +13 -0
- package/docs/learning/video-scripts/V29-team-continuity.md +66 -0
- package/docs/mcp-tool-profiles.md +7 -0
- package/docs/memory-retrieval.md +84 -2
- package/docs/operating.md +22 -3
- package/docs/privacy-and-data-flow.md +37 -9
- package/docs/security-assurance.md +28 -0
- package/docs/storage.md +18 -0
- package/docs/team-pilot.md +132 -0
- package/docs/team.md +234 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/index.js +30 -2
- package/mcp-memory-server/dist/mcp-tool-catalog.js +4 -1
- package/mcp-memory-server/dist/memory-reranker.js +109 -0
- package/mcp-memory-server/dist/team-cli.js +218 -0
- package/mcp-memory-server/dist/team-http.js +193 -0
- package/mcp-memory-server/dist/team-mcp.js +82 -0
- package/mcp-memory-server/dist/team-store.js +663 -0
- package/mcp-memory-server/package.json +1 -0
- package/package.json +1 -1
- package/scripts/cairn-cli.mjs +2 -0
- package/scripts/completion.sh +23 -3
- package/scripts/doctor.sh +17 -0
- package/scripts/lib/team-retention.mjs +29 -0
- package/scripts/probe-memory-reranker.mjs +22 -0
- package/scripts/test-ci-sharding.sh +4 -0
- package/scripts/test-completion.sh +8 -0
- package/scripts/test-container-health.mjs +2 -0
- package/scripts/test-container.sh +21 -1
- package/scripts/test-dependency-security.mjs +11 -1
- package/scripts/test-doctor.sh +28 -0
- package/scripts/test-package-install.sh +9 -0
- package/scripts/test-team-docs.sh +52 -0
- package/scripts/test-team-pilot.mjs +59 -0
- package/scripts/test-windows-native.mjs +5 -0
- package/scripts/uninstall.sh +13 -0
- package/scripts/verify-dependency-security.mjs +3 -2
- package/scripts/verify-team-retention.mjs +3 -0
- package/scripts/windows-platform.mjs +14 -2
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"check:extract": "node scripts/smoke-extract-cli.mjs",
|
|
17
17
|
"check:search": "node scripts/smoke-search-e2e.mjs",
|
|
18
18
|
"check:memory-retrieval": "node scripts/smoke-memory-retrieval.mjs && node scripts/smoke-memory-retrieval-mcp.mjs && node scripts/run-memory-retrieval-study.mjs --check --summary",
|
|
19
|
+
"check:memory-reranker": "node scripts/smoke-memory-reranker.mjs && node scripts/smoke-memory-reranker-mcp.mjs && node scripts/run-memory-reranker-study.mjs --check --summary",
|
|
19
20
|
"study:memory-retrieval": "npm run build && node scripts/run-memory-retrieval-study.mjs --summary",
|
|
20
21
|
"check:scope-guard": "node scripts/smoke-scope-guard.mjs",
|
|
21
22
|
"check:http-guard": "node scripts/smoke-http-guard.mjs",
|
package/package.json
CHANGED
package/scripts/cairn-cli.mjs
CHANGED
|
@@ -21,6 +21,7 @@ const NODE_COMMANDS = new Map([
|
|
|
21
21
|
["evidence", "work-evidence-cli.js"],
|
|
22
22
|
["playbook", "playbook-cli.js"],
|
|
23
23
|
["security", "security-cli.js"],
|
|
24
|
+
["team", "team-cli.js"],
|
|
24
25
|
]);
|
|
25
26
|
|
|
26
27
|
const POSIX_COMMANDS = new Map([
|
|
@@ -61,6 +62,7 @@ Usage:
|
|
|
61
62
|
cairn evidence <list|show|delete|prune|doctor>
|
|
62
63
|
cairn playbook <list|status|init|set|enable|disable|reset|check|record|receipts|instructions|doctor>
|
|
63
64
|
cairn security doctor [--project PATH] [--json]
|
|
65
|
+
cairn team <init|project|member|credential|serve|mcp|list|read|search|history|propose|proposals|proposal-show|review|audit|doctor|backup|restore|memory-delete>
|
|
64
66
|
cairn memory <path|export|import>
|
|
65
67
|
cairn audit-timer [--on-calendar SPEC] [--para-root PATH] [--render-only DIR]
|
|
66
68
|
cairn uninstall [--dry-run] [--yes] [--purge-memory] [--purge-packs] [PROJECT ...]
|
package/scripts/completion.sh
CHANGED
|
@@ -13,7 +13,7 @@ _cairn_complete() {
|
|
|
13
13
|
COMPREPLY=()
|
|
14
14
|
current=${COMP_WORDS[COMP_CWORD]}
|
|
15
15
|
previous=${COMP_WORDS[COMP_CWORD-1]:-}
|
|
16
|
-
commands="bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help"
|
|
16
|
+
commands="bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security team trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help"
|
|
17
17
|
if (( COMP_CWORD == 1 )); then
|
|
18
18
|
COMPREPLY=( $(compgen -W "$commands" -- "$current") )
|
|
19
19
|
return
|
|
@@ -27,6 +27,7 @@ _cairn_complete() {
|
|
|
27
27
|
sync-omp) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
|
|
28
28
|
doctor) COMPREPLY=( $(compgen -W "--repair" -- "$current") ) ;;
|
|
29
29
|
security) COMPREPLY=( $(compgen -W "doctor --project --json" -- "$current") ) ;;
|
|
30
|
+
team) COMPREPLY=( $(compgen -W "init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete --organization --data --project --roles --class --expires-at --output --input --confirm --port --key --query --proposal --decision --revision --project-root" -- "$current") ) ;;
|
|
30
31
|
trajectory) COMPREPLY=( $(compgen -W "list show prune --json --dry-run" -- "$current") ) ;;
|
|
31
32
|
artifact) COMPREPLY=( $(compgen -W "list show delete prune --kind --session --json --dry-run --include-protected" -- "$current") ) ;;
|
|
32
33
|
evidence) COMPREPLY=( $(compgen -W "list show delete prune doctor --status --json --dry-run --repair" -- "$current") ) ;;
|
|
@@ -100,6 +101,7 @@ _cairn() {
|
|
|
100
101
|
'sync-omp:install the oh-my-pi memory extension'
|
|
101
102
|
'doctor:check runtime dependencies and endpoints'
|
|
102
103
|
'security:audit local security posture'
|
|
104
|
+
'team:operate opt-in reviewed team continuity'
|
|
103
105
|
'trajectory:inspect and prune local session trajectories'
|
|
104
106
|
'artifact:inspect, delete, and prune local artifacts'
|
|
105
107
|
'evidence:inspect and manage local Git-linked work evidence'
|
|
@@ -132,6 +134,7 @@ _cairn() {
|
|
|
132
134
|
sync-omp) _arguments '--apply[apply changes]' '--live-root[omp agent root]:directory:_files -/' ;;
|
|
133
135
|
doctor) _arguments '--repair[repair trajectory metadata and indexes]' ;;
|
|
134
136
|
security) _values 'security command' doctor '--project[project root]:directory:_files -/' '--json[emit JSON]' ;;
|
|
137
|
+
team) _values 'team command' init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete '--organization[team option]:value:' '--data[team option]:value:' '--project[team option]:value:' '--roles[team option]:value:' '--class[team option]:value:' '--expires-at[team option]:value:' '--output[team option]:value:' '--input[team option]:value:' '--confirm[team option]:value:' '--port[team option]:value:' '--key[team option]:value:' '--query[team option]:value:' '--proposal[team option]:value:' '--decision[team option]:value:' '--revision[team option]:value:' '--project-root[team option]:value:' ;;
|
|
135
138
|
trajectory) _values 'trajectory command' list show prune ;;
|
|
136
139
|
artifact)
|
|
137
140
|
case $words[3] in
|
|
@@ -196,8 +199,8 @@ EOF
|
|
|
196
199
|
fish)
|
|
197
200
|
sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
|
|
198
201
|
complete -c cairn -f
|
|
199
|
-
for command in bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help
|
|
200
|
-
complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help" -a $command
|
|
202
|
+
for command in bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security team trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help
|
|
203
|
+
complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security team trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help" -a $command
|
|
201
204
|
end
|
|
202
205
|
complete -c cairn -n "__fish_seen_subcommand_from sync" -l apply
|
|
203
206
|
complete -c cairn -n "__fish_seen_subcommand_from sync" -l live-root -r
|
|
@@ -219,6 +222,23 @@ complete -c cairn -n "__fish_seen_subcommand_from doctor" -l repair
|
|
|
219
222
|
complete -c cairn -n "__fish_seen_subcommand_from security" -a "doctor"
|
|
220
223
|
complete -c cairn -n "__fish_seen_subcommand_from security" -l project -r
|
|
221
224
|
complete -c cairn -n "__fish_seen_subcommand_from security" -l json
|
|
225
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -a "init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete"
|
|
226
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l organization -r
|
|
227
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l data -r
|
|
228
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l project -r
|
|
229
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l roles -r
|
|
230
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l class -r
|
|
231
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l expires-at -r
|
|
232
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l output -r
|
|
233
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l input -r
|
|
234
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l confirm -r
|
|
235
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l port -r
|
|
236
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l key -r
|
|
237
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l query -r
|
|
238
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l proposal -r
|
|
239
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l decision -r
|
|
240
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l revision -r
|
|
241
|
+
complete -c cairn -n "__fish_seen_subcommand_from team" -l project-root -r
|
|
222
242
|
complete -c cairn -n "__fish_seen_subcommand_from trajectory" -a "list show prune"
|
|
223
243
|
complete -c cairn -n "__fish_seen_subcommand_from artifact" -a "list show delete prune"
|
|
224
244
|
complete -c cairn -n "__fish_seen_subcommand_from artifact; and __fish_seen_subcommand_from list" -l kind -r
|
package/scripts/doctor.sh
CHANGED
|
@@ -112,6 +112,23 @@ else
|
|
|
112
112
|
skip "embedding endpoint (CAIRN_MEMORY_EMBEDDING_URL unset)"
|
|
113
113
|
fi
|
|
114
114
|
|
|
115
|
+
# Optional learned reranking: use the runtime validator and a synthetic request.
|
|
116
|
+
# Never print provider bodies, credentials, URLs or user memory in diagnostics.
|
|
117
|
+
if [[ "${CAIRN_MEMORY_RERANK:-}" == 1 ]]; then
|
|
118
|
+
if node "$CAIRN_ROOT/scripts/probe-memory-reranker.mjs" >/dev/null 2>&1; then
|
|
119
|
+
pass "memory reranker accepted a synthetic ranking request"
|
|
120
|
+
else
|
|
121
|
+
rerank_status=$?
|
|
122
|
+
if [[ "$rerank_status" == 2 ]]; then
|
|
123
|
+
fail "memory reranker configuration invalid — set CAIRN_MEMORY_RERANK_URL and CAIRN_MEMORY_RERANK_MODEL; timeout must be 100–10000 ms"
|
|
124
|
+
else
|
|
125
|
+
fail "memory reranker unavailable or returned invalid ranking data — search falls back to its existing order"
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
else
|
|
129
|
+
skip "memory reranker (CAIRN_MEMORY_RERANK unset/not 1)"
|
|
130
|
+
fi
|
|
131
|
+
|
|
115
132
|
# 4. Git provider (collaboration commands). Report configuration only — auth is
|
|
116
133
|
# provider- and host-specific and lives in the wrapper, not the core.
|
|
117
134
|
case "${CAIRN_GIT_PROVIDER:-}" in
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
4
|
+
|
|
5
|
+
function canonical(path) {
|
|
6
|
+
const absolute = resolve(path);
|
|
7
|
+
if (existsSync(absolute)) return realpathSync(absolute);
|
|
8
|
+
const parent = dirname(absolute);
|
|
9
|
+
return parent === absolute ? absolute : join(canonical(parent), basename(absolute));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Uninstall never grants permission to purge the separate team store. */
|
|
13
|
+
export function assertUninstallRetainsTeam(targets, env = process.env, home = homedir()) {
|
|
14
|
+
const stores = new Set([join(home, '.cairnkeep', 'team'), ...(env.CAIRN_TEAM_BASE_DIR ? [env.CAIRN_TEAM_BASE_DIR] : [])]);
|
|
15
|
+
const normalized = path => {
|
|
16
|
+
const value = canonical(path);
|
|
17
|
+
return process.platform === 'win32' ? value.toLowerCase() : value;
|
|
18
|
+
};
|
|
19
|
+
for (const team of stores) {
|
|
20
|
+
if (!existsSync(resolve(team))) continue;
|
|
21
|
+
const protectedPath = normalized(team);
|
|
22
|
+
for (const target of targets) {
|
|
23
|
+
const path = normalized(target);
|
|
24
|
+
if (path === protectedPath || protectedPath.startsWith(path.endsWith(sep) ? path : `${path}${sep}`)) {
|
|
25
|
+
throw new Error('Uninstall refused: a removal target contains team data. Retain it, or relocate the team store with an explicitly verified backup first.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Exit codes are a value-free doctor protocol: 0 disabled/healthy,
|
|
2
|
+
// 2 invalid opt-in configuration, 3 unavailable or invalid ranking response.
|
|
3
|
+
import { getMemoryRerankerConfig, rerankMemoryResults } from '../mcp-memory-server/dist/memory-reranker.js';
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
export async function probeMemoryReranker() {
|
|
8
|
+
if (process.env.CAIRN_MEMORY_RERANK !== '1') return 0;
|
|
9
|
+
const config = getMemoryRerankerConfig();
|
|
10
|
+
if (!config) return 2;
|
|
11
|
+
const result = await rerankMemoryResults([
|
|
12
|
+
{ key: 'health/policy', value: 'Corrections append a new release ledger entry.' },
|
|
13
|
+
{ key: 'health/theme', value: 'The release ledger interface uses a dark theme.' },
|
|
14
|
+
], 'How are release ledger corrections recorded?', 2, config);
|
|
15
|
+
return result.status === 'reranked' ? 0 : 3;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let invokedDirectly = false;
|
|
19
|
+
try { invokedDirectly = Boolean(process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)); } catch {}
|
|
20
|
+
if (invokedDirectly) {
|
|
21
|
+
process.exitCode = await probeMemoryReranker();
|
|
22
|
+
}
|
|
@@ -17,6 +17,10 @@ assert.equal(flattened.length, smokeChecks.length);
|
|
|
17
17
|
assert.equal(new Set(flattened).size, smokeChecks.length);
|
|
18
18
|
assert.deepEqual([...flattened].sort(), smokeChecks.map(({ name }) => name).sort());
|
|
19
19
|
assert.ok(plan.every(({ checks }) => checks.length > 0));
|
|
20
|
+
const teamContracts = ['team', 'team-review', 'team-http', 'team-mcp', 'team-restore', 'team-cli'];
|
|
21
|
+
for (const name of teamContracts) assert.ok(flattened.includes(name), `missing team contract: ${name}`);
|
|
22
|
+
assert.ok(plan.every(({ checks }) => checks.some(({ name }) => teamContracts.includes(name))),
|
|
23
|
+
'native ACL-heavy team contracts must be distributed across all Windows shards');
|
|
20
24
|
const weights = plan.map(({ weight }) => weight);
|
|
21
25
|
assert.ok(Math.max(...weights) - Math.min(...weights) <= 5,
|
|
22
26
|
`native Windows shards are imbalanced: ${weights.join(", ")}`);
|
|
@@ -17,6 +17,11 @@ eval_flags='--task-set --adapter --output --repetitions --seed --json --yes --di
|
|
|
17
17
|
skill_commands='harvest list show review propose evaluate apply rollback'
|
|
18
18
|
for shell in bash zsh fish; do
|
|
19
19
|
grep -q 'capabilities' "$tmp/$shell"
|
|
20
|
+
grep -q 'team' "$tmp/$shell"
|
|
21
|
+
grep -q 'credential create set remove issue revoke' "$tmp/$shell"
|
|
22
|
+
for team_command in init project member credential serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete; do
|
|
23
|
+
grep -qF "$team_command" "$tmp/$shell"
|
|
24
|
+
done
|
|
20
25
|
grep -q 'list.*status.*enable.*disable.*reset.*logging\|list status enable disable reset logging' "$tmp/$shell"
|
|
21
26
|
grep -q -- '--json\|-l json' "$tmp/$shell"
|
|
22
27
|
for capability_id in $capability_ids; do
|
|
@@ -152,6 +157,9 @@ for shell in bash zsh fish; do
|
|
|
152
157
|
fi
|
|
153
158
|
done
|
|
154
159
|
"$ROOT/bin/cairn" completion powershell >"$tmp/powershell"
|
|
160
|
+
grep -qF "'team'" "$tmp/powershell"
|
|
161
|
+
grep -qF "'credential','create','set','remove','issue','revoke'" "$tmp/powershell"
|
|
162
|
+
grep -qF "'memory-delete'" "$tmp/powershell"
|
|
155
163
|
grep -qF "'protocol'" "$tmp/powershell"
|
|
156
164
|
grep -qF "'--trajectory'" "$tmp/powershell"
|
|
157
165
|
grep -qF "'--codex-jsonl'" "$tmp/powershell"
|
|
@@ -11,6 +11,8 @@ assert.match(quadlet, /^Secret=cairnkeep-http-token,type=mount,target=http-token
|
|
|
11
11
|
"the managed container secret must satisfy the probe's private-file contract");
|
|
12
12
|
assert.match(containerfile, /COPY --chmod=644 containers\/healthcheck\.mjs \/opt\/cairnkeep\/containers\/healthcheck\.mjs/, "probe imports must resolve within the packaged server");
|
|
13
13
|
assert.match(containerfile, /ln -s \/opt\/cairnkeep\/containers\/healthcheck\.mjs \/usr\/local\/lib\/cairnkeep\/container-healthcheck\.mjs/, "keep the established container probe path");
|
|
14
|
+
assert.match(containerfile, /COPY --chmod=644 containers\/healthcheck\.mjs[^\n]*\nRUN chmod 755 \/opt\/cairnkeep\/containers\b/,
|
|
15
|
+
"normalize the probe directory's traversal mode after COPY for unprivileged runtime users");
|
|
14
16
|
assert.match(source, /parseHttpPort/, "health destination must use the server's numeric port parser");
|
|
15
17
|
assert.match(source, /resolveHttpToken/, "health tokens must use the server's bounded resolver");
|
|
16
18
|
const { containerHealthcheck } = await import("../containers/healthcheck.mjs");
|
|
@@ -43,6 +43,21 @@ image_user=$("$ENGINE" image inspect --format '{{.Config.User}}' "$server_image"
|
|
|
43
43
|
exit 1
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
# Check the real legacy entrypoint before a failing credential probe can mask
|
|
47
|
+
# a module-load or directory-traversal error as an expected authentication denial.
|
|
48
|
+
"$ENGINE" run --rm --entrypoint node "$server_image" --input-type=module -e '
|
|
49
|
+
import assert from "node:assert/strict";
|
|
50
|
+
import { accessSync, constants, realpathSync } from "node:fs";
|
|
51
|
+
import { pathToFileURL } from "node:url";
|
|
52
|
+
assert.equal(process.getuid(), 10001);
|
|
53
|
+
accessSync("/opt/cairnkeep/containers", constants.R_OK | constants.X_OK);
|
|
54
|
+
const target = realpathSync("/usr/local/lib/cairnkeep/container-healthcheck.mjs");
|
|
55
|
+
assert.equal(target, "/opt/cairnkeep/containers/healthcheck.mjs");
|
|
56
|
+
accessSync(target, constants.R_OK);
|
|
57
|
+
await import(pathToFileURL(target).href);
|
|
58
|
+
console.log("container health entrypoint: readable, searchable and importable as runtime UID");
|
|
59
|
+
'
|
|
60
|
+
|
|
46
61
|
"$ENGINE" volume create "$volume" >/dev/null
|
|
47
62
|
node scripts/probe-memory-server.mjs --command \
|
|
48
63
|
"$ENGINE" run --rm -i --volume "$volume:/data" "$server_image" stdio
|
|
@@ -112,10 +127,15 @@ done
|
|
|
112
127
|
cat "$tmp/write.out"
|
|
113
128
|
# Exec probes see the original container environment, not the entrypoint's
|
|
114
129
|
# loaded secret. The deliberately public bind fixture must be rejected.
|
|
115
|
-
if "$ENGINE" exec "$name" node /usr/local/lib/cairnkeep/container-healthcheck.mjs; then
|
|
130
|
+
if "$ENGINE" exec "$name" node /usr/local/lib/cairnkeep/container-healthcheck.mjs >"$tmp/health.out" 2>"$tmp/health.err"; then
|
|
116
131
|
echo "FAIL: healthcheck accepted a non-private token file" >&2
|
|
117
132
|
exit 1
|
|
118
133
|
fi
|
|
134
|
+
[[ ! -s "$tmp/health.out" && ! -s "$tmp/health.err" ]] || {
|
|
135
|
+
cat "$tmp/health.out" "$tmp/health.err" >&2
|
|
136
|
+
echo "FAIL: healthcheck must reject unsafe credentials without a runtime error" >&2
|
|
137
|
+
exit 1
|
|
138
|
+
}
|
|
119
139
|
# Exercise the same legacy entrypoint against a private, container-owned file.
|
|
120
140
|
"$ENGINE" exec "$name" sh -c 'umask 077; cat /run/secrets/http-token > /tmp/health-token; CAIRN_MEMORY_HTTP_TOKEN_FILE=/tmp/health-token node /usr/local/lib/cairnkeep/container-healthcheck.mjs; status=$?; rm -f /tmp/health-token; exit "$status"'
|
|
121
141
|
"$ENGINE" rm -f "$name" >/dev/null
|
|
@@ -25,6 +25,16 @@ function check(label, mutate, expected) {
|
|
|
25
25
|
}
|
|
26
26
|
try {
|
|
27
27
|
check("valid dependency graph", () => {}, null);
|
|
28
|
+
for (const lockPath of ['package-lock.json', 'mcp-memory-server/package-lock.json']) {
|
|
29
|
+
for (const [name, vulnerable] of [['body-parser', '2.3.0'], ['fast-uri', '3.1.8'], ['ip-address', '10.7.3']]) {
|
|
30
|
+
check(`${lockPath} rejects vulnerable ${name}`, x => {
|
|
31
|
+
x[lockPath].packages[`node_modules/${name}`].version = vulnerable;
|
|
32
|
+
}, new RegExp(`resolved ${name}: requires`));
|
|
33
|
+
check(`${lockPath} rejects nested vulnerable ${name}`, x => {
|
|
34
|
+
x[lockPath].packages[`node_modules/fixture/node_modules/${name}`] = { version: vulnerable };
|
|
35
|
+
}, new RegExp(`resolved ${name}: requires`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
28
38
|
check("root manifest/lock mismatch", x => { x["package.json"].dependencies.zod = "^4.0.0"; }, /manifest and lock root Zod ranges differ/);
|
|
29
39
|
check("server manifest/lock mismatch", x => { x["mcp-memory-server/package-lock.json"].packages[""].dependencies.zod = "^4.0.0"; }, /manifest and lock root Zod ranges differ/);
|
|
30
40
|
check("matched server range differs from packaged CLI", x => {
|
|
@@ -34,7 +44,7 @@ try {
|
|
|
34
44
|
check("different resolved schema version", x => { x["mcp-memory-server/package-lock.json"].packages["node_modules/zod"].version = "4.0.0"; }, /root and server resolved Zod versions differ/);
|
|
35
45
|
check("missing direct schema", x => { delete x["package-lock.json"].packages["node_modules/zod"]; }, /direct Zod: version must be a string/);
|
|
36
46
|
check("existing SDK advisory check retained", x => { x["package-lock.json"].packages["node_modules/@modelcontextprotocol/sdk"].version = "1.0.0"; }, /resolved @modelcontextprotocol\/sdk: requires/);
|
|
37
|
-
console.log("PASS: dependency policy positive control,
|
|
47
|
+
console.log("PASS: dependency policy positive control, both-lock/nested advisory rejection and schema parity mutations");
|
|
38
48
|
} finally {
|
|
39
49
|
rmSync(scratch, { recursive: true, force: true });
|
|
40
50
|
}
|
package/scripts/test-doctor.sh
CHANGED
|
@@ -18,12 +18,14 @@ export HOME="$tmp/home"
|
|
|
18
18
|
mkdir -p "$HOME"
|
|
19
19
|
# Clean the inherited env so the test controls what is "configured".
|
|
20
20
|
unset CAIRN_LLM_API_URL CAIRN_MEMORY_EMBEDDING_URL CAIRN_GIT_PROVIDER CAIRN_AGENTFS_BASE_DIR
|
|
21
|
+
unset CAIRN_MEMORY_RERANK CAIRN_MEMORY_RERANK_URL CAIRN_MEMORY_RERANK_MODEL CAIRN_MEMORY_RERANK_API_KEY CAIRN_MEMORY_RERANK_TIMEOUT_MS
|
|
21
22
|
|
|
22
23
|
# 1. Nothing configured → only SKIP/PASS/WARN, exit 0.
|
|
23
24
|
proj="$tmp/clean"; mkdir -p "$proj"
|
|
24
25
|
( cd "$proj" && "$doctor" ) >"$tmp/out1" 2>&1 || fail "doctor exited non-zero with nothing configured:\n$(cat "$tmp/out1")"
|
|
25
26
|
grep -q "\[SKIP\]" "$tmp/out1" || fail "expected SKIP lines when nothing is configured"
|
|
26
27
|
grep -q "typed metadata and note transaction state" "$tmp/out1" || fail "expected typed/note diagnostics in the public doctor output"
|
|
28
|
+
grep -qF "[SKIP] memory reranker" "$tmp/out1" || fail "expected default-off reranker diagnostic"
|
|
27
29
|
grep -q "artifact store (not present" "$tmp/out1" || fail "expected absent opt-in artifact diagnostics"
|
|
28
30
|
grep -q "capability configuration (not present — managed overrides are unused)" "$tmp/out1" ||
|
|
29
31
|
fail "expected absent capability configuration diagnostics"
|
|
@@ -92,6 +94,12 @@ const server = createServer((request, response) => {
|
|
|
92
94
|
request.on("end", () => {
|
|
93
95
|
let body = {};
|
|
94
96
|
try { body = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch {}
|
|
97
|
+
if (request.url === "/v1/rerank") {
|
|
98
|
+
response.writeHead(200, { "Content-Type": "application/json" });
|
|
99
|
+
response.end(JSON.stringify({ results: body.model === "invalid-ranker" ? [] :
|
|
100
|
+
body.documents.map((_, index) => ({ index, relevance_score: -index })) }));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
95
103
|
const routeValid = request.url === "/v1/embeddings";
|
|
96
104
|
const keyValid = request.headers.authorization === "Bearer test-key";
|
|
97
105
|
const modelValid = body.model === "test-embedding";
|
|
@@ -153,6 +161,26 @@ EOF
|
|
|
153
161
|
fail "missing-model diagnostic leaked an upstream body or request id"
|
|
154
162
|
fi
|
|
155
163
|
|
|
164
|
+
# Reranker diagnostics validate configuration and actual data, not HTTP status alone.
|
|
165
|
+
if [[ -n "${port:-}" ]]; then
|
|
166
|
+
( cd "$proj" && CAIRN_MEMORY_RERANK=1 CAIRN_MEMORY_RERANK_URL="http://127.0.0.1:$port/v1" \
|
|
167
|
+
CAIRN_MEMORY_RERANK_MODEL=test-ranker "$doctor" ) >"$tmp/reranker-ok" 2>&1 ||
|
|
168
|
+
fail "doctor rejected a valid reranking response"
|
|
169
|
+
grep -qF "[PASS] memory reranker accepted a synthetic ranking request" "$tmp/reranker-ok" ||
|
|
170
|
+
fail "doctor did not probe reranking"
|
|
171
|
+
if ( cd "$proj" && CAIRN_MEMORY_RERANK=1 "$doctor" ) >"$tmp/reranker-config" 2>&1; then
|
|
172
|
+
fail "doctor accepted an incomplete opted-in reranker"
|
|
173
|
+
fi
|
|
174
|
+
grep -qF "[FAIL] memory reranker configuration invalid" "$tmp/reranker-config" ||
|
|
175
|
+
fail "doctor omitted the reranker configuration diagnostic"
|
|
176
|
+
if ( cd "$proj" && CAIRN_MEMORY_RERANK=1 CAIRN_MEMORY_RERANK_URL="http://127.0.0.1:$port/v1" \
|
|
177
|
+
CAIRN_MEMORY_RERANK_MODEL=invalid-ranker "$doctor" ) >"$tmp/reranker-invalid" 2>&1; then
|
|
178
|
+
fail "doctor mistook fallback for a healthy reranker"
|
|
179
|
+
fi
|
|
180
|
+
grep -qF "[FAIL] memory reranker unavailable or returned invalid ranking data" "$tmp/reranker-invalid" ||
|
|
181
|
+
fail "doctor omitted the invalid ranking response diagnostic"
|
|
182
|
+
fi
|
|
183
|
+
|
|
156
184
|
# 5. Artifact doctor passes a valid store, repairs only derived state, and
|
|
157
185
|
# refuses authoritative full-record corruption without leaking stored values.
|
|
158
186
|
artifact_cli="$ROOT/mcp-memory-server/dist/artifact-cli.js"
|
|
@@ -60,6 +60,15 @@ env -u CAIRN_NOTE_DISTILLATION cairn notes --help >/dev/null \
|
|
|
60
60
|
|| fail "installed package omitted the notes command"
|
|
61
61
|
|
|
62
62
|
installed_root="$tmp/prefix/lib/node_modules/@cairnkeep/cli"
|
|
63
|
+
cairn team --help >"$tmp/installed-team-help"
|
|
64
|
+
grep -qF 'experimental token-first team continuity' "$tmp/installed-team-help" || fail "installed package omitted team preview"
|
|
65
|
+
CAIRN_TEAM=1 cairn team init --organization demo-org --data "$tmp/installed-team" >"$tmp/team-init.json"
|
|
66
|
+
CAIRN_TEAM=1 cairn team project create alpha --data "$tmp/installed-team" >/dev/null
|
|
67
|
+
CAIRN_TEAM=1 cairn team backup --data "$tmp/installed-team" --output "$tmp/team-snapshot.json" >"$tmp/team-backup.json"
|
|
68
|
+
snapshot_digest=$(node -p 'require(process.argv[1]).snapshot_digest' "$tmp/team-backup.json")
|
|
69
|
+
CAIRN_TEAM=1 cairn team restore --input "$tmp/team-snapshot.json" --confirm "$snapshot_digest" --data "$tmp/installed-team-restored" >"$tmp/team-restore.json"
|
|
70
|
+
CAIRN_TEAM=1 cairn team doctor --data "$tmp/installed-team-restored" >"$tmp/team-doctor.json"
|
|
71
|
+
node -e 'if(!require(process.argv[1]).credentials_revoked||!require(process.argv[2]).ok)process.exit(1)' "$tmp/team-restore.json" "$tmp/team-doctor.json" || fail "installed team restore contract failed"
|
|
63
72
|
cairn skill --help >"$tmp/installed-skill-help"
|
|
64
73
|
cmp -s "$tmp/source-skill-help" "$tmp/installed-skill-help" || \
|
|
65
74
|
fail "source and installed skill help differ"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
4
|
+
cd "$ROOT"
|
|
5
|
+
lesson=docs/learning/lessons/L29-team-continuity.md
|
|
6
|
+
video=docs/learning/video-scripts/V29-team-continuity.md
|
|
7
|
+
for doc in docs/team.md "$lesson" "$video"; do
|
|
8
|
+
[[ -f "$doc" ]]
|
|
9
|
+
grep -qi 'preview' "$doc"
|
|
10
|
+
grep -qi 'independent.*review' "$doc"
|
|
11
|
+
grep -qi 'pilot' "$doc"
|
|
12
|
+
if ! grep -qF '2.25.0' "$doc"; then
|
|
13
|
+
echo "Missing team preview release compatibility: $doc" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
if grep -qiE 'unreleased|feature-branch preview|released Cairnkeep CLI does not include' "$doc"; then
|
|
17
|
+
echo 'Released team preview guidance must not claim the feature is absent' >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
done
|
|
21
|
+
[[ -f docs/team-pilot.md ]]
|
|
22
|
+
grep -qF 'team-pilot.md' docs/team.md
|
|
23
|
+
grep -qF 'team-pilot.md' "$lesson"
|
|
24
|
+
grep -qF 'team-pilot.md' "$video"
|
|
25
|
+
grep -qF 'not production admission' docs/team-pilot.md
|
|
26
|
+
grep -qF 'reviewer.token' docs/team-pilot.md
|
|
27
|
+
if grep -qF 'released 2.22.5 baseline' docs/README.md || grep -qF 'Cairnkeep 2.22.5 baseline' "$lesson"; then
|
|
28
|
+
echo 'Team guidance must not pin an obsolete released baseline' >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
grep -q '^\*\*Status:\*\* Brief' "$lesson"
|
|
32
|
+
grep -q '^## Acceptance criteria$' "$lesson"
|
|
33
|
+
grep -q '^## Privacy and trust boundary$' "$lesson"
|
|
34
|
+
grep -qF 'L29-team-continuity.md' docs/learning/README.md
|
|
35
|
+
grep -qF 'L29-team-continuity.md' docs/learning/tracks/operator.md
|
|
36
|
+
grep -qF '`cairn team`' docs/learning/CURRICULUM-MAP.md
|
|
37
|
+
for command in init project member credential serve mcp proposals proposal-show propose review search read audit doctor backup restore memory-delete; do
|
|
38
|
+
node mcp-memory-server/dist/team-cli.js --help | grep -q "cairn team $command"
|
|
39
|
+
done
|
|
40
|
+
node mcp-memory-server/dist/team-cli.js --help | grep -qF 'member remove SUBJECT --project ID'
|
|
41
|
+
for doc in docs/team.md "$lesson" "$video"; do
|
|
42
|
+
grep -qF 'cairn team member remove' "$doc"
|
|
43
|
+
done
|
|
44
|
+
for guide in operating storage privacy-and-data-flow security-assurance harness-compatibility agents; do
|
|
45
|
+
grep -qF 'team.md' "docs/$guide.md"
|
|
46
|
+
done
|
|
47
|
+
for control in team team-review team-http team-mcp team-restore team-cli; do
|
|
48
|
+
grep -qF "smoke-$control.mjs" "$lesson"
|
|
49
|
+
[[ -f "mcp-memory-server/scripts/smoke-$control.mjs" ]]
|
|
50
|
+
done
|
|
51
|
+
node scripts/test-team-pilot.mjs
|
|
52
|
+
echo 'PASS: team preview reference, source controls, admission boundary, learning and video routing'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
5
|
+
import { homedir, tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { readStableFile } from './lib/stable-file.mjs';
|
|
9
|
+
|
|
10
|
+
if (process.platform === 'win32') {
|
|
11
|
+
console.log('SKIP: isolated pilot Bash recipe is Linux/macOS; native CLI controls run separately');
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
const guide = readFileSync(join(root, 'docs/team-pilot.md'), 'utf8');
|
|
16
|
+
const blocks = [...guide.matchAll(/<!-- team-pilot-setup:start -->\s*```sh\n([\s\S]*?)```\s*<!-- team-pilot-setup:end -->/g)];
|
|
17
|
+
assert.equal(blocks.length, 1, 'exactly one maintained executable setup block');
|
|
18
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
19
|
+
const productionPaths = ['.codex/config.toml', '.claude.json', '.qwen/settings.json',
|
|
20
|
+
'.qwen/trustedFolders.json', '.config/opencode/opencode.json'];
|
|
21
|
+
const fingerprint = () => productionPaths.map(name => {
|
|
22
|
+
const path = join(homedir(), name);
|
|
23
|
+
return existsSync(path) ? hash(readFileSync(path)) : null;
|
|
24
|
+
});
|
|
25
|
+
const before = fingerprint();
|
|
26
|
+
const base = mkdtempSync(join(tmpdir(), 'cairn-team-pilot-doc-'));
|
|
27
|
+
const pilot = join(base, 'new lab');
|
|
28
|
+
const environment = { PATH: process.env.PATH, LANG: 'C.UTF-8',
|
|
29
|
+
TEAM_CAIRN: join(root, 'bin/cairn'), TEAM_PILOT_DIR: pilot };
|
|
30
|
+
try {
|
|
31
|
+
const readToken = name => readStableFile(join(pilot, name), {
|
|
32
|
+
label: 'Pilot credential', maxBytes: 256, privateMode: 0o600,
|
|
33
|
+
}).bytes;
|
|
34
|
+
const execute = () => spawnSync('bash', ['-c', blocks[0][1]], {
|
|
35
|
+
cwd: base, env: environment, encoding: 'utf8', timeout: 120000,
|
|
36
|
+
});
|
|
37
|
+
const result = execute();
|
|
38
|
+
assert.equal(result.error, undefined);
|
|
39
|
+
assert.equal(result.status, 0, result.stderr);
|
|
40
|
+
assert.equal(statSync(pilot).mode & 0o777, 0o700);
|
|
41
|
+
const files = ['author.token', 'reader.token', 'reviewer.token'];
|
|
42
|
+
const hashes = files.map(name => {
|
|
43
|
+
const bytes = readToken(name);
|
|
44
|
+
const token = bytes.toString('utf8').trim();
|
|
45
|
+
assert.equal(result.stdout.includes(token), false, 'credential never appears in setup output');
|
|
46
|
+
assert.equal(result.stderr.includes(token), false);
|
|
47
|
+
return hash(bytes);
|
|
48
|
+
});
|
|
49
|
+
assert.equal(new Set(hashes).size, 3, 'separate credentials, not a shared token');
|
|
50
|
+
assert.equal(existsSync(join(pilot, 'author-workspace/reviewer.token')), false);
|
|
51
|
+
assert.equal(existsSync(join(pilot, 'reader-workspace/reviewer.token')), false);
|
|
52
|
+
const retry = execute();
|
|
53
|
+
assert.notEqual(retry.status, 0, 'existing lab must not be reused or overwritten');
|
|
54
|
+
assert.deepEqual(files.map(name => hash(readToken(name))), hashes);
|
|
55
|
+
assert.deepEqual(fingerprint(), before, 'production harness configurations unchanged');
|
|
56
|
+
console.log('PASS: documented pilot setup, isolated identities, private credentials, no token output and no overwrite');
|
|
57
|
+
} finally {
|
|
58
|
+
rmSync(base, { recursive: true, force: true });
|
|
59
|
+
}
|
|
@@ -88,10 +88,12 @@ try {
|
|
|
88
88
|
writeFileSync(join(invalidDerivedPack, `${"e".repeat(64)}.json`), "{\"corrupt\":true}\n");
|
|
89
89
|
process.env.CAIRN_PACK_BASE_DIR = packBase;
|
|
90
90
|
const inheritedLlmUrl = process.env.CAIRN_LLM_API_URL;
|
|
91
|
+
const inheritedRerank = process.env.CAIRN_MEMORY_RERANK;
|
|
91
92
|
const exitCodeBeforeDoctor = process.exitCode;
|
|
92
93
|
const doctorWrite = process.stdout.write.bind(process.stdout);
|
|
93
94
|
let doctorOutput = "";
|
|
94
95
|
delete process.env.CAIRN_LLM_API_URL;
|
|
96
|
+
delete process.env.CAIRN_MEMORY_RERANK;
|
|
95
97
|
process.stdout.write = ((chunk) => { doctorOutput += String(chunk); return true; });
|
|
96
98
|
try {
|
|
97
99
|
await runWindowsCommand({ command: "doctor", args: ["--repair"], root });
|
|
@@ -100,9 +102,12 @@ try {
|
|
|
100
102
|
process.exitCode = exitCodeBeforeDoctor;
|
|
101
103
|
if (inheritedLlmUrl === undefined) delete process.env.CAIRN_LLM_API_URL;
|
|
102
104
|
else process.env.CAIRN_LLM_API_URL = inheritedLlmUrl;
|
|
105
|
+
if (inheritedRerank === undefined) delete process.env.CAIRN_MEMORY_RERANK;
|
|
106
|
+
else process.env.CAIRN_MEMORY_RERANK = inheritedRerank;
|
|
103
107
|
}
|
|
104
108
|
assert.equal(existsSync(invalidDerivedPack), false, "native Windows doctor forwards safe context-pack cache repair");
|
|
105
109
|
assert.match(doctorOutput, /\[PASS\] context pack diagnostics passed/);
|
|
110
|
+
assert.match(doctorOutput, /\[SKIP\] memory reranker unset\/not 1/);
|
|
106
111
|
|
|
107
112
|
let containerArgs = [];
|
|
108
113
|
runNativeContainer(["stdio", "--image", "example/windows:1", "--volume", "windows-data"], root, (_engine, args) => { containerArgs = args; });
|
package/scripts/uninstall.sh
CHANGED
|
@@ -91,6 +91,18 @@ while [[ $# -gt 0 ]]; do
|
|
|
91
91
|
esac
|
|
92
92
|
done
|
|
93
93
|
|
|
94
|
+
# Refuse an overlapping purge before any harness files or registrations change.
|
|
95
|
+
purge_targets=()
|
|
96
|
+
[[ $PURGE_MEMORY -eq 1 ]] && purge_targets+=("$STORE_DIR")
|
|
97
|
+
[[ $PURGE_PACKS -eq 1 ]] && purge_targets+=("$PACK_DIR")
|
|
98
|
+
if [[ $PURGE_MEMORY -eq 1 ]]; then
|
|
99
|
+
for project in "${PROJECTS[@]}"; do purge_targets+=("$project/.agentfs"); done
|
|
100
|
+
fi
|
|
101
|
+
if [[ ${#purge_targets[@]} -gt 0 ]]; then
|
|
102
|
+
command -v node >/dev/null 2>&1 || { echo 'Uninstall purge requires Node to verify team-data retention.' >&2; exit 1; }
|
|
103
|
+
node "$CAIRN_ROOT/scripts/verify-team-retention.mjs" "${purge_targets[@]}" || exit 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
94
106
|
# ---- backup bundle -----------------------------------------------------------
|
|
95
107
|
# Timestamp comes from the runtime clock on the user's machine.
|
|
96
108
|
BACKUP_DIR="$HOME/.cairnkeep-uninstall-$(date +%Y%m%d-%H%M%S)"
|
|
@@ -197,6 +209,7 @@ echo " Pi live root: $PI_LIVE_ROOT"
|
|
|
197
209
|
echo " Kimi root: $KIMI_LIVE_ROOT"
|
|
198
210
|
echo " memory store: $STORE_DIR ($([[ $PURGE_MEMORY -eq 1 ]] && echo 'WILL be purged' || echo 'kept'))"
|
|
199
211
|
echo " context packs: $PACK_DIR ($([[ $PURGE_PACKS -eq 1 ]] && echo 'WILL be purged' || echo 'kept'))"
|
|
212
|
+
echo " team data: retained (separate operator-managed store; no team purge option)"
|
|
200
213
|
[[ ${#PROJECTS[@]} -gt 0 ]] && echo " projects: ${PROJECTS[*]}"
|
|
201
214
|
|
|
202
215
|
if [[ $DRY_RUN -eq 0 && $ASSUME_YES -eq 0 ]]; then
|
|
@@ -7,9 +7,10 @@ const ROOT = resolve(process.cwd());
|
|
|
7
7
|
const MINIMUM_SDK = [1, 32, 1];
|
|
8
8
|
const MINIMUM_NODE_SERVER = [2, 0, 5];
|
|
9
9
|
const TRANSITIVE_MINIMUMS = new Map([
|
|
10
|
-
["
|
|
10
|
+
["body-parser", [2, 4, 0]],
|
|
11
|
+
["fast-uri", [3, 1, 9]],
|
|
11
12
|
["hono", [4, 13, 13]],
|
|
12
|
-
["ip-address", [10, 7,
|
|
13
|
+
["ip-address", [10, 7, 4]],
|
|
13
14
|
["proxy-addr", [2, 0, 8]],
|
|
14
15
|
]);
|
|
15
16
|
const DIRECT_HONO_PACKAGES = new Set(["hono", "@hono/node-server"]);
|
|
@@ -27,6 +27,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
27
27
|
import { HARNESS_IDS, harnessProjectAssets } from "./harness-registry.mjs";
|
|
28
28
|
import { reconcilePlaybookInstructions, removePlaybookInstructions } from "./playbook-instructions.mjs";
|
|
29
29
|
import { readStableFile, readStableText, readStableJson } from "./lib/stable-file.mjs";
|
|
30
|
+
import { assertUninstallRetainsTeam } from "./lib/team-retention.mjs";
|
|
30
31
|
|
|
31
32
|
const BOOTSTRAP_FILES = [
|
|
32
33
|
["env.example.template", ".ai/env.example"],
|
|
@@ -526,6 +527,14 @@ async function doctorWindows(root, args) {
|
|
|
526
527
|
} catch { line("FAIL", `embedding endpoint request failed (${process.env.CAIRN_MEMORY_EMBEDDING_URL})`); }
|
|
527
528
|
}
|
|
528
529
|
} else line("SKIP", "embedding endpoint unset");
|
|
530
|
+
if (process.env.CAIRN_MEMORY_RERANK === "1") {
|
|
531
|
+
let status = 3;
|
|
532
|
+
try { status = await (await import("./probe-memory-reranker.mjs")).probeMemoryReranker(); } catch {}
|
|
533
|
+
line(status === 0 ? "PASS" : "FAIL", status === 0
|
|
534
|
+
? "memory reranker accepted a synthetic ranking request"
|
|
535
|
+
: status === 2 ? "memory reranker configuration invalid — set URL/model and a timeout within 100–10000 ms"
|
|
536
|
+
: "memory reranker unavailable or returned invalid ranking data — search falls back to its existing order");
|
|
537
|
+
} else line("SKIP", "memory reranker unset/not 1");
|
|
529
538
|
const provider = process.env.CAIRN_GIT_PROVIDER?.trim();
|
|
530
539
|
if (!provider || provider === "none") line("SKIP", "git provider unset/none; collaboration commands are off");
|
|
531
540
|
else if (["github", "gitlab", "codeberg", "forgejo"].includes(provider)) line("PASS", `git provider configured: ${provider}`);
|
|
@@ -687,6 +696,8 @@ function uninstallWindows(root, args) {
|
|
|
687
696
|
if (options.has("--purge-memory")) targets.push(resolve(expandHome(process.env.CAIRN_AGENTFS_BASE_DIR || join(homedir(), ".cairnkeep"))));
|
|
688
697
|
if (options.has("--purge-packs")) targets.push(resolve(expandHome(process.env.CAIRN_PACK_BASE_DIR || join(homedir(), ".cairnkeep", "packs"))));
|
|
689
698
|
const candidates = [...new Set(targets)].filter(existsSync);
|
|
699
|
+
assertUninstallRetainsTeam(candidates);
|
|
700
|
+
console.log('team data: retained (separate operator-managed store; no team purge option)');
|
|
690
701
|
const existing = candidates.filter((target) => !candidates.some((parent) => parent !== target && target.toLowerCase().startsWith(`${parent.toLowerCase()}${sep}`)));
|
|
691
702
|
if (dryRun) { for (const target of existing) console.log(`would remove: ${target}`); return; }
|
|
692
703
|
const backup = join(homedir(), "cairnkeep-uninstall-backups", new Date().toISOString().replace(/[:.]/g, "-"));
|
|
@@ -762,14 +773,15 @@ export function powershellCompletion() {
|
|
|
762
773
|
const harnesses = HARNESS_IDS.map((id) => `'${id}'`).join(",");
|
|
763
774
|
return `Register-ArgumentCompleter -Native -CommandName cairn -ScriptBlock {
|
|
764
775
|
param($wordToComplete, $commandAst, $cursorPosition)
|
|
765
|
-
$commands = 'bootstrap','setup','memory-server','sync','sync-pi','sync-kimi','doctor','security','trajectory','artifact','evidence','playbook','capabilities','mcp-tools','pack','proposals','notes','eval','skill','graph','memory','audit-timer','uninstall','completion','version','help'
|
|
776
|
+
$commands = 'bootstrap','setup','memory-server','sync','sync-pi','sync-kimi','doctor','security','team','trajectory','artifact','evidence','playbook','capabilities','mcp-tools','pack','proposals','notes','eval','skill','graph','memory','audit-timer','uninstall','completion','version','help'
|
|
766
777
|
$setup = '--git','--harness','--memory','--policy','--yes','--json','init','existing','none',${harnesses},'local'
|
|
767
778
|
$playbook = 'list','status','init','set','enable','disable','reset','check','record','receipts','instructions','doctor','minimal','balanced','strict','must','should','may','off','start','finish','install','remove','context.recall','context.explore','work.plan','verify.tests','review.repository','review.security','docs.update','learning.capture','--project','--json','--enforce','--complexity','--familiarity','--risk','--public-change','--changed','--change-type','--completed','--skipped','--failed','--actor','--actor-kind','--session','--policy','--decision','--event','--action','--outcome','--reason'
|
|
768
779
|
$pack = 'init','lock','validate','install','import-okf','validate-okf','export-okf','list','show','remove','enable','disable','update','skills','approve-skill','revoke-skill','doctor','--repair','--project','--project-id','--json'
|
|
769
780
|
$proposals = 'create','list','show','apply','doctor','--session','--scope','--model','--category','--project','--json'
|
|
770
781
|
$security = 'doctor','--project','--json'
|
|
782
|
+
$team = 'init','project','member','credential','create','set','remove','issue','revoke','serve','mcp','list','read','search','history','propose','proposals','proposal-show','review','audit','doctor','backup','restore','memory-delete','--organization','--data','--project','--roles','--class','--expires-at','--output','--input','--confirm','--port','--key','--query','--proposal','--decision','--revision','--project-root'
|
|
771
783
|
$evaluation = 'validate','run','ablate','report','prune','delete','protocol','--task-set','--adapter','--output','--repetitions','--seed','--yes','--json','--disable','--experiment','--older-than-days','--dry-run','--trajectory','--codex-jsonl','--capture-authorized'
|
|
772
|
-
$candidates = if ($commandAst.ToString() -match '^\\s*cairn\\s+setup(?:\\s|$)') { $setup } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+playbook(?:\\s|$)') { $playbook } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+pack(?:\\s|$)') { $pack } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+proposals(?:\\s|$)') { $proposals } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+security(?:\\s|$)') { $security } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+eval(?:\\s|$)') { $evaluation } else { $commands }
|
|
784
|
+
$candidates = if ($commandAst.ToString() -match '^\\s*cairn\\s+setup(?:\\s|$)') { $setup } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+team(?:\\s|$)') { $team } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+playbook(?:\\s|$)') { $playbook } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+pack(?:\\s|$)') { $pack } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+proposals(?:\\s|$)') { $proposals } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+security(?:\\s|$)') { $security } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+eval(?:\\s|$)') { $evaluation } else { $commands }
|
|
773
785
|
$candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
774
786
|
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
775
787
|
}
|