@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
|
@@ -1,34 +1,56 @@
|
|
|
1
1
|
import type { ForwardedPermissionRequest } from "#src/authority/permission-forwarding";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
PromptPayload,
|
|
4
|
+
PromptRequester,
|
|
5
|
+
} from "#src/presentation/prompt-payload";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Build the payload for an ask forwarded up from a subagent.
|
|
6
9
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* A projection, not a synthesizer: the child ships its own complete payload, so
|
|
11
|
+
* the serving node renders the child's facts under the *parent's* budget — which
|
|
12
|
+
* is what makes a forwarded ask and a local one consistent in kind, a forwarded
|
|
13
|
+
* bash ask reading `command : …` exactly as a local one does (ADR 0011 §2).
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
* child's own payload instead, and the serving node renders the child's facts
|
|
14
|
-
* under its own budget — which is what makes a forwarded ask and a local one
|
|
15
|
-
* consistent for the first time (ADR 0011 §2).
|
|
16
|
-
*
|
|
17
|
-
* A request missing a field renders from whatever it does carry: fail-closed
|
|
15
|
+
* A request carrying no payload renders from whatever it does hold: fail-closed
|
|
18
16
|
* applies to presentation as it does to policy, so a version-skewed ask still
|
|
19
17
|
* reaches the human rather than resolving without one (ADR 0011 §9).
|
|
20
18
|
*/
|
|
21
19
|
export function buildForwardedAskPayload(
|
|
22
20
|
request: ForwardedPermissionRequest,
|
|
21
|
+
): PromptPayload {
|
|
22
|
+
// The child built its payload with `localRequester` — `forwarded: false`,
|
|
23
|
+
// `sessionId: null`. The serving node is the only party that knows the ask
|
|
24
|
+
// arrived over the wire, and the request's own provenance is authoritative
|
|
25
|
+
// (#292); everything else is the child's fact and passes through untouched.
|
|
26
|
+
const requester: PromptRequester = {
|
|
27
|
+
agentName: request.requesterAgentName,
|
|
28
|
+
forwarded: true,
|
|
29
|
+
sessionId: request.requesterSessionId,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return request.payload
|
|
33
|
+
? {
|
|
34
|
+
...request.payload,
|
|
35
|
+
request: { ...request.payload.request, requester },
|
|
36
|
+
}
|
|
37
|
+
: degradedForwardedPayload(request, requester);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The render for an ask that arrived without a payload.
|
|
42
|
+
*
|
|
43
|
+
* `kind: "forwarded"` narrows to meaning exactly this — not "an ask from a
|
|
44
|
+
* subagent", which every branch above is too.
|
|
45
|
+
*/
|
|
46
|
+
function degradedForwardedPayload(
|
|
47
|
+
request: ForwardedPermissionRequest,
|
|
48
|
+
requester: PromptRequester,
|
|
23
49
|
): PromptPayload {
|
|
24
50
|
return {
|
|
25
51
|
kind: "forwarded",
|
|
26
52
|
request: {
|
|
27
|
-
requester
|
|
28
|
-
agentName: request.requesterAgentName,
|
|
29
|
-
forwarded: true,
|
|
30
|
-
sessionId: request.requesterSessionId,
|
|
31
|
-
},
|
|
53
|
+
requester,
|
|
32
54
|
// The child's display projection: what the ask was about, as the child's
|
|
33
55
|
// own gate named it.
|
|
34
56
|
surface: request.surface ?? "",
|
|
@@ -39,7 +61,10 @@ export function buildForwardedAskPayload(
|
|
|
39
61
|
commandContext: null,
|
|
40
62
|
executedUnit: null,
|
|
41
63
|
},
|
|
42
|
-
|
|
64
|
+
// Nothing to carry: the wire no longer relays a sentence, and inventing
|
|
65
|
+
// evidence the child never sent is exactly the fiction the bounded
|
|
66
|
+
// renderers would then have to trust.
|
|
67
|
+
evidence: [],
|
|
43
68
|
annotations: [],
|
|
44
69
|
};
|
|
45
70
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import type { ExternalPathDisclosure } from "#src/denial-messages";
|
|
2
1
|
import type {
|
|
3
2
|
PromptEvidence,
|
|
4
3
|
PromptPayload,
|
|
5
4
|
} from "#src/presentation/prompt-payload";
|
|
6
5
|
import { localRequester } from "#src/presentation/prompt-payload";
|
|
7
6
|
|
|
7
|
+
/** A displayed external path paired with its resolved target, when distinct. */
|
|
8
|
+
export interface ExternalPathDisclosure {
|
|
9
|
+
/** The path as displayed (typed for tools, lexical-absolute for bash). */
|
|
10
|
+
path: string;
|
|
11
|
+
/** The canonical symlink-resolved target; present only when it differs. */
|
|
12
|
+
resolvedPath?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
/** The facts a path-shaped gate holds when it raises an ask. */
|
|
9
16
|
interface PathAskFacts {
|
|
10
17
|
toolName: string;
|
|
@@ -22,10 +22,10 @@ export interface PromptPayload {
|
|
|
22
22
|
*
|
|
23
23
|
* Present because the ask shapes are not separable by surface alone: a tool
|
|
24
24
|
* external-directory ask and a bash one share the `external_directory` surface,
|
|
25
|
-
* and the `path` gate and the per-tool gate differ only in wording. It
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* and the `path` gate and the per-tool gate differ only in wording. It gives
|
|
26
|
+
* every renderer an exhaustive switch rather than a set of string comparisons a
|
|
27
|
+
* new variant sails past — which is what let the parallel denial-context union
|
|
28
|
+
* ADR 0011 §7 described dissolve into this one (#746).
|
|
29
29
|
*/
|
|
30
30
|
export type PromptPayloadKind =
|
|
31
31
|
| "bash"
|
|
@@ -120,6 +120,167 @@ export function localRequester(agentName: string | null): PromptRequester {
|
|
|
120
120
|
return { agentName, forwarded: false, sessionId: null };
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/** Every {@link PromptPayloadKind}, for tolerant reads of a serialized payload. */
|
|
124
|
+
const PROMPT_PAYLOAD_KINDS = [
|
|
125
|
+
"bash",
|
|
126
|
+
"mcp",
|
|
127
|
+
"tool",
|
|
128
|
+
"path",
|
|
129
|
+
"external_directory",
|
|
130
|
+
"bash_external_directory",
|
|
131
|
+
"skill",
|
|
132
|
+
"skill_read",
|
|
133
|
+
"forwarded",
|
|
134
|
+
] as const satisfies readonly PromptPayloadKind[];
|
|
135
|
+
|
|
136
|
+
const BASH_COMMAND_CONTEXTS = [
|
|
137
|
+
"command_substitution",
|
|
138
|
+
"process_substitution",
|
|
139
|
+
"subshell",
|
|
140
|
+
] as const satisfies readonly BashCommandContext[];
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Narrow an unknown value to a {@link PromptPayload}, or `undefined`.
|
|
144
|
+
*
|
|
145
|
+
* Lives beside its type so a new request fact updates the guard next door
|
|
146
|
+
* rather than in a distant reader, following `isPermissionDecisionState`'s
|
|
147
|
+
* precedent.
|
|
148
|
+
*
|
|
149
|
+
* All-or-nothing: any malformed field yields `undefined` rather than a
|
|
150
|
+
* half-payload, so a consumer renders its own degraded view instead of
|
|
151
|
+
* presenting corrupt facts (ADR 0011 §9).
|
|
152
|
+
*/
|
|
153
|
+
export function asPromptPayload(value: unknown): PromptPayload | undefined {
|
|
154
|
+
const candidate = asObject(value);
|
|
155
|
+
if (!candidate) return undefined;
|
|
156
|
+
|
|
157
|
+
const kind = PROMPT_PAYLOAD_KINDS.find((entry) => entry === candidate.kind);
|
|
158
|
+
const request = asPromptRequestFacts(candidate.request);
|
|
159
|
+
const evidence = asArrayOf(candidate.evidence, asPromptEvidence);
|
|
160
|
+
const annotations = asArrayOf(candidate.annotations, asPromptAnnotation);
|
|
161
|
+
if (!kind || !request || !evidence || !annotations) return undefined;
|
|
162
|
+
|
|
163
|
+
return { kind, request, evidence, annotations };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function asPromptRequestFacts(value: unknown): PromptRequestFacts | undefined {
|
|
167
|
+
const candidate = asObject(value);
|
|
168
|
+
if (!candidate) return undefined;
|
|
169
|
+
|
|
170
|
+
const requester = asPromptRequester(candidate.requester);
|
|
171
|
+
const commandContext = asNullableMember(
|
|
172
|
+
candidate.commandContext,
|
|
173
|
+
BASH_COMMAND_CONTEXTS,
|
|
174
|
+
);
|
|
175
|
+
if (
|
|
176
|
+
!requester ||
|
|
177
|
+
commandContext === undefined ||
|
|
178
|
+
typeof candidate.surface !== "string" ||
|
|
179
|
+
typeof candidate.value !== "string" ||
|
|
180
|
+
!isNullableString(candidate.toolName) ||
|
|
181
|
+
!isNullableString(candidate.invokedToolName) ||
|
|
182
|
+
!isNullableString(candidate.matchedPattern) ||
|
|
183
|
+
!isNullableString(candidate.executedUnit)
|
|
184
|
+
) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
requester,
|
|
190
|
+
surface: candidate.surface,
|
|
191
|
+
toolName: candidate.toolName,
|
|
192
|
+
invokedToolName: candidate.invokedToolName,
|
|
193
|
+
value: candidate.value,
|
|
194
|
+
matchedPattern: candidate.matchedPattern,
|
|
195
|
+
commandContext: commandContext.value,
|
|
196
|
+
executedUnit: candidate.executedUnit,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function asPromptRequester(value: unknown): PromptRequester | undefined {
|
|
201
|
+
const candidate = asObject(value);
|
|
202
|
+
if (
|
|
203
|
+
!candidate ||
|
|
204
|
+
typeof candidate.forwarded !== "boolean" ||
|
|
205
|
+
!isNullableString(candidate.agentName) ||
|
|
206
|
+
!isNullableString(candidate.sessionId)
|
|
207
|
+
) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
agentName: candidate.agentName,
|
|
212
|
+
forwarded: candidate.forwarded,
|
|
213
|
+
sessionId: candidate.sessionId,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function asPromptEvidence(value: unknown): PromptEvidence | undefined {
|
|
218
|
+
const candidate = asObject(value);
|
|
219
|
+
if (
|
|
220
|
+
!candidate ||
|
|
221
|
+
typeof candidate.label !== "string" ||
|
|
222
|
+
typeof candidate.text !== "string" ||
|
|
223
|
+
!isNullableString(candidate.detail)
|
|
224
|
+
) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
label: candidate.label,
|
|
229
|
+
text: candidate.text,
|
|
230
|
+
detail: candidate.detail,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function asPromptAnnotation(value: unknown): PromptAnnotation | undefined {
|
|
235
|
+
const candidate = asObject(value);
|
|
236
|
+
if (
|
|
237
|
+
!candidate ||
|
|
238
|
+
typeof candidate.source !== "string" ||
|
|
239
|
+
typeof candidate.text !== "string"
|
|
240
|
+
) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
return { source: candidate.source, text: candidate.text };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
247
|
+
return typeof value === "object" && value !== null
|
|
248
|
+
? (value as Record<string, unknown>)
|
|
249
|
+
: undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Narrow every entry, or `undefined` when the array or any entry is malformed. */
|
|
253
|
+
function asArrayOf<T>(
|
|
254
|
+
value: unknown,
|
|
255
|
+
narrow: (entry: unknown) => T | undefined,
|
|
256
|
+
): T[] | undefined {
|
|
257
|
+
if (!Array.isArray(value)) return undefined;
|
|
258
|
+
const narrowed: T[] = [];
|
|
259
|
+
for (const entry of value) {
|
|
260
|
+
const result = narrow(entry);
|
|
261
|
+
if (!result) return undefined;
|
|
262
|
+
narrowed.push(result);
|
|
263
|
+
}
|
|
264
|
+
return narrowed;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isNullableString(value: unknown): value is string | null {
|
|
268
|
+
return value === null || typeof value === "string";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Narrow to `null` or a member of `members`, boxed so a valid `null` is
|
|
273
|
+
* distinguishable from the malformed `undefined`.
|
|
274
|
+
*/
|
|
275
|
+
function asNullableMember<T extends string>(
|
|
276
|
+
value: unknown,
|
|
277
|
+
members: readonly T[],
|
|
278
|
+
): { value: T | null } | undefined {
|
|
279
|
+
if (value === null) return { value: null };
|
|
280
|
+
const member = members.find((entry) => entry === value);
|
|
281
|
+
return member ? { value: member } : undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
123
284
|
/** Find the evidence entry a renderer knows by label. */
|
|
124
285
|
export function findEvidence(
|
|
125
286
|
payload: PromptPayload,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PromptPayload } from "#src/presentation/prompt-payload";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The payload facts the permission review log persists (ADR 0011 §6).
|
|
5
|
+
*
|
|
6
|
+
* The log is a renderer over the payload like any other, and this is its
|
|
7
|
+
* content decision: the request facts, and only those the log's own structured
|
|
8
|
+
* columns do not already carry. `toolName`, `command`, `path`, `target`, and
|
|
9
|
+
* `toolInputPreview` are written by the gates; restating them under a second
|
|
10
|
+
* name would grow the log rather than sharpen it.
|
|
11
|
+
*
|
|
12
|
+
* Evidence and annotations are deliberately absent.
|
|
13
|
+
* `docs/decisions/0010-permission-log-secret-exposure.md` bounds what the logs
|
|
14
|
+
* accumulate, and evidence is exactly the unbounded part — the point of this
|
|
15
|
+
* render is that the log's growth is a decision, not a side effect of how a
|
|
16
|
+
* prompt happened to be worded.
|
|
17
|
+
*
|
|
18
|
+
* A fact the ask does not carry is omitted rather than written as `null`, so a
|
|
19
|
+
* line states what was true rather than enumerating what was not.
|
|
20
|
+
*/
|
|
21
|
+
export function renderReviewLogFacts(
|
|
22
|
+
payload: PromptPayload,
|
|
23
|
+
): Record<string, unknown> {
|
|
24
|
+
const { request } = payload;
|
|
25
|
+
return {
|
|
26
|
+
surface: request.surface,
|
|
27
|
+
...present("matchedPattern", request.matchedPattern),
|
|
28
|
+
...present("executedUnit", request.executedUnit),
|
|
29
|
+
...present("commandContext", request.commandContext),
|
|
30
|
+
...present("invokedToolName", request.invokedToolName),
|
|
31
|
+
...forwardingFacts(payload),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where the ask came from, when it came from somewhere else.
|
|
37
|
+
*
|
|
38
|
+
* A local ask is the default and states nothing; a forwarded one names the
|
|
39
|
+
* session that raised it, so a decision can be correlated back to the child
|
|
40
|
+
* that asked.
|
|
41
|
+
*/
|
|
42
|
+
function forwardingFacts(payload: PromptPayload): Record<string, unknown> {
|
|
43
|
+
const { forwarded, sessionId } = payload.request.requester;
|
|
44
|
+
return forwarded
|
|
45
|
+
? { forwarded: true, ...present("requesterSessionId", sessionId) }
|
|
46
|
+
: {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function present<T>(key: string, value: T | null): Record<string, T> {
|
|
50
|
+
return value === null ? {} : { [key]: value };
|
|
51
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -49,6 +49,17 @@ export {
|
|
|
49
49
|
PERMISSIONS_READY_CHANNEL,
|
|
50
50
|
PERMISSIONS_UI_PROMPT_CHANNEL,
|
|
51
51
|
} from "./permission-events";
|
|
52
|
+
// The declaration bundle already inlines these through `PromptPermissionDetails`
|
|
53
|
+
// and `PermissionUiPromptEvent`; the named exports are what a consumer needs to
|
|
54
|
+
// annotate a variable of their own.
|
|
55
|
+
export type {
|
|
56
|
+
PromptAnnotation,
|
|
57
|
+
PromptEvidence,
|
|
58
|
+
PromptPayload,
|
|
59
|
+
PromptPayloadKind,
|
|
60
|
+
PromptRequester,
|
|
61
|
+
PromptRequestFacts,
|
|
62
|
+
} from "./presentation/prompt-payload";
|
|
52
63
|
export type { PermissionCheckResult, PermissionState, ToolInputFormatter };
|
|
53
64
|
|
|
54
65
|
/** Process-global key for the service slot. */
|
|
@@ -2,7 +2,6 @@ import { safeJsonStringify } from "./json-safe-stringify";
|
|
|
2
2
|
import { redactedJsonStringify } from "./log-redaction";
|
|
3
3
|
|
|
4
4
|
export const TOOL_INPUT_PREVIEW_MAX_LENGTH = 200;
|
|
5
|
-
export const TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH = 1000;
|
|
6
5
|
export const TOOL_TEXT_SUMMARY_MAX_LENGTH = 80;
|
|
7
6
|
|
|
8
7
|
export function truncateInlineText(value: string, maxLength: number): string {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { classifyToolKind, isMcpCheck } from "./access-intent/tool-kind";
|
|
2
|
-
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
3
2
|
import type { ToolInputFormatterLookup } from "./tool-input-formatter-registry";
|
|
4
3
|
import {
|
|
5
4
|
serializeRedactedToolInputPreview,
|
|
6
5
|
serializeToolInputPreview,
|
|
7
|
-
TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
|
|
8
6
|
TOOL_INPUT_PREVIEW_MAX_LENGTH,
|
|
9
7
|
TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
|
10
8
|
truncateInlineText,
|
|
@@ -21,27 +19,21 @@ import { getNonEmptyString, toRecord } from "./value-guards";
|
|
|
21
19
|
export interface ToolPreviewFormatterOptions {
|
|
22
20
|
toolInputPreviewMaxLength: number;
|
|
23
21
|
toolTextSummaryMaxLength: number;
|
|
24
|
-
toolInputLogPreviewMaxLength: number;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
type ConfigurablePreviewLimits = Pick<
|
|
28
|
-
PermissionSystemExtensionConfig,
|
|
29
|
-
"toolInputPreviewMaxLength" | "toolTextSummaryMaxLength"
|
|
30
|
-
>;
|
|
31
|
-
|
|
32
24
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
25
|
+
* The built-in `ToolPreviewFormatterOptions`.
|
|
26
|
+
*
|
|
27
|
+
* Takes no config: `toolInputPreviewMaxLength` and `toolTextSummaryMaxLength`
|
|
28
|
+
* are subsumed by the renderer budgets (`promptMaxRows` / `promptFieldMaxWidth`,
|
|
29
|
+
* ADR 0011 §5), so an operator's values no longer take effect. The constants
|
|
30
|
+
* remain because they still shape a *prompt* preview; what the review log
|
|
31
|
+
* persists is bounded by `reviewLogFieldMaxWidth` at the writer instead.
|
|
35
32
|
*/
|
|
36
|
-
export function resolveToolPreviewLimits(
|
|
37
|
-
config: ConfigurablePreviewLimits,
|
|
38
|
-
): ToolPreviewFormatterOptions {
|
|
33
|
+
export function resolveToolPreviewLimits(): ToolPreviewFormatterOptions {
|
|
39
34
|
return {
|
|
40
|
-
toolInputPreviewMaxLength:
|
|
41
|
-
|
|
42
|
-
toolTextSummaryMaxLength:
|
|
43
|
-
config.toolTextSummaryMaxLength ?? TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
|
44
|
-
toolInputLogPreviewMaxLength: TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
|
|
35
|
+
toolInputPreviewMaxLength: TOOL_INPUT_PREVIEW_MAX_LENGTH,
|
|
36
|
+
toolTextSummaryMaxLength: TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
|
45
37
|
};
|
|
46
38
|
}
|
|
47
39
|
|
|
@@ -147,14 +139,16 @@ export class ToolPreviewFormatter {
|
|
|
147
139
|
// ── Log formatting ──────────────────────────────────────────────────────
|
|
148
140
|
|
|
149
141
|
/**
|
|
150
|
-
* Serialize `input` to inline JSON
|
|
151
|
-
*
|
|
142
|
+
* Serialize `input` to inline JSON for the review log, masking
|
|
143
|
+
* sensitive-keyed values.
|
|
144
|
+
*
|
|
145
|
+
* Unbounded here: the writer narrows every field it persists to
|
|
146
|
+
* `reviewLogFieldMaxWidth`, so a second bound at the producer would be a
|
|
147
|
+
* limit the operator cannot see or change.
|
|
152
148
|
*/
|
|
153
149
|
formatGenericToolInputForLog(input: unknown): string | undefined {
|
|
154
150
|
const inline = serializeRedactedToolInputPreview(input);
|
|
155
|
-
return inline
|
|
156
|
-
? `input ${truncateInlineText(inline, this.options.toolInputLogPreviewMaxLength)}`
|
|
157
|
-
: undefined;
|
|
151
|
+
return inline ? `input ${inline}` : undefined;
|
|
158
152
|
}
|
|
159
153
|
|
|
160
154
|
/** Derive a loggable input preview string for the review log. */
|
|
@@ -168,16 +162,7 @@ export class ToolPreviewFormatter {
|
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
if (pathBearingTools.has(result.toolName)) {
|
|
171
|
-
|
|
172
|
-
result.toolName,
|
|
173
|
-
input,
|
|
174
|
-
);
|
|
175
|
-
return inputPreview
|
|
176
|
-
? truncateInlineText(
|
|
177
|
-
inputPreview,
|
|
178
|
-
this.options.toolInputLogPreviewMaxLength,
|
|
179
|
-
)
|
|
180
|
-
: undefined;
|
|
165
|
+
return this.formatToolInputForPrompt(result.toolName, input) || undefined;
|
|
181
166
|
}
|
|
182
167
|
|
|
183
168
|
return this.formatGenericToolInputForLog(input);
|