specpi 0.11.2 → 0.13.0
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/CHANGELOG.md +21 -1
- package/NPM_RELEASE.md +3 -1
- package/README.md +41 -29
- package/SECURITY_MODEL.md +36 -0
- package/THIRD_PARTY.md +18 -2
- package/docs/browser-testing.md +76 -0
- package/docs/delegation/README.md +264 -0
- package/docs/delegation/design-protocol.md +382 -0
- package/docs/delegation/design.md +525 -0
- package/docs/delegation/evaluation.md +307 -0
- package/docs/delegation/protocol.md +271 -0
- package/docs/delegation/research.md +216 -0
- package/extensions/browser/core.d.mts +64 -0
- package/extensions/browser/diagnostics.ts +275 -0
- package/extensions/browser/index.ts +349 -57
- package/extensions/browser/interactions.ts +118 -0
- package/extensions/browser/lifecycle.ts +28 -0
- package/extensions/command-guard/index.ts +118 -33
- package/extensions/delegation/core.mjs +772 -0
- package/extensions/delegation/errors.mjs +8 -0
- package/extensions/delegation/extension.mjs +475 -0
- package/extensions/delegation/index.ts +9 -0
- package/extensions/delegation/managed-files.mjs +13 -0
- package/extensions/delegation/native.mjs +155 -0
- package/extensions/delegation/presentation.mjs +315 -0
- package/extensions/delegation/protocol.mjs +296 -0
- package/extensions/delegation/provider.mjs +689 -0
- package/extensions/delegation/snapshot.mjs +532 -0
- package/extensions/delegation/worker.mjs +218 -0
- package/extensions/tool-wishlist/verification.mjs +1 -0
- package/extensions/workflow-controls/index.ts +5 -1
- package/package.json +17 -4
- package/scripts/check-package.mjs +29 -3
- package/scripts/check-pi-package.mjs +5 -0
- package/scripts/check-syntax.mjs +61 -0
- package/scripts/run-browser-tests.mjs +61 -0
- package/scripts/setup-browser-tests.mjs +38 -0
- package/scripts/site-browser.mjs +272 -0
- package/scripts/specpi.mjs +24 -0
- package/site/logo.svg +1 -9
- package/templates/AGENTS.md +1 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
import type { Locator, Page } from "playwright";
|
|
4
|
+
import { boundedInteger } from "./diagnostics.ts";
|
|
5
|
+
import { normalizeBrowserUrl } from "./core.mjs";
|
|
6
|
+
|
|
7
|
+
const Target = Type.String({
|
|
8
|
+
minLength: 1,
|
|
9
|
+
maxLength: 2000,
|
|
10
|
+
description: "CSS selector, exact text= locator, or current snapshot ref. First match is used.",
|
|
11
|
+
});
|
|
12
|
+
const Timeout = Type.Optional(
|
|
13
|
+
Type.Integer({
|
|
14
|
+
minimum: 1,
|
|
15
|
+
maximum: 30000,
|
|
16
|
+
description: "Whole-operation deadline in milliseconds; default 5000.",
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
export const PressParams = Type.Object(
|
|
20
|
+
{ target: Type.Optional(Target), key: Type.String({ minLength: 1, maxLength: 80 }), timeoutMs: Timeout },
|
|
21
|
+
{ additionalProperties: false },
|
|
22
|
+
);
|
|
23
|
+
export const SelectionParams = Type.Object(
|
|
24
|
+
{
|
|
25
|
+
target: Target,
|
|
26
|
+
options: Type.Array(
|
|
27
|
+
Type.Object(
|
|
28
|
+
{
|
|
29
|
+
value: Type.Optional(Type.String({ maxLength: 1000 })),
|
|
30
|
+
label: Type.Optional(Type.String({ maxLength: 1000 })),
|
|
31
|
+
index: Type.Optional(Type.Integer({ minimum: 0, maximum: 10000 })),
|
|
32
|
+
},
|
|
33
|
+
{ additionalProperties: false },
|
|
34
|
+
),
|
|
35
|
+
{ minItems: 1, maxItems: 50 },
|
|
36
|
+
),
|
|
37
|
+
timeoutMs: Timeout,
|
|
38
|
+
},
|
|
39
|
+
{ additionalProperties: false },
|
|
40
|
+
);
|
|
41
|
+
export const WaitParams = Type.Object(
|
|
42
|
+
{
|
|
43
|
+
condition: StringEnum(["attached", "detached", "visible", "hidden", "text", "url"] as const),
|
|
44
|
+
target: Type.Optional(Target),
|
|
45
|
+
text: Type.Optional(Type.String({ maxLength: 2000 })),
|
|
46
|
+
url: Type.Optional(Type.String({ minLength: 1, maxLength: 2000 })),
|
|
47
|
+
timeoutMs: Timeout,
|
|
48
|
+
},
|
|
49
|
+
{ additionalProperties: false },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export function interactionTimeout(value?: number): number {
|
|
53
|
+
return boundedInteger(value, 5000, 30000);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function validateKey(key: string): void {
|
|
57
|
+
const parts = key.split("+");
|
|
58
|
+
// A literal plus key is also supported; chords use Playwright key names.
|
|
59
|
+
const final = key === "+" ? "+" : parts.pop()!;
|
|
60
|
+
if (key === "+") {
|
|
61
|
+
parts.length = 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
parts.some((part) => !["Control", "ControlOrMeta", "Alt", "Shift", "Meta"].includes(part)) ||
|
|
66
|
+
new Set(parts).size !== parts.length ||
|
|
67
|
+
!(
|
|
68
|
+
Array.from(final).length === 1 ||
|
|
69
|
+
/^(?:Tab|Enter|Escape|Backspace|Delete|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Home|End|PageUp|PageDown|Space|Insert|F(?:[1-9]|1[0-2]))$/u.test(
|
|
70
|
+
final,
|
|
71
|
+
)
|
|
72
|
+
) ||
|
|
73
|
+
/[\u0000-\u001f\u007f]/u.test(key)
|
|
74
|
+
) {
|
|
75
|
+
throw new Error("Unsupported browser key or chord.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function selectionOptions(params: Static<typeof SelectionParams>) {
|
|
80
|
+
for (const option of params.options) {
|
|
81
|
+
if (Object.keys(option).length !== 1) {
|
|
82
|
+
throw new Error("Each option must specify exactly one of value, label, or index.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return params.options;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function validateWait(params: Static<typeof WaitParams>): void {
|
|
90
|
+
if (params.condition === "url") {
|
|
91
|
+
if (params.url === undefined || params.target !== undefined || params.text !== undefined) {
|
|
92
|
+
throw new Error("URL waits require only url, not target or text.");
|
|
93
|
+
}
|
|
94
|
+
} else if (
|
|
95
|
+
!params.target?.trim() ||
|
|
96
|
+
params.url !== undefined ||
|
|
97
|
+
(params.condition === "text") !== (params.text !== undefined)
|
|
98
|
+
) {
|
|
99
|
+
throw new Error("Element waits require target; only text waits require text. Do not supply url.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function waitForCondition(
|
|
104
|
+
page: Page,
|
|
105
|
+
locator: Locator | undefined,
|
|
106
|
+
params: Static<typeof WaitParams>,
|
|
107
|
+
timeout: number,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
if (params.condition === "url") {
|
|
110
|
+
const expected = normalizeBrowserUrl(params.url!);
|
|
111
|
+
await page.waitForURL((url) => url.href === expected, { timeout, waitUntil: "commit" });
|
|
112
|
+
} else if (params.condition === "text") {
|
|
113
|
+
const literal = params.text!.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
114
|
+
await locator!.filter({ hasText: new RegExp(`^${literal}$`, "u") }).waitFor({ state: "attached", timeout });
|
|
115
|
+
} else {
|
|
116
|
+
await locator!.waitFor({ state: params.condition, timeout });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const CLEANUP_TIMEOUT_MS = 1000;
|
|
2
|
+
|
|
3
|
+
export class BrowserCleanupError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super(
|
|
6
|
+
"Browser cleanup failed or did not settle within 1000ms. The context was detached and diagnostics discarded, but a browser process may remain; inspect local browser processes before continuing.",
|
|
7
|
+
);
|
|
8
|
+
this.name = "BrowserCleanupError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Bound settlement without claiming that an unresponsive external process was terminated. */
|
|
13
|
+
export async function settleBrowserCleanup(operations: Promise<unknown>[]): Promise<void> {
|
|
14
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
15
|
+
try {
|
|
16
|
+
const result = await Promise.race([
|
|
17
|
+
Promise.allSettled(operations),
|
|
18
|
+
new Promise<never>((_resolve, reject) => {
|
|
19
|
+
timer = setTimeout(() => reject(new BrowserCleanupError()), CLEANUP_TIMEOUT_MS);
|
|
20
|
+
}),
|
|
21
|
+
]);
|
|
22
|
+
if (result.some((entry) => entry.status === "rejected")) {
|
|
23
|
+
throw new BrowserCleanupError();
|
|
24
|
+
}
|
|
25
|
+
} finally {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -18,6 +18,7 @@ type State = {
|
|
|
18
18
|
categories: Record<string, number>;
|
|
19
19
|
rules: Record<string, number>;
|
|
20
20
|
criticalRule?: string;
|
|
21
|
+
onModeChanged?: () => void;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
function validRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -138,12 +139,17 @@ function decisionPrompt(decision: any, cwd: string, affected: string): string {
|
|
|
138
139
|
return boundedReason(fields, 1200);
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
function lockSession(state: State): void {
|
|
143
|
+
state.mode = "locked";
|
|
144
|
+
state.generation += 1;
|
|
145
|
+
state.sessionApprovals.clear();
|
|
146
|
+
state.onModeChanged?.();
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
function deny(state: State, reason: string, critical = false): { block: true; reason: string } {
|
|
142
150
|
state.blocks += 1;
|
|
143
151
|
if (critical) {
|
|
144
|
-
state
|
|
145
|
-
state.generation += 1;
|
|
146
|
-
state.sessionApprovals.clear();
|
|
152
|
+
lockSession(state);
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
return { block: true, reason: boundedReason(reason) };
|
|
@@ -175,6 +181,34 @@ export default function registerCommandGuard(
|
|
|
175
181
|
categories: {},
|
|
176
182
|
rules: {},
|
|
177
183
|
};
|
|
184
|
+
state.onModeChanged = () => pi.events?.emit("specpi:guard-policy-changed", { reason: "guard policy changed" });
|
|
185
|
+
let guardStateSubscription: (() => void) | undefined;
|
|
186
|
+
const subscribeGuardState = () => {
|
|
187
|
+
if (!guardStateSubscription) {
|
|
188
|
+
guardStateSubscription = pi.events?.on?.("specpi:guard-state", (request: any) => {
|
|
189
|
+
request.reply({ mode: state.ready && !state.startupFailed ? state.mode : undefined });
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
subscribeGuardState();
|
|
195
|
+
const delegationPolicy = (input: unknown): { fingerprint: string; summary: string } | undefined => {
|
|
196
|
+
let replies = 0;
|
|
197
|
+
let policy: any;
|
|
198
|
+
pi.events?.emit("specpi:delegation-policy", {
|
|
199
|
+
input,
|
|
200
|
+
reply(value: any) {
|
|
201
|
+
replies += 1;
|
|
202
|
+
policy = value;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
if (replies !== 1 || !/^[a-f0-9]{64}$/u.test(policy?.fingerprint) || typeof policy?.summary !== "string") {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { fingerprint: policy.fingerprint, summary: boundedReason(policy.summary, 1600) };
|
|
210
|
+
};
|
|
211
|
+
|
|
178
212
|
const reset = () => {
|
|
179
213
|
clearAnalysisCache();
|
|
180
214
|
state.mode = "guard";
|
|
@@ -192,10 +226,17 @@ export default function registerCommandGuard(
|
|
|
192
226
|
|
|
193
227
|
pi.on("session_start", async (_event, ctx) => {
|
|
194
228
|
reset();
|
|
229
|
+
const startupGeneration = state.generation;
|
|
195
230
|
try {
|
|
231
|
+
subscribeGuardState();
|
|
196
232
|
const choice = ctx.hasUI ? await startupChoice(ctx, startupTimeoutMs) : undefined;
|
|
233
|
+
if (state.generation !== startupGeneration) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let mode: "guard" | "strict" | "off" = "guard";
|
|
197
238
|
if (choice === "Strict") {
|
|
198
|
-
|
|
239
|
+
mode = "strict";
|
|
199
240
|
} else if (choice === "Off for this session") {
|
|
200
241
|
const confirmed = ctx.hasUI
|
|
201
242
|
? await withTimeout(
|
|
@@ -207,10 +248,15 @@ export default function registerCommandGuard(
|
|
|
207
248
|
startupTimeoutMs,
|
|
208
249
|
)
|
|
209
250
|
: false;
|
|
210
|
-
state.
|
|
251
|
+
if (state.generation !== startupGeneration) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
mode = confirmed ? "off" : "guard";
|
|
211
256
|
}
|
|
212
257
|
|
|
213
|
-
state.
|
|
258
|
+
state.mode = mode;
|
|
259
|
+
state.baseMode = mode;
|
|
214
260
|
state.ready = true;
|
|
215
261
|
state.startupFailed = false;
|
|
216
262
|
updateStatus(ctx, state);
|
|
@@ -221,6 +267,10 @@ export default function registerCommandGuard(
|
|
|
221
267
|
state.mode === "off" ? "warning" : "info",
|
|
222
268
|
);
|
|
223
269
|
} catch {
|
|
270
|
+
if (state.generation !== startupGeneration) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
224
274
|
state.startupFailed = true;
|
|
225
275
|
state.ready = false;
|
|
226
276
|
try {
|
|
@@ -232,6 +282,11 @@ export default function registerCommandGuard(
|
|
|
232
282
|
});
|
|
233
283
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
234
284
|
reset();
|
|
285
|
+
if (typeof guardStateSubscription === "function") {
|
|
286
|
+
guardStateSubscription();
|
|
287
|
+
guardStateSubscription = undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
235
290
|
try {
|
|
236
291
|
ctx.ui.setStatus("specpi-command-guard", undefined);
|
|
237
292
|
} catch {
|
|
@@ -256,6 +311,12 @@ export default function registerCommandGuard(
|
|
|
256
311
|
return;
|
|
257
312
|
}
|
|
258
313
|
|
|
314
|
+
if (!["guard", "strict", "off", "unlock", "clear-approvals"].includes(action)) {
|
|
315
|
+
ctx.ui.notify("Usage: /guard [status|guard|strict|off|unlock|clear-approvals]", "error");
|
|
316
|
+
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
259
320
|
if (state.mode === "locked" && action !== "unlock") {
|
|
260
321
|
ctx.ui.notify(
|
|
261
322
|
"The command guard is locked. Use /guard unlock after reviewing the critical rule.",
|
|
@@ -268,6 +329,7 @@ export default function registerCommandGuard(
|
|
|
268
329
|
if (action === "clear-approvals") {
|
|
269
330
|
state.sessionApprovals.clear();
|
|
270
331
|
state.generation += 1;
|
|
332
|
+
state.onModeChanged?.();
|
|
271
333
|
ctx.ui.notify("Session approvals cleared.", "info");
|
|
272
334
|
|
|
273
335
|
return;
|
|
@@ -280,6 +342,7 @@ export default function registerCommandGuard(
|
|
|
280
342
|
return;
|
|
281
343
|
}
|
|
282
344
|
|
|
345
|
+
const approvalGeneration = state.generation;
|
|
283
346
|
const ok =
|
|
284
347
|
ctx.hasUI &&
|
|
285
348
|
(await withTimeout(
|
|
@@ -290,11 +353,12 @@ export default function registerCommandGuard(
|
|
|
290
353
|
false,
|
|
291
354
|
approvalTimeoutMs,
|
|
292
355
|
));
|
|
293
|
-
if (ok) {
|
|
356
|
+
if (ok && state.generation === approvalGeneration) {
|
|
294
357
|
state.mode = state.baseMode;
|
|
295
358
|
state.generation += 1;
|
|
296
359
|
state.sessionApprovals.clear();
|
|
297
360
|
state.criticalRule = undefined;
|
|
361
|
+
state.onModeChanged?.();
|
|
298
362
|
updateStatus(ctx, state);
|
|
299
363
|
ctx.ui.notify(`Command guard unlocked in ${state.baseMode} mode.`, "warning");
|
|
300
364
|
}
|
|
@@ -303,6 +367,11 @@ export default function registerCommandGuard(
|
|
|
303
367
|
}
|
|
304
368
|
|
|
305
369
|
if (action === "off") {
|
|
370
|
+
if (state.mode === "off") {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const approvalGeneration = state.generation;
|
|
306
375
|
if (
|
|
307
376
|
!ctx.hasUI ||
|
|
308
377
|
!(await withTimeout(
|
|
@@ -312,7 +381,8 @@ export default function registerCommandGuard(
|
|
|
312
381
|
),
|
|
313
382
|
false,
|
|
314
383
|
approvalTimeoutMs,
|
|
315
|
-
))
|
|
384
|
+
)) ||
|
|
385
|
+
state.generation !== approvalGeneration
|
|
316
386
|
) {
|
|
317
387
|
return;
|
|
318
388
|
}
|
|
@@ -321,24 +391,31 @@ export default function registerCommandGuard(
|
|
|
321
391
|
state.baseMode = "off";
|
|
322
392
|
state.generation += 1;
|
|
323
393
|
state.sessionApprovals.clear();
|
|
394
|
+
state.onModeChanged?.();
|
|
324
395
|
updateStatus(ctx, state);
|
|
325
396
|
|
|
326
397
|
return;
|
|
327
398
|
}
|
|
328
399
|
|
|
329
400
|
if (action === "strict" || action === "guard") {
|
|
401
|
+
if (state.mode === action) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const approvalGeneration = state.generation;
|
|
330
406
|
if (
|
|
331
|
-
action === "guard" &&
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
407
|
+
(action === "guard" &&
|
|
408
|
+
state.mode === "strict" &&
|
|
409
|
+
(!ctx.hasUI ||
|
|
410
|
+
!(await withTimeout(
|
|
411
|
+
ctx.ui.confirm(
|
|
412
|
+
"Switch to Guard mode?",
|
|
413
|
+
"This weakens protection for the rest of this session.",
|
|
414
|
+
),
|
|
415
|
+
false,
|
|
416
|
+
approvalTimeoutMs,
|
|
417
|
+
)))) ||
|
|
418
|
+
state.generation !== approvalGeneration
|
|
342
419
|
) {
|
|
343
420
|
return;
|
|
344
421
|
}
|
|
@@ -347,12 +424,11 @@ export default function registerCommandGuard(
|
|
|
347
424
|
state.baseMode = action;
|
|
348
425
|
state.generation += 1;
|
|
349
426
|
state.sessionApprovals.clear();
|
|
427
|
+
state.onModeChanged?.();
|
|
350
428
|
updateStatus(ctx, state);
|
|
351
429
|
|
|
352
430
|
return;
|
|
353
431
|
}
|
|
354
|
-
|
|
355
|
-
ctx.ui.notify("Usage: /guard [status|guard|strict|off|unlock|clear-approvals]", "error");
|
|
356
432
|
},
|
|
357
433
|
});
|
|
358
434
|
|
|
@@ -462,9 +538,7 @@ export default function registerCommandGuard(
|
|
|
462
538
|
}
|
|
463
539
|
|
|
464
540
|
if (answer === "Lock session") {
|
|
465
|
-
state
|
|
466
|
-
state.generation += 1;
|
|
467
|
-
state.sessionApprovals.clear();
|
|
541
|
+
lockSession(state);
|
|
468
542
|
updateStatus(ctx, state);
|
|
469
543
|
|
|
470
544
|
return deny(state, "The session was locked by command-guard approval.");
|
|
@@ -557,9 +631,7 @@ export default function registerCommandGuard(
|
|
|
557
631
|
}
|
|
558
632
|
|
|
559
633
|
if (answer === "Lock session") {
|
|
560
|
-
state
|
|
561
|
-
state.generation += 1;
|
|
562
|
-
state.sessionApprovals.clear();
|
|
634
|
+
lockSession(state);
|
|
563
635
|
updateStatus(ctx, state);
|
|
564
636
|
|
|
565
637
|
return deny(state, "The session was locked by command-guard approval.");
|
|
@@ -573,7 +645,15 @@ export default function registerCommandGuard(
|
|
|
573
645
|
|
|
574
646
|
if (state.mode === "strict") {
|
|
575
647
|
recordDecision(state, { category: "unknown", ruleIds: ["tool.unknown-capability"] });
|
|
576
|
-
const
|
|
648
|
+
const capability = name === "delegate" ? delegationPolicy(input) : undefined;
|
|
649
|
+
if (name === "delegate" && !capability) {
|
|
650
|
+
return deny(state, "Delegation policy is unavailable or ambiguous; execution is denied.");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const effectiveInput = capability
|
|
654
|
+
? { input, delegationPolicyFingerprint: capability.fingerprint }
|
|
655
|
+
: input;
|
|
656
|
+
const approvalFingerprint = toolFingerprint(name, effectiveInput, ctx.cwd, state.mode);
|
|
577
657
|
if (!approvalFingerprint) {
|
|
578
658
|
return deny(state, "Unknown-tool approval input is malformed or exceeds the safety bound.");
|
|
579
659
|
}
|
|
@@ -589,7 +669,7 @@ export default function registerCommandGuard(
|
|
|
589
669
|
const approvalGeneration = state.generation;
|
|
590
670
|
const answer = await withTimeout(
|
|
591
671
|
ctx.ui.select(
|
|
592
|
-
`Unknown tool approval — name: ${boundedReason(name, 96)}; mode: ${state.mode}; capability is not in the reviewed command-guard catalog
|
|
672
|
+
`Unknown tool approval — name: ${boundedReason(name, 96)}; mode: ${state.mode}; ${capability?.summary ?? "capability is not in the reviewed command-guard catalog."}`,
|
|
593
673
|
["Deny (Recommended)", "Allow once", "Allow exact call for session", "Lock session"],
|
|
594
674
|
),
|
|
595
675
|
undefined,
|
|
@@ -598,7 +678,14 @@ export default function registerCommandGuard(
|
|
|
598
678
|
if (
|
|
599
679
|
state.generation !== approvalGeneration ||
|
|
600
680
|
state.mode === "locked" ||
|
|
601
|
-
toolFingerprint(
|
|
681
|
+
toolFingerprint(
|
|
682
|
+
name,
|
|
683
|
+
capability
|
|
684
|
+
? { input, delegationPolicyFingerprint: delegationPolicy(input)?.fingerprint }
|
|
685
|
+
: input,
|
|
686
|
+
ctx.cwd,
|
|
687
|
+
state.mode,
|
|
688
|
+
) !== approvalFingerprint
|
|
602
689
|
) {
|
|
603
690
|
return deny(state, "Command-guard state or input changed during approval; execution is denied.");
|
|
604
691
|
}
|
|
@@ -619,9 +706,7 @@ export default function registerCommandGuard(
|
|
|
619
706
|
}
|
|
620
707
|
|
|
621
708
|
if (answer === "Lock session") {
|
|
622
|
-
state
|
|
623
|
-
state.generation += 1;
|
|
624
|
-
state.sessionApprovals.clear();
|
|
709
|
+
lockSession(state);
|
|
625
710
|
updateStatus(ctx, state);
|
|
626
711
|
}
|
|
627
712
|
|