@trim21/personal-pi-extensions 0.0.183 → 0.0.186
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 +18 -5
- package/src/gh-readonly.ts +117 -11
package/package.json
CHANGED
package/src/bwrap/index.ts
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
*
|
|
12
12
|
* ## Modes
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Four modes, switchable at runtime:
|
|
15
15
|
*
|
|
16
16
|
* allow-all No sandbox. Network allowed. All commands run natively.
|
|
17
17
|
* workspace-write Sandbox enabled, network blocked. Project dir + /tmp writable.
|
|
18
18
|
* Model can request full access via bash tool parameter.
|
|
19
|
+
* allow-net Sandbox enabled, network allowed. Project dir + /tmp writable.
|
|
19
20
|
* readonly Sandbox enabled, network blocked, no writable paths.
|
|
20
21
|
*
|
|
21
22
|
* ## Escalation
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
* /bwrap Show current mode and paths
|
|
46
47
|
* /bwrap-allow-all Full access, sandbox off
|
|
47
48
|
* /bwrap-workspace-write Sandbox on, workspace writable
|
|
49
|
+
* /bwrap-allow-net Sandbox on, network on, workspace writable
|
|
48
50
|
* /bwrap-readonly Sandbox on, no writes
|
|
49
51
|
*
|
|
50
52
|
* Usage:
|
|
@@ -73,8 +75,9 @@ Three sandbox modes exist:
|
|
|
73
75
|
- allow-all: sandbox off, network on, full access
|
|
74
76
|
- readonly: sandbox on, network off, nothing writable
|
|
75
77
|
- workspace-write: sandbox on, network off, only workspace and /tmp writable
|
|
78
|
+
- allow-net: sandbox on, network on, only workspace and /tmp writable
|
|
76
79
|
|
|
77
|
-
In workspace-write and readonly modes, the bash tool has a
|
|
80
|
+
In workspace-write, allow-net and readonly modes, the bash tool has a
|
|
78
81
|
\`request_full_access\` boolean parameter.
|
|
79
82
|
Set it to true to request execution outside the sandbox.
|
|
80
83
|
The user must approve.
|
|
@@ -92,7 +95,8 @@ require request_full_access: true.
|
|
|
92
95
|
|
|
93
96
|
the \`request_full_access\` is only needed for:
|
|
94
97
|
- Writing to paths outside the configured writable directories,
|
|
95
|
-
- Operations requiring network access
|
|
98
|
+
- Operations requiring network access when the current mode blocks it
|
|
99
|
+
(workspace-write or readonly; allow-all and allow-net already have network).
|
|
96
100
|
Writing inside the workspace or /tmp, or reading any file, does not require escalation,
|
|
97
101
|
for example, the simple \`ls\`, \`cat\`, \`find\` or \`grep\` and git command that only read from .git directory but not change .git directory and other read only commands.
|
|
98
102
|
**If the command is readonly operator, do not use \`request_full_access\`**
|
|
@@ -146,7 +150,7 @@ function findBwrap(override?: string): string {
|
|
|
146
150
|
return override ?? bwrapPath;
|
|
147
151
|
}
|
|
148
152
|
|
|
149
|
-
type BwrapMode = "allow-all" | "workspace-write" | "readonly";
|
|
153
|
+
type BwrapMode = "allow-all" | "workspace-write" | "allow-net" | "readonly";
|
|
150
154
|
|
|
151
155
|
export interface BwrapConfig {
|
|
152
156
|
mode: BwrapMode;
|
|
@@ -168,7 +172,7 @@ export interface ResolvedBwrap {
|
|
|
168
172
|
extraArgs: string[];
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
175
|
+
export function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
172
176
|
const base = {
|
|
173
177
|
mode: config.mode,
|
|
174
178
|
bwrapPath: config.bwrapPath,
|
|
@@ -184,6 +188,9 @@ function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
|
184
188
|
case "workspace-write": {
|
|
185
189
|
return { ...base, bwrapEnabled: true, network: false };
|
|
186
190
|
}
|
|
191
|
+
case "allow-net": {
|
|
192
|
+
return { ...base, bwrapEnabled: true, network: true };
|
|
193
|
+
}
|
|
187
194
|
case "readonly": {
|
|
188
195
|
return { ...base, bwrapEnabled: true, network: false, writablePaths: [] };
|
|
189
196
|
}
|
|
@@ -508,6 +515,7 @@ function notifyMode(
|
|
|
508
515
|
const labels: Record<BwrapMode, string> = {
|
|
509
516
|
"allow-all": "allow-all: sandbox off, network on",
|
|
510
517
|
"workspace-write": "workspace-write: sandbox on, network off",
|
|
518
|
+
"allow-net": "allow-net: sandbox on, network on, workspace writable",
|
|
511
519
|
readonly: "readonly: sandbox on, network off, read-only fs",
|
|
512
520
|
};
|
|
513
521
|
ctx.ui.notify(labels[mode], "info");
|
|
@@ -768,6 +776,11 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
768
776
|
handler: (_args, ctx) => Promise.resolve(switchMode("workspace-write", ctx)),
|
|
769
777
|
});
|
|
770
778
|
|
|
779
|
+
pi.registerCommand("bwrap-allow-net", {
|
|
780
|
+
description: "Sandbox on, network on, workspace writable",
|
|
781
|
+
handler: (_args, ctx) => Promise.resolve(switchMode("allow-net", ctx)),
|
|
782
|
+
});
|
|
783
|
+
|
|
771
784
|
pi.registerCommand("bwrap-readonly", {
|
|
772
785
|
description: "Sandbox on, network off, no writes",
|
|
773
786
|
handler: (_args, ctx) => Promise.resolve(switchMode("readonly", ctx)),
|
package/src/gh-readonly.ts
CHANGED
|
@@ -206,11 +206,22 @@ const jobRunSchema = Type.Object({
|
|
|
206
206
|
name: Type.String(),
|
|
207
207
|
status: Type.String(),
|
|
208
208
|
conclusion: Type.Union([Type.String(), Type.Null()]),
|
|
209
|
+
html_url: Type.Optional(Type.String()),
|
|
209
210
|
steps: Type.Array(stepSchema),
|
|
210
211
|
});
|
|
211
212
|
|
|
212
213
|
const jobsResponseSchema = Type.Object({ jobs: Type.Array(jobRunSchema) });
|
|
213
214
|
|
|
215
|
+
const prHeadSchema = Type.Object({ headRefOid: Type.String() });
|
|
216
|
+
|
|
217
|
+
const workflowRunSchema = Type.Object({
|
|
218
|
+
id: Type.Number(),
|
|
219
|
+
name: Type.String(),
|
|
220
|
+
html_url: Type.String(),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const workflowRunsSchema = Type.Object({ workflow_runs: Type.Array(workflowRunSchema) });
|
|
224
|
+
|
|
214
225
|
function truncate(
|
|
215
226
|
text: string,
|
|
216
227
|
maxLines = 2000,
|
|
@@ -401,6 +412,15 @@ async function resolveRepo(
|
|
|
401
412
|
return nameWithOwner;
|
|
402
413
|
}
|
|
403
414
|
|
|
415
|
+
/** Job conclusions that count as "did not succeed" for CI result reporting. */
|
|
416
|
+
const FAILED_JOB_CONCLUSIONS = new Set([
|
|
417
|
+
"failure",
|
|
418
|
+
"timed_out",
|
|
419
|
+
"action_required",
|
|
420
|
+
"startup_failure",
|
|
421
|
+
"cancelled",
|
|
422
|
+
]);
|
|
423
|
+
|
|
404
424
|
export function statusIcon(conclusion: string | null): string {
|
|
405
425
|
switch (conclusion) {
|
|
406
426
|
case "success": {
|
|
@@ -1311,29 +1331,115 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1311
1331
|
const args = ["pr", "checks", String(number), ...repoArgs(repo), "--watch"];
|
|
1312
1332
|
if (fail_fast) args.push("--fail-fast");
|
|
1313
1333
|
|
|
1334
|
+
// gh 的退出码语义不可靠:checks 失败时返回 exit 1(SilentError),挂起时
|
|
1335
|
+
// 返回 exit 8(PendingError),且失败详情只在非结构化的 stdout 表格里。
|
|
1336
|
+
// 因此 watch 退出后直接用 Actions API 抓取该 PR head 提交关联的所有
|
|
1337
|
+
// workflow job,以 job 的真实 conclusion 为准判断成功/失败。
|
|
1314
1338
|
const result = await runGh(args, { cwd: ctx.cwd, signal, timeout: 600_000 });
|
|
1339
|
+
if (result.killed) {
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
result.reason === "timeout"
|
|
1342
|
+
? "gh pr checks --watch timed out after 10 minutes"
|
|
1343
|
+
: "gh pr checks --watch was aborted",
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1315
1346
|
|
|
1316
|
-
const
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1347
|
+
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1348
|
+
const prOut = await ghExec(
|
|
1349
|
+
["pr", "view", String(number), "--repo", effectiveRepo, "--json", "headRefOid"],
|
|
1350
|
+
{ cwd: ctx.cwd, signal, input: params },
|
|
1351
|
+
);
|
|
1352
|
+
const { headRefOid } = Value.Parse(prHeadSchema, JSON.parse(prOut));
|
|
1353
|
+
|
|
1354
|
+
onUpdate?.({
|
|
1355
|
+
content: [{ type: "text", text: `Fetching workflow jobs for PR #${number}...` }],
|
|
1356
|
+
details: {},
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
const runsOut = await ghExec(
|
|
1360
|
+
["api", `/repos/${effectiveRepo}/actions/runs?head_sha=${headRefOid}&per_page=100`],
|
|
1361
|
+
{ cwd: ctx.cwd, signal, input: params },
|
|
1362
|
+
);
|
|
1363
|
+
const { workflow_runs } = Value.Parse(workflowRunsSchema, JSON.parse(runsOut));
|
|
1364
|
+
|
|
1365
|
+
const failedJobs: {
|
|
1366
|
+
runId: number;
|
|
1367
|
+
runName: string;
|
|
1368
|
+
runUrl: string;
|
|
1369
|
+
jobId: number;
|
|
1370
|
+
jobName: string;
|
|
1371
|
+
conclusion: string;
|
|
1372
|
+
jobUrl?: string;
|
|
1373
|
+
}[] = [];
|
|
1374
|
+
let totalJobs = 0;
|
|
1375
|
+
|
|
1376
|
+
for (const run of workflow_runs) {
|
|
1377
|
+
const jobsOut = await ghExec(
|
|
1378
|
+
["api", `/repos/${effectiveRepo}/actions/runs/${run.id}/jobs?per_page=100`],
|
|
1379
|
+
{ cwd: ctx.cwd, signal, input: params },
|
|
1380
|
+
);
|
|
1381
|
+
const { jobs } = Value.Parse(jobsResponseSchema, JSON.parse(jobsOut));
|
|
1382
|
+
totalJobs += jobs.length;
|
|
1383
|
+
|
|
1384
|
+
for (const job of jobs) {
|
|
1385
|
+
if (!job.conclusion || FAILED_JOB_CONCLUSIONS.has(job.conclusion)) {
|
|
1386
|
+
failedJobs.push({
|
|
1387
|
+
runId: run.id,
|
|
1388
|
+
runName: run.name,
|
|
1389
|
+
runUrl: run.html_url,
|
|
1390
|
+
jobId: job.id,
|
|
1391
|
+
jobName: job.name,
|
|
1392
|
+
conclusion: job.conclusion ?? "in_progress",
|
|
1393
|
+
jobUrl: job.html_url,
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1319
1398
|
|
|
1320
|
-
|
|
1321
|
-
if (exitCode === 2) {
|
|
1399
|
+
if (totalJobs === 0) {
|
|
1322
1400
|
return {
|
|
1323
1401
|
content: [
|
|
1324
|
-
{
|
|
1402
|
+
{
|
|
1403
|
+
type: "text",
|
|
1404
|
+
text: `## PR #${number} CI Checks\n\nNo workflow runs found for head commit ${headRefOid.slice(0, 7)}.`,
|
|
1405
|
+
},
|
|
1325
1406
|
],
|
|
1326
|
-
details: { status: "
|
|
1407
|
+
details: { status: "no-jobs", totalJobs: 0, input: params },
|
|
1327
1408
|
};
|
|
1328
1409
|
}
|
|
1329
1410
|
|
|
1330
|
-
if (
|
|
1331
|
-
|
|
1411
|
+
if (failedJobs.length > 0) {
|
|
1412
|
+
const lines = failedJobs.map(
|
|
1413
|
+
(j) =>
|
|
1414
|
+
`- ${statusIcon(j.conclusion)} **${j.jobName}** (${j.conclusion}) — [job #${j.jobId}](${j.jobUrl ?? j.runUrl})\n` +
|
|
1415
|
+
` - workflow: [${j.runName} (#${j.runId})](${j.runUrl})`,
|
|
1416
|
+
);
|
|
1417
|
+
return {
|
|
1418
|
+
content: [
|
|
1419
|
+
{
|
|
1420
|
+
type: "text",
|
|
1421
|
+
text:
|
|
1422
|
+
`## PR #${number} CI Checks - FAILED\n\n` +
|
|
1423
|
+
`${failedJobs.length} of ${totalJobs} job(s) did not succeed:\n\n${lines.join("\n")}`,
|
|
1424
|
+
},
|
|
1425
|
+
],
|
|
1426
|
+
details: {
|
|
1427
|
+
status: "failure",
|
|
1428
|
+
totalJobs,
|
|
1429
|
+
failedJobs,
|
|
1430
|
+
input: params,
|
|
1431
|
+
},
|
|
1432
|
+
};
|
|
1332
1433
|
}
|
|
1333
1434
|
|
|
1334
1435
|
return {
|
|
1335
|
-
content: [
|
|
1336
|
-
|
|
1436
|
+
content: [
|
|
1437
|
+
{
|
|
1438
|
+
type: "text",
|
|
1439
|
+
text: `## PR #${number} CI Checks - PASSED\n\nAll ${totalJobs} job(s) succeeded.`,
|
|
1440
|
+
},
|
|
1441
|
+
],
|
|
1442
|
+
details: { status: "success", totalJobs, input: params },
|
|
1337
1443
|
};
|
|
1338
1444
|
},
|
|
1339
1445
|
});
|