Overview
The harness is the agent program inside a session’s sandbox: it reads your messages, calls the model, runs tools and writes the transcript. You pick it withharness on the environment; everything else (model, egress, secrets, MCP servers, skills, permissions) is written once and rendered into that harness’s own config.
All six harnesses are available on every workspace: claude-code (the default), opencode, codex, langgraph, crewai and custom.
Every harness gets the same sandbox, egress rules and secret handling: credentials stay outside the sandbox, the model key is a placeholder swapped at the egress edge, and the transcript uses one event vocabulary.
Claude Code
The default. Each session runs the unmodified Claude Code CLI in headless stream-json mode. Permissions map onto Claude Code’s permission modes and rules (see Permissions),ask rules park the turn for your approval, and a fork resumes the checkpoint’s conversation as a new Claude Code session.
The model must speak the Anthropic Messages API: a Veri deployment (served through /v1/deployments/{id}/v1/messages), Anthropic itself, or any compatible gateway.
OpenCode
environment.toml
- Each message starts one
opencode run --format jsonwith the message on stdin and--session <id>to continue the conversation; the turn ends when OpenCode exits (exit0is success). Expect a second or two of start-up per turn. Nothing runs between turns, so pauses and checkpoints are cheap. - The model is an OpenAI-compatible provider in
/workspace/opencode.jsonat the session’s model base URL (OpenCode appends/chat/completions, which every Veri deployment serves).model.namesets the model it requests; for a Veri deployment it defaults to the deployment id. The key in the config is{env:VERI_MODEL_KEY}, a placeholder: the real key is added at the egress edge and never enters the sandbox.api.anthropic.comis not supported as an OpenCode model endpoint; useclaude-codefor it. - The model’s
limitis in the config too.contextis the deployment’s served context (its--max-model-len), or 32768 when Veri cannot know it (a deployment launched without--max-model-len, or abase_urlendpoint). OpenCode asks for at mostmin(8192, context / 4)output tokens per request, so the rest of the window stays for the conversation. - MCP servers become
remoteentries inopencode.json(bearer tokens are placeholders too). Skills are written to/workspace/.opencode/skills/<name>/SKILL.md. - Permissions become OpenCode’s
permissionmap, in the same last-match-wins order (see the table below).askis refused when you create the environment: a headless OpenCode run rejects every approval prompt on its own, and there is no way to pause the turn for you. - Forking runs
opencode run --session <checkpoint's session> --fork, so each child continues the conversation as its own session. - Model-update checks, model catalog downloads, language-server downloads, default plugins and sharing are disabled. The package OpenCode installs into its config directories on start (
@opencode-ai/plugin) is preinstalled in the image, in~/.config/opencodeand/workspace/.opencode, so a session start downloads nothing. - Tool calls arrive when they finish: each produces an
agent.tool_useand itstool.resulttogether. Token usage is reported per model step (usagewithscope: "step") and per turn (scope: "run").
Codex
environment.toml
- Each message starts one
codex exec --json(Codex CLI 0.162.1) with the message on stdin andresume <thread id>to continue the conversation; the turn ends when Codex exits (exit0is success). Nothing runs between turns, so pauses and checkpoints are cheap. - Codex speaks only the OpenAI Responses API. For a Veri deployment it calls the deployment’s
/v1/deployments/{id}/responsesroute, which vLLM serves natively (tool calls go through the deployment’s--tool-call-parser). Abase_urlendpoint must serve{base_url}/responses, andmodel.namesets the model Codex requests. The key isVERI_MODEL_KEY, a placeholder: the real key is added at the egress edge and never enters the sandbox.api.anthropic.comis not supported as a Codex model endpoint; useclaude-codefor it. - The config is
/workspace/.codex/config.toml(Codex’s home directory,CODEX_HOME, which also keeps its session history). It runs withapproval_policy = "never"andsandbox_mode = "danger-full-access": the session container is the sandbox, so Codex’s own Linux sandbox is not used.model_context_windowis the deployment’s served context (its--max-model-len), or 32768 when Veri cannot know it, and Codex compacts the conversation before less thanmin(8192, context / 4)tokens are left for the reply. Web search is off (it runs on OpenAI’s servers), and so are plugin sync, Codex’s bundled skills, analytics and update checks, so a session start downloads nothing. - MCP servers become
mcp_serversentries (streamable HTTP). Each server’s credential is a placeholder in an environment variable that Codex reads (bearer_token_env_var, orenv_http_headersfor a named header such asx-api-key). Skills are written to/workspace/.agents/skills/<name>/SKILL.md. - Permissions: Codex runs every tool it has unless a rule forbids it, so
defaultmust beallowand the rules that take effect aredenyrules (see the table below). Command rules become Codex execpolicy prefix rules in/workspace/.codex/rules/veri.rules: Codex matches the leading words of every command it reads out of a shell script, sobash(git push *)andbash(git push)both forbidgit push --force. A forbidden command does not run: Codex hands the rejection back to the model, and the command does not appear as a tool call in the transcript. Like every command rule, this guards against mistakes, not against an agent set on getting around it; egress rules and secret scoping are the boundary. askis refused when you create the environment (harness_does_not_support_ask):codex execcancels every approval request on its own.- Forking runs
codex exec fork <checkpoint's thread>, so each child continues the conversation as its own thread. - Tool calls arrive as
agent.tool_usewhen they start andtool.resultwhen they finish, withkindcommand(and theexit_code),file_change,mcp,todoorweb_search. Reasoning arrives asagent.thinking. Token usage is reported once per turn (usagewithscope: "turn", andscope: "run"); retried model calls areerrorevents withsubtype: "api_retry". Codex’s warnings (every model it has no metadata for triggers “Model metadata for<name>not found”) are kept asagent.rawwithnative_type: "warning".
LangGraph and CrewAI
Run your own LangGraph graph or CrewAI crew in a session. Veri clones your repository into the sandbox, installs its requirements, and runs the agent under a small Python mediator that turns each message into one run and the run into the session transcript. See Bring your own agent for the repository layout.environment.toml
[agent] block the session runs Veri’s built-in example agent for the framework (a model plus a checksum tool and your MCP tools), which is handy for a first try.
How it runs:
- The first message starts the mediator. It clones
agent.repoatrefinto/workspace/agent(the repository’s host is allowed automatically, for git’s requests on that repository’s path only), installs the requirements into a virtualenv at/workspace/.veri/venv(PyPI is reachable under the defaultcodingegress preset), then loadsentry. The repository must be public for now: private repositories need GitHub access, which is coming soon. If any of this fails, the session fails with anerrorevent whosesourceisharness_setupand whose message says what went wrong. A resumed, rolled-back or forked session keeps/workspace, so it does not fetch or install again. - Each message is one run: a LangGraph
astreamover the graph’s node updates, or a CrewAICrew.kickoff. Messages your nodes return becomeagent.message, tool callsagent.tool_use+tool.result(kind: "mcp"for MCP tools), reasoning contentagent.thinking, and each model call’s token usageusagewithscope: "message"(plus ascope: "run"total per run). What your code prints goes to the harness log, not the transcript. - LangGraph: the graph runs with Veri’s checkpointer (SQLite at
/workspace/.veri/langgraph.sqlite, replacing any checkpointer your graph was compiled with), and the conversation is the thread named after the session id, so it survives pauses and resumes. A fork copies the checkpoint’s thread into the child’s own. The graph’s input state should have amessageskey (MessagesState); each message is added to it as a human message.interrupt()(human-in-the-loop inside the graph) is not supported: the run ends with an error. - CrewAI: your crew gets
inputs = {"message": <the message>, "history": <the earlier turns, as text>}, so task descriptions can use{message}and{history}. A factory entry is called for every message, so each run starts from a fresh crew. A crew has no conversation to fork. - The model:
OPENAI_BASE_URL/OPENAI_API_KEYandVERI_MODEL_BASE_URL/VERI_MODEL_KEY/VERI_MODEL_NAMEpoint at the session’s model, so stockChatOpenAI(model=os.environ["VERI_MODEL_NAME"])orcrewai.LLM(model="openai/" + os.environ["VERI_MODEL_NAME"])work unchanged. The key is a placeholder: the real key is added at the egress edge and never enters the sandbox.veri_pyagent.chat_model()andveri_pyagent.crewai_llm()return models already wired up, with an output budget that fits the deployment’s served context. - MCP servers:
await veri_pyagent.mcp_tools()returns them as LangChain tools;veri_pyagent.mcp_servers()returns them forcrewai.Agent(mcps=...). CrewAI names an MCP tool after its server’s URL, so forcrewaieach MCP server must be addressed by hostname, not IP address (400otherwise). - Permissions: these frameworks have no built-in shell, file or web tools (the ones your code adds are yours to govern), so the rules Veri enforces are MCP rules:
mcp,mcp.<server>andmcp.<server>.<tool>(globs allowed in the tool name), last match wins, and a denied tool is left out of the tools the helpers hand your agent.defaultmust beallow; shell, file, web and git rules are refused when you create the environment. Egress rules and secret scoping are the boundary for everything your code does. askis refused when you create the environment (harness_does_not_support_ask): neither framework can pause a run for a human decision.
Custom
Run your own agent program inside the session sandbox. Veri starts your command in the stock session image (Debian with Node.js 22, Python 3 with pip and venv, git, curl, ripgrep and a C toolchain; see Sandbox), feeds it your messages and turns what it prints into the transcript.environment.toml
- Your process starts on the first message and keeps running. If it exits, the next message starts it again, so a command that fetches code should tolerate the checkout already being there (as above). A checkpoint, rollback or resume restarts it too: the files survive, the process does not.
commandis up to 64 arguments of up to 4096 bytes each, so a short script can also be passed inline (["python3", "-c", "..."]).- Each message arrives as one JSON line on stdin:
{"type": "user.message", "text": "..."}. - Every line you print to stdout that is a JSON object with one of these
types is recorded as that event, with the other fields as its payload:agent.message,agent.thinking,agent.tool_use,tool.result,usage,error,agent.raw. Any other JSON line is recorded asagent.raw, and any other text line asagent.message. - Print
{"type": "run.completed"}when you have answered (add"is_error": trueor a"stop_reason"if you like). Exiting also ends the turn: exit0is success, anything else an error with your stderr attached. - The model:
VERI_MODEL_BASE_URL(OpenAI-compatible for a Veri deployment),VERI_MODEL_KEY(a placeholder that is swapped for the real key on requests to that host only; send it asAuthorization: Bearer $VERI_MODEL_KEY) andVERI_MODEL_NAME(model.name, or the deployment id). - MCP servers are in
/workspace/.mcp.jsonand skills in/workspace/.veri/skills/for your program to use as it sees fit.
python3 -c or keep it in your repository):
agent.py
permissions must be default = "allow" with no rules (egress rules and secret scoping still apply to everything the process does), there are no ask approvals, and there is no conversation to fork. Status, approval and billing events are the runtime’s own: a session.status line from your process is recorded as agent.raw, usage lines never carry billed time, and error lines cannot fail the session.
Your own container image is not supported: sandbox images are built when Veri deploys its runtime and cannot be pulled per session. Install what you need from your command (npm and PyPI are reachable by default) or ask us about a custom base image.
Permission names
Rules can use these harness-neutral names. Each harness renders them into its own config; Claude Code’s own names (Bash(git *), Read, mcp__server__tool) keep working for claude-code, and OpenCode, Codex, LangGraph and CrewAI accept the ones they have an equivalent for.
The last matching rule wins on every harness. Codex applies the strictest matching rule instead, so an
allow that would re-allow something an earlier deny forbids is refused for Codex rather than rendered; any other allow is already the default there. Creating an environment fails with 400 when a rule cannot be expressed: ask on OpenCode, Codex, LangGraph, CrewAI or custom (harness_does_not_support_ask), any rule on custom (harness_does_not_support_permissions), a Claude-only rule such as Read(./src/**) or Task on OpenCode, Codex, LangGraph or CrewAI, a refused name above on Codex, LangGraph or CrewAI, or a lowercase name that is not in the table.
