@cairnkeep/cli 2.9.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -1
- package/README.md +83 -29
- package/bin/cairn +6 -165
- package/bin/cairn-container +8 -0
- package/docs/building-an-overlay.md +42 -5
- package/docs/containers.md +10 -0
- package/docs/harness-compatibility.md +64 -3
- package/docs/learning/CURRICULUM-MAP.md +13 -6
- package/docs/learning/FEATURE-GUIDE.md +14 -3
- package/docs/learning/README.md +5 -2
- 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 +24 -8
- package/docs/learning/lessons/L03-first-project.md +34 -15
- 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 +83 -0
- package/docs/learning/lessons/L23-guided-setup.md +142 -0
- package/docs/learning/tracks/operator.md +8 -3
- package/docs/learning/tracks/quickstart.md +9 -4
- package/docs/native-windows.md +136 -0
- package/docs/operating.md +142 -20
- package/docs/privacy-and-data-flow.md +41 -1
- package/docs/quickstart.md +86 -0
- package/docs/storage.md +14 -2
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/artifact-store-windows-worker.js +74 -0
- package/mcp-memory-server/dist/artifact-store.js +196 -55
- package/mcp-memory-server/dist/capability-cli.js +2 -1
- package/mcp-memory-server/dist/capability-config.js +4 -3
- package/mcp-memory-server/dist/capability-harness.js +34 -7
- package/mcp-memory-server/dist/context-pack.js +84 -25
- package/mcp-memory-server/dist/eval-cli.js +6 -5
- package/mcp-memory-server/dist/eval-plan.js +35 -13
- package/mcp-memory-server/dist/eval-process.js +11 -4
- package/mcp-memory-server/dist/eval-report.js +7 -5
- package/mcp-memory-server/dist/graph-cli.js +7 -1
- package/mcp-memory-server/dist/index.js +19 -20
- package/mcp-memory-server/dist/mcp-tool-profile.js +6 -4
- package/mcp-memory-server/dist/note-store.js +12 -3
- package/mcp-memory-server/dist/pi-mcp-bridge.js +185 -0
- package/mcp-memory-server/dist/platform-security.js +102 -0
- package/mcp-memory-server/dist/skill-store.js +27 -12
- package/mcp-memory-server/package.json +2 -1
- package/package.json +6 -4
- package/pi/extensions/cairnkeep-memory.ts +61 -0
- package/schemas/cairnkeep-setup-policy.schema.json +55 -0
- package/schemas/cairnkeep-setup.schema.json +36 -0
- package/scripts/bootstrap.sh +7 -15
- package/scripts/cairn-cli.mjs +119 -0
- package/scripts/cairn-container-cli.mjs +121 -0
- package/scripts/cairn-hook.mjs +199 -0
- package/scripts/completion.sh +17 -6
- package/scripts/doctor.sh +58 -0
- package/scripts/fix-permissions.mjs +12 -0
- package/scripts/harness-registry.mjs +126 -0
- package/scripts/memory-wiki-audit.mjs +95 -0
- package/scripts/phase26-test-manifest.mjs +26 -0
- package/scripts/run-repository-tests.mjs +76 -0
- package/scripts/setup-core.mjs +312 -0
- package/scripts/setup-reconcile.mjs +332 -0
- package/scripts/setup.mjs +342 -0
- package/scripts/sync-pi-assets.sh +19 -4
- package/scripts/test-bootstrap-untracked.sh +2 -1
- package/scripts/test-completion.sh +41 -0
- package/scripts/test-doctor.sh +66 -0
- package/scripts/test-harness-registry.sh +94 -0
- package/scripts/test-learning-docs.sh +52 -3
- package/scripts/test-package-install.sh +69 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
- package/scripts/test-phase18-capability-lifecycle.sh +1 -1
- package/scripts/test-phase19-eval-lifecycle.sh +12 -2
- package/scripts/test-pi-lifecycle.mjs +190 -0
- package/scripts/test-setup-compatibility.mjs +187 -0
- package/scripts/test-setup-output.sh +131 -0
- package/scripts/test-setup-overlay.sh +113 -0
- package/scripts/test-setup-preflight.mjs +281 -0
- package/scripts/test-setup-reconcile.mjs +257 -0
- package/scripts/test-uninstall.sh +50 -0
- package/scripts/test-windows-native.mjs +159 -0
- package/scripts/uninstall.sh +73 -6
- package/scripts/verify-pi-mcp-bridge.mjs +588 -0
- package/scripts/windows-platform.mjs +733 -0
- package/templates/codex-config.toml.template +5 -0
- package/templates/start-codex.sh.template +30 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Cairnkeep quickstart
|
|
2
|
+
|
|
3
|
+
This is the shortest supported route from an empty directory or existing Git
|
|
4
|
+
repository to durable, local agent memory. The default transport is stdio and
|
|
5
|
+
the default store stays on this computer.
|
|
6
|
+
|
|
7
|
+
## Codex CLI
|
|
8
|
+
|
|
9
|
+
Install Cairnkeep, then let guided setup initialize an empty project and write
|
|
10
|
+
Codex's project-scoped MCP entry:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install --global @cairnkeep/cli
|
|
14
|
+
cairn setup /path/to/project --git init --harness codex --memory local --yes
|
|
15
|
+
cd /path/to/project
|
|
16
|
+
cairn doctor
|
|
17
|
+
./.ai/start-codex.sh
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Use `--git existing` instead when the target is already a Git work tree. On
|
|
21
|
+
native Windows, launch with `.\.ai\start-codex.cmd`.
|
|
22
|
+
|
|
23
|
+
Before the first session, review `.codex/config.toml` and accept Codex's project
|
|
24
|
+
trust prompt. The generated entry runs `cairn memory-server` locally. Setup
|
|
25
|
+
does not edit the user-wide Codex configuration and does not enable networking,
|
|
26
|
+
telemetry, embeddings, session capture, or context packs.
|
|
27
|
+
|
|
28
|
+
If `.codex/config.toml` already exists and differs, setup reports it as
|
|
29
|
+
`skipped` and preserves it byte-for-byte. Merge this table into the existing
|
|
30
|
+
file, then run `cairn doctor`:
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
[mcp_servers.cairn-memory]
|
|
34
|
+
command = "cairn"
|
|
35
|
+
args = ["memory-server"]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Claude Code
|
|
39
|
+
|
|
40
|
+
Claude Code has the full Cairnkeep operating layer, so it needs one explicit
|
|
41
|
+
machine-level sync in addition to project setup:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install --global @cairnkeep/cli
|
|
45
|
+
claude mcp add cairn-memory -s user -- cairn memory-server
|
|
46
|
+
cairn sync --apply
|
|
47
|
+
cairn setup /path/to/project --git init --harness claude --memory local --yes
|
|
48
|
+
cd /path/to/project
|
|
49
|
+
cairn doctor
|
|
50
|
+
./.ai/start-claude.sh
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Setup reports machine-level commands but never executes them. This keeps
|
|
54
|
+
project scaffolding separate from harness-wide changes.
|
|
55
|
+
|
|
56
|
+
## First memory cycle
|
|
57
|
+
|
|
58
|
+
In a harness with Cairnkeep commands, remember one durable fact and recall it:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
> /remember Use transactional migrations for schema changes
|
|
62
|
+
> /recall transactional migrations
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
In Codex or another MCP client, ask the client to call `memory_write`, then
|
|
66
|
+
`memory_search`. Close the session, start a new one, and repeat the search. The
|
|
67
|
+
result should survive the restart.
|
|
68
|
+
|
|
69
|
+
## What setup owns
|
|
70
|
+
|
|
71
|
+
`cairn setup` creates only the selected harness assets plus the common `.ai`,
|
|
72
|
+
`.planning`, and `.agentfs` scaffold. It initializes Git only with explicit
|
|
73
|
+
`--git init`, preserves unrelated files, does not overwrite modified files it
|
|
74
|
+
does not own, and records managed digests in private `.ai/cairnkeep.json`.
|
|
75
|
+
|
|
76
|
+
Useful follow-ups:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cairn setup . --git existing --harness codex --memory local --yes # reconcile
|
|
80
|
+
cairn doctor # diagnose
|
|
81
|
+
cairn uninstall --dry-run . # preview removal
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For all harnesses, remote HTTP, storage, profiles, context packs, overlays, and
|
|
85
|
+
recovery details, continue with [Operating Cairnkeep](operating.md) or the
|
|
86
|
+
[learning path](learning/README.md).
|
package/docs/storage.md
CHANGED
|
@@ -31,6 +31,17 @@ For the local launchers, the server working directory is normally the project
|
|
|
31
31
|
root. Bootstrap installs `.agentfs/.gitignore` so private project memory is not
|
|
32
32
|
accidentally committed. SQLite may also create `-wal` and `-shm` sidecar files.
|
|
33
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
|
+
|
|
34
45
|
`CAIRN_LLM_API_URL` and `CAIRN_MEMORY_EMBEDDING_URL` select optional model
|
|
35
46
|
services used to process/search memory. They do not change where the SQLite
|
|
36
47
|
databases are stored. Git-provider and routing configuration do not change
|
|
@@ -502,8 +513,9 @@ bearer_token_env_var = "CAIRN_MEMORY_HTTP_TOKEN"
|
|
|
502
513
|
Context-pack objects are content-addressed, immutable directories. Installation
|
|
503
514
|
copies verified content through a temporary directory and atomically publishes
|
|
504
515
|
the digest; a failed or concurrent install never changes project enablement.
|
|
505
|
-
Project pointers are separate atomic
|
|
506
|
-
|
|
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
|
|
507
519
|
and switch only the named project after exact-digest confirmation.
|
|
508
520
|
|
|
509
521
|
`cairn doctor` verifies objects, pointers, and interrupted-install remnants.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"identifier":"cairn-offline-fake-v1","package_version":"2.
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.12.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 {
|
|
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
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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() || (
|
|
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 {
|
|
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
|
-
|
|
251
|
+
hardenPrivatePath(temporary);
|
|
251
252
|
await rename(temporary, path);
|
|
252
|
-
|
|
253
|
+
hardenPrivatePath(path);
|
|
253
254
|
}
|
|
254
255
|
finally {
|
|
255
256
|
if (handle)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { lstat, mkdir, open, readdir, realpath, rename, rm, stat, } from "node:fs/promises";
|
|
5
5
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { isCapabilityContractEnabled, resolveCapabilityStatus, } from "./capability-config.js";
|
|
8
8
|
import { operatingCapabilityHandleSchema, } from "./capability-adapter.js";
|
|
9
9
|
import { CAPABILITY_CALLBACK_SCHEMA_VERSION, isOperatingCapabilitySettled, issueOperatingCapability, settleOperatingCapability, } from "./capability-store.js";
|
|
10
10
|
import { isTrajectoryCaptureEnabled } from "./trajectory-schema.js";
|
|
11
|
+
import { hardenPrivatePath, privatePathIsSafe } from "./platform-security.js";
|
|
11
12
|
const LEASE_DIRECTORY_NAME = "capability-leases-v1";
|
|
12
13
|
const LEASE_SCHEMA_VERSION = 1;
|
|
13
14
|
const MAX_LEASE_BYTES = 4_096;
|
|
@@ -124,12 +125,13 @@ async function ensureLeaseDirectory(options = {}) {
|
|
|
124
125
|
const rootInfo = await lstat(root);
|
|
125
126
|
if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink())
|
|
126
127
|
throw new Error("Harness state root is unsafe.");
|
|
128
|
+
hardenPrivatePath(root);
|
|
127
129
|
const directory = getHarnessCapabilityLeaseDirectory(options);
|
|
128
130
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
129
131
|
const info = await lstat(directory);
|
|
130
132
|
if (!info.isDirectory() || info.isSymbolicLink())
|
|
131
133
|
throw new Error("Harness lease directory is unsafe.");
|
|
132
|
-
|
|
134
|
+
hardenPrivatePath(directory);
|
|
133
135
|
return directory;
|
|
134
136
|
}
|
|
135
137
|
async function canonicalProjectBinding(rawRoot) {
|
|
@@ -185,9 +187,9 @@ async function writeLease(lease, options = {}) {
|
|
|
185
187
|
await handle.sync();
|
|
186
188
|
await handle.close();
|
|
187
189
|
handle = undefined;
|
|
188
|
-
|
|
190
|
+
hardenPrivatePath(temporary);
|
|
189
191
|
await rename(temporary, path);
|
|
190
|
-
|
|
192
|
+
hardenPrivatePath(path);
|
|
191
193
|
}
|
|
192
194
|
finally {
|
|
193
195
|
if (handle)
|
|
@@ -200,7 +202,7 @@ async function readLease(path) {
|
|
|
200
202
|
try {
|
|
201
203
|
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
202
204
|
const info = await handle.stat();
|
|
203
|
-
if (!info.isFile() || info.size > MAX_LEASE_BYTES || (
|
|
205
|
+
if (!info.isFile() || info.size > MAX_LEASE_BYTES || !privatePathIsSafe(path))
|
|
204
206
|
return undefined;
|
|
205
207
|
const bytes = await handle.readFile();
|
|
206
208
|
if (bytes.byteLength > MAX_LEASE_BYTES)
|
|
@@ -216,7 +218,32 @@ async function readLease(path) {
|
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
async function removeLease(path) {
|
|
219
|
-
|
|
221
|
+
let lastError;
|
|
222
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
223
|
+
try {
|
|
224
|
+
await rm(path, { force: true });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const code = error.code;
|
|
229
|
+
if (code !== "EPERM" && code !== "EBUSY")
|
|
230
|
+
throw error;
|
|
231
|
+
lastError = error;
|
|
232
|
+
if (process.platform === "win32" && attempt === 0) {
|
|
233
|
+
try {
|
|
234
|
+
const info = await lstat(path);
|
|
235
|
+
if (info.isFile() && !info.isSymbolicLink())
|
|
236
|
+
hardenPrivatePath(path);
|
|
237
|
+
}
|
|
238
|
+
catch (aclError) {
|
|
239
|
+
if (aclError.code === "ENOENT")
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 25 * (attempt + 1)));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw lastError;
|
|
220
247
|
}
|
|
221
248
|
function capabilityState(snapshot, id) {
|
|
222
249
|
const state = snapshot.capabilities.find((row) => row.id === id);
|
|
@@ -280,7 +307,7 @@ async function matchingLeases(input, options = {}) {
|
|
|
280
307
|
let entries;
|
|
281
308
|
try {
|
|
282
309
|
const info = await lstat(directory);
|
|
283
|
-
if (!info.isDirectory() || info.isSymbolicLink() || (
|
|
310
|
+
if (!info.isDirectory() || info.isSymbolicLink() || !privatePathIsSafe(directory))
|
|
284
311
|
return [];
|
|
285
312
|
entries = (await readdir(directory, { withFileTypes: true }))
|
|
286
313
|
.sort((left, right) => left.name.localeCompare(right.name))
|