pi-session-mcp 0.14.0 → 0.14.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/.pi-session-mcp-cli/writer-boundary.js +2 -1
- package/.pi-session-mcp-cli/writer-lifecycle.js +99 -18
- package/.pi-session-mcp-cli/writer-models-store.js +64 -0
- package/.pi-session-mcp-cli/writer-tree-removal.js +75 -0
- package/CHANGELOG.md +95 -0
- package/README.md +1 -1
- package/dist/capabilities.d.ts +13 -0
- package/dist/capabilities.js +17 -13
- package/dist/model-catalog.d.ts +18 -5
- package/dist/model-catalog.js +8 -5
- package/dist/sdk-pi-adapter.d.ts +6 -1
- package/dist/sdk-pi-adapter.js +35 -7
- package/dist/writer-boundary.d.ts +1 -1
- package/dist/writer-boundary.js +2 -1
- package/dist/writer-guard.js +1 -1
- package/dist/writer-launch.d.ts +2 -0
- package/dist/writer-launch.js +3 -0
- package/dist/writer-lifecycle.d.ts +8 -0
- package/dist/writer-lifecycle.js +99 -18
- package/dist/writer-models-store.d.ts +16 -0
- package/dist/writer-models-store.js +64 -0
- package/dist/writer-relay.js +12 -1
- package/dist/writer-tree-removal.d.ts +16 -0
- package/dist/writer-tree-removal.js +75 -0
- package/docs/acceptance.md +314 -0
- package/docs/adr/0012-leased-writer-pool.md +10 -6
- package/docs/adr/0013-hardened-read-only-profiles.md +30 -0
- package/docs/adr/0014-profile-probe-and-model-catalog.md +1 -1
- package/docs/doctor-design.md +1 -1
- package/docs/execution-modes.md +66 -10
- package/docs/installation.md +9 -0
- package/docs/isolation-setup.md +12 -2
- package/docs/roadmap.md +21 -0
- package/docs/sprints/2026-10-24-s8-writer-migration.md +5 -3
- package/docs/sprints/2026-10-29-s12-audit-fixes.md +174 -0
- package/docs/sprints/2026-10-30-s13-index-only-work.md +199 -0
- package/docs/threat-model.md +10 -1
- package/docs/tool-contracts.md +1 -1
- package/package.json +1 -1
- package/skills/pi-session-orchestration/SKILL.md +1 -1
- package/skills/pi-session-profiles/SKILL.md +5 -1
|
@@ -6,7 +6,8 @@ import { CONTAINER_EVENTS_DIR, EVENTS_DIR_ENV, writerEventsDir } from "./turn-ev
|
|
|
6
6
|
import { writerContainerName, writerLabel } from "./writer-fence.js";
|
|
7
7
|
export const NODE_IMAGE_REF = "docker.io/library/node@sha256:c4d5523090a817b7aa86d2111241fdd4f66d1e27782b44160e6aa63b357ecb2d";
|
|
8
8
|
export const IMAGE_PULL_TIMEOUT_MS = 900_000;
|
|
9
|
-
|
|
9
|
+
// Replace refs in a clone are worker-controlled: they could make unexported work look reachable or clean (#193).
|
|
10
|
+
export const BOUNDARY_GIT = "git -c safe.directory=* -c core.fsmonitor=false -c core.hooksPath=/dev/null -c core.useReplaceRefs=false";
|
|
10
11
|
export class BoundaryError extends Error {
|
|
11
12
|
code;
|
|
12
13
|
constructor(code) {
|
|
@@ -3,12 +3,15 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { chmod, mkdir, open, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { writerLimits } from "./writer-limits.js";
|
|
6
|
+
import { removeWriterTree } from "./writer-tree-removal.js";
|
|
7
|
+
import { copyModelsStore, hostModelsStorePath } from "./writer-models-store.js";
|
|
6
8
|
import { configSchema } from "./config.js";
|
|
7
9
|
import { renderWrapper } from "./writer-wrapper.js";
|
|
8
10
|
import { builtinSkillName } from "./skill-catalog.js";
|
|
9
11
|
import { resolveLibrarySkill, shippedLibrarySkill } from "./skill-library.js";
|
|
10
12
|
import { BOUNDARY_GIT, BoundaryError, IMAGE_PULL_TIMEOUT_MS, isSafeRefName, podmanInfo, resolveImage, throwawayArgs, verifyCloneLayout } from "./writer-boundary.js";
|
|
11
13
|
import { readWriterState, writerName, writerPaths, writeWriterState, WriterStateError } from "./writer-state.js";
|
|
14
|
+
const modelsStoreSource = (ctx) => ctx.modelsStorePath ?? hostModelsStorePath(ctx.home);
|
|
12
15
|
function leasedContext(ctx) {
|
|
13
16
|
if (ctx.leaseFd === undefined)
|
|
14
17
|
return ctx;
|
|
@@ -292,7 +295,7 @@ export async function provisionWriter(ctx, input) {
|
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
if (restart || existing?.phase === "provisioning")
|
|
295
|
-
await
|
|
298
|
+
await removeWriterTree(paths.root);
|
|
296
299
|
// With spool delivery the spool directory exists from provisioning on: launch would otherwise add it to the
|
|
297
300
|
// writer root, a host directory the acceptance harness digests around its own first launch.
|
|
298
301
|
const spool = config.events?.delivery === "spool" ? [paths.events] : [];
|
|
@@ -312,6 +315,7 @@ export async function provisionWriter(ctx, input) {
|
|
|
312
315
|
await jsonFile(join(paths.config, "execution-modes.json"), { schemaVersion: 1, workspaces: { [ctx.alias]: { mode: "host", acceptHostWrites: true } } });
|
|
313
316
|
await jsonFile(join(paths.config, "writer-guard.json"), { schemaVersion: 1, workspace: ctx.alias, baseRef: state.base.ref, baseCommit: commit });
|
|
314
317
|
await jsonFile(join(paths.agent, "auth.json"), {});
|
|
318
|
+
await copyModelsStore(modelsStoreSource(ctx), paths.agent);
|
|
315
319
|
await writeFile(paths.wrapper, renderWrapper(ctx.nodePath, ctx.releaseRoot, name), { mode: 0o700, flag: "wx" });
|
|
316
320
|
await writeWriterState(ctx.home, name, { ...state, phase: "not_ready" });
|
|
317
321
|
return { code: "writer_provisioned", status: "ok" };
|
|
@@ -320,6 +324,21 @@ export async function provisionWriter(ctx, input) {
|
|
|
320
324
|
return failure(error);
|
|
321
325
|
}
|
|
322
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Distinct object names among `objects` that the operator checkout does not have. Objects that an operator ref
|
|
329
|
+
* names need no further call; each remaining one costs one `cat-file`, since the runner has no stdin for a batch.
|
|
330
|
+
*/
|
|
331
|
+
async function missingObjects(ctx, objects) {
|
|
332
|
+
if (objects.length === 0)
|
|
333
|
+
return [];
|
|
334
|
+
const known = new Set((await git(ctx, ["for-each-ref", "--format=%(objectname)"], true))?.split("\n"));
|
|
335
|
+
const missing = [];
|
|
336
|
+
for (const object of new Set(objects)) {
|
|
337
|
+
if (!known.has(object) && await git(ctx, ["cat-file", "-e", object], true) === undefined)
|
|
338
|
+
missing.push(object);
|
|
339
|
+
}
|
|
340
|
+
return missing;
|
|
341
|
+
}
|
|
323
342
|
export async function inspectWork(ctx, name) {
|
|
324
343
|
ctx = leasedContext(ctx);
|
|
325
344
|
const state = await readWriterState(ctx.home, name);
|
|
@@ -327,25 +346,40 @@ export async function inspectWork(ctx, name) {
|
|
|
327
346
|
throw new LifecycleError("writer_state_invalid");
|
|
328
347
|
const paths = writerPaths(ctx.home, name);
|
|
329
348
|
await verifyCloneLayout(paths.clone);
|
|
330
|
-
const script = `set -eu\nexport GIT_CONFIG_NOSYSTEM=1\ncp .git/index /tmp/index\nmkdir /tmp/objects\nexport GIT_INDEX_FILE=/tmp/index GIT_OBJECT_DIRECTORY=/tmp/objects GIT_ALTERNATE_OBJECT_DIRECTORIES="$1/.git/objects"\n${BOUNDARY_GIT} add -A\n${BOUNDARY_GIT} rev-parse --verify HEAD\n${BOUNDARY_GIT} for-each-ref --format='%(refname) %(objectname)' refs/
|
|
349
|
+
const script = `set -eu\nexport GIT_CONFIG_NOSYSTEM=1\ncp .git/index /tmp/index\ncp .git/index /tmp/staged-index\nmkdir /tmp/objects\nexport GIT_INDEX_FILE=/tmp/index GIT_OBJECT_DIRECTORY=/tmp/objects GIT_ALTERNATE_OBJECT_DIRECTORIES="$1/.git/objects"\nstaged=$(GIT_INDEX_FILE=/tmp/staged-index ${BOUNDARY_GIT} write-tree)\n${BOUNDARY_GIT} add -A\n${BOUNDARY_GIT} rev-parse --verify HEAD\n${BOUNDARY_GIT} for-each-ref --format='%(refname) %(objectname)'\nif stash=$(${BOUNDARY_GIT} rev-parse -q --verify refs/stash); then\n log=$(${BOUNDARY_GIT} rev-list --walk-reflogs refs/stash)\n printf 'stash %s\\n' "$stash" $log\nfi\n${BOUNDARY_GIT} write-tree\nprintf '%s\\n' "$staged"\n${BOUNDARY_GIT} rev-parse 'HEAD^{tree}'\n${BOUNDARY_GIT} rev-parse "$2^{tree}"`;
|
|
331
350
|
const result = await ctx.runner.run("podman", throwawayArgs(name, paths, state.imageId, { clone: "ro", bundles: false }, script, [paths.clone, state.base.commit]), AbortSignal.timeout(IMAGE_PULL_TIMEOUT_MS));
|
|
332
351
|
if (result.exitCode !== 0)
|
|
333
352
|
throw new LifecycleError("writer_inspect_failed");
|
|
334
353
|
const lines = result.stdout.trim().split("\n");
|
|
335
354
|
const hash = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
336
|
-
const head = lines.shift(), baseTree = lines.pop(), headTree = lines.pop(), tree = lines.pop();
|
|
337
|
-
if (![head, baseTree, headTree, tree].every((value) => value !== undefined && hash.test(value)))
|
|
355
|
+
const head = lines.shift(), baseTree = lines.pop(), headTree = lines.pop(), indexTree = lines.pop(), tree = lines.pop();
|
|
356
|
+
if (![head, baseTree, headTree, indexTree, tree].every((value) => value !== undefined && hash.test(value)))
|
|
338
357
|
throw new LifecycleError("writer_inspect_failed");
|
|
339
|
-
const refs =
|
|
358
|
+
const refs = [], stashes = [];
|
|
359
|
+
for (const line of lines) {
|
|
340
360
|
const [name, commit, extra] = line.split(" ");
|
|
341
|
-
if (!
|
|
361
|
+
if (!commit || !hash.test(commit) || extra !== undefined)
|
|
362
|
+
throw new LifecycleError("writer_inspect_failed");
|
|
363
|
+
if (name === "stash")
|
|
364
|
+
stashes.push(commit);
|
|
365
|
+
// Only branch names reach a command (as export names); other refs such as a tag `pkg@1.0.0+build` are
|
|
366
|
+
// only compared, so any ref name Git accepts must not make inspection fail.
|
|
367
|
+
else if (name && (name.startsWith("refs/heads/") ? isSafeRefName(name) : name.startsWith("refs/")))
|
|
368
|
+
refs.push({ name, commit });
|
|
369
|
+
else
|
|
342
370
|
throw new LifecycleError("writer_inspect_failed");
|
|
343
|
-
|
|
344
|
-
});
|
|
371
|
+
}
|
|
372
|
+
const branches = refs.filter(({ name }) => name.startsWith("refs/heads/"));
|
|
373
|
+
// Refs outside branches and the stash are not exported (#202): they must name objects the operator
|
|
374
|
+
// checkout already has, or the clone holds work that only it can recover.
|
|
375
|
+
const others = refs.filter(({ name }) => !name.startsWith("refs/heads/") && name !== "refs/stash");
|
|
376
|
+
const unknownRefs = (await missingObjects(ctx, others.map(({ commit }) => commit))).length;
|
|
345
377
|
return {
|
|
346
|
-
baseline: head === state.base.commit &&
|
|
347
|
-
|
|
348
|
-
|
|
378
|
+
baseline: head === state.base.commit && branches.length === 1 && branches[0].name === state.base.ref && branches[0].commit === state.base.commit
|
|
379
|
+
&& !refs.some(({ name }) => name === "refs/stash") && stashes.length === 0 && unknownRefs === 0 && tree === baseTree && indexTree === baseTree,
|
|
380
|
+
dirty: tree !== headTree || indexTree !== headTree, head: head, branches, stashes, unknownRefs,
|
|
381
|
+
indexTree: indexTree, tree: tree, headTree: headTree,
|
|
382
|
+
digest: `sha256:${createHash("sha256").update("inspect-v2\n").update(result.stdout).digest("hex")}`,
|
|
349
383
|
};
|
|
350
384
|
}
|
|
351
385
|
async function requiredState(ctx, name) {
|
|
@@ -379,6 +413,7 @@ async function refreshPrivateFiles(ctx, name, state) {
|
|
|
379
413
|
await rm(path, { force: true });
|
|
380
414
|
await jsonFile(join(paths.config, "writer-guard.json"), { schemaVersion: 1, workspace: ctx.alias, baseRef: state.base.ref, baseCommit: state.base.commit });
|
|
381
415
|
await jsonFile(join(paths.agent, "auth.json"), {});
|
|
416
|
+
await copyModelsStore(modelsStoreSource(ctx), paths.agent);
|
|
382
417
|
}
|
|
383
418
|
export async function collectWriter(ctx, name) {
|
|
384
419
|
ctx = leasedContext(ctx);
|
|
@@ -403,19 +438,49 @@ ${BOUNDARY_GIT} for-each-ref --format='%(refname) %(objectname)' refs/heads | wh
|
|
|
403
438
|
${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "$ref" "$commit"
|
|
404
439
|
fi
|
|
405
440
|
done
|
|
441
|
+
# Every stash entry, newest first as in stash@{n}, under a derived name (#202). The tip covers a missing reflog.
|
|
442
|
+
if stash=$(${BOUNDARY_GIT} rev-parse -q --verify refs/stash); then
|
|
443
|
+
log=$(${BOUNDARY_GIT} rev-list --walk-reflogs refs/stash)
|
|
444
|
+
n=0 previous=
|
|
445
|
+
for commit in "$stash" $log; do
|
|
446
|
+
if [ "$commit" = "$previous" ]; then continue; fi
|
|
447
|
+
previous=$commit
|
|
448
|
+
if ${BOUNDARY_GIT} --git-dir=/tmp/scratch show-ref --verify --quiet "refs/heads/$4-stash-$n" \\
|
|
449
|
+
|| ! ${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$4-stash-$n" "$commit" 2>/dev/null; then
|
|
450
|
+
printf '%s\\n' ref_name_refused
|
|
451
|
+
fi
|
|
452
|
+
n=$((n + 1))
|
|
453
|
+
done
|
|
454
|
+
fi
|
|
406
455
|
cp .git/index /tmp/index
|
|
456
|
+
cp .git/index /tmp/staged-index
|
|
407
457
|
export GIT_INDEX_FILE=/tmp/index GIT_OBJECT_DIRECTORY=/tmp/scratch/objects
|
|
458
|
+
staged=$(GIT_INDEX_FILE=/tmp/staged-index ${BOUNDARY_GIT} write-tree)
|
|
408
459
|
${BOUNDARY_GIT} add -A
|
|
409
460
|
tree=$(${BOUNDARY_GIT} write-tree)
|
|
461
|
+
export GIT_AUTHOR_NAME=pi-writer GIT_AUTHOR_EMAIL=pi-writer@example.invalid
|
|
462
|
+
export GIT_COMMITTER_NAME=pi-writer GIT_COMMITTER_EMAIL=pi-writer@example.invalid
|
|
463
|
+
if [ "$staged" != "$(${BOUNDARY_GIT} rev-parse 'HEAD^{tree}')" ] && [ "$staged" != "$tree" ]; then
|
|
464
|
+
commit=$(printf '%s\\n' 'Index collected by pi-session-mcp' | ${BOUNDARY_GIT} commit-tree "$staged" -p HEAD)
|
|
465
|
+
branch=$(${BOUNDARY_GIT} symbolic-ref --short HEAD || printf '%s' "$4")
|
|
466
|
+
if ${BOUNDARY_GIT} --git-dir=/tmp/scratch show-ref --verify --quiet "refs/heads/$branch-index" \\
|
|
467
|
+
|| ! ${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$branch-index" "$commit" 2>/dev/null; then
|
|
468
|
+
printf '%s\\n' ref_name_refused
|
|
469
|
+
fi
|
|
470
|
+
fi
|
|
410
471
|
if [ "$tree" != "$(${BOUNDARY_GIT} rev-parse 'HEAD^{tree}')" ]; then
|
|
411
|
-
export GIT_AUTHOR_NAME=pi-writer GIT_AUTHOR_EMAIL=pi-writer@example.invalid
|
|
412
|
-
export GIT_COMMITTER_NAME=pi-writer GIT_COMMITTER_EMAIL=pi-writer@example.invalid
|
|
413
472
|
commit=$(printf '%s\\n' 'WIP collected by pi-session-mcp' | ${BOUNDARY_GIT} commit-tree "$tree" -p HEAD)
|
|
414
473
|
branch=$(${BOUNDARY_GIT} symbolic-ref --short HEAD || printf '%s' "$4")
|
|
415
|
-
|
|
474
|
+
# A taken name, or one that conflicts with an exported branch path, is refused rather than aborting the export.
|
|
475
|
+
if ${BOUNDARY_GIT} --git-dir=/tmp/scratch show-ref --verify --quiet "refs/heads/$branch-wip" \\
|
|
476
|
+
|| ! ${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$branch-wip" "$commit" 2>/dev/null; then
|
|
477
|
+
printf '%s\\n' ref_name_refused
|
|
478
|
+
fi
|
|
479
|
+
elif ! ${BOUNDARY_GIT} symbolic-ref -q HEAD >/dev/null 2>&1 && [ -n "$(${BOUNDARY_GIT} rev-list -n 1 HEAD --not --branches)" ]; then
|
|
480
|
+
# Clean detached HEAD that no branch of the clone reaches: export its commit under a derived name (#193).
|
|
481
|
+
if ${BOUNDARY_GIT} --git-dir=/tmp/scratch show-ref --verify --quiet "refs/heads/$4-detached" \\
|
|
482
|
+
|| ! ${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$4-detached" "$(${BOUNDARY_GIT} rev-parse --verify HEAD)" 2>/dev/null; then
|
|
416
483
|
printf '%s\\n' ref_name_refused
|
|
417
|
-
else
|
|
418
|
-
${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$branch-wip" "$commit"
|
|
419
484
|
fi
|
|
420
485
|
fi
|
|
421
486
|
if [ -z "$(${BOUNDARY_GIT} --git-dir=/tmp/scratch for-each-ref --format='%(refname)')" ]; then
|
|
@@ -450,6 +515,22 @@ fi`;
|
|
|
450
515
|
return { status: "failed", code: "writer_collect_incomplete", refs, refused: findings.length, findings };
|
|
451
516
|
}
|
|
452
517
|
const work = await inspectWork(ctx, name);
|
|
518
|
+
// Defence in depth (#193): whatever the boundary script exported or reported as nothing to collect,
|
|
519
|
+
// every commit the clone's work refers to must exist as a commit object in the operator checkout
|
|
520
|
+
// before a digest is recorded.
|
|
521
|
+
for (const sha of [work.head, ...work.branches.map((branch) => branch.commit), ...work.stashes]) {
|
|
522
|
+
if (sha === undefined || await git(ctx, ["cat-file", "-e", `${sha}^{commit}`], true) === undefined) {
|
|
523
|
+
return { status: "failed", code: "writer_collect_incomplete", refs, refused: findings.length, findings };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Tags, notes and other refs are not exported: their objects must already be in the operator checkout (#202).
|
|
527
|
+
if (work.unknownRefs > 0)
|
|
528
|
+
return { status: "failed", code: "writer_collect_incomplete", refs, refused: findings.length, findings };
|
|
529
|
+
for (const tree of [work.indexTree, work.tree]) {
|
|
530
|
+
if (tree !== work.headTree && await git(ctx, ["cat-file", "-e", `${tree}^{tree}`], true) === undefined) {
|
|
531
|
+
return { status: "failed", code: "writer_collect_incomplete", refs, refused: findings.length, findings };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
453
534
|
await writeWriterState(ctx.home, name, { ...state, collected: { digest: work.digest, at: new Date().toISOString() } });
|
|
454
535
|
return { status: "ok", code: refs.length === 0 ? "nothing_to_collect" : "writer_collected", refs, refused: findings.length, findings };
|
|
455
536
|
}
|
|
@@ -471,7 +552,7 @@ async function recreate(ctx, name, state, base) {
|
|
|
471
552
|
const next = { ...retained, base: { ref: `refs/heads/${base.short}`, commit: base.commit } };
|
|
472
553
|
await writeWriterState(ctx.home, name, { ...next, phase: "provisioning" });
|
|
473
554
|
for (const path of [paths.clone, paths.agent]) {
|
|
474
|
-
await
|
|
555
|
+
await removeWriterTree(path);
|
|
475
556
|
await privateDirectory(path);
|
|
476
557
|
}
|
|
477
558
|
await cloneFromBundle(ctx, paths, state.imageId, base, remote, state.tags);
|
|
@@ -560,7 +641,7 @@ export async function removeWriter(ctx, name, opts) {
|
|
|
560
641
|
if (!work.baseline && state.collected?.digest !== work.digest)
|
|
561
642
|
throw new LifecycleError("writer_uncollected");
|
|
562
643
|
}
|
|
563
|
-
await
|
|
644
|
+
await removeWriterTree(paths.root);
|
|
564
645
|
return { code: "writer_removed", status: "ok" };
|
|
565
646
|
}
|
|
566
647
|
catch (error) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { mkdir, open, rename, unlink } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
/** File name of Pi's persisted model catalog overlay, next to `auth.json` in an agent directory. */
|
|
6
|
+
export const MODELS_STORE_FILE = "models-store.json";
|
|
7
|
+
/** Upper bound for the copied catalog; a larger file is skipped rather than read into memory. */
|
|
8
|
+
export const MODELS_STORE_MAX_BYTES = 32 * 1024 * 1024;
|
|
9
|
+
/** The host's persisted catalog overlay, resolved like the host `auth.json` the launcher reads. */
|
|
10
|
+
export const hostModelsStorePath = (home) => join(home, ".pi", "agent", MODELS_STORE_FILE);
|
|
11
|
+
/**
|
|
12
|
+
* Copies the host's persisted Pi model catalog into a writer's private agent directory, so the writer's Pi resolves the
|
|
13
|
+
* same models as the host (#199). The file holds model catalog data, not credentials, and is copied byte for byte: it is
|
|
14
|
+
* neither parsed nor printed. A missing, non-regular, symbolic-link or oversized source is skipped, and so is any failure:
|
|
15
|
+
* the destination is then removed, so neither a stale copy nor a file left by a worker in the writable agent directory
|
|
16
|
+
* survives, and the writer keeps the static model list shipped with the SDK. The destination is replaced by an atomic
|
|
17
|
+
* rename of a 0600 temporary file, so a link at the destination path is replaced, never followed. Returns whether a copy
|
|
18
|
+
* was made.
|
|
19
|
+
*/
|
|
20
|
+
export async function copyModelsStore(source, agentDir) {
|
|
21
|
+
const copied = await copy(source, agentDir);
|
|
22
|
+
if (!copied)
|
|
23
|
+
await unlink(join(agentDir, MODELS_STORE_FILE)).catch(() => undefined);
|
|
24
|
+
return copied;
|
|
25
|
+
}
|
|
26
|
+
async function copy(source, agentDir) {
|
|
27
|
+
let temporary;
|
|
28
|
+
try {
|
|
29
|
+
// O_NOFOLLOW refuses a symbolic link at the source path; the opened file is checked again after open.
|
|
30
|
+
const input = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
31
|
+
let content;
|
|
32
|
+
try {
|
|
33
|
+
const stat = await input.stat();
|
|
34
|
+
if (!stat.isFile() || stat.size > MODELS_STORE_MAX_BYTES)
|
|
35
|
+
return false;
|
|
36
|
+
content = await input.readFile();
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await input.close();
|
|
40
|
+
}
|
|
41
|
+
if (content.length > MODELS_STORE_MAX_BYTES)
|
|
42
|
+
return false;
|
|
43
|
+
await mkdir(agentDir, { recursive: true, mode: 0o700 });
|
|
44
|
+
temporary = join(agentDir, `.${MODELS_STORE_FILE}-${randomUUID()}.tmp`);
|
|
45
|
+
const output = await open(temporary, "wx", 0o600);
|
|
46
|
+
try {
|
|
47
|
+
await output.writeFile(content);
|
|
48
|
+
await output.sync();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await output.close();
|
|
52
|
+
}
|
|
53
|
+
await rename(temporary, join(agentDir, MODELS_STORE_FILE));
|
|
54
|
+
temporary = undefined;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
if (temporary !== undefined)
|
|
62
|
+
await unlink(temporary).catch(() => undefined);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { chmod, lstat, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/** Upper bound for the entries one permission walk visits: a Go module cache holds many thousands. */
|
|
4
|
+
export const TREE_WALK_LIMIT = 5_000_000;
|
|
5
|
+
const OWNER_RWX = 0o700;
|
|
6
|
+
const errorCode = (error) => (error instanceof Error && "code" in error ? String(error.code) : undefined);
|
|
7
|
+
/**
|
|
8
|
+
* Gives the current user rwx on every real directory below `root` (and on `root` itself) that lacks it. Go creates
|
|
9
|
+
* its module cache with directories that have no owner write bit, and Node's recursive `rm` cannot unlink inside
|
|
10
|
+
* such a directory. Nothing is followed: every entry is `lstat`ed, a symbolic link is never opened or `chmod`ed,
|
|
11
|
+
* and only directories owned by the current user are changed. The walk visits only entries below `root`.
|
|
12
|
+
*/
|
|
13
|
+
export async function makeTreeRemovable(root, limit = TREE_WALK_LIMIT) {
|
|
14
|
+
const uid = process.getuid?.();
|
|
15
|
+
const pending = [root];
|
|
16
|
+
let visited = 0;
|
|
17
|
+
for (let path = pending.pop(); path !== undefined; path = pending.pop()) {
|
|
18
|
+
if (++visited > limit)
|
|
19
|
+
throw new Error("tree_walk_limit_exceeded");
|
|
20
|
+
let stat;
|
|
21
|
+
try {
|
|
22
|
+
stat = await lstat(path);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (errorCode(error) === "ENOENT")
|
|
26
|
+
continue;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
if (!stat.isDirectory())
|
|
30
|
+
continue; // Files and symbolic links need no change; removal unlinks a link without following it.
|
|
31
|
+
if (uid !== undefined && stat.uid === uid && (stat.mode & OWNER_RWX) !== OWNER_RWX)
|
|
32
|
+
await chmod(path, (stat.mode & 0o7777) | OWNER_RWX);
|
|
33
|
+
let names;
|
|
34
|
+
try {
|
|
35
|
+
names = await readdir(path);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// A directory of another user stays unreadable; the removal that follows reports it.
|
|
39
|
+
if (errorCode(error) === "EACCES" || errorCode(error) === "ENOENT")
|
|
40
|
+
continue;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
for (const name of names)
|
|
44
|
+
pending.push(join(path, name));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Removes a writer's storage tree. Permissions are restored first. Then everything except the top-level `clone` and
|
|
49
|
+
* `state.json` is deleted, then `clone`, then `state.json`, then the root. A part-way failure therefore keeps
|
|
50
|
+
* `state.json`, and as long as anything else fails also `clone`, so a retry without `--discard` can still inspect the
|
|
51
|
+
* writer instead of finding one that setup can no longer address.
|
|
52
|
+
*/
|
|
53
|
+
export async function removeWriterTree(root) {
|
|
54
|
+
let stat;
|
|
55
|
+
try {
|
|
56
|
+
stat = await lstat(root);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (errorCode(error) === "ENOENT")
|
|
60
|
+
return;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
if (!stat.isDirectory()) {
|
|
64
|
+
await rm(root, { force: true });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await makeTreeRemovable(root);
|
|
68
|
+
for (const name of await readdir(root)) {
|
|
69
|
+
if (name !== "state.json" && name !== "clone")
|
|
70
|
+
await rm(join(root, name), { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
await rm(join(root, "clone"), { recursive: true, force: true });
|
|
73
|
+
await rm(join(root, "state.json"), { force: true });
|
|
74
|
+
await rm(root, { recursive: true, force: true });
|
|
75
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,101 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.14.2 — 2026-10-30
|
|
6
|
+
|
|
7
|
+
Patch release under ADR 0003 for Sprint 13 and its follow-ups. The MCP tool
|
|
8
|
+
contract, result codes and configuration schema are unchanged. Collection adds
|
|
9
|
+
the export names `<branch>-index` and `<base>-stash-<n>`, and the versioned
|
|
10
|
+
collection digest means that a writer collected by 0.14.1 but not yet reset
|
|
11
|
+
must be collected once more before `finish`, `reset`, `recreate` or `remove`.
|
|
12
|
+
`npm audit` still reports `undici` 8.10.2, pinned by Pi SDK 1.4.0, the latest
|
|
13
|
+
SDK release ([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
|
|
14
|
+
|
|
15
|
+
- Writer cleanup no longer loses work that exists only in a stash entry
|
|
16
|
+
([#202](https://github.com/plagor44-ux/pi-session-mcp/issues/202)).
|
|
17
|
+
Collection exports every stash entry, including one without a reflog, as
|
|
18
|
+
`<base>-stash-<n>` in `stash@{n}` order, and records a digest only when every
|
|
19
|
+
stash commit exists in the operator checkout. Tags, notes, replace refs,
|
|
20
|
+
remote-tracking refs and refs in other namespaces stay unexported, but each
|
|
21
|
+
must name an object the operator checkout already has: otherwise the writer
|
|
22
|
+
is not at its baseline and collection is `writer_collect_incomplete`, so
|
|
23
|
+
`finish`, `reset`, `recreate` and `remove` without `--discard` refuse.
|
|
24
|
+
Provisioned tags and remote-tracking refs, and lightweight tags on exported
|
|
25
|
+
commits, still pass while the operator checkout has their objects; only such
|
|
26
|
+
a ref's name is lost at cleanup. Ref names outside the branch export rules,
|
|
27
|
+
such as a tag `pkg@1.0.0+build`, do not make inspection fail.
|
|
28
|
+
- Writer cleanup no longer loses work that exists only in the clone's Git
|
|
29
|
+
index ([#200](https://github.com/plagor44-ux/pi-session-mcp/issues/200)).
|
|
30
|
+
Inspection now also evaluates the original index tree on a temporary copy,
|
|
31
|
+
so a staged change that the worktree reverted (`git status` `MM`) is neither
|
|
32
|
+
baseline nor clean. Collection exports such a staged tree as an extra
|
|
33
|
+
`<branch>-index` commit next to the unchanged `<branch>-wip` and
|
|
34
|
+
`<base>-detached` exports, and records a digest only when the index and
|
|
35
|
+
worktree trees exist in the operator checkout. An index that cannot be
|
|
36
|
+
evaluated, such as an unmerged conflict, makes inspection and collection fail
|
|
37
|
+
closed (`writer_inspect_failed`, `writer_collect_failed`), so `finish`,
|
|
38
|
+
`reset`, `recreate` and `remove` without `--discard` refuse and the clone
|
|
39
|
+
stays unchanged. Upgrade note: the collection digest is versioned and covers
|
|
40
|
+
the index tree, so a digest recorded by 0.14.1 or earlier never authorizes
|
|
41
|
+
cleanup; collect a writer once more before resetting or removing it. Pool
|
|
42
|
+
admission was already index-aware through the in-boundary `git status`
|
|
43
|
+
probe, and Doctor's `writer_dirty` stays an offline ref-only hint.
|
|
44
|
+
- `--list-models`, `--probe-profile` and sessions now see the models of the
|
|
45
|
+
catalog overlay that Pi persisted earlier (`models-store.json`), restored
|
|
46
|
+
offline: no network request and no credential refresh. The catalog restores
|
|
47
|
+
it at creation; a session restores it for its selected provider only, within
|
|
48
|
+
the existing auth deadline. Writers receive a byte copy of the host's
|
|
49
|
+
persisted catalog in their private agent directory at provisioning, reset,
|
|
50
|
+
recreate and launch; without a host file the copy is removed. Before, a model
|
|
51
|
+
that only Pi's refreshed catalog offered failed with `probe_model_unknown` or
|
|
52
|
+
`unknown_model`
|
|
53
|
+
([#199](https://github.com/plagor44-ux/pi-session-mcp/issues/199)).
|
|
54
|
+
- `--writer <name> --remove`, provisioning restarts and recreation now remove a
|
|
55
|
+
writer tree that contains read-only directories, such as the Go module cache
|
|
56
|
+
of a writer with `--cache go`. Before, removal failed with
|
|
57
|
+
`writer_operation_failed` after deleting `state.json`, which left a writer
|
|
58
|
+
that setup could no longer address; `state.json` is now removed last
|
|
59
|
+
([#198](https://github.com/plagor44-ux/pi-session-mcp/issues/198)).
|
|
60
|
+
|
|
61
|
+
## 0.14.1 — 2026-10-30
|
|
62
|
+
|
|
63
|
+
Patch release under ADR 0003 for Sprint 12. The MCP tool contract, result
|
|
64
|
+
codes and configuration schema are unchanged. `npm audit` still reports
|
|
65
|
+
`undici` 8.10.2, pinned by Pi SDK 1.4.0, the latest SDK release
|
|
66
|
+
([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
|
|
67
|
+
|
|
68
|
+
Sprint 12 fixes three defects that an external audit of `9d9cf8c` reported and
|
|
69
|
+
that were reproduced on that commit
|
|
70
|
+
([Sprint 12](docs/sprints/2026-10-29-s12-audit-fixes.md)). The MCP tool
|
|
71
|
+
contract, result codes and configuration schema are unchanged.
|
|
72
|
+
|
|
73
|
+
- Read-only sessions no longer load Pi context files (`AGENTS.override.md`,
|
|
74
|
+
`AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`, `CLAUDE.MD`) from outside the
|
|
75
|
+
workspace into the model context
|
|
76
|
+
([#192](https://github.com/plagor44-ux/pi-session-mcp/issues/192)). Every
|
|
77
|
+
resource loader of a read-only profile keeps only files whose real path lies
|
|
78
|
+
inside the workspace's real path, on every reload and before the first
|
|
79
|
+
provider request, and reads them from that verified path. Behaviour change:
|
|
80
|
+
context files of directories above the workspace, including a main
|
|
81
|
+
repository above a linked worktree, are no longer loaded for read-only
|
|
82
|
+
profiles. Coding profiles are unchanged
|
|
83
|
+
([ADR 0013 amendment](docs/adr/0013-hardened-read-only-profiles.md)).
|
|
84
|
+
- Writer collection exports a clean detached `HEAD` whose commit no branch
|
|
85
|
+
reaches as `<base>-detached`, and records its release digest only when the
|
|
86
|
+
clone's `HEAD` and every branch commit exist in the operator checkout;
|
|
87
|
+
otherwise it reports `writer_collect_incomplete`. Before, such a commit was
|
|
88
|
+
reported as `nothing_to_collect` and finish, reset, recreate and remove
|
|
89
|
+
discarded it without `--discard`
|
|
90
|
+
([#193](https://github.com/plagor44-ux/pi-session-mcp/issues/193)).
|
|
91
|
+
Clone-side Git, including the writer guard, now ignores replace refs, and a
|
|
92
|
+
derived `-wip` or `-detached` name that conflicts with an exported branch
|
|
93
|
+
path is refused instead of aborting the export.
|
|
94
|
+
- The writer relay compares every public capability field of a writer's
|
|
95
|
+
profile with the host profile before it starts an inner session, including
|
|
96
|
+
skills, extensions, granted extension tools and shell; a mismatch counts as
|
|
97
|
+
`settingsChanged`. Before, a writer with a different skill selection was
|
|
98
|
+
admitted ([#194](https://github.com/plagor44-ux/pi-session-mcp/issues/194)).
|
|
99
|
+
|
|
5
100
|
## 0.14.0 — 2026-10-29
|
|
6
101
|
|
|
7
102
|
Minor release under ADR 0003 for
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ It is not the right tool when:
|
|
|
30
30
|
|
|
31
31
|
## Status
|
|
32
32
|
|
|
33
|
-
The package version is **0.14.
|
|
33
|
+
The package version is **0.14.2**.
|
|
34
34
|
|
|
35
35
|
[Per-repository execution modes](docs/execution-modes.md) make host writes explicit and block coding on read-only aliases. On container workspaces, the one host `pi-session-mcp` registration leases a rootless-Podman writer per coding session on Linux; no per-writer registrations or tool prefixes are needed ([ADR 0012](docs/adr/0012-leased-writer-pool.md)). Setup provisions, accepts, collects and resets the pool. Starts normally use the workspace default base; fix rounds can pass a short branch `base`. Start/get report `writer` and `base` for collection, and capabilities report configured `writers` and `defaultBase`. Reset, finish and recreate need no reconnect; finish returns the writer to the default base. See the [container upgrade runbook](docs/installation.md#container-writers).
|
|
36
36
|
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import type { ConfigurationMetadata } from "./config.js";
|
|
2
2
|
import type { WorkspaceModeEntry } from "./execution-modes.js";
|
|
3
3
|
import type { ConfiguredExecutionProfile } from "./execution-profile.js";
|
|
4
|
+
/** The public capability shape of one profile, exactly as `pi_capabilities_get` publishes it. */
|
|
5
|
+
export declare function projectExecutionProfile({ alias, default: isDefault, permissionProfile, provider, model, thinkingLevel, extensions, grantedExtensionTools, shell, skills }: ConfiguredExecutionProfile): {
|
|
6
|
+
alias: string;
|
|
7
|
+
default: boolean;
|
|
8
|
+
permissionProfile: import("./execution-profile.js").PermissionProfile;
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
thinkingLevel: import("./execution-profile.js").ThinkingLevel;
|
|
12
|
+
extensions: string[];
|
|
13
|
+
grantedExtensionTools: string[];
|
|
14
|
+
shell: "none" | "sandboxed";
|
|
15
|
+
skills: string[];
|
|
16
|
+
};
|
|
4
17
|
/** Pure projection of the snapshot captured at load time; never reads the configuration file. */
|
|
5
18
|
export declare function projectCapabilities(workspaces: ReadonlyMap<string, string>, executionProfiles: ReadonlyMap<string, ConfiguredExecutionProfile>, configuration: ConfigurationMetadata, workspaceModes?: ReadonlyMap<string, WorkspaceModeEntry>, eventDelivery?: "spool" | "off"): {
|
|
6
19
|
ok: true;
|
package/dist/capabilities.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import packageMetadata from "../package.json" with { type: "json" };
|
|
2
2
|
const compareAliases = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
3
|
+
/** The public capability shape of one profile, exactly as `pi_capabilities_get` publishes it. */
|
|
4
|
+
export function projectExecutionProfile({ alias, default: isDefault, permissionProfile, provider, model, thinkingLevel, extensions, grantedExtensionTools, shell, skills }) {
|
|
5
|
+
return {
|
|
6
|
+
alias,
|
|
7
|
+
default: isDefault,
|
|
8
|
+
permissionProfile,
|
|
9
|
+
provider,
|
|
10
|
+
model,
|
|
11
|
+
thinkingLevel,
|
|
12
|
+
extensions: (extensions ?? []).map((extension) => extension.spec),
|
|
13
|
+
grantedExtensionTools: [...(grantedExtensionTools ?? [])],
|
|
14
|
+
// Configured intent only: whether the server's probe succeeds is a start-time fact.
|
|
15
|
+
shell: shell ?? "none",
|
|
16
|
+
skills: (skills ?? []).map((skill) => skill.alias),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
3
19
|
/** Pure projection of the snapshot captured at load time; never reads the configuration file. */
|
|
4
20
|
export function projectCapabilities(workspaces, executionProfiles, configuration, workspaceModes = new Map(), eventDelivery = "off") {
|
|
5
21
|
return {
|
|
@@ -18,18 +34,6 @@ export function projectCapabilities(workspaces, executionProfiles, configuration
|
|
|
18
34
|
}),
|
|
19
35
|
executionProfiles: [...executionProfiles.values()]
|
|
20
36
|
.sort((left, right) => compareAliases(left.alias, right.alias))
|
|
21
|
-
.map(
|
|
22
|
-
alias,
|
|
23
|
-
default: isDefault,
|
|
24
|
-
permissionProfile,
|
|
25
|
-
provider,
|
|
26
|
-
model,
|
|
27
|
-
thinkingLevel,
|
|
28
|
-
extensions: (extensions ?? []).map((extension) => extension.spec),
|
|
29
|
-
grantedExtensionTools: [...(grantedExtensionTools ?? [])],
|
|
30
|
-
// Configured intent only: whether the server's probe succeeds is a start-time fact.
|
|
31
|
-
shell: shell ?? "none",
|
|
32
|
-
skills: (skills ?? []).map((skill) => skill.alias),
|
|
33
|
-
})),
|
|
37
|
+
.map(projectExecutionProfile),
|
|
34
38
|
};
|
|
35
39
|
}
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -17,7 +17,14 @@ export interface ModelCatalog {
|
|
|
17
17
|
export declare const CATALOG_ID_PATTERN: RegExp;
|
|
18
18
|
/** Bound for one offline authentication lookup. */
|
|
19
19
|
export declare const AUTH_CHECK_TIMEOUT_MS = 5000;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The part of the SDK's `ModelRuntime` the catalog reads. The runtime is created with `refreshOnCreate: true` and
|
|
22
|
+
* `allowModelNetwork: false`: that is an offline restore of the catalog Pi persisted earlier (the pi.dev overlay in
|
|
23
|
+
* `models-store.json`), which the SDK applies only inside its refresh. With network off, a provider reads its stored
|
|
24
|
+
* credential record, restores the cached catalog and returns before any credential is refreshed, so no request is made.
|
|
25
|
+
* The availability pass that follows reads credential records and resolves API-key values in-process, like the
|
|
26
|
+
* `checkAuth` calls below already do; no value leaves the process and no network call is made.
|
|
27
|
+
*/
|
|
21
28
|
export interface CatalogRuntime {
|
|
22
29
|
getProviders(): readonly {
|
|
23
30
|
readonly id: string;
|
|
@@ -30,11 +37,17 @@ export interface CatalogRuntime {
|
|
|
30
37
|
signal?: AbortSignal;
|
|
31
38
|
}): Promise<unknown>;
|
|
32
39
|
}
|
|
40
|
+
/** Options for an offline restore of Pi's persisted catalog: no model network, no credential refresh. */
|
|
41
|
+
export declare const OFFLINE_RUNTIME_OPTIONS: {
|
|
42
|
+
readonly refreshOnCreate: true;
|
|
43
|
+
readonly allowModelNetwork: false;
|
|
44
|
+
};
|
|
33
45
|
/**
|
|
34
46
|
* Reads the provider ids Pi knows offline, whether each has configured authentication, and its model
|
|
35
|
-
* ids. `checkAuth` reports only whether and how
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* contacted
|
|
47
|
+
* ids, including the models of Pi's persisted catalog overlay. `checkAuth` reports only whether and how
|
|
48
|
+
* a provider is configured; its result is reduced to a boolean. `hasConfiguredAuth` is not used. Never
|
|
49
|
+
* calls `getAuth`, `getAvailable` or `refresh` itself, so no credential value is resolved and no
|
|
50
|
+
* provider is contacted; the only refresh is the offline restore at creation. Providers registered only
|
|
51
|
+
* by Pi extensions are not part of the registry.
|
|
39
52
|
*/
|
|
40
53
|
export declare function createModelCatalog(createRuntime?: () => Promise<CatalogRuntime>): Promise<ModelCatalog>;
|
package/dist/model-catalog.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
export const CATALOG_ID_PATTERN = /^[A-Za-z0-9@][A-Za-z0-9._:@\/-]{0,255}$/;
|
|
6
6
|
/** Bound for one offline authentication lookup. */
|
|
7
7
|
export const AUTH_CHECK_TIMEOUT_MS = 5_000;
|
|
8
|
-
|
|
8
|
+
/** Options for an offline restore of Pi's persisted catalog: no model network, no credential refresh. */
|
|
9
|
+
export const OFFLINE_RUNTIME_OPTIONS = { refreshOnCreate: true, allowModelNetwork: false };
|
|
10
|
+
const createCatalogRuntime = () => import("@earendil-works/pi-coding-agent").then(({ ModelRuntime }) => ModelRuntime.create({ ...OFFLINE_RUNTIME_OPTIONS }));
|
|
9
11
|
const compareIds = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
|
|
10
12
|
/** True only for a settled, present result; a rejection or a lookup past the bound counts as not configured. */
|
|
11
13
|
const authConfigured = (runtime, providerId) => new Promise((resolve) => {
|
|
@@ -18,10 +20,11 @@ const authConfigured = (runtime, providerId) => new Promise((resolve) => {
|
|
|
18
20
|
});
|
|
19
21
|
/**
|
|
20
22
|
* Reads the provider ids Pi knows offline, whether each has configured authentication, and its model
|
|
21
|
-
* ids. `checkAuth` reports only whether and how
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* contacted
|
|
23
|
+
* ids, including the models of Pi's persisted catalog overlay. `checkAuth` reports only whether and how
|
|
24
|
+
* a provider is configured; its result is reduced to a boolean. `hasConfiguredAuth` is not used. Never
|
|
25
|
+
* calls `getAuth`, `getAvailable` or `refresh` itself, so no credential value is resolved and no
|
|
26
|
+
* provider is contacted; the only refresh is the offline restore at creation. Providers registered only
|
|
27
|
+
* by Pi extensions are not part of the registry.
|
|
25
28
|
*/
|
|
26
29
|
export async function createModelCatalog(createRuntime = createCatalogRuntime) {
|
|
27
30
|
const runtime = await createRuntime();
|
package/dist/sdk-pi-adapter.d.ts
CHANGED
|
@@ -74,10 +74,15 @@ export type PromptDisposition = Parameters<NonNullable<PromptOptions["preflightR
|
|
|
74
74
|
* typecheck (and therefore the Pi SDK canary) instead of being mapped silently.
|
|
75
75
|
*/
|
|
76
76
|
export declare function admitsTurn(disposition: PromptDisposition): boolean;
|
|
77
|
-
export declare function createPhase0ResourceLoader(cwd: string, skills?: readonly Skill[]): Promise<{
|
|
77
|
+
export declare function createPhase0ResourceLoader(cwd: string, skills?: readonly Skill[], permissionProfile?: ResolvedExecutionProfile["permissionProfile"]): Promise<{
|
|
78
78
|
resourceLoader: DefaultResourceLoader;
|
|
79
79
|
settingsManager: SettingsManager;
|
|
80
80
|
}>;
|
|
81
|
+
/**
|
|
82
|
+
* Created without the refresh-time availability pass, which would read and resolve every provider's credential. Pi's persisted
|
|
83
|
+
* catalog is restored per session for the selected provider only (`resolveVerifiedModel`). The extension path uses the same factory.
|
|
84
|
+
*/
|
|
85
|
+
export declare const defaultModelRuntimeFactory: () => Promise<ModelRuntime>;
|
|
81
86
|
export declare class SdkPiSessionAdapter implements PiSessionAdapter {
|
|
82
87
|
private runtimePromise;
|
|
83
88
|
private readonly modelRuntimeFactory;
|