Node-placed agent terminal: container PTY on the agent's node + WebRTC, shared node-local drives
Completes "agent on a node" (single-node): when an agent's placement points at a fleet node, its terminal container runs there and the browser reaches it over a direct WebRTC DataChannel (LAN speed), sharing a node-local volume with the sandbox. gw-04-local agents are byte-identical to before. - cm-sandbox/docker.rs: empty drive subpath → mount the whole volume at the target (volume_options None), so a per-agent node-local volume auto-creates at ~/drives. - cm-api/fleet.rs: NodeHub.open_pty/webrtc_offer carry optional container+session (injected only when Some); node-terminal caller passes None (host shell unchanged). - cm-runtime/terminals.rs: TerminalManager gains node_provider + placement (mirrors SandboxManager, draining-aware); node_local_drive_mount(agent) = clawmates_agent_<id> at ~/drives; placement_for() ensures + locates the container; attach uses driver_for(node) (local byte-identical). - cm-runtime/sandboxes.rs: a node-placed agent sandbox mounts the same per-agent volume → shares files with the terminal on that node. - cm-api/routes/terminal.rs: ticket response gains `node`; ws() bridges node-placed agents through the NodeHub relay (WebRTC + fallback) execing into the container; local path unchanged. server main wires with_node_provider. - frontend: agentTerminalConnector mints the ticket then picks WebRTC (node-placed, ⚡ direct / relayed badge) vs WS (local); webrtcConnector generalized to be endpoint-agnostic (node terminal reuses it). Known follow-up: terminal (uid 65532) and sandbox (uid 10001) share the volume but differ in uid — cross-container writes need an aligned uid/gid (group-writable). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
12212c72ac
commit
10c89f5157
@@ -13,7 +13,7 @@ import type { Agent } from "@/lib/api/schemas";
|
||||
import { panelParsers } from "@/lib/url/panel-params";
|
||||
|
||||
import { useSubHeader } from "./AppShell";
|
||||
import { useResilientTerminal, wsConnector } from "./terminal/core";
|
||||
import { agentTerminalConnector, useResilientTerminal, type TermMode } from "./terminal/core";
|
||||
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
@@ -279,15 +279,10 @@ function TerminalTab({
|
||||
useEffect(() => {
|
||||
activeRef.current = active;
|
||||
});
|
||||
const [mode, setMode] = useState<TermMode>("connecting");
|
||||
const { hostRef, refit } = useResilientTerminal(
|
||||
{
|
||||
connect: wsConnector(async () => {
|
||||
const res = await fetch(`/api/terminal/${agent.id}/ticket`, { method: "POST" });
|
||||
if (!res.ok) return null;
|
||||
const { ticket } = (await res.json()) as { ticket: string };
|
||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${proto}//${location.host}/api/terminal/${agent.id}/ws?ticket=${encodeURIComponent(ticket)}&session=${encodeURIComponent(session)}`;
|
||||
}),
|
||||
connect: agentTerminalConnector(agent.id, session, setMode),
|
||||
visible: () => activeRef.current,
|
||||
autoFocus: true,
|
||||
},
|
||||
@@ -302,10 +297,21 @@ function TerminalTab({
|
||||
}, [active, refit]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={hostRef}
|
||||
className="absolute inset-0 px-3 py-2"
|
||||
style={{ display: active ? "block" : "none" }}
|
||||
/>
|
||||
<div className="absolute inset-0" style={{ display: active ? "block" : "none" }}>
|
||||
{/* Transport badge — shown only when the container is node-placed. */}
|
||||
{mode === "direct" || mode === "relayed" ? (
|
||||
<div
|
||||
className="pointer-events-none absolute right-2 top-1.5 z-10 flex items-center gap-1 rounded px-1.5 py-0.5 font-mono text-[9px]"
|
||||
style={{
|
||||
background: mode === "direct" ? "rgba(95,208,138,.12)" : "rgba(255,255,255,.05)",
|
||||
border: `1px solid ${mode === "direct" ? "rgba(95,208,138,.3)" : "rgba(255,255,255,.1)"}`,
|
||||
color: mode === "direct" ? "#5fd08a" : "#8a8a92",
|
||||
}}
|
||||
>
|
||||
{mode === "direct" ? "⚡ direct" : "relayed"}
|
||||
</div>
|
||||
) : null}
|
||||
<div ref={hostRef} className="absolute inset-0 px-3 py-2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user