@lifeaitools/clauth 1.30.26 → 2.0.0

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.
@@ -47,6 +47,7 @@ import {
47
47
  runSurfaceAction,
48
48
  setPluginEnabled,
49
49
  supervisorHealth,
50
+ operation,
50
51
  } from "../supervisor-registry.js";
51
52
  import {
52
53
  AgentPool,
@@ -772,15 +773,13 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
772
773
  .btn-ccandme{background:linear-gradient(135deg,#1a1a2e,#16213e);color:#a78bfa;border:1px solid #4c1d95;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:600;transition:all .15s;white-space:nowrap}
773
774
  .btn-ccandme:hover{background:linear-gradient(135deg,#2d1b69,#1e1b4b);border-color:#7c3aed;color:#c4b5fd;transform:translateY(-1px)}
774
775
  .btn-ccandme:disabled{opacity:.5;cursor:not-allowed;transform:none}
775
- .btn-tintin{background:linear-gradient(135deg,#103c2f,#0f2f3f);color:#86efac;border:1px solid #166534;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:700;transition:all .15s;white-space:nowrap}
776
- .btn-tintin:hover{background:linear-gradient(135deg,#14532d,#155e75);border-color:#22c55e;color:#bbf7d0;transform:translateY(-1px)}
777
- .btn-tintin:disabled{opacity:.5;cursor:not-allowed;transform:none}
778
776
  .btn-claude{background:linear-gradient(135deg,#d97706,#f59e0b);color:#0a0f1a;padding:8px 18px;font-size:.85rem;border-radius:7px;border:none;cursor:pointer;font-weight:700;letter-spacing:.3px;transition:all .15s;white-space:nowrap}
779
777
  .btn-claude:hover{filter:brightness(1.1);transform:translateY(-1px)}
780
778
  .btn-claude:disabled{opacity:.4;cursor:not-allowed;transform:none;filter:none}
781
779
  .btn-tunnel-stop{background:#1e293b;color:#f87171;border:1px solid #334155;padding:6px 12px;font-size:.8rem;border-radius:6px;cursor:pointer;font-weight:500}
782
780
  .btn-tunnel-stop:hover{background:#2d1f1f;border-color:#f87171}
783
781
  .tunnel-err{font-size:.78rem;color:#f87171;width:100%;margin-top:4px}
782
+ .dash-panel{margin-bottom:1.25rem}
784
783
  .build-panel{background:#0f1a2d;border:1px solid #1e3a5f;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1.25rem;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
785
784
  .build-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
786
785
  .build-dot.idle{background:#64748b}
@@ -791,25 +790,6 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
791
790
  .build-label{font-size:.85rem;color:#94a3b8;flex:1}
792
791
  .build-label strong{color:#e2e8f0}
793
792
  .build-meta{font-family:'Courier New',monospace;font-size:.75rem;color:#64748b}
794
- .tintin-tab{position:fixed;top:50%;right:0;transform:translateY(-50%);z-index:81;writing-mode:vertical-rl;text-orientation:mixed;background:linear-gradient(180deg,#103c2f,#0f2f3f);color:#86efac;border:1px solid #166534;border-right:none;border-radius:8px 0 0 8px;padding:14px 6px;font-size:.78rem;font-weight:700;cursor:pointer;letter-spacing:.5px;transition:all .2s}
795
- .tintin-tab:hover{background:linear-gradient(180deg,#14532d,#155e75);color:#bbf7d0;padding-right:10px}
796
- .tintin-tab.active{background:#081c1a;border-color:#22c55e;color:#bbf7d0}
797
- .tintin-panel{position:fixed;top:0;right:-440px;bottom:0;z-index:80;width:min(420px,calc(100vw - 24px));background:#081c1a;border-left:1px solid #14532d;padding:0;box-shadow:-22px 0 60px rgba(0,0,0,.42);display:flex;flex-direction:column;transition:right .25s ease}
798
- .tintin-panel.open{right:0}
799
- .tintin-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:.8rem}
800
- .tintin-title{font-size:.95rem;font-weight:700;color:#dcfce7}
801
- .tintin-sub{font-size:.78rem;color:#6ee7b7;margin-top:2px}
802
- .tintin-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
803
- .tintin-field{display:flex;flex-direction:column;gap:4px}
804
- .tintin-field label{font-size:.72rem;color:#94a3b8;text-transform:uppercase;letter-spacing:.04em;font-weight:700}
805
- .tintin-input,.tintin-textarea{background:#031312;border:1px solid #14532d;border-radius:6px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.85rem;padding:8px 10px;outline:none;transition:border-color .15s}
806
- .tintin-input:focus,.tintin-textarea:focus{border-color:#22c55e}
807
- .tintin-textarea{width:100%;min-height:92px;resize:vertical;line-height:1.45;margin-bottom:10px}
808
- .tintin-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
809
- .tintin-status{font-family:'Courier New',monospace;font-size:.78rem;color:#94a3b8}
810
- .tintin-result{display:none;margin-top:12px;background:#020b0a;border:1px solid #134e4a;border-radius:8px;padding:10px;white-space:pre-wrap;word-break:break-word;font-family:'Courier New',monospace;font-size:.82rem;color:#bbf7d0;max-height:240px;overflow:auto}
811
- .tintin-result.open{display:block}
812
- @media (max-width:720px){.tintin-panel{width:100vw}.tintin-grid{grid-template-columns:1fr}}
813
793
  .mcp-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
814
794
  .mcp-label{font-size:.72rem;color:#64748b;min-width:80px;text-transform:uppercase;letter-spacing:.5px;font-weight:600}
815
795
  .mcp-val{flex:1;font-family:'Courier New',monospace;font-size:.82rem;color:#60a5fa;background:#0a0f1a;border:1px solid #1e3a5f;border-radius:4px;padding:6px 10px;word-break:break-all;user-select:all}
@@ -936,10 +916,16 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
936
916
  .supervisor-pill.warn{border-color:rgba(250,204,21,.35);color:#fde68a;background:rgba(250,204,21,.08)}
937
917
  .supervisor-pill.bad{border-color:rgba(248,113,113,.35);color:#fecaca;background:rgba(248,113,113,.08)}
938
918
  .supervisor-list{display:flex;flex-direction:column;gap:8px;max-height:320px;overflow:auto}
939
- .supervisor-row{border:1px solid #1e293b;border-radius:8px;padding:8px;background:rgba(15,23,42,.45)}
919
+ .supervisor-row{border:1px solid #1e293b;border-radius:8px;padding:6px 8px;background:rgba(15,23,42,.45);cursor:pointer}
920
+ .supervisor-row:hover{border-color:#334155}
921
+ .supervisor-row.selected{border-color:#38bdf8;background:rgba(56,189,248,.08)}
922
+ .supervisor-row.readonly{cursor:default}
940
923
  .supervisor-row-top{display:flex;justify-content:space-between;gap:8px;align-items:center}
941
924
  .supervisor-name{font-size:.82rem;color:#e2e8f0;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
942
925
  .supervisor-row-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}
926
+ .supervisor-cmdbar{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:8px;padding:8px;border:1px solid #1e293b;border-radius:8px;background:rgba(2,6,23,.4);min-height:36px}
927
+ .supervisor-cmdbar-empty{font-size:.74rem;color:#64748b}
928
+ .supervisor-cmdbar-name{font-size:.78rem;color:#e2e8f0;font-weight:600;margin-right:4px}
943
929
  .supervisor-log{font-family:'Courier New',monospace;font-size:.7rem;color:#94a3b8;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow:auto}
944
930
  .supervisor-feedback{margin-top:8px;padding:7px 9px;border-radius:6px;font-family:'Courier New',monospace;font-size:.72rem;color:#86efac;background:rgba(34,197,94,.08);border:1px solid rgba(74,222,128,.22);display:none}
945
931
  .supervisor-feedback.bad{color:#fecaca;background:rgba(248,113,113,.08);border-color:rgba(248,113,113,.25)}
@@ -982,13 +968,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
982
968
 
983
969
  <!-- ── Main view (shown after unlock) ──────── -->
984
970
  <div id="main-view">
985
- <div id="upgrade-banner" style="display:none" class="upgrade-banner">
986
- <div class="upgrade-content">
987
- <strong>⬆ clauth upgraded to v<span id="upgrade-to-version"></span></strong>
988
- <span id="upgrade-details"></span>
989
- <button onclick="dismissUpgrade()">Dismiss ✕</button>
990
- </div>
991
- </div>
971
+ <section class="dash-panel" data-panel="title-status">
992
972
  <div class="header">
993
973
  <div class="dot" id="dot"></div>
994
974
  <h1>🔐 clauth vault <span style="font-size:0.55em;opacity:0.45;font-weight:400">v${VERSION}</span>${isStaged ? `<span style="font-size:0.5em;background:#b45309;color:#fef3c7;border-radius:4px;padding:2px 10px;margin-left:12px;font-weight:600;letter-spacing:.5px">STAGED</span>` : ""}</h1>
@@ -1007,6 +987,16 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1007
987
  <div>Services: <span id="s-services">${whitelist ? whitelist.join(", ") : "all"}</span></div>
1008
988
  <div>Failures: <span id="s-fails">—</span></div>
1009
989
  </div>
990
+ </section>
991
+
992
+ <section class="dash-panel" data-panel="status-area">
993
+ <div id="upgrade-banner" style="display:none" class="upgrade-banner">
994
+ <div class="upgrade-content">
995
+ <strong>⬆ clauth upgraded to v<span id="upgrade-to-version"></span></strong>
996
+ <span id="upgrade-details"></span>
997
+ <button onclick="dismissUpgrade()">Dismiss ✕</button>
998
+ </div>
999
+ </div>
1010
1000
  <div class="build-panel" id="build-panel">
1011
1001
  <div class="build-dot idle" id="build-dot"></div>
1012
1002
  <div class="build-label" id="build-label">
@@ -1015,8 +1005,9 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1015
1005
  <span id="build-sha" style="font-family:'Courier New',monospace;font-size:.72rem;color:#60a5fa;background:#0a0f1a;border:1px solid #1e3a5f;border-radius:4px;padding:2px 8px;letter-spacing:.3px"></span>
1016
1006
  <div class="build-meta" id="build-meta" style="width:100%;margin-top:4px"></div>
1017
1007
  </div>
1008
+ </section>
1018
1009
 
1019
- <div class="toolbar">
1010
+ <div class="toolbar" data-panel="menu-bar">
1020
1011
  <button class="btn-refresh" onclick="loadServices()">↻ Refresh</button>
1021
1012
  <button class="btn-add" onclick="toggleAddService()">+ Add Service</button>
1022
1013
  <button class="btn-check" id="check-btn" onclick="checkAll()">⬤ Check All</button>
@@ -1108,7 +1099,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1108
1099
  </form>
1109
1100
  </div>
1110
1101
 
1111
- <div class="tunnel-panel" id="tunnel-panel">
1102
+ <div class="tunnel-panel" id="tunnel-panel" data-panel="tunnel-bar">
1112
1103
  <!-- not_configured -->
1113
1104
  <div class="tunnel-state not-configured" style="display:none;align-items:center;gap:10px;width:100%;flex-wrap:wrap">
1114
1105
  <div class="tunnel-dot off"></div>
@@ -1149,39 +1140,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1149
1140
  </div>
1150
1141
  </div>
1151
1142
 
1152
- <div class="tintin-tab" id="tintin-tab" onclick="toggleTinTinPanel()">TinTin</div>
1153
- <div class="tintin-panel" id="tintin-panel">
1154
- <div class="tintin-head" style="padding:12px 16px;border-bottom:1px solid #14532d;flex-shrink:0">
1155
- <div style="display:flex;align-items:center;justify-content:space-between">
1156
- <div class="tintin-title">TinTin</div>
1157
- <button class="btn-cancel" onclick="toggleTinTinPanel()" style="font-size:16px;padding:2px 6px">x</button>
1158
- </div>
1159
- <div class="tintin-sub">claude-sonnet-4-6 · clauth CLI dispatch</div>
1160
- <div style="margin-top:6px"><a href="/tintin/settings/ui" target="_blank" rel="noopener" style="font-size:.72rem;color:#86efac;text-decoration:none;border:1px solid #14532d;padding:2px 6px;border-radius:4px">Agent settings</a></div>
1161
- </div>
1162
- <div id="tintin-messages" style="flex:1;min-height:0;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:10px">
1163
- <div style="font-size:.78rem;color:#6ee7b7;font-style:italic">Ask anything. Runs a Claude CLI agent with full clauth + repo context.</div>
1164
- </div>
1165
- <div style="padding:10px 16px;border-top:1px solid #14532d;flex-shrink:0">
1166
- <div class="tintin-grid" style="margin-bottom:8px">
1167
- <div class="tintin-field">
1168
- <label>CWD</label>
1169
- <input class="tintin-input" id="tintin-cwd" value="C:\\Dev\\regen-root" spellcheck="false" autocomplete="off">
1170
- </div>
1171
- <div class="tintin-field">
1172
- <label>App</label>
1173
- <input class="tintin-input" id="tintin-app" value="clauth" spellcheck="false" autocomplete="off">
1174
- </div>
1175
- </div>
1176
- <textarea class="tintin-textarea" id="tintin-prompt" spellcheck="false" placeholder="Ask TinTin..." style="min-height:64px;margin-bottom:8px"></textarea>
1177
- <div style="display:flex;gap:8px;align-items:center">
1178
- <button class="btn-tintin" id="tintin-send" onclick="sendTinTinMessage()" style="flex:1">Send</button>
1179
- <span class="tintin-status" id="tintin-status">idle</span>
1180
- </div>
1181
- </div>
1182
- </div>
1183
-
1184
- <section class="supervisor-panel" id="supervisor-panel">
1143
+ <section class="supervisor-panel" id="supervisor-panel" data-panel="surfaces-log">
1185
1144
  <div class="supervisor-head">
1186
1145
  <div>
1187
1146
  <div class="supervisor-title">Local Software Factory Supervisor</div>
@@ -1199,6 +1158,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1199
1158
  <h4>Surfaces</h4>
1200
1159
  <div class="supervisor-kpi" id="supervisor-surface-count">—</div>
1201
1160
  <div class="supervisor-meta" id="supervisor-surface-meta">destination + owner matrix</div>
1161
+ <div class="supervisor-cmdbar" id="supervisor-cmdbar"><span class="supervisor-cmdbar-empty">Select a surface below to act on it.</span></div>
1202
1162
  <div class="supervisor-list" id="supervisor-surfaces" style="margin-top:8px"></div>
1203
1163
  </div>
1204
1164
  <div class="supervisor-card">
@@ -1211,7 +1171,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1211
1171
  </section>
1212
1172
 
1213
1173
 
1214
- <div class="tunnel-panel" id="webdav-panel" style="flex-direction:column;align-items:stretch;gap:8px">
1174
+ <div class="tunnel-panel" id="webdav-panel" data-panel="webdav-mounts" style="flex-direction:column;align-items:stretch;gap:8px">
1215
1175
  <div style="display:flex;align-items:center;gap:10px;width:100%">
1216
1176
  <div class="tunnel-dot off" id="webdav-dot"></div>
1217
1177
  <strong style="color:#e2e8f0;font-size:.88rem">WebDAV Mounts</strong>
@@ -1247,6 +1207,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1247
1207
  <div class="wizard-foot" id="wizard-foot"></div>
1248
1208
  </div>
1249
1209
 
1210
+ <section class="dash-panel" data-panel="search-credentials">
1250
1211
  <div id="project-tabs" class="project-tabs" style="display:none"></div>
1251
1212
  <div id="service-search" class="service-search">
1252
1213
  <span class="service-search-label">Search</span>
@@ -1254,7 +1215,8 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
1254
1215
  <span id="service-search-count" class="service-search-count"></span>
1255
1216
  </div>
1256
1217
  <div id="grid" class="grid"><p class="loading">Loading services…</p></div>
1257
- <div class="footer" id="originFooter">checking origin… · 10-strike lockout</div>
1218
+ </section>
1219
+ <div class="footer" id="originFooter" data-panel="footer">checking origin… · 10-strike lockout</div>
1258
1220
  </div>
1259
1221
 
1260
1222
  <script>
@@ -1496,9 +1458,17 @@ function showMain(ping) {
1496
1458
  loadWebdavMounts();
1497
1459
  updateBuildStatus();
1498
1460
  loadSupervisorCockpit();
1461
+ startSupervisorLogTail();
1499
1462
  refreshWriteLockUi();
1500
1463
  }
1501
1464
 
1465
+ let supervisorLogTailStarted = false;
1466
+ function startSupervisorLogTail() {
1467
+ if (supervisorLogTailStarted) return; // showMain() can run more than once per page load
1468
+ supervisorLogTailStarted = true;
1469
+ setInterval(loadSupervisorCockpit, 3000);
1470
+ }
1471
+
1502
1472
  function supervisorBadge(text, kind) {
1503
1473
  return '<span class="supervisor-pill ' + (kind || '') + '">' + htmlEscape(text) + '</span>';
1504
1474
  }
@@ -1518,6 +1488,22 @@ function supervisorFeedback(text, bad) {
1518
1488
  el.style.display = text ? "block" : "none";
1519
1489
  }
1520
1490
 
1491
+ const SUPERVISOR_SURFACE_ACTIONS = ["start", "stop", "restart", "reconcile", "test", "promote", "rollback"];
1492
+ let selectedSupervisorSurface = null; // { id, name } | null
1493
+ let lastSupervisorSurfaceRows = [];
1494
+
1495
+ const CLAUTH_SELF_PSEUDO_SURFACE = {
1496
+ __pseudo: true,
1497
+ id: "clauth:mcp-sse",
1498
+ plugin_id: "clauth",
1499
+ name: "clauth (self)",
1500
+ lifecycle_owner: "external",
1501
+ destination: "self",
1502
+ state: "self_managed",
1503
+ port: null,
1504
+ health: null,
1505
+ };
1506
+
1521
1507
  async function loadSupervisorCockpit() {
1522
1508
  const status = document.getElementById("supervisor-status");
1523
1509
  if (status) status.textContent = "Loading supervisor state…";
@@ -1527,16 +1513,27 @@ async function loadSupervisorCockpit() {
1527
1513
  supervisorJson("/v1/surfaces"),
1528
1514
  supervisorJson("/v1/logs?limit=40"),
1529
1515
  ]);
1530
- const surfaceRows = surfaces.surfaces || [];
1531
- document.getElementById("supervisor-surface-count").textContent = String(surfaceRows.length);
1516
+ const surfaceRows = [CLAUTH_SELF_PSEUDO_SURFACE, ...(surfaces.surfaces || [])];
1517
+ lastSupervisorSurfaceRows = surfaceRows;
1518
+ if (selectedSupervisorSurface && !surfaceRows.some(s => (s.plugin_id + ":" + s.id) === selectedSupervisorSurface.id)) {
1519
+ selectedSupervisorSurface = null;
1520
+ }
1521
+ document.getElementById("supervisor-surface-count").textContent = String(surfaceRows.length - 1);
1532
1522
  document.getElementById("supervisor-operation-count").textContent = String((logs.operations || []).length);
1533
1523
  document.getElementById("supervisor-surface-meta").textContent = "pm2 home: " + (health.pm2_home || "—");
1534
1524
  document.getElementById("supervisor-log-path").textContent = logs.log_path || "events.jsonl";
1535
- document.getElementById("supervisor-surfaces").innerHTML = surfaceRows.length ? surfaceRows.map(renderSupervisorSurface).join("") : '<div class="supervisor-sub">No surfaces declared.</div>';
1536
- document.getElementById("supervisor-events").textContent = (logs.events || []).slice(-20).reverse().map(e => {
1525
+ document.getElementById("supervisor-surfaces").innerHTML = surfaceRows.map(renderSupervisorSurface).join("");
1526
+ renderSupervisorCmdbar();
1527
+ const logEl = document.getElementById("supervisor-events");
1528
+ // Newest-at-bottom, like a real tail -- and only auto-scroll if the
1529
+ // reader was already at the bottom, so scrolling up to read history
1530
+ // during a poll tick doesn't get yanked back down.
1531
+ const wasAtBottom = logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight < 12;
1532
+ logEl.textContent = (logs.events || []).slice(-40).map(e => {
1537
1533
  const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
1538
1534
  return (e.ts || e.created_at || "") + " " + label;
1539
1535
  }).join("\\n") || "No events yet.";
1536
+ if (wasAtBottom) logEl.scrollTop = logEl.scrollHeight;
1540
1537
  if (status) status.innerHTML = supervisorBadge("supervisor " + (health.status === "ok" ? "healthy" : (health.status || "unknown")), health.status === "ok" ? "ok" : "warn") + supervisorBadge("vault " + (health.vault_locked ? "locked" : "unlocked"), health.vault_locked ? "warn" : "ok") + supervisorBadge("v" + (health.clauth_version || "${VERSION}"), "");
1541
1538
  } catch (err) {
1542
1539
  if (status) status.innerHTML = supervisorBadge("supervisor unreachable", "bad") + " " + htmlEscape(err.message || err);
@@ -1544,13 +1541,55 @@ async function loadSupervisorCockpit() {
1544
1541
  }
1545
1542
  }
1546
1543
 
1544
+ // Derives an "Open UI" target for a surface without storing one on disk —
1545
+ // a real localhost port wins, else the first externally-routable URL.
1546
+ function openUiUrlForSurface(surface) {
1547
+ if (surface.port && surface.port !== "auto") return "http://127.0.0.1:" + surface.port + "/";
1548
+ const external = (surface.routes || []).find(r => r.kind === "external" && r.url);
1549
+ return external ? external.url : null;
1550
+ }
1551
+
1552
+ function selectSupervisorSurface(compositeId, name) {
1553
+ selectedSupervisorSurface = selectedSupervisorSurface && selectedSupervisorSurface.id === compositeId
1554
+ ? null // clicking the already-selected card deselects it
1555
+ : { id: compositeId, name };
1556
+ document.querySelectorAll(".supervisor-row[data-surface-id]").forEach(row => {
1557
+ row.classList.toggle("selected", row.dataset.surfaceId === (selectedSupervisorSurface && selectedSupervisorSurface.id));
1558
+ });
1559
+ renderSupervisorCmdbar();
1560
+ }
1561
+
1562
+ function renderSupervisorCmdbar() {
1563
+ const bar = document.getElementById("supervisor-cmdbar");
1564
+ if (!bar) return;
1565
+ if (!selectedSupervisorSurface) {
1566
+ bar.innerHTML = '<span class="supervisor-cmdbar-empty">Select a surface below to act on it.</span>';
1567
+ return;
1568
+ }
1569
+ const { id, name } = selectedSupervisorSurface;
1570
+ bar.innerHTML = '<span class="supervisor-cmdbar-name">' + htmlEscape(name) + '</span>' +
1571
+ SUPERVISOR_SURFACE_ACTIONS.map(a => '<button class="supervisor-action" data-supervisor-action="' + a + '" onclick="runSupervisorSurface(' + jsArg(id) + ',' + jsArg(a) + ')">' + a + '</button>').join("");
1572
+ }
1573
+
1547
1574
  function renderSupervisorSurface(surface) {
1575
+ const compositeId = surface.plugin_id + ":" + surface.id;
1548
1576
  const ownerKind = surface.lifecycle_owner === "clauth" ? "ok" : (surface.lifecycle_owner === "plugin" ? "warn" : "");
1549
1577
  const stateKind = surface.state === "current" || surface.status === "healthy" ? "ok" : (surface.state === "unavailable" ? "bad" : "warn");
1550
- return '<div class="supervisor-row"><div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(surface.plugin_id + ":" + surface.id) + '</span>' + supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + supervisorBadge(surface.state || surface.status || "unknown", stateKind) + '</div>' +
1578
+ const openUrl = surface.__pseudo ? null : openUiUrlForSurface(surface);
1579
+ const isSelected = selectedSupervisorSurface && selectedSupervisorSurface.id === compositeId;
1580
+ const rowClass = "supervisor-row" + (surface.__pseudo ? " readonly" : "") + (isSelected ? " selected" : "");
1581
+ const onclick = surface.__pseudo ? "" : ' onclick="selectSupervisorSurface(' + jsArg(compositeId) + ',' + jsArg(surface.name || compositeId) + ')"';
1582
+ const openBtn = surface.__pseudo
1583
+ ? '<button class="supervisor-action" onclick="event.stopPropagation();openClauthSelfMcp()" title="Open clauth\\'s own MCP endpoint">Open</button>'
1584
+ : (openUrl ? '<button class="supervisor-action" onclick="event.stopPropagation();window.open(' + jsArg(openUrl) + ',\\'_blank\\')" title="Open this surface\\'s UI">Open UI</button>' : "");
1585
+ return '<div class="' + rowClass + '" data-surface-id="' + htmlEscape(compositeId) + '"' + onclick + '>' +
1586
+ '<div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(surface.name || compositeId) + '</span>' +
1587
+ supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + supervisorBadge(surface.state || surface.status || "unknown", stateKind) +
1588
+ openBtn +
1589
+ '</div>' +
1551
1590
  '<div class="supervisor-meta">' + htmlEscape(surface.destination || "—") + ' · port ' + htmlEscape(surface.port || "—") + '</div>' +
1552
1591
  '<div class="supervisor-meta">' + htmlEscape(surface.health || surface.health_url || "no health") + '</div>' +
1553
- '<div class="supervisor-row-actions">' + ["start","stop","restart","reconcile","test","promote","rollback"].map(a => '<button class="supervisor-action" data-supervisor-action="' + a + '" onclick="runSupervisorSurface(' + jsArg(surface.id) + ',' + jsArg(a) + ')">' + a + '</button>').join("") + '</div></div>';
1592
+ '</div>';
1554
1593
  }
1555
1594
 
1556
1595
  async function rescanSupervisorPlugins() {
@@ -1570,6 +1609,21 @@ async function runSupervisorSurface(id, action) {
1570
1609
  } catch (err) { supervisorFeedback("Surface " + action + " failed: " + (err.message || err), true); }
1571
1610
  }
1572
1611
 
1612
+ // clauth doesn't manage itself as a plugin, so its own MCP endpoint has no
1613
+ // entry in the real surfaces registry — this reads the vault value directly
1614
+ // (never hardcoded) rather than storing a synthetic surfaces row.
1615
+ async function openClauthSelfMcp() {
1616
+ try {
1617
+ const res = await fetch(BASE + "/v/mcp-clauth-endpoint");
1618
+ const text = (await res.text()).trim();
1619
+ if (!res.ok) {
1620
+ supervisorFeedback("Could not read clauth's own MCP endpoint: " + text, true);
1621
+ return;
1622
+ }
1623
+ window.open(text, "_blank");
1624
+ } catch (err) { supervisorFeedback("Could not read clauth's own MCP endpoint: " + (err.message || err), true); }
1625
+ }
1626
+
1573
1627
  // ── Unlock ──────────────────────────────────
1574
1628
  async function unlock() {
1575
1629
  const input = document.getElementById("lock-input");
@@ -1743,124 +1797,6 @@ async function launchCCandMe() {
1743
1797
  }
1744
1798
  }
1745
1799
 
1746
- // ── TinTin local CLI dispatch sidebar ──
1747
- let tintinPollTimer = null;
1748
-
1749
- function toggleTinTinPanel() {
1750
- const panel = document.getElementById("tintin-panel");
1751
- const tab = document.getElementById("tintin-tab");
1752
- if (!panel) return;
1753
- panel.classList.toggle("open");
1754
- if (tab) tab.classList.toggle("active");
1755
- }
1756
-
1757
- function fillTinTinSmoke() {
1758
- const prompt = document.getElementById("tintin-prompt");
1759
- const app = document.getElementById("tintin-app");
1760
- if (app) app.value = "clauth-dashboard";
1761
- if (prompt) prompt.value = "Reply exactly: tintin-dashboard-ok";
1762
- }
1763
-
1764
- function setTinTinStatus(text) {
1765
- const el = document.getElementById("tintin-status");
1766
- if (el) el.textContent = text || "";
1767
- }
1768
-
1769
- function appendTinTinMessage(role, text) {
1770
- const container = document.getElementById("tintin-messages");
1771
- if (!container || !text) return;
1772
- const bubble = document.createElement("div");
1773
- bubble.style.cssText = role === "user"
1774
- ? "background:#14532d;border:1px solid #166534;border-radius:8px;padding:8px 10px;font-size:.82rem;color:#bbf7d0;white-space:pre-wrap;word-break:break-word;align-self:flex-end;max-width:90%"
1775
- : "background:#0a1f1a;border:1px solid #134e4a;border-radius:8px;padding:8px 10px;font-size:.82rem;color:#86efac;font-family:'Courier New',monospace;white-space:pre-wrap;word-break:break-word;max-width:95%;max-height:300px;overflow:auto";
1776
- bubble.textContent = text;
1777
- container.appendChild(bubble);
1778
- container.scrollTop = container.scrollHeight;
1779
- }
1780
-
1781
- function setTinTinResult(text, open) {
1782
- if (open && text) appendTinTinMessage("assistant", text);
1783
- }
1784
-
1785
- async function sendTinTinMessage() {
1786
- const sendBtn = document.getElementById("tintin-send");
1787
- const promptEl = document.getElementById("tintin-prompt");
1788
- const appEl = document.getElementById("tintin-app");
1789
- const cwdEl = document.getElementById("tintin-cwd");
1790
- const prompt = (promptEl && promptEl.value || "").trim();
1791
- const appSlug = (appEl && appEl.value || "clauth-dashboard").trim() || "clauth-dashboard";
1792
- const cwd = (cwdEl && cwdEl.value || "").trim();
1793
- if (!prompt) {
1794
- setTinTinStatus("prompt required");
1795
- return;
1796
- }
1797
- if (tintinPollTimer) {
1798
- clearTimeout(tintinPollTimer);
1799
- tintinPollTimer = null;
1800
- }
1801
- const jobId = "tintin-" + Date.now();
1802
- const body = {
1803
- prompt,
1804
- job_id: jobId,
1805
- cwd,
1806
- agent_context: {
1807
- app: { slug: appSlug, route: "/clauth-dashboard", origin: window.location.origin },
1808
- repo: { root: cwd, cwd },
1809
- runtime: { agent: "clauth-cli", requested_by: "clauth-dashboard", model: "claude-sonnet-4-6" },
1810
- task: { intent: "general_chat", thread_id: jobId }
1811
- }
1812
- };
1813
-
1814
- appendTinTinMessage("user", prompt);
1815
- if (promptEl) promptEl.value = "";
1816
- if (sendBtn) { sendBtn.disabled = true; sendBtn.textContent = "Dispatching..."; }
1817
- setTinTinStatus("dispatching");
1818
- try {
1819
- const response = await fetch(BASE + "/tintin/dispatch", {
1820
- method: "POST",
1821
- headers: { "Content-Type": "application/json" },
1822
- body: JSON.stringify(body)
1823
- });
1824
- const result = await response.json();
1825
- if (!response.ok || result.error) {
1826
- setTinTinStatus("dispatch failed");
1827
- setTinTinResult(JSON.stringify(result, null, 2), true);
1828
- return;
1829
- }
1830
- setTinTinStatus("spawned pid " + result.pid + " - polling");
1831
- pollTinTinJob(jobId, 0);
1832
- } catch (err) {
1833
- setTinTinStatus("dispatch error");
1834
- setTinTinResult(err && err.message ? err.message : String(err), true);
1835
- } finally {
1836
- if (sendBtn) { sendBtn.disabled = false; sendBtn.textContent = "Dispatch"; }
1837
- }
1838
- }
1839
-
1840
- async function pollTinTinJob(jobId, attempt) {
1841
- try {
1842
- const response = await fetch(BASE + "/tintin/dispatch/" + encodeURIComponent(jobId), { cache: "no-store" });
1843
- const job = await response.json();
1844
- if (!response.ok || job.error) {
1845
- setTinTinStatus("job lookup failed");
1846
- setTinTinResult(JSON.stringify(job, null, 2), true);
1847
- return;
1848
- }
1849
- if (job.status === "running" && attempt < 60) {
1850
- setTinTinStatus("running pid " + (job.pid || "?") + " - " + (attempt + 1));
1851
- tintinPollTimer = setTimeout(function() { pollTinTinJob(jobId, attempt + 1); }, 1500);
1852
- return;
1853
- }
1854
- setTinTinStatus(job.status === "completed" ? "done" : job.status);
1855
- const out = (job.stdout || "").trim();
1856
- if (out) appendTinTinMessage("assistant", out);
1857
- if (job.stderr && job.stderr.trim()) appendTinTinMessage("assistant", "stderr: " + job.stderr.trim());
1858
- } catch (err) {
1859
- setTinTinStatus("poll error");
1860
- setTinTinResult(err && err.message ? err.message : String(err), true);
1861
- }
1862
- }
1863
-
1864
1800
  // ── Restart daemon (keeps boot.key — vault stays unlocked) ──
1865
1801
  async function restartDaemon() {
1866
1802
  if (!confirm("Restart the daemon?\\n\\nThe vault will stay unlocked (boot.key kept).")) return;
@@ -1932,8 +1868,16 @@ function switchProjectTab(key) {
1932
1868
  renderServiceGrid(allServices);
1933
1869
  }
1934
1870
 
1871
+ const MCP_SURFACE_DUPLICATE_SERVICES = new Set([
1872
+ "mcp-clauth-endpoint",
1873
+ "mcp-fs-endpoint",
1874
+ "mcp-regen-media-endpoint",
1875
+ "mcp-web-research-endpoint",
1876
+ ]);
1877
+
1935
1878
  function renderServiceGrid(services) {
1936
1879
  const grid = document.getElementById("grid");
1880
+ services = services.filter(s => !MCP_SURFACE_DUPLICATE_SERVICES.has(s.name));
1937
1881
  let filtered = services;
1938
1882
  if (activeProjectTab === "unassigned") {
1939
1883
  filtered = services.filter(s => !s.project);
@@ -3326,11 +3270,6 @@ setInterval(async () => {
3326
3270
  }, 5000);
3327
3271
 
3328
3272
  boot();
3329
-
3330
- const tintinSidebarScript = document.createElement("script");
3331
- tintinSidebarScript.src = "/tintin/sidebar.user.js";
3332
- tintinSidebarScript.defer = true;
3333
- document.body.appendChild(tintinSidebarScript);
3334
3273
  </script>
3335
3274
  </body>
3336
3275
  </html>`;
@@ -3353,561 +3292,6 @@ function clauthConfigDir() {
3353
3292
  return path.join(os.homedir(), ".config", "clauth");
3354
3293
  }
3355
3294
 
3356
- function tintinConfigPath() {
3357
- return process.env.CLAUTH_MONKEY_CONFIG || path.join(clauthConfigDir(), "monkey.config.json");
3358
- }
3359
-
3360
- function defaultTinTinConfig() {
3361
- return {
3362
- schema_version: 3,
3363
- trust_mode: process.env.CLAUTH_MONKEY_TRUST_MODE || "open-local",
3364
- agent_settings: {
3365
- default_repo_root: process.env.CLAUTH_TINTIN_REPO_ROOT || "C:\\Dev\\regen-root",
3366
- worktree_root: process.env.CLAUTH_TINTIN_WORKTREE_ROOT || "C:\\Dev\\regen-root.wt",
3367
- base_branch: process.env.CLAUTH_TINTIN_BASE_BRANCH || "develop",
3368
- isolation: process.env.CLAUTH_TINTIN_ISOLATION || "worktree",
3369
- launch_mode: "setup_only",
3370
- agents: {
3371
- claude: {
3372
- runtime: "claude",
3373
- model: process.env.CLAUTH_TINTIN_CLAUDE_MODEL || "claude-sonnet-4-6",
3374
- command: process.env.CLAUDE_BIN || "claude",
3375
- enabled: true,
3376
- },
3377
- codex: {
3378
- runtime: "codex",
3379
- model: process.env.CLAUTH_TINTIN_CODEX_MODEL || "gpt-5-codex",
3380
- command: process.env.CODEX_BIN || "codex",
3381
- enabled: true,
3382
- },
3383
- },
3384
- },
3385
- sidebar_settings: {
3386
- enabled: true,
3387
- default_app_slug: "tintin-console",
3388
- session_ttl_hours: 24,
3389
- event_history_limit: 200,
3390
- },
3391
- dispatch_settings: {
3392
- enabled: true,
3393
- default_agent: "claude",
3394
- default_model: process.env.CLAUTH_TINTIN_CLAUDE_MODEL || "claude-sonnet-4-6",
3395
- max_concurrent_workers: 2,
3396
- },
3397
- codevelop_settings: {
3398
- enabled: true,
3399
- default_repo: process.env.CLAUTH_TINTIN_REPO_ROOT || "C:\\Dev\\regen-root",
3400
- message_retention_limit: 500,
3401
- require_peer_join: true,
3402
- },
3403
- agent_sessions: [],
3404
- apps: [
3405
- {
3406
- slug: "rdc-marketing-engine",
3407
- origins: [
3408
- "http://localhost:3000",
3409
- "http://127.0.0.1:3000",
3410
- "https://app.regendevcorp.com",
3411
- "https://rdc-marketing-engine.dev.regendevcorp.com",
3412
- ],
3413
- manifest_url: "https://app.regendevcorp.com/.well-known/monkey.json",
3414
- cwd: "C:\\Dev\\regen-root",
3415
- capabilities: ["general_chat", "skill_request", "handoff"],
3416
- },
3417
- ],
3418
- };
3419
- }
3420
-
3421
- function mergeTinTinAgentSettings(input = {}) {
3422
- const defaults = defaultTinTinConfig().agent_settings;
3423
- const incoming = input && typeof input === "object" ? input : {};
3424
- return {
3425
- ...defaults,
3426
- ...incoming,
3427
- agents: {
3428
- ...defaults.agents,
3429
- ...(incoming.agents && typeof incoming.agents === "object" ? incoming.agents : {}),
3430
- },
3431
- };
3432
- }
3433
-
3434
- function mergeTinTinSectionSettings(sectionName, input = {}) {
3435
- const defaults = defaultTinTinConfig()[sectionName] || {};
3436
- const incoming = input && typeof input === "object" ? input : {};
3437
- return { ...defaults, ...incoming };
3438
- }
3439
-
3440
- function normalizeTinTinConfig(input = {}) {
3441
- const defaults = defaultTinTinConfig();
3442
- return {
3443
- schema_version: Math.max(Number(input.schema_version || 0), defaults.schema_version),
3444
- trust_mode: typeof input.trust_mode === "string" ? input.trust_mode : defaults.trust_mode,
3445
- agent_settings: mergeTinTinAgentSettings(input.agent_settings),
3446
- sidebar_settings: mergeTinTinSectionSettings("sidebar_settings", input.sidebar_settings),
3447
- dispatch_settings: mergeTinTinSectionSettings("dispatch_settings", input.dispatch_settings),
3448
- codevelop_settings: mergeTinTinSectionSettings("codevelop_settings", input.codevelop_settings),
3449
- agent_sessions: Array.isArray(input.agent_sessions) ? input.agent_sessions : defaults.agent_sessions,
3450
- apps: Array.isArray(input.apps) ? input.apps : defaults.apps,
3451
- };
3452
- }
3453
-
3454
- function loadTinTinConfig() {
3455
- const configPath = tintinConfigPath();
3456
- try {
3457
- if (!fs.existsSync(configPath)) return { ...normalizeTinTinConfig(), path: configPath, exists: false };
3458
- const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
3459
- return {
3460
- ...normalizeTinTinConfig(parsed),
3461
- path: configPath,
3462
- exists: true,
3463
- };
3464
- } catch (err) {
3465
- return {
3466
- ...normalizeTinTinConfig(),
3467
- path: configPath,
3468
- exists: false,
3469
- error: err.message,
3470
- };
3471
- }
3472
- }
3473
-
3474
- function saveTinTinConfig(input = {}) {
3475
- const configPath = tintinConfigPath();
3476
- const next = normalizeTinTinConfig(input);
3477
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
3478
- fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
3479
- return { ...next, path: configPath, exists: true };
3480
- }
3481
-
3482
- function slugPart(value, fallback = "session") {
3483
- return String(value || fallback).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || fallback;
3484
- }
3485
-
3486
- function assertInside(parent, child) {
3487
- const rel = path.relative(path.resolve(parent), path.resolve(child));
3488
- return rel && !rel.startsWith("..") && !path.isAbsolute(rel);
3489
- }
3490
-
3491
- function gitTopLevel(repoRoot) {
3492
- try {
3493
- return execSyncTop("git rev-parse --show-toplevel", {
3494
- cwd: repoRoot,
3495
- encoding: "utf8",
3496
- stdio: ["ignore", "pipe", "pipe"],
3497
- windowsHide: true,
3498
- }).trim();
3499
- } catch {
3500
- return null;
3501
- }
3502
- }
3503
-
3504
- function createTinTinAgentSession(input = {}) {
3505
- const config = loadTinTinConfig();
3506
- const settings = mergeTinTinAgentSettings(config.agent_settings);
3507
- const agentKey = slugPart(input.agent || input.runtime || "claude", "claude");
3508
- const agent = settings.agents?.[agentKey] || { runtime: agentKey, model: input.model || "", command: agentKey, enabled: true };
3509
- if (agent.enabled === false) return { ok: false, error: "agent_disabled", agent: agentKey };
3510
-
3511
- const sessionId = slugPart(input.session_id || makeTinTinId("agent"), "agent");
3512
- const repoRoot = path.resolve(String(input.repo_root || settings.default_repo_root || settings.cwd || process.cwd()));
3513
- const top = gitTopLevel(repoRoot);
3514
- if (!top) return { ok: false, error: "not_git_repo", repo_root: repoRoot };
3515
-
3516
- const isolation = String(input.isolation || settings.isolation || "worktree");
3517
- const baseBranch = slugPart(input.base_branch || settings.base_branch || "develop", "develop");
3518
- let cwd = top;
3519
- let branch = null;
3520
- let worktreePath = null;
3521
- let worktreeCreated = false;
3522
- const commands = [];
3523
-
3524
- if (isolation === "worktree") {
3525
- const worktreeRoot = path.resolve(String(input.worktree_root || settings.worktree_root || path.join(path.dirname(top), `${path.basename(top)}.wt`)));
3526
- fs.mkdirSync(worktreeRoot, { recursive: true });
3527
- worktreePath = path.join(worktreeRoot, `tintin-${agentKey}-${sessionId}`);
3528
- if (!assertInside(worktreeRoot, worktreePath)) return { ok: false, error: "worktree_path_escape", worktree_root: worktreeRoot, worktree_path: worktreePath };
3529
- branch = `wt/tintin/${agentKey}/${sessionId}`;
3530
- if (!fs.existsSync(worktreePath)) {
3531
- execSyncTop(`git worktree add -b ${JSON.stringify(branch)} ${JSON.stringify(worktreePath)} ${JSON.stringify(baseBranch)}`, {
3532
- cwd: top,
3533
- stdio: ["ignore", "pipe", "pipe"],
3534
- timeout: 60_000,
3535
- windowsHide: true,
3536
- });
3537
- worktreeCreated = true;
3538
- commands.push(`git worktree add -b ${branch} ${worktreePath} ${baseBranch}`);
3539
- }
3540
- cwd = worktreePath;
3541
- } else if (isolation !== "cwd") {
3542
- return { ok: false, error: "unsupported_isolation", isolation };
3543
- }
3544
-
3545
- const runtime = agent.runtime || agentKey;
3546
- const command = agent.command || runtime;
3547
- const model = input.model || agent.model || "";
3548
- const launchCommand = runtime === "codex"
3549
- ? `${command} exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check -C ${JSON.stringify(cwd)} -`
3550
- : `${command} -p <prompt> --dangerously-skip-permissions`;
3551
-
3552
- const result = {
3553
- ok: true,
3554
- agent_session_id: sessionId,
3555
- agent: agentKey,
3556
- runtime,
3557
- model,
3558
- isolation,
3559
- repo_root: top,
3560
- cwd,
3561
- worktree_root: isolation === "worktree" ? path.dirname(worktreePath) : null,
3562
- worktree_path: worktreePath,
3563
- worktree_created: worktreeCreated,
3564
- branch,
3565
- base_branch: baseBranch,
3566
- launch_mode: settings.launch_mode || "setup_only",
3567
- launch_command: launchCommand,
3568
- commands,
3569
- };
3570
- const sessionRecord = {
3571
- agent_session_id: result.agent_session_id,
3572
- agent: result.agent,
3573
- runtime: result.runtime,
3574
- model: result.model,
3575
- isolation: result.isolation,
3576
- repo_root: result.repo_root,
3577
- cwd: result.cwd,
3578
- worktree_path: result.worktree_path,
3579
- branch: result.branch,
3580
- base_branch: result.base_branch,
3581
- launch_mode: result.launch_mode,
3582
- created_at: new Date().toISOString(),
3583
- };
3584
- const sessions = [sessionRecord, ...(Array.isArray(config.agent_sessions) ? config.agent_sessions : [])
3585
- .filter((item) => item && item.agent_session_id !== sessionId)]
3586
- .slice(0, 100);
3587
- saveTinTinConfig({ ...config, agent_sessions: sessions });
3588
- return result;
3589
- }
3590
-
3591
- function isLoopbackOrigin(origin) {
3592
- if (!origin) return false;
3593
- try {
3594
- const parsed = new URL(origin);
3595
- return ["localhost", "127.0.0.1", "::1"].includes(parsed.hostname);
3596
- } catch {
3597
- return false;
3598
- }
3599
- }
3600
-
3601
- function tintinAppForOrigin(config, origin) {
3602
- if (!origin) return null;
3603
- return (config.apps || []).find((app) => (
3604
- Array.isArray(app.origins) && app.origins.some((allowed) => allowed === origin || allowed === "*")
3605
- )) || null;
3606
- }
3607
-
3608
- function checkTinTinBrowserAccess(req) {
3609
- const config = loadTinTinConfig();
3610
- const origin = req.headers.origin || "";
3611
- const trustMode = config.trust_mode || "open-local";
3612
- const matchedApp = tintinAppForOrigin(config, origin);
3613
-
3614
- if (trustMode === "disabled") {
3615
- return { allowed: false, config, reason: "monkey_disabled" };
3616
- }
3617
- if (!origin) {
3618
- return { allowed: true, config, app: null, reason: "non_browser_local" };
3619
- }
3620
- if (matchedApp) {
3621
- return { allowed: true, config, app: matchedApp, reason: "configured_origin" };
3622
- }
3623
- if (trustMode === "open-local" && isLoopbackOrigin(origin)) {
3624
- return { allowed: true, config, app: null, reason: "open_local_loopback" };
3625
- }
3626
- return { allowed: false, config, reason: "origin_not_allowed", origin };
3627
- }
3628
-
3629
- function publicTinTinConfig(config) {
3630
- return {
3631
- schema_version: config.schema_version || 1,
3632
- trust_mode: config.trust_mode || "open-local",
3633
- config_path: config.path,
3634
- config_exists: !!config.exists,
3635
- config_error: config.error || null,
3636
- agent_settings: mergeTinTinAgentSettings(config.agent_settings),
3637
- sidebar_settings: mergeTinTinSectionSettings("sidebar_settings", config.sidebar_settings),
3638
- dispatch_settings: mergeTinTinSectionSettings("dispatch_settings", config.dispatch_settings),
3639
- codevelop_settings: mergeTinTinSectionSettings("codevelop_settings", config.codevelop_settings),
3640
- agent_sessions: Array.isArray(config.agent_sessions) ? config.agent_sessions : [],
3641
- apps: (config.apps || []).map((app) => ({
3642
- slug: app.slug,
3643
- origins: app.origins || [],
3644
- manifest_url: app.manifest_url || null,
3645
- cwd: app.cwd || null,
3646
- capabilities: app.capabilities || [],
3647
- })),
3648
- };
3649
- }
3650
-
3651
- function tintinSettingsHtml() {
3652
- return `<!DOCTYPE html>
3653
- <html lang="en">
3654
- <head>
3655
- <meta charset="utf-8">
3656
- <meta name="viewport" content="width=device-width,initial-scale=1">
3657
- <title>clauth TinTin Settings</title>
3658
- <style>
3659
- body{margin:0;background:#07100e;color:#d9fbe8;font-family:system-ui,-apple-system,Segoe UI,sans-serif}
3660
- header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px;border-bottom:1px solid #14352f;background:#091814}
3661
- h1{font-size:20px;margin:0}.sub{font-size:12px;color:#8dd8c2;margin-top:3px}.pill{display:inline-flex;align-items:center;border:1px solid #14532d;color:#86efac;padding:2px 7px;font-size:11px;margin-top:7px}
3662
- main{display:grid;grid-template-columns:minmax(300px,430px) 1fr;gap:14px;padding:14px}
3663
- .stack{display:grid;gap:14px}.wide{display:grid;gap:14px}
3664
- section{border:1px solid #14352f;background:#091814;padding:14px}
3665
- h2{font-size:15px;margin:0 0 10px;color:#dcfce7}
3666
- label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#8dd8c2;font-weight:700;margin:10px 0 4px}
3667
- input,select,textarea{box-sizing:border-box;width:100%;border:1px solid #1d4d43;background:#031312;color:#d9fbe8;padding:8px 9px;font:13px ui-monospace,SFMono-Regular,Consolas,monospace;outline:none}
3668
- textarea{min-height:170px;resize:vertical}.short{min-height:92px}.tiny{min-height:58px}
3669
- .row{display:grid;grid-template-columns:1fr 1fr;gap:8px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
3670
- button,a.btn{border:1px solid #2d6a5e;background:#08231f;color:#b8ffe1;padding:7px 10px;font-size:12px;font-weight:700;text-decoration:none;cursor:pointer}
3671
- button:hover,a.btn:hover{background:#0f2a24}.danger{border-color:#5f3b3b;color:#fecaca;background:#241010}
3672
- pre{white-space:pre-wrap;word-break:break-word;border:1px solid #0d2a25;background:#020b09;color:#c8f9df;padding:10px;max-height:460px;overflow:auto;font-size:12px}
3673
- .status{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:#86efac}.hint{font-size:12px;color:#7dd3c0;line-height:1.4}.check{display:flex;align-items:center;gap:8px;margin-top:10px;color:#d9fbe8;font-size:13px}.check input{width:auto}
3674
- @media(max-width:900px){main{grid-template-columns:1fr}.row{grid-template-columns:1fr}}
3675
- </style>
3676
- </head>
3677
- <body>
3678
- <header>
3679
- <div><h1>TinTin Settings</h1><div class="sub">Persistent clauth control plane for agent setup, app sidebars, blackboard dispatch, and co-develop relay.</div><div class="pill">GET/PUT /tintin/settings</div></div>
3680
- <a class="btn" href="/">Vault Dashboard</a>
3681
- </header>
3682
- <main>
3683
- <div class="stack">
3684
- <section>
3685
- <h2>Agent Setup</h2>
3686
- <div class="hint">Creates a proper isolated cwd for Claude or Codex. Worktree setup is explicit and returned before any supervisor launches a process.</div>
3687
- <div class="row">
3688
- <div><label>agent</label><select id="agent"><option value="claude">claude</option><option value="codex">codex</option></select></div>
3689
- <div><label>isolation</label><select id="isolation"><option value="worktree">worktree</option><option value="cwd">cwd</option></select></div>
3690
- </div>
3691
- <label>repo root</label><input id="repoRoot">
3692
- <label>worktree root</label><input id="worktreeRoot">
3693
- <div class="row">
3694
- <div><label>base branch</label><input id="baseBranch"></div>
3695
- <div><label>session id</label><input id="sessionId" placeholder="auto if blank"></div>
3696
- </div>
3697
- <div class="actions">
3698
- <button onclick="loadSettings()" title="GET /tintin/settings">Load</button>
3699
- <button onclick="saveSettings()" title="PUT /tintin/settings">Save</button>
3700
- <button onclick="setupAgent()" title="POST /tintin/agent-sessions">Setup Agent</button>
3701
- </div>
3702
- <p class="status" id="status">idle</p>
3703
- </section>
3704
- <section>
3705
- <h2>Stored Agent Sessions</h2>
3706
- <pre id="agentSessions">No sessions loaded.</pre>
3707
- </section>
3708
- <section>
3709
- <h2>Return</h2>
3710
- <pre id="result">No return yet.</pre>
3711
- </section>
3712
- </div>
3713
- <div class="wide">
3714
- <section>
3715
- <h2>Agent Defaults</h2>
3716
- <div class="row">
3717
- <div><label>Claude model</label><input id="claudeModel"></div>
3718
- <div><label>Codex model</label><input id="codexModel"></div>
3719
- </div>
3720
- <div class="row">
3721
- <div><label>Claude command</label><input id="claudeCommand"></div>
3722
- <div><label>Codex command</label><input id="codexCommand"></div>
3723
- </div>
3724
- <div class="row">
3725
- <label class="check"><input type="checkbox" id="claudeEnabled"> Claude enabled</label>
3726
- <label class="check"><input type="checkbox" id="codexEnabled"> Codex enabled</label>
3727
- </div>
3728
- </section>
3729
- <section>
3730
- <h2>Sidebar Apps</h2>
3731
- <div class="row">
3732
- <div><label>trust mode</label><select id="trustMode"><option value="open-local">open-local</option><option value="configured-origins">configured-origins</option><option value="disabled">disabled</option></select></div>
3733
- <div><label>default app slug</label><input id="defaultAppSlug"></div>
3734
- </div>
3735
- <div class="row">
3736
- <div><label>session ttl hours</label><input id="sessionTtlHours" type="number" min="1" step="1"></div>
3737
- <div><label>event history limit</label><input id="eventHistoryLimit" type="number" min="1" step="1"></div>
3738
- </div>
3739
- <label class="check"><input type="checkbox" id="sidebarEnabled"> Sidebar sessions enabled</label>
3740
- <label>apps JSON</label><textarea class="short" id="appsJson" spellcheck="false"></textarea>
3741
- </section>
3742
- <section>
3743
- <h2>Blackboard Dispatch</h2>
3744
- <div class="row">
3745
- <div><label>default agent</label><select id="dispatchDefaultAgent"><option value="claude">claude</option><option value="codex">codex</option></select></div>
3746
- <div><label>default model</label><input id="dispatchDefaultModel"></div>
3747
- </div>
3748
- <div class="row">
3749
- <div><label>max workers</label><input id="dispatchMaxWorkers" type="number" min="1" step="1"></div>
3750
- <label class="check"><input type="checkbox" id="dispatchEnabled"> Dispatch enabled</label>
3751
- </div>
3752
- </section>
3753
- <section>
3754
- <h2>Co-develop Relay</h2>
3755
- <div class="row">
3756
- <div><label>default repo</label><input id="codevelopDefaultRepo"></div>
3757
- <div><label>message retention</label><input id="codevelopRetention" type="number" min="1" step="1"></div>
3758
- </div>
3759
- <div class="row">
3760
- <label class="check"><input type="checkbox" id="codevelopEnabled"> Co-develop enabled</label>
3761
- <label class="check"><input type="checkbox" id="codevelopRequirePeer"> Require peer join</label>
3762
- </div>
3763
- </section>
3764
- <section>
3765
- <h2>Settings JSON</h2>
3766
- <textarea id="settingsJson" spellcheck="false"></textarea>
3767
- </section>
3768
- </div>
3769
- </main>
3770
- <script>
3771
- const BASE = location.origin;
3772
- let currentSettings = {};
3773
- function setStatus(text){ document.getElementById("status").textContent = text; }
3774
- function show(value){ document.getElementById("result").textContent = typeof value === "string" ? value : JSON.stringify(value,null,2); }
3775
- function num(id, fallback){ const n = Number(document.getElementById(id).value); return Number.isFinite(n) ? n : fallback; }
3776
- function applySettings(data){
3777
- const cfg = data.agent_settings ? data : data.config || data;
3778
- currentSettings = cfg;
3779
- const s = cfg.agent_settings || {};
3780
- const side = cfg.sidebar_settings || {};
3781
- const dispatch = cfg.dispatch_settings || {};
3782
- const codevelop = cfg.codevelop_settings || {};
3783
- const agents = s.agents || {};
3784
- const claude = agents.claude || {};
3785
- const codex = agents.codex || {};
3786
- document.getElementById("trustMode").value = cfg.trust_mode || "open-local";
3787
- document.getElementById("repoRoot").value = s.default_repo_root || "";
3788
- document.getElementById("worktreeRoot").value = s.worktree_root || "";
3789
- document.getElementById("baseBranch").value = s.base_branch || "develop";
3790
- document.getElementById("isolation").value = s.isolation || "worktree";
3791
- document.getElementById("claudeModel").value = claude.model || "";
3792
- document.getElementById("codexModel").value = codex.model || "";
3793
- document.getElementById("claudeCommand").value = claude.command || "claude";
3794
- document.getElementById("codexCommand").value = codex.command || "codex";
3795
- document.getElementById("claudeEnabled").checked = claude.enabled !== false;
3796
- document.getElementById("codexEnabled").checked = codex.enabled !== false;
3797
- document.getElementById("sidebarEnabled").checked = side.enabled !== false;
3798
- document.getElementById("defaultAppSlug").value = side.default_app_slug || "";
3799
- document.getElementById("sessionTtlHours").value = side.session_ttl_hours || 24;
3800
- document.getElementById("eventHistoryLimit").value = side.event_history_limit || 200;
3801
- document.getElementById("appsJson").value = JSON.stringify(cfg.apps || [],null,2);
3802
- document.getElementById("dispatchEnabled").checked = dispatch.enabled !== false;
3803
- document.getElementById("dispatchDefaultAgent").value = dispatch.default_agent || "claude";
3804
- document.getElementById("dispatchDefaultModel").value = dispatch.default_model || "";
3805
- document.getElementById("dispatchMaxWorkers").value = dispatch.max_concurrent_workers || 2;
3806
- document.getElementById("codevelopEnabled").checked = codevelop.enabled !== false;
3807
- document.getElementById("codevelopDefaultRepo").value = codevelop.default_repo || "";
3808
- document.getElementById("codevelopRetention").value = codevelop.message_retention_limit || 500;
3809
- document.getElementById("codevelopRequirePeer").checked = codevelop.require_peer_join !== false;
3810
- document.getElementById("agentSessions").textContent = JSON.stringify(cfg.agent_sessions || [],null,2);
3811
- document.getElementById("settingsJson").value = JSON.stringify(cfg,null,2);
3812
- }
3813
- function collectSettings(){
3814
- let apps;
3815
- try { apps = JSON.parse(document.getElementById("appsJson").value || "[]"); }
3816
- catch(e){ throw new Error("apps JSON: " + e.message); }
3817
- const parsed = JSON.parse(document.getElementById("settingsJson").value || "{}");
3818
- return {
3819
- ...parsed,
3820
- trust_mode: document.getElementById("trustMode").value,
3821
- agent_settings: {
3822
- ...(parsed.agent_settings || {}),
3823
- default_repo_root: document.getElementById("repoRoot").value,
3824
- worktree_root: document.getElementById("worktreeRoot").value,
3825
- base_branch: document.getElementById("baseBranch").value,
3826
- isolation: document.getElementById("isolation").value,
3827
- agents: {
3828
- ...((parsed.agent_settings || {}).agents || {}),
3829
- claude: {
3830
- ...(((parsed.agent_settings || {}).agents || {}).claude || {}),
3831
- runtime: "claude",
3832
- model: document.getElementById("claudeModel").value,
3833
- command: document.getElementById("claudeCommand").value,
3834
- enabled: document.getElementById("claudeEnabled").checked
3835
- },
3836
- codex: {
3837
- ...(((parsed.agent_settings || {}).agents || {}).codex || {}),
3838
- runtime: "codex",
3839
- model: document.getElementById("codexModel").value,
3840
- command: document.getElementById("codexCommand").value,
3841
- enabled: document.getElementById("codexEnabled").checked
3842
- }
3843
- }
3844
- },
3845
- sidebar_settings: {
3846
- ...(parsed.sidebar_settings || {}),
3847
- enabled: document.getElementById("sidebarEnabled").checked,
3848
- default_app_slug: document.getElementById("defaultAppSlug").value,
3849
- session_ttl_hours: num("sessionTtlHours", 24),
3850
- event_history_limit: num("eventHistoryLimit", 200)
3851
- },
3852
- dispatch_settings: {
3853
- ...(parsed.dispatch_settings || {}),
3854
- enabled: document.getElementById("dispatchEnabled").checked,
3855
- default_agent: document.getElementById("dispatchDefaultAgent").value,
3856
- default_model: document.getElementById("dispatchDefaultModel").value,
3857
- max_concurrent_workers: num("dispatchMaxWorkers", 2)
3858
- },
3859
- codevelop_settings: {
3860
- ...(parsed.codevelop_settings || {}),
3861
- enabled: document.getElementById("codevelopEnabled").checked,
3862
- default_repo: document.getElementById("codevelopDefaultRepo").value,
3863
- message_retention_limit: num("codevelopRetention", 500),
3864
- require_peer_join: document.getElementById("codevelopRequirePeer").checked
3865
- },
3866
- agent_sessions: Array.isArray(parsed.agent_sessions) ? parsed.agent_sessions : (currentSettings.agent_sessions || []),
3867
- apps
3868
- };
3869
- }
3870
- async function loadSettings(){
3871
- setStatus("GET /tintin/settings");
3872
- const r = await fetch(BASE + "/tintin/settings", { cache:"no-store" });
3873
- const data = await r.json();
3874
- applySettings(data);
3875
- show(data);
3876
- setStatus(r.ok ? "loaded" : "load failed");
3877
- }
3878
- async function saveSettings(){
3879
- setStatus("PUT /tintin/settings");
3880
- let body;
3881
- try { body = collectSettings(); }
3882
- catch(e){ setStatus("invalid JSON"); show(e.message); return; }
3883
- const r = await fetch(BASE + "/tintin/settings", { method:"PUT", headers:{ "Content-Type":"application/json" }, body: JSON.stringify(body) });
3884
- const data = await r.json();
3885
- applySettings(data);
3886
- show(data);
3887
- setStatus(r.ok ? "saved" : "save failed");
3888
- }
3889
- async function setupAgent(){
3890
- const body = {
3891
- agent: document.getElementById("agent").value,
3892
- isolation: document.getElementById("isolation").value,
3893
- repo_root: document.getElementById("repoRoot").value,
3894
- worktree_root: document.getElementById("worktreeRoot").value,
3895
- base_branch: document.getElementById("baseBranch").value,
3896
- session_id: document.getElementById("sessionId").value || undefined
3897
- };
3898
- setStatus("POST /tintin/agent-sessions");
3899
- const r = await fetch(BASE + "/tintin/agent-sessions", { method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify(body) });
3900
- const data = await r.json();
3901
- show(data);
3902
- setStatus(r.ok && data.ok ? "agent setup ready" : "agent setup failed");
3903
- if (r.ok && data.ok) loadSettings().catch(function(){});
3904
- }
3905
- loadSettings().catch(err => { setStatus("load failed"); show(err.message || String(err)); });
3906
- </script>
3907
- </body>
3908
- </html>`;
3909
- }
3910
-
3911
3295
  // ── call_agent noauth-host guard (Gate B, bearer-gated tunnel) ───────────────
3912
3296
  // Pure decision function so the policy is testable without a daemon. Decides
3913
3297
  // whether a /call-agent request may proceed.
@@ -5091,6 +4475,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5091
4475
  saveClients(oauthClients);
5092
4476
  const logMsg = `[${new Date().toISOString()}] OAuth: registered public client ${clientId} (${client.client_name})\n`;
5093
4477
  try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
4478
+ operation("oauth.client_register", { client_id: clientId, client_name: client.client_name }, null, { ok: true });
5094
4479
  res.writeHead(201, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
5095
4480
  return res.end(JSON.stringify(client));
5096
4481
  }
@@ -5214,6 +4599,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5214
4599
 
5215
4600
  const logMsg = `[${new Date().toISOString()}] OAuth: token issued for ${stored.client_id} (token=${accessToken.slice(0,8)}…)\n`;
5216
4601
  try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
4602
+ operation("oauth.token_issue", { client_id: stored.client_id }, null, { ok: true });
5217
4603
  res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
5218
4604
  return res.end(JSON.stringify({ access_token: accessToken, token_type: "Bearer", scope: "mcp:tools", expires_in: 86400 }));
5219
4605
  }
@@ -5224,7 +4610,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5224
4610
  function toolsForPath(p) {
5225
4611
  const tools = filterMcpToolsForWriteMode(MCP_TOOLS);
5226
4612
  if (p === "/gws") return tools.filter(t => t.name.startsWith("gws_"));
5227
- if (p === "/clauth") return tools.filter(t => t.name.startsWith("clauth_") || t.name === "tintin_dispatch" || t.name === "call_agent" || t.name.startsWith("terminal_") || t.name.startsWith("channel_") || t.name.startsWith("handoff_"));
4613
+ if (p === "/clauth") return tools.filter(t => t.name.startsWith("clauth_") || t.name === "call_agent" || t.name.startsWith("terminal_") || t.name.startsWith("channel_") || t.name.startsWith("handoff_"));
5228
4614
  if (p === "/fs") return tools.filter(t => t.name.startsWith("fs_"));
5229
4615
  if (p === "/chitchat") return tools.filter(t => t.name.startsWith("chitchat_"));
5230
4616
  if (p === "/codevelop") return tools.filter(t => t.name.startsWith("codevelop_"));
@@ -5530,8 +4916,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5530
4916
  return res.end(JSON.stringify({ error: "name and path are required" }));
5531
4917
  }
5532
4918
  const result = webdavService.addMount(name.trim(), mountPath.trim());
5533
- if (result.error) { res.writeHead(409, { "Content-Type": "application/json", ...CORS }); return res.end(JSON.stringify(result)); }
4919
+ if (result.error) {
4920
+ operation("webdav.mount_add", { name: name.trim() }, null, { ok: false, error: result.error });
4921
+ res.writeHead(409, { "Content-Type": "application/json", ...CORS }); return res.end(JSON.stringify(result));
4922
+ }
5534
4923
  webdavService.shutdown();
4924
+ operation("webdav.mount_add", { name: name.trim(), path: mountPath.trim() }, null, { ok: true });
5535
4925
  return ok(res, result);
5536
4926
  }
5537
4927
 
@@ -5539,8 +4929,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5539
4929
  if (writeGuard(req, res)) return;
5540
4930
  const mountName = decodeURIComponent(reqPath.slice("/webdav/mounts/".length));
5541
4931
  const result = webdavService.removeMount(mountName);
5542
- if (result.error) { res.writeHead(404, { "Content-Type": "application/json", ...CORS }); return res.end(JSON.stringify(result)); }
4932
+ if (result.error) {
4933
+ operation("webdav.mount_remove", { name: mountName }, null, { ok: false, error: result.error });
4934
+ res.writeHead(404, { "Content-Type": "application/json", ...CORS }); return res.end(JSON.stringify(result));
4935
+ }
5543
4936
  webdavService.shutdown();
4937
+ operation("webdav.mount_remove", { name: mountName }, null, { ok: true });
5544
4938
  return ok(res, result);
5545
4939
  }
5546
4940
 
@@ -5554,6 +4948,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5554
4948
  return res.end(JSON.stringify({ error: "Invalid JSON" }));
5555
4949
  }
5556
4950
  const result = startCodevelopSession(body || {});
4951
+ operation("codevelop.start", { session_id: result?.session_id }, null, { ok: !result?.error, error: result?.error });
5557
4952
  return ok(res, result);
5558
4953
  }
5559
4954
 
@@ -5565,12 +4960,19 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5565
4960
  }
5566
4961
  const result = joinCodevelopSession(body || {});
5567
4962
  if (result.error) {
4963
+ operation("codevelop.join", { session_id: body?.session_id }, null, { ok: false, error: result.error });
5568
4964
  res.writeHead(result.error === "not_found" ? 404 : 400, { "Content-Type": "application/json", ...CORS });
5569
4965
  return res.end(JSON.stringify(result));
5570
4966
  }
4967
+ operation("codevelop.join", { session_id: body?.session_id }, null, { ok: true });
5571
4968
  return ok(res, result);
5572
4969
  }
5573
4970
 
4971
+ // codevelop/send is a genuine mutation (adds a message) but excluded from
4972
+ // per-route logging by design — a live peer-to-peer relay can send at a
4973
+ // frequency that would bloat events.jsonl (same risk noted in the audit-
4974
+ // logging retrofit plan for codevelop/poll). Session lifecycle
4975
+ // (start/join/stop) is logged; message throughput is not.
5574
4976
  if (method === "POST" && reqPath === "/codevelop/send") {
5575
4977
  let body;
5576
4978
  try { body = await readBody(req); } catch {
@@ -5654,9 +5056,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5654
5056
  }
5655
5057
  const result = stopCodevelopSession(body?.session_id);
5656
5058
  if (result.error) {
5059
+ operation("codevelop.stop", { session_id: body?.session_id }, null, { ok: false, error: result.error });
5657
5060
  res.writeHead(404, { "Content-Type": "application/json", ...CORS });
5658
5061
  return res.end(JSON.stringify(result));
5659
5062
  }
5063
+ operation("codevelop.stop", { session_id: body?.session_id }, null, { ok: true });
5660
5064
  return ok(res, result);
5661
5065
  }
5662
5066
 
@@ -5844,6 +5248,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5844
5248
  saveTokens(oauthTokens);
5845
5249
  const logMsg = `[${new Date().toISOString()}] OAuth: rolled credentials — all clients and tokens invalidated\n`;
5846
5250
  try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
5251
+ operation("oauth.roll_creds", {}, null, { ok: true });
5847
5252
  return ok(res, { clients_cleared: true, tokens_invalidated: true });
5848
5253
  }
5849
5254
 
@@ -5857,10 +5262,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5857
5262
  }
5858
5263
  if (body.action === "stop") {
5859
5264
  stopTunnel();
5265
+ operation("tunnel.stop", {}, null, { ok: true });
5860
5266
  return ok(res, { status: tunnelStatus, running: false });
5861
5267
  }
5862
5268
  // start
5863
5269
  await startTunnel();
5270
+ operation("tunnel.start", {}, null, { ok: !!tunnelProc, url: tunnelUrl, error: tunnelError });
5864
5271
  return ok(res, { status: tunnelStatus, running: !!tunnelProc, url: tunnelUrl, error: tunnelError });
5865
5272
  }
5866
5273
 
@@ -5870,6 +5277,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5870
5277
  if (tunnelProc) return ok(res, { status: tunnelStatus, message: "already running" });
5871
5278
  tunnelStatus = "starting";
5872
5279
  startTunnel().catch(() => {});
5280
+ operation("tunnel.start", {}, null, { ok: true, status: "starting" });
5873
5281
  return ok(res, { status: "starting" });
5874
5282
  }
5875
5283
 
@@ -5877,6 +5285,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5877
5285
  if (method === "POST" && reqPath === "/tunnel/stop") {
5878
5286
  if (lockedGuard(res)) return;
5879
5287
  stopTunnel();
5288
+ operation("tunnel.stop", {}, null, { ok: true });
5880
5289
  return ok(res, { status: tunnelStatus });
5881
5290
  }
5882
5291
 
@@ -5919,8 +5328,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5919
5328
  stdio: "ignore",
5920
5329
  });
5921
5330
  child.unref();
5331
+ operation("ccandme.launch", {}, null, { ok: true });
5922
5332
  return ok(res, { ok: true, message: "CCandMe launched" });
5923
5333
  } catch (err) {
5334
+ operation("ccandme.launch", {}, null, { ok: false, error: err.message });
5924
5335
  res.writeHead(500, { "Content-Type": "application/json", ...CORS });
5925
5336
  return res.end(JSON.stringify({ error: err.message }));
5926
5337
  }
@@ -5928,6 +5339,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5928
5339
 
5929
5340
  // POST /restart — spawn fresh process then exit (keeps boot.key, vault stays unlocked)
5930
5341
  if (method === "POST" && reqPath === "/restart") {
5342
+ operation("daemon.restart_requested", { port }, null, { ok: true });
5931
5343
  ok(res, { ok: true, message: "restarting" });
5932
5344
  const { spawn } = await import("child_process");
5933
5345
  const cliEntry = path.resolve(__dirname, "../index.js");
@@ -5949,454 +5361,6 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5949
5361
  return;
5950
5362
  }
5951
5363
 
5952
- // GET /tintin/manifest — app manifest for the TinTin sidebar loader.
5953
- // GET /monkey/manifest — backward-compat alias.
5954
- if (method === "GET" && (reqPath === "/tintin/manifest" || reqPath === "/monkey/manifest")) {
5955
- return ok(res, {
5956
- schema_version: 1,
5957
- app: {
5958
- slug: "clauth-dashboard",
5959
- name: "clauth Dashboard",
5960
- },
5961
- load: {
5962
- mode: "external-sidebar",
5963
- requires_compile_in: false,
5964
- recommended_loader: "chrome-extension-or-tampermonkey",
5965
- userscript_url: "/tintin/sidebar.user.js",
5966
- },
5967
- local_clauth: {
5968
- base_url: `http://127.0.0.1:${port}`,
5969
- capabilities_endpoint: "/tintin/capabilities",
5970
- session_endpoint: "/tintin/sessions",
5971
- transport: "tintin",
5972
- },
5973
- repo: {
5974
- root_hint: "C:\\Dev\\regen-root",
5975
- cwd_hint: "C:\\Dev\\regen-root",
5976
- },
5977
- capabilities: ["general_chat", "skill_request", "handoff", "dashboard_test"],
5978
- agent_context: {
5979
- app: { slug: "clauth-dashboard" },
5980
- runtime: { requested_by: "clauth-dashboard-monkey" },
5981
- },
5982
- });
5983
- }
5984
-
5985
- // GET /tintin/sidebar.user.js — shared app-side TinTin sidebar loader.
5986
- // GET /monkey/sidebar.user.js — backward-compat alias.
5987
- if (method === "GET" && (reqPath === "/tintin/sidebar.user.js" || reqPath === "/monkey/sidebar.user.js")) {
5988
- const candidates = [
5989
- process.env.CLAUTH_MONKEY_SIDEBAR_SCRIPT,
5990
- "C:\\Dev\\regen-root\\scripts\\monkey-sidebar.user.js",
5991
- path.resolve(process.cwd(), "..", "regen-root", "scripts", "monkey-sidebar.user.js"),
5992
- ].filter(Boolean);
5993
- const scriptPath = candidates.find((candidate) => {
5994
- try { return fs.existsSync(candidate); } catch { return false; }
5995
- });
5996
- if (!scriptPath) {
5997
- res.writeHead(404, { "Content-Type": "application/json", ...CORS });
5998
- return res.end(JSON.stringify({ error: "monkey_sidebar_script_not_found" }));
5999
- }
6000
- res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-cache, no-store, must-revalidate", ...CORS });
6001
- return res.end(fs.readFileSync(scriptPath, "utf8"));
6002
- }
6003
-
6004
- // GET /tintin/capabilities — browser-safe local TinTin sidebar capability probe.
6005
- // GET /monkey/capabilities — backward-compat alias.
6006
- if (method === "GET" && (reqPath === "/tintin/capabilities" || reqPath === "/monkey/capabilities")) {
6007
- const access = checkTinTinBrowserAccess(req);
6008
- if (!access.allowed) {
6009
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6010
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6011
- }
6012
- return ok(res, {
6013
- ok: true,
6014
- status: password ? "available" : "locked",
6015
- locked: !password,
6016
- trust_mode: access.config.trust_mode || "open-local",
6017
- app: access.app?.slug || null,
6018
- config: publicTinTinConfig(access.config),
6019
- transports: ["dispatch", "sse", "polling", "result"],
6020
- endpoints: {
6021
- sessions: "/tintin/sessions",
6022
- events: "/tintin/sessions/:session_id/events",
6023
- messages: "/tintin/sessions/:session_id/messages",
6024
- result: "/tintin/messages/:message_id/result",
6025
- },
6026
- });
6027
- }
6028
-
6029
- // GET /tintin/config — local operator view of the app allowlist/config.
6030
- // GET /monkey/config — backward-compat alias.
6031
- if (method === "GET" && (reqPath === "/tintin/config" || reqPath === "/monkey/config")) {
6032
- const access = checkTinTinBrowserAccess(req);
6033
- if (!access.allowed) {
6034
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6035
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6036
- }
6037
- return ok(res, { ok: true, ...publicTinTinConfig(access.config) });
6038
- }
6039
-
6040
- if (method === "GET" && reqPath === "/tintin/settings/ui") {
6041
- res.writeHead(200, { "Content-Type": "text/html", ...CORS });
6042
- return res.end(tintinSettingsHtml());
6043
- }
6044
-
6045
- if (method === "GET" && (reqPath === "/tintin/settings" || reqPath === "/monkey/settings")) {
6046
- const config = loadTinTinConfig();
6047
- return ok(res, { ok: true, ...publicTinTinConfig(config) });
6048
- }
6049
-
6050
- if ((method === "PUT" || method === "POST") && (reqPath === "/tintin/settings" || reqPath === "/monkey/settings")) {
6051
- let body;
6052
- try { body = await readBody(req); } catch {
6053
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
6054
- return res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
6055
- }
6056
- try {
6057
- const saved = saveTinTinConfig(body || {});
6058
- return ok(res, { ok: true, ...publicTinTinConfig(saved) });
6059
- } catch (err) {
6060
- res.writeHead(500, { "Content-Type": "application/json", ...CORS });
6061
- return res.end(JSON.stringify({ ok: false, error: "settings_save_failed", message: err.message }));
6062
- }
6063
- }
6064
-
6065
- if (method === "POST" && (reqPath === "/tintin/agent-sessions" || reqPath === "/monkey/agent-sessions")) {
6066
- let body;
6067
- try { body = await readBody(req); } catch {
6068
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
6069
- return res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
6070
- }
6071
- try {
6072
- const result = createTinTinAgentSession(body || {});
6073
- if (!result.ok) {
6074
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
6075
- return res.end(JSON.stringify(result));
6076
- }
6077
- return ok(res, result);
6078
- } catch (err) {
6079
- res.writeHead(500, { "Content-Type": "application/json", ...CORS });
6080
- return res.end(JSON.stringify({ ok: false, error: "agent_session_setup_failed", message: err.message }));
6081
- }
6082
- }
6083
-
6084
- // POST /tintin/sessions — create or attach to a local CLI-backed TinTin session.
6085
- // POST /monkey/sessions — backward-compat alias.
6086
- if (method === "POST" && (reqPath === "/tintin/sessions" || reqPath === "/monkey/sessions")) {
6087
- const access = checkTinTinBrowserAccess(req);
6088
- if (!access.allowed) {
6089
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6090
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6091
- }
6092
- let body;
6093
- try { body = await readBody(req); } catch {
6094
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
6095
- return res.end(JSON.stringify({ error: "Invalid JSON body" }));
6096
- }
6097
- const session = createTinTinSession(body || {});
6098
- pushTinTinEvent(session.id, {
6099
- source: "clauth",
6100
- type: "status",
6101
- content: "session ready",
6102
- payload: { status: "ready" },
6103
- });
6104
- return ok(res, {
6105
- ok: true,
6106
- session_id: session.id,
6107
- status: session.status,
6108
- agent_context: session.agent_context,
6109
- last_seq: session.seq,
6110
- });
6111
- }
6112
-
6113
- // GET /tintin/sessions — list in-memory local TinTin sessions.
6114
- // GET /monkey/sessions — backward-compat alias.
6115
- if (method === "GET" && (reqPath === "/tintin/sessions" || reqPath === "/monkey/sessions")) {
6116
- const access = checkTinTinBrowserAccess(req);
6117
- if (!access.allowed) {
6118
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6119
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6120
- }
6121
- return ok(res, {
6122
- ok: true,
6123
- sessions: [...tintinSessions.values()].map((session) => ({
6124
- session_id: session.id,
6125
- status: session.status,
6126
- created_at: session.created_at,
6127
- updated_at: session.updated_at,
6128
- last_seq: session.seq,
6129
- agent_context: session.agent_context,
6130
- })),
6131
- });
6132
- }
6133
-
6134
- const tintinSessionMatch = reqPath.match(/^\/(?:tintin|monkey)\/sessions\/([^/]+)$/);
6135
- if (method === "GET" && tintinSessionMatch) {
6136
- const access = checkTinTinBrowserAccess(req);
6137
- if (!access.allowed) {
6138
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6139
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6140
- }
6141
- const sessionId = decodeURIComponent(tintinSessionMatch[1]);
6142
- const session = getTinTinSession(sessionId);
6143
- if (!session) {
6144
- res.writeHead(404, { "Content-Type": "application/json", ...CORS });
6145
- return res.end(JSON.stringify({ error: "not_found", session_id: sessionId }));
6146
- }
6147
- return ok(res, {
6148
- ok: true,
6149
- session_id: session.id,
6150
- status: session.status,
6151
- created_at: session.created_at,
6152
- updated_at: session.updated_at,
6153
- last_seq: session.seq,
6154
- agent_context: session.agent_context,
6155
- });
6156
- }
6157
-
6158
- const tintinMessagesMatch = reqPath.match(/^\/(?:tintin|monkey)\/sessions\/([^/]+)\/messages$/);
6159
- if (method === "GET" && tintinMessagesMatch) {
6160
- const access = checkTinTinBrowserAccess(req);
6161
- if (!access.allowed) {
6162
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6163
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6164
- }
6165
- const sessionId = decodeURIComponent(tintinMessagesMatch[1]);
6166
- const afterSeq = Number(url.searchParams.get("after_seq") || url.searchParams.get("after") || 0);
6167
- const events = listTinTinEvents(sessionId, Number.isFinite(afterSeq) ? afterSeq : 0);
6168
- if (!events) {
6169
- res.writeHead(404, { "Content-Type": "application/json", ...CORS });
6170
- return res.end(JSON.stringify({ error: "not_found", session_id: sessionId }));
6171
- }
6172
- return ok(res, { ok: true, session_id: sessionId, events });
6173
- }
6174
-
6175
- if (method === "POST" && tintinMessagesMatch) {
6176
- const access = checkTinTinBrowserAccess(req);
6177
- if (!access.allowed) {
6178
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6179
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6180
- }
6181
- if (!password) {
6182
- res.writeHead(401, { "Content-Type": "application/json", ...CORS });
6183
- return res.end(JSON.stringify({ error: "Vault is locked", locked: true }));
6184
- }
6185
- const sessionId = decodeURIComponent(tintinMessagesMatch[1]);
6186
- let session = getTinTinSession(sessionId);
6187
- let body;
6188
- try { body = await readBody(req); } catch {
6189
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
6190
- return res.end(JSON.stringify({ error: "Invalid JSON body" }));
6191
- }
6192
- if (!session) session = createTinTinSession({ ...(body || {}), session_id: sessionId });
6193
- const content = String(body?.content || body?.prompt || "").trim();
6194
- if (!content) {
6195
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
6196
- return res.end(JSON.stringify({ error: "content required" }));
6197
- }
6198
-
6199
- const messageId = body.message_id || makeTinTinId("msg");
6200
- const jobId = body.job_id || messageId;
6201
- const agentContext = normalizeAgentContext(body.agent_context || {
6202
- ...session.agent_context,
6203
- task: {
6204
- ...(session.agent_context?.task || {}),
6205
- intent: body.skill ? "skill_request" : "general_chat",
6206
- thread_id: sessionId,
6207
- },
6208
- });
6209
- pushTinTinEvent(sessionId, {
6210
- source: "sidebar",
6211
- type: "message",
6212
- role: "user",
6213
- message_id: messageId,
6214
- job_id: jobId,
6215
- content,
6216
- payload: { skill: body.skill || null },
6217
- });
6218
-
6219
- const prompt = buildTinTinPrompt({
6220
- prompt: body.skill ? `Skill: ${body.skill}\n\n${content}` : content,
6221
- job_id: jobId,
6222
- agent_context: agentContext,
6223
- });
6224
- const dispatchCwd = resolveDispatchCwd(body.cwd || session.cwd, agentContext);
6225
- const result = spawnClaudeTask(prompt, jobId, dispatchCwd, agentContext);
6226
- if (result.error) {
6227
- pushTinTinEvent(sessionId, {
6228
- source: "clauth",
6229
- type: "error",
6230
- role: "assistant",
6231
- message_id: messageId,
6232
- job_id: jobId,
6233
- content: result.message || result.error,
6234
- payload: { code: result.error, retryable: result.error === "concurrency_limit" },
6235
- });
6236
- res.writeHead(503, { "Content-Type": "application/json", ...CORS });
6237
- return res.end(JSON.stringify(result));
6238
- }
6239
-
6240
- tintinMessageIndex.set(messageId, { session_id: sessionId, job_id: jobId });
6241
- pushTinTinEvent(sessionId, {
6242
- source: "clauth",
6243
- type: "status",
6244
- role: "assistant",
6245
- message_id: messageId,
6246
- job_id: jobId,
6247
- content: "spawned",
6248
- payload: result,
6249
- });
6250
- startTinTinMessageMonitor(sessionId, messageId, jobId);
6251
- return ok(res, {
6252
- ok: true,
6253
- session_id: sessionId,
6254
- message_id: messageId,
6255
- job_id: jobId,
6256
- status: "spawned",
6257
- dispatch: result,
6258
- });
6259
- }
6260
-
6261
- const tintinEventsMatch = reqPath.match(/^\/(?:tintin|monkey)\/sessions\/([^/]+)\/events$/);
6262
- if (method === "GET" && tintinEventsMatch) {
6263
- const access = checkTinTinBrowserAccess(req);
6264
- if (!access.allowed) {
6265
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6266
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6267
- }
6268
- const sessionId = decodeURIComponent(tintinEventsMatch[1]);
6269
- const afterSeq = Number(url.searchParams.get("after_seq") || url.searchParams.get("after") || 0);
6270
- const session = getTinTinSession(sessionId);
6271
- if (!session) {
6272
- res.writeHead(404, { "Content-Type": "application/json", ...CORS });
6273
- return res.end(JSON.stringify({ error: "not_found", session_id: sessionId }));
6274
- }
6275
-
6276
- res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-store", "Connection": "keep-alive", ...CORS });
6277
- const send = (event) => {
6278
- const eventType = event.type || "message";
6279
- res.write(`event: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`);
6280
- };
6281
- res.write(`event: ready\ndata: ${JSON.stringify({ ok: true, session_id: sessionId, last_seq: session.seq })}\n\n`);
6282
- for (const event of listTinTinEvents(sessionId, Number.isFinite(afterSeq) ? afterSeq : 0) || []) send(event);
6283
- const unsubscribe = subscribeTinTinEvents(sessionId, send);
6284
- const heartbeat = setInterval(() => {
6285
- try { res.write(`event: heartbeat\ndata: ${JSON.stringify({ session_id: sessionId, at: new Date().toISOString() })}\n\n`); }
6286
- catch { clearInterval(heartbeat); unsubscribe(); }
6287
- }, 15000);
6288
- req.on("close", () => {
6289
- clearInterval(heartbeat);
6290
- unsubscribe();
6291
- });
6292
- return;
6293
- }
6294
-
6295
- const tintinResultMatch = reqPath.match(/^\/(?:tintin|monkey)\/messages\/([^/]+)\/result$/);
6296
- if (method === "GET" && tintinResultMatch) {
6297
- const access = checkTinTinBrowserAccess(req);
6298
- if (!access.allowed) {
6299
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6300
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6301
- }
6302
- const messageId = decodeURIComponent(tintinResultMatch[1]);
6303
- const ref = tintinMessageIndex.get(messageId);
6304
- if (!ref) {
6305
- res.writeHead(404, { "Content-Type": "application/json", ...CORS });
6306
- return res.end(JSON.stringify({ error: "not_found", message_id: messageId }));
6307
- }
6308
- const job = tintinJobs.get(ref.job_id);
6309
- const session = getTinTinSession(ref.session_id);
6310
- return ok(res, {
6311
- ok: true,
6312
- message_id: messageId,
6313
- session_id: ref.session_id,
6314
- job_id: ref.job_id,
6315
- status: job?.status || "unknown",
6316
- stdout: job?.stdout || "",
6317
- stderr: job?.stderr || "",
6318
- events: session?.events.filter((event) => event.message_id === messageId) || [],
6319
- });
6320
- }
6321
-
6322
- // POST /tintin/dispatch — browser-safe, config-checked CLI worker spawn.
6323
- // POST /monkey/dispatch, /dispatch, and /monkey-dispatch remain legacy backward-compat aliases.
6324
- if (method === "POST" && (reqPath === "/tintin/dispatch" || reqPath === "/dispatch" || reqPath === "/monkey-dispatch" || reqPath === "/monkey/dispatch")) {
6325
- if (reqPath === "/tintin/dispatch" || reqPath === "/monkey/dispatch") {
6326
- const access = checkTinTinBrowserAccess(req);
6327
- if (!access.allowed) {
6328
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6329
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6330
- }
6331
- } else if (req.headers.origin) {
6332
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6333
- return res.end(JSON.stringify({ ok: false, error: "legacy_dispatch_rejects_browser_origin", use: "/tintin/dispatch" }));
6334
- }
6335
- let body = "";
6336
- req.on("data", d => body += d);
6337
- req.on("end", () => {
6338
- try {
6339
- const { prompt, job_id, cwd, agent_context } = JSON.parse(body || "{}");
6340
- if (!prompt && !job_id) {
6341
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
6342
- return res.end(JSON.stringify({ error: "prompt required" }));
6343
- }
6344
- const normalizedContext = normalizeAgentContext(agent_context);
6345
- const wrappedPrompt = buildTinTinPrompt({ prompt, job_id, agent_context: normalizedContext });
6346
- const dispatchCwd = resolveDispatchCwd(cwd, normalizedContext);
6347
- const result = spawnClaudeTask(wrappedPrompt, job_id || "untracked", dispatchCwd, normalizedContext);
6348
- const status = result.error ? 503 : 200;
6349
- res.writeHead(status, { "Content-Type": "application/json", ...CORS });
6350
- res.end(JSON.stringify({ ...result, context: normalizedContext }));
6351
- } catch {
6352
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
6353
- res.end(JSON.stringify({ error: "invalid JSON" }));
6354
- }
6355
- });
6356
- return;
6357
- }
6358
-
6359
- // POST /tintin/dispatch/:jobId/kill — kill a running dispatch job.
6360
- // POST /monkey/dispatch/:jobId/kill — backward-compat alias.
6361
- const dispatchKillMatch = reqPath.match(/^\/(?:tintin|monkey)\/dispatch\/([^/]+)\/kill$/);
6362
- if (method === "POST" && dispatchKillMatch) {
6363
- const jobId = decodeURIComponent(dispatchKillMatch[1]);
6364
- const job = tintinJobs.get(jobId);
6365
- if (!job) {
6366
- res.writeHead(404, { "Content-Type": "application/json", ...CORS });
6367
- return res.end(JSON.stringify({ ok: false, error: "not_found" }));
6368
- }
6369
- if (job.pid && job.status === "running") {
6370
- try { process.kill(job.pid); } catch {}
6371
- job.status = "killed";
6372
- job.completed_at = new Date().toISOString();
6373
- }
6374
- res.writeHead(200, { "Content-Type": "application/json", ...CORS });
6375
- return res.end(JSON.stringify({ ok: true, status: job.status, jobId }));
6376
- }
6377
-
6378
- const dispatchStatusMatch = reqPath.match(/^\/(?:tintin|monkey)\/dispatch\/([^/]+)$/) || reqPath.match(/^\/dispatch\/([^/]+)$/);
6379
- if (method === "GET" && dispatchStatusMatch) {
6380
- if (reqPath.startsWith("/tintin/dispatch/") || reqPath.startsWith("/monkey/dispatch/")) {
6381
- const access = checkTinTinBrowserAccess(req);
6382
- if (!access.allowed) {
6383
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6384
- return res.end(JSON.stringify({ ok: false, error: access.reason, origin: access.origin || null }));
6385
- }
6386
- } else if (req.headers.origin) {
6387
- res.writeHead(403, { "Content-Type": "application/json", ...CORS });
6388
- return res.end(JSON.stringify({ ok: false, error: "legacy_dispatch_rejects_browser_origin", use: "/tintin/dispatch/:job_id" }));
6389
- }
6390
- const jobId = decodeURIComponent(dispatchStatusMatch[1]);
6391
- const job = tintinJobs.get(jobId);
6392
- if (!job) {
6393
- res.writeHead(404, { "Content-Type": "application/json", ...CORS });
6394
- return res.end(JSON.stringify({ error: "not_found", job_id: jobId }));
6395
- }
6396
- res.writeHead(200, { "Content-Type": "application/json", ...CORS });
6397
- return res.end(JSON.stringify(job));
6398
- }
6399
-
6400
5364
  // ── call_agent (Gate B) ───────────────────────────────────────────────────
6401
5365
  // POST /call-agent — lean one-shot Haiku dispatch through the warm pool.
6402
5366
  // sync (default): blocks, returns { ok, package, jobId, model, ms }.
@@ -6535,6 +5499,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
6535
5499
  try {
6536
5500
  const { name } = JSON.parse(body || "{}");
6537
5501
  const result = await startChitchatSession(name || "collab");
5502
+ operation("chitchat.start", { name: name || "collab" }, null, { ok: !result?.error, session_id: result?.session_id, error: result?.error });
6538
5503
  res.writeHead(200, { "Content-Type": "application/json", ...CORS });
6539
5504
  res.end(JSON.stringify(result));
6540
5505
  } catch (e) {
@@ -6545,14 +5510,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
6545
5510
  return;
6546
5511
  }
6547
5512
 
6548
- // POST /handoff, /tintin/handoff, and /monkey/handoff — visible local handoff from claude.ai or a sidebar.
6549
- if (method === "POST" && (reqPath === "/handoff" || reqPath === "/tintin/handoff" || reqPath === "/monkey/handoff")) {
5513
+ // POST /handoff — visible local handoff from claude.ai or a sidebar.
5514
+ if (method === "POST" && reqPath === "/handoff") {
6550
5515
  let body;
6551
5516
  try { body = await readBody(req); } catch {
6552
5517
  res.writeHead(400, { "Content-Type": "application/json", ...CORS });
6553
5518
  return res.end(JSON.stringify({ error: "invalid JSON" }));
6554
5519
  }
6555
5520
  const result = await startHandoffSession(body || {});
5521
+ operation("handoff.start", {}, null, { ok: !result.error, error: result.error });
6556
5522
  const status = result.error ? 503 : 200;
6557
5523
  res.writeHead(status, { "Content-Type": "application/json", ...CORS });
6558
5524
  return res.end(JSON.stringify(result));
@@ -6570,6 +5536,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
6570
5536
  return res.end(JSON.stringify({ error: "session_id and message required" }));
6571
5537
  }
6572
5538
  const result = sendChitchatMessage(session_id, message);
5539
+ operation("chitchat.send", { session_id }, null, { ok: !result.error, error: result.error });
6573
5540
  const status = result.error === 'not_found' ? 404 : result.error ? 400 : 200;
6574
5541
  res.writeHead(status, { "Content-Type": "application/json", ...CORS });
6575
5542
  res.end(JSON.stringify(result));
@@ -6594,6 +5561,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
6594
5561
  }
6595
5562
  const tier = knowledge_tier || 'db_only';
6596
5563
  const result = startTerminalSession(name, tier, context_md || null);
5564
+ operation("terminal.start", { name, tier }, null, { ok: !result.error, error: result.error });
6597
5565
  const status = result.error ? 503 : 200;
6598
5566
  res.writeHead(status, { "Content-Type": "application/json", ...CORS });
6599
5567
  res.end(JSON.stringify(result));
@@ -6617,6 +5585,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
6617
5585
  return res.end(JSON.stringify({ error: "session_id and message required" }));
6618
5586
  }
6619
5587
  const result = sendTerminalMessage(session_id, message);
5588
+ operation("terminal.send", { session_id }, null, { ok: !result.error, error: result.error });
6620
5589
  const status = result.error === 'session_busy' ? 409 : result.error ? 404 : 200;
6621
5590
  res.writeHead(status, { "Content-Type": "application/json", ...CORS });
6622
5591
  res.end(JSON.stringify(result));
@@ -6670,6 +5639,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
6670
5639
  return res.end(JSON.stringify({ error: "session_id required" }));
6671
5640
  }
6672
5641
  const result = stopTerminalSession(session_id);
5642
+ operation("terminal.stop", { session_id }, null, { ok: !result.error, error: result.error });
6673
5643
  const status = result.error ? 404 : 200;
6674
5644
  res.writeHead(status, { "Content-Type": "application/json", ...CORS });
6675
5645
  res.end(JSON.stringify(result));
@@ -6729,6 +5699,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
6729
5699
  channelEvents.push(entry);
6730
5700
  if (channelEvents.length > MAX_CHANNEL_EVENTS) channelEvents.shift();
6731
5701
  console.log(`[channel] queued event ${eventId} type=${event}`);
5702
+ operation("channel.event", { event, resource, repository }, null, { ok: true, event_id: eventId });
6732
5703
  return ok(res, { received: true, event_id: eventId });
6733
5704
  } catch {
6734
5705
  res.writeHead(400, { "Content-Type": "application/json", ...CORS });
@@ -6741,6 +5712,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
6741
5712
  // GET|POST /shutdown (for daemon stop — programmatic, keeps boot.key)
6742
5713
  // Accept POST as well — older scripts and curl default to POST
6743
5714
  if ((method === "GET" || method === "POST") && reqPath === "/shutdown") {
5715
+ operation("daemon.shutdown", { port }, null, { ok: true });
6744
5716
  stopTunnel();
6745
5717
  ok(res, { ok: true, message: "shutting down" });
6746
5718
  removePid();
@@ -6750,6 +5722,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
6750
5722
 
6751
5723
  // POST /shutdown-ui (user-initiated stop — clears boot.key so password is required on restart)
6752
5724
  if (method === "POST" && reqPath === "/shutdown-ui") {
5725
+ operation("daemon.shutdown_ui", { port }, null, { ok: true });
6753
5726
  stopTunnel();
6754
5727
  // Clear boot.key so watchdog can't auto-unlock on restart
6755
5728
  const bootKeyPath = getBootKeyPath();
@@ -6831,10 +5804,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
6831
5804
  if (promoted) {
6832
5805
  const okLog = `[${new Date().toISOString()}] Make-live: promoted to live on port ${LIVE_PORT}\n`;
6833
5806
  try { fs.appendFileSync(LOG_FILE, okLog); } catch {}
5807
+ operation("daemon.make_live", { from_port: port, live_port: LIVE_PORT }, null, { ok: true });
6834
5808
  ok(res, { ok: true, message: "promoted to live", live_port: LIVE_PORT });
6835
5809
  } else {
6836
5810
  const failLog = `[${new Date().toISOString()}] Make-live: new daemon failed to start on port ${LIVE_PORT}\n`;
6837
5811
  try { fs.appendFileSync(LOG_FILE, failLog); } catch {}
5812
+ operation("daemon.make_live", { from_port: port, live_port: LIVE_PORT }, null, { ok: false, error: "new daemon failed to start" });
6838
5813
  ok(res, { ok: false, error: "New daemon failed to start on live port — check log" });
6839
5814
  }
6840
5815
 
@@ -7173,6 +6148,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7173
6148
  tunnelStatus = "starting";
7174
6149
  startTunnel().catch(() => {});
7175
6150
  }
6151
+ operation("vault.unlock", {}, null, { ok: true });
7176
6152
  return ok(res, { ok: true, locked: false, write_token: writeSession.token, write_expires_at: new Date(writeSession.expiresAt).toISOString() });
7177
6153
  } catch (authErr) {
7178
6154
  const msg = authErr.message || "";
@@ -7198,6 +6174,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7198
6174
  // No strike, no hard-lock — the verdict was never rendered.
7199
6175
  const failLog = `[${new Date().toISOString()}] [BACKEND ${backendKind}] vault backend unreachable, no auth strike — ${detail}\n`;
7200
6176
  try { fs.appendFileSync(LOG_FILE, failLog); } catch {}
6177
+ operation("vault.unlock", {}, null, { ok: false, backend_error: true, kind: backendKind });
7201
6178
  res.writeHead(503, { "Content-Type": "application/json", ...CORS });
7202
6179
  return res.end(JSON.stringify({
7203
6180
  error: friendly,
@@ -7222,6 +6199,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7222
6199
  authHardLocked = true;
7223
6200
  const lockLog = `[${new Date().toISOString()}] Server rejected with terminal verdict${reasonSuffix} — hard-locking locally to stop strike accrual; recover via the runbook (unlock machine + re-seal boot.key)\n`;
7224
6201
  try { fs.appendFileSync(LOG_FILE, lockLog); } catch {}
6202
+ operation("vault.unlock", {}, null, { ok: false, terminal: true, reason: serverReason });
7225
6203
  res.writeHead(401, { "Content-Type": "application/json", ...CORS });
7226
6204
  return res.end(JSON.stringify({ error: "Vault rejected credentials — recovery required", reason: serverReason, hard_locked: true, terminal: true }));
7227
6205
  }
@@ -7229,9 +6207,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7229
6207
  authHardLocked = true;
7230
6208
  const lockLog = `[${new Date().toISOString()}] Auth failure limit reached — vault hard-locked\n`;
7231
6209
  try { fs.appendFileSync(LOG_FILE, lockLog); } catch {}
6210
+ operation("vault.unlock", {}, null, { ok: false, hard_locked: true, reason: serverReason });
7232
6211
  res.writeHead(401, { "Content-Type": "application/json", ...CORS });
7233
6212
  return res.end(JSON.stringify({ error: "Too many failed attempts — restart daemon to try again", reason: serverReason, hard_locked: true }));
7234
6213
  }
6214
+ operation("vault.unlock", {}, null, { ok: false, reason: serverReason, failures_remaining: authRemaining });
7235
6215
  res.writeHead(401, { "Content-Type": "application/json", ...CORS });
7236
6216
  return res.end(JSON.stringify({ error: "Invalid password", reason: serverReason, failures_remaining: authRemaining }));
7237
6217
  }
@@ -7245,6 +6225,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7245
6225
  stopTunnel();
7246
6226
  const logLine = `[${new Date().toISOString()}] Vault locked\n`;
7247
6227
  try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
6228
+ operation("vault.lock", {}, null, { ok: true });
7248
6229
  return ok(res, { ok: true, locked: true, hard_locked: authHardLocked });
7249
6230
  }
7250
6231
 
@@ -7266,10 +6247,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7266
6247
  try {
7267
6248
  const { token, timestamp } = deriveToken(password, machineHash);
7268
6249
  const result = await api.updateService(password, machineHash, token, timestamp, service, { name: newName, label: newName });
7269
- if (result.error) return strike(res, 502, result.error);
6250
+ if (result.error) {
6251
+ operation("service.rename", { service, new_name: newName }, null, { ok: false, error: result.error });
6252
+ return strike(res, 502, result.error);
6253
+ }
7270
6254
  invalidateServiceStatusCache(machineHash);
6255
+ operation("service.rename", { service, new_name: newName }, null, { ok: true });
7271
6256
  return ok(res, { ok: true, old_name: service, new_name: newName });
7272
6257
  } catch (err) {
6258
+ operation("service.rename", { service, new_name: newName }, null, { ok: false, error: err.message });
7273
6259
  return strike(res, 502, err.message);
7274
6260
  }
7275
6261
  }
@@ -7282,10 +6268,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7282
6268
  try {
7283
6269
  const { token, timestamp } = deriveToken(password, machineHash);
7284
6270
  const result = await api.removeService(password, machineHash, token, timestamp, service, `CONFIRM REMOVE ${service.toUpperCase()}`);
7285
- if (result.error) return strike(res, 502, result.error);
6271
+ if (result.error) {
6272
+ operation("service.delete", { service }, null, { ok: false, error: result.error });
6273
+ return strike(res, 502, result.error);
6274
+ }
7286
6275
  invalidateServiceStatusCache(machineHash);
6276
+ operation("service.delete", { service }, null, { ok: true });
7287
6277
  return ok(res, { ok: true, deleted: service });
7288
6278
  } catch (err) {
6279
+ operation("service.delete", { service }, null, { ok: false, error: err.message });
7289
6280
  return strike(res, 502, err.message);
7290
6281
  }
7291
6282
  }
@@ -7311,10 +6302,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7311
6302
  try {
7312
6303
  const { token, timestamp } = deriveToken(password, machineHash);
7313
6304
  const result = await api.enable(password, machineHash, token, timestamp, service, enabled);
7314
- if (result.error) return strike(res, 502, result.error);
6305
+ if (result.error) {
6306
+ operation("service.toggle", { service, enabled }, null, { ok: false, error: result.error });
6307
+ return strike(res, 502, result.error);
6308
+ }
7315
6309
  invalidateServiceStatusCache(machineHash);
6310
+ operation("service.toggle", { service, enabled }, null, { ok: true });
7316
6311
  return ok(res, { ok: true, service, enabled });
7317
6312
  } catch (err) {
6313
+ operation("service.toggle", { service, enabled }, null, { ok: false, error: err.message });
7318
6314
  return strike(res, 502, err.message);
7319
6315
  }
7320
6316
  }
@@ -7401,6 +6397,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7401
6397
  return res.end(JSON.stringify({ error: "Service name required" }));
7402
6398
  }
7403
6399
  const result = await rotationEngine.rotateService(service);
6400
+ operation("service.rotate", { service }, null, { ok: !result?.error, error: result?.error });
7404
6401
  return ok(res, result);
7405
6402
  }
7406
6403
 
@@ -7414,6 +6411,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7414
6411
  return res.end(JSON.stringify({ error: "Invalid JSON body" }));
7415
6412
  }
7416
6413
  rotationEngine.setExpiry(service, body.expires_at, body.rotation_days);
6414
+ operation("service.set_expiry", { service }, null, { ok: true, expires_at: body.expires_at, rotation_days: body.rotation_days });
7417
6415
  return ok(res, { ok: true, service, expires_at: body.expires_at });
7418
6416
  }
7419
6417
 
@@ -7597,8 +6595,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7597
6595
  });
7598
6596
  }
7599
6597
 
6598
+ operation("tunnel.setup.cf_token", {}, null, { ok: true, accountId, accountName });
7600
6599
  return ok(res, { ok: true, accountId, accountName });
7601
6600
  } catch (err) {
6601
+ operation("tunnel.setup.cf_token", {}, null, { ok: false, error: err.message });
7602
6602
  return strike(res, 502, err.message);
7603
6603
  }
7604
6604
  }
@@ -7669,8 +6669,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7669
6669
  tunnelHostname = hostname;
7670
6670
  tunnelUrl = `https://${hostname}`;
7671
6671
 
6672
+ operation("tunnel.setup.cf_save", { hostname, tunnelId }, null, { ok: true });
7672
6673
  return ok(res, { ok: true, hostname });
7673
6674
  } catch (err) {
6675
+ operation("tunnel.setup.cf_save", { hostname }, null, { ok: false, error: err.message });
7674
6676
  return strike(res, 502, err.message);
7675
6677
  }
7676
6678
  }
@@ -7750,8 +6752,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7750
6752
  tunnelHostname = hostname;
7751
6753
  tunnelUrl = `https://${hostname}`;
7752
6754
 
6755
+ operation("tunnel.setup.cf_create_api", { hostname, tunnelId }, null, { ok: true });
7753
6756
  return ok(res, { ok: true, tunnelId, hostname });
7754
6757
  } catch (err) {
6758
+ operation("tunnel.setup.cf_create_api", { hostname }, null, { ok: false, error: err.message });
7755
6759
  return strike(res, 502, err.message);
7756
6760
  }
7757
6761
  }
@@ -7792,9 +6796,13 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7792
6796
  const proc = spawn("cloudflared", ["tunnel", "login"], { stdio: ["ignore","pipe","pipe"], windowsHide: true });
7793
6797
  proc.stdout.on("data", d => d.toString().split("\n").forEach(l => l.trim() && sendEvt({ line: l })));
7794
6798
  proc.stderr.on("data", d => d.toString().split("\n").forEach(l => l.trim() && sendEvt({ line: l })));
7795
- proc.on("close", code => { sendEvt({ done: true, code }); res.end(); });
6799
+ proc.on("close", code => {
6800
+ operation("tunnel.setup.cf_login", {}, null, { ok: code === 0, exit_code: code });
6801
+ sendEvt({ done: true, code }); res.end();
6802
+ });
7796
6803
  req.on("close", () => { try { proc.kill(); } catch {} });
7797
6804
  } catch (err) {
6805
+ operation("tunnel.setup.cf_login", {}, null, { ok: false, error: err.message });
7798
6806
  sendEvt({ done: true, code: 1, error: err.message });
7799
6807
  res.end();
7800
6808
  }
@@ -7866,9 +6874,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7866
6874
  tunnelHostname = hostname;
7867
6875
  tunnelUrl = `https://${hostname}`;
7868
6876
 
6877
+ operation("tunnel.setup.cf_create", { name, hostname, tunnelId }, null, { ok: true });
7869
6878
  sendEvt({ done: true, tunnelId, hostname });
7870
6879
  res.end();
7871
6880
  } catch (err) {
6881
+ operation("tunnel.setup.cf_create", { name, hostname }, null, { ok: false, error: err.message });
7872
6882
  sendEvt({ error: err.message, done: true });
7873
6883
  res.end();
7874
6884
  }
@@ -7900,8 +6910,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7900
6910
  writeSession = makeWriteToken();
7901
6911
  const logLine = `[${new Date().toISOString()}] Password changed\n`;
7902
6912
  try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
6913
+ operation("vault.change_password", {}, null, { ok: true });
7903
6914
  return ok(res, { ok: true, write_token: writeSession.token, write_expires_at: new Date(writeSession.expiresAt).toISOString() });
7904
6915
  } catch (err) {
6916
+ operation("vault.change_password", {}, null, { ok: false, error: err.message });
7905
6917
  res.writeHead(502, { "Content-Type": "application/json", ...CORS });
7906
6918
  return res.end(JSON.stringify({ error: err.message }));
7907
6919
  }
@@ -7929,11 +6941,17 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7929
6941
  }
7930
6942
 
7931
6943
  try {
6944
+ // NEVER log `value` — it is the secret being written.
7932
6945
  const { result, snapshot, normalized } = await writeCredentialWithRecovery({ password, machineHash, service, value, logFile: LOG_FILE });
7933
- if (result.error) return strike(res, 502, result.error);
6946
+ if (result.error) {
6947
+ operation("service.set", { service }, null, { ok: false, error: result.error });
6948
+ return strike(res, 502, result.error);
6949
+ }
7934
6950
  invalidateServiceStatusCache(machineHash);
6951
+ operation("service.set", { service }, null, { ok: true, normalized });
7935
6952
  return ok(res, { ok: true, service, recovery_snapshot: snapshot?.ok ? true : false, normalized });
7936
6953
  } catch (err) {
6954
+ operation("service.set", { service }, null, { ok: false, error: err.message });
7937
6955
  return strike(res, 502, err.message);
7938
6956
  }
7939
6957
  }
@@ -7962,11 +6980,17 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7962
6980
  try {
7963
6981
  const randomHex = crypto.randomBytes(32).toString("hex");
7964
6982
  const token = `${prefix}${randomHex}`;
6983
+ // NEVER log `token` — it is the secret being generated/stored.
7965
6984
  const { result, snapshot } = await writeCredentialWithRecovery({ password, machineHash, service, value: token, logFile: LOG_FILE, normalize: false });
7966
- if (result.error) return strike(res, 502, result.error);
6985
+ if (result.error) {
6986
+ operation("service.generate_token", { service }, null, { ok: false, error: result.error });
6987
+ return strike(res, 502, result.error);
6988
+ }
7967
6989
  invalidateServiceStatusCache(machineHash);
6990
+ operation("service.generate_token", { service }, null, { ok: true });
7968
6991
  return ok(res, { token, service, stored: true, recovery_snapshot: snapshot?.ok ? true : false });
7969
6992
  } catch (err) {
6993
+ operation("service.generate_token", { service }, null, { ok: false, error: err.message });
7970
6994
  return strike(res, 502, err.message);
7971
6995
  }
7972
6996
  }
@@ -7996,10 +7020,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
7996
7020
  try {
7997
7021
  const { token, timestamp } = deriveToken(password, machineHash);
7998
7022
  const result = await api.addService(password, machineHash, token, timestamp, name.trim().toLowerCase(), label || name.trim(), type, description || "", project || undefined);
7999
- if (result.error) return strike(res, 502, result.error);
7023
+ if (result.error) {
7024
+ operation("service.add", { name: name.trim().toLowerCase() }, null, { ok: false, error: result.error });
7025
+ return strike(res, 502, result.error);
7026
+ }
8000
7027
  invalidateServiceStatusCache(machineHash);
7028
+ operation("service.add", { name: name.trim().toLowerCase() }, null, { ok: true, key_type: type });
8001
7029
  return ok(res, { ok: true, service: name.trim().toLowerCase() });
8002
7030
  } catch (err) {
7031
+ operation("service.add", { name: name.trim().toLowerCase() }, null, { ok: false, error: err.message });
8003
7032
  return strike(res, 502, err.message);
8004
7033
  }
8005
7034
  }
@@ -8033,10 +7062,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
8033
7062
  try {
8034
7063
  const { token, timestamp } = deriveToken(password, machineHash);
8035
7064
  const result = await api.updateService(password, machineHash, token, timestamp, service.toLowerCase(), updates);
8036
- if (result.error) return strike(res, 502, result.error);
7065
+ if (result.error) {
7066
+ operation("service.update", { name: service.toLowerCase() }, null, { ok: false, error: result.error });
7067
+ return strike(res, 502, result.error);
7068
+ }
8037
7069
  invalidateServiceStatusCache(machineHash);
7070
+ operation("service.update", { name: service.toLowerCase() }, null, { ok: true, fields: Object.keys(updates) });
8038
7071
  return ok(res, { ok: true, service: service.toLowerCase(), ...updates });
8039
7072
  } catch (err) {
7073
+ operation("service.update", { name: service.toLowerCase() }, null, { ok: false, error: err.message });
8040
7074
  return strike(res, 502, err.message);
8041
7075
  }
8042
7076
  }
@@ -8087,11 +7121,16 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
8087
7121
  try {
8088
7122
  const { token, timestamp } = deriveToken(password, machineHash);
8089
7123
  const result = await api.createEnrollment(password, machineHash, token, timestamp, label, ttlMinutes);
8090
- if (result.error) return strike(res, 502, result.error);
7124
+ if (result.error) {
7125
+ operation("machine.enroll", { label, target }, null, { ok: false, error: result.error });
7126
+ return strike(res, 502, result.error);
7127
+ }
8091
7128
  const localConfig = new Conf(getConfOptions());
8092
7129
  const supabaseUrl = localConfig.get("supabase_url") || process.env.CLAUTH_SUPABASE_URL || "";
8093
7130
  const anonKey = localConfig.get("supabase_anon_key") || process.env.CLAUTH_SUPABASE_ANON_KEY || "";
7131
+ // NEVER log `enrollment_code` — it is a one-time credential.
8094
7132
  const scriptPath = writeEnrollmentScript({ supabaseUrl, anonKey, enrollmentCode: result.enrollment_code, label, target });
7133
+ operation("machine.enroll", { label, target }, null, { ok: true, expires_at: result.expires_at });
8095
7134
  return ok(res, {
8096
7135
  ok: true,
8097
7136
  enrollment_code: result.enrollment_code,
@@ -8099,6 +7138,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
8099
7138
  script_path: scriptPath,
8100
7139
  });
8101
7140
  } catch (err) {
7141
+ operation("machine.enroll", { label, target }, null, { ok: false, error: err.message });
8102
7142
  return strike(res, 502, err.message);
8103
7143
  }
8104
7144
  }
@@ -8561,9 +7601,35 @@ async function actionForeground(opts) {
8561
7601
  console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
8562
7602
  console.log(chalk.gray(` Lockout: 3 failures → exit\n`));
8563
7603
 
7604
+ // Plugin discovery previously only ran under `serve supervisor` or an
7605
+ // on-demand rescan — normal `serve start`/`foreground` boot never ran it,
7606
+ // so a stale/never-discovered plugin list could sit unnoticed until
7607
+ // someone happened to hit rescan. Discovery touching disk must never be
7608
+ // allowed to crash daemon boot, hence the try/catch.
7609
+ let discoveryResult = null;
7610
+ let discoveryError = null;
7611
+ try {
7612
+ discoveryResult = discoverPlugins();
7613
+ } catch (err) {
7614
+ discoveryError = err;
7615
+ console.log(chalk.yellow(` ⚠ plugin discovery failed at boot: ${err.message}`));
7616
+ }
7617
+
8564
7618
  const server = createServer(password, whitelist, port, tunnelHostname);
8565
7619
  server.listen(port, bindHost, () => {
8566
7620
  if (!isolated) writePid(process.pid, port);
7621
+ try {
7622
+ operation("daemon.started", { port, isolated }, null, { ok: true, version: VERSION }, "system");
7623
+ operation(
7624
+ "daemon.plugin_discovery",
7625
+ { port },
7626
+ null,
7627
+ discoveryError
7628
+ ? { ok: false, error: discoveryError.message }
7629
+ : { ok: true, plugin_count: (discoveryResult?.plugins || discoveryResult || []).length },
7630
+ "system",
7631
+ );
7632
+ } catch { /* startup logging must never block the daemon from serving */ }
8567
7633
  console.log(chalk.green(` clauth serve → http://${bindHost}:${port}`));
8568
7634
  if (tunnelHostname) {
8569
7635
  console.log(chalk.cyan(` Tunnel: https://${tunnelHostname}/sse`));
@@ -8604,7 +7670,7 @@ async function actionForeground(opts) {
8604
7670
  import { createInterface } from "readline";
8605
7671
  import { execSync, spawn as spawnProc, spawnSync } from "child_process";
8606
7672
 
8607
- // ── TinTin dispatch (formerly Monkey) — headless Claude CLI worker ───
7673
+ // ── Shared headless Claude CLI worker helpers (used by call-agent, terminal, codevelop) ───
8608
7674
  function findClaudeBinary() {
8609
7675
  const candidates = [
8610
7676
  process.env.CLAUDE_BIN,
@@ -9095,7 +8161,7 @@ async function runCallAgent(args = {}) {
9095
8161
  const persistMeta = { prompt, skill_slug: args.skill_slug, model, agent_context: args.agent_context };
9096
8162
 
9097
8163
  if (mode === "async") {
9098
- const jobId = makeTinTinId("call");
8164
+ const jobId = `call-${crypto.randomUUID()}`;
9099
8165
  // Persist the queued row FIRST so the { jobId } we return is recoverable
9100
8166
  // even if the daemon restarts before the worker settles.
9101
8167
  await persistCallAgentDispatch({ jobId, ...persistMeta });
@@ -9119,307 +8185,6 @@ async function runCallAgent(args = {}) {
9119
8185
  return { ok: true, package: rec.package, jobId: rec.jobId, model: rec.model, ms: rec.ms };
9120
8186
  }
9121
8187
 
9122
- const tintinJobs = new Map();
9123
- const MAX_TINTIN_JOBS = 100;
9124
- const tintinSessions = new Map();
9125
- const tintinMessageIndex = new Map();
9126
- const tintinEventStreams = new Map();
9127
- const tintinMessageMonitors = new Map();
9128
- const MAX_TINTIN_SESSION_EVENTS = 500;
9129
-
9130
- function normalizeAgentContext(input = {}) {
9131
- const context = input && typeof input === "object" ? input : {};
9132
- const app = context.app && typeof context.app === "object" ? context.app : {};
9133
- const repo = context.repo && typeof context.repo === "object" ? context.repo : {};
9134
- const runtime = context.runtime && typeof context.runtime === "object" ? context.runtime : {};
9135
- const task = context.task && typeof context.task === "object" ? context.task : {};
9136
-
9137
- return {
9138
- app: {
9139
- slug: String(app.slug || context.app_slug || "unknown-app"),
9140
- route: String(app.route || context.route || "/"),
9141
- origin: String(app.origin || context.origin || "unknown-origin"),
9142
- },
9143
- repo: {
9144
- root: String(repo.root || context.repo_root || ""),
9145
- cwd: String(repo.cwd || context.cwd || ""),
9146
- },
9147
- runtime: {
9148
- agent: String(runtime.agent || context.agent || "clauth-cli"),
9149
- requested_by: String(runtime.requested_by || context.requested_by || "tintin"),
9150
- },
9151
- task: {
9152
- intent: String(task.intent || context.intent || "tintin-dispatch"),
9153
- thread_id: task.thread_id || context.thread_id || null,
9154
- },
9155
- };
9156
- }
9157
-
9158
- function makeTinTinId(prefix) {
9159
- return `${prefix}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
9160
- }
9161
-
9162
- function createTinTinSession(input = {}) {
9163
- const sessionId = input.session_id || input.thread_id || makeTinTinId("ms");
9164
- const agentContext = normalizeAgentContext(input.agent_context || {
9165
- app: {
9166
- slug: input.app_id || input.app_slug || "unknown-app",
9167
- route: input.route || "/",
9168
- origin: input.origin || "unknown-origin",
9169
- },
9170
- repo: {
9171
- root: input.repo_root || "",
9172
- cwd: input.cwd || "",
9173
- },
9174
- runtime: {
9175
- agent: "clauth-cli",
9176
- requested_by: "tintin-sidebar",
9177
- },
9178
- task: {
9179
- intent: input.intent || "general_chat",
9180
- thread_id: sessionId,
9181
- },
9182
- });
9183
- const existing = tintinSessions.get(sessionId);
9184
- if (existing) {
9185
- existing.agent_context = agentContext;
9186
- existing.updated_at = new Date().toISOString();
9187
- tintinSessions.set(sessionId, existing);
9188
- return existing;
9189
- }
9190
- const session = {
9191
- id: sessionId,
9192
- status: "ready",
9193
- created_at: new Date().toISOString(),
9194
- updated_at: new Date().toISOString(),
9195
- seq: 0,
9196
- agent_context: agentContext,
9197
- cwd: input.cwd || agentContext.repo.cwd || agentContext.repo.root || "",
9198
- events: [],
9199
- };
9200
- tintinSessions.set(sessionId, session);
9201
- return session;
9202
- }
9203
-
9204
- function getTinTinSession(sessionId) {
9205
- return tintinSessions.get(sessionId);
9206
- }
9207
-
9208
- function pushTinTinEvent(sessionId, event) {
9209
- const session = getTinTinSession(sessionId);
9210
- if (!session) return null;
9211
- session.seq += 1;
9212
- session.updated_at = new Date().toISOString();
9213
- const fullEvent = {
9214
- id: event.id || makeTinTinId("mev"),
9215
- session_id: sessionId,
9216
- seq: session.seq,
9217
- source: event.source || "clauth",
9218
- type: event.type || "status",
9219
- role: event.role,
9220
- message_id: event.message_id,
9221
- job_id: event.job_id,
9222
- content: event.content,
9223
- payload: event.payload || {},
9224
- created_at: new Date().toISOString(),
9225
- };
9226
- session.events.push(fullEvent);
9227
- session.events = session.events.slice(-MAX_TINTIN_SESSION_EVENTS);
9228
- tintinSessions.set(sessionId, session);
9229
- const streams = tintinEventStreams.get(sessionId);
9230
- if (streams) {
9231
- for (const send of [...streams]) {
9232
- try {
9233
- send(fullEvent);
9234
- } catch {
9235
- streams.delete(send);
9236
- }
9237
- }
9238
- }
9239
- return fullEvent;
9240
- }
9241
-
9242
- function listTinTinEvents(sessionId, afterSeq = 0) {
9243
- const session = getTinTinSession(sessionId);
9244
- if (!session) return null;
9245
- return session.events.filter((event) => event.seq > afterSeq);
9246
- }
9247
-
9248
- function subscribeTinTinEvents(sessionId, send) {
9249
- const streams = tintinEventStreams.get(sessionId) || new Set();
9250
- streams.add(send);
9251
- tintinEventStreams.set(sessionId, streams);
9252
- return () => {
9253
- const current = tintinEventStreams.get(sessionId);
9254
- if (!current) return;
9255
- current.delete(send);
9256
- if (current.size === 0) tintinEventStreams.delete(sessionId);
9257
- };
9258
- }
9259
-
9260
- function startTinTinMessageMonitor(sessionId, messageId, jobId) {
9261
- const key = `${sessionId}:${messageId}`;
9262
- if (tintinMessageMonitors.has(key)) return;
9263
- let lastStdoutLength = 0;
9264
- const timer = setInterval(() => {
9265
- const job = tintinJobs.get(jobId);
9266
- if (!job) return;
9267
- const stdout = job.stdout || "";
9268
- if (stdout.length > lastStdoutLength) {
9269
- const chunk = stdout.slice(lastStdoutLength);
9270
- lastStdoutLength = stdout.length;
9271
- pushTinTinEvent(sessionId, {
9272
- source: "claude",
9273
- type: "delta",
9274
- role: "assistant",
9275
- message_id: messageId,
9276
- job_id: jobId,
9277
- content: chunk,
9278
- });
9279
- }
9280
- if (job.status !== "running") {
9281
- clearInterval(timer);
9282
- tintinMessageMonitors.delete(key);
9283
- pushTinTinEvent(sessionId, {
9284
- source: "clauth",
9285
- type: job.status === "completed" ? "done" : "error",
9286
- role: "assistant",
9287
- message_id: messageId,
9288
- job_id: jobId,
9289
- content: job.status === "completed" ? "" : (job.stderr || job.stdout || "TinTin worker failed"),
9290
- payload: {
9291
- status: job.status,
9292
- exit_code: job.exit_code,
9293
- completed_at: job.completed_at,
9294
- },
9295
- });
9296
- }
9297
- }, 500);
9298
- tintinMessageMonitors.set(key, timer);
9299
- }
9300
-
9301
- function resolveDispatchCwd(requestedCwd, agentContext = {}) {
9302
- const raw = requestedCwd || agentContext?.repo?.cwd || agentContext?.repo?.root || CHITCHAT_FALLBACK_CWD;
9303
- if (!raw || typeof raw !== "string") return CHITCHAT_FALLBACK_CWD;
9304
- const resolved = path.resolve(raw);
9305
- try {
9306
- const st = fs.statSync(resolved);
9307
- if (!st.isDirectory()) return CHITCHAT_FALLBACK_CWD;
9308
- return resolved;
9309
- } catch {
9310
- return CHITCHAT_FALLBACK_CWD;
9311
- }
9312
- }
9313
-
9314
- function buildTinTinPrompt({ prompt, job_id, agent_context }) {
9315
- const normalized = normalizeAgentContext(agent_context);
9316
- const jobInstruction = job_id
9317
- ? [
9318
- `TinTin job id: ${job_id}`,
9319
- "If the prompt does not contain all job details, use clauth at http://127.0.0.1:52437 to fetch exact credentials and query the app database/API directly.",
9320
- "Do not assume agent MCP/plugin tools are available to this spawned CLI process.",
9321
- ].join("\n")
9322
- : "";
9323
-
9324
- const body = prompt || [
9325
- "Process this TinTin job using the provided agent context.",
9326
- jobInstruction,
9327
- "Find the job payload from the consuming app's declared data path before making changes.",
9328
- ].filter(Boolean).join("\n\n");
9329
-
9330
- return [
9331
- "You are a clauth-spawned TinTin CLI agent (model: claude-sonnet-4-6).",
9332
- "Agent context setup follows. Treat it as the runtime contract for this task.",
9333
- "```json",
9334
- JSON.stringify(normalized, null, 2),
9335
- "```",
9336
- "",
9337
- "Execution rules:",
9338
- "- You are a local CLI process, not the parent agent runtime.",
9339
- "- You do not inherit Codex/Claude MCP or plugin tools.",
9340
- "- Use clauth HTTP, direct HTTP APIs, repo files, or app endpoints for access.",
9341
- "- Keep results tied to the supplied app, route, cwd, thread, and job id.",
9342
- "",
9343
- jobInstruction,
9344
- "",
9345
- body,
9346
- ].filter(Boolean).join("\n");
9347
- }
9348
-
9349
- function spawnClaudeTask(prompt, jobId, cwd, agentContext) {
9350
- if (activeCliWorkers >= MAX_CLI_WORKERS) {
9351
- return { error: 'concurrency_limit', message: `Max ${MAX_CLI_WORKERS} CLI workers active` };
9352
- }
9353
- const binary = findClaudeBinary();
9354
- if (!binary) {
9355
- return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
9356
- }
9357
-
9358
- const resolvedCwd = resolveDispatchCwd(cwd, agentContext);
9359
- activeCliWorkers++;
9360
- const startedAtIso = new Date().toISOString();
9361
- tintinJobs.set(jobId, {
9362
- jobId,
9363
- status: "running",
9364
- cwd: resolvedCwd,
9365
- pid: null,
9366
- started_at: startedAtIso,
9367
- completed_at: null,
9368
- exit_code: null,
9369
- stdout: "",
9370
- stderr: "",
9371
- context: normalizeAgentContext(agentContext),
9372
- });
9373
- while (tintinJobs.size > MAX_TINTIN_JOBS) {
9374
- const firstKey = tintinJobs.keys().next().value;
9375
- tintinJobs.delete(firstKey);
9376
- }
9377
- const isCmdShim = process.platform === "win32" && /\.cmd$/i.test(binary);
9378
- const command = isCmdShim ? "cmd" : binary;
9379
- const args = isCmdShim
9380
- ? ["/d", "/s", "/c", `"${binary}"`, "-p", prompt, "--dangerously-skip-permissions"]
9381
- : ["-p", prompt, "--dangerously-skip-permissions"];
9382
- const proc = spawnProc(command, args, {
9383
- cwd: resolvedCwd,
9384
- env: process.env,
9385
- stdio: ['ignore', 'pipe', 'pipe'],
9386
- shell: false,
9387
- windowsHide: true,
9388
- });
9389
-
9390
- const tracked = tintinJobs.get(jobId);
9391
- if (tracked) {
9392
- tracked.pid = proc.pid;
9393
- tintinJobs.set(jobId, tracked);
9394
- }
9395
-
9396
- const startedAt = Date.now();
9397
- proc.stdout?.on("data", (chunk) => {
9398
- const trackedJob = tintinJobs.get(jobId);
9399
- if (!trackedJob) return;
9400
- trackedJob.stdout = `${trackedJob.stdout}${chunk.toString()}`.slice(-20000);
9401
- tintinJobs.set(jobId, trackedJob);
9402
- });
9403
- proc.stderr?.on("data", (chunk) => {
9404
- const trackedJob = tintinJobs.get(jobId);
9405
- if (!trackedJob) return;
9406
- trackedJob.stderr = `${trackedJob.stderr}${chunk.toString()}`.slice(-20000);
9407
- tintinJobs.set(jobId, trackedJob);
9408
- });
9409
- proc.on('close', (code) => {
9410
- activeCliWorkers--;
9411
- const trackedJob = tintinJobs.get(jobId);
9412
- if (trackedJob) {
9413
- trackedJob.status = code === 0 ? "completed" : "failed";
9414
- trackedJob.exit_code = code;
9415
- trackedJob.completed_at = new Date().toISOString();
9416
- tintinJobs.set(jobId, trackedJob);
9417
- }
9418
- console.log(`[tintin] job ${jobId} exited code=${code} in ${Date.now() - startedAt}ms`);
9419
- });
9420
-
9421
- return { status: 'spawned', pid: proc.pid, jobId, cwd: resolvedCwd, activeWorkers: activeCliWorkers };
9422
- }
9423
8188
 
9424
8189
  // ── Terminal session manager ─────────────────────────────────────
9425
8190
  // Rolling-context approach: each session stores a context string.
@@ -9437,7 +8202,7 @@ function generateSessionId() {
9437
8202
  }
9438
8203
 
9439
8204
  function defaultTerminalCwd() {
9440
- const dir = process.env.CLAUTH_TERMINAL_CWD || path.join(os.tmpdir(), "clauth-tintin-terminal");
8205
+ const dir = process.env.CLAUTH_TERMINAL_CWD || path.join(os.tmpdir(), "clauth-terminal");
9441
8206
  try {
9442
8207
  fs.mkdirSync(dir, { recursive: true });
9443
8208
  } catch {
@@ -9471,7 +8236,7 @@ function startTerminalSession(name, knowledge_tier, context_md, cwd, use_warm =
9471
8236
  };
9472
8237
 
9473
8238
  // Try to acquire a warm worker only when explicitly requested. The warm pool
9474
- // is optimized for call_agent one-shots; TinTin browser chat needs reliable
8239
+ // is optimized for call_agent one-shots; a terminal session needs reliable
9475
8240
  // repeated turns more than experimental low-latency pinning.
9476
8241
  if (use_warm) {
9477
8242
  try {
@@ -9679,12 +8444,12 @@ function showTerminalSession(session_id) {
9679
8444
 
9680
8445
  if (process.platform === "win32") {
9681
8446
  try {
9682
- const title = `TinTin ${session.name || session_id}`;
8447
+ const title = `clauth terminal ${session.name || session_id}`;
9683
8448
  const lines = [
9684
8449
  `$Host.UI.RawUI.WindowTitle = ${JSON.stringify(title)}`,
9685
8450
  `Set-Location -LiteralPath ${JSON.stringify(session.cwd || os.homedir())}`,
9686
8451
  "Clear-Host",
9687
- `Write-Host ${JSON.stringify(`TinTin session ${session_id}`)} -ForegroundColor Cyan`,
8452
+ `Write-Host ${JSON.stringify(`clauth terminal session ${session_id}`)} -ForegroundColor Cyan`,
9688
8453
  `Write-Host ${JSON.stringify(`Name: ${session.name || ""}`)}`,
9689
8454
  `Write-Host ${JSON.stringify(`Status: ${session.status}`)}`,
9690
8455
  `Write-Host ${JSON.stringify(`CWD: ${session.cwd || ""}`)}`,
@@ -10653,7 +9418,7 @@ function isAllowedGitImportPath(p, allowedPrefixes = FS_GIT_IMPORT_ALLOWED_PREFI
10653
9418
  return allowedPrefixes.some((prefix) => p === prefix.replace(/\/$/, "") || p.startsWith(prefix));
10654
9419
  }
10655
9420
 
10656
- const MCP_TOOLS = [
9421
+ export const MCP_TOOLS = [
10657
9422
  {
10658
9423
  name: "clauth_ping",
10659
9424
  description: "Check if the vault is locked or unlocked, show failure count",
@@ -10803,7 +9568,7 @@ const MCP_TOOLS = [
10803
9568
  },
10804
9569
  {
10805
9570
  name: "call_agent",
10806
- description: "Call a headless Haiku agent for a single prompt or skill and get the answer back. Lean one-shot through a warm pool (model default claude-haiku-4-5, ~4-5s warm). sync mode (default) blocks and returns { ok, package, jobId, model, ms }; async returns { ok, jobId } for polling. Use this for call-and-return — not tintin_dispatch (which is fire-and-poll).",
9571
+ description: "Call a headless Haiku agent for a single prompt or skill and get the answer back. Lean one-shot through a warm pool (model default claude-haiku-4-5, ~4-5s warm). sync mode (default) blocks and returns { ok, package, jobId, model, ms }; async returns { ok, jobId } for polling.",
10807
9572
  inputSchema: {
10808
9573
  type: "object",
10809
9574
  properties: {
@@ -10844,24 +9609,6 @@ const MCP_TOOLS = [
10844
9609
  additionalProperties: false,
10845
9610
  },
10846
9611
  },
10847
- {
10848
- name: "tintin_dispatch", // formerly monkey_dispatch
10849
- description: "Dispatch a TinTin job to a headless Claude Code CLI worker with an app/repo/runtime/task agent_context envelope. Max 2 concurrent workers.",
10850
- inputSchema: {
10851
- type: "object",
10852
- properties: {
10853
- prompt: { type: "string", description: "Full prompt for the CLI worker to execute. Optional when job_id is supplied." },
10854
- job_id: { type: "string", description: "tintin job UUID or app job id for tracking" },
10855
- cwd: { type: "string", description: "Existing local directory where the CLI worker should run" },
10856
- agent_context: {
10857
- type: "object",
10858
- description: "App-neutral context envelope with app, repo, runtime, and task fields",
10859
- additionalProperties: true,
10860
- },
10861
- },
10862
- additionalProperties: false,
10863
- },
10864
- },
10865
9612
  {
10866
9613
  name: "handoff_start",
10867
9614
  description: "Start a visible local Claude Code handoff session from a /handoff command and return immediately with session metadata.",
@@ -10913,7 +9660,7 @@ const MCP_TOOLS = [
10913
9660
  },
10914
9661
  {
10915
9662
  name: "terminal_show",
10916
- description: "Reveal/focus a TinTin terminal session on this Windows machine. If no live Claude process window exists, opens a visible session inspector at the session cwd.",
9663
+ description: "Reveal/focus a clauth terminal session on this Windows machine. If no live Claude process window exists, opens a visible session inspector at the session cwd.",
10917
9664
  inputSchema: {
10918
9665
  type: "object",
10919
9666
  properties: {
@@ -12819,19 +11566,6 @@ async function handleMcpTool(vault, name, args) {
12819
11566
  return mcpResult(JSON.stringify(result));
12820
11567
  }
12821
11568
 
12822
- case "tintin_dispatch": // formerly monkey_dispatch
12823
- case "monkey_dispatch": {
12824
- const { prompt, job_id, cwd: requestedCwd, agent_context } = args;
12825
- if (!prompt && !job_id) return mcpError("prompt required");
12826
- const fallbackCwd = await resolveChitchatRoot(vault);
12827
- const normalizedContext = normalizeAgentContext(agent_context);
12828
- const dispatchCwd = resolveDispatchCwd(requestedCwd || fallbackCwd, normalizedContext);
12829
- const wrappedPrompt = buildTinTinPrompt({ prompt, job_id, agent_context: normalizedContext });
12830
- const result = spawnClaudeTask(wrappedPrompt, job_id || "untracked", dispatchCwd, normalizedContext);
12831
- if (result.error) return mcpError(`${result.error}: ${result.message}`);
12832
- return mcpResult(JSON.stringify({ ...result, context: normalizedContext }));
12833
- }
12834
-
12835
11569
  case "handoff_start": {
12836
11570
  const result = await startHandoffSession(args || {}, vault);
12837
11571
  if (result.error) return mcpError(`${result.error}: ${result.message}`);
@@ -13957,7 +12691,8 @@ async function actionUpgrade(opts) {
13957
12691
  async function actionSupervisor(opts) {
13958
12692
  opts.isolated = true;
13959
12693
  opts.port = String(opts.port || getSupervisorPort());
13960
- discoverPlugins();
12694
+ // discoverPlugins() now runs unconditionally inside actionForeground at
12695
+ // every boot — calling it here too would double-run discovery.
13961
12696
  return actionForeground(opts);
13962
12697
  }
13963
12698