@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.
- package/CHANGELOG.md +68 -1
- package/README.md +82 -27
- package/bin/cairn +6 -153
- package/bin/cairn-container +8 -0
- package/docs/building-an-overlay.md +41 -4
- package/docs/containers.md +10 -0
- package/docs/context-packs.md +94 -0
- package/docs/harness-compatibility.md +37 -3
- package/docs/learning/CURRICULUM-MAP.md +16 -5
- package/docs/learning/FEATURE-GUIDE.md +15 -4
- package/docs/learning/README.md +8 -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 +11 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L19-validated-skill-improvement.md +52 -21
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +67 -0
- package/docs/learning/lessons/L21-context-packs.md +79 -0
- package/docs/learning/lessons/L22-native-windows.md +83 -0
- package/docs/learning/lessons/L23-guided-setup.md +138 -0
- package/docs/learning/tracks/operator.md +10 -3
- package/docs/learning/tracks/quickstart.md +7 -3
- package/docs/mcp-tool-profiles.md +43 -0
- package/docs/native-windows.md +122 -0
- package/docs/operating.md +157 -18
- package/docs/privacy-and-data-flow.md +51 -1
- package/docs/storage.md +29 -0
- 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-cli.js +185 -0
- package/mcp-memory-server/dist/context-pack.js +889 -0
- 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 +8 -5
- package/mcp-memory-server/dist/eval-runner.js +2 -0
- package/mcp-memory-server/dist/eval-schema.js +1 -0
- package/mcp-memory-server/dist/graph-cli.js +7 -1
- package/mcp-memory-server/dist/index.js +87 -37
- package/mcp-memory-server/dist/mcp-tool-catalog.js +47 -0
- package/mcp-memory-server/dist/mcp-tool-cli.js +85 -0
- package/mcp-memory-server/dist/mcp-tool-profile.js +164 -0
- 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 +4 -1
- package/package.json +5 -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/schemas/context-pack.schema.json +33 -0
- package/schemas/eval-report.schema.json +1 -0
- 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 +33 -5
- package/scripts/doctor.sh +96 -0
- package/scripts/fix-permissions.mjs +12 -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 +310 -0
- package/scripts/setup-reconcile.mjs +331 -0
- package/scripts/setup.mjs +306 -0
- package/scripts/sync-pi-assets.sh +19 -4
- package/scripts/test-cli-dispatch.sh +6 -0
- package/scripts/test-completion.sh +44 -0
- package/scripts/test-doctor.sh +66 -0
- package/scripts/test-learning-docs.sh +53 -3
- package/scripts/test-package-install.sh +81 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
- package/scripts/test-phase18-capability-lifecycle.sh +1 -1
- package/scripts/test-pi-lifecycle.mjs +190 -0
- package/scripts/test-setup-compatibility.mjs +186 -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 +71 -0
- package/scripts/test-windows-native.mjs +136 -0
- package/scripts/uninstall.sh +95 -7
- package/scripts/verify-pi-mcp-bridge.mjs +588 -0
- package/scripts/windows-platform.mjs +725 -0
- 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
|
-
|
|
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.
|
|
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 {
|
|
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)
|