@cairnkeep/cli 2.20.1 → 2.21.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 +51 -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 +44 -5
- package/docs/privacy-and-data-flow.md +14 -0
- package/docs/releasing.md +16 -1
- package/docs/security-assurance.md +130 -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 +2 -1
- package/package.json +3 -2
- package/scripts/bootstrap.sh +1 -0
- package/scripts/cairn-cli.mjs +2 -0
- package/scripts/completion.sh +9 -3
- package/scripts/doctor.sh +20 -4
- package/scripts/setup-reconcile.mjs +3 -0
- package/scripts/test-bootstrap-untracked.sh +2 -0
- package/scripts/test-ci-sharding.sh +33 -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-doctor.sh +29 -11
- 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,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
5
|
+
tmp=$(mktemp -d)
|
|
6
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
7
|
+
|
|
8
|
+
project="$tmp/project"
|
|
9
|
+
fake_bin="$tmp/bin"
|
|
10
|
+
mkdir -p "$project/.ai" "$fake_bin"
|
|
11
|
+
cp "$ROOT/templates/start-codex.sh.template" "$project/.ai/start-codex.sh"
|
|
12
|
+
chmod +x "$project/.ai/start-codex.sh"
|
|
13
|
+
cat > "$project/.ai/.env" <<'EOF'
|
|
14
|
+
CAIRN_PRECEDENCE_TEST=file
|
|
15
|
+
CAIRN_FILE_ONLY=file-only
|
|
16
|
+
EOF
|
|
17
|
+
chmod 600 "$project/.ai/.env"
|
|
18
|
+
cat > "$fake_bin/codex" <<'EOF'
|
|
19
|
+
#!/usr/bin/env bash
|
|
20
|
+
printf '%s|%s\n' "$CAIRN_PRECEDENCE_TEST" "$CAIRN_FILE_ONLY"
|
|
21
|
+
EOF
|
|
22
|
+
chmod +x "$fake_bin/codex"
|
|
23
|
+
|
|
24
|
+
output=$(PATH="$fake_bin:$PATH" CAIRN_PRECEDENCE_TEST=ambient "$project/.ai/start-codex.sh")
|
|
25
|
+
[[ "$output" == "ambient|file-only" ]] || {
|
|
26
|
+
echo "FAIL: launcher environment precedence drifted: $output" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
echo "PASS: POSIX launcher keeps ambient overrides and adds project-only values"
|
|
@@ -166,4 +166,23 @@ grep -qF 'course-13-playbooks' "$coverage"
|
|
|
166
166
|
grep -qF 'L25-playbooks.md' docs/learning/tracks/practitioner.md
|
|
167
167
|
grep -qF 'L25-playbooks.md' docs/learning/tracks/operator.md
|
|
168
168
|
|
|
169
|
+
l28=docs/learning/lessons/L28-security-assurance.md
|
|
170
|
+
video28=docs/learning/video-scripts/V28-security-assurance.md
|
|
171
|
+
for file in "$l28" "$video28" docs/security-assurance.md; do
|
|
172
|
+
[[ -f "$file" ]]
|
|
173
|
+
done
|
|
174
|
+
grep -qF "Tested with:** Cairnkeep $version" "$l28"
|
|
175
|
+
for phrase in \
|
|
176
|
+
'cairn security doctor --project' \
|
|
177
|
+
'npm run security:baseline' \
|
|
178
|
+
'check:security-assurance' \
|
|
179
|
+
'check:http-guard' \
|
|
180
|
+
'not a certification'; do
|
|
181
|
+
grep -qF -- "$phrase" "$l28" || {
|
|
182
|
+
echo "L28 missing security-assurance boundary: $phrase" >&2
|
|
183
|
+
exit 1
|
|
184
|
+
}
|
|
185
|
+
done
|
|
186
|
+
grep -qF '`cairn security` | L28' "$coverage"
|
|
187
|
+
|
|
169
188
|
echo "PASS: public learning path structure, readiness, links, and version alignment"
|
|
@@ -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
|