- 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>
104 lines
4.3 KiB
Markdown
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.
|