@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +56 -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 +52 -2
  22. package/docs/learning/video-scripts/V28-security-assurance.md +45 -0
  23. package/docs/native-windows.md +10 -0
  24. package/docs/operating.md +10 -0
  25. package/docs/privacy-and-data-flow.md +6 -0
  26. package/docs/releasing.md +3 -2
  27. package/docs/security-assurance.md +67 -0
  28. package/examples/eval/bundled-fake.json +1 -1
  29. package/mcp-memory-server/dist/capability-registry.js +2 -15
  30. package/mcp-memory-server/dist/context-pack-retrieval.js +6 -6
  31. package/mcp-memory-server/dist/domain-retrieval-provider.js +100 -45
  32. package/mcp-memory-server/dist/eval-plan.js +15 -22
  33. package/mcp-memory-server/dist/eval-runner.js +17 -10
  34. package/mcp-memory-server/dist/eval-workspace.js +82 -23
  35. package/mcp-memory-server/dist/failure-signature.js +29 -8
  36. package/mcp-memory-server/dist/graph-cli.js +25 -10
  37. package/mcp-memory-server/dist/okf.js +4 -9
  38. package/mcp-memory-server/dist/playbook-receipt.js +3 -17
  39. package/mcp-memory-server/dist/skill-store.js +10 -26
  40. package/mcp-memory-server/dist/stable-file.js +85 -0
  41. package/mcp-memory-server/dist/work-evidence-store.js +7 -5
  42. package/omp/extensions/cairnkeep-capture.ts +29 -17
  43. package/package.json +1 -1
  44. package/scripts/lib/opencode-v2-plugin-harness.mjs +20 -3
  45. package/scripts/lib/stable-file.mjs +75 -0
  46. package/scripts/playbook-instructions.mjs +42 -20
  47. package/scripts/setup-core.mjs +5 -36
  48. package/scripts/setup-reconcile.mjs +32 -32
  49. package/scripts/setup.mjs +7 -31
  50. package/scripts/test-ci-sharding.sh +5 -0
  51. package/scripts/test-container-health.mjs +56 -0
  52. package/scripts/test-container.sh +8 -1
  53. package/scripts/test-learning-docs.sh +8 -0
  54. package/scripts/test-omp-staged-security.mjs +110 -0
  55. package/scripts/test-runtime-file-security.mjs +311 -0
  56. package/scripts/test-windows-native.mjs +3 -0
  57. package/scripts/verify-omp-capture.mjs +1 -0
  58. package/scripts/windows-platform.mjs +54 -22
@@ -0,0 +1,56 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, writeFileSync, chmodSync, rmSync, symlinkSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { hardenPrivatePath } from "../mcp-memory-server/dist/platform-security.js";
6
+
7
+ const source = readFileSync(new URL("../containers/healthcheck.mjs", import.meta.url), "utf8");
8
+ const containerfile = readFileSync(new URL("../Containerfile", import.meta.url), "utf8");
9
+ const quadlet = readFileSync(new URL("../containers/quadlet/cairnkeep.container", import.meta.url), "utf8");
10
+ assert.match(quadlet, /^Secret=cairnkeep-http-token,type=mount,target=http-token,uid=10001,gid=10001,mode=0400$/m,
11
+ "the managed container secret must satisfy the probe's private-file contract");
12
+ assert.match(containerfile, /COPY --chmod=644 containers\/healthcheck\.mjs \/opt\/cairnkeep\/containers\/healthcheck\.mjs/, "probe imports must resolve within the packaged server");
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(source, /parseHttpPort/, "health destination must use the server's numeric port parser");
15
+ assert.match(source, /resolveHttpToken/, "health tokens must use the server's bounded resolver");
16
+ const { containerHealthcheck } = await import("../containers/healthcheck.mjs");
17
+ const root = mkdtempSync(join(tmpdir(), "cairn-health-security-"));
18
+ const originalFetch = globalThis.fetch;
19
+ const token = "synthetic-health-token-0123456789";
20
+ const requests = [];
21
+ try {
22
+ globalThis.fetch = async (url, options) => {
23
+ requests.push({ url: String(url), options });
24
+ return Response.json({ result: {} });
25
+ };
26
+ assert.equal(await containerHealthcheck({}), 0);
27
+ for (const port of ["7801@external.example.test", "1/path", "-1", "65536", " 7801 "]) {
28
+ assert.equal(await containerHealthcheck({ MCP_HTTP_PORT: port, CAIRN_MEMORY_HTTP_TOKEN: token }), 1);
29
+ }
30
+ assert.equal(requests.length, 0, "disabled or invalid configuration must not fetch");
31
+ assert.equal(await containerHealthcheck({ MCP_HTTP_PORT: "7801", CAIRN_MEMORY_HTTP_TOKEN: token }), 0);
32
+ assert.equal(requests[0].url, "http://127.0.0.1:7801/mcp");
33
+ assert.equal(requests[0].options.redirect, "manual");
34
+ assert.equal(requests[0].options.headers.Authorization, `Bearer ${token}`);
35
+ assert.equal(JSON.parse(requests[0].options.body).method, "initialize");
36
+ const tokenFile = join(root, "token");
37
+ writeFileSync(tokenFile, token + "\n", { mode: 0o600 });
38
+ hardenPrivatePath(tokenFile);
39
+ assert.equal(await containerHealthcheck({ MCP_HTTP_PORT: "7801", CAIRN_MEMORY_HTTP_TOKEN_FILE: tokenFile }), 0);
40
+ if (process.platform !== "win32") {
41
+ chmodSync(tokenFile, 0o644);
42
+ assert.equal(await containerHealthcheck({ MCP_HTTP_PORT: "7801", CAIRN_MEMORY_HTTP_TOKEN_FILE: tokenFile }), 1);
43
+ chmodSync(tokenFile, 0o600);
44
+ const link = join(root, "linked-token");
45
+ symlinkSync(tokenFile, link);
46
+ assert.equal(await containerHealthcheck({ MCP_HTTP_PORT: "7801", CAIRN_MEMORY_HTTP_TOKEN_FILE: link }), 1);
47
+ }
48
+ writeFileSync(tokenFile, "x".repeat(64 * 1024 + 1));
49
+ assert.equal(await containerHealthcheck({ MCP_HTTP_PORT: "7801", CAIRN_MEMORY_HTTP_TOKEN_FILE: tokenFile }), 1);
50
+ globalThis.fetch = async () => new Response(null, { status: 302, headers: { Location: "https://external.example.test" } });
51
+ assert.equal(await containerHealthcheck({ MCP_HTTP_PORT: "7801", CAIRN_MEMORY_HTTP_TOKEN: token }), 1);
52
+ } finally {
53
+ globalThis.fetch = originalFetch;
54
+ rmSync(root, { recursive: true, force: true });
55
+ }
56
+ console.log("PASS: container health checks remain loopback-only with bounded, safe credentials and no redirects");
@@ -110,7 +110,14 @@ done
110
110
  exit 1
111
111
  }
112
112
  cat "$tmp/write.out"
113
- "$ENGINE" exec "$name" node /usr/local/lib/cairnkeep/container-healthcheck.mjs
113
+ # Exec probes see the original container environment, not the entrypoint's
114
+ # loaded secret. The deliberately public bind fixture must be rejected.
115
+ if "$ENGINE" exec "$name" node /usr/local/lib/cairnkeep/container-healthcheck.mjs; then
116
+ echo "FAIL: healthcheck accepted a non-private token file" >&2
117
+ exit 1
118
+ fi
119
+ # Exercise the same legacy entrypoint against a private, container-owned file.
120
+ "$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"'
114
121
  "$ENGINE" rm -f "$name" >/dev/null
115
122
 
116
123
  # Replacement boot with the same volume must recover the canary.
@@ -192,5 +192,13 @@ for phrase in \
192
192
  }
193
193
  done
194
194
  grep -qF '`cairn security` | L28' "$coverage"
195
+ for file in "$l28" "$video28" docs/security-assurance.md; do
196
+ for phrase in 'smoke-note-signatures.mjs' 'smoke-runtime-file-reads.mjs' 'smoke-bounded-input-reads.mjs' 'smoke-workspace-file-security.mjs' 'smoke-outbound-security.mjs' 'test-runtime-file-security.mjs' 'test-omp-staged-security.mjs' 'test-container-health.mjs' 'coverage-guided'; do
197
+ grep -qF -- "$phrase" "$file" || {
198
+ echo "Security learning document missing runtime regression boundary: $file ($phrase)" >&2
199
+ exit 1
200
+ }
201
+ done
202
+ done
195
203
 
196
204
  echo "PASS: public learning path structure, readiness, links, and version alignment"
@@ -0,0 +1,110 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import { syncBuiltinESMExports } from "node:module";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+
8
+ const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
9
+ const root = fs.mkdtempSync(join(tmpdir(), "cairn-omp-staged-security-"));
10
+ const originalRead = fs.readFileSync;
11
+ const originalDescriptorRead = fs.readSync;
12
+ const originalOpen = fs.openSync;
13
+ const failures = [];
14
+ function restore() {
15
+ fs.readFileSync = originalRead;
16
+ fs.readSync = originalDescriptorRead;
17
+ fs.openSync = originalOpen;
18
+ syncBuiltinESMExports();
19
+ }
20
+ function mock() {
21
+ const handlers = new Map();
22
+ const commands = new Map();
23
+ const messages = [];
24
+ return {
25
+ handlers, commands, messages,
26
+ pi: {
27
+ on(event, handler) { handlers.set(event, handler); },
28
+ registerCommand(name, command) { commands.set(name, command); },
29
+ getAllTools() { return []; },
30
+ sendMessage(payload) { messages.push(payload); },
31
+ },
32
+ };
33
+ }
34
+ function context(project, ui) {
35
+ return { cwd: project, agent: { kind: "main" }, hasUI: true, ui: { notify(text) { ui.push(text); } } };
36
+ }
37
+ async function control(name, operation) {
38
+ try { await operation(); }
39
+ catch (error) { failures.push(`${name}: ${error.message}`); }
40
+ }
41
+ try {
42
+ fs.writeFileSync(join(root, "package.json"), '{"type":"module"}');
43
+ const extension = join(root, "capture.ts");
44
+ fs.writeFileSync(extension, fs.readFileSync(join(repository, "omp", "extensions", "cairnkeep-capture.ts"), "utf8").replaceAll("@@INFRA_ROOT@@", repository.replaceAll("\\", "/")));
45
+ const { default: factory } = await import(pathToFileURL(extension).href);
46
+ const project = join(root, "project");
47
+ const staging = join(project, ".planning", "memory-staging");
48
+ fs.mkdirSync(staging, { recursive: true });
49
+ const staged = join(staging, "fixture.json");
50
+ const payload = '{"candidates":[{"value":"reviewed fixture"}]}';
51
+ fs.writeFileSync(staged, payload);
52
+ let instance = mock();
53
+ factory(instance.pi);
54
+ let ui = [];
55
+ await instance.handlers.get("session_start")({}, context(project, ui));
56
+ assert.equal(instance.messages.length, 1);
57
+ assert.match(ui[0], /1 memory candidate file/);
58
+ assert.match(instance.messages[0].content, /memory_write/);
59
+
60
+ const outside = join(root, "outside.json");
61
+ fs.writeFileSync(outside, '{"candidates":[{"value":"private-external-sentinel"}]}');
62
+ if (process.platform !== "win32") {
63
+ fs.symlinkSync(outside, join(staging, "linked.json"));
64
+ await control("OMP staged symlink exclusion", async () => {
65
+ instance = mock(); factory(instance.pi); ui = [];
66
+ await instance.handlers.get("session_start")({}, context(project, ui));
67
+ assert.match(ui[0], /1 memory candidate file/);
68
+ ui = [];
69
+ await instance.commands.get("cairn-staged").handler("", context(project, ui));
70
+ assert.equal(ui.join("\n").includes("private-external-sentinel"), false);
71
+ });
72
+ fs.unlinkSync(join(staging, "linked.json"));
73
+ }
74
+ await control("OMP staged growth", async () => {
75
+ const descriptors = new Set();
76
+ let grew = false;
77
+ let unbounded = false;
78
+ const change = () => {
79
+ if (!grew) { grew = true; fs.appendFileSync(staged, " ".repeat(64 * 1024)); }
80
+ };
81
+ fs.openSync = (path, ...args) => {
82
+ const fd = originalOpen(path, ...args);
83
+ if (path === staged) descriptors.add(fd);
84
+ return fd;
85
+ };
86
+ fs.readSync = (fd, ...args) => { if (descriptors.has(fd)) change(); return originalDescriptorRead(fd, ...args); };
87
+ fs.readFileSync = (path, ...args) => {
88
+ if (path === staged || descriptors.has(path)) { unbounded = true; change(); }
89
+ return originalRead(path, ...args);
90
+ };
91
+ syncBuiltinESMExports();
92
+ instance = mock(); factory(instance.pi); ui = [];
93
+ try {
94
+ await instance.handlers.get("session_start")({}, context(project, ui));
95
+ assert.equal(grew, true);
96
+ assert.equal(unbounded, false);
97
+ assert.equal(instance.messages.length, 0, "changed staged content must not become wakeup evidence");
98
+ } finally { restore(); }
99
+ });
100
+ fs.writeFileSync(staged, payload);
101
+ fs.writeFileSync(join(staging, "oversized.json"), '{"candidates":[{"value":"oversized-sentinel"}]}' + " ".repeat(8192));
102
+ ui = []; instance = mock(); factory(instance.pi);
103
+ await instance.commands.get("cairn-staged").handler("", context(project, ui));
104
+ await control("OMP listing bound", () => { assert.equal(ui.join("\n").includes("oversized-sentinel"), false); });
105
+ assert.deepEqual(failures, [], "staged inspection must be bounded, contained and fail open without trusting unsafe content");
106
+ } finally {
107
+ restore();
108
+ fs.rmSync(root, { recursive: true, force: true });
109
+ }
110
+ console.log("PASS: OMP staged inspection rejects symlinks, growth and oversized content without blocking the session");
@@ -0,0 +1,311 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import { syncBuiltinESMExports } from "node:module";
4
+ import { tmpdir } from "node:os";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { gzipSync } from "node:zlib";
8
+
9
+ import { readSetupPolicy, buildSetupPlan } from "./setup-core.mjs";
10
+ import { reconcileSetupPlan } from "./setup-reconcile.mjs";
11
+ import { diagnoseSetup } from "./setup.mjs";
12
+ import { reconcilePlaybookInstructions } from "./playbook-instructions.mjs";
13
+ import { createTar, runWindowsCommand } from "./windows-platform.mjs";
14
+ import { readStableFile, readStableText } from "./lib/stable-file.mjs";
15
+
16
+ const root = fs.mkdtempSync(join(tmpdir(), "cairn-cli-file-security-"));
17
+ const failures = [];
18
+ const original = {
19
+ lstat: fs.lstatSync, open: fs.openSync, close: fs.closeSync,
20
+ read: fs.readSync, readFile: fs.readFileSync,
21
+ rename: fs.renameSync, write: fs.writeFileSync,
22
+ };
23
+ let swap = 0;
24
+ function restore() {
25
+ fs.lstatSync = original.lstat;
26
+ fs.openSync = original.open;
27
+ fs.closeSync = original.close;
28
+ fs.readSync = original.read;
29
+ fs.readFileSync = original.readFile;
30
+ fs.renameSync = original.rename;
31
+ fs.writeFileSync = original.write;
32
+ syncBuiltinESMExports();
33
+ }
34
+ function write(path, value, mode = 0o600) {
35
+ fs.mkdirSync(dirname(path), { recursive: true });
36
+ fs.writeFileSync(path, value, { mode });
37
+ }
38
+ async function control(name, operation) {
39
+ try { await operation(); }
40
+ catch (error) { failures.push(`${name}: ${error.message}`); }
41
+ }
42
+ async function substitute(path, occurrence, replacement, operation) {
43
+ const candidate = join(root, "swaps", `${++swap}.candidate`);
44
+ const inspected = join(root, "swaps", `${swap}.inspected`);
45
+ write(candidate, replacement, fs.statSync(path).mode & 0o777);
46
+ let calls = 0;
47
+ let injected = false;
48
+ fs.lstatSync = (target, ...args) => {
49
+ const info = original.lstat(target, ...args);
50
+ if (target === path && ++calls === occurrence) {
51
+ injected = true;
52
+ fs.renameSync(path, inspected);
53
+ fs.renameSync(candidate, path);
54
+ }
55
+ return info;
56
+ };
57
+ syncBuiltinESMExports();
58
+ try { await operation(); }
59
+ finally { restore(); assert.equal(injected, true, "the actual reader must encounter replacement"); }
60
+ }
61
+ async function grow(path, operation) {
62
+ const inspectedSize = fs.statSync(path).size;
63
+ const descriptors = new Set();
64
+ let injected = false;
65
+ let unbounded = false;
66
+ const allocations = [];
67
+ function change() {
68
+ if (!injected) { injected = true; fs.appendFileSync(path, " ".repeat(2 * 1024 * 1024)); }
69
+ }
70
+ fs.openSync = (target, ...args) => {
71
+ const fd = original.open(target, ...args);
72
+ if (target === path) descriptors.add(fd);
73
+ return fd;
74
+ };
75
+ fs.closeSync = (fd) => { descriptors.delete(fd); return original.close(fd); };
76
+ fs.readSync = (fd, buffer, ...args) => {
77
+ if (descriptors.has(fd)) { change(); allocations.push(buffer.byteLength); }
78
+ return original.read(fd, buffer, ...args);
79
+ };
80
+ fs.readFileSync = (target, ...args) => {
81
+ if (target === path || descriptors.has(target)) { unbounded = true; change(); }
82
+ return original.readFile(target, ...args);
83
+ };
84
+ syncBuiltinESMExports();
85
+ try { await operation(); }
86
+ finally {
87
+ restore();
88
+ assert.equal(injected, true, "the actual reader must encounter growth");
89
+ assert.equal(unbounded, false, "CLI readers must not use an unbounded readFile result");
90
+ assert.ok(allocations.every((size) => size <= inspectedSize + 1));
91
+ assert.equal(descriptors.size, 0, "failed reads must close descriptors");
92
+ }
93
+ }
94
+
95
+ try {
96
+ const missingProject = join(root, "not-created-yet");
97
+ assert.equal(reconcilePlaybookInstructions(missingProject, { check: true }).status, "would-created");
98
+ assert.equal(fs.existsSync(missingProject), false, "instruction preflight must not create a new project");
99
+ assert.throws(() => reconcilePlaybookInstructions(missingProject), /ENOENT/);
100
+ const helperFixture = join(root, "helper.txt");
101
+ write(helperFixture, "bounded", 0o644);
102
+ assert.equal(readStableText(helperFixture, { label: "Fixture", maxBytes: 7 }).text, "bounded");
103
+ fs.readSync = (descriptor, buffer, offset, length, position) => original.read(descriptor, buffer, offset, Math.min(2, length), position);
104
+ syncBuiltinESMExports();
105
+ try { assert.equal(readStableText(helperFixture, { label: "Fixture", maxBytes: 7 }).text, "bounded", "partial reads must assemble complete content"); }
106
+ finally { restore(); }
107
+ let readAttempts = 0;
108
+ fs.readSync = (...args) => { readAttempts += 1; return original.read(...args); };
109
+ syncBuiltinESMExports();
110
+ try {
111
+ assert.throws(() => readStableFile(helperFixture, { label: "Fixture", maxBytes: 6 }), /unsafe/);
112
+ if (process.platform !== "win32") assert.throws(() => readStableFile("/dev/null", { label: "Fixture", maxBytes: 7 }), /unsafe/);
113
+ assert.equal(readAttempts, 0, "oversized and special files must be rejected before content reads");
114
+ } finally { restore(); }
115
+ write(helperFixture, Buffer.from([0xff]), 0o644);
116
+ assert.throws(() => readStableText(helperFixture, { label: "Fixture", maxBytes: 7 }), /UTF-8/);
117
+ if (process.platform !== "win32") assert.throws(() => readStableFile(helperFixture, { label: "Fixture", maxBytes: 7, privateMode: 0o600 }), /unsafe/);
118
+
119
+ const policyPath = join(root, "policy.json");
120
+ write(policyPath, '{"schema_version":1,"defaults":{},"constraints":{}}', 0o644);
121
+ assert.equal(readSetupPolicy(policyPath).schema_version, 1);
122
+ await control("setup policy growth", () => grow(policyPath, () => {
123
+ assert.throws(() => readSetupPolicy(policyPath), /unsafe|changed|limit/i);
124
+ }));
125
+
126
+ const project = join(root, "project");
127
+ fs.mkdirSync(project);
128
+ const plan = buildSetupPlan({
129
+ target: project,
130
+ preflight: { targetState: "empty", gitExecutable: "available", repository: "work-tree" },
131
+ choices: { git: "existing", harnesses: ["claude"], memory: "local", confirmed: true },
132
+ });
133
+ const created = await reconcileSetupPlan(plan);
134
+ assert.equal(diagnoseSetup(project).status, "complete");
135
+ const rerun = await reconcileSetupPlan(plan, { previousState: created.state });
136
+ assert.deepEqual(rerun.counts, { created: 0, updated: 0, unchanged: plan.assets.length, skipped: 0 },
137
+ "native mode representation must not discard generated-file ownership");
138
+ const statePath = join(project, ".ai", "cairnkeep.json");
139
+ const stateBytes = fs.readFileSync(statePath);
140
+ await control("setup diagnosis state growth", () => grow(statePath, () => {
141
+ assert.equal(diagnoseSetup(project).status, "incomplete");
142
+ }));
143
+ write(statePath, stateBytes);
144
+ const owned = join(project, ".ai", "start-claude.sh");
145
+ const ownedBytes = fs.readFileSync(owned);
146
+ write(owned, "# Caller-owned launcher.\n", 0o755);
147
+ await control("setup diagnosis owned replacement", () => substitute(owned, 1, ownedBytes, () => {
148
+ assert.equal(diagnoseSetup(project).status, "incomplete", "replacement must not restore recorded ownership");
149
+ }));
150
+ write(owned, "# Caller-owned launcher.\n", 0o755);
151
+ await control("setup reconciliation ownership replacement", () => substitute(owned, 2, ownedBytes, async () => {
152
+ await assert.rejects(() => reconcileSetupPlan(plan, { previousState: created.state }), /unsafe|changed/i);
153
+ }));
154
+ write(owned, ownedBytes, 0o755);
155
+ await control("setup mutation ownership recheck", async () => {
156
+ const update = { ...plan, assets: [ { ...plan.assets.find((asset) => asset.path === ".ai/start-claude.sh"), bytes: Buffer.from("# Generated update.\n") } ] };
157
+ let observed;
158
+ let inspected = false;
159
+ let injected = false;
160
+ fs.openSync = (path, ...args) => {
161
+ const descriptor = original.open(path, ...args);
162
+ if (path === owned) observed = descriptor;
163
+ return descriptor;
164
+ };
165
+ fs.closeSync = (descriptor) => {
166
+ if (descriptor === observed) inspected = true;
167
+ return original.close(descriptor);
168
+ };
169
+ fs.lstatSync = (path, ...args) => {
170
+ if (inspected && !injected && path === dirname(owned)) {
171
+ injected = true;
172
+ original.write(owned, "# Concurrent caller rule.\n");
173
+ }
174
+ return original.lstat(path, ...args);
175
+ };
176
+ syncBuiltinESMExports();
177
+ try {
178
+ await assert.rejects(() => reconcileSetupPlan(update, { previousState: created.state }), /changed|unsafe/i);
179
+ assert.equal(injected, true);
180
+ assert.equal(fs.readFileSync(owned, "utf8"), "# Concurrent caller rule.\n");
181
+ } finally { restore(); write(owned, ownedBytes, 0o755); }
182
+ });
183
+
184
+ const agents = join(project, "AGENTS.md");
185
+ write(agents, "# User rules\n\nRetain this line.\n", 0o640);
186
+ assert.equal(reconcilePlaybookInstructions(project, { check: true }).status, "would-updated");
187
+ await control("playbook instructions growth", () => grow(agents, () => {
188
+ assert.throws(() => reconcilePlaybookInstructions(project, { check: true }), /unsafe|changed|bounded/i);
189
+ }));
190
+ write(agents, "# User rules\n\nRetain this line.\n", 0o640);
191
+ fs.chmodSync(agents, 0o640);
192
+ assert.equal(reconcilePlaybookInstructions(project).status, "updated");
193
+ assert.match(fs.readFileSync(agents, "utf8"), /Retain this line/);
194
+ if (process.platform !== "win32") assert.equal(fs.statSync(agents).mode & 0o777, 0o640);
195
+ assert.equal(reconcilePlaybookInstructions(project).status, "unchanged");
196
+ await control("instruction mutation ownership recheck", () => {
197
+ write(agents, "# Prior instructions\n", 0o640);
198
+ let injected = false;
199
+ fs.writeFileSync = (path, ...args) => {
200
+ if (!injected && typeof path === "string" && basename(path).startsWith(".AGENTS.md.")) {
201
+ injected = true;
202
+ original.write(agents, "# Concurrent caller rule.\n");
203
+ }
204
+ return original.write(path, ...args);
205
+ };
206
+ fs.openSync = (path, ...args) => {
207
+ if (!injected && typeof path === "string" && basename(path).startsWith(".AGENTS.md.")) {
208
+ injected = true;
209
+ original.write(agents, "# Concurrent caller rule.\n");
210
+ }
211
+ return original.open(path, ...args);
212
+ };
213
+ syncBuiltinESMExports();
214
+ try {
215
+ assert.throws(() => reconcilePlaybookInstructions(project), /changed|unsafe/i);
216
+ assert.equal(injected, true);
217
+ assert.equal(fs.readFileSync(agents, "utf8"), "# Concurrent caller rule.\n");
218
+ assert.equal(fs.readdirSync(project).some((name) => name.startsWith(".AGENTS.md.")), false);
219
+ } finally { restore(); }
220
+ });
221
+ const unapproved = join(root, "unapproved");
222
+ fs.mkdirSync(unapproved);
223
+ write(join(unapproved, "AGENTS.md"), "# Private rule\n", 0o640);
224
+ if (process.platform !== "win32") {
225
+ const linkedProject = join(root, "linked-project");
226
+ fs.symlinkSync(unapproved, linkedProject, "dir");
227
+ await control("playbook project symlink", () => {
228
+ assert.throws(() => reconcilePlaybookInstructions(linkedProject), /unsafe|symlink|directory/i);
229
+ assert.equal(fs.readFileSync(join(unapproved, "AGENTS.md"), "utf8"), "# Private rule\n");
230
+ });
231
+ }
232
+
233
+ const archive = join(root, "memory.tgz");
234
+ const archiveBytes = gzipSync(createTar([["project.db", Buffer.from("New database fixture.")]]));
235
+ write(archive, archiveBytes);
236
+ const memory = join(root, "memory");
237
+ const database = join(memory, "project.db");
238
+ write(database, "Prior database fixture.");
239
+ const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
240
+ const previousBase = process.env.CAIRN_AGENTFS_BASE_DIR;
241
+ process.env.CAIRN_AGENTFS_BASE_DIR = memory;
242
+ try {
243
+ await control("Windows archive growth", () => grow(archive, async () => {
244
+ await assert.rejects(() => runWindowsCommand({ command: "memory", args: ["import", archive], root: repository }), /unsafe|changed/i);
245
+ }));
246
+ write(archive, archiveBytes);
247
+ fs.renameSync = (source, destination) => {
248
+ if (destination === database) throw Object.assign(new Error("Synthetic denied replacement."), { code: "EPERM" });
249
+ return original.rename(source, destination);
250
+ };
251
+ syncBuiltinESMExports();
252
+ await control("Windows denied replacement retention", async () => {
253
+ try {
254
+ await assert.rejects(() => runWindowsCommand({ command: "memory", args: ["import", archive], root: repository }), /denied/i);
255
+ assert.equal(fs.readFileSync(database, "utf8"), "Prior database fixture.", "replacement failure must retain the live database");
256
+ assert.deepEqual(fs.readdirSync(memory).filter((name) => name.includes(".tmp-")), [], "failed replacement must clean its own temporary files");
257
+ } finally { restore(); }
258
+ });
259
+ write(database, "Prior database fixture.");
260
+ let denied = 0;
261
+ fs.renameSync = (source, destination) => {
262
+ if (destination === database && denied++ < 2) {
263
+ assert.equal(fs.readFileSync(database, "utf8"), "Prior database fixture.");
264
+ throw Object.assign(new Error("Synthetic transient sharing violation."), { code: "EBUSY" });
265
+ }
266
+ return original.rename(source, destination);
267
+ };
268
+ syncBuiltinESMExports();
269
+ try {
270
+ await runWindowsCommand({ command: "memory", args: ["import", archive], root: repository });
271
+ assert.equal(denied, 3);
272
+ assert.equal(fs.readFileSync(database, "utf8"), "New database fixture.");
273
+ } finally { restore(); }
274
+ write(database, "Prior database fixture.");
275
+ if (process.platform !== "win32") {
276
+ const outside = join(root, "outside-target");
277
+ write(outside, "Outside sentinel.");
278
+ let injected = false;
279
+ let collidingTemporary;
280
+ function substituteTemporary(path) {
281
+ if (!injected && typeof path === "string" && dirname(path) === memory
282
+ && basename(path).startsWith("project.db.tmp-")) {
283
+ injected = true;
284
+ collidingTemporary = path;
285
+ fs.symlinkSync(outside, path);
286
+ }
287
+ }
288
+ fs.openSync = (path, ...args) => { substituteTemporary(path); return original.open(path, ...args); };
289
+ fs.writeFileSync = (path, ...args) => { substituteTemporary(path); return original.write(path, ...args); };
290
+ syncBuiltinESMExports();
291
+ await control("Windows exclusive temporary publication", async () => {
292
+ try {
293
+ await assert.rejects(() => runWindowsCommand({ command: "memory", args: ["import", archive], root: repository }), /exist|unsafe/i);
294
+ assert.equal(injected, true);
295
+ assert.equal(fs.readFileSync(outside, "utf8"), "Outside sentinel.", "temporary symlink must not receive private content");
296
+ assert.equal(fs.readFileSync(database, "utf8"), "Prior database fixture.");
297
+ assert.equal(fs.lstatSync(collidingTemporary).isSymbolicLink(), true, "a collision must not delete an unowned temporary name");
298
+ } finally { restore(); }
299
+ });
300
+ }
301
+ } finally {
302
+ if (previousBase === undefined) delete process.env.CAIRN_AGENTFS_BASE_DIR;
303
+ else process.env.CAIRN_AGENTFS_BASE_DIR = previousBase;
304
+ }
305
+
306
+ assert.deepEqual(failures, [], "CLI file boundaries must fail closed without changing ownership");
307
+ } finally {
308
+ restore();
309
+ fs.rmSync(root, { recursive: true, force: true });
310
+ }
311
+ console.log("PASS: bounded setup/instruction/archive reads and denied/exclusive Windows publication");
@@ -21,6 +21,9 @@ import { HARNESS_IDS } from "./harness-registry.mjs";
21
21
  const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
22
22
  // Runs on real Windows and POSIX, independently of shell-only doctor checks.
23
23
  await import("./test-setup-diagnosis.mjs");
24
+ await import("./test-runtime-file-security.mjs");
25
+ await import("./test-container-health.mjs");
26
+ await import("./test-omp-staged-security.mjs");
24
27
  const sandbox = mkdtempSync(join(tmpdir(), "cairn-windows-native-"));
25
28
  const originalBase = process.env.CAIRN_AGENTFS_BASE_DIR;
26
29
  const originalPackBase = process.env.CAIRN_PACK_BASE_DIR;
@@ -79,6 +79,7 @@ function writeFakeInfra(sandbox) {
79
79
  writeFileSync(stub, STUB_SERVER);
80
80
  chmodSync(stub, 0o755);
81
81
  copyFileSync(stageWrapper, join(fakeRoot, "scripts", "lib", "omp-capture-stage.mjs"));
82
+ copyFileSync(join(root, "scripts", "lib", "stable-file.mjs"), join(fakeRoot, "scripts", "lib", "stable-file.mjs"));
82
83
  return fakeRoot;
83
84
  }
84
85