@cairnkeep/cli 2.22.3 → 2.22.5

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/containers/healthcheck.mjs +27 -33
  3. package/containers/quadlet/cairnkeep.container +1 -1
  4. package/docs/containers.md +12 -0
  5. package/docs/domain-knowledge.md +15 -0
  6. package/docs/learning/CURRICULUM-MAP.md +1 -1
  7. package/docs/learning/FEATURE-GUIDE.md +1 -1
  8. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  9. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  10. package/docs/learning/lessons/L02-installation.md +2 -2
  11. package/docs/learning/lessons/L03-first-project.md +1 -1
  12. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  13. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  14. package/docs/learning/lessons/L21-context-packs.md +1 -1
  15. package/docs/learning/lessons/L22-native-windows.md +2 -2
  16. package/docs/learning/lessons/L23-guided-setup.md +1 -1
  17. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  18. package/docs/learning/lessons/L25-playbooks.md +1 -1
  19. package/docs/learning/lessons/L26-context-intelligence.md +1 -1
  20. package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
  21. package/docs/learning/lessons/L28-security-assurance.md +72 -2
  22. package/docs/learning/tracks/operator.md +6 -1
  23. package/docs/learning/video-scripts/V28-security-assurance.md +65 -0
  24. package/docs/native-windows.md +10 -0
  25. package/docs/operating.md +10 -0
  26. package/docs/privacy-and-data-flow.md +6 -0
  27. package/docs/releasing.md +61 -2
  28. package/docs/security-assurance.md +82 -0
  29. package/examples/eval/bundled-fake.json +1 -1
  30. package/mcp-memory-server/dist/capability-registry.js +2 -15
  31. package/mcp-memory-server/dist/context-pack-retrieval.js +6 -6
  32. package/mcp-memory-server/dist/domain-retrieval-provider.js +100 -45
  33. package/mcp-memory-server/dist/eval-plan.js +15 -22
  34. package/mcp-memory-server/dist/eval-runner.js +17 -10
  35. package/mcp-memory-server/dist/eval-workspace.js +82 -23
  36. package/mcp-memory-server/dist/failure-signature.js +29 -8
  37. package/mcp-memory-server/dist/graph-cli.js +25 -10
  38. package/mcp-memory-server/dist/note-store.js +24 -4
  39. package/mcp-memory-server/dist/okf.js +4 -9
  40. package/mcp-memory-server/dist/playbook-receipt.js +3 -17
  41. package/mcp-memory-server/dist/skill-store.js +10 -26
  42. package/mcp-memory-server/dist/stable-file.js +85 -0
  43. package/mcp-memory-server/dist/work-evidence-store.js +7 -5
  44. package/omp/extensions/cairnkeep-capture.ts +29 -17
  45. package/package.json +1 -1
  46. package/scripts/lib/opencode-v2-plugin-harness.mjs +20 -3
  47. package/scripts/lib/release-verification.mjs +131 -0
  48. package/scripts/lib/stable-file.mjs +75 -0
  49. package/scripts/playbook-instructions.mjs +42 -20
  50. package/scripts/setup-core.mjs +5 -36
  51. package/scripts/setup-reconcile.mjs +32 -32
  52. package/scripts/setup.mjs +7 -31
  53. package/scripts/test-ci-sharding.sh +5 -0
  54. package/scripts/test-container-health.mjs +56 -0
  55. package/scripts/test-container.sh +8 -1
  56. package/scripts/test-learning-docs.sh +8 -0
  57. package/scripts/test-omp-staged-security.mjs +110 -0
  58. package/scripts/test-release-verification.mjs +109 -0
  59. package/scripts/test-release-verification.sh +4 -0
  60. package/scripts/test-runtime-file-security.mjs +311 -0
  61. package/scripts/test-setup-reconcile.mjs +4 -2
  62. package/scripts/test-windows-native.mjs +4 -0
  63. package/scripts/verify-omp-capture.mjs +1 -0
  64. package/scripts/verify-phase19-runtime-evidence.mjs +43 -3
  65. package/scripts/verify-published-release.mjs +138 -0
  66. 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 temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
82
- writeFileSync(temporary, content, { mode });
83
- try { chmodSync(temporary, mode); } catch {}
84
- if (mode === 0o600) hardenWindowsAcl(temporary);
85
- for (let attempt = 0; attempt < 5; attempt += 1) {
86
- try {
87
- renameSync(temporary, path);
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 = JSON.parse(readFileSync(settingsPath, "utf8"));
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 = JSON.parse(readFileSync(settingsPath, "utf8"));
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
- if (statSync(source).size > 512 * 1024 * 1024) throw new Error("memory archive exceeds the 512 MiB compressed limit");
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)) copyFileSync(destination, `${destination}.bak-pre-import`);
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 copyFileSync(target, destination);
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 = JSON.parse(readFileSync(settingsPath, "utf8"));
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
- const info = lstatSync(agents);
696
- if (!info.isFile() || info.isSymbolicLink()) continue;
697
- if (!readFileSync(agents, "utf8").includes("<!-- cairnkeep:playbook:v1:start -->")) continue;
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}`);