@cairnkeep/cli 2.22.3 → 2.22.4
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 +56 -1
- package/containers/healthcheck.mjs +27 -33
- package/containers/quadlet/cairnkeep.container +1 -1
- package/docs/containers.md +12 -0
- package/docs/domain-knowledge.md +15 -0
- package/docs/learning/CURRICULUM-MAP.md +1 -1
- package/docs/learning/FEATURE-GUIDE.md +1 -1
- 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 +52 -2
- package/docs/learning/video-scripts/V28-security-assurance.md +45 -0
- package/docs/native-windows.md +10 -0
- package/docs/operating.md +10 -0
- package/docs/privacy-and-data-flow.md +6 -0
- package/docs/releasing.md +3 -2
- package/docs/security-assurance.md +67 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/capability-registry.js +2 -15
- package/mcp-memory-server/dist/context-pack-retrieval.js +6 -6
- package/mcp-memory-server/dist/domain-retrieval-provider.js +100 -45
- package/mcp-memory-server/dist/eval-plan.js +15 -22
- package/mcp-memory-server/dist/eval-runner.js +17 -10
- package/mcp-memory-server/dist/eval-workspace.js +82 -23
- package/mcp-memory-server/dist/failure-signature.js +29 -8
- package/mcp-memory-server/dist/graph-cli.js +25 -10
- package/mcp-memory-server/dist/okf.js +4 -9
- package/mcp-memory-server/dist/playbook-receipt.js +3 -17
- package/mcp-memory-server/dist/skill-store.js +10 -26
- package/mcp-memory-server/dist/stable-file.js +85 -0
- package/mcp-memory-server/dist/work-evidence-store.js +7 -5
- package/omp/extensions/cairnkeep-capture.ts +29 -17
- package/package.json +1 -1
- package/scripts/lib/opencode-v2-plugin-harness.mjs +20 -3
- package/scripts/lib/stable-file.mjs +75 -0
- package/scripts/playbook-instructions.mjs +42 -20
- package/scripts/setup-core.mjs +5 -36
- package/scripts/setup-reconcile.mjs +32 -32
- package/scripts/setup.mjs +7 -31
- package/scripts/test-ci-sharding.sh +5 -0
- package/scripts/test-container-health.mjs +56 -0
- package/scripts/test-container.sh +8 -1
- package/scripts/test-learning-docs.sh +8 -0
- package/scripts/test-omp-staged-security.mjs +110 -0
- package/scripts/test-runtime-file-security.mjs +311 -0
- package/scripts/test-windows-native.mjs +3 -0
- package/scripts/verify-omp-capture.mjs +1 -0
- package/scripts/windows-platform.mjs +54 -22
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import {
|
|
2
2
|
accessSync,
|
|
3
3
|
chmodSync,
|
|
4
|
+
closeSync,
|
|
4
5
|
copyFileSync,
|
|
5
6
|
cpSync,
|
|
6
7
|
existsSync,
|
|
8
|
+
fchmodSync,
|
|
9
|
+
fsyncSync,
|
|
7
10
|
lstatSync,
|
|
8
11
|
mkdirSync,
|
|
9
12
|
mkdtempSync,
|
|
13
|
+
openSync,
|
|
10
14
|
readFileSync,
|
|
11
15
|
readdirSync,
|
|
12
16
|
renameSync,
|
|
13
17
|
rmSync,
|
|
14
18
|
statSync,
|
|
15
|
-
unlinkSync,
|
|
16
19
|
writeFileSync,
|
|
17
20
|
constants as fsConstants,
|
|
18
21
|
} from "node:fs";
|
|
19
22
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
20
23
|
import { spawnSync } from "node:child_process";
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
21
25
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
22
26
|
import { homedir, tmpdir } from "node:os";
|
|
23
27
|
import { HARNESS_IDS, harnessProjectAssets } from "./harness-registry.mjs";
|
|
24
28
|
import { reconcilePlaybookInstructions, removePlaybookInstructions } from "./playbook-instructions.mjs";
|
|
29
|
+
import { readStableFile, readStableText, readStableJson } from "./lib/stable-file.mjs";
|
|
25
30
|
|
|
26
31
|
const BOOTSTRAP_FILES = [
|
|
27
32
|
["env.example.template", ".ai/env.example"],
|
|
@@ -78,19 +83,46 @@ function hardenWindowsAcl(path) {
|
|
|
78
83
|
|
|
79
84
|
function atomicWrite(path, content, mode = 0o600) {
|
|
80
85
|
mkdirSync(dirname(path), { recursive: true });
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const parent = lstatSync(dirname(path));
|
|
87
|
+
if (!parent.isDirectory() || parent.isSymbolicLink()) throw new Error("Windows file publication parent is unsafe.");
|
|
88
|
+
const temporary = `${path}.tmp-${process.pid}-${randomBytes(12).toString("hex")}`;
|
|
89
|
+
let descriptor;
|
|
90
|
+
let ownsTemporary = false;
|
|
91
|
+
try {
|
|
92
|
+
descriptor = openSync(temporary, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL
|
|
93
|
+
| (fsConstants.O_NOFOLLOW ?? 0), mode);
|
|
94
|
+
ownsTemporary = true;
|
|
95
|
+
writeFileSync(descriptor, content);
|
|
96
|
+
fchmodSync(descriptor, mode);
|
|
97
|
+
fsyncSync(descriptor);
|
|
98
|
+
closeSync(descriptor);
|
|
99
|
+
descriptor = undefined;
|
|
100
|
+
if (mode === 0o600) hardenWindowsAcl(temporary);
|
|
101
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
102
|
+
const currentParent = lstatSync(dirname(path));
|
|
103
|
+
if (!currentParent.isDirectory() || currentParent.isSymbolicLink()
|
|
104
|
+
|| parent.dev !== currentParent.dev || parent.ino !== currentParent.ino) throw new Error("Windows file publication parent changed.");
|
|
105
|
+
if (existsSync(path)) {
|
|
106
|
+
const current = lstatSync(path);
|
|
107
|
+
if (!current.isFile() || current.isSymbolicLink()) throw new Error("Windows file publication target is unsafe.");
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
renameSync(temporary, path);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// Never unlink the live destination to emulate replacement. A denied
|
|
113
|
+
// or busy file must retain its previous contents on publication failure.
|
|
114
|
+
if (attempt === 4 || !["EPERM", "EACCES", "EBUSY"].includes(error?.code)) throw error;
|
|
115
|
+
// Allow transient native sharing violations to settle without deleting
|
|
116
|
+
// the destination; total backoff is bounded to 250 milliseconds.
|
|
117
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25 * (attempt + 1));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
88
120
|
if (mode === 0o600) hardenWindowsAcl(path);
|
|
89
121
|
return;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
if (attempt === 4) throw error;
|
|
92
|
-
if (existsSync(path)) unlinkSync(path);
|
|
93
122
|
}
|
|
123
|
+
} finally {
|
|
124
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
125
|
+
if (ownsTemporary) rmSync(temporary, { force: true });
|
|
94
126
|
}
|
|
95
127
|
}
|
|
96
128
|
|
|
@@ -100,7 +132,7 @@ function installFile(source, destination, mode = 0o644) {
|
|
|
100
132
|
return false;
|
|
101
133
|
}
|
|
102
134
|
mkdirSync(dirname(destination), { recursive: true });
|
|
103
|
-
copyFileSync(source, destination);
|
|
135
|
+
copyFileSync(source, destination, fsConstants.COPYFILE_EXCL);
|
|
104
136
|
try { chmodSync(destination, mode); } catch {}
|
|
105
137
|
if (mode === 0o600) hardenWindowsAcl(destination);
|
|
106
138
|
console.log(`created: ${destination}`);
|
|
@@ -323,7 +355,7 @@ function syncFile(source, destination, apply, render = (value) => value) {
|
|
|
323
355
|
|
|
324
356
|
function registerClaudeHook(settingsPath, name, command, event, matcher) {
|
|
325
357
|
let settings = {};
|
|
326
|
-
if (existsSync(settingsPath)) settings =
|
|
358
|
+
if (existsSync(settingsPath)) settings = readStableJson(settingsPath, { label: "Harness settings", maxBytes: 1024 * 1024 });
|
|
327
359
|
settings.hooks ??= {};
|
|
328
360
|
settings.hooks[event] = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
|
|
329
361
|
const already = settings.hooks[event].some((entry) => entry?.hooks?.some((hook) => hook?.command?.includes(name)));
|
|
@@ -339,7 +371,7 @@ function registerClaudeHook(settingsPath, name, command, event, matcher) {
|
|
|
339
371
|
function claudeHookRegistered(settingsPath, name, event) {
|
|
340
372
|
if (!existsSync(settingsPath)) return false;
|
|
341
373
|
try {
|
|
342
|
-
const settings =
|
|
374
|
+
const settings = readStableJson(settingsPath, { label: "Harness settings", maxBytes: 1024 * 1024 });
|
|
343
375
|
return Array.isArray(settings.hooks?.[event])
|
|
344
376
|
&& settings.hooks[event].some((entry) => entry?.hooks?.some((hook) => hook?.command?.includes(name)));
|
|
345
377
|
} catch { return false; }
|
|
@@ -586,13 +618,12 @@ function memoryWindows(args) {
|
|
|
586
618
|
if (sub === "import") {
|
|
587
619
|
const source = resolve(archive);
|
|
588
620
|
if (!existsSync(source)) throw new Error(`no such archive: ${source}`);
|
|
589
|
-
|
|
590
|
-
const files = extractTar(gunzipSync(readFileSync(source), { maxOutputLength: 512 * 1024 * 1024 }));
|
|
621
|
+
const files = extractTar(gunzipSync(readStableFile(source, { label: "Memory archive", maxBytes: 512 * 1024 * 1024 }).bytes, { maxOutputLength: 512 * 1024 * 1024 }));
|
|
591
622
|
if (!files.length) throw new Error("archive contained no scope .db files");
|
|
592
623
|
mkdirSync(base, { recursive: true });
|
|
593
624
|
for (const [name, content] of files) {
|
|
594
625
|
const destination = join(base, name);
|
|
595
|
-
if (existsSync(destination))
|
|
626
|
+
if (existsSync(destination)) atomicWrite(`${destination}.bak-pre-import`, readStableFile(destination, { label: "Memory backup source", maxBytes: 512 * 1024 * 1024 }).bytes, 0o600);
|
|
596
627
|
atomicWrite(destination, content, 0o600);
|
|
597
628
|
}
|
|
598
629
|
console.log(`imported ${files.length} scope db file(s) -> ${base}`);
|
|
@@ -666,12 +697,12 @@ function uninstallWindows(root, args) {
|
|
|
666
697
|
const destination = join(backup, "items", String(manifest.length));
|
|
667
698
|
mkdirSync(dirname(destination), { recursive: true });
|
|
668
699
|
if (lstatSync(target).isDirectory()) cpSync(target, destination, { recursive: true, errorOnExist: false });
|
|
669
|
-
else
|
|
700
|
+
else writeFileSync(destination, readStableFile(target, { label: "Uninstall backup source", maxBytes: 512 * 1024 * 1024 }).bytes, { flag: "wx", mode: 0o600 });
|
|
670
701
|
manifest.push({ source: join("items", String(manifest.length)), destination: target, directory: lstatSync(target).isDirectory() });
|
|
671
702
|
};
|
|
672
703
|
const settingsPath = join(live, "settings.json");
|
|
673
704
|
if (existsSync(settingsPath)) {
|
|
674
|
-
const settings =
|
|
705
|
+
const settings = readStableJson(settingsPath, { label: "Harness settings", maxBytes: 1024 * 1024 });
|
|
675
706
|
let changed = false;
|
|
676
707
|
for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
|
|
677
708
|
if (!Array.isArray(entries)) continue;
|
|
@@ -692,9 +723,10 @@ function uninstallWindows(root, args) {
|
|
|
692
723
|
for (const project of projects) {
|
|
693
724
|
const agents = join(resolve(project), "AGENTS.md");
|
|
694
725
|
if (!existsSync(agents)) continue;
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
726
|
+
let text;
|
|
727
|
+
try { text = readStableText(agents, { label: "AGENTS.md", maxBytes: 1024 * 1024 }).text; }
|
|
728
|
+
catch { continue; }
|
|
729
|
+
if (!text.includes("<!-- cairnkeep:playbook:v1:start -->")) continue;
|
|
698
730
|
backupTarget(agents);
|
|
699
731
|
removePlaybookInstructions(resolve(project));
|
|
700
732
|
console.log(`removed Cairnkeep playbook block: ${agents}`);
|