@gotgenes/pi-permission-system 10.4.0 → 10.5.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 +12 -0
- package/package.json +1 -1
- package/src/handlers/gates/bash-command.ts +2 -2
- package/src/handlers/gates/bash-external-directory.ts +2 -2
- package/src/handlers/gates/bash-path.ts +2 -2
- package/src/handlers/gates/path.ts +2 -2
- package/src/handlers/gates/runner.ts +2 -2
- package/src/handlers/gates/tool-call-gate-pipeline.ts +10 -9
- package/src/index.ts +6 -2
- package/src/permission-resolver.ts +69 -2
- package/src/permission-session.ts +0 -20
- package/test/handlers/external-directory-session-dedup.test.ts +13 -13
- package/test/handlers/gates/bash-external-directory.test.ts +2 -2
- package/test/handlers/gates/bash-path.test.ts +2 -2
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +30 -21
- package/test/helpers/gate-fixtures.ts +5 -9
- package/test/helpers/handler-fixtures.ts +13 -17
- package/test/permission-resolver.test.ts +194 -0
- package/test/permission-session.test.ts +0 -68
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [10.5.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.4.0...pi-permission-system-v10.5.0) (2026-06-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add PermissionResolver class and route gate runner through it ([#340](https://github.com/gotgenes/pi-packages/issues/340)) ([4133601](https://github.com/gotgenes/pi-packages/commit/41336018d495f85b30b7b77fadb5912870f0dedd))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* suppress fallow unused-class-member for pre-Step-8 resolver methods ([#340](https://github.com/gotgenes/pi-packages/issues/340)) ([fd65626](https://github.com/gotgenes/pi-packages/commit/fd65626ae867457edeb829ea28d0ab94fe51dea6))
|
|
19
|
+
|
|
8
20
|
## [10.4.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.3.1...pi-permission-system-v10.4.0) (2026-06-07)
|
|
9
21
|
|
|
10
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BashCommand } from "#src/handlers/gates/bash-program";
|
|
2
2
|
import { pickMostRestrictive } from "#src/handlers/gates/candidate-check";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
4
|
import type { PermissionCheckResult } from "#src/types";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -30,7 +30,7 @@ export function resolveBashCommandCheck(
|
|
|
30
30
|
command: string,
|
|
31
31
|
commands: BashCommand[],
|
|
32
32
|
agentName: string | undefined,
|
|
33
|
-
resolver:
|
|
33
|
+
resolver: ScopedPermissionResolver,
|
|
34
34
|
): PermissionCheckResult {
|
|
35
35
|
const results = commands.map((cmd) => {
|
|
36
36
|
const result = resolver.resolve("bash", { command: cmd.text }, agentName);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getNonEmptyString, toRecord } from "#src/common";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
3
3
|
import { SessionApproval } from "#src/session-approval";
|
|
4
4
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
5
5
|
import type { PermissionCheckResult } from "#src/types";
|
|
@@ -21,7 +21,7 @@ import type { ToolCallContext } from "./types";
|
|
|
21
21
|
export function describeBashExternalDirectoryGate(
|
|
22
22
|
tcc: ToolCallContext,
|
|
23
23
|
bashProgram: BashProgram | null,
|
|
24
|
-
resolver:
|
|
24
|
+
resolver: ScopedPermissionResolver,
|
|
25
25
|
): GateResult {
|
|
26
26
|
if (tcc.toolName !== "bash" || !tcc.cwd) return null;
|
|
27
27
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getNonEmptyString, toRecord } from "#src/common";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
3
3
|
import { SessionApproval } from "#src/session-approval";
|
|
4
4
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
5
5
|
import type { PermissionCheckResult } from "#src/types";
|
|
@@ -25,7 +25,7 @@ import type { ToolCallContext } from "./types";
|
|
|
25
25
|
export function describeBashPathGate(
|
|
26
26
|
tcc: ToolCallContext,
|
|
27
27
|
bashProgram: BashProgram | null,
|
|
28
|
-
resolver:
|
|
28
|
+
resolver: ScopedPermissionResolver,
|
|
29
29
|
): GateResult {
|
|
30
30
|
if (tcc.toolName !== "bash") return null;
|
|
31
31
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getPathBearingToolPath } from "#src/path-utils";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
3
3
|
import { SessionApproval } from "#src/session-approval";
|
|
4
4
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
5
5
|
import type { GateDescriptor, GateResult } from "./descriptor";
|
|
@@ -15,7 +15,7 @@ import type { ToolCallContext } from "./types";
|
|
|
15
15
|
*/
|
|
16
16
|
export function describePathGate(
|
|
17
17
|
tcc: ToolCallContext,
|
|
18
|
-
resolver:
|
|
18
|
+
resolver: ScopedPermissionResolver,
|
|
19
19
|
): GateResult {
|
|
20
20
|
const filePath = getPathBearingToolPath(tcc.toolName, tcc.input);
|
|
21
21
|
if (!filePath) return null;
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import type { GatePrompter } from "#src/gate-prompter";
|
|
8
8
|
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
9
9
|
import { applyPermissionGate } from "#src/permission-gate";
|
|
10
|
-
import type {
|
|
10
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
11
11
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
12
12
|
import type { PermissionCheckResult } from "#src/types";
|
|
13
13
|
import type { GateDescriptor, GateResult } from "./descriptor";
|
|
@@ -28,7 +28,7 @@ import type { GateOutcome } from "./types";
|
|
|
28
28
|
*/
|
|
29
29
|
export class GateRunner {
|
|
30
30
|
constructor(
|
|
31
|
-
private readonly resolver:
|
|
31
|
+
private readonly resolver: ScopedPermissionResolver,
|
|
32
32
|
private readonly recorder: SessionApprovalRecorder,
|
|
33
33
|
private readonly prompter: GatePrompter,
|
|
34
34
|
private readonly reporter: DecisionReporter,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getNonEmptyString, toRecord } from "#src/common";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
3
3
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
4
4
|
import type { ToolInputFormatterLookup } from "#src/tool-input-formatter-registry";
|
|
5
5
|
import {
|
|
@@ -21,14 +21,14 @@ import type { GateOutcome, ToolCallContext } from "./types";
|
|
|
21
21
|
/**
|
|
22
22
|
* Narrow interface the pipeline needs from its session-side dependency.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* The three query methods needed to assemble gate inputs.
|
|
25
|
+
* The resolver is injected separately as a constructor parameter.
|
|
26
26
|
*
|
|
27
27
|
* `PermissionSession` satisfies this structurally at the construction call
|
|
28
28
|
* site; no `implements` clause is needed and would create a layer-inversion
|
|
29
29
|
* import from the domain module into the handler layer.
|
|
30
30
|
*/
|
|
31
|
-
export interface ToolCallGateInputs
|
|
31
|
+
export interface ToolCallGateInputs {
|
|
32
32
|
/** Active skill prompt entries for the skill-read gate. */
|
|
33
33
|
getActiveSkillEntries(): SkillPromptEntry[];
|
|
34
34
|
/** Combined infrastructure read directories (static + config-derived). */
|
|
@@ -50,6 +50,7 @@ export interface ToolCallGateInputs extends PermissionResolver {
|
|
|
50
50
|
*/
|
|
51
51
|
export class ToolCallGatePipeline {
|
|
52
52
|
constructor(
|
|
53
|
+
private readonly resolver: ScopedPermissionResolver,
|
|
53
54
|
private readonly inputs: ToolCallGateInputs,
|
|
54
55
|
private readonly customFormatters?: ToolInputFormatterLookup,
|
|
55
56
|
) {}
|
|
@@ -76,10 +77,10 @@ export class ToolCallGatePipeline {
|
|
|
76
77
|
const gateProducers: Array<() => GateResult | Promise<GateResult>> = [
|
|
77
78
|
() =>
|
|
78
79
|
describeSkillReadGate(tcc, () => this.inputs.getActiveSkillEntries()),
|
|
79
|
-
() => describePathGate(tcc, this.
|
|
80
|
+
() => describePathGate(tcc, this.resolver),
|
|
80
81
|
() => describeExternalDirectoryGate(tcc, infraDirs),
|
|
81
|
-
() => describeBashExternalDirectoryGate(tcc, bashProgram, this.
|
|
82
|
-
() => describeBashPathGate(tcc, bashProgram, this.
|
|
82
|
+
() => describeBashExternalDirectoryGate(tcc, bashProgram, this.resolver),
|
|
83
|
+
() => describeBashPathGate(tcc, bashProgram, this.resolver),
|
|
83
84
|
() => {
|
|
84
85
|
// Bash commands may chain several sub-commands (`a && b`, `a | b`, …);
|
|
85
86
|
// evaluate each unit from the shared parse on the bash surface and
|
|
@@ -91,9 +92,9 @@ export class ToolCallGatePipeline {
|
|
|
91
92
|
command ?? "",
|
|
92
93
|
bashProgram.commands(),
|
|
93
94
|
tcc.agentName ?? undefined,
|
|
94
|
-
this.
|
|
95
|
+
this.resolver,
|
|
95
96
|
)
|
|
96
|
-
: this.
|
|
97
|
+
: this.resolver.resolve(
|
|
97
98
|
tcc.toolName,
|
|
98
99
|
tcc.input,
|
|
99
100
|
tcc.agentName ?? undefined,
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { requestPermissionDecisionFromUi } from "./permission-dialog";
|
|
|
23
23
|
import { registerPermissionRpcHandlers } from "./permission-event-rpc";
|
|
24
24
|
import { PermissionManager } from "./permission-manager";
|
|
25
25
|
import { PermissionPrompter } from "./permission-prompter";
|
|
26
|
+
import { PermissionResolver } from "./permission-resolver";
|
|
26
27
|
import { PermissionSession } from "./permission-session";
|
|
27
28
|
import { LocalPermissionsService } from "./permissions-service";
|
|
28
29
|
import { PromptingGateway } from "./prompting-gateway";
|
|
@@ -158,13 +159,16 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
158
159
|
|
|
159
160
|
const lifecycle = new SessionLifecycleHandler(session, serviceLifecycle);
|
|
160
161
|
const agentPrep = new AgentPrepHandler(session, toolRegistry);
|
|
162
|
+
const resolver = new PermissionResolver(permissionManager, sessionRules);
|
|
163
|
+
|
|
161
164
|
const reporter = new GateDecisionReporter(session.logger, pi.events);
|
|
162
|
-
const gateRunner = new GateRunner(
|
|
165
|
+
const gateRunner = new GateRunner(resolver, session, gateway, reporter);
|
|
163
166
|
const toolCallGatePipeline = new ToolCallGatePipeline(
|
|
167
|
+
resolver,
|
|
164
168
|
session,
|
|
165
169
|
formatterRegistry,
|
|
166
170
|
);
|
|
167
|
-
const skillInputGatePipeline = new SkillInputGatePipeline(
|
|
171
|
+
const skillInputGatePipeline = new SkillInputGatePipeline(resolver);
|
|
168
172
|
const gates = new PermissionGateHandler(
|
|
169
173
|
session,
|
|
170
174
|
toolRegistry,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ScopedPermissionManager } from "./permission-manager";
|
|
2
|
+
import type { Rule } from "./rule";
|
|
3
|
+
import type { SessionRules } from "./session-rules";
|
|
4
|
+
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Resolves the effective permission for a surface/input, applying the current
|
|
@@ -8,10 +11,74 @@ import type { PermissionCheckResult } from "./types";
|
|
|
8
11
|
* previously threaded by hand: the ruleset was only ever fetched to be passed
|
|
9
12
|
* straight back into `checkPermission`, so the two are one operation.
|
|
10
13
|
*/
|
|
11
|
-
export interface
|
|
14
|
+
export interface ScopedPermissionResolver {
|
|
12
15
|
resolve(
|
|
13
16
|
surface: string,
|
|
14
17
|
input: unknown,
|
|
15
18
|
agentName?: string,
|
|
16
19
|
): PermissionCheckResult;
|
|
17
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Concrete collaborator that owns the resolution surface.
|
|
24
|
+
*
|
|
25
|
+
* Holds a `ScopedPermissionManager` and a `SessionRules` store, composing
|
|
26
|
+
* them so callers never thread the session ruleset by hand.
|
|
27
|
+
*
|
|
28
|
+
* Constructor deps:
|
|
29
|
+
* - `permissionManager` — the narrow session-scoped permission-checking interface
|
|
30
|
+
* - `sessionRules` — narrowed to `getRuleset` (ISP: the resolver only reads, never records)
|
|
31
|
+
*/
|
|
32
|
+
export class PermissionResolver implements ScopedPermissionResolver {
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly permissionManager: ScopedPermissionManager,
|
|
35
|
+
private readonly sessionRules: Pick<SessionRules, "getRuleset">,
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the effective permission for a surface/input, applying the current
|
|
40
|
+
* session rules. Composes `checkPermission` with `getRuleset()` so callers
|
|
41
|
+
* never thread the ruleset by hand.
|
|
42
|
+
*/
|
|
43
|
+
resolve(
|
|
44
|
+
surface: string,
|
|
45
|
+
input: unknown,
|
|
46
|
+
agentName?: string,
|
|
47
|
+
): PermissionCheckResult {
|
|
48
|
+
return this.checkPermission(
|
|
49
|
+
surface,
|
|
50
|
+
input,
|
|
51
|
+
agentName,
|
|
52
|
+
this.sessionRules.getRuleset(),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
checkPermission(
|
|
57
|
+
surface: string,
|
|
58
|
+
input: unknown,
|
|
59
|
+
agentName?: string,
|
|
60
|
+
sessionRules?: Rule[],
|
|
61
|
+
): PermissionCheckResult {
|
|
62
|
+
return this.permissionManager.checkPermission(
|
|
63
|
+
surface,
|
|
64
|
+
input,
|
|
65
|
+
agentName,
|
|
66
|
+
sessionRules,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// fallow-ignore-next-line unused-class-member
|
|
71
|
+
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
72
|
+
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// fallow-ignore-next-line unused-class-member
|
|
76
|
+
getConfigIssues(agentName?: string): string[] {
|
|
77
|
+
return this.permissionManager.getConfigIssues(agentName);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// fallow-ignore-next-line unused-class-member
|
|
81
|
+
getPolicyCacheStamp(agentName?: string): string {
|
|
82
|
+
return this.permissionManager.getPolicyCacheStamp(agentName);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -11,7 +11,6 @@ import type { ExtensionPaths } from "./extension-paths";
|
|
|
11
11
|
import type { ForwardingController } from "./forwarding-manager";
|
|
12
12
|
import type { GateHandlerSession } from "./gate-handler-session";
|
|
13
13
|
import type { ScopedPermissionManager } from "./permission-manager";
|
|
14
|
-
import type { PermissionResolver } from "./permission-resolver";
|
|
15
14
|
import type { PromptingGatewayLifecycle } from "./prompting-gateway";
|
|
16
15
|
import type { Rule } from "./rule";
|
|
17
16
|
import type { SessionApproval } from "./session-approval";
|
|
@@ -43,7 +42,6 @@ import type { PermissionCheckResult, PermissionState } from "./types";
|
|
|
43
42
|
*/
|
|
44
43
|
export class PermissionSession
|
|
45
44
|
implements
|
|
46
|
-
PermissionResolver,
|
|
47
45
|
SessionApprovalRecorder,
|
|
48
46
|
GateHandlerSession,
|
|
49
47
|
AgentPrepSession,
|
|
@@ -102,24 +100,6 @@ export class PermissionSession
|
|
|
102
100
|
);
|
|
103
101
|
}
|
|
104
102
|
|
|
105
|
-
/**
|
|
106
|
-
* Resolve the effective permission for a surface/input, applying the current
|
|
107
|
-
* session rules. Composes `checkPermission` with `getSessionRuleset` so
|
|
108
|
-
* callers never thread the ruleset by hand.
|
|
109
|
-
*/
|
|
110
|
-
resolve(
|
|
111
|
-
surface: string,
|
|
112
|
-
input: unknown,
|
|
113
|
-
agentName?: string,
|
|
114
|
-
): PermissionCheckResult {
|
|
115
|
-
return this.checkPermission(
|
|
116
|
-
surface,
|
|
117
|
-
input,
|
|
118
|
-
agentName,
|
|
119
|
-
this.getSessionRuleset(),
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
103
|
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
124
104
|
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
125
105
|
}
|
|
@@ -153,17 +153,6 @@ function makeStatefulSession(
|
|
|
153
153
|
vi
|
|
154
154
|
.fn<MockGateHandlerSession["getToolPreviewLimits"]>()
|
|
155
155
|
.mockReturnValue(resolveToolPreviewLimits(DEFAULT_EXTENSION_CONFIG)),
|
|
156
|
-
// Resolve delegation — closure reads `session` at call time so overrides win.
|
|
157
|
-
resolve:
|
|
158
|
-
overrides.resolve ??
|
|
159
|
-
vi.fn<MockGateHandlerSession["resolve"]>((surface, input, agentName) =>
|
|
160
|
-
session.checkPermission(
|
|
161
|
-
surface,
|
|
162
|
-
input,
|
|
163
|
-
agentName,
|
|
164
|
-
session.getSessionRuleset(),
|
|
165
|
-
),
|
|
166
|
-
),
|
|
167
156
|
};
|
|
168
157
|
return session;
|
|
169
158
|
}
|
|
@@ -180,11 +169,22 @@ function makeHandlerForSession(
|
|
|
180
169
|
.fn<GatePrompter["prompt"]>()
|
|
181
170
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
182
171
|
};
|
|
183
|
-
|
|
172
|
+
// Resolver delegates to session's checkPermission + getSessionRuleset so
|
|
173
|
+
// stateful approval tracking steers resolve automatically.
|
|
174
|
+
const resolver = {
|
|
175
|
+
resolve: (surface: string, input: unknown, agentName?: string) =>
|
|
176
|
+
session.checkPermission(
|
|
177
|
+
surface,
|
|
178
|
+
input,
|
|
179
|
+
agentName,
|
|
180
|
+
session.getSessionRuleset(),
|
|
181
|
+
),
|
|
182
|
+
};
|
|
183
|
+
const runner = new GateRunner(resolver, session, resolvedPrompter, reporter);
|
|
184
184
|
const handler = new PermissionGateHandler(
|
|
185
185
|
session,
|
|
186
186
|
makeToolRegistry(),
|
|
187
|
-
new ToolCallGatePipeline(session),
|
|
187
|
+
new ToolCallGatePipeline(resolver, session),
|
|
188
188
|
new SkillInputGatePipeline(session),
|
|
189
189
|
runner,
|
|
190
190
|
);
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
} from "#src/handlers/gates/descriptor";
|
|
10
10
|
import { isGateBypass, isGateDescriptor } from "#src/handlers/gates/descriptor";
|
|
11
11
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
12
|
-
import type {
|
|
12
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
13
13
|
import type { PermissionCheckResult } from "#src/types";
|
|
14
14
|
|
|
15
15
|
import { makeResolver } from "#test/helpers/gate-fixtures";
|
|
@@ -47,7 +47,7 @@ function makeCheckResult(
|
|
|
47
47
|
*/
|
|
48
48
|
async function describeGate(
|
|
49
49
|
tcc: ToolCallContext,
|
|
50
|
-
resolver:
|
|
50
|
+
resolver: ScopedPermissionResolver,
|
|
51
51
|
): Promise<GateResult> {
|
|
52
52
|
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
53
53
|
const bashProgram =
|
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
} from "#src/handlers/gates/descriptor";
|
|
20
20
|
import { isGateBypass, isGateDescriptor } from "#src/handlers/gates/descriptor";
|
|
21
21
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
22
|
-
import type {
|
|
22
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
25
|
makeGateCheckResult as makeCheckResult,
|
|
@@ -39,7 +39,7 @@ afterEach(() => {
|
|
|
39
39
|
*/
|
|
40
40
|
async function describeGate(
|
|
41
41
|
tcc: ToolCallContext,
|
|
42
|
-
resolver:
|
|
42
|
+
resolver: ScopedPermissionResolver,
|
|
43
43
|
): Promise<GateResult> {
|
|
44
44
|
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
45
45
|
const bashProgram =
|
|
@@ -40,9 +40,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
40
40
|
|
|
41
41
|
describe("evaluate — non-bash tool", () => {
|
|
42
42
|
it("returns allow when all gates pass", async () => {
|
|
43
|
+
const resolver = makeResolver(makeCheckResult());
|
|
43
44
|
const inputs = makeGateInputs();
|
|
44
|
-
const { runner } = makeGateRunner(
|
|
45
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
45
|
+
const { runner } = makeGateRunner();
|
|
46
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
46
47
|
|
|
47
48
|
const result = await pipeline.evaluate(
|
|
48
49
|
makeTcc({ toolName: "read", input: {} }),
|
|
@@ -53,12 +54,12 @@ describe("ToolCallGatePipeline", () => {
|
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
it("returns block when the tool gate denies", async () => {
|
|
56
|
-
const
|
|
57
|
+
const resolver = makeResolver(
|
|
57
58
|
makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
58
59
|
);
|
|
59
|
-
const inputs = makeGateInputs(
|
|
60
|
-
const { runner } = makeGateRunner(
|
|
61
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
60
|
+
const inputs = makeGateInputs();
|
|
61
|
+
const { runner } = makeGateRunner();
|
|
62
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
62
63
|
|
|
63
64
|
const result = await pipeline.evaluate(
|
|
64
65
|
makeTcc({ toolName: "read", input: {} }),
|
|
@@ -69,13 +70,14 @@ describe("ToolCallGatePipeline", () => {
|
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
it("short-circuits after the first blocking gate without evaluating later ones", async () => {
|
|
73
|
+
const resolver = makeResolver(makeCheckResult());
|
|
72
74
|
const inputs = makeGateInputs();
|
|
73
75
|
const { runner } = makeGateRunner();
|
|
74
76
|
const runSpy = vi
|
|
75
77
|
.spyOn(runner, "run")
|
|
76
78
|
.mockResolvedValue({ action: "block", reason: "first gate blocked" });
|
|
77
79
|
|
|
78
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
80
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
79
81
|
const result = await pipeline.evaluate(
|
|
80
82
|
makeTcc({ toolName: "read", input: {} }),
|
|
81
83
|
runner,
|
|
@@ -92,9 +94,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
92
94
|
toolTextSummaryMaxLength: 100,
|
|
93
95
|
toolInputLogPreviewMaxLength: 200,
|
|
94
96
|
}));
|
|
97
|
+
const resolver = makeResolver(makeCheckResult());
|
|
95
98
|
const inputs = makeGateInputs({ getToolPreviewLimits });
|
|
96
|
-
const { runner } = makeGateRunner(
|
|
97
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
99
|
+
const { runner } = makeGateRunner();
|
|
100
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
98
101
|
|
|
99
102
|
await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
|
|
100
103
|
|
|
@@ -103,9 +106,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
103
106
|
|
|
104
107
|
it("calls getInfrastructureReadDirs() during evaluate", async () => {
|
|
105
108
|
const getInfrastructureReadDirs = vi.fn<() => string[]>(() => []);
|
|
109
|
+
const resolver = makeResolver(makeCheckResult());
|
|
106
110
|
const inputs = makeGateInputs({ getInfrastructureReadDirs });
|
|
107
|
-
const { runner } = makeGateRunner(
|
|
108
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
111
|
+
const { runner } = makeGateRunner();
|
|
112
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
109
113
|
|
|
110
114
|
await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
|
|
111
115
|
|
|
@@ -114,9 +118,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
114
118
|
|
|
115
119
|
it("calls getActiveSkillEntries() during evaluate", async () => {
|
|
116
120
|
const getActiveSkillEntries = vi.fn<() => []>(() => []);
|
|
121
|
+
const resolver = makeResolver(makeCheckResult());
|
|
117
122
|
const inputs = makeGateInputs({ getActiveSkillEntries });
|
|
118
|
-
const { runner } = makeGateRunner(
|
|
119
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
123
|
+
const { runner } = makeGateRunner();
|
|
124
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
120
125
|
|
|
121
126
|
await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
|
|
122
127
|
|
|
@@ -124,9 +129,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
124
129
|
});
|
|
125
130
|
|
|
126
131
|
it("does not call BashProgram.parse for non-bash tools", async () => {
|
|
132
|
+
const resolver = makeResolver(makeCheckResult());
|
|
127
133
|
const inputs = makeGateInputs();
|
|
128
|
-
const { runner } = makeGateRunner(
|
|
129
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
134
|
+
const { runner } = makeGateRunner();
|
|
135
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
130
136
|
|
|
131
137
|
await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
|
|
132
138
|
|
|
@@ -138,9 +144,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
138
144
|
|
|
139
145
|
describe("evaluate — bash tool", () => {
|
|
140
146
|
it("returns allow when the bash command is permitted", async () => {
|
|
147
|
+
const resolver = makeResolver(makeCheckResult());
|
|
141
148
|
const inputs = makeGateInputs();
|
|
142
|
-
const { runner } = makeGateRunner(
|
|
143
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
149
|
+
const { runner } = makeGateRunner();
|
|
150
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
144
151
|
|
|
145
152
|
const result = await pipeline.evaluate(
|
|
146
153
|
makeTcc({ toolName: "bash", input: { command: "echo hello" } }),
|
|
@@ -151,9 +158,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
151
158
|
});
|
|
152
159
|
|
|
153
160
|
it("parses BashProgram exactly once per evaluate for bash tools with a command", async () => {
|
|
161
|
+
const resolver = makeResolver(makeCheckResult());
|
|
154
162
|
const inputs = makeGateInputs();
|
|
155
|
-
const { runner } = makeGateRunner(
|
|
156
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
163
|
+
const { runner } = makeGateRunner();
|
|
164
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
157
165
|
|
|
158
166
|
await pipeline.evaluate(
|
|
159
167
|
makeTcc({ toolName: "bash", input: { command: "echo hello" } }),
|
|
@@ -165,9 +173,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
165
173
|
});
|
|
166
174
|
|
|
167
175
|
it("does not parse BashProgram when the bash command is empty", async () => {
|
|
176
|
+
const resolver = makeResolver(makeCheckResult());
|
|
168
177
|
const inputs = makeGateInputs();
|
|
169
|
-
const { runner } = makeGateRunner(
|
|
170
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
178
|
+
const { runner } = makeGateRunner();
|
|
179
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
171
180
|
|
|
172
181
|
await pipeline.evaluate(
|
|
173
182
|
makeTcc({ toolName: "bash", input: { command: "" } }),
|
|
@@ -10,7 +10,7 @@ import { GateRunner } from "#src/handlers/gates/runner";
|
|
|
10
10
|
import type { SkillInputGateInputs } from "#src/handlers/gates/skill-input-gate-pipeline";
|
|
11
11
|
import type { ToolCallGateInputs } from "#src/handlers/gates/tool-call-gate-pipeline";
|
|
12
12
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
13
|
-
import type {
|
|
13
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
14
14
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
15
15
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
16
16
|
import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter";
|
|
@@ -25,7 +25,7 @@ import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
|
25
25
|
* mock access (`mockReturnValue`, `mockImplementation`, `mock.calls`).
|
|
26
26
|
*/
|
|
27
27
|
export function makeResolver(defaultCheck?: PermissionCheckResult) {
|
|
28
|
-
const resolve = vi.fn<
|
|
28
|
+
const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
|
|
29
29
|
if (defaultCheck) {
|
|
30
30
|
resolve.mockReturnValue(defaultCheck);
|
|
31
31
|
}
|
|
@@ -91,7 +91,7 @@ export function makeReporter(
|
|
|
91
91
|
export function makeGateRunner(
|
|
92
92
|
overrides: {
|
|
93
93
|
resolveResult?: PermissionCheckResult;
|
|
94
|
-
resolve?:
|
|
94
|
+
resolve?: ScopedPermissionResolver["resolve"];
|
|
95
95
|
recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
|
|
96
96
|
canConfirm?: GatePrompter["canConfirm"];
|
|
97
97
|
prompt?: GatePrompter["prompt"];
|
|
@@ -102,7 +102,7 @@ export function makeGateRunner(
|
|
|
102
102
|
const resolve =
|
|
103
103
|
overrides.resolve ??
|
|
104
104
|
vi
|
|
105
|
-
.fn<
|
|
105
|
+
.fn<ScopedPermissionResolver["resolve"]>()
|
|
106
106
|
.mockReturnValue(
|
|
107
107
|
overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
|
|
108
108
|
);
|
|
@@ -204,7 +204,7 @@ export function makePathDispatchResolver(
|
|
|
204
204
|
byPath: Record<string, PermissionCheckResult>,
|
|
205
205
|
defaultResult: PermissionCheckResult,
|
|
206
206
|
) {
|
|
207
|
-
const resolve = vi.fn<
|
|
207
|
+
const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
|
|
208
208
|
resolve.mockImplementation((_surface, input) => {
|
|
209
209
|
const path = (input as Record<string, unknown>).path;
|
|
210
210
|
if (typeof path === "string" && path in byPath) {
|
|
@@ -243,16 +243,12 @@ export function makeGateCheckResult(
|
|
|
243
243
|
*/
|
|
244
244
|
export function makeGateInputs(
|
|
245
245
|
overrides: {
|
|
246
|
-
resolve?: PermissionResolver["resolve"];
|
|
247
246
|
getActiveSkillEntries?: () => SkillPromptEntry[];
|
|
248
247
|
getInfrastructureReadDirs?: () => string[];
|
|
249
248
|
getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
|
|
250
249
|
} = {},
|
|
251
250
|
): ToolCallGateInputs {
|
|
252
251
|
return {
|
|
253
|
-
resolve:
|
|
254
|
-
overrides.resolve ??
|
|
255
|
-
vi.fn<PermissionResolver["resolve"]>().mockReturnValue(makeCheckResult()),
|
|
256
252
|
getActiveSkillEntries:
|
|
257
253
|
overrides.getActiveSkillEntries ??
|
|
258
254
|
vi.fn<() => SkillPromptEntry[]>(() => []),
|
|
@@ -124,10 +124,6 @@ export function makeCheckResult(
|
|
|
124
124
|
* field against `MockGateHandlerSession` individually — a missing field fails
|
|
125
125
|
* `pnpm run check` instead of failing silently at runtime.
|
|
126
126
|
*
|
|
127
|
-
* The `resolve` delegation is inlined as a closure that reads `session` at
|
|
128
|
-
* call time, so overriding `checkPermission` or `getSessionRuleset`
|
|
129
|
-
* automatically steers it without extra guards.
|
|
130
|
-
*
|
|
131
127
|
* Prompting is not part of this mock — pass `prompter` to `makeHandler`.
|
|
132
128
|
*/
|
|
133
129
|
export function makeSession(
|
|
@@ -169,17 +165,6 @@ export function makeSession(
|
|
|
169
165
|
vi
|
|
170
166
|
.fn<MockGateHandlerSession["getToolPreviewLimits"]>()
|
|
171
167
|
.mockReturnValue(resolveToolPreviewLimits(DEFAULT_EXTENSION_CONFIG)),
|
|
172
|
-
// Resolve delegation — closure reads `session` at call time so overrides win.
|
|
173
|
-
resolve:
|
|
174
|
-
overrides.resolve ??
|
|
175
|
-
vi.fn<MockGateHandlerSession["resolve"]>((surface, input, agentName) =>
|
|
176
|
-
session.checkPermission(
|
|
177
|
-
surface,
|
|
178
|
-
input,
|
|
179
|
-
agentName,
|
|
180
|
-
session.getSessionRuleset(),
|
|
181
|
-
),
|
|
182
|
-
),
|
|
183
168
|
};
|
|
184
169
|
return session;
|
|
185
170
|
}
|
|
@@ -293,7 +278,18 @@ export function makeHandler(overrides?: {
|
|
|
293
278
|
.mockReturnValue(overrides.tools.map((name) => ({ name }))),
|
|
294
279
|
})
|
|
295
280
|
: makeToolRegistry(overrides?.toolRegistry);
|
|
296
|
-
|
|
281
|
+
// Resolver delegates to session's checkPermission + getSessionRuleset —
|
|
282
|
+
// overriding session.checkPermission steers resolve automatically.
|
|
283
|
+
const resolver = {
|
|
284
|
+
resolve: (surface: string, input: unknown, agentName?: string) =>
|
|
285
|
+
session.checkPermission(
|
|
286
|
+
surface,
|
|
287
|
+
input,
|
|
288
|
+
agentName,
|
|
289
|
+
session.getSessionRuleset(),
|
|
290
|
+
),
|
|
291
|
+
};
|
|
292
|
+
const pipeline = new ToolCallGatePipeline(resolver, session);
|
|
297
293
|
const skillInputPipeline = new SkillInputGatePipeline(session);
|
|
298
294
|
const reporter = new GateDecisionReporter(session.logger, events);
|
|
299
295
|
const prompter: GatePrompter = overrides?.prompter ?? {
|
|
@@ -302,7 +298,7 @@ export function makeHandler(overrides?: {
|
|
|
302
298
|
.fn<GatePrompter["prompt"]>()
|
|
303
299
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
304
300
|
};
|
|
305
|
-
const runner = new GateRunner(
|
|
301
|
+
const runner = new GateRunner(resolver, session, prompter, reporter);
|
|
306
302
|
const handler = new PermissionGateHandler(
|
|
307
303
|
session,
|
|
308
304
|
toolRegistry,
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
3
|
+
import { PermissionResolver } from "#src/permission-resolver";
|
|
4
|
+
import type { Ruleset } from "#src/rule";
|
|
5
|
+
import { SessionApproval } from "#src/session-approval";
|
|
6
|
+
import { SessionRules } from "#src/session-rules";
|
|
7
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
8
|
+
|
|
9
|
+
function makePermissionManager() {
|
|
10
|
+
return {
|
|
11
|
+
configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
|
|
12
|
+
checkPermission: vi
|
|
13
|
+
.fn<
|
|
14
|
+
(
|
|
15
|
+
toolName: string,
|
|
16
|
+
input: unknown,
|
|
17
|
+
agentName?: string,
|
|
18
|
+
sessionRules?: Ruleset,
|
|
19
|
+
) => PermissionCheckResult
|
|
20
|
+
>()
|
|
21
|
+
.mockReturnValue({
|
|
22
|
+
state: "allow",
|
|
23
|
+
toolName: "read",
|
|
24
|
+
source: "tool",
|
|
25
|
+
origin: "builtin",
|
|
26
|
+
}),
|
|
27
|
+
getToolPermission: vi
|
|
28
|
+
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
29
|
+
.mockReturnValue("allow"),
|
|
30
|
+
getConfigIssues: vi.fn((): string[] => []),
|
|
31
|
+
getPolicyCacheStamp: vi.fn((): string => "stamp-1"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeResolver(
|
|
36
|
+
pm?: ScopedPermissionManager,
|
|
37
|
+
sessionRules?: Pick<SessionRules, "getRuleset">,
|
|
38
|
+
) {
|
|
39
|
+
const permissionManager = pm ?? makePermissionManager();
|
|
40
|
+
const rules = sessionRules ?? new SessionRules();
|
|
41
|
+
return {
|
|
42
|
+
resolver: new PermissionResolver(permissionManager, rules),
|
|
43
|
+
permissionManager,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
// no module-level vi.fn() stubs to reset
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("PermissionResolver", () => {
|
|
52
|
+
describe("resolve", () => {
|
|
53
|
+
it("forwards surface, input, and agentName, applying the empty session ruleset", () => {
|
|
54
|
+
const { resolver, permissionManager } = makeResolver();
|
|
55
|
+
|
|
56
|
+
resolver.resolve("bash", { command: "ls" }, "agent-x");
|
|
57
|
+
|
|
58
|
+
expect(permissionManager.checkPermission).toHaveBeenCalledWith(
|
|
59
|
+
"bash",
|
|
60
|
+
{ command: "ls" },
|
|
61
|
+
"agent-x",
|
|
62
|
+
[],
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("defaults agentName to undefined when omitted", () => {
|
|
67
|
+
const { resolver, permissionManager } = makeResolver();
|
|
68
|
+
|
|
69
|
+
resolver.resolve("read", { path: ".env" });
|
|
70
|
+
|
|
71
|
+
expect(permissionManager.checkPermission).toHaveBeenCalledWith(
|
|
72
|
+
"read",
|
|
73
|
+
{ path: ".env" },
|
|
74
|
+
undefined,
|
|
75
|
+
[],
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("applies a recorded session approval on the next resolve", () => {
|
|
80
|
+
const pm = makePermissionManager();
|
|
81
|
+
const sessionRules = new SessionRules();
|
|
82
|
+
const { resolver } = makeResolver(pm, sessionRules);
|
|
83
|
+
|
|
84
|
+
// Record an approval directly into the shared SessionRules instance.
|
|
85
|
+
sessionRules.record(SessionApproval.single("bash", "git *"));
|
|
86
|
+
resolver.resolve("bash", { command: "git status" });
|
|
87
|
+
|
|
88
|
+
const passedRules = vi.mocked(pm.checkPermission).mock.calls[0][3];
|
|
89
|
+
expect(passedRules).toHaveLength(1);
|
|
90
|
+
expect(passedRules?.[0]).toMatchObject({
|
|
91
|
+
surface: "bash",
|
|
92
|
+
pattern: "git *",
|
|
93
|
+
action: "allow",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns the PermissionManager's check result", () => {
|
|
98
|
+
const pm = makePermissionManager();
|
|
99
|
+
vi.mocked(pm.checkPermission).mockReturnValue({
|
|
100
|
+
state: "deny",
|
|
101
|
+
toolName: "bash",
|
|
102
|
+
source: "bash",
|
|
103
|
+
origin: "global",
|
|
104
|
+
matchedPattern: "rm *",
|
|
105
|
+
});
|
|
106
|
+
const { resolver } = makeResolver(pm);
|
|
107
|
+
|
|
108
|
+
const result = resolver.resolve("bash", { command: "rm -rf /" });
|
|
109
|
+
|
|
110
|
+
expect(result).toEqual({
|
|
111
|
+
state: "deny",
|
|
112
|
+
toolName: "bash",
|
|
113
|
+
source: "bash",
|
|
114
|
+
origin: "global",
|
|
115
|
+
matchedPattern: "rm *",
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("checkPermission", () => {
|
|
121
|
+
it("delegates to permissionManager.checkPermission with the given args", () => {
|
|
122
|
+
const { resolver, permissionManager } = makeResolver();
|
|
123
|
+
|
|
124
|
+
resolver.checkPermission("bash", { command: "ls" }, "agent-1");
|
|
125
|
+
|
|
126
|
+
expect(permissionManager.checkPermission).toHaveBeenCalledWith(
|
|
127
|
+
"bash",
|
|
128
|
+
{ command: "ls" },
|
|
129
|
+
"agent-1",
|
|
130
|
+
undefined,
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("passes optional sessionRules through when supplied", () => {
|
|
135
|
+
const { resolver, permissionManager } = makeResolver();
|
|
136
|
+
const extraRules: Ruleset = [
|
|
137
|
+
{ surface: "bash", pattern: "*", action: "allow", origin: "session" },
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
resolver.checkPermission(
|
|
141
|
+
"bash",
|
|
142
|
+
{ command: "ls" },
|
|
143
|
+
undefined,
|
|
144
|
+
extraRules,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(permissionManager.checkPermission).toHaveBeenCalledWith(
|
|
148
|
+
"bash",
|
|
149
|
+
{ command: "ls" },
|
|
150
|
+
undefined,
|
|
151
|
+
extraRules,
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("getToolPermission", () => {
|
|
157
|
+
it("delegates to permissionManager.getToolPermission", () => {
|
|
158
|
+
const pm = makePermissionManager();
|
|
159
|
+
vi.mocked(pm.getToolPermission).mockReturnValue("deny");
|
|
160
|
+
const { resolver } = makeResolver(pm);
|
|
161
|
+
|
|
162
|
+
const result = resolver.getToolPermission("write", "my-agent");
|
|
163
|
+
|
|
164
|
+
expect(pm.getToolPermission).toHaveBeenCalledWith("write", "my-agent");
|
|
165
|
+
expect(result).toBe("deny");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("getConfigIssues", () => {
|
|
170
|
+
it("delegates to permissionManager.getConfigIssues", () => {
|
|
171
|
+
const pm = makePermissionManager();
|
|
172
|
+
vi.mocked(pm.getConfigIssues).mockReturnValue(["issue-1"]);
|
|
173
|
+
const { resolver } = makeResolver(pm);
|
|
174
|
+
|
|
175
|
+
const result = resolver.getConfigIssues("agent-1");
|
|
176
|
+
|
|
177
|
+
expect(pm.getConfigIssues).toHaveBeenCalledWith("agent-1");
|
|
178
|
+
expect(result).toEqual(["issue-1"]);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("getPolicyCacheStamp", () => {
|
|
183
|
+
it("delegates to permissionManager.getPolicyCacheStamp", () => {
|
|
184
|
+
const pm = makePermissionManager();
|
|
185
|
+
vi.mocked(pm.getPolicyCacheStamp).mockReturnValue("stamp-abc");
|
|
186
|
+
const { resolver } = makeResolver(pm);
|
|
187
|
+
|
|
188
|
+
const result = resolver.getPolicyCacheStamp("agent-1");
|
|
189
|
+
|
|
190
|
+
expect(pm.getPolicyCacheStamp).toHaveBeenCalledWith("agent-1");
|
|
191
|
+
expect(result).toBe("stamp-abc");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -240,74 +240,6 @@ describe("PermissionSession", () => {
|
|
|
240
240
|
});
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
-
describe("resolve", () => {
|
|
244
|
-
it("forwards surface, input, and agentName, applying the empty session ruleset", () => {
|
|
245
|
-
const pm = makePermissionManager();
|
|
246
|
-
const { session } = createSession({ permissionManager: pm });
|
|
247
|
-
|
|
248
|
-
session.resolve("bash", { command: "ls" }, "agent-x");
|
|
249
|
-
|
|
250
|
-
expect(pm.checkPermission).toHaveBeenCalledWith(
|
|
251
|
-
"bash",
|
|
252
|
-
{ command: "ls" },
|
|
253
|
-
"agent-x",
|
|
254
|
-
[],
|
|
255
|
-
);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("defaults agentName to undefined when omitted", () => {
|
|
259
|
-
const pm = makePermissionManager();
|
|
260
|
-
const { session } = createSession({ permissionManager: pm });
|
|
261
|
-
|
|
262
|
-
session.resolve("read", { path: ".env" });
|
|
263
|
-
|
|
264
|
-
expect(pm.checkPermission).toHaveBeenCalledWith(
|
|
265
|
-
"read",
|
|
266
|
-
{ path: ".env" },
|
|
267
|
-
undefined,
|
|
268
|
-
[],
|
|
269
|
-
);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("applies a recorded session approval on the next resolve", () => {
|
|
273
|
-
const pm = makePermissionManager();
|
|
274
|
-
const { session } = createSession({ permissionManager: pm });
|
|
275
|
-
|
|
276
|
-
session.recordSessionApproval(SessionApproval.single("bash", "git *"));
|
|
277
|
-
session.resolve("bash", { command: "git status" });
|
|
278
|
-
|
|
279
|
-
const sessionRules = vi.mocked(pm.checkPermission).mock.calls[0][3];
|
|
280
|
-
expect(sessionRules).toHaveLength(1);
|
|
281
|
-
expect(sessionRules?.[0]).toMatchObject({
|
|
282
|
-
surface: "bash",
|
|
283
|
-
pattern: "git *",
|
|
284
|
-
action: "allow",
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("returns the PermissionManager's check result", () => {
|
|
289
|
-
const pm = makePermissionManager();
|
|
290
|
-
vi.mocked(pm.checkPermission).mockReturnValue({
|
|
291
|
-
state: "deny",
|
|
292
|
-
toolName: "bash",
|
|
293
|
-
source: "bash",
|
|
294
|
-
origin: "global",
|
|
295
|
-
matchedPattern: "rm *",
|
|
296
|
-
});
|
|
297
|
-
const { session } = createSession({ permissionManager: pm });
|
|
298
|
-
|
|
299
|
-
const result = session.resolve("bash", { command: "rm -rf /" });
|
|
300
|
-
|
|
301
|
-
expect(result).toEqual({
|
|
302
|
-
state: "deny",
|
|
303
|
-
toolName: "bash",
|
|
304
|
-
source: "bash",
|
|
305
|
-
origin: "global",
|
|
306
|
-
matchedPattern: "rm *",
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
243
|
describe("activate and deactivate", () => {
|
|
312
244
|
it("stores the context on activate", () => {
|
|
313
245
|
const { session, forwarding } = createSession();
|