@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +6 -3
  3. package/SECURITY.md +13 -0
  4. package/containers/entrypoint.sh +2 -1
  5. package/docs/README.md +2 -0
  6. package/docs/learning/CURRICULUM-MAP.md +4 -2
  7. package/docs/learning/FEATURE-GUIDE.md +6 -1
  8. package/docs/learning/README.md +1 -0
  9. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  10. package/docs/learning/lessons/L01-safe-trial.md +1 -1
  11. package/docs/learning/lessons/L02-installation.md +1 -1
  12. package/docs/learning/lessons/L03-first-project.md +1 -1
  13. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  14. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  15. package/docs/learning/lessons/L21-context-packs.md +1 -1
  16. package/docs/learning/lessons/L22-native-windows.md +1 -1
  17. package/docs/learning/lessons/L23-guided-setup.md +1 -1
  18. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  19. package/docs/learning/lessons/L25-playbooks.md +1 -1
  20. package/docs/learning/lessons/L26-context-intelligence.md +3 -0
  21. package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
  22. package/docs/learning/lessons/L28-security-assurance.md +104 -0
  23. package/docs/learning/video-scripts/V28-security-assurance.md +72 -0
  24. package/docs/operating.md +40 -5
  25. package/docs/privacy-and-data-flow.md +14 -0
  26. package/docs/releasing.md +9 -1
  27. package/docs/security-assurance.md +122 -0
  28. package/examples/eval/bundled-fake.json +1 -1
  29. package/mcp-memory-server/dist/artifact-schema.js +2 -7
  30. package/mcp-memory-server/dist/context-pack.js +152 -31
  31. package/mcp-memory-server/dist/eval-schema.js +13 -12
  32. package/mcp-memory-server/dist/http-security.js +104 -0
  33. package/mcp-memory-server/dist/index.js +50 -12
  34. package/mcp-memory-server/dist/okf.js +35 -9
  35. package/mcp-memory-server/dist/path-security.js +49 -0
  36. package/mcp-memory-server/dist/platform-security.js +154 -31
  37. package/mcp-memory-server/dist/project-environment.js +132 -0
  38. package/mcp-memory-server/dist/security-cli.js +46 -0
  39. package/mcp-memory-server/dist/security-doctor.js +211 -0
  40. package/mcp-memory-server/package.json +5 -4
  41. package/package.json +5 -4
  42. package/scripts/bootstrap.sh +1 -0
  43. package/scripts/cairn-cli.mjs +2 -0
  44. package/scripts/completion.sh +9 -3
  45. package/scripts/setup-reconcile.mjs +3 -0
  46. package/scripts/test-bootstrap-untracked.sh +2 -0
  47. package/scripts/test-cli-dispatch.sh +16 -1
  48. package/scripts/test-completion.sh +3 -0
  49. package/scripts/test-container-runner.sh +7 -0
  50. package/scripts/test-launcher-env-precedence.sh +30 -0
  51. package/scripts/test-learning-docs.sh +19 -0
  52. package/scripts/test-security-baseline.sh +4 -0
  53. package/scripts/verify-security-baseline.mjs +79 -0
  54. package/scripts/windows-platform.mjs +4 -2
  55. package/templates/start-claude.sh.template +3 -0
  56. package/templates/start-codex.sh.template +3 -0
  57. package/templates/start-kimi.sh.template +3 -0
  58. package/templates/start-omp.sh.template +3 -0
  59. package/templates/start-opencode.sh.template +3 -0
  60. package/templates/start-pi.sh.template +3 -0
  61. 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
- $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 } else { $commands }
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