diff --git a/README.md b/README.md index 0351395..243c49d 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Server flags: | `--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) | +| `--dry-run` | — | Log all action invocations but do not execute them | Server environment variables: | Variable | Default | Description | diff --git a/server/index.mjs b/server/index.mjs index 187f27a..35c5082 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -15,9 +15,10 @@ 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 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; @@ -61,6 +62,16 @@ app.use((req, _res, next) => { next(); }); +async function run_action(def, action, params, ctx) { + if (DRY_RUN) { + console.log(`[${ts()}] [DRY-RUN] action: ${action}`); + console.log(`[${ts()}] [DRY-RUN] caller: ${ctx.caller}`); + console.log(`[${ts()}] [DRY-RUN] params: ${JSON.stringify(params, null, 2)}`); + return { dry_run: true, action, params }; + } + return def.handler(params, ctx); +} + function validate_params(action_def, params) { const errors = []; for (const p of action_def.params) { @@ -97,7 +108,7 @@ app.post('/action', async (req, res) => { if (def.policy === 'auto-accept') { try { - const result = await def.handler(params, ctx); + const result = await run_action(def, action, params, ctx); return res.json({ status: 'accepted', result }); } catch (err) { return res.status(500).json({ status: 'error', error: err.message }); @@ -134,7 +145,7 @@ app.post('/queue/:id/approve', async (req, res) => { const ctx = { caller: entry.submitted_by, users, mail_perm_store, mailer_send }; const def = actions[entry.action]; try { - const result = await def.handler(entry.params, ctx); + const result = await run_action(def, entry.action, entry.params, ctx); res.json({ status: 'approved', result }); } catch (err) { res.status(500).json({ status: 'error', error: err.message }); @@ -162,4 +173,7 @@ app.post('/queue/:id/deny', (req, res) => { app.listen(PORT, BIND, () => { console.log(`claude-code-conduit server running on ${BIND}:${PORT}`); console.log(`Workspace root: ${process.env.CONDUIT_ROOT || '/workspace'}`); + if (DRY_RUN) { + console.log('DRY-RUN mode enabled — actions will be logged but not executed'); + } });