Skip to content

Add option to run extensions in a container#6590

Merged
jamadeo merged 5 commits intomainfrom
jackamadeo/extensions-in-containers
Jan 27, 2026
Merged

Add option to run extensions in a container#6590
jamadeo merged 5 commits intomainfrom
jackamadeo/extensions-in-containers

Conversation

@jamadeo
Copy link
Copy Markdown
Collaborator

@jamadeo jamadeo commented Jan 20, 2026

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.

Copilot AI review requested due to automatic review settings January 20, 2026 15:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Container struct to encapsulate Docker container IDs
  • Updated extension loading logic to wrap commands with docker exec when a container is set
  • Added set_container API endpoint and --container CLI flag
  • Modified McpClient to 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 exec before calling child_process_client. The container parameter is passed to child_process_client but 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)
            }


/// 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.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
/// This also updates the ExtensionManager's container setting.

Copilot uses AI. Check for mistakes.
Comment on lines +417 to +418
/// When set, all stdio extensions will be started via `docker exec` in the specified container.
/// This also updates the ExtensionManager's container setting.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
/// 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).

Copilot uses AI. Check for mistakes.
jamadeo and others added 2 commits January 20, 2026 10:49
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]>
Copilot AI review requested due to automatic review settings January 27, 2026 14:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
  1. Detecting this case and returning an error with a clear message
  2. Using docker cp to copy the file into the container
  3. 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)

Comment on lines +552 to +560
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);
})
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +595 to +603
let command = Command::new("docker").configure(|command| {
command
.arg("exec")
.arg("-i")
.arg(container_id)
.arg("goose")
.arg("mcp")
.arg(name);
});
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@jamadeo jamadeo changed the title Run containers in extensions Add option to run extensions in a container Jan 27, 2026
@jamadeo jamadeo requested a review from jh-block January 27, 2026 14:54
@jamadeo jamadeo merged commit 3fcf022 into main Jan 27, 2026
16 of 17 checks passed
@jamadeo jamadeo deleted the jackamadeo/extensions-in-containers branch January 27, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants