Decouples "many users" + "many server replicas" from "many machines" so the platform is tenant-isolated and horizontally safe on the current single node. - Per-signup workspaces (cm-auth): a new hosted-identity sign-in provisions and owns its own workspace instead of joining the first. Config-gated by auth.per_signup_workspace (default off); concurrent first-logins serialized by a per-subject advisory lock so no duplicate workspaces. - Terminal tickets in Postgres (migration 0016, hashed, single-use): any replica can redeem a ticket minted by another. Drops the in-process ticket map. - Container registry in Postgres (migration 0017, agent_containers): Terminal and Sandbox managers resolve an agent's container through a shared registry, so a 2nd replica reuses it instead of spawning a duplicate. node_id recorded as 'local' (Phase 2 hook). Boot reconcile removes only true orphans, so terminals now survive a redeploy (tmux sessions resume). - Per-workspace quotas (cm-api/quota.rs): plan-tier caps on agents + live containers, enforced at agent create + terminal spin-up (reconnects allowed), returned as HTTP 402. New GET /api/quota surfaces usage vs limits. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
82 lines
2.5 KiB
Rust
82 lines
2.5 KiB
Rust
//! Warm sandbox pool: pre-provisioned containers absorb the first-exec
|
|
//! latency. Real Docker — the pool fills in the background, an exec
|
|
//! takes a sandbox from it, and the warmer restores the target.
|
|
|
|
use std::process::Command;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use cm_domain::AgentId;
|
|
use cm_runtime::SandboxManager;
|
|
use cm_sandbox::DockerDriver;
|
|
|
|
const IMAGE: &str = "clawmates/agent-base:dev";
|
|
|
|
fn ensure_image() {
|
|
let exists = Command::new("docker")
|
|
.args(["image", "inspect", IMAGE])
|
|
.output()
|
|
.expect("docker available")
|
|
.status
|
|
.success();
|
|
if !exists {
|
|
let root = env!("CARGO_MANIFEST_DIR");
|
|
let status = Command::new("docker")
|
|
.args([
|
|
"build",
|
|
"-t",
|
|
IMAGE,
|
|
"-f",
|
|
&format!("{root}/../../images/agent-base/Dockerfile"),
|
|
&format!("{root}/../../images/agent-base"),
|
|
])
|
|
.status()
|
|
.expect("docker build runs");
|
|
assert!(status.success());
|
|
}
|
|
}
|
|
|
|
async fn pool_reaches(manager: &SandboxManager, target: usize) {
|
|
for _ in 0..120 {
|
|
if manager.pool_size().await == target {
|
|
return;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(250)).await;
|
|
}
|
|
panic!(
|
|
"pool never reached {target} (now {})",
|
|
manager.pool_size().await
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn the_pool_prefills_assigns_and_refills() {
|
|
ensure_image();
|
|
let driver: Arc<dyn cm_sandbox::SandboxDriver> =
|
|
Arc::new(DockerDriver::connect().expect("docker reachable"));
|
|
let pool = cm_testkit::test_pool().await;
|
|
let manager = Arc::new(SandboxManager::new(driver, pool, "local", IMAGE)).warm(2);
|
|
|
|
// The warmer fills the pool without any exec happening.
|
|
pool_reaches(&manager, 2).await;
|
|
|
|
// An exec is served from the pool — and works.
|
|
let agent = AgentId::new();
|
|
let result = manager.exec(agent, "id -u").await.unwrap();
|
|
assert_eq!(result.stdout.trim(), "10001");
|
|
|
|
// The warmer restores the target while the agent keeps its sandbox.
|
|
pool_reaches(&manager, 2).await;
|
|
let again = manager.exec(agent, "echo still-mine").await.unwrap();
|
|
assert_eq!(again.stdout.trim(), "still-mine");
|
|
assert_eq!(
|
|
manager.pool_size().await,
|
|
2,
|
|
"reuse must not drain the pool"
|
|
);
|
|
|
|
// Shutdown destroys assigned AND pooled sandboxes.
|
|
manager.shutdown().await;
|
|
assert_eq!(manager.pool_size().await, 0);
|
|
}
|