@gotgenes/pi-permission-system 25.2.2 → 25.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +1 -1
- package/config/config.example.json +3 -0
- package/dist/public.d.ts +162 -41
- package/docs/configuration.md +17 -2
- package/docs/cross-extension-api.md +14 -9
- package/package.json +1 -1
- package/schemas/permissions.schema.json +16 -0
- package/src/access-intent/bash/command-enumeration.ts +45 -117
- package/src/access-intent/bash/wrapper-analysis.ts +335 -0
- package/src/authority/approval-escalator.ts +26 -1
- package/src/authority/forwarded-request-server.ts +5 -14
- package/src/authority/local-user-authorizer.ts +2 -3
- package/src/authority/permission-prompt-component.ts +87 -47
- package/src/authority/permission-prompter.ts +9 -0
- package/src/config-loader.ts +2 -0
- package/src/config-schema.ts +14 -0
- package/src/extension-config.ts +10 -0
- package/src/handlers/gates/bash-command.ts +7 -2
- package/src/handlers/gates/bash-external-directory.ts +11 -6
- package/src/handlers/gates/bash-path.ts +10 -6
- package/src/handlers/gates/descriptor.ts +10 -1
- package/src/handlers/gates/external-directory.ts +13 -8
- package/src/handlers/gates/helpers.ts +6 -7
- package/src/handlers/gates/path.ts +11 -14
- package/src/handlers/gates/runner.ts +40 -19
- package/src/handlers/gates/skill-input-gate-pipeline.ts +1 -12
- package/src/handlers/gates/skill-input.ts +5 -2
- package/src/handlers/gates/skill-read.ts +6 -6
- package/src/handlers/gates/tool-call-gate-pipeline.ts +1 -5
- package/src/handlers/gates/tool.ts +10 -5
- package/src/handlers/tool-call-boundary.ts +30 -7
- package/src/index.ts +2 -0
- package/src/permission-events.ts +6 -0
- package/src/permission-prompts.ts +4 -72
- package/src/permission-request-id.ts +17 -0
- package/src/presentation/dialog-renderer.ts +404 -0
- package/src/presentation/forwarded-ask-payload.ts +45 -0
- package/src/presentation/legacy-message.ts +117 -0
- package/src/presentation/line-fitting.ts +27 -0
- package/src/presentation/path-ask-payload.ts +128 -0
- package/src/presentation/prompt-payload.ts +137 -0
- package/src/presentation/skill-ask-payload.ts +50 -0
- package/src/presentation/tool-ask-payload.ts +104 -0
- package/src/tool-preview-formatter.ts +1 -1
- package/src/types.ts +6 -0
- package/src/handlers/gates/external-directory-messages.ts +0 -28
|
@@ -7,10 +7,15 @@ import {
|
|
|
7
7
|
formatUserDeniedReason,
|
|
8
8
|
} from "#src/denial-messages";
|
|
9
9
|
import { applyPermissionGate } from "#src/permission-gate";
|
|
10
|
+
import { createPermissionRequestId } from "#src/permission-request-id";
|
|
10
11
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
11
12
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
12
13
|
import type { PermissionCheckResult } from "#src/types";
|
|
13
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
DecisionEventFacts,
|
|
16
|
+
GateDescriptor,
|
|
17
|
+
GateResult,
|
|
18
|
+
} from "./descriptor";
|
|
14
19
|
import { isGateBypass } from "./descriptor";
|
|
15
20
|
import {
|
|
16
21
|
buildDecisionEvent,
|
|
@@ -46,33 +51,44 @@ export class GateRunner {
|
|
|
46
51
|
/**
|
|
47
52
|
* Execute a gate: null → allow; bypass → log/emit side effects then allow;
|
|
48
53
|
* descriptor → full check→log→emit→approve cycle.
|
|
54
|
+
*
|
|
55
|
+
* The request id is minted here, before the branch, so a request that never
|
|
56
|
+
* prompts is identified exactly as one that does.
|
|
49
57
|
*/
|
|
50
|
-
async run(
|
|
51
|
-
gate: GateResult,
|
|
52
|
-
agentName: string | null,
|
|
53
|
-
toolCallId: string,
|
|
54
|
-
): Promise<GateOutcome> {
|
|
58
|
+
async run(gate: GateResult, agentName: string | null): Promise<GateOutcome> {
|
|
55
59
|
if (!gate) {
|
|
56
60
|
return { action: "allow" };
|
|
57
61
|
}
|
|
62
|
+
const requestId = createPermissionRequestId();
|
|
58
63
|
if (isGateBypass(gate)) {
|
|
59
64
|
if (gate.log) {
|
|
60
|
-
this.reporter.writeReviewLog(gate.log.event,
|
|
65
|
+
this.reporter.writeReviewLog(gate.log.event, {
|
|
66
|
+
...gate.log.details,
|
|
67
|
+
requestId,
|
|
68
|
+
});
|
|
61
69
|
}
|
|
62
70
|
if (gate.decision) {
|
|
63
|
-
this.
|
|
71
|
+
this.emitDecision(requestId, gate.decision);
|
|
64
72
|
}
|
|
65
73
|
return { action: "allow" };
|
|
66
74
|
}
|
|
67
|
-
return this.runDescriptor(gate, agentName,
|
|
75
|
+
return this.runDescriptor(gate, agentName, requestId);
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
// ── Private helpers ──────────────────────────────────────────────────────
|
|
71
79
|
|
|
80
|
+
/**
|
|
81
|
+
* The one place a decision event acquires its request id, so no emit path
|
|
82
|
+
* can be added that forgets it.
|
|
83
|
+
*/
|
|
84
|
+
private emitDecision(requestId: string, facts: DecisionEventFacts): void {
|
|
85
|
+
this.reporter.emitDecision({ requestId, ...facts });
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
private async runDescriptor(
|
|
73
89
|
descriptor: GateDescriptor,
|
|
74
90
|
agentName: string | null,
|
|
75
|
-
|
|
91
|
+
requestId: string,
|
|
76
92
|
): Promise<GateOutcome> {
|
|
77
93
|
// 1. Resolve permission state — pre-check, pre-resolved, or via resolver
|
|
78
94
|
let check: PermissionCheckResult;
|
|
@@ -94,15 +110,19 @@ export class GateRunner {
|
|
|
94
110
|
});
|
|
95
111
|
}
|
|
96
112
|
|
|
113
|
+
// 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
|
+
const logContext = { ...descriptor.logContext, agentName, requestId };
|
|
116
|
+
|
|
97
117
|
// 2. Session-hit fast path
|
|
98
118
|
if (check.source === "session") {
|
|
99
119
|
this.reporter.writeReviewLog("permission_request.session_approved", {
|
|
100
|
-
...
|
|
101
|
-
agentName,
|
|
120
|
+
...logContext,
|
|
102
121
|
resolution: "session_approved",
|
|
103
122
|
sessionApprovalPattern: check.matchedPattern,
|
|
104
123
|
});
|
|
105
|
-
this.
|
|
124
|
+
this.emitDecision(
|
|
125
|
+
requestId,
|
|
106
126
|
buildDecisionEvent(
|
|
107
127
|
descriptor.decision,
|
|
108
128
|
check,
|
|
@@ -121,11 +141,11 @@ export class GateRunner {
|
|
|
121
141
|
const yoloGrant = resolveYoloGrant(check, this.isYoloEnabled());
|
|
122
142
|
if (yoloGrant) {
|
|
123
143
|
this.reporter.writeReviewLog("permission_request.auto_approved", {
|
|
124
|
-
...
|
|
125
|
-
agentName,
|
|
144
|
+
...logContext,
|
|
126
145
|
resolution: "auto_approved",
|
|
127
146
|
});
|
|
128
|
-
this.
|
|
147
|
+
this.emitDecision(
|
|
148
|
+
requestId,
|
|
129
149
|
buildDecisionEvent(
|
|
130
150
|
descriptor.decision,
|
|
131
151
|
yoloGrant,
|
|
@@ -159,7 +179,7 @@ export class GateRunner {
|
|
|
159
179
|
sessionApproval: descriptor.sessionApproval?.toGateApproval(),
|
|
160
180
|
promptForApproval: async () => {
|
|
161
181
|
const decision = await this.prompter.escalate({
|
|
162
|
-
requestId
|
|
182
|
+
requestId,
|
|
163
183
|
...descriptor.promptDetails,
|
|
164
184
|
...(descriptor.sessionApproval
|
|
165
185
|
? { sessionApproval: descriptor.sessionApproval.toForwardedData() }
|
|
@@ -171,7 +191,7 @@ export class GateRunner {
|
|
|
171
191
|
},
|
|
172
192
|
writeLog: (event, details) =>
|
|
173
193
|
this.reporter.writeReviewLog(event, details),
|
|
174
|
-
logContext
|
|
194
|
+
logContext,
|
|
175
195
|
messages,
|
|
176
196
|
});
|
|
177
197
|
|
|
@@ -180,7 +200,8 @@ export class GateRunner {
|
|
|
180
200
|
gateResult.action === "allow" && gateResult.sessionApproval !== undefined;
|
|
181
201
|
|
|
182
202
|
// 5. Emit decision event
|
|
183
|
-
this.
|
|
203
|
+
this.emitDecision(
|
|
204
|
+
requestId,
|
|
184
205
|
buildDecisionEvent(
|
|
185
206
|
descriptor.decision,
|
|
186
207
|
check,
|
|
@@ -40,7 +40,7 @@ export interface GateNotifier {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Owns the skill-input gate assembly: raw permission pre-check, deny notify,
|
|
43
|
-
* `describeSkillInputGate` descriptor,
|
|
43
|
+
* `describeSkillInputGate` descriptor, and `runner.run(...)`.
|
|
44
44
|
*
|
|
45
45
|
* Constructed once in the composition root and injected into
|
|
46
46
|
* `PermissionGateHandler`, mirroring `ToolCallGatePipeline` for the `input`
|
|
@@ -70,23 +70,12 @@ export class SkillInputGatePipeline {
|
|
|
70
70
|
return runner.run(
|
|
71
71
|
describeSkillInputGate(skillName, agentName, check),
|
|
72
72
|
agentName,
|
|
73
|
-
createSkillInputRequestId(),
|
|
74
73
|
);
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
79
78
|
|
|
80
|
-
/**
|
|
81
|
-
* Mint a unique id for a skill-input permission request.
|
|
82
|
-
*
|
|
83
|
-
* Format is `skill-input-<timestamp>-<random>-<pid>`, matching the
|
|
84
|
-
* `createPermissionRequestId("skill-input")` pattern it replaces (#330).
|
|
85
|
-
*/
|
|
86
|
-
export function createSkillInputRequestId(): string {
|
|
87
|
-
return `skill-input-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
79
|
/**
|
|
91
80
|
* Format the deny warning shown in the UI when a skill is blocked.
|
|
92
81
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
2
|
+
import { buildSkillAskPayload } from "#src/presentation/skill-ask-payload";
|
|
2
3
|
import type { PermissionCheckResult } from "#src/types";
|
|
3
4
|
import type { GateDescriptor } from "./descriptor";
|
|
4
5
|
import { accessFactsFromValue } from "./helpers";
|
|
@@ -15,7 +16,8 @@ export function describeSkillInputGate(
|
|
|
15
16
|
agentName: string | null,
|
|
16
17
|
preCheck: PermissionCheckResult,
|
|
17
18
|
): GateDescriptor {
|
|
18
|
-
const
|
|
19
|
+
const payload = buildSkillAskPayload(skillName, agentName);
|
|
20
|
+
const message = renderLegacyMessage(payload);
|
|
19
21
|
return {
|
|
20
22
|
surface: "skill",
|
|
21
23
|
input: { name: skillName },
|
|
@@ -29,6 +31,7 @@ export function describeSkillInputGate(
|
|
|
29
31
|
source: "skill_input",
|
|
30
32
|
agentName,
|
|
31
33
|
message,
|
|
34
|
+
payload,
|
|
32
35
|
skillName,
|
|
33
36
|
accessIntent: accessFactsFromValue("skill", skillName),
|
|
34
37
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
2
|
-
import {
|
|
2
|
+
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
3
|
+
import { buildSkillPathAskPayload } from "#src/presentation/skill-ask-payload";
|
|
3
4
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
4
5
|
import { findSkillPathMatch } from "#src/skill-prompt-sanitizer";
|
|
5
6
|
import { toRecord } from "#src/value-guards";
|
|
@@ -42,11 +43,8 @@ export function describeSkillReadGate(
|
|
|
42
43
|
return null;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
path,
|
|
48
|
-
tcc.agentName ?? undefined,
|
|
49
|
-
);
|
|
46
|
+
const payload = buildSkillPathAskPayload(matchedSkill, path, tcc.agentName);
|
|
47
|
+
const skillReadMessage = renderLegacyMessage(payload);
|
|
50
48
|
|
|
51
49
|
return {
|
|
52
50
|
surface: "skill",
|
|
@@ -61,6 +59,7 @@ export function describeSkillReadGate(
|
|
|
61
59
|
source: "skill_read",
|
|
62
60
|
agentName: tcc.agentName,
|
|
63
61
|
message: skillReadMessage,
|
|
62
|
+
payload,
|
|
64
63
|
toolCallId: tcc.toolCallId,
|
|
65
64
|
toolName: tcc.toolName,
|
|
66
65
|
skillName: matchedSkill.name,
|
|
@@ -69,6 +68,7 @@ export function describeSkillReadGate(
|
|
|
69
68
|
},
|
|
70
69
|
logContext: {
|
|
71
70
|
source: "skill_read",
|
|
71
|
+
toolCallId: tcc.toolCallId,
|
|
72
72
|
skillName: matchedSkill.name,
|
|
73
73
|
agentName: tcc.agentName,
|
|
74
74
|
path,
|
|
@@ -137,11 +137,7 @@ export class ToolCallGatePipeline {
|
|
|
137
137
|
];
|
|
138
138
|
|
|
139
139
|
for (const produce of gateProducers) {
|
|
140
|
-
const outcome = await runner.run(
|
|
141
|
-
await produce(),
|
|
142
|
-
tcc.agentName,
|
|
143
|
-
tcc.toolCallId,
|
|
144
|
-
);
|
|
140
|
+
const outcome = await runner.run(await produce(), tcc.agentName);
|
|
145
141
|
if (outcome.action === "block") {
|
|
146
142
|
return outcome;
|
|
147
143
|
}
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
type ShellInvocation,
|
|
7
7
|
} from "#src/access-intent/tool-kind";
|
|
8
8
|
import { suggestSessionPattern } from "#src/pattern-suggest";
|
|
9
|
-
import {
|
|
9
|
+
import { renderLegacyMessage } from "#src/presentation/legacy-message";
|
|
10
|
+
import { buildToolAskPayload } from "#src/presentation/tool-ask-payload";
|
|
10
11
|
import { SessionApproval } from "#src/session-approval";
|
|
11
12
|
import type { ToolPreviewFormatter } from "#src/tool-preview-formatter";
|
|
12
13
|
import type { PermissionCheckResult } from "#src/types";
|
|
@@ -72,12 +73,15 @@ export function describeToolGate(
|
|
|
72
73
|
deriveSuggestionValue(gateSurface, check, accessPath),
|
|
73
74
|
);
|
|
74
75
|
|
|
75
|
-
const
|
|
76
|
+
const payload = buildToolAskPayload({
|
|
76
77
|
check,
|
|
77
|
-
tcc.agentName
|
|
78
|
-
|
|
78
|
+
agentName: tcc.agentName,
|
|
79
|
+
surface: gateSurface,
|
|
80
|
+
invokedToolName: tcc.toolName,
|
|
81
|
+
input: tcc.input,
|
|
79
82
|
formatter,
|
|
80
|
-
);
|
|
83
|
+
});
|
|
84
|
+
const askMessage = renderLegacyMessage(payload);
|
|
81
85
|
|
|
82
86
|
const decisionValue = deriveDecisionValue(
|
|
83
87
|
gateSurface,
|
|
@@ -108,6 +112,7 @@ export function describeToolGate(
|
|
|
108
112
|
source: "tool_call",
|
|
109
113
|
agentName: tcc.agentName,
|
|
110
114
|
message: askMessage,
|
|
115
|
+
payload,
|
|
111
116
|
toolCallId: tcc.toolCallId,
|
|
112
117
|
toolName: tcc.toolName,
|
|
113
118
|
sessionLabel: suggestion.label,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { DecisionRecorder } from "#src/decision-audit";
|
|
3
3
|
import type { DecisionReporter } from "#src/decision-reporter";
|
|
4
|
+
import { createPermissionRequestId } from "#src/permission-request-id";
|
|
4
5
|
import { toRecord } from "#src/value-guards";
|
|
5
6
|
import type { GateOutcome } from "./gates/types";
|
|
6
7
|
|
|
@@ -53,18 +54,40 @@ export function createFailClosedToolCall(
|
|
|
53
54
|
? { block: true, reason: outcome.reason }
|
|
54
55
|
: {};
|
|
55
56
|
} catch (error) {
|
|
56
|
-
audit
|
|
57
|
-
reporter.writeReviewLog("permission_request.blocked", {
|
|
58
|
-
toolName: bestEffortToolName(event),
|
|
59
|
-
command: bestEffortCommand(event),
|
|
60
|
-
resolution: "gate_error",
|
|
61
|
-
error: errorMessage(error),
|
|
62
|
-
});
|
|
57
|
+
recordGateError(reporter, audit, event, error);
|
|
63
58
|
return { block: true, reason: formatGateErrorReason(error) };
|
|
64
59
|
}
|
|
65
60
|
};
|
|
66
61
|
}
|
|
67
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Record a gate error without ever throwing.
|
|
65
|
+
*
|
|
66
|
+
* The block below this must be reached: the SDK does not catch a throwing
|
|
67
|
+
* handler, so an exception escaping the recording work would leave the command
|
|
68
|
+
* ungated. The request id is minted here rather than borrowed — the throw may
|
|
69
|
+
* have come from anywhere in the pipeline, so no gate's id is available.
|
|
70
|
+
*/
|
|
71
|
+
function recordGateError(
|
|
72
|
+
reporter: DecisionReporter,
|
|
73
|
+
audit: DecisionRecorder,
|
|
74
|
+
event: unknown,
|
|
75
|
+
error: unknown,
|
|
76
|
+
): void {
|
|
77
|
+
try {
|
|
78
|
+
audit.recordError();
|
|
79
|
+
reporter.writeReviewLog("permission_request.blocked", {
|
|
80
|
+
requestId: createPermissionRequestId(),
|
|
81
|
+
toolName: bestEffortToolName(event),
|
|
82
|
+
command: bestEffortCommand(event),
|
|
83
|
+
resolution: "gate_error",
|
|
84
|
+
error: errorMessage(error),
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
// The block is the guarantee; its bookkeeping is not.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
// ── Defensive event readers (never throw) ──────────────────────────────────
|
|
69
92
|
|
|
70
93
|
/** Best-effort tool name from a raw event; never throws. */
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { PermissionManager } from "./permission-manager";
|
|
|
38
38
|
import { PermissionResolver } from "./permission-resolver";
|
|
39
39
|
import { PermissionSession } from "./permission-session";
|
|
40
40
|
import { LocalPermissionsService } from "./permissions-service";
|
|
41
|
+
import { resolveRenderBudget } from "./presentation/dialog-renderer";
|
|
41
42
|
import { PermissionServiceLifecycle } from "./service-lifecycle";
|
|
42
43
|
import { PermissionSessionLogger } from "./session-logger";
|
|
43
44
|
import { SessionRules } from "./session-rules";
|
|
@@ -115,6 +116,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
115
116
|
events: pi.events,
|
|
116
117
|
getPromptPreferences: () => ({
|
|
117
118
|
doublePressToConfirm: configStore.current().doublePressToConfirm,
|
|
119
|
+
budget: resolveRenderBudget(configStore.current()),
|
|
118
120
|
}),
|
|
119
121
|
requestPermissionDecision,
|
|
120
122
|
forwardingDir: paths.forwardingDir,
|
package/src/permission-events.ts
CHANGED
|
@@ -101,6 +101,12 @@ export type PermissionDecisionResolution =
|
|
|
101
101
|
|
|
102
102
|
/** Payload emitted on `permissions:decision`. */
|
|
103
103
|
export interface PermissionDecisionEvent {
|
|
104
|
+
/**
|
|
105
|
+
* Identifies the permission request this decision resolves, minted when the
|
|
106
|
+
* request was created. Distinct from the host's tool-call id: one tool call
|
|
107
|
+
* runs several gates and so raises several requests.
|
|
108
|
+
*/
|
|
109
|
+
requestId: string;
|
|
104
110
|
/** Permission surface: "bash", "read", "mcp", "skill", "external_directory", etc. */
|
|
105
111
|
surface: string;
|
|
106
112
|
/** The value that was evaluated (command, tool name, skill name, path). */
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { classifyToolKind
|
|
2
|
-
import { matchQualifier } from "./denial-messages";
|
|
3
|
-
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
4
|
-
import type { ToolPreviewFormatter } from "./tool-preview-formatter";
|
|
5
|
-
import type { PermissionCheckResult } from "./types";
|
|
6
|
-
import { getNonEmptyString, toRecord } from "./value-guards";
|
|
1
|
+
import { classifyToolKind } from "./access-intent/tool-kind";
|
|
7
2
|
|
|
8
|
-
// NOTE:
|
|
9
|
-
//
|
|
10
|
-
//
|
|
3
|
+
// NOTE: the ask prompts are now payload builders under src/presentation/;
|
|
4
|
+
// denial text lives in denial-messages.ts. This module retains only the
|
|
5
|
+
// pre-check reasons, which are agent-facing rather than user-facing.
|
|
11
6
|
|
|
12
7
|
export function formatMissingToolNameReason(): string {
|
|
13
8
|
return "Tool call was blocked because no tool name was provided. Use a registered tool name from pi.getAllTools().";
|
|
@@ -29,66 +24,3 @@ export function formatUnknownToolReason(
|
|
|
29
24
|
|
|
30
25
|
return `Tool '${toolName}' is not registered in this runtime and was blocked before permission checks.${mcpHint} Registered tools: ${availableList}.`;
|
|
31
26
|
}
|
|
32
|
-
|
|
33
|
-
export function formatAskPrompt(
|
|
34
|
-
result: PermissionCheckResult,
|
|
35
|
-
agentName?: string,
|
|
36
|
-
input?: unknown,
|
|
37
|
-
formatter?: ToolPreviewFormatter,
|
|
38
|
-
): string {
|
|
39
|
-
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
40
|
-
|
|
41
|
-
if (classifyToolKind(result.toolName) === "bash") {
|
|
42
|
-
const subCommand = result.command ?? "";
|
|
43
|
-
const qualifier = matchQualifier(
|
|
44
|
-
result.matchedPattern,
|
|
45
|
-
result.commandContext,
|
|
46
|
-
);
|
|
47
|
-
const qualifierInfo = qualifier ? ` ${qualifier}` : "";
|
|
48
|
-
const fullCommand = getNonEmptyString(toRecord(input).command);
|
|
49
|
-
const fullCommandInfo =
|
|
50
|
-
fullCommand && fullCommand !== subCommand
|
|
51
|
-
? ` (full command: '${fullCommand}')`
|
|
52
|
-
: "";
|
|
53
|
-
return `${subject} requested bash command '${subCommand}'${qualifierInfo}${fullCommandInfo}. Allow this command?`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (isMcpCheck(result) && result.target) {
|
|
57
|
-
const patternInfo = result.matchedPattern
|
|
58
|
-
? ` (matched '${result.matchedPattern}')`
|
|
59
|
-
: "";
|
|
60
|
-
const mcpPreview = formatter
|
|
61
|
-
? formatter.formatToolInputForPrompt("mcp", input)
|
|
62
|
-
: "";
|
|
63
|
-
const previewSuffix = mcpPreview ? ` ${mcpPreview}` : "";
|
|
64
|
-
return `${subject} requested MCP target '${result.target}'${patternInfo}${previewSuffix}. Allow this call?`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const patternInfo = result.matchedPattern
|
|
68
|
-
? ` (matched '${result.matchedPattern}')`
|
|
69
|
-
: "";
|
|
70
|
-
const inputPreview = formatter
|
|
71
|
-
? formatter.formatToolInputForPrompt(result.toolName, input)
|
|
72
|
-
: "";
|
|
73
|
-
const inputSuffix = inputPreview ? ` ${inputPreview}` : "";
|
|
74
|
-
return `${subject} requested tool '${result.toolName}'${patternInfo}${inputSuffix}. Allow this call?`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function formatSkillAskPrompt(
|
|
78
|
-
skillName: string,
|
|
79
|
-
agentName?: string,
|
|
80
|
-
): string {
|
|
81
|
-
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
82
|
-
return `${subject} requested skill '${skillName}'. Allow loading this skill?`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function formatSkillPathAskPrompt(
|
|
86
|
-
skill: SkillPromptEntry,
|
|
87
|
-
readPath: string,
|
|
88
|
-
agentName?: string,
|
|
89
|
-
): string {
|
|
90
|
-
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
91
|
-
return `${subject} requested access to skill '${skill.name}' via '${readPath}'. Allow this read?`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// formatSkillPathDenyReason has been moved to denial-messages.ts.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mint the identifier for one permission request, at the moment the request is
|
|
5
|
+
* created rather than at the moment it prompts.
|
|
6
|
+
*
|
|
7
|
+
* Distinct from the host's `toolCallId`, which keeps flowing alongside it as
|
|
8
|
+
* the join back to the Pi transcript: a single tool call runs several gates and
|
|
9
|
+
* therefore raises several permission requests, so the SDK's id cannot identify
|
|
10
|
+
* one of them.
|
|
11
|
+
*
|
|
12
|
+
* The `perm-` prefix keeps the id self-identifying in a review log that also
|
|
13
|
+
* carries SDK tool-call ids.
|
|
14
|
+
*/
|
|
15
|
+
export function createPermissionRequestId(): string {
|
|
16
|
+
return `perm-${randomUUID()}`;
|
|
17
|
+
}
|