Add option to run extensions in a container#6590
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds support for running goose extensions inside Docker containers by passing a container ID. When configured, stdio and builtin extensions will be launched via docker exec inside the specified container instead of on the host.
Changes:
- New
Containerstruct to encapsulate Docker container IDs - Updated extension loading logic to wrap commands with
docker execwhen a container is set - Added
set_containerAPI endpoint and--containerCLI flag - Modified
McpClientto track which container (if any) an extension is running in
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/goose/src/agents/container.rs | New struct to hold Docker container IDs |
| crates/goose/src/agents/extension_manager.rs | Core logic to wrap stdio and builtin extension commands with docker exec |
| crates/goose/src/agents/agent.rs | Container state management and integration with extension loading |
| crates/goose/src/agents/mcp_client.rs | Added container tracking to MCP clients |
| crates/goose-server/src/routes/agent.rs | New API endpoint to set container for a session |
| crates/goose-cli/src/cli.rs | CLI flag to specify container ID |
| crates/goose-cli/src/session/builder.rs | Pass container from CLI to agent |
| crates/goose-cli/src/commands/bench.rs | Updated test config to include container field |
| crates/goose/tests/mcp_integration_test.rs | Updated test to pass None for container parameter |
| crates/goose/src/agents/extension_manager_extension.rs | Updated add_extension call to include container parameter |
| crates/goose/src/agents/mod.rs | Exported Container struct |
| ui/desktop/src/hooks/useChatStream.ts | Formatting cleanup (whitespace) |
| ui/desktop/src/components/BaseChat.tsx | Formatting cleanup (whitespace) |
Comments suppressed due to low confidence (1)
crates/goose/src/agents/extension_manager.rs:668
- InlinePython extensions will not run inside the container. Unlike Stdio and Builtin extensions, the command here is not wrapped with
docker execbefore callingchild_process_client. The container parameter is passed tochild_process_clientbut only used for storing in McpClient metadata, not for command execution. Either wrap the uvx command with docker exec similar to Stdio/Builtin extensions, or explicitly ignore the container parameter for InlinePython if this extension type should not support containers.
ExtensionConfig::InlinePython {
name,
code,
timeout,
dependencies,
..
} => {
let dir = tempdir()?;
let file_path = dir.path().join(format!("{}.py", name));
temp_dir = Some(dir);
std::fs::write(&file_path, code)?;
let command = Command::new("uvx").configure(|command| {
command.arg("--with").arg("mcp");
dependencies.iter().flatten().for_each(|dep| {
command.arg("--with").arg(dep);
});
command.arg("python").arg(file_path.to_str().unwrap());
});
let client = child_process_client(
command,
timeout,
self.provider.clone(),
Some(&effective_working_dir),
container.map(|c| c.id().to_string()),
)
.await?;
Box::new(client)
}
crates/goose/src/agents/agent.rs
Outdated
|
|
||
| /// Sets the Docker container for running stdio extensions. | ||
| /// When set, all stdio extensions will be started via `docker exec` in the specified container. | ||
| /// This also updates the ExtensionManager's container setting. |
There was a problem hiding this comment.
The documentation states "This also updates the ExtensionManager's container setting" but ExtensionManager doesn't have a container field - the container is only stored in the Agent and passed when adding extensions. This could be confusing. Consider either updating the documentation to clarify that the container is passed to ExtensionManager during extension loading, or removing this part of the comment.
| /// This also updates the ExtensionManager's container setting. |
crates/goose/src/agents/agent.rs
Outdated
| /// When set, all stdio extensions will be started via `docker exec` in the specified container. | ||
| /// This also updates the ExtensionManager's container setting. |
There was a problem hiding this comment.
Setting the container only affects extensions that are added after this call. Extensions that were already running will continue to run in their original location (host or previous container). Consider documenting this behavior in the function comment to clarify that existing extensions are not affected.
| /// When set, all stdio extensions will be started via `docker exec` in the specified container. | |
| /// This also updates the ExtensionManager's container setting. | |
| /// When set, all stdio extensions started after this call will be run via `docker exec` in the specified container. | |
| /// This does not affect extensions that are already running, which continue to run in their original location (host or previous container). |
Resolved conflicts by integrating both features: - Container support from extensions-in-containers branch - Working directory parameter from main branch - Updated method signature to add_extension_with_working_dir with both parameters - Fixed TypeScript hook to use stateRef instead of messagesRef 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
crates/goose/src/agents/extension_manager.rs:680
- InlinePython extensions won't work correctly when running in containers. The code is written to a temporary file on the host filesystem (line 661), but then executed inside the container where this file won't be accessible. Consider either:
- Detecting this case and returning an error with a clear message
- Using
docker cpto copy the file into the container - Using stdin to pipe the code into the container
For this minimal PR, option 1 (early error) would be safest.
ExtensionConfig::InlinePython {
name,
code,
timeout,
dependencies,
..
} => {
let dir = tempdir()?;
let file_path = dir.path().join(format!("{}.py", name));
temp_dir = Some(dir);
std::fs::write(&file_path, code)?;
let command = Command::new("uvx").configure(|command| {
command.arg("--with").arg("mcp");
dependencies.iter().flatten().for_each(|dep| {
command.arg("--with").arg(dep);
});
command.arg("python").arg(file_path.to_str().unwrap());
});
let client = child_process_client(
command,
timeout,
self.provider.clone(),
Some(&effective_working_dir),
container.map(|c| c.id().to_string()),
)
.await?;
Box::new(client)
| Command::new("docker").configure(|command| { | ||
| command.arg("exec").arg("-i"); | ||
| for (key, value) in &all_envs { | ||
| command.arg("-e").arg(format!("{}={}", key, value)); | ||
| } | ||
| command.arg(container_id); | ||
| command.arg(cmd); | ||
| command.args(args); | ||
| }) |
There was a problem hiding this comment.
The docker exec command doesn't set a working directory inside the container. When extensions run in containers, they should use the same working directory path. Add -w flag to specify the working directory:
command.arg("exec").arg("-i").arg("-w").arg(effective_working_dir.to_str().unwrap())
This ensures that GOOSE_WORKING_DIR and the actual process working directory are consistent, which is important for extensions that rely on relative paths.
| let command = Command::new("docker").configure(|command| { | ||
| command | ||
| .arg("exec") | ||
| .arg("-i") | ||
| .arg(container_id) | ||
| .arg("goose") | ||
| .arg("mcp") | ||
| .arg(name); | ||
| }); |
There was a problem hiding this comment.
The docker exec command for builtin extensions doesn't set a working directory inside the container. Add -w flag before the container ID to specify the working directory:
command.arg("exec").arg("-i").arg("-w").arg(effective_working_dir.to_str().unwrap()).arg(container_id)
This matches the pattern needed for stdio extensions and ensures extensions can access files relative to the working directory.
A minimal version of the devcontainer/goose-in-containers flow. Provide a container id, and goose will start its (non-remote) extensions inside it. No container "orchestration" in this change.