@cairnkeep/cli 2.22.1 → 2.22.2
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 +14 -0
- package/docs/context-packs.md +9 -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 +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/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 +2 -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 +10 -2
- package/docs/learning/video-scripts/V28-security-assurance.md +7 -0
- package/docs/security-assurance.md +1 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/context-pack-state.js +93 -0
- package/mcp-memory-server/dist/context-pack.js +14 -28
- package/mcp-memory-server/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable user-facing changes are documented here. This project follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.22.2] - 2026-10-07
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- Bind mutable private context-pack pointers, source records and graph-cache
|
|
13
|
+
reads to one opened descriptor. Reject replacement, non-private ownership,
|
|
14
|
+
symlink/special-file substitution, growth and invalid encoding under bounded
|
|
15
|
+
reads; close descriptors on success and failure. Update inspection uses the
|
|
16
|
+
same strict source-record parser as installation/listing.
|
|
17
|
+
- Add deterministic replacement and denial controls, including fabricated
|
|
18
|
+
pointer approvals, bounded parsing and descriptor cleanup. Keep schemas,
|
|
19
|
+
feature gates and approval policy unchanged; update recovery/security and
|
|
20
|
+
learning guidance. Existing security-analysis findings remain visible.
|
|
21
|
+
|
|
8
22
|
## [2.22.1] - 2026-10-07
|
|
9
23
|
|
|
10
24
|
### Fixed
|
package/docs/context-packs.md
CHANGED
|
@@ -69,6 +69,15 @@ If the bounded wait expires, let the active update finish, inspect `cairn doctor
|
|
|
69
69
|
for temporary remnants, and retry. A persistent lock is not proof of abandonment;
|
|
70
70
|
never delete it blindly or change ACLs to silence the error.
|
|
71
71
|
|
|
72
|
+
From 2.22.2, mutable private pointers, source records and graph-cache state are
|
|
73
|
+
opened once and read through that descriptor under their existing size limits.
|
|
74
|
+
Metadata, private ownership and path/parent identity are checked before and
|
|
75
|
+
after reading. Concurrent replacement or growth fails closed: retry after the
|
|
76
|
+
writer finishes, then inspect `cairn doctor` if the problem persists. Malformed
|
|
77
|
+
state is reported without quoting private JSON content. Missing state is not
|
|
78
|
+
an excuse to follow a dangling symlink or infer skill approval. These controls
|
|
79
|
+
do not protect against a hostile process with the same account's full access.
|
|
80
|
+
|
|
72
81
|
## Retrieval and skill approval
|
|
73
82
|
|
|
74
83
|
Set `CAIRN_CONTEXT_PACKS=1` and restart the memory server to register the
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Status:** Ready
|
|
4
4
|
**Track:** Quickstart
|
|
5
5
|
**Time:** 15 minutes
|
|
6
|
-
**Tested with:** Cairnkeep 2.22.
|
|
6
|
+
**Tested with:** Cairnkeep 2.22.2
|
|
7
7
|
|
|
8
8
|
## Outcome
|
|
9
9
|
|
|
@@ -26,7 +26,7 @@ ephemeral; the named course volume is persistent.
|
|
|
26
26
|
1. Pull the public image:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
podman pull ghcr.io/cairnkeep/cairnkeep:2.22.
|
|
29
|
+
podman pull ghcr.io/cairnkeep/cairnkeep:2.22.2
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
2. Start the stdio server with a read-only root filesystem and no Linux
|
|
@@ -38,7 +38,7 @@ ephemeral; the named course volume is persistent.
|
|
|
38
38
|
--read-only --cap-drop=all --security-opt=no-new-privileges \
|
|
39
39
|
--tmpfs=/tmp:rw,noexec,nosuid,size=64m,mode=1777 \
|
|
40
40
|
--volume cairnkeep-course-data:/data:Z,U \
|
|
41
|
-
ghcr.io/cairnkeep/cairnkeep:2.22.
|
|
41
|
+
ghcr.io/cairnkeep/cairnkeep:2.22.2 stdio
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
3. The process waits quietly for MCP input. Press `Ctrl-C` after confirming it
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Status:** Ready
|
|
4
4
|
**Track:** Quickstart
|
|
5
5
|
**Time:** 25 minutes
|
|
6
|
-
**Tested with:** Cairnkeep 2.22.
|
|
6
|
+
**Tested with:** Cairnkeep 2.22.2 and Node.js 22 or newer
|
|
7
7
|
|
|
8
8
|
## Outcome
|
|
9
9
|
|
|
@@ -47,7 +47,7 @@ sync. Codex project trust remains an explicit operator decision.
|
|
|
47
47
|
2. Install Cairnkeep:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
npm install --global @cairnkeep/cli@2.22.
|
|
50
|
+
npm install --global @cairnkeep/cli@2.22.2
|
|
51
51
|
cairn --version
|
|
52
52
|
```
|
|
53
53
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Status:** Ready
|
|
4
4
|
**Track:** Operator
|
|
5
5
|
**Time:** 30 minutes
|
|
6
|
-
**Tested with:** Cairnkeep 2.22.
|
|
6
|
+
**Tested with:** Cairnkeep 2.22.2, Windows x64, and Node.js 22 or newer
|
|
7
7
|
|
|
8
8
|
## Outcome
|
|
9
9
|
|
|
@@ -16,7 +16,7 @@ perform a reversible uninstall without relying on WSL or Git Bash.
|
|
|
16
16
|
From PowerShell:
|
|
17
17
|
|
|
18
18
|
```powershell
|
|
19
|
-
npm install --global @cairnkeep/cli@2.22.
|
|
19
|
+
npm install --global @cairnkeep/cli@2.22.2
|
|
20
20
|
cairn version
|
|
21
21
|
|
|
22
22
|
$Lab = Join-Path $env:TEMP 'Cairnkeep Windows Lab'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# L28 - Verify Cairnkeep's security posture
|
|
2
2
|
|
|
3
3
|
**Status:** Ready
|
|
4
|
-
**Tested with:** Cairnkeep 2.22.
|
|
4
|
+
**Tested with:** Cairnkeep 2.22.2 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.22.
|
|
14
|
+
- Install Cairnkeep 2.22.2 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.
|
|
@@ -67,6 +67,7 @@ npm run security:baseline
|
|
|
67
67
|
scripts/test-security-baseline.sh
|
|
68
68
|
npm --prefix mcp-memory-server run check:security-assurance
|
|
69
69
|
npm --prefix mcp-memory-server run check:http-guard
|
|
70
|
+
node mcp-memory-server/scripts/smoke-context-pack-state.mjs
|
|
70
71
|
```
|
|
71
72
|
|
|
72
73
|
The baseline rejects unpinned external Actions, `pull_request_target` and
|
|
@@ -83,6 +84,13 @@ Do not republish an immutable version to demonstrate the test.
|
|
|
83
84
|
The smoke tests cover token non-disclosure, request bounds, and deterministic
|
|
84
85
|
portable-path adversarial cases.
|
|
85
86
|
|
|
87
|
+
The pack-state control deterministically substitutes a second valid pointer
|
|
88
|
+
after metadata inspection. The descriptor-bound reader rejects it, including
|
|
89
|
+
fabricated approvals; growth, unsafe types and non-private state are rejected
|
|
90
|
+
under bounded reads. A resolved review thread or green CodeQL job does not mean
|
|
91
|
+
all historical alerts are fixed: inspect current findings and test the actual
|
|
92
|
+
boundary without dismissing results merely to improve an advisory score.
|
|
93
|
+
|
|
86
94
|
## Common failures
|
|
87
95
|
|
|
88
96
|
- A private `.ai` file is group/world accessible on POSIX: restore mode `0600`.
|
|
@@ -62,6 +62,13 @@ Windows contention/denial and unsafe-lock cases deterministically, then checks
|
|
|
62
62
|
real cross-process exclusion. Explain bounded retry versus lock stealing; a
|
|
63
63
|
Linux run with injected Windows errors is not native-Windows evidence.
|
|
64
64
|
|
|
65
|
+
Run `node mcp-memory-server/scripts/smoke-context-pack-state.mjs` next. Explain
|
|
66
|
+
check-then-reopen races with two synthetic pointer files: a valid metadata
|
|
67
|
+
check must not authorize reading a replacement file. Show descriptor-bound
|
|
68
|
+
rejection, bounded growth and cleanup; use no real private pointer/approval
|
|
69
|
+
data. Distinguish resolved old review comments from current analysis findings,
|
|
70
|
+
and do not imply a green CI run certifies an empty security backlog.
|
|
71
|
+
|
|
65
72
|
Briefly open `.github/workflows/publish.yml` beside the permission table in
|
|
66
73
|
`docs/releasing.md`. Contrast the read-only preparation job with the necessary
|
|
67
74
|
npm and OCI publication grants. Explain that explicit maps deny unspecified
|
|
@@ -46,6 +46,7 @@ be enabled and evaluated independently.
|
|
|
46
46
|
|---|---|---|
|
|
47
47
|
| Traversal, alternate separators, drive paths, ADS, forbidden Windows characters/device aliases, invalid Unicode, or case collisions in portable data | One cross-platform path validator is used by context packs, OKF, artifacts, and evaluations; deterministic adversarial cases run in the smoke suite | Filesystems can have additional locale- or mount-specific semantics; containment checks remain required at every write |
|
|
48
48
|
| Symlink or special-file substitution | Canonical roots, non-symlink regular-file checks, atomic replacement, and existing symlink/race regression tests | A hostile process running as the same account can race or replace resources outside Cairnkeep's process boundary |
|
|
49
|
+
| Replaced or growing private pack state between inspection and use | One descriptor for metadata and bounded content reads; name/parent identity rechecks, private ownership and encoding validation; deterministic replacement/denial controls | Concurrent legitimate replacement can require retry; these checks are not per-user ACLs or a defense against an account with full local control |
|
|
49
50
|
| Concurrent context-pack project pointer updates | Exclusive directory locks, bounded Windows contention retries, unsafe-lock rejection, injected denial controls and native cross-process exclusion tests | A persistent lock can block progress; recovery never infers abandonment or steals the lock |
|
|
50
51
|
| Unauthenticated or rebound HTTP access | HTTP refuses to start without a token, compares it in constant time, validates `Host`, denies CORS by default, and bounds headers, request time, keep-alive time, and bodies to 8 MiB | Bearer tokens are not user identities; deploy TLS at the ingress for non-loopback traffic |
|
|
51
52
|
| Excessive MCP authority | Explicit MCP annotations, restrictive profiles, capability intersection, and separate HTTP consent for sensitive optional tools | The default profile remains `full` for compatibility; operators should choose `read-only` or `custom` for observation-only clients |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"identifier":"cairn-offline-fake-v1","package_version":"2.22.
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.22.2","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { posixPrivateMetadataIsSafe, privatePathIsSafe } from "./platform-security.js";
|
|
4
|
+
function sameFile(left, right) {
|
|
5
|
+
return right.isFile() && !right.isSymbolicLink()
|
|
6
|
+
&& left.dev === right.dev && left.ino === right.ino && left.size === right.size
|
|
7
|
+
&& left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs
|
|
8
|
+
&& left.uid === right.uid && left.mode === right.mode;
|
|
9
|
+
}
|
|
10
|
+
// Mutable private pack state must be inspected and read through one opened
|
|
11
|
+
// descriptor. Pathname checks alone cannot authorize a later pathname read.
|
|
12
|
+
export function readPrivatePackJson(path, options) {
|
|
13
|
+
const { label, maxBytes } = options;
|
|
14
|
+
let descriptor;
|
|
15
|
+
try {
|
|
16
|
+
const nonBlocking = process.platform === "win32" ? 0 : (constants.O_NONBLOCK ?? 0);
|
|
17
|
+
descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | nonBlocking);
|
|
18
|
+
const opened = fstatSync(descriptor);
|
|
19
|
+
if (!opened.isFile() || opened.size > maxBytes
|
|
20
|
+
|| (process.platform !== "win32" && !posixPrivateMetadataIsSafe(opened))) {
|
|
21
|
+
throw new Error(`${label} is unsafe or too large.`);
|
|
22
|
+
}
|
|
23
|
+
const parent = lstatSync(dirname(path));
|
|
24
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || !sameFile(opened, lstatSync(path))) {
|
|
25
|
+
throw new Error(`${label} is unsafe or changed.`);
|
|
26
|
+
}
|
|
27
|
+
if (process.platform === "win32") {
|
|
28
|
+
if (!privatePathIsSafe(path))
|
|
29
|
+
throw new Error(`${label} is unsafe.`);
|
|
30
|
+
if (!sameFile(opened, lstatSync(path)))
|
|
31
|
+
throw new Error(`${label} changed during ACL inspection.`);
|
|
32
|
+
}
|
|
33
|
+
const buffer = Buffer.allocUnsafe(opened.size + 1);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
while (offset < buffer.byteLength) {
|
|
36
|
+
const count = readSync(descriptor, buffer, offset, buffer.byteLength - offset, null);
|
|
37
|
+
if (count === 0)
|
|
38
|
+
break;
|
|
39
|
+
offset += count;
|
|
40
|
+
}
|
|
41
|
+
const after = fstatSync(descriptor);
|
|
42
|
+
const named = lstatSync(path);
|
|
43
|
+
const parentAfter = lstatSync(dirname(path));
|
|
44
|
+
if (!sameFile(opened, after) || !sameFile(opened, named) || offset !== after.size
|
|
45
|
+
|| parentAfter.isSymbolicLink() || !parentAfter.isDirectory()
|
|
46
|
+
|| parent.dev !== parentAfter.dev || parent.ino !== parentAfter.ino) {
|
|
47
|
+
throw new Error(`${label} changed while it was being read.`);
|
|
48
|
+
}
|
|
49
|
+
let text;
|
|
50
|
+
try {
|
|
51
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, offset));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`${label} contains invalid UTF-8.`);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(text);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`${label} contains invalid JSON.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (descriptor === undefined && options.optional && error.code === "ENOENT") {
|
|
65
|
+
// A dangling link is unsafe state, not an absent project/source.
|
|
66
|
+
try {
|
|
67
|
+
lstatSync(path);
|
|
68
|
+
throw new Error(`${label} is unsafe or changed.`);
|
|
69
|
+
}
|
|
70
|
+
catch (missingError) {
|
|
71
|
+
if (missingError.code !== "ENOENT")
|
|
72
|
+
throw missingError;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const parent = lstatSync(dirname(path));
|
|
76
|
+
if (!parent.isDirectory() || parent.isSymbolicLink())
|
|
77
|
+
throw new Error(`${label} directory is unsafe.`);
|
|
78
|
+
}
|
|
79
|
+
catch (parentError) {
|
|
80
|
+
if (parentError.code !== "ENOENT")
|
|
81
|
+
throw parentError;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
if (error instanceof Error && error.message.startsWith(`${label} `))
|
|
86
|
+
throw error;
|
|
87
|
+
throw new Error(`${label} is unsafe or unreadable.`);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
if (descriptor !== undefined)
|
|
91
|
+
closeSync(descriptor);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { constants, existsSync, lstatSync,
|
|
3
|
+
import { constants, existsSync, lstatSync, realpathSync } from "node:fs";
|
|
4
4
|
import { chmod, mkdir, mkdtemp, open, readdir, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -9,9 +9,10 @@ import { z } from "zod";
|
|
|
9
9
|
import { EmbeddingCache, cosineSimilarity, embedTexts, getEmbeddingConfig, hashText } from "./embeddings.js";
|
|
10
10
|
import { doctorProgressiveContextCache, loadProgressiveContext, publicTree, selectHierarchicalFiles, } from "./context-pack-retrieval.js";
|
|
11
11
|
import { indexOkfBundle, validateOkfBundle, validatedOkfFileContent } from "./okf.js";
|
|
12
|
-
import { atomicReplace, hardenPrivatePath
|
|
12
|
+
import { atomicReplace, hardenPrivatePath } from "./platform-security.js";
|
|
13
13
|
import { portablePathCollisionKey, portableRelativePathIssue } from "./path-security.js";
|
|
14
14
|
import { acquireContextPackPointerLock } from "./context-pack-lock.js";
|
|
15
|
+
import { readPrivatePackJson } from "./context-pack-state.js";
|
|
15
16
|
const execFileAsync = promisify(execFile);
|
|
16
17
|
export const CONTEXT_PACK_MANIFEST = "context-pack.json";
|
|
17
18
|
export const CONTEXT_PACK_MAX_FILE_BYTES = 1024 * 1024;
|
|
@@ -78,15 +79,9 @@ function assertUtf8(bytes, path) {
|
|
|
78
79
|
function readPackSource(digest) {
|
|
79
80
|
const directory = join(packBaseDir(), "sources");
|
|
80
81
|
const path = join(directory, `${digest}.json`);
|
|
81
|
-
|
|
82
|
+
const value = readPrivatePackJson(path, { label: "Context pack source record", maxBytes: 64 * 1024, optional: true });
|
|
83
|
+
if (value === undefined)
|
|
82
84
|
return undefined;
|
|
83
|
-
const directoryInfo = lstatSync(directory);
|
|
84
|
-
const info = lstatSync(path);
|
|
85
|
-
if (!directoryInfo.isDirectory() || directoryInfo.isSymbolicLink() || !info.isFile() || info.isSymbolicLink()
|
|
86
|
-
|| info.size > 64 * 1024 || !privatePathIsSafe(path)) {
|
|
87
|
-
throw new Error(`Context pack source record is unsafe: ${digest}`);
|
|
88
|
-
}
|
|
89
|
-
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
90
85
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
91
86
|
throw new Error(`Context pack source record is invalid: ${digest}`);
|
|
92
87
|
const record = value;
|
|
@@ -604,13 +599,8 @@ function parseProjectPointer(value, expectedId) {
|
|
|
604
599
|
}
|
|
605
600
|
export function readProjectPointer(options) {
|
|
606
601
|
const identity = projectIdentity(options);
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const info = lstatSync(identity.path);
|
|
610
|
-
if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024
|
|
611
|
-
|| !privatePathIsSafe(identity.path))
|
|
612
|
-
throw new Error("Context pack project pointer is unsafe.");
|
|
613
|
-
return parseProjectPointer(JSON.parse(readFileSync(identity.path, "utf8")), identity.id);
|
|
602
|
+
const value = readPrivatePackJson(identity.path, { label: "Context pack project pointer", maxBytes: 1024 * 1024, optional: true });
|
|
603
|
+
return value === undefined ? emptyPointer(identity.id) : parseProjectPointer(value, identity.id);
|
|
614
604
|
}
|
|
615
605
|
async function acquirePointerLock(options) {
|
|
616
606
|
const identity = projectIdentity(options);
|
|
@@ -659,15 +649,10 @@ async function allPointers() {
|
|
|
659
649
|
throw new Error("Context pack project pointer directory is unsafe.");
|
|
660
650
|
const result = [];
|
|
661
651
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
662
|
-
if (!entry.
|
|
652
|
+
if (!entry.name.endsWith(".json"))
|
|
663
653
|
continue;
|
|
664
654
|
const path = join(directory, entry.name);
|
|
665
|
-
const
|
|
666
|
-
if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024
|
|
667
|
-
|| !privatePathIsSafe(path)) {
|
|
668
|
-
throw new Error(`Context pack project pointer is unsafe: ${path}`);
|
|
669
|
-
}
|
|
670
|
-
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
655
|
+
const raw = readPrivatePackJson(path, { label: "Context pack project pointer", maxBytes: 1024 * 1024 });
|
|
671
656
|
const value = raw;
|
|
672
657
|
if (typeof value.project_id !== "string")
|
|
673
658
|
throw new Error("Context pack project pointer is invalid.");
|
|
@@ -1149,14 +1134,14 @@ export async function doctorContextPacks(options = {}) {
|
|
|
1149
1134
|
for (const entry of await readdir(graphDirectory, { withFileTypes: true })) {
|
|
1150
1135
|
const match = /^([a-f0-9]{64})\.json$/.exec(entry.name);
|
|
1151
1136
|
const path = join(graphDirectory, entry.name);
|
|
1152
|
-
if (!entry.isFile() || entry.isSymbolicLink() || !match
|
|
1137
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !match) {
|
|
1153
1138
|
issues.push(`Invalid context pack graph cache entry: ${entry.name}`);
|
|
1154
1139
|
continue;
|
|
1155
1140
|
}
|
|
1156
1141
|
if (!existsSync(objectRoot(match[1])))
|
|
1157
1142
|
issues.push(`Orphaned context pack graph cache: ${entry.name}`);
|
|
1158
1143
|
try {
|
|
1159
|
-
const cached =
|
|
1144
|
+
const cached = readPrivatePackJson(path, { label: "Context pack graph cache", maxBytes: CONTEXT_PACK_MAX_FILE_BYTES });
|
|
1160
1145
|
if (cached.schema_version !== 1 || !Array.isArray(cached.files) || !Array.isArray(cached.diagnostics))
|
|
1161
1146
|
throw new Error("invalid graph cache schema");
|
|
1162
1147
|
const expected = expectedGraphs.get(match[1]);
|
|
@@ -1212,8 +1197,9 @@ export async function inspectContextPackUpdate(selector, options) {
|
|
|
1212
1197
|
const enabled = pointer.enabled.find((entry) => entry.id === selector || entry.digest === selector || entry.digest.startsWith(selector));
|
|
1213
1198
|
if (!enabled)
|
|
1214
1199
|
throw new Error("Context pack is not enabled for this project.");
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1200
|
+
const source = readPackSource(enabled.digest);
|
|
1201
|
+
if (!source)
|
|
1202
|
+
throw new Error("Context pack source record is missing.");
|
|
1217
1203
|
const current = await validateContextPack(objectRoot(enabled.digest));
|
|
1218
1204
|
const materialized = await materializeSource(source.kind === "local" ? source.path : source.url, source.kind === "git" ? source.ref : undefined);
|
|
1219
1205
|
let prepared;
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"check:graph-cli": "node scripts/smoke-graph-cli.mjs",
|
|
52
52
|
"check:mcp-trust": "node scripts/smoke-mcp-trust.mjs",
|
|
53
53
|
"check:pi-mcp-bridge": "node scripts/smoke-pi-mcp-bridge.mjs",
|
|
54
|
-
"check:context-pack": "node scripts/smoke-context-pack-lock.mjs && node scripts/smoke-context-pack.mjs",
|
|
54
|
+
"check:context-pack": "node scripts/smoke-context-pack-state.mjs && node scripts/smoke-context-pack-lock.mjs && node scripts/smoke-context-pack.mjs",
|
|
55
55
|
"check:context-pack-retrieval": "node scripts/smoke-context-pack-retrieval.mjs",
|
|
56
56
|
"check:context-pack-v216-compat": "node scripts/smoke-context-pack-v216-compat.mjs",
|
|
57
57
|
"check:context-usage": "node scripts/smoke-context-usage.mjs",
|