@cairnkeep/cli 2.21.0 → 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 +16 -0
- package/docs/learning/CURRICULUM-MAP.md +1 -1
- package/docs/learning/FEATURE-GUIDE.md +1 -1
- 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 +1 -1
- package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
- package/docs/learning/lessons/L28-security-assurance.md +2 -2
- package/docs/operating.md +4 -0
- package/docs/releasing.md +7 -0
- package/docs/security-assurance.md +8 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/package.json +1 -1
- package/package.json +1 -1
- package/scripts/doctor.sh +20 -4
- package/scripts/test-ci-sharding.sh +33 -0
- package/scripts/test-doctor.sh +29 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable user-facing changes are documented here. This project follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.21.1] - 2026-10-07
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Classify embedding health failures by sanitized HTTP status so operators can
|
|
13
|
+
distinguish authorization, missing endpoint/model, throttling, transport,
|
|
14
|
+
and upstream failures without exposing provider response bodies, request
|
|
15
|
+
identifiers, or credentials.
|
|
16
|
+
|
|
17
|
+
### CI
|
|
18
|
+
|
|
19
|
+
- Split all 48 memory-server smoke contracts exactly once across three
|
|
20
|
+
deterministically balanced native-Windows jobs while retaining Windows CLI,
|
|
21
|
+
package, global-install, Node 22/24/26, and full Linux matrix coverage. This
|
|
22
|
+
removes the serial 15-minute suite duplication from every Windows lane.
|
|
23
|
+
|
|
8
24
|
## [2.21.0] - 2026-10-07
|
|
9
25
|
|
|
10
26
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# L28 - Verify Cairnkeep's security posture
|
|
2
2
|
|
|
3
3
|
**Status:** Ready
|
|
4
|
-
**Tested with:** Cairnkeep 2.21.
|
|
4
|
+
**Tested with:** Cairnkeep 2.21.1 and Node.js 22 or newer
|
|
5
5
|
|
|
6
6
|
## Outcome
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ and choose least authority for a network-facing MCP client.
|
|
|
11
11
|
|
|
12
12
|
## Prerequisites
|
|
13
13
|
|
|
14
|
-
- Install Cairnkeep 2.21.
|
|
14
|
+
- Install Cairnkeep 2.21.1 or use a disposable source checkout.
|
|
15
15
|
- Complete [L20 - Least-authority MCP tool profiles](L20-mcp-tool-profiles.md).
|
|
16
16
|
- Use synthetic values only; never paste a real bearer token into a recording,
|
|
17
17
|
terminal transcript, or issue.
|
package/docs/operating.md
CHANGED
|
@@ -1049,6 +1049,10 @@ then checks `./.ai/.env` (or the current environment). It does not inspect a
|
|
|
1049
1049
|
harness's remote HTTP registration. Unconfigured optional dependencies are
|
|
1050
1050
|
skipped; it exits non-zero when the local server probe fails or a configured
|
|
1051
1051
|
dependency (LLM/embedding endpoint, writable store) is unreachable.
|
|
1052
|
+
Embedding probes report only a bounded classification and HTTP status, such as
|
|
1053
|
+
`authorization rejected (HTTP 403)` or `endpoint or model not found (HTTP
|
|
1054
|
+
404)`. Provider response bodies and request identifiers are discarded so a
|
|
1055
|
+
health check cannot echo sensitive upstream diagnostics.
|
|
1052
1056
|
|
|
1053
1057
|
It also checks an existing project-local trajectory database for SQLite
|
|
1054
1058
|
integrity, schema compatibility and index consistency. Because capture is
|
package/docs/releasing.md
CHANGED
|
@@ -29,6 +29,13 @@ from a workstation.
|
|
|
29
29
|
9. Verify both image digests, SBOM attestations, provenance attestations, and an
|
|
30
30
|
anonymous pull before announcing the release.
|
|
31
31
|
|
|
32
|
+
The native-Windows lanes retain stable check names for branch protection. Each
|
|
33
|
+
lane exercises the Windows CLI and packed global install on its Node version,
|
|
34
|
+
while the complete memory-server smoke catalog is deterministically balanced
|
|
35
|
+
across the three lanes. The Linux Node 22/24/26 matrix independently runs the
|
|
36
|
+
complete catalog on every supported runtime. This preserves coverage while
|
|
37
|
+
removing three serial copies of the slow Windows suite from the critical path.
|
|
38
|
+
|
|
32
39
|
Stable versions must use a stable GitHub Release and publish to npm's `latest`
|
|
33
40
|
tag. SemVer prerelease versions must use a GitHub prerelease and publish to
|
|
34
41
|
`next`. A mismatch fails before publication.
|
|
@@ -86,6 +86,10 @@ punctuation.
|
|
|
86
86
|
`cairn doctor` remains the operational dependency/store check. Run both before
|
|
87
87
|
exposing HTTP, after changing tool authority, and before a security-sensitive
|
|
88
88
|
release.
|
|
89
|
+
Its embedding probe retains only a sanitized failure class and HTTP status;
|
|
90
|
+
provider response bodies and request identifiers are never emitted. This makes
|
|
91
|
+
expired credentials distinguishable from a missing model without turning the
|
|
92
|
+
doctor output into a secondary secret or telemetry channel.
|
|
89
93
|
|
|
90
94
|
## Repository and release controls
|
|
91
95
|
|
|
@@ -120,3 +124,7 @@ The maintained gates are `npm run security:baseline`, both production
|
|
|
120
124
|
security-assurance and HTTP-guard smoke tests, native-Windows tests, and the
|
|
121
125
|
complete public test matrix. The baseline gate also rejects
|
|
122
126
|
`pull_request_target` and every unpinned non-local GitHub Action.
|
|
127
|
+
The native-Windows matrix runs the CLI, package, and global-install contracts on
|
|
128
|
+
Node 22, 24, and 26. It deterministically partitions the complete memory-server
|
|
129
|
+
smoke catalog across those lanes exactly once; the Linux matrix still runs the
|
|
130
|
+
complete catalog independently on every supported Node version.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"identifier":"cairn-offline-fake-v1","package_version":"2.21.
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.21.1","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"check:security-assurance": "node scripts/smoke-security-assurance.mjs",
|
|
63
63
|
"check:notes": "npm run build && npm run check:note-signatures && npm run check:note-distillation && npm run check:note-enrichment",
|
|
64
64
|
"test": "npm run test:smoke",
|
|
65
|
-
"test:smoke": "npm run build &&
|
|
65
|
+
"test:smoke": "npm run build && node scripts/run-smoke-suite.mjs",
|
|
66
66
|
"test:search": "npm run build && npm run check:search",
|
|
67
67
|
"test:memory": "npm run build && npm run check:embeddings && npm run check:extract && npm run check:search",
|
|
68
68
|
"test:all": "npm run test:memory",
|
package/package.json
CHANGED
package/scripts/doctor.sh
CHANGED
|
@@ -36,15 +36,31 @@ fail() { printf ' [FAIL] %s\n' "$1"; fails=$((fails + 1)); }
|
|
|
36
36
|
# reachable); a connect/timeout failure returns non-zero.
|
|
37
37
|
reachable() { curl -sS -m 5 -o /dev/null "$1" >/dev/null 2>&1; }
|
|
38
38
|
|
|
39
|
+
embedding_failure="request failed"
|
|
39
40
|
embedding_works() {
|
|
40
|
-
local payload
|
|
41
|
+
local payload http_status curl_status
|
|
41
42
|
payload=$(node -e 'process.stdout.write(JSON.stringify({ model: process.argv[1], input: ["cairnkeep health check"] }))' \
|
|
42
43
|
"$CAIRN_MEMORY_EMBEDDING_MODEL") || return 1
|
|
43
|
-
curl -
|
|
44
|
+
http_status=$(curl -sS -m 10 -o /dev/null -w '%{http_code}' \
|
|
44
45
|
-H "Authorization: Bearer $CAIRN_LLM_API_KEY" \
|
|
45
46
|
-H "Content-Type: application/json" \
|
|
46
47
|
--data "$payload" \
|
|
47
|
-
"${CAIRN_MEMORY_EMBEDDING_URL%/}/embeddings" >/dev/null
|
|
48
|
+
"${CAIRN_MEMORY_EMBEDDING_URL%/}/embeddings" 2>/dev/null)
|
|
49
|
+
curl_status=$?
|
|
50
|
+
if [[ $curl_status -ne 0 ]]; then
|
|
51
|
+
embedding_failure="transport failed"
|
|
52
|
+
return 1
|
|
53
|
+
fi
|
|
54
|
+
case "$http_status" in
|
|
55
|
+
2??) return 0 ;;
|
|
56
|
+
401|403) embedding_failure="authorization rejected (HTTP $http_status)" ;;
|
|
57
|
+
404) embedding_failure="endpoint or model not found (HTTP 404)" ;;
|
|
58
|
+
408|429) embedding_failure="temporarily unavailable (HTTP $http_status)" ;;
|
|
59
|
+
5??) embedding_failure="upstream service failed (HTTP $http_status)" ;;
|
|
60
|
+
[0-9][0-9][0-9]) embedding_failure="request failed (HTTP $http_status)" ;;
|
|
61
|
+
*) embedding_failure="request failed" ;;
|
|
62
|
+
esac
|
|
63
|
+
return 1
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
echo "cairn doctor"
|
|
@@ -90,7 +106,7 @@ if [[ -n "${CAIRN_MEMORY_EMBEDDING_URL:-}" ]]; then
|
|
|
90
106
|
elif embedding_works; then
|
|
91
107
|
pass "embedding endpoint accepted model ${CAIRN_MEMORY_EMBEDDING_MODEL} (${CAIRN_MEMORY_EMBEDDING_URL})"
|
|
92
108
|
else
|
|
93
|
-
fail "embedding
|
|
109
|
+
fail "embedding ${embedding_failure} for model ${CAIRN_MEMORY_EMBEDDING_MODEL} (${CAIRN_MEMORY_EMBEDDING_URL})"
|
|
94
110
|
fi
|
|
95
111
|
else
|
|
96
112
|
skip "embedding endpoint (CAIRN_MEMORY_EMBEDDING_URL unset)"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
5
|
+
node --input-type=module - "$ROOT" <<'NODE'
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
|
|
11
|
+
const root = process.argv[2];
|
|
12
|
+
const { buildShardPlan, smokeChecks } = await import(
|
|
13
|
+
pathToFileURL(join(root, "mcp-memory-server", "scripts", "run-smoke-suite.mjs")));
|
|
14
|
+
const plan = buildShardPlan(3);
|
|
15
|
+
const flattened = plan.flatMap(({ checks }) => checks.map(({ name }) => name));
|
|
16
|
+
assert.equal(flattened.length, smokeChecks.length);
|
|
17
|
+
assert.equal(new Set(flattened).size, smokeChecks.length);
|
|
18
|
+
assert.deepEqual([...flattened].sort(), smokeChecks.map(({ name }) => name).sort());
|
|
19
|
+
assert.ok(plan.every(({ checks }) => checks.length > 0));
|
|
20
|
+
const weights = plan.map(({ weight }) => weight);
|
|
21
|
+
assert.ok(Math.max(...weights) - Math.min(...weights) <= 5,
|
|
22
|
+
`native Windows shards are imbalanced: ${weights.join(", ")}`);
|
|
23
|
+
|
|
24
|
+
const pkg = JSON.parse(readFileSync(join(root, "mcp-memory-server", "package.json"), "utf8"));
|
|
25
|
+
assert.match(pkg.scripts["test:smoke"], /run-smoke-suite\.mjs/);
|
|
26
|
+
const workflow = readFileSync(join(root, ".github", "workflows", "ci.yml"), "utf8");
|
|
27
|
+
assert.match(workflow, /name: native-windows \(\$\{\{ matrix\.node \}\}\)/);
|
|
28
|
+
for (const shard of ["1/3", "2/3", "3/3"]) assert.ok(workflow.includes(`shard: "${shard}"`));
|
|
29
|
+
assert.match(workflow, /run-smoke-suite\.mjs --shard \$\{\{ matrix\.shard \}\}/);
|
|
30
|
+
assert.doesNotMatch(workflow.match(/ native-windows:[\s\S]*?\n repository:/)?.[0] ?? "",
|
|
31
|
+
/npm --prefix mcp-memory-server test/);
|
|
32
|
+
console.log(`PASS: ${smokeChecks.length} memory-server contracts split once across balanced Windows shards (${weights.join(", ")})`);
|
|
33
|
+
NODE
|
package/scripts/test-doctor.sh
CHANGED
|
@@ -92,13 +92,15 @@ const server = createServer((request, response) => {
|
|
|
92
92
|
request.on("end", () => {
|
|
93
93
|
let body = {};
|
|
94
94
|
try { body = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch {}
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
const routeValid = request.url === "/v1/embeddings";
|
|
96
|
+
const keyValid = request.headers.authorization === "Bearer test-key";
|
|
97
|
+
const modelValid = body.model === "test-embedding";
|
|
98
|
+
const unauthorized = body.model === "unauthorized-model";
|
|
99
|
+
const status = routeValid && keyValid && modelValid ? 200 : unauthorized ? 403 : 404;
|
|
100
|
+
response.writeHead(status, { "Content-Type": "application/json" });
|
|
101
|
+
response.end(JSON.stringify(status === 200
|
|
100
102
|
? { data: [{ index: 0, embedding: [1, 0] }] }
|
|
101
|
-
: {
|
|
103
|
+
: { message: "private upstream detail must not leak", request_id: "private-request-id" }));
|
|
102
104
|
});
|
|
103
105
|
});
|
|
104
106
|
server.listen(0, "127.0.0.1", () => {
|
|
@@ -122,17 +124,33 @@ EOF
|
|
|
122
124
|
grep -q "\[PASS\] embedding endpoint accepted model test-embedding" "$tmp/out3" ||
|
|
123
125
|
fail "expected a functional embedding PASS line"
|
|
124
126
|
|
|
125
|
-
proj4="$tmp/embedding-
|
|
127
|
+
proj4="$tmp/embedding-unauthorized"; mkdir -p "$proj4/.ai"
|
|
126
128
|
cat > "$proj4/.ai/.env" <<EOF
|
|
127
129
|
CAIRN_MEMORY_EMBEDDING_URL=http://127.0.0.1:$port/v1
|
|
128
|
-
CAIRN_MEMORY_EMBEDDING_MODEL=
|
|
130
|
+
CAIRN_MEMORY_EMBEDDING_MODEL=unauthorized-model
|
|
129
131
|
CAIRN_LLM_API_KEY=test-key
|
|
130
132
|
EOF
|
|
131
133
|
if ( cd "$proj4" && "$doctor" ) >"$tmp/out4" 2>&1; then
|
|
132
|
-
fail "doctor should reject
|
|
134
|
+
fail "doctor should reject an unauthorized embedding request:\n$(cat "$tmp/out4")"
|
|
135
|
+
fi
|
|
136
|
+
grep -q "\[FAIL\] embedding authorization rejected (HTTP 403) for model unauthorized-model" "$tmp/out4" ||
|
|
137
|
+
fail "expected a sanitized embedding authorization diagnostic"
|
|
138
|
+
! grep -q "private upstream detail\|private-request-id" "$tmp/out4" ||
|
|
139
|
+
fail "embedding diagnostic leaked an upstream body or request id"
|
|
140
|
+
|
|
141
|
+
proj5="$tmp/embedding-missing-model"; mkdir -p "$proj5/.ai"
|
|
142
|
+
cat > "$proj5/.ai/.env" <<EOF
|
|
143
|
+
CAIRN_MEMORY_EMBEDDING_URL=http://127.0.0.1:$port/v1
|
|
144
|
+
CAIRN_MEMORY_EMBEDDING_MODEL=missing-model
|
|
145
|
+
CAIRN_LLM_API_KEY=test-key
|
|
146
|
+
EOF
|
|
147
|
+
if ( cd "$proj5" && "$doctor" ) >"$tmp/out5" 2>&1; then
|
|
148
|
+
fail "doctor should reject a missing embedding model:\n$(cat "$tmp/out5")"
|
|
133
149
|
fi
|
|
134
|
-
grep -q "\[FAIL\] embedding
|
|
135
|
-
fail "expected a
|
|
150
|
+
grep -q "\[FAIL\] embedding endpoint or model not found (HTTP 404) for model missing-model" "$tmp/out5" ||
|
|
151
|
+
fail "expected a sanitized missing embedding model diagnostic"
|
|
152
|
+
! grep -q "private upstream detail\|private-request-id" "$tmp/out5" ||
|
|
153
|
+
fail "missing-model diagnostic leaked an upstream body or request id"
|
|
136
154
|
fi
|
|
137
155
|
|
|
138
156
|
# 5. Artifact doctor passes a valid store, repairs only derived state, and
|