From f2d32a3faa7732aa2df13779a5073138bc6bc262 Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Sat, 7 Mar 2026 21:34:35 +0000 Subject: [PATCH] Add request logging, simplify queue output, bump to v1.0.0 Each request logs timestamp, method, path and user. Queue entries log a single line on enqueue and on resolve. Drop the verbose approve/deny curl instructions from queue output. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- server/index.mjs | 75 +++++++++++++++++++++++++++--------------------- server/queue.mjs | 20 ++++++------- 3 files changed, 54 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 3454e4d..2e684cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-code-conduit", - "version": "0.1.0", + "version": "1.0.0", "description": "A supervised action bridge between Claude Code and the host system", "type": "module", "scripts": { diff --git a/server/index.mjs b/server/index.mjs index bbaff76..a5ba75d 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -1,18 +1,22 @@ -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 { create_auth_middleware, check_can_approve } from "./auth.mjs"; +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 { create_auth_middleware, check_can_approve } from './auth.mjs'; function get_arg(argv, flag) { const i = argv.indexOf(flag); return i !== -1 ? argv[i + 1] : null; } +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 secrets_path = get_arg(process.argv, "--secrets"); +const secrets_path = get_arg(process.argv, '--secrets'); let secrets; try { secrets = load_secrets(secrets_path); @@ -25,11 +29,16 @@ const { users } = secrets; const app = express(); app.use(express.json({ verify: (req, _res, buf) => { - req.raw_body = buf.toString("utf8"); + req.raw_body = buf.toString('utf8'); }, })); app.use(create_auth_middleware(users)); +app.use((req, _res, next) => { + console.log(`[${ts()}] ${req.method} ${req.path} — ${req.conduit_user}`); + next(); +}); + function validate_params(action_def, params) { const errors = []; for (const p of action_def.params) { @@ -41,7 +50,7 @@ function validate_params(action_def, params) { } // POST /action — main entry point -app.post("/action", async (req, res) => { +app.post('/action', async (req, res) => { const { action, ...params } = req.body ?? {}; if (!action) { @@ -55,72 +64,74 @@ app.post("/action", async (req, res) => { const errors = validate_params(def, params); if (errors.length) { - return res.status(400).json({ error: "Invalid params", details: errors }); + return res.status(400).json({ error: 'Invalid params', details: errors }); } - if (def.policy === "auto-deny") { - return res.status(403).json({ status: "denied", reason: "Policy: auto-deny" }); + if (def.policy === 'auto-deny') { + return res.status(403).json({ status: 'denied', reason: 'Policy: auto-deny' }); } - if (def.policy === "auto-accept") { + if (def.policy === 'auto-accept') { try { const result = await def.handler(params); - return res.json({ status: "accepted", result }); + return res.json({ status: 'accepted', result }); } catch (err) { - return res.status(500).json({ status: "error", error: err.message }); + return res.status(500).json({ status: 'error', error: err.message }); } } - if (def.policy === "queue") { + if (def.policy === 'queue') { const id = enqueue(action, params, req.conduit_user); - return res.status(202).json({ status: "queued", id }); + return res.status(202).json({ status: 'queued', id }); } }); // GET /queue — list pending items -app.get("/queue", (req, res) => { +app.get('/queue', (req, res) => { res.json(list_pending()); }); // POST /queue/:id/approve — user approves a queued action -app.post("/queue/:id/approve", async (req, res) => { +app.post('/queue/:id/approve', async (req, res) => { const entry = get_entry(req.params.id); if (!entry) { - return res.status(404).json({ error: "Not found" }); + return res.status(404).json({ error: 'Not found' }); } - if (entry.status !== "pending") { - return res.status(409).json({ error: "Already resolved" }); + if (entry.status !== 'pending') { + return res.status(409).json({ error: 'Already resolved' }); } if (!check_can_approve(users, req.conduit_user, entry.submitted_by)) { - return res.status(403).json({ error: "Not authorized to approve this entry" }); + return res.status(403).json({ error: 'Not authorized to approve this entry' }); } - resolve(req.params.id, "approved"); + entry.resolved_by = req.conduit_user; + resolve(req.params.id, 'approved'); const def = actions[entry.action]; try { const result = await def.handler(entry.params); - res.json({ status: "approved", result }); + res.json({ status: 'approved', result }); } catch (err) { - res.status(500).json({ status: "error", error: err.message }); + res.status(500).json({ status: 'error', error: err.message }); } }); // POST /queue/:id/deny — user denies a queued action -app.post("/queue/:id/deny", (req, res) => { +app.post('/queue/:id/deny', (req, res) => { const entry = get_entry(req.params.id); if (!entry) { - return res.status(404).json({ error: "Not found" }); + return res.status(404).json({ error: 'Not found' }); } - if (entry.status !== "pending") { - return res.status(409).json({ error: "Already resolved" }); + if (entry.status !== 'pending') { + return res.status(409).json({ error: 'Already resolved' }); } if (!check_can_approve(users, req.conduit_user, entry.submitted_by)) { - return res.status(403).json({ error: "Not authorized to deny this entry" }); + return res.status(403).json({ error: 'Not authorized to deny this entry' }); } - resolve(req.params.id, "denied"); - res.json({ status: "denied" }); + entry.resolved_by = req.conduit_user; + resolve(req.params.id, 'denied'); + res.json({ status: 'denied' }); }); app.listen(PORT, BIND, () => { diff --git a/server/queue.mjs b/server/queue.mjs index fa28e7e..8125607 100644 --- a/server/queue.mjs +++ b/server/queue.mjs @@ -1,7 +1,11 @@ -import { randomUUID } from "crypto"; +import { randomUUID } from 'crypto'; const pending = new Map(); +function ts() { + return new Date().toLocaleTimeString(); +} + export function enqueue(action, params, submitted_by) { const id = randomUUID(); const entry = { @@ -9,16 +13,11 @@ export function enqueue(action, params, submitted_by) { action, params, submitted_by, - status: "pending", + status: 'pending', created_at: new Date().toISOString(), }; pending.set(id, entry); - console.log(`\n[QUEUE] New request #${id.slice(0, 8)}`); - console.log(` Action: ${action}`); - console.log(` Params: ${JSON.stringify(params)}`); - console.log(` Submitted by: ${submitted_by}`); - console.log(` Approve: POST /queue/${id}/approve`); - console.log(` Deny: POST /queue/${id}/deny\n`); + console.log(`[${ts()}] [QUEUE] ${submitted_by} requested '${action}' (${id.slice(0, 8)}) — params: ${JSON.stringify(params)}`); return id; } @@ -27,7 +26,7 @@ export function get_entry(id) { } export function list_pending() { - return [...pending.values()].filter((e) => e.status === "pending"); + return [...pending.values()].filter((e) => e.status === 'pending'); } export function resolve(id, decision) { @@ -35,7 +34,8 @@ export function resolve(id, decision) { if (!entry) { return null; } - entry.status = decision; // "approved" | "denied" + entry.status = decision; // 'approved' | 'denied' entry.resolved_at = new Date().toISOString(); + console.log(`[${ts()}] [QUEUE] ${id.slice(0, 8)} ${decision} by ${entry.resolved_by ?? 'unknown'}`); return entry; }