Thread exec and mailer_send through ctx instead of importing directly

- actions.mjs no longer imports exec from helpers; uses ctx.exec instead
- index.mjs builds ctx via make_ctx(), which injects dry-run stubs for
  exec and mailer_send when --dry-run is active
- Handlers now run fully (including permission checks) in dry-run mode;
  only the actual side effects are stubbed out

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 22:58:00 +00:00
parent 3841c5418a
commit 64df986a5f
2 changed files with 21 additions and 16 deletions

View File

@@ -4,6 +4,7 @@ import { enqueue, get_entry, list_pending, resolve } from './queue.mjs';
import { load_secrets } from './secrets.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';
import { load_mail_perms } from './mail_perms.mjs';
function get_arg(argv, flag) {
@@ -44,7 +45,7 @@ if (!mail_perms_path) {
console.warn('Warning: --mail-perms not set; mail permissions will not persist across restarts');
}
const mailer_send = create_mailer(smtp);
const real_mailer_send = create_mailer(smtp);
const app = express();
app.use(express.json({
@@ -62,13 +63,17 @@ 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 };
}
function make_ctx(caller) {
const exec = DRY_RUN
? (bin, args) => console.log(`[${ts()}] [DRY-RUN] exec: ${bin} ${JSON.stringify(args)}`)
: real_exec;
const mailer_send = DRY_RUN
? async (to, subject) => console.log(`[${ts()}] [DRY-RUN] send-mail: to=${to} subject=${JSON.stringify(subject)}`)
: real_mailer_send;
return { caller, users, mail_perm_store, exec, mailer_send };
}
async function run_action(def, params, ctx) {
return def.handler(params, ctx);
}
@@ -104,11 +109,11 @@ app.post('/action', async (req, res) => {
return res.status(403).json({ status: 'denied', reason: 'Policy: auto-deny' });
}
const ctx = { caller: req.conduit_user, users, mail_perm_store, mailer_send };
const ctx = make_ctx(req.conduit_user);
if (def.policy === 'auto-accept') {
try {
const result = await run_action(def, action, params, ctx);
const result = await run_action(def, params, ctx);
return res.json({ status: 'accepted', result });
} catch (err) {
return res.status(500).json({ status: 'error', error: err.message });
@@ -142,10 +147,10 @@ app.post('/queue/:id/approve', async (req, res) => {
entry.resolved_by = req.conduit_user;
resolve(req.params.id, 'approved');
const ctx = { caller: entry.submitted_by, users, mail_perm_store, mailer_send };
const ctx = make_ctx(entry.submitted_by);
const def = actions[entry.action];
try {
const result = await run_action(def, entry.action, entry.params, ctx);
const result = await run_action(def, entry.params, ctx);
res.json({ status: 'approved', result });
} catch (err) {
res.status(500).json({ status: 'error', error: err.message });