@gotgenes/pi-permission-system 17.0.0 → 17.1.1
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 +14 -0
- package/package.json +1 -1
- package/src/access-intent/access-path.ts +5 -5
- package/src/access-intent/bash/bash-path-resolver.ts +535 -0
- package/src/access-intent/bash/program.ts +21 -14
- package/src/access-intent/bash/token-classification.ts +24 -1
- package/src/canonicalize-path.ts +11 -5
- package/src/forwarded-permissions/permission-forwarder.ts +11 -1
- package/src/forwarding-manager.ts +7 -1
- package/src/handlers/before-agent-start.ts +1 -1
- package/src/handlers/gates/bash-path-extractor.ts +7 -5
- package/src/handlers/gates/external-directory.ts +7 -18
- package/src/handlers/gates/path.ts +3 -2
- package/src/handlers/gates/skill-read.ts +4 -2
- package/src/handlers/gates/tool-call-gate-pipeline.ts +20 -4
- package/src/handlers/gates/tool.ts +4 -2
- package/src/index.ts +12 -1
- package/src/input-normalizer.ts +9 -4
- package/src/path-normalizer.ts +100 -0
- package/src/path-utils.ts +39 -28
- package/src/permission-manager.ts +21 -7
- package/src/permission-session.ts +35 -2
- package/src/prompting-gateway.ts +3 -0
- package/src/rule.ts +8 -6
- package/src/skill-prompt-sanitizer.ts +8 -10
- package/src/subagent-context.ts +17 -9
- package/test/access-intent/access-path.test.ts +73 -24
- package/test/access-intent/bash/program.test.ts +131 -65
- package/test/access-intent/bash/token-classification.test.ts +75 -0
- package/test/bash-external-directory.test.ts +53 -1
- package/test/canonicalize-path.test.ts +34 -8
- package/test/forwarding-manager.test.ts +7 -1
- package/test/handlers/external-directory-symlink-acceptance.test.ts +23 -4
- package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -1
- package/test/handlers/gates/bash-external-directory.test.ts +5 -1
- package/test/handlers/gates/bash-path.test.ts +6 -1
- package/test/handlers/gates/external-directory-policy.test.ts +26 -8
- package/test/handlers/gates/external-directory.test.ts +8 -1
- package/test/handlers/gates/path.test.ts +53 -14
- package/test/handlers/gates/skill-read.test.ts +26 -13
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -1
- package/test/handlers/gates/tool.test.ts +13 -1
- package/test/helpers/gate-fixtures.ts +10 -0
- package/test/helpers/session-fixtures.ts +3 -0
- package/test/input-normalizer.test.ts +104 -39
- package/test/path-normalizer.test.ts +166 -0
- package/test/path-utils.test.ts +130 -51
- package/test/permission-forwarder.test.ts +1 -0
- package/test/permission-manager-unified.test.ts +40 -0
- package/test/permission-resolver.test.ts +12 -3
- package/test/permission-session.test.ts +41 -0
- package/test/pi-infrastructure-read.test.ts +27 -4
- package/test/prompting-gateway.test.ts +1 -0
- package/test/rule.test.ts +88 -42
- package/test/session-rules.test.ts +21 -4
- package/test/skill-prompt-sanitizer.test.ts +37 -16
- package/test/subagent-context.test.ts +77 -31
- package/test/synthesize.test.ts +13 -11
- package/src/access-intent/bash/cwd-projection.ts +0 -500
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { describeSkillReadGate } from "#src/handlers/gates/skill-read";
|
|
3
3
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
4
|
+
import { PathNormalizer } from "#src/path-normalizer";
|
|
4
5
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
5
6
|
|
|
7
|
+
// All test tccs use cwd "/test/project"; one normalizer serves every call.
|
|
8
|
+
const normalizer = new PathNormalizer("linux", "/test/project");
|
|
9
|
+
|
|
6
10
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
7
11
|
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
8
12
|
const original =
|
|
@@ -41,34 +45,39 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
|
41
45
|
|
|
42
46
|
describe("describeSkillReadGate", () => {
|
|
43
47
|
it("returns null when tool is not read", () => {
|
|
44
|
-
const result = describeSkillReadGate(
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
const result = describeSkillReadGate(
|
|
49
|
+
makeTcc({ toolName: "write" }),
|
|
50
|
+
normalizer,
|
|
51
|
+
() => [makeSkillEntry()],
|
|
52
|
+
);
|
|
47
53
|
expect(result).toBeNull();
|
|
48
54
|
});
|
|
49
55
|
|
|
50
56
|
it("returns null when no active skill entries", () => {
|
|
51
|
-
const result = describeSkillReadGate(makeTcc(), () => []);
|
|
57
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => []);
|
|
52
58
|
expect(result).toBeNull();
|
|
53
59
|
});
|
|
54
60
|
|
|
55
61
|
it("returns null when read path does not match any skill", () => {
|
|
56
62
|
const result = describeSkillReadGate(
|
|
57
63
|
makeTcc({ input: { path: "/test/project/src/index.ts" } }),
|
|
64
|
+
normalizer,
|
|
58
65
|
() => [makeSkillEntry()],
|
|
59
66
|
);
|
|
60
67
|
expect(result).toBeNull();
|
|
61
68
|
});
|
|
62
69
|
|
|
63
70
|
it("returns null when input has no path", () => {
|
|
64
|
-
const result = describeSkillReadGate(
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
const result = describeSkillReadGate(
|
|
72
|
+
makeTcc({ input: {} }),
|
|
73
|
+
normalizer,
|
|
74
|
+
() => [makeSkillEntry()],
|
|
75
|
+
);
|
|
67
76
|
expect(result).toBeNull();
|
|
68
77
|
});
|
|
69
78
|
|
|
70
79
|
it("returns GateDescriptor with preResolved.state matching skill entry state (ask)", () => {
|
|
71
|
-
const result = describeSkillReadGate(makeTcc(), () => [
|
|
80
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
72
81
|
makeSkillEntry({ state: "ask" }),
|
|
73
82
|
]);
|
|
74
83
|
expect(result).not.toBeNull();
|
|
@@ -77,7 +86,7 @@ describe("describeSkillReadGate", () => {
|
|
|
77
86
|
});
|
|
78
87
|
|
|
79
88
|
it("returns GateDescriptor with preResolved.state matching skill entry state (allow)", () => {
|
|
80
|
-
const result = describeSkillReadGate(makeTcc(), () => [
|
|
89
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
81
90
|
makeSkillEntry({ state: "allow" }),
|
|
82
91
|
]);
|
|
83
92
|
expect(result).not.toBeNull();
|
|
@@ -86,7 +95,7 @@ describe("describeSkillReadGate", () => {
|
|
|
86
95
|
});
|
|
87
96
|
|
|
88
97
|
it("returns GateDescriptor with preResolved.state matching skill entry state (deny)", () => {
|
|
89
|
-
const result = describeSkillReadGate(makeTcc(), () => [
|
|
98
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
90
99
|
makeSkillEntry({ state: "deny" }),
|
|
91
100
|
]);
|
|
92
101
|
expect(result).not.toBeNull();
|
|
@@ -95,7 +104,7 @@ describe("describeSkillReadGate", () => {
|
|
|
95
104
|
});
|
|
96
105
|
|
|
97
106
|
it("decision surface is 'skill' and decision value is the skill name", () => {
|
|
98
|
-
const result = describeSkillReadGate(makeTcc(), () => [
|
|
107
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
99
108
|
makeSkillEntry({ name: "my-skill" }),
|
|
100
109
|
])!;
|
|
101
110
|
expect(result.decision.surface).toBe("skill");
|
|
@@ -103,7 +112,7 @@ describe("describeSkillReadGate", () => {
|
|
|
103
112
|
});
|
|
104
113
|
|
|
105
114
|
it("denialContext contains the skill name and read path", () => {
|
|
106
|
-
const result = describeSkillReadGate(makeTcc(), () => [
|
|
115
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
107
116
|
makeSkillEntry({ name: "librarian" }),
|
|
108
117
|
])!;
|
|
109
118
|
expect(result.denialContext).toEqual({
|
|
@@ -117,6 +126,7 @@ describe("describeSkillReadGate", () => {
|
|
|
117
126
|
it("promptDetails includes skill_read source and skillName", () => {
|
|
118
127
|
const result = describeSkillReadGate(
|
|
119
128
|
makeTcc({ agentName: "test-agent", toolCallId: "tc-42" }),
|
|
129
|
+
normalizer,
|
|
120
130
|
() => [makeSkillEntry({ name: "my-skill" })],
|
|
121
131
|
)!;
|
|
122
132
|
expect(result.promptDetails).toMatchObject({
|
|
@@ -132,6 +142,7 @@ describe("describeSkillReadGate", () => {
|
|
|
132
142
|
it("logContext includes skill_read source and skillName", () => {
|
|
133
143
|
const result = describeSkillReadGate(
|
|
134
144
|
makeTcc({ agentName: "agent-1" }),
|
|
145
|
+
normalizer,
|
|
135
146
|
() => [makeSkillEntry({ name: "librarian" })],
|
|
136
147
|
)!;
|
|
137
148
|
expect(result.logContext).toMatchObject({
|
|
@@ -142,7 +153,9 @@ describe("describeSkillReadGate", () => {
|
|
|
142
153
|
});
|
|
143
154
|
|
|
144
155
|
it("surface is 'skill' on the descriptor", () => {
|
|
145
|
-
const result = describeSkillReadGate(makeTcc(), () => [
|
|
156
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
157
|
+
makeSkillEntry(),
|
|
158
|
+
])!;
|
|
146
159
|
expect(result.surface).toBe("skill");
|
|
147
160
|
});
|
|
148
161
|
});
|
|
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import type { AccessPath } from "#src/access-intent/access-path";
|
|
4
4
|
import { ToolCallGatePipeline } from "#src/handlers/gates/tool-call-gate-pipeline";
|
|
5
|
+
import { PathNormalizer } from "#src/path-normalizer";
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
makeGateInputs,
|
|
@@ -172,7 +173,7 @@ describe("ToolCallGatePipeline", () => {
|
|
|
172
173
|
expect(mockBashProgramParse).toHaveBeenCalledTimes(1);
|
|
173
174
|
expect(mockBashProgramParse).toHaveBeenCalledWith(
|
|
174
175
|
"echo hello",
|
|
175
|
-
|
|
176
|
+
expect.any(PathNormalizer),
|
|
176
177
|
);
|
|
177
178
|
});
|
|
178
179
|
|
|
@@ -53,6 +53,7 @@ describe("describeToolGate", () => {
|
|
|
53
53
|
makeTcc({ toolName: "read" }),
|
|
54
54
|
makeCheckResult("ask"),
|
|
55
55
|
makeFormatter(),
|
|
56
|
+
"linux",
|
|
56
57
|
);
|
|
57
58
|
expect(desc.surface).toBe("read");
|
|
58
59
|
expect(desc.decision.surface).toBe("read");
|
|
@@ -63,6 +64,7 @@ describe("describeToolGate", () => {
|
|
|
63
64
|
makeTcc({ toolName: "write" }),
|
|
64
65
|
makeCheckResult("ask"),
|
|
65
66
|
makeFormatter(),
|
|
67
|
+
"linux",
|
|
66
68
|
);
|
|
67
69
|
expect(desc.decision.value).toBe("write");
|
|
68
70
|
});
|
|
@@ -76,6 +78,7 @@ describe("describeToolGate", () => {
|
|
|
76
78
|
makeTcc({ toolName: "bash", input: { command: "git status" } }),
|
|
77
79
|
check,
|
|
78
80
|
makeFormatter(),
|
|
81
|
+
"linux",
|
|
79
82
|
);
|
|
80
83
|
expect(desc.surface).toBe("bash");
|
|
81
84
|
expect(desc.decision.surface).toBe("bash");
|
|
@@ -91,6 +94,7 @@ describe("describeToolGate", () => {
|
|
|
91
94
|
makeTcc({ toolName: "mcp", input: { tool: "server:tool" } }),
|
|
92
95
|
check,
|
|
93
96
|
makeFormatter(),
|
|
97
|
+
"linux",
|
|
94
98
|
);
|
|
95
99
|
expect(desc.surface).toBe("mcp");
|
|
96
100
|
expect(desc.decision.surface).toBe("mcp");
|
|
@@ -99,7 +103,7 @@ describe("describeToolGate", () => {
|
|
|
99
103
|
|
|
100
104
|
it("populates denialContext with kind 'tool' and check result", () => {
|
|
101
105
|
const check = makeCheckResult("deny", { toolName: "read" });
|
|
102
|
-
const desc = describeToolGate(makeTcc(), check, makeFormatter());
|
|
106
|
+
const desc = describeToolGate(makeTcc(), check, makeFormatter(), "linux");
|
|
103
107
|
expect(desc.denialContext).toEqual({
|
|
104
108
|
kind: "tool",
|
|
105
109
|
check,
|
|
@@ -114,6 +118,7 @@ describe("describeToolGate", () => {
|
|
|
114
118
|
makeTcc({ agentName: "my-agent" }),
|
|
115
119
|
check,
|
|
116
120
|
makeFormatter(),
|
|
121
|
+
"linux",
|
|
117
122
|
);
|
|
118
123
|
expect(desc.denialContext.agentName).toBe("my-agent");
|
|
119
124
|
});
|
|
@@ -124,6 +129,7 @@ describe("describeToolGate", () => {
|
|
|
124
129
|
makeTcc({ toolName: "bash", input: { command: "ls" } }),
|
|
125
130
|
check,
|
|
126
131
|
makeFormatter(),
|
|
132
|
+
"linux",
|
|
127
133
|
);
|
|
128
134
|
expect(desc.denialContext).toMatchObject({
|
|
129
135
|
kind: "tool",
|
|
@@ -140,6 +146,7 @@ describe("describeToolGate", () => {
|
|
|
140
146
|
makeTcc({ toolName: "bash", input: { command: "git status" } }),
|
|
141
147
|
check,
|
|
142
148
|
makeFormatter(),
|
|
149
|
+
"linux",
|
|
143
150
|
);
|
|
144
151
|
expect(desc.sessionApproval).toBeDefined();
|
|
145
152
|
expect(desc.sessionApproval?.surface).toBe("bash");
|
|
@@ -156,6 +163,7 @@ describe("describeToolGate", () => {
|
|
|
156
163
|
}),
|
|
157
164
|
check,
|
|
158
165
|
makeFormatter(),
|
|
166
|
+
"linux",
|
|
159
167
|
);
|
|
160
168
|
expect(desc.sessionApproval?.surface).toBe("edit");
|
|
161
169
|
expect(desc.sessionApproval?.representativePattern).toBe("/test/project/*");
|
|
@@ -174,6 +182,7 @@ describe("describeToolGate", () => {
|
|
|
174
182
|
}),
|
|
175
183
|
check,
|
|
176
184
|
makeFormatter(),
|
|
185
|
+
"linux",
|
|
177
186
|
);
|
|
178
187
|
expect(desc.sessionApproval?.representativePattern).toBe(
|
|
179
188
|
"/test/project/src/*",
|
|
@@ -186,6 +195,7 @@ describe("describeToolGate", () => {
|
|
|
186
195
|
makeTcc({ toolName: "read", agentName: "my-agent", toolCallId: "tc-42" }),
|
|
187
196
|
check,
|
|
188
197
|
makeFormatter(),
|
|
198
|
+
"linux",
|
|
189
199
|
);
|
|
190
200
|
expect(desc.promptDetails).toMatchObject({
|
|
191
201
|
source: "tool_call",
|
|
@@ -203,6 +213,7 @@ describe("describeToolGate", () => {
|
|
|
203
213
|
makeTcc({ toolName: "bash", input: { command: "ls" } }),
|
|
204
214
|
check,
|
|
205
215
|
makeFormatter(),
|
|
216
|
+
"linux",
|
|
206
217
|
);
|
|
207
218
|
expect(desc.logContext).toMatchObject({
|
|
208
219
|
source: "tool_call",
|
|
@@ -216,6 +227,7 @@ describe("describeToolGate", () => {
|
|
|
216
227
|
makeTcc({ toolName: "edit", input: { path: "/a.ts" } }),
|
|
217
228
|
makeCheckResult("ask", { toolName: "edit" }),
|
|
218
229
|
makeFormatter(),
|
|
230
|
+
"linux",
|
|
219
231
|
);
|
|
220
232
|
expect(desc.surface).toBe("edit");
|
|
221
233
|
expect(desc.input).toEqual({ path: "/a.ts" });
|
|
@@ -10,6 +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 { PathNormalizer } from "#src/path-normalizer";
|
|
13
14
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
14
15
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
15
16
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
@@ -253,6 +254,8 @@ export function makeGateInputs(
|
|
|
253
254
|
getActiveSkillEntries?: () => SkillPromptEntry[];
|
|
254
255
|
getInfrastructureReadDirs?: () => string[];
|
|
255
256
|
getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
|
|
257
|
+
getPathNormalizer?: () => PathNormalizer;
|
|
258
|
+
getPlatform?: () => NodeJS.Platform;
|
|
256
259
|
} = {},
|
|
257
260
|
): ToolCallGateInputs {
|
|
258
261
|
return {
|
|
@@ -268,6 +271,13 @@ export function makeGateInputs(
|
|
|
268
271
|
toolTextSummaryMaxLength: 100,
|
|
269
272
|
toolInputLogPreviewMaxLength: 200,
|
|
270
273
|
})),
|
|
274
|
+
getPathNormalizer:
|
|
275
|
+
overrides.getPathNormalizer ??
|
|
276
|
+
vi.fn<() => PathNormalizer>(
|
|
277
|
+
() => new PathNormalizer(process.platform, "/test/cwd"),
|
|
278
|
+
),
|
|
279
|
+
getPlatform:
|
|
280
|
+
overrides.getPlatform ?? vi.fn<() => NodeJS.Platform>(() => "linux"),
|
|
271
281
|
};
|
|
272
282
|
}
|
|
273
283
|
|
|
@@ -126,6 +126,7 @@ export function makeRealSession(overrides?: {
|
|
|
126
126
|
sessionRules?: SessionRules;
|
|
127
127
|
configStore?: SessionConfigStore;
|
|
128
128
|
gateway?: PromptingGatewayLifecycle;
|
|
129
|
+
platform?: NodeJS.Platform;
|
|
129
130
|
}): {
|
|
130
131
|
session: PermissionSession;
|
|
131
132
|
paths: ExtensionPaths;
|
|
@@ -146,6 +147,7 @@ export function makeRealSession(overrides?: {
|
|
|
146
147
|
const sessionRules = overrides?.sessionRules ?? new SessionRules();
|
|
147
148
|
const configStore = overrides?.configStore ?? makeConfigStore();
|
|
148
149
|
const gateway = overrides?.gateway ?? makeGateway();
|
|
150
|
+
const platform = overrides?.platform ?? process.platform;
|
|
149
151
|
const session = new PermissionSession(
|
|
150
152
|
paths,
|
|
151
153
|
forwarding,
|
|
@@ -153,6 +155,7 @@ export function makeRealSession(overrides?: {
|
|
|
153
155
|
sessionRules,
|
|
154
156
|
configStore,
|
|
155
157
|
gateway,
|
|
158
|
+
platform,
|
|
156
159
|
);
|
|
157
160
|
return {
|
|
158
161
|
session,
|