from __future__ import annotations import asyncio import json from collections.abc import Callable, Mapping from dataclasses import dataclass, field from pathlib import Path from typing import cast import pytest import dify_agent.layers.shell.layer as shell_layer_module from dify_agent.layers.shell import DIFY_SHELL_LAYER_TYPE_ID, DifyShellEnvVarConfig, DifyShellLayerConfig from dify_agent.layers.shell.layer import ( CompleteRemoteCommandResult, DEFAULT_TERMINATE_GRACE_SECONDS, DifyShellLayer, DifyShellRuntimeState, ) from dify_agent.adapters.shell.protocols import ( ShellCommandResult, ShellCommandStatus, ShellFileTransferProtocol, ShellProviderError, SandboxExpiredError, ShellProviderProtocol, ShellResourceProtocol, ) from dify_agent.layers.execution_context import DifyExecutionContextLayerConfig def _command_result( job_id: str, *, status: str = "running", done: bool = False, exit_code: int | None = None, output: str = "", offset: int = 0, truncated: bool = False, output_path: str | None = "/tmp/output.log", ) -> ShellCommandResult: return ShellCommandResult( job_id=job_id, status=status, done=done, exit_code=exit_code, output=output, offset=offset, truncated=truncated, output_path=output_path, ) def _command_status( job_id: str, *, status: str = "terminated", done: bool = True, exit_code: int | None = 130, offset: int = 0, ) -> ShellCommandStatus: return ShellCommandStatus(job_id=job_id, status=status, done=done, exit_code=exit_code, offset=offset) def _parse_tagged_observation(result: object) -> tuple[dict[str, object], str]: assert isinstance(result, str) metadata_tag = "\n\n\n\n" assert result.startswith("\n") assert result.endswith("\n") metadata_block, output_block = result.split(metadata_tag, 1) metadata = json.loads(metadata_block.removeprefix("\n")) assert isinstance(metadata, dict) output = output_block.removesuffix("\n") return cast(dict[str, object], metadata), output def _assert_error_observation(result: object, *, job_id: str | None = None, includes: str | None = None) -> None: assert isinstance(result, dict) assert isinstance(result.get("error"), str) if job_id is None: assert "job_id" not in result else: assert result["job_id"] == job_id if includes is not None: assert includes in result["error"] def _capture_logged_exceptions(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, tuple[object, ...]]]: logged: list[tuple[str, tuple[object, ...]]] = [] def fake_exception(message: str, *args: object, **kwargs: object) -> None: del kwargs logged.append((message, args)) monkeypatch.setattr(shell_layer_module.logger, "exception", fake_exception) return logged @dataclass(slots=True) class RunCall: script: str cwd: str | None env: Mapping[str, str] | None timeout: float @dataclass(slots=True) class WaitCall: job_id: str offset: int timeout: float @dataclass(slots=True) class InputCall: job_id: str text: str offset: int timeout: float @dataclass(slots=True) class TailCall: job_id: str @dataclass(slots=True) class InterruptCall: job_id: str grace_seconds: float @dataclass(slots=True) class DeleteCall: job_id: str force: bool grace_seconds: float | None class _UnexpectedToolError(Exception): pass class FakeFiles(ShellFileTransferProtocol): async def upload(self, *, content: bytes, remote_path: str, cwd: str | None = None) -> None: raise AssertionError("resource.files should not be used by production shell layer logic") async def download(self, *, remote_path: str, cwd: str | None = None) -> bytes: raise AssertionError("resource.files should not be used by production shell layer logic") @dataclass(slots=True) class FakeCommands: run_handler: Callable[[str, str | None, Mapping[str, str] | None, float], ShellCommandResult] | None = None wait_handler: Callable[[str, int, float], ShellCommandResult] | None = None input_handler: Callable[[str, str, int, float], ShellCommandResult] | None = None tail_handler: Callable[[str], ShellCommandResult] | None = None interrupt_handler: Callable[[str, float], ShellCommandStatus] | None = None delete_handler: Callable[[str, bool, float | None], None] | None = None run_calls: list[RunCall] = field(default_factory=list) wait_calls: list[WaitCall] = field(default_factory=list) input_calls: list[InputCall] = field(default_factory=list) tail_calls: list[TailCall] = field(default_factory=list) interrupt_calls: list[InterruptCall] = field(default_factory=list) delete_calls: list[DeleteCall] = field(default_factory=list) async def run(self, script: str, *, cwd: str | None = None, env: dict[str, str] | None = None, timeout: float): self.run_calls.append(RunCall(script=script, cwd=cwd, env=env, timeout=timeout)) if self.run_handler is None: raise AssertionError("Unexpected run() call") return self.run_handler(script, cwd, env, timeout) async def wait(self, job_id: str, *, offset: int, timeout: float): self.wait_calls.append(WaitCall(job_id=job_id, offset=offset, timeout=timeout)) if self.wait_handler is None: raise AssertionError("Unexpected wait() call") return self.wait_handler(job_id, offset, timeout) async def read_output(self, job_id: str, *, offset: int): self.wait_calls.append(WaitCall(job_id=job_id, offset=offset, timeout=0.0)) if self.wait_handler is None: raise AssertionError("Unexpected read_output() call") return self.wait_handler(job_id, offset, 0.0) async def input(self, job_id: str, text: str, *, offset: int, timeout: float): self.input_calls.append(InputCall(job_id=job_id, text=text, offset=offset, timeout=timeout)) if self.input_handler is None: raise AssertionError("Unexpected input() call") return self.input_handler(job_id, text, offset, timeout) async def interrupt(self, job_id: str, *, grace_seconds: float): self.interrupt_calls.append(InterruptCall(job_id=job_id, grace_seconds=grace_seconds)) if self.interrupt_handler is None: raise AssertionError("Unexpected interrupt() call") return self.interrupt_handler(job_id, grace_seconds) async def tail(self, job_id: str): self.tail_calls.append(TailCall(job_id=job_id)) if self.tail_handler is None: raise AssertionError("Unexpected tail() call") return self.tail_handler(job_id) async def delete(self, job_id: str, *, force: bool = False, grace_seconds: float | None = None) -> None: self.delete_calls.append(DeleteCall(job_id=job_id, force=force, grace_seconds=grace_seconds)) if self.delete_handler is not None: self.delete_handler(job_id, force, grace_seconds) @dataclass(slots=True) class FakeResource(ShellResourceProtocol): commands: FakeCommands files: FakeFiles = field(default_factory=FakeFiles) suspended: bool = False deleted: bool = False _sandbox_id: str | None = None @property def sandbox_id(self) -> str | None: return self._sandbox_id async def suspend(self) -> None: self.suspended = True async def delete(self) -> None: self.deleted = True @dataclass(slots=True) class FakeProvider(ShellProviderProtocol): resource: FakeResource create_calls: int = 0 attach_calls: list[str] = field(default_factory=list) attach_error: ShellProviderError | None = None async def create(self) -> ShellResourceProtocol: self.create_calls += 1 return self.resource async def attach(self, sandbox_id: str) -> ShellResourceProtocol: self.attach_calls.append(sandbox_id) if self.attach_error is not None: raise self.attach_error self.resource._sandbox_id = sandbox_id return self.resource def _layer( *, commands: FakeCommands, config: DifyShellLayerConfig | None = None, shell_home_root: str = "/home", ) -> tuple[DifyShellLayer, FakeProvider]: provider = FakeProvider(resource=FakeResource(commands=commands)) layer = DifyShellLayer.from_config_with_settings( config or DifyShellLayerConfig(), shell_provider=provider, shell_home_root=shell_home_root, ) return layer, provider @dataclass(slots=True) class _ExecutionContextStub: config: DifyExecutionContextLayerConfig def _bind_execution_context(layer: DifyShellLayer, *, agent_id: str | None = "agent-1") -> None: layer.deps.execution_context = cast( object, _ExecutionContextStub(config=_execution_context_config(agent_id=agent_id)) ) def _execution_context_config(*, agent_id: str | None = None) -> DifyExecutionContextLayerConfig: return DifyExecutionContextLayerConfig( tenant_id="tenant-1", user_id="user-1", user_from="account", agent_id=agent_id, agent_mode="agent_app", invoke_from="service-api", ) def _runtime_state( *, session_id: str = "abc12ff", workspace_cwd: str = "~/workspace/abc12ff", sandbox_id: str | None = None, job_ids: list[str] | None = None, job_offsets: dict[str, int] | None = None, ) -> DifyShellRuntimeState: return DifyShellRuntimeState( session_id=session_id, workspace_cwd=workspace_cwd, sandbox_id=sandbox_id, job_ids=[] if job_ids is None else job_ids, job_offsets={} if job_offsets is None else job_offsets, ) def test_shell_type_id_constant_matches_implementation_class() -> None: assert DIFY_SHELL_LAYER_TYPE_ID == DifyShellLayer.type_id def test_resource_context_calls_provider_create_and_suspends_on_exit() -> None: layer, provider = _layer(commands=FakeCommands()) async def scenario() -> None: async with layer.resource_context(): assert provider.create_calls == 1 assert provider.resource.suspended is False assert provider.resource.deleted is False assert provider.resource.suspended is True assert provider.resource.deleted is False asyncio.run(scenario()) def test_shell_layer_create_allocates_workspace_and_bootstraps(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(shell_layer_module.time, "time", lambda: int("abc12", 16)) monkeypatch.setattr(shell_layer_module.secrets, "token_hex", lambda _nbytes: "ff") expected_home = "/home/agent-1" expected_workspace_cwd = "/home/agent-1/workspace/abc12ff" def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: assert env == {"HOME": expected_home} if cwd is None: assert 'mkdir -p "$HOME/workspace"' in script return _command_result("mkdir-job", status="exited", done=True, exit_code=0) assert cwd == expected_workspace_cwd assert "apt-get install -y ripgrep" in script return _command_result("bootstrap-job", status="exited", done=True, exit_code=0) layer, provider = _layer( commands=FakeCommands(run_handler=run_handler), config=DifyShellLayerConfig( cli_tools=[{"name": "ripgrep", "install_commands": ["apt-get install -y ripgrep"]}], ), ) _bind_execution_context(layer) async def scenario() -> None: async with layer.resource_context(): await layer.on_context_create() assert provider.resource.suspended is False asyncio.run(scenario()) assert layer.runtime_state.session_id == "abc12ff" assert layer.runtime_state.workspace_cwd == "~/workspace/abc12ff" assert [call.job_id for call in provider.resource.commands.delete_calls] == ["mkdir-job", "bootstrap-job"] assert provider.resource.suspended is True def test_shell_layer_uses_agent_specific_home_and_workspace_cwd( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(shell_layer_module.time, "time", lambda: int("abc12", 16)) monkeypatch.setattr(shell_layer_module.secrets, "token_hex", lambda _nbytes: "ff") expected_home = "/home/agent-1" expected_workspace_cwd = "/home/agent-1/workspace/abc12ff" def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: del timeout if script.startswith('mkdir -p "$HOME/workspace";'): assert cwd is None assert env == {"HOME": expected_home} return _command_result("mkdir-job", status="exited", done=True, exit_code=0) if script == "pwd": assert cwd == expected_workspace_cwd assert env == {"HOME": expected_home} return _command_result("user-job", status="exited", done=True, exit_code=0, output=expected_home, offset=13) raise AssertionError(f"Unexpected script: {script!r}") commands = FakeCommands(run_handler=run_handler) layer, _provider = _layer(commands=commands) _bind_execution_context(layer) tools = {tool.name: tool for tool in layer.tools} async def scenario() -> None: async with layer.resource_context(): await layer.on_context_create() run_result = await tools["shell_run"].function_schema.call({"script": "pwd"}, None) # pyright: ignore[reportArgumentType] metadata, output = _parse_tagged_observation(run_result) assert metadata["job_id"] == "user-job" assert output == expected_home asyncio.run(scenario()) assert layer.runtime_state.session_id == "abc12ff" assert layer.runtime_state.workspace_cwd == "~/workspace/abc12ff" assert layer.runtime_state.job_ids == ["user-job"] assert [call.job_id for call in commands.delete_calls] == ["mkdir-job"] def test_shell_layer_uses_configured_home_root_for_local_development( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: monkeypatch.setattr(shell_layer_module.time, "time", lambda: int("abc12", 16)) monkeypatch.setattr(shell_layer_module.secrets, "token_hex", lambda _nbytes: "ff") shell_home_root = tmp_path / "shell-home" expected_home = f"{shell_home_root}/agent-1" expected_workspace_cwd = f"{expected_home}/workspace/abc12ff" def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: del timeout if script.startswith('mkdir -p "$HOME/workspace";'): assert cwd is None assert env == {"HOME": expected_home} return _command_result("mkdir-job", status="exited", done=True, exit_code=0) if script == "pwd": assert cwd == expected_workspace_cwd assert env == {"HOME": expected_home} return _command_result("user-job", status="exited", done=True, exit_code=0, output=expected_home, offset=13) raise AssertionError(f"Unexpected script: {script!r}") layer, _provider = _layer(commands=FakeCommands(run_handler=run_handler), shell_home_root=f"{shell_home_root}/") _bind_execution_context(layer) tools = {tool.name: tool for tool in layer.tools} async def scenario() -> None: async with layer.resource_context(): await layer.on_context_create() await tools["shell_run"].function_schema.call({"script": "pwd"}, None) # pyright: ignore[reportArgumentType] asyncio.run(scenario()) assert layer.shell_home_root == str(shell_home_root) assert layer.runtime_state.workspace_cwd == "~/workspace/abc12ff" def test_shell_layer_suspend_does_not_close_before_resource_context_exits() -> None: layer, provider = _layer(commands=FakeCommands()) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): await layer.on_context_suspend() assert provider.resource.suspended is False assert provider.resource.suspended is True asyncio.run(scenario()) def test_shell_layer_resume_recreates_live_home_and_workspace() -> None: expected_home = "/home/agent-1" def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: del timeout assert script == 'mkdir -p "$HOME/workspace/abc12ff"' assert cwd is None assert env == {"HOME": expected_home} return _command_result("resume-job", status="exited", done=True, exit_code=0) commands = FakeCommands(run_handler=run_handler) layer, provider = _layer(commands=commands) _bind_execution_context(layer) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): await layer.on_context_resume() asyncio.run(scenario()) assert [call.job_id for call in commands.delete_calls] == ["resume-job"] assert provider.resource.suspended is True def test_shell_layer_resume_requires_agent_id_for_live_workspace_reentry() -> None: commands = FakeCommands() layer, _provider = _layer(commands=commands) _bind_execution_context(layer, agent_id=None) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): with pytest.raises(ValueError, match="requires execution_context\\.agent_id"): await layer.on_context_resume() asyncio.run(scenario()) assert commands.run_calls == [] def test_shell_layer_delete_cleans_workspace_and_tracked_jobs() -> None: def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: del env, timeout assert cwd is None assert script == 'rm -rf -- "$HOME/workspace/abc12ff"' return _command_result("cleanup-job", status="exited", done=True, exit_code=0) commands = FakeCommands(run_handler=run_handler) layer, provider = _layer(commands=commands) _bind_execution_context(layer) layer.runtime_state = _runtime_state(job_ids=["user-job"], job_offsets={"user-job": 9}) async def scenario() -> None: async with layer.resource_context(): await layer.on_context_delete() asyncio.run(scenario()) assert [call.job_id for call in commands.delete_calls] == ["cleanup-job", "user-job"] assert layer.runtime_state.job_ids == [] assert layer.runtime_state.job_offsets == {} assert provider.resource.deleted is True def test_shell_layer_tools_map_inputs_and_maintain_offsets_with_tail_end() -> None: def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: assert script == "pwd" assert cwd == "/home/agent-1/workspace/abc12ff" assert env == {"HOME": "/home/agent-1"} return _command_result( "user-job", status="running", done=False, output="head-output\n", offset=10, truncated=True, output_path="/tmp/initial.log", ) def wait_handler(job_id: str, offset: int, timeout: float) -> ShellCommandResult: assert job_id == "user-job" if timeout == 4.0: assert offset == 22 return _command_result("user-job", status="running", done=False, output="more\n", offset=30) raise AssertionError(f"Unexpected wait/read_output: offset={offset} timeout={timeout}") def input_handler(job_id: str, text: str, offset: int, timeout: float) -> ShellCommandResult: assert (job_id, text, offset, timeout) == ("user-job", "ls\n", 30, 5.0) return _command_result("user-job", status="exited", done=True, exit_code=0, output="file.txt\n", offset=34) def tail_handler(job_id: str) -> ShellCommandResult: assert job_id == "user-job" return _command_result( job_id, status="exited", done=True, exit_code=0, output="tail-output\n", offset=22, output_path="/tmp/resolved.log", ) def interrupt_handler(job_id: str, grace_seconds: float) -> ShellCommandStatus: assert (job_id, grace_seconds) == ("user-job", 1.5) return _command_status("user-job", status="terminated", done=True, exit_code=130, offset=34) commands = FakeCommands( run_handler=run_handler, wait_handler=wait_handler, input_handler=input_handler, tail_handler=tail_handler, interrupt_handler=interrupt_handler, ) layer, provider = _layer(commands=commands) _bind_execution_context(layer) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): run_result = await tools["shell_run"].function_schema.call({"script": "pwd"}, None) # pyright: ignore[reportArgumentType] wait_result = await tools["shell_wait"].function_schema.call( {"job_id": "user-job", "timeout": 4.0}, None, # pyright: ignore[reportArgumentType] ) input_result = await tools["shell_input"].function_schema.call( {"job_id": "user-job", "text": "ls\n", "timeout": 5.0}, None, # pyright: ignore[reportArgumentType] ) interrupt_result = await tools["shell_interrupt"].function_schema.call( {"job_id": "user-job", "grace_seconds": 1.5}, None, # pyright: ignore[reportArgumentType] ) run_metadata, run_output = _parse_tagged_observation(run_result) wait_metadata, wait_output = _parse_tagged_observation(wait_result) input_metadata, input_output = _parse_tagged_observation(input_result) interrupt_metadata, interrupt_output = _parse_tagged_observation(interrupt_result) assert run_metadata == { "job_id": "user-job", "status": "running", "done": False, "exit_code": None, "output_path": "/tmp/resolved.log", } assert "head-output" in run_output assert "tail-output" in run_output assert wait_metadata["job_id"] == "user-job" assert wait_output == "more\n" assert input_metadata["exit_code"] == 0 assert input_output == "file.txt\n" assert interrupt_metadata == { "job_id": "user-job", "status": "terminated", "done": True, "exit_code": 130, "output_path": "/tmp/resolved.log", } assert interrupt_output == "Job was interrupted." asyncio.run(scenario()) assert layer.runtime_state.job_offsets == {"user-job": 34} assert commands.tail_calls == [TailCall(job_id="user-job"), TailCall(job_id="user-job")] assert provider.resource.suspended is True def test_shell_run_keeps_original_offset_when_tail_lookup_fails_for_truncated_output() -> None: def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: assert script == "pwd" assert cwd == "/home/agent-1/workspace/abc12ff" assert env == {"HOME": "/home/agent-1"} return _command_result( "user-job", status="running", done=False, output="head-output\n", offset=10, truncated=True, output_path="/tmp/current.log", ) def tail_handler(job_id: str) -> ShellCommandResult: raise RuntimeError(f"tail unavailable for {job_id}") commands = FakeCommands(run_handler=run_handler, tail_handler=tail_handler) layer, provider = _layer(commands=commands) _bind_execution_context(layer) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): run_result = await tools["shell_run"].function_schema.call({"script": "pwd"}, None) # pyright: ignore[reportArgumentType] metadata, output = _parse_tagged_observation(run_result) assert metadata == { "job_id": "user-job", "status": "running", "done": False, "exit_code": None, "output_path": "/tmp/current.log", } assert "head-output" in output assert set(metadata) == {"job_id", "status", "done", "exit_code", "output_path"} asyncio.run(scenario()) assert layer.runtime_state.job_offsets == {"user-job": 10} assert commands.tail_calls == [TailCall(job_id="user-job")] assert provider.resource.suspended is True def test_shell_run_formats_large_non_truncated_output_without_tail_lookup() -> None: large_output = ("head-" + ("x" * 9000) + "-tail").replace("head-x", "head-y", 1) commands = FakeCommands( run_handler=lambda script, cwd, env, timeout: _command_result( "user-job", status="exited", done=True, exit_code=0, output=large_output, offset=len(large_output), output_path="/tmp/large.log", ) ) layer, _provider = _layer(commands=commands) _bind_execution_context(layer) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_run"].function_schema.call({"script": "cat large.log"}, None) # pyright: ignore[reportArgumentType] metadata, output = _parse_tagged_observation(result) assert metadata["output_path"] == "/tmp/large.log" assert output.startswith("head-y") assert output.endswith("(check the /tmp/large.log for full output)") assert "-tail" in output asyncio.run(scenario()) assert commands.tail_calls == [] def test_shell_interrupt_succeeds_when_tail_lookup_fails() -> None: commands = FakeCommands( interrupt_handler=lambda job_id, grace_seconds: _command_status(job_id, offset=22), tail_handler=lambda job_id: (_ for _ in ()).throw(RuntimeError("tail unavailable")), ) layer, _provider = _layer(commands=commands) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state(job_ids=["user-job"], job_offsets={"user-job": 22}) async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_interrupt"].function_schema.call({"job_id": "user-job"}, None) # pyright: ignore[reportArgumentType] metadata, output = _parse_tagged_observation(result) assert metadata == { "job_id": "user-job", "status": "terminated", "done": True, "exit_code": 130, } assert output == "Job was interrupted." asyncio.run(scenario()) def test_shell_run_returns_provider_timeout_error_observation_without_unexpected_logging( monkeypatch: pytest.MonkeyPatch, ) -> None: logged = _capture_logged_exceptions(monkeypatch) commands = FakeCommands( run_handler=lambda script, cwd, env, timeout: (_ for _ in ()).throw( ShellProviderError("provider timed out", code="timeout") ) ) layer, _provider = _layer(commands=commands) _bind_execution_context(layer) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_run"].function_schema.call({"script": "pwd"}, None) # pyright: ignore[reportArgumentType] _assert_error_observation(result, includes="timeout") asyncio.run(scenario()) assert logged == [] def test_shell_wait_returns_provider_request_error_observation_with_job_id_and_no_unexpected_logging( monkeypatch: pytest.MonkeyPatch, ) -> None: logged = _capture_logged_exceptions(monkeypatch) commands = FakeCommands( wait_handler=lambda job_id, offset, timeout: (_ for _ in ()).throw( ShellProviderError("provider unavailable", code="request_error") ) ) layer, _provider = _layer(commands=commands) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state(job_ids=["user-job"], job_offsets={"user-job": 3}) async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_wait"].function_schema.call( {"job_id": "user-job", "timeout": 4.0}, None, # pyright: ignore[reportArgumentType] ) _assert_error_observation(result, job_id="user-job", includes="request_error") asyncio.run(scenario()) assert logged == [] def test_shell_input_returns_provider_timeout_error_observation_with_job_id_and_no_unexpected_logging( monkeypatch: pytest.MonkeyPatch, ) -> None: logged = _capture_logged_exceptions(monkeypatch) commands = FakeCommands( input_handler=lambda job_id, text, offset, timeout: (_ for _ in ()).throw( ShellProviderError("provider timed out", code="timeout") ) ) layer, _provider = _layer(commands=commands) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state(job_ids=["user-job"], job_offsets={"user-job": 6}) async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_input"].function_schema.call( {"job_id": "user-job", "text": "ls\n", "timeout": 5.0}, None, # pyright: ignore[reportArgumentType] ) _assert_error_observation(result, job_id="user-job", includes="timeout") asyncio.run(scenario()) assert logged == [] def test_shell_interrupt_returns_provider_request_error_observation_with_job_id_and_no_unexpected_logging( monkeypatch: pytest.MonkeyPatch, ) -> None: logged = _capture_logged_exceptions(monkeypatch) commands = FakeCommands( interrupt_handler=lambda job_id, grace_seconds: (_ for _ in ()).throw( ShellProviderError("provider unavailable", code="request_error") ) ) layer, _provider = _layer(commands=commands) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state(job_ids=["user-job"], job_offsets={"user-job": 22}) async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_interrupt"].function_schema.call( {"job_id": "user-job", "grace_seconds": 1.5}, None, # pyright: ignore[reportArgumentType] ) _assert_error_observation(result, job_id="user-job", includes="request_error") asyncio.run(scenario()) assert logged == [] def test_shell_run_returns_error_observation_and_logs_unexpected_exception( monkeypatch: pytest.MonkeyPatch, ) -> None: logged = _capture_logged_exceptions(monkeypatch) commands = FakeCommands( run_handler=lambda script, cwd, env, timeout: (_ for _ in ()).throw(_UnexpectedToolError("boom")) ) layer, _provider = _layer(commands=commands) _bind_execution_context(layer) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_run"].function_schema.call({"script": "pwd"}, None) # pyright: ignore[reportArgumentType] _assert_error_observation(result, includes="boom") asyncio.run(scenario()) assert logged == [ ("Unexpected shell tool failure: tool=%s session_id=%s job_id=%s", ("shell_run", "abc12ff", None)) ] def test_shell_wait_returns_error_observation_and_logs_unexpected_exception( monkeypatch: pytest.MonkeyPatch, ) -> None: logged = _capture_logged_exceptions(monkeypatch) commands = FakeCommands( wait_handler=lambda job_id, offset, timeout: (_ for _ in ()).throw(_UnexpectedToolError("wait exploded")) ) layer, _provider = _layer(commands=commands) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state(job_ids=["user-job"], job_offsets={"user-job": 3}) async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_wait"].function_schema.call( {"job_id": "user-job", "timeout": 4.0}, None, # pyright: ignore[reportArgumentType] ) _assert_error_observation(result, job_id="user-job", includes="wait exploded") asyncio.run(scenario()) assert logged == [ ("Unexpected shell tool failure: tool=%s session_id=%s job_id=%s", ("shell_wait", "abc12ff", "user-job")) ] def test_shell_input_returns_error_observation_and_logs_unexpected_exception( monkeypatch: pytest.MonkeyPatch, ) -> None: logged = _capture_logged_exceptions(monkeypatch) commands = FakeCommands( input_handler=lambda job_id, text, offset, timeout: (_ for _ in ()).throw( _UnexpectedToolError("stdin exploded") ) ) layer, _provider = _layer(commands=commands) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state(job_ids=["user-job"], job_offsets={"user-job": 6}) async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_input"].function_schema.call( {"job_id": "user-job", "text": "ls\n", "timeout": 5.0}, None, # pyright: ignore[reportArgumentType] ) _assert_error_observation(result, job_id="user-job", includes="stdin exploded") asyncio.run(scenario()) assert logged == [ ( "Unexpected shell tool failure: tool=%s session_id=%s job_id=%s", ("shell_input", "abc12ff", "user-job"), ) ] def test_shell_interrupt_returns_error_observation_and_logs_unexpected_exception( monkeypatch: pytest.MonkeyPatch, ) -> None: logged = _capture_logged_exceptions(monkeypatch) commands = FakeCommands( interrupt_handler=lambda job_id, grace_seconds: (_ for _ in ()).throw( _UnexpectedToolError("interrupt exploded") ) ) layer, _provider = _layer(commands=commands) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state(job_ids=["user-job"], job_offsets={"user-job": 22}) async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_interrupt"].function_schema.call( {"job_id": "user-job", "grace_seconds": 1.5}, None, # pyright: ignore[reportArgumentType] ) _assert_error_observation(result, job_id="user-job", includes="interrupt exploded") asyncio.run(scenario()) assert logged == [ ( "Unexpected shell tool failure: tool=%s session_id=%s job_id=%s", ("shell_interrupt", "abc12ff", "user-job"), ) ] def test_shell_interrupt_logs_unexpected_tail_failure_but_still_succeeds( monkeypatch: pytest.MonkeyPatch, ) -> None: logged = _capture_logged_exceptions(monkeypatch) commands = FakeCommands( interrupt_handler=lambda job_id, grace_seconds: _command_status(job_id, offset=22), tail_handler=lambda job_id: (_ for _ in ()).throw(_UnexpectedToolError("tail exploded")), ) layer, _provider = _layer(commands=commands) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state(job_ids=["user-job"], job_offsets={"user-job": 22}) async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_interrupt"].function_schema.call({"job_id": "user-job"}, None) # pyright: ignore[reportArgumentType] metadata, output = _parse_tagged_observation(result) assert metadata == { "job_id": "user-job", "status": "terminated", "done": True, "exit_code": 130, } assert output == "Job was interrupted." asyncio.run(scenario()) assert logged == [ ( "Failed to fetch output path for interrupted shell job %s in session %s", ("user-job", "abc12ff"), ) ] def test_shell_run_propagates_cancelled_error() -> None: commands = FakeCommands( run_handler=lambda script, cwd, env, timeout: (_ for _ in ()).throw(asyncio.CancelledError()) ) layer, _provider = _layer(commands=commands) _bind_execution_context(layer) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): with pytest.raises(asyncio.CancelledError): await tools["shell_run"].function_schema.call({"script": "pwd"}, None) # pyright: ignore[reportArgumentType] asyncio.run(scenario()) def test_run_remote_script_complete_uses_read_output_before_wait_and_deletes_job() -> None: events: list[str] = [] def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: events.append("run") assert script == "printf 'abcdefghi'" assert cwd == "/home/agent-1/workspace/abc12ff" assert env == {"HOME": "/home/agent-1"} return _command_result("remote-job", status="running", done=False, output="abc", offset=3, truncated=True) def wait_handler(job_id: str, offset: int, timeout: float) -> ShellCommandResult: if timeout == 0.0: events.append("read_output") assert offset == 3 return _command_result(job_id, status="running", done=False, output="def", offset=6) events.append("wait") assert offset == 6 return _command_result(job_id, status="exited", done=True, exit_code=0, output="ghi", offset=9) commands = FakeCommands(run_handler=run_handler, wait_handler=wait_handler) layer, _provider = _layer(commands=commands) _bind_execution_context(layer) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): result = await layer.run_remote_script_complete("printf 'abcdefghi'") assert isinstance(result, CompleteRemoteCommandResult) assert result.output == "abcdefghi" assert result.output_complete is True assert result.incomplete_reason is None asyncio.run(scenario()) assert events == ["run", "read_output", "wait"] assert [call.job_id for call in commands.delete_calls] == ["remote-job"] def test_run_remote_script_complete_uses_agent_specific_home_and_workspace_cwd() -> None: expected_home = "/home/agent-1" expected_workspace_cwd = "/home/agent-1/workspace/abc12ff" def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: del timeout assert script == "pwd" assert cwd == expected_workspace_cwd assert env == {"HOME": expected_home} return _command_result( "remote-job", status="exited", done=True, exit_code=0, output=expected_home, offset=len(expected_home), ) commands = FakeCommands(run_handler=run_handler) layer, _provider = _layer(commands=commands) _bind_execution_context(layer) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): result = await layer.run_remote_script_complete("pwd") assert isinstance(result, CompleteRemoteCommandResult) assert result.output == expected_home asyncio.run(scenario()) assert [call.job_id for call in commands.delete_calls] == ["remote-job"] def test_run_remote_script_complete_passes_config_env_values_to_shellctl_env() -> None: def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: del timeout assert script == "printenv API_TOKEN" assert cwd == "/home/agent-1/workspace/abc12ff" assert env == {"HOME": "/home/agent-1", "API_TOKEN": "inline-secret-value"} return _command_result( "remote-job", status="exited", done=True, exit_code=0, output="inline-secret-value\n", offset=len("inline-secret-value\n"), ) commands = FakeCommands(run_handler=run_handler) layer, _provider = _layer( commands=commands, config=DifyShellLayerConfig(env=[DifyShellEnvVarConfig(name="API_TOKEN", value="inline-secret-value")]), ) _bind_execution_context(layer) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): result = await layer.run_remote_script_complete("printenv API_TOKEN") assert isinstance(result, CompleteRemoteCommandResult) assert result.output == "inline-secret-value\n" asyncio.run(scenario()) assert [call.job_id for call in commands.delete_calls] == ["remote-job"] def test_run_remote_script_uses_agent_specific_home_and_workspace_cwd() -> None: expected_home = "/home/agent-1" expected_workspace_cwd = "/home/agent-1/workspace/abc12ff" def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: del timeout assert script == "pwd" assert cwd == expected_workspace_cwd assert env == {"HOME": expected_home} return _command_result( "remote-job", status="exited", done=True, exit_code=0, output=expected_home, offset=len(expected_home), ) commands = FakeCommands(run_handler=run_handler) layer, _provider = _layer(commands=commands) _bind_execution_context(layer) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): result = await layer.run_remote_script("pwd") assert isinstance(result, CompleteRemoteCommandResult) assert result.output == expected_home asyncio.run(scenario()) assert [call.job_id for call in commands.delete_calls] == ["remote-job"] def test_run_remote_script_complete_returns_incomplete_reason_for_output_limit() -> None: def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: del script, timeout assert cwd == "/home/agent-1/workspace/abc12ff" assert env == {"HOME": "/home/agent-1"} return _command_result( "remote-job", status="running", done=False, output="hello world", offset=11, truncated=True, ) commands = FakeCommands( run_handler=run_handler, interrupt_handler=lambda job_id, grace_seconds: _command_status(job_id, status="terminated", offset=11), ) layer, _provider = _layer(commands=commands) _bind_execution_context(layer) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): result = await layer.run_remote_script_complete("printf 'hello world'", max_output_bytes=5) assert result.output == "hello" assert result.output_complete is False assert result.incomplete_reason == "output_limit" assert result.status == "terminated" asyncio.run(scenario()) assert commands.wait_calls == [] assert commands.interrupt_calls == [ InterruptCall(job_id="remote-job", grace_seconds=DEFAULT_TERMINATE_GRACE_SECONDS) ] def test_run_remote_script_complete_returns_incomplete_reason_for_timeout( monkeypatch: pytest.MonkeyPatch, ) -> None: now = 100.0 def fake_monotonic() -> float: return now monkeypatch.setattr(shell_layer_module.time, "monotonic", fake_monotonic) def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: nonlocal now assert script == "sleep 10" assert cwd == "/home/agent-1/workspace/abc12ff" assert env == {"HOME": "/home/agent-1"} assert timeout == pytest.approx(60.0, rel=0, abs=0.01) now = 161.0 return _command_result("remote-job", status="running", done=False, output="hello", offset=5) commands = FakeCommands( run_handler=run_handler, interrupt_handler=lambda job_id, grace_seconds: _command_status( job_id, status="terminated", done=True, exit_code=130, offset=5, ), ) layer, _provider = _layer(commands=commands) _bind_execution_context(layer) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): result = await layer.run_remote_script_complete("sleep 10", timeout=60.0) assert result.output == "hello" assert result.output_complete is False assert result.incomplete_reason == "timeout" assert result.status == "terminated" assert result.exit_code == 130 asyncio.run(scenario()) assert commands.wait_calls == [] assert commands.interrupt_calls == [ InterruptCall(job_id="remote-job", grace_seconds=DEFAULT_TERMINATE_GRACE_SECONDS) ] assert [call.job_id for call in commands.delete_calls] == ["remote-job"] def test_shell_layer_rejects_untracked_job_ids_without_provider_calls() -> None: commands = FakeCommands() layer, _provider = _layer(commands=commands) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): wait_result = await tools["shell_wait"].function_schema.call({"job_id": "missing"}, None) # pyright: ignore[reportArgumentType] input_result = await tools["shell_input"].function_schema.call( {"job_id": "missing", "text": "hello"}, None, # pyright: ignore[reportArgumentType] ) interrupt_result = await tools["shell_interrupt"].function_schema.call({"job_id": "missing"}, None) # pyright: ignore[reportArgumentType] _assert_error_observation(wait_result, job_id="missing") _assert_error_observation(input_result, job_id="missing") _assert_error_observation(interrupt_result, job_id="missing") asyncio.run(scenario()) assert commands.wait_calls == [] assert commands.input_calls == [] assert commands.interrupt_calls == [] def test_shell_layer_requires_agent_id_for_live_command_execution(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(shell_layer_module.time, "time", lambda: int("abc12", 16)) monkeypatch.setattr(shell_layer_module.secrets, "token_hex", lambda _nbytes: "ff") commands = FakeCommands() layer, _provider = _layer(commands=commands) _bind_execution_context(layer, agent_id=None) async def scenario() -> None: async with layer.resource_context(): with pytest.raises(ValueError, match="requires execution_context\\.agent_id"): await layer.on_context_create() asyncio.run(scenario()) assert commands.run_calls == [] def test_shell_layer_hooks_and_tools_fail_clearly_outside_active_resource_context() -> None: layer, _provider = _layer(commands=FakeCommands()) tools = {tool.name: tool for tool in layer.tools} layer.runtime_state = _runtime_state() async def scenario() -> None: result = await tools["shell_run"].function_schema.call({"script": "pwd"}, None) # pyright: ignore[reportArgumentType] _assert_error_observation(result, includes="shell resource") asyncio.run(scenario()) def test_shell_runtime_state_validates_workspace_identity_and_offset_keys() -> None: with pytest.raises(ValueError, match="5\\+2 lowercase hex format"): _ = DifyShellRuntimeState.model_validate( { "session_id": "../../tmp", "workspace_cwd": "~/workspace/../../tmp", "job_ids": [], "job_offsets": {}, } ) with pytest.raises(ValueError, match="workspace_cwd must equal"): _ = DifyShellRuntimeState.model_validate( { "session_id": "abc12ff", "workspace_cwd": "~/workspace/def34aa", "job_ids": [], "job_offsets": {}, } ) state = DifyShellRuntimeState.model_validate( { "session_id": "abc12ff", "workspace_cwd": "~/workspace/abc12ff", "job_ids": ['job"bad with spaces'], "job_offsets": {'job"bad with spaces': 0}, } ) assert state.job_ids == ['job"bad with spaces'] with pytest.raises(ValueError, match="unknown job ids"): _ = DifyShellRuntimeState.model_validate( { "session_id": "abc12ff", "workspace_cwd": "~/workspace/abc12ff", "job_ids": ["job-1"], "job_offsets": {"job-2": 3}, } ) def test_resource_context_attaches_when_sandbox_id_is_present() -> None: layer, provider = _layer(commands=FakeCommands()) layer.runtime_state = _runtime_state(sandbox_id="existing-sandbox-1") async def scenario() -> None: async with layer.resource_context(): assert provider.create_calls == 0 assert provider.attach_calls == ["existing-sandbox-1"] assert provider.resource.suspended is False assert provider.resource.suspended is True asyncio.run(scenario()) def test_resource_context_deletes_on_context_delete() -> None: commands = FakeCommands( run_handler=lambda script, cwd, env, timeout: _command_result( "cleanup-job", status="exited", done=True, exit_code=0 ) ) layer, provider = _layer(commands=commands) _bind_execution_context(layer) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): await layer.on_context_delete() assert provider.resource.deleted is True assert provider.resource.suspended is False asyncio.run(scenario()) def test_resource_context_suspends_on_context_suspend() -> None: layer, provider = _layer(commands=FakeCommands()) layer.runtime_state = _runtime_state() async def scenario() -> None: async with layer.resource_context(): await layer.on_context_suspend() assert provider.resource.suspended is True assert provider.resource.deleted is False asyncio.run(scenario()) def test_resource_context_persists_sandbox_id_from_provider() -> None: layer, provider = _layer(commands=FakeCommands()) provider.resource._sandbox_id = "new-sandbox-42" async def scenario() -> None: async with layer.resource_context(): pass asyncio.run(scenario()) assert layer.runtime_state.sandbox_id == "new-sandbox-42" def test_resource_context_propagates_sandbox_expired() -> None: """When attach() raises SandboxExpiredError, the error propagates to the caller. The user must start a new session — no in-place recovery is attempted.""" layer, provider = _layer(commands=FakeCommands()) provider.attach_error = SandboxExpiredError( "stale-sandbox-1", cause=ShellProviderError( 'request_failed (404): {"reason":"NOT_FOUND","message":"error: code = 400 reason = sandbox_expired message = sandbox has expired"}', code="request_failed", ), ) layer.runtime_state = _runtime_state(sandbox_id="stale-sandbox-1") async def scenario() -> None: with pytest.raises(SandboxExpiredError): async with layer.resource_context(): pass asyncio.run(scenario()) assert provider.attach_calls == ["stale-sandbox-1"] assert provider.create_calls == 0 def test_resource_context_reraises_non_expired_attach_error() -> None: layer, provider = _layer(commands=FakeCommands()) provider.attach_error = ShellProviderError("some other error", code="request_failed") layer.runtime_state = _runtime_state(sandbox_id="sandbox-1") async def scenario() -> None: async with layer.resource_context(): pass with pytest.raises(ShellProviderError, match="some other error"): asyncio.run(scenario()) # --------------------------------------------------------------------------- # Output redaction tests # --------------------------------------------------------------------------- def _layer_with_redaction( *, commands: FakeCommands, config: DifyShellLayerConfig | None = None, shell_redact_patterns: list[str] | None = None, token_value: str = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0.fake-long-jwe-token-value", ) -> tuple[DifyShellLayer, FakeProvider]: """Create a layer with agent_stub env injection and optional redaction patterns.""" provider = FakeProvider(resource=FakeResource(commands=commands)) layer = DifyShellLayer.from_config_with_settings( config or DifyShellLayerConfig(), shell_provider=provider, shell_home_root="/home", shell_redact_patterns=shell_redact_patterns, agent_stub_api_base_url="http://localhost:5050/agent-stub", agent_stub_token_factory=lambda execution_context, session_id: token_value, ) return layer, provider def test_redact_output_replaces_jwe_token_value() -> None: """The JWE token value should always be redacted from shell output.""" token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0.super-secret-token-12345" def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: return _command_result( "job-1", status="exited", done=True, exit_code=0, output=f"DIFY_AGENT_STUB_AUTH_JWE={token}\n", offset=100, ) commands = FakeCommands( run_handler=run_handler, tail_handler=lambda _: _command_result("job-1", done=True, exit_code=0, status="exited", offset=100), ) layer, _provider = _layer_with_redaction(commands=commands, token_value=token) _bind_execution_context(layer) layer.runtime_state = _runtime_state() tools = {tool.name: tool for tool in layer.tools} async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_run"].function_schema.call({"script": "env"}, None) # pyright: ignore[reportArgumentType] _, output = _parse_tagged_observation(result) assert token not in output assert "***" in output asyncio.run(scenario()) def test_redact_output_applies_server_level_patterns() -> None: """Server-level regex patterns from env var should redact matching content.""" def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: return _command_result( "job-1", status="exited", done=True, exit_code=0, output="api_key=sk-proj-abc123xyz\n", offset=30, ) commands = FakeCommands( run_handler=run_handler, tail_handler=lambda _: _command_result("job-1", done=True, exit_code=0, status="exited", offset=30), ) layer, _provider = _layer_with_redaction( commands=commands, shell_redact_patterns=[r"sk-proj-[A-Za-z0-9]+"], ) _bind_execution_context(layer) layer.runtime_state = _runtime_state() tools = {tool.name: tool for tool in layer.tools} async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_run"].function_schema.call({"script": "cat .env"}, None) # pyright: ignore[reportArgumentType] _, output = _parse_tagged_observation(result) assert "sk-proj-abc123xyz" not in output assert "api_key=***" in output asyncio.run(scenario()) def test_redact_output_applies_per_agent_config_patterns() -> None: """Per-agent redact_patterns from DifyShellLayerConfig should also apply.""" def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: return _command_result( "job-1", status="exited", done=True, exit_code=0, output="token: ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890\n", offset=50, ) commands = FakeCommands( run_handler=run_handler, tail_handler=lambda _: _command_result("job-1", done=True, exit_code=0, status="exited", offset=50), ) config = DifyShellLayerConfig(redact_patterns=[r"ghp_[A-Za-z0-9]{36}"]) layer, _provider = _layer_with_redaction(commands=commands, config=config) _bind_execution_context(layer) layer.runtime_state = _runtime_state() tools = {tool.name: tool for tool in layer.tools} async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_run"].function_schema.call({"script": "echo $TOKEN"}, None) # pyright: ignore[reportArgumentType] _, output = _parse_tagged_observation(result) assert "ghp_" not in output assert "token: ***" in output asyncio.run(scenario()) def test_redact_output_skips_short_jwe_values() -> None: """JWE values ≤8 chars should not be redacted to avoid false positives.""" def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> ShellCommandResult: return _command_result( "job-1", status="exited", done=True, exit_code=0, output="short\n", offset=6, ) commands = FakeCommands( run_handler=run_handler, tail_handler=lambda _: _command_result("job-1", done=True, exit_code=0, status="exited", offset=6), ) # Token value is short — should NOT be redacted even if it appears in output. layer, _provider = _layer_with_redaction(commands=commands, token_value="short") _bind_execution_context(layer) layer.runtime_state = _runtime_state() tools = {tool.name: tool for tool in layer.tools} async def scenario() -> None: async with layer.resource_context(): result = await tools["shell_run"].function_schema.call({"script": "echo hi"}, None) # pyright: ignore[reportArgumentType] _, output = _parse_tagged_observation(result) assert "short" in output asyncio.run(scenario())