pi-session-mcp 0.14.0 → 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/writer-boundary.js +2 -1
- package/.pi-session-mcp-cli/writer-lifecycle.js +18 -4
- package/CHANGELOG.md +39 -0
- package/README.md +1 -1
- package/dist/capabilities.d.ts +13 -0
- package/dist/capabilities.js +17 -13
- package/dist/sdk-pi-adapter.d.ts +1 -1
- package/dist/sdk-pi-adapter.js +25 -6
- 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 +189 -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/execution-modes.md +25 -3
- package/docs/isolation-setup.md +8 -2
- package/docs/roadmap.md +9 -0
- 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
|
@@ -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,45 @@
|
|
|
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
|
+
|
|
5
44
|
## 0.14.0 — 2026-10-29
|
|
6
45
|
|
|
7
46
|
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.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/sdk-pi-adapter.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ 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
|
}>;
|
package/dist/sdk-pi-adapter.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAgentSession, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createBashToolDefinition, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
2
3
|
import { homedir, userInfo } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
4
5
|
import { allowedReadRoots, createConfinedReadOnlyTools } from "./confined-tools.js";
|
|
5
6
|
import { PiExtensionError } from "./extension-spec.js";
|
|
6
7
|
import { classifyMcpStartupError, connectSessionMcpTools, } from "./mcp-session-tools.js";
|
|
@@ -298,7 +299,7 @@ function isTextBlock(block) {
|
|
|
298
299
|
return candidate.type === "text" && typeof candidate.text === "string";
|
|
299
300
|
}
|
|
300
301
|
/** Independently validated skills replace package and extension discovery on every reload. */
|
|
301
|
-
function phase0ResourceLoaderOptions(skills) {
|
|
302
|
+
function phase0ResourceLoaderOptions(skills, confinedCwd) {
|
|
302
303
|
// Keep the closure's snapshot private: the SDK may annotate or mutate returned objects.
|
|
303
304
|
const selected = skills.map((skill) => ({ ...skill, sourceInfo: { ...skill.sourceInfo } }));
|
|
304
305
|
return {
|
|
@@ -307,6 +308,24 @@ function phase0ResourceLoaderOptions(skills) {
|
|
|
307
308
|
noPromptTemplates: true,
|
|
308
309
|
noThemes: true,
|
|
309
310
|
noContextFiles: false,
|
|
311
|
+
...(confinedCwd === undefined ? {} : {
|
|
312
|
+
agentsFilesOverride: (current) => ({
|
|
313
|
+
agentsFiles: current.agentsFiles.flatMap((file) => {
|
|
314
|
+
try {
|
|
315
|
+
const workspace = realpathSync(confinedCwd), real = realpathSync(file.path);
|
|
316
|
+
const path = relative(workspace, real);
|
|
317
|
+
if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path))
|
|
318
|
+
return [];
|
|
319
|
+
// Re-read from the verified real path: the SDK read the link before this check.
|
|
320
|
+
return [{ path: file.path, content: readFileSync(real, "utf-8").replace(/^\uFEFF/, "") }];
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Unresolvable context files never enter a read-only session's model context.
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
}),
|
|
327
|
+
}),
|
|
328
|
+
}),
|
|
310
329
|
systemPrompt: "phase-0-system-prompt-disabled",
|
|
311
330
|
appendSystemPrompt: [],
|
|
312
331
|
systemPromptOverride: () => undefined,
|
|
@@ -316,9 +335,9 @@ function phase0ResourceLoaderOptions(skills) {
|
|
|
316
335
|
themesOverride: () => ({ themes: [], diagnostics: [] }),
|
|
317
336
|
};
|
|
318
337
|
}
|
|
319
|
-
export async function createPhase0ResourceLoader(cwd, skills = []) {
|
|
338
|
+
export async function createPhase0ResourceLoader(cwd, skills = [], permissionProfile) {
|
|
320
339
|
const settingsManager = SettingsManager.inMemory(undefined, { projectTrusted: false });
|
|
321
|
-
const resourceLoader = new DefaultResourceLoader({ cwd, agentDir: cwd, settingsManager, ...phase0ResourceLoaderOptions(skills) });
|
|
340
|
+
const resourceLoader = new DefaultResourceLoader({ cwd, agentDir: cwd, settingsManager, ...phase0ResourceLoaderOptions(skills, permissionProfile === "read-only" ? cwd : undefined) });
|
|
322
341
|
await resourceLoader.reload();
|
|
323
342
|
return { resourceLoader, settingsManager };
|
|
324
343
|
}
|
|
@@ -367,7 +386,7 @@ export class SdkPiSessionAdapter {
|
|
|
367
386
|
let resourceLoader;
|
|
368
387
|
let settingsManager;
|
|
369
388
|
try {
|
|
370
|
-
({ resourceLoader, settingsManager } = await createPhase0ResourceLoader(input.cwd, skills));
|
|
389
|
+
({ resourceLoader, settingsManager } = await createPhase0ResourceLoader(input.cwd, skills, executionProfile.permissionProfile));
|
|
371
390
|
}
|
|
372
391
|
catch {
|
|
373
392
|
throw this.failure(executionProfile, "resource_loader", "pi_session_creation_failed");
|
|
@@ -426,7 +445,7 @@ export class SdkPiSessionAdapter {
|
|
|
426
445
|
agentDir: cwd,
|
|
427
446
|
modelRuntime: runtime,
|
|
428
447
|
settingsManager: SettingsManager.inMemory(undefined, { projectTrusted: false }),
|
|
429
|
-
resourceLoaderOptions: { ...phase0ResourceLoaderOptions(skills), additionalExtensionPaths: directories },
|
|
448
|
+
resourceLoaderOptions: { ...phase0ResourceLoaderOptions(skills, executionProfile.permissionProfile === "read-only" ? cwd : undefined), additionalExtensionPaths: directories },
|
|
430
449
|
}));
|
|
431
450
|
this.checkResources(services.resourceLoader, skills, executionProfile, "extension_load");
|
|
432
451
|
const loaded = services.resourceLoader.getExtensions();
|
|
@@ -2,7 +2,7 @@ import type { CommandRunner } from "./client-adapters/types.js";
|
|
|
2
2
|
import { writerPaths, type WriterState } from "./writer-state.js";
|
|
3
3
|
export declare const NODE_IMAGE_REF = "docker.io/library/node@sha256:c4d5523090a817b7aa86d2111241fdd4f66d1e27782b44160e6aa63b357ecb2d";
|
|
4
4
|
export declare const IMAGE_PULL_TIMEOUT_MS = 900000;
|
|
5
|
-
export declare const BOUNDARY_GIT = "git -c safe.directory=* -c core.fsmonitor=false -c core.hooksPath=/dev/null";
|
|
5
|
+
export declare const BOUNDARY_GIT = "git -c safe.directory=* -c core.fsmonitor=false -c core.hooksPath=/dev/null -c core.useReplaceRefs=false";
|
|
6
6
|
type WriterPaths = ReturnType<typeof writerPaths>;
|
|
7
7
|
export declare class BoundaryError extends Error {
|
|
8
8
|
readonly code: "image_unavailable" | "clone_verification_failed" | "writer_fenced";
|
package/dist/writer-boundary.js
CHANGED
|
@@ -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) {
|
package/dist/writer-guard.js
CHANGED
|
@@ -13,7 +13,7 @@ const guardSchema = z.object({
|
|
|
13
13
|
}).strict();
|
|
14
14
|
const run = promisify(execFile);
|
|
15
15
|
const defaultGit = async (cwd) => {
|
|
16
|
-
const git = async (...args) => (await run("git", ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", ...args], { cwd, timeout: 10_000, encoding: "utf8" })).stdout;
|
|
16
|
+
const git = async (...args) => (await run("git", ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "-c", "core.useReplaceRefs=false", ...args], { cwd, timeout: 10_000, encoding: "utf8" })).stdout;
|
|
17
17
|
const [status, head, refs] = await Promise.all([
|
|
18
18
|
git("status", "--porcelain=v1", "-uall"),
|
|
19
19
|
git("rev-parse", "HEAD"),
|
|
@@ -66,6 +66,7 @@ export declare function provisionWriter(ctx: WriterContext, input: {
|
|
|
66
66
|
export declare function inspectWork(ctx: WriterContext, name: string): Promise<{
|
|
67
67
|
baseline: boolean;
|
|
68
68
|
digest: string;
|
|
69
|
+
head?: string;
|
|
69
70
|
branches: readonly {
|
|
70
71
|
name: string;
|
|
71
72
|
commit: string;
|
package/dist/writer-lifecycle.js
CHANGED
|
@@ -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/dist/writer-relay.js
CHANGED
|
@@ -2,6 +2,7 @@ import { constants } from "node:fs";
|
|
|
2
2
|
import { open } from "node:fs/promises";
|
|
3
3
|
import { Client } from "@modelcontextprotocol/client";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { projectExecutionProfile } from "./capabilities.js";
|
|
5
6
|
import { ChildStdioTransport } from "./child-stdio-transport.js";
|
|
6
7
|
import { PiSessionCreationError, WriterUnavailableError } from "./pi-adapter.js";
|
|
7
8
|
import { RegistryError } from "./session-registry.js";
|
|
@@ -71,6 +72,16 @@ const creationCodes = new Set([
|
|
|
71
72
|
"extension_not_installed", "extension_version_mismatch", "extension_load_failed", "extension_tool_conflict",
|
|
72
73
|
"extension_tool_missing", "skill_load_failed", "sandbox_unavailable", "pi_session_creation_failed",
|
|
73
74
|
]);
|
|
75
|
+
const exactFields = ["permissionProfile", "provider", "model", "thinkingLevel", "shell"];
|
|
76
|
+
const setFields = ["skills", "extensions", "grantedExtensionTools"];
|
|
77
|
+
const sameSet = (left, right) => {
|
|
78
|
+
const a = new Set(left), b = new Set(right);
|
|
79
|
+
return a.size === left.length && b.size === right.length && a.size === b.size && [...a].every(value => b.has(value));
|
|
80
|
+
};
|
|
81
|
+
/** Admission compares every public capability field; a duplicate on either side is a mismatch. */
|
|
82
|
+
function samePublishedProfile(host, inner) {
|
|
83
|
+
return exactFields.every(key => host[key] === inner[key]) && setFields.every(key => sameSet(host[key], inner[key]));
|
|
84
|
+
}
|
|
74
85
|
class InnerError extends Error {
|
|
75
86
|
code;
|
|
76
87
|
constructor(code) {
|
|
@@ -306,7 +317,7 @@ export class WriterRelayAdapter {
|
|
|
306
317
|
const capabilities = await connection.call("pi_capabilities_get", {}, capabilitiesSchema, remaining(), input.signal);
|
|
307
318
|
const inner = capabilities.executionProfiles.find(profile => profile.alias === input.executionProfile.alias);
|
|
308
319
|
const host = this.options.executionProfiles.get(input.executionProfile.alias);
|
|
309
|
-
if (!host || !inner ||
|
|
320
|
+
if (!host || !inner || !samePublishedProfile(projectExecutionProfile(host), inner)) {
|
|
310
321
|
counts.settingsChanged++;
|
|
311
322
|
continue;
|
|
312
323
|
}
|
package/docs/acceptance.md
CHANGED
|
@@ -2598,6 +2598,195 @@ image) reported `writer_accepted` from 09:48 to 09:51 UTC. Client-target
|
|
|
2598
2598
|
all 14 writers. Doctor reported no error, only `workspace_mode_unset` for one
|
|
2599
2599
|
workspace without a mode.
|
|
2600
2600
|
|
|
2601
|
+
### v0.14.1 pre-release acceptance
|
|
2602
|
+
|
|
2603
|
+
`v0.14.1` was accepted on 2026-10-30 from 06:04 to 06:15 UTC on its release
|
|
2604
|
+
candidate `a239610`, on Linux (`x64`) with Node.js `26.12.1`, npm `12.2.1`,
|
|
2605
|
+
Codex CLI `0.162.1` and Claude Code `2.1.314`. Level 2 passed in the working
|
|
2606
|
+
tree and for the packed tarball, each with delivery off and with delivery on.
|
|
2607
|
+
Level 1 passed except `npm audit`. Later commits on the release pull request
|
|
2608
|
+
change only this record.
|
|
2609
|
+
|
|
2610
|
+
The patch release ships Sprint 12 (#192, #193, #194) from `main` at `d9a47f5`;
|
|
2611
|
+
the candidate adds only the version, the changelog heading and the roadmap
|
|
2612
|
+
line. The MCP tool contract, result codes and configuration schema are
|
|
2613
|
+
unchanged. The source changes were accepted on the branch before the merge, see
|
|
2614
|
+
[Sprint 12 candidate acceptance](#sprint-12-candidate-acceptance-2026-10-30),
|
|
2615
|
+
including Level 3.
|
|
2616
|
+
|
|
2617
|
+
- Level 1 in the checkout of the candidate after `npm ci`: every command exited
|
|
2618
|
+
with `0`, except `npm audit`; `git status --short` printed nothing.
|
|
2619
|
+
- The test run, with `NODE_OPTIONS=--unhandled-rejections=strict`, reported
|
|
2620
|
+
141 files, 4044 passed and 14 skipped, and no unhandled error.
|
|
2621
|
+
- `npm audit` and `npm audit --omit=dev` exited with `1` and name only the
|
|
2622
|
+
known `undici` 8.10.2 finding through `@earendil-works/pi-coding-agent`
|
|
2623
|
+
1.4.0, the latest SDK release, with eight advisories
|
|
2624
|
+
([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
|
|
2625
|
+
- `npm pack` listed 933 files (tarball shasum
|
|
2626
|
+
`47cf66a3856530112c11095d07cb05b7ab9892f4`, identical in three packs). A
|
|
2627
|
+
scan of the unpacked files for private names, hosts and the maintainer's
|
|
2628
|
+
home path found none.
|
|
2629
|
+
- Doctor reported `packageVersion` `0.14.1`. It exited with `2` because no
|
|
2630
|
+
configuration was set (`config_missing`).
|
|
2631
|
+
- Level 2 in the working tree: 160 of 160 checks passed with delivery off and
|
|
2632
|
+
228 of 228 with `--events spool`.
|
|
2633
|
+
- Packed-tarball Level 2 from a fresh prefix: 160 of 160 with delivery off and
|
|
2634
|
+
228 of 228 with `--events spool`. The install smoke from another fresh prefix
|
|
2635
|
+
reported `smoke_passed` with version `0.14.1`.
|
|
2636
|
+
- In every Level 2 run the real client registrations were unchanged, no process
|
|
2637
|
+
remained, the leak check passed and the disposable directories were removed.
|
|
2638
|
+
- Podman-gated writer tests on the candidate (Podman `6.1.4`): 8 of 8 passed
|
|
2639
|
+
(7 lifecycle tests, including the detached-`HEAD` export of #193, and 1
|
|
2640
|
+
boundary test).
|
|
2641
|
+
- Bubblewrap-gated sandbox tests (`test/sandbox-shell.bwrap.test.ts`, bubblewrap
|
|
2642
|
+
`0.13.1`): 12 of 12 passed, none skipped.
|
|
2643
|
+
|
|
2644
|
+
Not established for `0.14.1` beyond the Sprint 12 record: a Level 3 cycle on
|
|
2645
|
+
the release candidate itself, and the published-release Level 2 and upgrades,
|
|
2646
|
+
which follow after publishing.
|
|
2647
|
+
|
|
2648
|
+
### Sprint 12 candidate acceptance (2026-10-30)
|
|
2649
|
+
|
|
2650
|
+
Recorded on 2026-10-29 and 2026-10-30 for the unreleased branch
|
|
2651
|
+
`fix/s12-audit-findings` ([Sprint 12](sprints/2026-10-29-s12-audit-fixes.md),
|
|
2652
|
+
#192, #193, #194). No release, tag or npm publish.
|
|
2653
|
+
|
|
2654
|
+
- Level 1 on the final source `9426682` in a separate detached worktree with
|
|
2655
|
+
Node.js `26.12.1`: `npm ci`, typecheck, `build:cli`, the test run (530 files,
|
|
2656
|
+
4044 passed, 14 skipped, 0 failed, no unhandled error, with
|
|
2657
|
+
`NODE_OPTIONS=--unhandled-rejections=strict`), `build` and
|
|
2658
|
+
`git diff --check 9d9cf8c..HEAD` exited with `0`. The same on the earlier
|
|
2659
|
+
candidate `896e29d`: 4043 passed, 0 failed.
|
|
2660
|
+
- Real rootless Podman with the pinned Node image (git `2.39.5`):
|
|
2661
|
+
`test/writer-lifecycle.podman.test.ts` 7 of 7, including the new clean
|
|
2662
|
+
detached-HEAD case, which fails against the source of `9d9cf8c`. In the image,
|
|
2663
|
+
`core.useReplaceRefs=false` makes a graft-hidden detached commit visible
|
|
2664
|
+
again.
|
|
2665
|
+
- Level 2 on `npm pack` of `896e29d`, installed into a fresh prefix below the
|
|
2666
|
+
home directory: 160 of 160 with delivery off and 228 of 228 with
|
|
2667
|
+
`--events spool`; real registrations and built files unchanged, no process
|
|
2668
|
+
left, path-leak scan 0 hits. Clients: Codex CLI `0.162.1` and Claude Code
|
|
2669
|
+
`2.1.312`. The two later commits change the guard's Git arguments and a
|
|
2670
|
+
regular-expression spelling; Level 2 was not repeated for them.
|
|
2671
|
+
- Live profile change (item D), on an expendable test configuration: a
|
|
2672
|
+
candidate with the new read-only profile `review-sonnet`
|
|
2673
|
+
(`anthropic/claude-sonnet-5-5`), Doctor without error, probe dry run, one
|
|
2674
|
+
probe turn (`probe_passed`), timestamped backup and atomic replacement,
|
|
2675
|
+
Doctor, then real use by a fresh server for the R1 and final reviews; rolled
|
|
2676
|
+
back to the backup afterwards, Doctor without error. No production
|
|
2677
|
+
configuration or writer was touched.
|
|
2678
|
+
- Live container writer (items B and C), with the packed candidate, an
|
|
2679
|
+
expendable configuration and a synthetic repository: profile `s12-code`
|
|
2680
|
+
(`deepseek/deepseek-flash`, coding, skill `pi-session-implementation`),
|
|
2681
|
+
one probe turn (`probe_passed`); `--repo s12-probe --mode container
|
|
2682
|
+
--profiles s12-code --writers 1 --apply` reported `writer_provisioned` and
|
|
2683
|
+
`writer_accepted`, `--writer s12-probe-w1 --verify` `mcp_verified`. A real
|
|
2684
|
+
relayed coding session completed its task; `--collect --apply` imported
|
|
2685
|
+
`refs/remotes/writer-s12-probe-w1/s12-live-c`, whose commit passed the
|
|
2686
|
+
oracle (4 of 4 tests), and `--finish --apply` reported `writer_reset`. With
|
|
2687
|
+
the host profile's skills then changed, a start failed with
|
|
2688
|
+
`writer_unavailable`, reason `none_admitted`, counts
|
|
2689
|
+
`settingsChanged: 1`, and collection afterwards found `nothing_to_collect`.
|
|
2690
|
+
After `--writer s12-probe-w1 --remove` and the provisioning request again,
|
|
2691
|
+
the writer was accepted and verified and a start succeeded. Rolled back:
|
|
2692
|
+
writer removed, configuration, repository and prefix deleted.
|
|
2693
|
+
- Level 3, ten-task cycle, on the final source `9426682` (`dist/main.js`
|
|
2694
|
+
SHA-256 `ab93e3a932b1d5147dcbb48bbb85d9442c2214e486101f8d427360b99672cfdb`)
|
|
2695
|
+
from a separate detached checkout, with a temporary credential-free
|
|
2696
|
+
configuration (`l3-read` read-only and `l3-code` coding, both
|
|
2697
|
+
`deepseek/deepseek-flash`), synthetic fixtures, events off, one probe turn
|
|
2698
|
+
per profile as preflight. Claude Code `2.1.312` ran as `claude -p` with a
|
|
2699
|
+
per-invocation `--mcp-config`, `--strict-mcp-config` and
|
|
2700
|
+
`--model claude-sonnet-5-5`; Codex CLI `0.162.1` as `codex exec
|
|
2701
|
+
--ignore-user-config` with `-c mcp_servers.*` overrides. Both clients could
|
|
2702
|
+
wait with `sleep` between polls.
|
|
2703
|
+
- Cycle 1 was not accepted: 5 of 10. The worker ignored a requested
|
|
2704
|
+
ordering in both follow-ups (T2), refused the abort task's essay prompt
|
|
2705
|
+
within about 1.5 seconds so that nothing was left to abort (T5), and the
|
|
2706
|
+
Codex coding snapshot could not be written from the Codex sandbox (T3). A
|
|
2707
|
+
path hit came from the worker's `assistantText`, not from server
|
|
2708
|
+
metadata. No Pi Session MCP defect was found; the cycle is kept unchanged
|
|
2709
|
+
with its own pre-registration.
|
|
2710
|
+
- Cycle 2, with a new pre-registration before its first task (order-free
|
|
2711
|
+
LOW oracle, a long deterministic listing as the abort prompt, snapshots
|
|
2712
|
+
inside the controller directory, the leak scan separating server metadata
|
|
2713
|
+
from `assistantText`): 10 of 10. Read-only fixture digests unchanged; both
|
|
2714
|
+
coding snapshots pass their oracle and the fixture tests and show only the
|
|
2715
|
+
allowed files; both abort tasks show running, abort, `aborted`, close, an
|
|
2716
|
+
empty `pi_session_list` and a fresh `idle` session. Zero leak hits, also in
|
|
2717
|
+
`assistantText`; no server or client process before or 10 seconds after any
|
|
2718
|
+
invocation. Polls per turn: Claude Code 1 to 2, Codex 2 to 4. Deviation:
|
|
2719
|
+
the Codex sandbox refused to spawn `git`, so the Codex snapshots' status
|
|
2720
|
+
lists were derived from the barrier snapshot trees against `HEAD`.
|
|
2721
|
+
The cycle-2 gate holds.
|
|
2722
|
+
|
|
2723
|
+
Not established: a release of these fixes, the operator rollout on either
|
|
2724
|
+
machine, macOS, and #120.
|
|
2725
|
+
|
|
2726
|
+
### v0.14.0 post-release evidence
|
|
2727
|
+
|
|
2728
|
+
Recorded on 2026-10-29 for the published release `0.14.0`.
|
|
2729
|
+
|
|
2730
|
+
Tag binding: a fresh clone of the annotated tag `v0.14.0` (tag object
|
|
2731
|
+
`f3a8b088a85fd5825c3256f4a7950ba051cbf388`) peels to the merge commit
|
|
2732
|
+
`fd4fdeacf9d02c04b6fd89314f7ec819b5506ed9` of #190, and
|
|
2733
|
+
`git describe --tags --exact-match` reports `v0.14.0`. CI on the merge commit
|
|
2734
|
+
(run 40928709705) and on the tag (run 40929602738) passed on the first attempt.
|
|
2735
|
+
The tag was created after the npm package had been published; the package
|
|
2736
|
+
records `gitHead` `d904fed`, the head of #190, whose tree (`3ae966b`) is
|
|
2737
|
+
identical to the merge commit's.
|
|
2738
|
+
|
|
2739
|
+
- Level 1 on the tag clone after `npm ci`, from 18:18 to 18:20 UTC with
|
|
2740
|
+
Node.js `26.12.1` and npm `12.2.1`: typecheck, `build:cli`, the test run
|
|
2741
|
+
(139 files, 4005 passed, 14 skipped, with
|
|
2742
|
+
`NODE_OPTIONS=--unhandled-rejections=strict`), `build` and
|
|
2743
|
+
`git diff --check` exited with `0`, and the status stayed clean. Doctor
|
|
2744
|
+
reported `packageVersion` `0.14.0` and exited with `2` because no
|
|
2745
|
+
configuration was set (`config_missing`).
|
|
2746
|
+
- `pi-session-mcp@0.14.0` on npm: 932 files, shasum
|
|
2747
|
+
`579cc111a7aa62987b5f35e79b34b69853c8a0e3`. It was published by the
|
|
2748
|
+
maintainer; `npm pack` of the tag clone gives the same shasum and file count.
|
|
2749
|
+
- Published npm package, Level 2 from a fresh prefix, 18:19 to 18:21 UTC:
|
|
2750
|
+
160 of 160 with delivery off and 228 of 228 with `--events spool`; real
|
|
2751
|
+
registrations and built files unchanged, no process left, leak check passed.
|
|
2752
|
+
Clients: Codex CLI `0.162.1` and Claude Code `2.1.312`. The install smoke on
|
|
2753
|
+
the published package reported `smoke_passed` with version `0.14.0`.
|
|
2754
|
+
- The GitHub release `v0.14.0` was published the same day.
|
|
2755
|
+
|
|
2756
|
+
Operator upgrade of the workstation from `0.13.1` to `0.14.0` on 2026-10-29
|
|
2757
|
+
from 18:33 UTC, following
|
|
2758
|
+
[Installation](installation.md#container-writers): no Pi session of this
|
|
2759
|
+
controller was open; other controller sessions without a coding session stayed
|
|
2760
|
+
open and need a reconnect. Both client targets were removed with `0.13.1`, then
|
|
2761
|
+
`npm install -g pi-session-mcp@0.14.0`. Doctor reported only
|
|
2762
|
+
`writer_not_ready` for the 18 writers. A dry run of the first provisioning
|
|
2763
|
+
command with unchanged settings reported `mode_unchanged` and
|
|
2764
|
+
`writer_planned`; with `--apply`, five container repositories with 18 writers
|
|
2765
|
+
(2 with `--tags reachable`, 4, 4 with `--cache go`, 4 with base `dev`, 4 with
|
|
2766
|
+
`--cache uv`, three of them with operator images) reported `writer_accepted`
|
|
2767
|
+
from 18:34 to 18:37 UTC. Client-target `--apply` and `--verify` passed for
|
|
2768
|
+
`claude-code:user` and `codex:user` (`mcp_verified`, `skill_verified` for both
|
|
2769
|
+
controller skills, and `hook_verified` for the wake hook); `--writer <name>
|
|
2770
|
+
--verify` passed for all 18 writers. Doctor reported no error, only the known
|
|
2771
|
+
`workspace_gitfile` and `workspace_mode_unset` warnings of the worktree
|
|
2772
|
+
aliases.
|
|
2773
|
+
|
|
2774
|
+
Operator upgrade of the second machine (Linux) from `0.13.1` to `0.14.0` on
|
|
2775
|
+
2026-10-29 from 18:38 UTC, with Node.js `24.22.0` and the same steps; one
|
|
2776
|
+
controller session's server process stayed open and needs a reconnect. Both
|
|
2777
|
+
client targets were removed with `0.13.1`, then
|
|
2778
|
+
`npm install -g pi-session-mcp@0.14.0`; Doctor reported only
|
|
2779
|
+
`writer_not_ready` for the 14 writers. With `--apply`, four container
|
|
2780
|
+
repositories with 14 writers (4, 2, 4 with an operator image and `--cache go`,
|
|
2781
|
+
4 with an operator image) reported `writer_accepted` from 18:39 to 18:42 UTC.
|
|
2782
|
+
Client-target `--apply` and `--verify` passed for `claude-code:user` and
|
|
2783
|
+
`codex:user` (`mcp_verified`, `skill_verified` for both controller skills), and
|
|
2784
|
+
`--writer <name> --verify` passed for all 14 writers. Doctor reported no
|
|
2785
|
+
error, only `workspace_mode_unset` for one workspace without a mode.
|
|
2786
|
+
|
|
2787
|
+
Not established for `0.14.0` after the upgrade: a coding session through a
|
|
2788
|
+
re-accepted writer, an agent-driven profile change end to end, and Level 3.
|
|
2789
|
+
|
|
2601
2790
|
### v0.14.0 pre-release acceptance
|
|
2602
2791
|
|
|
2603
2792
|
`v0.14.0` was accepted on 2026-10-29 from 17:56 to 18:01 UTC on its release
|
|
@@ -161,12 +161,16 @@ base.
|
|
|
161
161
|
In both cases the host server closes that launcher and tries the next
|
|
162
162
|
candidate. The inner writer guard stays the authority for baseline admission
|
|
163
163
|
inside the boundary.
|
|
164
|
-
- Before the inner start the relay compares
|
|
165
|
-
the writer's validated `pi_capabilities_get`
|
|
166
|
-
|
|
167
|
-
`
|
|
168
|
-
|
|
169
|
-
starts
|
|
164
|
+
- Before the inner start the relay compares every public capability field of
|
|
165
|
+
the requested profile's entry in the writer's validated `pi_capabilities_get`
|
|
166
|
+
with the host's configured profile: `permissionProfile`, `provider`, `model`,
|
|
167
|
+
`thinkingLevel` and `shell` exactly, and `skills`, `extensions` and
|
|
168
|
+
`grantedExtensionTools` as sets. A mismatch or a missing entry counts as
|
|
169
|
+
`settingsChanged`, starts no inner session and moves on to the next
|
|
170
|
+
candidate, so a writer never runs an outdated version of a host profile. The
|
|
171
|
+
launcher cannot do this check: it starts before the profile is known.
|
|
172
|
+
(Amended 2026-10-29, #194: the comparison covers every public capability
|
|
173
|
+
field, not only permission profile, provider, model and thinking level.)
|
|
170
174
|
- If no candidate is admitted, the start fails with `writer_unavailable`, a
|
|
171
175
|
content-free reason and content-free counts. `none_ready`: no writer of the
|
|
172
176
|
workspace is ready, migrated and has a recorded acceptance (whether that
|
|
@@ -145,6 +145,36 @@ directly after `grantedExtensionTools`. This is configured intent only.
|
|
|
145
145
|
MCP tools and the granted extension tools. A profile with granted extension
|
|
146
146
|
tools cannot have a sandboxed shell.
|
|
147
147
|
|
|
148
|
+
### Amendment 2026-10-29: context files
|
|
149
|
+
|
|
150
|
+
Issue #192. Previously every resource loader kept the SDK behaviour: Pi
|
|
151
|
+
context files (`AGENTS.override.md`, `AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`,
|
|
152
|
+
`CLAUDE.MD`) were loaded from the session directory and every ancestor
|
|
153
|
+
directory, and a context file reached through a symlink was followed to its
|
|
154
|
+
target, so a read-only session also loaded context from outside the workspace.
|
|
155
|
+
|
|
156
|
+
New rule: for **read-only profiles** every resource loader keeps only context
|
|
157
|
+
files whose real path lies inside the workspace's real path, on every reload
|
|
158
|
+
and before the first provider request. A symlinked context file that resolves
|
|
159
|
+
outside the workspace, a dangling one, and the context files of ancestor
|
|
160
|
+
directories above the workspace (including a main repository above a linked
|
|
161
|
+
worktree) are not loaded for read-only profiles; the linked worktree's own file
|
|
162
|
+
inside the workspace is. Coding profiles keep the SDK behaviour (the working
|
|
163
|
+
directory and its ancestors).
|
|
164
|
+
|
|
165
|
+
The filter runs on the SDK's result, after the SDK has picked the first
|
|
166
|
+
existing name per directory. If that file is dropped, the directory contributes
|
|
167
|
+
no context file: an `AGENTS.override.md` that resolves outside the workspace
|
|
168
|
+
also hides an `AGENTS.md` beside it. This fails closed. A kept file's content
|
|
169
|
+
is read again from its verified real path, so a link swapped after the SDK
|
|
170
|
+
read cannot bring in outside content.
|
|
171
|
+
|
|
172
|
+
Why: the read roots of this ADR. Section 1 confines the read tools of
|
|
173
|
+
read-only profiles to the workspace and the skill directories; context files
|
|
174
|
+
are model input rather than tool reads, and before this amendment they were the
|
|
175
|
+
one channel through which content from outside those roots still reached a
|
|
176
|
+
read-only session.
|
|
177
|
+
|
|
148
178
|
## Contract change and versioning
|
|
149
179
|
|
|
150
180
|
The capabilities field `shell`, the start error `sandbox_unavailable` and the
|
package/docs/execution-modes.md
CHANGED
|
@@ -182,10 +182,24 @@ pi-session-mcp-setup --writer my-project-w1 --finish --apply
|
|
|
182
182
|
|
|
183
183
|
Collection exports changed branches via a bundle into the operator's checkout as
|
|
184
184
|
`refs/remotes/writer-<name>/<branch>`. Uncommitted/untracked work is captured as a
|
|
185
|
-
synthetic `<branch>-wip` commit.
|
|
185
|
+
synthetic `<branch>-wip` commit. A clean detached HEAD whose commit no branch in
|
|
186
|
+
the clone reaches is exported as `<base>-detached` (for example `main-detached`,
|
|
187
|
+
imported as `refs/remotes/writer-<name>/main-detached`); a dirty detached HEAD
|
|
188
|
+
keeps the `<base>-wip` export. A `<base>-detached` or `<branch>-wip` name that
|
|
189
|
+
an exported branch already takes, or that conflicts with an exported branch's
|
|
190
|
+
path (such as a branch `main-detached/topic`), is refused and makes collection
|
|
191
|
+
incomplete (`writer_collect_incomplete`); the other branches are still
|
|
192
|
+
exported. Clone-side Git ignores replace refs. These refs are **untrusted until reviewed**;
|
|
186
193
|
setup does not merge or cherry-pick them. Collection does not change the clone.
|
|
187
|
-
Only a complete export records a digest authorizing later destructive cleanup
|
|
188
|
-
|
|
194
|
+
Only a complete export records a digest authorizing later destructive cleanup,
|
|
195
|
+
and the digest is recorded only when the clone's HEAD commit and every branch
|
|
196
|
+
commit exist in the operator checkout; otherwise collection is incomplete
|
|
197
|
+
(`writer_collect_incomplete`) and no digest is recorded. This includes the base
|
|
198
|
+
commit: if the operator checkout no longer has it (for example after a deleted
|
|
199
|
+
base branch and garbage collection), collect and finish fail this way even on a
|
|
200
|
+
pristine writer, without refused names; fetch the base again or resolve with
|
|
201
|
+
`--reset --discard --apply`. New work invalidates
|
|
202
|
+
that authorization. Stashes are not exported as branches:
|
|
189
203
|
resolve any needed stash inside the boundary before collection.
|
|
190
204
|
|
|
191
205
|
`--finish` collects, then resets a persistent writer or recreates an ephemeral
|
|
@@ -234,6 +248,14 @@ reset updates admission without a server restart. A missing/unusable runtime or
|
|
|
234
248
|
stale/unready writer launches a refusal MCP server: all eight tools return the
|
|
235
249
|
sanitized refusal, with no host fallback. See [Tool contracts](tool-contracts.md).
|
|
236
250
|
|
|
251
|
+
Before the relay starts an inner session it compares every public capability
|
|
252
|
+
field of the writer's profile with the host profile: `permissionProfile`,
|
|
253
|
+
`provider`, `model`, `thinkingLevel` and `shell` exactly, and `skills`,
|
|
254
|
+
`extensions` and `grantedExtensionTools` as sets. A mismatch or a missing
|
|
255
|
+
entry counts as `settingsChanged`, starts no inner session and moves on to the
|
|
256
|
+
next writer; if no writer is admitted, the start fails with
|
|
257
|
+
`writer_unavailable` and reason `none_admitted`.
|
|
258
|
+
|
|
237
259
|
## Reports and finding codes
|
|
238
260
|
|
|
239
261
|
Reports use sanitized targets `modes/workspace/<alias>` or `writer/<alias>/<name>`
|
package/docs/isolation-setup.md
CHANGED
|
@@ -225,12 +225,18 @@ Git metadata can redirect reads to worker-controlled configuration. Setup checks
|
|
|
225
225
|
clone layout and runs clone-side Git only in throwaway no-network containers.
|
|
226
226
|
Collect with `--writer <name> --collect --apply`, then review
|
|
227
227
|
`refs/remotes/writer-<name>/<branch>` in your own checkout, with dirty work at
|
|
228
|
-
`<branch>-wip
|
|
228
|
+
`<branch>-wip` and a clean detached HEAD at `<base>-detached` (for example
|
|
229
|
+
`main-detached`). Imported refs are untrusted until reviewed.
|
|
229
230
|
|
|
230
231
|
After taking over the result and closing its coding session, run
|
|
231
232
|
`pi-session-mcp-setup --writer <name> --finish --apply` before another task.
|
|
232
233
|
It collects then resets (persistent) or recreates (ephemeral). If any branch name
|
|
233
|
-
was refused, collection is incomplete and finish stops
|
|
234
|
+
was refused, collection is incomplete and finish stops; a `<base>-detached` or
|
|
235
|
+
`<branch>-wip` name that an exported branch takes or whose path it conflicts
|
|
236
|
+
with is refused the same way. The release digest is
|
|
237
|
+
recorded only when the clone's HEAD commit and every branch commit exist in the
|
|
238
|
+
operator checkout, otherwise collection is `writer_collect_incomplete` and no
|
|
239
|
+
digest is recorded. Rename inside the writer
|
|
234
240
|
or have the operator explicitly resolve loss with `--reset --discard --apply`.
|
|
235
241
|
Finish returns persistent and ephemeral writers to the workspace default base.
|
|
236
242
|
No reconnect is needed after finish, reset or recreate: each session starts a
|
package/docs/roadmap.md
CHANGED
|
@@ -41,6 +41,15 @@ mean they are implemented or that their gates passed.
|
|
|
41
41
|
profile), and the shipped controller skill `pi-session-profiles`, with which a
|
|
42
42
|
controlling agent creates, changes or retires profiles without a guided
|
|
43
43
|
dialog. Recorded in [ADR 0014](adr/0014-profile-probe-and-model-catalog.md).
|
|
44
|
+
- 2026-10-30: [Sprint 12](sprints/2026-10-29-s12-audit-fixes.md) fixes three
|
|
45
|
+
audit findings on `9d9cf8c` in one pull request, released in patch release
|
|
46
|
+
0.14.1: read-only context files confined to the workspace
|
|
47
|
+
([#192](https://github.com/plagor44-ux/pi-session-mcp/issues/192)), detached
|
|
48
|
+
`HEAD` collection and the release-digest gate
|
|
49
|
+
([#193](https://github.com/plagor44-ux/pi-session-mcp/issues/193)) and the
|
|
50
|
+
full writer profile comparison in the relay
|
|
51
|
+
([#194](https://github.com/plagor44-ux/pi-session-mcp/issues/194)). Levels 1
|
|
52
|
+
to 3 passed for the candidate. #120 still waits for a Pi SDK release.
|
|
44
53
|
|
|
45
54
|
The explicit 2026-10-17 user request adds a bounded development-only
|
|
46
55
|
[token-footprint measurement](sprints/2026-10-17-token-footprint.md), following
|
|
@@ -4,9 +4,11 @@ Planning basis: main `47b8d26` (Sprint 7 merged), installed release 0.11.2.
|
|
|
4
4
|
Tracking: [#79](https://github.com/plagor44-ux/pi-session-mcp/issues/79),
|
|
5
5
|
prerequisite [#121](https://github.com/plagor44-ux/pi-session-mcp/issues/121).
|
|
6
6
|
|
|
7
|
-
Status: **
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Status: **in progress** — steps 1–4 are done (pilot `pi-session-mcp`,
|
|
8
|
+
`node-app`, the operator images, go-app) and steps 5–7 are open; see
|
|
9
|
+
[Progress (2026-10-27)](#progress-2026-10-27) and the v0.11.6 and v0.11.8
|
|
10
|
+
post-release evidence in [Acceptance](../acceptance.md). The maintainer made
|
|
11
|
+
the choices on 2026-10-25; see [Decisions](#decisions-2026-10-25).
|
|
10
12
|
|
|
11
13
|
## Mandate
|
|
12
14
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Sprint 12: audit findings F1–F3
|
|
2
|
+
|
|
3
|
+
Planning basis: main `9d9cf8ca49e6e34dc16be6846b9324ad26ad92b9` (merge of PR
|
|
4
|
+
#191), published 0.14.0, 2026-10-29. Entry: an external audit of that commit
|
|
5
|
+
reported three defects with reproductions. This file plans the work and records
|
|
6
|
+
it as it lands; it claims nothing that its progress log does not state with
|
|
7
|
+
evidence.
|
|
8
|
+
|
|
9
|
+
Branch: `fix/s12-audit-findings`, one pull request, no release.
|
|
10
|
+
|
|
11
|
+
## Preflight (2026-10-29)
|
|
12
|
+
|
|
13
|
+
- main and `origin/main` at `9d9cf8c`; open issues #79, #120, #144; no open
|
|
14
|
+
pull requests; clean operator checkout; no other worktree in use.
|
|
15
|
+
- Doctor: no errors; only the known `workspace_gitfile` and
|
|
16
|
+
`workspace_mode_unset` warnings of the host worktree aliases.
|
|
17
|
+
- Level 1 on `9d9cf8c` in a clean detached worktree on the workstation:
|
|
18
|
+
`npm ci`, `npm run typecheck`, `npm run build:cli`,
|
|
19
|
+
`NODE_OPTIONS=--unhandled-rejections=strict npm test` (527 files, 4005
|
|
20
|
+
passed, 14 skipped, 0 failed, no unhandled error) and `npm run build` all exit
|
|
21
|
+
0. The audit's cloud run of the same commit reported 70 failed tests and four
|
|
22
|
+
unhandled errors; they do not reproduce here, so they are not attributed to
|
|
23
|
+
the product. The audit environment is not available to separate them further.
|
|
24
|
+
- Both `pi-session-mcp` writers were at `baseline` on an older main and were
|
|
25
|
+
reset without `--discard` to `9d9cf8c` (`writer_reset`).
|
|
26
|
+
|
|
27
|
+
## Findings, re-checked on `9d9cf8c`
|
|
28
|
+
|
|
29
|
+
| Id | Issue | Reproduction by the controller | Result |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| F1 | [#192](https://github.com/plagor44-ux/pi-session-mcp/issues/192) | Synthetic workspace whose `AGENTS.md` links to an outside marker file; `PI_CODING_AGENT_DIR` isolated; faux provider | Confined `read` refuses; `getAgentsFiles()` contains the marker; the real `SdkPiSessionAdapter` sends it as `project_instructions` to the faux provider. Confirmed. |
|
|
32
|
+
| F2 | [#193](https://github.com/plagor44-ux/pi-session-mcp/issues/193) | Real Git; boundary scripts run by an injected runner | Detached `HEAD` commit, clean tree: remove before collect `writer_uncollected`; collect `nothing_to_collect`, 0 refs, digest stored; commit absent from the operator checkout; finish `writer_reset`; remove `writer_removed`. Feature-branch counterproof exports the commit. Confirmed; not yet a Podman result. |
|
|
33
|
+
| F3 | [#194](https://github.com/plagor44-ux/pi-session-mcp/issues/194) | Real relay over stdio, fake writer server fixture | Identical profiles admitted; model drift refused with `settingsChanged`; host `skills: ["pi-session-implementation"]` against inner `skills: []` admitted. Confirmed. |
|
|
34
|
+
|
|
35
|
+
## Decisions taken at planning
|
|
36
|
+
|
|
37
|
+
- F1: for read-only profiles every resource loader keeps only context files
|
|
38
|
+
whose real path lies inside the workspace's real path. Context files of
|
|
39
|
+
ancestor directories above the workspace, including a main repository above a
|
|
40
|
+
linked worktree, are no longer loaded for read-only profiles: they are outside
|
|
41
|
+
the read roots of ADR 0013. Coding profiles keep the SDK's behaviour.
|
|
42
|
+
- F2: smallest fix inside the existing collection contract; the export name for
|
|
43
|
+
a detached `HEAD` is a documented behaviour addition, no new public code.
|
|
44
|
+
- F3: the relay compares every public capability field a writer reports, with
|
|
45
|
+
set semantics for skills, extensions and granted extension tools.
|
|
46
|
+
- #120: still waiting for an SDK release (see below); no override, no
|
|
47
|
+
`npm audit fix --force`.
|
|
48
|
+
- No release on this branch.
|
|
49
|
+
|
|
50
|
+
## Roles and routing
|
|
51
|
+
|
|
52
|
+
`pi_capabilities_get` and `--list-models` on 2026-10-29 settle which requested
|
|
53
|
+
models exist as authenticated profiles:
|
|
54
|
+
|
|
55
|
+
| Requested | Profile used | Status |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| GPT 6.1 Sol | `code` (openai `gpt-6.1-sol`) | available |
|
|
58
|
+
| GPT 6.1 Terra | none | unavailable: the catalog lists no `gpt-6.1-terra`; not substituted with another version |
|
|
59
|
+
| DeepSeek | `code-deepseek` (`deepseek-flash`) | available |
|
|
60
|
+
| GLM 5.3 | `review-glm`, `code-glm` (`zai-glm-5-3`) | available |
|
|
61
|
+
| Mistral Large 4 | `code-mistral` (`mistral-large-4`) | available |
|
|
62
|
+
| Sonnet 5.5 | no profile; provider `anthropic` authenticated | added only in an expendable test configuration (acceptance item D) |
|
|
63
|
+
| Grok 4.7 | `review-grok` (xai `grok-4.7`) | available: the R2 turn completed |
|
|
64
|
+
|
|
65
|
+
agy (Antigravity CLI, Gemini) plans the work packages, dependencies and review
|
|
66
|
+
routing; Pi workers implement and review; the controller integrates, verifies
|
|
67
|
+
and is the only editor of this file, the roadmap, `docs/acceptance.md` and the
|
|
68
|
+
CHANGELOG.
|
|
69
|
+
|
|
70
|
+
## Tasks
|
|
71
|
+
|
|
72
|
+
| Id | Task | Route | Change area |
|
|
73
|
+
| --- | --- | --- | --- |
|
|
74
|
+
| P1 | Work packages, dependencies, review routing | agy | none |
|
|
75
|
+
| P2 | Independent regression test plan | `review-glm` | none |
|
|
76
|
+
| T1 | F1, #192 | `pi-session-mcp` writer, `code` | `src/sdk-pi-adapter.ts`, `src/confined-tools.ts`, tests |
|
|
77
|
+
| T2 | F2, #193 | `pi-session-mcp` writer, `code-mistral` (agy's fallback for the unavailable Terra) | `src/writer-lifecycle.ts`, tests |
|
|
78
|
+
| T3 | F3, #194 | `pi-session-mcp` writer, `code-deepseek` | `src/writer-relay.ts`, `src/capabilities.ts`, tests, relay fixture |
|
|
79
|
+
| R1 | Security review of T1 and T3 | Sonnet 5.5, read-only | none |
|
|
80
|
+
| R2 | Adversarial review of T2 and the Git lifecycle checks | `review-grok` | none |
|
|
81
|
+
| D1 | Documentation and issue reconciliation (#120, ADR 0014 status, Sprint 8 status, #79, #144) | `pi-session-mcp` writer, `code-mistral`; controller as editor | `docs/`, `skills/pi-session-orchestration/SKILL.md` |
|
|
82
|
+
| A | Live acceptance A–E | controller | evaluation record |
|
|
83
|
+
|
|
84
|
+
## Progress log
|
|
85
|
+
|
|
86
|
+
Recorded as work lands on the branch.
|
|
87
|
+
|
|
88
|
+
- 2026-10-29: issues #192, #193 and #194 filed; this plan written before any
|
|
89
|
+
implementation.
|
|
90
|
+
- P1: agy (`gemini-3.8-flash-high`, plan mode) proposed the packages,
|
|
91
|
+
`code-mistral` as the F2 author in place of Terra, Sonnet for R1 and Grok
|
|
92
|
+
with a fallback for R2, and at most three review rounds. Adopted.
|
|
93
|
+
- P2: `review-glm` wrote the regression test plan; its edge cases (absent
|
|
94
|
+
against empty set fields, real-Git collection, a Podman case) went into the
|
|
95
|
+
task prompts.
|
|
96
|
+
- T3 (`code-deepseek`, writer w2): collected as
|
|
97
|
+
`writer-pi-session-mcp-w2/fix/f3-relay-profile-drift`; the controller ran the
|
|
98
|
+
new tests against the old source (7 fail) and the fix (all pass); integrated
|
|
99
|
+
as `230e60e`, writer finished (`writer_reset`).
|
|
100
|
+
- T1 (`code`, writer w1): collected as
|
|
101
|
+
`writer-pi-session-mcp-w1/fix/f1-context-files`; new tests against the old
|
|
102
|
+
source 9 of 12 fail, with the fix 148 of 148 pass; integrated as `a0a7d87`,
|
|
103
|
+
writer finished. The container-writer suite showed only the eight known
|
|
104
|
+
lock and hook timeouts (`override-lock`, `skill-customize-concurrency`,
|
|
105
|
+
`wake-hook`).
|
|
106
|
+
- R1 (Sonnet 5.5, read-only, through the test configuration of item D):
|
|
107
|
+
`NON_BLOCKING_ONLY`. Fixed by the controller: kept context files are read
|
|
108
|
+
again from their verified real path (`79e8a7a`), the set comparison is
|
|
109
|
+
symmetric with a duplicated-entry test (`3e14279`); the shadowing case is
|
|
110
|
+
covered by a test and documented. Not changed: synchronous `realpathSync` in
|
|
111
|
+
the loader override (the SDK hook is synchronous) and the trust in the
|
|
112
|
+
writer's own capability report, which is the boundary the relay already
|
|
113
|
+
assumes.
|
|
114
|
+
- T2 (`code-mistral`, writer w2): collected as
|
|
115
|
+
`writer-pi-session-mcp-w2/fix/f2-detached-collect`; the new real-Git tests
|
|
116
|
+
against the old source 3 of 8 fail, with the fix 8 of 8 pass, and the F2
|
|
117
|
+
reproduction now gets `writer_collected`; integrated as `236ca28`, writer
|
|
118
|
+
finished. The controller added the Podman case (`6ee7add`): with real
|
|
119
|
+
rootless Podman and the pinned Node image it passes, against the old source
|
|
120
|
+
it fails.
|
|
121
|
+
- D1 (`code-mistral`, writer w1): collected as
|
|
122
|
+
`writer-pi-session-mcp-w1/docs/s12-reconcile`; split by issue into
|
|
123
|
+
`f5a9712`, `58e0e7b`, `17845ff` and `ebe3c52`. Controller edits: the
|
|
124
|
+
shadowing and re-read rule in the ADR 0013 amendment, and the relay wording
|
|
125
|
+
(a mismatch counts as `settingsChanged`; `writer_unavailable` with
|
|
126
|
+
`none_admitted` only when no writer is admitted). Writer finished.
|
|
127
|
+
- R2 (`review-grok`, Grok 4.7, read-only): `NON_BLOCKING_ONLY`. Verified by
|
|
128
|
+
the controller and fixed in `1dfe4da`: a derived `-wip` or `-detached` name
|
|
129
|
+
that conflicts with an exported branch path aborted the export under
|
|
130
|
+
`set -eu` (reproduced with a branch `main-detached/topic`); a worker-made
|
|
131
|
+
`git replace --graft` made an unexported detached commit look reachable
|
|
132
|
+
(Grok's variant with a plain replacement did not reproduce, the graft did);
|
|
133
|
+
the reachability gate now also runs on `nothing_to_collect`. Recorded as
|
|
134
|
+
limits, not changed: an index-only staged change that the worktree reverted
|
|
135
|
+
(pre-existing), a base name of 192 to 200 characters (fails closed), and a
|
|
136
|
+
runner that reports a fetch it did not do (outside the threat model).
|
|
137
|
+
- Final integration review (Sonnet 5.5, read-only, on `896e29d`):
|
|
138
|
+
`NON_BLOCKING_ONLY`. Fixed: the in-boundary guard probe now ignores replace
|
|
139
|
+
refs too (`9426682`, with a test that fails without it), the BOM pattern is an
|
|
140
|
+
escape (`5d9f15b`), and execution modes names the pruned-base case of the
|
|
141
|
+
digest gate. Not changed: `inspectWork.head` stays optional because required
|
|
142
|
+
would break type-checked mocks outside this change.
|
|
143
|
+
- Acceptance: see
|
|
144
|
+
[Sprint 12 candidate acceptance](../acceptance.md#sprint-12-candidate-acceptance-2026-10-30).
|
|
145
|
+
Level 1 green on `9426682`; Level 2 green on the packed `896e29d`; live items
|
|
146
|
+
B, C and D passed on expendable configurations and were rolled back; the
|
|
147
|
+
Level 3 ten-task cycle 2 passed (10 of 10) after cycle 1 failed on fixture and
|
|
148
|
+
harness design (5 of 10, kept unchanged).
|
|
149
|
+
|
|
150
|
+
## Results
|
|
151
|
+
|
|
152
|
+
| Finding | Status | Commits | Evidence |
|
|
153
|
+
| --- | --- | --- | --- |
|
|
154
|
+
| F1 #192 | fixed | `a0a7d87`, `79e8a7a`, `5d9f15b`; docs `f5a9712` | `test/read-only-context-files.test.ts` (fails on `9d9cf8c`), faux-provider context check, R1 and final review |
|
|
155
|
+
| F2 #193 | fixed | `236ca28`, `6ee7add`, `1dfe4da`, `9426682`; docs `58e0e7b`, `896e29d` | real-Git collect tests, Podman test, guard test (each fails on the source before), R2 and final review |
|
|
156
|
+
| F3 #194 | fixed | `230e60e`, `3e14279`; docs `17845ff` | `test/writer-relay.test.ts` drift tests (fail on `9d9cf8c`), live refusal with `settingsChanged`, R1 and final review |
|
|
157
|
+
|
|
158
|
+
Remaining risk: #120 is unchanged. `npm audit --omit=dev` on 2026-10-29 still
|
|
159
|
+
reports `undici` 8.10.2 (high) through `@earendil-works/pi-coding-agent` 1.4.0
|
|
160
|
+
(moderate), eight advisories fixed in `undici` >= 8.12.2; the only offered fix
|
|
161
|
+
is a semver-major downgrade. No override and no `npm audit fix --force`.
|
|
162
|
+
|
|
163
|
+
Kept open: #79 (Sprint 8 steps 5 to 7 are not done), #120 and #144 (no macOS
|
|
164
|
+
claim). The sandbox resource limits of ADR 0013 are the follow-up issue
|
|
165
|
+
[#195](https://github.com/plagor44-ux/pi-session-mcp/issues/195), not part of
|
|
166
|
+
this sprint.
|
|
167
|
+
|
|
168
|
+
Cleanup: all controller Pi sessions closed; both `pi-session-mcp` writers
|
|
169
|
+
finished (`writer_reset`); the test writer `s12-probe-w1` removed; expendable
|
|
170
|
+
configurations, the synthetic repository, the pack prefix and the
|
|
171
|
+
controller's worktrees removed.
|
|
172
|
+
|
package/docs/threat-model.md
CHANGED
|
@@ -16,7 +16,16 @@ Pi Session MCP trusts the local process that launches and speaks MCP over inheri
|
|
|
16
16
|
(arguments the SDK would rewrite, such as a leading `@` or `~`, a `file:`
|
|
17
17
|
URL, Unicode spaces or NUL, are refused so both resolve the same path), and
|
|
18
18
|
the `*Operations` hooks of `read`, `grep` and `ls` check the absolute path
|
|
19
|
-
again. `rg` and `fd` do not follow symlinks during traversal.
|
|
19
|
+
again. `rg` and `fd` do not follow symlinks during traversal. For read-only
|
|
20
|
+
profiles the resource loader likewise keeps only Pi context files
|
|
21
|
+
(`AGENTS.override.md`, `AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`, `CLAUDE.MD`)
|
|
22
|
+
whose real path lies inside the workspace's real path, on every reload and
|
|
23
|
+
before the first provider request: a symlinked context file that resolves
|
|
24
|
+
outside the workspace, a dangling one and the context files of ancestor
|
|
25
|
+
directories above the workspace (including a main repository above a linked
|
|
26
|
+
worktree) are not loaded, while the linked worktree's own file inside the
|
|
27
|
+
workspace is. Coding profiles keep the SDK behaviour (the working directory
|
|
28
|
+
and its ancestors). A profile with
|
|
20
29
|
`shell: "sandboxed"` also gets `bash`, run as `/usr/bin/bwrap` (fixed path,
|
|
21
30
|
root-owned and not group- or world-writable, no `PATH` lookup) with
|
|
22
31
|
`--unshare-all`, `--disable-userns`, a cleared environment, no network, `/usr`
|
package/docs/tool-contracts.md
CHANGED
|
@@ -40,7 +40,7 @@ records name the package version and the build they ran on, see
|
|
|
40
40
|
```json
|
|
41
41
|
{
|
|
42
42
|
"ok": true,
|
|
43
|
-
"server": { "name": "pi-session-mcp", "version": "0.14.
|
|
43
|
+
"server": { "name": "pi-session-mcp", "version": "0.14.1" },
|
|
44
44
|
"configuration": {
|
|
45
45
|
"fingerprint": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
|
46
46
|
"reloadPolicy": "restart-required"
|
package/package.json
CHANGED
|
@@ -122,7 +122,7 @@ You integrate. Workers never integrate each other's changes. Resolve conflicts b
|
|
|
122
122
|
|
|
123
123
|
After taking over the result and closing its coding session, before the next task:
|
|
124
124
|
|
|
125
|
-
1. Run `pi-session-mcp-setup --writer <name> --collect --apply` from the operator environment. Review the collected refs in the operator's checkout: `refs/remotes/writer-<name>/<branch>`, with uncommitted work as `<branch>-wip`. They are untrusted until reviewed. Integrate only verified work yourself.
|
|
125
|
+
1. Run `pi-session-mcp-setup --writer <name> --collect --apply` from the operator environment. Review the collected refs in the operator's checkout: `refs/remotes/writer-<name>/<branch>`, with uncommitted work as `<branch>-wip` and a clean detached HEAD as `<base>-detached`. They are untrusted until reviewed. Integrate only verified work yourself.
|
|
126
126
|
2. Run `pi-session-mcp-setup --writer <name> --finish --apply`. It collects again, then resets a persistent writer or recreates an ephemeral writer. Do not reuse it until this succeeds. A refused branch name makes collection incomplete and finish stops: ask the operator to resolve it by renaming inside the boundary or explicitly discarding with `--reset --discard --apply`. Finish returns it to the workspace default base. No reconnect is needed after reset, finish or recreate. A setup operation on a writer held by a session fails with `writer_busy` after a short wait; close the session and allow launcher cleanup to finish.
|
|
127
127
|
3. A coding start needs the writer's clone at its baseline, even after a complete collection; otherwise it fails with the controller-action code for uncollected work in [references/start-errors.md](references/start-errors.md). To let a fresh session continue from collected work, for example after a failed turn or for a review fix round, create a local branch at the collected ref and run `pi-session-mcp-setup --writer <name> --reset --base <branch> --apply`, then call `pi_session_start` on `pi-session-mcp` with the coding profile and `base: "<branch>"`. Read `writer` from that result for collection. Default starts cannot use a writer moved to the fix branch.
|
|
128
128
|
4. Never run Git inside the writer's clone as the host user, including direct fetch from that clone. Use setup's bundle collection and inspect the imported refs in your own checkout. Never discard unreviewed work just to admit the next task.
|