@diegopetrucci/pi-extensions 0.1.73 → 0.1.76
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/LICENSE +2 -0
- package/README.md +9 -4
- package/extensions/agent-workflow-audit/index.ts +1398 -1009
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +3 -1
- package/extensions/annotate-git-diff/clipboard.ts +109 -100
- package/extensions/annotate-git-diff/git.ts +853 -767
- package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
- package/extensions/annotate-git-diff/index.ts +712 -671
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +45 -45
- package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
- package/extensions/annotate-git-diff/review-server.ts +337 -311
- package/extensions/annotate-git-diff/types.ts +132 -132
- package/extensions/annotate-git-diff/ui.ts +27 -27
- package/extensions/annotate-git-diff/watch.ts +74 -74
- package/extensions/annotate-git-diff/web/app.js +1947 -1884
- package/extensions/annotate-git-diff/web/index.html +5 -6
- package/extensions/annotate-last-message/README.md +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
- package/extensions/annotate-last-message/index.ts +210 -206
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +87 -66
- package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
- package/extensions/annotate-last-message/session.ts +94 -89
- package/extensions/annotate-last-message/theme.ts +199 -193
- package/extensions/annotate-last-message/types.ts +23 -23
- package/extensions/annotate-last-message/ui.ts +27 -25
- package/extensions/annotate-last-message/web/app.js +4 -7
- package/extensions/annotate-last-message/web/md-renderer.js +1 -1
- package/extensions/brrr/README.md +1 -1
- package/extensions/brrr/index.ts +213 -191
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +5 -79
- package/extensions/claude-fast/index.ts +31 -289
- package/extensions/claude-fast/package.json +2 -3
- package/extensions/code-reviewer/README.md +1 -1
- package/extensions/code-reviewer/index.ts +1598 -1350
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +55 -51
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/index.ts +312 -304
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +1854 -1735
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +41 -41
- package/extensions/dynamic-context-pruning/README.md +6 -38
- package/extensions/dynamic-context-pruning/index.ts +2124 -1980
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +71 -21
- package/extensions/fast/index.ts +513 -313
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -1
- package/extensions/git-footer/index.ts +458 -411
- package/extensions/git-footer/package.json +2 -3
- package/extensions/gnosis/index.ts +180 -130
- package/extensions/gnosis/package.json +1 -1
- package/extensions/inline-bash/index.ts +88 -88
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/index.ts +1669 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/index.ts +994 -1012
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/notify/index.ts +222 -230
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +5 -73
- package/extensions/openai-fast/index.ts +32 -272
- package/extensions/openai-fast/package.json +2 -3
- package/extensions/oracle/index.ts +1731 -1625
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/index.ts +658 -608
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/permission-gate/powershell-safety.ts +840 -793
- package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
- package/extensions/project-mcp-json/README.md +199 -0
- package/extensions/project-mcp-json/config.ts +564 -0
- package/extensions/project-mcp-json/index.ts +119 -0
- package/extensions/project-mcp-json/package.json +38 -0
- package/extensions/quiet-tools/index.ts +282 -264
- package/extensions/review/README.md +2 -2
- package/extensions/review/index.ts +1745 -1872
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +284 -265
- package/extensions/triage-comments/README.md +2 -2
- package/extensions/triage-comments/index.ts +2294 -1882
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +6 -3
- package/assets/social-preview.png +0 -0
- package/extensions/claude-fast/claude-fast.example.json +0 -4
- package/extensions/openai-fast/openai-fast.example.json +0 -4
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AgentSession,
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
ExtensionCommandContext,
|
|
8
|
+
ExtensionFactory,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
10
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
DefaultResourceLoader,
|
|
12
|
+
SessionManager,
|
|
13
|
+
SettingsManager,
|
|
14
|
+
createAgentSession,
|
|
15
|
+
getAgentDir,
|
|
16
|
+
getMarkdownTheme,
|
|
12
17
|
} from "@earendil-works/pi-coding-agent";
|
|
13
18
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
14
19
|
|
|
@@ -37,834 +42,1177 @@ type AuditStatus = "running" | "done" | "error" | "aborted";
|
|
|
37
42
|
type AuditMode = "execute" | "plan-only";
|
|
38
43
|
|
|
39
44
|
type ToolCall = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
args: unknown;
|
|
48
|
+
startedAt: number;
|
|
49
|
+
endedAt?: number;
|
|
50
|
+
isError?: boolean;
|
|
46
51
|
};
|
|
47
52
|
|
|
48
53
|
type AuditDetails = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
status: AuditStatus;
|
|
55
|
+
mode: AuditMode;
|
|
56
|
+
cwd: string;
|
|
57
|
+
focus?: string;
|
|
58
|
+
turns: number;
|
|
59
|
+
toolCalls: ToolCall[];
|
|
60
|
+
startedAt: number;
|
|
61
|
+
endedAt?: number;
|
|
62
|
+
error?: string;
|
|
63
|
+
initialGitStatus?: string;
|
|
64
|
+
finalGitStatus?: string;
|
|
65
|
+
reportLength?: number;
|
|
66
|
+
toolCallCount?: number;
|
|
62
67
|
};
|
|
63
68
|
|
|
64
69
|
type ParsedArgs = {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
help: boolean;
|
|
71
|
+
yes: boolean;
|
|
72
|
+
planOnly: boolean;
|
|
73
|
+
focus?: string;
|
|
74
|
+
error?: string;
|
|
70
75
|
};
|
|
71
76
|
|
|
72
77
|
type CreateAgentSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
|
|
73
78
|
|
|
74
|
-
function getModelRuntimeOption(ctx: {
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
function getModelRuntimeOption(ctx: {
|
|
80
|
+
modelRegistry?: unknown;
|
|
81
|
+
}): Pick<CreateAgentSessionOptions, "modelRuntime"> {
|
|
82
|
+
const modelRuntime = (
|
|
83
|
+
ctx.modelRegistry as { runtime?: CreateAgentSessionOptions["modelRuntime"] } | undefined
|
|
84
|
+
)?.runtime;
|
|
85
|
+
return modelRuntime ? { modelRuntime } : {};
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
function parseArgs(args: string): ParsedArgs {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
89
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
90
|
+
const focusParts: string[] = [];
|
|
91
|
+
const parsed: ParsedArgs = { help: false, yes: false, planOnly: false };
|
|
92
|
+
|
|
93
|
+
for (const part of parts) {
|
|
94
|
+
if (part === "--help" || part === "-h" || part === "help") {
|
|
95
|
+
parsed.help = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (part === "--yes" || part === "-y") {
|
|
99
|
+
parsed.yes = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (part === "--plan-only" || part === "--dry-run") {
|
|
103
|
+
parsed.planOnly = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (part.startsWith("-")) {
|
|
107
|
+
return { ...parsed, error: `Unknown /agent-workflow-audit option: ${part}\n${USAGE}` };
|
|
108
|
+
}
|
|
109
|
+
focusParts.push(part);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const focus = focusParts.join(" ").trim();
|
|
113
|
+
if (focus) parsed.focus = focus;
|
|
114
|
+
return parsed;
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
function notifyCommand(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
ctx: ExtensionCommandContext,
|
|
119
|
+
message: string,
|
|
120
|
+
type: "info" | "warning" | "error" = "info",
|
|
112
121
|
): void {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
if (ctx.hasUI) {
|
|
123
|
+
ctx.ui.notify(message, type);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
const writer = type === "error" ? console.error : console.log;
|
|
128
|
+
writer(message);
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
123
|
-
|
|
132
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
133
|
+
? (value as Record<string, unknown>)
|
|
134
|
+
: undefined;
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
function shorten(text: string, max: number): string {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
139
|
+
if (oneLine.length <= max) return oneLine;
|
|
140
|
+
return `${oneLine.slice(0, Math.max(1, max - 1))}…`;
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
function formatDuration(durationMs: number): string {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
if (durationMs < 1000) return `${durationMs}ms`;
|
|
145
|
+
if (durationMs < 60_000) return `${(durationMs / 1000).toFixed(1)}s`;
|
|
146
|
+
return `${(durationMs / 60_000).toFixed(1)}m`;
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
function messageContentToText(content: unknown): string {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
if (typeof content === "string") return content;
|
|
151
|
+
if (!Array.isArray(content)) return "";
|
|
152
|
+
const parts: string[] = [];
|
|
153
|
+
for (const part of content) {
|
|
154
|
+
if (part && typeof part === "object" && (part as { type?: string }).type === "text") {
|
|
155
|
+
const text = (part as { text?: unknown }).text;
|
|
156
|
+
if (typeof text === "string") parts.push(text);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return parts.join("\n").trim();
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
type AuditAssistantOutcome =
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
| { ok: true; answer: string; stopReason?: string }
|
|
164
|
+
| { ok: false; reason: string; stopReason?: string; errorMessage?: string };
|
|
154
165
|
|
|
155
166
|
function inspectFinalAssistant(messages: unknown[]): AuditAssistantOutcome {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
167
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
168
|
+
const message = messages[i] as {
|
|
169
|
+
role?: string;
|
|
170
|
+
content?: unknown;
|
|
171
|
+
stopReason?: unknown;
|
|
172
|
+
errorMessage?: unknown;
|
|
173
|
+
};
|
|
174
|
+
if (message?.role !== "assistant") continue;
|
|
175
|
+
const stopReason = typeof message.stopReason === "string" ? message.stopReason.trim() : "";
|
|
176
|
+
const errorMessage =
|
|
177
|
+
typeof message.errorMessage === "string" ? message.errorMessage.trim() : "";
|
|
178
|
+
const answer = messageContentToText(message.content);
|
|
179
|
+
if (stopReason === "error") {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
reason: `Agent Workflow Audit subagent error: ${errorMessage || "provider/model error"}`,
|
|
183
|
+
stopReason,
|
|
184
|
+
errorMessage,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (stopReason === "aborted") {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
reason: "Agent Workflow Audit subagent aborted before producing a usable report",
|
|
191
|
+
stopReason,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (answer) return { ok: true, answer, stopReason: stopReason || undefined };
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
reason: stopReason
|
|
198
|
+
? `Agent Workflow Audit subagent produced no final assistant text (stopReason: ${stopReason})`
|
|
199
|
+
: "Agent Workflow Audit subagent produced no final assistant text",
|
|
200
|
+
...(stopReason ? { stopReason } : {}),
|
|
201
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return { ok: false, reason: "Agent Workflow Audit subagent produced no assistant message" };
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
function isAbortLikeError(error: unknown): boolean {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
if (error && typeof error === "object" && (error as { name?: unknown }).name === "AbortError")
|
|
209
|
+
return true;
|
|
210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
+
return /aborted|cancelled|canceled/i.test(message);
|
|
190
212
|
}
|
|
191
213
|
|
|
192
214
|
function renderCollapsedReport(report: string, lineLimit = COLLAPSED_REPORT_LINES): string {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
215
|
+
const lines = report.trim().split("\n");
|
|
216
|
+
if (lines.length <= lineLimit) return lines.join("\n");
|
|
217
|
+
return [
|
|
218
|
+
...lines.slice(0, lineLimit),
|
|
219
|
+
`… (${lines.length - lineLimit} more lines; expand to view)`,
|
|
220
|
+
].join("\n");
|
|
196
221
|
}
|
|
197
222
|
|
|
198
223
|
function summarizeGitStatus(status: string | undefined): { lineCount: number; dirtyCount: number } {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
224
|
+
const lines =
|
|
225
|
+
status
|
|
226
|
+
?.split("\n")
|
|
227
|
+
.map((line) => line.trim())
|
|
228
|
+
.filter(Boolean) ?? [];
|
|
229
|
+
const dirty = lines.filter((line) => !line.startsWith("##"));
|
|
230
|
+
return { lineCount: lines.length, dirtyCount: dirty.length };
|
|
202
231
|
}
|
|
203
232
|
|
|
204
233
|
function appendRunBoundary(report: string, details: AuditDetails): string {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
234
|
+
const initial = summarizeGitStatus(details.initialGitStatus);
|
|
235
|
+
const final = summarizeGitStatus(details.finalGitStatus);
|
|
236
|
+
const statusChanged = (details.initialGitStatus ?? "") !== (details.finalGitStatus ?? "");
|
|
237
|
+
const duration = details.endedAt
|
|
238
|
+
? formatDuration(details.endedAt - details.startedAt)
|
|
239
|
+
: "unknown duration";
|
|
240
|
+
const lines = [
|
|
241
|
+
"---",
|
|
242
|
+
"## Audit run boundary",
|
|
243
|
+
`- Ran in an isolated in-memory Agent Workflow Audit subagent; intermediate tool transcript, raw command output, errors, retries, and search path were not added to the main session.`,
|
|
244
|
+
`- Child run: ${details.turns} turn(s), ${details.toolCalls.length} tool call(s), ${duration}.`,
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
if (details.initialGitStatus !== undefined || details.finalGitStatus !== undefined) {
|
|
248
|
+
lines.push(
|
|
249
|
+
`- Git status check: ${statusChanged ? "changed" : "unchanged"} (${initial.dirtyCount} dirty item(s) before, ${final.dirtyCount} dirty item(s) after).`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return `${report.trim()}\n\n${lines.join("\n")}`.trim();
|
|
223
254
|
}
|
|
224
255
|
|
|
225
|
-
async function gitStatusShort(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
256
|
+
async function gitStatusShort(
|
|
257
|
+
pi: ExtensionAPI,
|
|
258
|
+
cwd: string,
|
|
259
|
+
signal: AbortSignal | undefined,
|
|
260
|
+
): Promise<string | undefined> {
|
|
261
|
+
const result = await pi.exec("git", ["status", "--short", "--branch"], {
|
|
262
|
+
cwd,
|
|
263
|
+
signal,
|
|
264
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
265
|
+
});
|
|
266
|
+
if (result.killed || result.code !== 0) return undefined;
|
|
267
|
+
return result.stdout.trim();
|
|
233
268
|
}
|
|
234
269
|
|
|
235
270
|
function resolveToolPath(cwd: string, rawPath: unknown): string {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
271
|
+
const input = typeof rawPath === "string" && rawPath.trim() ? rawPath.trim() : ".";
|
|
272
|
+
const normalized = input.startsWith("@") ? input.slice(1) : input;
|
|
273
|
+
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
|
|
239
274
|
}
|
|
240
275
|
|
|
241
276
|
function isInside(parent: string, child: string): boolean {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
277
|
+
const parentResolved = path.resolve(parent);
|
|
278
|
+
const childResolved = path.resolve(child);
|
|
279
|
+
return (
|
|
280
|
+
childResolved === parentResolved || childResolved.startsWith(`${parentResolved}${path.sep}`)
|
|
281
|
+
);
|
|
245
282
|
}
|
|
246
283
|
|
|
247
|
-
async function assertToolPathInsideCwd(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
284
|
+
async function assertToolPathInsideCwd(
|
|
285
|
+
cwd: string,
|
|
286
|
+
rawPath: unknown,
|
|
287
|
+
toolName: string,
|
|
288
|
+
): Promise<string | undefined> {
|
|
289
|
+
if (rawPath !== undefined && typeof rawPath !== "string")
|
|
290
|
+
return `${toolName} path must be a string.`;
|
|
291
|
+
const root = await fs.realpath(cwd).catch(() => path.resolve(cwd));
|
|
292
|
+
const resolved = resolveToolPath(cwd, rawPath);
|
|
293
|
+
const realPath = await fs.realpath(resolved).catch(() => resolved);
|
|
294
|
+
if (!isInside(root, realPath))
|
|
295
|
+
return `${toolName} is limited to the audited checkout: ${realPath}`;
|
|
296
|
+
return undefined;
|
|
254
297
|
}
|
|
255
298
|
|
|
256
299
|
function getUnsafePatternReason(value: unknown, label: string): string | undefined {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
300
|
+
if (value === undefined) return undefined;
|
|
301
|
+
if (typeof value !== "string") return `${label} must be a string.`;
|
|
302
|
+
if (path.isAbsolute(value)) return `${label} must be relative to the audited checkout.`;
|
|
303
|
+
if (/(^|[/\\])\.\.(?:[/\\]|$)/.test(value))
|
|
304
|
+
return `${label} must not traverse outside the audited checkout.`;
|
|
305
|
+
return undefined;
|
|
262
306
|
}
|
|
263
307
|
|
|
264
308
|
type TokenizedSegment = { tokens: string[]; reason?: string };
|
|
265
309
|
|
|
266
310
|
const SAFE_DIRECT_COMMANDS = new Set([
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
311
|
+
"bun",
|
|
312
|
+
"cargo",
|
|
313
|
+
"dotnet",
|
|
314
|
+
"gh",
|
|
315
|
+
"git",
|
|
316
|
+
"go",
|
|
317
|
+
"gradle",
|
|
318
|
+
"gradlew",
|
|
319
|
+
"just",
|
|
320
|
+
"make",
|
|
321
|
+
"mvn",
|
|
322
|
+
"npm",
|
|
323
|
+
"pnpm",
|
|
324
|
+
"pwd",
|
|
325
|
+
"pytest",
|
|
326
|
+
"ruff",
|
|
327
|
+
"swift",
|
|
328
|
+
"task",
|
|
329
|
+
"tox",
|
|
330
|
+
"yarn",
|
|
287
331
|
]);
|
|
288
332
|
|
|
289
333
|
const READ_ONLY_GIT_SUBCOMMANDS = new Set([
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
334
|
+
"blame",
|
|
335
|
+
"branch",
|
|
336
|
+
"cat-file",
|
|
337
|
+
"describe",
|
|
338
|
+
"diff",
|
|
339
|
+
"for-each-ref",
|
|
340
|
+
"grep",
|
|
341
|
+
"log",
|
|
342
|
+
"ls-files",
|
|
343
|
+
"ls-tree",
|
|
344
|
+
"merge-base",
|
|
345
|
+
"name-rev",
|
|
346
|
+
"remote",
|
|
347
|
+
"rev-parse",
|
|
348
|
+
"shortlog",
|
|
349
|
+
"show",
|
|
350
|
+
"show-ref",
|
|
351
|
+
"status",
|
|
352
|
+
"whatchanged",
|
|
309
353
|
]);
|
|
310
354
|
|
|
311
355
|
const SAFE_GIT_GLOBAL_FLAGS = new Set(["--no-pager", "--no-optional-locks"]);
|
|
312
356
|
const SAFE_GIT_BRANCH_FLAGS = new Set([
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
357
|
+
"-a",
|
|
358
|
+
"--all",
|
|
359
|
+
"-r",
|
|
360
|
+
"--remotes",
|
|
361
|
+
"-v",
|
|
362
|
+
"-vv",
|
|
363
|
+
"--show-current",
|
|
364
|
+
"--list",
|
|
365
|
+
"--contains",
|
|
366
|
+
"--merged",
|
|
367
|
+
"--no-merged",
|
|
368
|
+
]);
|
|
369
|
+
const GH_GLOBAL_OPTIONS_WITH_VALUE = new Set([
|
|
370
|
+
"--repo",
|
|
371
|
+
"-R",
|
|
372
|
+
"--hostname",
|
|
373
|
+
"--jq",
|
|
374
|
+
"-q",
|
|
375
|
+
"--template",
|
|
324
376
|
]);
|
|
325
|
-
const GH_GLOBAL_OPTIONS_WITH_VALUE = new Set(["--repo", "-R", "--hostname", "--jq", "-q", "--template"]);
|
|
326
377
|
const GH_API_FIELD_FLAGS = new Set(["-f", "-F", "--field", "--raw-field", "--input"]);
|
|
327
378
|
const MUTATING_GH_API_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
328
379
|
const READ_ONLY_GH_SUBCOMMANDS: Record<string, Set<string> | undefined> = {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
380
|
+
api: undefined,
|
|
381
|
+
auth: new Set(["status"]),
|
|
382
|
+
issue: new Set(["view", "list", "status"]),
|
|
383
|
+
label: new Set(["list"]),
|
|
384
|
+
milestone: new Set(["list"]),
|
|
385
|
+
pr: new Set(["view", "list", "diff", "status", "checks"]),
|
|
386
|
+
release: new Set(["view", "list"]),
|
|
387
|
+
repo: new Set(["view", "list"]),
|
|
388
|
+
run: new Set(["view", "list"]),
|
|
389
|
+
search: undefined,
|
|
390
|
+
status: undefined,
|
|
391
|
+
workflow: new Set(["view", "list"]),
|
|
341
392
|
};
|
|
342
393
|
|
|
343
394
|
function splitShellSegments(command: string): string[] | { reason: string } {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
395
|
+
if (/[`<>|;\n\r]/.test(command) || /\$\(/.test(command)) {
|
|
396
|
+
return {
|
|
397
|
+
reason:
|
|
398
|
+
"Agent Workflow Audit bash blocks shell substitution, redirection, pipes, semicolons, and multi-line commands.",
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (/(^|[^&])&(?!&)|&&&/.test(command)) {
|
|
402
|
+
return {
|
|
403
|
+
reason: "Agent Workflow Audit bash allows only simple commands optionally joined by &&.",
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return command
|
|
407
|
+
.split(/&&/)
|
|
408
|
+
.map((segment) => segment.trim())
|
|
409
|
+
.filter(Boolean);
|
|
351
410
|
}
|
|
352
411
|
|
|
353
412
|
function tokenizeSegment(segment: string): TokenizedSegment {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
const tokens: string[] = [];
|
|
414
|
+
let token = "";
|
|
415
|
+
let tokenStarted = false;
|
|
416
|
+
let inSingleQuote = false;
|
|
417
|
+
let inDoubleQuote = false;
|
|
418
|
+
|
|
419
|
+
const pushToken = () => {
|
|
420
|
+
if (!tokenStarted) return;
|
|
421
|
+
tokens.push(token);
|
|
422
|
+
token = "";
|
|
423
|
+
tokenStarted = false;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
for (let index = 0; index < segment.length; index += 1) {
|
|
427
|
+
const char = segment[index];
|
|
428
|
+
if (inSingleQuote) {
|
|
429
|
+
if (char === "'") {
|
|
430
|
+
inSingleQuote = false;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
token += char;
|
|
434
|
+
tokenStarted = true;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (inDoubleQuote) {
|
|
438
|
+
if (char === '"') {
|
|
439
|
+
inDoubleQuote = false;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (char === "`" || char === "$") {
|
|
443
|
+
return {
|
|
444
|
+
tokens,
|
|
445
|
+
reason: "Agent Workflow Audit bash blocks shell expansion inside quoted arguments.",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
token += char;
|
|
449
|
+
tokenStarted = true;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (/\s/.test(char)) {
|
|
453
|
+
pushToken();
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (char === "'") {
|
|
457
|
+
inSingleQuote = true;
|
|
458
|
+
tokenStarted = true;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (char === '"') {
|
|
462
|
+
inDoubleQuote = true;
|
|
463
|
+
tokenStarted = true;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (char === "\\")
|
|
467
|
+
return { tokens, reason: "Agent Workflow Audit bash blocks shell escape sequences." };
|
|
468
|
+
if (char === "$")
|
|
469
|
+
return { tokens, reason: "Agent Workflow Audit bash blocks shell variable expansion." };
|
|
470
|
+
if (/[{}[\]*?]/.test(char))
|
|
471
|
+
return {
|
|
472
|
+
tokens,
|
|
473
|
+
reason: "Agent Workflow Audit bash blocks shell glob and brace metacharacters.",
|
|
474
|
+
};
|
|
475
|
+
token += char;
|
|
476
|
+
tokenStarted = true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (inSingleQuote || inDoubleQuote)
|
|
480
|
+
return { tokens, reason: "Agent Workflow Audit bash blocks unterminated shell quotes." };
|
|
481
|
+
pushToken();
|
|
482
|
+
return { tokens };
|
|
414
483
|
}
|
|
415
484
|
|
|
416
485
|
function getUnsafeShellFlagReason(token: string): string | undefined {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
486
|
+
const lower = token.toLowerCase();
|
|
487
|
+
if (
|
|
488
|
+
lower === "-c" ||
|
|
489
|
+
lower.startsWith("-c") ||
|
|
490
|
+
lower === "-p" ||
|
|
491
|
+
lower.startsWith("-p") ||
|
|
492
|
+
lower === "-f" ||
|
|
493
|
+
lower.startsWith("-f") ||
|
|
494
|
+
lower === "--dir" ||
|
|
495
|
+
lower.startsWith("--dir=") ||
|
|
496
|
+
lower === "--directory" ||
|
|
497
|
+
lower.startsWith("--directory=") ||
|
|
498
|
+
lower === "--working-directory" ||
|
|
499
|
+
lower.startsWith("--working-directory=") ||
|
|
500
|
+
lower === "--project-dir" ||
|
|
501
|
+
lower.startsWith("--project-dir=") ||
|
|
502
|
+
lower === "--build-file" ||
|
|
503
|
+
lower.startsWith("--build-file=") ||
|
|
504
|
+
lower === "--file" ||
|
|
505
|
+
lower.startsWith("--file=") ||
|
|
506
|
+
lower === "--prefix" ||
|
|
507
|
+
lower.startsWith("--prefix=") ||
|
|
508
|
+
lower === "--cwd" ||
|
|
509
|
+
lower.startsWith("--cwd=")
|
|
510
|
+
) {
|
|
511
|
+
return "Agent Workflow Audit bash blocks path-changing flags to avoid symlink/path escapes.";
|
|
512
|
+
}
|
|
513
|
+
return undefined;
|
|
434
514
|
}
|
|
435
515
|
|
|
436
516
|
function getUnsafeShellTokenReason(token: string): string | undefined {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
517
|
+
const values = [token];
|
|
518
|
+
const equalsIndex = token.indexOf("=");
|
|
519
|
+
if (equalsIndex >= 0 && equalsIndex < token.length - 1) values.push(token.slice(equalsIndex + 1));
|
|
520
|
+
|
|
521
|
+
for (const value of values) {
|
|
522
|
+
const normalized = value.startsWith("@") ? value.slice(1) : value;
|
|
523
|
+
if (normalized === "~" || normalized.startsWith("~/") || /^~[^/\\]*/.test(normalized)) {
|
|
524
|
+
return "Agent Workflow Audit bash blocks home-directory paths.";
|
|
525
|
+
}
|
|
526
|
+
if (path.isAbsolute(normalized))
|
|
527
|
+
return "Agent Workflow Audit bash blocks absolute paths; stay inside the audited checkout.";
|
|
528
|
+
if (/(^|[/\\])\.\.(?:[/\\]|$)/.test(normalized)) {
|
|
529
|
+
return "Agent Workflow Audit bash blocks path traversal outside the audited checkout.";
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return undefined;
|
|
452
533
|
}
|
|
453
534
|
|
|
454
535
|
function getCommandName(token: string): string {
|
|
455
|
-
|
|
536
|
+
return path.basename(token).toLowerCase();
|
|
456
537
|
}
|
|
457
538
|
|
|
458
539
|
function getBlockedCdReason(_tokens: string[]): string | undefined {
|
|
459
|
-
|
|
540
|
+
return "Agent Workflow Audit blocks shell cd to keep bash execution pinned to the audited checkout root. Report documented subdirectory cd steps as manual or workflow friction.";
|
|
460
541
|
}
|
|
461
542
|
|
|
462
543
|
const PACKAGE_VALUE_FLAGS = new Set(["--filter", "--workspace", "-w", "--prefix", "-c", "--cwd"]);
|
|
463
544
|
|
|
464
545
|
function skipPackageFlagValue(lowerTokens: string[], index: number): number {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
546
|
+
const token = lowerTokens[index];
|
|
547
|
+
if (PACKAGE_VALUE_FLAGS.has(token)) return index + 2;
|
|
548
|
+
if (
|
|
549
|
+
token.startsWith("--filter=") ||
|
|
550
|
+
token.startsWith("--workspace=") ||
|
|
551
|
+
token.startsWith("--prefix=") ||
|
|
552
|
+
token.startsWith("--cwd=")
|
|
553
|
+
)
|
|
554
|
+
return index + 1;
|
|
555
|
+
return token.startsWith("-") ? index + 1 : index;
|
|
469
556
|
}
|
|
470
557
|
|
|
471
558
|
function getPackageFirstCommand(lowerTokens: string[]): { command?: string; index: number } {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
559
|
+
let index = 1;
|
|
560
|
+
while (index < lowerTokens.length) {
|
|
561
|
+
const next = skipPackageFlagValue(lowerTokens, index);
|
|
562
|
+
if (next !== index) {
|
|
563
|
+
index = next;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
return { command: lowerTokens[index], index };
|
|
567
|
+
}
|
|
568
|
+
return { index };
|
|
482
569
|
}
|
|
483
570
|
|
|
484
|
-
function getPackageRunScript(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
571
|
+
function getPackageRunScript(
|
|
572
|
+
lowerTokens: string[],
|
|
573
|
+
runIndex: number,
|
|
574
|
+
commandName: string,
|
|
575
|
+
): string | undefined {
|
|
576
|
+
if (runIndex < 0)
|
|
577
|
+
return commandName === "yarn" ? getPackageFirstCommand(lowerTokens).command : undefined;
|
|
578
|
+
for (let index = runIndex + 1; index < lowerTokens.length;) {
|
|
579
|
+
const token = lowerTokens[index];
|
|
580
|
+
if (token === "--") return lowerTokens[index + 1];
|
|
581
|
+
const next = skipPackageFlagValue(lowerTokens, index);
|
|
582
|
+
if (next !== index) {
|
|
583
|
+
index = next;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
return token;
|
|
587
|
+
}
|
|
588
|
+
return undefined;
|
|
497
589
|
}
|
|
498
590
|
|
|
499
591
|
function getBlockedPackageManagerReason(commandName: string, tokens: string[]): string | undefined {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
592
|
+
const lowerTokens = tokens.map((token) => token.toLowerCase());
|
|
593
|
+
if (
|
|
594
|
+
lowerTokens.some(
|
|
595
|
+
(token) =>
|
|
596
|
+
token === "--prefix" ||
|
|
597
|
+
token.startsWith("--prefix=") ||
|
|
598
|
+
token === "--cwd" ||
|
|
599
|
+
token.startsWith("--cwd=") ||
|
|
600
|
+
token === "-c" ||
|
|
601
|
+
token === "--workspace" ||
|
|
602
|
+
token.startsWith("--workspace=") ||
|
|
603
|
+
token === "-w",
|
|
604
|
+
)
|
|
605
|
+
) {
|
|
606
|
+
return "Agent Workflow Audit blocks package-manager path/workspace flags to avoid symlink/path escapes.";
|
|
607
|
+
}
|
|
608
|
+
if (
|
|
609
|
+
lowerTokens.some((token) =>
|
|
610
|
+
[
|
|
611
|
+
"publish",
|
|
612
|
+
"unpublish",
|
|
613
|
+
"version",
|
|
614
|
+
"deprecate",
|
|
615
|
+
"dist-tag",
|
|
616
|
+
"owner",
|
|
617
|
+
"access",
|
|
618
|
+
"token",
|
|
619
|
+
"login",
|
|
620
|
+
"logout",
|
|
621
|
+
"adduser",
|
|
622
|
+
"team",
|
|
623
|
+
"profile",
|
|
624
|
+
"org",
|
|
625
|
+
"hook",
|
|
626
|
+
"config",
|
|
627
|
+
].includes(token),
|
|
628
|
+
)
|
|
629
|
+
) {
|
|
630
|
+
return "Agent Workflow Audit blocks package registry/account/config mutation commands.";
|
|
631
|
+
}
|
|
632
|
+
if (lowerTokens.some((token) => ["exec", "x", "dlx", "create"].includes(token))) {
|
|
633
|
+
return "Agent Workflow Audit blocks package-manager arbitrary execution commands such as exec, dlx, x, and create.";
|
|
634
|
+
}
|
|
635
|
+
const { command: firstCommand, index: firstCommandIndex } = getPackageFirstCommand(lowerTokens);
|
|
636
|
+
const allowedCommands = new Set([
|
|
637
|
+
"install",
|
|
638
|
+
"ci",
|
|
639
|
+
"test",
|
|
640
|
+
"run",
|
|
641
|
+
"start",
|
|
642
|
+
"build",
|
|
643
|
+
"lint",
|
|
644
|
+
"check",
|
|
645
|
+
]);
|
|
646
|
+
if (firstCommand && !allowedCommands.has(firstCommand)) {
|
|
647
|
+
return `Agent Workflow Audit blocks ${commandName} ${firstCommand}; only install/ci/test/run/start/build/lint/check are allowed.`;
|
|
648
|
+
}
|
|
649
|
+
if (firstCommand === "install" || firstCommand === "ci") {
|
|
650
|
+
if (
|
|
651
|
+
lowerTokens.some(
|
|
652
|
+
(token) => token === "-f" || token === "--force" || token.startsWith("--force="),
|
|
653
|
+
)
|
|
654
|
+
)
|
|
655
|
+
return "Agent Workflow Audit blocks forced package installs.";
|
|
656
|
+
if (
|
|
657
|
+
lowerTokens.some(
|
|
658
|
+
(token) =>
|
|
659
|
+
token === "-g" ||
|
|
660
|
+
token === "--global" ||
|
|
661
|
+
token.startsWith("--global=") ||
|
|
662
|
+
token === "--location=global" ||
|
|
663
|
+
token === "--location",
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
return "Agent Workflow Audit blocks global package installs.";
|
|
667
|
+
}
|
|
668
|
+
if (firstCommand === "install") {
|
|
669
|
+
for (let index = firstCommandIndex + 1; index < lowerTokens.length;) {
|
|
670
|
+
const next = skipPackageFlagValue(lowerTokens, index);
|
|
671
|
+
if (next !== index) {
|
|
672
|
+
index = next;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (!lowerTokens[index].startsWith("-"))
|
|
676
|
+
return "Agent Workflow Audit blocks package install operands; run documented dependency install commands without adding packages.";
|
|
677
|
+
index += 1;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const runnerIndex = lowerTokens.findIndex((token) =>
|
|
681
|
+
["node", "python", "python3", "ruby", "php"].includes(token),
|
|
682
|
+
);
|
|
683
|
+
if (runnerIndex >= 0) {
|
|
684
|
+
const runnerReason = getBlockedInterpreterReason(
|
|
685
|
+
lowerTokens[runnerIndex],
|
|
686
|
+
lowerTokens.slice(runnerIndex),
|
|
687
|
+
);
|
|
688
|
+
if (runnerReason) return runnerReason;
|
|
689
|
+
}
|
|
690
|
+
const runIndex = lowerTokens.indexOf("run");
|
|
691
|
+
const script = getPackageRunScript(lowerTokens, runIndex, commandName);
|
|
692
|
+
if (
|
|
693
|
+
script &&
|
|
694
|
+
/(^|[:_-])(deploy|release|publish|clean|reset|fix|format)(?:$|[:_-])/.test(script)
|
|
695
|
+
) {
|
|
696
|
+
return "Agent Workflow Audit blocks deploy/release/publish/clean/reset/fix/format package scripts.";
|
|
697
|
+
}
|
|
698
|
+
if (lowerTokens.some((token) => token === "--fix" || token.startsWith("--fix=")))
|
|
699
|
+
return "Agent Workflow Audit blocks package-script fix flags.";
|
|
700
|
+
return undefined;
|
|
542
701
|
}
|
|
543
702
|
|
|
544
703
|
function getBlockedTaskRunnerReason(commandName: string, tokens: string[]): string | undefined {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
704
|
+
const lowerTokens = tokens.map((token) => token.toLowerCase());
|
|
705
|
+
if (
|
|
706
|
+
lowerTokens.some(
|
|
707
|
+
(token) => token === "-c" || token === "--directory" || token.startsWith("--directory="),
|
|
708
|
+
)
|
|
709
|
+
) {
|
|
710
|
+
return `Agent Workflow Audit blocks ${commandName} directory-changing flags to avoid symlink/path escapes.`;
|
|
711
|
+
}
|
|
712
|
+
const targets = lowerTokens.slice(1).filter((token) => !token.startsWith("-"));
|
|
713
|
+
if (
|
|
714
|
+
targets.some((target) =>
|
|
715
|
+
/(^|[:_-])(deploy|release|publish|install|clean|reset|fix|format)(?:$|[:_-])/.test(target),
|
|
716
|
+
)
|
|
717
|
+
) {
|
|
718
|
+
return `Agent Workflow Audit blocks ${commandName} deploy/release/publish/install/clean/reset/fix/format targets.`;
|
|
719
|
+
}
|
|
720
|
+
return undefined;
|
|
554
721
|
}
|
|
555
722
|
|
|
556
|
-
function getGitSubcommand(tokens: string[]): {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
723
|
+
function getGitSubcommand(tokens: string[]): {
|
|
724
|
+
subcommand?: string;
|
|
725
|
+
index: number;
|
|
726
|
+
reason?: string;
|
|
727
|
+
} {
|
|
728
|
+
let index = 1;
|
|
729
|
+
while (index < tokens.length) {
|
|
730
|
+
const token = tokens[index];
|
|
731
|
+
if (!token.startsWith("-")) break;
|
|
732
|
+
if (token === "-C" || token.startsWith("-C")) {
|
|
733
|
+
return {
|
|
734
|
+
index,
|
|
735
|
+
reason:
|
|
736
|
+
"Agent Workflow Audit blocks git -C to keep git inspection pinned to the audited checkout root.",
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
if (
|
|
740
|
+
token === "--git-dir" ||
|
|
741
|
+
token.startsWith("--git-dir=") ||
|
|
742
|
+
token === "--work-tree" ||
|
|
743
|
+
token.startsWith("--work-tree=")
|
|
744
|
+
) {
|
|
745
|
+
return {
|
|
746
|
+
index,
|
|
747
|
+
reason:
|
|
748
|
+
"Agent Workflow Audit blocks git options that can target repositories outside the audited checkout.",
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (token === "-c" || token.startsWith("-c")) {
|
|
752
|
+
return {
|
|
753
|
+
index,
|
|
754
|
+
reason:
|
|
755
|
+
"Agent Workflow Audit blocks git -c because aliases/config can change command behavior.",
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
if (SAFE_GIT_GLOBAL_FLAGS.has(token)) {
|
|
759
|
+
index += 1;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
index,
|
|
764
|
+
reason: `Agent Workflow Audit blocks git global option ${token}; use a direct read-only git command.`,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
return { subcommand: tokens[index]?.toLowerCase(), index };
|
|
577
768
|
}
|
|
578
769
|
|
|
579
770
|
function isSafeGitBranchCommand(tokens: string[], subcommandIndex: number): boolean {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
771
|
+
const args = tokens.slice(subcommandIndex + 1);
|
|
772
|
+
if (args.length === 0) return true;
|
|
773
|
+
if (
|
|
774
|
+
args.some(
|
|
775
|
+
(arg) =>
|
|
776
|
+
/^-(?:d|D|m|M|c|C|f)$/.test(arg) ||
|
|
777
|
+
/^--(?:delete|move|copy|force|set-upstream-to|unset-upstream)$/.test(arg),
|
|
778
|
+
)
|
|
779
|
+
) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
const hasReadOnlyMode = args.some(
|
|
783
|
+
(arg) =>
|
|
784
|
+
SAFE_GIT_BRANCH_FLAGS.has(arg) ||
|
|
785
|
+
arg.startsWith("--contains=") ||
|
|
786
|
+
arg.startsWith("--merged=") ||
|
|
787
|
+
arg.startsWith("--no-merged="),
|
|
788
|
+
);
|
|
789
|
+
return (
|
|
790
|
+
hasReadOnlyMode &&
|
|
791
|
+
args.every(
|
|
792
|
+
(arg) =>
|
|
793
|
+
SAFE_GIT_BRANCH_FLAGS.has(arg) ||
|
|
794
|
+
arg.startsWith("--contains=") ||
|
|
795
|
+
arg.startsWith("--merged=") ||
|
|
796
|
+
arg.startsWith("--no-merged=") ||
|
|
797
|
+
!arg.startsWith("-"),
|
|
798
|
+
)
|
|
799
|
+
);
|
|
587
800
|
}
|
|
588
801
|
|
|
589
802
|
function isSafeGitRemoteCommand(tokens: string[], subcommandIndex: number): boolean {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
803
|
+
const args = tokens.slice(subcommandIndex + 1);
|
|
804
|
+
if (args.length === 0) return true;
|
|
805
|
+
if (args.length === 1 && (args[0] === "-v" || args[0] === "--verbose")) return true;
|
|
806
|
+
const action = args.find((arg) => !arg.startsWith("-"));
|
|
807
|
+
return action === "show" || action === "get-url";
|
|
595
808
|
}
|
|
596
809
|
|
|
597
810
|
function getUnsafeGitFlagReason(tokens: string[]): string | undefined {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
811
|
+
for (const token of tokens.slice(1)) {
|
|
812
|
+
const lower = token.toLowerCase();
|
|
813
|
+
if (lower === "--output" || lower.startsWith("--output="))
|
|
814
|
+
return "Agent Workflow Audit blocks git output-writing flags.";
|
|
815
|
+
if (lower === "-o" || lower.startsWith("-o"))
|
|
816
|
+
return "Agent Workflow Audit blocks git output-order/pager helper flags that can write or execute helpers.";
|
|
817
|
+
if (lower === "--open-files-in-pager" || lower.startsWith("--open-files-in-pager=")) {
|
|
818
|
+
return "Agent Workflow Audit blocks git pager-opening flags because pagers can execute local utilities.";
|
|
819
|
+
}
|
|
820
|
+
if (
|
|
821
|
+
lower === "--ext-diff" ||
|
|
822
|
+
lower.startsWith("--ext-diff=") ||
|
|
823
|
+
lower === "--textconv" ||
|
|
824
|
+
lower.startsWith("--textconv=")
|
|
825
|
+
) {
|
|
826
|
+
return "Agent Workflow Audit blocks git external diff/text conversion flags because they can execute helpers.";
|
|
827
|
+
}
|
|
828
|
+
if (lower === "--no-index" || lower.startsWith("--no-index="))
|
|
829
|
+
return "Agent Workflow Audit blocks git --no-index because it can inspect arbitrary filesystem paths.";
|
|
830
|
+
if (lower === "--exec-path" || lower.startsWith("--exec-path="))
|
|
831
|
+
return "Agent Workflow Audit blocks git exec-path overrides.";
|
|
832
|
+
}
|
|
833
|
+
return undefined;
|
|
612
834
|
}
|
|
613
835
|
|
|
614
836
|
function getBlockedGitReason(tokens: string[]): string | undefined {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
837
|
+
const unsafeFlagReason = getUnsafeGitFlagReason(tokens);
|
|
838
|
+
if (unsafeFlagReason) return unsafeFlagReason;
|
|
839
|
+
const parsed = getGitSubcommand(tokens);
|
|
840
|
+
if (parsed.reason) return parsed.reason;
|
|
841
|
+
if (!parsed.subcommand) return undefined;
|
|
842
|
+
if (!READ_ONLY_GIT_SUBCOMMANDS.has(parsed.subcommand)) {
|
|
843
|
+
return `Agent Workflow Audit blocks git ${parsed.subcommand}; only known read-only git subcommands are allowed.`;
|
|
844
|
+
}
|
|
845
|
+
if (parsed.subcommand === "branch" && !isSafeGitBranchCommand(tokens, parsed.index)) {
|
|
846
|
+
return "Agent Workflow Audit allows git branch only for read-only listing/show-current/contains/merged queries.";
|
|
847
|
+
}
|
|
848
|
+
if (parsed.subcommand === "remote" && !isSafeGitRemoteCommand(tokens, parsed.index)) {
|
|
849
|
+
return "Agent Workflow Audit allows git remote only for read-only list/show/get-url queries.";
|
|
850
|
+
}
|
|
851
|
+
return undefined;
|
|
630
852
|
}
|
|
631
853
|
|
|
632
854
|
function getGhCommand(tokens: string[]): { command?: string; subcommand?: string; index: number } {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
855
|
+
let index = 1;
|
|
856
|
+
while (index < tokens.length) {
|
|
857
|
+
const token = tokens[index];
|
|
858
|
+
if (!token.startsWith("-")) break;
|
|
859
|
+
if (GH_GLOBAL_OPTIONS_WITH_VALUE.has(token)) {
|
|
860
|
+
index += 2;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (
|
|
864
|
+
["--repo=", "--hostname=", "--jq=", "--template="].some((prefix) => token.startsWith(prefix))
|
|
865
|
+
) {
|
|
866
|
+
index += 1;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
index += 1;
|
|
870
|
+
}
|
|
871
|
+
return {
|
|
872
|
+
command: tokens[index]?.toLowerCase(),
|
|
873
|
+
subcommand: tokens[index + 1]?.toLowerCase(),
|
|
874
|
+
index,
|
|
875
|
+
};
|
|
648
876
|
}
|
|
649
877
|
|
|
650
878
|
function getBlockedGhApiReason(tokens: string[]): string | undefined {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
879
|
+
const parsed = getGhCommand(tokens);
|
|
880
|
+
if (parsed.command !== "api") return undefined;
|
|
881
|
+
const endpoint = tokens[parsed.index + 1]?.toLowerCase();
|
|
882
|
+
if (endpoint === "graphql")
|
|
883
|
+
return "Agent Workflow Audit blocks gh api graphql because it uses POST/body fields.";
|
|
884
|
+
for (let index = parsed.index + 1; index < tokens.length; index += 1) {
|
|
885
|
+
const token = tokens[index];
|
|
886
|
+
const lower = token.toLowerCase();
|
|
887
|
+
if (
|
|
888
|
+
GH_API_FIELD_FLAGS.has(token) ||
|
|
889
|
+
lower.startsWith("--field=") ||
|
|
890
|
+
lower.startsWith("--raw-field=") ||
|
|
891
|
+
lower.startsWith("--input=") ||
|
|
892
|
+
/^-[fF]/.test(token)
|
|
893
|
+
) {
|
|
894
|
+
return "Agent Workflow Audit allows read-only gh api calls only; request fields and input files are blocked.";
|
|
895
|
+
}
|
|
896
|
+
let method: string | undefined;
|
|
897
|
+
if (lower === "-x" || lower === "--method") method = tokens[index + 1];
|
|
898
|
+
else if (lower.startsWith("-x") && token.length > 2) method = token.slice(2).replace(/^=/, "");
|
|
899
|
+
else if (lower.startsWith("--method=")) method = token.slice("--method=".length);
|
|
900
|
+
if (method && MUTATING_GH_API_METHODS.has(method.toUpperCase())) {
|
|
901
|
+
return "Agent Workflow Audit allows read-only gh api calls only; mutating methods are blocked.";
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return undefined;
|
|
670
905
|
}
|
|
671
906
|
|
|
672
907
|
function getBlockedGhReason(tokens: string[]): string | undefined {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
908
|
+
const apiReason = getBlockedGhApiReason(tokens);
|
|
909
|
+
if (apiReason) return apiReason;
|
|
910
|
+
const parsed = getGhCommand(tokens);
|
|
911
|
+
const command = parsed.command;
|
|
912
|
+
if (!command) return undefined;
|
|
913
|
+
if (!(command in READ_ONLY_GH_SUBCOMMANDS)) {
|
|
914
|
+
return `Agent Workflow Audit blocks gh ${command}; only known read-only gh commands are allowed.`;
|
|
915
|
+
}
|
|
916
|
+
const allowedSubcommands = READ_ONLY_GH_SUBCOMMANDS[command];
|
|
917
|
+
if (allowedSubcommands && (!parsed.subcommand || !allowedSubcommands.has(parsed.subcommand))) {
|
|
918
|
+
return `Agent Workflow Audit blocks gh ${command}${parsed.subcommand ? ` ${parsed.subcommand}` : ""}; only known read-only gh subcommands are allowed.`;
|
|
919
|
+
}
|
|
920
|
+
if (
|
|
921
|
+
command === "auth" &&
|
|
922
|
+
tokens.some((token) => {
|
|
923
|
+
const lower = token.toLowerCase();
|
|
924
|
+
return lower === "-t" || lower === "--show-token" || lower.startsWith("--show-token=");
|
|
925
|
+
})
|
|
926
|
+
) {
|
|
927
|
+
return "Agent Workflow Audit blocks gh auth status --show-token because it reveals credentials.";
|
|
928
|
+
}
|
|
929
|
+
return undefined;
|
|
692
930
|
}
|
|
693
931
|
|
|
694
932
|
function hasInlineFlag(tokens: string[], flags: string[]): boolean {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
933
|
+
return tokens.some((token) => {
|
|
934
|
+
const lower = token.toLowerCase();
|
|
935
|
+
return flags.some(
|
|
936
|
+
(flag) =>
|
|
937
|
+
lower === flag ||
|
|
938
|
+
lower.startsWith(`${flag}=`) ||
|
|
939
|
+
(flag.length === 2 && lower.startsWith(flag) && lower !== flag.slice(0, 1)),
|
|
940
|
+
);
|
|
941
|
+
});
|
|
699
942
|
}
|
|
700
943
|
|
|
701
944
|
function getBlockedInterpreterReason(commandName: string, tokens: string[]): string | undefined {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
945
|
+
const lowerTokens = tokens.map((token) => token.toLowerCase());
|
|
946
|
+
if (
|
|
947
|
+
["node", "deno", "bun"].includes(commandName) &&
|
|
948
|
+
hasInlineFlag(lowerTokens, [
|
|
949
|
+
"-e",
|
|
950
|
+
"--eval",
|
|
951
|
+
"eval",
|
|
952
|
+
"-p",
|
|
953
|
+
"--print",
|
|
954
|
+
"-r",
|
|
955
|
+
"--require",
|
|
956
|
+
"--import",
|
|
957
|
+
"--preload",
|
|
958
|
+
"--loader",
|
|
959
|
+
"--experimental-loader",
|
|
960
|
+
])
|
|
961
|
+
) {
|
|
962
|
+
return `Agent Workflow Audit blocks inline/preload ${commandName} execution; run documented project scripts instead.`;
|
|
963
|
+
}
|
|
964
|
+
if (["python", "python3"].includes(commandName) && hasInlineFlag(lowerTokens, ["-c"])) {
|
|
965
|
+
return "Agent Workflow Audit blocks inline Python execution; run documented project scripts instead.";
|
|
966
|
+
}
|
|
967
|
+
if (["ruby", "php"].includes(commandName) && hasInlineFlag(lowerTokens, ["-e", "-r"])) {
|
|
968
|
+
return `Agent Workflow Audit blocks inline/preload ${commandName} execution; run documented project scripts instead.`;
|
|
969
|
+
}
|
|
970
|
+
return undefined;
|
|
713
971
|
}
|
|
714
972
|
|
|
715
973
|
function getBlockedLocalToolReason(commandName: string, tokens: string[]): string | undefined {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
974
|
+
const lowerTokens = tokens.map((token) => token.toLowerCase());
|
|
975
|
+
const firstCommand = lowerTokens.slice(1).find((token) => !token.startsWith("-"));
|
|
976
|
+
if (commandName === "go") {
|
|
977
|
+
if (!firstCommand || !["test", "build", "vet", "list", "version"].includes(firstCommand))
|
|
978
|
+
return `Agent Workflow Audit blocks go ${firstCommand ?? ""}; only test/build/vet/list/version are allowed.`;
|
|
979
|
+
}
|
|
980
|
+
if (commandName === "cargo") {
|
|
981
|
+
if (
|
|
982
|
+
!firstCommand ||
|
|
983
|
+
!["test", "build", "check", "clippy", "doc", "metadata", "tree", "version"].includes(
|
|
984
|
+
firstCommand,
|
|
985
|
+
)
|
|
986
|
+
)
|
|
987
|
+
return `Agent Workflow Audit blocks cargo ${firstCommand ?? ""}; only test/build/check/clippy/doc/metadata/tree/version are allowed.`;
|
|
988
|
+
}
|
|
989
|
+
if (commandName === "ruff") {
|
|
990
|
+
if (
|
|
991
|
+
lowerTokens.some(
|
|
992
|
+
(token) =>
|
|
993
|
+
token === "--fix" ||
|
|
994
|
+
token.startsWith("--fix=") ||
|
|
995
|
+
token === "--fix-only" ||
|
|
996
|
+
token.startsWith("--fix-only="),
|
|
997
|
+
) ||
|
|
998
|
+
firstCommand === "format"
|
|
999
|
+
)
|
|
1000
|
+
return "Agent Workflow Audit blocks ruff source-mutating fix/format commands.";
|
|
1001
|
+
}
|
|
1002
|
+
if (commandName === "dotnet") {
|
|
1003
|
+
if (firstCommand && ["add", "remove", "format", "tool", "new", "nuget"].includes(firstCommand))
|
|
1004
|
+
return `Agent Workflow Audit blocks dotnet ${firstCommand} mutation commands.`;
|
|
1005
|
+
}
|
|
1006
|
+
if (commandName === "swift") {
|
|
1007
|
+
if (firstCommand && !["test", "build", "--version", "-version"].includes(firstCommand))
|
|
1008
|
+
return `Agent Workflow Audit blocks swift ${firstCommand}; only test/build/version are allowed.`;
|
|
1009
|
+
}
|
|
1010
|
+
return undefined;
|
|
734
1011
|
}
|
|
735
1012
|
|
|
736
1013
|
function getBlockedExecutableReason(tokens: string[]): string | undefined {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1014
|
+
const executable = tokens[0];
|
|
1015
|
+
if (!executable) return "Agent Workflow Audit bash requires a command.";
|
|
1016
|
+
if (executable.startsWith("/"))
|
|
1017
|
+
return "Agent Workflow Audit bash blocks absolute-path executables.";
|
|
1018
|
+
if (/[\\/]/.test(executable))
|
|
1019
|
+
return "Agent Workflow Audit bash blocks path-qualified executables to avoid symlink/path escapes; use direct project/package commands.";
|
|
1020
|
+
|
|
1021
|
+
const commandName = getCommandName(executable);
|
|
1022
|
+
if (commandName === "cd") return getBlockedCdReason(tokens);
|
|
1023
|
+
if (!SAFE_DIRECT_COMMANDS.has(commandName)) {
|
|
1024
|
+
return `Agent Workflow Audit bash blocks direct ${commandName || "shell"} commands; use read/grep/find/ls for inspection or documented project commands.`;
|
|
1025
|
+
}
|
|
1026
|
+
const interpreterReason = getBlockedInterpreterReason(commandName, tokens);
|
|
1027
|
+
if (interpreterReason) return interpreterReason;
|
|
1028
|
+
const localToolReason = getBlockedLocalToolReason(commandName, tokens);
|
|
1029
|
+
if (localToolReason) return localToolReason;
|
|
1030
|
+
if (["npm", "pnpm", "yarn", "bun"].includes(commandName))
|
|
1031
|
+
return getBlockedPackageManagerReason(commandName, tokens);
|
|
1032
|
+
if (["make", "just", "task"].includes(commandName))
|
|
1033
|
+
return getBlockedTaskRunnerReason(commandName, tokens);
|
|
1034
|
+
if (
|
|
1035
|
+
commandName === "cargo" &&
|
|
1036
|
+
tokens
|
|
1037
|
+
.slice(1)
|
|
1038
|
+
.some((token) => /^(publish|login|owner|yank|install|release)(?:$|[:_-])/i.test(token))
|
|
1039
|
+
)
|
|
1040
|
+
return "Agent Workflow Audit blocks cargo registry/user mutation commands.";
|
|
1041
|
+
if (commandName === "mvn") {
|
|
1042
|
+
const goals = tokens
|
|
1043
|
+
.slice(1)
|
|
1044
|
+
.filter((token) => !token.startsWith("-"))
|
|
1045
|
+
.map((token) => token.toLowerCase());
|
|
1046
|
+
const allowedGoals = new Set([
|
|
1047
|
+
"test",
|
|
1048
|
+
"verify",
|
|
1049
|
+
"package",
|
|
1050
|
+
"compile",
|
|
1051
|
+
"validate",
|
|
1052
|
+
"dependency:tree",
|
|
1053
|
+
"help:effective-pom",
|
|
1054
|
+
"--version",
|
|
1055
|
+
]);
|
|
1056
|
+
if (goals.some((goal) => !allowedGoals.has(goal)))
|
|
1057
|
+
return "Agent Workflow Audit allows mvn only for test/verify/package/compile/validate/dependency:tree/help:effective-pom.";
|
|
1058
|
+
}
|
|
1059
|
+
if (["gradle", "gradlew"].includes(commandName)) {
|
|
1060
|
+
const lowerTokens = tokens.map((token) => token.toLowerCase());
|
|
1061
|
+
if (
|
|
1062
|
+
lowerTokens.some(
|
|
1063
|
+
(token) =>
|
|
1064
|
+
token === "-p" || token === "--project-dir" || token.startsWith("--project-dir="),
|
|
1065
|
+
)
|
|
1066
|
+
)
|
|
1067
|
+
return "Agent Workflow Audit blocks Gradle project-dir flags to avoid symlink/path escapes.";
|
|
1068
|
+
const tasks = lowerTokens.slice(1).filter((token) => !token.startsWith("-"));
|
|
1069
|
+
const allowedTasks = new Set([
|
|
1070
|
+
"test",
|
|
1071
|
+
"build",
|
|
1072
|
+
"check",
|
|
1073
|
+
"assemble",
|
|
1074
|
+
"tasks",
|
|
1075
|
+
"projects",
|
|
1076
|
+
"properties",
|
|
1077
|
+
"dependencies",
|
|
1078
|
+
]);
|
|
1079
|
+
if (tasks.some((task) => !allowedTasks.has(task)))
|
|
1080
|
+
return "Agent Workflow Audit allows Gradle only for test/build/check/assemble/tasks/projects/properties/dependencies.";
|
|
1081
|
+
}
|
|
1082
|
+
if (
|
|
1083
|
+
commandName === "dotnet" &&
|
|
1084
|
+
tokens
|
|
1085
|
+
.slice(1)
|
|
1086
|
+
.some(
|
|
1087
|
+
(token, index, items) =>
|
|
1088
|
+
token.toLowerCase() === "nuget" &&
|
|
1089
|
+
["push", "delete"].includes(items[index + 1]?.toLowerCase()),
|
|
1090
|
+
)
|
|
1091
|
+
)
|
|
1092
|
+
return "Agent Workflow Audit blocks dotnet nuget mutation commands.";
|
|
1093
|
+
if (commandName === "git") return getBlockedGitReason(tokens);
|
|
1094
|
+
if (commandName === "gh") return getBlockedGhReason(tokens);
|
|
1095
|
+
return undefined;
|
|
771
1096
|
}
|
|
772
1097
|
|
|
773
1098
|
function getBlockedBashReason(command: string, options: { planOnly: boolean }): string | undefined {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1099
|
+
const trimmed = command.trim();
|
|
1100
|
+
if (!trimmed) return "Agent Workflow Audit bash requires a non-empty command.";
|
|
1101
|
+
if (options.planOnly)
|
|
1102
|
+
return "Plan-only Agent Workflow Audit mode blocks bash/project command execution.";
|
|
1103
|
+
|
|
1104
|
+
const segments = splitShellSegments(trimmed);
|
|
1105
|
+
if (!Array.isArray(segments)) return segments.reason;
|
|
1106
|
+
for (const segment of segments) {
|
|
1107
|
+
const tokenized = tokenizeSegment(segment);
|
|
1108
|
+
if (tokenized.reason) return tokenized.reason;
|
|
1109
|
+
const tokens = tokenized.tokens;
|
|
1110
|
+
if (tokens.length === 0)
|
|
1111
|
+
return "Agent Workflow Audit bash requires non-empty command segments.";
|
|
1112
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0]))
|
|
1113
|
+
return "Agent Workflow Audit bash blocks inline environment assignments.";
|
|
1114
|
+
for (const token of tokens) {
|
|
1115
|
+
const flagReason = getUnsafeShellFlagReason(token);
|
|
1116
|
+
if (flagReason) return flagReason;
|
|
1117
|
+
const pathReason = getUnsafeShellTokenReason(token);
|
|
1118
|
+
if (pathReason) return pathReason;
|
|
1119
|
+
}
|
|
1120
|
+
const executableReason = getBlockedExecutableReason(tokens);
|
|
1121
|
+
if (executableReason) return executableReason;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return undefined;
|
|
797
1125
|
}
|
|
798
1126
|
|
|
799
|
-
function createAuditRuntimeGuardExtension(options: {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1127
|
+
function createAuditRuntimeGuardExtension(options: {
|
|
1128
|
+
cwd: string;
|
|
1129
|
+
maxTurns: number;
|
|
1130
|
+
planOnly: boolean;
|
|
1131
|
+
}): ExtensionFactory {
|
|
1132
|
+
return (pi) => {
|
|
1133
|
+
let currentTurn = 0;
|
|
1134
|
+
|
|
1135
|
+
pi.on("turn_start", async (event) => {
|
|
1136
|
+
currentTurn = event.turnIndex;
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
pi.on("tool_call", async (event) => {
|
|
1140
|
+
if (!["read", "grep", "find", "ls", "bash"].includes(event.toolName)) {
|
|
1141
|
+
return {
|
|
1142
|
+
block: true,
|
|
1143
|
+
reason: `agent-workflow-audit exposes only read, grep, find, ls, and guarded bash; ${event.toolName} is not allowed.`,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (currentTurn >= options.maxTurns - 1) {
|
|
1148
|
+
return {
|
|
1149
|
+
block: true,
|
|
1150
|
+
reason: `Tool use is disabled on final agent-workflow-audit turn ${options.maxTurns}/${options.maxTurns}. Answer now with the evidence already gathered.`,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (event.toolName === "read") {
|
|
1155
|
+
const reason = await assertToolPathInsideCwd(
|
|
1156
|
+
options.cwd,
|
|
1157
|
+
(event.input as { path?: unknown }).path,
|
|
1158
|
+
"read",
|
|
1159
|
+
);
|
|
1160
|
+
if (reason) return { block: true, reason };
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (event.toolName === "grep" || event.toolName === "find" || event.toolName === "ls") {
|
|
1164
|
+
const input = asRecord(event.input) ?? {};
|
|
1165
|
+
const reason = await assertToolPathInsideCwd(options.cwd, input.path, event.toolName);
|
|
1166
|
+
if (reason) return { block: true, reason };
|
|
1167
|
+
if (event.toolName === "grep") {
|
|
1168
|
+
const globReason = getUnsafePatternReason(input.glob, "grep glob");
|
|
1169
|
+
if (globReason) return { block: true, reason: globReason };
|
|
1170
|
+
}
|
|
1171
|
+
if (event.toolName === "find") {
|
|
1172
|
+
const patternReason = getUnsafePatternReason(input.pattern, "find pattern");
|
|
1173
|
+
if (patternReason) return { block: true, reason: patternReason };
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (event.toolName === "bash") {
|
|
1178
|
+
const input = event.input as { command?: unknown; timeout?: unknown };
|
|
1179
|
+
if (typeof input.timeout !== "number") input.timeout = DEFAULT_BASH_TIMEOUT_SECONDS;
|
|
1180
|
+
else
|
|
1181
|
+
input.timeout = Math.min(
|
|
1182
|
+
MAX_BASH_TIMEOUT_SECONDS,
|
|
1183
|
+
Math.max(1, Math.floor(input.timeout)),
|
|
1184
|
+
);
|
|
1185
|
+
const command = typeof input.command === "string" ? input.command : "";
|
|
1186
|
+
const reason = getBlockedBashReason(command, { planOnly: options.planOnly });
|
|
1187
|
+
if (reason) return { block: true, reason };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return undefined;
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
pi.on("tool_result", async (event) => ({
|
|
1194
|
+
content: [
|
|
1195
|
+
...(event.content ?? []),
|
|
1196
|
+
{
|
|
1197
|
+
type: "text",
|
|
1198
|
+
text: `\n\n[agent-workflow-audit turn budget] turn ${Math.min(currentTurn + 1, options.maxTurns)}/${options.maxTurns}`,
|
|
1199
|
+
},
|
|
1200
|
+
],
|
|
1201
|
+
}));
|
|
1202
|
+
};
|
|
860
1203
|
}
|
|
861
1204
|
|
|
862
|
-
function buildSystemPrompt(options: {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1205
|
+
function buildSystemPrompt(options: {
|
|
1206
|
+
cwd: string;
|
|
1207
|
+
maxTurns: number;
|
|
1208
|
+
maxRunSeconds: number;
|
|
1209
|
+
planOnly: boolean;
|
|
1210
|
+
}): string {
|
|
1211
|
+
const executionMode = options.planOnly
|
|
1212
|
+
? "Plan-only mode is active. Do not run project commands with bash. Read instructions and manifests, infer the likely workflow, and report what would be tried plus remaining uncertainty."
|
|
1213
|
+
: "Execution mode is active. Try documented setup, build, lint, test, run, and other obvious project commands when they are safe and relevant. A runtime guard blocks deploy/publish/VCS-mutating/destructive commands; if a command is blocked, report it as a workflow/safety finding instead of trying to bypass it.";
|
|
866
1214
|
|
|
867
|
-
|
|
1215
|
+
return `You are Agent Workflow Audit, an isolated subagent running inside The Last Harness. Your job is to stress-test how efficiently an agent can operate in the current repository and return a concise final audit report to the parent session.
|
|
868
1216
|
|
|
869
1217
|
Working directory: ${options.cwd}
|
|
870
1218
|
Turn budget: ${options.maxTurns} turns total, including your final answer.
|
|
@@ -913,8 +1261,13 @@ Unresolved prerequisites, missing context, or checks that were unsafe/blocked/no
|
|
|
913
1261
|
1-5 concise next actions. If product code or tests need fixing, say that separately from instruction improvements.`;
|
|
914
1262
|
}
|
|
915
1263
|
|
|
916
|
-
function buildUserPrompt(options: {
|
|
917
|
-
|
|
1264
|
+
function buildUserPrompt(options: {
|
|
1265
|
+
cwd: string;
|
|
1266
|
+
focus?: string;
|
|
1267
|
+
initialGitStatus?: string;
|
|
1268
|
+
planOnly: boolean;
|
|
1269
|
+
}): string {
|
|
1270
|
+
return `Task: audit the current repository for agent workflow efficiency.
|
|
918
1271
|
|
|
919
1272
|
Local checkout: ${options.cwd}
|
|
920
1273
|
Mode: ${options.planOnly ? "plan-only (do not execute project commands)" : "execute safe documented/inferred workflow commands"}
|
|
@@ -926,341 +1279,377 @@ Read the repo instructions/manifests, try or plan the workflow according to the
|
|
|
926
1279
|
}
|
|
927
1280
|
|
|
928
1281
|
function formatToolCall(call: ToolCall): string {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1282
|
+
const args = asRecord(call.args) ?? {};
|
|
1283
|
+
if (call.name === "read") {
|
|
1284
|
+
const readPath = typeof args.path === "string" ? args.path : "";
|
|
1285
|
+
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
1286
|
+
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
1287
|
+
const range =
|
|
1288
|
+
offset || limit ? `:${offset ?? 1}${limit ? `-${(offset ?? 1) + limit - 1}` : ""}` : "";
|
|
1289
|
+
return `read ${readPath}${range}`.trim();
|
|
1290
|
+
}
|
|
1291
|
+
if (call.name === "grep") {
|
|
1292
|
+
return `grep ${shorten(String(args.pattern ?? ""), 60)} ${String(args.path ?? ".")}`.trim();
|
|
1293
|
+
}
|
|
1294
|
+
if (call.name === "find") {
|
|
1295
|
+
return `find ${shorten(String(args.pattern ?? ""), 60)} ${String(args.path ?? ".")}`.trim();
|
|
1296
|
+
}
|
|
1297
|
+
if (call.name === "ls") return `ls ${String(args.path ?? ".")}`.trim();
|
|
1298
|
+
if (call.name === "bash") return `bash ${shorten(String(args.command ?? ""), 140)}`.trim();
|
|
1299
|
+
return call.name;
|
|
946
1300
|
}
|
|
947
1301
|
|
|
948
|
-
function updateAuditUi(
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1302
|
+
function updateAuditUi(
|
|
1303
|
+
ctx: ExtensionCommandContext,
|
|
1304
|
+
details: AuditDetails | undefined,
|
|
1305
|
+
preview?: string,
|
|
1306
|
+
): void {
|
|
1307
|
+
if (!ctx.hasUI) return;
|
|
1308
|
+
if (!details || details.status !== "running") {
|
|
1309
|
+
ctx.ui.setStatus(STATUS_ID, undefined);
|
|
1310
|
+
ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const theme = ctx.ui.theme;
|
|
1315
|
+
const elapsed = formatDuration(Date.now() - details.startedAt);
|
|
1316
|
+
ctx.ui.setStatus(
|
|
1317
|
+
STATUS_ID,
|
|
1318
|
+
`${theme.fg("accent", "🧭 workflow audit")} ${theme.fg("dim", `${details.mode} · ${details.turns} turns · ${details.toolCalls.length} tools · ${elapsed}`)}`,
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
const lastTool = details.toolCalls[details.toolCalls.length - 1];
|
|
1322
|
+
const lines = [
|
|
1323
|
+
"🧭 Agent Workflow Audit",
|
|
1324
|
+
`${details.mode} · ${details.turns} turn(s) · ${details.toolCalls.length} tool call(s) · ${elapsed}`,
|
|
1325
|
+
];
|
|
1326
|
+
if (lastTool) lines.push(`last tool: ${formatToolCall(lastTool)}`);
|
|
1327
|
+
if (preview?.trim()) lines.push(`preview: ${shorten(preview, 120)}`);
|
|
1328
|
+
ctx.ui.setWidget(WIDGET_ID, lines, { placement: "belowEditor" });
|
|
971
1329
|
}
|
|
972
1330
|
|
|
973
1331
|
async function runAudit(
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1332
|
+
pi: ExtensionAPI,
|
|
1333
|
+
ctx: ExtensionCommandContext,
|
|
1334
|
+
options: { focus?: string; planOnly: boolean; initialGitStatus?: string },
|
|
977
1335
|
): Promise<{ report: string; details: AuditDetails }> {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1336
|
+
if (!ctx.model)
|
|
1337
|
+
throw new Error("/agent-workflow-audit needs an active model, but ctx.model is unavailable.");
|
|
1338
|
+
|
|
1339
|
+
const cwd = path.resolve(ctx.cwd);
|
|
1340
|
+
const mode: AuditMode = options.planOnly ? "plan-only" : "execute";
|
|
1341
|
+
const details: AuditDetails = {
|
|
1342
|
+
status: "running",
|
|
1343
|
+
mode,
|
|
1344
|
+
cwd,
|
|
1345
|
+
focus: options.focus,
|
|
1346
|
+
turns: 0,
|
|
1347
|
+
toolCalls: [],
|
|
1348
|
+
startedAt: Date.now(),
|
|
1349
|
+
initialGitStatus: options.initialGitStatus,
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
let lastContent = "(auditing agent workflow...)";
|
|
1353
|
+
let session: AgentSession | undefined;
|
|
1354
|
+
let unsubscribe: (() => void) | undefined;
|
|
1355
|
+
let runTimeout: NodeJS.Timeout | undefined;
|
|
1356
|
+
let abortListenerAdded = false;
|
|
1357
|
+
let aborted = Boolean(ctx.signal?.aborted);
|
|
1358
|
+
|
|
1359
|
+
const emit = () => updateAuditUi(ctx, details, lastContent);
|
|
1360
|
+
const abort = () => {
|
|
1361
|
+
aborted = true;
|
|
1362
|
+
details.status = "aborted";
|
|
1363
|
+
details.endedAt = Date.now();
|
|
1364
|
+
lastContent = "Aborted";
|
|
1365
|
+
emit();
|
|
1366
|
+
void session?.abort();
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
if (ctx.signal?.aborted) abort();
|
|
1370
|
+
if (ctx.signal && !ctx.signal.aborted) {
|
|
1371
|
+
ctx.signal.addEventListener("abort", abort);
|
|
1372
|
+
abortListenerAdded = true;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
try {
|
|
1376
|
+
emit();
|
|
1377
|
+
const isolatedSettingsManager = SettingsManager.inMemory({});
|
|
1378
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
1379
|
+
cwd,
|
|
1380
|
+
agentDir: getAgentDir(),
|
|
1381
|
+
settingsManager: isolatedSettingsManager,
|
|
1382
|
+
noExtensions: true,
|
|
1383
|
+
noSkills: true,
|
|
1384
|
+
noPromptTemplates: true,
|
|
1385
|
+
noThemes: true,
|
|
1386
|
+
noContextFiles: true,
|
|
1387
|
+
extensionFactories: [
|
|
1388
|
+
createAuditRuntimeGuardExtension({ cwd, maxTurns: MAX_TURNS, planOnly: options.planOnly }),
|
|
1389
|
+
],
|
|
1390
|
+
systemPromptOverride: () =>
|
|
1391
|
+
buildSystemPrompt({
|
|
1392
|
+
cwd,
|
|
1393
|
+
maxTurns: MAX_TURNS,
|
|
1394
|
+
maxRunSeconds: Math.round(MAX_RUN_MS / 1000),
|
|
1395
|
+
planOnly: options.planOnly,
|
|
1396
|
+
}),
|
|
1397
|
+
appendSystemPromptOverride: () => [],
|
|
1398
|
+
skillsOverride: () => ({ skills: [], diagnostics: [] }),
|
|
1399
|
+
promptsOverride: () => ({ prompts: [], diagnostics: [] }),
|
|
1400
|
+
themesOverride: () => ({ themes: [], diagnostics: [] }),
|
|
1401
|
+
agentsFilesOverride: () => ({ agentsFiles: [] }),
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
await resourceLoader.reload();
|
|
1405
|
+
|
|
1406
|
+
const tools = options.planOnly
|
|
1407
|
+
? ["read", "grep", "find", "ls"]
|
|
1408
|
+
: ["read", "grep", "find", "ls", "bash"];
|
|
1409
|
+
const created = await createAgentSession({
|
|
1410
|
+
cwd,
|
|
1411
|
+
...getModelRuntimeOption(ctx),
|
|
1412
|
+
resourceLoader,
|
|
1413
|
+
settingsManager: isolatedSettingsManager,
|
|
1414
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
1415
|
+
model: ctx.model,
|
|
1416
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
1417
|
+
tools,
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
session = created.session;
|
|
1421
|
+
unsubscribe = session.subscribe((event) => {
|
|
1422
|
+
switch (event.type) {
|
|
1423
|
+
case "message_update":
|
|
1424
|
+
if (event.assistantMessageEvent?.type === "text_delta") {
|
|
1425
|
+
lastContent += event.assistantMessageEvent.delta ?? "";
|
|
1426
|
+
emit();
|
|
1427
|
+
}
|
|
1428
|
+
break;
|
|
1429
|
+
case "turn_end":
|
|
1430
|
+
details.turns += 1;
|
|
1431
|
+
emit();
|
|
1432
|
+
break;
|
|
1433
|
+
case "tool_execution_start":
|
|
1434
|
+
details.toolCalls.push({
|
|
1435
|
+
id: event.toolCallId,
|
|
1436
|
+
name: event.toolName,
|
|
1437
|
+
args: event.args,
|
|
1438
|
+
startedAt: Date.now(),
|
|
1439
|
+
});
|
|
1440
|
+
if (details.toolCalls.length > MAX_TOOL_CALLS_TO_KEEP) {
|
|
1441
|
+
details.toolCalls.splice(0, details.toolCalls.length - MAX_TOOL_CALLS_TO_KEEP);
|
|
1442
|
+
}
|
|
1443
|
+
emit();
|
|
1444
|
+
break;
|
|
1445
|
+
case "tool_execution_end": {
|
|
1446
|
+
const call = details.toolCalls.find((item) => item.id === event.toolCallId);
|
|
1447
|
+
if (call) {
|
|
1448
|
+
call.endedAt = Date.now();
|
|
1449
|
+
call.isError = event.isError;
|
|
1450
|
+
}
|
|
1451
|
+
emit();
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
if (!aborted) {
|
|
1458
|
+
const promptPromise = session.prompt(
|
|
1459
|
+
buildUserPrompt({
|
|
1460
|
+
cwd,
|
|
1461
|
+
focus: options.focus,
|
|
1462
|
+
initialGitStatus: options.initialGitStatus,
|
|
1463
|
+
planOnly: options.planOnly,
|
|
1464
|
+
}),
|
|
1465
|
+
{ expandPromptTemplates: false },
|
|
1466
|
+
);
|
|
1467
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
1468
|
+
runTimeout = setTimeout(() => {
|
|
1469
|
+
abort();
|
|
1470
|
+
reject(
|
|
1471
|
+
new Error(
|
|
1472
|
+
`/agent-workflow-audit timed out after ${Math.round(MAX_RUN_MS / 1000)} seconds.`,
|
|
1473
|
+
),
|
|
1474
|
+
);
|
|
1475
|
+
}, MAX_RUN_MS);
|
|
1476
|
+
});
|
|
1477
|
+
await Promise.race([promptPromise, timeoutPromise]);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const outcome = session
|
|
1481
|
+
? inspectFinalAssistant(session.state.messages)
|
|
1482
|
+
: {
|
|
1483
|
+
ok: false as const,
|
|
1484
|
+
reason: "Agent Workflow Audit subagent produced no assistant message",
|
|
1485
|
+
};
|
|
1486
|
+
if (outcome.ok) {
|
|
1487
|
+
lastContent = outcome.answer;
|
|
1488
|
+
details.status = "done";
|
|
1489
|
+
} else if (aborted) {
|
|
1490
|
+
lastContent = "Aborted";
|
|
1491
|
+
details.status = "aborted";
|
|
1492
|
+
} else {
|
|
1493
|
+
throw new Error(outcome.reason);
|
|
1494
|
+
}
|
|
1495
|
+
details.endedAt = Date.now();
|
|
1496
|
+
details.reportLength = lastContent.length;
|
|
1497
|
+
emit();
|
|
1498
|
+
return { report: lastContent, details };
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
const wasAbort = aborted || isAbortLikeError(error);
|
|
1501
|
+
const message = wasAbort ? "Aborted" : error instanceof Error ? error.message : String(error);
|
|
1502
|
+
details.status = wasAbort ? "aborted" : "error";
|
|
1503
|
+
details.error = wasAbort ? undefined : message;
|
|
1504
|
+
details.endedAt = Date.now();
|
|
1505
|
+
lastContent = message;
|
|
1506
|
+
details.reportLength = lastContent.length;
|
|
1507
|
+
emit();
|
|
1508
|
+
return { report: `## Agent Workflow Audit failed\n\n${message}`, details };
|
|
1509
|
+
} finally {
|
|
1510
|
+
if (runTimeout) clearTimeout(runTimeout);
|
|
1511
|
+
if (ctx.signal && abortListenerAdded) ctx.signal.removeEventListener("abort", abort);
|
|
1512
|
+
unsubscribe?.();
|
|
1513
|
+
session?.dispose();
|
|
1514
|
+
}
|
|
1143
1515
|
}
|
|
1144
1516
|
|
|
1145
1517
|
export const __test__ = {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1518
|
+
appendRunBoundary,
|
|
1519
|
+
assertToolPathInsideCwd,
|
|
1520
|
+
buildSystemPrompt,
|
|
1521
|
+
buildUserPrompt,
|
|
1522
|
+
createAuditRuntimeGuardExtension,
|
|
1523
|
+
inspectFinalAssistant,
|
|
1524
|
+
formatToolCall,
|
|
1525
|
+
getBlockedBashReason,
|
|
1526
|
+
parseArgs,
|
|
1527
|
+
renderCollapsedReport,
|
|
1156
1528
|
};
|
|
1157
1529
|
|
|
1158
1530
|
export default function agentWorkflowAuditExtension(pi: ExtensionAPI) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1531
|
+
pi.registerMessageRenderer(CUSTOM_TYPE, (message, options, theme) => {
|
|
1532
|
+
const { expanded } = options;
|
|
1533
|
+
const rendererOptions = options as { outputPad?: number };
|
|
1534
|
+
const outputPad = rendererOptions.outputPad ?? 0;
|
|
1535
|
+
const details = message.details as AuditDetails | undefined;
|
|
1536
|
+
const report = messageContentToText(message.content) || "(no report)";
|
|
1537
|
+
const status = details?.status ?? "done";
|
|
1538
|
+
const icon =
|
|
1539
|
+
status === "done"
|
|
1540
|
+
? theme.fg("success", "✓")
|
|
1541
|
+
: status === "error"
|
|
1542
|
+
? theme.fg("error", "✗")
|
|
1543
|
+
: status === "aborted"
|
|
1544
|
+
? theme.fg("warning", "◼")
|
|
1545
|
+
: theme.fg("warning", "⏳");
|
|
1546
|
+
const duration = details?.endedAt
|
|
1547
|
+
? formatDuration(details.endedAt - details.startedAt)
|
|
1548
|
+
: undefined;
|
|
1549
|
+
const toolCount = details?.toolCallCount ?? details?.toolCalls.length ?? 0;
|
|
1550
|
+
const meta = details
|
|
1551
|
+
? `${details.mode} · ${details.turns} turns · ${toolCount} tools${duration ? ` · ${duration}` : ""}`
|
|
1552
|
+
: "final report";
|
|
1553
|
+
const header = `${icon} ${theme.fg("toolTitle", theme.bold("agent-workflow-audit "))}${theme.fg("dim", meta)}`;
|
|
1554
|
+
|
|
1555
|
+
if (!expanded) {
|
|
1556
|
+
return new Text(
|
|
1557
|
+
`${header}\n\n${theme.fg("toolOutput", renderCollapsedReport(report))}`,
|
|
1558
|
+
outputPad,
|
|
1559
|
+
0,
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const container = new Container();
|
|
1564
|
+
container.addChild(new Text(header, outputPad, 0));
|
|
1565
|
+
if (details?.cwd)
|
|
1566
|
+
container.addChild(new Text(theme.fg("dim", `cwd: ${details.cwd}`), outputPad, 0));
|
|
1567
|
+
if (details?.focus)
|
|
1568
|
+
container.addChild(new Text(theme.fg("dim", `focus: ${details.focus}`), outputPad, 0));
|
|
1569
|
+
container.addChild(new Spacer(1));
|
|
1570
|
+
container.addChild(new Markdown(report, outputPad, 0, getMarkdownTheme()));
|
|
1571
|
+
return container;
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
pi.registerCommand("agent-workflow-audit", {
|
|
1575
|
+
description:
|
|
1576
|
+
"Run an isolated repo workflow audit and return only the final report to this session",
|
|
1577
|
+
getArgumentCompletions: (prefix) => {
|
|
1578
|
+
const commands = ["--yes", "--plan-only", "--help"];
|
|
1579
|
+
const normalized = prefix.trim().toLowerCase();
|
|
1580
|
+
const matches = commands.filter((command) => command.startsWith(normalized));
|
|
1581
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
1582
|
+
},
|
|
1583
|
+
handler: async (args, ctx) => {
|
|
1584
|
+
const parsed = parseArgs(args);
|
|
1585
|
+
if (parsed.help) {
|
|
1586
|
+
notifyCommand(ctx, USAGE, "info");
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (parsed.error) {
|
|
1590
|
+
notifyCommand(ctx, parsed.error, "error");
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (!ctx.model) {
|
|
1594
|
+
notifyCommand(
|
|
1595
|
+
ctx,
|
|
1596
|
+
"/agent-workflow-audit needs an active model before it can run the isolated audit.",
|
|
1597
|
+
"error",
|
|
1598
|
+
);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (!parsed.planOnly && !parsed.yes) {
|
|
1602
|
+
if (!ctx.hasUI) {
|
|
1603
|
+
notifyCommand(
|
|
1604
|
+
ctx,
|
|
1605
|
+
`${USAGE}\n\nNon-interactive execution mode requires --yes or --plan-only.`,
|
|
1606
|
+
"warning",
|
|
1607
|
+
);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
const confirmed = await ctx.ui.confirm(
|
|
1611
|
+
"Run Agent Workflow Audit?",
|
|
1612
|
+
"An isolated subagent will read repo instructions and may execute documented setup/build/lint/test/run commands in the current checkout. Only the final audit report will be added to this session; intermediate command output, errors, retries, and tool transcript stay in the child session. Project commands may still create dependencies or build artifacts. Continue?",
|
|
1613
|
+
);
|
|
1614
|
+
if (!confirmed) return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
await ctx.waitForIdle();
|
|
1618
|
+
const initialGitStatus = await gitStatusShort(pi, ctx.cwd, ctx.signal).catch(() => undefined);
|
|
1619
|
+
let report = "";
|
|
1620
|
+
let details: AuditDetails | undefined;
|
|
1621
|
+
try {
|
|
1622
|
+
const result = await runAudit(pi, ctx, {
|
|
1623
|
+
focus: parsed.focus,
|
|
1624
|
+
planOnly: parsed.planOnly,
|
|
1625
|
+
initialGitStatus,
|
|
1626
|
+
});
|
|
1627
|
+
report = result.report;
|
|
1628
|
+
details = result.details;
|
|
1629
|
+
} finally {
|
|
1630
|
+
updateAuditUi(ctx, undefined);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (!details) return;
|
|
1634
|
+
details.finalGitStatus = await gitStatusShort(pi, ctx.cwd, ctx.signal).catch(() => undefined);
|
|
1635
|
+
report = appendRunBoundary(report, details);
|
|
1636
|
+
details.reportLength = report.length;
|
|
1637
|
+
const parentDetails: AuditDetails = {
|
|
1638
|
+
...details,
|
|
1639
|
+
toolCallCount: details.toolCalls.length,
|
|
1640
|
+
toolCalls: [],
|
|
1641
|
+
initialGitStatus: undefined,
|
|
1642
|
+
finalGitStatus: undefined,
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
pi.sendMessage({
|
|
1646
|
+
customType: CUSTOM_TYPE,
|
|
1647
|
+
content: report,
|
|
1648
|
+
display: true,
|
|
1649
|
+
details: parentDetails,
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
if (!ctx.hasUI) console.log(report);
|
|
1653
|
+
},
|
|
1654
|
+
});
|
|
1266
1655
|
}
|