| """ |
| Executor seam β the stable interface generated code runs through. |
| |
| The agent is a CodeAct loop: the model emits code that gets executed in a |
| persistent, Jupyter-kernel-style namespace holding session state across steps. |
| Today that runs in-process via `PythonExecutor` (`python_executor.py`). ADR-0007 |
| ("Per-Session Sandboxed Code Execution") calls for swapping in a network-isolated, |
| per-session *sandboxed* executor behind exactly this interface β WITHOUT rewriting |
| the agent loop, which already depends only on the small surface below. |
| |
| This module extracts that seam (ADR-0007 Phase 0): an `Executor` Protocol capturing |
| the current `PythonExecutor` surface, plus a config-selected factory `get_executor()` |
| mirroring `get_log_sink()` in `src/logging_sink.py`. There is NO behavior change β |
| the default (`EXECUTOR=in_process`) still returns an in-process `PythonExecutor`. |
| |
| Selection is by the `EXECUTOR` env var (`in_process` | `sandbox`), default |
| `in_process`: |
| |
| EXECUTOR=in_process PythonExecutor() β runs code in this process (DEFAULT) |
| EXECUTOR=sandbox SandboxedExecutor() β per-session sandbox kernel (ADR-0007 |
| Phase 1). Runs code in an isolated kernel started by a |
| pluggable launcher (env `SANDBOX_LAUNCHER`, default |
| `container` for prod; set `subprocess` for dev/test). |
| |
| An unknown value raises ValueError (fail loud on misconfiguration, before any run). |
| |
| Sandbox env vars (all optional; sensible defaults) β mirrors the LOG_SINK style: |
| |
| SANDBOX_LAUNCHER subprocess | container (default: container) |
| SANDBOX_MCP_URL MCP HTTP server URL the in-kernel tool stubs dispatch to |
| (default: unset β tool stubs raise loudly if invoked) |
| SANDBOX_SESSION_ID logical session id (default: sess-<pid>) |
| SANDBOX_IMAGE sandbox container image (default: decouplerpy-sandbox:latest) |
| SANDBOX_KERNEL_PORT default kernel port (default: 8790; per-session a |
| free port is auto-picked by the launcher) |
| SANDBOX_HEALTH_TIMEOUT seconds to wait for /health after launch (default: 30) |
| """ |
|
|
| import os |
| from typing import Any, Protocol, runtime_checkable |
|
|
| __all__ = ["Executor", "get_executor"] |
|
|
|
|
| @runtime_checkable |
| class Executor(Protocol): |
| """Structural interface for a persistent code-execution environment. |
| |
| Any executor the agent uses must provide this surface. `PythonExecutor` |
| (in-process, the default) conforms to it today; a future `SandboxedExecutor` |
| (ADR-0007 Phase 1) will implement the same contract while proxying each call |
| to a Python kernel inside an isolated per-session container. Because the only |
| thing that crosses the boundary is captured stdout (a string), the interface |
| is trivially serializable over a socket/HTTP. |
| """ |
|
|
| def reset_environment(self) -> None: |
| """Reset the execution environment to a clean state.""" |
| ... |
|
|
| def send_functions(self, functions: dict[str, Any]) -> None: |
| """Inject tool functions into the execution namespace.""" |
| ... |
|
|
| def send_variables(self, variables: dict[str, Any]) -> None: |
| """Inject variables into the execution namespace.""" |
| ... |
|
|
| def execute(self, code: str) -> str: |
| """Execute code in the persistent namespace; return captured stdout.""" |
| ... |
|
|
| def __call__(self, code: str) -> str: |
| """Alias for `execute` β run `code`, return captured stdout.""" |
| ... |
|
|
|
|
| |
| |
| |
| def get_executor(kind: str = None) -> Executor: |
| """Return the configured Executor. |
| |
| `kind` overrides the `EXECUTOR` env var; default is `in_process`. An unknown |
| value raises ValueError (fail loud on misconfiguration, before any run). |
| |
| - `in_process` (DEFAULT) β `PythonExecutor()`, unchanged current behavior. |
| - `sandbox` β the ADR-0007 Phase 1 per-session `SandboxedExecutor`: a kernel |
| (subprocess in dev/test, container in prod) running behind this same |
| Executor interface. Launcher chosen by `SANDBOX_LAUNCHER` (default |
| `container`). See docs/adr/ADR-0007-sandboxed-code-execution.md. |
| """ |
| kind = (kind or os.environ.get("EXECUTOR", "in_process")).strip().lower() |
|
|
| if kind == "in_process": |
| |
| |
| from .python_executor import PythonExecutor |
|
|
| return PythonExecutor() |
|
|
| if kind == "sandbox": |
| |
| |
| from .sandbox.executor import SandboxedExecutor |
|
|
| return SandboxedExecutor() |
|
|
| raise ValueError(f"Unknown EXECUTOR={kind!r}. Valid values: ['in_process', 'sandbox'].") |
|
|