Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docker/agent-sandbox/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
# its main executable, so its install tree is kept intact and symlinked
# rather than copying a single binary out the way Claude Code's simpler,
# single-file layout would allow.
#
# `agent docker build --force` passes a fresh AGENT_CLI_CACHEBUST value:
# Docker caches a RUN step by its command text, not by what the installers
# would fetch today, so without this a rebuild would replay the cached
# layer and keep the old Claude Code/Codex versions. Declared here, not at
# the top, so the apt layer above stays cached.
ARG AGENT_CLI_CACHEBUST
RUN curl -fsSL https://claude.ai/install.sh | bash \
&& curl -fsSL https://chatgpt.com/codex/install.sh | sh \
&& chmod 755 /root \
Expand Down
6 changes: 3 additions & 3 deletions docs/sandboxing.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,17 +215,17 @@ Lists the same networks (name, session id, creation time, and whether it's tied
stashbase agent docker build [--force]
```

Builds the default sandbox image ahead of time instead of waiting to be prompted on first `agent run`, or rebuilds it with `--force` (e.g. after the embedded Dockerfile picks up new apt packages or a security patch) without needing to `docker rmi` it by hand first.
Builds the default sandbox image ahead of time instead of waiting to be prompted on first `agent run`, or rebuilds it with `--force` without needing to `docker rmi` it by hand first. `--force` also refreshes what's inside: it pulls the latest base image (`docker build --pull`) and reinstalls Claude Code and Codex at their current versions, since the image pins them and disables their in-app auto-updaters. Re-run it whenever you want newer agent CLIs; `agent run` also reminds you once the default image is more than 14 days old.

Add `--profile <name>` to target that profile's own `sandbox.image`/`sandbox.dockerfile` instead of the default — useful for pre-building or force-refreshing a custom image the same way, without needing to trigger a real `agent run` first. `--profile-source auto|global|directory` controls where `--profile` is loaded from, same as `agent run`/`agent validate`. A profile using a plain `image` reference has nothing to build (`docker run` pulls it automatically), so this reports that and does nothing rather than erroring.
Add `--profile <name>` to target that profile's own `sandbox.image`/`sandbox.dockerfile` instead of the default — useful for pre-building or force-refreshing a custom image the same way, without needing to trigger a real `agent run` first. For a custom Dockerfile, `--force` pulls the base image but otherwise follows Docker's normal layer cache. `--profile-source auto|global|directory` controls where `--profile` is loaded from, same as `agent run`/`agent validate`. A profile using a plain `image` reference has nothing to build (`docker run` pulls it automatically), so this reports that and does nothing rather than erroring.

### Checking readiness

```bash
stashbase agent docker doctor
```

Checks whether the Docker sandbox backend can actually run here — the `docker` CLI on PATH, the daemon reachable (and its version), and whether the default image is already built — without starting a real sandboxed run to find out. Exits non-zero if anything's not ready; pass `--json` for machine-readable output. Useful for onboarding or CI setup scripts that want to fail fast with a clear reason, rather than discovering a missing Docker install only when a real `agent run` fails.
Checks whether the Docker sandbox backend can actually run here — the `docker` CLI on PATH, the daemon reachable (and its version), and whether the default image is already built (with how old it is and the Claude Code/Codex versions inside it) — without starting a real sandboxed run to find out. Exits non-zero if anything's not ready; pass `--json` for machine-readable output. Useful for onboarding or CI setup scripts that want to fail fast with a clear reason, rather than discovering a missing Docker install only when a real `agent run` fails.

### Docker backend limitations

Expand Down
2 changes: 1 addition & 1 deletion src/cmd/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ pub struct AgentDockerStatusCommand {}

#[derive(Debug, Args)]
pub struct AgentDockerBuildCommand {
/// Rebuild even if the image already exists locally
/// Rebuild even if the image already exists, pulling the latest base image (and, for the default image, the latest Claude Code and Codex)
#[arg(long)]
pub force: bool,

Expand Down
59 changes: 44 additions & 15 deletions src/handlers/agent/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ pub async fn handle_docker_build_command(
// suppress it while still showing build progress, the same tradeoff
// `docker build` itself has. The JSON result below is still the last
// thing printed, so it remains the thing to parse.
crate::handlers::run::docker_sandbox::build_sandbox_image(&source)
crate::handlers::run::docker_sandbox::build_sandbox_image(&source, command.force)
.map_err(|error| anyhow::anyhow!("failed to build the Docker sandbox image: {error}"))?;
if raw_output {
println!(
Expand Down Expand Up @@ -479,9 +479,22 @@ pub async fn handle_docker_doctor_command(
} else {
Err("skipped: `docker` CLI not found".to_owned())
};
let image_built = crate::handlers::run::docker_sandbox::sandbox_image_exists(
&crate::handlers::run::docker_sandbox::AgentImageSource::Default,
);
let default_source = crate::handlers::run::docker_sandbox::AgentImageSource::Default;
let image_created_at = if daemon_version.is_ok() {
crate::handlers::run::docker_sandbox::sandbox_image_created_at(&default_source)
} else {
None
};
let image_built = image_created_at.is_some();
let agent_versions = if image_built {
crate::handlers::run::docker_sandbox::sandbox_image_agent_versions(&default_source)
} else {
Default::default()
};
let now = chrono::Utc::now();
let stale_hint = image_created_at.and_then(|created_at| {
crate::handlers::run::docker_sandbox::stale_default_image_hint(created_at, now)
});
let all_ok = binary_available && daemon_version.is_ok();

if raw_output {
Expand All @@ -491,6 +504,10 @@ pub async fn handle_docker_doctor_command(
"daemon_version": daemon_version.as_ref().ok(),
"daemon_error": daemon_version.as_ref().err(),
"default_image_built": image_built,
"default_image_created_at": image_created_at.map(|created_at| created_at.to_rfc3339()),
"default_image_stale": stale_hint.is_some(),
"claude_code_version": agent_versions.claude,
"codex_version": agent_versions.codex,
"ready": all_ok,
});
println!("{}", get_formatted_json_string(&json, true)?);
Expand All @@ -512,18 +529,30 @@ pub async fn handle_docker_doctor_command(
),
Err(error) => println!("{} Docker daemon reachable: {error}", "✗".red_if_tty()),
}
println!(
"{} Default sandbox image built",
if image_built {
"✓".green_if_tty()
} else {
"○".yellow_if_tty()
match image_created_at {
Some(created_at) => {
let unknown = || "unknown".to_owned();
println!(
"{} Default sandbox image built {} (Claude Code {}, Codex {})",
if stale_hint.is_some() {
"○".yellow_if_tty()
} else {
"✓".green_if_tty()
},
crate::handlers::run::docker_sandbox::format_image_age(created_at, now),
agent_versions.claude.clone().unwrap_or_else(unknown),
agent_versions.codex.clone().unwrap_or_else(unknown),
);
if let Some(hint) = &stale_hint {
println!(" ({hint})");
}
}
None => {
println!("{} Default sandbox image built", "○".yellow_if_tty());
println!(
" (not required — `agent run` builds it on first use, or run `agent docker build`)"
);
}
);
if !image_built {
println!(
" (not required — `agent run` builds it on first use, or run `agent docker build`)"
);
}
println!();
if all_ok {
Expand Down
Loading
Loading