- 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.6 KiB
4.6 KiB
Architecture
File layout
server/
index.mjs — Express app, routing, policy enforcement
actions.mjs — Action registry (add new actions here)
auth.mjs — HMAC-SHA256 middleware, canApprove/policy checks
config.mjs — Loads config.json + secrets.json
helpers.mjs — exec(), path resolution, volume mapping
queue.mjs — In-memory queue for policy=queue actions
mail_perms.mjs — Persistent mail permission store (JSON file)
mailer.mjs — Nodemailer wrapper
policy_overrides.mjs — Runtime policy override store (JSON file)
google_calendar.mjs — Google Calendar API wrapper (googleapis)
bin/
ccc-server.mjs — Server entry point
ccc-client.mjs — CLI client (used by agent)
ccc-queue.mjs — TUI queue manager (used by human on host)
ccc-keygen.mjs — Secret generation / filtering utility
Request lifecycle
- Client signs request with HMAC-SHA256 (includes timestamp; replays rejected after 30s)
auth.mjsmiddleware validates signature, setsreq.conduit_userPOST /actionlooks up the action definition inactions.mjs- Validates required params
- Applies policy:
auto-accept→ runs handler immediately, returns resultqueue→ enqueues, returns{ status: 'queued', id }; human approves/denies via TUIauto-deny→ rejects immediately
Action handler context (ctx)
{
caller, // string — authenticated username
users, // object — full users map from secrets.json
mail_perm_store, // MailPermStore instance
exec, // (bin, args) => void — runs a host process (no-op in --dry-run)
mailer_send, // async (to, subject, body) => void
calendar, // Google_Calendar_Client | null — null if not configured
}
// Effective policy resolution (index.mjs, evaluated per request):
// 1. action's group is disabled → auto-deny
// 2. action has individual override → use it
// 3. fall back to hardcoded policy in actions.mjs
New modules are instantiated in index.mjs and added to make_ctx().
Config file (config.json)
{
"secrets": "../secrets.json", // required — path to users/secrets
"mail_perms": "mail-perms.json", // optional — persists mail permissions
"smtp": { ... }, // optional — enables send-email action
"bind": "127.0.0.1",
"port": 3015
}
All relative paths are resolved from the config file's directory.
Secrets file (secrets.json)
{
"users": {
"agent": { "secret": "<hmac-key>", "canApprove": [] },
"user": { "secret": "<hmac-key>", "canApprove": ["agent"] }
}
}
canApprove lists whose queued actions a user may approve/deny.
Path resolution
Container paths are translated to host paths via VOLUME_MAPPING in server/helpers.mjs. Paths outside known volumes are rejected. Default mapping:
| Container | Host |
|---|---|
/workspace |
<CONTAINER_PATH>/workspace |
/home/claude |
<CONTAINER_PATH>/claude-home |
Edit CONTAINER_PATH and VOLUME_MAPPING in helpers.mjs to match the local setup.
Adding a new action
// server/actions.mjs
'my-action': {
description: 'What it does',
params: [
{ name: 'foo', required: true, type: 'string' },
{ name: 'bar', required: false, type: 'path' },
],
policy: 'auto-accept', // or 'queue' | 'auto-deny'
group: 'my-group', // optional — makes it togglable in the TUI policy view
handler: ({ foo, bar }, ctx) => {
// ctx is optional — omit second arg if unused
return { result: foo };
},
},
Permission model
canApproveinsecrets.json— static root of trust, never runtime-writable- Queue approve/deny — requires
canApproveover the submitting user - Mail permissions — requires
canApproveover the target user - Policy overrides — requires
canApprove.length > 0(i.e. any user trusted to approve others)
This precedent means: to mutate permissions, you must have authority over the affected party. Agents start with canApprove: [] so they can never elevate their own access.
Adding a new injectable service (like mailer or calendar)
- Create
server/my_service.mjsexporting a factory function - Extend
load_configinserver/config.mjsto read and return the new config block - Instantiate the service in
index.mjsusing the config values - Add it to the
make_ctx()return object - Use it in action handlers via
ctx.my_service
See contributing.md for the full checklist when adding any feature.