@komarspn/pi-permission-system 16.0.2
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 +2234 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/config/config.example.json +39 -0
- package/package.json +82 -0
- package/schemas/permissions.schema.json +158 -0
- package/src/active-agent.ts +72 -0
- package/src/async-cache.ts +21 -0
- package/src/bash-arity.ts +210 -0
- package/src/builtin-tool-input-formatters.ts +82 -0
- package/src/canonicalize-path.ts +30 -0
- package/src/common.ts +121 -0
- package/src/config-loader.ts +432 -0
- package/src/config-modal.ts +259 -0
- package/src/config-paths.ts +47 -0
- package/src/config-reporter.ts +34 -0
- package/src/config-store.ts +222 -0
- package/src/decision-audit.ts +75 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +232 -0
- package/src/expand-home.ts +28 -0
- package/src/extension-config.ts +79 -0
- package/src/extension-paths.ts +66 -0
- package/src/forwarded-permissions/io.ts +404 -0
- package/src/forwarded-permissions/permission-forwarder.ts +580 -0
- package/src/forwarding-manager.ts +74 -0
- package/src/gate-prompter.ts +12 -0
- package/src/handlers/before-agent-start.ts +94 -0
- package/src/handlers/gates/bash-command.ts +75 -0
- package/src/handlers/gates/bash-external-directory.ts +127 -0
- package/src/handlers/gates/bash-path-extractor.ts +15 -0
- package/src/handlers/gates/bash-path.ts +152 -0
- package/src/handlers/gates/bash-program.ts +1143 -0
- package/src/handlers/gates/bash-token-classification.ts +105 -0
- package/src/handlers/gates/candidate-check.ts +32 -0
- package/src/handlers/gates/descriptor.ts +81 -0
- package/src/handlers/gates/external-directory-messages.ts +20 -0
- package/src/handlers/gates/external-directory.ts +133 -0
- package/src/handlers/gates/helpers.ts +76 -0
- package/src/handlers/gates/path.ts +91 -0
- package/src/handlers/gates/runner.ts +186 -0
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +46 -0
- package/src/handlers/gates/skill-read.ts +87 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +129 -0
- package/src/handlers/gates/tool.ts +102 -0
- package/src/handlers/gates/types.ts +13 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/lifecycle.ts +95 -0
- package/src/handlers/permission-gate-handler.ts +190 -0
- package/src/handlers/tool-call-boundary.ts +91 -0
- package/src/index.ts +225 -0
- package/src/input-normalizer.ts +157 -0
- package/src/logging.ts +113 -0
- package/src/mcp-targets.ts +170 -0
- package/src/node-modules-discovery.ts +76 -0
- package/src/normalize.ts +43 -0
- package/src/path-utils.ts +355 -0
- package/src/pattern-suggest.ts +132 -0
- package/src/permission-dialog.ts +138 -0
- package/src/permission-event-rpc.ts +223 -0
- package/src/permission-events.ts +266 -0
- package/src/permission-forwarding.ts +188 -0
- package/src/permission-gate.ts +94 -0
- package/src/permission-manager.ts +392 -0
- package/src/permission-merge.ts +32 -0
- package/src/permission-prompter.ts +142 -0
- package/src/permission-prompts.ts +93 -0
- package/src/permission-resolver.ts +109 -0
- package/src/permission-session.ts +189 -0
- package/src/permission-ui-prompt.ts +127 -0
- package/src/permissions-service.ts +63 -0
- package/src/persistent-approval-recorder.ts +139 -0
- package/src/policy-loader.ts +350 -0
- package/src/prompting-gateway.ts +104 -0
- package/src/rule.ts +188 -0
- package/src/scope-merge.ts +72 -0
- package/src/service-lifecycle.ts +49 -0
- package/src/service.ts +163 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-approval.ts +43 -0
- package/src/session-logger.ts +91 -0
- package/src/session-rules.ts +79 -0
- package/src/skill-prompt-sanitizer.ts +292 -0
- package/src/status.ts +35 -0
- package/src/subagent-context.ts +104 -0
- package/src/subagent-lifecycle-events.ts +72 -0
- package/src/subagent-registry.ts +105 -0
- package/src/synthesize.ts +92 -0
- package/src/system-prompt-sanitizer.ts +274 -0
- package/src/tool-access-extractor-registry.ts +68 -0
- package/src/tool-input-formatter-registry.ts +67 -0
- package/src/tool-input-preview.ts +34 -0
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +207 -0
- package/src/tool-registry.ts +148 -0
- package/src/types.ts +64 -0
- package/src/wildcard-matcher.ts +120 -0
- package/src/yolo-mode.ts +30 -0
- package/test/active-agent.test.ts +155 -0
- package/test/async-cache.test.ts +48 -0
- package/test/bash-arity.test.ts +144 -0
- package/test/bash-external-directory.test.ts +956 -0
- package/test/builtin-tool-input-formatters.test.ts +109 -0
- package/test/canonicalize-path.test.ts +93 -0
- package/test/common.test.ts +287 -0
- package/test/composition-root.test.ts +603 -0
- package/test/config-loader.test.ts +740 -0
- package/test/config-modal.test.ts +320 -0
- package/test/config-paths.test.ts +83 -0
- package/test/config-pipeline.test.ts +90 -0
- package/test/config-reporter.test.ts +147 -0
- package/test/config-store.test.ts +466 -0
- package/test/decision-audit.test.ts +72 -0
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +656 -0
- package/test/detect-permissive-bash-fallback.test.ts +56 -0
- package/test/expand-home.test.ts +93 -0
- package/test/extension-config.test.ts +129 -0
- package/test/extension-paths.test.ts +108 -0
- package/test/forwarded-permissions/io.test.ts +251 -0
- package/test/forwarding-manager.test.ts +194 -0
- package/test/handlers/before-agent-start.test.ts +317 -0
- package/test/handlers/external-directory-integration.test.ts +623 -0
- package/test/handlers/external-directory-session-dedup.test.ts +430 -0
- package/test/handlers/external-directory-symlink-acceptance.test.ts +149 -0
- package/test/handlers/gates/bash-command-metamorphic.test.ts +83 -0
- package/test/handlers/gates/bash-command.test.ts +191 -0
- package/test/handlers/gates/bash-external-directory.test.ts +269 -0
- package/test/handlers/gates/bash-path.test.ts +337 -0
- package/test/handlers/gates/bash-program.test.ts +410 -0
- package/test/handlers/gates/bash-token-classification.test.ts +241 -0
- package/test/handlers/gates/candidate-check.test.ts +52 -0
- package/test/handlers/gates/external-directory-messages.test.ts +61 -0
- package/test/handlers/gates/external-directory.test.ts +259 -0
- package/test/handlers/gates/helpers.test.ts +177 -0
- package/test/handlers/gates/path.test.ts +294 -0
- package/test/handlers/gates/runner.test.ts +447 -0
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +131 -0
- package/test/handlers/gates/skill-read.test.ts +158 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +252 -0
- package/test/handlers/gates/tool.test.ts +223 -0
- package/test/handlers/input-events.test.ts +168 -0
- package/test/handlers/input.test.ts +199 -0
- package/test/handlers/lifecycle.test.ts +221 -0
- package/test/handlers/tool-call-boundary.test.ts +145 -0
- package/test/handlers/tool-call-events.test.ts +277 -0
- package/test/handlers/tool-call.test.ts +395 -0
- package/test/handlers/validate-requested-tool.test.ts +92 -0
- package/test/helpers/gate-fixtures.ts +323 -0
- package/test/helpers/handler-fixtures.ts +335 -0
- package/test/helpers/make-fake-pi.ts +100 -0
- package/test/helpers/manager-harness.ts +112 -0
- package/test/helpers/session-fixtures.ts +204 -0
- package/test/input-normalizer.test.ts +367 -0
- package/test/logging.test.ts +51 -0
- package/test/mcp-targets.test.ts +233 -0
- package/test/node-modules-discovery.test.ts +97 -0
- package/test/normalize.test.ts +247 -0
- package/test/path-utils.test.ts +650 -0
- package/test/pattern-suggest.test.ts +248 -0
- package/test/permission-dialog.test.ts +241 -0
- package/test/permission-event-rpc.test.ts +541 -0
- package/test/permission-events.test.ts +402 -0
- package/test/permission-forwarder.test.ts +369 -0
- package/test/permission-forwarding.test.ts +315 -0
- package/test/permission-gate.test.ts +305 -0
- package/test/permission-manager-unified.test.ts +3368 -0
- package/test/permission-merge.test.ts +61 -0
- package/test/permission-prompter.test.ts +518 -0
- package/test/permission-prompts.test.ts +363 -0
- package/test/permission-resolver.test.ts +265 -0
- package/test/permission-session.test.ts +363 -0
- package/test/permission-ui-prompt.test.ts +146 -0
- package/test/permissions-service.test.ts +177 -0
- package/test/persistent-approval-recorder.test.ts +133 -0
- package/test/pi-infrastructure-read.test.ts +369 -0
- package/test/policy-loader.test.ts +561 -0
- package/test/prompting-gateway.test.ts +230 -0
- package/test/rule.test.ts +604 -0
- package/test/scope-merge.test.ts +116 -0
- package/test/service-lifecycle.test.ts +163 -0
- package/test/service.test.ts +308 -0
- package/test/session-approval.test.ts +75 -0
- package/test/session-logger.test.ts +200 -0
- package/test/session-rules.test.ts +304 -0
- package/test/session-start.test.ts +112 -0
- package/test/skill-prompt-sanitizer.test.ts +374 -0
- package/test/status.test.ts +10 -0
- package/test/subagent-context.test.ts +326 -0
- package/test/subagent-lifecycle-events.test.ts +132 -0
- package/test/subagent-registry.test.ts +145 -0
- package/test/synthesize.test.ts +300 -0
- package/test/system-prompt-sanitizer.test.ts +382 -0
- package/test/tool-access-extractor-registry.test.ts +77 -0
- package/test/tool-input-formatter-registry.test.ts +75 -0
- package/test/tool-input-preview.test.ts +129 -0
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/test/tool-preview-formatter.test.ts +458 -0
- package/test/tool-registry.test.ts +197 -0
- package/test/wildcard-matcher.test.ts +424 -0
- package/test/yolo-mode.test.ts +188 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { DecisionReporter } from "#src/decision-reporter";
|
|
2
|
+
import {
|
|
3
|
+
formatDenyReason,
|
|
4
|
+
formatUnavailableReason,
|
|
5
|
+
formatUserDeniedReason,
|
|
6
|
+
} from "#src/denial-messages";
|
|
7
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
8
|
+
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
9
|
+
import { applyPermissionGate } from "#src/permission-gate";
|
|
10
|
+
import type { PersistentApprovalRecorder } from "#src/persistent-approval-recorder";
|
|
11
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
12
|
+
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
13
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
14
|
+
import type { GateDescriptor, GateResult } from "./descriptor";
|
|
15
|
+
import { isGateBypass } from "./descriptor";
|
|
16
|
+
import { buildDecisionEvent, deriveResolution } from "./helpers";
|
|
17
|
+
import type { GateOutcome } from "./types";
|
|
18
|
+
|
|
19
|
+
// ── GateRunner class ───────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Executes permission gate checks for a single gate result (null, bypass, or
|
|
23
|
+
* descriptor).
|
|
24
|
+
*
|
|
25
|
+
* Constructed once per handler with its four role collaborators and reused
|
|
26
|
+
* for every gate in a tool-call pipeline. The `run` method absorbs the null /
|
|
27
|
+
* bypass / descriptor dispatch that previously lived as an anonymous closure
|
|
28
|
+
* in `PermissionGateHandler.handleToolCall`.
|
|
29
|
+
*/
|
|
30
|
+
export class GateRunner {
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly resolver: ScopedPermissionResolver,
|
|
33
|
+
private readonly recorder: SessionApprovalRecorder,
|
|
34
|
+
private readonly prompter: GatePrompter,
|
|
35
|
+
private readonly reporter: DecisionReporter,
|
|
36
|
+
private readonly persistentRecorder: PersistentApprovalRecorder,
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Execute a gate: null → allow; bypass → log/emit side effects then allow;
|
|
41
|
+
* descriptor → full check→log→emit→approve cycle.
|
|
42
|
+
*/
|
|
43
|
+
async run(
|
|
44
|
+
gate: GateResult,
|
|
45
|
+
agentName: string | null,
|
|
46
|
+
toolCallId: string,
|
|
47
|
+
): Promise<GateOutcome> {
|
|
48
|
+
if (!gate) {
|
|
49
|
+
return { action: "allow" };
|
|
50
|
+
}
|
|
51
|
+
if (isGateBypass(gate)) {
|
|
52
|
+
if (gate.log) {
|
|
53
|
+
this.reporter.writeReviewLog(gate.log.event, gate.log.details);
|
|
54
|
+
}
|
|
55
|
+
if (gate.decision) {
|
|
56
|
+
this.reporter.emitDecision(gate.decision);
|
|
57
|
+
}
|
|
58
|
+
return { action: "allow" };
|
|
59
|
+
}
|
|
60
|
+
return this.runDescriptor(gate, agentName, toolCallId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
private async runDescriptor(
|
|
66
|
+
descriptor: GateDescriptor,
|
|
67
|
+
agentName: string | null,
|
|
68
|
+
toolCallId: string,
|
|
69
|
+
): Promise<GateOutcome> {
|
|
70
|
+
// 1. Resolve permission state — pre-check, pre-resolved, or via resolver
|
|
71
|
+
let check: PermissionCheckResult;
|
|
72
|
+
if (descriptor.preCheck) {
|
|
73
|
+
check = descriptor.preCheck;
|
|
74
|
+
} else if (descriptor.preResolved) {
|
|
75
|
+
check = {
|
|
76
|
+
state: descriptor.preResolved.state,
|
|
77
|
+
toolName: descriptor.surface,
|
|
78
|
+
source: "tool",
|
|
79
|
+
origin: "builtin",
|
|
80
|
+
};
|
|
81
|
+
} else {
|
|
82
|
+
check = this.resolver.resolve(
|
|
83
|
+
descriptor.surface,
|
|
84
|
+
descriptor.input,
|
|
85
|
+
agentName ?? undefined,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 2. Session-hit fast path
|
|
90
|
+
if (check.source === "session") {
|
|
91
|
+
this.reporter.writeReviewLog("permission_request.session_approved", {
|
|
92
|
+
...descriptor.logContext,
|
|
93
|
+
agentName,
|
|
94
|
+
resolution: "session_approved",
|
|
95
|
+
sessionApprovalPattern: check.matchedPattern,
|
|
96
|
+
});
|
|
97
|
+
this.reporter.emitDecision(
|
|
98
|
+
buildDecisionEvent(
|
|
99
|
+
descriptor.decision,
|
|
100
|
+
check,
|
|
101
|
+
agentName,
|
|
102
|
+
"allow",
|
|
103
|
+
"session_approved",
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
return { action: "allow" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. Apply the deny/ask/allow gate
|
|
110
|
+
const canConfirm = this.prompter.canConfirm();
|
|
111
|
+
|
|
112
|
+
// Construct messages from the centralized formatter.
|
|
113
|
+
const messages = {
|
|
114
|
+
denyReason: formatDenyReason(descriptor.denialContext),
|
|
115
|
+
unavailableReason: formatUnavailableReason(descriptor.denialContext),
|
|
116
|
+
userDeniedReason: (decision: PermissionPromptDecision) =>
|
|
117
|
+
formatUserDeniedReason(descriptor.denialContext, decision.denialReason),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
let autoApproved = false;
|
|
121
|
+
const gateResult = await applyPermissionGate({
|
|
122
|
+
state: check.state,
|
|
123
|
+
canConfirm,
|
|
124
|
+
sessionApproval: descriptor.sessionApproval?.toGateApproval(),
|
|
125
|
+
promptForApproval: async () => {
|
|
126
|
+
const decision = await this.prompter.prompt({
|
|
127
|
+
requestId: toolCallId,
|
|
128
|
+
...descriptor.promptDetails,
|
|
129
|
+
});
|
|
130
|
+
autoApproved = decision.autoApproved === true;
|
|
131
|
+
return decision;
|
|
132
|
+
},
|
|
133
|
+
writeLog: (event, details) =>
|
|
134
|
+
this.reporter.writeReviewLog(event, details),
|
|
135
|
+
logContext: { ...descriptor.logContext, agentName },
|
|
136
|
+
messages,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 4. Determine whether session approval was granted
|
|
140
|
+
const hasSessionApproval =
|
|
141
|
+
gateResult.action === "allow" && gateResult.sessionApproval !== undefined;
|
|
142
|
+
|
|
143
|
+
// 5. Emit decision event
|
|
144
|
+
this.reporter.emitDecision(
|
|
145
|
+
buildDecisionEvent(
|
|
146
|
+
descriptor.decision,
|
|
147
|
+
check,
|
|
148
|
+
agentName,
|
|
149
|
+
gateResult.action === "allow" ? "allow" : "deny",
|
|
150
|
+
deriveResolution(
|
|
151
|
+
check.state,
|
|
152
|
+
gateResult.action,
|
|
153
|
+
hasSessionApproval,
|
|
154
|
+
canConfirm,
|
|
155
|
+
autoApproved,
|
|
156
|
+
gateResult.action === "allow"
|
|
157
|
+
? gateResult.persistentApprovalScope
|
|
158
|
+
: undefined,
|
|
159
|
+
),
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// 6. Record session approval — tell the store; it owns the per-pattern loop
|
|
164
|
+
// hasSessionApproval already implies gateResult.action === "allow"
|
|
165
|
+
if (hasSessionApproval && descriptor.sessionApproval) {
|
|
166
|
+
this.recorder.recordSessionApproval(descriptor.sessionApproval);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
gateResult.action === "allow" &&
|
|
171
|
+
gateResult.persistentApprovalScope &&
|
|
172
|
+
descriptor.sessionApproval
|
|
173
|
+
) {
|
|
174
|
+
this.persistentRecorder.recordApproval(
|
|
175
|
+
gateResult.persistentApprovalScope,
|
|
176
|
+
descriptor.sessionApproval,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (gateResult.action === "block") {
|
|
181
|
+
return { action: "block", reason: gateResult.reason };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { action: "allow" };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -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,46 @@
|
|
|
1
|
+
import { formatSkillAskPrompt } from "#src/permission-prompts";
|
|
2
|
+
import { SessionApproval } from "#src/session-approval";
|
|
3
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
4
|
+
import type { GateDescriptor } from "./descriptor";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build a pure descriptor for the skill-input permission gate.
|
|
8
|
+
*
|
|
9
|
+
* Takes the pre-computed check result so the gate can reuse the result the
|
|
10
|
+
* caller already obtained (e.g. to conditionally emit a deny warning) without
|
|
11
|
+
* re-running the check inside the runner.
|
|
12
|
+
*/
|
|
13
|
+
export function describeSkillInputGate(
|
|
14
|
+
skillName: string,
|
|
15
|
+
agentName: string | null,
|
|
16
|
+
preCheck: PermissionCheckResult,
|
|
17
|
+
): GateDescriptor {
|
|
18
|
+
const message = formatSkillAskPrompt(skillName, agentName ?? undefined);
|
|
19
|
+
return {
|
|
20
|
+
surface: "skill",
|
|
21
|
+
input: { name: skillName },
|
|
22
|
+
preCheck,
|
|
23
|
+
denialContext: {
|
|
24
|
+
kind: "skill_input",
|
|
25
|
+
skillName,
|
|
26
|
+
agentName: agentName ?? undefined,
|
|
27
|
+
},
|
|
28
|
+
promptDetails: {
|
|
29
|
+
source: "skill_input",
|
|
30
|
+
agentName,
|
|
31
|
+
message,
|
|
32
|
+
skillName,
|
|
33
|
+
},
|
|
34
|
+
logContext: {
|
|
35
|
+
source: "skill_input",
|
|
36
|
+
skillName,
|
|
37
|
+
agentName,
|
|
38
|
+
message,
|
|
39
|
+
},
|
|
40
|
+
decision: {
|
|
41
|
+
surface: "skill",
|
|
42
|
+
value: skillName,
|
|
43
|
+
},
|
|
44
|
+
sessionApproval: SessionApproval.single("skill", skillName),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { toRecord } from "#src/common";
|
|
2
|
+
import { normalizePathForComparison } from "#src/path-utils";
|
|
3
|
+
import { formatSkillPathAskPrompt } from "#src/permission-prompts";
|
|
4
|
+
import { SessionApproval } from "#src/session-approval";
|
|
5
|
+
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
6
|
+
import { findSkillPathMatch } from "#src/skill-prompt-sanitizer";
|
|
7
|
+
import type { GateDescriptor } from "./descriptor";
|
|
8
|
+
import type { ToolCallContext } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a pure descriptor for the skill-read permission gate.
|
|
12
|
+
*
|
|
13
|
+
* Returns `null` when the gate does not apply (tool is not `read`, no active
|
|
14
|
+
* skill entries, or the read path does not match any skill).
|
|
15
|
+
* Returns a GateDescriptor with preResolved state from the matched skill entry.
|
|
16
|
+
*/
|
|
17
|
+
export function describeSkillReadGate(
|
|
18
|
+
tcc: ToolCallContext,
|
|
19
|
+
getActiveSkillEntries: () => SkillPromptEntry[],
|
|
20
|
+
): GateDescriptor | null {
|
|
21
|
+
const activeSkillEntries = getActiveSkillEntries();
|
|
22
|
+
|
|
23
|
+
if (tcc.toolName !== "read" || activeSkillEntries.length === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const inputRecord = toRecord(tcc.input);
|
|
28
|
+
const path = typeof inputRecord.path === "string" ? inputRecord.path : "";
|
|
29
|
+
if (!path) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (tcc.cwd === undefined) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalizedReadPath = normalizePathForComparison(path, tcc.cwd);
|
|
38
|
+
const matchedSkill = findSkillPathMatch(
|
|
39
|
+
normalizedReadPath,
|
|
40
|
+
activeSkillEntries,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!matchedSkill) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const skillReadMessage = formatSkillPathAskPrompt(
|
|
48
|
+
matchedSkill,
|
|
49
|
+
path,
|
|
50
|
+
tcc.agentName ?? undefined,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
surface: "skill",
|
|
55
|
+
input: { name: matchedSkill.name },
|
|
56
|
+
denialContext: {
|
|
57
|
+
kind: "skill_read",
|
|
58
|
+
skillName: matchedSkill.name,
|
|
59
|
+
readPath: path,
|
|
60
|
+
agentName: tcc.agentName ?? undefined,
|
|
61
|
+
},
|
|
62
|
+
promptDetails: {
|
|
63
|
+
source: "skill_read",
|
|
64
|
+
agentName: tcc.agentName,
|
|
65
|
+
message: skillReadMessage,
|
|
66
|
+
toolCallId: tcc.toolCallId,
|
|
67
|
+
toolName: tcc.toolName,
|
|
68
|
+
skillName: matchedSkill.name,
|
|
69
|
+
path,
|
|
70
|
+
},
|
|
71
|
+
logContext: {
|
|
72
|
+
source: "skill_read",
|
|
73
|
+
skillName: matchedSkill.name,
|
|
74
|
+
agentName: tcc.agentName,
|
|
75
|
+
path,
|
|
76
|
+
message: skillReadMessage,
|
|
77
|
+
},
|
|
78
|
+
decision: {
|
|
79
|
+
surface: "skill",
|
|
80
|
+
value: matchedSkill.name,
|
|
81
|
+
},
|
|
82
|
+
preResolved: {
|
|
83
|
+
state: matchedSkill.state,
|
|
84
|
+
},
|
|
85
|
+
sessionApproval: SessionApproval.single("skill", matchedSkill.name),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { getNonEmptyString, toRecord } from "#src/common";
|
|
2
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
3
|
+
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
4
|
+
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
|
5
|
+
import type { ToolInputFormatterLookup } from "#src/tool-input-formatter-registry";
|
|
6
|
+
import {
|
|
7
|
+
ToolPreviewFormatter,
|
|
8
|
+
type ToolPreviewFormatterOptions,
|
|
9
|
+
} from "#src/tool-preview-formatter";
|
|
10
|
+
import { resolveBashCommandCheck } from "./bash-command";
|
|
11
|
+
import { describeBashExternalDirectoryGate } from "./bash-external-directory";
|
|
12
|
+
import { describeBashPathGate } from "./bash-path";
|
|
13
|
+
import { BashProgram } from "./bash-program";
|
|
14
|
+
import type { GateResult } from "./descriptor";
|
|
15
|
+
import { describeExternalDirectoryGate } from "./external-directory";
|
|
16
|
+
import { describePathGate } from "./path";
|
|
17
|
+
import type { GateRunner } from "./runner";
|
|
18
|
+
import { describeSkillReadGate } from "./skill-read";
|
|
19
|
+
import { describeToolGate } from "./tool";
|
|
20
|
+
import type { GateOutcome, ToolCallContext } from "./types";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Narrow interface the pipeline needs from its session-side dependency.
|
|
24
|
+
*
|
|
25
|
+
* The three query methods needed to assemble gate inputs.
|
|
26
|
+
* The resolver is injected separately as a constructor parameter.
|
|
27
|
+
*
|
|
28
|
+
* `PermissionSession` satisfies this structurally at the construction call
|
|
29
|
+
* site; no `implements` clause is needed and would create a layer-inversion
|
|
30
|
+
* import from the domain module into the handler layer.
|
|
31
|
+
*/
|
|
32
|
+
export interface ToolCallGateInputs {
|
|
33
|
+
/** Active skill prompt entries for the skill-read gate. */
|
|
34
|
+
getActiveSkillEntries(): SkillPromptEntry[];
|
|
35
|
+
/** Combined infrastructure read directories (static + config-derived). */
|
|
36
|
+
getInfrastructureReadDirs(): string[];
|
|
37
|
+
/** Resolved tool-preview formatter options from the current config. */
|
|
38
|
+
getToolPreviewLimits(): ToolPreviewFormatterOptions;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Owns the ordered tool-call gate-producer assembly and the run loop.
|
|
43
|
+
*
|
|
44
|
+
* Constructed once in the composition root and injected into
|
|
45
|
+
* `PermissionGateHandler`. `evaluate(tcc, runner)` encapsulates:
|
|
46
|
+
* - bash-command extraction and single `BashProgram.parse` (#308)
|
|
47
|
+
* - `ToolPreviewFormatter` construction from `getToolPreviewLimits()`
|
|
48
|
+
* - infrastructure-dir list from `getInfrastructureReadDirs()`
|
|
49
|
+
* - all six gate producers in their prescribed order
|
|
50
|
+
* - the run loop that returns the first block outcome, or allow
|
|
51
|
+
*/
|
|
52
|
+
export class ToolCallGatePipeline {
|
|
53
|
+
constructor(
|
|
54
|
+
private readonly resolver: ScopedPermissionResolver,
|
|
55
|
+
private readonly inputs: ToolCallGateInputs,
|
|
56
|
+
private readonly customFormatters?: ToolInputFormatterLookup,
|
|
57
|
+
private readonly customExtractors?: ToolAccessExtractorLookup,
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
async evaluate(
|
|
61
|
+
tcc: ToolCallContext,
|
|
62
|
+
runner: GateRunner,
|
|
63
|
+
): Promise<GateOutcome> {
|
|
64
|
+
// Parse the bash command exactly once per evaluate; the three bash gates
|
|
65
|
+
// share this single BashProgram instead of each re-parsing (#308).
|
|
66
|
+
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
67
|
+
const bashProgram =
|
|
68
|
+
tcc.toolName === "bash" && command
|
|
69
|
+
? await BashProgram.parse(command)
|
|
70
|
+
: null;
|
|
71
|
+
|
|
72
|
+
const formatter = new ToolPreviewFormatter(
|
|
73
|
+
this.inputs.getToolPreviewLimits(),
|
|
74
|
+
this.customFormatters,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const infraDirs = this.inputs.getInfrastructureReadDirs();
|
|
78
|
+
|
|
79
|
+
const gateProducers: Array<() => GateResult | Promise<GateResult>> = [
|
|
80
|
+
() =>
|
|
81
|
+
describeSkillReadGate(tcc, () => this.inputs.getActiveSkillEntries()),
|
|
82
|
+
() => describePathGate(tcc, this.resolver, this.customExtractors),
|
|
83
|
+
() =>
|
|
84
|
+
describeExternalDirectoryGate(
|
|
85
|
+
tcc,
|
|
86
|
+
infraDirs,
|
|
87
|
+
this.resolver,
|
|
88
|
+
this.customExtractors,
|
|
89
|
+
),
|
|
90
|
+
() => describeBashExternalDirectoryGate(tcc, bashProgram, this.resolver),
|
|
91
|
+
() => describeBashPathGate(tcc, bashProgram, this.resolver),
|
|
92
|
+
() => {
|
|
93
|
+
// Bash commands may chain several sub-commands (`a && b`, `a | b`, …);
|
|
94
|
+
// evaluate each unit from the shared parse on the bash surface and
|
|
95
|
+
// select the most restrictive, rather than matching the whole program
|
|
96
|
+
// string (#301). Other tools evaluate their single input directly.
|
|
97
|
+
const toolCheck =
|
|
98
|
+
tcc.toolName === "bash" && bashProgram
|
|
99
|
+
? resolveBashCommandCheck(
|
|
100
|
+
command ?? "",
|
|
101
|
+
bashProgram.commands(),
|
|
102
|
+
tcc.agentName ?? undefined,
|
|
103
|
+
this.resolver,
|
|
104
|
+
)
|
|
105
|
+
: this.resolver.resolve(
|
|
106
|
+
tcc.toolName,
|
|
107
|
+
tcc.input,
|
|
108
|
+
tcc.agentName ?? undefined,
|
|
109
|
+
);
|
|
110
|
+
const toolDescriptor = describeToolGate(tcc, toolCheck, formatter);
|
|
111
|
+
toolDescriptor.preCheck = toolCheck;
|
|
112
|
+
return toolDescriptor;
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
for (const produce of gateProducers) {
|
|
117
|
+
const outcome = await runner.run(
|
|
118
|
+
await produce(),
|
|
119
|
+
tcc.agentName,
|
|
120
|
+
tcc.toolCallId,
|
|
121
|
+
);
|
|
122
|
+
if (outcome.action === "block") {
|
|
123
|
+
return outcome;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { action: "allow" };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPathBearingToolPath,
|
|
3
|
+
normalizePathForComparison,
|
|
4
|
+
PATH_BEARING_TOOLS,
|
|
5
|
+
} from "#src/path-utils";
|
|
6
|
+
import { suggestSessionPattern } from "#src/pattern-suggest";
|
|
7
|
+
import { formatAskPrompt } from "#src/permission-prompts";
|
|
8
|
+
import { SessionApproval } from "#src/session-approval";
|
|
9
|
+
import type { ToolPreviewFormatter } from "#src/tool-preview-formatter";
|
|
10
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
11
|
+
import type { GateDescriptor } from "./descriptor";
|
|
12
|
+
import { deriveDecisionValue } from "./helpers";
|
|
13
|
+
import type { ToolCallContext } from "./types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Derive the value used for session-approval pattern suggestions.
|
|
17
|
+
*
|
|
18
|
+
* Bash → command string; MCP → qualified target;
|
|
19
|
+
* path-bearing tools → the file path resolved to its canonical (cwd-anchored,
|
|
20
|
+
* absolute) form so the suggested pattern matches the policy values a later
|
|
21
|
+
* call produces; others → catch-all wildcard.
|
|
22
|
+
*/
|
|
23
|
+
function deriveSuggestionValue(
|
|
24
|
+
tcc: ToolCallContext,
|
|
25
|
+
check: PermissionCheckResult,
|
|
26
|
+
): string {
|
|
27
|
+
if (tcc.toolName === "bash") return check.command ?? "";
|
|
28
|
+
if (tcc.toolName === "mcp") return check.target ?? "mcp";
|
|
29
|
+
const path = getPathBearingToolPath(tcc.toolName, tcc.input);
|
|
30
|
+
if (path === null) return "*";
|
|
31
|
+
return tcc.cwd ? normalizePathForComparison(path, tcc.cwd) : path;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a pure descriptor for the normal tool permission gate.
|
|
36
|
+
*
|
|
37
|
+
* Takes a pre-computed PermissionCheckResult (from checkPermission) and
|
|
38
|
+
* returns a GateDescriptor that the runner can execute. No side effects.
|
|
39
|
+
*/
|
|
40
|
+
export function describeToolGate(
|
|
41
|
+
tcc: ToolCallContext,
|
|
42
|
+
check: PermissionCheckResult,
|
|
43
|
+
formatter: ToolPreviewFormatter,
|
|
44
|
+
): GateDescriptor {
|
|
45
|
+
const permissionLogContext = formatter.getPermissionLogContext(
|
|
46
|
+
check,
|
|
47
|
+
tcc.input,
|
|
48
|
+
PATH_BEARING_TOOLS,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Compute session approval suggestion for the "for this session" option.
|
|
52
|
+
const suggestion = suggestSessionPattern(
|
|
53
|
+
tcc.toolName,
|
|
54
|
+
deriveSuggestionValue(tcc, check),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const askMessage = formatAskPrompt(
|
|
58
|
+
check,
|
|
59
|
+
tcc.agentName ?? undefined,
|
|
60
|
+
tcc.input,
|
|
61
|
+
formatter,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
surface: tcc.toolName,
|
|
66
|
+
input: tcc.input,
|
|
67
|
+
denialContext: {
|
|
68
|
+
kind: "tool",
|
|
69
|
+
check,
|
|
70
|
+
agentName: tcc.agentName ?? undefined,
|
|
71
|
+
input: tcc.input,
|
|
72
|
+
},
|
|
73
|
+
sessionApproval: SessionApproval.single(
|
|
74
|
+
suggestion.surface,
|
|
75
|
+
suggestion.pattern,
|
|
76
|
+
),
|
|
77
|
+
promptDetails: {
|
|
78
|
+
source: "tool_call",
|
|
79
|
+
agentName: tcc.agentName,
|
|
80
|
+
message: askMessage,
|
|
81
|
+
toolCallId: tcc.toolCallId,
|
|
82
|
+
toolName: tcc.toolName,
|
|
83
|
+
sessionLabel: suggestion.label,
|
|
84
|
+
...permissionLogContext,
|
|
85
|
+
},
|
|
86
|
+
logContext: {
|
|
87
|
+
source: "tool_call",
|
|
88
|
+
toolCallId: tcc.toolCallId,
|
|
89
|
+
toolName: tcc.toolName,
|
|
90
|
+
message: askMessage,
|
|
91
|
+
...permissionLogContext,
|
|
92
|
+
},
|
|
93
|
+
decision: {
|
|
94
|
+
surface: tcc.toolName,
|
|
95
|
+
value: deriveDecisionValue(
|
|
96
|
+
tcc.toolName,
|
|
97
|
+
check,
|
|
98
|
+
getPathBearingToolPath(tcc.toolName, tcc.input) ?? undefined,
|
|
99
|
+
),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Outcome of a single permission gate evaluation. */
|
|
2
|
+
export type GateOutcome =
|
|
3
|
+
| { action: "allow" }
|
|
4
|
+
| { action: "block"; reason: string };
|
|
5
|
+
|
|
6
|
+
/** Pre-validated context shared across all gates. */
|
|
7
|
+
export interface ToolCallContext {
|
|
8
|
+
toolName: string;
|
|
9
|
+
agentName: string | null;
|
|
10
|
+
input: unknown;
|
|
11
|
+
toolCallId: string;
|
|
12
|
+
cwd: string | undefined;
|
|
13
|
+
}
|