@gotgenes/pi-permission-system 10.0.0 → 10.1.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 +26 -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 +49 -69
- package/src/mcp-targets.ts +56 -46
- package/src/permission-prompter.ts +7 -58
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +77 -9
- package/src/permissions-service.ts +53 -0
- 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 +86 -22
- 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 +63 -148
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +150 -93
- 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/helpers/gate-fixtures.ts +147 -16
- package/test/helpers/handler-fixtures.ts +143 -27
- 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-prompter.test.ts +33 -44
- package/test/permission-session.test.ts +160 -27
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +0 -4
- 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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { getNonEmptyString, toRecord } from "#src/common";
|
|
2
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
3
|
+
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
4
|
+
import type { ToolInputFormatterLookup } from "#src/tool-input-formatter-registry";
|
|
5
|
+
import {
|
|
6
|
+
ToolPreviewFormatter,
|
|
7
|
+
type ToolPreviewFormatterOptions,
|
|
8
|
+
} from "#src/tool-preview-formatter";
|
|
9
|
+
import { resolveBashCommandCheck } from "./bash-command";
|
|
10
|
+
import { describeBashExternalDirectoryGate } from "./bash-external-directory";
|
|
11
|
+
import { describeBashPathGate } from "./bash-path";
|
|
12
|
+
import { BashProgram } from "./bash-program";
|
|
13
|
+
import type { GateResult } from "./descriptor";
|
|
14
|
+
import { describeExternalDirectoryGate } from "./external-directory";
|
|
15
|
+
import { describePathGate } from "./path";
|
|
16
|
+
import type { GateRunner } from "./runner";
|
|
17
|
+
import { describeSkillReadGate } from "./skill-read";
|
|
18
|
+
import { describeToolGate } from "./tool";
|
|
19
|
+
import type { GateOutcome, ToolCallContext } from "./types";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Narrow interface the pipeline needs from its session-side dependency.
|
|
23
|
+
*
|
|
24
|
+
* Extends `PermissionResolver` (the `resolve` method gate factories use)
|
|
25
|
+
* with the three query methods needed to assemble gate inputs.
|
|
26
|
+
*
|
|
27
|
+
* `PermissionSession` satisfies this structurally at the construction call
|
|
28
|
+
* site; no `implements` clause is needed and would create a layer-inversion
|
|
29
|
+
* import from the domain module into the handler layer.
|
|
30
|
+
*/
|
|
31
|
+
export interface ToolCallGateInputs extends PermissionResolver {
|
|
32
|
+
/** Active skill prompt entries for the skill-read gate. */
|
|
33
|
+
getActiveSkillEntries(): SkillPromptEntry[];
|
|
34
|
+
/** Combined infrastructure read directories (static + config-derived). */
|
|
35
|
+
getInfrastructureReadDirs(): string[];
|
|
36
|
+
/** Resolved tool-preview formatter options from the current config. */
|
|
37
|
+
getToolPreviewLimits(): ToolPreviewFormatterOptions;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Owns the ordered tool-call gate-producer assembly and the run loop.
|
|
42
|
+
*
|
|
43
|
+
* Constructed once in the composition root and injected into
|
|
44
|
+
* `PermissionGateHandler`. `evaluate(tcc, runner)` encapsulates:
|
|
45
|
+
* - bash-command extraction and single `BashProgram.parse` (#308)
|
|
46
|
+
* - `ToolPreviewFormatter` construction from `getToolPreviewLimits()`
|
|
47
|
+
* - infrastructure-dir list from `getInfrastructureReadDirs()`
|
|
48
|
+
* - all six gate producers in their prescribed order
|
|
49
|
+
* - the run loop that returns the first block outcome, or allow
|
|
50
|
+
*/
|
|
51
|
+
export class ToolCallGatePipeline {
|
|
52
|
+
constructor(
|
|
53
|
+
private readonly inputs: ToolCallGateInputs,
|
|
54
|
+
private readonly customFormatters?: ToolInputFormatterLookup,
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
async evaluate(
|
|
58
|
+
tcc: ToolCallContext,
|
|
59
|
+
runner: GateRunner,
|
|
60
|
+
): Promise<GateOutcome> {
|
|
61
|
+
// Parse the bash command exactly once per evaluate; the three bash gates
|
|
62
|
+
// share this single BashProgram instead of each re-parsing (#308).
|
|
63
|
+
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
64
|
+
const bashProgram =
|
|
65
|
+
tcc.toolName === "bash" && command
|
|
66
|
+
? await BashProgram.parse(command)
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
const formatter = new ToolPreviewFormatter(
|
|
70
|
+
this.inputs.getToolPreviewLimits(),
|
|
71
|
+
this.customFormatters,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const infraDirs = this.inputs.getInfrastructureReadDirs();
|
|
75
|
+
|
|
76
|
+
const gateProducers: Array<() => GateResult | Promise<GateResult>> = [
|
|
77
|
+
() =>
|
|
78
|
+
describeSkillReadGate(tcc, () => this.inputs.getActiveSkillEntries()),
|
|
79
|
+
() => describePathGate(tcc, this.inputs),
|
|
80
|
+
() => describeExternalDirectoryGate(tcc, infraDirs),
|
|
81
|
+
() => describeBashExternalDirectoryGate(tcc, bashProgram, this.inputs),
|
|
82
|
+
() => describeBashPathGate(tcc, bashProgram, this.inputs),
|
|
83
|
+
() => {
|
|
84
|
+
// Bash commands may chain several sub-commands (`a && b`, `a | b`, …);
|
|
85
|
+
// evaluate each unit from the shared parse on the bash surface and
|
|
86
|
+
// select the most restrictive, rather than matching the whole program
|
|
87
|
+
// string (#301). Other tools evaluate their single input directly.
|
|
88
|
+
const toolCheck =
|
|
89
|
+
tcc.toolName === "bash" && bashProgram
|
|
90
|
+
? resolveBashCommandCheck(
|
|
91
|
+
command ?? "",
|
|
92
|
+
bashProgram.commands(),
|
|
93
|
+
tcc.agentName ?? undefined,
|
|
94
|
+
this.inputs,
|
|
95
|
+
)
|
|
96
|
+
: this.inputs.resolve(
|
|
97
|
+
tcc.toolName,
|
|
98
|
+
tcc.input,
|
|
99
|
+
tcc.agentName ?? undefined,
|
|
100
|
+
);
|
|
101
|
+
const toolDescriptor = describeToolGate(tcc, toolCheck, formatter);
|
|
102
|
+
toolDescriptor.preCheck = toolCheck;
|
|
103
|
+
return toolDescriptor;
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
for (const produce of gateProducers) {
|
|
108
|
+
const outcome = await runner.run(
|
|
109
|
+
await produce(),
|
|
110
|
+
tcc.agentName,
|
|
111
|
+
tcc.toolCallId,
|
|
112
|
+
);
|
|
113
|
+
if (outcome.action === "block") {
|
|
114
|
+
return outcome;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { action: "allow" };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ServiceLifecycle } from "#src/service-lifecycle";
|
|
4
|
+
import type { SessionLifecycleSession } from "#src/session-lifecycle-session";
|
|
4
5
|
import { PERMISSION_SYSTEM_STATUS_KEY } from "#src/status";
|
|
5
6
|
|
|
6
7
|
/** Minimal subset of SessionStartEvent used by this handler. */
|
|
@@ -18,15 +19,14 @@ interface ResourcesDiscoverPayload {
|
|
|
18
19
|
*
|
|
19
20
|
* Constructor deps:
|
|
20
21
|
* - `session` — encapsulates all mutable session state
|
|
21
|
-
* - `
|
|
22
|
-
* (skipped for
|
|
23
|
-
*
|
|
22
|
+
* - `serviceLifecycle` — owns the process-global service publication;
|
|
23
|
+
* `activate` publishes (skipped for registered subagent children) and emits
|
|
24
|
+
* the ready event; `teardown` unsubscribes all session listeners and unpublishes
|
|
24
25
|
*/
|
|
25
26
|
export class SessionLifecycleHandler {
|
|
26
27
|
constructor(
|
|
27
|
-
private readonly session:
|
|
28
|
-
private readonly
|
|
29
|
-
private readonly cleanupRpc: () => void,
|
|
28
|
+
private readonly session: SessionLifecycleSession,
|
|
29
|
+
private readonly serviceLifecycle: ServiceLifecycle,
|
|
30
30
|
) {}
|
|
31
31
|
|
|
32
32
|
handleSessionStart(
|
|
@@ -55,7 +55,7 @@ export class SessionLifecycleHandler {
|
|
|
55
55
|
// session id) is available, so an in-process subagent child can be
|
|
56
56
|
// identified and excluded. Emitting ready here keeps the
|
|
57
57
|
// service-resolvable-when-ready ordering contract.
|
|
58
|
-
this.
|
|
58
|
+
this.serviceLifecycle.activate(ctx);
|
|
59
59
|
return Promise.resolve();
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -79,7 +79,7 @@ export class SessionLifecycleHandler {
|
|
|
79
79
|
ctx.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, undefined);
|
|
80
80
|
}
|
|
81
81
|
this.session.shutdown();
|
|
82
|
-
this.
|
|
82
|
+
this.serviceLifecycle.teardown();
|
|
83
83
|
return Promise.resolve();
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -3,40 +3,23 @@ import type {
|
|
|
3
3
|
InputEventResult,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
emitDecisionEvent,
|
|
9
|
-
type PermissionEventBus,
|
|
10
|
-
} from "#src/permission-events";
|
|
11
|
-
import { applyPermissionGate } from "#src/permission-gate";
|
|
12
|
-
import type { PromptPermissionDetails } from "#src/permission-prompter";
|
|
6
|
+
import { toRecord } from "#src/common";
|
|
7
|
+
import type { GateHandlerSession } from "#src/gate-handler-session";
|
|
13
8
|
import {
|
|
14
9
|
formatMissingToolNameReason,
|
|
15
|
-
formatSkillAskPrompt,
|
|
16
10
|
formatUnknownToolReason,
|
|
17
11
|
} from "#src/permission-prompts";
|
|
18
|
-
import type { PermissionSession } from "#src/permission-session";
|
|
19
|
-
import type { ToolInputFormatterLookup } from "#src/tool-input-formatter-registry";
|
|
20
|
-
import {
|
|
21
|
-
resolveToolPreviewLimits,
|
|
22
|
-
ToolPreviewFormatter,
|
|
23
|
-
} from "#src/tool-preview-formatter";
|
|
24
12
|
import {
|
|
25
13
|
checkRequestedToolRegistration,
|
|
26
14
|
getToolNameFromValue,
|
|
27
15
|
type ToolRegistry,
|
|
28
16
|
} from "#src/tool-registry";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
import {
|
|
35
|
-
import { describeExternalDirectoryGate } from "./gates/external-directory";
|
|
36
|
-
import { describePathGate } from "./gates/path";
|
|
37
|
-
import { runGateCheck } from "./gates/runner";
|
|
38
|
-
import { describeSkillReadGate } from "./gates/skill-read";
|
|
39
|
-
import { describeToolGate } from "./gates/tool";
|
|
17
|
+
import type { GateRunner } from "./gates/runner";
|
|
18
|
+
import type {
|
|
19
|
+
GateNotifier,
|
|
20
|
+
SkillInputGatePipeline,
|
|
21
|
+
} from "./gates/skill-input-gate-pipeline";
|
|
22
|
+
import type { ToolCallGatePipeline } from "./gates/tool-call-gate-pipeline";
|
|
40
23
|
import type { ToolCallContext } from "./gates/types";
|
|
41
24
|
|
|
42
25
|
/** Minimal subset of InputEvent used by handleInput. */
|
|
@@ -48,16 +31,19 @@ interface InputPayload {
|
|
|
48
31
|
* Handles permission gate events: tool_call and input.
|
|
49
32
|
*
|
|
50
33
|
* Constructor deps:
|
|
51
|
-
* - `session` —
|
|
52
|
-
* - `events` — event bus for emitting permissions:decision broadcasts
|
|
34
|
+
* - `session` — narrow two-method context role: bind per-event context, resolve agent name
|
|
53
35
|
* - `toolRegistry` — Pi tool API subset (getAll + setActive)
|
|
36
|
+
* - `pipeline` — owns tool-call gate-producer assembly and the run loop
|
|
37
|
+
* - `skillInputPipeline` — owns skill-input gate assembly (pre-check, notify, run)
|
|
38
|
+
* - `runner` — pre-built gate runner (constructed in the composition root)
|
|
54
39
|
*/
|
|
55
40
|
export class PermissionGateHandler {
|
|
56
41
|
constructor(
|
|
57
|
-
private readonly session:
|
|
58
|
-
private readonly events: PermissionEventBus,
|
|
42
|
+
private readonly session: GateHandlerSession,
|
|
59
43
|
private readonly toolRegistry: ToolRegistry,
|
|
60
|
-
private readonly
|
|
44
|
+
private readonly pipeline: ToolCallGatePipeline,
|
|
45
|
+
private readonly skillInputPipeline: SkillInputGatePipeline,
|
|
46
|
+
private readonly runner: GateRunner,
|
|
61
47
|
) {}
|
|
62
48
|
|
|
63
49
|
async handleToolCall(
|
|
@@ -88,138 +74,10 @@ export class PermissionGateHandler {
|
|
|
88
74
|
cwd: ctx.cwd,
|
|
89
75
|
};
|
|
90
76
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
tcc.toolName === "bash" && command
|
|
96
|
-
? await BashProgram.parse(command)
|
|
97
|
-
: null;
|
|
98
|
-
|
|
99
|
-
// ── Shared gate adapter closures ─────────────────────────────────────
|
|
100
|
-
const canConfirm = () => this.session.canPrompt(ctx);
|
|
101
|
-
const promptPermission = (details: PromptPermissionDetails) =>
|
|
102
|
-
this.session.prompt(ctx, details);
|
|
103
|
-
const emitDecision: GateRunnerDeps["emitDecision"] = (e) =>
|
|
104
|
-
emitDecisionEvent(this.events, e);
|
|
105
|
-
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger.review is a plain function closure; no this-binding issue
|
|
106
|
-
const writeReviewLog = this.session.logger.review;
|
|
107
|
-
const checkPermission: GateRunnerDeps["checkPermission"] = (
|
|
108
|
-
surface,
|
|
109
|
-
input,
|
|
110
|
-
agent,
|
|
111
|
-
sessionRules,
|
|
112
|
-
) => this.session.checkPermission(surface, input, agent, sessionRules);
|
|
113
|
-
const getSessionRuleset = () => this.session.getSessionRuleset();
|
|
114
|
-
const recordSessionApproval: GateRunnerDeps["recordSessionApproval"] = (
|
|
115
|
-
approval,
|
|
116
|
-
) => this.session.recordSessionApproval(approval);
|
|
117
|
-
|
|
118
|
-
// ── Shared runner deps (built once, reused for all gates) ────────────
|
|
119
|
-
const runnerDeps: GateRunnerDeps = {
|
|
120
|
-
checkPermission,
|
|
121
|
-
getSessionRuleset,
|
|
122
|
-
recordSessionApproval,
|
|
123
|
-
writeReviewLog,
|
|
124
|
-
emitDecision,
|
|
125
|
-
canConfirm,
|
|
126
|
-
promptPermission,
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// ── Unified gate executor ─────────────────────────────────────────────
|
|
130
|
-
// Handles the bypass log/emit branch, calls runGateCheck for descriptors,
|
|
131
|
-
// and returns a block result or undefined (allow / no-op).
|
|
132
|
-
const runGate = async (
|
|
133
|
-
gate: GateResult,
|
|
134
|
-
): Promise<{ block: true; reason: string } | undefined> => {
|
|
135
|
-
if (!gate) {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
if (isGateBypass(gate)) {
|
|
139
|
-
if (gate.log) {
|
|
140
|
-
writeReviewLog(gate.log.event, gate.log.details);
|
|
141
|
-
}
|
|
142
|
-
if (gate.decision) {
|
|
143
|
-
emitDecision(gate.decision);
|
|
144
|
-
}
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
const result = await runGateCheck(
|
|
148
|
-
gate,
|
|
149
|
-
tcc.agentName,
|
|
150
|
-
tcc.toolCallId,
|
|
151
|
-
runnerDeps,
|
|
152
|
-
);
|
|
153
|
-
return result.action === "block"
|
|
154
|
-
? { block: true, reason: result.reason }
|
|
155
|
-
: undefined;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const formatter = new ToolPreviewFormatter(
|
|
159
|
-
resolveToolPreviewLimits(this.session.config),
|
|
160
|
-
this.customFormatters,
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
// ── Ordered gate pipeline ─────────────────────────────────────────────
|
|
164
|
-
// infraDirs is computed once, outside the pipeline, exactly as before.
|
|
165
|
-
const infraDirs = [
|
|
166
|
-
...this.session.getInfrastructureDirs(),
|
|
167
|
-
...this.session.getInfrastructureReadPaths(),
|
|
168
|
-
];
|
|
169
|
-
|
|
170
|
-
const gateProducers: Array<() => GateResult | Promise<GateResult>> = [
|
|
171
|
-
() =>
|
|
172
|
-
describeSkillReadGate(tcc, () => this.session.getActiveSkillEntries()),
|
|
173
|
-
() => describePathGate(tcc, checkPermission, getSessionRuleset),
|
|
174
|
-
() => describeExternalDirectoryGate(tcc, infraDirs),
|
|
175
|
-
() =>
|
|
176
|
-
describeBashExternalDirectoryGate(
|
|
177
|
-
tcc,
|
|
178
|
-
bashProgram,
|
|
179
|
-
checkPermission,
|
|
180
|
-
getSessionRuleset,
|
|
181
|
-
),
|
|
182
|
-
() =>
|
|
183
|
-
describeBashPathGate(
|
|
184
|
-
tcc,
|
|
185
|
-
bashProgram,
|
|
186
|
-
checkPermission,
|
|
187
|
-
getSessionRuleset,
|
|
188
|
-
),
|
|
189
|
-
() => {
|
|
190
|
-
// Bash commands may chain several sub-commands (`a && b`, `a | b`, …);
|
|
191
|
-
// evaluate each unit from the shared parse on the bash surface and
|
|
192
|
-
// select the most restrictive, rather than matching the whole program
|
|
193
|
-
// string (#301). Other tools evaluate their single input directly.
|
|
194
|
-
const toolCheck =
|
|
195
|
-
tcc.toolName === "bash" && bashProgram
|
|
196
|
-
? resolveBashCommandCheck(
|
|
197
|
-
command ?? "",
|
|
198
|
-
bashProgram.commands(),
|
|
199
|
-
tcc.agentName ?? undefined,
|
|
200
|
-
getSessionRuleset(),
|
|
201
|
-
checkPermission,
|
|
202
|
-
)
|
|
203
|
-
: checkPermission(
|
|
204
|
-
tcc.toolName,
|
|
205
|
-
tcc.input,
|
|
206
|
-
tcc.agentName ?? undefined,
|
|
207
|
-
getSessionRuleset(),
|
|
208
|
-
);
|
|
209
|
-
const toolDescriptor = describeToolGate(tcc, toolCheck, formatter);
|
|
210
|
-
toolDescriptor.preCheck = toolCheck;
|
|
211
|
-
return toolDescriptor;
|
|
212
|
-
},
|
|
213
|
-
];
|
|
214
|
-
|
|
215
|
-
for (const produce of gateProducers) {
|
|
216
|
-
const blocked = await runGate(await produce());
|
|
217
|
-
if (blocked) {
|
|
218
|
-
return blocked;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return {};
|
|
77
|
+
const outcome = await this.pipeline.evaluate(tcc, this.runner);
|
|
78
|
+
return outcome.action === "block"
|
|
79
|
+
? { block: true, reason: outcome.reason }
|
|
80
|
+
: {};
|
|
223
81
|
}
|
|
224
82
|
|
|
225
83
|
async handleInput(
|
|
@@ -234,84 +92,22 @@ export class PermissionGateHandler {
|
|
|
234
92
|
}
|
|
235
93
|
|
|
236
94
|
const agentName = this.session.resolveAgentName(ctx);
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
? `Skill '${skillName}' is not permitted for agent '${agentName}'.`
|
|
246
|
-
: `Skill '${skillName}' is not permitted by the current skill policy.`;
|
|
247
|
-
ctx.ui.notify(notifyMessage, "warning");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const skillInputMessage = formatSkillAskPrompt(
|
|
95
|
+
const notifier: GateNotifier = {
|
|
96
|
+
warn: (message) => {
|
|
97
|
+
if (ctx.hasUI) {
|
|
98
|
+
ctx.ui.notify(message, "warning");
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const outcome = await this.skillInputPipeline.evaluate(
|
|
251
103
|
skillName,
|
|
252
|
-
agentName
|
|
104
|
+
agentName,
|
|
105
|
+
notifier,
|
|
106
|
+
this.runner,
|
|
253
107
|
);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
state: check.state,
|
|
258
|
-
canConfirm: skillInputCanConfirm,
|
|
259
|
-
promptForApproval: async () => {
|
|
260
|
-
const decision = await this.session.prompt(ctx, {
|
|
261
|
-
requestId: this.session.createPermissionRequestId("skill-input"),
|
|
262
|
-
source: "skill_input",
|
|
263
|
-
agentName,
|
|
264
|
-
message: skillInputMessage,
|
|
265
|
-
skillName,
|
|
266
|
-
});
|
|
267
|
-
skillInputAutoApproved = decision.autoApproved === true;
|
|
268
|
-
return decision;
|
|
269
|
-
},
|
|
270
|
-
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger.review is a plain function closure; no this-binding issue
|
|
271
|
-
writeLog: this.session.logger.review,
|
|
272
|
-
logContext: {
|
|
273
|
-
source: "skill_input",
|
|
274
|
-
skillName,
|
|
275
|
-
agentName,
|
|
276
|
-
message: skillInputMessage,
|
|
277
|
-
},
|
|
278
|
-
messages: {
|
|
279
|
-
denyReason: skillInputMessage,
|
|
280
|
-
unavailableReason:
|
|
281
|
-
"Skill requires approval, but no interactive UI is available.",
|
|
282
|
-
userDeniedReason: () => "User denied skill.",
|
|
283
|
-
},
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
emitDecisionEvent(this.events, {
|
|
287
|
-
surface: "skill",
|
|
288
|
-
value: skillName,
|
|
289
|
-
result: skillInputGate.action === "allow" ? "allow" : "deny",
|
|
290
|
-
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- defensive fallback; TypeScript narrows check.state before the ternary's else branch */
|
|
291
|
-
resolution:
|
|
292
|
-
check.state === "allow"
|
|
293
|
-
? "policy_allow"
|
|
294
|
-
: check.state === "deny"
|
|
295
|
-
? "policy_deny"
|
|
296
|
-
: skillInputGate.action === "allow"
|
|
297
|
-
? skillInputAutoApproved
|
|
298
|
-
? "auto_approved"
|
|
299
|
-
: "user_approved"
|
|
300
|
-
: skillInputCanConfirm
|
|
301
|
-
? "user_denied"
|
|
302
|
-
: "confirmation_unavailable",
|
|
303
|
-
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
304
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- ?? null normalises undefined to null for the log record
|
|
305
|
-
origin: check.origin ?? null,
|
|
306
|
-
agentName: agentName ?? null,
|
|
307
|
-
matchedPattern: check.matchedPattern ?? null,
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
if (skillInputGate.action === "block") {
|
|
311
|
-
return { action: "handled" };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return { action: "continue" };
|
|
108
|
+
return outcome.action === "block"
|
|
109
|
+
? { action: "handled" }
|
|
110
|
+
: { action: "continue" };
|
|
315
111
|
}
|
|
316
112
|
}
|
|
317
113
|
|
package/src/index.ts
CHANGED
|
@@ -1,39 +1,35 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
2
|
import { registerBuiltinToolInputFormatters } from "./builtin-tool-input-formatters";
|
|
6
3
|
import { registerPermissionSystemCommand } from "./config-modal";
|
|
7
4
|
import { getGlobalConfigPath } from "./config-paths";
|
|
8
|
-
import
|
|
5
|
+
import { GateDecisionReporter } from "./decision-reporter";
|
|
6
|
+
import {
|
|
7
|
+
PermissionForwarder,
|
|
8
|
+
type PermissionForwarderDeps,
|
|
9
|
+
} from "./forwarded-permissions/permission-forwarder";
|
|
9
10
|
import { ForwardingManager } from "./forwarding-manager";
|
|
10
11
|
import {
|
|
11
12
|
AgentPrepHandler,
|
|
12
13
|
PermissionGateHandler,
|
|
13
14
|
SessionLifecycleHandler,
|
|
14
15
|
} from "./handlers";
|
|
15
|
-
import {
|
|
16
|
+
import { GateRunner } from "./handlers/gates/runner";
|
|
17
|
+
import { SkillInputGatePipeline } from "./handlers/gates/skill-input-gate-pipeline";
|
|
18
|
+
import { ToolCallGatePipeline } from "./handlers/gates/tool-call-gate-pipeline";
|
|
16
19
|
import { requestPermissionDecisionFromUi } from "./permission-dialog";
|
|
17
20
|
import { registerPermissionRpcHandlers } from "./permission-event-rpc";
|
|
18
|
-
import { emitReadyEvent } from "./permission-events";
|
|
19
21
|
import { PermissionPrompter } from "./permission-prompter";
|
|
20
22
|
import { PermissionSession } from "./permission-session";
|
|
23
|
+
import { LocalPermissionsService } from "./permissions-service";
|
|
21
24
|
import {
|
|
22
25
|
createExtensionRuntime,
|
|
23
26
|
logResolvedConfigPaths,
|
|
24
27
|
refreshExtensionConfig,
|
|
25
28
|
saveExtensionConfig,
|
|
26
29
|
} from "./runtime";
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
29
|
-
publishPermissionsService,
|
|
30
|
-
unpublishPermissionsService,
|
|
31
|
-
} from "./service";
|
|
30
|
+
import { PermissionServiceLifecycle } from "./service-lifecycle";
|
|
32
31
|
import { createSessionLogger } from "./session-logger";
|
|
33
|
-
import {
|
|
34
|
-
isRegisteredSubagentChild,
|
|
35
|
-
isSubagentExecutionContext,
|
|
36
|
-
} from "./subagent-context";
|
|
32
|
+
import { isSubagentExecutionContext } from "./subagent-context";
|
|
37
33
|
import { subscribeSubagentLifecycle } from "./subagent-lifecycle-events";
|
|
38
34
|
import { getSubagentSessionRegistry } from "./subagent-registry";
|
|
39
35
|
import { ToolInputFormatterRegistry } from "./tool-input-formatter-registry";
|
|
@@ -48,17 +44,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
48
44
|
const formatterRegistry = new ToolInputFormatterRegistry();
|
|
49
45
|
registerBuiltinToolInputFormatters(formatterRegistry);
|
|
50
46
|
|
|
51
|
-
const
|
|
52
|
-
getConfig: () => runtime.config,
|
|
53
|
-
writeReviewLog: runtime.writeReviewLog.bind(runtime),
|
|
54
|
-
subagentSessionsDir: runtime.subagentSessionsDir,
|
|
55
|
-
forwardingDir: runtime.forwardingDir,
|
|
56
|
-
registry: subagentRegistry,
|
|
57
|
-
events: pi.events,
|
|
58
|
-
requestPermissionDecisionFromUi,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const forwardingDeps: PermissionForwardingDeps = {
|
|
47
|
+
const forwardingDeps: PermissionForwarderDeps = {
|
|
62
48
|
forwardingDir: runtime.forwardingDir,
|
|
63
49
|
subagentSessionsDir: runtime.subagentSessionsDir,
|
|
64
50
|
registry: subagentRegistry,
|
|
@@ -72,6 +58,14 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
72
58
|
shouldAutoApprove: () =>
|
|
73
59
|
shouldAutoApprovePermissionState("ask", runtime.config),
|
|
74
60
|
};
|
|
61
|
+
const forwarder = new PermissionForwarder(forwardingDeps);
|
|
62
|
+
|
|
63
|
+
const prompter = new PermissionPrompter({
|
|
64
|
+
getConfig: () => runtime.config,
|
|
65
|
+
writeReviewLog: runtime.writeReviewLog.bind(runtime),
|
|
66
|
+
events: pi.events,
|
|
67
|
+
forwarder,
|
|
68
|
+
});
|
|
75
69
|
|
|
76
70
|
refreshExtensionConfig(runtime);
|
|
77
71
|
|
|
@@ -80,7 +74,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
80
74
|
createSessionLogger(runtime),
|
|
81
75
|
new ForwardingManager(
|
|
82
76
|
runtime.subagentSessionsDir,
|
|
83
|
-
|
|
77
|
+
forwarder,
|
|
84
78
|
subagentRegistry,
|
|
85
79
|
),
|
|
86
80
|
{
|
|
@@ -119,36 +113,11 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
119
113
|
writeReviewLog: runtime.writeReviewLog.bind(runtime),
|
|
120
114
|
});
|
|
121
115
|
|
|
122
|
-
const permissionsService
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
surface,
|
|
128
|
-
input,
|
|
129
|
-
agentName,
|
|
130
|
-
sessionRules,
|
|
131
|
-
);
|
|
132
|
-
},
|
|
133
|
-
getToolPermission(toolName, agentName) {
|
|
134
|
-
return runtime.permissionManager.getToolPermission(toolName, agentName);
|
|
135
|
-
},
|
|
136
|
-
registerToolInputFormatter(toolName, formatter) {
|
|
137
|
-
return formatterRegistry.register(toolName, formatter);
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
// Publish the service to the process-global slot only when this instance is
|
|
142
|
-
// not an in-process subagent child, then emit ready. Deferred to
|
|
143
|
-
// session_start (vs. factory init) because identifying a child requires the
|
|
144
|
-
// session id from ctx, which the factory body does not have. A registered
|
|
145
|
-
// child therefore never clobbers the parent's published service. See #302.
|
|
146
|
-
const activateServiceForSession = (ctx: ExtensionContext): void => {
|
|
147
|
-
if (!isRegisteredSubagentChild(ctx, subagentRegistry)) {
|
|
148
|
-
publishPermissionsService(permissionsService);
|
|
149
|
-
}
|
|
150
|
-
emitReadyEvent(pi.events);
|
|
151
|
-
};
|
|
116
|
+
const permissionsService = new LocalPermissionsService(
|
|
117
|
+
runtime.permissionManager,
|
|
118
|
+
runtime.sessionRules,
|
|
119
|
+
formatterRegistry,
|
|
120
|
+
);
|
|
152
121
|
|
|
153
122
|
// Subscribe to @gotgenes/pi-subagents' child lifecycle events so child
|
|
154
123
|
// sessions register/unregister without the core calling us (ADR 0002).
|
|
@@ -157,27 +126,38 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
157
126
|
subagentRegistry,
|
|
158
127
|
);
|
|
159
128
|
|
|
129
|
+
// PermissionServiceLifecycle owns the process-global service publication:
|
|
130
|
+
// activate() publishes (skipped for registered subagent children — see #302)
|
|
131
|
+
// and emits ready; teardown() unsubscribes all session listeners and
|
|
132
|
+
// unpublishes. Deferred to session_start because identifying a child
|
|
133
|
+
// requires the session id from ctx, unavailable at factory-init time.
|
|
134
|
+
const serviceLifecycle = new PermissionServiceLifecycle(
|
|
135
|
+
permissionsService,
|
|
136
|
+
subagentRegistry,
|
|
137
|
+
pi.events,
|
|
138
|
+
[rpcHandles.unsubCheck, rpcHandles.unsubPrompt, unsubSubagentLifecycle],
|
|
139
|
+
);
|
|
140
|
+
|
|
160
141
|
const toolRegistry = {
|
|
161
142
|
getAll: () => pi.getAllTools(),
|
|
162
143
|
setActive: (names: string[]) => pi.setActiveTools(names),
|
|
163
144
|
};
|
|
164
145
|
|
|
165
|
-
const lifecycle = new SessionLifecycleHandler(
|
|
146
|
+
const lifecycle = new SessionLifecycleHandler(session, serviceLifecycle);
|
|
147
|
+
const agentPrep = new AgentPrepHandler(session, toolRegistry);
|
|
148
|
+
const reporter = new GateDecisionReporter(session.logger, pi.events);
|
|
149
|
+
const gateRunner = new GateRunner(session, session, session, reporter);
|
|
150
|
+
const toolCallGatePipeline = new ToolCallGatePipeline(
|
|
166
151
|
session,
|
|
167
|
-
|
|
168
|
-
() => {
|
|
169
|
-
rpcHandles.unsubCheck();
|
|
170
|
-
rpcHandles.unsubPrompt();
|
|
171
|
-
unsubSubagentLifecycle();
|
|
172
|
-
unpublishPermissionsService(permissionsService);
|
|
173
|
-
},
|
|
152
|
+
formatterRegistry,
|
|
174
153
|
);
|
|
175
|
-
const
|
|
154
|
+
const skillInputGatePipeline = new SkillInputGatePipeline(session);
|
|
176
155
|
const gates = new PermissionGateHandler(
|
|
177
156
|
session,
|
|
178
|
-
pi.events,
|
|
179
157
|
toolRegistry,
|
|
180
|
-
|
|
158
|
+
toolCallGatePipeline,
|
|
159
|
+
skillInputGatePipeline,
|
|
160
|
+
gateRunner,
|
|
181
161
|
);
|
|
182
162
|
|
|
183
163
|
pi.on("session_start", (event, ctx) =>
|