pi-permission-system 0.2.1 → 0.2.2

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,20 @@ 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
+ ## [0.2.2] - 2026-03-13
9
+
10
+ ### Changed
11
+ - Removed delegation task restriction logic — the `task` tool is no longer restricted to orchestrator agent only
12
+ - Simplified tool permission lookup to use explicit `tools` entries for arbitrary registered tools instead of MCP fallback
13
+ - Renamed `TOOL_PERMISSION_NAMES` to `BUILT_IN_TOOL_PERMISSION_NAMES` to clarify it covers only canonical Pi tools
14
+ - Updated schema descriptions for `tools` and `mcp` fields to guide configuration usage
15
+
16
+ ### Removed
17
+ - Removed delegation-specific permission checks (`isDelegationAllowedAgent`, `getDelegationBlockReason`) from permission evaluation
18
+
19
+ ### Tests
20
+ - Added comprehensive test coverage for tool permission lookup behavior
21
+
8
22
  ## [0.2.1] - 2026-03-13
9
23
 
10
24
  ### Added
package/README.md CHANGED
@@ -79,7 +79,7 @@ The extension integrates via Pi's lifecycle hooks:
79
79
  **Additional behaviors:**
80
80
  - Unknown/unregistered tools are blocked before permission checks (prevents bypass attempts)
81
81
  - The `Available tools:` system prompt section is rewritten to match the filtered active tool set
82
- - The `task` delegation tool is restricted to the `orchestrator` agent only
82
+ - Extension-provided tools like `task`, `mcp`, and third-party tools are handled by exact registered name instead of private built-in hardcodes
83
83
  - When a subagent hits an `ask` permission without direct UI access, the request can be forwarded to the main interactive session for confirmation
84
84
  - When a subagent triggers an `ask` permission without UI access, the request can be forwarded to the main session and answered there
85
85
 
@@ -111,14 +111,14 @@ Both logs write to files only under the extension directory. No debug output is
111
111
 
112
112
  The policy file is a JSON object with these sections:
113
113
 
114
- | Section | Description |
115
- |-----------------|------------------------------------------|
116
- | `defaultPolicy` | Fallback permissions per category |
117
- | `tools` | Built-in tool permissions |
118
- | `bash` | Command pattern permissions |
119
- | `mcp` | MCP server/tool permissions |
120
- | `skills` | Skill name pattern permissions |
121
- | `special` | Reserved permission checks |
114
+ | Section | Description |
115
+ |-----------------|-----------------------------------------------------|
116
+ | `defaultPolicy` | Fallback permissions per category |
117
+ | `tools` | Exact-name tool permissions for registered tools |
118
+ | `bash` | Command pattern permissions |
119
+ | `mcp` | MCP server/tool permissions for calls routed through a registered `mcp` tool |
120
+ | `skills` | Skill name pattern permissions |
121
+ | `special` | Reserved permission checks |
122
122
 
123
123
  > **Note:** Trailing commas are **not** supported. If parsing fails, the extension falls back to `ask` for all categories.
124
124
 
@@ -147,7 +147,7 @@ permission:
147
147
 
148
148
  **Precedence:** Agent frontmatter overrides global config (shallow-merged per section).
149
149
 
150
- **MCP behavior:** `permission.tools.mcp` is the coarse entry/fallback permission for the built-in `mcp` tool. More specific `permission.mcp` target rules override that fallback when they match.
150
+ **MCP behavior:** `permission.tools.mcp` is the coarse entry/fallback permission for a registered `mcp` tool when one is available. More specific `permission.mcp` target rules override that fallback when they match.
151
151
 
152
152
  **Limitations:** The frontmatter parser is intentionally minimal. Use only `key: value` scalars and nested maps. Avoid arrays, multi-line scalars, and YAML anchors.
153
153
 
@@ -173,33 +173,34 @@ Sets fallback permissions when no specific rule matches:
173
173
 
174
174
  ### `tools`
175
175
 
176
- Controls built-in tools by exact name (no wildcards):
176
+ Controls tools by exact registered name (no wildcards). This is the recommended standalone format for **all** tool entries, including Pi built-ins and arbitrary third-party extension tools.
177
177
 
178
- | Tool | Description |
179
- |---------|--------------------------------|
180
- | `bash` | Shell command execution |
181
- | `read` | File reading |
182
- | `write` | File creation/overwriting |
183
- | `edit` | Surgical file edits |
184
- | `grep` | Pattern searching |
185
- | `find` | File discovery |
186
- | `ls` | Directory listing |
187
- | `mcp` | MCP proxy tool entry/fallback |
178
+ | Tool name example | Description |
179
+ |-----------------------|-------------|
180
+ | `bash` | Shell command execution (tool-level fallback before `bash` pattern rules) |
181
+ | `read` / `write` | Canonical Pi built-in file tools |
182
+ | `mcp` | Registered MCP proxy tool entry/fallback when available |
183
+ | `task` | Delegation tool handled like any other registered extension tool |
184
+ | `third_party_tool` | Arbitrary registered extension tool |
188
185
 
189
186
  ```jsonc
190
187
  {
191
188
  "tools": {
192
189
  "read": "allow",
193
190
  "write": "deny",
194
- "edit": "deny",
195
- "mcp": "allow"
191
+ "mcp": "allow",
192
+ "third_party_tool": "ask"
196
193
  }
197
194
  }
198
195
  ```
199
196
 
197
+ Unknown or absent tools are not required in the config. If another extension is not installed, its tool simply will not be registered at runtime, and this extension will block attempts to call that missing tool before permission checks run.
198
+
200
199
  > **Note:** Setting `tools.bash` affects the *default* for bash commands, but `bash` patterns can provide command-level overrides.
201
200
  >
202
- > **Note:** Setting `tools.mcp` controls coarse access to the built-in `mcp` tool. Specific `mcp` rules still override it when a target pattern matches.
201
+ > **Note:** Setting `tools.mcp` controls coarse access to a registered `mcp` tool when one is available. Specific `mcp` rules still override it when a target pattern matches.
202
+ >
203
+ > **Note:** Top-level shorthand is only supported for the canonical Pi built-ins (`bash`, `read`, `write`, `edit`, `grep`, `find`, `ls`) in agent frontmatter. Use `permission.tools.<name>` for `mcp`, `task`, and any third-party tool.
203
204
 
204
205
  ### `bash`
205
206
 
@@ -241,7 +242,7 @@ MCP permissions match against derived targets from tool input. These rules are m
241
242
 
242
243
  #### MCP Tool Fallback via `tools.mcp`
243
244
 
244
- The `mcp` built-in tool can use `tools.mcp` as an entry permission point. This provides a fallback when no specific MCP pattern matches:
245
+ A registered `mcp` tool can use `tools.mcp` as an entry permission point. This provides a fallback when no specific MCP pattern matches:
245
246
 
246
247
  ```jsonc
247
248
  {
@@ -456,7 +457,7 @@ npx --yes ajv-cli@5 validate \
456
457
  |---------|-------|----------|
457
458
  | Config not applied (everything asks) | File not found or parse error | Verify file at `~/.pi/agent/pi-permissions.jsonc`; check for trailing commas |
458
459
  | Per-agent override not applied | Frontmatter parsing issue | Ensure `---` delimiters at file top; keep YAML simple; restart session |
459
- | Tool blocked as unregistered | Unknown tool name | Use built-in `mcp` tool for server tools: `{ "tool": "server:tool" }` |
460
+ | Tool blocked as unregistered | Unknown tool name | Use a registered `mcp` tool for server tools: `{ "tool": "server:tool" }` |
460
461
  | `/skill:<name>` blocked | Missing context or deny policy | Requires active agent context; `ask` behaves as block in headless mode |
461
462
 
462
463
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-permission-system",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -31,12 +31,14 @@
31
31
  }
32
32
  },
33
33
  "tools": {
34
+ "description": "Exact-name permissions for registered tools. Use this map for the canonical Pi built-ins and any extension-provided or third-party tools.",
34
35
  "$ref": "#/$defs/permissionMap"
35
36
  },
36
37
  "bash": {
37
38
  "$ref": "#/$defs/permissionMap"
38
39
  },
39
40
  "mcp": {
41
+ "description": "Pattern-based permissions for targets invoked through a registered `mcp` tool when available.",
40
42
  "$ref": "#/$defs/permissionMap"
41
43
  },
42
44
  "skills": {
package/src/index.ts CHANGED
@@ -27,8 +27,6 @@ const LEGACY_PERMISSION_FORWARDING_RESPONSES_DIR = join(LEGACY_PERMISSION_FORWAR
27
27
  const PERMISSION_FORWARDING_POLL_INTERVAL_MS = 250;
28
28
  const PERMISSION_FORWARDING_TIMEOUT_MS = 10 * 60 * 1000;
29
29
  const SUBAGENT_ENV_HINT_KEYS = ["PI_IS_SUBAGENT", "PI_SUBAGENT_SESSION_ID", "PI_AGENT_ROUTER_SUBAGENT"] as const;
30
- const ORCHESTRATOR_AGENT_NAME = "orchestrator";
31
- const DELEGATION_TOOL_NAME = "task";
32
30
 
33
31
  const AVAILABLE_SKILLS_OPEN_TAG = "<available_skills>";
34
32
  const AVAILABLE_SKILLS_CLOSE_TAG = "</available_skills>";
@@ -377,15 +375,6 @@ function getActiveAgentNameFromSystemPrompt(systemPrompt: string | undefined): s
377
375
  return normalizeAgentName(match[1]);
378
376
  }
379
377
 
380
- function isDelegationAllowedAgent(agentName: string | null): boolean {
381
- return Boolean(agentName && agentName.toLowerCase() === ORCHESTRATOR_AGENT_NAME);
382
- }
383
-
384
- function getDelegationBlockReason(agentName: string | null): string {
385
- const resolvedAgent = agentName ?? "none";
386
- return `Tool '${DELEGATION_TOOL_NAME}' is restricted to '${ORCHESTRATOR_AGENT_NAME}'. Active agent '${resolvedAgent}' cannot delegate.`;
387
- }
388
-
389
378
  function formatMissingToolNameReason(): string {
390
379
  return "Tool call was blocked because no tool name was provided. Use a registered tool name from pi.getAllTools().";
391
380
  }
@@ -397,7 +386,7 @@ function formatUnknownToolReason(toolName: string, availableToolNames: readonly
397
386
 
398
387
  const mcpHint = toolName === "mcp"
399
388
  ? ""
400
- : " If this was intended as an MCP server tool, call the built-in 'mcp' tool (for example: {\"tool\":\"server:tool\"}).";
389
+ : " If this was intended as an MCP server tool, call the registered 'mcp' tool when available (for example: {\"tool\":\"server:tool\"}).";
401
390
 
402
391
  return `Tool '${toolName}' is not registered in this runtime and was blocked before permission checks.${mcpHint} Registered tools: ${availableList}.`;
403
392
  }
@@ -1080,10 +1069,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1080
1069
  };
1081
1070
 
1082
1071
  const shouldExposeTool = (toolName: string, agentName: string | null): boolean => {
1083
- if (toolName === DELEGATION_TOOL_NAME && !isDelegationAllowedAgent(agentName)) {
1084
- return false;
1085
- }
1086
-
1087
1072
  // Use tool-level permission check for tool injection decisions
1088
1073
  // This ensures that agent-specific tool deny rules (e.g., bash: deny) are respected
1089
1074
  // before any command-level permissions are considered
@@ -1234,10 +1219,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1234
1219
  };
1235
1220
  }
1236
1221
 
1237
- if (toolName === DELEGATION_TOOL_NAME && !isDelegationAllowedAgent(agentName)) {
1238
- return { block: true, reason: getDelegationBlockReason(agentName) };
1239
- }
1240
-
1241
1222
  if (isToolCallEventType("read", event) && activeSkillEntries.length > 0) {
1242
1223
  const normalizedReadPath = normalizePathForComparison(event.input.path, ctx.cwd);
1243
1224
  const matchedSkill = findSkillPathMatch(normalizedReadPath, activeSkillEntries);
@@ -24,7 +24,7 @@ const AGENTS_DIR = join(homedir(), ".pi", "agent", "agents");
24
24
  const LEGACY_GLOBAL_SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json");
25
25
  const GLOBAL_MCP_CONFIG_PATH = join(homedir(), ".pi", "agent", "mcp.json");
26
26
 
27
- const TOOL_PERMISSION_NAMES = new Set(["bash", "read", "write", "edit", "grep", "find", "ls", "mcp", "task"]);
27
+ const BUILT_IN_TOOL_PERMISSION_NAMES = new Set(["bash", "read", "write", "edit", "grep", "find", "ls"]);
28
28
  const SPECIAL_PERMISSION_KEYS = new Set(["doom_loop", "external_directory"]);
29
29
  const MCP_BASELINE_TARGETS = new Set(["mcp_status", "mcp_list", "mcp_search", "mcp_describe", "mcp_connect"]);
30
30
 
@@ -211,7 +211,7 @@ function normalizeRawPermission(raw: unknown): AgentPermissions {
211
211
  continue;
212
212
  }
213
213
 
214
- if (TOOL_PERMISSION_NAMES.has(key)) {
214
+ if (BUILT_IN_TOOL_PERMISSION_NAMES.has(key)) {
215
215
  normalized.tools = { ...(normalized.tools || {}), [key]: value };
216
216
  continue;
217
217
  }
@@ -590,7 +590,10 @@ export class PermissionManager {
590
590
  * This is used for tool injection decisions where we need to know if a tool is allowed/denied
591
591
  * at the tool level before checking specific command permissions.
592
592
  *
593
- * @param toolName - The name of the tool (e.g., "bash", "read", "write")
593
+ * Exact-name entries in `tools` work for arbitrary registered extension tools.
594
+ * Canonical Pi tools with dedicated categories still use their specialized fallbacks.
595
+ *
596
+ * @param toolName - The name of the tool (for example "bash", "read", or a third-party tool name)
594
597
  * @param agentName - Optional agent name to check agent-specific permissions
595
598
  * @returns The permission state for the tool at the tool level
596
599
  */
@@ -598,44 +601,23 @@ export class PermissionManager {
598
601
  const { merged } = this.resolvePermissions(agentName);
599
602
  const normalizedToolName = toolName.trim();
600
603
 
601
- // Handle special permission keys (doom_loop, external_directory)
602
604
  if (SPECIAL_PERMISSION_KEYS.has(normalizedToolName)) {
603
605
  return merged.defaultPolicy.special;
604
606
  }
605
607
 
606
- // Handle skill tool
607
608
  if (normalizedToolName === "skill") {
608
609
  return merged.defaultPolicy.skills;
609
610
  }
610
611
 
611
- // For bash tool, return the tool-level permission (not command-level)
612
612
  if (normalizedToolName === "bash") {
613
613
  return merged.tools?.bash || merged.defaultPolicy.bash;
614
614
  }
615
615
 
616
- // Handle mcp tool
617
616
  if (normalizedToolName === "mcp") {
618
617
  return merged.tools?.mcp || merged.defaultPolicy.mcp;
619
618
  }
620
619
 
621
- // Handle other tool permission names
622
- if (TOOL_PERMISSION_NAMES.has(normalizedToolName)) {
623
- return merged.tools?.[normalizedToolName] || merged.defaultPolicy.tools;
624
- }
625
-
626
- // For MCP tools (qualified names like "server_tool"), check mcp permissions
627
- if (normalizedToolName.includes("_")) {
628
- const mcpMatch = findCompiledPermissionMatch(
629
- compilePermissionPatternsFromSources(this.loadGlobalConfig().mcp, this.loadAgentPermissions(agentName).mcp),
630
- normalizedToolName
631
- );
632
- if (mcpMatch) {
633
- return mcpMatch.state;
634
- }
635
- }
636
-
637
- // Default to the tools default policy
638
- return merged.defaultPolicy.tools;
620
+ return merged.tools?.[normalizedToolName] || merged.defaultPolicy.tools;
639
621
  }
640
622
 
641
623
  checkPermission(toolName: string, input: unknown, agentName?: string): PermissionCheckResult {
@@ -731,7 +713,7 @@ export class PermissionManager {
731
713
  };
732
714
  }
733
715
 
734
- if (TOOL_PERMISSION_NAMES.has(normalizedToolName)) {
716
+ if (BUILT_IN_TOOL_PERMISSION_NAMES.has(normalizedToolName)) {
735
717
  return {
736
718
  toolName,
737
719
  state: merged.tools?.[normalizedToolName] || merged.defaultPolicy.tools,
@@ -739,21 +721,18 @@ export class PermissionManager {
739
721
  };
740
722
  }
741
723
 
742
- const mcpMatch = findCompiledPermissionMatch(compiledMcp, toolName);
743
- if (mcpMatch) {
724
+ const explicitToolPermission = merged.tools?.[normalizedToolName];
725
+ if (explicitToolPermission) {
744
726
  return {
745
727
  toolName,
746
- state: mcpMatch.state,
747
- matchedPattern: mcpMatch.matchedPattern,
748
- target: mcpMatch.matchedName,
749
- source: "mcp",
728
+ state: explicitToolPermission,
729
+ source: "tool",
750
730
  };
751
731
  }
752
732
 
753
733
  return {
754
734
  toolName,
755
- state: merged.defaultPolicy.mcp || "deny",
756
- target: toolName,
735
+ state: merged.defaultPolicy.tools,
757
736
  source: "default",
758
737
  };
759
738
  }
package/src/test.ts CHANGED
@@ -128,7 +128,7 @@ runTest("BashFilter uses opencode-style last-match hierarchy", () => {
128
128
  assert.equal(generic.matchedPattern, "git *");
129
129
  });
130
130
 
131
- runTest("PermissionManager built-in permission checking", () => {
131
+ runTest("PermissionManager canonical built-in permission checking", () => {
132
132
  const { manager, cleanup } = createManager({
133
133
  defaultPolicy: {
134
134
  tools: "deny",
@@ -195,7 +195,7 @@ permission:
195
195
  }
196
196
  });
197
197
 
198
- runTest("MCP wildcard matching", () => {
198
+ runTest("MCP wildcard matching uses the registered mcp tool", () => {
199
199
  const { manager, cleanup } = createManager({
200
200
  defaultPolicy: {
201
201
  tools: "ask",
@@ -206,24 +206,57 @@ runTest("MCP wildcard matching", () => {
206
206
  },
207
207
  mcp: {
208
208
  "*": "deny",
209
- "subagent_*": "ask",
210
- "subagent_query-*": "allow",
209
+ "research_*": "ask",
210
+ "research_query-*": "allow",
211
211
  },
212
212
  });
213
213
 
214
214
  try {
215
- const queryDocs = manager.checkPermission("subagent_query-docs", {});
215
+ const queryDocs = manager.checkPermission("mcp", { tool: "research:query-docs" });
216
216
  assert.equal(queryDocs.state, "allow");
217
217
  assert.equal(queryDocs.source, "mcp");
218
- assert.equal(queryDocs.matchedPattern, "subagent_query-*");
218
+ assert.equal(queryDocs.matchedPattern, "research_query-*");
219
+ assert.equal(queryDocs.target, "research_query-docs");
219
220
 
220
- const resolve = manager.checkPermission("subagent_resolve-context", {});
221
+ const resolve = manager.checkPermission("mcp", { tool: "research:resolve-context" });
221
222
  assert.equal(resolve.state, "ask");
222
- assert.equal(resolve.matchedPattern, "subagent_*");
223
+ assert.equal(resolve.matchedPattern, "research_*");
224
+ assert.equal(resolve.target, "research_resolve-context");
223
225
 
224
- const unknown = manager.checkPermission("web_search_provider", {});
226
+ const unknown = manager.checkPermission("mcp", { tool: "search:provider" });
225
227
  assert.equal(unknown.state, "deny");
226
228
  assert.equal(unknown.matchedPattern, "*");
229
+ assert.equal(unknown.target, "search_provider");
230
+ } finally {
231
+ cleanup();
232
+ }
233
+ });
234
+
235
+ runTest("Arbitrary extension tools use exact-name tool permissions instead of MCP fallback", () => {
236
+ const { manager, cleanup } = createManager({
237
+ defaultPolicy: {
238
+ tools: "deny",
239
+ bash: "ask",
240
+ mcp: "allow",
241
+ skills: "ask",
242
+ special: "ask",
243
+ },
244
+ tools: {
245
+ third_party_tool: "allow",
246
+ },
247
+ mcp: {
248
+ "*": "deny",
249
+ },
250
+ });
251
+
252
+ try {
253
+ const allowed = manager.checkPermission("third_party_tool", {});
254
+ assert.equal(allowed.state, "allow");
255
+ assert.equal(allowed.source, "tool");
256
+
257
+ const fallback = manager.checkPermission("another_extension_tool", {});
258
+ assert.equal(fallback.state, "deny");
259
+ assert.equal(fallback.source, "default");
227
260
  } finally {
228
261
  cleanup();
229
262
  }
@@ -538,7 +571,47 @@ permission:
538
571
  }
539
572
  });
540
573
 
541
- runTest("task uses tool permissions instead of MCP fallback", () => {
574
+ runTest("Only canonical built-ins support top-level shorthand in agent frontmatter", () => {
575
+ const { manager, cleanup } = createManager(
576
+ {
577
+ defaultPolicy: {
578
+ tools: "deny",
579
+ bash: "ask",
580
+ mcp: "deny",
581
+ skills: "ask",
582
+ special: "ask",
583
+ },
584
+ },
585
+ {
586
+ reviewer: `---
587
+ name: reviewer
588
+ permission:
589
+ find: allow
590
+ task: allow
591
+ mcp: allow
592
+ ---
593
+ `,
594
+ },
595
+ );
596
+
597
+ try {
598
+ const findResult = manager.checkPermission("find", {}, "reviewer");
599
+ assert.equal(findResult.state, "allow");
600
+ assert.equal(findResult.source, "tool");
601
+
602
+ const taskResult = manager.checkPermission("task", {}, "reviewer");
603
+ assert.equal(taskResult.state, "deny");
604
+ assert.equal(taskResult.source, "default");
605
+
606
+ const mcpResult = manager.checkPermission("mcp", { tool: "exa:web_search_exa" }, "reviewer");
607
+ assert.equal(mcpResult.state, "deny");
608
+ assert.equal(mcpResult.source, "default");
609
+ } finally {
610
+ cleanup();
611
+ }
612
+ });
613
+
614
+ runTest("task uses exact-name tool permissions like any registered extension tool", () => {
542
615
  const { manager, cleanup } = createManager(
543
616
  {
544
617
  defaultPolicy: {
@@ -574,7 +647,7 @@ runTest("Tool registry resolves event tool names from string and object payloads
574
647
  runTest("Tool registry blocks unregistered tools and handles aliases", () => {
575
648
  const registeredTools = [{ toolName: "mcp" }, { toolName: "read" }, { toolName: "bash" }];
576
649
 
577
- const unknownCheck = checkRequestedToolRegistration("subagent_query-docs", registeredTools);
650
+ const unknownCheck = checkRequestedToolRegistration("third_party_tool", registeredTools);
578
651
  assert.equal(unknownCheck.status, "unregistered");
579
652
  if (unknownCheck.status === "unregistered") {
580
653
  assert.deepEqual(unknownCheck.availableToolNames, ["bash", "mcp", "read"]);
@@ -587,7 +660,7 @@ runTest("Tool registry blocks unregistered tools and handles aliases", () => {
587
660
  assert.equal(missingNameCheck.status, "missing-tool-name");
588
661
  });
589
662
 
590
- runTest("getToolPermission returns tool-level deny for agent with bash: deny", () => {
663
+ runTest("getToolPermission returns tool-level policy for canonical and extension tools", () => {
591
664
  const { manager, cleanup } = createManager(
592
665
  {
593
666
  defaultPolicy: {
@@ -599,8 +672,8 @@ runTest("getToolPermission returns tool-level deny for agent with bash: deny", (
599
672
  },
600
673
  },
601
674
  {
602
- orchestrator: `---
603
- name: orchestrator
675
+ reviewer: `---
676
+ name: reviewer
604
677
  permission:
605
678
  tools:
606
679
  bash: deny
@@ -612,23 +685,18 @@ permission:
612
685
  );
613
686
 
614
687
  try {
615
- // Tool-level check for bash should return deny for orchestrator
616
- const bashPermission = manager.getToolPermission("bash", "orchestrator");
688
+ const bashPermission = manager.getToolPermission("bash", "reviewer");
617
689
  assert.equal(bashPermission, "deny");
618
690
 
619
- // Tool-level check for task should return allow
620
- const taskPermission = manager.getToolPermission("task", "orchestrator");
691
+ const taskPermission = manager.getToolPermission("task", "reviewer");
621
692
  assert.equal(taskPermission, "allow");
622
693
 
623
- // Tool-level check for read should return deny
624
- const readPermission = manager.getToolPermission("read", "orchestrator");
694
+ const readPermission = manager.getToolPermission("read", "reviewer");
625
695
  assert.equal(readPermission, "deny");
626
696
 
627
- // When no agent specified, should fall back to default policy
628
697
  const defaultBashPermission = manager.getToolPermission("bash");
629
698
  assert.equal(defaultBashPermission, "ask");
630
699
 
631
- // Global config tools setting should work
632
700
  const { manager: manager2, cleanup: cleanup2 } = createManager({
633
701
  defaultPolicy: {
634
702
  tools: "deny",
@@ -653,4 +721,29 @@ permission:
653
721
  }
654
722
  });
655
723
 
724
+ runTest("getToolPermission supports arbitrary extension tool names", () => {
725
+ const { manager, cleanup } = createManager({
726
+ defaultPolicy: {
727
+ tools: "deny",
728
+ bash: "ask",
729
+ mcp: "allow",
730
+ skills: "ask",
731
+ special: "ask",
732
+ },
733
+ tools: {
734
+ third_party_tool: "allow",
735
+ },
736
+ });
737
+
738
+ try {
739
+ const explicitPermission = manager.getToolPermission("third_party_tool");
740
+ assert.equal(explicitPermission, "allow");
741
+
742
+ const fallbackPermission = manager.getToolPermission("missing_extension_tool");
743
+ assert.equal(fallbackPermission, "deny");
744
+ } finally {
745
+ cleanup();
746
+ }
747
+ });
748
+
656
749
  console.log("All permission system tests passed.");