@cairnkeep/cli 2.3.0 → 2.4.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 +78 -0
- package/README.md +221 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +1 -1
- package/docs/design/meta-agent.md +439 -0
- 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 +2 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/operating.md +531 -4
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +9 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +4 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +48 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +602 -3
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +251 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +37 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { isCapabilityContractEnabled, resolveCapabilityStatus, } from "./capability-config.js";
|
|
5
|
+
import { isTrajectoryCaptureEnabled } from "./trajectory-schema.js";
|
|
6
|
+
import { CAPABILITY_CALLBACK_SCHEMA_VERSION, appendCapabilityRecord, issueOperatingCapability, settleOperatingCapability, } from "./capability-store.js";
|
|
7
|
+
import { capabilityIdSchema, capabilitySourceSchema, } from "./capability-schema.js";
|
|
8
|
+
const MAX_TRACKED_OPERATING_INVOCATIONS = 10_000;
|
|
9
|
+
const correlationIdSchema = z.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(256)
|
|
12
|
+
.regex(/^(?:cairn:)?[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/)
|
|
13
|
+
.refine((value) => value !== "unknown");
|
|
14
|
+
const invocationIdSchema = z.string().regex(/^cap:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
15
|
+
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
16
|
+
export const capabilityAdapterClassificationSchema = z.strictObject({
|
|
17
|
+
harness: z.enum(["claude-code", "opencode", "pi", "other"]),
|
|
18
|
+
source: z.enum(["mcp", "notes-cli", "audit-timer", "operating-command", "operating-workflow"]),
|
|
19
|
+
transport: z.enum(["stdio", "http", "local-process", "harness-command"]),
|
|
20
|
+
});
|
|
21
|
+
export const operatingCapabilityHandleSchema = z.strictObject({
|
|
22
|
+
schema_version: z.literal(CAPABILITY_CALLBACK_SCHEMA_VERSION),
|
|
23
|
+
capability_id: capabilityIdSchema,
|
|
24
|
+
invocation_id: invocationIdSchema,
|
|
25
|
+
correlation_id: correlationIdSchema,
|
|
26
|
+
harness: capabilityAdapterClassificationSchema.shape.harness,
|
|
27
|
+
source: capabilityAdapterClassificationSchema.shape.source,
|
|
28
|
+
transport: capabilityAdapterClassificationSchema.shape.transport,
|
|
29
|
+
started_at: z.iso.datetime(),
|
|
30
|
+
state_source: capabilitySourceSchema,
|
|
31
|
+
configuration_digest: digestSchema,
|
|
32
|
+
});
|
|
33
|
+
export const operatingCapabilityDisabledResultSchema = z.strictObject({
|
|
34
|
+
schema_version: z.literal(CAPABILITY_CALLBACK_SCHEMA_VERSION),
|
|
35
|
+
capability_id: capabilityIdSchema,
|
|
36
|
+
disabled: z.literal(true),
|
|
37
|
+
measured: z.literal(false),
|
|
38
|
+
});
|
|
39
|
+
export const operatingCapabilityBypassResultSchema = z.strictObject({
|
|
40
|
+
schema_version: z.literal(CAPABILITY_CALLBACK_SCHEMA_VERSION),
|
|
41
|
+
capability_id: capabilityIdSchema,
|
|
42
|
+
disabled: z.literal(false),
|
|
43
|
+
measured: z.literal(false),
|
|
44
|
+
});
|
|
45
|
+
export const operatingCapabilityFinishInputSchema = z.strictObject({
|
|
46
|
+
outcome: z.enum(["success", "error", "timeout"]),
|
|
47
|
+
});
|
|
48
|
+
export const operatingCapabilityFinishResultSchema = z.strictObject({
|
|
49
|
+
schema_version: z.literal(CAPABILITY_CALLBACK_SCHEMA_VERSION),
|
|
50
|
+
invocation_id: invocationIdSchema,
|
|
51
|
+
finalized: z.boolean(),
|
|
52
|
+
});
|
|
53
|
+
let fallbackCorrelationId;
|
|
54
|
+
const operatingStarts = new Map();
|
|
55
|
+
function resolvedCapability(options) {
|
|
56
|
+
return options.snapshot.capabilities.find(({ id }) => id === options.capabilityId);
|
|
57
|
+
}
|
|
58
|
+
function hasLoggingConsent(options) {
|
|
59
|
+
return options.snapshot.contract_enabled
|
|
60
|
+
&& options.snapshot.logging.enabled
|
|
61
|
+
&& options.classification.transport !== "http"
|
|
62
|
+
&& isTrajectoryCaptureEnabled();
|
|
63
|
+
}
|
|
64
|
+
function isMeasuredCapabilityEnabled(options) {
|
|
65
|
+
return resolvedCapability(options)?.enabled === true;
|
|
66
|
+
}
|
|
67
|
+
function correlationId(explicit) {
|
|
68
|
+
const parsed = correlationIdSchema.safeParse(explicit);
|
|
69
|
+
if (parsed.success)
|
|
70
|
+
return parsed.data;
|
|
71
|
+
fallbackCorrelationId ??= `cairn:${randomUUID()}`;
|
|
72
|
+
return fallbackCorrelationId;
|
|
73
|
+
}
|
|
74
|
+
function invocationId() {
|
|
75
|
+
return `cap:${randomUUID()}`;
|
|
76
|
+
}
|
|
77
|
+
function rememberOperatingStart(id, monotonicStart) {
|
|
78
|
+
operatingStarts.set(id, monotonicStart);
|
|
79
|
+
while (operatingStarts.size > MAX_TRACKED_OPERATING_INVOCATIONS) {
|
|
80
|
+
const oldest = operatingStarts.keys().next().value;
|
|
81
|
+
if (!oldest)
|
|
82
|
+
break;
|
|
83
|
+
operatingStarts.delete(oldest);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function returnedOutcome(result) {
|
|
87
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
88
|
+
return { outcome: "success" };
|
|
89
|
+
const root = result;
|
|
90
|
+
const structured = root.structuredContent;
|
|
91
|
+
const value = structured && typeof structured === "object" && !Array.isArray(structured)
|
|
92
|
+
? structured
|
|
93
|
+
: root;
|
|
94
|
+
if (value.timedOut === true || value.timed_out === true) {
|
|
95
|
+
return { outcome: "timeout", errorCode: "result-timeout" };
|
|
96
|
+
}
|
|
97
|
+
if (value.ok === false)
|
|
98
|
+
return { outcome: "error", errorCode: "result-error" };
|
|
99
|
+
return { outcome: "success" };
|
|
100
|
+
}
|
|
101
|
+
function thrownOutcome(error) {
|
|
102
|
+
const name = error && typeof error === "object" && "name" in error
|
|
103
|
+
? error.name
|
|
104
|
+
: undefined;
|
|
105
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
106
|
+
return { outcome: "timeout", errorCode: "callback-timeout" };
|
|
107
|
+
}
|
|
108
|
+
return { outcome: "error", errorCode: "callback-error" };
|
|
109
|
+
}
|
|
110
|
+
function finalRecord(options, handle, finishedAt, durationMs, outcome, errorCode) {
|
|
111
|
+
return {
|
|
112
|
+
schema_version: CAPABILITY_CALLBACK_SCHEMA_VERSION,
|
|
113
|
+
capability_id: handle.capability_id,
|
|
114
|
+
invocation_id: handle.invocation_id,
|
|
115
|
+
correlation_id: handle.correlation_id,
|
|
116
|
+
harness: handle.harness,
|
|
117
|
+
source: handle.source,
|
|
118
|
+
transport: handle.transport,
|
|
119
|
+
started_at: handle.started_at,
|
|
120
|
+
finished_at: finishedAt,
|
|
121
|
+
duration_ms: durationMs,
|
|
122
|
+
outcome,
|
|
123
|
+
...(errorCode === undefined ? {} : { error_code: errorCode }),
|
|
124
|
+
state_source: handle.state_source,
|
|
125
|
+
configuration_digest: handle.configuration_digest,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function persistFinal(options, record) {
|
|
129
|
+
try {
|
|
130
|
+
await appendCapabilityRecord(options.projectRoot, record, {
|
|
131
|
+
...(options.testStoreFault === undefined ? {} : { testStoreFault: options.testStoreFault }),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Callback measurement is fail-open by contract.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function createHandle(options, startedAt) {
|
|
139
|
+
const classification = capabilityAdapterClassificationSchema.parse(options.classification);
|
|
140
|
+
const state = resolvedCapability(options);
|
|
141
|
+
if (!state)
|
|
142
|
+
throw new Error("Capability state is missing from the resolved snapshot.");
|
|
143
|
+
return operatingCapabilityHandleSchema.parse({
|
|
144
|
+
schema_version: CAPABILITY_CALLBACK_SCHEMA_VERSION,
|
|
145
|
+
capability_id: options.capabilityId,
|
|
146
|
+
invocation_id: invocationId(),
|
|
147
|
+
correlation_id: correlationId(options.correlationId),
|
|
148
|
+
...classification,
|
|
149
|
+
started_at: startedAt,
|
|
150
|
+
state_source: state.source,
|
|
151
|
+
configuration_digest: options.snapshot.configuration_digest,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
export function withCapability(options, callback) {
|
|
155
|
+
if (!options.snapshot.contract_enabled
|
|
156
|
+
|| !options.snapshot.logging.enabled
|
|
157
|
+
|| options.classification.transport === "http"
|
|
158
|
+
|| !isMeasuredCapabilityEnabled(options)) {
|
|
159
|
+
return callback;
|
|
160
|
+
}
|
|
161
|
+
return async function measuredCallback(...args) {
|
|
162
|
+
if (!isTrajectoryCaptureEnabled())
|
|
163
|
+
return callback.apply(this, args);
|
|
164
|
+
const monotonicStart = performance.now();
|
|
165
|
+
const startedAt = new Date().toISOString();
|
|
166
|
+
const handle = createHandle(options, startedAt);
|
|
167
|
+
try {
|
|
168
|
+
const result = await callback.apply(this, args);
|
|
169
|
+
const finishedAt = new Date().toISOString();
|
|
170
|
+
const durationMs = Math.max(0, performance.now() - monotonicStart);
|
|
171
|
+
const classified = returnedOutcome(result);
|
|
172
|
+
await persistFinal(options, finalRecord(options, handle, finishedAt, durationMs, classified.outcome, classified.errorCode));
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const finishedAt = new Date().toISOString();
|
|
177
|
+
const durationMs = Math.max(0, performance.now() - monotonicStart);
|
|
178
|
+
const classified = thrownOutcome(error);
|
|
179
|
+
await persistFinal(options, finalRecord(options, handle, finishedAt, durationMs, classified.outcome, classified.errorCode));
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export async function startOperatingCapability(options) {
|
|
185
|
+
const state = resolvedCapability(options);
|
|
186
|
+
const disabled = options.snapshot.contract_enabled && state?.enabled === false;
|
|
187
|
+
if (disabled) {
|
|
188
|
+
const result = operatingCapabilityDisabledResultSchema.parse({
|
|
189
|
+
schema_version: CAPABILITY_CALLBACK_SCHEMA_VERSION,
|
|
190
|
+
capability_id: options.capabilityId,
|
|
191
|
+
disabled: true,
|
|
192
|
+
measured: false,
|
|
193
|
+
});
|
|
194
|
+
if (!hasLoggingConsent(options))
|
|
195
|
+
return result;
|
|
196
|
+
const monotonicStart = performance.now();
|
|
197
|
+
const startedAt = new Date().toISOString();
|
|
198
|
+
const handle = createHandle(options, startedAt);
|
|
199
|
+
const finishedAt = new Date().toISOString();
|
|
200
|
+
await persistFinal(options, finalRecord(options, handle, finishedAt, Math.max(0, performance.now() - monotonicStart), "disabled", "capability-disabled"));
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
if (!hasLoggingConsent(options) || !isMeasuredCapabilityEnabled(options)) {
|
|
204
|
+
return operatingCapabilityBypassResultSchema.parse({
|
|
205
|
+
schema_version: CAPABILITY_CALLBACK_SCHEMA_VERSION,
|
|
206
|
+
capability_id: options.capabilityId,
|
|
207
|
+
disabled: false,
|
|
208
|
+
measured: false,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const monotonicStart = performance.now();
|
|
212
|
+
const handle = createHandle(options, new Date().toISOString());
|
|
213
|
+
const issued = await issueOperatingCapability(options.projectRoot, handle, {
|
|
214
|
+
...(options.testStoreFault === undefined ? {} : { testStoreFault: options.testStoreFault }),
|
|
215
|
+
});
|
|
216
|
+
if (!issued) {
|
|
217
|
+
return operatingCapabilityBypassResultSchema.parse({
|
|
218
|
+
schema_version: CAPABILITY_CALLBACK_SCHEMA_VERSION,
|
|
219
|
+
capability_id: options.capabilityId,
|
|
220
|
+
disabled: false,
|
|
221
|
+
measured: false,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
rememberOperatingStart(handle.invocation_id, monotonicStart);
|
|
225
|
+
return handle;
|
|
226
|
+
}
|
|
227
|
+
function operatingFinishResult(invocationIdValue, finalized) {
|
|
228
|
+
return operatingCapabilityFinishResultSchema.parse({
|
|
229
|
+
schema_version: CAPABILITY_CALLBACK_SCHEMA_VERSION,
|
|
230
|
+
invocation_id: invocationIdValue,
|
|
231
|
+
finalized,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
export async function finishOperatingCapability(projectRoot, rawHandle, rawFinish) {
|
|
235
|
+
const handle = operatingCapabilityHandleSchema.parse(rawHandle);
|
|
236
|
+
const finish = operatingCapabilityFinishInputSchema.parse(rawFinish);
|
|
237
|
+
try {
|
|
238
|
+
if (!isCapabilityContractEnabled()
|
|
239
|
+
|| !isTrajectoryCaptureEnabled()
|
|
240
|
+
|| handle.transport === "http") {
|
|
241
|
+
await settleOperatingCapability(projectRoot, handle);
|
|
242
|
+
operatingStarts.delete(handle.invocation_id);
|
|
243
|
+
return operatingFinishResult(handle.invocation_id, false);
|
|
244
|
+
}
|
|
245
|
+
const current = await resolveCapabilityStatus({ projectRoot });
|
|
246
|
+
const capability = current.capabilities.find(({ id }) => id === handle.capability_id);
|
|
247
|
+
const eligible = current.contract_enabled
|
|
248
|
+
&& current.logging.enabled
|
|
249
|
+
&& capability?.enabled === true
|
|
250
|
+
&& capability.source === handle.state_source
|
|
251
|
+
&& current.configuration_digest === handle.configuration_digest;
|
|
252
|
+
if (!eligible) {
|
|
253
|
+
await settleOperatingCapability(projectRoot, handle);
|
|
254
|
+
operatingStarts.delete(handle.invocation_id);
|
|
255
|
+
return operatingFinishResult(handle.invocation_id, false);
|
|
256
|
+
}
|
|
257
|
+
const finishedAt = new Date().toISOString();
|
|
258
|
+
const monotonicStart = operatingStarts.get(handle.invocation_id);
|
|
259
|
+
operatingStarts.delete(handle.invocation_id);
|
|
260
|
+
const durationMs = monotonicStart === undefined
|
|
261
|
+
? Math.max(0, Date.parse(finishedAt) - Date.parse(handle.started_at))
|
|
262
|
+
: Math.max(0, performance.now() - monotonicStart);
|
|
263
|
+
const errorCode = finish.outcome === "error"
|
|
264
|
+
? "callback-error"
|
|
265
|
+
: finish.outcome === "timeout"
|
|
266
|
+
? "callback-timeout"
|
|
267
|
+
: undefined;
|
|
268
|
+
const options = {
|
|
269
|
+
projectRoot,
|
|
270
|
+
snapshot: current,
|
|
271
|
+
capabilityId: handle.capability_id,
|
|
272
|
+
classification: {
|
|
273
|
+
harness: handle.harness,
|
|
274
|
+
source: handle.source,
|
|
275
|
+
transport: handle.transport,
|
|
276
|
+
},
|
|
277
|
+
correlationId: handle.correlation_id,
|
|
278
|
+
};
|
|
279
|
+
const finalized = await settleOperatingCapability(projectRoot, handle, finalRecord(options, handle, finishedAt, durationMs, finish.outcome, errorCode));
|
|
280
|
+
return operatingFinishResult(handle.invocation_id, finalized);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
operatingStarts.delete(handle.invocation_id);
|
|
284
|
+
return operatingFinishResult(handle.invocation_id, false);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { capabilityAdapterClassificationSchema, finishOperatingCapability, operatingCapabilityBypassResultSchema, operatingCapabilityHandleSchema, startOperatingCapability, } from "./capability-adapter.js";
|
|
5
|
+
import { readCapabilityConfig, resetCapabilityLogging, resetCapabilityOverride, resolveCapabilityStatus, setCapabilityLogging, setCapabilityOverride, } from "./capability-config.js";
|
|
6
|
+
import { CAPABILITY_CALLBACK_SCHEMA_VERSION, doctorCapabilityRecords, } from "./capability-store.js";
|
|
7
|
+
import { CAPABILITY_SCHEMA_VERSION, capabilityIdSchema, } from "./capability-schema.js";
|
|
8
|
+
import { beginHarnessCapability, finishHarnessCapability, harnessCapabilityBeforeInputSchema, harnessCapabilityTerminalInputSchema, observeHarnessCwdChanged, recoverHarnessCapabilities, } from "./capability-harness.js";
|
|
9
|
+
process.stdout.on("error", (error) => {
|
|
10
|
+
if (error.code === "EPIPE")
|
|
11
|
+
process.exit(0);
|
|
12
|
+
throw error;
|
|
13
|
+
});
|
|
14
|
+
const sessionSchemaPattern = /^(?:cairn:)?[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
|
|
15
|
+
const configName = ".ai/capabilities.json";
|
|
16
|
+
const callbackStoreName = ".agentfs/trajectory.db";
|
|
17
|
+
const MAX_HARNESS_INPUT_BYTES = 8 * 1024;
|
|
18
|
+
function usage() {
|
|
19
|
+
return `cairn capabilities — managed capability state
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
cairn capabilities list [--json]
|
|
23
|
+
cairn capabilities status [--json]
|
|
24
|
+
cairn capabilities enable <capability-id> [--json]
|
|
25
|
+
cairn capabilities disable <capability-id> [--json]
|
|
26
|
+
cairn capabilities reset <capability-id>|--all [--json]
|
|
27
|
+
cairn capabilities logging <enable|disable|reset> [--json]
|
|
28
|
+
|
|
29
|
+
MCP capability changes require a memory-server restart. Operating capability
|
|
30
|
+
changes apply on the next invocation.
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
function parseOutputArgs(args, positionalCount) {
|
|
34
|
+
const jsonCount = args.filter((arg) => arg === "--json").length;
|
|
35
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
36
|
+
if (jsonCount > 1 || positional.length !== positionalCount) {
|
|
37
|
+
throw new Error("invalid-arguments");
|
|
38
|
+
}
|
|
39
|
+
return { positional, mode: jsonCount === 1 ? "json" : "human" };
|
|
40
|
+
}
|
|
41
|
+
function requireCapabilityId(value) {
|
|
42
|
+
const parsed = capabilityIdSchema.safeParse(value);
|
|
43
|
+
if (!parsed.success)
|
|
44
|
+
throw new Error("invalid-capability-id");
|
|
45
|
+
return parsed.data;
|
|
46
|
+
}
|
|
47
|
+
function humanList(status) {
|
|
48
|
+
const heading = status.contract_enabled
|
|
49
|
+
? "Capability contract: enabled"
|
|
50
|
+
: "Capability contract: disabled (showing staged state; legacy behavior remains active)";
|
|
51
|
+
const rows = status.capabilities.map((row) => [
|
|
52
|
+
row.id,
|
|
53
|
+
row.kind,
|
|
54
|
+
row.enabled ? "enabled" : "disabled",
|
|
55
|
+
row.source,
|
|
56
|
+
row.restart_required ? "restart required" : "next invocation",
|
|
57
|
+
].join("\t"));
|
|
58
|
+
return [heading, ...rows].join("\n");
|
|
59
|
+
}
|
|
60
|
+
function humanStatus(status) {
|
|
61
|
+
const issues = status.issues.length === 0
|
|
62
|
+
? ["Issues: none"]
|
|
63
|
+
: ["Issues:", ...status.issues.map((issue) => {
|
|
64
|
+
const subject = issue.capability_id ?? issue.setting;
|
|
65
|
+
return ` ${issue.code}${subject === undefined ? "" : ` (${subject})`}`;
|
|
66
|
+
})];
|
|
67
|
+
return [
|
|
68
|
+
humanList(status),
|
|
69
|
+
`Logging callbacks: ${status.logging.enabled ? "enabled" : "disabled"} (${status.logging.source})`,
|
|
70
|
+
`Configuration digest: ${status.configuration_digest}`,
|
|
71
|
+
...issues,
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
function writeStatus(status, mode, compact) {
|
|
75
|
+
process.stdout.write(`${mode === "json" ? JSON.stringify(status) : compact ? humanList(status) : humanStatus(status)}\n`);
|
|
76
|
+
}
|
|
77
|
+
async function currentStatus() {
|
|
78
|
+
return resolveCapabilityStatus({ projectRoot: process.cwd() });
|
|
79
|
+
}
|
|
80
|
+
async function assertConfigCanMutate(allowInvalidReset) {
|
|
81
|
+
if (allowInvalidReset)
|
|
82
|
+
return;
|
|
83
|
+
const { issues } = await readCapabilityConfig({ projectRoot: process.cwd() });
|
|
84
|
+
if (issues.some(({ code }) => code === "invalid-config"))
|
|
85
|
+
throw new Error("invalid-config");
|
|
86
|
+
}
|
|
87
|
+
function parsePrivateStartArgs(args) {
|
|
88
|
+
const capabilityId = requireCapabilityId(args[0]);
|
|
89
|
+
const values = new Map();
|
|
90
|
+
for (let index = 1; index < args.length; index += 2) {
|
|
91
|
+
const name = args[index];
|
|
92
|
+
const value = args[index + 1];
|
|
93
|
+
if (!name || !value || !["--harness", "--source", "--transport", "--session"].includes(name)
|
|
94
|
+
|| values.has(name)) {
|
|
95
|
+
throw new Error("invalid-private-arguments");
|
|
96
|
+
}
|
|
97
|
+
values.set(name, value);
|
|
98
|
+
}
|
|
99
|
+
if (values.size < 3 || !values.has("--harness") || !values.has("--source") || !values.has("--transport")) {
|
|
100
|
+
throw new Error("invalid-private-arguments");
|
|
101
|
+
}
|
|
102
|
+
const classification = capabilityAdapterClassificationSchema.parse({
|
|
103
|
+
harness: values.get("--harness"),
|
|
104
|
+
source: values.get("--source"),
|
|
105
|
+
transport: values.get("--transport"),
|
|
106
|
+
});
|
|
107
|
+
const correlationId = values.get("--session");
|
|
108
|
+
if (correlationId !== undefined
|
|
109
|
+
&& (!sessionSchemaPattern.test(correlationId) || correlationId === "unknown")) {
|
|
110
|
+
throw new Error("invalid-session");
|
|
111
|
+
}
|
|
112
|
+
return { capabilityId, classification, ...(correlationId === undefined ? {} : { correlationId }) };
|
|
113
|
+
}
|
|
114
|
+
async function operatingOptions(args) {
|
|
115
|
+
const parsed = parsePrivateStartArgs(args);
|
|
116
|
+
return {
|
|
117
|
+
projectRoot: process.cwd(),
|
|
118
|
+
snapshot: await currentStatus(),
|
|
119
|
+
capabilityId: parsed.capabilityId,
|
|
120
|
+
classification: parsed.classification,
|
|
121
|
+
...(parsed.correlationId === undefined ? {} : { correlationId: parsed.correlationId }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function privateGuard(args) {
|
|
125
|
+
const options = await operatingOptions(args);
|
|
126
|
+
const state = options.snapshot.capabilities.find(({ id }) => id === options.capabilityId);
|
|
127
|
+
const result = options.snapshot.contract_enabled && state?.enabled === false
|
|
128
|
+
? await startOperatingCapability(options)
|
|
129
|
+
: operatingCapabilityBypassResultSchema.parse({
|
|
130
|
+
schema_version: CAPABILITY_CALLBACK_SCHEMA_VERSION,
|
|
131
|
+
capability_id: options.capabilityId,
|
|
132
|
+
disabled: false,
|
|
133
|
+
measured: false,
|
|
134
|
+
});
|
|
135
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
136
|
+
}
|
|
137
|
+
async function privateStart(args) {
|
|
138
|
+
const result = await startOperatingCapability(await operatingOptions(args));
|
|
139
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
140
|
+
}
|
|
141
|
+
async function privateFinish(args) {
|
|
142
|
+
if (args.length !== 4 || args[0] !== "--handle" || args[2] !== "--outcome") {
|
|
143
|
+
throw new Error("invalid-private-arguments");
|
|
144
|
+
}
|
|
145
|
+
const handle = operatingCapabilityHandleSchema.parse(JSON.parse(args[1]));
|
|
146
|
+
if (!["success", "error", "timeout"].includes(args[3])) {
|
|
147
|
+
throw new Error("invalid-outcome");
|
|
148
|
+
}
|
|
149
|
+
const result = await finishOperatingCapability(process.cwd(), handle, {
|
|
150
|
+
outcome: args[3],
|
|
151
|
+
});
|
|
152
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
153
|
+
}
|
|
154
|
+
async function readBoundedHarnessInput() {
|
|
155
|
+
const chunks = [];
|
|
156
|
+
let total = 0;
|
|
157
|
+
for await (const rawChunk of process.stdin) {
|
|
158
|
+
const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
|
|
159
|
+
total += chunk.byteLength;
|
|
160
|
+
if (total > MAX_HARNESS_INPUT_BYTES)
|
|
161
|
+
throw new Error("invalid-harness-input");
|
|
162
|
+
chunks.push(chunk);
|
|
163
|
+
}
|
|
164
|
+
if (total === 0)
|
|
165
|
+
throw new Error("invalid-harness-input");
|
|
166
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
167
|
+
}
|
|
168
|
+
async function privateHarnessBefore(args) {
|
|
169
|
+
if (args.length !== 0)
|
|
170
|
+
throw new Error("invalid-private-arguments");
|
|
171
|
+
let result = {
|
|
172
|
+
schema_version: 1,
|
|
173
|
+
decision: "allow",
|
|
174
|
+
};
|
|
175
|
+
try {
|
|
176
|
+
const input = harnessCapabilityBeforeInputSchema.parse(await readBoundedHarnessInput());
|
|
177
|
+
result = await beginHarnessCapability(input);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Native bridges validate their harness event before this fail-open local seam.
|
|
181
|
+
}
|
|
182
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
183
|
+
}
|
|
184
|
+
async function privateHarnessTerminal(args) {
|
|
185
|
+
if (args.length !== 0)
|
|
186
|
+
throw new Error("invalid-private-arguments");
|
|
187
|
+
let result = { schema_version: 1, finalized: false };
|
|
188
|
+
try {
|
|
189
|
+
const input = harnessCapabilityTerminalInputSchema.parse(await readBoundedHarnessInput());
|
|
190
|
+
result = await finishHarnessCapability(input);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Terminal failures cannot change owner results or disclose rejected input.
|
|
194
|
+
}
|
|
195
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
196
|
+
}
|
|
197
|
+
async function privateHarnessCwd(args) {
|
|
198
|
+
if (args.length !== 0)
|
|
199
|
+
throw new Error("invalid-private-arguments");
|
|
200
|
+
let result = { schema_version: 1, observed: false };
|
|
201
|
+
try {
|
|
202
|
+
const input = await readBoundedHarnessInput();
|
|
203
|
+
result = await observeHarnessCwdChanged(input);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// CWD observations never rebind identity and remain fail-open.
|
|
207
|
+
}
|
|
208
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
209
|
+
}
|
|
210
|
+
async function privateHarnessRecover(args) {
|
|
211
|
+
if (args.length !== 0)
|
|
212
|
+
throw new Error("invalid-private-arguments");
|
|
213
|
+
let result = { schema_version: 1, recovered: 0, pruned: 0, pending: 0 };
|
|
214
|
+
try {
|
|
215
|
+
result = await recoverHarnessCapabilities();
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Recovery is local, value-free, and fail-open.
|
|
219
|
+
}
|
|
220
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
221
|
+
}
|
|
222
|
+
async function configDoctor() {
|
|
223
|
+
const path = join(process.cwd(), configName);
|
|
224
|
+
let exists = false;
|
|
225
|
+
let unsafePermissions = false;
|
|
226
|
+
try {
|
|
227
|
+
const info = await lstat(path);
|
|
228
|
+
exists = true;
|
|
229
|
+
unsafePermissions = info.isSymbolicLink() || !info.isFile() || (info.mode & 0o077) !== 0;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code !== "ENOENT")
|
|
233
|
+
unsafePermissions = true;
|
|
234
|
+
}
|
|
235
|
+
const { issues: configIssues } = await readCapabilityConfig({ projectRoot: process.cwd() });
|
|
236
|
+
const issues = [
|
|
237
|
+
...configIssues,
|
|
238
|
+
...(unsafePermissions ? [{ code: "unsafe-permissions" }] : []),
|
|
239
|
+
];
|
|
240
|
+
const state = issues.some(({ code }) => code === "invalid-config" || code === "unsafe-permissions")
|
|
241
|
+
? "FAIL"
|
|
242
|
+
: issues.length > 0 ? "WARN" : "PASS";
|
|
243
|
+
return { name: configName, state, exists, issues };
|
|
244
|
+
}
|
|
245
|
+
async function callbackDoctor() {
|
|
246
|
+
try {
|
|
247
|
+
const result = await doctorCapabilityRecords(process.cwd());
|
|
248
|
+
return {
|
|
249
|
+
name: callbackStoreName,
|
|
250
|
+
state: result.ok ? "PASS" : "FAIL",
|
|
251
|
+
exists: result.exists,
|
|
252
|
+
issues: result.issues,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return {
|
|
257
|
+
name: callbackStoreName,
|
|
258
|
+
state: "FAIL",
|
|
259
|
+
exists: true,
|
|
260
|
+
issues: ["diagnostic-failed"],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function privateDoctor(args) {
|
|
265
|
+
if (args.length !== 1 || args[0] !== "--json")
|
|
266
|
+
throw new Error("invalid-private-arguments");
|
|
267
|
+
const [configuration, callbacks] = await Promise.all([configDoctor(), callbackDoctor()]);
|
|
268
|
+
const value = {
|
|
269
|
+
schema_version: CAPABILITY_SCHEMA_VERSION,
|
|
270
|
+
ok: configuration.state !== "FAIL" && callbacks.state !== "FAIL",
|
|
271
|
+
configuration,
|
|
272
|
+
callbacks,
|
|
273
|
+
};
|
|
274
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
275
|
+
if (!value.ok)
|
|
276
|
+
process.exitCode = 2;
|
|
277
|
+
}
|
|
278
|
+
async function main() {
|
|
279
|
+
const [command = "help", ...args] = process.argv.slice(2);
|
|
280
|
+
try {
|
|
281
|
+
if (command === "list" || command === "status") {
|
|
282
|
+
const { mode } = parseOutputArgs(args, 0);
|
|
283
|
+
writeStatus(await currentStatus(), mode, command === "list");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (command === "enable" || command === "disable") {
|
|
287
|
+
const { positional, mode } = parseOutputArgs(args, 1);
|
|
288
|
+
const id = requireCapabilityId(positional[0]);
|
|
289
|
+
await assertConfigCanMutate(false);
|
|
290
|
+
await setCapabilityOverride({ projectRoot: process.cwd(), id, enabled: command === "enable" });
|
|
291
|
+
writeStatus(await currentStatus(), mode, false);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (command === "reset") {
|
|
295
|
+
const { positional, mode } = parseOutputArgs(args, 1);
|
|
296
|
+
const id = positional[0] === "--all" ? "all" : requireCapabilityId(positional[0]);
|
|
297
|
+
await assertConfigCanMutate(id === "all");
|
|
298
|
+
await resetCapabilityOverride({ projectRoot: process.cwd(), id });
|
|
299
|
+
writeStatus(await currentStatus(), mode, false);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (command === "logging") {
|
|
303
|
+
const { positional, mode } = parseOutputArgs(args, 1);
|
|
304
|
+
const operation = positional[0];
|
|
305
|
+
if (!operation || !["enable", "disable", "reset"].includes(operation)) {
|
|
306
|
+
throw new Error("invalid-logging-operation");
|
|
307
|
+
}
|
|
308
|
+
await assertConfigCanMutate(false);
|
|
309
|
+
if (operation === "reset")
|
|
310
|
+
await resetCapabilityLogging({ projectRoot: process.cwd() });
|
|
311
|
+
else
|
|
312
|
+
await setCapabilityLogging({ projectRoot: process.cwd(), enabled: operation === "enable" });
|
|
313
|
+
writeStatus(await currentStatus(), mode, false);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (command === "guard") {
|
|
317
|
+
await privateGuard(args);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (command === "start") {
|
|
321
|
+
await privateStart(args);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (command === "finish") {
|
|
325
|
+
await privateFinish(args);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (command === "doctor") {
|
|
329
|
+
await privateDoctor(args);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (command === "harness-before") {
|
|
333
|
+
await privateHarnessBefore(args);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (command === "harness-terminal") {
|
|
337
|
+
await privateHarnessTerminal(args);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (command === "harness-cwd") {
|
|
341
|
+
await privateHarnessCwd(args);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (command === "harness-recover" || command === "harness-prune") {
|
|
345
|
+
await privateHarnessRecover(args);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
349
|
+
if (args.length !== 0)
|
|
350
|
+
throw new Error("invalid-arguments");
|
|
351
|
+
process.stdout.write(usage());
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
throw new Error("unknown-command");
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
process.stderr.write("cairn capabilities: invalid command, arguments, or managed state.\n");
|
|
358
|
+
process.exitCode = 2;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
await main();
|