skip to content

How do you require human approval before AutoGen's CodeExecutorAgent runs generated code?

level: seniorimportance: should knowfreq 40%

answer

  1. a callback on the execution path
  2. approval_func on the agent
  3. request carries code plus context
  4. denial reason returns to the conversation
  5. not a substitute for isolation

basics

~20 s

Pass an approval callback to CodeExecutorAgent via its approval_func argument. Before each execution the agent calls it with the pending code and conversation context; returning a denial stops the run and the reason goes back into the conversation instead of any output.

solid answer

~50 s

`CodeExecutorAgent` accepts an `approval_func`. The agent extracts the code as usual, then calls your function with an `ApprovalRequest` carrying the code and the messages that led to it, and expects an `ApprovalResponse` back with an approved flag and a reason. Approve and the executor runs; deny and nothing runs — the reason becomes the agent's reply, so the model sees why it was blocked and can propose something else. Both sync and async callbacks are supported, which matters because a real gate is usually I/O: post the code to a review UI, wait for a click, return the verdict. The important property is placement: the check sits inside the agent, on the only path to the executor, so no group-chat routing, retry or handoff can bypass it. Two design points follow — a human must actually be shown the code, and the callback blocks the run, so decide what a timeout means.

code

python · 26 lines
python
import asyncio

from autogen_agentchat.agents import ApprovalRequest, ApprovalResponse, CodeExecutorAgent
from autogen_agentchat.messages import TextMessage
from autogen_core import CancellationToken
from autogen_ext.code_executors.docker import DockerCommandLineCodeExecutor


def approve(request: ApprovalRequest) -> ApprovalResponse:
    print("Pending code:\n", request.code)
    if "subprocess" in request.code:
        return ApprovalResponse(approved=False, reason="spawning processes is not allowed")
    return ApprovalResponse(approved=True, reason="pure computation")


async def main() -> None:
    async with DockerCommandLineCodeExecutor(work_dir="coding") as executor:
        agent = CodeExecutorAgent(
            "executor", code_executor=executor, approval_func=approve
        )
        message = TextMessage(content="```python\nprint(6 * 7)\n```", source="coder")
        response = await agent.on_messages([message], CancellationToken())
        print(response.chat_message.content)


asyncio.run(main())

go deeper

for a junior

Know that AutoGen lets you attach an approval callback to the executor agent so generated code is not run until something says yes, and that a denial is reported back into the conversation.

for a middle

Explain the wiring: approval_func receives an ApprovalRequest with the code and conversation context and returns an ApprovalResponse with an approved flag and a reason; denial skips the executor entirely.

for a senior

Demonstrate production judgment — async callbacks so a human wait does not block the loop, an explicit deny-on-timeout policy, audit logging of who approved what, and the point that a gate controls whether code runs while a container controls what it costs.

for a principal

Own the systemic angle: approvals introduce unbounded human latency, which forces runs to become resumable rather than request-scoped, and an approval rate near 100 percent is evidence the control has become theatre and the agent's scope needs narrowing.

## Why a gate exists at this layer Code execution is the one agent capability where a wrong step is not merely an unhelpful answer — it writes files, calls APIs, spends money, deletes things. AutoGen's answer is a gate on the execution path itself rather than a convention elsewhere in the conversation, because conventions elsewhere are bypassable: a group chat can route a message a different way, an agent can retry, a handoff can reintroduce the same code. A check that lives inside `CodeExecutorAgent`, between extraction and the executor call, is on the only road. ## The mechanism Construct the agent with `approval_func=...`. The flow per turn is: extract fenced code blocks from the incoming messages; if any are runnable, build an `ApprovalRequest` containing the code and the message context; invoke your callback; read the returned `ApprovalResponse`. The response carries an approved flag and a reason string. On approval the executor runs and you get the normal output-plus-exit-code reply. On denial nothing executes and the agent's reply carries the reason — which is what lets a model-backed loop react: it sees "denied: this deletes a production table" as conversation content and can produce something narrower. Both synchronous and asynchronous callables are supported. Prefer the async form for anything real, because a genuine human gate is network-bound — you are publishing the code to a queue or a review surface and awaiting a decision. A blocking sync callback inside an async agent run stalls the event loop and everything else scheduled on it. ## What a good callback actually does The naive implementation prints the code and calls `input()`. That is fine for a demo and useless in production, where the run happens on a server and the approver is a person in a browser or a chat client. A serviceable gate has four parts: 1. **Render the code where a human can see it.** Approval without display is theatre. Include the surrounding conversation from the request — the same script is fine in one context and catastrophic in another. 2. **Identify the decision.** Log who approved, what exact code, and when. Approval is an audit event; if you cannot reconstruct it later it did not happen. 3. **Decide the timeout policy explicitly.** The callback blocks the agent turn. If nobody clicks in five minutes, does the run deny (fail closed, safe, sometimes annoying) or hang? Deny-on-timeout with a clear reason is almost always right, and the reason lets the model or the caller retry deliberately. 4. **Auto-approve the boring cases, carefully.** Prompting a human for every `print(2 + 2)` guarantees rubber-stamping, which is worse than no gate because it manufactures a false record. Cheap heuristics — deny anything touching the network, subprocess, or filesystem outside the working directory; auto-approve pure computation — keep human attention on what deserves it. Be honest in an interview that such heuristics are advisory: they inspect model-authored source text and can be evaded, so they set attention priority, not a security boundary. ## What the gate is and is not It is a control on *whether* code runs. It is not a sandbox. If the approver waves through a script that then runs under a local executor, the code has your filesystem, your environment variables and your network — the gate delayed nothing. Approval and isolation are complementary: the container decides what a mistake costs, the gate decides whether the mistake happens at all. Interviewers like this question because a candidate who reaches only for approval, or only for a container, has half the answer. ## Placement alternatives You can also gate outside the agent: keep a human participant in the conversation who must speak before the executor agent takes a turn, or stop the run and hand control back to your application between the coding step and the execution step. Both are legitimate and are how you get an approval step in flows that do not use `CodeExecutorAgent` at all. Their weakness is the one above — they depend on the orchestration continuing to route that way. When execution is the risk, put the gate on the executor. ## Operational reality A gate changes the shape of the system: turns now have unbounded human latency, so an agent run is no longer a request you hold a socket open for. That pushes you toward persisting the run and resuming it, and toward measuring approval rate and time-to-decision. If approvals run at 99 percent yes, the gate has become a formality and either the heuristics or the agent's scope needs tightening.

  • Why should the approval callback be async in a production deployment?
    Because a real gate is I/O: you publish the code to a review UI or chat client and wait for a person. A synchronous callback that blocks on that wait stalls the event loop running the agent, so every other concurrent run stalls with it. The async form lets the runtime keep serving other sessions while this turn awaits a human decision.
  • Does an approval gate make the local executor safe to use?
    No. The gate controls whether code runs; it does nothing about what the code can reach once approved. Under the local executor an approved script still has your filesystem, environment variables and network position — and humans approve things that look reasonable and are not. Keep both controls: a container to bound the blast radius, the gate to bound what gets attempted.
  • What happens to the conversation when the callback denies?
    The executor is never invoked, and the agent replies with the denial reason instead of program output. That text is ordinary conversation content, so a model-backed agent or a downstream participant can read why it was blocked and propose a narrower approach. Writing a specific reason therefore has real value — "denied: writes outside the working directory" steers the next attempt, "denied" does not.
  • How do you keep a gate from degrading into rubber-stamping?
    Send only the decisions worth a human's attention. Auto-approve pure computation, auto-deny categories you never intend to allow, and route the remainder to a person with the code and the surrounding conversation rendered. Then measure: a near-100 percent approval rate means the gate is theatre, and either the routing heuristics or the agent's scope of action needs tightening.

saying these in an interview costs you the question

  • Treating an approval gate as a replacement for sandboxing
  • Blocking on synchronous input() inside an async agent run
  • Approving without ever displaying the code to a human
  • Leaving the timeout behaviour undefined so runs hang indefinitely
  • Prompting for every trivial execution until approvals become reflexive

context