pi-session-mcp 0.13.1 → 0.14.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/.pi-session-mcp-cli/config.js +21 -3
- package/.pi-session-mcp-cli/doctor.js +6 -2
- package/.pi-session-mcp-cli/skill-library.js +1 -1
- package/.pi-session-mcp-cli/writer-boundary.js +2 -1
- package/.pi-session-mcp-cli/writer-lifecycle.js +18 -4
- package/CHANGELOG.md +87 -0
- package/README.md +1 -1
- package/dist/capabilities.d.ts +13 -0
- package/dist/capabilities.js +17 -13
- package/dist/config.d.ts +3 -1
- package/dist/config.js +21 -3
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +6 -2
- package/dist/model-catalog.d.ts +40 -0
- package/dist/model-catalog.js +38 -0
- package/dist/profile-probe.d.ts +28 -0
- package/dist/profile-probe.js +164 -0
- package/dist/sdk-pi-adapter.d.ts +1 -1
- package/dist/sdk-pi-adapter.js +25 -6
- package/dist/setup-cli.d.ts +7 -0
- package/dist/setup-cli.js +36 -0
- package/dist/setup-main.js +10 -3
- package/dist/setup-probe-main.d.ts +14 -0
- package/dist/setup-probe-main.js +76 -0
- package/dist/setup-probe-result.d.ts +39 -0
- package/dist/setup-probe-result.js +62 -0
- package/dist/setup-writers-common.js +1 -1
- package/dist/skill-library.js +1 -1
- 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-lifecycle.d.ts +1 -0
- package/dist/writer-lifecycle.js +18 -4
- package/dist/writer-relay.js +12 -1
- package/docs/acceptance.md +313 -1
- 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 +89 -0
- package/docs/client-setup.md +18 -12
- package/docs/doctor-design.md +1 -0
- package/docs/execution-modes.md +25 -3
- package/docs/installation.md +87 -0
- package/docs/isolation-setup.md +8 -2
- package/docs/roadmap.md +20 -1
- package/docs/skills.md +42 -25
- package/docs/sprints/2026-10-24-s8-writer-migration.md +5 -3
- package/docs/sprints/2026-10-29-s12-audit-fixes.md +172 -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 +124 -0
|
@@ -6,7 +6,7 @@ import { MAX_PROFILE_EXTENSIONS, parseExtensionSpec } from "./extension-spec.js"
|
|
|
6
6
|
import { isMcpServers, snapshotMcpServers } from "./mcp-config.js";
|
|
7
7
|
import { builtinSkillName, isSkillCatalogPath, MAX_PROFILE_SKILLS, MAX_SKILL_CATALOG_ENTRIES } from "./skill-catalog.js";
|
|
8
8
|
import { ALIAS_PATTERN, ImmutableMap, } from "./execution-profile.js";
|
|
9
|
-
import { builtinSkillPath, skillOverrideDirectory } from "./skill-library.js";
|
|
9
|
+
import { builtinSkillPath, CONTROLLER_SKILLS, skillOverrideDirectory } from "./skill-library.js";
|
|
10
10
|
import { loadExecutionModes } from "./execution-modes.js";
|
|
11
11
|
import { isAcceptableExtensionToolName, MAX_GRANTED_EXTENSION_TOOLS, PROFILE_TOOLS } from "./session-tools.js";
|
|
12
12
|
const alias = z.string().regex(ALIAS_PATTERN);
|
|
@@ -74,13 +74,25 @@ export class ConfigError extends Error {
|
|
|
74
74
|
code;
|
|
75
75
|
/** Dotted schema issue locations for Doctor; built by `issueLocations`, never a value, message or path. */
|
|
76
76
|
locations;
|
|
77
|
-
|
|
77
|
+
/** `skillCatalog` aliases refused because they name a controller skill (#188); Doctor reports them on their own. */
|
|
78
|
+
refusedControllerSkills;
|
|
79
|
+
constructor(code, locations = [], refusedControllerSkills = []) {
|
|
78
80
|
super(CONFIG_ERROR_MESSAGES[code]);
|
|
79
81
|
this.name = "ConfigError";
|
|
80
82
|
this.code = code;
|
|
81
83
|
this.locations = Object.freeze([...locations]);
|
|
84
|
+
this.refusedControllerSkills = Object.freeze([...refusedControllerSkills]);
|
|
82
85
|
}
|
|
83
86
|
}
|
|
87
|
+
const CONTROLLER_SKILL_ISSUE = "controller_skill";
|
|
88
|
+
/** Catalog aliases of controller-skill issues; only alias-shaped keys, so no value or path can pass. */
|
|
89
|
+
function controllerSkillAliases(issues) {
|
|
90
|
+
return [...new Set(issues.flatMap((issue) => {
|
|
91
|
+
const skillAlias = issue.path[1];
|
|
92
|
+
return issue.code === "custom" && issue.params?.reason === CONTROLLER_SKILL_ISSUE && issue.path[0] === "skillCatalog"
|
|
93
|
+
&& typeof skillAlias === "string" && ALIAS_PATTERN.test(skillAlias) ? [skillAlias] : [];
|
|
94
|
+
}))].sort();
|
|
95
|
+
}
|
|
84
96
|
export const MAX_CONFIG_ISSUE_LOCATIONS = 5;
|
|
85
97
|
/**
|
|
86
98
|
* Sorted, deduplicated and bounded issue locations; an issue at the root has the empty location.
|
|
@@ -115,6 +127,12 @@ export const configSchema = z.object({
|
|
|
115
127
|
}).strict().superRefine((config, context) => {
|
|
116
128
|
// A profile may select only cataloged skills; there is no implicit or default skill.
|
|
117
129
|
const catalog = config.skillCatalog ?? {};
|
|
130
|
+
// The catalog serves Pi workers, which are never the controlling agent (#188).
|
|
131
|
+
for (const [skillAlias, value] of Object.entries(catalog)) {
|
|
132
|
+
if (CONTROLLER_SKILLS.includes(builtinSkillName(value) ?? "")) {
|
|
133
|
+
context.addIssue({ code: "custom", path: ["skillCatalog", skillAlias], message: "A controller skill cannot be a worker skill", params: { reason: CONTROLLER_SKILL_ISSUE } });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
118
136
|
for (const [profileAlias, profile] of Object.entries(config.executionProfiles)) {
|
|
119
137
|
if ((profile.skills ?? []).some((skill) => !Object.hasOwn(catalog, skill))) {
|
|
120
138
|
context.addIssue({ code: "custom", path: ["executionProfiles", profileAlias, "skills"], message: "A selected skill must be a skillCatalog alias" });
|
|
@@ -170,7 +188,7 @@ export async function loadConfig(path = process.env.PI_SESSION_MCP_CONFIG) {
|
|
|
170
188
|
const thinkingLevelIssue = error.issues.some((issue) => issue.path.at(-1) === "thinkingLevel");
|
|
171
189
|
// Only the events field's own issues map to the delivery code; an unknown top-level key stays a profile-config error.
|
|
172
190
|
const eventsIssue = error.issues.some((issue) => issue.path[0] === "events");
|
|
173
|
-
throw new ConfigError(thinkingLevelIssue ? "invalid_thinking_level" : eventsIssue ? "invalid_event_delivery" : "invalid_execution_profile_config", issueLocations(error.issues));
|
|
191
|
+
throw new ConfigError(thinkingLevelIssue ? "invalid_thinking_level" : eventsIssue ? "invalid_event_delivery" : "invalid_execution_profile_config", issueLocations(error.issues), controllerSkillAliases(error.issues));
|
|
174
192
|
}
|
|
175
193
|
throw error;
|
|
176
194
|
}
|
|
@@ -33,7 +33,7 @@ async function readPrefix(path, maxBytes) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
const defaultFileSystem = { readFile, stat, access };
|
|
36
|
-
const CHECK_ORDER = ["node_version_unsupported", "package_metadata_unreadable", "build_missing", "build_stale", "version_mismatch", "config_missing", "config_unreadable", "config_invalid", "workspace_missing", "workspace_not_directory", "workspace_unreadable", "workspace_not_writable", "workspace_gitfile", "workspace_mode_unset", "workspace_base_unset", "workspace_host_writes_accepted", "container_runtime_unavailable", "container_profile_refused", "writer_lease_directory", "writer_not_ready", "writer_not_migrated", "writer_off_default_base", "writer_dirty", "writer_git_identity_missing", "writer_uncollected_bundle", "extension_missing", "extension_version_mismatch", "extension_unpinned", "skill_missing", "skill_unreadable", "skill_duplicate", "skill_override_outdated", "sandbox_unavailable", "workspace_git_unavailable", "event_spool_root"];
|
|
36
|
+
const CHECK_ORDER = ["node_version_unsupported", "package_metadata_unreadable", "build_missing", "build_stale", "version_mismatch", "config_missing", "config_unreadable", "config_invalid", "skill_controller_selected", "workspace_missing", "workspace_not_directory", "workspace_unreadable", "workspace_not_writable", "workspace_gitfile", "workspace_mode_unset", "workspace_base_unset", "workspace_host_writes_accepted", "container_runtime_unavailable", "container_profile_refused", "writer_lease_directory", "writer_not_ready", "writer_not_migrated", "writer_off_default_base", "writer_dirty", "writer_git_identity_missing", "writer_uncollected_bundle", "extension_missing", "extension_version_mismatch", "extension_unpinned", "skill_missing", "skill_unreadable", "skill_duplicate", "skill_override_outdated", "sandbox_unavailable", "workspace_git_unavailable", "event_spool_root"];
|
|
37
37
|
function failure(id, subject, remediation) { return Object.freeze({ id, severity: "error", subject, remediation }); }
|
|
38
38
|
function warning(id, subject, remediation) { return Object.freeze({ id, severity: "warning", subject, remediation }); }
|
|
39
39
|
function ok(id, subject) { return Object.freeze({ id, severity: "ok", subject }); }
|
|
@@ -426,8 +426,12 @@ export async function runDoctor(dependencies = {}) {
|
|
|
426
426
|
else {
|
|
427
427
|
// Locations name the offending field; an error without one, or at the root, keeps the plain subject.
|
|
428
428
|
const locations = error instanceof ConfigError ? error.locations : [];
|
|
429
|
+
const controller = new Set((error instanceof ConfigError ? error.refusedControllerSkills : []).map((skillAlias) => `skillCatalog.${skillAlias}`));
|
|
429
430
|
for (const location of locations.length ? locations : [""]) {
|
|
430
|
-
|
|
431
|
+
const subject = location ? `configuration:${location}` : "configuration";
|
|
432
|
+
checks.push(controller.has(location)
|
|
433
|
+
? failure("skill_controller_selected", subject, "remove this skillCatalog entry: controller skills are for the controlling agent, not for Pi workers")
|
|
434
|
+
: failure("config_invalid", subject, "fix configuration using the documented schema"));
|
|
431
435
|
}
|
|
432
436
|
}
|
|
433
437
|
}
|
|
@@ -13,7 +13,7 @@ export const SKILL_NAME_PATTERN = /^(?=.{1,64}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
13
13
|
/** A `skillCatalog` value `builtin:<name>` names a library skill instead of a path. */
|
|
14
14
|
export const BUILTIN_SKILL_PREFIX = "builtin:";
|
|
15
15
|
/** Skills for the controlling agent, which setup installs into Claude Code and Codex. */
|
|
16
|
-
export const CONTROLLER_SKILLS = Object.freeze(["pi-session-orchestration"]);
|
|
16
|
+
export const CONTROLLER_SKILLS = Object.freeze(["pi-session-orchestration", "pi-session-profiles"]);
|
|
17
17
|
export const MAX_SKILL_TREE_FILES = 32;
|
|
18
18
|
export const MAX_SKILL_TREE_BYTES = 1024 * 1024;
|
|
19
19
|
const MAX_SKILL_TREE_DEPTH = 8;
|
|
@@ -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) {
|
|
@@ -344,7 +344,7 @@ export async function inspectWork(ctx, name) {
|
|
|
344
344
|
});
|
|
345
345
|
return {
|
|
346
346
|
baseline: head === state.base.commit && refs.length === 1 && refs[0].name === state.base.ref && refs[0].commit === state.base.commit && tree === baseTree,
|
|
347
|
-
dirty: tree !== headTree, branches: refs.filter(({ name }) => name.startsWith("refs/heads/")),
|
|
347
|
+
dirty: tree !== headTree, head: head, branches: refs.filter(({ name }) => name.startsWith("refs/heads/")),
|
|
348
348
|
digest: `sha256:${createHash("sha256").update(result.stdout).digest("hex")}`,
|
|
349
349
|
};
|
|
350
350
|
}
|
|
@@ -412,10 +412,16 @@ if [ "$tree" != "$(${BOUNDARY_GIT} rev-parse 'HEAD^{tree}')" ]; then
|
|
|
412
412
|
export GIT_COMMITTER_NAME=pi-writer GIT_COMMITTER_EMAIL=pi-writer@example.invalid
|
|
413
413
|
commit=$(printf '%s\\n' 'WIP collected by pi-session-mcp' | ${BOUNDARY_GIT} commit-tree "$tree" -p HEAD)
|
|
414
414
|
branch=$(${BOUNDARY_GIT} symbolic-ref --short HEAD || printf '%s' "$4")
|
|
415
|
-
|
|
415
|
+
# A taken name, or one that conflicts with an exported branch path, is refused rather than aborting the export.
|
|
416
|
+
if ${BOUNDARY_GIT} --git-dir=/tmp/scratch show-ref --verify --quiet "refs/heads/$branch-wip" \\
|
|
417
|
+
|| ! ${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$branch-wip" "$commit" 2>/dev/null; then
|
|
418
|
+
printf '%s\\n' ref_name_refused
|
|
419
|
+
fi
|
|
420
|
+
elif ! ${BOUNDARY_GIT} symbolic-ref -q HEAD >/dev/null 2>&1 && [ -n "$(${BOUNDARY_GIT} rev-list -n 1 HEAD --not --branches)" ]; then
|
|
421
|
+
# Clean detached HEAD that no branch of the clone reaches: export its commit under a derived name (#193).
|
|
422
|
+
if ${BOUNDARY_GIT} --git-dir=/tmp/scratch show-ref --verify --quiet "refs/heads/$4-detached" \\
|
|
423
|
+
|| ! ${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$4-detached" "$(${BOUNDARY_GIT} rev-parse --verify HEAD)" 2>/dev/null; then
|
|
416
424
|
printf '%s\\n' ref_name_refused
|
|
417
|
-
else
|
|
418
|
-
${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$branch-wip" "$commit"
|
|
419
425
|
fi
|
|
420
426
|
fi
|
|
421
427
|
if [ -z "$(${BOUNDARY_GIT} --git-dir=/tmp/scratch for-each-ref --format='%(refname)')" ]; then
|
|
@@ -450,6 +456,14 @@ fi`;
|
|
|
450
456
|
return { status: "failed", code: "writer_collect_incomplete", refs, refused: findings.length, findings };
|
|
451
457
|
}
|
|
452
458
|
const work = await inspectWork(ctx, name);
|
|
459
|
+
// Defence in depth (#193): whatever the boundary script exported or reported as nothing to collect,
|
|
460
|
+
// every commit the clone's work refers to must exist as a commit object in the operator checkout
|
|
461
|
+
// before a digest is recorded.
|
|
462
|
+
for (const sha of [work.head, ...work.branches.map((branch) => branch.commit)]) {
|
|
463
|
+
if (sha === undefined || await git(ctx, ["cat-file", "-e", `${sha}^{commit}`], true) === undefined) {
|
|
464
|
+
return { status: "failed", code: "writer_collect_incomplete", refs, refused: findings.length, findings };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
453
467
|
await writeWriterState(ctx.home, name, { ...state, collected: { digest: work.digest, at: new Date().toISOString() } });
|
|
454
468
|
return { status: "ok", code: refs.length === 0 ? "nothing_to_collect" : "writer_collected", refs, refused: findings.length, findings };
|
|
455
469
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,93 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.14.1 — 2026-10-30
|
|
6
|
+
|
|
7
|
+
Patch release under ADR 0003 for Sprint 12. The MCP tool contract, result
|
|
8
|
+
codes and configuration schema are unchanged. `npm audit` still reports
|
|
9
|
+
`undici` 8.10.2, pinned by Pi SDK 1.4.0, the latest SDK release
|
|
10
|
+
([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
|
|
11
|
+
|
|
12
|
+
Sprint 12 fixes three defects that an external audit of `9d9cf8c` reported and
|
|
13
|
+
that were reproduced on that commit
|
|
14
|
+
([Sprint 12](docs/sprints/2026-10-29-s12-audit-fixes.md)). The MCP tool
|
|
15
|
+
contract, result codes and configuration schema are unchanged.
|
|
16
|
+
|
|
17
|
+
- Read-only sessions no longer load Pi context files (`AGENTS.override.md`,
|
|
18
|
+
`AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`, `CLAUDE.MD`) from outside the
|
|
19
|
+
workspace into the model context
|
|
20
|
+
([#192](https://github.com/plagor44-ux/pi-session-mcp/issues/192)). Every
|
|
21
|
+
resource loader of a read-only profile keeps only files whose real path lies
|
|
22
|
+
inside the workspace's real path, on every reload and before the first
|
|
23
|
+
provider request, and reads them from that verified path. Behaviour change:
|
|
24
|
+
context files of directories above the workspace, including a main
|
|
25
|
+
repository above a linked worktree, are no longer loaded for read-only
|
|
26
|
+
profiles. Coding profiles are unchanged
|
|
27
|
+
([ADR 0013 amendment](docs/adr/0013-hardened-read-only-profiles.md)).
|
|
28
|
+
- Writer collection exports a clean detached `HEAD` whose commit no branch
|
|
29
|
+
reaches as `<base>-detached`, and records its release digest only when the
|
|
30
|
+
clone's `HEAD` and every branch commit exist in the operator checkout;
|
|
31
|
+
otherwise it reports `writer_collect_incomplete`. Before, such a commit was
|
|
32
|
+
reported as `nothing_to_collect` and finish, reset, recreate and remove
|
|
33
|
+
discarded it without `--discard`
|
|
34
|
+
([#193](https://github.com/plagor44-ux/pi-session-mcp/issues/193)).
|
|
35
|
+
Clone-side Git, including the writer guard, now ignores replace refs, and a
|
|
36
|
+
derived `-wip` or `-detached` name that conflicts with an exported branch
|
|
37
|
+
path is refused instead of aborting the export.
|
|
38
|
+
- The writer relay compares every public capability field of a writer's
|
|
39
|
+
profile with the host profile before it starts an inner session, including
|
|
40
|
+
skills, extensions, granted extension tools and shell; a mismatch counts as
|
|
41
|
+
`settingsChanged`. Before, a writer with a different skill selection was
|
|
42
|
+
admitted ([#194](https://github.com/plagor44-ux/pi-session-mcp/issues/194)).
|
|
43
|
+
|
|
44
|
+
## 0.14.0 — 2026-10-29
|
|
45
|
+
|
|
46
|
+
Minor release under ADR 0003 for
|
|
47
|
+
[ADR 0014](docs/adr/0014-profile-probe-and-model-catalog.md)
|
|
48
|
+
([#139](https://github.com/plagor44-ux/pi-session-mcp/issues/139)):
|
|
49
|
+
agent-driven execution-profile setup. The MCP tool contract is unchanged. Setup
|
|
50
|
+
gains `--list-models` and `--probe-profile`, setup installs a second controller
|
|
51
|
+
skill, Doctor gains the check `skill_controller_selected`, and a configuration
|
|
52
|
+
whose skill catalog names a controller skill no longer loads
|
|
53
|
+
([#188](https://github.com/plagor44-ux/pi-session-mcp/issues/188)). `npm audit`
|
|
54
|
+
still reports `undici` 8.10.2, pinned by Pi SDK 1.4.0, the latest SDK release,
|
|
55
|
+
with eight advisories ([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
|
|
56
|
+
|
|
57
|
+
Pre-release acceptance on the candidate: Level 1 except `npm audit`, and Level 2
|
|
58
|
+
in the working tree and for the packed tarball, each with delivery off and on
|
|
59
|
+
([acceptance record](docs/acceptance.md#v0140-pre-release-acceptance)). The
|
|
60
|
+
Level 2 harness now expects both controller skills. Not established: an
|
|
61
|
+
agent-driven profile change end to end, a container writer acceptance of a new
|
|
62
|
+
profile, and Level 3.
|
|
63
|
+
|
|
64
|
+
Agent-driven execution-profile setup
|
|
65
|
+
([#139](https://github.com/plagor44-ux/pi-session-mcp/issues/139),
|
|
66
|
+
[ADR 0014](docs/adr/0014-profile-probe-and-model-catalog.md)):
|
|
67
|
+
|
|
68
|
+
- `pi-session-mcp-setup --list-models` prints the provider and model ids Pi
|
|
69
|
+
knows offline, with authentication presence only. Ids that could read as a
|
|
70
|
+
path are omitted and counted; providers registered only by extensions are not
|
|
71
|
+
listed.
|
|
72
|
+
- `pi-session-mcp-setup --probe-profile <alias>[,<alias>...]` checks profiles
|
|
73
|
+
offline (dry run by default); with `--apply` it sends one fixed PONG prompt
|
|
74
|
+
per profile in a private temporary directory and never prints the reply. It
|
|
75
|
+
is the only setup operation that uses a provider.
|
|
76
|
+
- The shipped controller skill `pi-session-profiles` lets a controlling agent
|
|
77
|
+
create, change or retire execution profiles itself. Setup installs it next to
|
|
78
|
+
`pi-session-orchestration`, so every client target reports one more skill
|
|
79
|
+
finding.
|
|
80
|
+
|
|
81
|
+
Controller skills as worker skills
|
|
82
|
+
([#188](https://github.com/plagor44-ux/pi-session-mcp/issues/188)):
|
|
83
|
+
|
|
84
|
+
- The configuration rejects the controller skills as worker skills: a
|
|
85
|
+
`skillCatalog` value `builtin:pi-session-orchestration` or
|
|
86
|
+
`builtin:pi-session-profiles` makes it invalid
|
|
87
|
+
(`invalid_execution_profile_config`), and Doctor reports the new error check
|
|
88
|
+
`skill_controller_selected` for `configuration:skillCatalog.<alias>`, which
|
|
89
|
+
blocks setup and the probe like `config_invalid`. A
|
|
90
|
+
configuration that used such an entry no longer starts; remove the entry.
|
|
91
|
+
|
|
5
92
|
## 0.13.1 — 2026-10-29
|
|
6
93
|
|
|
7
94
|
Patch release under ADR 0003 with an unchanged MCP contract. It ships the
|
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.
|
|
33
|
+
The package version is **0.14.1**.
|
|
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/config.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ export declare class ConfigError extends Error {
|
|
|
6
6
|
readonly code: ConfigErrorCode;
|
|
7
7
|
/** Dotted schema issue locations for Doctor; built by `issueLocations`, never a value, message or path. */
|
|
8
8
|
readonly locations: readonly string[];
|
|
9
|
-
|
|
9
|
+
/** `skillCatalog` aliases refused because they name a controller skill (#188); Doctor reports them on their own. */
|
|
10
|
+
readonly refusedControllerSkills: readonly string[];
|
|
11
|
+
constructor(code: ConfigErrorCode, locations?: readonly string[], refusedControllerSkills?: readonly string[]);
|
|
10
12
|
}
|
|
11
13
|
export declare const MAX_CONFIG_ISSUE_LOCATIONS = 5;
|
|
12
14
|
/**
|
package/dist/config.js
CHANGED
|
@@ -6,7 +6,7 @@ import { MAX_PROFILE_EXTENSIONS, parseExtensionSpec } from "./extension-spec.js"
|
|
|
6
6
|
import { isMcpServers, snapshotMcpServers } from "./mcp-config.js";
|
|
7
7
|
import { builtinSkillName, isSkillCatalogPath, MAX_PROFILE_SKILLS, MAX_SKILL_CATALOG_ENTRIES } from "./skill-catalog.js";
|
|
8
8
|
import { ALIAS_PATTERN, ImmutableMap, } from "./execution-profile.js";
|
|
9
|
-
import { builtinSkillPath, skillOverrideDirectory } from "./skill-library.js";
|
|
9
|
+
import { builtinSkillPath, CONTROLLER_SKILLS, skillOverrideDirectory } from "./skill-library.js";
|
|
10
10
|
import { loadExecutionModes } from "./execution-modes.js";
|
|
11
11
|
import { isAcceptableExtensionToolName, MAX_GRANTED_EXTENSION_TOOLS, PROFILE_TOOLS } from "./session-tools.js";
|
|
12
12
|
const alias = z.string().regex(ALIAS_PATTERN);
|
|
@@ -74,13 +74,25 @@ export class ConfigError extends Error {
|
|
|
74
74
|
code;
|
|
75
75
|
/** Dotted schema issue locations for Doctor; built by `issueLocations`, never a value, message or path. */
|
|
76
76
|
locations;
|
|
77
|
-
|
|
77
|
+
/** `skillCatalog` aliases refused because they name a controller skill (#188); Doctor reports them on their own. */
|
|
78
|
+
refusedControllerSkills;
|
|
79
|
+
constructor(code, locations = [], refusedControllerSkills = []) {
|
|
78
80
|
super(CONFIG_ERROR_MESSAGES[code]);
|
|
79
81
|
this.name = "ConfigError";
|
|
80
82
|
this.code = code;
|
|
81
83
|
this.locations = Object.freeze([...locations]);
|
|
84
|
+
this.refusedControllerSkills = Object.freeze([...refusedControllerSkills]);
|
|
82
85
|
}
|
|
83
86
|
}
|
|
87
|
+
const CONTROLLER_SKILL_ISSUE = "controller_skill";
|
|
88
|
+
/** Catalog aliases of controller-skill issues; only alias-shaped keys, so no value or path can pass. */
|
|
89
|
+
function controllerSkillAliases(issues) {
|
|
90
|
+
return [...new Set(issues.flatMap((issue) => {
|
|
91
|
+
const skillAlias = issue.path[1];
|
|
92
|
+
return issue.code === "custom" && issue.params?.reason === CONTROLLER_SKILL_ISSUE && issue.path[0] === "skillCatalog"
|
|
93
|
+
&& typeof skillAlias === "string" && ALIAS_PATTERN.test(skillAlias) ? [skillAlias] : [];
|
|
94
|
+
}))].sort();
|
|
95
|
+
}
|
|
84
96
|
export const MAX_CONFIG_ISSUE_LOCATIONS = 5;
|
|
85
97
|
/**
|
|
86
98
|
* Sorted, deduplicated and bounded issue locations; an issue at the root has the empty location.
|
|
@@ -115,6 +127,12 @@ export const configSchema = z.object({
|
|
|
115
127
|
}).strict().superRefine((config, context) => {
|
|
116
128
|
// A profile may select only cataloged skills; there is no implicit or default skill.
|
|
117
129
|
const catalog = config.skillCatalog ?? {};
|
|
130
|
+
// The catalog serves Pi workers, which are never the controlling agent (#188).
|
|
131
|
+
for (const [skillAlias, value] of Object.entries(catalog)) {
|
|
132
|
+
if (CONTROLLER_SKILLS.includes(builtinSkillName(value) ?? "")) {
|
|
133
|
+
context.addIssue({ code: "custom", path: ["skillCatalog", skillAlias], message: "A controller skill cannot be a worker skill", params: { reason: CONTROLLER_SKILL_ISSUE } });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
118
136
|
for (const [profileAlias, profile] of Object.entries(config.executionProfiles)) {
|
|
119
137
|
if ((profile.skills ?? []).some((skill) => !Object.hasOwn(catalog, skill))) {
|
|
120
138
|
context.addIssue({ code: "custom", path: ["executionProfiles", profileAlias, "skills"], message: "A selected skill must be a skillCatalog alias" });
|
|
@@ -170,7 +188,7 @@ export async function loadConfig(path = process.env.PI_SESSION_MCP_CONFIG) {
|
|
|
170
188
|
const thinkingLevelIssue = error.issues.some((issue) => issue.path.at(-1) === "thinkingLevel");
|
|
171
189
|
// Only the events field's own issues map to the delivery code; an unknown top-level key stays a profile-config error.
|
|
172
190
|
const eventsIssue = error.issues.some((issue) => issue.path[0] === "events");
|
|
173
|
-
throw new ConfigError(thinkingLevelIssue ? "invalid_thinking_level" : eventsIssue ? "invalid_event_delivery" : "invalid_execution_profile_config", issueLocations(error.issues));
|
|
191
|
+
throw new ConfigError(thinkingLevelIssue ? "invalid_thinking_level" : eventsIssue ? "invalid_event_delivery" : "invalid_execution_profile_config", issueLocations(error.issues), controllerSkillAliases(error.issues));
|
|
174
192
|
}
|
|
175
193
|
throw error;
|
|
176
194
|
}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type ExtensionSpec } from "./extension-spec.js";
|
|
|
3
3
|
import { type ReleasedSkillTree } from "./skill-library.js";
|
|
4
4
|
export type DoctorSeverity = "ok" | "warning" | "error";
|
|
5
5
|
export type DoctorExitCode = 0 | 1 | 2 | 64;
|
|
6
|
-
export type DoctorCheckId = "node_version_unsupported" | "package_metadata_unreadable" | "build_missing" | "build_stale" | "version_mismatch" | "config_missing" | "config_unreadable" | "config_invalid" | "workspace_missing" | "workspace_not_directory" | "workspace_unreadable" | "workspace_not_writable" | "workspace_gitfile" | "workspace_mode_unset" | "workspace_base_unset" | "workspace_host_writes_accepted" | "container_runtime_unavailable" | "container_profile_refused" | "writer_lease_directory" | "writer_not_ready" | "writer_not_migrated" | "writer_off_default_base" | "writer_dirty" | "writer_git_identity_missing" | "writer_uncollected_bundle" | "extension_missing" | "extension_version_mismatch" | "extension_unpinned" | "skill_missing" | "skill_unreadable" | "skill_duplicate" | "skill_override_outdated" | "sandbox_unavailable" | "workspace_git_unavailable" | "event_spool_root";
|
|
6
|
+
export type DoctorCheckId = "node_version_unsupported" | "package_metadata_unreadable" | "build_missing" | "build_stale" | "version_mismatch" | "config_missing" | "config_unreadable" | "config_invalid" | "skill_controller_selected" | "workspace_missing" | "workspace_not_directory" | "workspace_unreadable" | "workspace_not_writable" | "workspace_gitfile" | "workspace_mode_unset" | "workspace_base_unset" | "workspace_host_writes_accepted" | "container_runtime_unavailable" | "container_profile_refused" | "writer_lease_directory" | "writer_not_ready" | "writer_not_migrated" | "writer_off_default_base" | "writer_dirty" | "writer_git_identity_missing" | "writer_uncollected_bundle" | "extension_missing" | "extension_version_mismatch" | "extension_unpinned" | "skill_missing" | "skill_unreadable" | "skill_duplicate" | "skill_override_outdated" | "sandbox_unavailable" | "workspace_git_unavailable" | "event_spool_root";
|
|
7
7
|
export interface DoctorCheck {
|
|
8
8
|
readonly id: DoctorCheckId;
|
|
9
9
|
readonly severity: DoctorSeverity;
|
package/dist/doctor.js
CHANGED
|
@@ -33,7 +33,7 @@ async function readPrefix(path, maxBytes) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
const defaultFileSystem = { readFile, stat, access };
|
|
36
|
-
const CHECK_ORDER = ["node_version_unsupported", "package_metadata_unreadable", "build_missing", "build_stale", "version_mismatch", "config_missing", "config_unreadable", "config_invalid", "workspace_missing", "workspace_not_directory", "workspace_unreadable", "workspace_not_writable", "workspace_gitfile", "workspace_mode_unset", "workspace_base_unset", "workspace_host_writes_accepted", "container_runtime_unavailable", "container_profile_refused", "writer_lease_directory", "writer_not_ready", "writer_not_migrated", "writer_off_default_base", "writer_dirty", "writer_git_identity_missing", "writer_uncollected_bundle", "extension_missing", "extension_version_mismatch", "extension_unpinned", "skill_missing", "skill_unreadable", "skill_duplicate", "skill_override_outdated", "sandbox_unavailable", "workspace_git_unavailable", "event_spool_root"];
|
|
36
|
+
const CHECK_ORDER = ["node_version_unsupported", "package_metadata_unreadable", "build_missing", "build_stale", "version_mismatch", "config_missing", "config_unreadable", "config_invalid", "skill_controller_selected", "workspace_missing", "workspace_not_directory", "workspace_unreadable", "workspace_not_writable", "workspace_gitfile", "workspace_mode_unset", "workspace_base_unset", "workspace_host_writes_accepted", "container_runtime_unavailable", "container_profile_refused", "writer_lease_directory", "writer_not_ready", "writer_not_migrated", "writer_off_default_base", "writer_dirty", "writer_git_identity_missing", "writer_uncollected_bundle", "extension_missing", "extension_version_mismatch", "extension_unpinned", "skill_missing", "skill_unreadable", "skill_duplicate", "skill_override_outdated", "sandbox_unavailable", "workspace_git_unavailable", "event_spool_root"];
|
|
37
37
|
function failure(id, subject, remediation) { return Object.freeze({ id, severity: "error", subject, remediation }); }
|
|
38
38
|
function warning(id, subject, remediation) { return Object.freeze({ id, severity: "warning", subject, remediation }); }
|
|
39
39
|
function ok(id, subject) { return Object.freeze({ id, severity: "ok", subject }); }
|
|
@@ -426,8 +426,12 @@ export async function runDoctor(dependencies = {}) {
|
|
|
426
426
|
else {
|
|
427
427
|
// Locations name the offending field; an error without one, or at the root, keeps the plain subject.
|
|
428
428
|
const locations = error instanceof ConfigError ? error.locations : [];
|
|
429
|
+
const controller = new Set((error instanceof ConfigError ? error.refusedControllerSkills : []).map((skillAlias) => `skillCatalog.${skillAlias}`));
|
|
429
430
|
for (const location of locations.length ? locations : [""]) {
|
|
430
|
-
|
|
431
|
+
const subject = location ? `configuration:${location}` : "configuration";
|
|
432
|
+
checks.push(controller.has(location)
|
|
433
|
+
? failure("skill_controller_selected", subject, "remove this skillCatalog entry: controller skills are for the controlling agent, not for Pi workers")
|
|
434
|
+
: failure("config_invalid", subject, "fix configuration using the documented schema"));
|
|
431
435
|
}
|
|
432
436
|
}
|
|
433
437
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline view of Pi's model registry behind an injectable interface, so tests and callers need no
|
|
3
|
+
* provider credentials. Only documented package-root exports of the SDK are used.
|
|
4
|
+
*/
|
|
5
|
+
export interface CatalogProvider {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly authConfigured: boolean;
|
|
8
|
+
readonly models: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ModelCatalog {
|
|
11
|
+
providers(): readonly CatalogProvider[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A provider or model id that is safe to print: no whitespace and no leading `/`, so it never starts a path.
|
|
15
|
+
* A leading `@` is allowed for scoped model ids; ids may contain `/` after the first character.
|
|
16
|
+
*/
|
|
17
|
+
export declare const CATALOG_ID_PATTERN: RegExp;
|
|
18
|
+
/** Bound for one offline authentication lookup. */
|
|
19
|
+
export declare const AUTH_CHECK_TIMEOUT_MS = 5000;
|
|
20
|
+
/** The part of the SDK's `ModelRuntime` the catalog reads; created without model network and without a refresh. */
|
|
21
|
+
export interface CatalogRuntime {
|
|
22
|
+
getProviders(): readonly {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
}[];
|
|
25
|
+
getModels(providerId: string): readonly {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
}[];
|
|
28
|
+
/** Resolves the configured authentication's kind, or `undefined` when none is configured. */
|
|
29
|
+
checkAuth(providerId: string, options?: {
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
}): Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads the provider ids Pi knows offline, whether each has configured authentication, and its model
|
|
35
|
+
* ids. `checkAuth` reports only whether and how a provider is configured; its result is reduced to a
|
|
36
|
+
* boolean. `hasConfiguredAuth` is not used: it reads a snapshot that only an online availability refresh
|
|
37
|
+
* fills. Never calls `getAuth` or `getAvailable`, so no credential value is resolved and no provider is
|
|
38
|
+
* contacted. Providers registered only by Pi extensions are not part of the registry.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createModelCatalog(createRuntime?: () => Promise<CatalogRuntime>): Promise<ModelCatalog>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A provider or model id that is safe to print: no whitespace and no leading `/`, so it never starts a path.
|
|
3
|
+
* A leading `@` is allowed for scoped model ids; ids may contain `/` after the first character.
|
|
4
|
+
*/
|
|
5
|
+
export const CATALOG_ID_PATTERN = /^[A-Za-z0-9@][A-Za-z0-9._:@\/-]{0,255}$/;
|
|
6
|
+
/** Bound for one offline authentication lookup. */
|
|
7
|
+
export const AUTH_CHECK_TIMEOUT_MS = 5_000;
|
|
8
|
+
const createCatalogRuntime = () => import("@earendil-works/pi-coding-agent").then(({ ModelRuntime }) => ModelRuntime.create({ refreshOnCreate: false, allowModelNetwork: false }));
|
|
9
|
+
const compareIds = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
|
|
10
|
+
/** True only for a settled, present result; a rejection or a lookup past the bound counts as not configured. */
|
|
11
|
+
const authConfigured = (runtime, providerId) => new Promise((resolve) => {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timer = setTimeout(() => { controller.abort(); resolve(false); }, AUTH_CHECK_TIMEOUT_MS);
|
|
14
|
+
Promise.resolve()
|
|
15
|
+
.then(() => runtime.checkAuth(providerId, { signal: controller.signal }))
|
|
16
|
+
.then((auth) => resolve(auth !== undefined && auth !== null), () => resolve(false))
|
|
17
|
+
.finally(() => clearTimeout(timer));
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Reads the provider ids Pi knows offline, whether each has configured authentication, and its model
|
|
21
|
+
* ids. `checkAuth` reports only whether and how a provider is configured; its result is reduced to a
|
|
22
|
+
* boolean. `hasConfiguredAuth` is not used: it reads a snapshot that only an online availability refresh
|
|
23
|
+
* fills. Never calls `getAuth` or `getAvailable`, so no credential value is resolved and no provider is
|
|
24
|
+
* contacted. Providers registered only by Pi extensions are not part of the registry.
|
|
25
|
+
*/
|
|
26
|
+
export async function createModelCatalog(createRuntime = createCatalogRuntime) {
|
|
27
|
+
const runtime = await createRuntime();
|
|
28
|
+
const providers = await Promise.all(runtime.getProviders().map(async (provider) => {
|
|
29
|
+
const configured = await authConfigured(runtime, provider.id);
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
id: provider.id,
|
|
32
|
+
authConfigured: configured,
|
|
33
|
+
models: Object.freeze(runtime.getModels(provider.id).map((model) => model.id).sort(compareIds)),
|
|
34
|
+
});
|
|
35
|
+
}));
|
|
36
|
+
providers.sort((left, right) => compareIds(left.id, right.id));
|
|
37
|
+
return Object.freeze({ providers: () => Object.freeze(providers) });
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AppConfig } from "./config.js";
|
|
2
|
+
import type { DoctorResult } from "./doctor.js";
|
|
3
|
+
import type { ModelCatalog } from "./model-catalog.js";
|
|
4
|
+
import { type PiSessionAdapter } from "./pi-adapter.js";
|
|
5
|
+
import type { ProbeReport } from "./setup-probe-result.js";
|
|
6
|
+
export declare const PROBE_PROMPT = "Reply with exactly the word PONG and nothing else.";
|
|
7
|
+
export declare const PROBE_DEADLINES: {
|
|
8
|
+
readonly startMs: 60000;
|
|
9
|
+
readonly turnMs: 120000;
|
|
10
|
+
readonly cleanupMs: 10000;
|
|
11
|
+
};
|
|
12
|
+
export declare function isPong(reply: string): boolean;
|
|
13
|
+
export interface ProbeDependencies {
|
|
14
|
+
readonly configPath: string;
|
|
15
|
+
readonly loadConfig: (path: string) => Promise<AppConfig>;
|
|
16
|
+
readonly doctor: () => Promise<DoctorResult>;
|
|
17
|
+
readonly catalog: () => Promise<ModelCatalog>;
|
|
18
|
+
readonly adapter: () => Promise<PiSessionAdapter>;
|
|
19
|
+
readonly makeTempDir: () => Promise<string>;
|
|
20
|
+
readonly removeDir: (path: string) => Promise<void>;
|
|
21
|
+
readonly now?: () => number;
|
|
22
|
+
readonly deadlines?: typeof PROBE_DEADLINES;
|
|
23
|
+
readonly signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
export declare function runProfileProbe(request: {
|
|
26
|
+
profiles: readonly string[];
|
|
27
|
+
apply: boolean;
|
|
28
|
+
}, deps: ProbeDependencies): Promise<ProbeReport>;
|