Files
claude-code-conduit/claude-info/architecture.md
mikael-lovqvists-claude-agent dd7743501a Add runtime policy overrides and TUI policy management view
- 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>
2026-05-21 16:01:45 +00:00

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

  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)

{
  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

  • 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 for the full checklist when adding any feature.