Fleet P2b: run agent sandboxes on connected nodes (RemoteDriver + placement)
Agents can now provision their sandbox on a connected fleet node instead of the gateway host. Local stays the strict default, so existing agents are byte-for- byte unaffected until explicitly placed elsewhere. Security parity: the daemon links the REAL cm-sandbox DockerDriver and runs the typed container ops (sb_provision/sb_exec/sb_destroy/sb_health/sb_list) through it — identical hardening (cap-drop ALL, seccomp, no-net, read-only, non-root) to local sandboxes. cm-sandbox spec types are now Serialize/Deserialize so the spec crosses the channel. - cm-api: RemoteDriver (impl SandboxDriver over the node channel) + HubDriverProvider (impl cm_runtime::NodeDriverProvider, hands out a driver only for connected nodes via a sync online set) + NodeHub.call/is_connected. AppState.with_node_hub so the hub is shared with the placement provider. - cm-runtime SandboxManager: driver_for(node_id) routes by the recorded agent_containers.node_id (local default = existing driver, identical path); placement_node() reads the workspace setting and falls back to local if the node is offline; exec/release route accordingly. NodeDriverProvider trait. - DB: 0020_workspace_placement + repo (for_agent/get/set/clear). - main.rs: build the NodeHub first; inject HubDriverProvider into the agent manager + share the hub with AppState. - API+UI: GET/PUT /api/fleet/placement + a "Run agents on: Local / <node>" selector in the Fleet overview. Note: a node must be able to pull the agent image (the daemon docker-pulls it); interactive PTY for agent containers on remote nodes is not wired (Terminal app stays local) — the in-dashboard node shell already covers host access. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
33aa9c0693
commit
fb59378aa2
@@ -188,6 +188,39 @@ export function LocalHardware() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Choose where new agent sandboxes provision (a connected node, or local). */
|
||||
function PlacementSection() {
|
||||
const { nodes } = useNodes();
|
||||
const { data, refresh } = useFetchJson<{ node: string | null }>("/api/fleet/placement");
|
||||
const current = data?.node ?? "local";
|
||||
const online = nodes.filter((n) => n.status === "online");
|
||||
const set = useCallback(
|
||||
(node: string) => {
|
||||
fetch("/api/fleet/placement", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ node }) }).then(refresh);
|
||||
},
|
||||
[refresh],
|
||||
);
|
||||
return (
|
||||
<section style={{ borderRadius: 16, background: "#0f0f13", border: "1px solid rgba(255,255,255,.08)", padding: 18 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 9, marginBottom: 12 }}>
|
||||
<span style={{ width: 30, height: 30, borderRadius: 8, background: "rgba(255,111,97,.1)", border: "1px solid rgba(255,111,97,.25)", display: "flex", alignItems: "center", justifyContent: "center", color: "#ff8a7a" }}><Cpu size={15} /></span>
|
||||
<span style={{ fontSize: 15, fontWeight: 700, color: "#f3f3f5", flex: 1 }}>Run agents on</span>
|
||||
</div>
|
||||
<select
|
||||
value={current}
|
||||
onChange={(e) => set(e.target.value)}
|
||||
style={{ width: "100%", padding: "10px 12px", borderRadius: 10, border: "1px solid rgba(255,255,255,.14)", background: "#08080a", color: "#f3f3f5", fontSize: 13.5, cursor: "pointer" }}
|
||||
>
|
||||
<option value="local">Local — the gateway host (default)</option>
|
||||
{online.map((n) => (
|
||||
<option key={n.id} value={n.id}>{n.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<p style={{ fontSize: 12, color: "#7a7a82", marginTop: 10, marginBottom: 0, lineHeight: 1.5 }}>New agent sandboxes provision on this host. Falls back to local automatically if the node goes offline. Existing agents are unaffected until their sandbox is next created.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface TsDevice {
|
||||
name: string | null;
|
||||
addr: string | null;
|
||||
@@ -291,6 +324,10 @@ export function FleetOverview() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 22 }}>
|
||||
<PlacementSection />
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
|
||||
{nodes.map((n) => (
|
||||
<div key={n.id} style={{ display: "inline-flex", alignItems: "center", gap: 8, padding: "9px 13px", borderRadius: 999, background: "#101014", border: "1px solid rgba(255,255,255,.08)" }}>
|
||||
|
||||
Reference in New Issue
Block a user