@gotgenes/pi-permission-system 5.9.0 → 5.11.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/package.json +1 -1
- package/src/forwarding-manager.ts +1 -1
- package/src/handlers/before-agent-start.ts +76 -76
- package/src/handlers/gates/descriptor.ts +1 -1
- package/src/handlers/index.ts +6 -15
- package/src/handlers/lifecycle.ts +55 -59
- package/src/handlers/permission-gate-handler.ts +346 -0
- package/src/index.ts +46 -54
- package/src/permission-prompter.ts +23 -6
- package/src/permission-session.ts +281 -0
- package/src/runtime.ts +5 -30
- package/src/session-logger.ts +1 -1
- package/src/skill-prompt-sanitizer.ts +15 -4
- package/src/tool-registry.ts +6 -0
- package/tests/handlers/before-agent-start.test.ts +116 -167
- package/tests/handlers/input-events.test.ts +87 -92
- package/tests/handlers/input.test.ts +98 -128
- package/tests/handlers/lifecycle.test.ts +97 -227
- package/tests/handlers/tool-call-events.test.ts +146 -166
- package/tests/handlers/tool-call.test.ts +102 -97
- package/tests/permission-prompter.test.ts +1 -1
- package/tests/permission-session.test.ts +607 -0
- package/tests/runtime.test.ts +2 -77
- package/src/handlers/input.ts +0 -126
- package/src/handlers/tool-call.ts +0 -210
- package/src/handlers/types.ts +0 -90
package/tests/runtime.test.ts
CHANGED
|
@@ -8,8 +8,6 @@ const {
|
|
|
8
8
|
mockCreateLogger,
|
|
9
9
|
mockLoadAndMergeConfigs,
|
|
10
10
|
mockSyncPermissionSystemStatus,
|
|
11
|
-
mockGetActiveAgentName,
|
|
12
|
-
mockGetActiveAgentNameFromSystemPrompt,
|
|
13
11
|
mockBuildResolvedConfigLogEntry,
|
|
14
12
|
mockDiscoverGlobalNodeModulesRoot,
|
|
15
13
|
} = vi.hoisted(() => ({
|
|
@@ -24,9 +22,6 @@ const {
|
|
|
24
22
|
mockCreateLogger: vi.fn(),
|
|
25
23
|
mockLoadAndMergeConfigs: vi.fn(),
|
|
26
24
|
mockSyncPermissionSystemStatus: vi.fn(),
|
|
27
|
-
mockGetActiveAgentName: vi.fn<() => string | null>(),
|
|
28
|
-
mockGetActiveAgentNameFromSystemPrompt:
|
|
29
|
-
vi.fn<(prompt?: string) => string | null>(),
|
|
30
25
|
mockBuildResolvedConfigLogEntry: vi.fn(),
|
|
31
26
|
mockDiscoverGlobalNodeModulesRoot: vi.fn<() => string | null>(),
|
|
32
27
|
}));
|
|
@@ -50,11 +45,6 @@ vi.mock("../src/status", () => ({
|
|
|
50
45
|
getPermissionSystemStatus: vi.fn(),
|
|
51
46
|
}));
|
|
52
47
|
|
|
53
|
-
vi.mock("../src/active-agent", () => ({
|
|
54
|
-
getActiveAgentName: mockGetActiveAgentName,
|
|
55
|
-
getActiveAgentNameFromSystemPrompt: mockGetActiveAgentNameFromSystemPrompt,
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
48
|
vi.mock("../src/config-reporter", () => ({
|
|
59
49
|
buildResolvedConfigLogEntry: mockBuildResolvedConfigLogEntry,
|
|
60
50
|
}));
|
|
@@ -89,7 +79,6 @@ import {
|
|
|
89
79
|
createPermissionManagerForCwd,
|
|
90
80
|
derivePiProjectPaths,
|
|
91
81
|
refreshExtensionConfig,
|
|
92
|
-
resolveAgentName,
|
|
93
82
|
} from "../src/runtime";
|
|
94
83
|
|
|
95
84
|
// ── test suite ─────────────────────────────────────────────────────────────
|
|
@@ -578,69 +567,5 @@ describe("refreshExtensionConfig", () => {
|
|
|
578
567
|
});
|
|
579
568
|
});
|
|
580
569
|
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
describe("resolveAgentName", () => {
|
|
584
|
-
function makeRuntime() {
|
|
585
|
-
mockCreateLogger.mockReturnValue({
|
|
586
|
-
debug: mockLoggerDebug,
|
|
587
|
-
review: mockLoggerReview,
|
|
588
|
-
});
|
|
589
|
-
return createExtensionRuntime({ agentDir: "/test/agent" });
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function makeCtx(): ExtensionContext {
|
|
593
|
-
return {
|
|
594
|
-
cwd: "/test/project",
|
|
595
|
-
hasUI: false,
|
|
596
|
-
ui: {},
|
|
597
|
-
sessionManager: { getEntries: vi.fn(), addEntry: vi.fn() },
|
|
598
|
-
} as unknown as ExtensionContext;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
beforeEach(() => {
|
|
602
|
-
mockLoggerDebug.mockReset().mockReturnValue(undefined);
|
|
603
|
-
mockGetActiveAgentName.mockReset().mockReturnValue(null);
|
|
604
|
-
mockGetActiveAgentNameFromSystemPrompt.mockReset().mockReturnValue(null);
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
it("returns and stores name from getActiveAgentName when available", () => {
|
|
608
|
-
const runtime = makeRuntime();
|
|
609
|
-
mockGetActiveAgentName.mockReturnValue("session-agent");
|
|
610
|
-
const result = resolveAgentName(runtime, makeCtx());
|
|
611
|
-
expect(result).toBe("session-agent");
|
|
612
|
-
expect(runtime.lastKnownActiveAgentName).toBe("session-agent");
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
it("falls back to getActiveAgentNameFromSystemPrompt when session name is null", () => {
|
|
616
|
-
const runtime = makeRuntime();
|
|
617
|
-
mockGetActiveAgentName.mockReturnValue(null);
|
|
618
|
-
mockGetActiveAgentNameFromSystemPrompt.mockReturnValue("prompt-agent");
|
|
619
|
-
const result = resolveAgentName(runtime, makeCtx(), "system prompt text");
|
|
620
|
-
expect(result).toBe("prompt-agent");
|
|
621
|
-
expect(runtime.lastKnownActiveAgentName).toBe("prompt-agent");
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
it("falls back to lastKnownActiveAgentName when both sources return null", () => {
|
|
625
|
-
const runtime = makeRuntime();
|
|
626
|
-
runtime.lastKnownActiveAgentName = "remembered-agent";
|
|
627
|
-
mockGetActiveAgentName.mockReturnValue(null);
|
|
628
|
-
mockGetActiveAgentNameFromSystemPrompt.mockReturnValue(null);
|
|
629
|
-
const result = resolveAgentName(runtime, makeCtx());
|
|
630
|
-
expect(result).toBe("remembered-agent");
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
it("returns null when all sources are null and no prior name", () => {
|
|
634
|
-
const runtime = makeRuntime();
|
|
635
|
-
const result = resolveAgentName(runtime, makeCtx());
|
|
636
|
-
expect(result).toBeNull();
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
it("does not update lastKnownActiveAgentName when falling back to stored value", () => {
|
|
640
|
-
const runtime = makeRuntime();
|
|
641
|
-
runtime.lastKnownActiveAgentName = "remembered-agent";
|
|
642
|
-
resolveAgentName(runtime, makeCtx());
|
|
643
|
-
// Value unchanged — not overwritten with null
|
|
644
|
-
expect(runtime.lastKnownActiveAgentName).toBe("remembered-agent");
|
|
645
|
-
});
|
|
646
|
-
});
|
|
570
|
+
// resolveAgentName was moved to PermissionSession (#129)
|
|
571
|
+
// Tests live in tests/permission-session.test.ts
|
package/src/handlers/input.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionContext,
|
|
3
|
-
InputEventResult,
|
|
4
|
-
} from "@mariozechner/pi-coding-agent";
|
|
5
|
-
|
|
6
|
-
/** Minimal subset of InputEvent used by this handler. */
|
|
7
|
-
interface InputPayload {
|
|
8
|
-
text: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
import { emitDecisionEvent } from "../permission-events";
|
|
12
|
-
import { applyPermissionGate } from "../permission-gate";
|
|
13
|
-
import { formatSkillAskPrompt } from "../permission-prompts";
|
|
14
|
-
import type { HandlerDeps } from "./types";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Parse a `/skill:<name>` prefix from user input.
|
|
18
|
-
* Returns the skill name, or null if the text is not a skill invocation.
|
|
19
|
-
*/
|
|
20
|
-
export function extractSkillNameFromInput(text: string): string | null {
|
|
21
|
-
const trimmed = text.trim();
|
|
22
|
-
if (!trimmed.startsWith("/skill:")) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const afterPrefix = trimmed.slice("/skill:".length);
|
|
27
|
-
if (!afterPrefix) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const firstWhitespace = afterPrefix.search(/\s/);
|
|
32
|
-
const skillName = (
|
|
33
|
-
firstWhitespace === -1 ? afterPrefix : afterPrefix.slice(0, firstWhitespace)
|
|
34
|
-
).trim();
|
|
35
|
-
return skillName || null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function handleInput(
|
|
39
|
-
deps: HandlerDeps,
|
|
40
|
-
event: InputPayload,
|
|
41
|
-
ctx: ExtensionContext,
|
|
42
|
-
): Promise<InputEventResult> {
|
|
43
|
-
deps.session.runtimeContext = ctx;
|
|
44
|
-
deps.forwarding.start(ctx);
|
|
45
|
-
|
|
46
|
-
const skillName = extractSkillNameFromInput(event.text);
|
|
47
|
-
if (!skillName) {
|
|
48
|
-
return { action: "continue" };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const agentName = deps.resolveAgentName(ctx);
|
|
52
|
-
const check = deps.session.permissionManager.checkPermission(
|
|
53
|
-
"skill",
|
|
54
|
-
{ name: skillName },
|
|
55
|
-
agentName ?? undefined,
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
if (check.state === "deny" && ctx.hasUI) {
|
|
59
|
-
const notifyMessage = agentName
|
|
60
|
-
? `Skill '${skillName}' is not permitted for agent '${agentName}'.`
|
|
61
|
-
: `Skill '${skillName}' is not permitted by the current skill policy.`;
|
|
62
|
-
ctx.ui.notify(notifyMessage, "warning");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const skillInputMessage = formatSkillAskPrompt(
|
|
66
|
-
skillName,
|
|
67
|
-
agentName ?? undefined,
|
|
68
|
-
);
|
|
69
|
-
const skillInputCanConfirm = deps.canRequestPermissionConfirmation(ctx);
|
|
70
|
-
let skillInputAutoApproved = false;
|
|
71
|
-
const skillInputGate = await applyPermissionGate({
|
|
72
|
-
state: check.state,
|
|
73
|
-
canConfirm: skillInputCanConfirm,
|
|
74
|
-
promptForApproval: async () => {
|
|
75
|
-
const decision = await deps.promptPermission(ctx, {
|
|
76
|
-
requestId: deps.createPermissionRequestId("skill-input"),
|
|
77
|
-
source: "skill_input",
|
|
78
|
-
agentName,
|
|
79
|
-
message: skillInputMessage,
|
|
80
|
-
skillName,
|
|
81
|
-
});
|
|
82
|
-
skillInputAutoApproved = decision.autoApproved === true;
|
|
83
|
-
return decision;
|
|
84
|
-
},
|
|
85
|
-
writeLog: deps.logger.review,
|
|
86
|
-
logContext: {
|
|
87
|
-
source: "skill_input",
|
|
88
|
-
skillName,
|
|
89
|
-
agentName,
|
|
90
|
-
message: skillInputMessage,
|
|
91
|
-
},
|
|
92
|
-
messages: {
|
|
93
|
-
denyReason: skillInputMessage,
|
|
94
|
-
unavailableReason:
|
|
95
|
-
"Skill requires approval, but no interactive UI is available.",
|
|
96
|
-
userDeniedReason: () => "User denied skill.",
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
emitDecisionEvent(deps.events, {
|
|
101
|
-
surface: "skill",
|
|
102
|
-
value: skillName,
|
|
103
|
-
result: skillInputGate.action === "allow" ? "allow" : "deny",
|
|
104
|
-
resolution:
|
|
105
|
-
check.state === "allow"
|
|
106
|
-
? "policy_allow"
|
|
107
|
-
: check.state === "deny"
|
|
108
|
-
? "policy_deny"
|
|
109
|
-
: skillInputGate.action === "allow"
|
|
110
|
-
? skillInputAutoApproved
|
|
111
|
-
? "auto_approved"
|
|
112
|
-
: "user_approved"
|
|
113
|
-
: skillInputCanConfirm
|
|
114
|
-
? "user_denied"
|
|
115
|
-
: "confirmation_unavailable",
|
|
116
|
-
origin: check.origin ?? null,
|
|
117
|
-
agentName: agentName ?? null,
|
|
118
|
-
matchedPattern: check.matchedPattern ?? null,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
if (skillInputGate.action === "block") {
|
|
122
|
-
return { action: "handled" };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return { action: "continue" };
|
|
126
|
-
}
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
import { toRecord } from "../common";
|
|
4
|
-
import { emitDecisionEvent } from "../permission-events";
|
|
5
|
-
import {
|
|
6
|
-
formatMissingToolNameReason,
|
|
7
|
-
formatUnknownToolReason,
|
|
8
|
-
} from "../permission-prompts";
|
|
9
|
-
import {
|
|
10
|
-
checkRequestedToolRegistration,
|
|
11
|
-
getToolNameFromValue,
|
|
12
|
-
} from "../tool-registry";
|
|
13
|
-
import { describeBashExternalDirectoryGate } from "./gates/bash-external-directory";
|
|
14
|
-
import type { GateRunnerDeps } from "./gates/descriptor";
|
|
15
|
-
import { isGateBypass } from "./gates/descriptor";
|
|
16
|
-
import { describeExternalDirectoryGate } from "./gates/external-directory";
|
|
17
|
-
import { runGateCheck } from "./gates/runner";
|
|
18
|
-
import { describeSkillReadGate } from "./gates/skill-read";
|
|
19
|
-
import { describeToolGate } from "./gates/tool";
|
|
20
|
-
import type { ToolCallContext } from "./gates/types";
|
|
21
|
-
import type { HandlerDeps, PromptPermissionDetails } from "./types";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Extract the tool input from an event, checking both `input` and `arguments`
|
|
25
|
-
* fields (different Pi SDK versions use different names).
|
|
26
|
-
*/
|
|
27
|
-
export function getEventInput(event: unknown): unknown {
|
|
28
|
-
const record = toRecord(event);
|
|
29
|
-
|
|
30
|
-
if (record.input !== undefined) {
|
|
31
|
-
return record.input;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (record.arguments !== undefined) {
|
|
35
|
-
return record.arguments;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return {};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function handleToolCall(
|
|
42
|
-
deps: HandlerDeps,
|
|
43
|
-
event: unknown,
|
|
44
|
-
ctx: ExtensionContext,
|
|
45
|
-
): Promise<{ block?: true; reason?: string }> {
|
|
46
|
-
deps.session.runtimeContext = ctx;
|
|
47
|
-
deps.forwarding.start(ctx);
|
|
48
|
-
|
|
49
|
-
const agentName = deps.resolveAgentName(ctx);
|
|
50
|
-
const toolName = getToolNameFromValue(event);
|
|
51
|
-
|
|
52
|
-
if (!toolName) {
|
|
53
|
-
return { block: true, reason: formatMissingToolNameReason() };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const registrationCheck = checkRequestedToolRegistration(
|
|
57
|
-
toolName,
|
|
58
|
-
deps.getAllTools(),
|
|
59
|
-
);
|
|
60
|
-
if (registrationCheck.status === "missing-tool-name") {
|
|
61
|
-
return { block: true, reason: formatMissingToolNameReason() };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (registrationCheck.status === "unregistered") {
|
|
65
|
-
return {
|
|
66
|
-
block: true,
|
|
67
|
-
reason: formatUnknownToolReason(
|
|
68
|
-
registrationCheck.requestedToolName,
|
|
69
|
-
registrationCheck.availableToolNames,
|
|
70
|
-
),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const input = getEventInput(event);
|
|
75
|
-
const toolCallId =
|
|
76
|
-
typeof (event as Record<string, unknown>).toolCallId === "string"
|
|
77
|
-
? ((event as Record<string, unknown>).toolCallId as string)
|
|
78
|
-
: "";
|
|
79
|
-
|
|
80
|
-
const tcc: ToolCallContext = {
|
|
81
|
-
toolName,
|
|
82
|
-
agentName,
|
|
83
|
-
input,
|
|
84
|
-
toolCallId,
|
|
85
|
-
cwd: ctx.cwd,
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// ── Shared gate adapter closures ───────────────────────────────────────
|
|
89
|
-
const canConfirm = () => deps.canRequestPermissionConfirmation(ctx);
|
|
90
|
-
const promptPermission = (details: PromptPermissionDetails) =>
|
|
91
|
-
deps.promptPermission(ctx, details);
|
|
92
|
-
const emitDecision: GateRunnerDeps["emitDecision"] = (e) =>
|
|
93
|
-
emitDecisionEvent(deps.events, e);
|
|
94
|
-
const { review: writeReviewLog } = deps.logger;
|
|
95
|
-
const checkPermission: GateRunnerDeps["checkPermission"] = (
|
|
96
|
-
surface,
|
|
97
|
-
input,
|
|
98
|
-
agent,
|
|
99
|
-
sessionRules,
|
|
100
|
-
) =>
|
|
101
|
-
deps.session.permissionManager.checkPermission(
|
|
102
|
-
surface,
|
|
103
|
-
input,
|
|
104
|
-
agent,
|
|
105
|
-
sessionRules,
|
|
106
|
-
);
|
|
107
|
-
const getSessionRuleset = () => deps.session.sessionRules.getRuleset();
|
|
108
|
-
const approveSessionRule = (surface: string, pattern: string) =>
|
|
109
|
-
deps.session.sessionRules.approve(surface, pattern);
|
|
110
|
-
|
|
111
|
-
// ── Shared runner deps (built once, reused for all gates) ─────────────
|
|
112
|
-
const runnerDeps: GateRunnerDeps = {
|
|
113
|
-
checkPermission,
|
|
114
|
-
getSessionRuleset,
|
|
115
|
-
approveSessionRule,
|
|
116
|
-
writeReviewLog,
|
|
117
|
-
emitDecision,
|
|
118
|
-
canConfirm,
|
|
119
|
-
promptPermission,
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// ── Skill-read gate (descriptor + runner) ────────────────────────────────
|
|
123
|
-
const skillDescriptor = describeSkillReadGate(
|
|
124
|
-
tcc,
|
|
125
|
-
() => deps.session.activeSkillEntries,
|
|
126
|
-
);
|
|
127
|
-
if (skillDescriptor) {
|
|
128
|
-
const skillResult = await runGateCheck(
|
|
129
|
-
skillDescriptor,
|
|
130
|
-
tcc.agentName,
|
|
131
|
-
tcc.toolCallId,
|
|
132
|
-
runnerDeps,
|
|
133
|
-
);
|
|
134
|
-
if (skillResult.action === "block") {
|
|
135
|
-
return { block: true, reason: skillResult.reason };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── External-directory gate (descriptor + runner) ─────────────────────────
|
|
140
|
-
const infraDirs = [
|
|
141
|
-
...deps.piInfrastructureDirs,
|
|
142
|
-
...deps.getPiInfrastructureReadPaths(),
|
|
143
|
-
];
|
|
144
|
-
const extDirDesc = describeExternalDirectoryGate(tcc, infraDirs);
|
|
145
|
-
if (extDirDesc) {
|
|
146
|
-
if (isGateBypass(extDirDesc)) {
|
|
147
|
-
if (extDirDesc.log) {
|
|
148
|
-
writeReviewLog(extDirDesc.log.event, extDirDesc.log.details);
|
|
149
|
-
}
|
|
150
|
-
if (extDirDesc.decision) {
|
|
151
|
-
emitDecision(extDirDesc.decision);
|
|
152
|
-
}
|
|
153
|
-
} else {
|
|
154
|
-
const extDirResult = await runGateCheck(
|
|
155
|
-
extDirDesc,
|
|
156
|
-
tcc.agentName,
|
|
157
|
-
tcc.toolCallId,
|
|
158
|
-
runnerDeps,
|
|
159
|
-
);
|
|
160
|
-
if (extDirResult.action === "block") {
|
|
161
|
-
return { block: true, reason: extDirResult.reason };
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── Bash external-directory gate (descriptor + runner) ─────────────────────
|
|
167
|
-
const bashExtDesc = await describeBashExternalDirectoryGate(
|
|
168
|
-
tcc,
|
|
169
|
-
checkPermission,
|
|
170
|
-
getSessionRuleset,
|
|
171
|
-
);
|
|
172
|
-
if (bashExtDesc) {
|
|
173
|
-
if (isGateBypass(bashExtDesc)) {
|
|
174
|
-
if (bashExtDesc.log) {
|
|
175
|
-
writeReviewLog(bashExtDesc.log.event, bashExtDesc.log.details);
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
const bashExtResult = await runGateCheck(
|
|
179
|
-
bashExtDesc,
|
|
180
|
-
tcc.agentName,
|
|
181
|
-
tcc.toolCallId,
|
|
182
|
-
runnerDeps,
|
|
183
|
-
);
|
|
184
|
-
if (bashExtResult.action === "block") {
|
|
185
|
-
return { block: true, reason: bashExtResult.reason };
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ── Normal tool permission gate (descriptor + runner) ───────────────────────────
|
|
191
|
-
const toolCheck = checkPermission(
|
|
192
|
-
tcc.toolName,
|
|
193
|
-
tcc.input,
|
|
194
|
-
tcc.agentName ?? undefined,
|
|
195
|
-
getSessionRuleset(),
|
|
196
|
-
);
|
|
197
|
-
const toolDescriptor = describeToolGate(tcc, toolCheck);
|
|
198
|
-
toolDescriptor.preCheck = toolCheck;
|
|
199
|
-
const toolResult = await runGateCheck(
|
|
200
|
-
toolDescriptor,
|
|
201
|
-
tcc.agentName,
|
|
202
|
-
tcc.toolCallId,
|
|
203
|
-
runnerDeps,
|
|
204
|
-
);
|
|
205
|
-
if (toolResult.action === "block") {
|
|
206
|
-
return { block: true, reason: toolResult.reason };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return {};
|
|
210
|
-
}
|
package/src/handlers/types.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
import type { ForwardingController } from "../forwarding-manager";
|
|
4
|
-
import type { PermissionPromptDecision } from "../permission-dialog";
|
|
5
|
-
import type { PermissionEventBus } from "../permission-events";
|
|
6
|
-
import type { PermissionManager } from "../permission-manager";
|
|
7
|
-
import type { SessionState } from "../runtime";
|
|
8
|
-
import type { SessionLogger } from "../session-logger";
|
|
9
|
-
|
|
10
|
-
export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
11
|
-
|
|
12
|
-
/** Details passed when prompting the user for a permission decision. */
|
|
13
|
-
export interface PromptPermissionDetails {
|
|
14
|
-
requestId: string;
|
|
15
|
-
source: PermissionReviewSource;
|
|
16
|
-
agentName: string | null;
|
|
17
|
-
message: string;
|
|
18
|
-
toolCallId?: string;
|
|
19
|
-
toolName?: string;
|
|
20
|
-
skillName?: string;
|
|
21
|
-
path?: string;
|
|
22
|
-
command?: string;
|
|
23
|
-
target?: string;
|
|
24
|
-
toolInputPreview?: string;
|
|
25
|
-
/** Override label for the "for this session" dialog option. */
|
|
26
|
-
sessionLabel?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Explicit dependency bag passed to each extracted event handler.
|
|
31
|
-
*
|
|
32
|
-
* Mutable session state lives in `session`; handlers read and write
|
|
33
|
-
* `deps.session.*` directly. Logging, infrastructure paths, and the
|
|
34
|
-
* event bus are promoted to top-level fields so handlers and gate
|
|
35
|
-
* adapters never reach through nested objects for leaf operations.
|
|
36
|
-
*/
|
|
37
|
-
export interface HandlerDeps {
|
|
38
|
-
// ── Session state ─────────────────────────────────────────────────────
|
|
39
|
-
/** Mutable session state: permissionManager, sessionRules, cache keys. */
|
|
40
|
-
readonly session: SessionState;
|
|
41
|
-
|
|
42
|
-
// ── Logging ────────────────────────────────────────────────────────────
|
|
43
|
-
readonly logger: SessionLogger;
|
|
44
|
-
|
|
45
|
-
// ── Immutable infrastructure paths ───────────────────────────────────
|
|
46
|
-
readonly piInfrastructureDirs: readonly string[];
|
|
47
|
-
/** Returns config-derived infrastructure read paths (current at call time). */
|
|
48
|
-
getPiInfrastructureReadPaths(): string[];
|
|
49
|
-
|
|
50
|
-
// ── Event bus ────────────────────────────────────────────────────────
|
|
51
|
-
/** Event bus for emitting permissions:decision broadcast events. */
|
|
52
|
-
readonly events: PermissionEventBus;
|
|
53
|
-
|
|
54
|
-
// ── Factories ──────────────────────────────────────────────────────────
|
|
55
|
-
/** Create a new PermissionManager scoped to cwd's config hierarchy. */
|
|
56
|
-
createPermissionManagerForCwd(
|
|
57
|
-
cwd: string | undefined | null,
|
|
58
|
-
): PermissionManager;
|
|
59
|
-
|
|
60
|
-
// ── Config & lifecycle helpers ─────────────────────────────────────────
|
|
61
|
-
/** Reload merged config from disk; optionally update the stored runtime context. */
|
|
62
|
-
refreshExtensionConfig(ctx?: ExtensionContext): void;
|
|
63
|
-
/** Write the resolved config path set to the review and debug logs. */
|
|
64
|
-
logResolvedConfigPaths(): void;
|
|
65
|
-
|
|
66
|
-
// ── Permission helpers ─────────────────────────────────────────────────
|
|
67
|
-
/**
|
|
68
|
-
* Resolve the active agent name from the session context or system prompt.
|
|
69
|
-
* Updates session.lastKnownActiveAgentName as a side effect.
|
|
70
|
-
*/
|
|
71
|
-
resolveAgentName(ctx: ExtensionContext, systemPrompt?: string): string | null;
|
|
72
|
-
/** Whether the current context can show an interactive permission prompt. */
|
|
73
|
-
canRequestPermissionConfirmation(ctx: ExtensionContext): boolean;
|
|
74
|
-
/** Prompt the user for a permission decision, log the outcome, and return it. */
|
|
75
|
-
promptPermission(
|
|
76
|
-
ctx: ExtensionContext,
|
|
77
|
-
details: PromptPermissionDetails,
|
|
78
|
-
): Promise<PermissionPromptDecision>;
|
|
79
|
-
/** Generate a unique ID for a permission request. */
|
|
80
|
-
createPermissionRequestId(prefix: string): string;
|
|
81
|
-
|
|
82
|
-
// ── Forwarding ─────────────────────────────────────────────────────────
|
|
83
|
-
readonly forwarding: ForwardingController;
|
|
84
|
-
/** Unsubscribe the permissions:rpc:check and permissions:rpc:prompt handlers. */
|
|
85
|
-
stopPermissionRpcHandlers(): void;
|
|
86
|
-
|
|
87
|
-
// ── Pi API subset ──────────────────────────────────────────────────────
|
|
88
|
-
getAllTools(): unknown[];
|
|
89
|
-
setActiveTools(names: string[]): void;
|
|
90
|
-
}
|