Survey + fixes so the pipeline passes at the Docker level (no k8s).
- Remove k8s: drop the `sandbox-k8s` job (kind/Calico/--features k8s-tests) and the
"Helm chart lints" gate step. release.yml was already k8s-clean.
- Rust job:
- `cargo fmt --all` — fix pre-existing formatting drift (fmt --check was failing).
- clippy -D warnings: fix 3 lib warnings (cm-brain sort_by_key→Reverse, cm-api
fleet.rs doc list indentation, node_rules map_or→is_none_or).
- Regenerate the .sqlx offline cache (was missing the cm-runtime run_loop test
query → offline compile failed). DB-backed tests use testcontainers at runtime.
- Set SQLX_OFFLINE=true on the rust + e2e jobs so query! macros compile against
the committed cache deterministically (no DB needed at compile time).
- Frontend job:
- Fix the 1 ESLint error (useAgentTelemetry: no setState-synchronously-in-effect;
tag the slice with agentId + derive null on mismatch).
- Fix 2 stale panel-params tests (`terminal` is a valid app id now; assert the
current APP_IDS + use a genuinely-unknown id for the reject case).
Verified locally: fmt clean, clippy --all-targets -D warnings clean (offline),
frontend lint 0 errors, tsc clean, 86/86 frontend tests pass, build OK.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
113 lines
4.3 KiB
Rust
113 lines
4.3 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
/// Hardening parameters for one agent sandbox. The non-negotiable controls
|
|
/// (uid 10001, cap-drop ALL, no-new-privileges, seccomp profile, read-only
|
|
/// rootfs, no network) are enforced by the driver and are not configurable
|
|
/// here by design — only resource limits vary per deployment.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SandboxSpec {
|
|
/// Unique container name, e.g. `clawmates-sbx-{agent_id}`.
|
|
pub name: String,
|
|
/// Image reference; preloaded in air-gapped installs (never pulled).
|
|
pub image: String,
|
|
pub memory_bytes: i64,
|
|
pub nano_cpus: i64,
|
|
pub pids_limit: i64,
|
|
/// Egress-enabled containers exist ONLY for the browser tool: they
|
|
/// hold no credentials and have no broker route; their output is
|
|
/// tainted `web`. Everything else runs with no network at all.
|
|
pub egress: bool,
|
|
/// What this sandbox is for — drives the reaper label and whether the
|
|
/// rootfs/home is writable.
|
|
pub kind: SandboxKind,
|
|
/// Named-volume subpath mounts (Terminal drives). Empty for tool sandboxes.
|
|
pub mounts: Vec<DriveMount>,
|
|
}
|
|
|
|
/// A read-write mount of a per-agent subpath of a named Docker volume into the
|
|
/// container — used to expose the Files drives inside the Terminal.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DriveMount {
|
|
/// The engine's named volume (e.g. `clawmates_filedata`).
|
|
pub volume: String,
|
|
/// Subpath within the volume (per-agent isolation), e.g. `{ws}/documents/{agent}`.
|
|
pub subpath: String,
|
|
/// Mount target inside the container, e.g. `/home/agent/drives/documents`.
|
|
pub target: String,
|
|
pub read_only: bool,
|
|
}
|
|
|
|
/// The flavour of a sandbox container. Agent + Browser are hardened tool
|
|
/// sandboxes; Terminal is the interactive themed dev shell.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum SandboxKind {
|
|
/// Hardened, no-egress agent tool sandbox (read-only rootfs, tmpfs home).
|
|
Agent,
|
|
/// Egress-enabled browser sandbox (no credentials; output tainted `web`).
|
|
Browser,
|
|
/// Interactive themed terminal: a writable home so the baked zsh /
|
|
/// oh-my-zsh / powerlevel10k config + history work. Still non-root,
|
|
/// cap-drop ALL, seccomp, no-new-privileges and resource-limited.
|
|
Terminal,
|
|
}
|
|
|
|
impl SandboxKind {
|
|
/// The [`crate::SANDBOX_LABEL`] value, so each manager reaps only its own.
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
SandboxKind::Agent => "agent",
|
|
SandboxKind::Browser => "browser",
|
|
SandboxKind::Terminal => "terminal",
|
|
}
|
|
}
|
|
/// Terminal keeps a writable rootfs + home (baked dotfiles + history);
|
|
/// the hardened tool sandboxes stay read-only with a tmpfs home.
|
|
pub fn writable_home(self) -> bool {
|
|
matches!(self, SandboxKind::Terminal)
|
|
}
|
|
|
|
/// The uid:gid the container runs as. Terminal matches the server's nonroot
|
|
/// uid (65532) so it shares read-write ownership of the file-drive volume;
|
|
/// the hardened tool sandboxes run as 10001.
|
|
pub fn run_user(self) -> &'static str {
|
|
match self {
|
|
SandboxKind::Terminal => "65532:65532",
|
|
_ => "10001:10001",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An attached interactive PTY exec (a `docker exec -it` session): combined
|
|
/// TTY output as byte chunks + a writer for keystrokes, plus the exec id so
|
|
/// the window size can be resized.
|
|
pub struct PtySession {
|
|
pub exec_id: String,
|
|
pub output:
|
|
std::pin::Pin<Box<dyn futures::Stream<Item = Result<Vec<u8>, crate::SandboxError>> + Send>>,
|
|
pub input: std::pin::Pin<Box<dyn tokio::io::AsyncWrite + Send>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SandboxHandle {
|
|
/// Container id assigned by the engine.
|
|
pub id: String,
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ExecResult {
|
|
pub exit_code: i64,
|
|
pub stdout: String,
|
|
pub stderr: String,
|
|
}
|
|
|
|
/// A sandbox the driver currently knows about (label-filtered), used by the
|
|
/// reaper to find orphans — containers that outlived the process that made them.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ManagedSandbox {
|
|
/// Engine container/pod id.
|
|
pub id: String,
|
|
/// Creation time, unix seconds (for TTL-based reaping).
|
|
pub created_unix: i64,
|
|
}
|