- 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>
4.3 KiB
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:
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:
{
"groups": { "calendar": "disabled" },
"actions": { "calendar-list-events": "queue" }
}
Effective policy resolution (in order of precedence):
- Action's group is
"disabled"→auto-deny - Action has an individual override → use it
- 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.