@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -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
+ }