@cotal-ai/manager 0.16.0 → 0.17.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.
Files changed (53) hide show
  1. package/dist/attach-endpoint.d.ts +53 -59
  2. package/dist/attach-endpoint.d.ts.map +1 -1
  3. package/dist/attach-endpoint.js +120 -244
  4. package/dist/attach-endpoint.js.map +1 -1
  5. package/dist/commands.js +9 -4
  6. package/dist/commands.js.map +1 -1
  7. package/dist/console/app.js +68 -30
  8. package/dist/console/index.html +3 -2
  9. package/dist/console/session-bundle.js +9736 -0
  10. package/dist/console-crypto-shim.d.ts +16 -0
  11. package/dist/console-crypto-shim.d.ts.map +1 -0
  12. package/dist/console-crypto-shim.js +20 -0
  13. package/dist/console-crypto-shim.js.map +1 -0
  14. package/dist/console-session-entry.d.ts +2 -0
  15. package/dist/console-session-entry.d.ts.map +1 -0
  16. package/dist/console-session-entry.js +26 -0
  17. package/dist/console-session-entry.js.map +1 -0
  18. package/dist/endpoint-evict.d.ts +29 -0
  19. package/dist/endpoint-evict.d.ts.map +1 -0
  20. package/dist/endpoint-evict.js +80 -0
  21. package/dist/endpoint-evict.js.map +1 -0
  22. package/dist/manager-service-contract.d.ts +132 -0
  23. package/dist/manager-service-contract.d.ts.map +1 -0
  24. package/dist/manager-service-contract.js +332 -0
  25. package/dist/manager-service-contract.js.map +1 -0
  26. package/dist/manager.d.ts +504 -22
  27. package/dist/manager.d.ts.map +1 -1
  28. package/dist/manager.js +2193 -257
  29. package/dist/manager.js.map +1 -1
  30. package/dist/runtime/pty.d.ts.map +1 -1
  31. package/dist/runtime/pty.js +15 -2
  32. package/dist/runtime/pty.js.map +1 -1
  33. package/dist/session/bridge.d.ts +63 -0
  34. package/dist/session/bridge.d.ts.map +1 -0
  35. package/dist/session/bridge.js +157 -0
  36. package/dist/session/bridge.js.map +1 -0
  37. package/dist/session/establish.d.ts +212 -0
  38. package/dist/session/establish.d.ts.map +1 -0
  39. package/dist/session/establish.js +174 -0
  40. package/dist/session/establish.js.map +1 -0
  41. package/dist/session/index.d.ts +12 -0
  42. package/dist/session/index.d.ts.map +1 -0
  43. package/dist/session/index.js +15 -0
  44. package/dist/session/index.js.map +1 -0
  45. package/dist/session/plane.d.ts +120 -0
  46. package/dist/session/plane.d.ts.map +1 -0
  47. package/dist/session/plane.js +327 -0
  48. package/dist/session/plane.js.map +1 -0
  49. package/dist/static-lifecycle.d.ts +97 -0
  50. package/dist/static-lifecycle.d.ts.map +1 -0
  51. package/dist/static-lifecycle.js +262 -0
  52. package/dist/static-lifecycle.js.map +1 -0
  53. package/package.json +9 -5
@@ -1,36 +1,32 @@
1
- // Cotal console client: one xterm pane per managed agent, each wired straight to
2
- // the manager's attach WebSocket (addon-attach). We discover agents over HTTP
3
- // (the manager's /agents list, which it cross-references with mesh presence) and
4
- // stream the actual terminal bytes over the direct socket never the mesh.
1
+ // Cotal console client (P2 item 6): one xterm pane per managed agent, each wired to the manager
2
+ // over a mesh §13.6 SESSION — never a 127.0.0.1 terminal socket. Per pane we POST /session/<name>
3
+ // (the loopback face mints a holder-bound offer + a per-session, rails-only caller credential),
4
+ // connect to the broker's WebSocket listener with that credential, open the caller rail with the
5
+ // redeemed grant, and drive xterm over the SAME core rail code the manager serves. Agents are
6
+ // discovered over HTTP (/agents, cross-referenced with mesh presence).
5
7
  const { Terminal } = window;
6
8
  const { FitAddon } = window.FitAddon;
7
- const { AttachAddon } = window.AttachAddon;
9
+ const S = window.CotalSession; // the served session bundle (wsconnect + core rail + frame codec)
8
10
 
9
11
  const grid = document.getElementById("grid");
10
12
  const empty = document.getElementById("empty");
11
13
  const meta = document.getElementById("meta");
12
- const panes = new Map(); // name -> { el, term, fit, ws, status }
14
+ const panes = new Map(); // name -> { el, term, fit, session, status }
13
15
 
14
16
  // The console token, taken once from this page's own URL and kept only in memory. Deliberately not
15
17
  // stored in a cookie: cookies are host-scoped and not port-scoped, so a cookie set here would be
16
18
  // sent to every other HTTP service on this host, and two managers on one host would clobber each
17
- // other's. The manager serves the static shell openly and requires this on the routes that carry
18
- // real data (/agents) and on the attach socket.
19
+ // other's. The manager serves the static shell openly and requires this on every route that carries
20
+ // real data (/agents, /feed) or mints one (/session/<name>).
19
21
  // Preferred source is the FRAGMENT: a browser never sends it to a server, so the credential stays
20
22
  // out of request logs, proxies, and `Referer`. The query is accepted as a fallback for a hand-built
21
- // URL. Either way it lives only in this variable — never a cookie (host-scoped, not port-scoped, so
22
- // it would leak to every other service on this host and collide between two managers).
23
+ // URL.
23
24
  const TOKEN =
24
25
  new URLSearchParams(location.hash.replace(/^#/, "")).get("t") ??
25
26
  new URLSearchParams(location.search).get("t") ??
26
27
  "";
27
28
  const withToken = (path) => `${path}${path.includes("?") ? "&" : "?"}t=${encodeURIComponent(TOKEN)}`;
28
29
 
29
- function wsUrl(name) {
30
- const proto = location.protocol === "https:" ? "wss" : "ws";
31
- return `${proto}://${location.host}${withToken(`/attach/${encodeURIComponent(name)}`)}`;
32
- }
33
-
34
30
  function layout() {
35
31
  const n = panes.size;
36
32
  const cols = n <= 1 ? 1 : n <= 4 ? 2 : 3;
@@ -56,30 +52,71 @@ function addPane(agent) {
56
52
  term.loadAddon(fit);
57
53
  term.open(el.querySelector(".term"));
58
54
 
59
- const ws = new WebSocket(wsUrl(agent.name));
60
- ws.binaryType = "arraybuffer";
61
- ws.onopen = () => {
62
- term.loadAddon(new AttachAddon(ws));
63
- fit.fit();
64
- sendResize(ws, term);
65
- };
66
- // addon-attach forwards keystrokes; resize is our own text frame (r:cols,rows).
67
- term.onResize(() => sendResize(ws, term));
68
-
69
- const pane = { el, term, fit, ws, status: agent.status };
55
+ const pane = { el, term, fit, session: undefined, status: agent.status };
70
56
  panes.set(agent.name, pane);
71
57
  empty.style.display = "none";
72
58
  layout();
59
+ void openSession(agent.name, pane);
73
60
  }
74
61
 
75
- function sendResize(ws, term) {
76
- if (ws.readyState === WebSocket.OPEN) ws.send(`r:${term.cols},${term.rows}`);
62
+ // Open the mesh §13.6 session for one pane: POST /session, connect over the broker ws with the
63
+ // per-session cred, open the caller rail with the grant, and bridge it to xterm.
64
+ async function openSession(name, pane) {
65
+ const { term, fit } = pane;
66
+ if (!S) { term.write("\r\n[cotal: session bundle not loaded]\r\n"); return; }
67
+ let est;
68
+ try {
69
+ const res = await fetch(withToken(`/session/${encodeURIComponent(name)}`), { method: "POST" });
70
+ if (!res.ok) { term.write(`\r\n[cotal: attach failed (${res.status})]\r\n`); return; }
71
+ est = await res.json();
72
+ } catch { term.write("\r\n[cotal: attach request failed]\r\n"); return; }
73
+ const { grant, wsUrl, creds } = est;
74
+ let nc;
75
+ try {
76
+ nc = await S.wsconnect({
77
+ servers: wsUrl,
78
+ ...(creds ? { authenticator: S.credsAuthenticator(new TextEncoder().encode(creds)) } : {}),
79
+ });
80
+ } catch { term.write("\r\n[cotal: broker connection failed]\r\n"); return; }
81
+ if (!panes.has(name)) { void nc.close(); return; } // pane removed while connecting
82
+
83
+ // Mark the pane's dot as dead when the SESSION ends — honest even if /agents still lists the
84
+ // agent for a beat (the terminal is gone). `ended` wins over the roster-poll dot (setStatus).
85
+ const markEnded = () => { pane.ended = true; const dot = pane.el.querySelector(".dot"); if (dot) dot.className = "dot exited"; };
86
+ const rail = S.openSessionRail({
87
+ nc,
88
+ grant,
89
+ role: "caller",
90
+ onData: (data) => {
91
+ let frame;
92
+ try { frame = S.decodeTerminalFrame(data); } catch { return; }
93
+ if (frame.k === "data") term.write(S.terminalFrameBytes(frame));
94
+ else if (frame.k === "end") { term.write(`\r\n[cotal: session ended - ${frame.reason}]\r\n`); markEnded(); }
95
+ else if (frame.k === "drop") term.write(`\r\n[cotal: ${frame.bytes} bytes dropped - backpressure]\r\n`);
96
+ },
97
+ onClose: () => { term.write("\r\n[cotal: session closed]\r\n"); markEnded(); },
98
+ onProtocolError: (reason) => { term.write(`\r\n[cotal: session error - ${reason}]\r\n`); markEnded(); },
99
+ });
100
+
101
+ // The caller's `out` subscription must be live before the serving side replays (EPS is at-most-
102
+ // once, no retention). flush() forces the SUB, then send `ready` and go.
103
+ await nc.flush();
104
+ try { rail.send({ k: "ready" }); } catch { /* the onProtocolError path surfaces it */ }
105
+ fit.fit();
106
+ // xterm's onData is a STRING, but the §13.6 data frame carries raw bytes — UTF-8-encode it (a raw
107
+ // string would base64-encode its chars as NUL bytes the pty silently ignores: no echo).
108
+ term.onData((data) => { try { rail.send(S.encodeTerminalData(new TextEncoder().encode(data))); } catch (e) { console.error("[cotal] keystroke send failed", e); } });
109
+ // Guard the resize: a pane fitting before it is laid out can compute a 0 dimension, which the
110
+ // §13.6 codec rejects — a bad frame the serving side now tolerates, but never emit it in the first place.
111
+ term.onResize(() => { if (term.cols > 0 && term.rows > 0) { try { rail.send({ k: "resize", cols: term.cols, rows: term.rows }); } catch { /* advisory */ } } });
112
+ pane.session = { nc, rail };
77
113
  }
78
114
 
79
115
  function removePane(name) {
80
116
  const p = panes.get(name);
81
117
  if (!p) return;
82
- try { p.ws.close(); } catch {}
118
+ try { p.session?.rail.close(); } catch {}
119
+ try { void p.session?.nc.close(); } catch {}
83
120
  p.term.dispose();
84
121
  p.el.remove();
85
122
  panes.delete(name);
@@ -91,6 +128,7 @@ function setStatus(name, status) {
91
128
  const p = panes.get(name);
92
129
  if (!p) return;
93
130
  p.status = status;
131
+ if (p.ended) { p.el.querySelector(".dot").className = "dot exited"; return; } // a dead session stays dead in the UI
94
132
  p.el.querySelector(".dot").className = `dot ${status === "running" ? "running" : "exited"}`;
95
133
  }
96
134
 
@@ -105,7 +143,7 @@ async function poll() {
105
143
  setStatus(a.name, a.status);
106
144
  }
107
145
  for (const name of [...panes.keys()]) if (!seen.has(name)) removePane(name);
108
- } catch (e) {
146
+ } catch {
109
147
  meta.textContent = "manager unreachable";
110
148
  }
111
149
  }
@@ -58,11 +58,12 @@
58
58
  <h1>COTAL CONSOLE</h1>
59
59
  <span class="meta" id="meta">connecting…</span>
60
60
  </header>
61
- <div id="grid"><div id="empty">no agents yet <code>cotal start --agent claude --name …</code></div></div>
61
+ <div id="grid"><div id="empty">no agents yet: <code>cotal start --agent claude --name …</code></div></div>
62
62
 
63
63
  <script src="/assets/xterm.js"></script>
64
64
  <script src="/assets/addon-fit.js"></script>
65
- <script src="/assets/addon-attach.js"></script>
65
+ <!-- P2 item 6: the mesh §13.6 session client bundle (wsconnect + core rail + frame codec). -->
66
+ <script src="/session-bundle.js"></script>
66
67
  <script src="/app.js"></script>
67
68
  </body>
68
69
  </html>