pi-better-subagents 0.6.0 → 0.7.0
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/README.md +1 -1
- package/child-incidents.ts +2 -2
- package/docs/failure-observations.md +1 -1
- package/failures.ts +48 -6
- package/incident-model.ts +11 -4
- package/index.ts +47 -18
- package/output-payload.ts +2 -3
- package/package.json +1 -1
- package/permission-policy.ts +9 -3
- package/registry.ts +19 -2
- package/shared-failure-observations.ts +48 -12
- package/shared-log-utils.ts +6 -12
- package/shared-navigator.ts +169 -125
- package/shared-sandbox-core.ts +78 -14
- package/shared-task-apply-patch.ts +409 -0
- package/shared-task-files.ts +69 -7
- package/shared-task-sandbox.ts +11 -0
- package/shared-task-tools.ts +111 -0
- package/subagent-tools.ts +95 -0
- package/task-guard.ts +42 -7
- package/task-policy.ts +28 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Generated from packages/task-sandbox/tools.ts. Do not edit directly.
|
|
2
|
+
/**
|
|
3
|
+
* Which extension tools a confined subagent may use (ADR 0009).
|
|
4
|
+
*
|
|
5
|
+
* - Guarded tools are harness adapters that go through the task's guarded file
|
|
6
|
+
* operations, so the file rules govern them. Today: apply_patch.
|
|
7
|
+
* - Trusted tools are third-party tools admitted by name AND owning package.
|
|
8
|
+
* They run in the child Pi process, outside the file rules; a human ticks each
|
|
9
|
+
* one in /sandbox. Network tools are refused while Network is Off.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
|
|
14
|
+
export interface TrustedToolEntry {
|
|
15
|
+
/** Tool name as registered. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Owning package, as Pi reports its source (e.g. `npm:@juicesharp/rpiv-web-tools`). */
|
|
18
|
+
package: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SubagentToolSettings {
|
|
22
|
+
/** The guarded apply_patch adapter. */
|
|
23
|
+
applyPatch: boolean;
|
|
24
|
+
/** Ticked trusted tools. Everything not listed is off. */
|
|
25
|
+
trusted: TrustedToolEntry[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const WEB_TOOLS_PACKAGE = "npm:@juicesharp/rpiv-web-tools";
|
|
29
|
+
|
|
30
|
+
export function defaultSubagentTools(): SubagentToolSettings {
|
|
31
|
+
return {
|
|
32
|
+
applyPatch: true,
|
|
33
|
+
trusted: [{ name: "web_fetch", package: WEB_TOOLS_PACKAGE }, { name: "web_search", package: WEB_TOOLS_PACKAGE }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Tools that reach the network; refused while the profile's Network access is Off. */
|
|
38
|
+
export function isNetworkTool(name: string): boolean {
|
|
39
|
+
return ["web_fetch", "web_search", "firecrawl_scrape", "firecrawl_extract", "mcp", "mcpScript", "remote_bash"].includes(name) ||
|
|
40
|
+
name.startsWith("mcp__");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Names never offered as trusted: builtins and the guarded adapters. */
|
|
44
|
+
export const RESERVED_TOOL_NAMES = Object.freeze(["read", "write", "edit", "bash", "grep", "find", "ls", "powershell", "apply_patch"]);
|
|
45
|
+
|
|
46
|
+
/** Packages that are this harness: their tools are never offered as trusted tools. */
|
|
47
|
+
const HARNESS_PACKAGES = /(^|[/:])pi-better-[a-z-]+$/;
|
|
48
|
+
|
|
49
|
+
/** Strict decoding: a malformed tool list must not silently admit anything. */
|
|
50
|
+
export function parseSubagentTools(value: unknown): SubagentToolSettings {
|
|
51
|
+
if (value === undefined) return defaultSubagentTools();
|
|
52
|
+
if (!value || typeof value !== "object") throw new Error("Invalid subagent tool settings.");
|
|
53
|
+
const v = value as Record<string, unknown>;
|
|
54
|
+
if (typeof v.applyPatch !== "boolean" || !Array.isArray(v.trusted)) throw new Error("Invalid subagent tool settings.");
|
|
55
|
+
const trusted: TrustedToolEntry[] = [];
|
|
56
|
+
for (const entry of v.trusted) {
|
|
57
|
+
const e = entry as Record<string, unknown> | null;
|
|
58
|
+
if (!e || typeof e.name !== "string" || !e.name || typeof e.package !== "string" || !e.package) {
|
|
59
|
+
throw new Error("Invalid trusted subagent tool entry.");
|
|
60
|
+
}
|
|
61
|
+
if (RESERVED_TOOL_NAMES.includes(e.name)) throw new Error(`${e.name} cannot be a trusted tool.`);
|
|
62
|
+
if (!trusted.some((t) => t.name === e.name && t.package === e.package)) trusted.push({ name: e.name, package: e.package });
|
|
63
|
+
}
|
|
64
|
+
return { applyPatch: v.applyPatch, trusted };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ToolSource {
|
|
68
|
+
name: string;
|
|
69
|
+
sourceInfo?: { path?: string; source?: string; baseDir?: string };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DiscoveredTool {
|
|
73
|
+
name: string;
|
|
74
|
+
package: string;
|
|
75
|
+
/**
|
|
76
|
+
* What to load and admit: the package root directory, or, for an extension
|
|
77
|
+
* file with no package manifest, that file itself. Loading a manifest-less
|
|
78
|
+
* directory would make Pi discover every extension in it.
|
|
79
|
+
*/
|
|
80
|
+
root: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The owning package of a registered tool, or undefined for builtins and inline registrations. */
|
|
84
|
+
export function toolPackage(tool: ToolSource): DiscoveredTool | undefined {
|
|
85
|
+
const info = tool.sourceInfo;
|
|
86
|
+
if (!info?.path || info.path.startsWith("<")) return undefined;
|
|
87
|
+
const base = info.baseDir ?? dirname(info.path);
|
|
88
|
+
if (!existsSync(join(base, "package.json"))) {
|
|
89
|
+
// A single extension file (e.g. ~/.pi/agent/extensions/foo.ts): it is its own package.
|
|
90
|
+
return { name: tool.name, package: info.path, root: info.path };
|
|
91
|
+
}
|
|
92
|
+
const pkg = info.source && info.source !== "builtin" ? info.source : base;
|
|
93
|
+
return { name: tool.name, package: pkg, root: base };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Candidate trusted tools in a running Pi, excluding builtins, guarded names and this harness. */
|
|
97
|
+
export function discoverTrustedTools(tools: readonly ToolSource[]): DiscoveredTool[] {
|
|
98
|
+
const found: DiscoveredTool[] = [];
|
|
99
|
+
for (const tool of tools) {
|
|
100
|
+
if (RESERVED_TOOL_NAMES.includes(tool.name)) continue;
|
|
101
|
+
const owner = toolPackage(tool);
|
|
102
|
+
if (!owner || [owner.package, owner.root].some((id) => HARNESS_PACKAGES.test(id.replace(/\/+$/, "")))) continue;
|
|
103
|
+
if (!found.some((t) => t.name === owner.name && t.package === owner.package)) found.push(owner);
|
|
104
|
+
}
|
|
105
|
+
return found.sort((a, b) => a.name.localeCompare(b.name) || a.package.localeCompare(b.package));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A package label for display: `npm:@scope/name` → `@scope/name`. */
|
|
109
|
+
export function packageLabel(pkg: string): string {
|
|
110
|
+
return pkg.startsWith("npm:") ? pkg.slice(4) : pkg;
|
|
111
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch-time plan for a confined child's extension tools (ADR 0009).
|
|
3
|
+
*
|
|
4
|
+
* Guarded: apply_patch, a harness adapter on the guarded file operations.
|
|
5
|
+
* Trusted: a tool ticked in /sandbox, admitted by name AND owning package, whose
|
|
6
|
+
* package is loaded into the child. `toolExtensions` in config.json still picks
|
|
7
|
+
* the package to load (an override), but the loaded package must be the ticked one.
|
|
8
|
+
*/
|
|
9
|
+
import { canonicalizePath } from "./shared-sandbox-core.ts";
|
|
10
|
+
import { isNetworkTool, packageLabel, toolPackage, type SubagentToolSettings, type ToolSource } from "./shared-task-tools.ts";
|
|
11
|
+
import type { TaskExtensionTool } from "./task-policy.ts";
|
|
12
|
+
|
|
13
|
+
export const APPLY_PATCH = "apply_patch";
|
|
14
|
+
|
|
15
|
+
export interface TaskToolPlan {
|
|
16
|
+
/** apply_patch is a task builtin for this run. */
|
|
17
|
+
applyPatch: boolean;
|
|
18
|
+
/** Trusted tools the child admits, with the package root recorded in the child policy. */
|
|
19
|
+
trusted: (TaskExtensionTool & { loadPath: string })[];
|
|
20
|
+
/** Requested tools that will not exist in the child, with the reason. */
|
|
21
|
+
refused: { name: string; reason: string }[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function specList(value: unknown): string[] {
|
|
25
|
+
if (typeof value === "string") return value.trim() ? [value.trim()] : [];
|
|
26
|
+
return Array.isArray(value) ? value.filter((s): s is string => typeof s === "string" && !!s.trim()).map((s) => s.trim()) : [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function planTaskTools(options: {
|
|
30
|
+
requested: readonly string[];
|
|
31
|
+
settings: SubagentToolSettings;
|
|
32
|
+
network: boolean;
|
|
33
|
+
builtins: readonly string[];
|
|
34
|
+
/** Tools registered in the parent Pi (`pi.getAllTools()`), used to find a ticked tool's package. */
|
|
35
|
+
registered: readonly ToolSource[];
|
|
36
|
+
toolExtensions?: Record<string, string | string[]> | null;
|
|
37
|
+
resolvePath: (spec: string) => string | undefined;
|
|
38
|
+
}): TaskToolPlan {
|
|
39
|
+
const plan: TaskToolPlan = { applyPatch: false, trusted: [], refused: [] };
|
|
40
|
+
for (const name of options.requested) {
|
|
41
|
+
if (options.builtins.includes(name)) continue;
|
|
42
|
+
if (name === APPLY_PATCH) {
|
|
43
|
+
if (options.settings.applyPatch) plan.applyPatch = true;
|
|
44
|
+
else plan.refused.push({ name, reason: "apply_patch is off in /sandbox (Subagents · Tools)" });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const ticked = options.settings.trusted.filter((entry) => entry.name === name);
|
|
48
|
+
if (!ticked.length) {
|
|
49
|
+
plan.refused.push({ name, reason: "not a guarded tool and not ticked as trusted in /sandbox (Subagents · Tools)" });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const network = isNetworkTool(name);
|
|
53
|
+
if (network && !options.network) {
|
|
54
|
+
plan.refused.push({ name, reason: "needs Network access, which is Off for subagents" });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Which package to load: a config override, else the package that registers
|
|
58
|
+
// the tool in this Pi, else an installed npm package by its spec.
|
|
59
|
+
const override = specList(options.toolExtensions?.[name]);
|
|
60
|
+
let chosen: { package: string; loadPath: string } | undefined;
|
|
61
|
+
let problem: string | undefined;
|
|
62
|
+
for (const entry of ticked) {
|
|
63
|
+
if (override.length) {
|
|
64
|
+
if (!override.includes(entry.package)) continue;
|
|
65
|
+
const loadPath = options.resolvePath(entry.package);
|
|
66
|
+
if (loadPath) { chosen = { package: entry.package, loadPath }; break; }
|
|
67
|
+
problem = `its package ${packageLabel(entry.package)} is not installed`;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const owner = options.registered.map(toolPackage).find((tool) => tool?.name === name && tool.package === entry.package);
|
|
71
|
+
const loadPath = owner?.root ?? (entry.package.startsWith("npm:") ? options.resolvePath(entry.package) : undefined);
|
|
72
|
+
if (loadPath) { chosen = { package: entry.package, loadPath }; break; }
|
|
73
|
+
problem = `its package ${packageLabel(entry.package)} can't be found (not installed or not loaded in this Pi)`;
|
|
74
|
+
}
|
|
75
|
+
if (!chosen) {
|
|
76
|
+
plan.refused.push({ name, reason: problem ?? `config.json toolExtensions maps it to ${override.map(packageLabel).join(", ")}, not the ticked package ${ticked.map((t) => packageLabel(t.package)).join(", ")}` });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// A directory is admitted as a package root; a file admits only itself.
|
|
80
|
+
let root: string;
|
|
81
|
+
try { root = canonicalizePath(chosen.loadPath); }
|
|
82
|
+
catch { plan.refused.push({ name, reason: `its package path ${chosen.loadPath} can't be resolved` }); continue; }
|
|
83
|
+
plan.trusted.push({ name, package: chosen.package, root, network, loadPath: chosen.loadPath });
|
|
84
|
+
}
|
|
85
|
+
return plan;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** "apply_patch (guarded) · web_fetch, web_search (trusted)" for the launch Runtime line. */
|
|
89
|
+
export function describeTaskTools(plan: TaskToolPlan): string | undefined {
|
|
90
|
+
const parts = [
|
|
91
|
+
...(plan.applyPatch ? [`guarded ${APPLY_PATCH}`] : []),
|
|
92
|
+
...(plan.trusted.length ? [`trusted ${plan.trusted.map((tool) => `${tool.name} (${packageLabel(tool.package)})`).join(", ")}`] : []),
|
|
93
|
+
];
|
|
94
|
+
return parts.length ? parts.join(" · ") : undefined;
|
|
95
|
+
}
|
package/task-guard.ts
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
/** Installed as the final inline extension by the trusted native launcher. */
|
|
2
2
|
import { SettingsManager, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { describeSandboxSupport } from "./shared-sandbox-core.ts";
|
|
4
|
-
import { installTaskTools, TASK_BUILTINS } from "./shared-task-sandbox.ts";
|
|
5
|
-
import { parseTaskPolicy } from "./task-policy.ts";
|
|
3
|
+
import { canonicalizePath, describeSandboxSupport } from "./shared-sandbox-core.ts";
|
|
4
|
+
import { GUARDED_TASK_TOOLS, installTaskTools, TASK_BUILTINS } from "./shared-task-sandbox.ts";
|
|
5
|
+
import { parseTaskPolicy, type TaskPolicy } from "./task-policy.ts";
|
|
6
6
|
import { SANDBOX_POLICY_CHANNEL, SANDBOX_POLICY_REQUEST_CHANNEL } from "./permission-policy.ts";
|
|
7
7
|
import { failureDispositionTool, intentBashDefinition } from "./child-incidents.ts";
|
|
8
8
|
import { DISPOSITION_TOOL } from "./incident-model.ts";
|
|
9
9
|
|
|
10
10
|
const TRUSTED_INLINE_SOURCE = "<inline:task-sandbox>";
|
|
11
11
|
|
|
12
|
+
function inside(root: string, path: string): boolean {
|
|
13
|
+
return path === root || path.startsWith(root.endsWith("/") ? root : `${root}/`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A trusted extension tool is admitted only when its name AND its canonical
|
|
18
|
+
* source lie inside the package root the parent recorded, and a network tool
|
|
19
|
+
* only while Network access is On. Another package registering the same name
|
|
20
|
+
* is refused.
|
|
21
|
+
*/
|
|
22
|
+
export function trustedToolRefusal(policy: TaskPolicy, name: string, sourcePath: string | undefined): string | undefined {
|
|
23
|
+
const entry = policy.extensionTools.find((tool) => tool.name === name);
|
|
24
|
+
if (!entry) return `${name} is not a trusted tool for this run`;
|
|
25
|
+
if (entry.network && !policy.permissions.network) return `${name} needs Network access, which is Off`;
|
|
26
|
+
if (!sourcePath || sourcePath.startsWith("<")) return `${name} has no package source`;
|
|
27
|
+
let canonical: string;
|
|
28
|
+
try { canonical = canonicalizePath(sourcePath); } catch { return `${name} source cannot be resolved`; }
|
|
29
|
+
if (!inside(entry.root, canonical)) return `${name} comes from ${sourcePath}, not the trusted package ${entry.package}`;
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
export default function taskGuard(pi: ExtensionAPI, input: unknown, fatal: (error: unknown) => never): void {
|
|
13
34
|
const policy = parseTaskPolicy(input);
|
|
14
35
|
const support = describeSandboxSupport();
|
|
@@ -25,8 +46,11 @@ export default function taskGuard(pi: ExtensionAPI, input: unknown, fatal: (erro
|
|
|
25
46
|
trustedSources: [TRUSTED_INLINE_SOURCE],
|
|
26
47
|
// Structured command intent (#315): validated before the confined command runs.
|
|
27
48
|
bashDefinition: (cwd, operations) => intentBashDefinition(cwd, operations) as any,
|
|
49
|
+
applyPatch: policy.applyPatch,
|
|
28
50
|
// The disposition tool performs no file or command I/O; only this inline registration is admitted.
|
|
29
|
-
|
|
51
|
+
// Trusted tools are admitted by name and package (ADR 0009).
|
|
52
|
+
admitExtensionTool: (name, _input, sourcePath) => (name === DISPOSITION_TOOL && sourcePath === TRUSTED_INLINE_SOURCE) ||
|
|
53
|
+
trustedToolRefusal(policy, name, sourcePath) === undefined,
|
|
30
54
|
});
|
|
31
55
|
pi.registerTool(failureDispositionTool(() => currentCwd) as any);
|
|
32
56
|
const profile = Object.freeze({ enabled: true, ...policy.permissions });
|
|
@@ -43,9 +67,19 @@ export default function taskGuard(pi: ExtensionAPI, input: unknown, fatal: (erro
|
|
|
43
67
|
shellPath = SettingsManager.create(ctx.cwd, policy.agentDir, { projectTrusted: false }).getShellPath();
|
|
44
68
|
currentCwd = ctx.cwd;
|
|
45
69
|
boundary.register(ctx.cwd);
|
|
46
|
-
const
|
|
70
|
+
const builtins: readonly string[] = policy.applyPatch ? [...TASK_BUILTINS, ...GUARDED_TASK_TOOLS] : TASK_BUILTINS;
|
|
71
|
+
const selected = policy.tools.filter((name) => builtins.includes(name));
|
|
47
72
|
boundary.assertInstalled(selected);
|
|
48
|
-
const
|
|
73
|
+
const inventory = pi.getAllTools();
|
|
74
|
+
const trusted: string[] = [];
|
|
75
|
+
const refused: { name: string; reason: string }[] = [];
|
|
76
|
+
for (const name of policy.tools) {
|
|
77
|
+
if (builtins.includes(name) || !policy.extensionTools.some((tool) => tool.name === name)) continue;
|
|
78
|
+
const registered = inventory.find((tool) => tool.name === name);
|
|
79
|
+
const reason = registered ? trustedToolRefusal(policy, name, registered.sourceInfo?.path) : `${name} was not registered by its package`;
|
|
80
|
+
if (reason) refused.push({ name, reason }); else trusted.push(name);
|
|
81
|
+
}
|
|
82
|
+
const activated = selected.length || trusted.length ? [...selected, ...trusted, DISPOSITION_TOOL] : selected;
|
|
49
83
|
pi.setActiveTools(activated);
|
|
50
84
|
const active = new Set(pi.getActiveTools());
|
|
51
85
|
if (active.size !== activated.length || activated.some((name) => !active.has(name))) {
|
|
@@ -53,7 +87,8 @@ export default function taskGuard(pi: ExtensionAPI, input: unknown, fatal: (erro
|
|
|
53
87
|
}
|
|
54
88
|
pi.events.emit(SANDBOX_POLICY_CHANNEL, status);
|
|
55
89
|
// A typed lifecycle marker, never inferred from assistant prose.
|
|
56
|
-
process.stdout.write(`${JSON.stringify({ type: "task_sandbox_ready", root: policy.root, tools: selected
|
|
90
|
+
process.stdout.write(`${JSON.stringify({ type: "task_sandbox_ready", root: policy.root, tools: selected,
|
|
91
|
+
...(trusted.length ? { trusted } : {}), ...(refused.length ? { refused } : {}) })}\n`);
|
|
57
92
|
} catch (error) { fatal(error); }
|
|
58
93
|
});
|
|
59
94
|
}
|
package/task-policy.ts
CHANGED
|
@@ -17,6 +17,20 @@ export type TaskPolicy = Readonly<{
|
|
|
17
17
|
permissions: Readonly<SandboxPermissions>;
|
|
18
18
|
denyWrite: readonly string[];
|
|
19
19
|
tools: readonly string[];
|
|
20
|
+
/** The guarded apply_patch adapter is a task builtin for this run. */
|
|
21
|
+
applyPatch: boolean;
|
|
22
|
+
/** Trusted extension tools admitted by name AND canonical package root (ADR 0009). */
|
|
23
|
+
extensionTools: readonly TaskExtensionTool[];
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
export type TaskExtensionTool = Readonly<{
|
|
27
|
+
name: string;
|
|
28
|
+
/** Owning package as the parent resolved it (for messages). */
|
|
29
|
+
package: string;
|
|
30
|
+
/** Canonical package root; the registered tool's source must lie inside it. */
|
|
31
|
+
root: string;
|
|
32
|
+
/** Refused while the profile's Network access is Off. */
|
|
33
|
+
network: boolean;
|
|
20
34
|
}>;
|
|
21
35
|
|
|
22
36
|
/**
|
|
@@ -45,6 +59,13 @@ export function parseTaskPolicy(value: unknown): TaskPolicy {
|
|
|
45
59
|
if (typeof p.commands !== "boolean" || typeof p.network !== "boolean") throw new Error("Invalid task sandbox capabilities.");
|
|
46
60
|
if (!Array.isArray(v.denyWrite) || !v.denyWrite.every((path) => typeof path === "string" && isAbsolute(path))) throw new Error("Invalid task sandbox protected paths.");
|
|
47
61
|
if (!Array.isArray(v.tools) || !v.tools.every((name) => typeof name === "string" && name.length > 0)) throw new Error("Invalid task sandbox tool selection.");
|
|
62
|
+
if (v.applyPatch !== undefined && typeof v.applyPatch !== "boolean") throw new Error("Invalid task sandbox apply_patch setting.");
|
|
63
|
+
const extensionTools = v.extensionTools ?? [];
|
|
64
|
+
if (!Array.isArray(extensionTools) || !extensionTools.every((entry) => {
|
|
65
|
+
const e = entry as Record<string, unknown> | null;
|
|
66
|
+
return !!e && typeof e.name === "string" && e.name.length > 0 && typeof e.package === "string" &&
|
|
67
|
+
typeof e.root === "string" && isAbsolute(e.root) && typeof e.network === "boolean";
|
|
68
|
+
})) throw new Error("Invalid task sandbox extension tools.");
|
|
48
69
|
return Object.freeze({
|
|
49
70
|
version: 1, root: absolute("root"), home: absolute("home"), agentDir: absolute("agentDir"), profilePath: absolute("profilePath"), scratch: absolute("scratch"),
|
|
50
71
|
permissions: Object.freeze({
|
|
@@ -52,12 +73,17 @@ export function parseTaskPolicy(value: unknown): TaskPolicy {
|
|
|
52
73
|
commands: p.commands, network: p.network,
|
|
53
74
|
} as SandboxPermissions),
|
|
54
75
|
denyWrite: Object.freeze([...v.denyWrite]), tools: Object.freeze([...v.tools]),
|
|
76
|
+
applyPatch: v.applyPatch === true,
|
|
77
|
+
extensionTools: Object.freeze((extensionTools as TaskExtensionTool[]).map((e) => Object.freeze({
|
|
78
|
+
name: e.name, package: e.package, root: e.root, network: e.network,
|
|
79
|
+
}))),
|
|
55
80
|
});
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
export function prepareTaskRuntime(options: {
|
|
59
84
|
root: string; controlDir: string; tools: readonly string[]; piBin: string;
|
|
60
85
|
permissions?: SandboxPermissions; extensionPaths?: readonly string[]; runtimeRoots?: readonly string[];
|
|
86
|
+
applyPatch?: boolean; extensionTools?: readonly TaskExtensionTool[];
|
|
61
87
|
}): { file: string; fileArgs: string[]; policy: TaskPolicy; policyPath: string } {
|
|
62
88
|
if (typeof PiCodingAgent.getAgentDir !== "function" || typeof PiCodingAgent.getPackageDir !== "function") {
|
|
63
89
|
throw new Error("Task sandbox requires a supported active Pi SDK; refusing an unguarded child.");
|
|
@@ -93,6 +119,8 @@ export function prepareTaskRuntime(options: {
|
|
|
93
119
|
runtimeCodeRoot(sdkEntry), runtimeCodeRoot(ownEntry), ...(options.extensionPaths ?? []).map(runtimeCodeRoot),
|
|
94
120
|
])],
|
|
95
121
|
tools: options.tools,
|
|
122
|
+
applyPatch: options.applyPatch === true,
|
|
123
|
+
extensionTools: (options.extensionTools ?? []).map((tool) => ({ ...tool, root: canonicalizePath(tool.root) })),
|
|
96
124
|
});
|
|
97
125
|
compileWritePolicy({ writableRoot: root, home, permissions: policy.permissions, denyWrite: policy.denyWrite, runtimeCompatibility: true });
|
|
98
126
|
const policyPath = join(controlDir, "task-policy.json");
|