adk-code 2.3.0

Code execution substrate for ADK-Rust — typed executor abstraction, sandbox policy model, and built-in execution backends
Documentation

adk-code

Language-aware code execution toolchain for ADK-Rust.

Crates.io Documentation License

Overview

adk-code handles compilation, diagnostics, and language-specific pipelines. It delegates actual execution to adk-sandbox backends, cleanly separating language toolchains from isolation.

The crate provides:

  • Typed executor abstraction (CodeExecutor trait) with lifecycle management
  • Sandbox policy model (SandboxPolicy, BackendCapabilities) with fail-closed validation
  • Rust-first code execution via RustExecutor (check → build → delegate) and legacy RustSandboxExecutor
  • Embedded JavaScript execution via EmbeddedJsExecutor (boa_engine, embedded-js feature)
  • Embedded Python execution via MontyOneShotExecutor / MontyReplExecutor (Pydantic Monty, embedded-python feature)
  • WASM guest module execution via WasmGuestExecutor (phase 1 placeholder)
  • Docker container execution via DockerExecutor (persistent, docker feature) and ContainerCommandExecutor (ephemeral, always available)
  • Vertex AI Agent Engine managed sandboxes via VertexSandboxClient / SandboxCodeExecutor (vertex-sandbox feature)
  • CodeTool implementing adk_core::Tool for LLM agent integration
  • Structured Rust compiler diagnostics parsing
  • Workspace abstraction for multi-agent collaborative project builds
  • A2A-compatible collaboration transport layer

Architecture

Agent → CodeTool (adk-code)
            │
            ▼
       RustExecutor
       check → build → delegate
            │
            ▼
       SandboxBackend (adk-sandbox)
       ProcessBackend / WasmBackend

The RustExecutor pipeline:

  1. Check — rustc --error-format=json → parse structured diagnostics → halt on errors
  2. Build — compile to binary using the harness template
  3. Execute — delegate to a SandboxBackend via ExecRequest

Quick Start

use adk_code::{CodeTool, RustExecutor, RustExecutorConfig};
use adk_sandbox::ProcessBackend;
use std::sync::Arc;

let backend = Arc::new(ProcessBackend::default());
let executor = RustExecutor::new(backend, RustExecutorConfig::default());
let tool = CodeTool::new(executor);

// Use with any LLM agent
let agent = LlmAgentBuilder::new("code_agent")
    .instruction("Execute Rust code when asked to compute something.")
    .tool(Arc::new(tool))
    .build()?;

User code must provide fn run(input: serde_json::Value) -> serde_json::Value. The harness wraps it with fn main(), stdin parsing, and stdout serialization.

Feature Flags

Feature Description Default
(none) Core types, RustExecutor, RustSandboxExecutor, ContainerCommandExecutor, WasmGuestExecutor, CodeTool, Workspace ✅
embedded-js EmbeddedJsExecutor via boa_engine ❌
embedded-python MontyOneShotExecutor / MontyReplExecutor via the Monty interpreter ❌
docker DockerExecutor via bollard (persistent Docker containers) ❌
vertex-sandbox VertexSandboxClient / SandboxCodeExecutor / VertexSandboxTool — Vertex AI Agent Engine managed sandboxes ❌

Execution Backends

Backend Matrix

Backend Isolation Timeout Network Filesystem Environment Persistent
RustSandboxExecutor HostLocal ✅ ❌ ❌ ✅ ❌
RustExecutor Delegated ✅ Delegated Delegated Delegated ❌
EmbeddedJsExecutor InProcess ✅† ✅* ✅* ✅* ❌
MontyOneShotExecutor InProcess ✅ ✅* ✅ ✅ ❌
MontyReplExecutor InProcess ✅ ✅* ✅ ✅ ✅
WasmGuestExecutor InProcess ✅ ✅* ✅* ✅* ❌
ContainerCommandExecutor ContainerEphemeral ✅ ✅ ✅ ✅ ❌
DockerExecutor ContainerPersistent ✅ ✅ ✅ ✅ ✅

*Enforcement by omission — the engine has no APIs for these operations.

†The deadline is enforced on the result; runtime limits stop the interpreter. See EmbeddedJsExecutor.

validate_policy() rejects every request on a backend that reports enforce_environment_policy: false: both EnvironmentPolicy::None and EnvironmentPolicy::AllowList restrict what the code sees, and such a backend passes the host environment through.

RustSandboxExecutor (legacy)

Host-local Rust compilation and execution. Compiles with rustc, runs the binary as a child process. Enforces timeouts (including stdin delivery), output truncation, and environment restrictions, but not network or filesystem restrictions.

Process Environment
rustc Cleared, plus the toolchain allowlist from ProcessBackend::toolchain_env() (PATH, HOME, RUSTUP_HOME, CARGO_HOME, RUSTUP_TOOLCHAIN, TMPDIR, and on Windows LIB, SystemRoot, and similar)
Compiled binary Cleared, plus the variables named by EnvironmentPolicy::AllowList (and SystemRoot on Windows)

env! and option_env! in user code therefore cannot embed host credentials into the binary, and std::env::var sees only allowlisted variables.

use adk_code::{RustSandboxExecutor, RustSandboxConfig, CodeExecutor};

let executor = RustSandboxExecutor::new(RustSandboxConfig {
    rustc_path: "rustc".to_string(),
    rustc_flags: vec![],
    serde_json_path: None,
});

RustExecutor (new)

Separates compilation from isolation by delegating execution to a SandboxBackend. The check → build → execute pipeline provides structured diagnostics.

use adk_code::{RustExecutor, RustExecutorConfig};
use adk_sandbox::ProcessBackend;
use std::sync::Arc;

let backend = Arc::new(ProcessBackend::default());
let executor = RustExecutor::new(backend, RustExecutorConfig {
    rustc_path: "rustc".to_string(),
    serde_json_path: None,
    rustc_flags: vec![],
});
Step Where it runs Environment
Check, build Inside the backend when it reports filesystem_read_isolation and supports Language::Command (Unix only); otherwise on the host Cleared, plus the toolchain allowlist
Execute Inside the backend Whatever the backend exposes; ProcessBackend clears it

Compile-time file access such as include_str! follows wherever rustc runs. rustc runs on the host only when the backend does not confine reads, so it reaches no file the binary could not also read at run time. On Windows, rustc always runs on the host.

RustExecutor::execute() returns a CodeResult with:

  • exec_result — sandbox execution result (stdout, stderr, exit_code, duration)
  • diagnostics — compiler warnings from the check step
  • output — structured JSON extracted from the last stdout line
  • display_stdout — everything before the structured output line

EmbeddedJsExecutor (embedded-js feature)

In-process JavaScript execution via boa_engine. Useful for lightweight transforms and deterministic state shaping. No network/filesystem/environment APIs available (enforcement by omission).

use adk_code::{EmbeddedJsExecutor, CodeExecutor, ExecutionRequest,
    ExecutionLanguage, ExecutionPayload, SandboxPolicy};

let executor = EmbeddedJsExecutor::new();
let request = ExecutionRequest {
    language: ExecutionLanguage::JavaScript,
    payload: ExecutionPayload::Source {
        code: "return input.x + 1;".to_string(),
    },
    argv: vec![],
    stdin: None,
    input: Some(serde_json::json!({ "x": 41 })),
    sandbox: SandboxPolicy::strict_js(), // 5-second timeout
    identity: None,
};

User code is wrapped in an IIFE so return works. Input is injected as a global input variable. Return value is converted to JSON.

Each evaluation runs on its own thread, and execute() returns ExecutionStatus::Timeout once the policy timeout passes. Boa cannot be preempted, so an abandoned evaluation runs until it finishes or reaches a runtime limit:

Limit Value
Loop iterations per call frame 10,000,000
Call depth 512
VM stack values 10,240

Runtime-limit errors cannot be caught by try/catch; a limit reached before the deadline yields ExecutionStatus::Failed. Loop limits count per call frame, work inside a single native builtin (such as a pathological regular expression) is not counted, and there is no heap limit.

Monty executors (embedded-python feature)

In-process Python execution via the Pydantic Monty interpreter — no container, no subprocess, microsecond startup. One builder produces two products: build_one_shot() runs each call in a fresh interpreter, build_repl() persists interpreter state (variables, functions, imports) across calls.

Every OS call Monty can emit (filesystem, os.getenv/os.environ, datetime.now()/date.today()) is serviced against grants the host authors at construction; ungranted access raises a catchable in-script OSError. Monty has no network or subprocess surface at all. Registered host functions become callable Python functions, and both executors describe their built environment through CodeExecutor::prompt_snippet().

use adk_code::{MontyExecutorBuilder, PathAccess};
use serde_json::json;

let builder = MontyExecutorBuilder::new()
    .allow_path("/data", "/srv/agent/data", PathAccess::ReadOnly)
    .allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
    .environ_var("PROJECT", "acme")
    .system_clock()
    .function_fn("row_count", "Count rows in the loaded dataset.", |args, _kwargs| async move {
        Ok(json!(args.len()))
    });

let one_shot = builder.clone().build_one_shot()?;   // fresh interpreter per call
let repl = builder.build_repl()?;                   // state persists across calls

The per-request SandboxPolicy may only narrow within the builder's grants; a request exceeding them is rejected fail-closed before any code runs. The value of the script's final expression becomes ExecutionResult::output; print() output is captured as stdout.

The embedded_python module is also the workspace's shared Monty integration kernel: it exposes the JSON↔Monty conversion (json_to_monty / monty_to_json), the OS-call servicing function (resolve_os_call), PathAccess, and re-exports of the monty / monty-types / monty-fs crates — so the Monty release is pinned exactly once, here. adk-codeact-monty builds its CodeRuntime on this kernel.

WasmGuestExecutor

Executes precompiled .wasm guest modules. Phase 1 is a placeholder that validates module format (magic number, minimum size) but does not execute. Full runtime integration is deferred.

use adk_code::{WasmGuestExecutor, WasmGuestConfig};

let executor = WasmGuestExecutor::with_config(WasmGuestConfig {
    max_memory_bytes: 64 * 1024 * 1024, // 64 MB
    max_fuel: Some(1_000_000_000),       // 1B instructions
});

Accepts only ExecutionPayload::GuestModule with GuestModuleFormat::Wasm. Source payloads are rejected with a descriptive error pointing to EmbeddedJsExecutor or ContainerCommandExecutor.

ContainerCommandExecutor (always available)

Shells out to docker run (or podman) for each execution. Each call spawns a new ephemeral container. Simpler but less efficient than DockerExecutor.

use adk_code::{ContainerCommandExecutor, ContainerConfig};

let executor = ContainerCommandExecutor::new(ContainerConfig {
    runtime: "docker".to_string(),
    default_image: "python:3.12-slim".to_string(),
    extra_flags: vec![],
    auto_remove: true,
});

Supports Python, JavaScript, and Command languages. Enforces network policy via --network=none, filesystem via bind mounts, and environment via --env.

DockerExecutor (docker feature)

Persistent Docker container that survives across multiple execute() calls. Uses bollard to manage the container lifecycle via the Docker API.

use adk_code::{DockerExecutor, DockerConfig, CodeExecutor};

let executor = DockerExecutor::new(
    DockerConfig::python()
        .pip_install(&["numpy", "pandas"])
        .with_network()
)?;
executor.start().await?;

// Multiple executions reuse the same container
let result1 = executor.execute(request1).await?;
let result2 = executor.execute(request2).await?;

executor.cleanup().await?; // Prefer explicit cleanup over Drop

Presets: DockerConfig::python(), DockerConfig::node(), DockerConfig::custom("image").

Builder methods: setup_command(), pip_install(), npm_install(), with_network(), bind_mount(), env().

Lifecycle: start() → execute() (reusable) → stop() / cleanup(). Set auto_start: true (default) to start on first execute.

The container's network mode, bind mounts, and environment are fixed at creation, so execute() checks each request's SandboxPolicy against the DockerConfig with DockerConfig::validate_sandbox_policy() and rejects a stricter request with ExecutionError::UnsupportedPolicy:

Policy Satisfied when
NetworkPolicy::Disabled The config does not call with_network()
FilesystemPolicy::None There are no bind mounts
FilesystemPolicy::WorkspaceReadOnly { root } Every bind mount is :ro and its host path lies under root
FilesystemPolicy::WorkspaceReadWrite { root } Every bind mount's host path lies under root
FilesystemPolicy::Paths { read_only, read_write } Every bind mount lies under a read_write path, or is :ro and lies under a read_only path
EnvironmentPolicy::None The config sets no env() variables
EnvironmentPolicy::AllowList(names) Every env() variable is in names; the named host variables are also passed to the exec

The networked container in the example above therefore needs requests built with NetworkPolicy::Enabled; SandboxPolicy::strict_rust() disables the network and is rejected.

Vertex AI Agent Engine sandboxes (vertex-sandbox feature)

A client for the Agent Engine sandboxEnvironments surface (v1beta1) — fully managed, isolated code-execution sandboxes under a reasoning engine. Built on the shared adk-gcp plumbing (ADC credential caching, bounded transport, LRO polling, scope validation).

use adk_code::vertex_sandbox::{
    CreateSandboxRequest, SandboxCodeExecutor, VertexSandboxClient, VertexSandboxConfig,
    VertexSandboxTool,
};
use std::sync::Arc;

let client = Arc::new(VertexSandboxClient::new_with_adc(
    VertexSandboxConfig::new("my-project", "us-central1"),
)?);

// Direct: create, execute, delete.
let sandbox = client.create_sandbox("4242", CreateSandboxRequest::new("my-sandbox")).await?;
let name = sandbox.name.unwrap();
let result = client.execute_code(&name, "print('hello')", &[]).await?;
println!("{}", result.stdout);
client.delete_sandbox(&name).await?;

// Managed: per-session lazy creation with recreate-on-not-running semantics
// (adk-python AgentEngineSandboxCodeExecutor parity), plus an agent tool.
let executor = Arc::new(SandboxCodeExecutor::for_engine(client, "4242"));
let tool = VertexSandboxTool::new(executor);
  • create_sandbox / delete_sandbox wait their long-running operations; get_sandbox / list_sandboxes are plain reads; :execute is synchronous.
  • execute_code implements the chunk conventions shared with adk-python and the Vertex AI SDK: a JSON code chunk, file_name-attributed file chunks, and msg_out/msg_err console output.
  • Files are limited to 100 MB per request (rejected before sending) and per response. Every :execute call resets the sandbox TTL server-side.

CodeTool

CodeTool implements adk_core::Tool (name: code_exec) and dispatches to RustExecutor. Errors are returned as structured JSON, never as ToolError.

Parameters schema:

  • language — "rust" (default, only supported value in phase 1)
  • code — Rust source code (required)
  • input — optional JSON input passed to run()
  • timeout_secs — 1–300, default 30

Required scopes: code:execute, code:execute:rust

Structured Diagnostics

Compile errors include parsed RustDiagnostic structs with level, message, spans, and error codes:

{
  "status": "compile_error",
  "diagnostics": [
    {
      "level": "error",
      "message": "expected `;`",
      "code": "E0308",
      "spans": [{ "file_name": "main.rs", "line_start": 3, "column_start": 15 }]
    }
  ]
}

Use parse_diagnostics(stderr) to parse rustc --error-format=json output into Vec<RustDiagnostic>.

Sandbox Policy Model

SandboxPolicy describes requested execution constraints. BackendCapabilities describes what a backend can enforce. validate_policy() and validate_request() implement fail-closed semantics — execution is rejected before user code runs if the backend cannot enforce a requested control.

Preset policies:

  • SandboxPolicy::strict_rust() — no network, no filesystem, no env, 30s timeout, 1 MB limits
  • SandboxPolicy::host_local() — network allowed (host-local can't restrict), 30s timeout
  • SandboxPolicy::strict_js() — same as strict_rust but 5s timeout

Harness

The harness template (HARNESS_TEMPLATE) wraps user code:

  • Injects use serde_json::Value;
  • Provides fn main() that reads JSON from stdin, calls run(), prints JSON to stdout
  • Only serde_json is available as an external crate; full std library is available

Source validation (validate_rust_source()) rejects:

  • fn main — harness provides it
  • #![...] — crate-level attributes not supported

Comment stripping (strip_comments()) reduces false positives from patterns in comments.

Workspace

Workspace is a shared project context for multi-agent collaborative code generation. Specialist agents coordinate through typed CollaborationEvents with ownership, correlation, and wait/resume semantics.

use adk_code::{Workspace, CollaborationEventKind};
use std::time::Duration;

let ws = Workspace::new("./my-project")
    .project_name("my-project")
    .session_id("session-123")
    .build();

// Request work from another specialist
ws.request_work("corr-1", "api-routes", "frontend_engineer");

// Publish completed work
ws.publish_work("corr-1", "api-routes", "backend_engineer",
    serde_json::json!({ "routes": ["/api/users"] }));

// Wait for correlated response
let result = ws.wait_for_work("corr-1", Duration::from_secs(5)).await;

Convenience methods on Workspace:

Method Event Kind
request_work() NeedWork
claim_work() WorkClaimed
publish_work() WorkPublished
request_feedback() FeedbackRequested
provide_feedback() FeedbackProvided
signal_blocked() Blocked
signal_completed() Completed

Wait methods: wait_for(correlation_id, timeout), wait_for_work(), wait_for_feedback(), wait_for_kind(correlation_id, kind, timeout).

The WorkspaceBuilder supports project_name(), session_id(), created_at(), and channel_capacity() (default 256).

A2A Collaboration Transport

The a2a_compat module provides a CollaborationTransport trait abstracting the event transport layer. Phase 1 uses LocalTransport (in-process broadcast channel). The event model maps cleanly onto the ADK A2A protocol for future remote specialist execution:

Collaboration Concept A2A Concept
CollaborationEvent A2A Message or TaskStatusUpdateEvent
correlation_id A2A task_id
producer / consumer A2A agent card sender / receiver
NeedWork Submitted
WorkClaimed Working
WorkPublished Completed + artifact
FeedbackRequested InputRequired
Completed Completed (final)

Error Types

Two error enums:

ExecutionError — legacy executor errors:

Variant Description
UnsupportedPolicy Backend cannot enforce a requested sandbox control
UnsupportedLanguage Backend does not support the requested language
CompileFailed Compilation failed
Timeout Execution exceeded timeout
ExecutionFailed Runtime failure
Rejected Rejected before running (policy/scope check)
InvalidRequest Malformed request
InternalError Thread panic or unexpected failure

CodeError — new pipeline errors with structured diagnostics:

Variant Description
CompileError Compilation errors with Vec<RustDiagnostic> and raw stderr
DependencyNotFound Required dependency (e.g., serde_json) not found
Sandbox Underlying SandboxError from the backend
InvalidCode Source code invalid before compilation

Both implement From<...> for AdkError with appropriate component/category/code mappings.

Migration from Previous API

The previous adk-code API (CodeExecutor, ExecutionRequest, RustSandboxExecutor, etc.) is deprecated. See the compat module for the full migration table.

Old Type (deprecated) New Type Crate
CodeExecutor SandboxBackend adk-sandbox
ExecutionRequest ExecRequest adk-sandbox
ExecutionResult ExecResult adk-sandbox
RustSandboxExecutor RustExecutor adk-code
RustSandboxConfig RustExecutorConfig adk-code
RustCodeTool (adk-tool) CodeTool adk-code

Key API changes:

  • SandboxBackend has no lifecycle methods — just execute(ExecRequest)
  • ExecRequest is flat: language, code, stdin, timeout, memory_limit_mb, env
  • RustExecutor::new(backend, config) takes a SandboxBackend instead of embedding isolation
  • Deprecated aliases compile with warnings for one release cycle (removed in v0.6.0)

License

Apache-2.0