File size: 5,048 Bytes
6a88274 a495d22 6a88274 a495d22 6a88274 c3b49d6 6a88274 c3b49d6 6a88274 c3b49d6 6a88274 a495d22 6a88274 a495d22 6a88274 c3b49d6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | """
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."""
...
# --------------------------------------------------------------------------- #
# Factory
# --------------------------------------------------------------------------- #
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":
# Imported here (not at module top) so the interface + factory stay
# import-light and free of any circular import with python_executor.
from .python_executor import PythonExecutor
return PythonExecutor()
if kind == "sandbox":
# Imported lazily so the factory stays import-light: pulling in the
# sandbox package (launchers, urllib) only happens when it's selected.
from .sandbox.executor import SandboxedExecutor
return SandboxedExecutor()
raise ValueError(f"Unknown EXECUTOR={kind!r}. Valid values: ['in_process', 'sandbox'].")
|