- Status: proposed
- Date: 2026-02-16
Recent traces exposed a structural runtime flaw:
- Generated/persisted code is executed via
eval(..., binding)on the Agent execution context. - Tool code can define methods (
def fetch_headlines) that escape the call and pollute later method lookup. - Polluted lookup can bypass normal dynamic-dispatch lanes (
method_missing-> lifecycle/observability/contract validation), producing inconsistent delegation traces. - This failure is not a prompt-quality issue; it is execution-surface leakage.
Observed symptom class:
- A call path appears to delegate and returns
Outcome.ok. - Subsequent calls show missing depth-1 delegated traces for equivalent capability execution.
- Behavior remains superficially successful while lifecycle invariants are violated.
This conflicts with project tenets:
- Agent-first mental model: Tool behavior must flow through the runtime lifecycle.
- Runtime ergonomics and clarity: execution boundaries should be explicit and reliable.
- Tolerant interfaces by default: runtime should tolerate idiomatic generated Ruby without leaking side effects across calls.
- Ubiquitous language: Tool Builder/Tool/Worker lanes must remain mechanically distinct in execution.
Adopt per-attempt execution sandbox isolation for generated code.
Generated code executes against an ephemeral sandbox object per attempt, not directly on Agent binding.
The sandbox owns attempt-local runtime variables:
contextargskwargsresult
The sandbox is discarded after attempt completion.
Sandbox forwards only the supported runtime surface to Agent:
tool(...)delegate(...)remember(...)memory(read access)Agent::Outcomeconstruction access
No implicit full-Agent self exposure is provided.
def remains allowed in generated code.
Method definitions become sandbox-local for that attempt and do not mutate Agent method space.
result/return semantics remain unchanged from caller perspective.
Pre-execution guardrails continue rejecting direct class/module mutation patterns against runtime host objects (for example class Agent, class << Agent, Module#class_eval against Agent).
Such violations stay in recoverable validation lanes (ADR 0016), with bounded retry feedback.
Sandbox execution is first-class in logs and preserves existing lifecycle invariants:
- delegated call boundaries remain visible,
- contract validation boundaries stay intact,
- guardrail/execution/outcome repair lanes remain deterministic.
Rollout logs include execution_receiver (legacy | sandbox) so trace analysis can verify delegation-fidelity improvements before removing legacy execution.
In scope:
- runtime execution receiver change for generated code,
- sandbox API forwarding contract,
- regression coverage for method-leak prevention,
- observability updates needed to verify sandbox path behavior.
Out of scope:
- prompt redesign,
- tool naming/decomposition policy changes,
- model tiering or recursion primitives.
- Eliminates cross-call method pollution from generated code.
- Restores deterministic dispatch through runtime lifecycle boundaries.
- Keeps generated Ruby ergonomic (
defsupported) without global side effects. - Reduces hidden divergence between apparent success and lifecycle correctness.
- Structural runtime refactor around execution context.
- Generated code that depended on broad Agent
selfsurface will fail fast and require regeneration/repair. - Sandbox-forward surface must stay explicit and documented as runtime evolves.
- Ban
defin generated code.- Rejected: non-idiomatic Ruby restriction; addresses symptom, not execution-boundary cause.
- Keep Agent binding and patch prompt warnings only.
- Rejected: does not provide mechanical isolation; leakage remains possible.
- Keep Agent binding and add post-hoc cleanup of leaked methods.
- Rejected: brittle, incomplete, and order-dependent.
- Expand host object allowlist broadly (
selfas Agent with more methods).- Rejected: increases accidental coupling and leakage risk.
- Introduce internal sandbox object and execute generated code against sandbox receiver.
- Keep call-local
context/args/kwargs/resultbehavior consistent with current contract. - Emit
execution_receiver: legacy|sandboxin call logs during rollout.
- Implement explicit forwarding of
tool/delegate/remember/memory. - Add tests for allowed API calls through sandbox.
- Add regression test for method-definition leakage (define in one call, ensure no cross-call host mutation).
- Add trace-level test ensuring delegated depth-1 calls remain visible across repeated ask scenarios.
- Ensure guardrail classification for host-mutation patterns remains recoverable where appropriate.
- Keep prompt API references aligned with sandbox-forwarded surface only.
- Execution isolation must not bypass existing validation/repair lanes.
- Sandbox must not expose arbitrary host internals.
- Lifecycle logging must remain complete for both top-level and delegated calls.
- Backward compatibility with leaked-method behavior is intentionally not preserved.
- ADR 0016 governs validation-first retries and transactional guardrail recovery; this ADR fixes execution receiver integrity inside that lifecycle.
- ADR 0014 contract validation remains the outcome boundary invariant.
- ADR 0012 persistence/repair remains unchanged; persisted artifacts now execute in an isolated receiver.
- ADR 0019 structured conversation-history access remains unchanged;
context[:conversation_history]stays available through sandboxcontext.
- What is the minimal long-term forwarded API surface for sandboxed code?
- When should
execution_receiverlogging droplegacysupport and become sandbox-only? - Should sandbox enforce additional static restrictions beyond existing guardrail policy, or rely solely on current policy lanes?