ADR 009: Agent Harness — Sandboxed Execution Runtime
- Status: Accepted (implementation deferred; design approved)
- Date: 2026-08-07
- Deciders: Software Architecture Team + Product Owner
Context and Problem Statement
Agents currently execute prompts and built-in skills inside the operator process. Evre 3 introduces action-oriented agents: coding agents that run commands and edit files, and conversational agents reached through external channels (WhatsApp, Telegram). Running arbitrary code or long-lived external sessions inside the operator pod is unsafe (no isolation, no resource caps, no cleanup) and operationally fragile.
Beyond execution, the platform needs observability and management: operators want to see what agents/sandboxes are running, browse historical execution logs, and manage agents (enable/disable features, add/edit agents, update workflows, human-in-the-loop approvals) driven by agent status.
The platform needs a harness: an execution runtime that spawns isolated, ephemeral, resource-bounded sandboxes on demand, a channel layer that connects external messaging endpoints to agents, and a management surface over both.
Decision Drivers
- Isolation: Agent code execution must not run in the operator process. Kernel-level isolation (Kubernetes Pod) is preferred over in-process exec.
- Ephemerality: Sandboxes are short-lived, TTL-bounded, and deleted after use (or idle timeout) — no long-running pet pods.
- Channel Agnosticism: WhatsApp and Telegram share one inbound message pipeline; more channels are adapter additions.
- On-Demand: No sandbox pod exists until an agent actually needs execution; cold start must be bounded.
- Management First: Monitoring (what is running, past logs) and management (enable/disable, add/edit agents, update workflows, HITL) are first-class platform concerns, not afterthoughts.
- Reuse: Existing
AiAgentCRD carries capability declarations; the harness is a platform service, not a per-agent deployment.
Considered Options
- In-process exec (
ProcessBuilder): Simple, but no isolation, no resource limits, no cleanup guarantees — rejected. - Ephemeral Kubernetes Pods per execution (chosen): Operator creates a
Job/barePodfrom an agent-selected image, streams commands via exec/attach, TTL-reaps it. Real isolation, native resource limits, natural cleanup. - Sidecar container in the operator pod: Persistent sandbox per operator, but couples operator lifetime to agent execution and complicates image/resource variance — rejected.
Decision Outcome
A Harness service in tuluat-engine (com.tuluat.engine.harness) with three layers: sandbox runtime, channel adapters, and a management/observability surface.
1. Sandbox Runtime — SandboxManager
- Namespace:
tuluat-sandbox(dedicated namespace, decided by product owner — keeps sandbox pods and their RBAC/NetworkPolicies separate fromtuluat-systemoperator control plane). SandboxSpec:image,command,cpu,memory,timeoutSeconds,ttlSeconds,volumeSize.SandboxSession start(SandboxSpec spec, String agentName):- Creates an ephemeral Pod in
tuluat-sandboxwith resource limits,restartPolicy: Never, and an emptyDir/PVC workdir. - Labels:
app.kubernetes.io/managed-by=tuluat-harness,tuluat.ai/agent=<name>,tuluat.ai/session=<uuid>. CommandResult exec(SandboxSession session, String command):- Streams command via Fabric8 exec with a
PipedInputStream/output. - Enforces
timeoutSeconds; kills on timeout. void terminate(SandboxSession session): deletes the Pod (graceful → force), best-effort.- Reaper:
@Scheduled(fixedDelay = 60s)scanstuluat.ai/session-labeled pods intuluat-sandbox, deletes any past TTL or idle timeout. Prevents leaked sandboxes. - Sandbox images (decided): both BYO and curated.
AiAgentSpec.harness.sandbox.imageallows bring-your-own images per agent.- A curated set (java, python, node, golang) is offered as shorthand via
imageRef: "python"resolving to a platform-managed registry image; exact tags/registry to be detailed later by the product owner.
2. Channel Layer — ChannelAdapter SPI
ChannelMessage inbound(ChannelEnvelope envelope)normalizes external messages into{ channel, channelUserId, text, metadata }.- Channels (decided): WhatsApp and Telegram first — no Twilio/Meta vendor lock-in decision yet; each adapter targets the official provider API.
WhatsAppChannelAdapter:POST /api/v1/channels/whatsapp/webhook(Spring MVC controller intuluat-app).- Parses WhatsApp Business Cloud API payload (
entry[].changes[].value.messages[]); echo 200 immediately, process async. - Verification handshake:
hub.mode=subscribe&hub.verify_token=...&hub.challenge=.... - Outbound replies via
POST {phone-number-id}/messageswith bearer token from envWHATSAPP_ACCESS_TOKEN/WHATSAPP_PHONE_NUMBER_ID. TelegramChannelAdapter:- Polls or webhook-receives updates from Bot API (
getUpdateslong-poll orsetWebhook). - Token from env
TELEGRAM_BOT_TOKEN; chat ids from update payload; replies viasendMessage. WebhookChannelAdapter(generic): any JSON webhook → same pipeline (test-friendly, WireMock-stubable).
3. Management & Observability Surface
Management is REST API first (tuluat-app controllers; a future Evre 3 web portal consumes the same API). Driven by agent status.
Observability
- What is running: extend
AiAgentStatuswith a runtime block: - Past logs: extend the existing
workflow_session_logsentity usage with agent execution logs (per agent, not only per session):agent_execution_logstable —agent_name,session_id,node_id,level,message,guardrail_result,created_at. REST:GET /api/v1/agents/{name}/logs?since=&limit=. - Metrics: existing Prometheus telemetry extended with
tuluat_agent_runs_total,tuluat_agent_guardrail_blocks_total,tuluat_sandbox_pods_active.
Management (by agent status)
- Enable/disable features:
AiAgentSpecalready carries optional blocks (guardrails,mcpServers,a2a,harness,skills). Management API patches the CRD spec: PATCH /api/v1/agents/{name}/features— toggle guardrails/mcp/a2a/harness enabled flags without full spec rewrite.- Reconcile on spec change is already handled by the operator.
- Add/edit agents:
POST /api/v1/agents/PUT /api/v1/agents/{name}— validated CRD create/update via the operator (Kubernetes API is the source of truth; controller proxies with validation). - Update workflows:
PUT /api/v1/workflows/{name}— patchAiWorkflowSpec(nodes/edges); operator re-reconciles. - Human-in-the-loop: existing
POST /api/v1/sessions/{id}/approveretained; approval inbox API lists sessions awaiting approval: GET /api/v1/approvals?status=PENDING→ session, currentNode, input, requestedAt.- Approve/reject per session; results feed Temporal
ApprovalSignal. - Status-driven filtering: list endpoints accept
?status=filters (GET /api/v1/agents?status=Failed,GET /api/v1/sessions?status=RUNNING) so the portal can render dashboards per state.
4. Agent Binding — AiAgentSpec.harness (new optional block)
spec:
harness:
enabled: true
type: "CODE" # CODE | CHANNEL | BOTH
sandbox:
imageRef: "python" # curated shorthand OR
image: "myorg/custom-runner:1.0" # bring-your-own
cpu: "500m"
memory: "512Mi"
timeoutSeconds: 300
ttlSeconds: 600
channels:
- type: "WHATSAPP"
phoneNumber: "+15551234567"
verifyTokenSecretRef: { name: "whatsapp-secret", key: "verify-token" }
- type: "TELEGRAM"
botTokenSecretRef: { name: "telegram-secret", key: "bot-token" }
CODE: agent may request a sandbox via executeShell(command) tool; output returns to the LLM loop.
* CHANNEL: webhook routes inbound messages to the agent's chat loop; replies sent back through the adapter.
* Harness is a capability declaration — pods are created on demand, never pre-provisioned.
* Interaction surface (decided): chat/API only — no embedded terminal in Evre 3 portal; sandbox interaction happens through the agent's chat loop and REST API.
5. Security Model
- Sandbox pods live in
tuluat-sandboxwith a dedicatedServiceAccount(no cluster permissions,automountServiceAccountToken: false), restrictiveSecurityContext(runAsNonRoot, readOnlyRootFilesystem, no privileged). - Outbound network: default allow for model/API access; optional
NetworkPolicyegress-limited per sandbox. - Secrets:
verifyTokenSecretRef,botTokenSecretRef,WHATSAPP_ACCESS_TOKENvia KustomizesecretGenerator, never in manifests. - Per-agent concurrent sandbox cap (default 1) enforced by
SandboxManager.
6. Out of Scope (this ADR)
- WhatsApp/Telegram provider onboarding/approval (Meta/Telegram-side).
- Persistent dev containers / attachable IDEs (interaction is chat/API).
- Windows/GPU sandbox images (later).
- Exact curated image tag/registry matrix (deferred — product owner will detail).
Positives
- Real isolation with Kubernetes-native resource limits and TTL cleanup in a dedicated namespace.
- Uniform channel pipeline — WhatsApp and Telegram are two adapters over one SPI.
- Management/observability are first-class — status-driven APIs cover enable/disable, add/edit, workflow updates, HITL inbox.
- On-demand economics — zero sandbox pods when no agent executes code.
- Testable with Fabric8 KubernetesMockServer for operator tests; WireMock for webhook tests.
Negatives
- Sandbox cold start (image pull) adds latency to first code execution.
- Operator's ServiceAccount needs RBAC to create pods in
tuluat-sandbox. kubectl execstreaming adds complexity vs in-process exec.- Channel providers (Meta/Telegram) impose their own onboarding and rate limits.
Resolved Questions
- Namespace:
tuluat-sandbox(dedicated) — decided. - Channels: WhatsApp + Telegram first, official provider APIs — decided.
- Sandbox images: both BYO (
sandbox.image) and curated shorthand (sandbox.imageRef) — decided; exact image matrix deferred. - Coding agent UI: chat/API only, no embedded terminal — decided.
Open Questions (for later detailing)
- Curated image registry/tag matrix (
imageRef: java|python|node|golang→ concrete images). - Telegram transport: long-poll vs webhook (depends on cluster ingress availability).
tuluat-sandboxRBAC policy details (which agents may create sandboxes, namespace-level quotas).- Agent execution log retention period and rotation.