@gotgenes/pi-permission-system 25.4.0 → 26.0.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 +33 -0
- package/README.md +14 -12
- package/config/config.example.json +1 -2
- package/dist/public.d.ts +31 -10
- package/docs/configuration.md +23 -19
- package/docs/cross-extension-api.md +30 -4
- package/docs/migration/0745-prompt-payload-contracts.md +68 -0
- package/docs/migration/0746-review-log-fields.md +69 -0
- package/docs/troubleshooting.md +2 -1
- package/package.json +1 -1
- package/schemas/permissions.schema.json +14 -4
- package/src/access-intent/tool-kind.ts +1 -1
- package/src/authority/approval-escalator.ts +6 -4
- package/src/authority/authorizer.ts +3 -3
- package/src/authority/forwarded-request-server.ts +0 -2
- package/src/authority/forwarding-io.ts +7 -5
- package/src/authority/permission-forwarding.ts +12 -3
- package/src/authority/permission-prompter.ts +5 -4
- package/src/config-loader.ts +31 -0
- package/src/config-schema.ts +13 -4
- package/src/extension-config.ts +8 -9
- package/src/handlers/gates/bash-external-directory.ts +1 -12
- package/src/handlers/gates/bash-path.ts +1 -10
- package/src/handlers/gates/descriptor.ts +16 -5
- package/src/handlers/gates/external-directory.ts +1 -13
- package/src/handlers/gates/path.ts +1 -9
- package/src/handlers/gates/runner.ts +24 -14
- package/src/handlers/gates/skill-input-gate-pipeline.ts +2 -2
- package/src/handlers/gates/skill-input.ts +1 -10
- package/src/handlers/gates/skill-read.ts +1 -11
- package/src/handlers/gates/tool.ts +1 -11
- package/src/log-field-cap.ts +82 -0
- package/src/logging.ts +24 -3
- package/src/permission-events.ts +15 -2
- package/src/permission-prompts.ts +4 -3
- package/src/permission-session.ts +1 -1
- package/src/permission-ui-prompt.ts +4 -2
- package/src/presentation/agent-renderer.ts +215 -0
- package/src/presentation/dialog-renderer.ts +8 -64
- package/src/presentation/fact-vocabulary.ts +103 -0
- package/src/presentation/forwarded-ask-payload.ts +42 -17
- package/src/presentation/path-ask-payload.ts +8 -1
- package/src/presentation/prompt-payload.ts +165 -4
- package/src/presentation/review-log-renderer.ts +51 -0
- package/src/service.ts +11 -0
- package/src/tool-input-preview.ts +0 -1
- package/src/tool-preview-formatter.ts +18 -33
- package/src/denial-messages.ts +0 -269
- package/src/presentation/legacy-message.ts +0 -117
package/src/denial-messages.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { classifyToolKind, isMcpCheck } from "./access-intent/tool-kind";
|
|
2
|
-
import { EXTENSION_ID } from "./extension-config";
|
|
3
|
-
import type { BashCommandContext, PermissionCheckResult } from "./types";
|
|
4
|
-
|
|
5
|
-
// ── Extension attribution tag ──────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
export const EXTENSION_TAG = `[${EXTENSION_ID}]`;
|
|
8
|
-
|
|
9
|
-
// ── External-path resolved-target disclosure ────────────────────────────────
|
|
10
|
-
|
|
11
|
-
/** A displayed external path paired with its resolved target, when distinct. */
|
|
12
|
-
export interface ExternalPathDisclosure {
|
|
13
|
-
/** The path as displayed (typed for tools, lexical-absolute for bash). */
|
|
14
|
-
path: string;
|
|
15
|
-
/** The canonical symlink-resolved target; present only when it differs. */
|
|
16
|
-
resolvedPath?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** ` (resolves to '<canonical>')` when a distinct target exists, else `""`. */
|
|
20
|
-
export function resolvesToSuffix(resolvedPath?: string): string {
|
|
21
|
-
return resolvedPath ? ` (resolves to '${resolvedPath}')` : "";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ── Denial context discriminated union ─────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
export type DenialContext =
|
|
27
|
-
| {
|
|
28
|
-
kind: "tool";
|
|
29
|
-
check: PermissionCheckResult;
|
|
30
|
-
agentName?: string;
|
|
31
|
-
input?: unknown;
|
|
32
|
-
}
|
|
33
|
-
| {
|
|
34
|
-
kind: "path";
|
|
35
|
-
toolName: string;
|
|
36
|
-
pathValue: string;
|
|
37
|
-
agentName?: string;
|
|
38
|
-
}
|
|
39
|
-
| {
|
|
40
|
-
kind: "external_directory";
|
|
41
|
-
toolName: string;
|
|
42
|
-
pathValue: string;
|
|
43
|
-
resolvedPath?: string;
|
|
44
|
-
cwd: string;
|
|
45
|
-
agentName?: string;
|
|
46
|
-
}
|
|
47
|
-
| {
|
|
48
|
-
kind: "bash_external_directory";
|
|
49
|
-
command: string;
|
|
50
|
-
externalPaths: ExternalPathDisclosure[];
|
|
51
|
-
cwd: string;
|
|
52
|
-
agentName?: string;
|
|
53
|
-
}
|
|
54
|
-
| {
|
|
55
|
-
kind: "bash_path";
|
|
56
|
-
command: string;
|
|
57
|
-
pathValue: string;
|
|
58
|
-
agentName?: string;
|
|
59
|
-
}
|
|
60
|
-
| {
|
|
61
|
-
kind: "skill_read";
|
|
62
|
-
skillName: string;
|
|
63
|
-
readPath: string;
|
|
64
|
-
agentName?: string;
|
|
65
|
-
}
|
|
66
|
-
| {
|
|
67
|
-
kind: "skill_input";
|
|
68
|
-
skillName: string;
|
|
69
|
-
agentName?: string;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// ── Public formatter API ───────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
/** Format the block reason when permission policy denies an operation. */
|
|
75
|
-
export function formatDenyReason(ctx: DenialContext): string {
|
|
76
|
-
return `${EXTENSION_TAG} ${buildDenyBody(ctx)}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Format the block reason when no live authority could answer the ask.
|
|
81
|
-
*
|
|
82
|
-
* `denialReason` is optional because the plain no-UI case has nothing to add;
|
|
83
|
-
* an authority that abandoned for a specific reason (a forwarding target that
|
|
84
|
-
* is not serving, a request that could not be written) supplies one, and the
|
|
85
|
-
* model sees it (#719).
|
|
86
|
-
*/
|
|
87
|
-
export function formatUnavailableReason(
|
|
88
|
-
ctx: DenialContext,
|
|
89
|
-
denialReason?: string,
|
|
90
|
-
): string {
|
|
91
|
-
return `${EXTENSION_TAG} ${buildUnavailableBody(ctx, denialReason)}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Format the block reason when the user denies at an interactive prompt. */
|
|
95
|
-
export function formatUserDeniedReason(
|
|
96
|
-
ctx: DenialContext,
|
|
97
|
-
denialReason?: string,
|
|
98
|
-
): string {
|
|
99
|
-
return `${EXTENSION_TAG} ${buildUserDeniedBody(ctx, denialReason)}`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ── Private body builders ──────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
function subject(agentName?: string): string {
|
|
105
|
-
return agentName ? `Agent '${agentName}'` : "Current agent";
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function reasonSuffix(denialReason?: string): string {
|
|
109
|
-
return denialReason ? ` Reason: ${denialReason}.` : "";
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function buildDenyBody(ctx: DenialContext): string {
|
|
113
|
-
switch (ctx.kind) {
|
|
114
|
-
case "tool":
|
|
115
|
-
return buildToolDenyBody(ctx);
|
|
116
|
-
case "path":
|
|
117
|
-
return `${subject(ctx.agentName)} is not permitted to access path '${ctx.pathValue}' via tool '${ctx.toolName}'.`;
|
|
118
|
-
case "external_directory":
|
|
119
|
-
return `${subject(ctx.agentName)} is not permitted to run tool '${ctx.toolName}' for path '${ctx.pathValue}'${resolvesToSuffix(ctx.resolvedPath)} outside working directory '${ctx.cwd}'.`;
|
|
120
|
-
case "bash_external_directory":
|
|
121
|
-
return `${subject(ctx.agentName)} is not permitted to run bash command '${ctx.command}' which references path(s) outside working directory '${ctx.cwd}': ${formatExternalPathList(ctx.externalPaths)}.`;
|
|
122
|
-
case "bash_path":
|
|
123
|
-
return `${subject(ctx.agentName)} is not permitted to access path '${ctx.pathValue}' via tool 'bash'.`;
|
|
124
|
-
case "skill_read":
|
|
125
|
-
return `${subject(ctx.agentName)} is not permitted to access skill '${ctx.skillName}' via '${ctx.readPath}'.`;
|
|
126
|
-
case "skill_input":
|
|
127
|
-
return `${subject(ctx.agentName)} is not permitted to access skill '${ctx.skillName}'.`;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function buildToolDenyBody(
|
|
132
|
-
ctx: Extract<DenialContext, { kind: "tool" }>,
|
|
133
|
-
): string {
|
|
134
|
-
const parts: string[] = [];
|
|
135
|
-
const { check, agentName } = ctx;
|
|
136
|
-
|
|
137
|
-
if (agentName) {
|
|
138
|
-
parts.push(`Agent '${agentName}'`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (isMcpCheck(check) && check.target) {
|
|
142
|
-
parts.push(`is not permitted to run MCP target '${check.target}'`);
|
|
143
|
-
} else {
|
|
144
|
-
parts.push(`is not permitted to run '${check.toolName}'`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (check.command) {
|
|
148
|
-
parts.push(`command '${check.command}'`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const qualifier = matchQualifier(check.matchedPattern, check.commandContext);
|
|
152
|
-
if (qualifier) {
|
|
153
|
-
parts.push(qualifier);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// reasonSuffix appends ` Reason: <reason>.` after the sentence-ending period.
|
|
157
|
-
return `${parts.join(" ")}.${reasonSuffix(check.reason)}`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Human-readable label for a nested bash execution context, or `undefined` for
|
|
162
|
-
* a current-shell (top-level) command.
|
|
163
|
-
*/
|
|
164
|
-
export function describeBashCommandContext(
|
|
165
|
-
context?: BashCommandContext,
|
|
166
|
-
): string | undefined {
|
|
167
|
-
switch (context) {
|
|
168
|
-
case "command_substitution":
|
|
169
|
-
return "command substitution";
|
|
170
|
-
case "process_substitution":
|
|
171
|
-
return "process substitution";
|
|
172
|
-
case "subshell":
|
|
173
|
-
return "subshell";
|
|
174
|
-
default:
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Build the parenthetical qualifier for a bash decision, folding the matched
|
|
181
|
-
* rule and (for a nested command) its execution context into one clause, e.g.
|
|
182
|
-
* `(matched 'rm *', inside command substitution)`. Returns `""` when neither
|
|
183
|
-
* applies.
|
|
184
|
-
*/
|
|
185
|
-
export function matchQualifier(
|
|
186
|
-
matchedPattern?: string,
|
|
187
|
-
context?: BashCommandContext,
|
|
188
|
-
): string {
|
|
189
|
-
const parts: string[] = [];
|
|
190
|
-
if (matchedPattern) {
|
|
191
|
-
parts.push(`matched '${matchedPattern}'`);
|
|
192
|
-
}
|
|
193
|
-
const label = describeBashCommandContext(context);
|
|
194
|
-
if (label) {
|
|
195
|
-
parts.push(`inside ${label}`);
|
|
196
|
-
}
|
|
197
|
-
return parts.length > 0 ? `(${parts.join(", ")})` : "";
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function buildUnavailableBody(
|
|
201
|
-
ctx: DenialContext,
|
|
202
|
-
denialReason?: string,
|
|
203
|
-
): string {
|
|
204
|
-
return `${buildUnavailableSentence(ctx)}${reasonSuffix(denialReason)}`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function buildUnavailableSentence(ctx: DenialContext): string {
|
|
208
|
-
switch (ctx.kind) {
|
|
209
|
-
case "tool": {
|
|
210
|
-
const { check } = ctx;
|
|
211
|
-
if (classifyToolKind(check.toolName) === "bash" && check.command) {
|
|
212
|
-
return `Running bash command '${check.command}' requires approval, but no interactive UI is available.`;
|
|
213
|
-
}
|
|
214
|
-
if (isMcpCheck(check) && check.target) {
|
|
215
|
-
return "Using tool 'mcp' requires approval, but no interactive UI is available.";
|
|
216
|
-
}
|
|
217
|
-
return `Using tool '${check.toolName}' requires approval, but no interactive UI is available.`;
|
|
218
|
-
}
|
|
219
|
-
case "path":
|
|
220
|
-
return `Accessing '${ctx.pathValue}' requires approval, but no interactive UI is available.`;
|
|
221
|
-
case "external_directory":
|
|
222
|
-
return `Accessing '${ctx.pathValue}'${resolvesToSuffix(ctx.resolvedPath)} outside the working directory requires approval, but no interactive UI is available.`;
|
|
223
|
-
case "bash_external_directory":
|
|
224
|
-
return `Bash command '${ctx.command}' references path(s) outside the working directory and requires approval, but no interactive UI is available.`;
|
|
225
|
-
case "bash_path":
|
|
226
|
-
return `Bash command '${ctx.command}' accesses path '${ctx.pathValue}' which requires approval, but no interactive UI is available.`;
|
|
227
|
-
case "skill_read":
|
|
228
|
-
return `Accessing skill '${ctx.skillName}' requires approval, but no interactive UI is available.`;
|
|
229
|
-
case "skill_input":
|
|
230
|
-
return `Accessing skill '${ctx.skillName}' requires approval, but no interactive UI is available.`;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function buildUserDeniedBody(
|
|
235
|
-
ctx: DenialContext,
|
|
236
|
-
denialReason?: string,
|
|
237
|
-
): string {
|
|
238
|
-
switch (ctx.kind) {
|
|
239
|
-
case "tool": {
|
|
240
|
-
const { check } = ctx;
|
|
241
|
-
if (isMcpCheck(check) && check.target) {
|
|
242
|
-
return `User denied MCP target '${check.target}'.${reasonSuffix(denialReason)}`;
|
|
243
|
-
}
|
|
244
|
-
if (classifyToolKind(check.toolName) === "bash" && check.command) {
|
|
245
|
-
return `User denied bash command '${check.command}'.${reasonSuffix(denialReason)}`;
|
|
246
|
-
}
|
|
247
|
-
return `User denied tool '${check.toolName}'.${reasonSuffix(denialReason)}`;
|
|
248
|
-
}
|
|
249
|
-
case "path":
|
|
250
|
-
return `User denied access to path '${ctx.pathValue}'.${reasonSuffix(denialReason)}`;
|
|
251
|
-
case "external_directory":
|
|
252
|
-
return `User denied external directory access for tool '${ctx.toolName}' path '${ctx.pathValue}'${resolvesToSuffix(ctx.resolvedPath)}.${reasonSuffix(denialReason)}`;
|
|
253
|
-
case "bash_external_directory":
|
|
254
|
-
return `User denied external directory access for bash command '${ctx.command}'.${reasonSuffix(denialReason)}`;
|
|
255
|
-
case "bash_path":
|
|
256
|
-
return `User denied path access for bash command '${ctx.command}' (path '${ctx.pathValue}').${reasonSuffix(denialReason)}`;
|
|
257
|
-
case "skill_read":
|
|
258
|
-
return `User denied access to skill '${ctx.skillName}'.${reasonSuffix(denialReason)}`;
|
|
259
|
-
case "skill_input":
|
|
260
|
-
return `User denied access to skill '${ctx.skillName}'.${reasonSuffix(denialReason)}`;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/** Render an external-path disclosure list for the bash deny body's path clause. */
|
|
265
|
-
function formatExternalPathList(paths: ExternalPathDisclosure[]): string {
|
|
266
|
-
return paths
|
|
267
|
-
.map(({ path, resolvedPath }) => `${path}${resolvesToSuffix(resolvedPath)}`)
|
|
268
|
-
.join(", ");
|
|
269
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { matchQualifier, resolvesToSuffix } from "#src/denial-messages";
|
|
2
|
-
import {
|
|
3
|
-
allEvidence,
|
|
4
|
-
findEvidence,
|
|
5
|
-
type PromptPayload,
|
|
6
|
-
} from "#src/presentation/prompt-payload";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Render the flat `message` string every consumer still reads.
|
|
10
|
-
*
|
|
11
|
-
* Transitional, and deliberately the *only* place that string is produced: it
|
|
12
|
-
* reads nothing but the payload, so the existing prompt-text tests are the
|
|
13
|
-
* proof that the payload carries everything the six former assemblers said.
|
|
14
|
-
*
|
|
15
|
-
* The bounded renderers replace it consumer by consumer — the dialog and the
|
|
16
|
-
* fallback first, then the wire and the broadcast, then the review log — and
|
|
17
|
-
* this module goes when the last `message` reader does. Its coupling to the
|
|
18
|
-
* evidence labels the builders emit is the price of that byte-for-byte
|
|
19
|
-
* equivalence, and it is why it is scoped to the transition.
|
|
20
|
-
*/
|
|
21
|
-
export function renderLegacyMessage(payload: PromptPayload): string {
|
|
22
|
-
const { request } = payload;
|
|
23
|
-
const subject = request.requester.agentName
|
|
24
|
-
? `Agent '${request.requester.agentName}'`
|
|
25
|
-
: "Current agent";
|
|
26
|
-
|
|
27
|
-
switch (payload.kind) {
|
|
28
|
-
case "bash":
|
|
29
|
-
return `${subject} requested bash command '${request.value}'${bashQualifier(payload)}${fullCommandSuffix(payload)}. Allow this command?`;
|
|
30
|
-
case "mcp":
|
|
31
|
-
return `${subject} requested MCP target '${request.value}'${patternSuffix(payload)}${inputSuffix(payload)}. Allow this call?`;
|
|
32
|
-
case "tool":
|
|
33
|
-
return `${subject} requested tool '${request.value}'${patternSuffix(payload)}${inputSuffix(payload)}. Allow this call?`;
|
|
34
|
-
case "path":
|
|
35
|
-
return `${subject} requested tool '${request.toolName}' for path '${request.value}'. Allow this path access?`;
|
|
36
|
-
case "external_directory":
|
|
37
|
-
return `${subject} requested tool '${request.toolName}' for path '${request.value}'${resolvedSuffix(payload)} outside working directory '${workingDirectory(payload)}'. Allow this external directory access?`;
|
|
38
|
-
case "bash_external_directory":
|
|
39
|
-
return `${subject} requested bash command '${request.value}' which references path(s) outside working directory '${workingDirectory(payload)}': ${externalPathList(payload)}. Allow this external directory access?`;
|
|
40
|
-
case "skill":
|
|
41
|
-
return `${subject} requested skill '${request.value}'. Allow loading this skill?`;
|
|
42
|
-
case "skill_read":
|
|
43
|
-
return `${subject} requested access to skill '${request.value}' via '${textOf(payload, "read path")}'. Allow this read?`;
|
|
44
|
-
case "forwarded":
|
|
45
|
-
return renderForwarded(payload);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ── Per-kind fragments ──────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
/** The bash parenthetical: the matched rule plus, when nested, its context. */
|
|
52
|
-
function bashQualifier(payload: PromptPayload): string {
|
|
53
|
-
const qualifier = matchQualifier(
|
|
54
|
-
payload.request.matchedPattern ?? undefined,
|
|
55
|
-
payload.request.commandContext ?? undefined,
|
|
56
|
-
);
|
|
57
|
-
return qualifier ? ` ${qualifier}` : "";
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** ` (matched '<pattern>')` for the non-bash surfaces. */
|
|
61
|
-
function patternSuffix(payload: PromptPayload): string {
|
|
62
|
-
const { matchedPattern } = payload.request;
|
|
63
|
-
return matchedPattern ? ` (matched '${matchedPattern}')` : "";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** The enclosing command, when the gated unit is only part of it. */
|
|
67
|
-
function fullCommandSuffix(payload: PromptPayload): string {
|
|
68
|
-
const full = findEvidence(payload, "full command");
|
|
69
|
-
return full ? ` (full command: '${full.text}')` : "";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** The per-tool input preview, when the formatter produced one. */
|
|
73
|
-
function inputSuffix(payload: PromptPayload): string {
|
|
74
|
-
const preview = findEvidence(payload, "input");
|
|
75
|
-
return preview ? ` ${preview.text}` : "";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** ` (resolves to '<canonical>')` when the alias names a distinct location. */
|
|
79
|
-
function resolvedSuffix(payload: PromptPayload): string {
|
|
80
|
-
return resolvesToSuffix(findEvidence(payload, "resolves to")?.text);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** The comma-joined external paths, each with its canonical alias. */
|
|
84
|
-
function externalPathList(payload: PromptPayload): string {
|
|
85
|
-
return allEvidence(payload, "external path")
|
|
86
|
-
.map(
|
|
87
|
-
(entry) => `${entry.text}${resolvesToSuffix(entry.detail ?? undefined)}`,
|
|
88
|
-
)
|
|
89
|
-
.join(", ");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function workingDirectory(payload: PromptPayload): string {
|
|
93
|
-
return textOf(payload, "working directory");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The child's ask, prefixed with its provenance.
|
|
98
|
-
*
|
|
99
|
-
* Until the payload replaces `message` on the wire, a forwarded request carries
|
|
100
|
-
* the child's pre-rendered sentence, which arrives as a single evidence entry.
|
|
101
|
-
*/
|
|
102
|
-
function renderForwarded(payload: PromptPayload): string {
|
|
103
|
-
const { requester } = payload.request;
|
|
104
|
-
return [
|
|
105
|
-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: a version-skewed request carries "" rather than null
|
|
106
|
-
`Subagent '${requester.agentName || "unknown"}' requested permission.`,
|
|
107
|
-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: a version-skewed request carries "" rather than null
|
|
108
|
-
`Session ID: ${requester.sessionId || "unknown"}`,
|
|
109
|
-
"",
|
|
110
|
-
textOf(payload, "requested"),
|
|
111
|
-
].join("\n");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** The text of an evidence entry the render requires, or the empty string. */
|
|
115
|
-
function textOf(payload: PromptPayload, label: string): string {
|
|
116
|
-
return findEvidence(payload, label)?.text ?? "";
|
|
117
|
-
}
|