container_shell: isolated network by default, plus optional network transport#93
container_shell: isolated network by default, plus optional network transport#93anticomputer wants to merge 9 commits into
Conversation
Start container_shell containers with `--network none` by default so the managed Docker container has no network access unless a caller explicitly opts in. A new CONTAINER_NETWORK environment variable selects the docker network mode (e.g. "bridge", "host", or a user-defined network); an unset, empty, or whitespace-only value falls back to "none" so isolation cannot be silently disabled. Expose CONTAINER_NETWORK on the container_shell toolboxes. The base, SAST, and malware-analysis toolboxes inherit the isolated default; the network analysis toolbox defaults to "bridge" since it needs egress for recon. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0fc0c170-07c4-4eff-b075-3e1f420c4ba3
There was a problem hiding this comment.
Pull request overview
This PR hardens the container_shell MCP server by making Docker containers start with networking disabled by default (--network none), requiring callers/toolboxes to explicitly opt into outbound connectivity via a new CONTAINER_NETWORK environment variable.
Changes:
- Add
CONTAINER_NETWORK(defaulting tonone, with blank/whitespace values failing closed tonone) and pass it todocker run --network. - Add unit tests covering default, opt-in (
bridge), and unset/blank fallback behavior. - Plumb
CONTAINER_NETWORKthrough the container_shell toolbox YAMLs, withnetwork_analysisdefaulting tobridge.
Show a summary per file
| File | Description |
|---|---|
src/seclab_taskflows/mcp_servers/container_shell.py |
Introduces CONTAINER_NETWORK and applies it to docker run for default network isolation. |
tests/test_container_shell.py |
Adds tests for default/opt-in/fallback network behavior. |
src/seclab_taskflows/toolboxes/container_shell_base.yaml |
Exposes CONTAINER_NETWORK to the base toolbox environment. |
src/seclab_taskflows/toolboxes/container_shell_sast.yaml |
Exposes CONTAINER_NETWORK to the SAST toolbox environment. |
src/seclab_taskflows/toolboxes/container_shell_malware_analysis.yaml |
Exposes CONTAINER_NETWORK to the malware analysis toolbox environment. |
src/seclab_taskflows/toolboxes/container_shell_network_analysis.yaml |
Exposes CONTAINER_NETWORK with a default of bridge for network tooling. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Low
Add a module docstring listing the CONTAINER_* configuration variables and explaining how a toolbox exposes CONTAINER_NETWORK so callers can select a Docker network mode at run time, since the agent only forwards a toolbox's declared env entries to the server. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0fc0c170-07c4-4eff-b075-3e1f420c4ba3
Include the configured network mode in the persistent container name hash so a run configured for one network never reuses a persistent container created with a different, more permissive network. Without this, a container started with CONTAINER_NETWORK=bridge could be reused by a later run that requests the default "none", silently re-enabling egress. Also restore the real environment before reloading the module in the network fallback tests so module-level configuration does not leak between tests, and add coverage that the persistent name varies with the network mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0fc0c170-07c4-4eff-b075-3e1f420c4ba3
|
Thanks for the review — both issues were valid and are addressed in 065b75b. Persistent container reuse ignored network mode (flagged at
Test reload leakage (flagged on the two fallback tests): the tests now capture the real Added a |
Reloading the container_shell module re-runs its module-level atexit.register(_stop_container), stacking a new handler each time and causing duplicate stop attempts at exit. Unregister the current handler before every reload in the tests so exactly one remains. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0fc0c170-07c4-4eff-b075-3e1f420c4ba3
|
Also addressed the second-pass note about |
The container_shell server previously ran only over stdio as a locally launched subprocess. Add CONTAINER_SHELL_TRANSPORT (default stdio) plus CONTAINER_SHELL_HOST/PORT so the same server can run over http, streamable-http, or sse as a network-accessible MCP server that a remote agent can connect to. Blank values fall back to stdio so the local default cannot be silently changed, and unknown transports raise a clear error. Which Docker daemon the server drives stays controlled by DOCKER_HOST, so no code change is needed to point it at a specific daemon. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0fc0c170-07c4-4eff-b075-3e1f420c4ba3
Give the tool a name that is unambiguous when several MCP servers are loaded together. The FastMCP tool name follows the function name, so the rename covers the tool definition, the confirm lists in every container shell toolbox, the tests, and the docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0fc0c170-07c4-4eff-b075-3e1f420c4ba3
Connect to a container_shell MCP server that is already running elsewhere over Streamable HTTP instead of launching it as a local stdio subprocess. The endpoint comes from CONTAINER_SHELL_URL. This lets a network-isolated agent use containerized shell execution without being handed a Docker socket: the server owns the daemon and is reached over one allowlisted host service port. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0fc0c170-07c4-4eff-b075-3e1f420c4ba3
Parsing CONTAINER_SHELL_PORT with int() at import time made a non-integer value raise during import and break the default stdio transport, even though host and port are ignored for stdio. Keep the value as a raw string at import and convert it to an int inside _run_server() only when a network transport is selected, raising a clear error there if it is not a valid integer. Also parameterize the blank-network fallback test to cover an explicit empty string alongside whitespace-only input, sort the stdlib imports in the test module, and add tests that an invalid port leaves stdio working while failing network transport startup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0fc0c170-07c4-4eff-b075-3e1f420c4ba3
A blank or whitespace-only CONTAINER_SHELL_HOST was passed verbatim to the network transport, which could bind unexpectedly. Strip it and fall back to 127.0.0.1 when empty, matching the fail-closed handling of CONTAINER_NETWORK and CONTAINER_SHELL_TRANSPORT. Also pin the transport to stdio in the invalid-port stdio test so it does not depend on the ambient CONTAINER_SHELL_TRANSPORT value, and add a test for the host blank fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0fc0c170-07c4-4eff-b075-3e1f420c4ba3
| You have access to an isolated Docker container. Use `container_shell_exec` to run commands. | ||
| The working directory is /workspace (mapped from the host workspace if configured). | ||
|
|
| You have access to an isolated Docker container running on a remote shell | ||
| server. Use `container_shell_exec` to run commands inside it. The working | ||
| directory is /workspace (mapped from the host workspace if configured). | ||
|
|
| Runs arbitrary CLI commands inside an isolated Docker container. One container | ||
| per MCP server process — started on the first `shell_exec` call, stopped on | ||
| per MCP server process — started on the first `container_shell_exec` call, stopped on | ||
| exit. An optional host directory is mounted at `/workspace` inside the container. | ||
|
|
The
container_shellMCP server currently starts its managed Docker container on the default bridge network, so any command run throughshell_exechas unrestricted outbound network access. For an analysis sandbox that runs untrusted code and processes untrusted inputs, network access should be opt-in rather than the default. This PR makes that the default and, relatedly, lets the server itself run as a network-reachable MCP endpoint so it can be deployed as an isolated sidecar.Isolated container network by default
This change starts containers with
--network noneby default, so the container is egress-locked unless a caller explicitly enables networking.CONTAINER_NETWORKenvironment variable (defaultnone) that selects the Docker network mode passed todocker run --network. Callers opt into networking by setting it tobridge,host, or a user-defined network name.none, so the isolation default cannot be silently disabled by a blank variable.CONTAINER_NETWORKon the container_shell toolboxes. Thebase,sast, andmalware_analysistoolboxes inherit the isolated default; thenetwork_analysistoolbox defaults tobridgebecause its recon tools (e.g.nmap) need egress.Verified end-to-end against a real Docker daemon:
nonelononenonelobridgebridgeeth0,loSelecting a network mode from a toolbox
The agent forwards only a toolbox's declared
enventries to this server, so a network mode is run-time selectable only if the toolbox exposes the knob. A toolbox opts in with a passthrough line in itsenvblock, e.g.CONTAINER_NETWORK: "{{ env('CONTAINER_NETWORK', 'none') }}"(use'bridge'as the default for a toolbox that should be networked by default). A toolbox that omits the line inherits the isolatednonedefault. This is documented in thecontainer_shellmodule docstring.Optional network transport
container_shellruns only over stdio today, as a subprocess launched locally by the agent. That is the right default when the server sits next to a local Docker daemon, but it means the server cannot be run as a standalone, network-reachable MCP endpoint. This adds an opt-in transport selector so the same server can also be served over HTTP/SSE and connected to as a remote MCP server, without changing existing behavior.CONTAINER_SHELL_TRANSPORT— MCP transport. Defaults tostdio(unchanged behavior). Set tohttp,streamable-http, orsseto serve over the network.CONTAINER_SHELL_HOST/CONTAINER_SHELL_PORT— bind address for the network transports (default127.0.0.1/8080); ignored for stdio.CONTAINER_SHELL_TRANSPORTfalls back tostdio, so the local default cannot be silently changed; an unknown transport raises a clearValueErrorlisting the supported values.Which Docker daemon the server drives is orthogonal to the transport: the docker CLI already honours
DOCKER_HOSTfrom the environment, so pointing the server at a specific daemon needs no code change here.Manual smoke test: started the server with
CONTAINER_SHELL_TRANSPORT=http, confirmed it binds the configured host/port and answers an MCPinitializehandshake over HTTP (serverInfo.name: "ContainerShell"), then verified the listener is released on shutdown.Rename the tool to container_shell_exec
The shell tool is renamed from
shell_exectocontainer_shell_execso it is unambiguous when several MCP servers are loaded together. FastMCP derives the tool name from the function name, so the rename covers the tool definition, theconfirmlists in every container shell toolbox, the tests, and the docs. Verified over MCP:list_toolsagainst the running server returns['container_shell_exec'].Remote container shell toolbox
Adds
container_shell_remote, a toolbox that connects to acontainer_shellserver already running elsewhere over Streamable HTTP instead of launching it as a local stdio subprocess. The endpoint is read fromCONTAINER_SHELL_URL. This is what lets a network-isolated agent use containerized shell execution without being handed a Docker socket: the server owns the daemon and is reached over a single allowlisted host service port.This toolbox depends on GitHubSecurityLab/seclab-taskflow-agent#279, which env-templates the toolbox
urlsourl: "{{ env('CONTAINER_SHELL_URL') }}"resolves at connect time. Verified by loading the real toolbox through the agent model and confirming the url resolves to the concrete endpoint.Verification
Added unit tests covering the network default/opt-in/blank-fallback, persistent-container keying by network mode, the stdio transport default, blank-transport fallback, the host blank-fallback, lazy port parsing (an invalid port leaves stdio working but fails a network transport), the exact
mcp.run(...)wiring for each network transport, and the unknown-transport error. Fullcontainer_shellsuite passes (47 tests) andhatch fmt --linter --checkis clean.The tool rename keeps the full
container_shellsuite green (47 tests). The remote toolbox's url resolution is covered by the agent PR above.