Skip to content

Commit daec99a

Browse files
committed
control-plane: add POST actions (restart-bridge, kill-session, deploy)
- POST /restart-bridge β€” kills and restarts slack-bridge tmux session - POST /kill-session/:name β€” kills a tmux session by name (validated) - POST /deploy β€” triggers deploy.sh as admin user - All POST routes require BAUDBOT_CP_TOKEN (enforced even when GETs are open) - Session name validation prevents injection
1 parent d4472dd commit daec99a

1 file changed

Lines changed: 76 additions & 1 deletion

File tree

β€Žcontrol-plane/server.mjsβ€Ž

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import { createServer } from "node:http";
2323
import { execSync } from "node:child_process";
24-
import { readFileSync, existsSync, readdirSync } from "node:fs";
24+
import { readFileSync, readlinkSync, existsSync, readdirSync } from "node:fs";
2525
import { timingSafeEqual } from "node:crypto";
2626
import { join } from "node:path";
2727
import express from "express";
@@ -254,6 +254,81 @@ app.get("/config", (_req, res) => {
254254
res.json(config);
255255
});
256256

257+
// ── Actions ─────────────────────────────────────────────────────────────────
258+
259+
/** Require auth token for all POST actions (even if GET routes are open). */
260+
function requireToken(req, res) {
261+
if (!TOKEN) {
262+
res.status(403).json({ error: "No BAUDBOT_CP_TOKEN configured β€” POST actions disabled" });
263+
return false;
264+
}
265+
const auth = req.headers.authorization || "";
266+
const match = auth.match(/^Bearer\s+(.+)$/i);
267+
if (!match || !tokenMatches(match[1])) {
268+
res.status(401).json({ error: "unauthorized" });
269+
return false;
270+
}
271+
return true;
272+
}
273+
274+
app.post("/restart-bridge", (req, res) => {
275+
if (!requireToken(req, res)) return;
276+
try {
277+
// Kill existing bridge
278+
run(`sudo -u ${AGENT_USER} env TMUX_TMPDIR=${AGENT_HOME}/.tmux-sock tmux kill-session -t slack-bridge 2>/dev/null`);
279+
280+
// Find the control-agent socket UUID
281+
const aliasPath = join(AGENT_HOME, ".pi", "session-control", "control-agent.alias");
282+
let uuid = null;
283+
try {
284+
const target = readlinkSync(aliasPath);
285+
uuid = target.replace(".sock", "");
286+
} catch {}
287+
288+
if (!uuid) {
289+
return res.json({ ok: false, error: "Could not find control-agent socket UUID" });
290+
}
291+
292+
// Start bridge in tmux
293+
const cmd = `sudo -u ${AGENT_USER} env TMUX_TMPDIR=${AGENT_HOME}/.tmux-sock tmux new-session -d -s slack-bridge "export PATH=${AGENT_HOME}/.varlock/bin:${AGENT_HOME}/opt/node-v22.14.0-linux-x64/bin:\\$PATH && export PI_SESSION_ID=${uuid} && cd ${AGENT_HOME}/runtime/slack-bridge && exec varlock run --path ${AGENT_HOME}/.config/ -- node bridge.mjs"`;
294+
run(cmd, 10000);
295+
res.json({ ok: true, action: "restart-bridge", uuid });
296+
} catch (err) {
297+
res.status(500).json({ ok: false, error: err.message });
298+
}
299+
});
300+
301+
app.post("/kill-session/:name", (req, res) => {
302+
if (!requireToken(req, res)) return;
303+
const name = req.params.name;
304+
// Validate session name (alphanumeric, hyphens, underscores only)
305+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
306+
return res.status(400).json({ error: "Invalid session name" });
307+
}
308+
try {
309+
const result = run(`sudo -u ${AGENT_USER} env TMUX_TMPDIR=${AGENT_HOME}/.tmux-sock tmux kill-session -t ${name} 2>&1`);
310+
res.json({ ok: true, action: "kill-session", session: name, output: result });
311+
} catch (err) {
312+
res.status(500).json({ ok: false, error: err.message });
313+
}
314+
});
315+
316+
app.post("/deploy", (req, res) => {
317+
if (!requireToken(req, res)) return;
318+
try {
319+
// deploy.sh runs as the current (admin) user
320+
const srcDir = run("dirname $(dirname $(readlink -f /proc/self/exe || echo /usr/local/bin/baudbot))") || join(process.env.HOME || "/root", "baudbot");
321+
const deployScript = join(srcDir, "bin", "deploy.sh");
322+
if (!existsSync(deployScript)) {
323+
return res.status(404).json({ ok: false, error: `deploy.sh not found at ${deployScript}` });
324+
}
325+
const output = run(`bash ${deployScript} 2>&1`, 30000);
326+
res.json({ ok: true, action: "deploy", output });
327+
} catch (err) {
328+
res.status(500).json({ ok: false, error: err.message });
329+
}
330+
});
331+
257332
// ── Dashboard ───────────────────────────────────────────────────────────────
258333

259334
app.get("/", (_req, res) => res.redirect("/dashboard"));

0 commit comments

Comments
Β (0)