@trim21/personal-pi-extensions 0.0.240 → 0.0.241
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/core.ts +7 -1
- package/src/bwrap/runtime.ts +31 -5
- package/src/gh-readonly.ts +31 -1
package/package.json
CHANGED
package/src/bwrap/core.ts
CHANGED
|
@@ -167,7 +167,13 @@ function findDefaultBwrap(): string {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
export function findBwrap(override?: string): string {
|
|
170
|
-
|
|
170
|
+
if (override) {
|
|
171
|
+
if (!existsSync(override)) {
|
|
172
|
+
throw new Error(`bwrap not found at configured path: ${override}`);
|
|
173
|
+
}
|
|
174
|
+
return override;
|
|
175
|
+
}
|
|
176
|
+
return findDefaultBwrap();
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
export function buildBwrapArgs(resolved: ResolvedBwrap, cwd: string): string[] {
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -73,6 +73,7 @@ function notifyMode(
|
|
|
73
73
|
export class BwrapRuntime {
|
|
74
74
|
private resolved: ResolvedBwrap | undefined;
|
|
75
75
|
private sandboxDisabled = false;
|
|
76
|
+
private bwrapUnavailable = false;
|
|
76
77
|
|
|
77
78
|
setup(pi: ExtensionAPI): void {
|
|
78
79
|
pi.registerFlag("no-bwrap", {
|
|
@@ -84,13 +85,18 @@ export class BwrapRuntime {
|
|
|
84
85
|
pi.on("session_start", (_event, ctx) => {
|
|
85
86
|
this.sandboxDisabled = pi.getFlag("no-bwrap") === true && ctx.hasUI;
|
|
86
87
|
this.resolved = undefined;
|
|
88
|
+
this.bwrapUnavailable = false;
|
|
87
89
|
const runtime = this.resolve(ctx);
|
|
88
90
|
if (runtime.bwrapEnabled) {
|
|
89
91
|
try {
|
|
90
92
|
findBwrap(runtime.bwrapPath);
|
|
91
93
|
} catch (error) {
|
|
92
|
-
|
|
94
|
+
// Fail closed: a missing bwrap binary must not silently degrade to an
|
|
95
|
+
// unsandboxed allow-all session. Commands are refused until the user
|
|
96
|
+
// explicitly opts out via --no-bwrap or the bwrap-allow-all command.
|
|
97
|
+
this.bwrapUnavailable = true;
|
|
93
98
|
this.resolved = undefined;
|
|
99
|
+
ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("error", "bwrap: unavailable"));
|
|
94
100
|
ctx.ui.notify(error instanceof Error ? error.message : "bwrap not found", "error");
|
|
95
101
|
return;
|
|
96
102
|
}
|
|
@@ -110,10 +116,16 @@ export class BwrapRuntime {
|
|
|
110
116
|
|
|
111
117
|
pi.on("before_agent_start", (event, ctx) => {
|
|
112
118
|
const runtime = this.resolve(ctx);
|
|
113
|
-
const
|
|
114
|
-
?
|
|
115
|
-
: "
|
|
116
|
-
|
|
119
|
+
const modeText = ctx.hasUI
|
|
120
|
+
? `Current bwrap mode: **${runtime.mode}**. The bwrap runtime selects sandboxing and, when requested, user approval for unsandboxed execution.`
|
|
121
|
+
: "This headless session is forced into bwrap readonly mode. Unsandboxed execution cannot be approved.";
|
|
122
|
+
const unavailableText = this.bwrapUnavailable
|
|
123
|
+
? " bwrap is unavailable (binary not found): bash commands are refused unless the user explicitly approves unsandboxed execution."
|
|
124
|
+
: "";
|
|
125
|
+
return {
|
|
126
|
+
systemPrompt:
|
|
127
|
+
event.systemPrompt + `\n\n## Command Execution\n${modeText}${unavailableText}\n`,
|
|
128
|
+
};
|
|
117
129
|
});
|
|
118
130
|
|
|
119
131
|
this.registerCommands(pi);
|
|
@@ -128,10 +140,17 @@ export class BwrapRuntime {
|
|
|
128
140
|
reset(): void {
|
|
129
141
|
this.resolved = undefined;
|
|
130
142
|
this.sandboxDisabled = false;
|
|
143
|
+
this.bwrapUnavailable = false;
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
async execute(request: BwrapExecutionRequest) {
|
|
134
147
|
const runtime = this.resolve(request.ctx);
|
|
148
|
+
if (this.bwrapUnavailable && runtime.bwrapEnabled && request.requestFullAccess !== true) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"bwrap (bubblewrap) not found; refusing to execute commands without sandboxing. " +
|
|
151
|
+
"Install bubblewrap and restart the session, or pass --no-bwrap to disable the sandbox explicitly.",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
135
154
|
if (request.requestFullAccess === true && runtime.bwrapEnabled) {
|
|
136
155
|
await this.approveFullAccess(request.ctx, request.command, request.requestFullAccessReason);
|
|
137
156
|
}
|
|
@@ -229,6 +248,13 @@ export class BwrapRuntime {
|
|
|
229
248
|
handler: (args, ctx) =>
|
|
230
249
|
this.runCommand(pi, specs.bwrap, args, ctx, (commandCtx) => {
|
|
231
250
|
const runtime = this.resolve(commandCtx);
|
|
251
|
+
if (this.bwrapUnavailable) {
|
|
252
|
+
commandCtx.ui.notify(
|
|
253
|
+
"bwrap is unavailable: binary not found. Commands are refused unless sandboxing is explicitly disabled.",
|
|
254
|
+
"error",
|
|
255
|
+
);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
232
258
|
if (!runtime.bwrapEnabled) {
|
|
233
259
|
commandCtx.ui.notify(`bwrap disabled (mode: ${runtime.mode})`, "info");
|
|
234
260
|
return;
|
package/src/gh-readonly.ts
CHANGED
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { spawn } from "node:child_process";
|
|
32
|
+
import { existsSync } from "node:fs";
|
|
32
33
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
33
34
|
import { homedir } from "node:os";
|
|
34
|
-
import { dirname, join, resolve } from "node:path";
|
|
35
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
35
36
|
|
|
36
37
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
37
38
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
@@ -52,6 +53,22 @@ interface GhResult {
|
|
|
52
53
|
|
|
53
54
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Check whether the `gh` CLI is on the system, scanning PATH like
|
|
58
|
+
* `findDefaultBwrap`. The extension registers no tools when `gh` is missing, so
|
|
59
|
+
* the model never sees GitHub tools that would fail on every call.
|
|
60
|
+
*/
|
|
61
|
+
export function isGhAvailable(): boolean {
|
|
62
|
+
const pathEnv = process.env.PATH ?? "";
|
|
63
|
+
for (const directory of pathEnv.split(delimiter)) {
|
|
64
|
+
if (existsSync(join(directory, "gh"))) return true;
|
|
65
|
+
}
|
|
66
|
+
for (const candidate of ["/usr/bin/gh", "/usr/local/bin/gh", "/run/current-system/sw/bin/gh"]) {
|
|
67
|
+
if (existsSync(candidate)) return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
export function runGh(
|
|
56
73
|
args: string[],
|
|
57
74
|
ctx: { cwd?: string; signal?: AbortSignal; timeout?: number },
|
|
@@ -1082,6 +1099,19 @@ export async function writeLogFile(
|
|
|
1082
1099
|
// ── tools ────────────────────────────────────────────────────────────────────
|
|
1083
1100
|
|
|
1084
1101
|
export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
1102
|
+
// Fail fast: the `gh` CLI is the only backend for these tools. Without it the
|
|
1103
|
+
// extension registers nothing and reports the problem at session start, so
|
|
1104
|
+
// the user gets one clear error instead of a dozen failing tool calls.
|
|
1105
|
+
if (!isGhAvailable()) {
|
|
1106
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1107
|
+
ctx.ui.notify(
|
|
1108
|
+
"gh CLI not found in PATH: GitHub read-only tools are disabled. Install GitHub CLI (https://cli.github.com/) and reload the session.",
|
|
1109
|
+
"error",
|
|
1110
|
+
);
|
|
1111
|
+
});
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1085
1115
|
// ── read-github-issue ──────────────────────────────────────────────────────
|
|
1086
1116
|
pi.registerTool({
|
|
1087
1117
|
name: "read-github-issue",
|