@gotgenes/pi-permission-system 5.15.0 → 5.16.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 +15 -0
- package/README.md +9 -0
- package/config/config.example.json +6 -1
- package/package.json +1 -1
- package/schemas/permissions.schema.json +7 -2
- package/src/handlers/gates/helpers.ts +4 -1
- package/src/handlers/gates/tool.ts +25 -9
- package/src/input-normalizer.ts +12 -1
- package/src/pattern-suggest.ts +12 -2
- package/tests/handlers/gates/helpers.test.ts +15 -2
- package/tests/input-normalizer.test.ts +41 -4
- package/tests/pattern-suggest.test.ts +40 -12
- package/tests/permission-manager-unified.test.ts +131 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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
|
+
## [5.16.0](https://github.com/gotgenes/pi-permission-system/compare/v5.15.0...v5.16.0) (2026-05-13)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* decision events include file path for path-bearing tools ([#147](https://github.com/gotgenes/pi-permission-system/issues/147)) ([eea226d](https://github.com/gotgenes/pi-permission-system/commit/eea226d990d4358983cc319d54b7544849b2b453))
|
|
14
|
+
* normalizeInput returns file path for path-bearing tools ([#147](https://github.com/gotgenes/pi-permission-system/issues/147)) ([0b48995](https://github.com/gotgenes/pi-permission-system/commit/0b4899563ed3aaf2dd264650a98964168e7ecba1))
|
|
15
|
+
* path-scoped session approvals for path-bearing tools ([#147](https://github.com/gotgenes/pi-permission-system/issues/147)) ([1feacc5](https://github.com/gotgenes/pi-permission-system/commit/1feacc53d4ac1653bf974886ce77e13aff014b68))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
* document per-tool path patterns ([#147](https://github.com/gotgenes/pi-permission-system/issues/147)) ([81245f6](https://github.com/gotgenes/pi-permission-system/commit/81245f6ac98500e6c4d3c2191ceb2db032284f05))
|
|
21
|
+
* plan per-tool path patterns for path-bearing tools ([#147](https://github.com/gotgenes/pi-permission-system/issues/147)) ([9458706](https://github.com/gotgenes/pi-permission-system/commit/9458706d85e4711b73677ee1abcc8f3ad7d18a2e))
|
|
22
|
+
|
|
8
23
|
## [5.15.0](https://github.com/gotgenes/pi-permission-system/compare/v5.14.1...v5.15.0) (2026-05-13)
|
|
9
24
|
|
|
10
25
|
|
package/README.md
CHANGED
|
@@ -34,6 +34,12 @@ pi install npm:@gotgenes/pi-permission-system
|
|
|
34
34
|
{
|
|
35
35
|
"permission": {
|
|
36
36
|
"*": "allow",
|
|
37
|
+
"read": {
|
|
38
|
+
"*": "allow",
|
|
39
|
+
"*.env": "deny",
|
|
40
|
+
"*.env.*": "deny",
|
|
41
|
+
"*.env.example": "allow"
|
|
42
|
+
},
|
|
37
43
|
"bash": {
|
|
38
44
|
"rm -rf *": "deny",
|
|
39
45
|
"sudo *": "ask"
|
|
@@ -56,6 +62,9 @@ All permissions use one of three states:
|
|
|
56
62
|
When the dialog prompts, you can approve once or approve a pattern for the rest of the session.
|
|
57
63
|
See [docs/session-approvals.md](docs/session-approvals.md) for details on session-scoped rules and pattern suggestions.
|
|
58
64
|
|
|
65
|
+
For path-bearing tools (`read`, `write`, `edit`, `find`, `grep`, `ls`), patterns in the permission map are matched against the file path from `input.path`.
|
|
66
|
+
This lets you express rules like "allow reads but deny `.env` files" — see the example config above.
|
|
67
|
+
|
|
59
68
|
## Configuration
|
|
60
69
|
|
|
61
70
|
Config lives in one JSON file per scope:
|
package/package.json
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"permission": {
|
|
43
43
|
"description": "Flat permission policy. Each key is a surface name; values are a PermissionState string (catch-all) or a pattern→action map.",
|
|
44
|
-
"markdownDescription": "Flat permission policy.\n\nEach top-level key is a surface name:\n- `\"*\"` — universal fallback (replaces `defaultPolicy.tools` from the legacy format)\n- Tool names (`read`, `write`, `bash`, `mcp`, `skill`, `external_directory`, etc.)\n\nA **string** value is shorthand for `{ \"*\": action }` (surface-level catch-all).\nAn **object** value maps wildcard patterns to actions — last matching pattern wins.\n\n**Merge order (lowest → highest precedence):** global → project → per-agent frontmatter.",
|
|
44
|
+
"markdownDescription": "Flat permission policy.\n\nEach top-level key is a surface name:\n- `\"*\"` — universal fallback (replaces `defaultPolicy.tools` from the legacy format)\n- Tool names (`read`, `write`, `bash`, `mcp`, `skill`, `external_directory`, etc.)\n\nA **string** value is shorthand for `{ \"*\": action }` (surface-level catch-all).\nAn **object** value maps wildcard patterns to actions — last matching pattern wins.\n\nFor path-bearing tools (`read`, `write`, `edit`, `find`, `grep`, `ls`), patterns are matched against the file path from `input.path`. For example, `\"read\": { \"*\": \"allow\", \"*.env\": \"deny\" }` allows reads but denies `.env` files.\n\n**Merge order (lowest → highest precedence):** global → project → per-agent frontmatter.",
|
|
45
45
|
"type": "object",
|
|
46
46
|
"propertyNames": {
|
|
47
47
|
"description": "A surface name or the universal fallback key '*'.",
|
|
@@ -63,7 +63,12 @@
|
|
|
63
63
|
"examples": [
|
|
64
64
|
{
|
|
65
65
|
"*": "ask",
|
|
66
|
-
"read":
|
|
66
|
+
"read": {
|
|
67
|
+
"*": "allow",
|
|
68
|
+
"*.env": "deny",
|
|
69
|
+
"*.env.*": "deny",
|
|
70
|
+
"*.env.example": "allow"
|
|
71
|
+
},
|
|
67
72
|
"write": "deny",
|
|
68
73
|
"bash": {
|
|
69
74
|
"*": "ask",
|
|
@@ -3,14 +3,17 @@ import type { PermissionCheckResult } from "../../types";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Derive the human-readable value for a decision event from a check result.
|
|
6
|
-
* Bash → extracted command; MCP → qualified target;
|
|
6
|
+
* Bash → extracted command; MCP → qualified target;
|
|
7
|
+
* path-bearing tools → file path; others → tool name.
|
|
7
8
|
*/
|
|
8
9
|
export function deriveDecisionValue(
|
|
9
10
|
toolName: string,
|
|
10
11
|
check: Pick<PermissionCheckResult, "command" | "target">,
|
|
12
|
+
path?: string,
|
|
11
13
|
): string {
|
|
12
14
|
if (toolName === "bash") return check.command ?? toolName;
|
|
13
15
|
if (toolName === "mcp") return check.target ?? toolName;
|
|
16
|
+
if (path) return path;
|
|
14
17
|
return toolName;
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PATH_BEARING_TOOLS } from "../../path-utils";
|
|
1
|
+
import { getPathBearingToolPath, PATH_BEARING_TOOLS } from "../../path-utils";
|
|
2
2
|
import { suggestSessionPattern } from "../../pattern-suggest";
|
|
3
3
|
import {
|
|
4
4
|
formatAskPrompt,
|
|
@@ -11,6 +11,21 @@ import type { GateDescriptor } from "./descriptor";
|
|
|
11
11
|
import { deriveDecisionValue } from "./helpers";
|
|
12
12
|
import type { ToolCallContext } from "./types";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Derive the value used for session-approval pattern suggestions.
|
|
16
|
+
*
|
|
17
|
+
* Bash → command string; MCP → qualified target;
|
|
18
|
+
* path-bearing tools → file path; others → catch-all wildcard.
|
|
19
|
+
*/
|
|
20
|
+
function deriveSuggestionValue(
|
|
21
|
+
tcc: ToolCallContext,
|
|
22
|
+
check: PermissionCheckResult,
|
|
23
|
+
): string {
|
|
24
|
+
if (tcc.toolName === "bash") return check.command ?? "";
|
|
25
|
+
if (tcc.toolName === "mcp") return check.target ?? "mcp";
|
|
26
|
+
return getPathBearingToolPath(tcc.toolName, tcc.input) ?? "*";
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
/**
|
|
15
30
|
* Build a pure descriptor for the normal tool permission gate.
|
|
16
31
|
*
|
|
@@ -28,13 +43,10 @@ export function describeToolGate(
|
|
|
28
43
|
);
|
|
29
44
|
|
|
30
45
|
// Compute session approval suggestion for the "for this session" option.
|
|
31
|
-
const
|
|
32
|
-
tcc.toolName
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
? (check.target ?? "mcp")
|
|
36
|
-
: "*";
|
|
37
|
-
const suggestion = suggestSessionPattern(tcc.toolName, suggestionValue);
|
|
46
|
+
const suggestion = suggestSessionPattern(
|
|
47
|
+
tcc.toolName,
|
|
48
|
+
deriveSuggestionValue(tcc, check),
|
|
49
|
+
);
|
|
38
50
|
|
|
39
51
|
// Build the unavailable-reason message. Bash gets the command embedded.
|
|
40
52
|
const inputCommand =
|
|
@@ -85,7 +97,11 @@ export function describeToolGate(
|
|
|
85
97
|
},
|
|
86
98
|
decision: {
|
|
87
99
|
surface: tcc.toolName,
|
|
88
|
-
value: deriveDecisionValue(
|
|
100
|
+
value: deriveDecisionValue(
|
|
101
|
+
tcc.toolName,
|
|
102
|
+
check,
|
|
103
|
+
getPathBearingToolPath(tcc.toolName, tcc.input) ?? undefined,
|
|
104
|
+
),
|
|
89
105
|
},
|
|
90
106
|
};
|
|
91
107
|
}
|
package/src/input-normalizer.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toRecord } from "./common";
|
|
2
2
|
import { createMcpPermissionTargets } from "./mcp-targets";
|
|
3
|
+
import { getPathBearingToolPath, PATH_BEARING_TOOLS } from "./path-utils";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Surface-normalized representation of a tool invocation used by
|
|
@@ -85,7 +86,17 @@ export function normalizeInput(
|
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
// ---
|
|
89
|
+
// --- Path-bearing tools (read, write, edit, grep, find, ls) ---
|
|
90
|
+
if (PATH_BEARING_TOOLS.has(toolName)) {
|
|
91
|
+
const path = getPathBearingToolPath(toolName, input);
|
|
92
|
+
return {
|
|
93
|
+
surface: toolName,
|
|
94
|
+
values: [path ?? "*"],
|
|
95
|
+
resultExtras: {},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Extension tools (non-path-bearing) ---
|
|
89
100
|
return {
|
|
90
101
|
surface: toolName,
|
|
91
102
|
values: ["*"],
|
package/src/pattern-suggest.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { prefix } from "./bash-arity";
|
|
2
|
+
import { PATH_BEARING_TOOLS } from "./path-utils";
|
|
2
3
|
import { deriveApprovalPattern } from "./session-rules";
|
|
3
4
|
|
|
4
5
|
/** The suggestion returned for a "Yes, for this session" dialog option. */
|
|
@@ -69,7 +70,11 @@ function buildLabel(pattern: string, surface: string): string {
|
|
|
69
70
|
case "external_directory":
|
|
70
71
|
return `Yes, allow access to external directory "${pattern}" for this session`;
|
|
71
72
|
default:
|
|
72
|
-
//
|
|
73
|
+
// Path-bearing tools with a specific path pattern show the pattern.
|
|
74
|
+
if (PATH_BEARING_TOOLS.has(surface) && pattern !== "*") {
|
|
75
|
+
return `Yes, allow ${surface} "${pattern}" for this session`;
|
|
76
|
+
}
|
|
77
|
+
// Tool surfaces with catch-all or extension tools.
|
|
73
78
|
return `Yes, allow tool "${surface}" for this session`;
|
|
74
79
|
}
|
|
75
80
|
}
|
|
@@ -100,7 +105,12 @@ export function suggestSessionPattern(
|
|
|
100
105
|
pattern = deriveApprovalPattern(value);
|
|
101
106
|
break;
|
|
102
107
|
default:
|
|
103
|
-
//
|
|
108
|
+
// Path-bearing tools: derive a directory-scoped pattern from the path.
|
|
109
|
+
if (PATH_BEARING_TOOLS.has(surface) && value !== "*") {
|
|
110
|
+
pattern = deriveApprovalPattern(value);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
// Extension tools / fallback.
|
|
104
114
|
pattern = "*";
|
|
105
115
|
break;
|
|
106
116
|
}
|
|
@@ -26,9 +26,22 @@ describe("deriveDecisionValue", () => {
|
|
|
26
26
|
expect(deriveDecisionValue("mcp", {})).toBe("mcp");
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it("returns toolName for
|
|
29
|
+
it("returns toolName for non-path-bearing tools", () => {
|
|
30
|
+
expect(deriveDecisionValue("my_extension_tool", {})).toBe(
|
|
31
|
+
"my_extension_tool",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns path for path-bearing tools when path is provided", () => {
|
|
36
|
+
expect(deriveDecisionValue("read", {}, "/project/src/main.ts")).toBe(
|
|
37
|
+
"/project/src/main.ts",
|
|
38
|
+
);
|
|
39
|
+
expect(deriveDecisionValue("write", {}, "src/.env")).toBe("src/.env");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("falls back to toolName for path-bearing tools when path is missing", () => {
|
|
30
43
|
expect(deriveDecisionValue("read", {})).toBe("read");
|
|
31
|
-
expect(deriveDecisionValue("write", {
|
|
44
|
+
expect(deriveDecisionValue("write", {}, undefined)).toBe("write");
|
|
32
45
|
});
|
|
33
46
|
});
|
|
34
47
|
|
|
@@ -71,22 +71,59 @@ describe("normalizeInput — non-MCP surfaces", () => {
|
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
describe("
|
|
75
|
-
it("uses
|
|
74
|
+
describe("path-bearing tools (read, write, edit, grep, find, ls)", () => {
|
|
75
|
+
it("uses input.path as the lookup value when path is present", () => {
|
|
76
76
|
for (const tool of ["read", "write", "edit", "grep", "find", "ls"]) {
|
|
77
|
-
const result = normalizeInput(
|
|
77
|
+
const result = normalizeInput(
|
|
78
|
+
tool,
|
|
79
|
+
{ path: "/project/src/main.ts" },
|
|
80
|
+
[],
|
|
81
|
+
);
|
|
78
82
|
expect(result.surface).toBe(tool);
|
|
79
|
-
expect(result.values).toEqual(["
|
|
83
|
+
expect(result.values).toEqual(["/project/src/main.ts"]);
|
|
80
84
|
expect(result.resultExtras).toEqual({});
|
|
81
85
|
}
|
|
82
86
|
});
|
|
83
87
|
|
|
88
|
+
it("falls back to '*' when input.path is missing", () => {
|
|
89
|
+
for (const tool of ["read", "write", "edit", "grep", "find", "ls"]) {
|
|
90
|
+
const result = normalizeInput(tool, {}, []);
|
|
91
|
+
expect(result.values).toEqual(["*"]);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("falls back to '*' when input.path is empty string", () => {
|
|
96
|
+
const result = normalizeInput("read", { path: "" }, []);
|
|
97
|
+
expect(result.values).toEqual(["*"]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("falls back to '*' when input.path is not a string", () => {
|
|
101
|
+
const result = normalizeInput("write", { path: 42 }, []);
|
|
102
|
+
expect(result.values).toEqual(["*"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("falls back to '*' when input is null", () => {
|
|
106
|
+
const result = normalizeInput("edit", null, []);
|
|
107
|
+
expect(result.values).toEqual(["*"]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("extension tools (non-path-bearing)", () => {
|
|
84
112
|
it("uses '*' as the lookup value for extension tools", () => {
|
|
85
113
|
const result = normalizeInput("my_extension_tool", { some: "input" }, []);
|
|
86
114
|
expect(result.surface).toBe("my_extension_tool");
|
|
87
115
|
expect(result.values).toEqual(["*"]);
|
|
88
116
|
expect(result.resultExtras).toEqual({});
|
|
89
117
|
});
|
|
118
|
+
|
|
119
|
+
it("uses '*' even when extension tool has a path field", () => {
|
|
120
|
+
const result = normalizeInput(
|
|
121
|
+
"my_extension_tool",
|
|
122
|
+
{ path: "/some/path" },
|
|
123
|
+
[],
|
|
124
|
+
);
|
|
125
|
+
expect(result.values).toEqual(["*"]);
|
|
126
|
+
});
|
|
90
127
|
});
|
|
91
128
|
});
|
|
92
129
|
|
|
@@ -121,28 +121,51 @@ describe("suggestSessionPattern", () => {
|
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
describe("tool surfaces", () => {
|
|
125
|
-
it("returns
|
|
126
|
-
const result = suggestSessionPattern("read", "
|
|
127
|
-
expect(result).toMatchObject({
|
|
124
|
+
describe("path-bearing tool surfaces", () => {
|
|
125
|
+
it("returns directory-scoped pattern for read with a file path", () => {
|
|
126
|
+
const result = suggestSessionPattern("read", "/outside/project/file.ts");
|
|
127
|
+
expect(result).toMatchObject({
|
|
128
|
+
surface: "read",
|
|
129
|
+
pattern: "/outside/project/*",
|
|
130
|
+
});
|
|
128
131
|
});
|
|
129
132
|
|
|
130
|
-
it("returns
|
|
131
|
-
const result = suggestSessionPattern("write", "
|
|
132
|
-
expect(result).toMatchObject({
|
|
133
|
+
it("returns directory-scoped pattern for write with a file path", () => {
|
|
134
|
+
const result = suggestSessionPattern("write", "src/main.ts");
|
|
135
|
+
expect(result).toMatchObject({
|
|
136
|
+
surface: "write",
|
|
137
|
+
pattern: "src/*",
|
|
138
|
+
});
|
|
133
139
|
});
|
|
134
140
|
|
|
135
|
-
it("returns *
|
|
136
|
-
const result = suggestSessionPattern("
|
|
137
|
-
expect(result).toMatchObject({ surface: "
|
|
141
|
+
it("returns * when value is '*' (fallback)", () => {
|
|
142
|
+
const result = suggestSessionPattern("read", "*");
|
|
143
|
+
expect(result).toMatchObject({ surface: "read", pattern: "*" });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("label includes the path pattern for path-bearing tools", () => {
|
|
147
|
+
const result = suggestSessionPattern("read", "/tmp/data/file.txt");
|
|
148
|
+
expect(result.label).toBe(
|
|
149
|
+
'Yes, allow read "/tmp/data/*" for this session',
|
|
150
|
+
);
|
|
138
151
|
});
|
|
139
152
|
|
|
140
|
-
it("label shows tool name
|
|
153
|
+
it("label shows tool name when pattern is *", () => {
|
|
141
154
|
const result = suggestSessionPattern("find", "*");
|
|
142
155
|
expect(result.label).toBe('Yes, allow tool "find" for this session');
|
|
143
156
|
});
|
|
144
157
|
});
|
|
145
158
|
|
|
159
|
+
describe("non-path-bearing tool surfaces", () => {
|
|
160
|
+
it("returns * for extension tools", () => {
|
|
161
|
+
const result = suggestSessionPattern("my_extension_tool", "*");
|
|
162
|
+
expect(result).toMatchObject({
|
|
163
|
+
surface: "my_extension_tool",
|
|
164
|
+
pattern: "*",
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
146
169
|
describe("label field", () => {
|
|
147
170
|
it("bash label includes surface prefix and pattern", () => {
|
|
148
171
|
const result = suggestSessionPattern("bash", "git status");
|
|
@@ -173,7 +196,12 @@ describe("suggestSessionPattern", () => {
|
|
|
173
196
|
);
|
|
174
197
|
});
|
|
175
198
|
|
|
176
|
-
it("tool label
|
|
199
|
+
it("path-bearing tool label includes path pattern", () => {
|
|
200
|
+
const result = suggestSessionPattern("edit", "src/file.ts");
|
|
201
|
+
expect(result.label).toBe('Yes, allow edit "src/*" for this session');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("tool label shows tool name when value is *", () => {
|
|
177
205
|
const result = suggestSessionPattern("edit", "*");
|
|
178
206
|
expect(result.label).toBe('Yes, allow tool "edit" for this session');
|
|
179
207
|
});
|
|
@@ -980,3 +980,134 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
980
980
|
});
|
|
981
981
|
});
|
|
982
982
|
});
|
|
983
|
+
|
|
984
|
+
// ---------------------------------------------------------------------------
|
|
985
|
+
// Per-tool path patterns (#147)
|
|
986
|
+
// ---------------------------------------------------------------------------
|
|
987
|
+
|
|
988
|
+
describe("checkPermission — per-tool path patterns", () => {
|
|
989
|
+
it("denies read of .env when path pattern matches", () => {
|
|
990
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
991
|
+
read: { "*": "allow", "*.env": "deny" },
|
|
992
|
+
});
|
|
993
|
+
try {
|
|
994
|
+
const result = manager.checkPermission("read", { path: ".env" });
|
|
995
|
+
expect(result.state).toBe("deny");
|
|
996
|
+
expect(result.matchedPattern).toBe("*.env");
|
|
997
|
+
} finally {
|
|
998
|
+
cleanup();
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it("allows read of non-.env file when .env is denied", () => {
|
|
1003
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
1004
|
+
read: { "*": "allow", "*.env": "deny" },
|
|
1005
|
+
});
|
|
1006
|
+
try {
|
|
1007
|
+
const result = manager.checkPermission("read", {
|
|
1008
|
+
path: "src/main.ts",
|
|
1009
|
+
});
|
|
1010
|
+
expect(result.state).toBe("allow");
|
|
1011
|
+
} finally {
|
|
1012
|
+
cleanup();
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
it("allows write to src/ when only src/ is allowed", () => {
|
|
1017
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
1018
|
+
write: { "*": "deny", "src/*": "allow" },
|
|
1019
|
+
});
|
|
1020
|
+
try {
|
|
1021
|
+
const result = manager.checkPermission("write", {
|
|
1022
|
+
path: "src/main.ts",
|
|
1023
|
+
});
|
|
1024
|
+
expect(result.state).toBe("allow");
|
|
1025
|
+
expect(result.matchedPattern).toBe("src/*");
|
|
1026
|
+
} finally {
|
|
1027
|
+
cleanup();
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
it("denies write outside src/ when only src/ is allowed", () => {
|
|
1032
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
1033
|
+
write: { "*": "deny", "src/*": "allow" },
|
|
1034
|
+
});
|
|
1035
|
+
try {
|
|
1036
|
+
const result = manager.checkPermission("write", {
|
|
1037
|
+
path: "vendor/lib.ts",
|
|
1038
|
+
});
|
|
1039
|
+
expect(result.state).toBe("deny");
|
|
1040
|
+
} finally {
|
|
1041
|
+
cleanup();
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it("backward compat: 'read': 'allow' allows read of any path", () => {
|
|
1046
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
1047
|
+
read: "allow",
|
|
1048
|
+
});
|
|
1049
|
+
try {
|
|
1050
|
+
const result = manager.checkPermission("read", { path: ".env" });
|
|
1051
|
+
expect(result.state).toBe("allow");
|
|
1052
|
+
} finally {
|
|
1053
|
+
cleanup();
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it("backward compat: 'read': 'deny' denies read of any path", () => {
|
|
1058
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
1059
|
+
read: "deny",
|
|
1060
|
+
});
|
|
1061
|
+
try {
|
|
1062
|
+
const result = manager.checkPermission("read", {
|
|
1063
|
+
path: "src/main.ts",
|
|
1064
|
+
});
|
|
1065
|
+
expect(result.state).toBe("deny");
|
|
1066
|
+
} finally {
|
|
1067
|
+
cleanup();
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it("session rule for specific path overrides config deny", () => {
|
|
1072
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
1073
|
+
read: { "*": "allow", "*.env": "deny" },
|
|
1074
|
+
});
|
|
1075
|
+
try {
|
|
1076
|
+
const sessionRules: Ruleset = [sessionAllow("read", ".env")];
|
|
1077
|
+
const result = manager.checkPermission(
|
|
1078
|
+
"read",
|
|
1079
|
+
{ path: ".env" },
|
|
1080
|
+
undefined,
|
|
1081
|
+
sessionRules,
|
|
1082
|
+
);
|
|
1083
|
+
expect(result.state).toBe("allow");
|
|
1084
|
+
expect(result.source).toBe("session");
|
|
1085
|
+
} finally {
|
|
1086
|
+
cleanup();
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it("falls back to '*' when input.path is missing", () => {
|
|
1091
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
1092
|
+
read: { "*": "allow", "*.env": "deny" },
|
|
1093
|
+
});
|
|
1094
|
+
try {
|
|
1095
|
+
const result = manager.checkPermission("read", {});
|
|
1096
|
+
expect(result.state).toBe("allow");
|
|
1097
|
+
} finally {
|
|
1098
|
+
cleanup();
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
it("getToolPermission still returns surface-level state (not path-specific)", () => {
|
|
1103
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
1104
|
+
read: { "*": "allow", "*.env": "deny" },
|
|
1105
|
+
});
|
|
1106
|
+
try {
|
|
1107
|
+
const toolState = manager.getToolPermission("read");
|
|
1108
|
+
expect(toolState).toBe("allow");
|
|
1109
|
+
} finally {
|
|
1110
|
+
cleanup();
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
});
|