Module 6 · Tools and Agents

Sandboxing and Human Approval

Lesson 18 of 24 12 min read Updated

Sandboxing and human approval let a generative AI agent do useful work while keeping authority smaller than its reasoning ability. The model may read instructions, inspect files, run calculations, browse selected resources, or draft an external action, but the surrounding system decides where that activity runs and whether any side effect is allowed. The outcome is an agent that can propose, test, and explain, yet cannot silently delete data, spend money, leak credentials, or contact people without a deliberate grant.

In the Tools and Agents part of this course, this is the control layer that makes tool use practical. A tool-calling model is not just producing text; it is asking your application to operate computers and services on its behalf. Sandboxing narrows what those computers and services can reach. Human approval pauses high-impact operations until a person can inspect the proposed command, inputs, target account, and expected effect.

Purpose and outcome

A sandbox is an execution environment with boundaries around files, processes, credentials, network access, time, memory, and sometimes operating-system calls. The agent can use it as a workbench: parse a CSV, run tests, render a small script, or fetch documentation from an allowlisted host. Human approval is a workflow state, not a vague instruction. The system records a proposed action, shows the relevant context to a reviewer, accepts an approve or reject decision, and only then gives the executor permission to perform the side effect.

The important separation is between cognition and authority. The model may infer that a refund should be issued, but the refund tool should require a signed approval token. The model may write a shell command, but the sandbox runner should validate the executable, mount only the permitted directory, set a timeout, and remove network access unless that network is necessary.

How the mechanism works

A typical agent request passes through five layers. First, the planner produces an intended tool call such as run_python, fetch_url, or send_email. Second, a policy engine classifies the action by side effect, data sensitivity, destination, cost, and reversibility. Third, a sandbox broker creates an environment with the matching restrictions. Fourth, the tool runner executes the action and returns bounded output. Fifth, if the action is high impact, an approval service stores the proposal and waits for a human decision before execution.

Sandbox internals depend on the platform, but the ideas are stable. File isolation usually means a temporary working directory or a container volume mounted read-only or read-write according to the task. Process isolation means the agent cannot see or signal host processes. Network isolation can be a total block, an allowlist, or a proxy that logs and filters requests. Credential isolation means the environment starts with no ambient secrets; each tool receives only a scoped token when policy allows it. Resource isolation uses CPU, memory, disk, output, and wall-clock limits so accidental infinite loops and oversized outputs do not become incidents.

Approval internals are also concrete. An approval request should contain the exact operation, target resource, normalized parameters, requester identity, generated explanation, risk category, expiration time, and an idempotency key. The executor should check that the approval is still valid, was granted by an eligible reviewer, matches the exact action being executed, and has not already been consumed. Approval of a summary alone is weak; approval of a structured operation is enforceable.

Policy anatomy

Policy is usually expressed as a small decision table or ruleset. Read-only actions on non-sensitive workspace data can run automatically in a sandbox. Network calls to unknown hosts are denied or routed for review. Writes, sends, deletes, purchases, permission changes, and actions involving regulated or customer data require approval. The policy output should be one of a few states: allow, allow in sandbox, require approval, or deny. That limited vocabulary keeps the rest of the system testable.

Tool schemas should make risk visible. A send_email tool should have fields for recipients, subject, body, attachments, and account. A run_command tool should receive an argument list rather than a raw shell string where possible. A fetch_url tool should expose the destination host for policy checks before any request leaves the sandbox.

Example 1: classify an agent action

This first example turns a vague safety rule into a deterministic decision. It allows read-only workspace work, requires approval for writes and sends, and denies unknown external network activity.

from dataclasses import dataclass

@dataclass(frozen=True)
class Action:
    name: str
    side_effect: str
    data_scope: str
    estimated_cost_usd: float = 0.0

def decision_for(action: Action) -> str:
    if action.side_effect == "none" and action.data_scope in {"public", "workspace"}:
        return "allow_in_sandbox"
    if action.side_effect in {"write", "send", "delete"}:
        return "needs_human_approval"
    if action.estimated_cost_usd > 1.00:
        return "needs_human_approval"
    return "deny"

actions = [
    Action("summarize_pdf", "none", "workspace"),
    Action("send_refund_email", "send", "customer"),
    Action("open_unknown_url", "network", "external"),
]

for action in actions:
    print(action.name, decision_for(action))

The expected output is that summarize_pdf can run in the sandbox, send_refund_email needs approval, and open_unknown_url is denied. This is intentionally small, but it shows the main design habit: classify the requested capability before any tool runs.

Example 2: enforce the sandbox boundary

Classification is not enough. The runner must enforce the decision even if the model asks nicely, retries, or hides the operation in a longer plan. The next example simulates command and URL enforcement.

from dataclasses import dataclass
from urllib.parse import urlparse

@dataclass(frozen=True)
class SandboxPolicy:
    allowed_commands: frozenset[str]
    allowed_hosts: frozenset[str]
    max_seconds: int

class PolicyError(Exception):
    pass

def run_command(command: list[str], policy: SandboxPolicy) -> str:
    executable = command[0]
    if executable not in policy.allowed_commands:
        raise PolicyError(f"blocked command: {executable}")
    return f"ran {executable} with timeout {policy.max_seconds}s"

def fetch_url(url: str, policy: SandboxPolicy) -> str:
    host = urlparse(url).hostname or ""
    if host not in policy.allowed_hosts:
        raise PolicyError(f"blocked host: {host}")
    return f"fetched {host}"

policy = SandboxPolicy(
    allowed_commands=frozenset({"python", "pytest"}),
    allowed_hosts=frozenset({"docs.example.com"}),
    max_seconds=10,
)

print(run_command(["python", "check.py"], policy))
try:
    print(fetch_url("https://api.payment.example/refunds", policy))
except PolicyError as exc:
    print(exc)

The first call is allowed because python is an approved executable. The second call is blocked because the host is outside the allowlist. In a real implementation, this logic would sit in front of containers, subprocess APIs, or a network proxy, but the invariant is the same: policy is checked by deterministic code, not by the model’s self-restraint.

Example 3: consume approval before execution

The final example models the approval lifecycle. The request starts as proposed, a separate reviewer approves it, and execution is allowed only once the state changes.

from dataclasses import dataclass
from enum import Enum

class State(str, Enum):
    PROPOSED = "proposed"
    APPROVED = "approved"
    EXECUTED = "executed"
    REJECTED = "rejected"

@dataclass
class ApprovalRequest:
    request_id: str
    actor: str
    action: str
    summary: str
    state: State = State.PROPOSED

def approve(request: ApprovalRequest, reviewer: str) -> None:
    if reviewer == request.actor:
        raise ValueError("requester cannot approve their own action")
    if request.state is not State.PROPOSED:
        raise ValueError("only proposed requests can be approved")
    request.state = State.APPROVED

def execute(request: ApprovalRequest) -> str:
    if request.state is not State.APPROVED:
        raise ValueError("approval required before execution")
    request.state = State.EXECUTED
    return f"executed {request.action}"

request = ApprovalRequest(
    request_id="req-42",
    actor="agent",
    action="create_calendar_invites",
    summary="Invite three approved attendees to the project review.",
)

approve(request, reviewer="maria")
print(execute(request))
print(request.state.value)

This prints executed create_calendar_invites and then executed. Two protections matter: the requester cannot approve the request, and execution refuses anything that is not approved. Production systems also add expiration, audit logs, reviewer roles, and single-use approval tokens.

Design choices and trade-offs

A tighter sandbox reduces blast radius but can make legitimate work harder. No network access is safest for running untrusted code, but retrieval agents may need selected documentation or internal APIs. A host allowlist is easier to reason about than unrestricted egress, while a proxy can add logging and content filtering at the cost of operational complexity. Read-only mounts protect source data, but some tasks need a writable scratch directory for generated files. Short timeouts prevent runaway jobs but may interrupt valid data processing.

Approval placement has similar trade-offs. Requiring approval before every tool call is safe but slow and teaches users to click through prompts. Requiring approval only for irreversible side effects keeps the workflow useful, but the policy must correctly identify those effects. A good rule is to allow reversible computation automatically, require review for external side effects, and deny actions where the system cannot present enough context for an informed decision.

Failure modes and troubleshooting

One common symptom is an agent that says a command succeeded even though no file changed. Check the tool transcript and sandbox mount configuration. The likely cause is that the command ran in an isolated scratch directory while the application expected output in a persistent workspace. Correct it by declaring output paths, copying approved artifacts out of the sandbox, and showing those paths in verification.

A second symptom is repeated approval prompts for the same action. Inspect the idempotency key and state transition log. The cause is often that approval is attached to a natural-language summary rather than a canonical operation, so each retry looks different. Correct it by hashing normalized tool name, target, parameters, and requester identity, then storing approval against that canonical record.

A third symptom is unexpected network access from a tool that should be offline. Review environment variables, package-manager hooks, and DNS or proxy logs. The cause may be ambient credentials, default proxy settings, or a library that downloads models or telemetry. Correct it by starting sandboxes with an empty environment, passing explicit credentials only to approved tools, blocking egress at the network layer, and testing with a request to a known disallowed host.

Security, performance, and reliability

Sandboxing is a security boundary only when the underlying platform enforces it. A prompt that says “do not access secrets” is not a sandbox. Use operating-system permissions, containers, virtual machines, browser profiles, network policy, and secret brokers as appropriate for the risk. Keep logs useful but careful: record the tool name, policy decision, reviewer, target identifiers, timing, and failure class, but avoid dumping private document contents or raw credentials.

Performance depends on how often environments are created and destroyed. Fresh sandboxes give strong isolation but add startup cost. Reused sandboxes are faster but require cleanup and stronger controls against cross-task contamination. Reliability improves when every tool has bounded output, deterministic errors, retry limits, and a clear degraded path such as returning a draft instead of sending it.

Hands-on lab: build a local approval gate

Prerequisites: Python 3, a terminal, and a scratch directory with no sensitive files. The lab is local and does not require network access. Step 1: create a policy function with the action fields from Example 1. Step 2: add a runner that accepts only python and pytest commands and rejects unknown hosts. Step 3: create an approval request object for a side-effecting action such as creating calendar invites. Step 4: require a reviewer different from the agent before calling the executor. Step 5: run each script and compare the printed output with the expected output shown in the examples.

Verification is concrete. A read-only summarize action should print allow_in_sandbox. A send or delete action should print needs_human_approval. A blocked host should raise or print a policy error before any simulated fetch succeeds. An unapproved approval request should fail if you call execute before approve. Cleanup is simple: delete the scratch directory and any temporary scripts. If you adapted this in an application, also revoke test credentials and remove temporary reviewer accounts.

Assessment exercises

  • Design a policy table for an agent that can read support tickets, draft replies, issue refunds, and update account status. Which actions are automatic, which need approval, and which should be denied?
  • Modify Example 2 so fetch_url permits two documentation hosts but still blocks payment APIs. What tests prove the block happens before the request?
  • Explain why approving the sentence “send the customer a helpful reply” is weaker than approving a structured email operation with recipient, subject, and body.
  • An agent keeps failing because its sandbox has no network access. What evidence would you gather before adding an allowlist entry?
  • Choose one high-impact action in your own system and define the approval request fields needed to make that approval auditable and single-use.

Summary

Sandboxing and human approval turn agentic AI from unconstrained suggestion into controlled delegation. The sandbox gives the model a limited place to compute, inspect, and test. The approval gate reserves irreversible or external side effects for explicit human review. The core engineering move is to make authority structured: classify actions, enforce policy outside the model, run tools with narrow resources, and execute high-impact work only when the stored approval exactly matches the requested operation.

Progress is saved in this browser.