@erichll/pi-sandbox 0.15.3 → 0.17.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/src/index.ts CHANGED
@@ -3,7 +3,6 @@ import type {
3
3
  ExtensionAPI,
4
4
  ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
- import { fileURLToPath } from "node:url";
7
6
  import {
8
7
  createBashToolDefinition,
9
8
  createLocalBashOperations,
@@ -35,14 +34,12 @@ import type {
35
34
  import { validateSubagentModel } from "./subagent.ts";
36
35
  import { Type } from "typebox";
37
36
  import {
38
- createExternalWorkerSupervisor,
39
- type ExternalWorkerSupervisor,
40
- } from "./external-supervisor.ts";
41
- import {
42
- createExternalRunsView,
43
- externalRunsViewEnabledWhen,
44
- type ExternalRunsView,
45
- } from "./external-runs-view.ts";
37
+ loadPiSubagentsNativeRuntime,
38
+ nativeSubagentCallBlockReason,
39
+ PI_SANDBOX_ACKNOWLEDGEMENT,
40
+ terminalChildrenHaveSandboxAcknowledgement,
41
+ type CapabilityCeilingHandle,
42
+ } from "./pi-subagents-native.ts";
46
43
 
47
44
  const EXTENSION_NAME = "pi-sandbox";
48
45
  const registrations = new WeakMap<ExtensionAPI, Promise<void>>();
@@ -54,12 +51,19 @@ export type PiSandboxExtensionOptions = {
54
51
  sandbox?: Pick<SandboxCommandOptions, "broker" | "platform">;
55
52
  /** Test/embedding override. Normal extension loading uses trusted global config. */
56
53
  hostIPC?: HostIPCConfig;
54
+ /** Test/embedding override. Normal extension loading uses trusted global config. */
55
+ allowedNativeAgents?: readonly string[];
57
56
  };
58
57
 
59
58
  function sessionId(ctx: ExtensionContext): string {
60
59
  return ctx.sessionManager.getSessionId();
61
60
  }
62
61
 
62
+ function piSubagentsSessionId(ctx: ExtensionContext): string {
63
+ const getSessionFile = (ctx.sessionManager as { getSessionFile?: () => string | null | undefined }).getSessionFile;
64
+ return getSessionFile?.call(ctx.sessionManager) ?? ctx.sessionManager.getSessionId();
65
+ }
66
+
63
67
  function isPiSubagentsSource(
64
68
  sourceInfo: ReturnType<ExtensionAPI["getAllTools"]>[number]["sourceInfo"],
65
69
  ): boolean {
@@ -91,55 +95,11 @@ function externalSubagentDiagnostic(pi: ExtensionAPI): {
91
95
  }
92
96
  return {
93
97
  message:
94
- "pi-subagents orchestration active; pi-sandbox protects Bash execution. External workers are not yet wrapped in an outer Sandbox Runtime sandbox.",
98
+ "pi-subagents protected native-background mode active; child writes are restricted to sandboxed Bash. This is a tool boundary, not outer worker process isolation.",
95
99
  level: "info",
96
100
  };
97
101
  }
98
102
 
99
- const EXTERNAL_WORKER_LAUNCHER = fileURLToPath(
100
- new URL("./external-worker-launcher.mjs", import.meta.url),
101
- );
102
-
103
- type ExternalWorkerIsolation = {
104
- restore(): void;
105
- };
106
-
107
- function enableExternalWorkerIsolation(
108
- supervisor: ExternalWorkerSupervisor,
109
- ): ExternalWorkerIsolation {
110
- if (process.platform !== "linux" && process.platform !== "darwin") {
111
- throw new Error(`external worker isolation is unavailable on ${process.platform}`);
112
- }
113
- if (process.env.PI_SUBAGENT_PI_BINARY) {
114
- throw new Error(
115
- "external worker isolation refuses to replace an existing PI_SUBAGENT_PI_BINARY wrapper",
116
- );
117
- }
118
- const entry = process.argv[1];
119
- if (!entry) {
120
- throw new Error("external worker isolation cannot determine the real Pi CLI entrypoint");
121
- }
122
- const injected = {
123
- PI_SUBAGENT_PI_BINARY: EXTERNAL_WORKER_LAUNCHER,
124
- PI_SANDBOX_EXTERNAL_REAL_PI_BINARY: process.execPath,
125
- PI_SANDBOX_EXTERNAL_REAL_PI_PREFIX: JSON.stringify([entry]),
126
- PI_SANDBOX_EXTERNAL_ALLOW_READ: [
127
- process.cwd(),
128
- process.env.PI_CODING_AGENT_DIR,
129
- ].filter((value): value is string => Boolean(value)).join(":"),
130
- PI_SANDBOX_EXTERNAL_SUPERVISOR_SOCKET: supervisor.socketPath,
131
- PI_SANDBOX_EXTERNAL_SUPERVISOR_CAPABILITY: supervisor.capability,
132
- };
133
- Object.assign(process.env, injected);
134
- return {
135
- restore() {
136
- for (const [key, value] of Object.entries(injected)) {
137
- if (process.env[key] === value) delete process.env[key];
138
- }
139
- },
140
- };
141
- }
142
-
143
103
  function humanApproval(ctx: ExtensionContext): HumanApproval {
144
104
  return async (request, reason, signal) => {
145
105
  if (!ctx.hasUI) return "deny";
@@ -261,11 +221,19 @@ async function performRegistration(
261
221
  const config = loadPiSandboxConfig();
262
222
  const subagentProvider =
263
223
  options.subagentProvider ?? config.subagents.provider;
264
- const externalWorkerIsolation = config.subagents.externalWorkerIsolation;
265
- const externalRunsViewEnabled = externalRunsViewEnabledWhen(
266
- subagentProvider,
267
- externalWorkerIsolation,
268
- );
224
+ const isNativeSubagentChild = process.env.PI_SUBAGENT_CHILD === "1";
225
+ const protectExternalOrchestration =
226
+ subagentProvider === "pi-subagents" && !isNativeSubagentChild;
227
+ const allowedNativeAgents = options.allowedNativeAgents ??
228
+ config.subagents.allowedNativeAgents;
229
+ if (protectExternalOrchestration && !allowedNativeAgents?.length) {
230
+ throw new Error(
231
+ `${EXTENSION_NAME}: pi-subagents protected mode requires allowedNativeAgents`,
232
+ );
233
+ }
234
+ const nativeRuntime = protectExternalOrchestration
235
+ ? await loadPiSubagentsNativeRuntime(allowedNativeAgents!)
236
+ : undefined;
269
237
  const additionalAllowRead = config.filesystem.additionalAllowRead;
270
238
  const network = config.network;
271
239
  const hostIPC = options.hostIPC ?? config.hostIPC;
@@ -279,10 +247,8 @@ async function performRegistration(
279
247
  }))
280
248
  : undefined;
281
249
  const cwd = process.cwd();
282
- let isolation: ExternalWorkerIsolation | undefined;
283
- let supervisor: ExternalWorkerSupervisor | undefined;
284
- let fleetView: ExternalRunsView | undefined;
285
- let externalIsolationFailure: string | undefined;
250
+ let capabilityCeiling: CapabilityCeilingHandle | undefined;
251
+ let validatedNativeAgents: string[] = [];
286
252
  const localBash = createBashToolDefinition(cwd);
287
253
 
288
254
  pi.registerTool({
@@ -560,93 +526,91 @@ async function performRegistration(
560
526
  currentTurn = event.turnIndex;
561
527
  });
562
528
 
563
- // pi-subagents owns the public tool, so a failed wrapper bootstrap cannot
564
- // be fixed by replacing that tool. Block at Pi's pre-execution boundary:
565
- // enforce must never degrade into a host worker launch.
566
- pi.on("tool_call", (event) => {
529
+ pi.on("tool_call", (event, ctx) => {
530
+ if (event.toolName === "subagent" && protectExternalOrchestration) {
531
+ const reason = nativeSubagentCallBlockReason(event.input);
532
+ if (reason) {
533
+ return { block: true, reason: `${EXTENSION_NAME}: ${reason}` };
534
+ }
535
+ if (event.input.action === undefined) {
536
+ try {
537
+ validatedNativeAgents = nativeRuntime!.validateAllowedAgents(
538
+ ctx.cwd,
539
+ ctx.model?.provider,
540
+ );
541
+ capabilityCeiling?.dispose();
542
+ capabilityCeiling = nativeRuntime!.registerCeiling(
543
+ piSubagentsSessionId(ctx),
544
+ validatedNativeAgents,
545
+ );
546
+ } catch (error) {
547
+ return {
548
+ block: true,
549
+ reason: `${EXTENSION_NAME}: protected launch validation failed: ${error instanceof Error ? error.message : String(error)}`,
550
+ };
551
+ }
552
+ }
553
+ }
554
+ return undefined;
555
+ });
556
+
557
+ pi.on("tool_result", (event) => {
567
558
  if (
559
+ protectExternalOrchestration &&
568
560
  event.toolName === "subagent" &&
569
- subagentProvider === "pi-subagents" &&
570
- externalWorkerIsolation === "enforce" &&
571
- !isolation
561
+ (event.input.action === "status" || event.input.action === "debug.run") &&
562
+ !event.isError &&
563
+ !terminalChildrenHaveSandboxAcknowledgement(event.details)
572
564
  ) {
573
565
  return {
574
- block: true,
575
- reason: `${EXTENSION_NAME}: external worker isolation is unavailable; refusing host worker launch${externalIsolationFailure ? `: ${externalIsolationFailure}` : ""}`,
566
+ content: [{
567
+ type: "text",
568
+ text: `${EXTENSION_NAME}: child result did not acknowledge ${PI_SANDBOX_ACKNOWLEDGEMENT}; protected execution proof is missing`,
569
+ }],
570
+ isError: true,
576
571
  };
577
572
  }
578
573
  return undefined;
579
574
  });
580
575
 
581
576
  pi.on("session_start", async (_event, ctx) => {
582
- // A new session may replace a prior supervisor ownership; drop the old
583
- // session's FleetView registrations before reconstructing.
584
- fleetView?.close();
585
- fleetView = undefined;
586
- if (
587
- subagentProvider === "pi-subagents" &&
588
- externalWorkerIsolation === "enforce"
589
- ) {
590
- try {
591
- isolation?.restore();
592
- await supervisor?.close();
593
- supervisor = undefined;
594
- if (externalRunsViewEnabled) {
595
- fleetView = await createExternalRunsView(sessionId(ctx));
596
- }
597
- supervisor = await createExternalWorkerSupervisor(
598
- (worker) => ({
599
- broker: getBoundaryBroker(),
600
- command: "external pi-subagents worker network request",
601
- cwd: worker.cwd,
602
- sessionId: sessionId(ctx),
603
- scopeKey: `${sessionId(ctx)}:turn:${currentTurn}`,
604
- agentName: `pi-subagents-worker:${worker.id}`,
605
- humanApproval: humanApproval(ctx),
606
- }),
607
- {
608
- // Observability-only: never influences allow/deny, and a failure
609
- // here must not stop the supervisor from answering approval RPCs
610
- // or make the worker launch on the host.
611
- registered: (worker) => fleetView?.registered(worker),
612
- unregistered: (worker) => fleetView?.unregistered(worker.id),
613
- },
614
- network,
615
- );
616
- isolation = enableExternalWorkerIsolation(supervisor);
617
- externalIsolationFailure = undefined;
618
- } catch (error) {
619
- const message = `${EXTENSION_NAME}: external worker isolation unavailable; child launches will fail closed: ${error instanceof Error ? error.message : String(error)}`;
620
- externalIsolationFailure = message;
621
- isolation?.restore();
622
- isolation = undefined;
623
- await supervisor?.close();
624
- supervisor = undefined;
625
- fleetView?.close();
626
- fleetView = undefined;
627
- console.error(message);
628
- if (ctx.hasUI) ctx.ui.notify(message, "warning");
577
+ if (isNativeSubagentChild) {
578
+ pi.events?.emit("subagent:acknowledge-extension", {
579
+ id: PI_SANDBOX_ACKNOWLEDGEMENT,
580
+ });
581
+ }
582
+ capabilityCeiling?.dispose();
583
+ capabilityCeiling = undefined;
584
+ if (protectExternalOrchestration) {
585
+ validatedNativeAgents = nativeRuntime!.validateAllowedAgents(
586
+ ctx.cwd,
587
+ ctx.model?.provider,
588
+ );
589
+ capabilityCeiling = nativeRuntime!.registerCeiling(
590
+ piSubagentsSessionId(ctx),
591
+ validatedNativeAgents,
592
+ );
593
+ const ownership = externalSubagentDiagnostic(pi);
594
+ if (ownership.level === "warning") {
595
+ capabilityCeiling.dispose();
596
+ capabilityCeiling = undefined;
597
+ throw new Error(`${EXTENSION_NAME}: ${ownership.message}`);
629
598
  }
630
599
  }
631
600
  if (ctx.hasUI) {
632
- if (subagentProvider === "pi-subagents") {
601
+ if (protectExternalOrchestration) {
633
602
  const diagnostic = externalSubagentDiagnostic(pi);
634
- ctx.ui.notify(
635
- externalWorkerIsolation === "enforce" && isolation
636
- ? "pi-subagents orchestration active; external worker process trees require outer Sandbox Runtime isolation."
637
- : diagnostic.message,
638
- externalWorkerIsolation === "enforce" && isolation
639
- ? "info"
640
- : diagnostic.level,
641
- );
642
- } else if (subagentProvider === "builtin") {
603
+ ctx.ui.notify(diagnostic.message, diagnostic.level);
604
+ } else if (subagentProvider === "builtin" && !isNativeSubagentChild) {
643
605
  ctx.ui.notify(
644
606
  "pi-sandbox subagent provider: builtin; worker process trees use the outer Sandbox Runtime sandbox",
645
607
  "info",
646
608
  );
647
609
  } else {
648
610
  ctx.ui.notify(
649
- "pi-sandbox subagent provider: off; pi-sandbox protects Bash execution only",
611
+ isNativeSubagentChild
612
+ ? "pi-sandbox active in native subagent child; writes are restricted to sandboxed Bash"
613
+ : "pi-sandbox subagent provider: off; pi-sandbox protects Bash execution only",
650
614
  "info",
651
615
  );
652
616
  }
@@ -671,12 +635,8 @@ async function performRegistration(
671
635
  });
672
636
 
673
637
  pi.on("session_shutdown", async () => {
674
- isolation?.restore();
675
- isolation = undefined;
676
- fleetView?.close();
677
- fleetView = undefined;
678
- await supervisor?.close();
679
- supervisor = undefined;
638
+ capabilityCeiling?.dispose();
639
+ capabilityCeiling = undefined;
680
640
  await subagents?.shutdown();
681
641
  });
682
642
  }
@@ -0,0 +1,266 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { createJiti } from "jiti";
5
+
6
+ /**
7
+ * Compatible pi-subagents line: 0.65.x. Patch updates (e.g. 0.65.1) are accepted;
8
+ * any minor or major bump fails closed until this adapter is revalidated against
9
+ * the new internal layout. The exports/ceiling checks below are the real
10
+ * compatibility gate; this range only guards against blind accept-on-drift.
11
+ */
12
+ export const PI_SUBAGENTS_COMPAT_RANGE = ">=0.65.0 <0.66.0";
13
+ export const NATIVE_CHILD_TOOLS = ["bash", "read", "grep", "find", "ls"] as const;
14
+ export const PI_SANDBOX_ACKNOWLEDGEMENT = "@erichll:pi-sandbox";
15
+
16
+ type Agent = {
17
+ name: string;
18
+ localName?: string;
19
+ runner?: { type?: unknown };
20
+ disabled?: boolean;
21
+ extensions?: string[];
22
+ extensionsFromDefault?: boolean;
23
+ allowNestedSubagents?: boolean;
24
+ };
25
+
26
+ type DiscoveryModule = {
27
+ discoverAgents(cwd: string, scope: "both", preferredProvider?: string): {
28
+ agents: Agent[];
29
+ };
30
+ resolveAgentName(name: string, agents: Agent[]): {
31
+ agent?: Agent;
32
+ error?: string;
33
+ };
34
+ };
35
+
36
+ type ConfigModule = {
37
+ loadConfig(): { scheduledRuns?: { enabled?: boolean } };
38
+ };
39
+
40
+ export type CapabilityCeilingHandle = { dispose(): void };
41
+ type CeilingModule = {
42
+ SUBAGENT_CAPABILITY_CEILING_VERSION: unknown;
43
+ registerSubagentCapabilityCeiling(options: {
44
+ sessionId: string;
45
+ source: string;
46
+ ceiling: {
47
+ allowedAgents: readonly string[];
48
+ allowedTools: readonly string[];
49
+ };
50
+ }): CapabilityCeilingHandle;
51
+ };
52
+
53
+ export type PiSubagentsNativeRuntime = {
54
+ validateAllowedAgents(
55
+ cwd: string,
56
+ preferredProvider?: string,
57
+ ): string[];
58
+ registerCeiling(sessionId: string, allowedAgents: readonly string[]): CapabilityCeilingHandle;
59
+ };
60
+
61
+ function requiredFunction(
62
+ value: unknown,
63
+ name: string,
64
+ ): asserts value is (...args: never[]) => unknown {
65
+ if (typeof value !== "function") {
66
+ throw new Error(`pi-subagents 0.65.x compatibility failure: missing ${name}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Accept only versions inside PI_SUBAGENTS_COMPAT_RANGE (0.65.x, including
72
+ * prerelease suffixes like 0.65.2-beta.1) without pulling in a semver runtime.
73
+ */
74
+ export function isCompatiblePiSubagentsVersion(version: unknown): boolean {
75
+ if (typeof version !== "string") return false;
76
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
77
+ if (!match) return false;
78
+ return Number(match[1] ?? "") === 0 && Number(match[2] ?? "") === 65;
79
+ }
80
+
81
+ /** Load the public ceiling API and the 0.65.x discovery/config internals.
82
+ * This intentionally fails closed on package version, export, or layout drift.
83
+ */
84
+ export async function loadPiSubagentsNativeRuntime(
85
+ configuredAgents: readonly string[],
86
+ ): Promise<PiSubagentsNativeRuntime> {
87
+ const entry = fileURLToPath(import.meta.resolve("pi-subagents"));
88
+ const root = dirname(entry);
89
+ const packageJson = JSON.parse(
90
+ await readFile(join(root, "package.json"), "utf8"),
91
+ ) as { version?: unknown; exports?: Record<string, unknown> };
92
+ if (!isCompatiblePiSubagentsVersion(packageJson.version)) {
93
+ throw new Error(
94
+ `pi-subagents protected mode requires ${PI_SUBAGENTS_COMPAT_RANGE}; found ${String(packageJson.version)}`,
95
+ );
96
+ }
97
+ if (packageJson.exports?.["./capability-ceiling"] !== "./src/api/capability-ceiling.ts") {
98
+ throw new Error(
99
+ "pi-subagents 0.65.x compatibility failure: capability-ceiling export changed",
100
+ );
101
+ }
102
+
103
+ const jiti = createJiti(import.meta.url, {
104
+ interopDefault: false,
105
+ fsCache: false,
106
+ });
107
+ const [ceiling, discovery, config] = await Promise.all([
108
+ jiti.import(pathToFileURL(join(root, "src/api/capability-ceiling.ts")).href) as Promise<CeilingModule>,
109
+ jiti.import(pathToFileURL(join(root, "src/agents/agents.ts")).href) as Promise<DiscoveryModule>,
110
+ jiti.import(pathToFileURL(join(root, "src/extension/config.ts")).href) as Promise<ConfigModule>,
111
+ ]).catch((error) => {
112
+ throw new Error(
113
+ `pi-subagents 0.65.x compatibility failure: ${error instanceof Error ? error.message : String(error)}`,
114
+ );
115
+ });
116
+ if (ceiling.SUBAGENT_CAPABILITY_CEILING_VERSION !== 1) {
117
+ throw new Error("pi-subagents 0.65.x compatibility failure: unsupported capability ceiling version");
118
+ }
119
+ requiredFunction(ceiling.registerSubagentCapabilityCeiling, "registerSubagentCapabilityCeiling");
120
+ requiredFunction(discovery.discoverAgents, "discoverAgents");
121
+ requiredFunction(discovery.resolveAgentName, "resolveAgentName");
122
+ requiredFunction(config.loadConfig, "loadConfig");
123
+
124
+ const validateAllowedAgents = (
125
+ cwd: string,
126
+ preferredProvider?: string,
127
+ ): string[] => {
128
+ const upstreamConfig = config.loadConfig();
129
+ if (upstreamConfig.scheduledRuns?.enabled !== false) {
130
+ throw new Error(
131
+ "pi-subagents protected mode requires scheduledRuns.enabled=false in the pi-subagents config",
132
+ );
133
+ }
134
+ const discovered = discovery.discoverAgents(cwd, "both", preferredProvider);
135
+ const canonical: string[] = [];
136
+ for (const requested of configuredAgents) {
137
+ const resolved = discovery.resolveAgentName(requested, discovered.agents);
138
+ if (resolved.error) throw new Error(resolved.error);
139
+ if (!resolved.agent) {
140
+ throw new Error(`pi-subagents protected mode cannot resolve allowed agent '${requested}'`);
141
+ }
142
+ const agent = resolved.agent;
143
+ if (agent.name !== requested) {
144
+ throw new Error(
145
+ `pi-subagents protected mode requires canonical agent names; '${requested}' resolves to '${agent.name}'`,
146
+ );
147
+ }
148
+ if (agent.disabled === true) {
149
+ throw new Error(`pi-subagents protected mode rejects disabled agent '${requested}'`);
150
+ }
151
+ if (agent.runner !== undefined && agent.runner.type !== "pi") {
152
+ throw new Error(
153
+ `pi-subagents protected mode rejects runner '${String(agent.runner.type)}' for agent '${requested}'`,
154
+ );
155
+ }
156
+ if (agent.extensions !== undefined && agent.extensionsFromDefault !== true) {
157
+ throw new Error(
158
+ `pi-subagents protected mode requires ambient extensions for agent '${requested}'; remove its explicit extensions override`,
159
+ );
160
+ }
161
+ if (agent.allowNestedSubagents === true) {
162
+ throw new Error(
163
+ `pi-subagents protected mode rejects allowNestedSubagents for agent '${requested}'`,
164
+ );
165
+ }
166
+ canonical.push(agent.name);
167
+ }
168
+ return canonical;
169
+ };
170
+
171
+ // Validate once while constructing the adapter as an internal-structure gate.
172
+ if (configuredAgents.length === 0) {
173
+ throw new Error("pi-subagents protected mode requires at least one allowed agent");
174
+ }
175
+ return {
176
+ validateAllowedAgents,
177
+ registerCeiling(sessionId, allowedAgents) {
178
+ return ceiling.registerSubagentCapabilityCeiling({
179
+ sessionId,
180
+ source: PI_SANDBOX_ACKNOWLEDGEMENT,
181
+ ceiling: { allowedAgents, allowedTools: NATIVE_CHILD_TOOLS },
182
+ });
183
+ },
184
+ };
185
+ }
186
+
187
+ export function nativeSubagentCallBlockReason(
188
+ input: Record<string, unknown>,
189
+ ): string | undefined {
190
+ const action = typeof input.action === "string" ? input.action.trim() : undefined;
191
+ if (action?.startsWith("schedule.")) {
192
+ return "scheduled subagent operations are disabled in protected mode";
193
+ }
194
+ const forbiddenActions = new Set([
195
+ "create", "update", "delete", "eject", "enable", "disable", "reset",
196
+ "refine", "refine.rollback", "watchdog.configure", "grant-spawn-budget",
197
+ "mission.create", "mission.update", "mission.resolve-decision",
198
+ "mission.attach-run", "mission.close", "inspector.open", "inspector.close",
199
+ "project.open", "project.close", "worktree.discard", "worktree.cleanup",
200
+ "lane.recordMerge", "lane.recordSupersession",
201
+ ]);
202
+ if (action && forbiddenActions.has(action)) {
203
+ return `subagent management action '${action}' is disabled in protected mode`;
204
+ }
205
+ if (action === "resume") {
206
+ return "subagent resume may launch a child and is disabled in protected mode";
207
+ }
208
+ if (action !== undefined) return undefined;
209
+ if (input.workflow !== undefined) {
210
+ return "extension-owned named workflows are disabled in protected mode";
211
+ }
212
+ const isDirect = typeof input.agent === "string" && input.agent.trim() !== "";
213
+ if (input.workflowScript !== undefined || input.workflowScriptPath !== undefined) {
214
+ return "workflowScript and workflowScriptPath are disabled because pi-subagents 0.65.x disables ambient extensions for their native children";
215
+ }
216
+ if (!isDirect) {
217
+ return "protected mode supports only direct agent launches";
218
+ }
219
+ if (input.async !== true) {
220
+ return "protected mode requires every child launch to set async: true explicitly";
221
+ }
222
+ return undefined;
223
+ }
224
+
225
+ export function hasSandboxAcknowledgement(value: unknown): boolean {
226
+ if (!value || typeof value !== "object") return false;
227
+ const record = value as Record<string, unknown>;
228
+ const acknowledgement = record.runtimeAcknowledgedExtensions;
229
+ if (
230
+ acknowledgement && typeof acknowledgement === "object" &&
231
+ Array.isArray((acknowledgement as { ids?: unknown }).ids) &&
232
+ (acknowledgement as { ids: unknown[] }).ids.includes(PI_SANDBOX_ACKNOWLEDGEMENT)
233
+ ) return true;
234
+ return Object.values(record).some((child) =>
235
+ Array.isArray(child)
236
+ ? child.some(hasSandboxAcknowledgement)
237
+ : hasSandboxAcknowledgement(child)
238
+ );
239
+ }
240
+
241
+ export function terminalChildrenHaveSandboxAcknowledgement(value: unknown): boolean {
242
+ let foundTerminalChild = false;
243
+ let missing = false;
244
+ const visit = (candidate: unknown): void => {
245
+ if (missing || !candidate || typeof candidate !== "object") return;
246
+ if (Array.isArray(candidate)) {
247
+ for (const item of candidate) visit(item);
248
+ return;
249
+ }
250
+ const record = candidate as Record<string, unknown>;
251
+ const state = record.state ?? record.status;
252
+ if (
253
+ typeof record.agent === "string" &&
254
+ ["completed", "failed", "stopped", "timed_out", "interrupted"].includes(String(state))
255
+ ) {
256
+ foundTerminalChild = true;
257
+ if (!hasSandboxAcknowledgement({
258
+ runtimeAcknowledgedExtensions: record.runtimeAcknowledgedExtensions,
259
+ })) missing = true;
260
+ return;
261
+ }
262
+ for (const child of Object.values(record)) visit(child);
263
+ };
264
+ visit(value);
265
+ return !foundTerminalChild || !missing;
266
+ }
@@ -1,8 +0,0 @@
1
- export type ExternalNetworkPolicy = {
2
- allowedDomains: string[];
3
- deniedDomains: string[];
4
- };
5
-
6
- export function decodeExternalNetworkPolicy(
7
- value: unknown,
8
- ): ExternalNetworkPolicy;
@@ -1,27 +0,0 @@
1
- import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
2
-
3
- /**
4
- * Decode the supervisor-owned network policy delivered during registration.
5
- * Any missing, extra, or malformed data becomes a deny-all policy so a broken
6
- * transport can never widen an external worker's access.
7
- */
8
- export function decodeExternalNetworkPolicy(value) {
9
- if (
10
- !value ||
11
- typeof value !== "object" ||
12
- Array.isArray(value) ||
13
- Object.keys(value).some(
14
- (key) => key !== "allowedDomains" && key !== "deniedDomains",
15
- )
16
- ) {
17
- return { allowedDomains: [], deniedDomains: ["*"] };
18
- }
19
- const result = NetworkConfigSchema.safeParse(value);
20
- if (!result.success) {
21
- return { allowedDomains: [], deniedDomains: ["*"] };
22
- }
23
- return {
24
- allowedDomains: [...result.data.allowedDomains],
25
- deniedDomains: [...result.data.deniedDomains],
26
- };
27
- }