@gotgenes/pi-permission-system 10.0.0 → 10.2.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 +1 -1
- package/package.json +1 -1
- package/src/agent-prep-session.ts +28 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +11 -0
- package/src/forwarded-permissions/permission-forwarder.ts +549 -0
- package/src/forwarding-manager.ts +3 -7
- package/src/gate-handler-session.ts +13 -0
- package/src/gate-prompter.ts +14 -0
- package/src/handlers/before-agent-start.ts +2 -3
- package/src/handlers/gates/bash-command.ts +4 -18
- package/src/handlers/gates/bash-external-directory.ts +3 -15
- package/src/handlers/gates/bash-path.ts +3 -16
- package/src/handlers/gates/descriptor.ts +0 -28
- package/src/handlers/gates/path.ts +3 -15
- package/src/handlers/gates/runner.ts +142 -105
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +44 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
- package/src/handlers/lifecycle.ts +9 -9
- package/src/handlers/permission-gate-handler.ts +34 -238
- package/src/index.ts +53 -69
- package/src/mcp-targets.ts +56 -46
- package/src/permission-manager.ts +69 -3
- package/src/permission-prompter.ts +7 -58
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +83 -27
- package/src/permissions-service.ts +53 -0
- package/src/runtime.ts +1 -37
- package/src/service-lifecycle.ts +49 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-lifecycle-session.ts +24 -0
- package/src/tool-input-preview.ts +0 -62
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +6 -4
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +62 -0
- package/test/forwarding-manager.test.ts +26 -44
- package/test/handlers/before-agent-start.test.ts +45 -21
- package/test/handlers/external-directory-integration.test.ts +83 -114
- package/test/handlers/external-directory-session-dedup.test.ts +102 -55
- package/test/handlers/gates/bash-command.test.ts +49 -90
- package/test/handlers/gates/bash-external-directory.test.ts +54 -95
- package/test/handlers/gates/bash-path.test.ts +54 -157
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +151 -186
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +128 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
- package/test/handlers/input.test.ts +1 -2
- package/test/handlers/lifecycle.test.ts +49 -33
- package/test/handlers/tool-call-events.test.ts +1 -1
- package/test/handlers/tool-call.test.ts +44 -153
- package/test/helpers/gate-fixtures.ts +212 -17
- package/test/helpers/handler-fixtures.ts +226 -29
- package/test/mcp-targets.test.ts +55 -0
- package/test/permission-forwarder.test.ts +295 -0
- package/test/permission-forwarding.test.ts +0 -282
- package/test/permission-manager-unified.test.ts +159 -1
- package/test/permission-prompter.test.ts +33 -44
- package/test/permission-session.test.ts +211 -105
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +2 -86
- package/test/service-lifecycle.test.ts +162 -0
- package/test/tool-input-preview.test.ts +0 -111
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/src/forwarded-permissions/polling.ts +0 -411
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getNonEmptyString, toRecord } from "#src/common";
|
|
2
|
-
import type {
|
|
2
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
3
3
|
import { SessionApproval } from "#src/session-approval";
|
|
4
4
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
5
5
|
import type { PermissionCheckResult } from "#src/types";
|
|
@@ -9,14 +9,6 @@ import type { GateResult } from "./descriptor";
|
|
|
9
9
|
import { formatBashExternalDirectoryAskPrompt } from "./external-directory-messages";
|
|
10
10
|
import type { ToolCallContext } from "./types";
|
|
11
11
|
|
|
12
|
-
/** Function type for checkPermission used by the descriptor factory. */
|
|
13
|
-
type CheckPermissionFn = (
|
|
14
|
-
surface: string,
|
|
15
|
-
input: unknown,
|
|
16
|
-
agentName?: string,
|
|
17
|
-
sessionRules?: Rule[],
|
|
18
|
-
) => PermissionCheckResult;
|
|
19
|
-
|
|
20
12
|
/**
|
|
21
13
|
* Build a pure descriptor for the bash external-directory permission gate.
|
|
22
14
|
*
|
|
@@ -29,8 +21,7 @@ type CheckPermissionFn = (
|
|
|
29
21
|
export function describeBashExternalDirectoryGate(
|
|
30
22
|
tcc: ToolCallContext,
|
|
31
23
|
bashProgram: BashProgram | null,
|
|
32
|
-
|
|
33
|
-
getSessionRuleset: () => Rule[],
|
|
24
|
+
resolver: PermissionResolver,
|
|
34
25
|
): GateResult {
|
|
35
26
|
if (tcc.toolName !== "bash" || !tcc.cwd) return null;
|
|
36
27
|
|
|
@@ -42,8 +33,6 @@ export function describeBashExternalDirectoryGate(
|
|
|
42
33
|
const externalPaths = bashProgram.externalPaths(tcc.cwd);
|
|
43
34
|
if (externalPaths.length === 0) return null;
|
|
44
35
|
|
|
45
|
-
const bashSessionRules = getSessionRuleset();
|
|
46
|
-
|
|
47
36
|
// Collect paths whose resolved state is not already "allow".
|
|
48
37
|
// Checking state (not source) ensures config-level allow rules (source: "special")
|
|
49
38
|
// suppress the prompt just as session-level allow rules (source: "session") do.
|
|
@@ -52,11 +41,10 @@ export function describeBashExternalDirectoryGate(
|
|
|
52
41
|
check: PermissionCheckResult;
|
|
53
42
|
}> = [];
|
|
54
43
|
for (const p of externalPaths) {
|
|
55
|
-
const check =
|
|
44
|
+
const check = resolver.resolve(
|
|
56
45
|
"external_directory",
|
|
57
46
|
{ path: p },
|
|
58
47
|
tcc.agentName ?? undefined,
|
|
59
|
-
bashSessionRules,
|
|
60
48
|
);
|
|
61
49
|
if (check.state !== "allow") {
|
|
62
50
|
uncoveredEntries.push({ path: p, check });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getNonEmptyString, toRecord } from "#src/common";
|
|
2
|
-
import type {
|
|
2
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
3
3
|
import { SessionApproval } from "#src/session-approval";
|
|
4
4
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
5
5
|
import type { PermissionCheckResult } from "#src/types";
|
|
@@ -9,14 +9,6 @@ import type { GateResult } from "./descriptor";
|
|
|
9
9
|
import { formatPathAskPrompt } from "./path";
|
|
10
10
|
import type { ToolCallContext } from "./types";
|
|
11
11
|
|
|
12
|
-
/** Function type for checkPermission used by the descriptor factory. */
|
|
13
|
-
type CheckPermissionFn = (
|
|
14
|
-
surface: string,
|
|
15
|
-
input: unknown,
|
|
16
|
-
agentName?: string,
|
|
17
|
-
sessionRules?: Rule[],
|
|
18
|
-
) => PermissionCheckResult;
|
|
19
|
-
|
|
20
12
|
/**
|
|
21
13
|
* Build a pure descriptor for the cross-cutting path permission gate (bash).
|
|
22
14
|
*
|
|
@@ -33,8 +25,7 @@ type CheckPermissionFn = (
|
|
|
33
25
|
export function describeBashPathGate(
|
|
34
26
|
tcc: ToolCallContext,
|
|
35
27
|
bashProgram: BashProgram | null,
|
|
36
|
-
|
|
37
|
-
getSessionRuleset: () => Rule[],
|
|
28
|
+
resolver: PermissionResolver,
|
|
38
29
|
): GateResult {
|
|
39
30
|
if (tcc.toolName !== "bash") return null;
|
|
40
31
|
|
|
@@ -46,20 +37,16 @@ export function describeBashPathGate(
|
|
|
46
37
|
const tokens = bashProgram.pathTokens();
|
|
47
38
|
if (tokens.length === 0) return null;
|
|
48
39
|
|
|
49
|
-
// Check each token against path rules with session rules appended.
|
|
50
|
-
const sessionRules = getSessionRuleset();
|
|
51
|
-
|
|
52
40
|
// Tokens whose resolved state needs a check (deny/ask), paired with the
|
|
53
41
|
// token that produced them so the descriptor can derive its pattern.
|
|
54
42
|
const uncovered: Array<{ token: string; check: PermissionCheckResult }> = [];
|
|
55
43
|
let allSessionCovered = true;
|
|
56
44
|
|
|
57
45
|
for (const token of tokens) {
|
|
58
|
-
const check =
|
|
46
|
+
const check = resolver.resolve(
|
|
59
47
|
"path",
|
|
60
48
|
{ path: token },
|
|
61
49
|
tcc.agentName ?? undefined,
|
|
62
|
-
sessionRules,
|
|
63
50
|
);
|
|
64
51
|
|
|
65
52
|
// No explicit path rule matched — only the universal default fired.
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { DenialContext } from "#src/denial-messages";
|
|
2
|
-
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
3
2
|
import type { PermissionDecisionEvent } from "#src/permission-events";
|
|
4
3
|
import type { PromptPermissionDetails } from "#src/permission-prompter";
|
|
5
|
-
import type { Rule } from "#src/rule";
|
|
6
4
|
import type { SessionApproval } from "#src/session-approval";
|
|
7
5
|
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
8
6
|
|
|
@@ -70,32 +68,6 @@ export interface GateBypass {
|
|
|
70
68
|
/** Union of possible gate function return values. */
|
|
71
69
|
export type GateResult = GateDescriptor | GateBypass | null;
|
|
72
70
|
|
|
73
|
-
// ── Runner dependency interface ────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Infrastructure dependencies for the gate runner.
|
|
77
|
-
*
|
|
78
|
-
* Built once in the orchestrator and reused for all gates.
|
|
79
|
-
* Handles all side effects: permission checks, logging, event emission,
|
|
80
|
-
* session-rule recording.
|
|
81
|
-
*/
|
|
82
|
-
export interface GateRunnerDeps {
|
|
83
|
-
checkPermission(
|
|
84
|
-
surface: string,
|
|
85
|
-
input: unknown,
|
|
86
|
-
agentName?: string,
|
|
87
|
-
sessionRules?: Rule[],
|
|
88
|
-
): PermissionCheckResult;
|
|
89
|
-
getSessionRuleset(): Rule[];
|
|
90
|
-
recordSessionApproval(approval: SessionApproval): void;
|
|
91
|
-
writeReviewLog(event: string, details: Record<string, unknown>): void;
|
|
92
|
-
emitDecision(event: PermissionDecisionEvent): void;
|
|
93
|
-
canConfirm(): boolean;
|
|
94
|
-
promptPermission(
|
|
95
|
-
details: PromptPermissionDetails,
|
|
96
|
-
): Promise<PermissionPromptDecision>;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
71
|
// ── Type guard helpers ─────────────────────────────────────────────────────
|
|
100
72
|
|
|
101
73
|
/** Check whether a GateResult is a GateBypass (early allow). */
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import { getPathBearingToolPath } from "#src/path-utils";
|
|
2
|
-
import type {
|
|
2
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
3
3
|
import { SessionApproval } from "#src/session-approval";
|
|
4
4
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
5
|
-
import type { PermissionCheckResult } from "#src/types";
|
|
6
5
|
import type { GateDescriptor, GateResult } from "./descriptor";
|
|
7
6
|
import type { ToolCallContext } from "./types";
|
|
8
7
|
|
|
9
|
-
/** Function type for checkPermission used by the descriptor factory. */
|
|
10
|
-
type CheckPermissionFn = (
|
|
11
|
-
surface: string,
|
|
12
|
-
input: unknown,
|
|
13
|
-
agentName?: string,
|
|
14
|
-
sessionRules?: Rule[],
|
|
15
|
-
) => PermissionCheckResult;
|
|
16
|
-
|
|
17
8
|
/**
|
|
18
9
|
* Build a pure descriptor for the cross-cutting path permission gate (tools).
|
|
19
10
|
*
|
|
@@ -24,18 +15,15 @@ type CheckPermissionFn = (
|
|
|
24
15
|
*/
|
|
25
16
|
export function describePathGate(
|
|
26
17
|
tcc: ToolCallContext,
|
|
27
|
-
|
|
28
|
-
getSessionRuleset: () => Rule[],
|
|
18
|
+
resolver: PermissionResolver,
|
|
29
19
|
): GateResult {
|
|
30
20
|
const filePath = getPathBearingToolPath(tcc.toolName, tcc.input);
|
|
31
21
|
if (!filePath) return null;
|
|
32
22
|
|
|
33
|
-
const
|
|
34
|
-
const check = checkPermission(
|
|
23
|
+
const check = resolver.resolve(
|
|
35
24
|
"path",
|
|
36
25
|
{ path: filePath },
|
|
37
26
|
tcc.agentName ?? undefined,
|
|
38
|
-
sessionRules,
|
|
39
27
|
);
|
|
40
28
|
|
|
41
29
|
if (check.state === "allow") return null;
|
|
@@ -1,133 +1,170 @@
|
|
|
1
|
+
import type { DecisionReporter } from "#src/decision-reporter";
|
|
1
2
|
import {
|
|
2
3
|
formatDenyReason,
|
|
3
4
|
formatUnavailableReason,
|
|
4
5
|
formatUserDeniedReason,
|
|
5
6
|
} from "#src/denial-messages";
|
|
7
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
6
8
|
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
7
9
|
import { applyPermissionGate } from "#src/permission-gate";
|
|
10
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
11
|
+
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
8
12
|
import type { PermissionCheckResult } from "#src/types";
|
|
9
|
-
import type { GateDescriptor,
|
|
13
|
+
import type { GateDescriptor, GateResult } from "./descriptor";
|
|
14
|
+
import { isGateBypass } from "./descriptor";
|
|
10
15
|
import { buildDecisionEvent, deriveResolution } from "./helpers";
|
|
11
16
|
import type { GateOutcome } from "./types";
|
|
12
17
|
|
|
18
|
+
// ── GateRunner class ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* This is the single site for:
|
|
17
|
-
* - Permission checking (or using pre-resolved state)
|
|
18
|
-
* - Session-hit fast path
|
|
19
|
-
* - Interactive prompt orchestration
|
|
20
|
-
* - Decision event emission
|
|
21
|
-
* - Session-rule recording
|
|
21
|
+
* Executes permission gate checks for a single gate result (null, bypass, or
|
|
22
|
+
* descriptor).
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
+
* Constructed once per handler with its four role collaborators and reused
|
|
25
|
+
* for every gate in a tool-call pipeline. The `run` method absorbs the null /
|
|
26
|
+
* bypass / descriptor dispatch that previously lived as an anonymous closure
|
|
27
|
+
* in `PermissionGateHandler.handleToolCall`.
|
|
24
28
|
*/
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
29
|
+
export class GateRunner {
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly resolver: PermissionResolver,
|
|
32
|
+
private readonly recorder: SessionApprovalRecorder,
|
|
33
|
+
private readonly prompter: GatePrompter,
|
|
34
|
+
private readonly reporter: DecisionReporter,
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execute a gate: null → allow; bypass → log/emit side effects then allow;
|
|
39
|
+
* descriptor → full check→log→emit→approve cycle.
|
|
40
|
+
*/
|
|
41
|
+
async run(
|
|
42
|
+
gate: GateResult,
|
|
43
|
+
agentName: string | null,
|
|
44
|
+
toolCallId: string,
|
|
45
|
+
): Promise<GateOutcome> {
|
|
46
|
+
if (!gate) {
|
|
47
|
+
return { action: "allow" };
|
|
48
|
+
}
|
|
49
|
+
if (isGateBypass(gate)) {
|
|
50
|
+
if (gate.log) {
|
|
51
|
+
this.reporter.writeReviewLog(gate.log.event, gate.log.details);
|
|
52
|
+
}
|
|
53
|
+
if (gate.decision) {
|
|
54
|
+
this.reporter.emitDecision(gate.decision);
|
|
55
|
+
}
|
|
56
|
+
return { action: "allow" };
|
|
57
|
+
}
|
|
58
|
+
return this.runDescriptor(gate, agentName, toolCallId);
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
private async runDescriptor(
|
|
64
|
+
descriptor: GateDescriptor,
|
|
65
|
+
agentName: string | null,
|
|
66
|
+
toolCallId: string,
|
|
67
|
+
): Promise<GateOutcome> {
|
|
68
|
+
// 1. Resolve permission state — pre-check, pre-resolved, or via resolver
|
|
69
|
+
let check: PermissionCheckResult;
|
|
70
|
+
if (descriptor.preCheck) {
|
|
71
|
+
check = descriptor.preCheck;
|
|
72
|
+
} else if (descriptor.preResolved) {
|
|
73
|
+
check = {
|
|
74
|
+
state: descriptor.preResolved.state,
|
|
75
|
+
toolName: descriptor.surface,
|
|
76
|
+
source: "tool",
|
|
77
|
+
origin: "builtin",
|
|
78
|
+
};
|
|
79
|
+
} else {
|
|
80
|
+
check = this.resolver.resolve(
|
|
81
|
+
descriptor.surface,
|
|
82
|
+
descriptor.input,
|
|
83
|
+
agentName ?? undefined,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Session-hit fast path
|
|
88
|
+
if (check.source === "session") {
|
|
89
|
+
this.reporter.writeReviewLog("permission_request.session_approved", {
|
|
90
|
+
...descriptor.logContext,
|
|
91
|
+
agentName,
|
|
92
|
+
resolution: "session_approved",
|
|
93
|
+
sessionApprovalPattern: check.matchedPattern,
|
|
94
|
+
});
|
|
95
|
+
this.reporter.emitDecision(
|
|
96
|
+
buildDecisionEvent(
|
|
97
|
+
descriptor.decision,
|
|
98
|
+
check,
|
|
99
|
+
agentName,
|
|
100
|
+
"allow",
|
|
101
|
+
"session_approved",
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
return { action: "allow" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. Apply the deny/ask/allow gate
|
|
108
|
+
const canConfirm = this.prompter.canConfirm();
|
|
109
|
+
|
|
110
|
+
// Construct messages from the centralized formatter.
|
|
111
|
+
const messages = {
|
|
112
|
+
denyReason: formatDenyReason(descriptor.denialContext),
|
|
113
|
+
unavailableReason: formatUnavailableReason(descriptor.denialContext),
|
|
114
|
+
userDeniedReason: (decision: PermissionPromptDecision) =>
|
|
115
|
+
formatUserDeniedReason(descriptor.denialContext, decision.denialReason),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let autoApproved = false;
|
|
119
|
+
const gateResult = await applyPermissionGate({
|
|
120
|
+
state: check.state,
|
|
121
|
+
canConfirm,
|
|
122
|
+
sessionApproval: descriptor.sessionApproval?.toGateApproval(),
|
|
123
|
+
promptForApproval: async () => {
|
|
124
|
+
const decision = await this.prompter.promptPermission({
|
|
125
|
+
requestId: toolCallId,
|
|
126
|
+
...descriptor.promptDetails,
|
|
127
|
+
});
|
|
128
|
+
autoApproved = decision.autoApproved === true;
|
|
129
|
+
return decision;
|
|
130
|
+
},
|
|
131
|
+
writeLog: (event, details) =>
|
|
132
|
+
this.reporter.writeReviewLog(event, details),
|
|
133
|
+
logContext: { ...descriptor.logContext, agentName },
|
|
134
|
+
messages,
|
|
58
135
|
});
|
|
59
|
-
|
|
136
|
+
|
|
137
|
+
// 4. Determine whether session approval was granted
|
|
138
|
+
const hasSessionApproval =
|
|
139
|
+
gateResult.action === "allow" && gateResult.sessionApproval !== undefined;
|
|
140
|
+
|
|
141
|
+
// 5. Emit decision event
|
|
142
|
+
this.reporter.emitDecision(
|
|
60
143
|
buildDecisionEvent(
|
|
61
144
|
descriptor.decision,
|
|
62
145
|
check,
|
|
63
146
|
agentName,
|
|
64
|
-
"allow",
|
|
65
|
-
|
|
147
|
+
gateResult.action === "allow" ? "allow" : "deny",
|
|
148
|
+
deriveResolution(
|
|
149
|
+
check.state,
|
|
150
|
+
gateResult.action,
|
|
151
|
+
hasSessionApproval,
|
|
152
|
+
canConfirm,
|
|
153
|
+
autoApproved,
|
|
154
|
+
),
|
|
66
155
|
),
|
|
67
156
|
);
|
|
68
|
-
return { action: "allow" };
|
|
69
|
-
}
|
|
70
157
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
denyReason: formatDenyReason(descriptor.denialContext),
|
|
77
|
-
unavailableReason: formatUnavailableReason(descriptor.denialContext),
|
|
78
|
-
userDeniedReason: (decision: PermissionPromptDecision) =>
|
|
79
|
-
formatUserDeniedReason(descriptor.denialContext, decision.denialReason),
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
let autoApproved = false;
|
|
83
|
-
const gateResult = await applyPermissionGate({
|
|
84
|
-
state: check.state,
|
|
85
|
-
canConfirm,
|
|
86
|
-
sessionApproval: descriptor.sessionApproval?.toGateApproval(),
|
|
87
|
-
promptForApproval: async () => {
|
|
88
|
-
const decision = await deps.promptPermission({
|
|
89
|
-
requestId: toolCallId,
|
|
90
|
-
...descriptor.promptDetails,
|
|
91
|
-
});
|
|
92
|
-
autoApproved = decision.autoApproved === true;
|
|
93
|
-
return decision;
|
|
94
|
-
},
|
|
95
|
-
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger methods are plain functions; no this-binding issue
|
|
96
|
-
writeLog: deps.writeReviewLog,
|
|
97
|
-
logContext: { ...descriptor.logContext, agentName },
|
|
98
|
-
messages,
|
|
99
|
-
});
|
|
158
|
+
// 6. Record session approval — tell the store; it owns the per-pattern loop
|
|
159
|
+
// hasSessionApproval already implies gateResult.action === "allow"
|
|
160
|
+
if (hasSessionApproval && descriptor.sessionApproval) {
|
|
161
|
+
this.recorder.recordSessionApproval(descriptor.sessionApproval);
|
|
162
|
+
}
|
|
100
163
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
164
|
+
if (gateResult.action === "block") {
|
|
165
|
+
return { action: "block", reason: gateResult.reason };
|
|
166
|
+
}
|
|
104
167
|
|
|
105
|
-
|
|
106
|
-
deps.emitDecision(
|
|
107
|
-
buildDecisionEvent(
|
|
108
|
-
descriptor.decision,
|
|
109
|
-
check,
|
|
110
|
-
agentName,
|
|
111
|
-
gateResult.action === "allow" ? "allow" : "deny",
|
|
112
|
-
deriveResolution(
|
|
113
|
-
check.state,
|
|
114
|
-
gateResult.action,
|
|
115
|
-
hasSessionApproval,
|
|
116
|
-
canConfirm,
|
|
117
|
-
autoApproved,
|
|
118
|
-
),
|
|
119
|
-
),
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
// 6. Record session approval — tell the store; it owns the per-pattern loop
|
|
123
|
-
// hasSessionApproval already implies gateResult.action === "allow"
|
|
124
|
-
if (hasSessionApproval && descriptor.sessionApproval) {
|
|
125
|
-
deps.recordSessionApproval(descriptor.sessionApproval);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (gateResult.action === "block") {
|
|
129
|
-
return { action: "block", reason: gateResult.reason };
|
|
168
|
+
return { action: "allow" };
|
|
130
169
|
}
|
|
131
|
-
|
|
132
|
-
return { action: "allow" };
|
|
133
170
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
2
|
+
import type { GateRunner } from "./runner";
|
|
3
|
+
import { describeSkillInputGate } from "./skill-input";
|
|
4
|
+
import type { GateOutcome } from "./types";
|
|
5
|
+
|
|
6
|
+
// ── Interfaces ────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Narrow interface the pipeline needs from its session-side dependency.
|
|
10
|
+
*
|
|
11
|
+
* A raw `checkPermission` (no session rules) — preserves the skill-input
|
|
12
|
+
* semantics established in #326 where the skill-input gate intentionally
|
|
13
|
+
* bypasses session-rule resolution.
|
|
14
|
+
*
|
|
15
|
+
* `PermissionSession` satisfies this structurally at the construction call
|
|
16
|
+
* site; no `implements` clause is needed and would create a layer-inversion
|
|
17
|
+
* import from the domain module into the handler layer.
|
|
18
|
+
*/
|
|
19
|
+
export interface SkillInputGateInputs {
|
|
20
|
+
checkPermission(
|
|
21
|
+
surface: string,
|
|
22
|
+
input: unknown,
|
|
23
|
+
agentName?: string,
|
|
24
|
+
): PermissionCheckResult;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Narrow UI seam: warn the user when a skill is denied.
|
|
29
|
+
*
|
|
30
|
+
* The handler builds this per-event from `ctx`, encapsulating the `hasUI`
|
|
31
|
+
* guard so the pipeline never touches `ExtensionContext` directly
|
|
32
|
+
* (Tell-Don't-Ask: the pipeline tells the notifier to warn; the notifier
|
|
33
|
+
* decides whether a UI is present).
|
|
34
|
+
*/
|
|
35
|
+
export interface GateNotifier {
|
|
36
|
+
warn(message: string): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Pipeline ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Owns the skill-input gate assembly: raw permission pre-check, deny notify,
|
|
43
|
+
* `describeSkillInputGate` descriptor, request-id mint, and `runner.run(...)`.
|
|
44
|
+
*
|
|
45
|
+
* Constructed once in the composition root and injected into
|
|
46
|
+
* `PermissionGateHandler`, mirroring `ToolCallGatePipeline` for the `input`
|
|
47
|
+
* path.
|
|
48
|
+
*
|
|
49
|
+
* `evaluate` is not `async` because it has no `await` of its own — it returns
|
|
50
|
+
* `runner.run(...)` directly (`@typescript-eslint/require-await` would reject
|
|
51
|
+
* an `async` body with no `await`).
|
|
52
|
+
*/
|
|
53
|
+
export class SkillInputGatePipeline {
|
|
54
|
+
constructor(private readonly inputs: SkillInputGateInputs) {}
|
|
55
|
+
|
|
56
|
+
evaluate(
|
|
57
|
+
skillName: string,
|
|
58
|
+
agentName: string | null,
|
|
59
|
+
notifier: GateNotifier,
|
|
60
|
+
runner: GateRunner,
|
|
61
|
+
): Promise<GateOutcome> {
|
|
62
|
+
const check = this.inputs.checkPermission(
|
|
63
|
+
"skill",
|
|
64
|
+
{ name: skillName },
|
|
65
|
+
agentName ?? undefined,
|
|
66
|
+
);
|
|
67
|
+
if (check.state === "deny") {
|
|
68
|
+
notifier.warn(formatSkillDenyNotice(skillName, agentName));
|
|
69
|
+
}
|
|
70
|
+
return runner.run(
|
|
71
|
+
describeSkillInputGate(skillName, agentName, check),
|
|
72
|
+
agentName,
|
|
73
|
+
createSkillInputRequestId(),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
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
|
+
/**
|
|
91
|
+
* Format the deny warning shown in the UI when a skill is blocked.
|
|
92
|
+
*
|
|
93
|
+
* Intentionally untagged (no `[pi-permission-system]` prefix) — this is a
|
|
94
|
+
* UI notify distinct from the gate deny reasons the runner routes through
|
|
95
|
+
* `formatDenyReason`.
|
|
96
|
+
*/
|
|
97
|
+
export function formatSkillDenyNotice(
|
|
98
|
+
skillName: string,
|
|
99
|
+
agentName: string | null,
|
|
100
|
+
): string {
|
|
101
|
+
return agentName
|
|
102
|
+
? `Skill '${skillName}' is not permitted for agent '${agentName}'.`
|
|
103
|
+
: `Skill '${skillName}' is not permitted by the current skill policy.`;
|
|
104
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { formatSkillAskPrompt } from "#src/permission-prompts";
|
|
2
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
3
|
+
import type { GateDescriptor } from "./descriptor";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build a pure descriptor for the skill-input permission gate.
|
|
7
|
+
*
|
|
8
|
+
* Takes the pre-computed check result so the gate can reuse the result the
|
|
9
|
+
* caller already obtained (e.g. to conditionally emit a deny warning) without
|
|
10
|
+
* re-running the check inside the runner.
|
|
11
|
+
*/
|
|
12
|
+
export function describeSkillInputGate(
|
|
13
|
+
skillName: string,
|
|
14
|
+
agentName: string | null,
|
|
15
|
+
preCheck: PermissionCheckResult,
|
|
16
|
+
): GateDescriptor {
|
|
17
|
+
const message = formatSkillAskPrompt(skillName, agentName ?? undefined);
|
|
18
|
+
return {
|
|
19
|
+
surface: "skill",
|
|
20
|
+
input: { name: skillName },
|
|
21
|
+
preCheck,
|
|
22
|
+
denialContext: {
|
|
23
|
+
kind: "skill_input",
|
|
24
|
+
skillName,
|
|
25
|
+
agentName: agentName ?? undefined,
|
|
26
|
+
},
|
|
27
|
+
promptDetails: {
|
|
28
|
+
source: "skill_input",
|
|
29
|
+
agentName,
|
|
30
|
+
message,
|
|
31
|
+
skillName,
|
|
32
|
+
},
|
|
33
|
+
logContext: {
|
|
34
|
+
source: "skill_input",
|
|
35
|
+
skillName,
|
|
36
|
+
agentName,
|
|
37
|
+
message,
|
|
38
|
+
},
|
|
39
|
+
decision: {
|
|
40
|
+
surface: "skill",
|
|
41
|
+
value: skillName,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|