@gotgenes/pi-permission-system 1.0.0 → 1.1.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,21 @@ 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.1.0](https://github.com/gotgenes/pi-permission-system/compare/v1.0.0...v1.1.0) (2026-05-03)
9
+
10
+
11
+ ### Features
12
+
13
+ * 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))
14
+ * 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))
15
+ * 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))
16
+
17
+
18
+ ### Documentation
19
+
20
+ * 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))
21
+ * 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))
22
+
8
23
  ## [1.0.0](https://github.com/gotgenes/pi-permission-system/compare/v0.8.0...v1.0.0) (2026-05-03)
9
24
 
10
25
 
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.1.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",
@@ -237,8 +237,18 @@ function getConfiguredMcpServerNamesFromPaths(
237
237
  );
238
238
  }
239
239
 
240
- function normalizeRawPermission(raw: unknown): AgentPermissions {
240
+ const DEPRECATED_SPECIAL_KEYS: ReadonlySet<string> = new Set([
241
+ "tool_call_limit",
242
+ ]);
243
+
244
+ export interface NormalizeResult {
245
+ permissions: AgentPermissions;
246
+ configIssues: string[];
247
+ }
248
+
249
+ export function normalizeRawPermission(raw: unknown): NormalizeResult {
241
250
  const record = toRecord(raw);
251
+ const configIssues: string[] = [];
242
252
  const normalizedTools = normalizePermissionRecord(record.tools);
243
253
 
244
254
  const normalized: AgentPermissions = {
@@ -250,6 +260,20 @@ function normalizeRawPermission(raw: unknown): AgentPermissions {
250
260
  special: normalizePermissionRecord(record.special),
251
261
  };
252
262
 
263
+ // Detect deprecated keys in the raw special sub-object before discarding.
264
+ const rawSpecial = toRecord(record.special);
265
+ for (const key of DEPRECATED_SPECIAL_KEYS) {
266
+ if (key in rawSpecial) {
267
+ configIssues.push(
268
+ `special.${key} is deprecated and ignored — remove it from your policy file.`,
269
+ );
270
+ // Ensure the key is stripped even if its value was a valid PermissionState.
271
+ if (normalized.special) {
272
+ delete normalized.special[key];
273
+ }
274
+ }
275
+ }
276
+
253
277
  for (const [key, value] of Object.entries(record)) {
254
278
  if (!isPermissionState(value)) {
255
279
  continue;
@@ -265,7 +289,7 @@ function normalizeRawPermission(raw: unknown): AgentPermissions {
265
289
  }
266
290
  }
267
291
 
268
- return normalized;
292
+ return { permissions: normalized, configIssues };
269
293
  }
270
294
 
271
295
  function parseQualifiedMcpToolName(
@@ -522,6 +546,7 @@ export class PermissionManager {
522
546
  private configuredMcpServerNamesCache: FileCacheEntry<
523
547
  readonly string[]
524
548
  > | null = null;
549
+ private accumulatedConfigIssues: string[] = [];
525
550
 
526
551
  constructor(
527
552
  options: {
@@ -554,6 +579,20 @@ export class PermissionManager {
554
579
  : null;
555
580
  }
556
581
 
582
+ private accumulateConfigIssues(issues: string[]): void {
583
+ for (const issue of issues) {
584
+ if (!this.accumulatedConfigIssues.includes(issue)) {
585
+ this.accumulatedConfigIssues.push(issue);
586
+ }
587
+ }
588
+ }
589
+
590
+ getConfigIssues(agentName?: string): string[] {
591
+ // Trigger a load/resolve to ensure issues are collected.
592
+ this.resolvePermissions(agentName);
593
+ return [...this.accumulatedConfigIssues];
594
+ }
595
+
557
596
  private loadGlobalConfig(): GlobalPermissionConfig {
558
597
  const stamp = getFileStamp(this.globalConfigPath);
559
598
  if (this.globalConfigCache?.stamp === stamp) {
@@ -564,7 +603,9 @@ export class PermissionManager {
564
603
  try {
565
604
  const raw = readFileSync(this.globalConfigPath, "utf-8");
566
605
  const parsed = JSON.parse(stripJsonComments(raw)) as unknown;
567
- const normalized = normalizeRawPermission(parsed);
606
+ const { permissions: normalized, configIssues } =
607
+ normalizeRawPermission(parsed);
608
+ this.accumulateConfigIssues(configIssues);
568
609
 
569
610
  value = {
570
611
  defaultPolicy: normalizePolicy(normalized.defaultPolicy),
@@ -596,7 +637,9 @@ export class PermissionManager {
596
637
  try {
597
638
  const raw = readFileSync(this.projectGlobalConfigPath, "utf-8");
598
639
  const parsed = JSON.parse(stripJsonComments(raw)) as unknown;
599
- value = normalizeRawPermission(parsed);
640
+ const result = normalizeRawPermission(parsed);
641
+ value = result.permissions;
642
+ this.accumulateConfigIssues(result.configIssues);
600
643
  } catch {
601
644
  value = {};
602
645
  }
@@ -629,7 +672,9 @@ export class PermissionManager {
629
672
  value = {};
630
673
  } else {
631
674
  const parsed = parseSimpleYamlMap(frontmatter);
632
- value = normalizeRawPermission(parsed.permission);
675
+ const result = normalizeRawPermission(parsed.permission);
676
+ value = result.permissions;
677
+ this.accumulateConfigIssues(result.configIssues);
633
678
  }
634
679
  } catch {
635
680
  value = {};
@@ -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,
@@ -2357,3 +2364,83 @@ test("getResolvedPolicyPaths returns false for missing files and null for absent
2357
2364
  rmSync(tempDir, { recursive: true, force: true });
2358
2365
  }
2359
2366
  });
2367
+
2368
+ // --- tool_call_limit deprecation tests (#18) ---
2369
+
2370
+ test("normalizeRawPermission emits deprecation issue for special.tool_call_limit (integer)", () => {
2371
+ const result = normalizeRawPermission({ special: { tool_call_limit: 5 } });
2372
+ assert.equal(result.configIssues.length, 1);
2373
+ assert.ok(result.configIssues[0].includes("tool_call_limit"));
2374
+ assert.equal(result.permissions.special?.tool_call_limit, undefined);
2375
+ });
2376
+
2377
+ test("normalizeRawPermission emits deprecation issue for special.tool_call_limit (string)", () => {
2378
+ const result = normalizeRawPermission({
2379
+ special: { tool_call_limit: "allow" },
2380
+ });
2381
+ assert.equal(result.configIssues.length, 1);
2382
+ assert.ok(result.configIssues[0].includes("tool_call_limit"));
2383
+ assert.equal(result.permissions.special?.tool_call_limit, undefined);
2384
+ });
2385
+
2386
+ test("normalizeRawPermission emits no issues for valid special keys", () => {
2387
+ const result = normalizeRawPermission({
2388
+ special: { doom_loop: "deny" },
2389
+ });
2390
+ assert.equal(result.configIssues.length, 0);
2391
+ assert.equal(result.permissions.special?.doom_loop, "deny");
2392
+ });
2393
+
2394
+ test("normalizeRawPermission emits no issues when special is absent", () => {
2395
+ const result = normalizeRawPermission({ tools: { read: "allow" } });
2396
+ assert.equal(result.configIssues.length, 0);
2397
+ });
2398
+
2399
+ test("PermissionManager.getConfigIssues returns deprecation for tool_call_limit in global config", () => {
2400
+ const config: GlobalPermissionConfig = {
2401
+ defaultPolicy: {
2402
+ tools: "ask",
2403
+ bash: "ask",
2404
+ mcp: "ask",
2405
+ skills: "ask",
2406
+ special: "ask",
2407
+ },
2408
+ tools: {},
2409
+ bash: {},
2410
+ mcp: {},
2411
+ skills: {},
2412
+ special: { tool_call_limit: "allow" as PermissionState, doom_loop: "deny" },
2413
+ };
2414
+ const { manager, cleanup } = createManager(config);
2415
+ try {
2416
+ const issues = manager.getConfigIssues();
2417
+ assert.equal(issues.length, 1);
2418
+ assert.ok(issues[0].includes("tool_call_limit"));
2419
+ } finally {
2420
+ cleanup();
2421
+ }
2422
+ });
2423
+
2424
+ test("PermissionManager.getConfigIssues returns empty array for clean config", () => {
2425
+ const config: GlobalPermissionConfig = {
2426
+ defaultPolicy: {
2427
+ tools: "ask",
2428
+ bash: "ask",
2429
+ mcp: "ask",
2430
+ skills: "ask",
2431
+ special: "ask",
2432
+ },
2433
+ tools: {},
2434
+ bash: {},
2435
+ mcp: {},
2436
+ skills: {},
2437
+ special: { doom_loop: "deny" },
2438
+ };
2439
+ const { manager, cleanup } = createManager(config);
2440
+ try {
2441
+ const issues = manager.getConfigIssues();
2442
+ assert.equal(issues.length, 0);
2443
+ } finally {
2444
+ cleanup();
2445
+ }
2446
+ });