@container-tool-exec Feature: Container-aware tool execution and I/O forwarding As a CleverAgents developer I want tools to execute inside containers when the execution environment is CONTAINER So that file-based tools operate on the container filesystem transparently Background: Given I have the container tool execution module imported # -- PathMapper --------------------------------------------------------- Scenario: PathMapper maps host path to container path Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace" When I map host path "/tmp/sandbox/src/main.py" to container Then the container path should be "/workspace/src/main.py" Scenario: PathMapper maps container path to host path Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace" When I map container path "/workspace/src/main.py" to host Then the host path should be "/tmp/sandbox/src/main.py" Scenario: PathMapper returns unmapped path when outside root Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace" When I map host path "/usr/lib/libc.so" to container Then the container path should be "/usr/lib/libc.so" Scenario: PathMapper maps root path exactly Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace" When I map host path "/tmp/sandbox" to container Then the container path should be "/workspace" Scenario: PathMapper is_host_path returns true for host paths Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace" Then "/tmp/sandbox/file.txt" should be a host path And "/usr/lib/libc.so" should not be a host path Scenario: PathMapper is_container_path returns true for container paths Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace" Then "/workspace/file.txt" should be a container path And "/tmp/other/file.txt" should not be a container path # -- ContainerConfig ---------------------------------------------------- Scenario: ContainerConfig has sensible defaults When I create a default ContainerConfig Then the workspace_folder should be "/workspace" And the container timeout_seconds should be 120 # -- ContainerMetadata -------------------------------------------------- Scenario: ContainerMetadata is frozen and holds execution details When I create a ContainerMetadata with container_id "abc123" and image "node:20" Then the container_metadata container_id should be "abc123" And the container_metadata image should be "node:20" And the container_metadata timed_out should be false # -- ContainerToolExecutor basic ---------------------------------------- Scenario: ContainerToolExecutor can be instantiated with config Given I have a ContainerConfig with workspace "/workspace" and container_id "test-ctr" When I create a ContainerToolExecutor with that config Then the executor config container_id should be "test-ctr" And the executor should have a path mapper Scenario: ContainerToolExecutor maps input paths before execution Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I map tool inputs with path "/tmp/sandbox/src/file.py" Then the mapped input path should be "/workspace/src/file.py" Scenario: ContainerToolExecutor maps output paths after execution Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I map tool output with path "/workspace/build/output.txt" Then the mapped output path should be "/tmp/sandbox/build/output.txt" Scenario: ContainerToolExecutor handles nested input path mapping Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I map tool inputs with nested paths under "/tmp/sandbox" Then all nested paths should be mapped to container paths # -- Timeout handling --------------------------------------------------- Scenario: ContainerToolExecutor reports timeout as structured error Given I have a ContainerToolExecutor with a mock that times out When I execute tool "file_read" with inputs in the container Then the container tool result should indicate failure And the container tool result error should mention "timed out" And the tool result metadata should contain container info with timed_out true # -- Error reporting ---------------------------------------------------- Scenario: ContainerToolExecutor reports non-zero exit as structured error Given I have a ContainerToolExecutor with a mock that returns exit code 1 When I execute tool "file_read" with inputs in the container Then the container tool result should indicate failure And the container tool result error should mention "exit code 1" Scenario: ContainerToolExecutor reports OSError as structured error Given I have a ContainerToolExecutor with subprocess raising OSError When I execute tool "compile" with inputs in the container Then the container tool result should indicate failure # -- Successful execution ----------------------------------------------- Scenario: ContainerToolExecutor returns parsed JSON output on success Given I have a ContainerToolExecutor with a mock that returns JSON output When I execute tool "lint" with inputs in the container Then the container tool result should indicate success And the tool result output should contain the parsed JSON And the tool result metadata should contain container info Scenario: ContainerToolExecutor returns raw output when not JSON Given I have a ContainerToolExecutor with a mock that returns plain text When I execute tool "echo" with inputs in the container Then the container tool result should indicate success And the tool result output should contain raw_output # -- Command building --------------------------------------------------- Scenario: _build_exec_command places --container-id flag adjacent to its value Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I build an exec command for tool "test_tool" with timeout 30 Then the command should have "--container-id" adjacent to its value # -- ToolRunner container routing --------------------------------------- Scenario: ToolRunner delegates to ContainerToolExecutor when env is CONTAINER Given I have a ToolRunner with a ContainerToolExecutor mock When I execute a tool with container execution environment Then the ContainerToolExecutor should have been called And the resolver should have been called with the correct arguments Scenario: ToolRunner returns error when container executor is not configured Given I have a ToolRunner without a ContainerToolExecutor When I execute a tool with container execution environment Then the container tool result error should mention "ContainerToolExecutor must be configured" # -- ToolInvocation audit trail ----------------------------------------- Scenario: ToolInvocation has container_metadata field When I create a ToolInvocation with container_metadata Then the ToolInvocation container_metadata should contain "container_id" And the ToolInvocation container_metadata should contain "image" Scenario: ToolInvocation container_metadata defaults to None When I create a ToolInvocation without container_metadata Then the ToolInvocation container_metadata should be None # -- ContainerExecutionError -------------------------------------------- Scenario: ContainerExecutionError carries exit_code and stderr When I raise a ContainerExecutionError with exit_code 2 and stderr "oops" Then the error exit_code should be 2 And the error stderr should be "oops" Scenario: ContainerTimeoutError carries timeout_seconds When I raise a ContainerTimeoutError with timeout 30 Then the timeout error message should mention "30" And the timeout error timed_out should be true # -- Signal exit code preservation ---------------------------------------- Scenario: ContainerToolExecutor preserves negative exit codes from signal kills Given I have a ContainerToolExecutor with a mock that returns exit code -9 When I execute tool "compile" with inputs in the container Then the container tool result should indicate failure And the tool result metadata should have exit_code -9 # -- Path mapping with spaces -------------------------------------------- Scenario: _looks_like_path accepts paths with spaces Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I map tool inputs with path "/tmp/sandbox/My Documents/file.py" Then the mapped input path should be "/workspace/My Documents/file.py" # -- Output truncation --------------------------------------------------- Scenario: ContainerToolExecutor truncates output exceeding max bytes Given I have a ContainerToolExecutor with a mock that returns oversized output When I execute tool "big_output" with inputs in the container Then the container tool result stdout should be truncated # -- Path traversal protection ------------------------------------------- Scenario: sync_results_to_host rejects path traversal attempts Given I have a ContainerToolExecutor with a mock that succeeds on sync using a temp dir When I attempt to sync a path that traverses outside the sandbox Then a ContainerExecutionError should be raised for path traversal # -- Sync results ------------------------------------------------------- Scenario: sync_results_to_host maps container path to host Given I have a ContainerToolExecutor with a mock that succeeds on sync using a temp dir When I sync container path "/workspace/output.txt" to host Then the synced host path should be under the host sandbox root # -- execute_tool input validation (P2-19) -------------------------------- Scenario: execute_tool rejects empty tool_name Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I execute tool with empty tool_name Then a ValueError should be raised with message "non-empty" Scenario: execute_tool rejects non-dict inputs Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I execute tool with non-dict inputs Then a container TypeError should be raised with message "must be a dict" Scenario: execute_tool rejects negative timeout_seconds Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I execute tool with negative timeout Then a ValueError should be raised with message "must be > 0" # -- extract_container_metadata helper (P1-9) ----------------------------- Scenario: extract_container_metadata returns container dict from ToolResult When I create a ToolResult with container metadata in metadata dict Then extract_container_metadata should return the container dict # ========================================================================= # Safe initialization — PathMapper validation guards # ========================================================================= Scenario: PathMapper rejects null bytes in host_root When I try to create a PathMapper with null bytes in host_root Then a PathMapper ValueError should be raised mentioning "null" Scenario: PathMapper rejects null bytes in container_root When I try to create a PathMapper with null bytes in container_root Then a PathMapper ValueError should be raised mentioning "null" Scenario: PathMapper rejects empty host_root When I try to create a PathMapper with empty host_root Then a PathMapper ValueError should be raised mentioning "empty" Scenario: PathMapper rejects filesystem-root host_root When I try to create a PathMapper with host_root "/" Then a PathMapper ValueError should be raised mentioning "/" Scenario: PathMapper rejects filesystem-root container_root When I try to create a PathMapper with container_root "/" Then a PathMapper ValueError should be raised mentioning "/" Scenario: PathMapper rejects overlapping roots When I try to create a PathMapper with overlapping roots Then a PathMapper ValueError should be raised mentioning "overlap" Scenario: PathMapper is_host_path returns false for null-byte paths Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace" Then a path with null bytes should not be a host path Scenario: PathMapper is_container_path returns false for null-byte paths Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace" Then a path with null bytes should not be a container path # ========================================================================= # Safe initialization — ContainerConfig workspace_folder guards # ========================================================================= Scenario: ContainerConfig rejects relative workspace_folder When I try to create a ContainerConfig with workspace_folder "relative/path" Then a ContainerConfig ValueError should be raised mentioning "absolute" Scenario: ContainerConfig rejects workspace_folder that normalises to root When I try to create a ContainerConfig with workspace_folder "/." Then a ContainerConfig ValueError should be raised mentioning "root" Scenario: ContainerConfig rejects workspace_folder with dotdot traversal When I try to create a ContainerConfig with workspace_folder "/workspace/../etc" Then a ContainerConfig ValueError should be raised mentioning ".." # ========================================================================= # Safe initialization — ContainerToolExecutor binary resolution # ========================================================================= Scenario: ContainerToolExecutor without devcontainer binary uses workspace-folder target args Given I have a ContainerToolExecutor with no container_id configured When I request the target CLI arguments Then the target args should use "--workspace-folder" Scenario: ContainerToolExecutor raises when devcontainer binary is missing at execution Given I have a ContainerToolExecutor with no devcontainer binary When I try to require the devcontainer binary Then a ContainerExecutionError should be raised about missing binary # ========================================================================= # Cleanup — sync_results_to_host error paths # ========================================================================= Scenario: sync_results_to_host rejects null bytes in container_path Given I have a ContainerToolExecutor with a mock that succeeds on sync using a temp dir When I try to sync a path containing null bytes Then a sync ValueError should be raised mentioning "null" Scenario: sync_results_to_host rejects relative container_path Given I have a ContainerToolExecutor with a mock that succeeds on sync using a temp dir When I try to sync a relative container path Then a sync ValueError should be raised mentioning "absolute" Scenario: sync_results_to_host raises on timeout during file sync Given I have a ContainerToolExecutor with a mock that times out on sync When I try to sync container path to host with timeout Then a ContainerTimeoutError should be raised from sync Scenario: sync_results_to_host raises on non-zero exit during file sync Given I have a ContainerToolExecutor with a mock that returns non-zero exit on sync When I try to sync container path to host with failure Then a ContainerExecutionError should be raised from sync failure Scenario: sync_results_to_host raises on OSError during file write Given I have a ContainerToolExecutor with a mock that triggers OSError on write When I try to sync container path to host triggering write error Then a ContainerExecutionError should be raised from write failure # ========================================================================= # Safe execution — _looks_like_path heuristic boundary checks # ========================================================================= Scenario: _looks_like_path rejects strings with newlines When I check whether a string with newlines looks like a path Then it should not look like a path Scenario: _looks_like_path rejects strings with null bytes When I check whether a string with null bytes looks like a path Then it should not look like a path Scenario: _looks_like_path rejects protocol-relative URIs When I check whether a protocol-relative URI looks like a path Then it should not look like a path Scenario: _looks_like_path rejects strings with query parameters When I check whether a path with query string looks like a path Then it should not look like a path Scenario: _looks_like_path rejects strings with fragment identifiers When I check whether a path with fragment identifier looks like a path Then it should not look like a path Scenario: _looks_like_path rejects strings with scheme separators When I check whether a URL-like string looks like a path Then it should not look like a path # ========================================================================= # Safe execution — recursion depth guards on path mapping # ========================================================================= Scenario: _map_input_paths stops at max recursion depth Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I map deeply nested input paths exceeding the recursion limit Then the input mapping should return without error Scenario: _map_output_paths stops at max recursion depth Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I map deeply nested output paths exceeding the recursion limit Then the output mapping should return without error # ========================================================================= # Safe execution — _parse_output edge cases # ========================================================================= Scenario: _parse_output returns result key for non-dict JSON When I parse stdout containing a JSON array Then the parsed output should wrap it under "result" key Scenario: _parse_output returns empty dict for blank stdout When I parse empty stdout Then the parsed output should be an empty dict # ========================================================================= # Safe execution — _map_output_paths skips raw_output key # ========================================================================= Scenario: _map_output_paths preserves raw_output without path mapping Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I map output containing a raw_output key with container path Then the raw_output value should remain unmapped # ========================================================================= # Safe execution — container tool non-JSON-serialisable inputs # ========================================================================= Scenario: execute_tool returns error for non-JSON-serialisable inputs Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I execute a tool with non-serialisable inputs Then the container tool result should indicate failure And the container tool result error should mention "JSON-serialisable" # ========================================================================= # ToolRunner full lifecycle — discover, activate, deactivate # ========================================================================= Scenario: ToolRunner discover returns tools from registry Given I have a ToolRunner with a populated registry When I call discover on the runner Then the discovered tools list should not be empty Scenario: ToolRunner activate marks a tool as ready Given I have a ToolRunner with a populated registry When I activate a tool on the runner Then the activated tool spec should be returned Scenario: ToolRunner activate raises ToolError for unknown tool Given I have a ToolRunner with a populated registry When I try to activate an unknown tool Then a ToolError should be raised for activation Scenario: ToolRunner deactivate cleans up active tool Given I have a ToolRunner with a populated registry When I activate and then deactivate a tool Then deactivate should return true And a second deactivate should return false # ========================================================================= # ToolRunner host execution path # ========================================================================= Scenario: ToolRunner executes tool on host and returns success Given I have a ToolRunner with a host-routed tool When I execute the host tool with valid inputs Then the host tool result should indicate success And the host tool result output should contain handler data Scenario: ToolRunner normalises non-dict handler output Given I have a ToolRunner with a tool returning non-dict output When I execute the non-dict-output tool Then the result output should wrap the value under "result" Scenario: ToolRunner catches handler exception and returns error Given I have a ToolRunner with a tool that raises an exception When I execute the raising tool Then the tool result should indicate failure with exception info Scenario: ToolRunner returns error for non-JSON-serialisable host output Given I have a ToolRunner with a tool returning non-serialisable output When I execute the non-serialisable-output tool Then the tool result should indicate failure mentioning "not JSON-serialisable" Scenario: ToolRunner returns error for non-JSON-serialisable host inputs Given I have a ToolRunner with a host-routed tool When I execute the host tool with non-serialisable inputs Then the tool result should indicate failure mentioning "not JSON-serialisable" Scenario: ToolRunner raises ToolError for unregistered tool in execute Given I have a ToolRunner with an empty registry When I try to execute an unregistered tool Then a ToolError should be raised for execution # ========================================================================= # ToolRunner container routing — additional edge cases # ========================================================================= Scenario: ToolRunner returns error when container env resolver raises ContainerUnavailableError Given I have a ToolRunner with an unavailable-container resolver When I execute a tool through the unavailable-container runner Then the tool result should indicate container unavailable Scenario: ToolRunner validates missing required input fields for container tools Given I have a ToolRunner with a container executor and required-field tool When I execute the container tool with missing required fields Then the tool result error should mention "Missing required input fields" Scenario: ToolRunner returns error for non-JSON-serialisable container inputs Given I have a ToolRunner with a container executor for JSON validation When I execute a container tool with non-serialisable inputs Then the container tool result error should mention "not JSON-serialisable" Scenario: ToolRunner catches container executor exception Given I have a ToolRunner with a failing container executor When I execute a tool through the failing container runner Then the tool result error should mention "Container execution error" # ========================================================================= # Safe execution — list-type value mapping branches # ========================================================================= Scenario: _map_input_paths handles list values containing host paths Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I map tool inputs containing a list of host paths Then the list values should be mapped to container paths Scenario: _map_output_paths handles list values containing container paths Given I have a ContainerToolExecutor with host_root "/tmp/sandbox" and container_root "/workspace" When I map tool output containing a list of container paths Then the list values should be mapped to host paths # ========================================================================= # Safe execution — PathMapper null byte rejection in mapping methods # ========================================================================= Scenario: PathMapper host_to_container rejects null bytes Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace" When I try to map a host path with null bytes to container Then a mapping ValueError should be raised about null bytes Scenario: PathMapper container_to_host rejects null bytes Given I have a PathMapper with host_root "/tmp/sandbox" and container_root "/workspace" When I try to map a container path with null bytes to host Then a mapping ValueError should be raised about null bytes # ========================================================================= # ToolResult validation — success/error consistency # ========================================================================= Scenario: ToolResult rejects success=True with error message When I try to create a ToolResult with success true and an error Then a ToolResult validation error should be raised Scenario: ToolResult rejects success=False without error message When I try to create a ToolResult with success false and no error Then a ToolResult validation error should be raised # ========================================================================= # ToolError structured attributes # ========================================================================= Scenario: ToolError carries structured context attributes When I create a ToolError with known attributes Then the ToolError should expose tool_name, error_type, and details