@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.
- package/dist/attach-endpoint.d.ts +53 -59
- package/dist/attach-endpoint.d.ts.map +1 -1
- package/dist/attach-endpoint.js +120 -244
- package/dist/attach-endpoint.js.map +1 -1
- package/dist/commands.js +9 -4
- package/dist/commands.js.map +1 -1
- package/dist/console/app.js +68 -30
- package/dist/console/index.html +3 -2
- package/dist/console/session-bundle.js +9736 -0
- package/dist/console-crypto-shim.d.ts +16 -0
- package/dist/console-crypto-shim.d.ts.map +1 -0
- package/dist/console-crypto-shim.js +20 -0
- package/dist/console-crypto-shim.js.map +1 -0
- package/dist/console-session-entry.d.ts +2 -0
- package/dist/console-session-entry.d.ts.map +1 -0
- package/dist/console-session-entry.js +26 -0
- package/dist/console-session-entry.js.map +1 -0
- package/dist/endpoint-evict.d.ts +29 -0
- package/dist/endpoint-evict.d.ts.map +1 -0
- package/dist/endpoint-evict.js +80 -0
- package/dist/endpoint-evict.js.map +1 -0
- package/dist/manager-service-contract.d.ts +132 -0
- package/dist/manager-service-contract.d.ts.map +1 -0
- package/dist/manager-service-contract.js +332 -0
- package/dist/manager-service-contract.js.map +1 -0
- package/dist/manager.d.ts +504 -22
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +2193 -257
- package/dist/manager.js.map +1 -1
- package/dist/runtime/pty.d.ts.map +1 -1
- package/dist/runtime/pty.js +15 -2
- package/dist/runtime/pty.js.map +1 -1
- package/dist/session/bridge.d.ts +63 -0
- package/dist/session/bridge.d.ts.map +1 -0
- package/dist/session/bridge.js +157 -0
- package/dist/session/bridge.js.map +1 -0
- package/dist/session/establish.d.ts +212 -0
- package/dist/session/establish.d.ts.map +1 -0
- package/dist/session/establish.js +174 -0
- package/dist/session/establish.js.map +1 -0
- package/dist/session/index.d.ts +12 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +15 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/plane.d.ts +120 -0
- package/dist/session/plane.d.ts.map +1 -0
- package/dist/session/plane.js +327 -0
- package/dist/session/plane.js.map +1 -0
- package/dist/static-lifecycle.d.ts +97 -0
- package/dist/static-lifecycle.d.ts.map +1 -0
- package/dist/static-lifecycle.js +262 -0
- package/dist/static-lifecycle.js.map +1 -0
- package/package.json +9 -5
package/dist/console/app.js
CHANGED
|
@@ -1,36 +1,32 @@
|
|
|
1
|
-
// Cotal console client: one xterm pane per managed agent, each wired
|
|
2
|
-
//
|
|
3
|
-
// (the
|
|
4
|
-
//
|
|
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
|
|
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,
|
|
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
|
|
18
|
-
// real data (/agents)
|
|
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.
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
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.
|
|
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
|
|
146
|
+
} catch {
|
|
109
147
|
meta.textContent = "manager unreachable";
|
|
110
148
|
}
|
|
111
149
|
}
|
package/dist/console/index.html
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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>
|