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 +14 -0
- package/README.md +27 -26
- package/package.json +1 -1
- package/schemas/permissions.schema.json +2 -0
- package/src/index.ts +1 -20
- package/src/permission-manager.ts +13 -34
- package/src/test.ts +115 -22
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
|
-
-
|
|
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` |
|
|
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
|
|
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
|
|
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
|
|
179
|
-
|
|
180
|
-
| `bash`
|
|
181
|
-
| `read`
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
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
|
-
"
|
|
195
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
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 (
|
|
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
|
|
743
|
-
if (
|
|
724
|
+
const explicitToolPermission = merged.tools?.[normalizedToolName];
|
|
725
|
+
if (explicitToolPermission) {
|
|
744
726
|
return {
|
|
745
727
|
toolName,
|
|
746
|
-
state:
|
|
747
|
-
|
|
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.
|
|
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
|
-
"
|
|
210
|
-
"
|
|
209
|
+
"research_*": "ask",
|
|
210
|
+
"research_query-*": "allow",
|
|
211
211
|
},
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
try {
|
|
215
|
-
const queryDocs = manager.checkPermission("
|
|
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, "
|
|
218
|
+
assert.equal(queryDocs.matchedPattern, "research_query-*");
|
|
219
|
+
assert.equal(queryDocs.target, "research_query-docs");
|
|
219
220
|
|
|
220
|
-
const resolve = manager.checkPermission("
|
|
221
|
+
const resolve = manager.checkPermission("mcp", { tool: "research:resolve-context" });
|
|
221
222
|
assert.equal(resolve.state, "ask");
|
|
222
|
-
assert.equal(resolve.matchedPattern, "
|
|
223
|
+
assert.equal(resolve.matchedPattern, "research_*");
|
|
224
|
+
assert.equal(resolve.target, "research_resolve-context");
|
|
223
225
|
|
|
224
|
-
const unknown = manager.checkPermission("
|
|
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("
|
|
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("
|
|
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
|
|
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
|
-
|
|
603
|
-
name:
|
|
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
|
-
|
|
616
|
-
const bashPermission = manager.getToolPermission("bash", "orchestrator");
|
|
688
|
+
const bashPermission = manager.getToolPermission("bash", "reviewer");
|
|
617
689
|
assert.equal(bashPermission, "deny");
|
|
618
690
|
|
|
619
|
-
|
|
620
|
-
const taskPermission = manager.getToolPermission("task", "orchestrator");
|
|
691
|
+
const taskPermission = manager.getToolPermission("task", "reviewer");
|
|
621
692
|
assert.equal(taskPermission, "allow");
|
|
622
693
|
|
|
623
|
-
|
|
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.");
|