@cairnkeep/cli 2.8.0 → 2.11.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/README.md +82 -27
  3. package/bin/cairn +6 -153
  4. package/bin/cairn-container +8 -0
  5. package/docs/building-an-overlay.md +41 -4
  6. package/docs/containers.md +10 -0
  7. package/docs/context-packs.md +94 -0
  8. package/docs/harness-compatibility.md +37 -3
  9. package/docs/learning/CURRICULUM-MAP.md +16 -5
  10. package/docs/learning/FEATURE-GUIDE.md +15 -4
  11. package/docs/learning/README.md +8 -2
  12. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  13. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  14. package/docs/learning/lessons/L02-installation.md +11 -2
  15. package/docs/learning/lessons/L03-first-project.md +1 -1
  16. package/docs/learning/lessons/L19-validated-skill-improvement.md +52 -21
  17. package/docs/learning/lessons/L20-mcp-tool-profiles.md +67 -0
  18. package/docs/learning/lessons/L21-context-packs.md +79 -0
  19. package/docs/learning/lessons/L22-native-windows.md +83 -0
  20. package/docs/learning/lessons/L23-guided-setup.md +138 -0
  21. package/docs/learning/tracks/operator.md +10 -3
  22. package/docs/learning/tracks/quickstart.md +7 -3
  23. package/docs/mcp-tool-profiles.md +43 -0
  24. package/docs/native-windows.md +122 -0
  25. package/docs/operating.md +157 -18
  26. package/docs/privacy-and-data-flow.md +51 -1
  27. package/docs/storage.md +29 -0
  28. package/examples/eval/bundled-fake.json +1 -1
  29. package/mcp-memory-server/dist/artifact-store-windows-worker.js +74 -0
  30. package/mcp-memory-server/dist/artifact-store.js +196 -55
  31. package/mcp-memory-server/dist/capability-cli.js +2 -1
  32. package/mcp-memory-server/dist/capability-config.js +4 -3
  33. package/mcp-memory-server/dist/capability-harness.js +34 -7
  34. package/mcp-memory-server/dist/context-pack-cli.js +185 -0
  35. package/mcp-memory-server/dist/context-pack.js +889 -0
  36. package/mcp-memory-server/dist/eval-cli.js +6 -5
  37. package/mcp-memory-server/dist/eval-plan.js +35 -13
  38. package/mcp-memory-server/dist/eval-process.js +11 -4
  39. package/mcp-memory-server/dist/eval-report.js +8 -5
  40. package/mcp-memory-server/dist/eval-runner.js +2 -0
  41. package/mcp-memory-server/dist/eval-schema.js +1 -0
  42. package/mcp-memory-server/dist/graph-cli.js +7 -1
  43. package/mcp-memory-server/dist/index.js +87 -37
  44. package/mcp-memory-server/dist/mcp-tool-catalog.js +47 -0
  45. package/mcp-memory-server/dist/mcp-tool-cli.js +85 -0
  46. package/mcp-memory-server/dist/mcp-tool-profile.js +164 -0
  47. package/mcp-memory-server/dist/note-store.js +12 -3
  48. package/mcp-memory-server/dist/pi-mcp-bridge.js +185 -0
  49. package/mcp-memory-server/dist/platform-security.js +102 -0
  50. package/mcp-memory-server/dist/skill-store.js +27 -12
  51. package/mcp-memory-server/package.json +4 -1
  52. package/package.json +5 -4
  53. package/pi/extensions/cairnkeep-memory.ts +61 -0
  54. package/schemas/cairnkeep-setup-policy.schema.json +55 -0
  55. package/schemas/cairnkeep-setup.schema.json +36 -0
  56. package/schemas/context-pack.schema.json +33 -0
  57. package/schemas/eval-report.schema.json +1 -0
  58. package/scripts/cairn-cli.mjs +119 -0
  59. package/scripts/cairn-container-cli.mjs +121 -0
  60. package/scripts/cairn-hook.mjs +199 -0
  61. package/scripts/completion.sh +33 -5
  62. package/scripts/doctor.sh +96 -0
  63. package/scripts/fix-permissions.mjs +12 -0
  64. package/scripts/memory-wiki-audit.mjs +95 -0
  65. package/scripts/phase26-test-manifest.mjs +26 -0
  66. package/scripts/run-repository-tests.mjs +76 -0
  67. package/scripts/setup-core.mjs +310 -0
  68. package/scripts/setup-reconcile.mjs +331 -0
  69. package/scripts/setup.mjs +306 -0
  70. package/scripts/sync-pi-assets.sh +19 -4
  71. package/scripts/test-cli-dispatch.sh +6 -0
  72. package/scripts/test-completion.sh +44 -0
  73. package/scripts/test-doctor.sh +66 -0
  74. package/scripts/test-learning-docs.sh +53 -3
  75. package/scripts/test-package-install.sh +81 -0
  76. package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
  77. package/scripts/test-phase18-capability-lifecycle.sh +1 -1
  78. package/scripts/test-pi-lifecycle.mjs +190 -0
  79. package/scripts/test-setup-compatibility.mjs +186 -0
  80. package/scripts/test-setup-output.sh +131 -0
  81. package/scripts/test-setup-overlay.sh +113 -0
  82. package/scripts/test-setup-preflight.mjs +281 -0
  83. package/scripts/test-setup-reconcile.mjs +257 -0
  84. package/scripts/test-uninstall.sh +71 -0
  85. package/scripts/test-windows-native.mjs +136 -0
  86. package/scripts/uninstall.sh +95 -7
  87. package/scripts/verify-pi-mcp-bridge.mjs +588 -0
  88. package/scripts/windows-platform.mjs +725 -0
  89. package/templates/env.example.template +11 -0
@@ -16,6 +16,9 @@ request unless the corresponding endpoint and credential are configured.
16
16
  | `domain_knowledge_sync` | Files selected by the sync configuration | `ANYTHINGLLM_BASE_URL` |
17
17
  | `route_check` | A health request, with no memory or prompt content | `CAIRN_ROUTE_ENDPOINT` |
18
18
  | `context_explore` | Repository path and query are passed to the configured local executable | `CAIRN_EXPLORE_BINARY`; any further data flow is controlled by that tool |
19
+ | Local context-pack install/list/read/substring search | None | Verified immutable files and project pointers under `CAIRN_PACK_BASE_DIR` |
20
+ | Embedding-ranked context-pack search | Enabled file chunks not already cached, plus the query | The explicitly configured embedding endpoint; failure falls back locally |
21
+ | Authenticated HTTP context-pack retrieval | MCP requests and enabled document/approved-skill content | The explicit Cairnkeep HTTP server; requires separate pack HTTP consent |
19
22
  | Optional Graphify workflow | The repository path, exact query/symbol names, root `graphify-out/` work directory, and local `.planning/graphs/` publication are passed to or produced by the operator-installed `graphify` executable | Local Graphify subprocess with a minimal environment and no provider credentials; managed build uses code-only `update`, never semantic document extraction |
20
23
  | Remote HTTP memory | MCP requests and responses, including memory content | The explicitly registered Cairnkeep HTTP server |
21
24
  | Opt-in trajectory capture | None | Local `<project>/.agentfs/trajectory.db` only; no model or HTTP path exists |
@@ -33,6 +36,8 @@ request unless the corresponding endpoint and credential are configured.
33
36
  | Automatic compaction recovery | None | Structured categories are injected locally; raw summary and other bodies are excluded |
34
37
  | Explicit artifact read/show | The redacted body only when HTTP is separately enabled and the operator/client explicitly reads it | Local terminal/stdio, or the explicitly registered authenticated server |
35
38
  | Opt-in evaluation coordinator | Adapter-defined inference may leave the machine only according to the explicit operator-owned adapter and inherited environment | Local adapter subprocess; Cairnkeep supplies no provider, endpoint, credential, model, or network default |
39
+ | Guided project setup | None | Selected scaffold assets and private `.ai/cairnkeep.json` state in the target; machine sync is never automatic |
40
+ | Pi memory extension | None | Local `cairn memory-server` stdio child; no HTTP transport is inherited |
36
41
 
37
42
  Model endpoints may be local or remote. Cairnkeep cannot determine a provider's
38
43
  retention, training, or logging policy; verify it before sending confidential
@@ -88,6 +93,35 @@ and its generated `revert.sh` restores exact bytes, modes, and layout. Explicit
88
93
  `cairn eval delete` and `prune` remove selected contained experiment trees with
89
94
  no hidden tombstone.
90
95
 
96
+ ## Guided setup and Pi bridge
97
+
98
+ Guided setup performs a read-only target and Git preflight before project
99
+ writes. Its schema-v1 `.ai/cairnkeep.json` record contains the package version,
100
+ Git and memory choices, selected harness names, and setup-owned asset paths,
101
+ digests, modes, and template identifiers. It does not contain credentials,
102
+ endpoints, absolute paths, prompts, or memory values. The file is mode `0600`
103
+ on POSIX and receives the private managed-file ACL on native Windows. Setup
104
+ never invokes machine sync, and it does not start a harness or server child.
105
+
106
+ When the explicitly installed Pi memory extension starts a session, it spawns
107
+ `cairn memory-server` as a child in the project directory over local stdio. The
108
+ bridge removes `MCP_HTTP_PORT` from the child environment, retains only the
109
+ newest 16 KiB of child stderr for bounded failure diagnostics, caps startup,
110
+ calls, catalogs, and results, and closes the child at shutdown. The child still
111
+ inherits the remaining operator environment needed by the normal local server,
112
+ so those environment values and the project directory are a child-process trust
113
+ boundary even though they are not persisted by the bridge.
114
+
115
+ Catalog and call replay is allow-listed to MCP tool metadata and result fields.
116
+ Names, schemas, annotations, content, `structuredContent`, `_meta`, and error
117
+ state are preserved without sanitizing away protocol meaning; Pi receives the
118
+ annotations in trusted result `details` because Pi 0.84.1 has no native
119
+ annotations field. The bridge does not log requests or results, create a second
120
+ catalog, activate prompts or skills, add remote access, or persist a transcript.
121
+ Sanitized test replay uses synthetic tool arguments, results, stderr, and
122
+ project paths only; release evidence must not include credentials, private
123
+ endpoints, memory values, or identifying local paths.
124
+
91
125
  ## Evaluation adapter and report flow
92
126
 
93
127
  `CAIRN_EVAL` is unset/off by default. With it off, `cairn eval` returns a fixed
@@ -166,7 +200,9 @@ network-free, and permanently scoped as framework-only evidence.
166
200
  SIGINT/SIGTERM stops new schedule admission, terminates the active adapter,
167
201
  checkpoints cancellation, performs bounded cleanup, and retains a partial
168
202
  report. POSIX process groups receive TERM followed by bounded KILL escalation.
169
- There is no Windows descendant-process-tree termination guarantee.
203
+ Native Windows uses `taskkill.exe /T` for the exact child tree and `/F` for
204
+ bounded force escalation; it does not enumerate or terminate unrelated
205
+ processes.
170
206
 
171
207
  Turns aggregate only under exact matching compatibility IDs. Missing token
172
208
  components/totals, unknown verifier outcomes, incompatible turn semantics, and
@@ -512,3 +548,17 @@ provide tenant isolation or per-scope authorization.
512
548
  Before sharing diagnostics, remove credentials, private endpoints, database
513
549
  files, memory values, local paths, and project names. Report vulnerabilities
514
550
  through the private channel in [SECURITY.md](../SECURITY.md).
551
+
552
+ ## Context-pack trust boundary
553
+
554
+ Pack validation proves that installed bytes match the manifest and immutable
555
+ digest. It does not authenticate a publisher. Review the source, commit, license,
556
+ and content before enabling a digest; signatures are not yet supported. Local
557
+ directory installation, listing, reading, and substring search make no network
558
+ request. Git installation/update contacts only the operator-supplied source and
559
+ is never scheduled in the background.
560
+
561
+ All enabled documents carry pack and file provenance in MCP results. Skill files
562
+ are excluded until the current project approves the exact pack digest, path, and
563
+ file digest. Updating the pack invalidates that approval. Approval only makes a
564
+ skill readable; Cairnkeep never copies it into a harness or executes it.
package/docs/storage.md CHANGED
@@ -21,6 +21,9 @@ registration, memory remains on that computer.
21
21
  | Opt-in session trajectories and capability callback state | `<harness project root>/.agentfs/trajectory.db` |
22
22
  | Opt-in local artifacts and compaction revisions | `<project root>/.agentfs/artifacts.db` |
23
23
  | Opt-in evaluation reports and note snapshots | `<project root>/.agentfs/eval/experiments/<experiment-id>/` |
24
+ | Immutable context-pack objects and source records | `${CAIRN_PACK_BASE_DIR:-~/.cairnkeep/packs}/objects/` and `sources/` |
25
+ | Digest-pinned context-pack project pointers and approvals | `${CAIRN_PACK_BASE_DIR:-~/.cairnkeep/packs}/projects/` |
26
+ | Optional context-pack embedding cache | `${CAIRN_PACK_BASE_DIR:-~/.cairnkeep/packs}/cache/` (outside immutable objects) |
24
27
  | Opt-in derived hindsight notes | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/notes/` |
25
28
  | Any named/global scope, such as `identity` or `work` | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/<scope>.db` |
26
29
 
@@ -28,6 +31,17 @@ For the local launchers, the server working directory is normally the project
28
31
  root. Bootstrap installs `.agentfs/.gitignore` so private project memory is not
29
32
  accidentally committed. SQLite may also create `-wal` and `-shm` sidecar files.
30
33
 
34
+ Private-file protection is platform-equivalent rather than mode-identical.
35
+ Linux and macOS use `0600` files and `0700` directories. Native Windows removes
36
+ inherited ACLs and grants access only to the current SID, Local System, and the
37
+ built-in Administrators group. `cairn doctor` fails closed when a managed MCP
38
+ profile or context-pack pointer has broader access. Context-pack objects also
39
+ receive a read-only attribute after their content digest is verified.
40
+
41
+ Where this document names a generated `revert.sh`, native Windows produces the
42
+ equivalent backup manifest and `revert.ps1`; it restores exact bytes and the
43
+ Windows ACL contract rather than emulating POSIX mode bits.
44
+
31
45
  `CAIRN_LLM_API_URL` and `CAIRN_MEMORY_EMBEDDING_URL` select optional model
32
46
  services used to process/search memory. They do not change where the SQLite
33
47
  databases are stored. Git-provider and routing configuration do not change
@@ -493,3 +507,18 @@ bearer_token_env_var = "CAIRN_MEMORY_HTTP_TOKEN"
493
507
  "X-Cairn-Scopes" = "identity,personal,project"
494
508
  "X-Cairn-AnythingLLM-Workspaces" = "engineering-patterns"
495
509
  ```
510
+
511
+ ## Context-pack storage
512
+
513
+ Context-pack objects are content-addressed, immutable directories. Installation
514
+ copies verified content through a temporary directory and atomically publishes
515
+ the digest; a failed or concurrent install never changes project enablement.
516
+ Project pointers are separate atomic private files (`0600` on POSIX, restricted
517
+ ACLs on Windows) keyed by canonical local project identity or a validated
518
+ remote project ID. Updates retain old objects
519
+ and switch only the named project after exact-digest confirmation.
520
+
521
+ `cairn doctor` verifies objects, pointers, and interrupted-install remnants.
522
+ It never chooses a pack version or grants a skill approval. `cairn pack remove`
523
+ refuses any referenced digest. Ordinary uninstall retains this directory even
524
+ when memory is removed; `--purge-packs` is the separate backup-first consent.
@@ -1 +1 @@
1
- {"identifier":"cairn-offline-fake-v1","package_version":"2.8.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
1
+ {"identifier":"cairn-offline-fake-v1","package_version":"2.11.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
@@ -0,0 +1,74 @@
1
+ import { AgentFS } from "agentfs-sdk";
2
+ import { deleteArtifactForWindowsWorker, doctorArtifactStore, getArtifactDbPath, listArtifacts, pruneArtifactsForWindowsWorker, putArtifact, readArtifact, readLatestCompaction, recordUnsupportedCompactionAdapter, } from "./artifact-store.js";
3
+ async function readRequest() {
4
+ const chunks = [];
5
+ for await (const chunk of process.stdin)
6
+ chunks.push(Buffer.from(chunk));
7
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
8
+ if (!parsed || typeof parsed.operation !== "string" || typeof parsed.projectRoot !== "string") {
9
+ throw new Error("Invalid Windows artifact worker request.");
10
+ }
11
+ return parsed;
12
+ }
13
+ function resolvedOptions(options = {}) {
14
+ const { now, ...rest } = options;
15
+ return { ...rest, ...(now ? { now: new Date(now) } : {}) };
16
+ }
17
+ async function rawOperation(request) {
18
+ const agent = await AgentFS.open({ id: "artifacts", path: getArtifactDbPath(request.projectRoot) });
19
+ try {
20
+ if (request.action === "list")
21
+ return await agent.kv.list(request.prefix ?? "");
22
+ if (request.action === "get")
23
+ return await agent.kv.get(request.key ?? "");
24
+ if (request.action === "set") {
25
+ await agent.kv.set(request.key ?? "", request.value);
26
+ return null;
27
+ }
28
+ if (request.action === "delete") {
29
+ await agent.kv.delete(request.key ?? "");
30
+ return null;
31
+ }
32
+ throw new Error("Unsupported raw Windows artifact operation.");
33
+ }
34
+ finally {
35
+ await agent.close();
36
+ }
37
+ }
38
+ async function dispatch(request) {
39
+ const limits = request.limits;
40
+ if (request.operation === "put" && limits) {
41
+ return { result: await putArtifact(request.projectRoot, request.candidate, limits, resolvedOptions(request.options)) };
42
+ }
43
+ if (request.operation === "read" && request.identifier) {
44
+ return { result: await readArtifact(request.identifier, request.projectRoot) };
45
+ }
46
+ if (request.operation === "list") {
47
+ return { result: await listArtifacts(request.projectRoot, request.filters) };
48
+ }
49
+ if (request.operation === "latest") {
50
+ return { result: await readLatestCompaction(request.projectRoot, request.sessionRef) };
51
+ }
52
+ if (request.operation === "delete" && request.identifier) {
53
+ return deleteArtifactForWindowsWorker(request.identifier, request.projectRoot, request.options);
54
+ }
55
+ if (request.operation === "prune" && limits) {
56
+ return pruneArtifactsForWindowsWorker(request.projectRoot, limits, resolvedOptions(request.options));
57
+ }
58
+ if (request.operation === "record-unsupported") {
59
+ await recordUnsupportedCompactionAdapter(request.projectRoot, request.diagnostic);
60
+ return { result: null };
61
+ }
62
+ if (request.operation === "doctor" && limits) {
63
+ return { result: await doctorArtifactStore(request.projectRoot, request.repair, limits) };
64
+ }
65
+ if (request.operation === "raw")
66
+ return { result: await rawOperation(request) };
67
+ throw new Error("Unsupported Windows artifact worker operation.");
68
+ }
69
+ dispatch(await readRequest())
70
+ .then((response) => process.stdout.write(JSON.stringify(response)))
71
+ .catch((error) => {
72
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
73
+ process.exitCode = 1;
74
+ });
@@ -1,9 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
3
4
  import { dirname, isAbsolute, relative, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
4
6
  import { AgentFS } from "agentfs-sdk";
5
7
  import { ARTIFACT_SCHEMA_VERSION, artifactEnvelopeSchema, artifactWriteInputSchema, canonicalBytes, canonicalJson, getArtifactLimits, sha256Hex, truncateUtf8, } from "./artifact-schema.js";
6
8
  import { redactLocalValue } from "./trajectory-redaction.js";
9
+ import { atomicReplace } from "./platform-security.js";
7
10
  const META_KEY = "artifact/meta/schema-version";
8
11
  const FULL_PREFIX = "artifact/full/";
9
12
  const INDEX_PREFIX = "artifact/index/";
@@ -14,6 +17,40 @@ const SEQUENCE_PREFIX = "compaction/sequence/";
14
17
  const DIAGNOSTIC_PREFIX = "artifact/meta/unsupported-adapter/";
15
18
  const MAX_DIAGNOSTICS = 32;
16
19
  const mutationQueues = new Map();
20
+ const artifactWorkerPath = fileURLToPath(new URL("./artifact-store-windows-worker.js", import.meta.url));
21
+ function useWindowsArtifactWorker() {
22
+ return (process.platform === "win32" || process.env.CAIRN_TEST_WINDOWS_ARTIFACT_WORKER === "1")
23
+ && resolve(process.argv[1] ?? "") !== resolve(artifactWorkerPath);
24
+ }
25
+ async function runArtifactWorker(request) {
26
+ return new Promise((resolvePromise, rejectPromise) => {
27
+ const child = spawn(process.execPath, [artifactWorkerPath], {
28
+ env: process.env,
29
+ stdio: ["pipe", "pipe", "pipe"],
30
+ windowsHide: true,
31
+ });
32
+ let stdout = "";
33
+ let stderr = "";
34
+ child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
35
+ child.stderr.on("data", (chunk) => {
36
+ stderr = `${stderr}${chunk.toString("utf8")}`.slice(-1024 * 1024);
37
+ });
38
+ child.once("error", rejectPromise);
39
+ child.once("close", (code) => {
40
+ if (code !== 0) {
41
+ rejectPromise(new Error(stderr.trim() || "Windows artifact worker failed."));
42
+ return;
43
+ }
44
+ try {
45
+ resolvePromise(JSON.parse(stdout));
46
+ }
47
+ catch (error) {
48
+ rejectPromise(error);
49
+ }
50
+ });
51
+ child.stdin.end(JSON.stringify(request));
52
+ });
53
+ }
17
54
  export function getArtifactDbPath(projectRoot = process.cwd()) {
18
55
  return resolve(projectRoot, ".agentfs", "artifacts.db");
19
56
  }
@@ -273,7 +310,7 @@ async function rebuildPointers(agent) {
273
310
  if (latest)
274
311
  await agent.kv.set(PROJECT_LATEST_KEY, latest.artifact_id);
275
312
  }
276
- async function rewriteArtifactDatabase(projectRoot, rows) {
313
+ async function rewriteArtifactDatabase(projectRoot, rows, deferReplacement = false) {
277
314
  const dbPath = getArtifactDbPath(projectRoot);
278
315
  const rewritePath = `${dbPath}.rewrite-${randomUUID()}`;
279
316
  const rewrite = await AgentFS.open({ id: "artifacts", path: rewritePath });
@@ -288,11 +325,33 @@ async function rewriteArtifactDatabase(projectRoot, rows) {
288
325
  await rewrite.close();
289
326
  }
290
327
  chmodSync(rewritePath, 0o600);
328
+ if (deferReplacement)
329
+ return rewritePath;
291
330
  for (const suffix of ["-wal", "-shm"]) {
292
331
  rmSync(`${rewritePath}${suffix}`, { force: true });
293
332
  rmSync(`${dbPath}${suffix}`, { force: true });
294
333
  }
295
- renameSync(rewritePath, dbPath);
334
+ await atomicReplace(rewritePath, dbPath);
335
+ return undefined;
336
+ }
337
+ async function installArtifactWorkerRewrite(projectRoot, rewritePath) {
338
+ if (!rewritePath)
339
+ throw new Error("Windows artifact worker did not produce a replacement database.");
340
+ const dbPath = getArtifactDbPath(projectRoot);
341
+ if (!rewritePath.startsWith(`${dbPath}.rewrite-`) || dirname(rewritePath) !== dirname(dbPath)) {
342
+ throw new Error("Windows artifact worker returned an invalid replacement path.");
343
+ }
344
+ try {
345
+ for (const suffix of ["-wal", "-shm"]) {
346
+ rmSync(`${rewritePath}${suffix}`, { force: true });
347
+ rmSync(`${dbPath}${suffix}`, { force: true });
348
+ }
349
+ await atomicReplace(rewritePath, dbPath);
350
+ }
351
+ finally {
352
+ for (const suffix of ["", "-wal", "-shm"])
353
+ rmSync(`${rewritePath}${suffix}`, { force: true });
354
+ }
296
355
  }
297
356
  function planPrune(artifacts, limits, options) {
298
357
  const sorted = sortedOldest(artifacts);
@@ -369,6 +428,15 @@ async function pruneInTransaction(agent, limits, options) {
369
428
  export async function putArtifact(projectRoot, candidate, limits = getArtifactLimits(), options = {}) {
370
429
  const prepared = prepareInput(projectRoot, candidate, limits);
371
430
  return withMutationLock(projectRoot, async () => {
431
+ if (useWindowsArtifactWorker()) {
432
+ return (await runArtifactWorker({
433
+ operation: "put",
434
+ projectRoot: resolve(projectRoot),
435
+ candidate,
436
+ limits,
437
+ options,
438
+ })).result;
439
+ }
372
440
  const agent = await openArtifactStore(projectRoot, true);
373
441
  if (!agent)
374
442
  throw new Error("Unable to open the local artifact store.");
@@ -443,6 +511,13 @@ export async function putCompactionArtifact(projectRoot, candidate, limits = get
443
511
  return putArtifact(projectRoot, parsed, limits, options);
444
512
  }
445
513
  export async function readArtifact(identifier, projectRoot = process.cwd()) {
514
+ if (useWindowsArtifactWorker()) {
515
+ return (await runArtifactWorker({
516
+ operation: "read",
517
+ identifier,
518
+ projectRoot: resolve(projectRoot),
519
+ })).result;
520
+ }
446
521
  const agent = await openArtifactStore(projectRoot, false);
447
522
  if (!agent)
448
523
  throw new Error("Artifact not found.");
@@ -455,6 +530,13 @@ export async function readArtifact(identifier, projectRoot = process.cwd()) {
455
530
  }
456
531
  }
457
532
  export async function listArtifacts(projectRoot = process.cwd(), filters = {}) {
533
+ if (useWindowsArtifactWorker()) {
534
+ return (await runArtifactWorker({
535
+ operation: "list",
536
+ projectRoot: resolve(projectRoot),
537
+ filters,
538
+ })).result;
539
+ }
458
540
  const agent = await openArtifactStore(projectRoot, false);
459
541
  if (!agent)
460
542
  return { schema_version: ARTIFACT_SCHEMA_VERSION, artifacts: [], logical_bytes: 0 };
@@ -486,6 +568,13 @@ export async function listArtifacts(projectRoot = process.cwd(), filters = {}) {
486
568
  }
487
569
  }
488
570
  export async function readLatestCompaction(projectRoot = process.cwd(), sessionRef) {
571
+ if (useWindowsArtifactWorker()) {
572
+ return (await runArtifactWorker({
573
+ operation: "latest",
574
+ projectRoot: resolve(projectRoot),
575
+ sessionRef,
576
+ })).result;
577
+ }
489
578
  const agent = await openArtifactStore(projectRoot, false);
490
579
  if (!agent)
491
580
  return null;
@@ -505,70 +594,114 @@ export async function readLatestCompaction(projectRoot = process.cwd(), sessionR
505
594
  }
506
595
  }
507
596
  export const selectRecoveryArtifact = readLatestCompaction;
597
+ async function deleteArtifactLocally(identifier, projectRoot, options, deferReplacement) {
598
+ const agent = await openArtifactStore(projectRoot, false);
599
+ if (!agent)
600
+ throw new Error("Artifact not found.");
601
+ let retainedRows;
602
+ let result;
603
+ try {
604
+ await assertCompatibleSchema(agent, false);
605
+ const artifact = await findByIdentifier(agent, identifier);
606
+ if (!options.dryRun) {
607
+ await inImmediateTransaction(agent, async () => {
608
+ await deleteArtifactRows(agent, artifact);
609
+ await rebuildPointers(agent);
610
+ });
611
+ await agent.getDatabase().exec("PRAGMA wal_checkpoint(TRUNCATE)");
612
+ retainedRows = await agent.kv.list("");
613
+ }
614
+ result = {
615
+ schema_version: ARTIFACT_SCHEMA_VERSION,
616
+ artifact_id: artifact.artifact_id,
617
+ deleted: !options.dryRun,
618
+ dry_run: Boolean(options.dryRun),
619
+ };
620
+ }
621
+ finally {
622
+ await agent.close();
623
+ }
624
+ const rewritePath = retainedRows
625
+ ? await rewriteArtifactDatabase(projectRoot, retainedRows, deferReplacement)
626
+ : undefined;
627
+ return { result, rewritePath };
628
+ }
629
+ export async function deleteArtifactForWindowsWorker(identifier, projectRoot, options = {}) {
630
+ return deleteArtifactLocally(identifier, projectRoot, options, !options.dryRun);
631
+ }
508
632
  export async function deleteArtifact(identifier, projectRoot = process.cwd(), options = {}) {
509
633
  return withMutationLock(projectRoot, async () => {
510
- const agent = await openArtifactStore(projectRoot, false);
511
- if (!agent)
512
- throw new Error("Artifact not found.");
513
- let retainedRows;
514
- let result;
515
- try {
516
- await assertCompatibleSchema(agent, false);
517
- const artifact = await findByIdentifier(agent, identifier);
518
- if (!options.dryRun) {
519
- await inImmediateTransaction(agent, async () => {
520
- await deleteArtifactRows(agent, artifact);
521
- await rebuildPointers(agent);
522
- });
523
- await agent.getDatabase().exec("PRAGMA wal_checkpoint(TRUNCATE)");
524
- retainedRows = await agent.kv.list("");
525
- }
526
- result = {
527
- schema_version: ARTIFACT_SCHEMA_VERSION,
528
- artifact_id: artifact.artifact_id,
529
- deleted: !options.dryRun,
530
- dry_run: Boolean(options.dryRun),
531
- };
532
- }
533
- finally {
534
- await agent.close();
634
+ if (useWindowsArtifactWorker()) {
635
+ const response = await runArtifactWorker({
636
+ operation: "delete",
637
+ identifier,
638
+ projectRoot: resolve(projectRoot),
639
+ options,
640
+ });
641
+ if (!options.dryRun)
642
+ await installArtifactWorkerRewrite(projectRoot, response.rewritePath);
643
+ return response.result;
535
644
  }
536
- if (retainedRows)
537
- await rewriteArtifactDatabase(projectRoot, retainedRows);
538
- return result;
645
+ return (await deleteArtifactLocally(identifier, projectRoot, options, false)).result;
539
646
  });
540
647
  }
648
+ async function pruneArtifactsLocally(projectRoot, limits, options, deferReplacement) {
649
+ const agent = await openArtifactStore(projectRoot, false);
650
+ if (!agent) {
651
+ return { result: { schema_version: 1, dry_run: Boolean(options.dryRun), removed: [], remaining_artifacts: 0, logical_bytes: 0 } };
652
+ }
653
+ let retainedRows;
654
+ let result;
655
+ try {
656
+ await assertCompatibleSchema(agent, false);
657
+ const resolved = {
658
+ dryRun: options.dryRun ?? false,
659
+ includeProtected: options.includeProtected ?? false,
660
+ now: options.now ?? new Date(),
661
+ };
662
+ result = await inImmediateTransaction(agent, () => pruneInTransaction(agent, limits, resolved));
663
+ if (!resolved.dryRun && result.removed.length > 0) {
664
+ await agent.getDatabase().exec("PRAGMA wal_checkpoint(TRUNCATE)");
665
+ retainedRows = await agent.kv.list("");
666
+ }
667
+ }
668
+ finally {
669
+ await agent.close();
670
+ }
671
+ const rewritePath = retainedRows
672
+ ? await rewriteArtifactDatabase(projectRoot, retainedRows, deferReplacement)
673
+ : undefined;
674
+ return { result, ...(rewritePath ? { rewritePath } : {}) };
675
+ }
676
+ export async function pruneArtifactsForWindowsWorker(projectRoot, limits = getArtifactLimits(), options = {}) {
677
+ return pruneArtifactsLocally(projectRoot, limits, options, !options.dryRun);
678
+ }
541
679
  export async function pruneArtifacts(projectRoot, limits = getArtifactLimits(), options = {}) {
542
680
  return withMutationLock(projectRoot, async () => {
543
- const agent = await openArtifactStore(projectRoot, false);
544
- if (!agent) {
545
- return { schema_version: 1, dry_run: Boolean(options.dryRun), removed: [], remaining_artifacts: 0, logical_bytes: 0 };
546
- }
547
- let retainedRows;
548
- let result;
549
- try {
550
- await assertCompatibleSchema(agent, false);
551
- const resolved = {
552
- dryRun: options.dryRun ?? false,
553
- includeProtected: options.includeProtected ?? false,
554
- now: options.now ?? new Date(),
555
- };
556
- result = await inImmediateTransaction(agent, () => pruneInTransaction(agent, limits, resolved));
557
- if (!resolved.dryRun && result.removed.length > 0) {
558
- await agent.getDatabase().exec("PRAGMA wal_checkpoint(TRUNCATE)");
559
- retainedRows = await agent.kv.list("");
560
- }
561
- }
562
- finally {
563
- await agent.close();
681
+ if (useWindowsArtifactWorker()) {
682
+ const response = await runArtifactWorker({
683
+ operation: "prune",
684
+ projectRoot: resolve(projectRoot),
685
+ limits,
686
+ options,
687
+ });
688
+ if (!options.dryRun && response.rewritePath)
689
+ await installArtifactWorkerRewrite(projectRoot, response.rewritePath);
690
+ return response.result;
564
691
  }
565
- if (retainedRows)
566
- await rewriteArtifactDatabase(projectRoot, retainedRows);
567
- return result;
692
+ return (await pruneArtifactsLocally(projectRoot, limits, options, false)).result;
568
693
  });
569
694
  }
570
695
  export async function recordUnsupportedCompactionAdapter(projectRoot, diagnostic = {}) {
571
696
  await withMutationLock(projectRoot, async () => {
697
+ if (useWindowsArtifactWorker()) {
698
+ await runArtifactWorker({
699
+ operation: "record-unsupported",
700
+ projectRoot: resolve(projectRoot),
701
+ diagnostic,
702
+ });
703
+ return;
704
+ }
572
705
  const agent = await openArtifactStore(projectRoot, true);
573
706
  if (!agent)
574
707
  return;
@@ -727,6 +860,14 @@ function inspectRetentionState(artifacts, limits) {
727
860
  return issues;
728
861
  }
729
862
  export async function doctorArtifactStore(projectRoot = process.cwd(), repair = false, limits = getArtifactLimits()) {
863
+ if (useWindowsArtifactWorker()) {
864
+ return (await runArtifactWorker({
865
+ operation: "doctor",
866
+ projectRoot: resolve(projectRoot),
867
+ repair,
868
+ limits,
869
+ })).result;
870
+ }
730
871
  const dbPath = getArtifactDbPath(projectRoot);
731
872
  if (!existsSync(dbPath)) {
732
873
  return { schema_version: 1, exists: false, ok: true, repaired: false, integrity: "not_present", valid_artifacts: 0, indexed_artifacts: 0, issues: [] };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { lstat } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
+ import { privatePathIsSafe } from "./platform-security.js";
4
5
  import { capabilityAdapterClassificationSchema, finishOperatingCapability, operatingCapabilityBypassResultSchema, operatingCapabilityHandleSchema, startOperatingCapability, } from "./capability-adapter.js";
5
6
  import { readCapabilityConfig, resetCapabilityLogging, resetCapabilityOverride, resolveCapabilityStatus, setCapabilityLogging, setCapabilityOverride, } from "./capability-config.js";
6
7
  import { CAPABILITY_CALLBACK_SCHEMA_VERSION, doctorCapabilityRecords, } from "./capability-store.js";
@@ -226,7 +227,7 @@ async function configDoctor() {
226
227
  try {
227
228
  const info = await lstat(path);
228
229
  exists = true;
229
- unsafePermissions = info.isSymbolicLink() || !info.isFile() || (info.mode & 0o077) !== 0;
230
+ unsafePermissions = info.isSymbolicLink() || !info.isFile() || !privatePathIsSafe(path);
230
231
  }
231
232
  catch (error) {
232
233
  if (error.code !== "ENOENT")
@@ -1,7 +1,8 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
- import { chmod, lstat, mkdir, open, rename, rm, } from "node:fs/promises";
3
+ import { lstat, mkdir, open, rename, rm, } from "node:fs/promises";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
+ import { hardenPrivatePath } from "./platform-security.js";
5
6
  import { CAPABILITY_REGISTRY } from "./capability-registry.js";
6
7
  import { CAPABILITY_IDS, CAPABILITY_SCHEMA_VERSION, capabilityIdSchema, capabilityManagedConfigSchema, capabilityStatusSchema, } from "./capability-schema.js";
7
8
  const MAX_CONFIG_BYTES = 64 * 1024;
@@ -247,9 +248,9 @@ async function atomicWriteConfig(path, config) {
247
248
  await handle.sync();
248
249
  await handle.close();
249
250
  handle = undefined;
250
- await chmod(temporary, 0o600);
251
+ hardenPrivatePath(temporary);
251
252
  await rename(temporary, path);
252
- await chmod(path, 0o600);
253
+ hardenPrivatePath(path);
253
254
  }
254
255
  finally {
255
256
  if (handle)