Files
claude-code-conduit/claude-info/plan-policy-overrides.md
mikael-lovqvists-claude-agent dd7743501a Add runtime policy overrides and TUI policy management view
- server/policy_overrides.mjs: persistent group/action override store
- Group toggle: disabling a group forces all its actions to auto-deny without
  touching individual overrides; re-enabling restores them intact
- Action overrides: cycle auto-accept/queue/auto-deny/clear per action
- Effective policy resolution in index.mjs: group > action override > hardcoded default
- auth.mjs: add check_can_manage_policies (requires canApprove.length > 0)
- Three new endpoints: GET /policies, POST /policies/group/:group, POST /policies/action/:action
- client/conduit.mjs: get_policies, set_group_policy, set_action_policy
- ccc-queue.mjs: policy view (Tab to switch); group/action panel with space/e/←/→
- actions.mjs: group fields on desktop/email/calendar actions; list-actions includes group
- config.example.json: add policy_overrides key
- Bump version to 1.3.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 16:01:45 +00:00

104 lines
4.3 KiB
Markdown

# Plan: Runtime policy overrides & TUI policy management
Status: **implemented**
## Permission model
**Principle: to mutate permissions, you must have authority over the affected party.**
This mirrors `set-mail-permission`, which requires `canApprove` over the target user. Policy overrides affect all agent users, so the caller must have `canApprove` over at least one other user — making them effectively a trusted human.
Implemented as `check_can_manage_policies(users, caller)` in `server/auth.mjs`:
```js
return (users[caller]?.canApprove?.length ?? 0) > 0;
```
**Why this matters as a precedent:** any future permission-mutating operation in CCC (or derived systems) should require the same gate. Agents start with `canApprove: []` by design, so they can never grant themselves new access. Only humans can change what agents are allowed to do.
**The non-negotiable boundary (from issue #3):** `canApprove` itself is never runtime-writable. It is loaded from `secrets.json` at startup and is the root of the entire trust tree.
---
## Data model
Two independent stores, never entangled:
```json
{
"groups": { "calendar": "disabled" },
"actions": { "calendar-list-events": "queue" }
}
```
**Effective policy resolution** (in order of precedence):
1. Action's group is `"disabled"``auto-deny`
2. Action has an individual override → use it
3. Fall back to hardcoded policy in `actions.mjs`
**Key invariant:** group toggling never touches action overrides. Disabling a group just sets the group flag. Re-enabling it removes the flag — individual overrides snap back intact.
---
## Group membership
Actions in `actions.mjs` carry an optional `group` field:
| Group | Actions |
|---|---|
| `calendar` | all four calendar actions |
| `email` | `send-email` only (permission management actions are not gated by this) |
| `desktop` | `edit-file`, `open-browser`, `open-terminal` |
`list-actions`, `set-mail-permission`, `get-mail-permissions`, and meta-actions have no group.
---
## New files
- `server/policy_overrides.mjs` — load/save store, `effective()`, `set_group()`, `set_action()`, `get_all()`
---
## Changed files
| File | Change |
|---|---|
| `server/auth.mjs` | Add `check_can_manage_policies` |
| `server/config.mjs` | Add `policy_overrides_path` |
| `server/index.mjs` | Wire up store; use effective policy in action dispatch; add `/policies` endpoints |
| `server/actions.mjs` | Add `group` field; update `list-actions` to include group |
| `client/conduit.mjs` | Add `get_policies`, `set_group_policy`, `set_action_policy` |
| `bin/ccc-queue.mjs` | Policy view: group toggles + action policy cycling |
| `config.example.json` | Add `policy_overrides` key |
---
## API endpoints (human-only)
| Method | Path | Auth required | Description |
|---|---|---|---|
| `GET` | `/policies` | any authenticated | Returns full policy state |
| `POST` | `/policies/group/:group` | `canApprove.length > 0` | Toggle group disabled state |
| `POST` | `/policies/action/:action` | `canApprove.length > 0` | Set or clear action override |
---
## TUI: policy view
`[Tab]` switches between queue view and policies view.
```
┌─ Groups ──────────────────────┐ ┌─ Actions ──────────────────────────────────┐
│ │ │ │
│ > [●] calendar │ │ auto-accept calendar-list-events │
│ [●] email │ │ queue calendar-create-event │
│ [●] desktop │ │ queue calendar-update-event │
│ │ │ queue calendar-delete-event │
└───────────────────────────────┘ └────────────────────────────────────────────┘
[space] toggle group [→] edit actions [tab] queue view [q] quit
```
When a group is disabled, actions render greyed with `auto-deny`. `[e]` on the right panel cycles an action's override: `auto-accept → queue → auto-deny → (clear/default) → …`.
Action overrides display a `*` marker when a non-default policy is set.