# 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.