pi-session-mcp 0.12.1 → 0.13.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/client-adapters/mcp-verifier.js +2 -1
- package/.pi-session-mcp-cli/config.js +36 -3
- package/.pi-session-mcp-cli/doctor.js +52 -4
- package/.pi-session-mcp-cli/execution-modes.js +10 -2
- package/.pi-session-mcp-cli/execution-profile.js +1 -0
- package/.pi-session-mcp-cli/sandbox-shell.js +254 -0
- package/.pi-session-mcp-cli/session-tools.js +6 -2
- package/.pi-session-mcp-cli/writer-boundary.js +27 -0
- package/.pi-session-mcp-cli/writer-lifecycle.js +6 -4
- package/.pi-session-mcp-cli/writer-limits.js +19 -0
- package/.pi-session-mcp-cli/writer-state.js +5 -2
- package/CHANGELOG.md +147 -0
- package/CONTRIBUTING.md +8 -1
- package/README.md +5 -2
- package/dist/capabilities.d.ts +1 -0
- package/dist/capabilities.js +3 -1
- package/dist/client-adapters/mcp-verifier.js +2 -1
- package/dist/config.d.ts +12 -1
- package/dist/config.js +36 -3
- package/dist/confined-tools.d.ts +30 -0
- package/dist/confined-tools.js +122 -0
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +52 -4
- package/dist/execution-modes.d.ts +13 -1
- package/dist/execution-modes.js +10 -2
- package/dist/execution-profile.d.ts +4 -0
- package/dist/execution-profile.js +1 -0
- package/dist/main.js +10 -2
- package/dist/pi-adapter.d.ts +1 -1
- package/dist/pi-adapter.js +1 -0
- package/dist/sandbox-shell.d.ts +71 -0
- package/dist/sandbox-shell.js +254 -0
- package/dist/sdk-pi-adapter.d.ts +17 -1
- package/dist/sdk-pi-adapter.js +65 -10
- package/dist/session-tools.d.ts +4 -2
- package/dist/session-tools.js +6 -2
- package/dist/setup-cli.d.ts +1 -2
- package/dist/setup-cli.js +40 -9
- package/dist/setup-modes.d.ts +3 -0
- package/dist/setup-modes.js +18 -1
- package/dist/setup-writers-common.d.ts +78 -0
- package/dist/setup-writers-common.js +177 -0
- package/dist/setup-writers-plan.d.ts +34 -0
- package/dist/setup-writers-plan.js +105 -0
- package/dist/setup-writers-reconcile.d.ts +8 -0
- package/dist/setup-writers-reconcile.js +169 -0
- package/dist/setup-writers.d.ts +6 -30
- package/dist/setup-writers.js +19 -353
- package/dist/tool-schemas.d.ts +4 -0
- package/dist/tool-schemas.js +1 -0
- package/dist/wake-hook.d.ts +11 -0
- package/dist/wake-hook.js +51 -8
- package/dist/writer-boundary.d.ts +2 -0
- package/dist/writer-boundary.js +27 -0
- package/dist/writer-lifecycle.d.ts +1 -0
- package/dist/writer-lifecycle.js +6 -4
- package/dist/writer-limits.d.ts +13 -0
- package/dist/writer-limits.js +19 -0
- package/dist/writer-relay.js +1 -1
- package/dist/writer-state.d.ts +11 -0
- package/dist/writer-state.js +5 -2
- package/docs/acceptance.md +271 -0
- package/docs/adr/0009-execution-modes.md +3 -2
- package/docs/adr/0012-leased-writer-pool.md +3 -3
- package/docs/adr/0013-hardened-read-only-profiles.md +184 -0
- package/docs/architecture.md +1 -1
- package/docs/client-setup.md +3 -3
- package/docs/doctor-design.md +3 -1
- package/docs/execution-modes.md +60 -4
- package/docs/installation.md +25 -1
- package/docs/isolation-setup.md +31 -2
- package/docs/roadmap.md +14 -0
- package/docs/session-mcp.md +15 -0
- package/docs/threat-model.md +51 -0
- package/docs/tool-contracts.md +33 -4
- package/package.json +1 -1
- package/skills/pi-session-orchestration/SKILL.md +2 -2
- package/skills/pi-session-orchestration/references/start-errors.md +1 -1
package/dist/writer-lifecycle.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { chmod, mkdir, open, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
|
+
import { writerLimits } from "./writer-limits.js";
|
|
5
6
|
import { configSchema } from "./config.js";
|
|
6
7
|
import { renderWrapper } from "./writer-wrapper.js";
|
|
7
8
|
import { builtinSkillName } from "./skill-catalog.js";
|
|
@@ -220,7 +221,7 @@ async function storedConfig(paths) {
|
|
|
220
221
|
async function sameSettings(paths, existing, settings, now) {
|
|
221
222
|
const stored = await storedConfig(paths);
|
|
222
223
|
return stored !== undefined && existing.imageId === now.imageId && existing.lifecycle === settings.lifecycle
|
|
223
|
-
&& existing.network === settings.network && existing.cache === settings.cache && existing.tags === settings.tags
|
|
224
|
+
&& existing.network === settings.network && existing.cache === settings.cache && existing.tags === settings.tags && existing.clean === settings.clean
|
|
224
225
|
&& JSON.stringify(existing.profiles) === JSON.stringify(settings.profiles)
|
|
225
226
|
&& JSON.stringify(existing.providers) === JSON.stringify(now.providers) && canonical(withoutDeliveryOff(stored)) === canonical(withoutDeliveryOff(now.config));
|
|
226
227
|
}
|
|
@@ -295,12 +296,13 @@ export async function provisionWriter(ctx, input) {
|
|
|
295
296
|
// With spool delivery the spool directory exists from provisioning on: launch would otherwise add it to the
|
|
296
297
|
// writer root, a host directory the acceptance harness digests around its own first launch.
|
|
297
298
|
const spool = config.events?.delivery === "spool" ? [paths.events] : [];
|
|
298
|
-
const cache = settings.cache === "
|
|
299
|
+
const cache = settings.cache === "none" ? [] : [paths[settings.cache]];
|
|
299
300
|
for (const path of [paths.root, paths.clone, paths.config, paths.agent, paths.bundles, ...spool, ...cache])
|
|
300
301
|
await privateDirectory(path);
|
|
301
302
|
const state = {
|
|
302
303
|
schemaVersion: 1, alias: ctx.alias, index, phase: "provisioning", lifecycle: settings.lifecycle,
|
|
303
|
-
profiles: [...settings.profiles], providers, network: settings.network, cache: settings.cache, tags: settings.tags, imageId,
|
|
304
|
+
profiles: [...settings.profiles], providers, network: settings.network, cache: settings.cache, tags: settings.tags, clean: settings.clean, imageId,
|
|
305
|
+
...(writerLimits(settings) ? { limits: writerLimits(settings) } : {}),
|
|
304
306
|
base: { ref: `refs/heads/${short}`, commit },
|
|
305
307
|
};
|
|
306
308
|
await writeWriterState(ctx.home, name, state);
|
|
@@ -511,7 +513,7 @@ ${BOUNDARY_GIT} for-each-ref --format='%(refname)' refs/heads | while read -r re
|
|
|
511
513
|
if [ "$ref" != "refs/heads/$2" ]; then ${BOUNDARY_GIT} update-ref -d "$ref"; fi
|
|
512
514
|
done
|
|
513
515
|
${BOUNDARY_GIT} stash clear
|
|
514
|
-
${BOUNDARY_GIT} clean -fd
|
|
516
|
+
${BOUNDARY_GIT} clean ${state.clean === "all" ? "-ffdx" : "-fd"}
|
|
515
517
|
${identityScript(4, 5)}`;
|
|
516
518
|
const result = await ctx.runner.run("podman", throwawayArgs(name, paths, state.imageId, { clone: "rw", bundles: true }, script, [base.ref, base.short, base.commit, ...await operatorIdentity(ctx)]), AbortSignal.timeout(IMAGE_PULL_TIMEOUT_MS));
|
|
517
519
|
if (result.exitCode !== 0)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const cpusSchema: z.ZodNumber;
|
|
3
|
+
export declare const cpuSharesSchema: z.ZodNumber;
|
|
4
|
+
export declare const memorySchema: z.ZodString;
|
|
5
|
+
export declare const writerLimitsSchema: z.ZodObject<{
|
|
6
|
+
cpus: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
cpuShares: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
memory: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strict>;
|
|
10
|
+
export type WriterLimits = z.infer<typeof writerLimitsSchema>;
|
|
11
|
+
/** Omitted settings remove previous limits; never persist an empty limits object. */
|
|
12
|
+
export declare function writerLimits(settings: WriterLimits): WriterLimits | undefined;
|
|
13
|
+
export declare function sameWriterLimits(left: WriterLimits | undefined, right: WriterLimits | undefined): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const cpusSchema = z.number().positive().max(1024).multipleOf(0.01);
|
|
3
|
+
export const cpuSharesSchema = z.number().int().min(2).max(262144);
|
|
4
|
+
export const memorySchema = z.string().regex(/^[1-9][0-9]{0,6}[mg]$/);
|
|
5
|
+
export const writerLimitsSchema = z.object({
|
|
6
|
+
cpus: cpusSchema.optional(),
|
|
7
|
+
cpuShares: cpuSharesSchema.optional(),
|
|
8
|
+
memory: memorySchema.optional(),
|
|
9
|
+
}).strict();
|
|
10
|
+
/** Omitted settings remove previous limits; never persist an empty limits object. */
|
|
11
|
+
export function writerLimits(settings) {
|
|
12
|
+
const { cpus, cpuShares, memory } = settings;
|
|
13
|
+
if (cpus === undefined && cpuShares === undefined && memory === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
return { ...(cpus === undefined ? {} : { cpus }), ...(cpuShares === undefined ? {} : { cpuShares }), ...(memory === undefined ? {} : { memory }) };
|
|
16
|
+
}
|
|
17
|
+
export function sameWriterLimits(left, right) {
|
|
18
|
+
return left?.cpus === right?.cpus && left?.cpuShares === right?.cpuShares && left?.memory === right?.memory;
|
|
19
|
+
}
|
package/dist/writer-relay.js
CHANGED
|
@@ -69,7 +69,7 @@ const creationCodes = new Set([
|
|
|
69
69
|
"model_unavailable", "thinking_level_unavailable", "execution_selection_mismatch", "external_mcp_unavailable",
|
|
70
70
|
"external_mcp_no_tools", "external_mcp_grant_tool_missing", "external_mcp_activation_mismatch",
|
|
71
71
|
"extension_not_installed", "extension_version_mismatch", "extension_load_failed", "extension_tool_conflict",
|
|
72
|
-
"extension_tool_missing", "skill_load_failed", "pi_session_creation_failed",
|
|
72
|
+
"extension_tool_missing", "skill_load_failed", "sandbox_unavailable", "pi_session_creation_failed",
|
|
73
73
|
]);
|
|
74
74
|
class InnerError extends Error {
|
|
75
75
|
code;
|
package/dist/writer-state.d.ts
CHANGED
|
@@ -33,12 +33,22 @@ export declare const writerStateSchema: z.ZodObject<{
|
|
|
33
33
|
cache: z.ZodEnum<{
|
|
34
34
|
go: "go";
|
|
35
35
|
none: "none";
|
|
36
|
+
npm: "npm";
|
|
36
37
|
uv: "uv";
|
|
37
38
|
}>;
|
|
38
39
|
tags: z.ZodDefault<z.ZodEnum<{
|
|
39
40
|
none: "none";
|
|
40
41
|
reachable: "reachable";
|
|
41
42
|
}>>;
|
|
43
|
+
clean: z.ZodDefault<z.ZodEnum<{
|
|
44
|
+
all: "all";
|
|
45
|
+
untracked: "untracked";
|
|
46
|
+
}>>;
|
|
47
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
48
|
+
cpus: z.ZodOptional<z.ZodNumber>;
|
|
49
|
+
cpuShares: z.ZodOptional<z.ZodNumber>;
|
|
50
|
+
memory: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, z.core.$strict>>;
|
|
42
52
|
imageId: z.ZodString;
|
|
43
53
|
base: z.ZodObject<{
|
|
44
54
|
ref: z.ZodString;
|
|
@@ -73,6 +83,7 @@ export declare function writerPaths(home: string, name: string): {
|
|
|
73
83
|
events: string;
|
|
74
84
|
go: string;
|
|
75
85
|
uv: string;
|
|
86
|
+
npm: string;
|
|
76
87
|
wrapper: string;
|
|
77
88
|
state: string;
|
|
78
89
|
acceptanceFailure: string;
|
package/dist/writer-state.js
CHANGED
|
@@ -5,6 +5,7 @@ import { lstat, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { PACKAGE_VERSION } from "./package-metadata.js";
|
|
8
|
+
import { writerLimitsSchema } from "./writer-limits.js";
|
|
8
9
|
import { ALIAS_PATTERN } from "./execution-profile.js";
|
|
9
10
|
export const WRITER_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,63}-w([1-9]|1[0-6])$/;
|
|
10
11
|
export class WriterStateError extends Error {
|
|
@@ -31,8 +32,10 @@ export const writerStateSchema = z.object({
|
|
|
31
32
|
model: z.string().min(1).max(200),
|
|
32
33
|
}).strict()).refine((providers) => new Set(providers.map(({ provider }) => provider)).size === providers.length).readonly(),
|
|
33
34
|
network: z.enum(["pasta", "none"]),
|
|
34
|
-
cache: z.enum(["none", "go", "uv"]),
|
|
35
|
+
cache: z.enum(["none", "go", "uv", "npm"]),
|
|
35
36
|
tags: z.enum(["none", "reachable"]).default("none"),
|
|
37
|
+
clean: z.enum(["untracked", "all"]).default("untracked"),
|
|
38
|
+
limits: writerLimitsSchema.optional(),
|
|
36
39
|
imageId: hex64,
|
|
37
40
|
base: z.object({ ref: z.string(), commit: z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/) }).strict(),
|
|
38
41
|
acceptance: z.object({
|
|
@@ -62,7 +65,7 @@ export function writerPaths(home, name) {
|
|
|
62
65
|
const root = join(writersRoot(home), name);
|
|
63
66
|
return {
|
|
64
67
|
root, clone: join(root, "clone"), agent: join(root, "agent"), bundles: join(root, "bundles"),
|
|
65
|
-
config: join(root, "config"), events: join(root, "events"), go: join(root, "go"), uv: join(root, "uv"), wrapper: join(root, "wrapper.sh"), state: join(root, "state.json"),
|
|
68
|
+
config: join(root, "config"), events: join(root, "events"), go: join(root, "go"), uv: join(root, "uv"), npm: join(root, "npm"), wrapper: join(root, "wrapper.sh"), state: join(root, "state.json"),
|
|
66
69
|
acceptanceFailure: join(root, "acceptance-failure.json"),
|
|
67
70
|
};
|
|
68
71
|
}
|
package/docs/acceptance.md
CHANGED
|
@@ -2457,6 +2457,277 @@ Not established for `0.11.6`:
|
|
|
2457
2457
|
wrapper's periodic credential copy (#136);
|
|
2458
2458
|
- the VS Code wake; macOS delivery.
|
|
2459
2459
|
|
|
2460
|
+
### v0.13.0 post-release evidence
|
|
2461
|
+
|
|
2462
|
+
Recorded on 2026-10-28 for the published release `0.13.0`.
|
|
2463
|
+
|
|
2464
|
+
Tag binding: a fresh clone of the annotated tag `v0.13.0` (tag object
|
|
2465
|
+
`8c2d513ccac17916fdf7cf3fc36b3ccd454138ed`) peels to the merge commit
|
|
2466
|
+
`d1c63dac3b996952976aab4b5ad11665e7108fa3` of #171, and
|
|
2467
|
+
`git describe --tags --exact-match` reports `v0.13.0`. CI on the merge commit
|
|
2468
|
+
(run 40757781881) and on the tag (run 40759098803) passed on the first attempt.
|
|
2469
|
+
|
|
2470
|
+
- Level 1 on the tag clone after `npm ci`, from 21:01 to 21:03 UTC with
|
|
2471
|
+
Node.js `26.12.1` and npm `12.2.1`: typecheck, `build:cli`, the test run
|
|
2472
|
+
(125 files, 3936 passed, 14 skipped, with
|
|
2473
|
+
`NODE_OPTIONS=--unhandled-rejections=strict`), `build` and
|
|
2474
|
+
`git diff --check` exited with `0`, and the status stayed clean. Doctor
|
|
2475
|
+
reported `packageVersion` `0.13.0` and exited with `2` because no
|
|
2476
|
+
configuration was set (`config_missing`).
|
|
2477
|
+
- `pi-session-mcp@0.13.0` on npm: 916 files, shasum
|
|
2478
|
+
`866d0ecfa3b4e1a71462d291b1776897229f7827`, `gitHead` `4521d39` (the head of
|
|
2479
|
+
#171, whose tree equals the merge commit). `npm pack` of the tag clone gives
|
|
2480
|
+
the same shasum and file count.
|
|
2481
|
+
- Published npm package, Level 2 from a fresh prefix, 21:03 to 21:05 UTC:
|
|
2482
|
+
160 of 160 with delivery off and 228 of 228 with `--events spool`; real
|
|
2483
|
+
registrations and built files unchanged, no process left, leak check passed.
|
|
2484
|
+
Clients: Codex CLI `0.162.1` and Claude Code `2.1.312`. The install smoke on
|
|
2485
|
+
the published package reported `smoke_passed` with version `0.13.0`.
|
|
2486
|
+
- The GitHub release `v0.13.0` was published the same day.
|
|
2487
|
+
|
|
2488
|
+
Operator upgrade of the second machine (Linux) from `0.12.1` to `0.13.0` on
|
|
2489
|
+
2026-10-28 from 20:34 UTC, following
|
|
2490
|
+
[Installation](installation.md#container-writers), with Node.js `24.22.0`:
|
|
2491
|
+
|
|
2492
|
+
- Both client targets were removed with `0.12.1`, then
|
|
2493
|
+
`npm install -g pi-session-mcp@0.13.0`.
|
|
2494
|
+
- Four container repositories with 14 writers were migrated by repeating their
|
|
2495
|
+
provisioning commands with unchanged settings and `--apply`: 2, 4, 4
|
|
2496
|
+
(operator image, `--cache go`) and 4 (operator image) writers. Every writer
|
|
2497
|
+
reported `writer_accepted`.
|
|
2498
|
+
- `--apply` and `--verify` of both client targets passed (`mcp_verified`), the
|
|
2499
|
+
open point of the pre-release record, and `--writer <name> --verify` passed
|
|
2500
|
+
for all 14 writers. Doctor reported no error.
|
|
2501
|
+
|
|
2502
|
+
Operator upgrade of the workstation from `0.12.1` to `0.13.0` on 2026-10-28
|
|
2503
|
+
from 20:47 UTC, with the same steps: no writer container ran at the start;
|
|
2504
|
+
five container repositories with 18 writers (2, 4, 4 with `--cache go`, 4 with
|
|
2505
|
+
base `dev`, 4 with `--cache uv`, three of them with operator images) reported
|
|
2506
|
+
`writer_accepted`; client-target `--apply` and `--verify` passed for
|
|
2507
|
+
`claude-code:user` and `codex:user` (`mcp_verified`, `skill_verified`, and
|
|
2508
|
+
`hook_verified` for the wake hook); `--writer <name> --verify` passed for all
|
|
2509
|
+
18 writers. Doctor reported no error.
|
|
2510
|
+
|
|
2511
|
+
Live check of `shell: "sandboxed"` on both machines, the open point of the
|
|
2512
|
+
pre-release record. Two read-only profiles were added to each configuration,
|
|
2513
|
+
copies of the existing review profiles with `"shell": "sandboxed"`, one per
|
|
2514
|
+
provider family. Doctor reported `sandbox_unavailable` and
|
|
2515
|
+
`workspace_git_unavailable` as `ok`. A stdio driver against the installed
|
|
2516
|
+
server started one session per profile on the pi-session-mcp workspace (a plain
|
|
2517
|
+
checkout, not a linked worktree) and sent one prompt with five steps. All four
|
|
2518
|
+
sessions completed with the same results:
|
|
2519
|
+
|
|
2520
|
+
- `git log -1 --format=%h` through `bash` printed the checkout's head commit.
|
|
2521
|
+
- `touch sandbox-probe.txt && ls sandbox-probe.txt` succeeded in the sandbox;
|
|
2522
|
+
afterwards the file did not exist on the host and `git status` of the host
|
|
2523
|
+
checkout was clean.
|
|
2524
|
+
- `getent hosts example.com || echo NO_NETWORK` printed `NO_NETWORK`.
|
|
2525
|
+
- `echo HOME=$HOME` printed `HOME=/tmp/home`.
|
|
2526
|
+
- `read` of `/etc/hostname` returned `Path is outside the allowed read roots`.
|
|
2527
|
+
|
|
2528
|
+
Finding from these checks: the first workstation edit left out the required
|
|
2529
|
+
profile field `default`. Doctor then reported only `config_invalid` with the
|
|
2530
|
+
generic remediation and did not name the profile or the field; parsing the file
|
|
2531
|
+
with the package's configuration schema named both. With `"default": false`
|
|
2532
|
+
added, Doctor reported no error.
|
|
2533
|
+
|
|
2534
|
+
Not established for `0.13.0`: the sandbox on a host that restricts unprivileged
|
|
2535
|
+
user namespaces; Git inside the sandbox on a linked worktree in live use (the
|
|
2536
|
+
bubblewrap-gated tests cover it); the macOS upgrade; Level 3.
|
|
2537
|
+
|
|
2538
|
+
### v0.13.1 pre-release acceptance
|
|
2539
|
+
|
|
2540
|
+
`v0.13.1` was accepted on 2026-10-29 from 06:40 to 06:48 UTC on its release
|
|
2541
|
+
candidate `89a366e`, on Linux (`x64`) with Node.js `26.12.1`, npm `12.2.1`,
|
|
2542
|
+
Codex CLI `0.162.1` and Claude Code `2.1.312`. Level 2 passed in the working
|
|
2543
|
+
tree and for the packed tarball, each with delivery off and with delivery on.
|
|
2544
|
+
Level 1 passed except `npm audit`. After the candidate, one lockfile-only
|
|
2545
|
+
commit moved the development-path `@hono/node-server` 2.1.1 → 2.1.5; Level 1
|
|
2546
|
+
was run again on it after `npm ci` (same counts). The packed package and the
|
|
2547
|
+
working-tree build that Level 2 used do not contain that package. Later commits
|
|
2548
|
+
on the release pull request change only this record.
|
|
2549
|
+
|
|
2550
|
+
The patch release ships #173, #176, #177, #179 and #180 under an unchanged MCP
|
|
2551
|
+
contract. The setup report schema is unchanged; a broken kept writer now
|
|
2552
|
+
reports its own code on its target instead of `writer_reconcile_failed`.
|
|
2553
|
+
|
|
2554
|
+
- Level 1 in a clean worktree of the candidate after `npm ci`: every command
|
|
2555
|
+
exited with `0`, except `npm audit`; `git status --short` printed nothing.
|
|
2556
|
+
- The test run, with `NODE_OPTIONS=--unhandled-rejections=strict`, reported
|
|
2557
|
+
136 files, 3948 passed and 14 skipped, and no unhandled error.
|
|
2558
|
+
- `npm audit --omit=dev` exited with `1` and names only the known `undici`
|
|
2559
|
+
finding through `@earendil-works/pi-coding-agent` 1.4.0, now with eight
|
|
2560
|
+
advisories ([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
|
|
2561
|
+
`npm audit` also named `@hono/node-server` 2.1.1 in the development path
|
|
2562
|
+
(GHSA-rmxm-3fg6-px4f), patched in the lockfile afterwards as above.
|
|
2563
|
+
- `npm pack` listed 922 files (tarball shasum
|
|
2564
|
+
`93f9c5bc9e268af7e0be5e395d97558ec801668a`, identical in two packs). A scan
|
|
2565
|
+
of the packed files for private names, hosts and paths found none; its only
|
|
2566
|
+
matches for `/home/` are the already redacted paths in this file and in the
|
|
2567
|
+
six `docs/evaluation/extension-functions-2026-10-17/cursor/` evidence files
|
|
2568
|
+
that earlier releases shipped unchanged.
|
|
2569
|
+
- Doctor reported `packageVersion` `0.13.1`. It exited with `2` because no
|
|
2570
|
+
configuration was set (`config_missing`).
|
|
2571
|
+
- Level 2 in the working tree: 160 of 160 checks passed with delivery off and
|
|
2572
|
+
228 of 228 with `--events spool`.
|
|
2573
|
+
- Packed-tarball Level 2 from a fresh prefix: 160 of 160 with delivery off and
|
|
2574
|
+
228 of 228 with `--events spool`. The install smoke from another fresh prefix
|
|
2575
|
+
reported `smoke_passed` with version `0.13.1`.
|
|
2576
|
+
- In every Level 2 run the real client registrations were unchanged, no process
|
|
2577
|
+
remained, the leak check passed and the disposable directories were removed.
|
|
2578
|
+
- Podman-gated writer tests on the candidate (Podman `6.1.4`): 7 of 7 passed.
|
|
2579
|
+
- Bubblewrap-gated sandbox tests (`test/sandbox-shell.bwrap.test.ts`, bubblewrap
|
|
2580
|
+
`0.13.1`): 12 of 12 passed, none skipped.
|
|
2581
|
+
- Review: #179, #180 and #181 had an independent review (same model family as
|
|
2582
|
+
the author) with no blocking findings; its follow-ups were fixed before the
|
|
2583
|
+
merge of #184. The final test-file split commit was checked by comparing the
|
|
2584
|
+
sorted test names before and after (464, identical).
|
|
2585
|
+
|
|
2586
|
+
Not established for `0.13.1`: a live wake-hook race, a live reconcile with a
|
|
2587
|
+
broken writer, client-target `--verify` through the installed package, which
|
|
2588
|
+
follows the upgrade; Level 3.
|
|
2589
|
+
|
|
2590
|
+
### v0.13.0 pre-release acceptance
|
|
2591
|
+
|
|
2592
|
+
`v0.13.0` was accepted on 2026-10-28 from 19:30 to 19:36 UTC on its release
|
|
2593
|
+
candidate `f7442b3`, on Linux (`x64`) with Node.js `26.12.1`, npm `12.2.1`,
|
|
2594
|
+
Codex CLI `0.162.1` and Claude Code `2.1.312`. Level 2 passed in the working
|
|
2595
|
+
tree and for the packed tarball, each with delivery off and with delivery on.
|
|
2596
|
+
Level 1 passed except `npm audit`. Later commits on the release pull request
|
|
2597
|
+
change only this record.
|
|
2598
|
+
|
|
2599
|
+
The minor release ships ADR 0013 (#169) and the writer settings changes #162,
|
|
2600
|
+
#163, #165 and #166. It changes the MCP contract (read-only confinement,
|
|
2601
|
+
profile field and capabilities field `shell`, start error
|
|
2602
|
+
`sandbox_unavailable`); the setup report schema is unchanged.
|
|
2603
|
+
|
|
2604
|
+
- Level 1 in a clean worktree of the candidate after `npm ci`: every command
|
|
2605
|
+
exited with `0`, except `npm audit`; `git status --short` printed nothing.
|
|
2606
|
+
- The test run, with `NODE_OPTIONS=--unhandled-rejections=strict`, reported
|
|
2607
|
+
125 files, 3936 passed and 14 skipped, and no unhandled error.
|
|
2608
|
+
- `npm audit` and `npm audit --omit=dev` exited with `1` and name only the
|
|
2609
|
+
known `undici` finding through `@earendil-works/pi-coding-agent` 1.4.0
|
|
2610
|
+
([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
|
|
2611
|
+
- `npm pack` listed 916 files (tarball shasum
|
|
2612
|
+
`1259d57d2d59099ad72e796609ecab3d581e9b06`, identical in two packs). A scan
|
|
2613
|
+
of the packed files for private names, hosts and paths found none; its only
|
|
2614
|
+
matches for `/home/` are the already redacted
|
|
2615
|
+
`<repo>/eval/results/.../home/.pi/...` paths of disposable evaluation homes
|
|
2616
|
+
in six `docs/evaluation/extension-functions-2026-10-17/cursor/` evidence
|
|
2617
|
+
files, which `0.12.1` shipped unchanged.
|
|
2618
|
+
- Doctor reported `packageVersion` `0.13.0`. It exited with `2` because no
|
|
2619
|
+
configuration was set (`config_missing`).
|
|
2620
|
+
- Level 2 in the working tree: 160 of 160 checks passed with delivery off and
|
|
2621
|
+
228 of 228 with `--events spool`.
|
|
2622
|
+
- Packed-tarball Level 2 from a fresh prefix: 160 of 160 with delivery off and
|
|
2623
|
+
228 of 228 with `--events spool`. The install smoke from another fresh prefix
|
|
2624
|
+
reported `smoke_passed` with version `0.13.0`.
|
|
2625
|
+
- In every Level 2 run the real client registrations were unchanged, no process
|
|
2626
|
+
remained, the leak check passed and the disposable directories were removed.
|
|
2627
|
+
- Podman-gated writer tests on the candidate: 7 of 7 passed.
|
|
2628
|
+
- Bubblewrap-gated sandbox tests (`test/sandbox-shell.bwrap.test.ts`, bubblewrap
|
|
2629
|
+
`0.13.1`): 12 of 12 passed, none skipped.
|
|
2630
|
+
- Review: #169 had an independent review of its full diff (same model family as
|
|
2631
|
+
the author) that found no confinement or sandbox escape; its follow-ups were
|
|
2632
|
+
fixed before the merge of #170.
|
|
2633
|
+
|
|
2634
|
+
Not established for `0.13.0`: a live session with `shell: "sandboxed"` against
|
|
2635
|
+
a real provider, client-target `--verify` through the installed package, which
|
|
2636
|
+
follows the upgrade; Level 3.
|
|
2637
|
+
|
|
2638
|
+
### v0.12.1 post-release evidence
|
|
2639
|
+
|
|
2640
|
+
Recorded on 2026-10-28 for the published release `0.12.1`.
|
|
2641
|
+
|
|
2642
|
+
Tag binding: a fresh clone of the annotated tag `v0.12.1` (tag object
|
|
2643
|
+
`eecd6dbf0c220ded997c697f281f6b984d9ebf4e`) peels to the merge commit
|
|
2644
|
+
`91799f2e84b514110f77c24986d59e72c5ed98e5` of #161, and
|
|
2645
|
+
`git describe --tags --exact-match` reports `v0.12.1`. CI on the merge commit
|
|
2646
|
+
and on the tag passed on the first attempt.
|
|
2647
|
+
|
|
2648
|
+
- Level 1 on the tag clone after `npm ci`, from 14:43 to 14:47 UTC with
|
|
2649
|
+
Node.js `26.12.1` and npm `12.2.1`: typecheck, `build:cli`, the test run
|
|
2650
|
+
(120 files, 3708 passed, 14 skipped, with
|
|
2651
|
+
`NODE_OPTIONS=--unhandled-rejections=strict`), `build` and
|
|
2652
|
+
`git diff --check` exited with `0`, and the status stayed clean.
|
|
2653
|
+
- `pi-session-mcp@0.12.1` on npm: 907 files, shasum
|
|
2654
|
+
`58dc7235e0d54d4290d92934d877223e22eec03d`, `gitHead` `cc25693` (the head of
|
|
2655
|
+
#161, whose tree equals the merge commit). `npm pack` of the tag clone gives
|
|
2656
|
+
the same shasum and file count.
|
|
2657
|
+
- Published npm package, Level 2 from a fresh prefix: 160 of 160 with delivery
|
|
2658
|
+
off and 228 of 228 with `--events spool`; real registrations and built files
|
|
2659
|
+
unchanged, no process left, leak check passed. Clients: Codex CLI `0.162.1`
|
|
2660
|
+
and Claude Code `2.1.312`. The install smoke on the published package
|
|
2661
|
+
reported `smoke_passed` with version `0.12.1`.
|
|
2662
|
+
- The GitHub release `v0.12.1` was published the same day.
|
|
2663
|
+
|
|
2664
|
+
Workstation, upgraded from `0.12.0` to `0.12.1` by the release session: Doctor
|
|
2665
|
+
reported no error, client-target `--verify` passed for `claude-code:user` and
|
|
2666
|
+
`codex:user` (`mcp_verified`, `skill_verified`, and `hook_verified` for the
|
|
2667
|
+
wake hook), the open point of the pre-release record, and
|
|
2668
|
+
`--writer <name> --verify` passed for all 18 writers (`mcp_verified`).
|
|
2669
|
+
|
|
2670
|
+
Operator upgrade of the second machine (Linux) from `0.11.8` to `0.12.1` on
|
|
2671
|
+
2026-10-28 from 14:50 UTC, following
|
|
2672
|
+
[Installation](installation.md#container-writers), with Node.js `24.22.0`:
|
|
2673
|
+
|
|
2674
|
+
- No pi-session-mcp server process and no writer container ran at the start.
|
|
2675
|
+
- Both client targets were removed with `0.11.8`, then
|
|
2676
|
+
`npm install -g pi-session-mcp@0.12.1`. Doctor reported every writer
|
|
2677
|
+
`writer_not_ready`, each container workspace `workspace_base_unset` and each
|
|
2678
|
+
writer `writer_git_identity_missing`.
|
|
2679
|
+
- Four container repositories with 14 writers were migrated by repeating their
|
|
2680
|
+
provisioning commands with unchanged settings, each first as a dry run
|
|
2681
|
+
(`planned`), then with `--apply`: 2, 4, 4 (operator image, `--cache go`) and
|
|
2682
|
+
4 (operator image) writers. Every writer reported `writer_accepted`.
|
|
2683
|
+
- `--finish --apply` of all 14 writers reported `writer_reset`; no work was
|
|
2684
|
+
collected. Doctor then reported no writer or base finding.
|
|
2685
|
+
- `--apply` and `--verify` of both client targets passed (`mcp_verified`), and
|
|
2686
|
+
`--writer <name> --verify` passed for all 14 writers. No writer registration
|
|
2687
|
+
and no container remained.
|
|
2688
|
+
|
|
2689
|
+
Live checks of #154 and #151 on the workstation, 15:02 to 15:04 UTC, with the
|
|
2690
|
+
coding profile `code-deepseek` through the one host registration and wake
|
|
2691
|
+
events from the host spool:
|
|
2692
|
+
|
|
2693
|
+
- Settings change: a writer whose settings differ is refused with
|
|
2694
|
+
`writer_settings_changed`. Both repositories' writers were collected
|
|
2695
|
+
(`nothing_to_collect`), removed with `--writer <name> --remove` and
|
|
2696
|
+
provisioned again with `--tags reachable` (2 writers) and `--cache uv`
|
|
2697
|
+
(4 writers, operator image with uv `0.13.6`); every writer reported
|
|
2698
|
+
`writer_accepted`.
|
|
2699
|
+
- Tags (#154): the writer with `--tags reachable` held 14 tags, the tags of
|
|
2700
|
+
the operator checkout merged into `main`. In the session,
|
|
2701
|
+
`git describe --tags` reported `v0.12.1` and
|
|
2702
|
+
`git show v0.12.0:package.json` read the file at that tag. The writer without
|
|
2703
|
+
the setting held no tags.
|
|
2704
|
+
- Identity (#154): an empty commit in the session, without `GIT_AUTHOR_*` or
|
|
2705
|
+
`GIT_COMMITTER_*` and without `--author`, recorded the operator's name as
|
|
2706
|
+
author and committer. `git config user.name` inside the writer failed with
|
|
2707
|
+
exit `4` because `.git/config` is mounted read-only. The collected ref showed
|
|
2708
|
+
the same identity. The test ref was removed afterwards.
|
|
2709
|
+
- uv cache (#151): three sessions on the same writer, with `--finish --apply`
|
|
2710
|
+
between the second and the third. In the first, `UV_CACHE_DIR` was
|
|
2711
|
+
`/pi/uv/cache` and empty; `uv sync --frozen` took 1.9 s and left 95,576 KiB
|
|
2712
|
+
in 1,648 files. The second and third sessions found the same size and file
|
|
2713
|
+
count before they ran anything, and `uv sync --frozen --offline` after
|
|
2714
|
+
removing `.venv` took 0.07 s and 0.08 s.
|
|
2715
|
+
|
|
2716
|
+
Findings from these checks:
|
|
2717
|
+
|
|
2718
|
+
- Reset and finish keep Git-ignored files in the clone, as designed: `.venv`
|
|
2719
|
+
from the first session was still present in the third, after the finish.
|
|
2720
|
+
The opt-in `--clean all` removes them
|
|
2721
|
+
([#162](https://github.com/plagor44-ux/pi-session-mcp/issues/162)); it is
|
|
2722
|
+
not part of the released `0.12.1`.
|
|
2723
|
+
- A provisioning command with changed settings reported `mode_applied` and
|
|
2724
|
+
recorded the new mode although every writer failed with
|
|
2725
|
+
`writer_settings_changed`; Doctor reported no mismatch
|
|
2726
|
+
([#163](https://github.com/plagor44-ux/pi-session-mcp/issues/163), open).
|
|
2727
|
+
|
|
2728
|
+
Not established for `0.12.1`: a busy pool (`writer_unavailable`) and a `base`
|
|
2729
|
+
fix round in live use; Level 3; the macOS upgrade.
|
|
2730
|
+
|
|
2460
2731
|
### v0.12.1 pre-release acceptance
|
|
2461
2732
|
|
|
2462
2733
|
`v0.12.1` was accepted on 2026-10-28 from 13:12 to 13:17 UTC on its release
|
|
@@ -105,8 +105,9 @@ This decision does not itself bump the package version or authorize publication.
|
|
|
105
105
|
it passes, then require contained provider-free acceptance bound to wrapper,
|
|
106
106
|
package version, image ID, Podman version, network and cache.
|
|
107
107
|
6. **Credential lifetime.** Launcher checks every 60 seconds. Only selected
|
|
108
|
-
OAuth tokens expired or within 2 minutes of expiry
|
|
109
|
-
|
|
108
|
+
OAuth tokens expired or within 2 minutes of expiry (raised to 10 minutes
|
|
109
|
+
in [#142](https://github.com/plagor44-ux/pi-session-mcp/issues/142), see
|
|
110
|
+
[Execution modes](../execution-modes.md)) trigger host login warmup (invalid/missing expiry also triggers it); measured warmup refreshes
|
|
110
111
|
expired/almost expired tokens only. It then re-copies credentials, with
|
|
111
112
|
OAuth refresh tokens emptied. Direct verify disables refresh with
|
|
112
113
|
`PI_WRITER_NO_CREDENTIAL_REFRESH=1`, makes no session/provider request and
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# ADR 0012: One registration with a leased writer pool
|
|
2
2
|
|
|
3
|
-
**Status:** Accepted (2026-10-28, maintainer decision)
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
**Status:** Accepted (2026-10-28, maintainer decision); implemented in
|
|
4
|
+
[#157](https://github.com/plagor44-ux/pi-session-mcp/issues/157) and released
|
|
5
|
+
in 0.12.0. Amends
|
|
6
6
|
[ADR 0002](0002-embedded-pi-sdk.md) (container sessions are relayed to a Pi
|
|
7
7
|
session in a writer process; host sessions stay embedded),
|
|
8
8
|
[ADR 0008](0008-coding-profile-isolation.md) (adopts its option 4 in container
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# ADR 0013: Hardened read-only profiles
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted (2026-10-28); released in 0.13.0. Amends
|
|
4
|
+
[ADR 0006](0006-pi-extension-allowlist.md) and
|
|
5
|
+
[ADR 0010](0010-read-only-extension-tools.md). Issue #169.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Read-only profiles have `read`, `grep`, `find` and `ls` and no shell. Review
|
|
10
|
+
workers cannot run `git log`, `git blame`, a test file or a short script, and
|
|
11
|
+
read more files instead.
|
|
12
|
+
|
|
13
|
+
Read-only sessions run in the host server process. The SDK's read tools accept
|
|
14
|
+
absolute paths, `..`, `~` and symlinks, so a read-only worker can read every
|
|
15
|
+
file the operator can read, including Pi credentials outside the workspace.
|
|
16
|
+
Today only the absence of a network path keeps that content local. A granted
|
|
17
|
+
research tool with free-form input (search, documentation lookup) would give a
|
|
18
|
+
prompt-injected worker a channel to send it out.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
### 1. Workspace-confined read tools for every read-only profile
|
|
23
|
+
|
|
24
|
+
`read`, `grep`, `find` and `ls` of **every** read-only profile accept only
|
|
25
|
+
paths inside the allowed read roots:
|
|
26
|
+
|
|
27
|
+
- the session's workspace root, and
|
|
28
|
+
- the directory of each selected skill file, because the model loads skills
|
|
29
|
+
with `read` and they live outside the workspace. If that directory equals or
|
|
30
|
+
contains the operator's home directory, the configuration file's directory
|
|
31
|
+
(and, for a symlinked configuration file, its target's directory), `~/.pi`
|
|
32
|
+
or Pi's agent directory (`~/.pi/agent`, or `PI_CODING_AGENT_DIR`), only the
|
|
33
|
+
skill file itself is a root, so a catalog entry such as `./SKILL.md` next to
|
|
34
|
+
the configuration or Pi's credentials does not expose them. The real file of
|
|
35
|
+
a symlinked skill file is a root as well, so the model can read it.
|
|
36
|
+
|
|
37
|
+
The server replaces the SDK's built-in definitions with definitions built from
|
|
38
|
+
the root exports `createReadToolDefinition`, `createGrepToolDefinition`,
|
|
39
|
+
`createFindToolDefinition` and `createLsToolDefinition`, registered under the
|
|
40
|
+
same names as SDK custom tools. Two layers check each call:
|
|
41
|
+
|
|
42
|
+
1. Before the SDK tool runs, the `path` argument is resolved against the
|
|
43
|
+
session directory and canonicalized with `realpath` (for a path that does
|
|
44
|
+
not exist yet, the nearest existing ancestor plus the remaining segments).
|
|
45
|
+
It must equal an allowed root or lie below one. Arguments the SDK would
|
|
46
|
+
rewrite before resolving (a leading `@` or `~`, a `file:` URL, Unicode
|
|
47
|
+
spaces, NUL) are refused, so this check and the SDK always resolve the
|
|
48
|
+
same path.
|
|
49
|
+
2. The `*Operations` hooks of `read`, `grep` and `ls` check the absolute path
|
|
50
|
+
the SDK hands them again. This also covers `read`'s file-name variants and
|
|
51
|
+
symlinks met during reading.
|
|
52
|
+
|
|
53
|
+
`grep` and `find` start `rg` and `fd` on the checked path. Neither follows
|
|
54
|
+
symlinks during traversal by default, so a symlink below the workspace does not
|
|
55
|
+
lead out of it. A refusal is an ordinary tool error,
|
|
56
|
+
`Path is outside the allowed read roots`, without the path.
|
|
57
|
+
|
|
58
|
+
At admission the server checks with `getToolDefinition` that each of these
|
|
59
|
+
names resolves to the server's own definition; otherwise the start fails with
|
|
60
|
+
`pi_session_creation_failed`.
|
|
61
|
+
|
|
62
|
+
The confinement is always on; there is no configuration field. Reading outside
|
|
63
|
+
the workspace has no review use, and a per-profile switch would invite the
|
|
64
|
+
unconfined variant for exactly the profiles that get external tools.
|
|
65
|
+
|
|
66
|
+
### 2. Sandboxed shell, opt-in per profile
|
|
67
|
+
|
|
68
|
+
The optional profile field `shell` takes the single value `"sandboxed"`. It is
|
|
69
|
+
allowed only on `read-only` profiles, not together with
|
|
70
|
+
`grantedExtensionTools` (granted extension tools may write, so the shell's
|
|
71
|
+
read-only guarantee would not hold), and only on Linux; any other combination
|
|
72
|
+
fails config validation with `invalid_execution_profile_config`.
|
|
73
|
+
|
|
74
|
+
Such a profile also gets `bash`, built with `createBashToolDefinition(cwd,
|
|
75
|
+
{ operations, exposeSessionEnvironment: false })` and a custom
|
|
76
|
+
`BashOperations.exec`. Each command runs as
|
|
77
|
+
`/usr/bin/bwrap <fixed arguments> -- /usr/bin/bash -c <command>`:
|
|
78
|
+
|
|
79
|
+
- `--unshare-all` (no network), `--unshare-user` (which bubblewrap requires
|
|
80
|
+
for `--disable-userns`), `--disable-userns`, `--die-with-parent`,
|
|
81
|
+
`--new-session`, `--clearenv` with
|
|
82
|
+
`PATH=/usr/local/bin:/usr/bin:/bin` and `HOME=/tmp/home`;
|
|
83
|
+
- `/usr` and `/etc` read-only; `/bin`, `/lib`, `/lib64` and `/sbin` recreated
|
|
84
|
+
as the host's merged-`/usr` symlinks or bound read-only; `--dev /dev`,
|
|
85
|
+
`--proc /proc`, a tmpfs `/tmp` limited to 512 MiB; no other host path, so the operator's home
|
|
86
|
+
is not visible;
|
|
87
|
+
- the workspace at its host path as an overlay with a temporary upper layer
|
|
88
|
+
(`--overlay-src <workspace> --tmp-overlay <workspace>`): writes succeed
|
|
89
|
+
inside the sandbox and vanish when the command ends;
|
|
90
|
+
- for a linked worktree (a `.git` file instead of a directory), the main
|
|
91
|
+
repository's Git directory as a second temporary overlay at its host path,
|
|
92
|
+
so `git log` and `git blame` work. The server accepts the pointer only if
|
|
93
|
+
`gitdir:` names `<common>/worktrees/<name>`, that directory's `gitdir` file
|
|
94
|
+
points back to the workspace's `.git`, its `commondir` resolves to
|
|
95
|
+
`<common>`, and `<common>` holds `HEAD`, `objects` and `refs`. Otherwise
|
|
96
|
+
nothing extra is bound and Git commands fail inside the sandbox. The read
|
|
97
|
+
tools stay confined to the workspace either way.
|
|
98
|
+
|
|
99
|
+
A command without a `timeout` argument gets a default of 600 seconds; on
|
|
100
|
+
timeout or abort the server kills bubblewrap, and the PID namespace takes every
|
|
101
|
+
process of the sandbox with it.
|
|
102
|
+
|
|
103
|
+
The bubblewrap path is fixed and must be a trusted system binary (root-owned,
|
|
104
|
+
not group- or world-writable); there is no `PATH` lookup. The tool's own
|
|
105
|
+
environment, the controller's environment and `PI_*` session variables never
|
|
106
|
+
reach the command.
|
|
107
|
+
|
|
108
|
+
Fail closed: before a session with `shell: "sandboxed"` starts, the server runs
|
|
109
|
+
`/usr/bin/true` through the same sandbox for the session's workspace, bounded
|
|
110
|
+
and cancelled with the start request. If the binary is untrusted or the probe
|
|
111
|
+
fails, `pi_session_start` fails with the new code `sandbox_unavailable`, class
|
|
112
|
+
`needs_operator`. A command whose sandbox cannot start fails as that command;
|
|
113
|
+
there is never an unsandboxed fallback.
|
|
114
|
+
|
|
115
|
+
### 3. Research tools
|
|
116
|
+
|
|
117
|
+
There is no new mechanism. Operators grant them through the existing
|
|
118
|
+
per-profile `mcpServers` with `readOnly: true`. The documentation advises
|
|
119
|
+
query-style tools over free URL fetching. Now that every read-only profile is
|
|
120
|
+
confined, granting research tools to read-only profiles no longer exposes files
|
|
121
|
+
outside the workspace and the skill directories.
|
|
122
|
+
|
|
123
|
+
### Doctor and capabilities
|
|
124
|
+
|
|
125
|
+
Doctor stays offline. When any profile has `shell: "sandboxed"`, it probes
|
|
126
|
+
each configured workspace with the session's fixed arguments, including a
|
|
127
|
+
linked worktree's main Git directory, and reports `sandbox_unavailable` as an
|
|
128
|
+
error when the trusted bubblewrap binary is missing or a probe fails, for
|
|
129
|
+
example because the installed version lacks the overlay options, unprivileged
|
|
130
|
+
user namespaces are disabled or restricted by AppArmor (see
|
|
131
|
+
[ADR 0008](0008-coding-profile-isolation.md)), or the workspace's filesystem
|
|
132
|
+
cannot be an overlay lower layer. For each
|
|
133
|
+
workspace with an unusable gitfile pointer, it warns with
|
|
134
|
+
`workspace_git_unavailable` when any profile has a sandboxed shell.
|
|
135
|
+
|
|
136
|
+
Capabilities expose `shell` (`"none"` or `"sandboxed"`) for every profile,
|
|
137
|
+
directly after `grantedExtensionTools`. This is configured intent only.
|
|
138
|
+
|
|
139
|
+
## Amendments
|
|
140
|
+
|
|
141
|
+
- ADR 0006: read-only profiles have `read`, `grep`, `find` and `ls`, and
|
|
142
|
+
additionally `bash` with `shell: "sandboxed"`. The read tools are
|
|
143
|
+
workspace-confined.
|
|
144
|
+
- ADR 0010: the active tools of a read-only profile are the above, the granted
|
|
145
|
+
MCP tools and the granted extension tools. A profile with granted extension
|
|
146
|
+
tools cannot have a sandboxed shell.
|
|
147
|
+
|
|
148
|
+
## Contract change and versioning
|
|
149
|
+
|
|
150
|
+
The capabilities field `shell`, the start error `sandbox_unavailable` and the
|
|
151
|
+
confinement of existing read-only profiles change the public contract and need
|
|
152
|
+
minor release **0.13.0** under
|
|
153
|
+
[ADR 0003](0003-post-mvp-maintenance.md#versioning). The confinement breaks
|
|
154
|
+
read-only workflows that read files outside the workspace. This decision does
|
|
155
|
+
not itself bump the version or authorize publication.
|
|
156
|
+
|
|
157
|
+
## Consequences
|
|
158
|
+
|
|
159
|
+
- Read-only workers can run Git, tests and scripts without touching the host
|
|
160
|
+
tree or the network.
|
|
161
|
+
- The sandbox shows only `/usr`, `/etc` and the workspace. Toolchains under the
|
|
162
|
+
home directory or `/opt` (nvm, `~/.local`) are not available; Node.js and Git
|
|
163
|
+
from the distribution are.
|
|
164
|
+
- Lock-dependent tests that need a supported local filesystem can fail on the
|
|
165
|
+
overlay (seen with `test/setup-modes.test.ts` in the #169 probe).
|
|
166
|
+
- The sandbox has no memory, process-count or overlay upper-layer limits.
|
|
167
|
+
A prompt-injected worker can exhaust memory or processes of the operator's
|
|
168
|
+
user until the command times out, as a host-mode coding session already
|
|
169
|
+
can. Limits through a cgroup scope are a possible follow-up.
|
|
170
|
+
- When `bash` truncates long output, the SDK stores the full output in the
|
|
171
|
+
host's temporary directory and names that file. Confined `read` refuses it
|
|
172
|
+
and the sandbox's `/tmp` is a fresh tmpfs, so only the truncated output is
|
|
173
|
+
available; workers should narrow their commands instead.
|
|
174
|
+
- `ls` omits entries that are symlinks pointing outside the read roots,
|
|
175
|
+
without a notice.
|
|
176
|
+
- The confinement checks a path when the call starts. A concurrent writer in
|
|
177
|
+
the same workspace, for example a host-mode coding session, could swap a
|
|
178
|
+
checked directory for a symlink before `rg` or `fd` opens it. Read-only
|
|
179
|
+
workers cannot create such symlinks themselves.
|
|
180
|
+
- Extension hooks and granted extension tools keep running in the server
|
|
181
|
+
process with the operator's rights (ADR 0006/0010); confinement covers the
|
|
182
|
+
built-in tools only.
|
|
183
|
+
- macOS, a VM backend and network allowlists inside the sandbox are out of
|
|
184
|
+
scope. Coding profiles are unchanged (ADR 0008/0009).
|
package/docs/architecture.md
CHANGED
|
@@ -47,7 +47,7 @@ reconciled only when the registration still matches that binding. A changed
|
|
|
47
47
|
release fails closed and cannot silently inherit or replace another release's
|
|
48
48
|
registration.
|
|
49
49
|
|
|
50
|
-
`pi_capabilities_get` projects the validated workspace and execution-profile maps plus the immutable configuration metadata captured while loading the file. It returns aliases plus the configured permission/provider/model/thinking intent, the configured extension specs and the selected skill aliases, sorted by alias, `configuration: { fingerprint, reloadPolicy: "restart-required" }` and `events: { delivery: "spool" | "off" }`. The fingerprint is SHA-256 over the exact loaded file bytes, including formatting; it is not a current-file freshness check. The projection makes no provider, authentication, filesystem, network, server-reachability, or catalog-state probe, so it cannot assert availability.
|
|
50
|
+
`pi_capabilities_get` projects the validated workspace and execution-profile maps plus the immutable configuration metadata captured while loading the file. It returns workspace aliases with their execution `mode` (plus `writers` and `defaultBase` for container workspaces) and profile aliases with the configured permission/provider/model/thinking intent, the configured extension specs, `grantedExtensionTools`, `shell` (`none` or `sandboxed`) and the selected skill aliases, sorted by alias, `configuration: { fingerprint, reloadPolicy: "restart-required" }` and `events: { delivery: "spool" | "off" }`. The fingerprint is SHA-256 over the exact loaded file bytes, including formatting; it is not a current-file freshness check. The projection makes no provider, authentication, filesystem, network, server-reachability, or catalog-state probe, so it cannot assert availability.
|
|
51
51
|
|
|
52
52
|
## Leased container writers
|
|
53
53
|
|