- 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>
131 lines
4.6 KiB
Markdown
131 lines
4.6 KiB
Markdown
# 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
|
|
|
|
1. Client signs request with HMAC-SHA256 (includes timestamp; replays rejected after 30s)
|
|
2. `auth.mjs` middleware validates signature, sets `req.conduit_user`
|
|
3. `POST /action` looks up the action definition in `actions.mjs`
|
|
4. Validates required params
|
|
5. Applies policy:
|
|
- `auto-accept` → runs handler immediately, returns result
|
|
- `queue` → enqueues, returns `{ status: 'queued', id }`; human approves/denies via TUI
|
|
- `auto-deny` → rejects immediately
|
|
|
|
## Action handler context (`ctx`)
|
|
|
|
```js
|
|
{
|
|
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`)
|
|
|
|
```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`)
|
|
|
|
```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
|
|
|
|
```js
|
|
// 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
|
|
|
|
- `canApprove` in `secrets.json` — static root of trust, never runtime-writable
|
|
- Queue approve/deny — requires `canApprove` over the submitting user
|
|
- Mail permissions — requires `canApprove` over 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)
|
|
|
|
1. Create `server/my_service.mjs` exporting a factory function
|
|
2. Extend `load_config` in `server/config.mjs` to read and return the new config block
|
|
3. Instantiate the service in `index.mjs` using the config values
|
|
4. Add it to the `make_ctx()` return object
|
|
5. Use it in action handlers via `ctx.my_service`
|
|
|
|
See [`contributing.md`](contributing.md) for the full checklist when adding any feature.
|