Files
claude-code-conduit/client/index.mjs
mikael-lovqvists-claude-agent 67c1c3f9a4 Add HMAC auth, user permissions, snake_case rename
Each request is signed with HMAC-SHA256 over timestamp+body using a
per-user secret loaded from a --secrets file (never env vars or git).
Users have a canApprove list controlling who may approve queued actions.
Queue entries track submitted_by for permission checks on approve/deny.

Also renames all identifiers to snake_case throughout the codebase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 20:18:41 +00:00

94 lines
2.7 KiB
JavaScript

#!/usr/bin/env node
// Conduit client — thin CLI wrapper for Claude to call the conduit server.
// Usage:
// node client/index.mjs --secrets /path/to/secrets.json --user agent <action> [key=value ...]
// node client/index.mjs --secrets /path/to/secrets.json --user agent list-actions
// node client/index.mjs --secrets /path/to/secrets.json --user agent edit-file filename=/workspace/foo.mjs
import { readFileSync } from "fs";
import { sign_request } from "./auth.mjs";
const BASE_URL = process.env.CONDUIT_URL || "http://localhost:3015";
function get_arg(argv, flag) {
const i = argv.indexOf(flag);
return i !== -1 ? argv[i + 1] : null;
}
async function call_action(action, params, auth_headers) {
const body_string = JSON.stringify({ action, ...params });
const res = await fetch(`${BASE_URL}/action`, {
method: "POST",
headers: { "Content-Type": "application/json", ...auth_headers(body_string) },
body: body_string,
});
const body = await res.json();
return { status: res.status, body };
}
function parse_args(argv) {
// Skip --secrets and --user flags and their values
const filtered = [];
let i = 2;
while (i < argv.length) {
if (argv[i] === "--secrets" || argv[i] === "--user") {
i += 2;
} else {
filtered.push(argv[i]);
i++;
}
}
const [action, ...rest] = filtered;
const params = {};
for (const arg of rest) {
const eq = arg.indexOf("=");
if (eq === -1) {
console.error(`Bad argument (expected key=value): ${arg}`);
process.exit(1);
}
params[arg.slice(0, eq)] = arg.slice(eq + 1);
}
return { action, params };
}
async function main() {
const secrets_path = get_arg(process.argv, "--secrets");
const username = get_arg(process.argv, "--user");
if (!secrets_path || !username) {
console.error("Usage: conduit --secrets <path> --user <name> <action> [key=value ...]");
process.exit(1);
}
let secrets;
try {
secrets = JSON.parse(readFileSync(secrets_path, "utf8"));
} catch (err) {
console.error(`Cannot read secrets file: ${err.message}`);
process.exit(1);
}
const user_entry = secrets.users?.[username];
if (!user_entry) {
console.error(`User '${username}' not found in secrets file`);
process.exit(1);
}
const { action, params } = parse_args(process.argv);
if (!action) {
console.error("Usage: conduit --secrets <path> --user <name> <action> [key=value ...]");
process.exit(1);
}
const auth_headers = (body_string) => sign_request(user_entry.secret, username, body_string);
const { status, body } = await call_action(action, params, auth_headers);
console.log(JSON.stringify(body, null, 2));
process.exit(status >= 400 ? 1 : 0);
}
main().catch((err) => {
console.error("Conduit error:", err.message);
process.exit(1);
});