@gotgenes/pi-permission-system 1.0.0 → 1.2.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,34 @@ 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
+ ## [1.2.0](https://github.com/gotgenes/pi-permission-system/compare/v1.1.0...v1.2.0) (2026-05-03)
9
+
10
+
11
+ ### Features
12
+
13
+ * drop legacy settings.json fallback for MCP server names ([#19](https://github.com/gotgenes/pi-permission-system/issues/19)) ([3978f94](https://github.com/gotgenes/pi-permission-system/commit/3978f94acfe01b32e6e37c4fa0f5ca3b22881208))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * plan drop legacy settings.json MCP fallback ([#19](https://github.com/gotgenes/pi-permission-system/issues/19)) ([fd88aac](https://github.com/gotgenes/pi-permission-system/commit/fd88aac8e52c0b617b5ce2582c700bbb86b9bf84))
19
+ * **retro:** add retro notes for issue [#18](https://github.com/gotgenes/pi-permission-system/issues/18) ([1bb9cc5](https://github.com/gotgenes/pi-permission-system/commit/1bb9cc52abe159d77b8ab29960bafdb2740c9c98))
20
+
21
+ ## [1.1.0](https://github.com/gotgenes/pi-permission-system/compare/v1.0.0...v1.1.0) (2026-05-03)
22
+
23
+
24
+ ### Features
25
+
26
+ * emit deprecation warning for special.tool_call_limit ([#18](https://github.com/gotgenes/pi-permission-system/issues/18)) ([1170d40](https://github.com/gotgenes/pi-permission-system/commit/1170d401d3adc438ad3c69bf96f5264b981ed4d5))
27
+ * notify user of deprecated config fields at startup ([#18](https://github.com/gotgenes/pi-permission-system/issues/18)) ([3408672](https://github.com/gotgenes/pi-permission-system/commit/3408672afb94783f173b579e9e33fe088f0971f3))
28
+ * surface config issues from PermissionManager ([#18](https://github.com/gotgenes/pi-permission-system/issues/18)) ([4c8103b](https://github.com/gotgenes/pi-permission-system/commit/4c8103bed99c3d31813f5450a6f4d1938fd74f25))
29
+
30
+
31
+ ### Documentation
32
+
33
+ * plan drop unread special.tool_call_limit from schema ([#18](https://github.com/gotgenes/pi-permission-system/issues/18)) ([c45f6f7](https://github.com/gotgenes/pi-permission-system/commit/c45f6f7e5a504cac4f6156a97f51ff9588639d94))
34
+ * remove tool_call_limit from schema and README ([#18](https://github.com/gotgenes/pi-permission-system/issues/18)) ([780b414](https://github.com/gotgenes/pi-permission-system/commit/780b41431b1ac06ccf11dca00e1c59575386bbd2))
35
+
8
36
  ## [1.0.0](https://github.com/gotgenes/pi-permission-system/compare/v0.8.0...v1.0.0) (2026-05-03)
9
37
 
10
38
 
package/README.md CHANGED
@@ -328,7 +328,6 @@ Reserved permission checks:
328
328
  | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
329
329
  | `doom_loop` | Controls doom loop detection behavior |
330
330
  | `external_directory` | Enforces ask/allow/deny decisions for path-bearing built-in tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) when they target paths outside the active working directory |
331
- | `tool_call_limit` | _(schema only, not enforced yet)_ |
332
331
 
333
332
  ```jsonc
334
333
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -53,17 +53,6 @@
53
53
  },
54
54
  "external_directory": {
55
55
  "$ref": "#/$defs/permissionState"
56
- },
57
- "tool_call_limit": {
58
- "oneOf": [
59
- {
60
- "$ref": "#/$defs/permissionState"
61
- },
62
- {
63
- "type": "integer",
64
- "minimum": 0
65
- }
66
- ]
67
56
  }
68
57
  }
69
58
  }
package/src/index.ts CHANGED
@@ -1569,6 +1569,13 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1569
1569
  startForwardedPermissionPolling(ctx);
1570
1570
  logResolvedConfigPaths();
1571
1571
 
1572
+ const policyIssues = permissionManager.getConfigIssues(
1573
+ lastKnownActiveAgentName,
1574
+ );
1575
+ for (const issue of policyIssues) {
1576
+ notifyWarning(issue);
1577
+ }
1578
+
1572
1579
  if (event.reason === "reload") {
1573
1580
  writeDebugLog("lifecycle.reload", {
1574
1581
  triggeredBy: "session_start",
@@ -31,9 +31,6 @@ function defaultGlobalConfigPath(): string {
31
31
  function defaultAgentsDir(): string {
32
32
  return join(getAgentDir(), "agents");
33
33
  }
34
- function defaultLegacyGlobalSettingsPath(): string {
35
- return join(getAgentDir(), "settings.json");
36
- }
37
34
  function defaultGlobalMcpConfigPath(): string {
38
35
  return join(getAgentDir(), "mcp.json");
39
36
  }
@@ -237,8 +234,18 @@ function getConfiguredMcpServerNamesFromPaths(
237
234
  );
238
235
  }
239
236
 
240
- function normalizeRawPermission(raw: unknown): AgentPermissions {
237
+ const DEPRECATED_SPECIAL_KEYS: ReadonlySet<string> = new Set([
238
+ "tool_call_limit",
239
+ ]);
240
+
241
+ export interface NormalizeResult {
242
+ permissions: AgentPermissions;
243
+ configIssues: string[];
244
+ }
245
+
246
+ export function normalizeRawPermission(raw: unknown): NormalizeResult {
241
247
  const record = toRecord(raw);
248
+ const configIssues: string[] = [];
242
249
  const normalizedTools = normalizePermissionRecord(record.tools);
243
250
 
244
251
  const normalized: AgentPermissions = {
@@ -250,6 +257,20 @@ function normalizeRawPermission(raw: unknown): AgentPermissions {
250
257
  special: normalizePermissionRecord(record.special),
251
258
  };
252
259
 
260
+ // Detect deprecated keys in the raw special sub-object before discarding.
261
+ const rawSpecial = toRecord(record.special);
262
+ for (const key of DEPRECATED_SPECIAL_KEYS) {
263
+ if (key in rawSpecial) {
264
+ configIssues.push(
265
+ `special.${key} is deprecated and ignored — remove it from your policy file.`,
266
+ );
267
+ // Ensure the key is stripped even if its value was a valid PermissionState.
268
+ if (normalized.special) {
269
+ delete normalized.special[key];
270
+ }
271
+ }
272
+ }
273
+
253
274
  for (const [key, value] of Object.entries(record)) {
254
275
  if (!isPermissionState(value)) {
255
276
  continue;
@@ -265,7 +286,7 @@ function normalizeRawPermission(raw: unknown): AgentPermissions {
265
286
  }
266
287
  }
267
288
 
268
- return normalized;
289
+ return { permissions: normalized, configIssues };
269
290
  }
270
291
 
271
292
  function parseQualifiedMcpToolName(
@@ -500,7 +521,6 @@ export class PermissionManager {
500
521
  private readonly agentsDir: string;
501
522
  private readonly projectGlobalConfigPath: string | null;
502
523
  private readonly projectAgentsDir: string | null;
503
- private readonly legacyGlobalSettingsPath: string;
504
524
  private readonly globalMcpConfigPath: string;
505
525
  private readonly configuredMcpServerNamesOverride: readonly string[] | null;
506
526
  private globalConfigCache: FileCacheEntry<GlobalPermissionConfig> | null =
@@ -522,6 +542,7 @@ export class PermissionManager {
522
542
  private configuredMcpServerNamesCache: FileCacheEntry<
523
543
  readonly string[]
524
544
  > | null = null;
545
+ private accumulatedConfigIssues: string[] = [];
525
546
 
526
547
  constructor(
527
548
  options: {
@@ -529,7 +550,6 @@ export class PermissionManager {
529
550
  agentsDir?: string;
530
551
  projectGlobalConfigPath?: string;
531
552
  projectAgentsDir?: string;
532
- legacyGlobalSettingsPath?: string;
533
553
  globalMcpConfigPath?: string;
534
554
  mcpServerNames?: readonly string[];
535
555
  } = {},
@@ -539,8 +559,6 @@ export class PermissionManager {
539
559
  this.agentsDir = options.agentsDir || defaultAgentsDir();
540
560
  this.projectGlobalConfigPath = options.projectGlobalConfigPath || null;
541
561
  this.projectAgentsDir = options.projectAgentsDir || null;
542
- this.legacyGlobalSettingsPath =
543
- options.legacyGlobalSettingsPath || defaultLegacyGlobalSettingsPath();
544
562
  this.globalMcpConfigPath =
545
563
  options.globalMcpConfigPath || defaultGlobalMcpConfigPath();
546
564
  this.configuredMcpServerNamesOverride = options.mcpServerNames
@@ -554,6 +572,20 @@ export class PermissionManager {
554
572
  : null;
555
573
  }
556
574
 
575
+ private accumulateConfigIssues(issues: string[]): void {
576
+ for (const issue of issues) {
577
+ if (!this.accumulatedConfigIssues.includes(issue)) {
578
+ this.accumulatedConfigIssues.push(issue);
579
+ }
580
+ }
581
+ }
582
+
583
+ getConfigIssues(agentName?: string): string[] {
584
+ // Trigger a load/resolve to ensure issues are collected.
585
+ this.resolvePermissions(agentName);
586
+ return [...this.accumulatedConfigIssues];
587
+ }
588
+
557
589
  private loadGlobalConfig(): GlobalPermissionConfig {
558
590
  const stamp = getFileStamp(this.globalConfigPath);
559
591
  if (this.globalConfigCache?.stamp === stamp) {
@@ -564,7 +596,9 @@ export class PermissionManager {
564
596
  try {
565
597
  const raw = readFileSync(this.globalConfigPath, "utf-8");
566
598
  const parsed = JSON.parse(stripJsonComments(raw)) as unknown;
567
- const normalized = normalizeRawPermission(parsed);
599
+ const { permissions: normalized, configIssues } =
600
+ normalizeRawPermission(parsed);
601
+ this.accumulateConfigIssues(configIssues);
568
602
 
569
603
  value = {
570
604
  defaultPolicy: normalizePolicy(normalized.defaultPolicy),
@@ -596,7 +630,9 @@ export class PermissionManager {
596
630
  try {
597
631
  const raw = readFileSync(this.projectGlobalConfigPath, "utf-8");
598
632
  const parsed = JSON.parse(stripJsonComments(raw)) as unknown;
599
- value = normalizeRawPermission(parsed);
633
+ const result = normalizeRawPermission(parsed);
634
+ value = result.permissions;
635
+ this.accumulateConfigIssues(result.configIssues);
600
636
  } catch {
601
637
  value = {};
602
638
  }
@@ -629,7 +665,9 @@ export class PermissionManager {
629
665
  value = {};
630
666
  } else {
631
667
  const parsed = parseSimpleYamlMap(frontmatter);
632
- value = normalizeRawPermission(parsed.permission);
668
+ const result = normalizeRawPermission(parsed.permission);
669
+ value = result.permissions;
670
+ this.accumulateConfigIssues(result.configIssues);
633
671
  }
634
672
  } catch {
635
673
  value = {};
@@ -795,7 +833,7 @@ export class PermissionManager {
795
833
  return this.configuredMcpServerNamesOverride;
796
834
  }
797
835
 
798
- const paths = [this.globalMcpConfigPath, this.legacyGlobalSettingsPath];
836
+ const paths = [this.globalMcpConfigPath];
799
837
  const stamp = paths
800
838
  .map((path) => `${path}:${getFileStamp(path)}`)
801
839
  .join("|");
@@ -32,7 +32,10 @@ import {
32
32
  SUBAGENT_ENV_HINT_KEYS,
33
33
  SUBAGENT_PARENT_SESSION_ENV_KEY,
34
34
  } from "../src/permission-forwarding.js";
35
- import { PermissionManager } from "../src/permission-manager.js";
35
+ import {
36
+ normalizeRawPermission,
37
+ PermissionManager,
38
+ } from "../src/permission-manager.js";
36
39
  import {
37
40
  findSkillPathMatch,
38
41
  parseAllSkillPromptSections,
@@ -44,7 +47,11 @@ import {
44
47
  checkRequestedToolRegistration,
45
48
  getToolNameFromValue,
46
49
  } from "../src/tool-registry.js";
47
- import type { AgentPermissions, GlobalPermissionConfig } from "../src/types.js";
50
+ import type {
51
+ AgentPermissions,
52
+ GlobalPermissionConfig,
53
+ PermissionState,
54
+ } from "../src/types.js";
48
55
  import {
49
56
  canResolveAskPermissionRequest,
50
57
  shouldAutoApprovePermissionState,
@@ -904,6 +911,67 @@ test("MCP proxy tool infers server-prefixed aliases from configured server names
904
911
  }
905
912
  });
906
913
 
914
+ test("MCP server names in settings.json are not used — only mcp.json is consulted", () => {
915
+ const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-test-"));
916
+ const globalConfigPath = join(baseDir, "pi-permissions.jsonc");
917
+ const mcpConfigPath = join(baseDir, "mcp.json");
918
+ const settingsJsonPath = join(baseDir, "settings.json");
919
+ const agentsDir = join(baseDir, "agents");
920
+ mkdirSync(agentsDir, { recursive: true });
921
+
922
+ // Policy: allow any target prefixed with legacy-server, default mcp is ask.
923
+ // If legacy-server were known as a configured server name, a tool named
924
+ // "some_tool_legacy-server" would derive "legacy-server_some_tool_legacy-server"
925
+ // which matches this rule and returns "allow".
926
+ // After the fix, settings.json is ignored, so no server name is derived and the
927
+ // result falls through to the default mcp policy ("ask").
928
+ const config: GlobalPermissionConfig = {
929
+ defaultPolicy: {
930
+ tools: "ask",
931
+ bash: "ask",
932
+ mcp: "ask",
933
+ skills: "ask",
934
+ special: "ask",
935
+ },
936
+ tools: {},
937
+ bash: {},
938
+ mcp: { "legacy-server_*": "allow" },
939
+ skills: {},
940
+ special: {},
941
+ };
942
+
943
+ writeFileSync(
944
+ globalConfigPath,
945
+ `${JSON.stringify(config, null, 2)}\n`,
946
+ "utf8",
947
+ );
948
+ // mcp.json does not know about legacy-server.
949
+ writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers: {} }), "utf8");
950
+ // settings.json has legacy-server — the legacy source that must now be ignored.
951
+ writeFileSync(
952
+ settingsJsonPath,
953
+ JSON.stringify({ mcpServers: { "legacy-server": {} } }),
954
+ "utf8",
955
+ );
956
+
957
+ const manager = new PermissionManager({
958
+ globalConfigPath,
959
+ agentsDir,
960
+ globalMcpConfigPath: mcpConfigPath,
961
+ });
962
+
963
+ try {
964
+ // "legacy-server" must not be derived from settings.json.
965
+ // The bare tool name falls through to the default mcp policy → "ask".
966
+ const result = manager.checkPermission("mcp", {
967
+ tool: "some_tool_legacy-server",
968
+ });
969
+ assert.equal(result.state, "ask");
970
+ } finally {
971
+ rmSync(baseDir, { recursive: true, force: true });
972
+ }
973
+ });
974
+
907
975
  test("MCP describe mode normalizes qualified tool names without duplicating server prefixes", () => {
908
976
  const { manager, cleanup } = createManager(
909
977
  {
@@ -2357,3 +2425,83 @@ test("getResolvedPolicyPaths returns false for missing files and null for absent
2357
2425
  rmSync(tempDir, { recursive: true, force: true });
2358
2426
  }
2359
2427
  });
2428
+
2429
+ // --- tool_call_limit deprecation tests (#18) ---
2430
+
2431
+ test("normalizeRawPermission emits deprecation issue for special.tool_call_limit (integer)", () => {
2432
+ const result = normalizeRawPermission({ special: { tool_call_limit: 5 } });
2433
+ assert.equal(result.configIssues.length, 1);
2434
+ assert.ok(result.configIssues[0].includes("tool_call_limit"));
2435
+ assert.equal(result.permissions.special?.tool_call_limit, undefined);
2436
+ });
2437
+
2438
+ test("normalizeRawPermission emits deprecation issue for special.tool_call_limit (string)", () => {
2439
+ const result = normalizeRawPermission({
2440
+ special: { tool_call_limit: "allow" },
2441
+ });
2442
+ assert.equal(result.configIssues.length, 1);
2443
+ assert.ok(result.configIssues[0].includes("tool_call_limit"));
2444
+ assert.equal(result.permissions.special?.tool_call_limit, undefined);
2445
+ });
2446
+
2447
+ test("normalizeRawPermission emits no issues for valid special keys", () => {
2448
+ const result = normalizeRawPermission({
2449
+ special: { doom_loop: "deny" },
2450
+ });
2451
+ assert.equal(result.configIssues.length, 0);
2452
+ assert.equal(result.permissions.special?.doom_loop, "deny");
2453
+ });
2454
+
2455
+ test("normalizeRawPermission emits no issues when special is absent", () => {
2456
+ const result = normalizeRawPermission({ tools: { read: "allow" } });
2457
+ assert.equal(result.configIssues.length, 0);
2458
+ });
2459
+
2460
+ test("PermissionManager.getConfigIssues returns deprecation for tool_call_limit in global config", () => {
2461
+ const config: GlobalPermissionConfig = {
2462
+ defaultPolicy: {
2463
+ tools: "ask",
2464
+ bash: "ask",
2465
+ mcp: "ask",
2466
+ skills: "ask",
2467
+ special: "ask",
2468
+ },
2469
+ tools: {},
2470
+ bash: {},
2471
+ mcp: {},
2472
+ skills: {},
2473
+ special: { tool_call_limit: "allow" as PermissionState, doom_loop: "deny" },
2474
+ };
2475
+ const { manager, cleanup } = createManager(config);
2476
+ try {
2477
+ const issues = manager.getConfigIssues();
2478
+ assert.equal(issues.length, 1);
2479
+ assert.ok(issues[0].includes("tool_call_limit"));
2480
+ } finally {
2481
+ cleanup();
2482
+ }
2483
+ });
2484
+
2485
+ test("PermissionManager.getConfigIssues returns empty array for clean config", () => {
2486
+ const config: GlobalPermissionConfig = {
2487
+ defaultPolicy: {
2488
+ tools: "ask",
2489
+ bash: "ask",
2490
+ mcp: "ask",
2491
+ skills: "ask",
2492
+ special: "ask",
2493
+ },
2494
+ tools: {},
2495
+ bash: {},
2496
+ mcp: {},
2497
+ skills: {},
2498
+ special: { doom_loop: "deny" },
2499
+ };
2500
+ const { manager, cleanup } = createManager(config);
2501
+ try {
2502
+ const issues = manager.getConfigIssues();
2503
+ assert.equal(issues.length, 0);
2504
+ } finally {
2505
+ cleanup();
2506
+ }
2507
+ });