@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/CHANGELOG.md +31 -0
- package/README.md +51 -79
- package/package.json +8 -6
- package/src/config.ts +44 -14
- package/src/index.ts +96 -136
- package/src/pi-subagents-native.ts +266 -0
- package/src/external-network-policy.d.mts +0 -8
- package/src/external-network-policy.mjs +0 -27
- package/src/external-runs-view.ts +0 -173
- package/src/external-supervisor.ts +0 -189
- package/src/external-worker-launcher.mjs +0 -266
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
265
|
-
const
|
|
266
|
-
subagentProvider
|
|
267
|
-
|
|
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
|
|
283
|
-
let
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
!
|
|
561
|
+
(event.input.action === "status" || event.input.action === "debug.run") &&
|
|
562
|
+
!event.isError &&
|
|
563
|
+
!terminalChildrenHaveSandboxAcknowledgement(event.details)
|
|
572
564
|
) {
|
|
573
565
|
return {
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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 (
|
|
601
|
+
if (protectExternalOrchestration) {
|
|
633
602
|
const diagnostic = externalSubagentDiagnostic(pi);
|
|
634
|
-
ctx.ui.notify(
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
675
|
-
|
|
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,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
|
-
}
|