@chenacc/agentbridge 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenacc/agentbridge",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Control local coding agents from a phone through a secure WeChat QR web console.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/config.js CHANGED
@@ -170,6 +170,10 @@ export function loadConfig(env = process.env, workspace = process.cwd()) {
170
170
  // recognise its own child in /api/health and restart it to change the public
171
171
  // address, without ever touching a bridge the user started by hand.
172
172
  autostart: env.AGENTBRIDGE_AUTOSTART === "1",
173
+ // Shared secret between the plugin that spawned this bridge and the bridge
174
+ // itself, so the plugin may ask for a restart to change the advertised address
175
+ // without handing that power to anything else that can reach loopback.
176
+ autostartToken: (env.AGENTBRIDGE_AUTOSTART_TOKEN ?? "").trim() || null,
173
177
  // Outbound notifications: unset webhook means the feature is simply off.
174
178
  notifyWebhook: pick(env, "AGENTBRIDGE_NOTIFY_WEBHOOK", notifyFile.webhook),
175
179
  notifyFormat: pick(env, "AGENTBRIDGE_NOTIFY_FORMAT", notifyFile.format ?? "json").toLowerCase(),
package/src/server.js CHANGED
@@ -386,6 +386,25 @@ export async function createBridge(options = {}) {
386
386
  return shutdownPromise;
387
387
  };
388
388
 
389
+ /**
390
+ * A restart requested by the plugin must not cut a running Agent turn in half.
391
+ *
392
+ * The plugin retries on the next desktop launch in that case; refusing is the
393
+ * safer answer than silently killing work the user is waiting on.
394
+ */
395
+ const refuseWhenBusy = () => {
396
+ const active = sessions.list().filter((session) => ["starting", "running", "stopping"].includes(session.status));
397
+ if (!active.length) return;
398
+ throw Object.assign(new Error("仍有 Agent 任务正在运行,请先停止任务再关闭 Bridge"), {
399
+ statusCode: 409,
400
+ code: "ACTIVE_SESSIONS",
401
+ diagnostics: {
402
+ count: active.length,
403
+ sessions: active.map(({ id, title, status, adapterId }) => ({ id, title, status, adapterId })),
404
+ },
405
+ });
406
+ };
407
+
389
408
  const server = http.createServer(async (req, res) => {
390
409
  const url = new URL(req.url, `http://${req.headers.host ?? "localhost"}`);
391
410
  try {
@@ -654,6 +673,31 @@ export async function createBridge(options = {}) {
654
673
  // point remain authenticated (and gated by the access key when enabled).
655
674
  if (req.method === "GET" && await serveStatic(res, url.pathname, url.search)) return;
656
675
 
676
+ // The DSH plugin starts this bridge itself and sometimes has to restart it,
677
+ // because the address it advertises (cloudflared hostname) only exists after
678
+ // the tunnel is up. Restarting needs a stop, and a stop needs an
679
+ // authorisation: loopback alone would let any local page POST here, so the
680
+ // caller must also present the token the plugin generated when it spawned
681
+ // this process. The token lives in the bridge's data directory, which keeps
682
+ // it out of the command line and out of the logs.
683
+ if (req.method === "POST" && url.pathname === "/api/local/autostart/shutdown") {
684
+ if (!isLoopback(req.socket.remoteAddress)) throw Object.assign(new Error("Local access only"), { statusCode: 403 });
685
+ if (!config.autostartToken || !keyMatches(req.headers["x-agentbridge-autostart"], config.autostartToken)) {
686
+ throw Object.assign(new Error("Autostart token required"), { statusCode: 401 });
687
+ }
688
+ refuseWhenBusy();
689
+ shuttingDown = true;
690
+ json(res, 202, { accepted: true, message: "AgentBridge is restarting to change its public address" });
691
+ setImmediate(() => {
692
+ void shutdown().catch((error) => {
693
+ console.error("AgentBridge safe shutdown failed", error);
694
+ shuttingDown = false;
695
+ shutdownPromise = null;
696
+ });
697
+ });
698
+ return;
699
+ }
700
+
657
701
  requireAccessKey(req, url, config);
658
702
  const device = pairing.authenticate(requestToken(req));
659
703
  if (!device) throw Object.assign(new Error("Authentication required"), { statusCode: 401 });
@@ -794,17 +838,7 @@ export async function createBridge(options = {}) {
794
838
  let match;
795
839
  if (req.method === "POST" && url.pathname === "/api/local/shutdown") {
796
840
  if (!isLoopback(req.socket.remoteAddress)) throw Object.assign(new Error("Local access only"), { statusCode: 403 });
797
- const active = sessions.list().filter((session) => ["starting", "running", "stopping"].includes(session.status));
798
- if (active.length) {
799
- throw Object.assign(new Error("仍有 Agent 任务正在运行,请先停止任务再关闭 Bridge"), {
800
- statusCode: 409,
801
- code: "ACTIVE_SESSIONS",
802
- diagnostics: {
803
- count: active.length,
804
- sessions: active.map(({ id, title, status, adapterId }) => ({ id, title, status, adapterId })),
805
- },
806
- });
807
- }
841
+ refuseWhenBusy();
808
842
  shuttingDown = true;
809
843
  json(res, 202, { accepted: true, message: "AgentBridge 正在安全停止" });
810
844
  setImmediate(() => {