@gotgenes/pi-permission-system 4.4.0 → 4.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 +30 -0
- package/package.json +1 -1
- package/src/input-normalizer.ts +94 -0
- package/src/mcp-targets.ts +160 -0
- package/src/permission-manager.ts +53 -310
- package/src/rule.ts +32 -0
- package/tests/input-normalizer.test.ts +150 -0
- package/tests/mcp-targets.test.ts +178 -0
- package/tests/permission-manager-unified.test.ts +375 -0
- package/tests/rule.test.ts +81 -1
- package/src/defaults.ts +0 -10
- package/tests/defaults.test.ts +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ 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
|
+
## [4.5.0](https://github.com/gotgenes/pi-permission-system/compare/v4.4.1...v4.5.0) (2026-05-05)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add evaluateFirst multi-candidate evaluate helper ([6b1fa60](https://github.com/gotgenes/pi-permission-system/commit/6b1fa603e6eaf750624d1f553ddb84755ab9b78e))
|
|
14
|
+
* add input normalizer for non-MCP surfaces ([6d25624](https://github.com/gotgenes/pi-permission-system/commit/6d256241e3f599aa9db5952a16b10d10b72e5321))
|
|
15
|
+
* add MCP input normalization to input-normalizer ([6fa58b2](https://github.com/gotgenes/pi-permission-system/commit/6fa58b211f06eef5885f1e6f238285cdb77aab09))
|
|
16
|
+
* concatenate session rules into composed ruleset ([e85e844](https://github.com/gotgenes/pi-permission-system/commit/e85e844f414c6dfd14ffbbc74826c976d8f0f234))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
* mark unified checkPermission as implemented in target architecture ([bb7214a](https://github.com/gotgenes/pi-permission-system/commit/bb7214a8363b6b1ad70ae1233f297d28c36cd423))
|
|
22
|
+
* plan unified checkPermission evaluate path ([#81](https://github.com/gotgenes/pi-permission-system/issues/81)) ([6562328](https://github.com/gotgenes/pi-permission-system/commit/65623287aa899424829b0e31e7c9aa46f17e3f81))
|
|
23
|
+
* **retro:** add retro notes for issue [#82](https://github.com/gotgenes/pi-permission-system/issues/82) ([f748fe0](https://github.com/gotgenes/pi-permission-system/commit/f748fe00105dbce087ec0a41653171c53364d531))
|
|
24
|
+
|
|
25
|
+
## [4.4.1](https://github.com/gotgenes/pi-permission-system/compare/v4.4.0...v4.4.1) (2026-05-05)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Documentation
|
|
29
|
+
|
|
30
|
+
* plan delete deprecated defaults.ts stub ([#82](https://github.com/gotgenes/pi-permission-system/issues/82)) ([36fcace](https://github.com/gotgenes/pi-permission-system/commit/36fcaceab824b4334315767ba1cfd3fe24cff1b7))
|
|
31
|
+
* **retro:** add retro notes for issue [#80](https://github.com/gotgenes/pi-permission-system/issues/80) ([bfa11d5](https://github.com/gotgenes/pi-permission-system/commit/bfa11d539920dc24efbcab33329595da4e93e152))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Miscellaneous Chores
|
|
35
|
+
|
|
36
|
+
* delete deprecated defaults.ts stub ([#82](https://github.com/gotgenes/pi-permission-system/issues/82)) ([40ae42a](https://github.com/gotgenes/pi-permission-system/commit/40ae42ad710bc502f19d8a27e409cc22a6bca4f8))
|
|
37
|
+
|
|
8
38
|
## [4.4.0](https://github.com/gotgenes/pi-permission-system/compare/v4.3.0...v4.4.0) (2026-05-05)
|
|
9
39
|
|
|
10
40
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { toRecord } from "./common";
|
|
2
|
+
import { createMcpPermissionTargets } from "./mcp-targets";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Surface-normalized representation of a tool invocation used by
|
|
6
|
+
* `checkPermission()` to feed a single `evaluateFirst()` call.
|
|
7
|
+
*/
|
|
8
|
+
export interface NormalizedInput {
|
|
9
|
+
/** The permission surface for `evaluate()` (e.g. "bash", "mcp", "skill"). */
|
|
10
|
+
surface: string;
|
|
11
|
+
/**
|
|
12
|
+
* Candidate lookup values in priority order (most-specific first).
|
|
13
|
+
* Most surfaces produce a single-element array; MCP produces a
|
|
14
|
+
* multi-candidate list derived from the invocation input.
|
|
15
|
+
*/
|
|
16
|
+
values: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Surface-specific fields forwarded verbatim into `PermissionCheckResult`
|
|
19
|
+
* (e.g. `{ command }` for bash, `{ target }` for mcp).
|
|
20
|
+
*/
|
|
21
|
+
resultExtras: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SPECIAL_PERMISSION_KEYS = new Set(["external_directory"]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Map a raw tool invocation to the surface/values/extras triple needed by
|
|
28
|
+
* `checkPermission()`.
|
|
29
|
+
*
|
|
30
|
+
* @param toolName - Normalized (trimmed) tool name from the tool-call event.
|
|
31
|
+
* @param input - Raw input payload from the tool-call event.
|
|
32
|
+
* @param configuredMcpServerNames - Ordered list of MCP server names from the
|
|
33
|
+
* global MCP config, used to derive server-qualified MCP targets.
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeInput(
|
|
36
|
+
toolName: string,
|
|
37
|
+
input: unknown,
|
|
38
|
+
configuredMcpServerNames: readonly string[],
|
|
39
|
+
): NormalizedInput {
|
|
40
|
+
// --- Special surfaces (external_directory) ---
|
|
41
|
+
if (SPECIAL_PERMISSION_KEYS.has(toolName)) {
|
|
42
|
+
const record = toRecord(input);
|
|
43
|
+
const pathValue = typeof record.path === "string" ? record.path : null;
|
|
44
|
+
return {
|
|
45
|
+
surface: toolName,
|
|
46
|
+
values: [pathValue ?? "*"],
|
|
47
|
+
resultExtras: {},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- Skill ---
|
|
52
|
+
if (toolName === "skill") {
|
|
53
|
+
const record = toRecord(input);
|
|
54
|
+
const skillName = record.name;
|
|
55
|
+
const lookupValue = typeof skillName === "string" ? skillName : "*";
|
|
56
|
+
return {
|
|
57
|
+
surface: "skill",
|
|
58
|
+
values: [lookupValue],
|
|
59
|
+
resultExtras: {},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Bash ---
|
|
64
|
+
if (toolName === "bash") {
|
|
65
|
+
const record = toRecord(input);
|
|
66
|
+
const command = typeof record.command === "string" ? record.command : "";
|
|
67
|
+
return {
|
|
68
|
+
surface: "bash",
|
|
69
|
+
values: [command],
|
|
70
|
+
resultExtras: { command },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- MCP ---
|
|
75
|
+
if (toolName === "mcp") {
|
|
76
|
+
const mcpTargets = [
|
|
77
|
+
...createMcpPermissionTargets(input, configuredMcpServerNames),
|
|
78
|
+
"mcp",
|
|
79
|
+
];
|
|
80
|
+
const fallbackTarget = mcpTargets[0] ?? "mcp";
|
|
81
|
+
return {
|
|
82
|
+
surface: "mcp",
|
|
83
|
+
values: mcpTargets,
|
|
84
|
+
resultExtras: { target: fallbackTarget },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Tool surfaces (read, write, edit, grep, find, ls, extension tools) ---
|
|
89
|
+
return {
|
|
90
|
+
surface: toolName,
|
|
91
|
+
values: ["*"],
|
|
92
|
+
resultExtras: {},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { getNonEmptyString, toRecord } from "./common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a qualified MCP tool name of the form `server:tool`.
|
|
5
|
+
*
|
|
6
|
+
* Returns `{ server, tool }` when the string contains exactly one colon with
|
|
7
|
+
* non-empty text on both sides; otherwise returns `null`.
|
|
8
|
+
*/
|
|
9
|
+
export function parseQualifiedMcpToolName(
|
|
10
|
+
value: string,
|
|
11
|
+
): { server: string; tool: string } | null {
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (!trimmed) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const colonIndex = trimmed.indexOf(":");
|
|
18
|
+
if (colonIndex <= 0 || colonIndex >= trimmed.length - 1) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const server = trimmed.slice(0, colonIndex).trim();
|
|
23
|
+
const tool = trimmed.slice(colonIndex + 1).trim();
|
|
24
|
+
if (!server || !tool) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { server, tool };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function addDerivedMcpServerTargets(
|
|
32
|
+
toolName: string,
|
|
33
|
+
configuredServerNames: readonly string[],
|
|
34
|
+
pushTarget: (value: string | null) => void,
|
|
35
|
+
): void {
|
|
36
|
+
const trimmedToolName = toolName.trim();
|
|
37
|
+
if (!trimmedToolName) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const serverName of configuredServerNames) {
|
|
42
|
+
const trimmedServerName = serverName.trim();
|
|
43
|
+
if (!trimmedServerName) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!trimmedToolName.endsWith(`_${trimmedServerName}`)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (trimmedToolName.startsWith(`${trimmedServerName}_`)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pushTarget(`${trimmedServerName}_${trimmedToolName}`);
|
|
56
|
+
pushTarget(`${trimmedServerName}:${trimmedToolName}`);
|
|
57
|
+
pushTarget(trimmedServerName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pushMcpToolPermissionTargets(
|
|
62
|
+
rawReference: string,
|
|
63
|
+
serverHint: string | null,
|
|
64
|
+
configuredServerNames: readonly string[],
|
|
65
|
+
pushTarget: (value: string | null) => void,
|
|
66
|
+
): void {
|
|
67
|
+
const qualified = parseQualifiedMcpToolName(rawReference);
|
|
68
|
+
const resolvedServer = serverHint ?? qualified?.server ?? null;
|
|
69
|
+
const resolvedTool = qualified?.tool ?? rawReference;
|
|
70
|
+
|
|
71
|
+
if (resolvedServer) {
|
|
72
|
+
pushTarget(`${resolvedServer}_${resolvedTool}`);
|
|
73
|
+
pushTarget(`${resolvedServer}:${resolvedTool}`);
|
|
74
|
+
pushTarget(resolvedServer);
|
|
75
|
+
} else {
|
|
76
|
+
addDerivedMcpServerTargets(resolvedTool, configuredServerNames, pushTarget);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pushTarget(resolvedTool);
|
|
80
|
+
pushTarget(rawReference);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Derive the ordered list of MCP permission-lookup candidates from a raw MCP
|
|
85
|
+
* tool invocation input.
|
|
86
|
+
*
|
|
87
|
+
* Candidates are ordered from most-specific to least-specific so that
|
|
88
|
+
* `evaluateFirst()` stops at the first non-default match.
|
|
89
|
+
*/
|
|
90
|
+
export function createMcpPermissionTargets(
|
|
91
|
+
input: unknown,
|
|
92
|
+
configuredServerNames: readonly string[] = [],
|
|
93
|
+
): string[] {
|
|
94
|
+
const record = toRecord(input);
|
|
95
|
+
const tool = getNonEmptyString(record.tool);
|
|
96
|
+
const server = getNonEmptyString(record.server);
|
|
97
|
+
const connect = getNonEmptyString(record.connect);
|
|
98
|
+
const describe = getNonEmptyString(record.describe);
|
|
99
|
+
const search = getNonEmptyString(record.search);
|
|
100
|
+
|
|
101
|
+
const targets: string[] = [];
|
|
102
|
+
const pushTarget = (value: string | null) => {
|
|
103
|
+
if (!value) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!targets.includes(value)) {
|
|
107
|
+
targets.push(value);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (tool) {
|
|
112
|
+
pushMcpToolPermissionTargets(
|
|
113
|
+
tool,
|
|
114
|
+
server,
|
|
115
|
+
configuredServerNames,
|
|
116
|
+
pushTarget,
|
|
117
|
+
);
|
|
118
|
+
pushTarget("mcp_call");
|
|
119
|
+
return targets;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (connect) {
|
|
123
|
+
pushTarget(`mcp_connect_${connect}`);
|
|
124
|
+
pushTarget(connect);
|
|
125
|
+
pushTarget("mcp_connect");
|
|
126
|
+
return targets;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (describe) {
|
|
130
|
+
pushMcpToolPermissionTargets(
|
|
131
|
+
describe,
|
|
132
|
+
server,
|
|
133
|
+
configuredServerNames,
|
|
134
|
+
pushTarget,
|
|
135
|
+
);
|
|
136
|
+
pushTarget("mcp_describe");
|
|
137
|
+
return targets;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (search) {
|
|
141
|
+
if (server) {
|
|
142
|
+
pushTarget(`mcp_server_${server}`);
|
|
143
|
+
pushTarget(server);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pushTarget(search);
|
|
147
|
+
pushTarget("mcp_search");
|
|
148
|
+
return targets;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (server) {
|
|
152
|
+
pushTarget(`mcp_server_${server}`);
|
|
153
|
+
pushTarget(server);
|
|
154
|
+
pushTarget("mcp_list");
|
|
155
|
+
return targets;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pushTarget("mcp_status");
|
|
159
|
+
return targets;
|
|
160
|
+
}
|