@trim21/personal-pi-extensions 0.0.168 → 0.0.171
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/package.json +1 -1
- package/src/bwrap/index.ts +34 -48
- package/src/spawn-agent.ts +157 -21
- package/src/workspace-guard.ts +8 -26
package/package.json
CHANGED
package/src/bwrap/index.ts
CHANGED
|
@@ -104,29 +104,22 @@ work inside the sandbox, run it WITHOUT full access first. If it fails with
|
|
|
104
104
|
and set \`request_full_access_reason\` to describe the failure.
|
|
105
105
|
`;
|
|
106
106
|
|
|
107
|
-
const
|
|
108
|
-
## Command Execution (
|
|
107
|
+
const HEADLESS_SANDBOX_PROMPT = `
|
|
108
|
+
## Command Execution (headless sandbox)
|
|
109
109
|
|
|
110
|
-
You are running inside a read-only sandbox
|
|
110
|
+
You are running inside a read-only sandbox without an interactive UI.
|
|
111
111
|
|
|
112
112
|
- The bash tool is read-only: no filesystem writes, no network access, and
|
|
113
113
|
\`request_full_access\` is denied. Do not pass \`request_full_access\`.
|
|
114
114
|
- Use the write/edit tools for file changes inside your workspace.
|
|
115
|
-
- Writes outside the workspace are rejected
|
|
116
|
-
|
|
117
|
-
- If a command truly requires network or system-level access,
|
|
118
|
-
|
|
115
|
+
- Writes outside the workspace are rejected because no UI is available for
|
|
116
|
+
approval.
|
|
117
|
+
- If a command truly requires network or system-level access, explain that it
|
|
118
|
+
cannot be approved in this session.
|
|
119
119
|
`;
|
|
120
120
|
|
|
121
121
|
const PROTECTED_DIRS = [".git", ".pi", ".agent"];
|
|
122
122
|
|
|
123
|
-
/**
|
|
124
|
-
* pi-subagents sets PI_SUBAGENT_CHILD=1 in every spawned child session
|
|
125
|
-
* (foreground and async alike). Subagent sessions are headless and must not
|
|
126
|
-
* be able to bypass the sandbox, so bwrap forces read-only bash there.
|
|
127
|
-
*/
|
|
128
|
-
const isSubagentChild = process.env.PI_SUBAGENT_CHILD === "1";
|
|
129
|
-
|
|
130
123
|
const bwrapPath = findDefaultBwrap();
|
|
131
124
|
|
|
132
125
|
function findDefaultBwrap(): string {
|
|
@@ -198,11 +191,11 @@ function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
|
198
191
|
}
|
|
199
192
|
|
|
200
193
|
/**
|
|
201
|
-
*
|
|
194
|
+
* Headless sessions are forced read-only regardless of config: no writable
|
|
202
195
|
* paths (including configured extraWritablePaths), no network, and no
|
|
203
196
|
* user-supplied extra args that could add writable mounts.
|
|
204
197
|
*/
|
|
205
|
-
export function
|
|
198
|
+
export function resolveHeadlessBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
206
199
|
return resolveBwrap({
|
|
207
200
|
...config,
|
|
208
201
|
mode: "readonly",
|
|
@@ -524,22 +517,10 @@ export type EscalationDecision = { kind: "dialog" } | { kind: "deny"; reason: st
|
|
|
524
517
|
|
|
525
518
|
/**
|
|
526
519
|
* Escalation (`request_full_access`) policy:
|
|
527
|
-
* -
|
|
528
|
-
*
|
|
529
|
-
* - Any other headless session is denied: there is no user to approve.
|
|
530
|
-
* - Interactive sessions require the user approval dialog.
|
|
520
|
+
* - Headless sessions are denied because there is no user to approve.
|
|
521
|
+
* - Sessions with UI require the user approval dialog.
|
|
531
522
|
*/
|
|
532
|
-
export function resolveEscalation(opts: {
|
|
533
|
-
hasUI: boolean;
|
|
534
|
-
isSubagentChild: boolean;
|
|
535
|
-
}): EscalationDecision {
|
|
536
|
-
if (opts.isSubagentChild) {
|
|
537
|
-
return {
|
|
538
|
-
kind: "deny",
|
|
539
|
-
reason:
|
|
540
|
-
"request_full_access is disabled in subagent sessions: the bash tool is read-only and cannot escalate. Ask the parent session to run this command.",
|
|
541
|
-
};
|
|
542
|
-
}
|
|
523
|
+
export function resolveEscalation(opts: { hasUI: boolean }): EscalationDecision {
|
|
543
524
|
if (!opts.hasUI) {
|
|
544
525
|
return {
|
|
545
526
|
kind: "deny",
|
|
@@ -583,10 +564,10 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
583
564
|
|
|
584
565
|
let resolved: ResolvedBwrap | null = null;
|
|
585
566
|
|
|
586
|
-
function getResolved(): ResolvedBwrap {
|
|
587
|
-
//
|
|
588
|
-
if (
|
|
589
|
-
return
|
|
567
|
+
function getResolved(hasUI: boolean): ResolvedBwrap {
|
|
568
|
+
// Without an approval path, ignore config and force bash read-only.
|
|
569
|
+
if (!hasUI) {
|
|
570
|
+
return resolveHeadlessBwrap(loadConfig(localCwd));
|
|
590
571
|
}
|
|
591
572
|
if (!resolved) {
|
|
592
573
|
resolved = resolveBwrap(loadConfig(localCwd));
|
|
@@ -595,7 +576,6 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
595
576
|
}
|
|
596
577
|
|
|
597
578
|
function setMode(mode: BwrapMode) {
|
|
598
|
-
if (isSubagentChild) return; // mode switching is not allowed in subagents
|
|
599
579
|
const config = loadConfig(localCwd);
|
|
600
580
|
config.mode = mode;
|
|
601
581
|
resolved = resolveBwrap(config);
|
|
@@ -613,7 +593,8 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
613
593
|
},
|
|
614
594
|
executionMode: localBash.executionMode,
|
|
615
595
|
async execute(id, params, signal, onUpdate, ctx) {
|
|
616
|
-
const
|
|
596
|
+
const hasUI = ctx?.hasUI ?? false;
|
|
597
|
+
const r = getResolved(hasUI);
|
|
617
598
|
|
|
618
599
|
if (!r.bwrapEnabled) {
|
|
619
600
|
return localBash.execute(id, params, signal, onUpdate);
|
|
@@ -622,7 +603,7 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
622
603
|
const escalate = params.request_full_access === true;
|
|
623
604
|
|
|
624
605
|
if (escalate) {
|
|
625
|
-
const policy = resolveEscalation({ hasUI
|
|
606
|
+
const policy = resolveEscalation({ hasUI });
|
|
626
607
|
if (policy.kind === "deny") {
|
|
627
608
|
throw new Error(policy.reason);
|
|
628
609
|
}
|
|
@@ -672,9 +653,9 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
672
653
|
pi.on("session_start", (_event, ctx) => {
|
|
673
654
|
const noBwrap = pi.getFlag("no-bwrap") === true;
|
|
674
655
|
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
if (noBwrap &&
|
|
656
|
+
// Headless sessions are always sandboxed read-only because they cannot
|
|
657
|
+
// approve disabling the sandbox.
|
|
658
|
+
if (noBwrap && ctx.hasUI) {
|
|
678
659
|
resolved = null;
|
|
679
660
|
ctx.ui.notify("bwrap sandbox disabled via --no-bwrap", "warning");
|
|
680
661
|
return;
|
|
@@ -691,7 +672,7 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
691
672
|
}
|
|
692
673
|
|
|
693
674
|
const config = loadConfig(ctx.cwd);
|
|
694
|
-
resolved =
|
|
675
|
+
resolved = ctx.hasUI ? resolveBwrap(config) : resolveHeadlessBwrap(config);
|
|
695
676
|
|
|
696
677
|
if (resolved.bwrapEnabled) {
|
|
697
678
|
try {
|
|
@@ -719,20 +700,20 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
719
700
|
resolved = null;
|
|
720
701
|
});
|
|
721
702
|
|
|
722
|
-
pi.on("before_agent_start", (event) => {
|
|
723
|
-
const r = getResolved();
|
|
703
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
704
|
+
const r = getResolved(ctx.hasUI);
|
|
724
705
|
|
|
725
706
|
return {
|
|
726
|
-
systemPrompt:
|
|
727
|
-
? event.systemPrompt + "\n\n" +
|
|
728
|
-
: event.systemPrompt + "\n\n" +
|
|
707
|
+
systemPrompt: ctx.hasUI
|
|
708
|
+
? event.systemPrompt + "\n\n" + SANDBOX_PROMPT + `\n\nCurrent mode: **${r.mode}**\n`
|
|
709
|
+
: event.systemPrompt + "\n\n" + HEADLESS_SANDBOX_PROMPT,
|
|
729
710
|
};
|
|
730
711
|
});
|
|
731
712
|
|
|
732
713
|
pi.registerCommand("bwrap", {
|
|
733
714
|
description: "Show bwrap sandbox configuration",
|
|
734
715
|
handler: (_args, ctx) => {
|
|
735
|
-
const r = getResolved();
|
|
716
|
+
const r = getResolved(ctx.hasUI);
|
|
736
717
|
if (!r.bwrapEnabled) {
|
|
737
718
|
ctx.ui.notify(`bwrap disabled (mode: ${r.mode})`, "info");
|
|
738
719
|
return Promise.resolve();
|
|
@@ -758,8 +739,13 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
758
739
|
theme: Theme;
|
|
759
740
|
setStatus: (k: string, t: string | undefined) => void;
|
|
760
741
|
};
|
|
742
|
+
hasUI: boolean;
|
|
761
743
|
},
|
|
762
744
|
) {
|
|
745
|
+
if (!ctx.hasUI) {
|
|
746
|
+
ctx.ui.notify("bwrap mode cannot be changed without an interactive UI", "warning");
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
763
749
|
setMode(mode);
|
|
764
750
|
|
|
765
751
|
ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("accent", `bwrap: ${mode}`));
|
package/src/spawn-agent.ts
CHANGED
|
@@ -29,7 +29,10 @@ import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-cor
|
|
|
29
29
|
import {
|
|
30
30
|
type AgentSessionEvent,
|
|
31
31
|
type ExtensionAPI,
|
|
32
|
+
type ExtensionUIContext,
|
|
32
33
|
getMarkdownTheme,
|
|
34
|
+
type RpcExtensionUIRequest,
|
|
35
|
+
type RpcExtensionUIResponse,
|
|
33
36
|
truncateTail,
|
|
34
37
|
withFileMutationQueue,
|
|
35
38
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -176,18 +179,18 @@ async function writePromptToTempFile(agentName: string, prompt: string): Promise
|
|
|
176
179
|
|
|
177
180
|
export function buildSubagentArgs(
|
|
178
181
|
agent: AgentConfig,
|
|
179
|
-
|
|
182
|
+
_task: string,
|
|
180
183
|
systemPromptPath: string | undefined,
|
|
181
184
|
): string[] {
|
|
182
|
-
//
|
|
183
|
-
// --no-session
|
|
185
|
+
// RPC mode emits agent and extension UI events as JSON lines and accepts
|
|
186
|
+
// dialog responses over stdin. --no-session keeps the child ephemeral.
|
|
187
|
+
// --no-extensions disables
|
|
184
188
|
// extension discovery; only the extensions explicitly loaded below (the
|
|
185
189
|
// unconditional guards plus per-tool overrides) run inside the subagent.
|
|
186
|
-
const args: string[] = ["--mode", "
|
|
190
|
+
const args: string[] = ["--mode", "rpc", "--no-session", "--no-extensions"];
|
|
187
191
|
|
|
188
192
|
// Protection layers that must be present in every subagent regardless of
|
|
189
|
-
// its declared toolset: the workspace write guard and the bwrap sandbox
|
|
190
|
-
// (forced read-only for subagents via PI_SUBAGENT_CHILD=1).
|
|
193
|
+
// its declared toolset: the workspace write guard and the bwrap sandbox.
|
|
191
194
|
for (const ext of UNCONDITIONAL_EXTENSIONS) {
|
|
192
195
|
args.push("-e", extensionPath(ext));
|
|
193
196
|
}
|
|
@@ -210,7 +213,6 @@ export function buildSubagentArgs(
|
|
|
210
213
|
}
|
|
211
214
|
args.push("--tools", tools.join(","));
|
|
212
215
|
if (systemPromptPath) args.push("--append-system-prompt", systemPromptPath);
|
|
213
|
-
args.push(`Task: ${task}`);
|
|
214
216
|
return args;
|
|
215
217
|
}
|
|
216
218
|
|
|
@@ -218,12 +220,86 @@ export function buildSubagentArgs(
|
|
|
218
220
|
|
|
219
221
|
type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
220
222
|
|
|
223
|
+
function dialogOptions(signal: AbortSignal | undefined, timeout: number | undefined) {
|
|
224
|
+
return {
|
|
225
|
+
...(signal && { signal }),
|
|
226
|
+
...(timeout !== undefined && { timeout }),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Forward one RPC extension UI request to the parent session. */
|
|
231
|
+
export async function forwardSubagentUIRequest(
|
|
232
|
+
request: RpcExtensionUIRequest,
|
|
233
|
+
ui: ExtensionUIContext,
|
|
234
|
+
signal?: AbortSignal,
|
|
235
|
+
): Promise<RpcExtensionUIResponse | undefined> {
|
|
236
|
+
switch (request.method) {
|
|
237
|
+
case "select": {
|
|
238
|
+
const value = await ui.select(
|
|
239
|
+
request.title,
|
|
240
|
+
request.options,
|
|
241
|
+
dialogOptions(signal, request.timeout),
|
|
242
|
+
);
|
|
243
|
+
return value === undefined
|
|
244
|
+
? { type: "extension_ui_response", id: request.id, cancelled: true }
|
|
245
|
+
: { type: "extension_ui_response", id: request.id, value };
|
|
246
|
+
}
|
|
247
|
+
case "confirm": {
|
|
248
|
+
const confirmed = await ui.confirm(
|
|
249
|
+
request.title,
|
|
250
|
+
request.message,
|
|
251
|
+
dialogOptions(signal, request.timeout),
|
|
252
|
+
);
|
|
253
|
+
return { type: "extension_ui_response", id: request.id, confirmed };
|
|
254
|
+
}
|
|
255
|
+
case "input": {
|
|
256
|
+
const value = await ui.input(
|
|
257
|
+
request.title,
|
|
258
|
+
request.placeholder,
|
|
259
|
+
dialogOptions(signal, request.timeout),
|
|
260
|
+
);
|
|
261
|
+
return value === undefined
|
|
262
|
+
? { type: "extension_ui_response", id: request.id, cancelled: true }
|
|
263
|
+
: { type: "extension_ui_response", id: request.id, value };
|
|
264
|
+
}
|
|
265
|
+
case "editor": {
|
|
266
|
+
const value = await ui.editor(request.title, request.prefill);
|
|
267
|
+
return value === undefined
|
|
268
|
+
? { type: "extension_ui_response", id: request.id, cancelled: true }
|
|
269
|
+
: { type: "extension_ui_response", id: request.id, value };
|
|
270
|
+
}
|
|
271
|
+
case "notify": {
|
|
272
|
+
ui.notify(request.message, request.notifyType);
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
case "setStatus": {
|
|
276
|
+
ui.setStatus(request.statusKey, request.statusText);
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
case "setWidget": {
|
|
280
|
+
ui.setWidget(request.widgetKey, request.widgetLines, {
|
|
281
|
+
placement: request.widgetPlacement,
|
|
282
|
+
});
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
case "setTitle": {
|
|
286
|
+
ui.setTitle(request.title);
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
case "set_editor_text": {
|
|
290
|
+
ui.setEditorText(request.text);
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
221
296
|
export async function runAgent(
|
|
222
297
|
agent: AgentConfig,
|
|
223
298
|
task: string,
|
|
224
299
|
cwd: string,
|
|
225
300
|
signal: AbortSignal | undefined,
|
|
226
301
|
onUpdate: OnUpdateCallback | undefined,
|
|
302
|
+
parentUI?: ExtensionUIContext,
|
|
227
303
|
): Promise<SubagentDetails> {
|
|
228
304
|
const result: SubagentDetails = {
|
|
229
305
|
agent: agent.name,
|
|
@@ -253,10 +329,8 @@ export async function runAgent(
|
|
|
253
329
|
const proc = spawn(invocation.command, invocation.args, {
|
|
254
330
|
cwd,
|
|
255
331
|
shell: false,
|
|
256
|
-
stdio: ["
|
|
257
|
-
|
|
258
|
-
// bwrap's subagent policy) can recognize and treat it accordingly.
|
|
259
|
-
env: { ...process.env, PI_SUBAGENT_CHILD: "1" },
|
|
332
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
333
|
+
env: { ...process.env },
|
|
260
334
|
});
|
|
261
335
|
|
|
262
336
|
let logLines: string[] = [];
|
|
@@ -281,10 +355,57 @@ export async function runAgent(
|
|
|
281
355
|
|
|
282
356
|
let buffer = "";
|
|
283
357
|
|
|
358
|
+
const sendRpc = (message: object) => {
|
|
359
|
+
proc.stdin.write(`${JSON.stringify(message)}\n`);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
let requestedShutdown = false;
|
|
363
|
+
const requestShutdown = () => {
|
|
364
|
+
if (requestedShutdown) return;
|
|
365
|
+
requestedShutdown = true;
|
|
366
|
+
proc.stdin.end();
|
|
367
|
+
};
|
|
368
|
+
|
|
284
369
|
const processLine = (line: string) => {
|
|
285
370
|
if (!line.trim()) return;
|
|
286
|
-
const
|
|
287
|
-
if (!
|
|
371
|
+
const record = parseJsonRecord(line);
|
|
372
|
+
if (!record) return;
|
|
373
|
+
|
|
374
|
+
if (record.type === "extension_ui_request") {
|
|
375
|
+
const request = record as RpcExtensionUIRequest;
|
|
376
|
+
if (!parentUI) {
|
|
377
|
+
if (
|
|
378
|
+
request.method === "select" ||
|
|
379
|
+
request.method === "confirm" ||
|
|
380
|
+
request.method === "input" ||
|
|
381
|
+
request.method === "editor"
|
|
382
|
+
) {
|
|
383
|
+
sendRpc({ type: "extension_ui_response", id: request.id, cancelled: true });
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
void forwardSubagentUIRequest(request, parentUI, signal)
|
|
388
|
+
.then((response) => {
|
|
389
|
+
if (response) sendRpc(response);
|
|
390
|
+
return;
|
|
391
|
+
})
|
|
392
|
+
.catch(() => {
|
|
393
|
+
sendRpc({ type: "extension_ui_response", id: request.id, cancelled: true });
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (record.type === "response") {
|
|
399
|
+
if (record.command === "prompt" && record.success === false) {
|
|
400
|
+
result.errorMessage =
|
|
401
|
+
typeof record.error === "string" ? record.error : "Subagent prompt was rejected";
|
|
402
|
+
result.stopReason = "error";
|
|
403
|
+
requestShutdown();
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const event = record as AgentSessionEvent;
|
|
288
409
|
|
|
289
410
|
switch (event.type) {
|
|
290
411
|
case "message_update": {
|
|
@@ -323,6 +444,10 @@ export async function runAgent(
|
|
|
323
444
|
|
|
324
445
|
break;
|
|
325
446
|
}
|
|
447
|
+
case "agent_settled": {
|
|
448
|
+
requestShutdown();
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
326
451
|
// No default
|
|
327
452
|
}
|
|
328
453
|
};
|
|
@@ -338,6 +463,12 @@ export async function runAgent(
|
|
|
338
463
|
result.stderr += data.toString();
|
|
339
464
|
});
|
|
340
465
|
|
|
466
|
+
proc.stdin.on("error", (error) => {
|
|
467
|
+
if (!requestedShutdown) result.stderr += error.message;
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
sendRpc({ type: "prompt", message: `Task: ${task}` });
|
|
471
|
+
|
|
341
472
|
const exitCode = await new Promise<number>((resolve) => {
|
|
342
473
|
proc.on("close", (code) => {
|
|
343
474
|
if (buffer.trim()) processLine(buffer);
|
|
@@ -372,12 +503,10 @@ export async function runAgent(
|
|
|
372
503
|
}
|
|
373
504
|
|
|
374
505
|
/**
|
|
375
|
-
* Parse one line of the subagent's
|
|
376
|
-
*
|
|
377
|
-
* rejected. The cast here is the single trust boundary: downstream branches
|
|
378
|
-
* are fully type-narrowed via the `AgentSessionEvent` discriminated union.
|
|
506
|
+
* Parse one line of the subagent's RPC stream into a JSON object. Non-JSON
|
|
507
|
+
* lines and records without a type discriminator are rejected.
|
|
379
508
|
*/
|
|
380
|
-
function
|
|
509
|
+
function parseJsonRecord(line: string): Record<string, unknown> | null {
|
|
381
510
|
let raw: unknown;
|
|
382
511
|
try {
|
|
383
512
|
raw = JSON.parse(line);
|
|
@@ -386,7 +515,7 @@ function parseJsonEvent(line: string): AgentSessionEvent | null {
|
|
|
386
515
|
}
|
|
387
516
|
if (typeof raw !== "object" || raw === null) return null;
|
|
388
517
|
if (typeof (raw as Record<string, unknown>).type !== "string") return null;
|
|
389
|
-
return raw as
|
|
518
|
+
return raw as Record<string, unknown>;
|
|
390
519
|
}
|
|
391
520
|
|
|
392
521
|
/** Session entry customType used to mark the injected subagent list. */
|
|
@@ -395,7 +524,7 @@ export function formatAgentListSection(agents: AgentConfig[]): string {
|
|
|
395
524
|
return [
|
|
396
525
|
"## Available subagents",
|
|
397
526
|
"",
|
|
398
|
-
"You can delegate tasks to the following subagent types by calling the `
|
|
527
|
+
"You can delegate tasks to the following subagent types by calling the `spawn-agent` tool with their name in the `agent` parameter:",
|
|
399
528
|
"",
|
|
400
529
|
...lines,
|
|
401
530
|
].join("\n");
|
|
@@ -460,7 +589,14 @@ export default function spawnAgent(pi: ExtensionAPI) {
|
|
|
460
589
|
};
|
|
461
590
|
}
|
|
462
591
|
|
|
463
|
-
const result = await runAgent(
|
|
592
|
+
const result = await runAgent(
|
|
593
|
+
agent,
|
|
594
|
+
params.task,
|
|
595
|
+
ctx.cwd,
|
|
596
|
+
signal,
|
|
597
|
+
onUpdate,
|
|
598
|
+
ctx.hasUI ? ctx.ui : undefined,
|
|
599
|
+
);
|
|
464
600
|
|
|
465
601
|
const isError =
|
|
466
602
|
result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
package/src/workspace-guard.ts
CHANGED
|
@@ -27,13 +27,6 @@ import { normalizeForEdit, replace } from "./opencode-edit-engine.js";
|
|
|
27
27
|
const WRITE_TOOLS = new Set(["write", "edit"]);
|
|
28
28
|
const ALWAYS_ALLOW = ["/tmp"];
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* pi-subagents sets PI_SUBAGENT_CHILD=1 in every spawned child session.
|
|
32
|
-
* Subagents are headless, so there is no approval path: writes outside the
|
|
33
|
-
* workspace are rejected outright instead of asking for approval.
|
|
34
|
-
*/
|
|
35
|
-
const isSubagentChild = process.env.PI_SUBAGENT_CHILD === "1";
|
|
36
|
-
|
|
37
30
|
/** Maximum lines of the diff preview shown in the approval dialog. */
|
|
38
31
|
const MAX_PREVIEW_LINES = 100;
|
|
39
32
|
|
|
@@ -187,19 +180,11 @@ export interface WriteGuardDecision {
|
|
|
187
180
|
|
|
188
181
|
/**
|
|
189
182
|
* Decide how to handle a write outside the workspace.
|
|
190
|
-
* Subagent sessions (and any other headless session) have no approval path,
|
|
191
|
-
* so the write is rejected outright.
|
|
192
183
|
*/
|
|
193
184
|
export function decideOutsideWorkspaceWrite(
|
|
194
185
|
rawPath: string,
|
|
195
|
-
opts: { hasUI: boolean
|
|
186
|
+
opts: { hasUI: boolean },
|
|
196
187
|
): WriteGuardDecision {
|
|
197
|
-
if (opts.isSubagentChild) {
|
|
198
|
-
return {
|
|
199
|
-
block: true,
|
|
200
|
-
reason: `Path "${rawPath}" is outside the subagent workspace. Writes outside the workspace are rejected in subagent sessions; ask the parent session to apply this change.`,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
188
|
if (!opts.hasUI) {
|
|
204
189
|
return {
|
|
205
190
|
block: true,
|
|
@@ -213,15 +198,13 @@ export default function workspaceGuard(pi: ExtensionAPI) {
|
|
|
213
198
|
pi.on("before_agent_start", (event, ctx) => {
|
|
214
199
|
const currentCwd = ctx.cwd;
|
|
215
200
|
return {
|
|
216
|
-
systemPrompt:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
`write and edit to paths inside the workspace "${currentCwd}" or /tmp are auto-allowed. ` +
|
|
224
|
-
`Paths outside require user approval before execution.`,
|
|
201
|
+
systemPrompt:
|
|
202
|
+
event.systemPrompt +
|
|
203
|
+
`\nWorkspace write protection is active. ` +
|
|
204
|
+
`write and edit to paths inside the workspace "${currentCwd}" or /tmp are auto-allowed. ` +
|
|
205
|
+
(ctx.hasUI
|
|
206
|
+
? `Paths outside require user approval before execution.`
|
|
207
|
+
: `Writes outside the workspace are rejected because no UI is available for approval.`),
|
|
225
208
|
};
|
|
226
209
|
});
|
|
227
210
|
|
|
@@ -237,7 +220,6 @@ export default function workspaceGuard(pi: ExtensionAPI) {
|
|
|
237
220
|
|
|
238
221
|
const decision = decideOutsideWorkspaceWrite(rawPath, {
|
|
239
222
|
hasUI: ctx.hasUI,
|
|
240
|
-
isSubagentChild,
|
|
241
223
|
});
|
|
242
224
|
if (decision.block) return decision;
|
|
243
225
|
|