Fleet P2b: node sandbox-readiness check (hardened workload on a node)
Proves a connected node can host hardened agent workloads end-to-end, without
touching the agent run loop (zero blast radius on existing agents).
- Daemon: typed `sb_check` op — pulls a tiny image and runs it fully locked down
(cap-drop ALL, no-new-privileges, no network, read-only rootfs, non-root,
memory/pids caps), then tears it down. Fixed command; nothing caller-supplied
runs (preserves the exec-hardening invariant).
- cm-api: NodeHub.sandbox_check + POST /api/nodes/{id}/sandbox-check.
- UI: a shield "sandbox check" button on each online node card streams the
result (✓ SANDBOX READY + container id/uname).
This validates the full provision→run→destroy mechanism on nodes. The remaining
P2 work — wiring real agent deploys to auto-place onto nodes — is its own
subsystem (a RemoteDriver reusing the local DockerDriver for security parity,
agent-image distribution to nodes, and node-routing in SandboxManager) and is
best done as a focused pass; it is intentionally NOT bundled here to keep the
core agent path untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f5f96508eb
commit
33aa9c0693
@@ -4,7 +4,7 @@
|
||||
// comes from the nodes registry (`GET /api/nodes`), polled every 3s.
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Cpu, HardDrive, MemoryStick, Network, Plus, Server, Terminal, Trash2 } from "lucide-react";
|
||||
import { Cpu, HardDrive, MemoryStick, Network, Plus, Server, ShieldCheck, Terminal, Trash2 } from "lucide-react";
|
||||
|
||||
import { useFetchJson } from "@/lib/api/use-fetch";
|
||||
|
||||
@@ -79,6 +79,17 @@ function Bar({ label, pct, detail, color }: { label: string; pct: number; detail
|
||||
export function NodeCard({ node, onRemoved }: { node: FleetNode; onRemoved: () => void }) {
|
||||
const h = node.health;
|
||||
const [term, setTerm] = useState(false);
|
||||
const [check, setCheck] = useState<{ ok: boolean; output: string } | null>(null);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const runCheck = useCallback(() => {
|
||||
setChecking(true);
|
||||
setCheck(null);
|
||||
fetch(`/api/nodes/${node.id}/sandbox-check`, { method: "POST" })
|
||||
.then((r) => r.json())
|
||||
.then((d: { ok: boolean; output: string }) => setCheck(d))
|
||||
.catch((e: Error) => setCheck({ ok: false, output: e.message }))
|
||||
.finally(() => setChecking(false));
|
||||
}, [node.id]);
|
||||
const memPct = h && h.memTotal > 0 ? (h.memUsed / h.memTotal) * 100 : 0;
|
||||
const diskUsedPct = h && h.diskTotal > 0 ? ((h.diskTotal - h.diskFree) / h.diskTotal) * 100 : 0;
|
||||
const remove = useCallback(() => {
|
||||
@@ -99,7 +110,10 @@ export function NodeCard({ node, onRemoved }: { node: FleetNode; onRemoved: () =
|
||||
</div>
|
||||
</div>
|
||||
{node.status === "online" ? (
|
||||
<button type="button" onClick={() => setTerm(true)} title="Open terminal" aria-label="Open terminal" style={{ width: 30, height: 30, borderRadius: 8, border: "1px solid rgba(94,200,216,.3)", background: "rgba(94,200,216,.08)", color: "#5ec8d8", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><Terminal size={14} /></button>
|
||||
<>
|
||||
<button type="button" onClick={runCheck} disabled={checking} title="Run sandbox readiness check" aria-label="Sandbox check" style={{ width: 30, height: 30, borderRadius: 8, border: "1px solid rgba(95,208,138,.3)", background: "rgba(95,208,138,.08)", color: "#5fd08a", cursor: checking ? "default" : "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><ShieldCheck size={14} /></button>
|
||||
<button type="button" onClick={() => setTerm(true)} title="Open terminal" aria-label="Open terminal" style={{ width: 30, height: 30, borderRadius: 8, border: "1px solid rgba(94,200,216,.3)", background: "rgba(94,200,216,.08)", color: "#5ec8d8", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><Terminal size={14} /></button>
|
||||
</>
|
||||
) : null}
|
||||
<button type="button" onClick={remove} title="Remove node" aria-label="Remove node" style={{ width: 30, height: 30, borderRadius: 8, border: "1px solid rgba(255,255,255,.1)", background: "transparent", color: "#7a7a82", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><Trash2 size={14} /></button>
|
||||
</div>
|
||||
@@ -124,6 +138,16 @@ export function NodeCard({ node, onRemoved }: { node: FleetNode; onRemoved: () =
|
||||
{node.status === "pending" ? "waiting for the daemon to connect…" : "no health reported yet"}
|
||||
</div>
|
||||
)}
|
||||
{checking || check ? (
|
||||
<div style={{ borderRadius: 9, background: "#08080a", border: `1px solid ${check && !check.ok ? "rgba(255,111,97,.3)" : "rgba(95,208,138,.3)"}`, padding: 10 }}>
|
||||
<div style={{ fontFamily: mono, fontSize: 9.5, letterSpacing: ".08em", color: check && !check.ok ? "#ff8a7a" : "#5fd08a", marginBottom: check ? 6 : 0 }}>
|
||||
{checking ? "RUNNING SANDBOX CHECK…" : check?.ok ? "✓ SANDBOX READY" : "✗ CHECK FAILED"}
|
||||
</div>
|
||||
{check ? (
|
||||
<pre style={{ fontFamily: mono, fontSize: 10.5, color: "#bfe9d4", whiteSpace: "pre-wrap", wordBreak: "break-word", margin: 0, maxHeight: 120, overflow: "auto" }}>{check.output}</pre>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{term ? <NodeTerminal nodeId={node.id} nodeName={node.name} onClose={() => setTerm(false)} /> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user