From ba8c0701f86da40f87bf7434f9b7e21fae6ac4d0 Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Tue, 17 Mar 2026 23:16:30 +0000 Subject: [PATCH] Refactor server config: single --config flag replaces --secrets/--mail-perms - New server/config.mjs loads config.json, resolves secrets path relative to config dir, returns users/smtp/mail_perms_path/bind/port - server/secrets.mjs removed (logic absorbed into config.mjs) - smtp moves from secrets.json to config.json - secrets.json now contains only users (pure credentials) - config.example.json added as reference template - .gitignore/.npmignore updated to cover config.json and mail-perms.json - README updated with new setup and flags Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 2 ++ .npmignore | 2 ++ README.md | 42 ++++++++++++++++++++++++--------- config.example.json | 13 ++++++++++ secrets.example.json | 7 ------ server/config.mjs | 56 ++++++++++++++++++++++++++++++++++++++++++++ server/index.mjs | 18 +++++++------- server/secrets.mjs | 23 ------------------ 8 files changed, 113 insertions(+), 50 deletions(-) create mode 100644 config.example.json create mode 100644 server/config.mjs delete mode 100644 server/secrets.mjs diff --git a/.gitignore b/.gitignore index 3aed10f..e5eb3c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ secrets.json +config.json +mail-perms.json package-lock.json diff --git a/.npmignore b/.npmignore index 56415ce..3b8a235 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,4 @@ secrets.json filtered-secrets.json +config.json +mail-perms.json diff --git a/README.md b/README.md index eadc9e7..2502009 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,28 @@ ccc-keygen --filter agent --output agent-secrets.json The full `secrets.json` stays on the host. `agent-secrets.json` goes into the container. +### Create a config file + +Copy [`config.example.json`](config.example.json) to `config.json` and edit it: + +```json +{ + "secrets": "../secrets.json", + "mail_perms": "mail-perms.json", + "smtp": { + "host": "smtp.example.com", + "port": 587, + "secure": false, + "auth": { "user": "relay@example.com", "pass": "" }, + "from": "agent@example.com" + }, + "bind": "127.0.0.1", + "port": 3015 +} +``` + +`secrets` and `mail_perms` paths are resolved relative to the config file. `smtp`, `mail_perms`, `bind`, and `port` are all optional. + --- ## Running @@ -56,24 +78,22 @@ The full `secrets.json` stays on the host. `agent-secrets.json` goes into the co ### Server (host) ```bash -ccc-server --secrets secrets.json -ccc-server --secrets secrets.json --mail-perms mail-perms.json +ccc-server --config config.json +ccc-server --config config.json --dry-run ``` Server flags: | Flag | Env variable | Description | |------|-------------|-------------| -| `--secrets ` | — | Path to secrets file (required) | -| `--bind ` | `CONDUIT_BIND` | Address to bind to (default `127.0.0.1`) | -| `--mail-perms ` | `CONDUIT_MAIL_PERMS` | File to persist mail permissions (optional; in-memory only if omitted) | +| `--config ` | `CONDUIT_CONFIG` | Path to config file (required) | | `--dry-run` | — | Log all action invocations but do not execute them | -Server environment variables: -| Variable | Default | Description | -|----------|---------|-------------| -| `CONDUIT_PORT` | `3015` | Port to listen on | -| `CONDUIT_BIND` | `127.0.0.1` | Address to bind to | -| `CONDUIT_ROOT` | `/workspace` | Label printed at startup (informational only — path resolution uses `VOLUME_MAPPING` in `server/helpers.mjs`) | +Server environment variables (override config file values): +| Variable | Description | +|----------|-------------| +| `CONDUIT_PORT` | Port to listen on | +| `CONDUIT_BIND` | Address to bind to | +| `CONDUIT_ROOT` | Label printed at startup (informational only — path resolution uses `VOLUME_MAPPING` in `server/helpers.mjs`) | ### Client (container / agent) diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..3f93834 --- /dev/null +++ b/config.example.json @@ -0,0 +1,13 @@ +{ + "secrets": "../secrets.json", + "mail_perms": "mail-perms.json", + "smtp": { + "host": "smtp.example.com", + "port": 587, + "secure": false, + "auth": { "user": "relay@example.com", "pass": "" }, + "from": "agent@example.com" + }, + "bind": "127.0.0.1", + "port": 3015 +} diff --git a/secrets.example.json b/secrets.example.json index 6c50082..f93c8b7 100644 --- a/secrets.example.json +++ b/secrets.example.json @@ -1,11 +1,4 @@ { - "smtp": { - "host": "smtp.example.com", - "port": 587, - "secure": false, - "auth": { "user": "relay@example.com", "pass": "" }, - "from": "agent@example.com" - }, "users": { "": { "secret": "", "canApprove": [] }, "": { "secret": "", "canApprove": [""] } diff --git a/server/config.mjs b/server/config.mjs new file mode 100644 index 0000000..90e3041 --- /dev/null +++ b/server/config.mjs @@ -0,0 +1,56 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +export function load_config(file_path) { + if (!file_path) { + throw new Error('--config is required'); + } + let raw; + try { + raw = readFileSync(file_path, 'utf8'); + } catch (err) { + throw new Error(`Cannot read config file at ${file_path}: ${err.message}`); + } + let parsed; + try { + parsed = JSON.parse(raw); + } catch (err) { + throw new Error(`Config file is not valid JSON: ${err.message}`); + } + + if (!parsed.secrets) { + throw new Error("Config file must have a 'secrets' field pointing to the secrets file"); + } + + // Resolve secrets path relative to the config file's directory + const config_dir = resolve(file_path, '..'); + const secrets_path = resolve(config_dir, parsed.secrets); + + let secrets_raw; + try { + secrets_raw = readFileSync(secrets_path, 'utf8'); + } catch (err) { + throw new Error(`Cannot read secrets file at ${secrets_path}: ${err.message}`); + } + let secrets; + try { + secrets = JSON.parse(secrets_raw); + } catch (err) { + throw new Error(`Secrets file is not valid JSON: ${err.message}`); + } + if (!secrets.users || typeof secrets.users !== 'object') { + throw new Error("Secrets file must have a top-level 'users' object"); + } + + const mail_perms_path = parsed.mail_perms + ? resolve(config_dir, parsed.mail_perms) + : null; + + return { + users: secrets.users, + smtp: parsed.smtp ?? null, + mail_perms_path, + bind: parsed.bind ?? null, + port: parsed.port ?? null, + }; +} diff --git a/server/index.mjs b/server/index.mjs index 822024e..f6f0287 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -1,7 +1,7 @@ import express from 'express'; import { actions } from './actions.mjs'; import { enqueue, get_entry, list_pending, resolve } from './queue.mjs'; -import { load_secrets } from './secrets.mjs'; +import { load_config } from './config.mjs'; import { create_auth_middleware, check_can_approve } from './auth.mjs'; import { create_mailer } from './mailer.mjs'; import { exec as real_exec } from './helpers.mjs'; @@ -16,22 +16,22 @@ function ts() { return new Date().toLocaleTimeString(); } -const PORT = process.env.CONDUIT_PORT || 3015; -const BIND = get_arg(process.argv, '--bind') || process.env.CONDUIT_BIND || '127.0.0.1'; const VERBOSE = process.argv.includes('--verbose'); const DRY_RUN = process.argv.includes('--dry-run'); -const secrets_path = get_arg(process.argv, '--secrets'); -const mail_perms_path = get_arg(process.argv, '--mail-perms') || process.env.CONDUIT_MAIL_PERMS || null; +const config_path = get_arg(process.argv, '--config') || process.env.CONDUIT_CONFIG; -let secrets; +let cfg; try { - secrets = load_secrets(secrets_path); + cfg = load_config(config_path); } catch (err) { console.error(`Fatal: ${err.message}`); process.exit(1); } -const { users, smtp } = secrets; + +const { users, smtp, mail_perms_path } = cfg; +const PORT = process.env.CONDUIT_PORT || cfg.port || 3015; +const BIND = process.env.CONDUIT_BIND || cfg.bind || '127.0.0.1'; let mail_perm_store; try { @@ -42,7 +42,7 @@ try { } if (!mail_perms_path) { - console.warn('Warning: --mail-perms not set; mail permissions will not persist across restarts'); + console.warn('Warning: mail_perms not set in config; mail permissions will not persist across restarts'); } const real_mailer_send = create_mailer(smtp); diff --git a/server/secrets.mjs b/server/secrets.mjs deleted file mode 100644 index 0adc199..0000000 --- a/server/secrets.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import { readFileSync } from "fs"; - -export function load_secrets(file_path) { - if (!file_path) { - throw new Error("--secrets is required"); - } - let raw; - try { - raw = readFileSync(file_path, "utf8"); - } catch (err) { - throw new Error(`Cannot read secrets file at ${file_path}: ${err.message}`); - } - let parsed; - try { - parsed = JSON.parse(raw); - } catch (err) { - throw new Error(`Secrets file is not valid JSON: ${err.message}`); - } - if (!parsed.users || typeof parsed.users !== 'object') { - throw new Error("Secrets file must have a top-level 'users' object"); - } - return parsed; // callers destructure { users, smtp } -}