@gotgenes/pi-permission-system 25.3.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 +48 -0
- package/README.md +14 -12
- package/config/config.example.json +1 -2
- package/dist/public.d.ts +37 -10
- package/docs/configuration.md +23 -19
- package/docs/cross-extension-api.md +43 -12
- 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 +32 -5
- 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 +26 -6
- package/src/handlers/gates/external-directory.ts +1 -13
- package/src/handlers/gates/helpers.ts +6 -7
- package/src/handlers/gates/path.ts +1 -9
- package/src/handlers/gates/runner.ts +62 -31
- package/src/handlers/gates/skill-input-gate-pipeline.ts +3 -14
- package/src/handlers/gates/skill-input.ts +1 -10
- package/src/handlers/gates/skill-read.ts +2 -11
- package/src/handlers/gates/tool-call-gate-pipeline.ts +1 -5
- package/src/handlers/gates/tool.ts +1 -11
- package/src/handlers/tool-call-boundary.ts +30 -7
- package/src/log-field-cap.ts +82 -0
- package/src/logging.ts +24 -3
- package/src/permission-events.ts +21 -2
- package/src/permission-prompts.ts +4 -3
- package/src/permission-request-id.ts +17 -0
- 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
|
@@ -34,7 +34,9 @@ import {
|
|
|
34
34
|
} from "#src/authority/permission-forwarding";
|
|
35
35
|
import type { ServingLookup } from "#src/authority/serving-registry";
|
|
36
36
|
import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
|
|
37
|
+
import { createPermissionRequestId } from "#src/permission-request-id";
|
|
37
38
|
import { buildUiPrompt } from "#src/permission-ui-prompt";
|
|
39
|
+
import type { PromptPayload } from "#src/presentation/prompt-payload";
|
|
38
40
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
39
41
|
import { toRecord } from "#src/value-guards";
|
|
40
42
|
import type { TerminalAuthorizer } from "./authorizer";
|
|
@@ -67,7 +69,7 @@ function getContextSystemPrompt(ctx: ForwarderContext): string | undefined {
|
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* The facts a forwarded request relays unchanged from the child's ask: the
|
|
70
|
-
* prompt
|
|
72
|
+
* prompt payload, the optional display projection, and the optional
|
|
71
73
|
* session-approval suggestion.
|
|
72
74
|
*
|
|
73
75
|
* Bundled into one object so the two-hop private chain
|
|
@@ -75,7 +77,14 @@ function getContextSystemPrompt(ctx: ForwarderContext): string | undefined {
|
|
|
75
77
|
* relayed value instead of three positional optionals.
|
|
76
78
|
*/
|
|
77
79
|
interface ForwardedRequestFacts {
|
|
78
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The requester's own permission request id, adopted as the forwarded
|
|
82
|
+
* request's id so one id runs from the child's gate to the serving node's
|
|
83
|
+
* decision instead of a third being minted here.
|
|
84
|
+
*/
|
|
85
|
+
requestId: string;
|
|
86
|
+
/** The child's complete prompt payload, relayed for the serving node to render. */
|
|
87
|
+
payload: PromptPayload;
|
|
79
88
|
display?: ForwardedPromptDisplay;
|
|
80
89
|
sessionApproval?: ForwardedSessionApproval;
|
|
81
90
|
/** The child-fixed access facts; the edge completes them into a `ForwardedAccessIntent`. */
|
|
@@ -111,6 +120,23 @@ function abandon(denialReason: string): PermissionPromptDecision {
|
|
|
111
120
|
};
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
/** Ids this node is willing to use as a request/response filename. */
|
|
124
|
+
const FILENAME_SAFE_REQUEST_ID = /^[A-Za-z0-9._-]+$/;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The id to write on the forwarded request: the requester's own, or a fresh
|
|
128
|
+
* mint when that id could not safely name a file.
|
|
129
|
+
*
|
|
130
|
+
* At a relay hop the adopted id came from a request file on disk, which the
|
|
131
|
+
* tolerant reader validates only as a string — so this is the boundary that
|
|
132
|
+
* keeps an inbound id from choosing an outbound path.
|
|
133
|
+
*/
|
|
134
|
+
function forwardableRequestId(requesterRequestId: string): string {
|
|
135
|
+
return FILENAME_SAFE_REQUEST_ID.test(requesterRequestId)
|
|
136
|
+
? requesterRequestId
|
|
137
|
+
: createPermissionRequestId();
|
|
138
|
+
}
|
|
139
|
+
|
|
114
140
|
/**
|
|
115
141
|
* Authorizer for a subagent session: escalate the ask up the tree to the
|
|
116
142
|
* parent's authority.
|
|
@@ -146,7 +172,8 @@ export class ParentAuthorizer implements TerminalAuthorizer {
|
|
|
146
172
|
): Promise<PermissionPromptDecision> {
|
|
147
173
|
const uiPrompt = buildUiPrompt(details);
|
|
148
174
|
return this.waitForForwardedApproval(this.ctx, {
|
|
149
|
-
|
|
175
|
+
requestId: details.requestId,
|
|
176
|
+
payload: details.payload,
|
|
150
177
|
display: {
|
|
151
178
|
source: uiPrompt.source,
|
|
152
179
|
surface: uiPrompt.surface,
|
|
@@ -250,7 +277,7 @@ export class ParentAuthorizer implements TerminalAuthorizer {
|
|
|
250
277
|
requesterSessionId: string,
|
|
251
278
|
targetSessionId: string,
|
|
252
279
|
): ForwardedPermissionRequest {
|
|
253
|
-
const requestId =
|
|
280
|
+
const requestId = forwardableRequestId(facts.requestId);
|
|
254
281
|
const requesterAgentName =
|
|
255
282
|
getActiveAgentName(ctx) ??
|
|
256
283
|
getActiveAgentNameFromSystemPrompt(getContextSystemPrompt(ctx)) ??
|
|
@@ -275,7 +302,7 @@ export class ParentAuthorizer implements TerminalAuthorizer {
|
|
|
275
302
|
requesterSessionId,
|
|
276
303
|
targetSessionId,
|
|
277
304
|
requesterAgentName,
|
|
278
|
-
|
|
305
|
+
payload: facts.payload,
|
|
279
306
|
...(facts.display
|
|
280
307
|
? {
|
|
281
308
|
source: facts.display.source,
|
|
@@ -49,9 +49,9 @@ export interface Authorizer {
|
|
|
49
49
|
* ADR 0007's terminal-cannot-defer invariant.
|
|
50
50
|
*
|
|
51
51
|
* One method, one responsibility. `DenyingAuthorizer` ignores `details`;
|
|
52
|
-
* `LocalUserAuthorizer`
|
|
53
|
-
* event from
|
|
54
|
-
*
|
|
52
|
+
* `LocalUserAuthorizer` renders `payload` for the human and derives the UI
|
|
53
|
+
* event from the request facts; `ParentAuthorizer` ships `payload` over the
|
|
54
|
+
* wire so the serving node renders it under its own budget.
|
|
55
55
|
*/
|
|
56
56
|
export interface TerminalAuthorizer {
|
|
57
57
|
authorize(
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from "#src/authority/permission-forwarding";
|
|
15
15
|
import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
|
|
16
16
|
import { buildForwardedAskPayload } from "#src/presentation/forwarded-ask-payload";
|
|
17
|
-
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
18
17
|
import { SessionApproval } from "#src/session-approval";
|
|
19
18
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
20
19
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
@@ -100,7 +99,6 @@ function buildForwardedAskDetails(
|
|
|
100
99
|
requestId: request.id,
|
|
101
100
|
source: request.source ?? "tool_call",
|
|
102
101
|
agentName: request.requesterAgentName || null,
|
|
103
|
-
message: renderLegacyMessage(payload),
|
|
104
102
|
payload,
|
|
105
103
|
surface: request.surface ?? null,
|
|
106
104
|
value: request.value ?? null,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
OWNER_ONLY_FILE_MODE,
|
|
24
24
|
} from "#src/log-file-permissions";
|
|
25
25
|
import type { PermissionUiPromptSource } from "#src/permission-events";
|
|
26
|
+
import { asPromptPayload } from "#src/presentation/prompt-payload";
|
|
26
27
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
27
28
|
|
|
28
29
|
/** Valid `permissions:ui_prompt` source values, for tolerant request reads. */
|
|
@@ -396,8 +397,7 @@ export function readForwardedPermissionRequest(
|
|
|
396
397
|
typeof parsed.createdAt !== "number" ||
|
|
397
398
|
typeof parsed.requesterSessionId !== "string" ||
|
|
398
399
|
typeof parsed.targetSessionId !== "string" ||
|
|
399
|
-
typeof parsed.requesterAgentName !== "string"
|
|
400
|
-
typeof parsed.message !== "string"
|
|
400
|
+
typeof parsed.requesterAgentName !== "string"
|
|
401
401
|
) {
|
|
402
402
|
logPermissionForwardingWarning(
|
|
403
403
|
logger,
|
|
@@ -412,9 +412,11 @@ export function readForwardedPermissionRequest(
|
|
|
412
412
|
requesterSessionId: parsed.requesterSessionId,
|
|
413
413
|
targetSessionId: parsed.targetSessionId,
|
|
414
414
|
requesterAgentName: parsed.requesterAgentName,
|
|
415
|
-
|
|
416
|
-
//
|
|
417
|
-
// child
|
|
415
|
+
// Tolerant read: the payload and display fields are optional and may be
|
|
416
|
+
// absent (older child) or malformed; reconstruct only the well-formed
|
|
417
|
+
// ones. An older child's `message` is deliberately not salvaged — a
|
|
418
|
+
// skewed ask renders from the fields it does carry (ADR 0011 §9).
|
|
419
|
+
payload: asPromptPayload(parsed.payload),
|
|
418
420
|
source: asUiPromptSource(parsed.source),
|
|
419
421
|
surface: asNullableDisplayString(parsed.surface),
|
|
420
422
|
value: asNullableDisplayString(parsed.value),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { PermissionUiPromptSource } from "#src/permission-events";
|
|
3
|
+
import type { PromptPayload } from "#src/presentation/prompt-payload";
|
|
3
4
|
import type { PermissionDecisionState } from "./permission-dialog";
|
|
4
5
|
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
5
6
|
|
|
@@ -53,9 +54,9 @@ const SESSION_FORWARDING_RESPONSES_DIRECTORY_NAME = "responses";
|
|
|
53
54
|
* Display fields relayed from a forwarding child to the parent UI so the parent
|
|
54
55
|
* can emit a non-degraded `permissions:ui_prompt` event.
|
|
55
56
|
*
|
|
56
|
-
* Carried separately from the prompt
|
|
57
|
+
* Carried separately from the prompt payload because the parent reconstructs
|
|
57
58
|
* the original event from the escalated ask's details (`buildUiPrompt`), not
|
|
58
|
-
* from the
|
|
59
|
+
* from the payload's own facts.
|
|
59
60
|
*/
|
|
60
61
|
export interface ForwardedPromptDisplay {
|
|
61
62
|
source: PermissionUiPromptSource;
|
|
@@ -123,7 +124,15 @@ export type ForwardedPermissionRequest = {
|
|
|
123
124
|
requesterSessionId: string;
|
|
124
125
|
targetSessionId: string;
|
|
125
126
|
requesterAgentName: string;
|
|
126
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The child's complete prompt payload (ADR 0011 §2), so the serving node
|
|
129
|
+
* renders the child's own facts under the *parent's* budget rather than
|
|
130
|
+
* relaying a sentence the child assembled under its own configuration.
|
|
131
|
+
*
|
|
132
|
+
* Optional for version-skew tolerance: an older child omits it, and the
|
|
133
|
+
* serving node renders from the display fields it does carry (ADR 0011 §9).
|
|
134
|
+
*/
|
|
135
|
+
payload?: PromptPayload;
|
|
127
136
|
/**
|
|
128
137
|
* Original prompt display fields, persisted so the parent emits a
|
|
129
138
|
* non-degraded event. Optional for version-skew tolerance: a parent on a
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ForwardedSessionApproval,
|
|
5
5
|
} from "#src/authority/permission-forwarding";
|
|
6
6
|
import type { PromptPayload } from "#src/presentation/prompt-payload";
|
|
7
|
+
import { renderReviewLogFacts } from "#src/presentation/review-log-renderer";
|
|
7
8
|
import type { ReviewLogger } from "#src/session-logger";
|
|
8
9
|
import type { TerminalAuthorizer } from "./authorizer";
|
|
9
10
|
|
|
@@ -27,13 +28,13 @@ export interface PromptPermissionDetails {
|
|
|
27
28
|
requestId: string;
|
|
28
29
|
source: PermissionReviewSource;
|
|
29
30
|
agentName: string | null;
|
|
30
|
-
message: string;
|
|
31
31
|
/**
|
|
32
32
|
* The complete structured description of this ask (ADR 0011 §2).
|
|
33
33
|
*
|
|
34
34
|
* Required: every ask carries one, and the type is what guarantees it rather
|
|
35
|
-
* than a convention each gate has to remember.
|
|
36
|
-
*
|
|
35
|
+
* than a convention each gate has to remember. Every consumer — the dialog,
|
|
36
|
+
* the wire, the broadcast, the review log, the agent-facing denial text — is
|
|
37
|
+
* a render over it, so no two of them can disagree.
|
|
37
38
|
*/
|
|
38
39
|
payload: PromptPayload;
|
|
39
40
|
toolCallId?: string;
|
|
@@ -149,7 +150,7 @@ export class PermissionPrompter implements PermissionPrompterApi {
|
|
|
149
150
|
requestId: details.requestId,
|
|
150
151
|
source: details.source,
|
|
151
152
|
agentName: details.agentName,
|
|
152
|
-
|
|
153
|
+
...renderReviewLogFacts(details.payload),
|
|
153
154
|
toolCallId: details.toolCallId ?? null,
|
|
154
155
|
toolName: details.toolName ?? null,
|
|
155
156
|
skillName: details.skillName ?? null,
|
package/src/config-loader.ts
CHANGED
|
@@ -224,6 +224,7 @@ export function mergeUnifiedConfigs(
|
|
|
224
224
|
"forwardingTimeoutMs",
|
|
225
225
|
"promptMaxRows",
|
|
226
226
|
"promptFieldMaxWidth",
|
|
227
|
+
"reviewLogFieldMaxWidth",
|
|
227
228
|
"toolInputPreviewMaxLength",
|
|
228
229
|
"toolTextSummaryMaxLength",
|
|
229
230
|
] as const) {
|
|
@@ -374,6 +375,9 @@ export function loadAndMergeConfigs(
|
|
|
374
375
|
const bashFallbackIssue = detectPermissiveBashFallback(merged.permission);
|
|
375
376
|
if (bashFallbackIssue) allIssues.push(bashFallbackIssue);
|
|
376
377
|
|
|
378
|
+
const deprecatedCapsIssue = detectDeprecatedPreviewCaps(merged);
|
|
379
|
+
if (deprecatedCapsIssue) allIssues.push(deprecatedCapsIssue);
|
|
380
|
+
|
|
377
381
|
return {
|
|
378
382
|
global: globalConfig,
|
|
379
383
|
project: projectConfig,
|
|
@@ -414,6 +418,33 @@ export function detectPermissiveBashFallback(
|
|
|
414
418
|
);
|
|
415
419
|
}
|
|
416
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Detect a config still setting one of the two superseded tool-preview caps.
|
|
423
|
+
*
|
|
424
|
+
* `toolInputPreviewMaxLength` and `toolTextSummaryMaxLength` bounded one
|
|
425
|
+
* preview inside a prompt, never the prompt itself, which is why they never
|
|
426
|
+
* bounded it; `promptMaxRows` and `promptFieldMaxWidth` supersede them
|
|
427
|
+
* (ADR 0011 §5). Both stay valid in the schema so an existing config is not
|
|
428
|
+
* rejected fail-closed — they are simply no longer read.
|
|
429
|
+
*
|
|
430
|
+
* Pure, following `detectPermissiveBashFallback`: it takes the merged config
|
|
431
|
+
* and returns a message; the caller owns pushing it onto the issue list.
|
|
432
|
+
*/
|
|
433
|
+
export function detectDeprecatedPreviewCaps(
|
|
434
|
+
config: UnifiedPermissionConfig,
|
|
435
|
+
): string | undefined {
|
|
436
|
+
const set = (
|
|
437
|
+
["toolInputPreviewMaxLength", "toolTextSummaryMaxLength"] as const
|
|
438
|
+
).filter((key) => config[key] !== undefined);
|
|
439
|
+
if (set.length === 0) return undefined;
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
`Permission config sets ${set.map((key) => `'${key}'`).join(" and ")}, ` +
|
|
443
|
+
"which is deprecated and ignored. The prompt is bounded by " +
|
|
444
|
+
"'promptMaxRows' and 'promptFieldMaxWidth' instead; remove the setting."
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
417
448
|
/**
|
|
418
449
|
* Load and normalize a unified config file.
|
|
419
450
|
* Returns an empty config with no issues if the file does not exist.
|
package/src/config-schema.ts
CHANGED
|
@@ -208,17 +208,26 @@ export const unifiedConfigSchema = z
|
|
|
208
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
209
|
default: 400,
|
|
210
210
|
}),
|
|
211
|
+
reviewLogFieldMaxWidth: z.number().int().min(1).optional().meta({
|
|
212
|
+
description:
|
|
213
|
+
"Maximum characters of any one value written to the permission review log. Omit to use the default (1000).",
|
|
214
|
+
markdownDescription:
|
|
215
|
+
"Maximum characters of any one value written to the permission review log.\n\nOmit to use the default (1000). Every string the review log writes is narrowed to this width and marked with an ellipsis, so the log's growth is a decision you make rather than a side effect of how long a command happened to be. Raise it to keep longer values \u2014 a bash command exceeding the width is stored shortened.\n\nThis is a length bound, not redaction: it never inspects a value to decide what to hide. Key-name masking is unchanged and applies independently.",
|
|
216
|
+
default: 1000,
|
|
217
|
+
}),
|
|
211
218
|
toolInputPreviewMaxLength: z.number().int().min(1).optional().meta({
|
|
219
|
+
deprecated: true,
|
|
212
220
|
description:
|
|
213
|
-
"
|
|
221
|
+
"Deprecated and ignored. Superseded by promptMaxRows and promptFieldMaxWidth, which bound the whole prompt rather than one preview. Still accepted so an existing config is not rejected; remove it.",
|
|
214
222
|
markdownDescription:
|
|
215
|
-
"
|
|
223
|
+
"**Deprecated and ignored.** Superseded by `promptMaxRows` and `promptFieldMaxWidth`, which bound the whole permission prompt rather than one preview inside it.\n\nStill accepted so an existing config is not rejected fail-closed, but the value no longer takes effect. Remove it.",
|
|
216
224
|
}),
|
|
217
225
|
toolTextSummaryMaxLength: z.number().int().min(1).optional().meta({
|
|
226
|
+
deprecated: true,
|
|
218
227
|
description:
|
|
219
|
-
"
|
|
228
|
+
"Deprecated and ignored. Superseded by promptMaxRows and promptFieldMaxWidth, which bound the whole prompt rather than one summary. Still accepted so an existing config is not rejected; remove it.",
|
|
220
229
|
markdownDescription:
|
|
221
|
-
"
|
|
230
|
+
"**Deprecated and ignored.** Superseded by `promptMaxRows` and `promptFieldMaxWidth`, which bound the whole permission prompt rather than one summary inside it.\n\nStill accepted so an existing config is not rejected fail-closed, but the value no longer takes effect. Remove it.",
|
|
222
231
|
}),
|
|
223
232
|
piInfrastructureReadPaths: z.array(z.string().min(1)).optional().meta({
|
|
224
233
|
description:
|
package/src/extension-config.ts
CHANGED
|
@@ -26,10 +26,8 @@ export interface PermissionSystemExtensionConfig {
|
|
|
26
26
|
promptMaxRows?: number;
|
|
27
27
|
/** Max characters of any one field shown in a permission prompt. Defaults to 400. */
|
|
28
28
|
promptFieldMaxWidth?: number;
|
|
29
|
-
/** Max
|
|
30
|
-
|
|
31
|
-
/** Max length of inline pattern/path summaries (grep/find/ls) in permission prompts. Defaults to 80. */
|
|
32
|
-
toolTextSummaryMaxLength?: number;
|
|
29
|
+
/** Max characters of any one value written to the permission review log. Defaults to 1000. */
|
|
30
|
+
reviewLogFieldMaxWidth?: number;
|
|
33
31
|
/** Non-bash tools that carry shell semantics, keyed by tool name. */
|
|
34
32
|
shellTools?: ShellToolsConfig;
|
|
35
33
|
/** Ordered names of registered live-authority chain links to consult before the terminal authorizer. */
|
|
@@ -86,12 +84,13 @@ export function normalizePermissionSystemConfig(
|
|
|
86
84
|
if (raw.promptFieldMaxWidth !== undefined) {
|
|
87
85
|
result.promptFieldMaxWidth = raw.promptFieldMaxWidth;
|
|
88
86
|
}
|
|
89
|
-
if (raw.
|
|
90
|
-
result.
|
|
91
|
-
}
|
|
92
|
-
if (raw.toolTextSummaryMaxLength !== undefined) {
|
|
93
|
-
result.toolTextSummaryMaxLength = raw.toolTextSummaryMaxLength;
|
|
87
|
+
if (raw.reviewLogFieldMaxWidth !== undefined) {
|
|
88
|
+
result.reviewLogFieldMaxWidth = raw.reviewLogFieldMaxWidth;
|
|
94
89
|
}
|
|
90
|
+
// `toolInputPreviewMaxLength` / `toolTextSummaryMaxLength` are deliberately
|
|
91
|
+
// absent: the schema and the merge still accept them so the deprecation
|
|
92
|
+
// detector can see an operator's setting, but no runtime consumer may read
|
|
93
|
+
// one (ADR 0011 §5, #745).
|
|
95
94
|
if (raw.shellTools !== undefined) {
|
|
96
95
|
result.shellTools = raw.shellTools;
|
|
97
96
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
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
3
|
import { buildBashExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
|
|
5
4
|
import { SessionApproval } from "#src/session-approval";
|
|
6
5
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
@@ -85,26 +84,17 @@ export function describeBashExternalDirectoryGate(
|
|
|
85
84
|
toolName: tcc.toolName,
|
|
86
85
|
matchedPattern: preCheck.matchedPattern,
|
|
87
86
|
});
|
|
88
|
-
const bashExtMessage = renderLegacyMessage(payload);
|
|
89
87
|
|
|
90
88
|
const patterns = uncoveredPaths.map((p) => deriveApprovalPattern(p));
|
|
91
89
|
|
|
92
90
|
return {
|
|
93
91
|
surface: "external_directory",
|
|
94
92
|
input: {},
|
|
95
|
-
|
|
96
|
-
kind: "bash_external_directory",
|
|
97
|
-
command,
|
|
98
|
-
externalPaths: disclosures,
|
|
99
|
-
cwd: tcc.cwd,
|
|
100
|
-
agentName: tcc.agentName ?? undefined,
|
|
101
|
-
},
|
|
93
|
+
payload,
|
|
102
94
|
sessionApproval: SessionApproval.multiple("external_directory", patterns),
|
|
103
95
|
promptDetails: {
|
|
104
96
|
source: "tool_call",
|
|
105
97
|
agentName: tcc.agentName,
|
|
106
|
-
message: bashExtMessage,
|
|
107
|
-
payload,
|
|
108
98
|
toolCallId: tcc.toolCallId,
|
|
109
99
|
toolName: tcc.toolName,
|
|
110
100
|
command,
|
|
@@ -117,7 +107,6 @@ export function describeBashExternalDirectoryGate(
|
|
|
117
107
|
agentName: tcc.agentName,
|
|
118
108
|
command,
|
|
119
109
|
externalPaths: uncoveredPaths,
|
|
120
|
-
message: bashExtMessage,
|
|
121
110
|
},
|
|
122
111
|
decision: {
|
|
123
112
|
surface: "external_directory",
|
|
@@ -1,7 +1,6 @@
|
|
|
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
4
|
import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
|
|
6
5
|
import { SessionApproval } from "#src/session-approval";
|
|
7
6
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
@@ -120,23 +119,15 @@ export function describeBashPathGate(
|
|
|
120
119
|
agentName: tcc.agentName,
|
|
121
120
|
matchedPattern: worstCheck.matchedPattern,
|
|
122
121
|
});
|
|
123
|
-
const askMessage = renderLegacyMessage(payload);
|
|
124
122
|
|
|
125
123
|
return {
|
|
126
124
|
surface: "path",
|
|
127
125
|
input: { path: worstToken },
|
|
128
|
-
|
|
129
|
-
kind: "bash_path",
|
|
130
|
-
command,
|
|
131
|
-
pathValue: worstToken,
|
|
132
|
-
agentName: tcc.agentName ?? undefined,
|
|
133
|
-
},
|
|
126
|
+
payload,
|
|
134
127
|
sessionApproval: SessionApproval.single("path", pattern),
|
|
135
128
|
promptDetails: {
|
|
136
129
|
source: "tool_call",
|
|
137
130
|
agentName: tcc.agentName,
|
|
138
|
-
message: askMessage,
|
|
139
|
-
payload,
|
|
140
131
|
toolCallId: tcc.toolCallId,
|
|
141
132
|
toolName: tcc.toolName,
|
|
142
133
|
command,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PromptPermissionDetails } from "#src/authority/permission-prompter";
|
|
2
|
-
import type { DenialContext } from "#src/denial-messages";
|
|
3
2
|
import type { PermissionDecisionEvent } from "#src/permission-events";
|
|
3
|
+
import type { PromptPayload } from "#src/presentation/prompt-payload";
|
|
4
4
|
import type { SessionApproval } from "#src/session-approval";
|
|
5
5
|
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
6
6
|
|
|
@@ -18,16 +18,27 @@ export interface GateDescriptor {
|
|
|
18
18
|
surface: string;
|
|
19
19
|
/** Input passed to checkPermission. */
|
|
20
20
|
input: unknown;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The complete structured description of this ask (ADR 0011 §2).
|
|
23
|
+
*
|
|
24
|
+
* The descriptor's one presentation fact: every render over it — the dialog,
|
|
25
|
+
* the agent-facing denial text, the review log — reads this and nothing
|
|
26
|
+
* else, so a gate states its facts once.
|
|
27
|
+
*/
|
|
28
|
+
payload: PromptPayload;
|
|
23
29
|
/**
|
|
24
30
|
* Session-approval suggestion for the "for this session" option.
|
|
25
31
|
* Wraps either a single pattern or multiple patterns behind a unified
|
|
26
32
|
* interface — the runner never needs to know which case applies.
|
|
27
33
|
*/
|
|
28
34
|
sessionApproval?: SessionApproval;
|
|
29
|
-
/**
|
|
30
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Details passed to the interactive permission prompt.
|
|
37
|
+
*
|
|
38
|
+
* The runner stamps both `requestId` (which it mints) and `payload` (which
|
|
39
|
+
* the descriptor owns), so neither is a gate's to supply twice.
|
|
40
|
+
*/
|
|
41
|
+
promptDetails: Omit<PromptPermissionDetails, "requestId" | "payload">;
|
|
31
42
|
/** Extra context fields written to the review log alongside gate outcomes. */
|
|
32
43
|
logContext: Record<string, unknown>;
|
|
33
44
|
/** Surface and value for the decision event (may differ from the check surface). */
|
|
@@ -51,6 +62,15 @@ export interface GateDescriptor {
|
|
|
51
62
|
preCheck?: PermissionCheckResult;
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
/**
|
|
66
|
+
* A decision event's facts, before the runner stamps the request id it minted.
|
|
67
|
+
*
|
|
68
|
+
* A gate knows what was decided but not which request it was deciding — the id
|
|
69
|
+
* is minted in `GateRunner.run`. Producing this type rather than the full event
|
|
70
|
+
* is what routes every emit through the runner's single stamping site.
|
|
71
|
+
*/
|
|
72
|
+
export type DecisionEventFacts = Omit<PermissionDecisionEvent, "requestId">;
|
|
73
|
+
|
|
54
74
|
/**
|
|
55
75
|
* Early allow result — gate has determined the action without needing the runner.
|
|
56
76
|
*
|
|
@@ -62,7 +82,7 @@ export interface GateBypass {
|
|
|
62
82
|
/** Optional review log entry to emit. */
|
|
63
83
|
log?: { event: string; details: Record<string, unknown> };
|
|
64
84
|
/** Optional decision event to emit. */
|
|
65
|
-
decision?:
|
|
85
|
+
decision?: DecisionEventFacts;
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
/** Union of possible gate function return values. */
|
|
@@ -1,7 +1,6 @@
|
|
|
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
4
|
import { buildExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
|
|
6
5
|
import { SessionApproval } from "#src/session-approval";
|
|
7
6
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
@@ -87,26 +86,16 @@ export function describeExternalDirectoryGate(
|
|
|
87
86
|
agentName: tcc.agentName,
|
|
88
87
|
matchedPattern: preCheck.matchedPattern,
|
|
89
88
|
});
|
|
90
|
-
const extDirMessage = renderLegacyMessage(payload);
|
|
91
89
|
|
|
92
90
|
return {
|
|
93
91
|
surface: "external_directory",
|
|
94
92
|
input: {},
|
|
95
93
|
preCheck,
|
|
96
|
-
|
|
97
|
-
kind: "external_directory",
|
|
98
|
-
toolName: tcc.toolName,
|
|
99
|
-
pathValue: externalDirectoryPath,
|
|
100
|
-
resolvedPath: resolvedAlias,
|
|
101
|
-
cwd: tcc.cwd,
|
|
102
|
-
agentName: tcc.agentName ?? undefined,
|
|
103
|
-
},
|
|
94
|
+
payload,
|
|
104
95
|
sessionApproval: SessionApproval.single("external_directory", pattern),
|
|
105
96
|
promptDetails: {
|
|
106
97
|
source: "tool_call",
|
|
107
98
|
agentName: tcc.agentName,
|
|
108
|
-
message: extDirMessage,
|
|
109
|
-
payload,
|
|
110
99
|
toolCallId: tcc.toolCallId,
|
|
111
100
|
toolName: tcc.toolName,
|
|
112
101
|
path: externalDirectoryPath,
|
|
@@ -118,7 +107,6 @@ export function describeExternalDirectoryGate(
|
|
|
118
107
|
toolName: tcc.toolName,
|
|
119
108
|
agentName: tcc.agentName,
|
|
120
109
|
path: externalDirectoryPath,
|
|
121
|
-
message: extDirMessage,
|
|
122
110
|
},
|
|
123
111
|
decision: {
|
|
124
112
|
surface: "external_directory",
|
|
@@ -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,7 +1,6 @@
|
|
|
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
4
|
import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
|
|
6
5
|
import { SessionApproval } from "#src/session-approval";
|
|
7
6
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
@@ -59,18 +58,11 @@ export function describePathGate(
|
|
|
59
58
|
const descriptor: GateDescriptor = {
|
|
60
59
|
surface: "path",
|
|
61
60
|
input: { path: filePath },
|
|
62
|
-
|
|
63
|
-
kind: "path",
|
|
64
|
-
toolName: tcc.toolName,
|
|
65
|
-
pathValue: filePath,
|
|
66
|
-
agentName: tcc.agentName ?? undefined,
|
|
67
|
-
},
|
|
61
|
+
payload,
|
|
68
62
|
sessionApproval: SessionApproval.single("path", pattern),
|
|
69
63
|
promptDetails: {
|
|
70
64
|
source: "tool_call",
|
|
71
65
|
agentName: tcc.agentName,
|
|
72
|
-
message: renderLegacyMessage(payload),
|
|
73
|
-
payload,
|
|
74
66
|
toolCallId: tcc.toolCallId,
|
|
75
67
|
toolName: tcc.toolName,
|
|
76
68
|
path: filePath,
|