--- issue: 18 issue_title: "Drop unread special.tool_call_limit from permissions schema" --- # Drop unread `special.tool_call_limit` from permissions schema ## Problem Statement `schemas/permissions.schema.json` declares `special.tool_call_limit` with a `oneOf [permissionState, integer]` shape, but no runtime code reads it. `SpecialPermissionName` in `src/types.ts` and `SPECIAL_PERMISSION_KEYS` in `src/permission-manager.ts` both omit `tool_call_limit`. AGENTS.md is explicit: *"Treat any declared config field not read at runtime as a maintenance trap.* *Remove it or document its purpose."* The field also appears in the `README.md` special-permissions table with the note *"schema only, not enforced yet"*. ## Goals - Remove `special.tool_call_limit` from `schemas/permissions.schema.json`. - Remove the `tool_call_limit` row from the `README.md` special-permissions table. - Add a tolerant-loader deprecation warning: if a user's parsed policy contains `special.tool_call_limit`, emit a single non-fatal config issue per occurrence and discard the value. - Add tests covering the deprecation warning path. ## Non-Goals - Implementing a tool-call-limit feature. If we want one later, file a fresh issue with a real implementation, schema entry, example, and tests in lockstep. - Changing any other permission surface or default policy state. ## Background ### Relevant modules | File | Role | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `schemas/permissions.schema.json` | Declares `special.tool_call_limit` — the field to remove. | | `src/permission-manager.ts` | `normalizePermissionRecord()` already silently discards integer values (they fail the `isPermissionState` check). If the user writes `"tool_call_limit": "allow"`, it would survive normalization but is never read by `SPECIAL_PERMISSION_KEYS`. The deprecation warning needs to fire *before* the value is discarded. | | `src/types.ts` | `SpecialPermissionName` does not include `tool_call_limit` — no change needed. | | `config/config.example.json` | Does not reference `tool_call_limit` — no change needed. | | `README.md` | Contains one row in the special-permissions table for `tool_call_limit` marked as *schema only, not enforced yet*. | ### Permission surface `special` — but only the schema and docs are affected. No runtime permission decisions change because the key was never read. ## Design Overview ### Schema change Remove the `tool_call_limit` property from `special` in `schemas/permissions.schema.json`. The `special` object retains `doom_loop` and `external_directory`. ### Deprecation warning AGENTS.md's Configuration rules require: > When removing a previously accepted config field, keep the loader tolerant: accept the legacy key, emit a single non-fatal config issue per occurrence describing the deprecation, and discard the value. The right place to emit the warning is inside `normalizeRawPermission()` in `src/permission-manager.ts`, since that is the single normalization gateway for both global and per-agent configs. Today it returns a plain `AgentPermissions` object with no side channel for warnings. **Approach:** Add a `configIssues` array to the return type (or use a parallel mechanism) so callers can surface deprecation messages. Concretely: ```typescript interface NormalizeResult { permissions: AgentPermissions; configIssues: string[]; } ``` `normalizeRawPermission()` checks for `tool_call_limit` in the `special` sub-object of the raw input. If found, it pushes a message like: ```text special.tool_call_limit is deprecated and ignored — remove it from your policy file. ``` The value is discarded as today (integer values already fail `isPermissionState`; string values would be stripped from the normalized output explicitly). The `configIssues` array is threaded up through `loadGlobalConfig()`, `loadProjectGlobalConfig()`, `loadAgentPermissions()`, and exposed via a new `getConfigIssues(agentName?)` method on `PermissionManager`. The extension entry point (`src/index.ts`) already has a warning-notification path (`notifyWarning`) used for misplaced-key detection; the deprecation issues can be surfaced through the same channel. ### Merge precedence No change — global → project → per-agent remains the same. The deprecation warning fires independently at each layer that contains the key. ## Module-Level Changes ### `schemas/permissions.schema.json` — changed Remove the `tool_call_limit` property (and its `oneOf` definition) from the `special` object. ### `src/permission-manager.ts` — changed - Extend `normalizeRawPermission()` to return config issues alongside the normalized permissions (new `NormalizeResult` type or equivalent). - Detect `tool_call_limit` in the raw `special` sub-object and push a deprecation message. - Explicitly strip `tool_call_limit` from the normalized `special` record (currently happens implicitly for integer values but not for valid PermissionState strings). - Thread config issues through the load methods and cache them. - Add `getConfigIssues(agentName?): string[]` to `PermissionManager`. ### `src/index.ts` — changed - After loading permissions, call `getConfigIssues()` and surface any messages through the existing `notifyWarning` path (same pattern as misplaced-key detection). ### `README.md` — changed - Remove the `tool_call_limit` row from the `### special` permissions table. ### `tests/` — new or changed test file - Test that `normalizeRawPermission` (or the new wrapper) emits a deprecation issue when `special.tool_call_limit` is present (both integer and string forms). - Test that the normalized output does not contain `tool_call_limit` in `special`. - Test that configs without `tool_call_limit` produce no deprecation issues. ## TDD Order 1. **Red: deprecation detection for `special.tool_call_limit`.** Write a test that calls the normalization function with `{ special: { tool_call_limit: 5 } }` and asserts a config-issue string is returned containing `"tool_call_limit"`. Write a second case with `{ special: { tool_call_limit: "allow" } }`. Write a third case with `{ special: { doom_loop: "deny" } }` asserting no issues. Commit: `test: cover tool_call_limit deprecation warning (#18)` 2. **Green: implement deprecation detection in normalizer.** Extend `normalizeRawPermission()` to return config issues. Detect and warn on `tool_call_limit`; explicitly strip it from the output. Commit: `feat: emit deprecation warning for special.tool_call_limit (#18)` 3. **Red → Green: `PermissionManager.getConfigIssues()` integration.** Write a test constructing a `PermissionManager` with a temp config containing `special.tool_call_limit` and assert `getConfigIssues()` returns the deprecation message. Implement `getConfigIssues()` on `PermissionManager` by threading issues through the load path. Commit: `feat: surface config issues from PermissionManager (#18)` 4. **Schema and docs cleanup.** Remove `tool_call_limit` from `schemas/permissions.schema.json`. Remove the `tool_call_limit` row from `README.md`. Commit: `docs: remove tool_call_limit from schema and README (#18)` 5. **Wire warning into extension entry point.** In `src/index.ts`, call `getConfigIssues()` during initialization and surface messages via `notifyWarning`. Commit: `feat: notify user of deprecated config fields at startup (#18)` ## Risks and Mitigations | Risk | Mitigation | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Could this silently weaken a permission?** | No. `tool_call_limit` was never enforced — removing it changes zero runtime decisions. The deprecation warning makes the removal *more* visible, not less. | | **Users with `tool_call_limit` in their config get a schema-validation error.** | The schema drops the field, but the loader remains tolerant: it parses with `stripJsonComments` + `JSON.parse`, not schema validation. The deprecation warning tells the user to remove it. | | **On-disk identity change.** | None. Config directory, log filenames, `/permission-system` slash command, and event channel names are untouched. | | **`normalizeRawPermission` return-type change ripples through callers.** | The change is internal to `permission-manager.ts`. All call sites are in the same file. The public API gains only an additive `getConfigIssues()` method. | ## Open Questions None — the scope and approach are unambiguous.