@gotgenes/pi-permission-system 3.9.0 → 3.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 +36 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/defaults.ts +6 -0
- package/src/handlers/lifecycle.ts +1 -1
- package/src/handlers/tool-call.ts +20 -19
- package/src/permission-manager.ts +110 -128
- package/src/rule.ts +5 -0
- package/src/runtime.ts +3 -3
- package/src/session-rules.ts +54 -0
- package/src/synthesize.ts +152 -0
- package/src/types.ts +1 -1
- package/tests/handlers/before-agent-start.test.ts +3 -4
- package/tests/handlers/input.test.ts +3 -4
- package/tests/handlers/lifecycle.test.ts +7 -8
- package/tests/handlers/tool-call.test.ts +27 -19
- package/tests/permission-system.test.ts +195 -1
- package/tests/rule.test.ts +31 -0
- package/tests/runtime.test.ts +5 -4
- package/tests/session-rules.test.ts +226 -0
- package/tests/synthesize.test.ts +413 -0
- package/src/session-approval-cache.ts +0 -81
- package/tests/session-approval-cache.test.ts +0 -131
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { dirname, sep } from "node:path";
|
|
2
|
-
|
|
3
|
-
import { isPathWithinDirectory } from "./external-directory";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Ephemeral in-memory cache of session-scoped permission approvals.
|
|
7
|
-
* Keyed by permission surface (e.g. "external_directory"), values are
|
|
8
|
-
* normalized directory prefixes that have been approved for the session.
|
|
9
|
-
*
|
|
10
|
-
* Cleared on session_shutdown — never persisted to disk.
|
|
11
|
-
*/
|
|
12
|
-
export class SessionApprovalCache {
|
|
13
|
-
private approvals = new Map<string, Set<string>>();
|
|
14
|
-
|
|
15
|
-
/** Record a directory prefix as approved for the given surface. */
|
|
16
|
-
approve(surface: string, prefix: string): void {
|
|
17
|
-
let prefixes = this.approvals.get(surface);
|
|
18
|
-
if (!prefixes) {
|
|
19
|
-
prefixes = new Set();
|
|
20
|
-
this.approvals.set(surface, prefixes);
|
|
21
|
-
}
|
|
22
|
-
prefixes.add(prefix);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check whether a path falls under any approved prefix for the given surface.
|
|
27
|
-
* Uses `isPathWithinDirectory()` for correct separator-aware prefix matching.
|
|
28
|
-
*/
|
|
29
|
-
has(surface: string, path: string): boolean {
|
|
30
|
-
const prefixes = this.approvals.get(surface);
|
|
31
|
-
if (!prefixes) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
for (const prefix of prefixes) {
|
|
35
|
-
if (isPathWithinDirectory(path, prefix)) {
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Find and return the matching approved prefix, or null if none matches. */
|
|
43
|
-
findMatchingPrefix(surface: string, path: string): string | null {
|
|
44
|
-
const prefixes = this.approvals.get(surface);
|
|
45
|
-
if (!prefixes) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
for (const prefix of prefixes) {
|
|
49
|
-
if (isPathWithinDirectory(path, prefix)) {
|
|
50
|
-
return prefix;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Remove all session approvals. */
|
|
57
|
-
clear(): void {
|
|
58
|
-
this.approvals.clear();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Derive the directory prefix to approve from a normalized path.
|
|
64
|
-
* Returns `dirname(path)` with a trailing separator so that
|
|
65
|
-
* prefix matching via `isPathWithinDirectory()` works correctly.
|
|
66
|
-
*
|
|
67
|
-
* For paths that already end with a separator (directories),
|
|
68
|
-
* the trailing separator is stripped by dirname and re-added.
|
|
69
|
-
*/
|
|
70
|
-
export function deriveApprovalPrefix(normalizedPath: string): string {
|
|
71
|
-
// If the path already ends with a separator, it's a directory — return as-is.
|
|
72
|
-
if (normalizedPath.endsWith(sep)) {
|
|
73
|
-
return normalizedPath;
|
|
74
|
-
}
|
|
75
|
-
const dir = dirname(normalizedPath);
|
|
76
|
-
if (dir === normalizedPath) {
|
|
77
|
-
// Root path — dirname('/') === '/'
|
|
78
|
-
return dir;
|
|
79
|
-
}
|
|
80
|
-
return dir.endsWith(sep) ? dir : `${dir}${sep}`;
|
|
81
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
// Mock node:os so tilde-expansion is deterministic across platforms.
|
|
4
|
-
vi.mock("node:os", () => {
|
|
5
|
-
const homedir = vi.fn(() => "/mock/home");
|
|
6
|
-
return {
|
|
7
|
-
homedir,
|
|
8
|
-
default: { homedir },
|
|
9
|
-
};
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
deriveApprovalPrefix,
|
|
14
|
-
SessionApprovalCache,
|
|
15
|
-
} from "../src/session-approval-cache";
|
|
16
|
-
|
|
17
|
-
describe("SessionApprovalCache", () => {
|
|
18
|
-
describe("approve and has", () => {
|
|
19
|
-
it("returns false when no approvals exist", () => {
|
|
20
|
-
const cache = new SessionApprovalCache();
|
|
21
|
-
expect(cache.has("external_directory", "/some/path")).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("returns true for a path under an approved prefix", () => {
|
|
25
|
-
const cache = new SessionApprovalCache();
|
|
26
|
-
cache.approve("external_directory", "/other/project/src/");
|
|
27
|
-
expect(cache.has("external_directory", "/other/project/src/foo.ts")).toBe(
|
|
28
|
-
true,
|
|
29
|
-
);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("returns true for the exact approved prefix path", () => {
|
|
33
|
-
const cache = new SessionApprovalCache();
|
|
34
|
-
cache.approve("external_directory", "/other/project/src/");
|
|
35
|
-
expect(cache.has("external_directory", "/other/project/src/")).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("returns false for a path outside the approved prefix", () => {
|
|
39
|
-
const cache = new SessionApprovalCache();
|
|
40
|
-
cache.approve("external_directory", "/other/project/src/");
|
|
41
|
-
expect(cache.has("external_directory", "/other/project/lib/foo.ts")).toBe(
|
|
42
|
-
false,
|
|
43
|
-
);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("returns false for a sibling directory that shares a string prefix", () => {
|
|
47
|
-
const cache = new SessionApprovalCache();
|
|
48
|
-
cache.approve("external_directory", "/other/project/");
|
|
49
|
-
// /other/project-b/ should NOT match /other/project/
|
|
50
|
-
expect(cache.has("external_directory", "/other/project-b/foo.ts")).toBe(
|
|
51
|
-
false,
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("handles multiple approved prefixes for the same surface", () => {
|
|
56
|
-
const cache = new SessionApprovalCache();
|
|
57
|
-
cache.approve("external_directory", "/other/project-a/");
|
|
58
|
-
cache.approve("external_directory", "/other/project-b/");
|
|
59
|
-
expect(cache.has("external_directory", "/other/project-a/foo.ts")).toBe(
|
|
60
|
-
true,
|
|
61
|
-
);
|
|
62
|
-
expect(cache.has("external_directory", "/other/project-b/bar.ts")).toBe(
|
|
63
|
-
true,
|
|
64
|
-
);
|
|
65
|
-
expect(cache.has("external_directory", "/other/project-c/baz.ts")).toBe(
|
|
66
|
-
false,
|
|
67
|
-
);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("does not duplicate identical prefixes", () => {
|
|
71
|
-
const cache = new SessionApprovalCache();
|
|
72
|
-
cache.approve("external_directory", "/other/project/");
|
|
73
|
-
cache.approve("external_directory", "/other/project/");
|
|
74
|
-
// Set semantics — just verify it still works
|
|
75
|
-
expect(cache.has("external_directory", "/other/project/foo.ts")).toBe(
|
|
76
|
-
true,
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("surface isolation", () => {
|
|
82
|
-
it("does not match across different surface types", () => {
|
|
83
|
-
const cache = new SessionApprovalCache();
|
|
84
|
-
cache.approve("external_directory", "/other/project/");
|
|
85
|
-
expect(cache.has("some_other_surface", "/other/project/foo.ts")).toBe(
|
|
86
|
-
false,
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe("clear", () => {
|
|
92
|
-
it("removes all approvals", () => {
|
|
93
|
-
const cache = new SessionApprovalCache();
|
|
94
|
-
cache.approve("external_directory", "/other/project/");
|
|
95
|
-
cache.approve("some_surface", "/another/path/");
|
|
96
|
-
cache.clear();
|
|
97
|
-
expect(cache.has("external_directory", "/other/project/foo.ts")).toBe(
|
|
98
|
-
false,
|
|
99
|
-
);
|
|
100
|
-
expect(cache.has("some_surface", "/another/path/file")).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe("deriveApprovalPrefix", () => {
|
|
106
|
-
it("returns parent directory with trailing separator for a file path", () => {
|
|
107
|
-
expect(deriveApprovalPrefix("/other/project/src/foo.ts")).toBe(
|
|
108
|
-
"/other/project/src/",
|
|
109
|
-
);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("returns the directory itself with trailing separator for a directory path", () => {
|
|
113
|
-
expect(deriveApprovalPrefix("/other/project/src/")).toBe(
|
|
114
|
-
"/other/project/src/",
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("returns the directory itself when path has no trailing separator", () => {
|
|
119
|
-
// For a path like /other/project/src (directory), dirname gives /other/project
|
|
120
|
-
// but we can't distinguish dir from file without stat. dirname is the safe choice.
|
|
121
|
-
expect(deriveApprovalPrefix("/other/project/src")).toBe("/other/project/");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("handles root path", () => {
|
|
125
|
-
expect(deriveApprovalPrefix("/")).toBe("/");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("handles single-level path", () => {
|
|
129
|
-
expect(deriveApprovalPrefix("/foo")).toBe("/");
|
|
130
|
-
});
|
|
131
|
-
});
|