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
@@ -23,4 +23,5 @@ pub mod terminal_tabs;
|
||||
pub mod threads;
|
||||
pub mod topology_runs;
|
||||
pub mod users;
|
||||
pub mod workspace_placement;
|
||||
pub mod workspaces;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//! Per-workspace default placement node for new agent sandboxes.
|
||||
|
||||
use cm_domain::{AgentId, WorkspaceId};
|
||||
use sqlx::{PgPool, Row};
|
||||
|
||||
use crate::DbError;
|
||||
|
||||
/// The placement node for the workspace that owns `agent_id` (None ⇒ local).
|
||||
pub async fn for_agent(pool: &PgPool, agent_id: AgentId) -> Result<Option<String>, DbError> {
|
||||
let row = sqlx::query(
|
||||
"SELECT p.node_id FROM workspace_placement p
|
||||
JOIN agents a ON a.workspace_id = p.workspace_id
|
||||
WHERE a.id = $1",
|
||||
)
|
||||
.bind(agent_id.as_uuid())
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(row.map(|r| r.get("node_id")))
|
||||
}
|
||||
|
||||
/// A workspace's placement node (None ⇒ local).
|
||||
pub async fn get(pool: &PgPool, workspace_id: WorkspaceId) -> Result<Option<String>, DbError> {
|
||||
let row = sqlx::query("SELECT node_id FROM workspace_placement WHERE workspace_id = $1")
|
||||
.bind(workspace_id.as_uuid())
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(row.map(|r| r.get("node_id")))
|
||||
}
|
||||
|
||||
/// Set the workspace's placement node.
|
||||
pub async fn set(pool: &PgPool, workspace_id: WorkspaceId, node_id: &str) -> Result<(), DbError> {
|
||||
sqlx::query(
|
||||
"INSERT INTO workspace_placement (workspace_id, node_id, updated_at)
|
||||
VALUES ($1, $2, now())
|
||||
ON CONFLICT (workspace_id) DO UPDATE SET node_id = excluded.node_id, updated_at = now()",
|
||||
)
|
||||
.bind(workspace_id.as_uuid())
|
||||
.bind(node_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the workspace's placement (revert to local).
|
||||
pub async fn clear(pool: &PgPool, workspace_id: WorkspaceId) -> Result<(), DbError> {
|
||||
sqlx::query("DELETE FROM workspace_placement WHERE workspace_id = $1")
|
||||
.bind(workspace_id.as_uuid())
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user