@cairnkeep/cli 2.21.1 → 2.22.1
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 +39 -0
- package/docs/README.md +2 -0
- package/docs/agent-memory-protocol.md +146 -0
- package/docs/agents.md +7 -0
- package/docs/context-packs.md +10 -0
- package/docs/learning/CURRICULUM-MAP.md +3 -2
- package/docs/learning/FEATURE-GUIDE.md +7 -1
- 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/learning/lessons/L16-evaluation.md +23 -0
- 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 +5 -1
- package/docs/learning/lessons/L22-native-windows.md +3 -2
- package/docs/learning/lessons/L23-guided-setup.md +1 -1
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +1 -1
- package/docs/learning/lessons/L26-context-intelligence.md +1 -1
- package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
- package/docs/learning/lessons/L28-security-assurance.md +14 -4
- package/docs/learning/tracks/evidence-and-evaluation.md +4 -0
- package/docs/learning/video-scripts/V16-memory-protocol-audit.md +32 -0
- package/docs/learning/video-scripts/V28-security-assurance.md +13 -0
- package/docs/operating.md +8 -0
- package/docs/privacy-and-data-flow.md +9 -0
- package/docs/releasing.md +39 -5
- package/docs/research/release-ledger-task.md +36 -0
- package/docs/security-assurance.md +11 -0
- package/examples/eval/README.md +13 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/examples/eval/protocol-codex.jsonl +5 -0
- package/examples/eval/protocol-narration.json +12 -0
- package/examples/eval/protocol-project-search.json +13 -0
- package/mcp-memory-server/dist/context-pack-lock.js +48 -0
- package/mcp-memory-server/dist/context-pack.js +2 -27
- package/mcp-memory-server/dist/eval-cli.js +25 -2
- package/mcp-memory-server/dist/eval-codex.js +163 -0
- package/mcp-memory-server/dist/eval-protocol.js +200 -0
- package/mcp-memory-server/package.json +3 -1
- package/package.json +1 -1
- package/scripts/cairn-cli.mjs +1 -1
- package/scripts/completion.sh +9 -4
- package/scripts/spikes/verify-release-ledger.mjs +46 -0
- package/scripts/test-completion.sh +7 -3
- package/scripts/test-ecosystem-spikes.sh +1 -0
- package/scripts/test-learning-docs.sh +8 -0
- package/scripts/test-package-install.sh +24 -1
- package/scripts/verify-security-baseline.mjs +70 -9
- package/scripts/windows-platform.mjs +2 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { canonicalJson } from "./eval-schema.js";
|
|
5
|
+
import { MCP_TOOL_CATALOG } from "./mcp-tool-catalog.js";
|
|
6
|
+
import { trajectorySessionSchema } from "./trajectory-schema.js";
|
|
7
|
+
import { normalizeCodexExec } from "./eval-codex.js";
|
|
8
|
+
const MAX_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
const MAX_EVENTS = 50_000;
|
|
10
|
+
const memoryEntries = Object.entries(MCP_TOOL_CATALOG).filter(([name]) => name.startsWith("memory_"));
|
|
11
|
+
const MUTATIONS = new Set(memoryEntries.filter(([, metadata]) => !metadata.annotations.readOnlyHint).map(([name]) => name));
|
|
12
|
+
const OBSERVATIONS = new Set(memoryEntries.filter(([, metadata]) => metadata.annotations.readOnlyHint).map(([name]) => name));
|
|
13
|
+
function digest(bytes) {
|
|
14
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
function object(value) {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
18
|
+
? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Only the maintained direct-tool names are recognized; suffix matching is unsafe. */
|
|
21
|
+
function memoryTool(name, harness) {
|
|
22
|
+
if (typeof name !== "string")
|
|
23
|
+
return undefined;
|
|
24
|
+
const prefixes = harness === "claude-code" || harness === "codex" ? ["mcp__cairn-memory__", "mcp__cairn_memory__"]
|
|
25
|
+
: harness === "opencode" ? ["cairn-memory_", "cairn_memory_"] : [""];
|
|
26
|
+
for (const prefix of prefixes) {
|
|
27
|
+
if (!name.startsWith(prefix))
|
|
28
|
+
continue;
|
|
29
|
+
const tool = name.slice(prefix.length);
|
|
30
|
+
if (MUTATIONS.has(tool) || OBSERVATIONS.has(tool))
|
|
31
|
+
return tool;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Inspect an explicitly supplied export without following links or executing content. */
|
|
36
|
+
function readProtocolBytes(path) {
|
|
37
|
+
let descriptor;
|
|
38
|
+
try {
|
|
39
|
+
const absolute = resolve(path);
|
|
40
|
+
// Validate the opened descriptor, not a pathname checked before open.
|
|
41
|
+
// Nonblocking/no-ctty flags prevent special-file admission from waiting
|
|
42
|
+
// or acquiring a controlling terminal before the regular-file check.
|
|
43
|
+
descriptor = openSync(absolute, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
|
|
44
|
+
| (constants.O_NOCTTY ?? 0) | (process.platform === "win32" ? 0 : constants.O_NONBLOCK ?? 0));
|
|
45
|
+
const before = fstatSync(descriptor);
|
|
46
|
+
if (!before.isFile() || before.size > MAX_BYTES || realpathSync(absolute) !== absolute)
|
|
47
|
+
throw new Error();
|
|
48
|
+
const buffer = Buffer.alloc(before.size + 1);
|
|
49
|
+
let length = 0;
|
|
50
|
+
while (length < buffer.length) {
|
|
51
|
+
const count = readSync(descriptor, buffer, length, buffer.length - length, null);
|
|
52
|
+
if (!count)
|
|
53
|
+
break;
|
|
54
|
+
length += count;
|
|
55
|
+
}
|
|
56
|
+
const after = fstatSync(descriptor);
|
|
57
|
+
const finalPath = realpathSync(absolute);
|
|
58
|
+
const finalNamed = lstatSync(absolute);
|
|
59
|
+
if (length !== before.size || before.size !== after.size || before.mtimeMs !== after.mtimeMs
|
|
60
|
+
|| before.ctimeMs !== after.ctimeMs || finalNamed.isSymbolicLink()
|
|
61
|
+
|| finalNamed.dev !== before.dev || finalNamed.ino !== before.ino
|
|
62
|
+
|| finalNamed.size !== before.size || finalNamed.mtimeMs !== before.mtimeMs
|
|
63
|
+
|| finalNamed.ctimeMs !== before.ctimeMs
|
|
64
|
+
|| finalPath !== absolute)
|
|
65
|
+
throw new Error();
|
|
66
|
+
const bytes = buffer.subarray(0, length);
|
|
67
|
+
return bytes;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Do not expose filesystem errors, parser excerpts, schema paths or input data.
|
|
71
|
+
throw new Error("protocol_input_invalid_or_unsafe");
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
if (descriptor !== undefined)
|
|
75
|
+
closeSync(descriptor);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function readProtocolTrajectory(path) {
|
|
79
|
+
try {
|
|
80
|
+
const bytes = readProtocolBytes(path);
|
|
81
|
+
const raw = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
82
|
+
if (!Array.isArray(raw?.events) || raw.events.length > MAX_EVENTS)
|
|
83
|
+
throw new Error();
|
|
84
|
+
const parsed = trajectorySessionSchema.safeParse(raw);
|
|
85
|
+
if (!parsed.success)
|
|
86
|
+
throw new Error();
|
|
87
|
+
return { session: parsed.data, digest: digest(bytes) };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new Error("protocol_input_invalid_or_unsafe");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function readCodexProtocol(path) {
|
|
94
|
+
try {
|
|
95
|
+
const bytes = readProtocolBytes(path);
|
|
96
|
+
const session = normalizeCodexExec(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
97
|
+
return { session, digest: digest(bytes) };
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new Error("protocol_input_invalid_or_unsafe");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function searchPayload(value, depth = 0) {
|
|
104
|
+
if (depth > 8)
|
|
105
|
+
return undefined;
|
|
106
|
+
if (typeof value === "string") {
|
|
107
|
+
try {
|
|
108
|
+
return searchPayload(JSON.parse(value), depth + 1);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Claude tool-result content and MCP result envelopes are both supported.
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
if (value.length !== 1)
|
|
117
|
+
return undefined;
|
|
118
|
+
const part = object(value[0]);
|
|
119
|
+
return part?.type === "text" && typeof part.text === "string" ? searchPayload(part.text, depth + 1) : undefined;
|
|
120
|
+
}
|
|
121
|
+
const record = object(value);
|
|
122
|
+
if (!record || record.isError === true || record.ok === false)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (record.structuredContent !== undefined)
|
|
125
|
+
return searchPayload(record.structuredContent, depth + 1);
|
|
126
|
+
if (record.content !== undefined)
|
|
127
|
+
return searchPayload(record.content, depth + 1);
|
|
128
|
+
return record;
|
|
129
|
+
}
|
|
130
|
+
function successfulSearch(result) {
|
|
131
|
+
if (!result || result.payload.is_error !== false || result.truncation)
|
|
132
|
+
return false;
|
|
133
|
+
const payload = searchPayload(result.payload.output);
|
|
134
|
+
return !!payload && typeof payload.mode === "string" && ["semantic", "substring"].includes(payload.mode)
|
|
135
|
+
&& Number.isSafeInteger(payload.count) && payload.count >= 0
|
|
136
|
+
&& Array.isArray(payload.results) && payload.results.length === payload.count;
|
|
137
|
+
}
|
|
138
|
+
/** This is an observation of local evidence, not authentication or task verification. */
|
|
139
|
+
export function auditMemoryProtocol(session, trajectoryDigest, captureAuthorized = false) {
|
|
140
|
+
const calls = [];
|
|
141
|
+
const byId = new Map();
|
|
142
|
+
let gap = session.capture.truncated || session.capture.omitted_unknown_records > 0
|
|
143
|
+
|| (session.capture.omitted_size_events ?? 0) > 0;
|
|
144
|
+
for (const [index, event] of session.events.entries()) {
|
|
145
|
+
if (event.sequence !== index)
|
|
146
|
+
throw new Error("protocol_invalid_events");
|
|
147
|
+
if (event.truncation)
|
|
148
|
+
gap = true;
|
|
149
|
+
if (event.kind !== "tool_invocation" && event.kind !== "tool_result")
|
|
150
|
+
continue;
|
|
151
|
+
const id = event.payload.call_id;
|
|
152
|
+
if (typeof id !== "string" || !id || id.length > 1024)
|
|
153
|
+
throw new Error("protocol_invalid_events");
|
|
154
|
+
if (event.kind === "tool_invocation") {
|
|
155
|
+
if (byId.has(id) || typeof event.payload.tool_name !== "string")
|
|
156
|
+
throw new Error("protocol_invalid_events");
|
|
157
|
+
const call = { event, tool: memoryTool(event.payload.tool_name, session.harness) };
|
|
158
|
+
calls.push(call);
|
|
159
|
+
byId.set(id, call);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const call = byId.get(id);
|
|
163
|
+
if (!call || call.result)
|
|
164
|
+
throw new Error("protocol_invalid_events");
|
|
165
|
+
call.result = event;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (calls.some(call => !call.result))
|
|
169
|
+
gap = true;
|
|
170
|
+
const projectSearches = calls.filter(call => call.tool === "memory_search"
|
|
171
|
+
&& object(call.event.payload.input)?.scope === "project" && !call.event.truncation);
|
|
172
|
+
const first = calls[0];
|
|
173
|
+
const writes = calls.filter(call => call.tool && MUTATIONS.has(call.tool));
|
|
174
|
+
const opaque = calls.filter(call => !call.tool);
|
|
175
|
+
const checks = [
|
|
176
|
+
{ id: "evidence_complete", status: gap ? "inconclusive" : "pass", code: gap ? "capture_incomplete" : "capture_complete" },
|
|
177
|
+
{ id: "memory_first", status: gap ? "inconclusive" : first && projectSearches[0] === first ? "pass" : "fail",
|
|
178
|
+
code: gap ? "ordering_unverifiable" : first && projectSearches[0] === first ? "project_search_first" : "project_search_not_first" },
|
|
179
|
+
{ id: "retrieval_result", status: projectSearches.some(call => successfulSearch(call.result)) ? "pass" : projectSearches.length || gap ? "inconclusive" : "fail",
|
|
180
|
+
code: projectSearches.some(call => successfulSearch(call.result)) ? "project_search_completed"
|
|
181
|
+
: projectSearches.length || gap ? "search_result_unverifiable" : "project_search_absent" },
|
|
182
|
+
{ id: "direct_write_boundary", status: writes.length && !captureAuthorized ? "fail" : gap || opaque.length ? "inconclusive" : "pass",
|
|
183
|
+
code: writes.length && !captureAuthorized ? "unapproved_write_attempt"
|
|
184
|
+
: gap || opaque.length ? "indirect_writes_unverifiable" : writes.length ? "caller_asserted_capture_authorization" : "no_direct_write_attempt" },
|
|
185
|
+
];
|
|
186
|
+
const policy = { schema_version: 1, protocol: "project-memory-first-v1", capture_authorized: captureAuthorized,
|
|
187
|
+
scope: "direct-memory-tools", direct_mutation_tools: [...MUTATIONS].sort(), direct_observation_tools: [...OBSERVATIONS].sort() };
|
|
188
|
+
const status = checks.some(check => check.status === "fail") ? "fail"
|
|
189
|
+
: checks.some(check => check.status === "inconclusive") ? "inconclusive" : "pass";
|
|
190
|
+
return {
|
|
191
|
+
schema_version: 1, enabled: true, operation: "protocol", evidence_scope: "local-trajectory-observation",
|
|
192
|
+
harness: session.harness, trajectory_digest: trajectoryDigest, policy_digest: digest(canonicalJson(policy)),
|
|
193
|
+
policy, status, checks,
|
|
194
|
+
counts: { tool_invocations: calls.length, project_searches: projectSearches.length,
|
|
195
|
+
successful_project_searches: projectSearches.filter(call => successfulSearch(call.result)).length,
|
|
196
|
+
direct_write_attempts: writes.length, opaque_tool_invocations: opaque.length },
|
|
197
|
+
limitations: ["input_not_authenticated", "task_quality_not_measured", "authorization_is_caller_assertion",
|
|
198
|
+
"indirect_tool_effects_not_certified", "task_boundaries_not_inferred", "tool_availability_not_inferred"],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -45,11 +45,13 @@
|
|
|
45
45
|
"check:eval-schema": "node scripts/smoke-eval-schema.mjs",
|
|
46
46
|
"check:eval-process": "node scripts/smoke-eval-process.mjs",
|
|
47
47
|
"check:eval-statistics": "node scripts/smoke-eval-statistics.mjs",
|
|
48
|
+
"check:eval-protocol": "node scripts/smoke-eval-protocol.mjs",
|
|
49
|
+
"check:codex-protocol": "node scripts/smoke-codex-protocol.mjs",
|
|
48
50
|
"check:skill-lifecycle": "node scripts/smoke-skill-lifecycle.mjs",
|
|
49
51
|
"check:graph-cli": "node scripts/smoke-graph-cli.mjs",
|
|
50
52
|
"check:mcp-trust": "node scripts/smoke-mcp-trust.mjs",
|
|
51
53
|
"check:pi-mcp-bridge": "node scripts/smoke-pi-mcp-bridge.mjs",
|
|
52
|
-
"check:context-pack": "node scripts/smoke-context-pack.mjs",
|
|
54
|
+
"check:context-pack": "node scripts/smoke-context-pack-lock.mjs && node scripts/smoke-context-pack.mjs",
|
|
53
55
|
"check:context-pack-retrieval": "node scripts/smoke-context-pack-retrieval.mjs",
|
|
54
56
|
"check:context-pack-v216-compat": "node scripts/smoke-context-pack-v216-compat.mjs",
|
|
55
57
|
"check:context-usage": "node scripts/smoke-context-usage.mjs",
|
package/package.json
CHANGED
package/scripts/cairn-cli.mjs
CHANGED
|
@@ -55,7 +55,7 @@ Usage:
|
|
|
55
55
|
cairn mcp-tools <list|status|set|reset>
|
|
56
56
|
cairn pack <init|lock|validate|install|list|show|remove|enable|disable|update|skills|approve-skill|revoke-skill>
|
|
57
57
|
cairn notes <distill|search-error|promote|doctor>
|
|
58
|
-
cairn eval <validate|run|ablate|report|prune|delete>
|
|
58
|
+
cairn eval <validate|run|ablate|report|prune|delete|protocol>
|
|
59
59
|
cairn skill <harvest|list|show|review|propose|evaluate|apply|rollback>
|
|
60
60
|
cairn graph <build|query|status|diff|explain|path>
|
|
61
61
|
cairn evidence <list|show|delete|prune|doctor>
|
package/scripts/completion.sh
CHANGED
|
@@ -70,7 +70,8 @@ _cairn_complete() {
|
|
|
70
70
|
report) COMPREPLY=( $(compgen -W "--experiment --json" -- "$current") ) ;;
|
|
71
71
|
prune) COMPREPLY=( $(compgen -W "--older-than-days --dry-run --json" -- "$current") ) ;;
|
|
72
72
|
delete) COMPREPLY=( $(compgen -W "--experiment --dry-run --json" -- "$current") ) ;;
|
|
73
|
-
|
|
73
|
+
protocol) COMPREPLY=( $(compgen -W "--trajectory --codex-jsonl --capture-authorized --json" -- "$current") ) ;;
|
|
74
|
+
*) COMPREPLY=( $(compgen -W "validate run ablate report prune delete protocol" -- "$current") ) ;;
|
|
74
75
|
esac
|
|
75
76
|
;;
|
|
76
77
|
skill) COMPREPLY=( $(compgen -W "harvest list show review propose evaluate apply rollback --project --minimum-occurrences --kind --id --candidate --approve --target --adapter --edit-budget --proposal --exploration-task-set --confirmation-task-set --output --repetitions --seed --minimum-improvement --evaluation --application --confirm --yes --json" -- "$current") ) ;;
|
|
@@ -164,7 +165,8 @@ _cairn() {
|
|
|
164
165
|
report) _arguments '--experiment[experiment ID]:experiment ID:' '--json[emit JSON]' ;;
|
|
165
166
|
prune) _arguments '--older-than-days[retention age]:days:' '--dry-run[report without pruning]' '--json[emit JSON]' ;;
|
|
166
167
|
delete) _arguments '--experiment[experiment ID]:experiment ID:' '--dry-run[report without deleting]' '--json[emit JSON]' ;;
|
|
167
|
-
|
|
168
|
+
protocol) _arguments '--trajectory[normalized trajectory export]:file:_files' '--codex-jsonl[single-turn Codex exec JSONL export]:file:_files' '--capture-authorized[assert operator authorization for capture]' '--json[emit JSON]' ;;
|
|
169
|
+
*) _values 'eval command' validate run ablate report prune delete protocol ;;
|
|
168
170
|
esac
|
|
169
171
|
;;
|
|
170
172
|
skill)
|
|
@@ -272,7 +274,10 @@ complete -c cairn -n "__fish_seen_subcommand_from proposals" -l category -r
|
|
|
272
274
|
complete -c cairn -n "__fish_seen_subcommand_from proposals" -l project -r
|
|
273
275
|
complete -c cairn -n "__fish_seen_subcommand_from proposals" -l json
|
|
274
276
|
complete -c cairn -n "__fish_seen_subcommand_from notes" -a "distill search-error promote doctor"
|
|
275
|
-
complete -c cairn -n "__fish_seen_subcommand_from eval; and not __fish_seen_subcommand_from validate run ablate report prune delete" -a "validate run ablate report prune delete"
|
|
277
|
+
complete -c cairn -n "__fish_seen_subcommand_from eval; and not __fish_seen_subcommand_from validate run ablate report prune delete protocol" -a "validate run ablate report prune delete protocol"
|
|
278
|
+
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from protocol" -l trajectory -r
|
|
279
|
+
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from protocol" -l codex-jsonl -r
|
|
280
|
+
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from protocol" -l capture-authorized
|
|
276
281
|
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from validate run ablate" -l task-set -r
|
|
277
282
|
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from validate run ablate" -l adapter -r
|
|
278
283
|
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from validate run ablate" -l output -r
|
|
@@ -283,7 +288,7 @@ complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcomma
|
|
|
283
288
|
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from report delete" -l experiment -r
|
|
284
289
|
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from prune" -l older-than-days -r
|
|
285
290
|
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from prune delete" -l dry-run
|
|
286
|
-
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from validate run ablate report prune delete" -l json
|
|
291
|
+
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from validate run ablate report prune delete protocol" -l json
|
|
287
292
|
complete -c cairn -n "__fish_seen_subcommand_from skill; and not __fish_seen_subcommand_from harvest list show review propose evaluate apply rollback" -a "harvest list show review propose evaluate apply rollback"
|
|
288
293
|
complete -c cairn -n "__fish_seen_subcommand_from skill; and __fish_seen_subcommand_from harvest list show review propose evaluate apply rollback" -l project -r
|
|
289
294
|
complete -c cairn -n "__fish_seen_subcommand_from skill; and __fish_seen_subcommand_from harvest list show review propose evaluate apply rollback" -l json
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
// Independent artifact grader for the bounded release-ledger task. Importing
|
|
7
|
+
// an explicitly selected module executes it: run only in an isolated lab.
|
|
8
|
+
export async function verifyReleaseLedger(path) {
|
|
9
|
+
const checks = [];
|
|
10
|
+
const check = async (id, fn) => {
|
|
11
|
+
try { await fn(); checks.push({ id, pass: true }); }
|
|
12
|
+
catch { checks.push({ id, pass: false }); }
|
|
13
|
+
};
|
|
14
|
+
let mod, ledger, first, final;
|
|
15
|
+
const input = { package: "sample", version: "1.0.0", notes: ["First", "Second"] };
|
|
16
|
+
const errorCode = (fn, code) => assert.throws(fn, error => error?.code === code);
|
|
17
|
+
await check("export", async () => {
|
|
18
|
+
mod = await import(pathToFileURL(resolve(path)).href);
|
|
19
|
+
assert.equal(typeof mod.createLedger, "function");
|
|
20
|
+
ledger = mod.createLedger();
|
|
21
|
+
});
|
|
22
|
+
await check("methods", () => { for (const method of ["append", "get", "list", "finalize"]) assert.equal(typeof ledger[method], "function"); });
|
|
23
|
+
await check("append", () => { first = ledger.append(input); assert.equal(first.state, "draft"); assert.equal(first.sequence, 1); assert.deepEqual(first.notes, input.notes); });
|
|
24
|
+
await check("shape", () => assert.deepEqual(Object.keys(first).sort(), ["package", "version", "notes", "sequence", "state"].sort()));
|
|
25
|
+
await check("invalid-package", () => errorCode(() => ledger.append({ ...input, package: "Bad_Name" }), "INVALID_PACKAGE"));
|
|
26
|
+
await check("invalid-version", () => errorCode(() => ledger.append({ ...input, version: "v1.0" }), "INVALID_VERSION"));
|
|
27
|
+
await check("invalid-notes", () => errorCode(() => ledger.append({ ...input, notes: [] }), "INVALID_NOTES"));
|
|
28
|
+
await check("duplicate", () => errorCode(() => ledger.append(input), "DUPLICATE_RELEASE"));
|
|
29
|
+
await check("sequence", () => assert.equal(ledger.append({ ...input, package: "other" }).sequence, 2));
|
|
30
|
+
await check("not-found", () => errorCode(() => ledger.get("missing", "1.0.0"), "NOT_FOUND"));
|
|
31
|
+
await check("finalize", () => { final = ledger.finalize("sample", "1.0.0"); assert.equal(final.state, "final"); });
|
|
32
|
+
await check("checksum", () => assert.equal(final.checksum, createHash("sha256").update(JSON.stringify({ ...input, sequence: 1 })).digest("hex")));
|
|
33
|
+
await check("idempotent", () => assert.deepEqual(ledger.finalize("sample", "1.0.0"), final));
|
|
34
|
+
await check("detached-get", () => { const copy = ledger.get("sample", "1.0.0"); copy.notes.push("tamper"); copy.state = "draft"; const fresh = ledger.get("sample", "1.0.0"); assert.deepEqual(fresh.notes, ["First", "Second"]); assert.equal(fresh.state, "final"); });
|
|
35
|
+
await check("ordered-detached-list", () => { const copies = ledger.list(); assert.deepEqual(copies.map(entry => entry.sequence), [1, 2]); copies[0].notes[0] = "tamper"; assert.equal(ledger.list()[0].notes[0], "First"); });
|
|
36
|
+
return { checks, passed: checks.filter(check => check.pass).length, total: checks.length };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
|
40
|
+
if (process.argv.length !== 3) { process.stderr.write("Usage: verify-release-ledger.mjs MODULE_PATH\n"); process.exitCode = 2; }
|
|
41
|
+
else {
|
|
42
|
+
const value = await verifyReleaseLedger(process.argv[2]);
|
|
43
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
44
|
+
process.exitCode = value.passed === value.total ? 0 : 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -12,8 +12,8 @@ for shell in bash zsh fish; do
|
|
|
12
12
|
done
|
|
13
13
|
|
|
14
14
|
capability_ids='memory.write memory.search notes.distill wiki graph security.audit route.check context.explore'
|
|
15
|
-
eval_commands='validate run ablate report prune delete'
|
|
16
|
-
eval_flags='--task-set --adapter --output --repetitions --seed --json --yes --disable --experiment --older-than-days --dry-run'
|
|
15
|
+
eval_commands='validate run ablate report prune delete protocol'
|
|
16
|
+
eval_flags='--task-set --adapter --output --repetitions --seed --json --yes --disable --experiment --older-than-days --dry-run --trajectory --codex-jsonl --capture-authorized'
|
|
17
17
|
skill_commands='harvest list show review propose evaluate apply rollback'
|
|
18
18
|
for shell in bash zsh fish; do
|
|
19
19
|
grep -q 'capabilities' "$tmp/$shell"
|
|
@@ -70,7 +70,7 @@ for shell in bash zsh fish; do
|
|
|
70
70
|
done
|
|
71
71
|
|
|
72
72
|
"$ROOT/bin/cairn" help >"$tmp/root-help"
|
|
73
|
-
grep -qF 'cairn eval <validate|run|ablate|report|prune|delete>' "$tmp/root-help"
|
|
73
|
+
grep -qF 'cairn eval <validate|run|ablate|report|prune|delete|protocol>' "$tmp/root-help"
|
|
74
74
|
node "$ROOT/mcp-memory-server/dist/eval-cli.js" --help >"$tmp/eval-help"
|
|
75
75
|
for command in $eval_commands; do
|
|
76
76
|
grep -q "cairn eval $command" "$tmp/eval-help"
|
|
@@ -152,6 +152,10 @@ for shell in bash zsh fish; do
|
|
|
152
152
|
fi
|
|
153
153
|
done
|
|
154
154
|
"$ROOT/bin/cairn" completion powershell >"$tmp/powershell"
|
|
155
|
+
grep -qF "'protocol'" "$tmp/powershell"
|
|
156
|
+
grep -qF "'--trajectory'" "$tmp/powershell"
|
|
157
|
+
grep -qF "'--codex-jsonl'" "$tmp/powershell"
|
|
158
|
+
grep -qF "'--capture-authorized'" "$tmp/powershell"
|
|
155
159
|
if ! grep -qE "['\"]setup['\"]" "$tmp/powershell"; then
|
|
156
160
|
completion_surface_complete=false
|
|
157
161
|
fi
|
|
@@ -74,6 +74,7 @@ done < <(bin/cairn help | sed -n 's/^ cairn \([a-z-]*\).*/\1/p' | sort -u)
|
|
|
74
74
|
node <<'NODE'
|
|
75
75
|
const { existsSync, readdirSync, readFileSync } = require("node:fs");
|
|
76
76
|
const { dirname, resolve } = require("node:path");
|
|
77
|
+
const packageVersion = JSON.parse(readFileSync("package.json", "utf8")).version;
|
|
77
78
|
|
|
78
79
|
function markdownFiles(root) {
|
|
79
80
|
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
|
@@ -84,6 +85,13 @@ function markdownFiles(root) {
|
|
|
84
85
|
|
|
85
86
|
for (const file of markdownFiles("docs/learning")) {
|
|
86
87
|
const text = readFileSync(file, "utf8");
|
|
88
|
+
if (file.startsWith("docs/learning/lessons/")) {
|
|
89
|
+
for (const match of text.matchAll(/(?:@cairnkeep\/cli@|ghcr\.io\/cairnkeep\/cairnkeep(?:-workspace)?:)(\d+\.\d+\.\d+(?:-[\w.-]+)?)/g)) {
|
|
90
|
+
if (match[1] !== packageVersion) {
|
|
91
|
+
throw new Error(`stale installation command in ${file}: ${match[1]} instead of ${packageVersion}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
87
95
|
for (const match of text.matchAll(/\]\(([^)]+)\)/g)) {
|
|
88
96
|
const target = match[1].split("#", 1)[0];
|
|
89
97
|
if (!target || /^[a-z]+:/i.test(target)) continue;
|
|
@@ -76,6 +76,21 @@ cmp -s "$tmp/source-eval-disabled.json" "$tmp/installed-eval-disabled.json" || \
|
|
|
76
76
|
fail "source and installed disabled eval identity differ"
|
|
77
77
|
[[ ! -e "$tmp/installed-unwritten-output" ]] || fail "installed disabled eval created output"
|
|
78
78
|
|
|
79
|
+
CAIRN_EVAL=1 cairn eval protocol \
|
|
80
|
+
--trajectory "$installed_root/examples/eval/protocol-project-search.json" --json >"$tmp/protocol-pass.json"
|
|
81
|
+
node -e 'const v=require(process.argv[1]);if(v.status!=="pass"||v.evidence_scope!=="local-trajectory-observation")process.exit(1)' "$tmp/protocol-pass.json" \
|
|
82
|
+
|| fail "installed memory protocol positive control failed"
|
|
83
|
+
set +e
|
|
84
|
+
CAIRN_EVAL=1 cairn eval protocol \
|
|
85
|
+
--trajectory "$installed_root/examples/eval/protocol-narration.json" --json >"$tmp/protocol-fail.json"
|
|
86
|
+
protocol_status=$?
|
|
87
|
+
set -e
|
|
88
|
+
[[ "$protocol_status" -eq 1 ]] || fail "installed memory protocol counted narration as evidence"
|
|
89
|
+
CAIRN_EVAL=1 cairn eval protocol \
|
|
90
|
+
--codex-jsonl "$installed_root/examples/eval/protocol-codex.jsonl" --json >"$tmp/protocol-codex.json"
|
|
91
|
+
node -e 'const v=require(process.argv[1]);if(v.status!=="pass"||v.harness!=="codex")process.exit(1)' "$tmp/protocol-codex.json" \
|
|
92
|
+
|| fail "installed native Codex protocol control failed"
|
|
93
|
+
|
|
79
94
|
for required in \
|
|
80
95
|
schemas/capability-contract.schema.json \
|
|
81
96
|
schemas/capability-callback.schema.json \
|
|
@@ -159,11 +174,16 @@ for required in \
|
|
|
159
174
|
mcp-memory-server/dist/eval-report.js \
|
|
160
175
|
mcp-memory-server/dist/eval-runner.js \
|
|
161
176
|
mcp-memory-server/dist/eval-cli.js \
|
|
177
|
+
mcp-memory-server/dist/eval-protocol.js \
|
|
178
|
+
mcp-memory-server/dist/eval-codex.js \
|
|
162
179
|
scripts/fake-eval-adapter.mjs \
|
|
163
180
|
examples/eval/task-set.json \
|
|
164
181
|
examples/eval/bundled-fake.json \
|
|
165
182
|
examples/eval/adapter.json \
|
|
166
183
|
examples/eval/ecosystem-decisions.json \
|
|
184
|
+
examples/eval/protocol-project-search.json \
|
|
185
|
+
examples/eval/protocol-narration.json \
|
|
186
|
+
examples/eval/protocol-codex.jsonl \
|
|
167
187
|
examples/eval/README.md
|
|
168
188
|
do
|
|
169
189
|
[[ -f "$installed_root/$required" ]] || fail "npm tarball omitted $required"
|
|
@@ -183,13 +203,16 @@ for asset in \
|
|
|
183
203
|
examples/eval/bundled-fake.json \
|
|
184
204
|
examples/eval/adapter.json \
|
|
185
205
|
examples/eval/ecosystem-decisions.json \
|
|
206
|
+
examples/eval/protocol-project-search.json \
|
|
207
|
+
examples/eval/protocol-narration.json \
|
|
208
|
+
examples/eval/protocol-codex.jsonl \
|
|
186
209
|
examples/eval/README.md
|
|
187
210
|
do
|
|
188
211
|
[[ ! -x "$installed_root/$asset" ]] || \
|
|
189
212
|
fail "installed public eval data asset is unexpectedly executable: $asset"
|
|
190
213
|
done
|
|
191
214
|
eval_assets=$(cd "$installed_root/examples/eval" && find . -type f -print | LC_ALL=C sort)
|
|
192
|
-
[[ "$eval_assets" == $'./README.md\n./adapter.json\n./bundled-fake.json\n./ecosystem-decisions.json\n./task-set.json' ]] || \
|
|
215
|
+
[[ "$eval_assets" == $'./README.md\n./adapter.json\n./bundled-fake.json\n./ecosystem-decisions.json\n./protocol-codex.jsonl\n./protocol-narration.json\n./protocol-project-search.json\n./task-set.json' ]] || \
|
|
193
216
|
fail "installed eval examples contain an unexpected path"
|
|
194
217
|
|
|
195
218
|
node - "$installed_root/examples/eval/adapter.json" <<'NODE'
|
|
@@ -35,6 +35,10 @@ function walk(value, visit, path = "workflow") {
|
|
|
35
35
|
|
|
36
36
|
function validateWorkflow(name, workflow) {
|
|
37
37
|
assert.ok(workflow && typeof workflow === "object" && !Array.isArray(workflow), `${name} is not a workflow mapping`);
|
|
38
|
+
assert.ok(workflow.permissions && typeof workflow.permissions === "object" && !Array.isArray(workflow.permissions), `${name} must have an explicit workflow permission mapping`);
|
|
39
|
+
for (const [permission, level] of Object.entries(workflow.permissions)) {
|
|
40
|
+
assert.ok(["read", "none"].includes(level), `${name} has a non-read-only workflow permission: ${permission}`);
|
|
41
|
+
}
|
|
38
42
|
const triggers = workflow.on;
|
|
39
43
|
const triggerNames = typeof triggers === "string" ? [triggers] : Array.isArray(triggers) ? triggers : Object.keys(triggers ?? {});
|
|
40
44
|
assert.ok(!triggerNames.includes("pull_request_target"), `${name} uses pull_request_target`);
|
|
@@ -51,24 +55,81 @@ function validateWorkflow(name, workflow) {
|
|
|
51
55
|
});
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
const PUBLICATION_PERMISSIONS = {
|
|
59
|
+
prepare: { actions: "read", contents: "read" },
|
|
60
|
+
npm: { contents: "write", "id-token": "write" },
|
|
61
|
+
containers: {
|
|
62
|
+
contents: "read",
|
|
63
|
+
packages: "write",
|
|
64
|
+
attestations: "write",
|
|
65
|
+
"id-token": "write",
|
|
66
|
+
"artifact-metadata": "write",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function validatePublicationPermissions(workflow) {
|
|
71
|
+
validateWorkflow("publish.yml", workflow);
|
|
72
|
+
assert.deepEqual(workflow.permissions, { contents: "read" }, "publication workflow default must be contents-read only");
|
|
73
|
+
assert.deepEqual(Object.keys(workflow.jobs).sort(), Object.keys(PUBLICATION_PERMISSIONS).sort(), "publication jobs require explicit permission review");
|
|
74
|
+
for (const [job, permissions] of Object.entries(PUBLICATION_PERMISSIONS)) {
|
|
75
|
+
assert.deepEqual(workflow.jobs[job].permissions, permissions, `publication ${job} permissions must match its reviewed tasks`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const publish = sources.find(({ name }) => name === "publish.yml")?.workflow;
|
|
80
|
+
assert.ok(publish, "publication workflow is missing");
|
|
81
|
+
const permissionControl = structuredClone(publish);
|
|
82
|
+
permissionControl.permissions = { contents: "read" };
|
|
83
|
+
for (const [job, permissions] of Object.entries(PUBLICATION_PERMISSIONS)) {
|
|
84
|
+
permissionControl.jobs[job].permissions = structuredClone(permissions);
|
|
85
|
+
}
|
|
86
|
+
validatePublicationPermissions(permissionControl);
|
|
87
|
+
|
|
88
|
+
let permissionFixtures = 0;
|
|
89
|
+
function rejectPermissionMutation(mutate, expected) {
|
|
90
|
+
const fixture = structuredClone(permissionControl);
|
|
91
|
+
mutate(fixture);
|
|
92
|
+
assert.throws(() => validatePublicationPermissions(fixture), expected, "unsafe publication permission mutation was accepted");
|
|
93
|
+
permissionFixtures += 1;
|
|
94
|
+
}
|
|
95
|
+
for (const permission of ["actions", "contents", "id-token", "packages", "attestations", "artifact-metadata"]) {
|
|
96
|
+
rejectPermissionMutation((fixture) => { fixture.permissions[permission] = "write"; }, /non-read-only workflow permission/);
|
|
97
|
+
}
|
|
98
|
+
rejectPermissionMutation((fixture) => { delete fixture.permissions; }, /explicit workflow permission mapping/);
|
|
99
|
+
rejectPermissionMutation((fixture) => { fixture.permissions = "write-all"; }, /explicit workflow permission mapping/);
|
|
100
|
+
rejectPermissionMutation((fixture) => { fixture.jobs.extra = { permissions: { contents: "read" } }; }, /explicit permission review/);
|
|
101
|
+
for (const [job, permissions] of Object.entries(PUBLICATION_PERMISSIONS)) {
|
|
102
|
+
rejectPermissionMutation((fixture) => { delete fixture.jobs[job].permissions; }, /permissions must match/);
|
|
103
|
+
rejectPermissionMutation((fixture) => { fixture.jobs[job].permissions = "write-all"; }, /broad scalar permissions/);
|
|
104
|
+
for (const permission of Object.keys(permissions)) {
|
|
105
|
+
rejectPermissionMutation((fixture) => { delete fixture.jobs[job].permissions[permission]; }, /permissions must match/);
|
|
106
|
+
}
|
|
107
|
+
for (const permission of ["actions", "contents", "id-token", "packages", "attestations", "artifact-metadata"]) {
|
|
108
|
+
if (permissions[permission] === "write") continue;
|
|
109
|
+
rejectPermissionMutation((fixture) => { fixture.jobs[job].permissions[permission] = "write"; }, /permissions must match/);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
55
112
|
|
|
56
113
|
const maliciousFixtures = [
|
|
57
|
-
"name: bad\non: push\npermissions: write-all\njobs: {}\n",
|
|
58
|
-
"name: bad\non: pull_request_target\njobs: {}\n",
|
|
59
|
-
"name: bad\non: push\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: >-\n actions/checkout@v7\n",
|
|
60
|
-
"name: bad\non: push\njobs:\n call:\n uses: 'owner/repo/.github/workflows/reuse.yml@main'\n",
|
|
61
|
-
`name: bad\non: push\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: evil/action@${"a".repeat(40)}\n`,
|
|
114
|
+
["name: bad\non: push\npermissions: write-all\njobs: {}\n", /explicit workflow permission mapping/],
|
|
115
|
+
["name: bad\non: pull_request_target\npermissions: {contents: read}\njobs: {}\n", /uses pull_request_target/],
|
|
116
|
+
["name: bad\non: push\npermissions: {contents: read}\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: >-\n actions/checkout@v7\n", /not pinned to a full commit/],
|
|
117
|
+
["name: bad\non: push\npermissions: {contents: read}\njobs:\n call:\n uses: 'owner/repo/.github/workflows/reuse.yml@main'\n", /not pinned to a full commit/],
|
|
118
|
+
[`name: bad\non: push\npermissions: {contents: read}\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: evil/action@${"a".repeat(40)}\n`, /not on the maintained allowlist/],
|
|
62
119
|
];
|
|
63
|
-
for (const [index, fixture] of maliciousFixtures.entries()) {
|
|
64
|
-
assert.throws(() => validateWorkflow(`malicious-${index}.yml`, parse(fixture)),
|
|
120
|
+
for (const [index, [fixture, expected]] of maliciousFixtures.entries()) {
|
|
121
|
+
assert.throws(() => validateWorkflow(`malicious-${index}.yml`, parse(fixture)), expected, `malicious workflow fixture ${index} was accepted`);
|
|
65
122
|
}
|
|
66
123
|
|
|
124
|
+
for (const { name, workflow } of sources) validateWorkflow(name, workflow);
|
|
125
|
+
validatePublicationPermissions(publish);
|
|
126
|
+
|
|
67
127
|
const security = sources.find(({ name }) => name === "security.yml")?.text ?? "";
|
|
68
128
|
assert.match(security, /github\/codeql-action\/init@[a-f0-9]{40}/);
|
|
69
129
|
assert.match(security, /actions\/dependency-review-action@[a-f0-9]{40}/);
|
|
70
130
|
assert.match(security, /queries:\s*security-extended/);
|
|
71
131
|
const scorecard = sources.find(({ name }) => name === "scorecard.yml")?.text ?? "";
|
|
132
|
+
assert.ok(Object.hasOwn(parse(scorecard).on, "workflow_dispatch"), "Scorecard must support manual recovery without a source push");
|
|
72
133
|
assert.match(scorecard, /ossf\/scorecard-action@[a-f0-9]{40}/);
|
|
73
134
|
assert.match(scorecard, /publish_results:\s*true/);
|
|
74
135
|
|
|
@@ -76,4 +137,4 @@ const rootPackage = JSON.parse(readFileSync(join(root, "package.json"), "utf8"))
|
|
|
76
137
|
assert.match(rootPackage.scripts["audit:runtime"] ?? "", /npm audit --omit=dev/);
|
|
77
138
|
assert.match(rootPackage.scripts["check:public"] ?? "", /security:baseline/);
|
|
78
139
|
|
|
79
|
-
console.log(`PASS: ${workflowFiles.length} workflows
|
|
140
|
+
console.log(`PASS: ${workflowFiles.length} workflows, ${maliciousFixtures.length} action/trigger fixtures and ${permissionFixtures} permission mutations enforce parsed YAML, full-SHA actions, read-only defaults and job-scoped publication authority`);
|
|
@@ -736,7 +736,8 @@ export function powershellCompletion() {
|
|
|
736
736
|
$pack = 'init','lock','validate','install','import-okf','validate-okf','export-okf','list','show','remove','enable','disable','update','skills','approve-skill','revoke-skill','doctor','--repair','--project','--project-id','--json'
|
|
737
737
|
$proposals = 'create','list','show','apply','doctor','--session','--scope','--model','--category','--project','--json'
|
|
738
738
|
$security = 'doctor','--project','--json'
|
|
739
|
-
$
|
|
739
|
+
$evaluation = 'validate','run','ablate','report','prune','delete','protocol','--task-set','--adapter','--output','--repetitions','--seed','--yes','--json','--disable','--experiment','--older-than-days','--dry-run','--trajectory','--codex-jsonl','--capture-authorized'
|
|
740
|
+
$candidates = if ($commandAst.ToString() -match '^\\s*cairn\\s+setup(?:\\s|$)') { $setup } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+playbook(?:\\s|$)') { $playbook } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+pack(?:\\s|$)') { $pack } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+proposals(?:\\s|$)') { $proposals } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+security(?:\\s|$)') { $security } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+eval(?:\\s|$)') { $evaluation } else { $commands }
|
|
740
741
|
$candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
741
742
|
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
742
743
|
}
|