# 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": "", "canApprove": [] }, "user": { "secret": "", "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` | `/workspace` | | `/home/claude` | `/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.