@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
|
@@ -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). */
|
|
@@ -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,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,
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { AskEscalator } from "#src/authority/authorizer-selection";
|
|
2
2
|
import type { PermissionPromptDecision } from "#src/authority/permission-dialog";
|
|
3
3
|
import type { DecisionReporter } from "#src/decision-reporter";
|
|
4
|
-
import {
|
|
5
|
-
formatDenyReason,
|
|
6
|
-
formatUnavailableReason,
|
|
7
|
-
formatUserDeniedReason,
|
|
8
|
-
} from "#src/denial-messages";
|
|
9
4
|
import { applyPermissionGate } from "#src/permission-gate";
|
|
10
5
|
import { createPermissionRequestId } from "#src/permission-request-id";
|
|
11
6
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
7
|
+
import {
|
|
8
|
+
renderPolicyDenial,
|
|
9
|
+
renderUnavailableDenial,
|
|
10
|
+
renderUserDenial,
|
|
11
|
+
} from "#src/presentation/agent-renderer";
|
|
12
|
+
import { renderReviewLogFacts } from "#src/presentation/review-log-renderer";
|
|
12
13
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
13
14
|
import type { PermissionCheckResult } from "#src/types";
|
|
14
15
|
import type {
|
|
@@ -111,8 +112,16 @@ export class GateRunner {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
// The fields every review-log write for this gate shares, whatever the
|
|
114
|
-
// resolution — built once so a field added here reaches all of them.
|
|
115
|
-
|
|
115
|
+
// resolution — built once so a field added here reaches all of them. The
|
|
116
|
+
// payload's request facts are stamped here rather than by each gate, for
|
|
117
|
+
// the same reason `requestId` is: a gate cannot forget what it never
|
|
118
|
+
// supplies (ADR 0011 §6).
|
|
119
|
+
const logContext = {
|
|
120
|
+
...descriptor.logContext,
|
|
121
|
+
...renderReviewLogFacts(descriptor.payload),
|
|
122
|
+
agentName,
|
|
123
|
+
requestId,
|
|
124
|
+
};
|
|
116
125
|
|
|
117
126
|
// 2. Session-hit fast path
|
|
118
127
|
if (check.source === "session") {
|
|
@@ -160,16 +169,16 @@ export class GateRunner {
|
|
|
160
169
|
// 3. Apply the deny/ask/allow gate — always escalate on ask; the selected
|
|
161
170
|
// Authorizer answers (the DenyingAuthorizer by denying with a marker).
|
|
162
171
|
|
|
163
|
-
//
|
|
172
|
+
// The agent-facing renders of this ask. The rule reason is the operator's
|
|
173
|
+
// deny-with-reason text, which lives on the resolved check rather than the
|
|
174
|
+
// payload: no human render wants it, because a deny never prompts.
|
|
175
|
+
const { payload } = descriptor;
|
|
164
176
|
const messages = {
|
|
165
|
-
denyReason:
|
|
177
|
+
denyReason: renderPolicyDenial(payload, check.reason ?? null),
|
|
166
178
|
unavailableReason: (decision: PermissionPromptDecision) =>
|
|
167
|
-
|
|
168
|
-
descriptor.denialContext,
|
|
169
|
-
decision.denialReason,
|
|
170
|
-
),
|
|
179
|
+
renderUnavailableDenial(payload, decision.denialReason ?? null),
|
|
171
180
|
userDeniedReason: (decision: PermissionPromptDecision) =>
|
|
172
|
-
|
|
181
|
+
renderUserDenial(payload, decision.denialReason ?? null),
|
|
173
182
|
};
|
|
174
183
|
|
|
175
184
|
let autoApproved = false;
|
|
@@ -180,6 +189,7 @@ export class GateRunner {
|
|
|
180
189
|
promptForApproval: async () => {
|
|
181
190
|
const decision = await this.prompter.escalate({
|
|
182
191
|
requestId,
|
|
192
|
+
payload,
|
|
183
193
|
...descriptor.promptDetails,
|
|
184
194
|
...(descriptor.sessionApproval
|
|
185
195
|
? { sessionApproval: descriptor.sessionApproval.toForwardedData() }
|
|
@@ -80,8 +80,8 @@ export class SkillInputGatePipeline {
|
|
|
80
80
|
* Format the deny warning shown in the UI when a skill is blocked.
|
|
81
81
|
*
|
|
82
82
|
* Intentionally untagged (no `[pi-permission-system]` prefix) — this is a
|
|
83
|
-
* UI notify distinct from the
|
|
84
|
-
* `
|
|
83
|
+
* UI notify distinct from the agent-facing deny reasons the runner routes
|
|
84
|
+
* through `renderPolicyDenial`.
|
|
85
85
|
*/
|
|
86
86
|
export function formatSkillDenyNotice(
|
|
87
87
|
skillName: string,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
2
1
|
import { buildSkillAskPayload } from "#src/presentation/skill-ask-payload";
|
|
3
2
|
import type { PermissionCheckResult } from "#src/types";
|
|
4
3
|
import type { GateDescriptor } from "./descriptor";
|
|
@@ -17,21 +16,14 @@ export function describeSkillInputGate(
|
|
|
17
16
|
preCheck: PermissionCheckResult,
|
|
18
17
|
): GateDescriptor {
|
|
19
18
|
const payload = buildSkillAskPayload(skillName, agentName);
|
|
20
|
-
const message = renderLegacyMessage(payload);
|
|
21
19
|
return {
|
|
22
20
|
surface: "skill",
|
|
23
21
|
input: { name: skillName },
|
|
24
22
|
preCheck,
|
|
25
|
-
|
|
26
|
-
kind: "skill_input",
|
|
27
|
-
skillName,
|
|
28
|
-
agentName: agentName ?? undefined,
|
|
29
|
-
},
|
|
23
|
+
payload,
|
|
30
24
|
promptDetails: {
|
|
31
25
|
source: "skill_input",
|
|
32
26
|
agentName,
|
|
33
|
-
message,
|
|
34
|
-
payload,
|
|
35
27
|
skillName,
|
|
36
28
|
accessIntent: accessFactsFromValue("skill", skillName),
|
|
37
29
|
},
|
|
@@ -39,7 +31,6 @@ export function describeSkillInputGate(
|
|
|
39
31
|
source: "skill_input",
|
|
40
32
|
skillName,
|
|
41
33
|
agentName,
|
|
42
|
-
message,
|
|
43
34
|
},
|
|
44
35
|
decision: {
|
|
45
36
|
surface: "skill",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
2
|
-
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
3
2
|
import { buildSkillPathAskPayload } from "#src/presentation/skill-ask-payload";
|
|
4
3
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
5
4
|
import { findSkillPathMatch } from "#src/skill-prompt-sanitizer";
|
|
@@ -44,22 +43,14 @@ export function describeSkillReadGate(
|
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
const payload = buildSkillPathAskPayload(matchedSkill, path, tcc.agentName);
|
|
47
|
-
const skillReadMessage = renderLegacyMessage(payload);
|
|
48
46
|
|
|
49
47
|
return {
|
|
50
48
|
surface: "skill",
|
|
51
49
|
input: { name: matchedSkill.name },
|
|
52
|
-
|
|
53
|
-
kind: "skill_read",
|
|
54
|
-
skillName: matchedSkill.name,
|
|
55
|
-
readPath: path,
|
|
56
|
-
agentName: tcc.agentName ?? undefined,
|
|
57
|
-
},
|
|
50
|
+
payload,
|
|
58
51
|
promptDetails: {
|
|
59
52
|
source: "skill_read",
|
|
60
53
|
agentName: tcc.agentName,
|
|
61
|
-
message: skillReadMessage,
|
|
62
|
-
payload,
|
|
63
54
|
toolCallId: tcc.toolCallId,
|
|
64
55
|
toolName: tcc.toolName,
|
|
65
56
|
skillName: matchedSkill.name,
|
|
@@ -72,7 +63,6 @@ export function describeSkillReadGate(
|
|
|
72
63
|
skillName: matchedSkill.name,
|
|
73
64
|
agentName: tcc.agentName,
|
|
74
65
|
path,
|
|
75
|
-
message: skillReadMessage,
|
|
76
66
|
},
|
|
77
67
|
decision: {
|
|
78
68
|
surface: "skill",
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
type ShellInvocation,
|
|
7
7
|
} from "#src/access-intent/tool-kind";
|
|
8
8
|
import { suggestSessionPattern } from "#src/pattern-suggest";
|
|
9
|
-
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
10
9
|
import { buildToolAskPayload } from "#src/presentation/tool-ask-payload";
|
|
11
10
|
import { SessionApproval } from "#src/session-approval";
|
|
12
11
|
import type { ToolPreviewFormatter } from "#src/tool-preview-formatter";
|
|
@@ -81,7 +80,6 @@ export function describeToolGate(
|
|
|
81
80
|
input: tcc.input,
|
|
82
81
|
formatter,
|
|
83
82
|
});
|
|
84
|
-
const askMessage = renderLegacyMessage(payload);
|
|
85
83
|
|
|
86
84
|
const decisionValue = deriveDecisionValue(
|
|
87
85
|
gateSurface,
|
|
@@ -98,12 +96,7 @@ export function describeToolGate(
|
|
|
98
96
|
return {
|
|
99
97
|
surface: gateSurface,
|
|
100
98
|
input: tcc.input,
|
|
101
|
-
|
|
102
|
-
kind: "tool",
|
|
103
|
-
check,
|
|
104
|
-
agentName: tcc.agentName ?? undefined,
|
|
105
|
-
input: tcc.input,
|
|
106
|
-
},
|
|
99
|
+
payload,
|
|
107
100
|
sessionApproval: SessionApproval.single(
|
|
108
101
|
suggestion.surface,
|
|
109
102
|
suggestion.pattern,
|
|
@@ -111,8 +104,6 @@ export function describeToolGate(
|
|
|
111
104
|
promptDetails: {
|
|
112
105
|
source: "tool_call",
|
|
113
106
|
agentName: tcc.agentName,
|
|
114
|
-
message: askMessage,
|
|
115
|
-
payload,
|
|
116
107
|
toolCallId: tcc.toolCallId,
|
|
117
108
|
toolName: tcc.toolName,
|
|
118
109
|
sessionLabel: suggestion.label,
|
|
@@ -123,7 +114,6 @@ export function describeToolGate(
|
|
|
123
114
|
source: "tool_call",
|
|
124
115
|
toolCallId: tcc.toolCallId,
|
|
125
116
|
toolName: tcc.toolName,
|
|
126
|
-
message: askMessage,
|
|
127
117
|
...permissionLogContext,
|
|
128
118
|
},
|
|
129
119
|
decision: {
|