pi-permission-system 0.5.0 → 0.6.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 +9 -0
- package/package.json +4 -4
- package/src/index.ts +19 -4
- package/src/permission-manager.ts +19 -0
- package/tests/permission-system.test.ts +259 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-05-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added `hasAllowedSkills()` method to `PermissionManager` that checks whether the resolved permission config has any explicitly allowed skills, returning true when the default skills policy is not "deny" or at least one individual skill entry has state "allow".
|
|
14
|
+
- Added skill-scoped `read` tool exception so the `read` tool remains exposed to agents with explicitly allowed skills even when the `read` tool-level permission is "deny", enabling skill file access without granting unrestricted read access.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Widened Pi peer dependency ranges to `^0.74.0 || ^0.75.0` and bumped dev dependencies to `^0.75.5`.
|
|
18
|
+
|
|
10
19
|
## [0.5.0] - 2026-05-22
|
|
11
20
|
|
|
12
21
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-permission-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@sinclair/typebox": "^0.34.49",
|
|
64
|
-
"@earendil-works/pi-ai": "^0.75.
|
|
65
|
-
"@earendil-works/pi-coding-agent": "^0.75.
|
|
66
|
-
"@earendil-works/pi-tui": "^0.75.
|
|
64
|
+
"@earendil-works/pi-ai": "^0.74.0 || ^0.75.0",
|
|
65
|
+
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0",
|
|
66
|
+
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"jsonc-parser": "^3.3.1"
|
package/src/index.ts
CHANGED
|
@@ -562,7 +562,7 @@ function formatSkillPathAskPrompt(skill: SkillPromptEntry, readPath: string, age
|
|
|
562
562
|
|
|
563
563
|
function formatSkillPathDenyReason(skill: SkillPromptEntry, readPath: string, agentName?: string): string {
|
|
564
564
|
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
565
|
-
return `${subject} is not permitted to access
|
|
565
|
+
return `${subject} is not permitted to access this skill.`;
|
|
566
566
|
}
|
|
567
567
|
|
|
568
568
|
function extractSkillNameUnderRoot(normalizedReadPath: string, normalizedSkillsRoot: string): string | null {
|
|
@@ -1734,7 +1734,18 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1734
1734
|
// This ensures that agent-specific tool deny rules (e.g., bash: deny) are respected
|
|
1735
1735
|
// before any command-level permissions are considered
|
|
1736
1736
|
const toolPermission = permissionManager.getToolPermission(toolName, agentName ?? undefined);
|
|
1737
|
-
|
|
1737
|
+
if (toolPermission !== "deny") {
|
|
1738
|
+
return true;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// If the read tool is denied but the agent has explicitly allowed skills,
|
|
1742
|
+
// expose read anyway so the agent can read skill files. The tool_call handler
|
|
1743
|
+
// will restrict reads to skill paths only.
|
|
1744
|
+
if (toolName === "read" && permissionManager.hasAllowedSkills(agentName ?? undefined)) {
|
|
1745
|
+
return true;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
return false;
|
|
1738
1749
|
};
|
|
1739
1750
|
|
|
1740
1751
|
const refreshSessionRuntimeState = (ctx: ExtensionContext): void => {
|
|
@@ -1915,7 +1926,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1915
1926
|
});
|
|
1916
1927
|
return {
|
|
1917
1928
|
block: true,
|
|
1918
|
-
reason: `Accessing skill
|
|
1929
|
+
reason: `Accessing this skill requires approval, but no interactive UI is available.`,
|
|
1919
1930
|
};
|
|
1920
1931
|
}
|
|
1921
1932
|
|
|
@@ -1931,10 +1942,14 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1931
1942
|
});
|
|
1932
1943
|
if (!decision.approved) {
|
|
1933
1944
|
const denialReason = decision.denialReason ? ` Reason: ${decision.denialReason}.` : "";
|
|
1934
|
-
return { block: true, reason: `User denied access to skill
|
|
1945
|
+
return { block: true, reason: `User denied access to this skill.${denialReason}` };
|
|
1935
1946
|
}
|
|
1936
1947
|
}
|
|
1937
1948
|
}
|
|
1949
|
+
|
|
1950
|
+
if (readSkill) {
|
|
1951
|
+
return {};
|
|
1952
|
+
}
|
|
1938
1953
|
}
|
|
1939
1954
|
|
|
1940
1955
|
const input = getEventInput(event);
|
|
@@ -732,6 +732,25 @@ export class PermissionManager {
|
|
|
732
732
|
return merged.bash || {};
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Check whether the resolved permission config has any explicitly allowed skills.
|
|
737
|
+
* Used to decide if path-bearing tools like `read` should remain exposed to an agent
|
|
738
|
+
* even when the tool-level permission is `deny`, so the agent can read skill files.
|
|
739
|
+
*
|
|
740
|
+
* Returns true when any of these conditions holds:
|
|
741
|
+
* - The default skills policy is not "deny" (allows all skills by default)
|
|
742
|
+
* - At least one individual skill entry has state "allow"
|
|
743
|
+
*/
|
|
744
|
+
hasAllowedSkills(agentName?: string): boolean {
|
|
745
|
+
const { merged } = this.resolvePermissions(agentName);
|
|
746
|
+
const defaultPolicy = merged.defaultPolicy.skills;
|
|
747
|
+
if (defaultPolicy !== "deny") {
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
const skillsRecord = merged.skills || {};
|
|
751
|
+
return Object.values(skillsRecord).some((state) => state === "allow");
|
|
752
|
+
}
|
|
753
|
+
|
|
735
754
|
private getConfiguredMcpServerNames(): readonly string[] {
|
|
736
755
|
if (this.configuredMcpServerNamesOverride) {
|
|
737
756
|
return this.configuredMcpServerNamesOverride;
|
|
@@ -1465,6 +1465,85 @@ permission:
|
|
|
1465
1465
|
}
|
|
1466
1466
|
});
|
|
1467
1467
|
|
|
1468
|
+
runTest("hasAllowedSkills detects explicitly allowed skills", () => {
|
|
1469
|
+
// Agent with specific allowed skills
|
|
1470
|
+
const { manager, cleanup } = createManager(
|
|
1471
|
+
{
|
|
1472
|
+
defaultPolicy: { tools: "ask", bash: "ask", mcp: "ask", skills: "deny", special: "ask" },
|
|
1473
|
+
skills: { "allowed-skill": "allow" },
|
|
1474
|
+
},
|
|
1475
|
+
{},
|
|
1476
|
+
);
|
|
1477
|
+
|
|
1478
|
+
try {
|
|
1479
|
+
assert.equal(manager.hasAllowedSkills(), true);
|
|
1480
|
+
} finally {
|
|
1481
|
+
cleanup();
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
runTest("hasAllowedSkills returns false when no skills are allowed", () => {
|
|
1486
|
+
const { manager, cleanup } = createManager(
|
|
1487
|
+
{
|
|
1488
|
+
defaultPolicy: { tools: "ask", bash: "ask", mcp: "ask", skills: "deny", special: "ask" },
|
|
1489
|
+
},
|
|
1490
|
+
{},
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
try {
|
|
1494
|
+
assert.equal(manager.hasAllowedSkills(), false);
|
|
1495
|
+
} finally {
|
|
1496
|
+
cleanup();
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
runTest("hasAllowedSkills returns true when default skills policy is not deny", () => {
|
|
1501
|
+
const { manager, cleanup } = createManager(
|
|
1502
|
+
{
|
|
1503
|
+
defaultPolicy: { tools: "ask", bash: "ask", mcp: "ask", skills: "allow", special: "ask" },
|
|
1504
|
+
},
|
|
1505
|
+
{},
|
|
1506
|
+
);
|
|
1507
|
+
|
|
1508
|
+
try {
|
|
1509
|
+
assert.equal(manager.hasAllowedSkills(), true);
|
|
1510
|
+
} finally {
|
|
1511
|
+
cleanup();
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
runTest("hasAllowedSkills respects per-agent skill allow overrides", () => {
|
|
1516
|
+
const { manager, cleanup } = createManager(
|
|
1517
|
+
{
|
|
1518
|
+
defaultPolicy: { tools: "ask", bash: "ask", mcp: "ask", skills: "deny", special: "ask" },
|
|
1519
|
+
skills: { "*": "deny", "reviewer-skill": "allow" },
|
|
1520
|
+
},
|
|
1521
|
+
{},
|
|
1522
|
+
);
|
|
1523
|
+
|
|
1524
|
+
try {
|
|
1525
|
+
assert.equal(manager.hasAllowedSkills(), true);
|
|
1526
|
+
} finally {
|
|
1527
|
+
cleanup();
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
runTest("hasAllowedSkills returns false for agent with all denied skills", () => {
|
|
1532
|
+
const { manager, cleanup } = createManager(
|
|
1533
|
+
{
|
|
1534
|
+
defaultPolicy: { tools: "ask", bash: "ask", mcp: "ask", skills: "deny", special: "ask" },
|
|
1535
|
+
skills: { "*": "deny" },
|
|
1536
|
+
},
|
|
1537
|
+
{},
|
|
1538
|
+
);
|
|
1539
|
+
|
|
1540
|
+
try {
|
|
1541
|
+
assert.equal(manager.hasAllowedSkills(), false);
|
|
1542
|
+
} finally {
|
|
1543
|
+
cleanup();
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1468
1547
|
runTest("getToolPermission supports arbitrary extension tool names", () => {
|
|
1469
1548
|
const { manager, cleanup } = createManager({
|
|
1470
1549
|
defaultPolicy: {
|
|
@@ -2257,7 +2336,7 @@ await runAsyncTest("tool_call blocks direct reads of denied skill files even whe
|
|
|
2257
2336
|
});
|
|
2258
2337
|
|
|
2259
2338
|
assert.equal(result.block, true);
|
|
2260
|
-
assert.match(String(result.reason), /not permitted to access
|
|
2339
|
+
assert.match(String(result.reason), /not permitted to access this skill/);
|
|
2261
2340
|
} finally {
|
|
2262
2341
|
await harness.cleanup();
|
|
2263
2342
|
}
|
|
@@ -2296,7 +2375,7 @@ await runAsyncTest("tool_call blocks reads below denied skill directories", asyn
|
|
|
2296
2375
|
});
|
|
2297
2376
|
|
|
2298
2377
|
assert.equal(result.block, true);
|
|
2299
|
-
assert.match(String(result.reason), /not permitted to access
|
|
2378
|
+
assert.match(String(result.reason), /not permitted to access this skill/);
|
|
2300
2379
|
} finally {
|
|
2301
2380
|
await harness.cleanup();
|
|
2302
2381
|
}
|
|
@@ -2323,7 +2402,7 @@ await runAsyncTest("tool_call blocks project skill reads even when the skill was
|
|
|
2323
2402
|
});
|
|
2324
2403
|
|
|
2325
2404
|
assert.equal(result.block, true);
|
|
2326
|
-
assert.match(String(result.reason), /not permitted to access
|
|
2405
|
+
assert.match(String(result.reason), /not permitted to access this skill/);
|
|
2327
2406
|
} finally {
|
|
2328
2407
|
await harness.cleanup();
|
|
2329
2408
|
}
|
|
@@ -2366,6 +2445,108 @@ await runAsyncTest("tool_call still allows reads for explicitly allowed skill fi
|
|
|
2366
2445
|
}
|
|
2367
2446
|
});
|
|
2368
2447
|
|
|
2448
|
+
await runAsyncTest("tool_call allows read of allowed skill even when read tool is deny", async () => {
|
|
2449
|
+
const harness = createToolCallHarness(
|
|
2450
|
+
{
|
|
2451
|
+
defaultPolicy: { tools: "allow", bash: "ask", mcp: "ask", skills: "deny", special: "allow" },
|
|
2452
|
+
tools: { read: "deny" },
|
|
2453
|
+
skills: { "allowed-skill": "allow" },
|
|
2454
|
+
},
|
|
2455
|
+
["read"],
|
|
2456
|
+
);
|
|
2457
|
+
const allowedSkillPath = join(harness.cwd, "skills", "allowed", "SKILL.md");
|
|
2458
|
+
const prompt = [
|
|
2459
|
+
'<active_agent name="orchestrator" mode="direct">',
|
|
2460
|
+
"<available_skills>",
|
|
2461
|
+
" <skill>",
|
|
2462
|
+
" <name>allowed-skill</name>",
|
|
2463
|
+
" <description>Allowed skill</description>",
|
|
2464
|
+
` <location>${allowedSkillPath}</location>`,
|
|
2465
|
+
" </skill>",
|
|
2466
|
+
"</available_skills>",
|
|
2467
|
+
].join("\n");
|
|
2468
|
+
|
|
2469
|
+
try {
|
|
2470
|
+
const ctx = createMockContext(harness.cwd, harness.prompts);
|
|
2471
|
+
const startResult = await Promise.resolve(harness.handlers.before_agent_start?.({ systemPrompt: prompt }, ctx)) as Record<string, unknown> | undefined;
|
|
2472
|
+
assert.equal(String(startResult?.systemPrompt ?? prompt).includes("allowed-skill"), true);
|
|
2473
|
+
|
|
2474
|
+
const result = await runToolCall(harness, {
|
|
2475
|
+
toolName: "read",
|
|
2476
|
+
toolCallId: "skill-read-overrides-read-deny",
|
|
2477
|
+
input: { path: allowedSkillPath },
|
|
2478
|
+
});
|
|
2479
|
+
|
|
2480
|
+
assert.deepEqual(result, {});
|
|
2481
|
+
} finally {
|
|
2482
|
+
await harness.cleanup();
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
await runAsyncTest("tool_call allows read of allowed skill even when tools default policy is deny", async () => {
|
|
2487
|
+
const harness = createToolCallHarness(
|
|
2488
|
+
{
|
|
2489
|
+
defaultPolicy: { tools: "deny", bash: "ask", mcp: "ask", skills: "deny", special: "allow" },
|
|
2490
|
+
skills: { "allowed-skill": "allow" },
|
|
2491
|
+
},
|
|
2492
|
+
["read"],
|
|
2493
|
+
);
|
|
2494
|
+
const allowedSkillPath = join(harness.cwd, "skills", "allowed", "SKILL.md");
|
|
2495
|
+
const prompt = [
|
|
2496
|
+
'<active_agent name="orchestrator" mode="direct">',
|
|
2497
|
+
"<available_skills>",
|
|
2498
|
+
" <skill>",
|
|
2499
|
+
" <name>allowed-skill</name>",
|
|
2500
|
+
" <description>Allowed skill</description>",
|
|
2501
|
+
` <location>${allowedSkillPath}</location>`,
|
|
2502
|
+
" </skill>",
|
|
2503
|
+
"</available_skills>",
|
|
2504
|
+
].join("\n");
|
|
2505
|
+
|
|
2506
|
+
try {
|
|
2507
|
+
const ctx = createMockContext(harness.cwd, harness.prompts);
|
|
2508
|
+
const startResult = await Promise.resolve(harness.handlers.before_agent_start?.({ systemPrompt: prompt }, ctx)) as Record<string, unknown> | undefined;
|
|
2509
|
+
assert.equal(String(startResult?.systemPrompt ?? prompt).includes("allowed-skill"), true);
|
|
2510
|
+
|
|
2511
|
+
const result = await runToolCall(harness, {
|
|
2512
|
+
toolName: "read",
|
|
2513
|
+
toolCallId: "skill-read-overrides-tools-deny",
|
|
2514
|
+
input: { path: allowedSkillPath },
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
assert.deepEqual(result, {});
|
|
2518
|
+
} finally {
|
|
2519
|
+
await harness.cleanup();
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
|
|
2523
|
+
await runAsyncTest("tool_call still blocks read of non-skill file when read tool is deny", async () => {
|
|
2524
|
+
const harness = createToolCallHarness(
|
|
2525
|
+
{
|
|
2526
|
+
defaultPolicy: { tools: "allow", bash: "ask", mcp: "ask", skills: "allow", special: "allow" },
|
|
2527
|
+
tools: { read: "deny" },
|
|
2528
|
+
},
|
|
2529
|
+
["read"],
|
|
2530
|
+
);
|
|
2531
|
+
const nonSkillPath = join(harness.cwd, "src", "main.ts");
|
|
2532
|
+
|
|
2533
|
+
try {
|
|
2534
|
+
const ctx = createMockContext(harness.cwd, harness.prompts);
|
|
2535
|
+
await Promise.resolve(harness.handlers.before_agent_start?.({ systemPrompt: "No skills in this prompt" }, ctx));
|
|
2536
|
+
|
|
2537
|
+
const result = await runToolCall(harness, {
|
|
2538
|
+
toolName: "read",
|
|
2539
|
+
toolCallId: "non-skill-read-denied",
|
|
2540
|
+
input: { path: nonSkillPath },
|
|
2541
|
+
});
|
|
2542
|
+
|
|
2543
|
+
assert.equal(result.block, true);
|
|
2544
|
+
assert.match(String(result.reason), /not permitted to run 'read'/);
|
|
2545
|
+
} finally {
|
|
2546
|
+
await harness.cleanup();
|
|
2547
|
+
}
|
|
2548
|
+
});
|
|
2549
|
+
|
|
2369
2550
|
// ---------------------------------------------------------------------------
|
|
2370
2551
|
// external_directory special permission
|
|
2371
2552
|
// ---------------------------------------------------------------------------
|
|
@@ -2838,4 +3019,79 @@ await runAsyncTest("permission review logs redact raw prompts and tool input pre
|
|
|
2838
3019
|
}
|
|
2839
3020
|
});
|
|
2840
3021
|
|
|
3022
|
+
// ---------------------------------------------------------------------------
|
|
3023
|
+
// Targeted smoke test: skill read denial via agent-level '*': deny
|
|
3024
|
+
// ---------------------------------------------------------------------------
|
|
3025
|
+
|
|
3026
|
+
await runAsyncTest("TARGETED SMOKE: agent 'code' with '*': deny blocked from reading 'test-driven-development/SKILL.md'", async () => {
|
|
3027
|
+
const harness = createToolCallHarness(
|
|
3028
|
+
{
|
|
3029
|
+
defaultPolicy: { tools: "allow", bash: "ask", mcp: "ask", skills: "allow", special: "allow" },
|
|
3030
|
+
},
|
|
3031
|
+
["read"],
|
|
3032
|
+
);
|
|
3033
|
+
const tddSkillPath = join(harness.cwd, ".pi", "agent", "skills", "test-driven-development", "SKILL.md");
|
|
3034
|
+
|
|
3035
|
+
try {
|
|
3036
|
+
writeFileSync(join(harness.baseDir, "agents", "code.md"), [
|
|
3037
|
+
"---",
|
|
3038
|
+
"name: code",
|
|
3039
|
+
"permission:",
|
|
3040
|
+
" skills:",
|
|
3041
|
+
" '*': deny",
|
|
3042
|
+
"---",
|
|
3043
|
+
"",
|
|
3044
|
+
].join("\n"), "utf8");
|
|
3045
|
+
|
|
3046
|
+
const ctx = createMockContext(harness.cwd, harness.prompts);
|
|
3047
|
+
await Promise.resolve(harness.handlers.before_agent_start?.(
|
|
3048
|
+
{ systemPrompt: '<active_agent name="code" mode="delegated">\nNo skills listed.' },
|
|
3049
|
+
ctx,
|
|
3050
|
+
));
|
|
3051
|
+
|
|
3052
|
+
// ---- Check 1: The read IS blocked ----
|
|
3053
|
+
const result = await runToolCall(harness, {
|
|
3054
|
+
toolName: "read",
|
|
3055
|
+
toolCallId: "tdd-skill-read-denied",
|
|
3056
|
+
input: { path: tddSkillPath },
|
|
3057
|
+
});
|
|
3058
|
+
assert.equal(result.block, true, "CHECK 1 FAILED: Read of skill denied via '*': deny must be blocked");
|
|
3059
|
+
console.log(" [PASS] CHECK 1: block = true (read correctly denied)");
|
|
3060
|
+
|
|
3061
|
+
// ---- Check 2: Reason does NOT contain the skill name ----
|
|
3062
|
+
const reason = String(result.reason ?? "");
|
|
3063
|
+
const nameLeaked = reason.includes("test-driven-development");
|
|
3064
|
+
console.log(" [CHECK 2] Skill name leaked: " + nameLeaked);
|
|
3065
|
+
console.log(" [CHECK 2] Reason text: " + reason);
|
|
3066
|
+
assert.equal(nameLeaked, false, "CHECK 2 FAILED: Deny reason leaks the skill name—security concern");
|
|
3067
|
+
|
|
3068
|
+
// ---- Check 3: yoloMode does NOT auto-approve a deny-state read ----
|
|
3069
|
+
const { savePermissionSystemConfig, DEFAULT_EXTENSION_CONFIG } = await import("../src/extension-config.js");
|
|
3070
|
+
savePermissionSystemConfig({ ...DEFAULT_EXTENSION_CONFIG, yoloMode: true });
|
|
3071
|
+
const yoloResult = await runToolCall(harness, {
|
|
3072
|
+
toolName: "read",
|
|
3073
|
+
toolCallId: "tdd-skill-read-yolo",
|
|
3074
|
+
input: { path: tddSkillPath },
|
|
3075
|
+
});
|
|
3076
|
+
assert.equal(yoloResult.block, true, "CHECK 3 FAILED: yoloMode must NOT auto-approve denied skill read");
|
|
3077
|
+
console.log(" [PASS] CHECK 3: yoloMode does NOT auto-approve deny-state read");
|
|
3078
|
+
savePermissionSystemConfig({ ...DEFAULT_EXTENSION_CONFIG, yoloMode: false });
|
|
3079
|
+
|
|
3080
|
+
// ---- Check 4: inferSkillEntryFromReadPath correctly identifies the skill ----
|
|
3081
|
+
// Non-skill path should NOT be blocked by skill policy
|
|
3082
|
+
const nonSkillResult = await runToolCall(harness, {
|
|
3083
|
+
toolName: "read",
|
|
3084
|
+
toolCallId: "non-skill-check",
|
|
3085
|
+
input: { path: join(harness.cwd, "src", "main.ts") },
|
|
3086
|
+
});
|
|
3087
|
+
assert.deepEqual(nonSkillResult, {}, "CHECK 4 FAILED: Non-skill reads should pass through unblocked");
|
|
3088
|
+
// The skill path IS blocked with skill-specific reason (not "not permitted to run 'read'")
|
|
3089
|
+
// This proves inference found "test-driven-development"
|
|
3090
|
+
assert.equal(reason.includes("not permitted to access this skill"), true, "CHECK 4 FAILED: Block was not skill-specific");
|
|
3091
|
+
console.log(" [PASS] CHECK 4: inferSkillEntryFromReadPath correctly identifies 'test-driven-development'");
|
|
3092
|
+
} finally {
|
|
3093
|
+
await harness.cleanup();
|
|
3094
|
+
}
|
|
3095
|
+
});
|
|
3096
|
+
|
|
2841
3097
|
console.log("All permission system tests passed.");
|