pi-better-harness 0.41.0 → 0.41.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/node_modules/pi-better-subagents/docs/usage.md +13 -6
- package/node_modules/pi-better-subagents/index.ts +44 -11
- package/node_modules/pi-better-subagents/package.json +1 -1
- package/node_modules/pi-better-subagents/subagent-tools.ts +43 -0
- package/node_modules/pi-better-subagents/task-policy.ts +4 -2
- package/package.json +2 -2
|
@@ -146,7 +146,10 @@ Each attempt gets a new run id, log, timing record, and capacity slot while the
|
|
|
146
146
|
native session id and history stay the same. The launch recipe preserves the
|
|
147
147
|
worker's role, tools, skill exposure, model, and workspace, including uncommitted
|
|
148
148
|
changes. Repeating a launch-only option is accepted only with the saved value;
|
|
149
|
-
a different `skills` list is refused rather than silently dropped.
|
|
149
|
+
a different `skills` list is refused rather than silently dropped. A continuation
|
|
150
|
+
whose frozen tool loadout can no longer be admitted (for example its trusted
|
|
151
|
+
package is gone) is refused before anything is spawned, instead of reopening the
|
|
152
|
+
saved conversation with different tools.
|
|
150
153
|
|
|
151
154
|
A run stopped with `subagent_stop` (status `killed`) can be continued the same
|
|
152
155
|
way. Stopping ends that execution, not the saved conversation or its
|
|
@@ -522,11 +525,15 @@ chosen in `/sandbox` → Subagents · Tools
|
|
|
522
525
|
name is refused. A single extension file with no package manifest is loaded
|
|
523
526
|
and admitted by itself, never its directory. Known network tool names
|
|
524
527
|
(`web_fetch`, `web_search`, `firecrawl_scrape`, `firecrawl_extract`, `mcp`, `mcpScript`, `remote_bash`, and any `mcp__*` name) are refused while Network access is Off; this is a name list, not a
|
|
525
|
-
network sandbox. A ticked tool whose package can't be found is
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
528
|
+
network sandbox. A ticked tool whose package can't be found is left out of a
|
|
529
|
+
default loadout and reported with its reason in the launch output; an explicit
|
|
530
|
+
`tools` request for a tool the task boundary cannot provide refuses the launch.
|
|
531
|
+
Ticked tools join the default tool list; an explicit `tools` list still decides.
|
|
532
|
+
|
|
533
|
+
Other tools are disabled in confined children. One that only joined the default
|
|
534
|
+
loadout is omitted from the child and listed in launch output with the reason;
|
|
535
|
+
one the caller named in `tools` refuses the launch with the reason. The launch
|
|
536
|
+
line shows what was admitted, for example
|
|
530
537
|
`Runtime: isolated · guarded apply_patch · trusted web_fetch (@juicesharp/rpiv-web-tools)`.
|
|
531
538
|
Provider extensions are trusted runtime code; arbitrary extension tools are not
|
|
532
539
|
admitted merely because their names were requested. Confined children disable project
|
|
@@ -62,7 +62,7 @@ import { resolveExtensions, extensionArgs } from "./extensions.ts";
|
|
|
62
62
|
import { prepareTaskRuntime, applyDirectApplicationLoadout, RUN_APPLICATION, SDK_BUILTIN_TOOLS } from "./task-policy.ts";
|
|
63
63
|
import { canonicalizePath, takeRecoverySnapshot } from "./shared-sandbox-core.ts";
|
|
64
64
|
import { TASK_BUILTINS } from "./shared-task-sandbox.ts";
|
|
65
|
-
import { APPLY_PATCH, describeTaskTools, planTaskTools } from "./subagent-tools.ts";
|
|
65
|
+
import { APPLY_PATCH, describeTaskTools, planTaskTools, selectEffectiveTaskTools } from "./subagent-tools.ts";
|
|
66
66
|
import {
|
|
67
67
|
announceExecutionAdapter, observeSandboxPermissions, resolveCommandSettings, resolveSubagentPermissions,
|
|
68
68
|
} from "./permission-policy.ts";
|
|
@@ -2574,13 +2574,51 @@ export default function (pi: ExtensionAPI) {
|
|
|
2574
2574
|
}).tools.join(",");
|
|
2575
2575
|
}
|
|
2576
2576
|
const bridgeToolNames = new Set<string>(bridgeEnabled ? bridgePlan.tools : []);
|
|
2577
|
+
// A bridge tool the supervisor withholds (no production schema, clean,
|
|
2578
|
+
// excluded) explains itself better than extension admission would.
|
|
2579
|
+
const withheldBridgeReasons = new Map<string, string>(bridgePlan.withheld.map((tool) => [tool.name, tool.reason]));
|
|
2580
|
+
// Candidates for the task-tool plan: exclusions and bridge-supplied
|
|
2581
|
+
// names never reach extension admission.
|
|
2582
|
+
const plannedTools = allow.split(",").filter((name) => name && !requestedExcludes.has(name) && !bridgeToolNames.has(name));
|
|
2577
2583
|
const taskTools = sandboxEnabled ? planTaskTools({
|
|
2578
|
-
requested:
|
|
2584
|
+
requested: plannedTools,
|
|
2579
2585
|
settings: permissionPlan.tools, network: permissionPlan.permissions?.network ?? true,
|
|
2580
2586
|
processAccess: permissionPlan.permissions?.processAccess ?? "off",
|
|
2581
2587
|
builtins: [...TASK_BUILTINS, "codemode"], registered: pi.getAllTools?.() ?? [],
|
|
2582
2588
|
toolExtensions: cfg.toolExtensions, resolvePath: resolveExtensionPath,
|
|
2583
2589
|
}) : undefined;
|
|
2590
|
+
// One effective selection feeds the task policy, child preflight, worker
|
|
2591
|
+
// bridge and frozen recipe, so a refused tool cannot leak into any of them
|
|
2592
|
+
// (#669). A name this attempt requires - the caller's explicit `tools`, or a
|
|
2593
|
+
// continuation's frozen loadout - refuses the launch instead of narrowing it;
|
|
2594
|
+
// an inherited default is dropped from the child with the reason.
|
|
2595
|
+
const requiredToolNames = new Set(requestedTools === undefined
|
|
2596
|
+
? []
|
|
2597
|
+
: requestedTools.split(",").map((name) => name.trim()).filter(Boolean));
|
|
2598
|
+
const toolSelection = taskTools
|
|
2599
|
+
? selectEffectiveTaskTools({
|
|
2600
|
+
tools: allow.split(",").filter(Boolean), plan: taskTools, required: requiredToolNames,
|
|
2601
|
+
})
|
|
2602
|
+
: undefined;
|
|
2603
|
+
if (toolSelection?.required.length) {
|
|
2604
|
+
const detail = toolSelection.required
|
|
2605
|
+
.map((tool) => `${tool.name} (${withheldBridgeReasons.get(tool.name) ?? tool.reason})`).join(", ");
|
|
2606
|
+
throw resume
|
|
2607
|
+
? new ContinuationError(
|
|
2608
|
+
`Cannot continue ${resume.prior.id}: its frozen launch requires tool(s) this parent can no longer provide: ${detail}. ` +
|
|
2609
|
+
`Launch a new run instead of continuing this conversation.`,
|
|
2610
|
+
)
|
|
2611
|
+
: new Error(
|
|
2612
|
+
`Requested tool(s) are not available in the task sandbox: ${detail}. ` +
|
|
2613
|
+
`Remove them from the tools list, or make them available in /sandbox (Subagents · Tools); nothing was launched.`,
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
if (toolSelection) {
|
|
2617
|
+
if (plannedTools.length > 0 && toolSelection.tools.length === 0) {
|
|
2618
|
+
throw new Error(`No requested tool has a verified task sandbox adapter: ${toolSelection.refused.map((tool) => `${tool.name} (${withheldBridgeReasons.get(tool.name) ?? tool.reason})`).join(", ")}.`);
|
|
2619
|
+
}
|
|
2620
|
+
allow = toolSelection.tools.join(",");
|
|
2621
|
+
}
|
|
2584
2622
|
const dependencyOptions = {
|
|
2585
2623
|
model, virtual: (() => {
|
|
2586
2624
|
const slash = model?.indexOf("/") ?? -1;
|
|
@@ -2671,15 +2709,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2671
2709
|
p.prompt,
|
|
2672
2710
|
];
|
|
2673
2711
|
|
|
2674
|
-
const piBin = resolvePiBinary();
|
|
2675
2712
|
const selectedTools = allow.split(",").filter((name) => name && !excludes.has(name));
|
|
2676
|
-
const
|
|
2677
|
-
const unavailableTools = unavailable.map((tool) => tool.name);
|
|
2678
|
-
if (sandboxEnabled && selectedTools.length && unavailableTools.length === selectedTools.length) {
|
|
2679
|
-
throw new Error(`No requested tool has a verified task sandbox adapter: ${unavailable.map((tool) => `${tool.name} (${tool.reason})`).join(", ")}.`);
|
|
2680
|
-
}
|
|
2713
|
+
const dropped = toolSelection?.dropped ?? [];
|
|
2681
2714
|
const taskRuntime = sandboxEnabled && requestedSandboxDir ? prepareTaskRuntime({
|
|
2682
|
-
root: requestedSandboxDir, controlDir: join(runDir(id), "control"),
|
|
2715
|
+
root: requestedSandboxDir, controlDir: join(runDir(id), "control"),
|
|
2683
2716
|
tools: selectedTools,
|
|
2684
2717
|
permissions: permissionPlan.permissions && { ...permissionPlan.permissions, ...commandSettings },
|
|
2685
2718
|
applyPatch: taskTools?.applyPatch === true,
|
|
@@ -2708,7 +2741,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2708
2741
|
try { ctx.ui?.notify?.(`Subagent ${id}: recovery snapshot failed (run continues): ${outcome.detail}`, "warning"); } catch { /* ignore */ }
|
|
2709
2742
|
});
|
|
2710
2743
|
}
|
|
2711
|
-
const cmd = taskRuntime ? { file: taskRuntime.file, fileArgs: [...taskRuntime.fileArgs, ...args] } : { file: process.execPath, fileArgs: [join(selfDir(), "child-runtime.mjs"),
|
|
2744
|
+
const cmd = taskRuntime ? { file: taskRuntime.file, fileArgs: [...taskRuntime.fileArgs, ...args] } : { file: process.execPath, fileArgs: [join(selfDir(), "child-runtime.mjs"), resolvePiBinary(), ...args] };
|
|
2712
2745
|
const sandboxDir = taskRuntime ? requestedSandboxDir : undefined;
|
|
2713
2746
|
|
|
2714
2747
|
// Reservations prevent competing admissions, but do not grandfather
|
|
@@ -2945,7 +2978,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2945
2978
|
: resolution.specs.length
|
|
2946
2979
|
? `Runtime: isolated · extensions ${resolution.specs.join(", ")}\n`
|
|
2947
2980
|
: `Runtime: isolated · built-in tools only\n`;
|
|
2948
|
-
const warn = (
|
|
2981
|
+
const warn = (dropped.length ? `Task sandbox: ${dropped.map((tool) => `${tool.name} disabled (${withheldBridgeReasons.get(tool.name) ?? tool.reason})`).join("; ")}.\n` : "") +
|
|
2949
2982
|
(!sandboxEnabled && resolution.unmapped.length
|
|
2950
2983
|
? `NOTE: no extension mapped for ${resolution.unmapped.join(", ")} — ` +
|
|
2951
2984
|
`${resolution.unmapped.length > 1 ? "these tools" : "this tool"} will NOT exist in the child. ` +
|
|
@@ -90,6 +90,49 @@ export function planTaskTools(options: {
|
|
|
90
90
|
return plan;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/** One effective child tool list and what the plan refused, split by who asked for it. */
|
|
94
|
+
export interface EffectiveTaskToolSelection {
|
|
95
|
+
/** The child tool list: refused names are absent, so policy, preflight and recipe agree. */
|
|
96
|
+
tools: string[];
|
|
97
|
+
/** Requested names the plan refuses, each with an actionable reason. */
|
|
98
|
+
refused: { name: string; reason: string }[];
|
|
99
|
+
/** Refused names this attempt requires (explicit request or frozen recipe): the launch must fail. */
|
|
100
|
+
required: { name: string; reason: string }[];
|
|
101
|
+
/** Refused names that only came from the default/inherited loadout: dropped with a warning. */
|
|
102
|
+
dropped: { name: string; reason: string }[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The single effective tool selection every downstream authority reads (ADR 0009).
|
|
107
|
+
*
|
|
108
|
+
* A refused tool never reaches the task policy, the child preflight, the worker
|
|
109
|
+
* bridge or the frozen launch recipe: the child cannot register it, so listing it
|
|
110
|
+
* only makes the parent announce a tool the child then dies without (#669).
|
|
111
|
+
*
|
|
112
|
+
* Who asked matters. A name this attempt requires - an explicit `tools` request,
|
|
113
|
+
* or a continuation's frozen loadout - stops the launch, because silently
|
|
114
|
+
* narrowing it would change what the caller (or the saved conversation) asked
|
|
115
|
+
* for. An inherited default is dropped from the child instead, with the reason.
|
|
116
|
+
*/
|
|
117
|
+
export function selectEffectiveTaskTools(options: {
|
|
118
|
+
/** Candidate loadout after the confinement adjustments. */
|
|
119
|
+
tools: readonly string[];
|
|
120
|
+
plan: TaskToolPlan;
|
|
121
|
+
/** Names this attempt requires; absent means the whole loadout is implicit. */
|
|
122
|
+
required?: ReadonlySet<string>;
|
|
123
|
+
}): EffectiveTaskToolSelection {
|
|
124
|
+
const refused = options.plan.refused.filter((tool) => options.tools.includes(tool.name));
|
|
125
|
+
const refusedNames = new Set(refused.map((tool) => tool.name));
|
|
126
|
+
const required = refused.filter((tool) => options.required?.has(tool.name) === true);
|
|
127
|
+
const requiredNames = new Set(required.map((tool) => tool.name));
|
|
128
|
+
return {
|
|
129
|
+
tools: options.tools.filter((name) => !refusedNames.has(name)),
|
|
130
|
+
refused,
|
|
131
|
+
required,
|
|
132
|
+
dropped: refused.filter((tool) => !requiredNames.has(tool.name)),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
93
136
|
/** "apply_patch (guarded) · web_fetch, web_search (trusted)" for the launch Runtime line. */
|
|
94
137
|
export function describeTaskTools(plan: TaskToolPlan): string | undefined {
|
|
95
138
|
const parts = [
|
|
@@ -172,7 +172,7 @@ export function parseTaskPolicy(value: unknown): TaskPolicy { if (!value || t
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
export function prepareTaskRuntime(options: {
|
|
175
|
-
root: string; controlDir: string; tools: readonly string[]; piBin
|
|
175
|
+
root: string; controlDir: string; tools: readonly string[]; piBin?: string;
|
|
176
176
|
permissions?: SandboxPermissions & Partial<CommandSettings>; extensionPaths?: readonly string[]; runtimeRoots?: readonly string[];
|
|
177
177
|
applyPatch?: boolean; extensionTools?: readonly TaskExtensionTool[];
|
|
178
178
|
}): { file: string; fileArgs: string[]; policy: TaskPolicy; policyPath: string } {
|
|
@@ -194,7 +194,9 @@ export function prepareTaskRuntime(options: {
|
|
|
194
194
|
mkdirSync(controlDir, { recursive: true });
|
|
195
195
|
const sdkEntry = canonicalizePath(join(PiCodingAgent.getPackageDir(), "dist", "index.js"));
|
|
196
196
|
const cliPaths = ["cli.js", join("bundle", "cli.js")].map((entry) => canonicalizePath(join(PiCodingAgent.getPackageDir(), "dist", entry)));
|
|
197
|
-
|
|
197
|
+
// The guarded runtime invokes this SDK directly. PATH may name a managed
|
|
198
|
+
// shell launcher, so only validate a CLI when the caller explicitly pins one.
|
|
199
|
+
if (options.piBin !== undefined && !cliPaths.includes(canonicalizePath(options.piBin))) {
|
|
198
200
|
throw new Error("Task sandbox requires the active Pi SDK CLI; a different pi executable is on PATH.");
|
|
199
201
|
}
|
|
200
202
|
const ownEntry = fileURLToPath(import.meta.url);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.1",
|
|
4
4
|
"description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, model routing, and Outcome & Todo.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"pi-ollama-cloud": "0.13.1",
|
|
64
64
|
"pi-better-sandbox": "0.14.1",
|
|
65
65
|
"pi-better-ssh": "0.1.4",
|
|
66
|
-
"pi-better-subagents": "0.28.
|
|
66
|
+
"pi-better-subagents": "0.28.1",
|
|
67
67
|
"proper-lockfile": "^4.1.2",
|
|
68
68
|
"shell-quote": "1.12.0",
|
|
69
69
|
"smol-toml": "1.9.0",
|