@cairnkeep/cli 2.20.0 → 2.21.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 +45 -0
- package/README.md +6 -3
- package/SECURITY.md +13 -0
- package/containers/entrypoint.sh +2 -1
- package/docs/README.md +2 -0
- package/docs/learning/CURRICULUM-MAP.md +4 -2
- package/docs/learning/FEATURE-GUIDE.md +6 -1
- package/docs/learning/README.md +1 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +1 -1
- package/docs/learning/lessons/L02-installation.md +1 -1
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +1 -1
- 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 +3 -0
- package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
- package/docs/learning/lessons/L28-security-assurance.md +104 -0
- package/docs/learning/video-scripts/V28-security-assurance.md +72 -0
- package/docs/operating.md +40 -5
- package/docs/privacy-and-data-flow.md +14 -0
- package/docs/releasing.md +9 -1
- package/docs/security-assurance.md +122 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/artifact-schema.js +2 -7
- package/mcp-memory-server/dist/context-pack.js +152 -31
- package/mcp-memory-server/dist/eval-schema.js +13 -12
- package/mcp-memory-server/dist/http-security.js +104 -0
- package/mcp-memory-server/dist/index.js +50 -12
- package/mcp-memory-server/dist/okf.js +35 -9
- package/mcp-memory-server/dist/path-security.js +49 -0
- package/mcp-memory-server/dist/platform-security.js +154 -31
- package/mcp-memory-server/dist/project-environment.js +132 -0
- package/mcp-memory-server/dist/security-cli.js +46 -0
- package/mcp-memory-server/dist/security-doctor.js +211 -0
- package/mcp-memory-server/package.json +5 -4
- package/package.json +5 -4
- package/scripts/bootstrap.sh +1 -0
- package/scripts/cairn-cli.mjs +2 -0
- package/scripts/completion.sh +9 -3
- package/scripts/setup-reconcile.mjs +3 -0
- package/scripts/test-bootstrap-untracked.sh +2 -0
- package/scripts/test-cli-dispatch.sh +16 -1
- package/scripts/test-completion.sh +3 -0
- package/scripts/test-container-runner.sh +7 -0
- package/scripts/test-launcher-env-precedence.sh +30 -0
- package/scripts/test-learning-docs.sh +19 -0
- package/scripts/test-security-baseline.sh +4 -0
- package/scripts/verify-security-baseline.mjs +79 -0
- package/scripts/windows-platform.mjs +4 -2
- package/templates/start-claude.sh.template +3 -0
- package/templates/start-codex.sh.template +3 -0
- package/templates/start-kimi.sh.template +3 -0
- package/templates/start-omp.sh.template +3 -0
- package/templates/start-opencode.sh.template +3 -0
- package/templates/start-pi.sh.template +3 -0
- package/templates/start-qwen.sh.template +3 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { parse } from "yaml";
|
|
9
|
+
|
|
10
|
+
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
11
|
+
const workflowDirectory = join(root, ".github", "workflows");
|
|
12
|
+
const workflowFiles = readdirSync(workflowDirectory).filter((name) => /\.ya?ml$/.test(name)).sort();
|
|
13
|
+
const sources = workflowFiles.map((name) => {
|
|
14
|
+
const text = readFileSync(join(workflowDirectory, name), "utf8");
|
|
15
|
+
return { name, text, workflow: parse(text) };
|
|
16
|
+
});
|
|
17
|
+
const ALLOWED_ACTIONS = [
|
|
18
|
+
/^actions\//,
|
|
19
|
+
/^github\/codeql-action\//,
|
|
20
|
+
/^docker\//,
|
|
21
|
+
/^ossf\/scorecard-action(?:\/|$)/,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function walk(value, visit, path = "workflow") {
|
|
25
|
+
if (!value || typeof value !== "object") return;
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
value.forEach((entry, index) => walk(entry, visit, `${path}[${index}]`));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
for (const [key, child] of Object.entries(value)) {
|
|
31
|
+
visit(key, child, `${path}.${key}`);
|
|
32
|
+
walk(child, visit, `${path}.${key}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validateWorkflow(name, workflow) {
|
|
37
|
+
assert.ok(workflow && typeof workflow === "object" && !Array.isArray(workflow), `${name} is not a workflow mapping`);
|
|
38
|
+
const triggers = workflow.on;
|
|
39
|
+
const triggerNames = typeof triggers === "string" ? [triggers] : Array.isArray(triggers) ? triggers : Object.keys(triggers ?? {});
|
|
40
|
+
assert.ok(!triggerNames.includes("pull_request_target"), `${name} uses pull_request_target`);
|
|
41
|
+
walk(workflow, (key, value, path) => {
|
|
42
|
+
if (key === "permissions" && typeof value === "string") {
|
|
43
|
+
assert.ok(!["read-all", "write-all"].includes(value), `${name} has broad scalar permissions at ${path}`);
|
|
44
|
+
}
|
|
45
|
+
if (key !== "uses") return;
|
|
46
|
+
assert.equal(typeof value, "string", `${name} has a non-string action reference at ${path}`);
|
|
47
|
+
if (value.startsWith("./")) return;
|
|
48
|
+
assert.match(value, /^[^@\s]+@[a-f0-9]{40}$/, `${name} action is not pinned to a full commit at ${path}: ${value}`);
|
|
49
|
+
const action = value.slice(0, value.lastIndexOf("@"));
|
|
50
|
+
assert.ok(ALLOWED_ACTIONS.some((allowed) => allowed.test(action)), `${name} action is not on the maintained allowlist at ${path}: ${action}`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const { name, workflow } of sources) validateWorkflow(name, workflow);
|
|
55
|
+
|
|
56
|
+
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`,
|
|
62
|
+
];
|
|
63
|
+
for (const [index, fixture] of maliciousFixtures.entries()) {
|
|
64
|
+
assert.throws(() => validateWorkflow(`malicious-${index}.yml`, parse(fixture)), undefined, `malicious workflow fixture ${index} was accepted`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const security = sources.find(({ name }) => name === "security.yml")?.text ?? "";
|
|
68
|
+
assert.match(security, /github\/codeql-action\/init@[a-f0-9]{40}/);
|
|
69
|
+
assert.match(security, /actions\/dependency-review-action@[a-f0-9]{40}/);
|
|
70
|
+
assert.match(security, /queries:\s*security-extended/);
|
|
71
|
+
const scorecard = sources.find(({ name }) => name === "scorecard.yml")?.text ?? "";
|
|
72
|
+
assert.match(scorecard, /ossf\/scorecard-action@[a-f0-9]{40}/);
|
|
73
|
+
assert.match(scorecard, /publish_results:\s*true/);
|
|
74
|
+
|
|
75
|
+
const rootPackage = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
76
|
+
assert.match(rootPackage.scripts["audit:runtime"] ?? "", /npm audit --omit=dev/);
|
|
77
|
+
assert.match(rootPackage.scripts["check:public"] ?? "", /security:baseline/);
|
|
78
|
+
|
|
79
|
+
console.log(`PASS: ${workflowFiles.length} workflows and ${maliciousFixtures.length} adversarial fixtures enforce parsed YAML, allowlisted full-SHA actions, bounded triggers, and required security controls`);
|
|
@@ -275,6 +275,7 @@ export function bootstrapWindows(root, args) {
|
|
|
275
275
|
if (!existsSync(modulePath)) atomicWrite(modulePath, launcherModule(), 0o755);
|
|
276
276
|
const powershellPath = join(target, ".ai", "start-harness.ps1");
|
|
277
277
|
if (!existsSync(powershellPath)) atomicWrite(powershellPath, launcherPowerShell(), 0o600);
|
|
278
|
+
hardenWindowsAcl(join(target, ".ai"));
|
|
278
279
|
if (!options.has("--untracked")) {
|
|
279
280
|
reconcilePlaybookInstructions(target);
|
|
280
281
|
}
|
|
@@ -729,12 +730,13 @@ export function powershellCompletion() {
|
|
|
729
730
|
const harnesses = HARNESS_IDS.map((id) => `'${id}'`).join(",");
|
|
730
731
|
return `Register-ArgumentCompleter -Native -CommandName cairn -ScriptBlock {
|
|
731
732
|
param($wordToComplete, $commandAst, $cursorPosition)
|
|
732
|
-
$commands = 'bootstrap','setup','memory-server','sync','sync-pi','sync-kimi','doctor','trajectory','artifact','evidence','playbook','capabilities','mcp-tools','pack','proposals','notes','eval','skill','graph','memory','audit-timer','uninstall','completion','version','help'
|
|
733
|
+
$commands = 'bootstrap','setup','memory-server','sync','sync-pi','sync-kimi','doctor','security','trajectory','artifact','evidence','playbook','capabilities','mcp-tools','pack','proposals','notes','eval','skill','graph','memory','audit-timer','uninstall','completion','version','help'
|
|
733
734
|
$setup = '--git','--harness','--memory','--policy','--yes','--json','init','existing','none',${harnesses},'local'
|
|
734
735
|
$playbook = 'list','status','init','set','enable','disable','reset','check','record','receipts','instructions','doctor','minimal','balanced','strict','must','should','may','off','start','finish','install','remove','context.recall','context.explore','work.plan','verify.tests','review.repository','review.security','docs.update','learning.capture','--project','--json','--enforce','--complexity','--familiarity','--risk','--public-change','--changed','--change-type','--completed','--skipped','--failed','--actor','--actor-kind','--session','--policy','--decision','--event','--action','--outcome','--reason'
|
|
735
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'
|
|
736
737
|
$proposals = 'create','list','show','apply','doctor','--session','--scope','--model','--category','--project','--json'
|
|
737
|
-
$
|
|
738
|
+
$security = 'doctor','--project','--json'
|
|
739
|
+
$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 } else { $commands }
|
|
738
740
|
$candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
739
741
|
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
740
742
|
}
|
|
@@ -41,10 +41,13 @@ resolve_cairn_root() {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
if [[ -f "$ROOT_DIR/.ai/.env" ]]; then
|
|
44
|
+
local_env_snapshot=$(export -p)
|
|
45
|
+
readonly local_env_snapshot
|
|
44
46
|
set -a
|
|
45
47
|
# shellcheck disable=SC1091
|
|
46
48
|
. "$ROOT_DIR/.ai/.env"
|
|
47
49
|
set +a
|
|
50
|
+
eval "$local_env_snapshot"
|
|
48
51
|
fi
|
|
49
52
|
if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
|
|
50
53
|
# shellcheck disable=SC1091
|
|
@@ -6,10 +6,13 @@ set -euo pipefail
|
|
|
6
6
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
7
7
|
|
|
8
8
|
if [[ -f "$ROOT_DIR/.ai/.env" ]]; then
|
|
9
|
+
local_env_snapshot=$(export -p)
|
|
10
|
+
readonly local_env_snapshot
|
|
9
11
|
set -a
|
|
10
12
|
# shellcheck disable=SC1091
|
|
11
13
|
. "$ROOT_DIR/.ai/.env"
|
|
12
14
|
set +a
|
|
15
|
+
eval "$local_env_snapshot"
|
|
13
16
|
fi
|
|
14
17
|
if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
|
|
15
18
|
# shellcheck disable=SC1091
|
|
@@ -14,10 +14,13 @@ set -euo pipefail
|
|
|
14
14
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
15
15
|
|
|
16
16
|
if [[ -f "$ROOT_DIR/.ai/.env" ]]; then
|
|
17
|
+
local_env_snapshot=$(export -p)
|
|
18
|
+
readonly local_env_snapshot
|
|
17
19
|
set -a
|
|
18
20
|
# shellcheck disable=SC1091
|
|
19
21
|
. "$ROOT_DIR/.ai/.env"
|
|
20
22
|
set +a
|
|
23
|
+
eval "$local_env_snapshot"
|
|
21
24
|
fi
|
|
22
25
|
if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
|
|
23
26
|
# shellcheck disable=SC1091
|
|
@@ -5,10 +5,13 @@ set -euo pipefail
|
|
|
5
5
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
6
6
|
|
|
7
7
|
if [[ -f "$ROOT_DIR/.ai/.env" ]]; then
|
|
8
|
+
local_env_snapshot=$(export -p)
|
|
9
|
+
readonly local_env_snapshot
|
|
8
10
|
set -a
|
|
9
11
|
# shellcheck disable=SC1091
|
|
10
12
|
. "$ROOT_DIR/.ai/.env"
|
|
11
13
|
set +a
|
|
14
|
+
eval "$local_env_snapshot"
|
|
12
15
|
fi
|
|
13
16
|
if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
|
|
14
17
|
# shellcheck disable=SC1091
|
|
@@ -38,10 +38,13 @@ resolve_cairn_root() {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if [[ -f "$ROOT_DIR/.ai/.env" ]]; then
|
|
41
|
+
local_env_snapshot=$(export -p)
|
|
42
|
+
readonly local_env_snapshot
|
|
41
43
|
set -a
|
|
42
44
|
# shellcheck disable=SC1091
|
|
43
45
|
. "$ROOT_DIR/.ai/.env"
|
|
44
46
|
set +a
|
|
47
|
+
eval "$local_env_snapshot"
|
|
45
48
|
fi
|
|
46
49
|
if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
|
|
47
50
|
# shellcheck disable=SC1091
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
8
8
|
if [[ -f "$ROOT_DIR/.ai/.env" ]]; then
|
|
9
|
+
local_env_snapshot=$(export -p)
|
|
10
|
+
readonly local_env_snapshot
|
|
9
11
|
set -a
|
|
10
12
|
# shellcheck disable=SC1091
|
|
11
13
|
. "$ROOT_DIR/.ai/.env"
|
|
12
14
|
set +a
|
|
15
|
+
eval "$local_env_snapshot"
|
|
13
16
|
fi
|
|
14
17
|
if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
|
|
15
18
|
# shellcheck disable=SC1091
|
|
@@ -7,10 +7,13 @@ set -euo pipefail
|
|
|
7
7
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
8
8
|
|
|
9
9
|
if [[ -f "$ROOT_DIR/.ai/.env" ]]; then
|
|
10
|
+
local_env_snapshot=$(export -p)
|
|
11
|
+
readonly local_env_snapshot
|
|
10
12
|
set -a
|
|
11
13
|
# shellcheck disable=SC1091
|
|
12
14
|
. "$ROOT_DIR/.ai/.env"
|
|
13
15
|
set +a
|
|
16
|
+
eval "$local_env_snapshot"
|
|
14
17
|
fi
|
|
15
18
|
if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
|
|
16
19
|
# shellcheck disable=SC1091
|