@cairnkeep/cli 2.24.0 → 2.26.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 +65 -0
- package/docs/README.md +6 -0
- package/docs/agents.md +18 -0
- package/docs/design/team-mode.md +17 -9
- package/docs/harness-compatibility.md +20 -0
- package/docs/learning/CURRICULUM-MAP.md +4 -2
- package/docs/learning/FEATURE-GUIDE.md +35 -3
- 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/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 +6 -2
- package/docs/learning/lessons/L29-team-continuity.md +127 -0
- package/docs/learning/tracks/operator.md +4 -0
- package/docs/learning/video-scripts/V28-security-assurance.md +4 -0
- package/docs/learning/video-scripts/V29-team-continuity.md +84 -0
- package/docs/operating.md +23 -3
- package/docs/privacy-and-data-flow.md +45 -6
- package/docs/provider-data-flow-review.md +48 -0
- package/docs/security-assurance.md +39 -0
- package/docs/storage.md +32 -0
- package/docs/team-onboarding.md +226 -0
- package/docs/team-pilot.md +134 -0
- package/docs/team.md +245 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/capability-config.js +12 -1
- package/mcp-memory-server/dist/team-cli.js +228 -0
- package/mcp-memory-server/dist/team-client.js +71 -0
- package/mcp-memory-server/dist/team-http.js +193 -0
- package/mcp-memory-server/dist/team-mcp.js +89 -0
- package/mcp-memory-server/dist/team-store.js +669 -0
- package/mcp-memory-server/package.json +2 -2
- package/package.json +1 -1
- package/scripts/cairn-cli.mjs +7 -0
- package/scripts/completion.sh +31 -3
- package/scripts/lib/team-retention.mjs +29 -0
- package/scripts/team-connect.mjs +366 -0
- package/scripts/test-ci-sharding.sh +4 -0
- package/scripts/test-codeql-cohort.mjs +35 -0
- package/scripts/test-completion.sh +11 -0
- package/scripts/test-package-install.sh +10 -0
- package/scripts/test-phase18-capability-lifecycle.sh +2 -2
- package/scripts/test-security-baseline.sh +1 -0
- package/scripts/test-team-docs.sh +61 -0
- package/scripts/test-team-installed.mjs +51 -0
- package/scripts/test-team-pilot.mjs +101 -0
- package/scripts/uninstall.sh +13 -0
- package/scripts/verify-security-baseline.mjs +20 -0
- package/scripts/verify-team-retention.mjs +3 -0
- package/scripts/windows-platform.mjs +6 -2
|
@@ -215,8 +215,8 @@ NODE
|
|
|
215
215
|
local first_pid=$!
|
|
216
216
|
run_capability "$project" disable security.audit >"$tmp/concurrent-two.out" 2>&1 &
|
|
217
217
|
local second_pid=$!
|
|
218
|
-
wait "$first_pid" || fail "first concurrent mutation failed"
|
|
219
|
-
wait "$second_pid" || fail "second concurrent mutation failed"
|
|
218
|
+
wait "$first_pid" || { cat "$tmp/concurrent-one.out" >&2; fail "first concurrent mutation failed"; }
|
|
219
|
+
wait "$second_pid" || { cat "$tmp/concurrent-two.out" >&2; fail "second concurrent mutation failed"; }
|
|
220
220
|
node - "$config_path" <<'NODE'
|
|
221
221
|
const fs = require("fs");
|
|
222
222
|
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
[[ -f docs/team-onboarding.md ]]
|
|
23
|
+
for doc in docs/team.md docs/team-pilot.md "$lesson" "$video"; do
|
|
24
|
+
grep -qF 'team-onboarding.md' "$doc"
|
|
25
|
+
done
|
|
26
|
+
for heading in 'Choose the right workflow' 'Operator handoff' 'Reader quick-check' 'Contributor and reviewer' 'Deployment readiness'; do
|
|
27
|
+
grep -qF "$heading" docs/team-onboarding.md
|
|
28
|
+
done
|
|
29
|
+
grep -qF 'not production admission' docs/team-onboarding.md
|
|
30
|
+
grep -qF 'no real-data deployment is authorized' docs/team-onboarding.md
|
|
31
|
+
grep -qF 'team-pilot.md' docs/team.md
|
|
32
|
+
grep -qF 'team-pilot.md' "$lesson"
|
|
33
|
+
grep -qF 'team-pilot.md' "$video"
|
|
34
|
+
grep -qF 'not production admission' docs/team-pilot.md
|
|
35
|
+
grep -qF 'reviewer.token' docs/team-pilot.md
|
|
36
|
+
if grep -qF 'released 2.22.5 baseline' docs/README.md || grep -qF 'Cairnkeep 2.22.5 baseline' "$lesson"; then
|
|
37
|
+
echo 'Team guidance must not pin an obsolete released baseline' >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
grep -q '^\*\*Status:\*\* Brief' "$lesson"
|
|
41
|
+
grep -q '^## Acceptance criteria$' "$lesson"
|
|
42
|
+
grep -q '^## Privacy and trust boundary$' "$lesson"
|
|
43
|
+
grep -qF 'L29-team-continuity.md' docs/learning/README.md
|
|
44
|
+
grep -qF 'L29-team-continuity.md' docs/learning/tracks/operator.md
|
|
45
|
+
grep -qF '`cairn team`' docs/learning/CURRICULUM-MAP.md
|
|
46
|
+
for command in connect disconnect init project member credential serve mcp proposals proposal-show propose review search read audit doctor backup restore memory-delete; do
|
|
47
|
+
node mcp-memory-server/dist/team-cli.js --help | grep -q "cairn team $command"
|
|
48
|
+
done
|
|
49
|
+
node mcp-memory-server/dist/team-cli.js --help | grep -qF 'member remove SUBJECT --project ID'
|
|
50
|
+
for doc in docs/team.md "$lesson" "$video"; do
|
|
51
|
+
grep -qF 'cairn team member remove' "$doc"
|
|
52
|
+
done
|
|
53
|
+
for guide in operating storage privacy-and-data-flow security-assurance harness-compatibility agents; do
|
|
54
|
+
grep -qF 'team.md' "docs/$guide.md"
|
|
55
|
+
done
|
|
56
|
+
for control in team team-review team-http team-mcp team-restore team-cli team-onboarding; do
|
|
57
|
+
grep -qF "smoke-$control.mjs" "$lesson"
|
|
58
|
+
[[ -f "mcp-memory-server/scripts/smoke-$control.mjs" ]]
|
|
59
|
+
done
|
|
60
|
+
node scripts/test-team-pilot.mjs
|
|
61
|
+
echo 'PASS: team preview reference, source controls, admission boundary, learning and video routing'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, existsSync, realpathSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
// The caller passes the ACTUAL installed package, never a source-relative dist.
|
|
10
|
+
const root = resolve(process.argv[2]);
|
|
11
|
+
const { openTeamStore } = await import(pathToFileURL(join(root, 'mcp-memory-server/dist/team-store.js')));
|
|
12
|
+
const { createTeamHttpServer } = await import(pathToFileURL(join(root, 'mcp-memory-server/dist/team-http.js')));
|
|
13
|
+
const { hardenPrivatePath } = await import(pathToFileURL(join(root, 'mcp-memory-server/dist/platform-security.js')));
|
|
14
|
+
const exec = promisify(execFile);
|
|
15
|
+
const base = realpathSync(mkdtempSync(join(tmpdir(), 'cairn-installed-team-'))); hardenPrivatePath(base);
|
|
16
|
+
const project = join(base, 'project with spaces'); mkdirSync(project, { mode: 0o700 }); hardenPrivatePath(project);
|
|
17
|
+
const home = join(base, 'home'); mkdirSync(home, { mode: 0o700 }); hardenPrivatePath(home);
|
|
18
|
+
const data = join(base, 'store');
|
|
19
|
+
process.env.CAIRN_TEAM = '1'; process.env.CAIRN_TEAM_HTTP = '1';
|
|
20
|
+
let store, server;
|
|
21
|
+
try {
|
|
22
|
+
store = await openTeamStore(data, { create: true, organization: 'trial-org' });
|
|
23
|
+
await store.admin({ operation: 'project-create', project: 'trial-project' });
|
|
24
|
+
await store.admin({ operation: 'member-set', subject: 'reader', project: 'trial-project', roles: ['reader'] });
|
|
25
|
+
const credential = await store.issue({ subject: 'reader', credential_class: 'workload', expires_at: new Date(Date.now() + 3600000).toISOString() });
|
|
26
|
+
const tokenFile = join(base, 'reader.token'); writeFileSync(tokenFile, credential.token, { mode: 0o600 }); hardenPrivatePath(tokenFile);
|
|
27
|
+
const original = '{"mcpServers":{"cairn-memory":{"command":"personal-unchanged"},"other":{"command":"other-unchanged"}}}\n';
|
|
28
|
+
const config = join(project, '.mcp.json'); writeFileSync(config, original, { mode: 0o600 });
|
|
29
|
+
server = createTeamHttpServer(store); await new Promise(r => server.listen(0, '127.0.0.1', r));
|
|
30
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('CAIRN_')));
|
|
31
|
+
Object.assign(env, { HOME: home, USERPROFILE: home, CAIRN_TEAM: '1' });
|
|
32
|
+
const run = args => exec(process.execPath, [join(root, 'bin/cairn'), 'team', ...args], { env, cwd: base, timeout: process.platform === 'win32' ? 120000 : 30000, maxBuffer: 32768 });
|
|
33
|
+
const args = ['connect', '--project-root', project, '--organization', 'trial-org', '--project', 'trial-project', '--url', `http://127.0.0.1:${server.address().port}`, '--token-file', tokenFile, '--harness', 'claude'];
|
|
34
|
+
assert.match((await run(['connect', '--help'])).stdout, /no-write plan/);
|
|
35
|
+
const plan = await run(args); assert.equal(JSON.parse(plan.stdout).planned, true);
|
|
36
|
+
assert.equal(readFileSync(config, 'utf8'), original, 'installed no-apply preserves exact bytes');
|
|
37
|
+
await run([...args, '--apply', '--yes']);
|
|
38
|
+
const doctor = await run(['doctor', '--client', '--project-root', project]);
|
|
39
|
+
assert.equal(JSON.parse(doctor.stdout).ok, true);
|
|
40
|
+
assert.equal(doctor.stdout.includes(credential.token), false);
|
|
41
|
+
assert.equal(existsSync(join(home, '.cairnkeep/team')), false, 'client diagnostics open no administrator store');
|
|
42
|
+
await run(['disconnect', '--project-root', project]);
|
|
43
|
+
assert.ok(JSON.parse(readFileSync(config, 'utf8')).mcpServers['cairn-team-memory'], 'disconnect plan is read-only');
|
|
44
|
+
await run(['disconnect', '--project-root', project, '--apply', '--yes']);
|
|
45
|
+
assert.equal(readFileSync(config, 'utf8'), original, 'installed disconnect restores exact untouched original');
|
|
46
|
+
assert.equal(readFileSync(tokenFile, 'utf8'), credential.token);
|
|
47
|
+
console.log('PASS: actual installed team plan/connect/client doctor/disconnect, preserved MCPs and no admin store');
|
|
48
|
+
} finally {
|
|
49
|
+
if (server) await new Promise(r => { server.close(r); server.closeAllConnections(); });
|
|
50
|
+
await store?.close(); rmSync(base, { recursive: true, force: true });
|
|
51
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { execFile, 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 { promisify } from 'node:util';
|
|
9
|
+
import { readStableFile } from './lib/stable-file.mjs';
|
|
10
|
+
|
|
11
|
+
if (process.platform === 'win32') {
|
|
12
|
+
console.log('SKIP: isolated pilot Bash recipe is Linux/macOS; native CLI controls run separately');
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const guide = readFileSync(join(root, 'docs/team-pilot.md'), 'utf8');
|
|
17
|
+
const blocks = [...guide.matchAll(/<!-- team-pilot-setup:start -->\s*```sh\n([\s\S]*?)```\s*<!-- team-pilot-setup:end -->/g)];
|
|
18
|
+
assert.equal(blocks.length, 1, 'exactly one maintained executable setup block');
|
|
19
|
+
const onboarding = readFileSync(join(root, 'docs/team-onboarding.md'), 'utf8');
|
|
20
|
+
const readerBlocks = [...onboarding.matchAll(/<!-- team-reader-check:start -->\s*```sh\n([\s\S]*?)```\s*<!-- team-reader-check:end -->/g)];
|
|
21
|
+
assert.equal(readerBlocks.length, 1, 'exactly one maintained reader check');
|
|
22
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
23
|
+
const productionPaths = ['.codex/config.toml', '.claude.json', '.qwen/settings.json',
|
|
24
|
+
'.qwen/trustedFolders.json', '.config/opencode/opencode.json'];
|
|
25
|
+
const fingerprint = () => productionPaths.map(name => {
|
|
26
|
+
const path = join(homedir(), name);
|
|
27
|
+
return existsSync(path) ? hash(readFileSync(path)) : null;
|
|
28
|
+
});
|
|
29
|
+
const before = fingerprint();
|
|
30
|
+
const base = mkdtempSync(join(tmpdir(), 'cairn-team-pilot-doc-'));
|
|
31
|
+
const pilot = join(base, 'new lab');
|
|
32
|
+
const environment = { PATH: process.env.PATH, LANG: 'C.UTF-8',
|
|
33
|
+
TEAM_CAIRN: join(root, 'bin/cairn'), TEAM_PILOT_DIR: pilot };
|
|
34
|
+
try {
|
|
35
|
+
const readToken = name => readStableFile(join(pilot, name), {
|
|
36
|
+
label: 'Pilot credential', maxBytes: 256, privateMode: 0o600,
|
|
37
|
+
}).bytes;
|
|
38
|
+
const execute = () => spawnSync('bash', ['-c', blocks[0][1]], {
|
|
39
|
+
cwd: base, env: environment, encoding: 'utf8', timeout: 120000,
|
|
40
|
+
});
|
|
41
|
+
const result = execute();
|
|
42
|
+
assert.equal(result.error, undefined);
|
|
43
|
+
assert.equal(result.status, 0, result.stderr);
|
|
44
|
+
assert.equal(statSync(pilot).mode & 0o777, 0o700);
|
|
45
|
+
const files = ['author.token', 'reader.token', 'reviewer.token'];
|
|
46
|
+
const hashes = files.map(name => {
|
|
47
|
+
const bytes = readToken(name);
|
|
48
|
+
const token = bytes.toString('utf8').trim();
|
|
49
|
+
assert.equal(result.stdout.includes(token), false, 'credential never appears in setup output');
|
|
50
|
+
assert.equal(result.stderr.includes(token), false);
|
|
51
|
+
return hash(bytes);
|
|
52
|
+
});
|
|
53
|
+
assert.equal(new Set(hashes).size, 3, 'separate credentials, not a shared token');
|
|
54
|
+
assert.equal(existsSync(join(pilot, 'author-workspace/reviewer.token')), false);
|
|
55
|
+
assert.equal(existsSync(join(pilot, 'reader-workspace/reviewer.token')), false);
|
|
56
|
+
const retry = execute();
|
|
57
|
+
assert.notEqual(retry.status, 0, 'existing lab must not be reused or overwritten');
|
|
58
|
+
assert.deepEqual(files.map(name => hash(readToken(name))), hashes);
|
|
59
|
+
const previousTeam = process.env.CAIRN_TEAM;
|
|
60
|
+
let store, server;
|
|
61
|
+
try {
|
|
62
|
+
process.env.CAIRN_TEAM = '1';
|
|
63
|
+
const { openTeamStore } = await import('../mcp-memory-server/dist/team-store.js');
|
|
64
|
+
const { createTeamHttpServer } = await import('../mcp-memory-server/dist/team-http.js');
|
|
65
|
+
store = await openTeamStore(join(pilot, 'store'));
|
|
66
|
+
const previousHttp = process.env.CAIRN_TEAM_HTTP;
|
|
67
|
+
try {
|
|
68
|
+
process.env.CAIRN_TEAM_HTTP = '1';
|
|
69
|
+
server = createTeamHttpServer(store);
|
|
70
|
+
} finally {
|
|
71
|
+
if (previousHttp === undefined) delete process.env.CAIRN_TEAM_HTTP;
|
|
72
|
+
else process.env.CAIRN_TEAM_HTTP = previousHttp;
|
|
73
|
+
}
|
|
74
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
75
|
+
const readerEnvironment = { ...environment, TEAM_PILOT_URL: `http://127.0.0.1:${server.address().port}` };
|
|
76
|
+
// Asynchronous child execution lets the parent serve the real API request.
|
|
77
|
+
const runReader = (extra = '', env = readerEnvironment) => promisify(execFile)('bash', ['-c', readerBlocks[0][1] + extra], {
|
|
78
|
+
cwd: base, env, timeout: 30000, maxBuffer: 16384,
|
|
79
|
+
});
|
|
80
|
+
const reader = await runReader();
|
|
81
|
+
assert.deepEqual(JSON.parse(reader.stdout), [], 'reader lists no unapproved content');
|
|
82
|
+
for (const name of files) {
|
|
83
|
+
const token = readToken(name).toString('utf8').trim();
|
|
84
|
+
assert.equal((reader.stdout + reader.stderr).includes(token), false);
|
|
85
|
+
}
|
|
86
|
+
await assert.rejects(runReader('\nteam_pilot_reader proposals\n'), error => error.code === 1 && /denied/.test(error.stderr));
|
|
87
|
+
const { TEAM_PILOT_URL, ...missingUrl } = readerEnvironment;
|
|
88
|
+
await assert.rejects(runReader('', missingUrl), error => error.code !== 0 && /TEAM_PILOT_URL/.test(error.stderr));
|
|
89
|
+
assert.deepEqual(files.map(name => hash(readToken(name))), hashes, 'reader check does not rewrite credentials');
|
|
90
|
+
} finally {
|
|
91
|
+
if (server) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); }
|
|
92
|
+
await store?.close();
|
|
93
|
+
if (previousTeam === undefined) delete process.env.CAIRN_TEAM;
|
|
94
|
+
else process.env.CAIRN_TEAM = previousTeam;
|
|
95
|
+
}
|
|
96
|
+
assert.deepEqual(fingerprint(), before, 'production harness configurations unchanged');
|
|
97
|
+
console.log('PASS: documented real reader request, server role denial, required inputs and credential preservation');
|
|
98
|
+
console.log('PASS: documented pilot setup, isolated identities, private credentials, no token output and no overwrite');
|
|
99
|
+
} finally {
|
|
100
|
+
rmSync(base, { recursive: true, force: true });
|
|
101
|
+
}
|
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
|
|
@@ -33,6 +33,25 @@ function walk(value, visit, path = "workflow") {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function validateCodeqlCohort(workflows, dependabot) {
|
|
37
|
+
const references = [];
|
|
38
|
+
for (const workflow of workflows) walk(workflow, (key, value) => {
|
|
39
|
+
if (key === "uses" && typeof value === "string" && value.startsWith("github/codeql-action/")) {
|
|
40
|
+
assert.match(value, /^github\/codeql-action\/[^@\s]+@[a-f0-9]{40}$/, "CodeQL actions must be pinned to a full commit");
|
|
41
|
+
references.push(value);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
assert.ok(references.length > 0, "CodeQL cohort is missing");
|
|
45
|
+
assert.equal(new Set(references.map(value => value.split("@")[1])).size, 1, "CodeQL cohort must use the same commit across workflows");
|
|
46
|
+
for (const action of ["init", "analyze", "upload-sarif"]) {
|
|
47
|
+
assert.ok(references.some(value => value.startsWith(`github/codeql-action/${action}@`)), `CodeQL cohort is missing ${action}`);
|
|
48
|
+
}
|
|
49
|
+
const updates = dependabot?.updates?.filter(update => update["package-ecosystem"] === "github-actions" && update.directory === "/") ?? [];
|
|
50
|
+
assert.equal(updates.length, 1, "CodeQL cohort requires one root GitHub Actions update group");
|
|
51
|
+
const group = updates[0].groups?.["codeql-action"];
|
|
52
|
+
assert.deepEqual(group, { patterns: ["github/codeql-action/*"] }, "CodeQL dependency group must cover the entire cohort without exclusions or update-type restrictions");
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
function validateWorkflow(name, workflow) {
|
|
37
56
|
assert.ok(workflow && typeof workflow === "object" && !Array.isArray(workflow), `${name} is not a workflow mapping`);
|
|
38
57
|
assert.ok(workflow.permissions && typeof workflow.permissions === "object" && !Array.isArray(workflow.permissions), `${name} must have an explicit workflow permission mapping`);
|
|
@@ -123,6 +142,7 @@ for (const [index, [fixture, expected]] of maliciousFixtures.entries()) {
|
|
|
123
142
|
|
|
124
143
|
for (const { name, workflow } of sources) validateWorkflow(name, workflow);
|
|
125
144
|
validatePublicationPermissions(publish);
|
|
145
|
+
validateCodeqlCohort(sources.map(source => source.workflow), parse(readFileSync(join(root, ".github", "dependabot.yml"), "utf8")));
|
|
126
146
|
|
|
127
147
|
const security = sources.find(({ name }) => name === "security.yml")?.text ?? "";
|
|
128
148
|
assert.match(security, /github\/codeql-action\/init@[a-f0-9]{40}/);
|
|
@@ -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"],
|
|
@@ -695,6 +696,8 @@ function uninstallWindows(root, args) {
|
|
|
695
696
|
if (options.has("--purge-memory")) targets.push(resolve(expandHome(process.env.CAIRN_AGENTFS_BASE_DIR || join(homedir(), ".cairnkeep"))));
|
|
696
697
|
if (options.has("--purge-packs")) targets.push(resolve(expandHome(process.env.CAIRN_PACK_BASE_DIR || join(homedir(), ".cairnkeep", "packs"))));
|
|
697
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)');
|
|
698
701
|
const existing = candidates.filter((target) => !candidates.some((parent) => parent !== target && target.toLowerCase().startsWith(`${parent.toLowerCase()}${sep}`)));
|
|
699
702
|
if (dryRun) { for (const target of existing) console.log(`would remove: ${target}`); return; }
|
|
700
703
|
const backup = join(homedir(), "cairnkeep-uninstall-backups", new Date().toISOString().replace(/[:.]/g, "-"));
|
|
@@ -770,14 +773,15 @@ export function powershellCompletion() {
|
|
|
770
773
|
const harnesses = HARNESS_IDS.map((id) => `'${id}'`).join(",");
|
|
771
774
|
return `Register-ArgumentCompleter -Native -CommandName cairn -ScriptBlock {
|
|
772
775
|
param($wordToComplete, $commandAst, $cursorPosition)
|
|
773
|
-
$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'
|
|
774
777
|
$setup = '--git','--harness','--memory','--policy','--yes','--json','init','existing','none',${harnesses},'local'
|
|
775
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'
|
|
776
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'
|
|
777
780
|
$proposals = 'create','list','show','apply','doctor','--session','--scope','--model','--category','--project','--json'
|
|
778
781
|
$security = 'doctor','--project','--json'
|
|
782
|
+
$team = 'connect','disconnect','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','--connection','--client','--url','--token-file','--harness','--access','--apply','--yes'
|
|
779
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'
|
|
780
|
-
$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 }
|
|
781
785
|
$candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
782
786
|
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
783
787
|
}
|