@gotgenes/pi-permission-system 25.2.2 → 25.4.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 +30 -0
- package/README.md +1 -1
- package/config/config.example.json +3 -0
- package/dist/public.d.ts +162 -41
- package/docs/configuration.md +17 -2
- package/docs/cross-extension-api.md +14 -9
- package/package.json +1 -1
- package/schemas/permissions.schema.json +16 -0
- package/src/access-intent/bash/command-enumeration.ts +45 -117
- package/src/access-intent/bash/wrapper-analysis.ts +335 -0
- package/src/authority/approval-escalator.ts +26 -1
- package/src/authority/forwarded-request-server.ts +5 -14
- package/src/authority/local-user-authorizer.ts +2 -3
- package/src/authority/permission-prompt-component.ts +87 -47
- package/src/authority/permission-prompter.ts +9 -0
- package/src/config-loader.ts +2 -0
- package/src/config-schema.ts +14 -0
- package/src/extension-config.ts +10 -0
- package/src/handlers/gates/bash-command.ts +7 -2
- package/src/handlers/gates/bash-external-directory.ts +11 -6
- package/src/handlers/gates/bash-path.ts +10 -6
- package/src/handlers/gates/descriptor.ts +10 -1
- package/src/handlers/gates/external-directory.ts +13 -8
- package/src/handlers/gates/helpers.ts +6 -7
- package/src/handlers/gates/path.ts +11 -14
- package/src/handlers/gates/runner.ts +40 -19
- package/src/handlers/gates/skill-input-gate-pipeline.ts +1 -12
- package/src/handlers/gates/skill-input.ts +5 -2
- package/src/handlers/gates/skill-read.ts +6 -6
- package/src/handlers/gates/tool-call-gate-pipeline.ts +1 -5
- package/src/handlers/gates/tool.ts +10 -5
- package/src/handlers/tool-call-boundary.ts +30 -7
- package/src/index.ts +2 -0
- package/src/permission-events.ts +6 -0
- package/src/permission-prompts.ts +4 -72
- package/src/permission-request-id.ts +17 -0
- package/src/presentation/dialog-renderer.ts +404 -0
- package/src/presentation/forwarded-ask-payload.ts +45 -0
- package/src/presentation/legacy-message.ts +117 -0
- package/src/presentation/line-fitting.ts +27 -0
- package/src/presentation/path-ask-payload.ts +128 -0
- package/src/presentation/prompt-payload.ts +137 -0
- package/src/presentation/skill-ask-payload.ts +50 -0
- package/src/presentation/tool-ask-payload.ts +104 -0
- package/src/tool-preview-formatter.ts +1 -1
- package/src/types.ts +6 -0
- package/src/handlers/gates/external-directory-messages.ts +0 -28
|
@@ -3,12 +3,7 @@ import type {
|
|
|
3
3
|
ExtensionUIContext,
|
|
4
4
|
KeybindingsManager,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
type Component,
|
|
8
|
-
matchesKey,
|
|
9
|
-
truncateToWidth,
|
|
10
|
-
wrapTextWithAnsi,
|
|
11
|
-
} from "@earendil-works/pi-tui";
|
|
6
|
+
import { type Component, matchesKey } from "@earendil-works/pi-tui";
|
|
12
7
|
import {
|
|
13
8
|
type PermissionPromptDecision,
|
|
14
9
|
type RequestPermissionOptions,
|
|
@@ -22,6 +17,14 @@ import {
|
|
|
22
17
|
type PromptViewState,
|
|
23
18
|
reducePrompt,
|
|
24
19
|
} from "#src/authority/permission-prompt-decision";
|
|
20
|
+
import {
|
|
21
|
+
completeViewBudget,
|
|
22
|
+
type DialogView,
|
|
23
|
+
type RenderBudget,
|
|
24
|
+
renderPromptDialog,
|
|
25
|
+
} from "#src/presentation/dialog-renderer";
|
|
26
|
+
import { fitLinesToWidth } from "#src/presentation/line-fitting";
|
|
27
|
+
import type { PromptPayload } from "#src/presentation/prompt-payload";
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Inline `ctx.ui.custom` permission dialog for TUI sessions.
|
|
@@ -43,15 +46,16 @@ export type PermissionPromptUi = Pick<
|
|
|
43
46
|
type PromptKeybindings = Pick<KeybindingsManager, "matches">;
|
|
44
47
|
|
|
45
48
|
/** The resolved presentation context selected once per activation. */
|
|
46
|
-
export interface PermissionPromptView {
|
|
49
|
+
export interface PermissionPromptView extends PromptPreferences {
|
|
47
50
|
mode: ExtensionContext["mode"];
|
|
48
51
|
ui: PermissionPromptUi;
|
|
49
|
-
doublePressToConfirm: boolean;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/** Live prompt-behavior preferences read at prompt time (see `doublePressToConfirm`). */
|
|
53
55
|
export interface PromptPreferences {
|
|
54
56
|
doublePressToConfirm: boolean;
|
|
57
|
+
/** How much room a render has; the terminal width is added per frame. */
|
|
58
|
+
budget: RenderBudget;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/**
|
|
@@ -64,15 +68,30 @@ export interface PromptPreferences {
|
|
|
64
68
|
export function requestPermissionDecision(
|
|
65
69
|
view: PermissionPromptView,
|
|
66
70
|
title: string,
|
|
67
|
-
|
|
71
|
+
payload: PromptPayload,
|
|
68
72
|
options?: RequestPermissionOptions,
|
|
69
73
|
): Promise<PermissionPromptDecision> {
|
|
70
74
|
if (view.mode === "tui") {
|
|
71
|
-
return presentInlinePermissionPrompt(view, title,
|
|
75
|
+
return presentInlinePermissionPrompt(view, title, payload, options);
|
|
72
76
|
}
|
|
73
|
-
|
|
77
|
+
// The fallback renders once and cannot re-render, so it neither paints nor
|
|
78
|
+
// offers an expansion; it substitutes a nominal width for the terminal size
|
|
79
|
+
// it is never told, and the host's own select wraps from there.
|
|
80
|
+
const rendered = renderPromptDialog(payload, {
|
|
81
|
+
...view.budget,
|
|
82
|
+
width: FALLBACK_RENDER_WIDTH,
|
|
83
|
+
});
|
|
84
|
+
return requestPermissionDecisionFromUi(
|
|
85
|
+
view.ui,
|
|
86
|
+
title,
|
|
87
|
+
rendered.lines.join("\n"),
|
|
88
|
+
options,
|
|
89
|
+
);
|
|
74
90
|
}
|
|
75
91
|
|
|
92
|
+
/** The width the `select`/`input` fallback renders against. */
|
|
93
|
+
const FALLBACK_RENDER_WIDTH = 80;
|
|
94
|
+
|
|
76
95
|
/** Minimal theme surface the dialog uses; satisfied by the real SDK theme. */
|
|
77
96
|
interface PromptTheme {
|
|
78
97
|
fg(color: string, text: string): string;
|
|
@@ -92,7 +111,7 @@ const OPTION_ORDER: readonly PromptKey[] = ["y", "s", "n", "r"];
|
|
|
92
111
|
export function presentInlinePermissionPrompt(
|
|
93
112
|
view: PermissionPromptView,
|
|
94
113
|
title: string,
|
|
95
|
-
|
|
114
|
+
payload: PromptPayload,
|
|
96
115
|
options?: RequestPermissionOptions,
|
|
97
116
|
): Promise<PermissionPromptDecision> {
|
|
98
117
|
const config: PromptModelConfig = {
|
|
@@ -106,7 +125,8 @@ export function presentInlinePermissionPrompt(
|
|
|
106
125
|
theme,
|
|
107
126
|
config,
|
|
108
127
|
title,
|
|
109
|
-
|
|
128
|
+
payload,
|
|
129
|
+
view.budget,
|
|
110
130
|
(data) => handleToolsExpandAction(data, keybindings, view.ui),
|
|
111
131
|
() => {
|
|
112
132
|
tui.requestRender();
|
|
@@ -145,12 +165,15 @@ function handleToolsExpandAction(
|
|
|
145
165
|
class PermissionPromptComponent implements Component {
|
|
146
166
|
private state: PromptViewState;
|
|
147
167
|
private reasonBuffer = "";
|
|
168
|
+
/** Whether the operator asked to see the complete request (ADR 0011 §4). */
|
|
169
|
+
private expanded = false;
|
|
148
170
|
|
|
149
171
|
constructor(
|
|
150
172
|
private readonly theme: PromptTheme,
|
|
151
173
|
private readonly config: PromptModelConfig,
|
|
152
174
|
private readonly title: string,
|
|
153
|
-
private readonly
|
|
175
|
+
private readonly payload: PromptPayload,
|
|
176
|
+
private readonly budget: RenderBudget,
|
|
154
177
|
private readonly handleAppAction: (data: string) => boolean,
|
|
155
178
|
private readonly requestRender: () => void,
|
|
156
179
|
private readonly done: (decision: PermissionPromptDecision) => void,
|
|
@@ -163,26 +186,66 @@ class PermissionPromptComponent implements Component {
|
|
|
163
186
|
}
|
|
164
187
|
|
|
165
188
|
render(width: number): string[] {
|
|
166
|
-
return
|
|
189
|
+
return fitLinesToWidth(this.renderStep(width), width);
|
|
167
190
|
}
|
|
168
191
|
|
|
169
|
-
private renderStep(): string[] {
|
|
192
|
+
private renderStep(width: number): string[] {
|
|
170
193
|
switch (this.state.step) {
|
|
171
194
|
case "decision":
|
|
172
|
-
return this.renderDecision();
|
|
195
|
+
return this.renderDecision(width);
|
|
173
196
|
case "reason":
|
|
174
|
-
return this.renderReason();
|
|
197
|
+
return this.renderReason(width);
|
|
175
198
|
case "scope":
|
|
176
199
|
return this.renderScope();
|
|
177
200
|
}
|
|
178
201
|
}
|
|
179
202
|
|
|
203
|
+
/**
|
|
204
|
+
* The ask itself, bounded to the budget at this frame's width.
|
|
205
|
+
*
|
|
206
|
+
* Rendered per frame rather than once, because the row budget is a function
|
|
207
|
+
* of the width the host gives us, which a resize changes.
|
|
208
|
+
*/
|
|
209
|
+
private renderAsk(width: number): DialogView {
|
|
210
|
+
return renderPromptDialog(
|
|
211
|
+
this.payload,
|
|
212
|
+
this.expanded ? completeViewBudget(width) : { ...this.budget, width },
|
|
213
|
+
(text) => this.theme.fg("warning", text),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The key hints, naming the expansion only when it would do something.
|
|
219
|
+
*
|
|
220
|
+
* An affordance advertised when there is nothing to expand is noise; one
|
|
221
|
+
* left unadvertised when the render dropped something is a decision made
|
|
222
|
+
* without the evidence.
|
|
223
|
+
*/
|
|
224
|
+
private hint(view: DialogView): string {
|
|
225
|
+
const keys = [
|
|
226
|
+
"↑/↓ move",
|
|
227
|
+
"enter confirm",
|
|
228
|
+
"esc deny",
|
|
229
|
+
"press a letter, then again to confirm",
|
|
230
|
+
];
|
|
231
|
+
if (this.expanded) {
|
|
232
|
+
keys.push("ctrl+o collapse");
|
|
233
|
+
} else if (view.elided) {
|
|
234
|
+
keys.push("ctrl+o full request");
|
|
235
|
+
}
|
|
236
|
+
return this.theme.fg("muted", keys.join(" · "));
|
|
237
|
+
}
|
|
238
|
+
|
|
180
239
|
handleInput(data: string): void {
|
|
181
240
|
if (this.state.step === "reason") {
|
|
182
241
|
this.handleReasonInput(data);
|
|
183
242
|
return;
|
|
184
243
|
}
|
|
185
244
|
if (this.handleAppAction(data)) {
|
|
245
|
+
// One "expand" for the operator: the host expands its pending tool call
|
|
246
|
+
// and the dialog expands its own render, on the same keystroke.
|
|
247
|
+
this.expanded = !this.expanded;
|
|
248
|
+
this.requestRender();
|
|
186
249
|
return;
|
|
187
250
|
}
|
|
188
251
|
const event = this.toEvent(data);
|
|
@@ -247,8 +310,9 @@ class PermissionPromptComponent implements Component {
|
|
|
247
310
|
this.requestRender();
|
|
248
311
|
}
|
|
249
312
|
|
|
250
|
-
private renderDecision(): string[] {
|
|
251
|
-
const
|
|
313
|
+
private renderDecision(width: number): string[] {
|
|
314
|
+
const ask = this.renderAsk(width);
|
|
315
|
+
const lines = [this.theme.fg("accent", this.title), ...ask.lines, ""];
|
|
252
316
|
for (const key of OPTION_ORDER) {
|
|
253
317
|
const label = key === "s" ? this.config.sessionLabel : OPTION_LABELS[key];
|
|
254
318
|
const selected = this.state.highlightedKey === key;
|
|
@@ -257,20 +321,14 @@ class PermissionPromptComponent implements Component {
|
|
|
257
321
|
lines.push(selected ? this.theme.fg("accent", row) : row);
|
|
258
322
|
}
|
|
259
323
|
lines.push("");
|
|
260
|
-
lines.push(
|
|
261
|
-
this.state.hint ||
|
|
262
|
-
this.theme.fg(
|
|
263
|
-
"muted",
|
|
264
|
-
"↑/↓ move · enter confirm · esc deny · press a letter, then again to confirm",
|
|
265
|
-
),
|
|
266
|
-
);
|
|
324
|
+
lines.push(this.state.hint || this.hint(ask));
|
|
267
325
|
return lines;
|
|
268
326
|
}
|
|
269
327
|
|
|
270
|
-
private renderReason(): string[] {
|
|
328
|
+
private renderReason(width: number): string[] {
|
|
271
329
|
const lines = [
|
|
272
330
|
this.theme.fg("accent", this.title),
|
|
273
|
-
this.
|
|
331
|
+
...this.renderAsk(width).lines,
|
|
274
332
|
"",
|
|
275
333
|
`Reason (required): ${this.reasonBuffer}\u2588`,
|
|
276
334
|
];
|
|
@@ -307,24 +365,6 @@ class PermissionPromptComponent implements Component {
|
|
|
307
365
|
}
|
|
308
366
|
}
|
|
309
367
|
|
|
310
|
-
/**
|
|
311
|
-
* Fit rendered lines to the terminal width, satisfying the `ctx.ui.custom`
|
|
312
|
-
* contract that every returned line be a single visual row no wider than
|
|
313
|
-
* `width`. Long lines (e.g. a wide tool-preview message) are wrapped rather
|
|
314
|
-
* than clipped so no content is lost; the final `truncateToWidth` guards the
|
|
315
|
-
* edge cases `wrapTextWithAnsi` cannot split (a lone wide grapheme).
|
|
316
|
-
*/
|
|
317
|
-
function fitToWidth(lines: string[], width: number): string[] {
|
|
318
|
-
if (width <= 0) {
|
|
319
|
-
return [];
|
|
320
|
-
}
|
|
321
|
-
return lines.flatMap((line) =>
|
|
322
|
-
wrapTextWithAnsi(line, width).map((wrapped) =>
|
|
323
|
-
truncateToWidth(wrapped, width),
|
|
324
|
-
),
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
368
|
function isPrintable(data: string): boolean {
|
|
329
369
|
if (data.length !== 1) {
|
|
330
370
|
return false;
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ForwardedAccessFacts,
|
|
4
4
|
ForwardedSessionApproval,
|
|
5
5
|
} from "#src/authority/permission-forwarding";
|
|
6
|
+
import type { PromptPayload } from "#src/presentation/prompt-payload";
|
|
6
7
|
import type { ReviewLogger } from "#src/session-logger";
|
|
7
8
|
import type { TerminalAuthorizer } from "./authorizer";
|
|
8
9
|
|
|
@@ -27,6 +28,14 @@ export interface PromptPermissionDetails {
|
|
|
27
28
|
source: PermissionReviewSource;
|
|
28
29
|
agentName: string | null;
|
|
29
30
|
message: string;
|
|
31
|
+
/**
|
|
32
|
+
* The complete structured description of this ask (ADR 0011 §2).
|
|
33
|
+
*
|
|
34
|
+
* Required: every ask carries one, and the type is what guarantees it rather
|
|
35
|
+
* than a convention each gate has to remember. `message` is a render over it
|
|
36
|
+
* for the duration of the transition, so the two cannot disagree.
|
|
37
|
+
*/
|
|
38
|
+
payload: PromptPayload;
|
|
30
39
|
toolCallId?: string;
|
|
31
40
|
toolName?: string;
|
|
32
41
|
skillName?: string;
|
package/src/config-loader.ts
CHANGED
|
@@ -222,6 +222,8 @@ export function mergeUnifiedConfigs(
|
|
|
222
222
|
// Number scalars: override replaces base when defined
|
|
223
223
|
for (const key of [
|
|
224
224
|
"forwardingTimeoutMs",
|
|
225
|
+
"promptMaxRows",
|
|
226
|
+
"promptFieldMaxWidth",
|
|
225
227
|
"toolInputPreviewMaxLength",
|
|
226
228
|
"toolTextSummaryMaxLength",
|
|
227
229
|
] as const) {
|
package/src/config-schema.ts
CHANGED
|
@@ -194,6 +194,20 @@ export const unifiedConfigSchema = z
|
|
|
194
194
|
"How long a subagent waits for the parent session to answer a forwarded permission request, in milliseconds.\n\nOmit to use the default (`600000`, ten minutes). A child whose in-process parent is not draining its inbox at all gives up in a couple of seconds regardless of this value, so lower it only to bound how long you are willing to leave an *unanswered* prompt pending.",
|
|
195
195
|
default: 600000,
|
|
196
196
|
}),
|
|
197
|
+
promptMaxRows: z.number().int().min(1).optional().meta({
|
|
198
|
+
description:
|
|
199
|
+
"Maximum rows a permission prompt renders before eliding its evidence. Omit to use the default (24).",
|
|
200
|
+
markdownDescription:
|
|
201
|
+
"Maximum rows a permission prompt renders before eliding its evidence.\n\nOmit to use the default (24). The request's own facts — the requesting agent, the tool, the matched rule, the decision-relevant value — are never elided by this budget; what gives way is the supporting evidence, and `Ctrl+O` expands the prompt to the complete request.",
|
|
202
|
+
default: 24,
|
|
203
|
+
}),
|
|
204
|
+
promptFieldMaxWidth: z.number().int().min(1).optional().meta({
|
|
205
|
+
description:
|
|
206
|
+
"Maximum characters of any one field shown in a permission prompt. Omit to use the default (400).",
|
|
207
|
+
markdownDescription:
|
|
208
|
+
"Maximum characters of any one field shown in a permission prompt.\n\nOmit to use the default (400). This is what bounds a single pathological field — a long here-string command, say — that would otherwise fill the prompt through wrapping. A shortened field is marked with an ellipsis, and `Ctrl+O` shows it in full.",
|
|
209
|
+
default: 400,
|
|
210
|
+
}),
|
|
197
211
|
toolInputPreviewMaxLength: z.number().int().min(1).optional().meta({
|
|
198
212
|
description:
|
|
199
213
|
"Maximum character length of the inline-JSON tool-input preview shown in permission prompts. Omit to use the default (200). Set to a large value to disable truncation.",
|
package/src/extension-config.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface PermissionSystemExtensionConfig {
|
|
|
22
22
|
piInfrastructureReadPaths?: string[];
|
|
23
23
|
/** How long a subagent waits for the parent's answer to a forwarded ask, in ms. Defaults to 600000. */
|
|
24
24
|
forwardingTimeoutMs?: number;
|
|
25
|
+
/** Max rows a permission prompt renders before eliding its evidence. Defaults to 24. */
|
|
26
|
+
promptMaxRows?: number;
|
|
27
|
+
/** Max characters of any one field shown in a permission prompt. Defaults to 400. */
|
|
28
|
+
promptFieldMaxWidth?: number;
|
|
25
29
|
/** Max length of the inline-JSON input preview shown in permission prompts. Defaults to 200. */
|
|
26
30
|
toolInputPreviewMaxLength?: number;
|
|
27
31
|
/** Max length of inline pattern/path summaries (grep/find/ls) in permission prompts. Defaults to 80. */
|
|
@@ -76,6 +80,12 @@ export function normalizePermissionSystemConfig(
|
|
|
76
80
|
if (raw.forwardingTimeoutMs !== undefined) {
|
|
77
81
|
result.forwardingTimeoutMs = raw.forwardingTimeoutMs;
|
|
78
82
|
}
|
|
83
|
+
if (raw.promptMaxRows !== undefined) {
|
|
84
|
+
result.promptMaxRows = raw.promptMaxRows;
|
|
85
|
+
}
|
|
86
|
+
if (raw.promptFieldMaxWidth !== undefined) {
|
|
87
|
+
result.promptFieldMaxWidth = raw.promptFieldMaxWidth;
|
|
88
|
+
}
|
|
79
89
|
if (raw.toolInputPreviewMaxLength !== undefined) {
|
|
80
90
|
result.toolInputPreviewMaxLength = raw.toolInputPreviewMaxLength;
|
|
81
91
|
}
|
|
@@ -83,7 +83,7 @@ export function resolveBashCommandCheck(
|
|
|
83
83
|
input: { command: cmd.text },
|
|
84
84
|
agentName,
|
|
85
85
|
});
|
|
86
|
-
const
|
|
86
|
+
const floored =
|
|
87
87
|
cmd.wrapperKind && base.state === "allow"
|
|
88
88
|
? {
|
|
89
89
|
...base,
|
|
@@ -91,7 +91,12 @@ export function resolveBashCommandCheck(
|
|
|
91
91
|
matchedPattern: WRAPPER_SENTINEL[cmd.wrapperKind],
|
|
92
92
|
}
|
|
93
93
|
: base;
|
|
94
|
-
|
|
94
|
+
const result = cmd.context
|
|
95
|
+
? { ...floored, commandContext: cmd.context }
|
|
96
|
+
: floored;
|
|
97
|
+
return cmd.executedUnit === undefined
|
|
98
|
+
? result
|
|
99
|
+
: { ...result, executedUnit: cmd.executedUnit };
|
|
95
100
|
});
|
|
96
101
|
return (
|
|
97
102
|
pickMostRestrictive(results) ??
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { BashProgram } from "#src/access-intent/bash/program";
|
|
2
2
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
3
|
+
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
4
|
+
import { buildBashExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
|
|
3
5
|
import { SessionApproval } from "#src/session-approval";
|
|
4
6
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
5
7
|
import type { GateResult } from "./descriptor";
|
|
6
|
-
import { formatBashExternalDirectoryAskPrompt } from "./external-directory-messages";
|
|
7
8
|
import { selectUncoveredExternalPaths } from "./external-directory-policy";
|
|
8
9
|
import { accessFactsFromPath } from "./helpers";
|
|
9
10
|
import type { ToolCallContext } from "./types";
|
|
@@ -76,12 +77,15 @@ export function describeBashExternalDirectoryGate(
|
|
|
76
77
|
resolvedPath: path.resolvedAlias(),
|
|
77
78
|
}));
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
+
const payload = buildBashExternalDirectoryAskPayload({
|
|
80
81
|
command,
|
|
81
|
-
disclosures,
|
|
82
|
-
tcc.cwd,
|
|
83
|
-
tcc.agentName
|
|
84
|
-
|
|
82
|
+
externalPaths: disclosures,
|
|
83
|
+
cwd: tcc.cwd,
|
|
84
|
+
agentName: tcc.agentName,
|
|
85
|
+
toolName: tcc.toolName,
|
|
86
|
+
matchedPattern: preCheck.matchedPattern,
|
|
87
|
+
});
|
|
88
|
+
const bashExtMessage = renderLegacyMessage(payload);
|
|
85
89
|
|
|
86
90
|
const patterns = uncoveredPaths.map((p) => deriveApprovalPattern(p));
|
|
87
91
|
|
|
@@ -100,6 +104,7 @@ export function describeBashExternalDirectoryGate(
|
|
|
100
104
|
source: "tool_call",
|
|
101
105
|
agentName: tcc.agentName,
|
|
102
106
|
message: bashExtMessage,
|
|
107
|
+
payload,
|
|
103
108
|
toolCallId: tcc.toolCallId,
|
|
104
109
|
toolName: tcc.toolName,
|
|
105
110
|
command,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { AccessPath } from "#src/access-intent/access-path";
|
|
2
2
|
import type { BashProgram } from "#src/access-intent/bash/program";
|
|
3
3
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
|
+
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
5
|
+
import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
|
|
4
6
|
import { SessionApproval } from "#src/session-approval";
|
|
5
7
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
6
8
|
import type { PermissionCheckResult } from "#src/types";
|
|
7
9
|
import { pickMostRestrictive } from "./candidate-check";
|
|
8
10
|
import type { GateResult } from "./descriptor";
|
|
9
11
|
import { accessFactsFromPath } from "./helpers";
|
|
10
|
-
import { formatPathAskPrompt } from "./path";
|
|
11
12
|
import type { ToolCallContext } from "./types";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -113,11 +114,13 @@ export function describeBashPathGate(
|
|
|
113
114
|
// path), so it matches the values a later call produces. For an unknown base
|
|
114
115
|
// (`forLiteral`) `value()` is the raw token.
|
|
115
116
|
const pattern = deriveApprovalPattern(worstEntry.path.value());
|
|
116
|
-
const
|
|
117
|
-
tcc.toolName,
|
|
118
|
-
worstToken,
|
|
119
|
-
tcc.agentName
|
|
120
|
-
|
|
117
|
+
const payload = buildPathAskPayload({
|
|
118
|
+
toolName: tcc.toolName,
|
|
119
|
+
pathValue: worstToken,
|
|
120
|
+
agentName: tcc.agentName,
|
|
121
|
+
matchedPattern: worstCheck.matchedPattern,
|
|
122
|
+
});
|
|
123
|
+
const askMessage = renderLegacyMessage(payload);
|
|
121
124
|
|
|
122
125
|
return {
|
|
123
126
|
surface: "path",
|
|
@@ -133,6 +136,7 @@ export function describeBashPathGate(
|
|
|
133
136
|
source: "tool_call",
|
|
134
137
|
agentName: tcc.agentName,
|
|
135
138
|
message: askMessage,
|
|
139
|
+
payload,
|
|
136
140
|
toolCallId: tcc.toolCallId,
|
|
137
141
|
toolName: tcc.toolName,
|
|
138
142
|
command,
|
|
@@ -51,6 +51,15 @@ export interface GateDescriptor {
|
|
|
51
51
|
preCheck?: PermissionCheckResult;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* A decision event's facts, before the runner stamps the request id it minted.
|
|
56
|
+
*
|
|
57
|
+
* A gate knows what was decided but not which request it was deciding — the id
|
|
58
|
+
* is minted in `GateRunner.run`. Producing this type rather than the full event
|
|
59
|
+
* is what routes every emit through the runner's single stamping site.
|
|
60
|
+
*/
|
|
61
|
+
export type DecisionEventFacts = Omit<PermissionDecisionEvent, "requestId">;
|
|
62
|
+
|
|
54
63
|
/**
|
|
55
64
|
* Early allow result — gate has determined the action without needing the runner.
|
|
56
65
|
*
|
|
@@ -62,7 +71,7 @@ export interface GateBypass {
|
|
|
62
71
|
/** Optional review log entry to emit. */
|
|
63
72
|
log?: { event: string; details: Record<string, unknown> };
|
|
64
73
|
/** Optional decision event to emit. */
|
|
65
|
-
decision?:
|
|
74
|
+
decision?: DecisionEventFacts;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
/** Union of possible gate function return values. */
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { getToolInputPath } from "#src/access-intent/tool-input-path";
|
|
2
2
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
3
3
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
|
+
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
5
|
+
import { buildExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
|
|
4
6
|
import { SessionApproval } from "#src/session-approval";
|
|
5
7
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
6
8
|
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
|
7
9
|
import type { GateResult } from "./descriptor";
|
|
8
|
-
import { formatExternalDirectoryAskPrompt } from "./external-directory-messages";
|
|
9
10
|
import { resolveExternalDirectoryPolicy } from "./external-directory-policy";
|
|
10
11
|
import { accessFactsFromPath } from "./helpers";
|
|
11
12
|
import type { ToolCallContext } from "./types";
|
|
@@ -69,13 +70,6 @@ export function describeExternalDirectoryGate(
|
|
|
69
70
|
|
|
70
71
|
// ── Build descriptor for permission check ───────────────────────────────
|
|
71
72
|
const resolvedAlias = accessPath.resolvedAlias();
|
|
72
|
-
const extDirMessage = formatExternalDirectoryAskPrompt(
|
|
73
|
-
tcc.toolName,
|
|
74
|
-
externalDirectoryPath,
|
|
75
|
-
resolvedAlias,
|
|
76
|
-
tcc.cwd,
|
|
77
|
-
tcc.agentName ?? undefined,
|
|
78
|
-
);
|
|
79
73
|
|
|
80
74
|
// The runner consumes this preCheck and skips its own resolve.
|
|
81
75
|
const preCheck = resolveExternalDirectoryPolicy(
|
|
@@ -85,6 +79,16 @@ export function describeExternalDirectoryGate(
|
|
|
85
79
|
);
|
|
86
80
|
const pattern = deriveApprovalPattern(accessPath.value());
|
|
87
81
|
|
|
82
|
+
const payload = buildExternalDirectoryAskPayload({
|
|
83
|
+
toolName: tcc.toolName,
|
|
84
|
+
pathValue: externalDirectoryPath,
|
|
85
|
+
resolvedPath: resolvedAlias,
|
|
86
|
+
cwd: tcc.cwd,
|
|
87
|
+
agentName: tcc.agentName,
|
|
88
|
+
matchedPattern: preCheck.matchedPattern,
|
|
89
|
+
});
|
|
90
|
+
const extDirMessage = renderLegacyMessage(payload);
|
|
91
|
+
|
|
88
92
|
return {
|
|
89
93
|
surface: "external_directory",
|
|
90
94
|
input: {},
|
|
@@ -102,6 +106,7 @@ export function describeExternalDirectoryGate(
|
|
|
102
106
|
source: "tool_call",
|
|
103
107
|
agentName: tcc.agentName,
|
|
104
108
|
message: extDirMessage,
|
|
109
|
+
payload,
|
|
105
110
|
toolCallId: tcc.toolCallId,
|
|
106
111
|
toolName: tcc.toolName,
|
|
107
112
|
path: externalDirectoryPath,
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { AccessPath } from "#src/access-intent/access-path";
|
|
2
2
|
import { classifyToolKind } from "#src/access-intent/tool-kind";
|
|
3
3
|
import type { ForwardedAccessFacts } from "#src/authority/permission-forwarding";
|
|
4
|
-
import type {
|
|
5
|
-
PermissionDecisionEvent,
|
|
6
|
-
PermissionDecisionResolution,
|
|
7
|
-
} from "#src/permission-events";
|
|
4
|
+
import type { PermissionDecisionResolution } from "#src/permission-events";
|
|
8
5
|
import type { PermissionCheckResult } from "#src/types";
|
|
6
|
+
import type { DecisionEventFacts } from "./descriptor";
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
9
|
* Build the child-fixed access facts for a path-shaped gate from its
|
|
@@ -62,11 +60,12 @@ export function deriveDecisionValue(
|
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
/**
|
|
65
|
-
* Build a
|
|
63
|
+
* Build a decision event's facts from the gate's inputs.
|
|
66
64
|
*
|
|
67
65
|
* Centralises the `origin / agentName / matchedPattern ?? null` normalization
|
|
68
66
|
* that is otherwise duplicated across the session-hit path and the gate-result
|
|
69
|
-
* path in `runGateCheck`.
|
|
67
|
+
* path in `runGateCheck`. The request id is stamped by the runner, which is
|
|
68
|
+
* where it was minted.
|
|
70
69
|
*/
|
|
71
70
|
export function buildDecisionEvent(
|
|
72
71
|
decision: { surface: string; value: string },
|
|
@@ -74,7 +73,7 @@ export function buildDecisionEvent(
|
|
|
74
73
|
agentName: string | null,
|
|
75
74
|
result: "allow" | "deny",
|
|
76
75
|
resolution: PermissionDecisionResolution,
|
|
77
|
-
):
|
|
76
|
+
): DecisionEventFacts {
|
|
78
77
|
return {
|
|
79
78
|
surface: decision.surface,
|
|
80
79
|
value: decision.value,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getToolInputPath } from "#src/access-intent/tool-input-path";
|
|
2
2
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
3
3
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
|
+
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
5
|
+
import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
|
|
4
6
|
import { SessionApproval } from "#src/session-approval";
|
|
5
7
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
6
8
|
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
|
@@ -47,6 +49,13 @@ export function describePathGate(
|
|
|
47
49
|
// the policy values a later call produces.
|
|
48
50
|
const pattern = deriveApprovalPattern(accessPath.value());
|
|
49
51
|
|
|
52
|
+
const payload = buildPathAskPayload({
|
|
53
|
+
toolName: tcc.toolName,
|
|
54
|
+
pathValue: filePath,
|
|
55
|
+
agentName: tcc.agentName,
|
|
56
|
+
matchedPattern: check.matchedPattern,
|
|
57
|
+
});
|
|
58
|
+
|
|
50
59
|
const descriptor: GateDescriptor = {
|
|
51
60
|
surface: "path",
|
|
52
61
|
input: { path: filePath },
|
|
@@ -60,11 +69,8 @@ export function describePathGate(
|
|
|
60
69
|
promptDetails: {
|
|
61
70
|
source: "tool_call",
|
|
62
71
|
agentName: tcc.agentName,
|
|
63
|
-
message:
|
|
64
|
-
|
|
65
|
-
filePath,
|
|
66
|
-
tcc.agentName ?? undefined,
|
|
67
|
-
),
|
|
72
|
+
message: renderLegacyMessage(payload),
|
|
73
|
+
payload,
|
|
68
74
|
toolCallId: tcc.toolCallId,
|
|
69
75
|
toolName: tcc.toolName,
|
|
70
76
|
path: filePath,
|
|
@@ -86,12 +92,3 @@ export function describePathGate(
|
|
|
86
92
|
|
|
87
93
|
return descriptor;
|
|
88
94
|
}
|
|
89
|
-
|
|
90
|
-
export function formatPathAskPrompt(
|
|
91
|
-
toolName: string,
|
|
92
|
-
pathValue: string,
|
|
93
|
-
agentName?: string,
|
|
94
|
-
): string {
|
|
95
|
-
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
96
|
-
return `${subject} requested tool '${toolName}' for path '${pathValue}'. Allow this path access?`;
|
|
97
|
-
}
|