quadwork 1.5.2 → 1.5.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.
Files changed (79) hide show
  1. package/bin/quadwork.js +13 -1
  2. package/out/404.html +1 -1
  3. package/out/__next.__PAGE__.txt +1 -1
  4. package/out/__next._full.txt +1 -1
  5. package/out/__next._head.txt +1 -1
  6. package/out/__next._index.txt +1 -1
  7. package/out/__next._tree.txt +1 -1
  8. package/out/_next/static/chunks/{0-7v31f-nsgw-.js → 09sq17vme9g6p.js} +1 -1
  9. package/out/_next/static/chunks/{0m83k84.midd1.js → 0wreuebrwlg.2.js} +1 -1
  10. package/out/_not-found/__next._full.txt +1 -1
  11. package/out/_not-found/__next._head.txt +1 -1
  12. package/out/_not-found/__next._index.txt +1 -1
  13. package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
  14. package/out/_not-found/__next._not-found.txt +1 -1
  15. package/out/_not-found/__next._tree.txt +1 -1
  16. package/out/_not-found.html +1 -1
  17. package/out/_not-found.txt +1 -1
  18. package/out/app-shell/__next._full.txt +1 -1
  19. package/out/app-shell/__next._head.txt +1 -1
  20. package/out/app-shell/__next._index.txt +1 -1
  21. package/out/app-shell/__next._tree.txt +1 -1
  22. package/out/app-shell/__next.app-shell.__PAGE__.txt +1 -1
  23. package/out/app-shell/__next.app-shell.txt +1 -1
  24. package/out/app-shell.html +1 -1
  25. package/out/app-shell.txt +1 -1
  26. package/out/index.html +1 -1
  27. package/out/index.txt +1 -1
  28. package/out/project/_/__next._full.txt +2 -2
  29. package/out/project/_/__next._head.txt +1 -1
  30. package/out/project/_/__next._index.txt +1 -1
  31. package/out/project/_/__next._tree.txt +1 -1
  32. package/out/project/_/__next.project.$d$id.__PAGE__.txt +2 -2
  33. package/out/project/_/__next.project.$d$id.txt +1 -1
  34. package/out/project/_/__next.project.txt +1 -1
  35. package/out/project/_/memory/__next._full.txt +1 -1
  36. package/out/project/_/memory/__next._head.txt +1 -1
  37. package/out/project/_/memory/__next._index.txt +1 -1
  38. package/out/project/_/memory/__next._tree.txt +1 -1
  39. package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +1 -1
  40. package/out/project/_/memory/__next.project.$d$id.memory.txt +1 -1
  41. package/out/project/_/memory/__next.project.$d$id.txt +1 -1
  42. package/out/project/_/memory/__next.project.txt +1 -1
  43. package/out/project/_/memory.html +1 -1
  44. package/out/project/_/memory.txt +1 -1
  45. package/out/project/_/queue/__next._full.txt +1 -1
  46. package/out/project/_/queue/__next._head.txt +1 -1
  47. package/out/project/_/queue/__next._index.txt +1 -1
  48. package/out/project/_/queue/__next._tree.txt +1 -1
  49. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +1 -1
  50. package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
  51. package/out/project/_/queue/__next.project.$d$id.txt +1 -1
  52. package/out/project/_/queue/__next.project.txt +1 -1
  53. package/out/project/_/queue.html +1 -1
  54. package/out/project/_/queue.txt +1 -1
  55. package/out/project/_.html +1 -1
  56. package/out/project/_.txt +2 -2
  57. package/out/settings/__next._full.txt +1 -1
  58. package/out/settings/__next._head.txt +1 -1
  59. package/out/settings/__next._index.txt +1 -1
  60. package/out/settings/__next._tree.txt +1 -1
  61. package/out/settings/__next.settings.__PAGE__.txt +1 -1
  62. package/out/settings/__next.settings.txt +1 -1
  63. package/out/settings.html +1 -1
  64. package/out/settings.txt +1 -1
  65. package/out/setup/__next._full.txt +1 -1
  66. package/out/setup/__next._head.txt +1 -1
  67. package/out/setup/__next._index.txt +1 -1
  68. package/out/setup/__next._tree.txt +1 -1
  69. package/out/setup/__next.setup.__PAGE__.txt +1 -1
  70. package/out/setup/__next.setup.txt +1 -1
  71. package/out/setup.html +1 -1
  72. package/out/setup.txt +1 -1
  73. package/package.json +1 -1
  74. package/server/routes.js +100 -3
  75. package/server/routes.telegramBridge.test.js +97 -2
  76. package/templates/config.toml +8 -0
  77. /package/out/_next/static/{yMYfZ4LAn8Fy22suFUnOy → X4zdS6Y6HkLOaElNeHwnq}/_buildManifest.js +0 -0
  78. /package/out/_next/static/{yMYfZ4LAn8Fy22suFUnOy → X4zdS6Y6HkLOaElNeHwnq}/_clientMiddlewareManifest.js +0 -0
  79. /package/out/_next/static/{yMYfZ4LAn8Fy22suFUnOy → X4zdS6Y6HkLOaElNeHwnq}/_ssgManifest.js +0 -0
package/out/setup.txt CHANGED
@@ -11,7 +11,7 @@ d:I[11717,["/_next/static/chunks/04_t39bv8y9pe.js","/_next/static/chunks/0ox7p_s
11
11
  f:I[92243,["/_next/static/chunks/04_t39bv8y9pe.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default",1]
12
12
  :HL["/_next/static/chunks/0ccoe1hsu70ql.css","style"]
13
13
  :HL["/_next/static/media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
14
- 0:{"P":null,"c":["","setup"],"q":"","i":false,"f":[[["",{"children":["setup",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0ccoe1hsu70ql.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/04_t39bv8y9pe.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/_next/static/chunks/0ox7p_szjhn69.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"geist_mono_8d43a2aa-module__8Li5zG__variable h-full","children":["$","body",null,{"className":"h-full flex flex-col","children":[["$","$L2",null,{}],["$","div",null,{"className":"flex flex-1 min-h-0","children":[["$","$L3",null,{}],["$","main",null,{"className":"flex-1 min-w-0 overflow-auto","children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]]}]]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$L6",null,{}],[["$","script","script-0",{"src":"/_next/static/chunks/084lff9v4p_vh.js","async":true,"nonce":"$undefined"}]],["$","$L7",null,{"children":["$","$8",null,{"name":"Next.MetadataOutlet","children":"$@9"}]}]]}],{},null,false,null]},null,false,"$@a"]},null,false,null],["$","$1","h",{"children":[null,["$","$Lb",null,{"children":"$Lc"}],["$","div",null,{"hidden":true,"children":["$","$Ld",null,{"children":["$","$8",null,{"name":"Next.Metadata","children":"$Le"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$f",[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0ccoe1hsu70ql.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"yMYfZ4LAn8Fy22suFUnOy"}
14
+ 0:{"P":null,"c":["","setup"],"q":"","i":false,"f":[[["",{"children":["setup",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0ccoe1hsu70ql.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/04_t39bv8y9pe.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/_next/static/chunks/0ox7p_szjhn69.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"geist_mono_8d43a2aa-module__8Li5zG__variable h-full","children":["$","body",null,{"className":"h-full flex flex-col","children":[["$","$L2",null,{}],["$","div",null,{"className":"flex flex-1 min-h-0","children":[["$","$L3",null,{}],["$","main",null,{"className":"flex-1 min-w-0 overflow-auto","children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]]}]]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$L6",null,{}],[["$","script","script-0",{"src":"/_next/static/chunks/084lff9v4p_vh.js","async":true,"nonce":"$undefined"}]],["$","$L7",null,{"children":["$","$8",null,{"name":"Next.MetadataOutlet","children":"$@9"}]}]]}],{},null,false,null]},null,false,"$@a"]},null,false,null],["$","$1","h",{"children":[null,["$","$Lb",null,{"children":"$Lc"}],["$","div",null,{"hidden":true,"children":["$","$Ld",null,{"children":["$","$8",null,{"name":"Next.Metadata","children":"$Le"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$f",[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0ccoe1hsu70ql.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"X4zdS6Y6HkLOaElNeHwnq"}
15
15
  10:[]
16
16
  a:"$W10"
17
17
  c:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quadwork",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "Unified dashboard for multi-agent coding teams — 4 AI agents, one terminal",
5
5
  "bin": {
6
6
  "quadwork": "./bin/quadwork.js"
package/server/routes.js CHANGED
@@ -2385,6 +2385,63 @@ function telegramConfigToml(projectId) {
2385
2385
  return path.join(CONFIG_DIR, `telegram-${projectId}.toml`);
2386
2386
  }
2387
2387
 
2388
+ // #383: path to a project's AgentChattr config.toml. The install
2389
+ // handler patches this file to declare the `telegram-bridge` agent
2390
+ // so AC's registry accepts the bridge's register call.
2391
+ function projectAgentchattrConfigPath(projectId) {
2392
+ return path.join(CONFIG_DIR, projectId, "agentchattr", "config.toml");
2393
+ }
2394
+
2395
+ // #383 Bug 1: prefer the per-project agentchattr_url. Every project
2396
+ // after the first uses a distinct port (8301, 8302, ...), so reading
2397
+ // the global default silently routed bridge traffic to the wrong AC
2398
+ // instance.
2399
+ function resolveProjectAgentchattrUrl(cfg, project) {
2400
+ return (
2401
+ (project && project.agentchattr_url) ||
2402
+ (cfg && cfg.agentchattr_url) ||
2403
+ "http://127.0.0.1:8300"
2404
+ );
2405
+ }
2406
+
2407
+ // #383 Bug 2: the upstream bridge only reads `agentchattr_url` from
2408
+ // inside `[telegram]`. A separate `[agentchattr]` section is silently
2409
+ // ignored and the bridge falls back to its hardcoded :8300 default.
2410
+ function buildTelegramBridgeToml(tg) {
2411
+ return (
2412
+ `[telegram]\n` +
2413
+ `bot_token = "${tg.bot_token}"\n` +
2414
+ `chat_id = "${tg.chat_id}"\n` +
2415
+ `agentchattr_url = "${tg.agentchattr_url}"\n`
2416
+ );
2417
+ }
2418
+
2419
+ // #383 Bug 3: AC's registry rejects any base name not pre-declared
2420
+ // in config.toml with `400 unknown base`. The bridge registers as
2421
+ // `telegram-bridge`, so every per-project AC config must declare it.
2422
+ // Idempotent: only appends if the section is not already present.
2423
+ function patchAgentchattrConfigForTelegramBridge(tomlText) {
2424
+ if (/^\[agents\.telegram-bridge\]\s*$/m.test(tomlText)) {
2425
+ return { text: tomlText, changed: false };
2426
+ }
2427
+ const sep = tomlText.length === 0 || tomlText.endsWith("\n") ? "" : "\n";
2428
+ const block = `\n[agents.telegram-bridge]\nlabel = "Telegram Bridge"\n`;
2429
+ return { text: tomlText + sep + block, changed: true };
2430
+ }
2431
+
2432
+ // #383 Bug 4: the upstream bridge treats env vars as higher
2433
+ // precedence than TOML values. If the parent shell exported
2434
+ // TELEGRAM_BOT_TOKEN for a different bot, the bridge silently ran
2435
+ // as the wrong identity. Scrub those keys from the child's env so
2436
+ // the TOML is the single source of truth.
2437
+ function buildTelegramBridgeSpawnEnv(parentEnv) {
2438
+ const env = { ...parentEnv };
2439
+ delete env.TELEGRAM_BOT_TOKEN;
2440
+ delete env.TELEGRAM_CHAT_ID;
2441
+ delete env.AGENTCHATTR_URL;
2442
+ return env;
2443
+ }
2444
+
2388
2445
  // #353: per-project log file for the bridge subprocess. The start
2389
2446
  // handler redirects stdout + stderr here so crashes (ImportError,
2390
2447
  // config parse, auth failure) are recoverable instead of
@@ -2502,7 +2559,8 @@ function getProjectTelegram(projectId) {
2502
2559
  return {
2503
2560
  bot_token: resolveToken(project.telegram.bot_token || ""),
2504
2561
  chat_id: project.telegram.chat_id || "",
2505
- agentchattr_url: cfg.agentchattr_url || "http://127.0.0.1:8300",
2562
+ // #383 Bug 1: prefer per-project URL over the global default.
2563
+ agentchattr_url: resolveProjectAgentchattrUrl(cfg, project),
2506
2564
  };
2507
2565
  } catch {
2508
2566
  return null;
@@ -2638,7 +2696,31 @@ router.post("/api/telegram", async (req, res) => {
2638
2696
  `pip output tail:\n${pipOutput.split("\n").slice(-10).join("\n")}`,
2639
2697
  });
2640
2698
  }
2641
- return res.json({ ok: true });
2699
+ // #383 Bug 3: ensure every known project's AC config declares
2700
+ // the `telegram-bridge` agent. Without this, AC's registry
2701
+ // rejects the bridge's register call with `400 unknown base`
2702
+ // and the bridge enters an infinite re-register loop.
2703
+ // Idempotent — append-only, skips configs that already have
2704
+ // the section. Does NOT restart AC servers; the operator
2705
+ // must click SERVER → Restart to load the new agent slug.
2706
+ const patched = [];
2707
+ try {
2708
+ const cfgAll = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
2709
+ for (const proj of cfgAll.projects || []) {
2710
+ if (!proj || !proj.id) continue;
2711
+ const acPath = projectAgentchattrConfigPath(proj.id);
2712
+ if (!fs.existsSync(acPath)) continue;
2713
+ try {
2714
+ const before = fs.readFileSync(acPath, "utf-8");
2715
+ const { text, changed } = patchAgentchattrConfigForTelegramBridge(before);
2716
+ if (changed) {
2717
+ fs.writeFileSync(acPath, text);
2718
+ patched.push(proj.id);
2719
+ }
2720
+ } catch {}
2721
+ }
2722
+ } catch {}
2723
+ return res.json({ ok: true, patched_projects: patched });
2642
2724
  }
2643
2725
  case "start": {
2644
2726
  const projectId = body.project_id;
@@ -2661,7 +2743,9 @@ router.post("/api/telegram", async (req, res) => {
2661
2743
  const tg = getProjectTelegram(projectId);
2662
2744
  if (!tg || !tg.bot_token || !tg.chat_id) return res.json({ ok: false, error: "Save bot_token and chat_id in project settings first." });
2663
2745
  const tomlPath = telegramConfigToml(projectId);
2664
- const tomlContent = `[telegram]\nbot_token = "${tg.bot_token}"\nchat_id = "${tg.chat_id}"\n\n[agentchattr]\nurl = "${tg.agentchattr_url}"\n`;
2746
+ // #383 Bug 2: write agentchattr_url inside [telegram]; the
2747
+ // bridge's load_config only reads from that section.
2748
+ const tomlContent = buildTelegramBridgeToml(tg);
2665
2749
  fs.writeFileSync(tomlPath, tomlContent, { mode: 0o600 });
2666
2750
  fs.chmodSync(tomlPath, 0o600);
2667
2751
  // #353: pre-flight import check so a fresh install with no
@@ -2710,9 +2794,15 @@ router.post("/api/telegram", async (req, res) => {
2710
2794
  }
2711
2795
  let child;
2712
2796
  try {
2797
+ // #383 Bug 4: scrub TELEGRAM_*/AGENTCHATTR_URL from the child
2798
+ // env so an operator shell that exports a different bot's
2799
+ // token (common on machines running AC2) can't silently
2800
+ // override the TOML. Makes the TOML the single source of
2801
+ // truth for the bridge's identity.
2713
2802
  child = spawn(venvPython, [bridgeScript, "--config", tomlPath], {
2714
2803
  detached: true,
2715
2804
  stdio: ["ignore", outFd, errFd],
2805
+ env: buildTelegramBridgeSpawnEnv(process.env),
2716
2806
  });
2717
2807
  child.unref();
2718
2808
  if (child.pid) fs.writeFileSync(telegramPidFile(projectId), String(child.pid));
@@ -2898,6 +2988,13 @@ module.exports.readLastLines = readLastLines;
2898
2988
  // #380: expose checkTelegramBridgePythonDeps so the bridge test can
2899
2989
  // exercise the venv-path interpreter argument round trip.
2900
2990
  module.exports.checkTelegramBridgePythonDeps = checkTelegramBridgePythonDeps;
2991
+ // #383: pure helpers exposed for unit tests in
2992
+ // routes.telegramBridge.test.js. No production callers outside
2993
+ // this file.
2994
+ module.exports.resolveProjectAgentchattrUrl = resolveProjectAgentchattrUrl;
2995
+ module.exports.buildTelegramBridgeToml = buildTelegramBridgeToml;
2996
+ module.exports.patchAgentchattrConfigForTelegramBridge = patchAgentchattrConfigForTelegramBridge;
2997
+ module.exports.buildTelegramBridgeSpawnEnv = buildTelegramBridgeSpawnEnv;
2901
2998
  // #236: expose sendViaWebSocket so the chat-ws-send regression test
2902
2999
  // can verify the ack/body/error paths against a fake AC ws server.
2903
3000
  module.exports.sendViaWebSocket = sendViaWebSocket;
@@ -11,7 +11,14 @@ const fs = require("node:fs");
11
11
  const os = require("node:os");
12
12
  const path = require("node:path");
13
13
  const { execFileSync } = require("node:child_process");
14
- const { readLastLines, checkTelegramBridgePythonDeps } = require("./routes");
14
+ const {
15
+ readLastLines,
16
+ checkTelegramBridgePythonDeps,
17
+ resolveProjectAgentchattrUrl,
18
+ buildTelegramBridgeToml,
19
+ patchAgentchattrConfigForTelegramBridge,
20
+ buildTelegramBridgeSpawnEnv,
21
+ } = require("./routes");
15
22
 
16
23
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "qw-bridge-log-"));
17
24
  function write(name, content) {
@@ -133,8 +140,96 @@ try {
133
140
  venvSkipped = true;
134
141
  }
135
142
 
143
+ // 12) #383 Bug 1: resolveProjectAgentchattrUrl prefers the
144
+ // per-project URL over the global default. Every project
145
+ // after the first uses a distinct port, so silently reading
146
+ // the global default routed bridge traffic to the wrong AC
147
+ // instance.
148
+ assert.equal(
149
+ resolveProjectAgentchattrUrl(
150
+ { agentchattr_url: "http://127.0.0.1:8300" },
151
+ { id: "quadwork", agentchattr_url: "http://127.0.0.1:8301" },
152
+ ),
153
+ "http://127.0.0.1:8301",
154
+ );
155
+ // Falls back to global default when the project has no URL of
156
+ // its own (legacy single-project installs).
157
+ assert.equal(
158
+ resolveProjectAgentchattrUrl(
159
+ { agentchattr_url: "http://127.0.0.1:8300" },
160
+ { id: "legacy" },
161
+ ),
162
+ "http://127.0.0.1:8300",
163
+ );
164
+ // Hard-coded fallback when neither is set.
165
+ assert.equal(
166
+ resolveProjectAgentchattrUrl({}, {}),
167
+ "http://127.0.0.1:8300",
168
+ );
169
+
170
+ // 13) #383 Bug 2: buildTelegramBridgeToml writes agentchattr_url
171
+ // inside [telegram]. The upstream bridge's load_config only
172
+ // reads from that section — a separate [agentchattr] section
173
+ // is silently ignored and the bridge falls back to its
174
+ // hardcoded :8300 default.
175
+ const toml13 = buildTelegramBridgeToml({
176
+ bot_token: "123:abc",
177
+ chat_id: "-42",
178
+ agentchattr_url: "http://127.0.0.1:8301",
179
+ });
180
+ assert.match(toml13, /^\[telegram\]/);
181
+ assert.match(toml13, /bot_token = "123:abc"/);
182
+ assert.match(toml13, /chat_id = "-42"/);
183
+ assert.match(toml13, /agentchattr_url = "http:\/\/127\.0\.0\.1:8301"/);
184
+ // Must NOT emit a separate [agentchattr] section — the bridge
185
+ // would silently ignore it.
186
+ assert.equal(toml13.includes("\n[agentchattr]\n"), false);
187
+
188
+ // 14) #383 Bug 3: patchAgentchattrConfigForTelegramBridge is
189
+ // idempotent. The Install Bridge migration may run multiple
190
+ // times; it must not duplicate the section or corrupt the
191
+ // file.
192
+ const baseConfig =
193
+ "[agents.head]\nlabel = \"Head\"\n\n[agents.dev]\nlabel = \"Dev\"\n";
194
+ const first = patchAgentchattrConfigForTelegramBridge(baseConfig);
195
+ assert.equal(first.changed, true);
196
+ assert.match(first.text, /^\[agents\.telegram-bridge\]$/m);
197
+ assert.match(first.text, /label = "Telegram Bridge"/);
198
+ // Running a second time is a no-op.
199
+ const second = patchAgentchattrConfigForTelegramBridge(first.text);
200
+ assert.equal(second.changed, false);
201
+ assert.equal(second.text, first.text);
202
+ // A config that was hand-patched during diagnosis is recognized
203
+ // as already-correct — do not clobber the operator's edit.
204
+ const handPatched =
205
+ baseConfig + "\n[agents.telegram-bridge]\nlabel = \"Telegram Bridge\"\n";
206
+ const third = patchAgentchattrConfigForTelegramBridge(handPatched);
207
+ assert.equal(third.changed, false);
208
+ assert.equal(third.text, handPatched);
209
+
210
+ // 15) #383 Bug 4: buildTelegramBridgeSpawnEnv strips the three
211
+ // env vars the upstream bridge treats as higher-precedence
212
+ // than TOML. Without this, an operator shell that exported a
213
+ // different bot's token (common on machines running AC2)
214
+ // silently overrode the QuadWork-written TOML and the bridge
215
+ // ran as the wrong identity.
216
+ const scrubbed = buildTelegramBridgeSpawnEnv({
217
+ PATH: "/usr/bin",
218
+ HOME: "/home/op",
219
+ TELEGRAM_BOT_TOKEN: "wrong-token",
220
+ TELEGRAM_CHAT_ID: "-999",
221
+ AGENTCHATTR_URL: "http://127.0.0.1:9999",
222
+ });
223
+ assert.equal(scrubbed.TELEGRAM_BOT_TOKEN, undefined);
224
+ assert.equal(scrubbed.TELEGRAM_CHAT_ID, undefined);
225
+ assert.equal(scrubbed.AGENTCHATTR_URL, undefined);
226
+ // Non-telegram keys must pass through untouched — the bridge
227
+ // still needs PATH/HOME/etc. to find python and open files.
228
+ assert.equal(scrubbed.PATH, "/usr/bin");
229
+ assert.equal(scrubbed.HOME, "/home/op");
230
+
136
231
  console.log(
137
- "routes.telegramBridge.test.js: all assertions passed (11 cases" +
232
+ "routes.telegramBridge.test.js: all assertions passed (15 cases" +
138
233
  (venvSkipped ? ", case 11 pip step skipped" : "") +
139
234
  ")",
140
235
  );
@@ -37,6 +37,14 @@ cwd = "{{dev_cwd}}"
37
37
  color = "#da7756"
38
38
  label = "Dev Builder"
39
39
 
40
+ # #383: AC's registry rejects bases not declared in config.toml.
41
+ # The Telegram bridge registers as `telegram-bridge`, so every
42
+ # per-project AC config must declare it. The bridge has no
43
+ # command/cwd of its own — it is a long-running external client
44
+ # that posts to AC's HTTP API.
45
+ [agents.telegram-bridge]
46
+ label = "Telegram Bridge"
47
+
40
48
  [routing]
41
49
  default = "none"
42
50
  max_agent_hops = 30