@trim21/personal-pi-extensions 0.0.168 → 0.0.170

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.168",
3
+ "version": "0.0.170",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -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 SUBAGENT_SANDBOX_PROMPT = `
108
- ## Command Execution (subagent sandbox)
107
+ const HEADLESS_SANDBOX_PROMPT = `
108
+ ## Command Execution (headless sandbox)
109
109
 
110
- You are running inside a read-only sandbox in a subagent session.
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. If a change outside the
116
- workspace is needed, ask the parent session to apply it.
117
- - If a command truly requires network or system-level access, stop and ask
118
- the parent session to run it, where the user can approve it.
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
- * Subagent sessions are forced read-only regardless of config: no writable
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 resolveSubagentBwrap(config: BwrapConfig): ResolvedBwrap {
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
- * - Subagent sessions are always denied: bash is read-only there and there is
528
- * no approval path (headless), so escalation would silently bypass the sandbox.
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
- // Subagents ignore config and runtime switches: bash is always read-only.
588
- if (isSubagentChild) {
589
- return resolveSubagentBwrap(loadConfig(localCwd));
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 r = getResolved();
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: ctx?.hasUI ?? false, isSubagentChild });
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
- // Subagent sessions are always sandboxed read-only; --no-bwrap must not
676
- // disable that (the flag is only honored in the parent session).
677
- if (noBwrap && !isSubagentChild) {
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 = isSubagentChild ? resolveSubagentBwrap(config) : resolveBwrap(config);
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: isSubagentChild
727
- ? event.systemPrompt + "\n\n" + SUBAGENT_SANDBOX_PROMPT
728
- : event.systemPrompt + "\n\n" + SANDBOX_PROMPT + `\n\nCurrent mode: **${r.mode}**\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}`));
@@ -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; isSubagentChild: 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: isSubagentChild
217
- ? event.systemPrompt +
218
- `\nSubagent write protection is active. ` +
219
- `write and edit are allowed inside "${currentCwd}" and /tmp. ` +
220
- `Writes outside the workspace are rejected without approval — ask the parent session to make such changes.`
221
- : event.systemPrompt +
222
- `\nWorkspace write protection is active. ` +
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