
Agent Integration
- 1.2k installs
- 3 repo stars
- Updated June 9, 2026
- veris-ai/veris-skills
Agent Integration is a Claude Code skill that turns any raw repository into a fully configured Veris-compatible agent and pushes its environment in one command for developers who adopt the Veris agent platform.
About
Agent Integration is a Veris skill that end-to-end prepares a raw agent repository for the Veris platform and publishes its environment. The workflow installs veris-cli via uv tool install veris-cli with pip install veris-cli as fallback, creates .veris/config.yaml environment bindings with veris env create --self-serve, and pushes configuration using veris env push. The skill includes troubleshooting for missing veris commands and unconfigured environments. Developers reach for Agent Integration when scaffolding a new agent repo that must become Veris-ready without manually wiring config files and CLI bootstrap steps. The default prompt targets making an agent repo Veris-ready from scratch and pushing with veris env push.
- End-to-end repo preparation for Veris from scratch
- Automates creation of .veris/config.yaml with self-serve environment binding
- Handles veris env push after configuration
- Includes troubleshooting for common bootstrap, auth, and startup failures
- Supports headless API-key login for CI and automation contexts
Agent Integration by the numbers
- 1,169 all-time installs (skills.sh)
- +60 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #944 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/veris-ai/veris-skills --skill agent-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.2k |
|---|---|
| repo stars | ★ 3 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 9, 2026 |
| Repository | veris-ai/veris-skills ↗ |
How do you integrate a repo with Veris agents?
Turn any raw repository into a fully configured Veris-compatible agent and push its environment in one command.
Who is it for?
Agent developers adopting Veris who have a raw repository and need CLI bootstrap, config scaffolding, and environment push in one guided flow.
Skip if: Developers not using Veris or repos that already have complete .veris/config.yaml and deployed environments.
When should I use this skill?
A developer asks to make a repository Veris-ready, install veris-cli, or run veris env push for agent deployment.
What you get
.veris/config.yaml, Veris environment binding, and pushed agent environment via veris-cli.
- .veris/config.yaml
- pushed Veris environment
Files
Integrate this agent repo with Veris from scratch.
This skill takes a repo from "plain customer agent source" to "Veris-ready and pushable." If the user provided a path to an agent repo, use that as the repo root. Otherwise use the current working directory.
Treat any existing .veris/ files or old scaffold output as starting material only. Use the current bundled references in this skill as the source of truth for what you generate.
Core framing: the agent is the constant, Veris is the test harness
Veris exists to test an agent under realistic conditions. The agent is the thing being tested; Veris is the harness around it. That asymmetry drives every decision in this skill:
- The agent runs the same way in Veris as it does in production. If the agent speaks HTTP to a Slack Web API in prod, it speaks HTTP to the Veris Slack mock in sim. If it shells out to a CLI in prod, it shells out in sim. No special simulation code path.
- All integration work lives in `.veris/`.
.veris/veris.yaml,.veris/Dockerfile.sandbox,.veris/config.yaml,.veris/.dockerignoreare the deployment descriptor — the equivalent of a Helm chart ordocker-compose.yamlfor this agent. They describe how to stand the agent up for this environment. They do not contain behavior that belongs inside the agent. - Do not write wrappers, shims, or glue code that "adapts" the agent to Veris. A Python file that wraps a CLI agent to expose a callable, a script that translates Veris's actor format into the agent's native format, a patched version of the agent that accepts Veris-specific parameters — all of these are the wrong shape. They mean the thing you end up testing is not the agent.
- Do not modify the agent's source code to make it work in Veris. If the agent assumes something Veris can't satisfy as-is, that's either a Veris platform gap to be logged, or a real issue with the agent that would also break production. Either way, the fix does not belong in the agent's source.
- If you find yourself needing a wrapper, stop and treat it as a finding. Ask: what is the agent's real production integration path? If the agent has an HTTP server in prod, use that. If it's CLI-only in prod and Veris's actor can't drive a CLI, escalate — that's a Veris capability gap, not a license to invent glue.
- The one legitimate `.veris/` file that is not pure config is a container-orchestration `start.sh` for bundling multiple processes (e.g., a database alongside the agent). Even that starts and runs the agent as-shipped; it does not transform its behavior.
When in doubt: the agent's author should be able to read .veris/ and recognize it as "the deploy config for Veris," not as "someone forked and patched my agent."
Transport bridges are an explicit exception
A transport bridge translates between the actor's channel format (e.g. voice_ws PCM16) and the agent framework's native transport (e.g. LiveKit WebRTC, SIP media, a proprietary message envelope) while preserving the underlying payload byte-for-byte. It is not a wrapper. The same shape exists in production for the agent's other product surfaces — mobile clients, kiosks, IVR vendors — so the bridge is genuine product code, not Veris-specific glue.
A bridge is allowed when:
- The agent's framework cannot be reconfigured to speak the actor's channel format directly (e.g., LiveKit Agents is WebRTC end-to-end and has no raw-PCM16-WS transport).
- The bridge is pure transport translation: same audio bytes / same JSON payloads in and out, with no semantic reshaping. (For voice, the audio bytes the agent's model receives must be identical to what the actor sent. For text, the message payload the agent sees must be identical to what the actor sent.)
- The bridge lives in the agent's repo as production code (e.g.,
app/bridge.py), exercised by the agent's own tests — not in.veris/. It is the agent'svoice_ws-on-WebRTC product surface; Veris is just one caller.
A bridge is not allowed when:
- It reshapes content the agent sees: rewriting messages, restructuring tool-call JSON, normalizing STT output, translating Veris-specific actor fields into the agent's native parameters. That is a wrapper, and the no-wrapper rule above applies.
- The framework can be configured to speak the actor's channel (Pipecat with
RawAudioFrameSerializerforvoice_ws, the framework's own HTTP plugin forhttp, etc.). Configure the framework first; bridge only if the transport is fixed.
Quick rubric: "same bytes, different network format" = transport bridge (allowed). "Different bytes / different shape" = wrapper (not allowed).
See reference/infrastructure-patterns.md Pattern 9 for the architecture and reference/voice-channels.md for the voice-specific application.
Reporting client-tool calls to the grader is a sanctioned exception (voice agents only)
This is the one exception that genuinely breaks the "no Veris-specific code path" rule — and it is deliberate. It applies only to voice agents built on hosted speech-to-speech platforms (ElevenLabs Conversational AI, OpenAI Realtime, Gemini Live, Vapi, and the like) whose tools execute inside the agent process — as client tools that round-trip on the vendor's WebSocket, or as Vapi-style server tools the platform POSTs back to the agent's webhook over HTTP. Either way the call never reaches the spoken transcript.
Why it's needed: the voice grader builds its trace from the spoken transcript plus any tool-call events the agent reports. A client tool never reaches the transcript, so without a report the grader can't see the tool ran and false-flags real actions (a card freeze, a replacement) as hallucinations. Text/HTTP agents don't have this problem — their tool calls are captured automatically — so this exception is voice-only.
The fix: after each tool runs, the agent POSTs an agent_tool_call event to the sandbox engine, and the platform renders it into the graded trace. Keep it strictly minimal so it stays instrumentation, not a wrapper:
- No-op outside a simulation. Gate on
SIMULATION_ID(unset in production → return immediately). Production behavior is unchanged. - Fire-and-forget, fail-soft. Short timeout, swallow errors, log a warning. A reporting failure must never break the call or change the agent's output.
- Observe, don't reshape. Report after the real tool runs, passing the real name/args/result through unchanged. It records what happened; it does not alter what the agent does or what the model sees.
This is sanctioned only for client-tool grader visibility, only for voice agents, and only in this minimal shape — it is not a license for general shims. The exact endpoint, event schema, and a copy-paste hook are in reference/voice-channels.md.
Core rules
- Explain what you are about to do before each major step.
- Surface decisions with real tradeoffs and let the user choose.
- Cite concrete evidence from the repo when you classify dependencies or decide how the agent should be integrated.
- Do not silently preserve stale Veris config. Migrate it to the current preferred shape.
- Do not generate
.env.simulation. The current runtime flow isagent.environmentplusveris env vars set. - Prefer the current
actor.channelsschema and canonical service names. Do not generate legacypersona.modality,email_address, or old service aliases unless the user explicitly asks for compatibility. - Do not write Python wrappers, shell shims, or any "adapter" code that translates between Veris and the agent. Use the agent's real production interface. If that isn't possible as-is, surface it as a platform gap, not as a wrapper opportunity.
- Ask before external or irreversible actions:
- installing
veris-cli - running
veris login - running
veris env create - setting environment variables with
veris env vars set - pushing with
veris env push
Fast-track mode
If the user says "go all the way", "do everything", or otherwise pre-approves the full flow:
- Skip intermediate checkpoints (end-of-Phase 2, end-of-Phase 3, end-of-Phase 4)
- Still explain decisions inline as you make them, so the user can follow along
- Still stop and ask before truly irreversible or external actions:
veris env create,veris env push,veris env vars setwith real secrets - If a decision has genuinely ambiguous tradeoffs (e.g., bundle-vs-external for a heavy service), pause and ask even in fast-track mode
- At the end, present a consolidated summary of all decisions made
Read these files when needed
- For current service names and detection: reference/service-mapping.md
- For env overrides and mock credentials: reference/env-var-overrides.md
- For bundleable local infra: reference/bundling-recipes.md
- For container restructuring patterns: reference/infrastructure-patterns.md
- For voice agents (
voice_wschannel, framework choice, trailing silence, reporting client-tool calls so the grader can see them): reference/voice-channels.md - For current
veris.yamlstructure: reference/veris-yaml-schema.md - For generated config examples: templates/veris-yaml.md
- For Dockerfile patterns: templates/dockerfile-sandbox.md
- For runtime env var handling: templates/env-vars.md
- For multi-process startup scripts: templates/start-sh.md
- For integration failures: phases/troubleshooting.md
Workflow Overview
| Phase | Goal |
|---|---|
| 0 | Bootstrap Veris tooling and environment |
| 1 | Discover the repo and current runtime |
| 2 | Analyze dependencies and service strategy |
| 3 | Choose integration mode and container architecture |
| 4 | Generate .veris/veris.yaml |
| 5 | Generate .veris/Dockerfile.sandbox and supporting files |
| 6 | Configure runtime env vars, validate, and push |
| 7 | Smoke-validate with a single scenario + simulation |
---
Phase 0: Bootstrap Veris Tooling And Environment
[Phase 0/7]
Tell the user: "I'm going to make sure this repo has the Veris tooling and environment wiring needed for the rest of the integration work."
0.1 Verify repo root
Confirm the directory is an agent repo, not just a parent folder. Look for source code, dependency manifests, and app entrypoints.
0.2 Verify veris-cli
Check whether veris is installed and working.
If not installed:
- Prefer
uv tool install veris-cli - Fallback:
pip install veris-cli
Explain which install path you are using and why.
0.3 Verify Veris authentication
Check whether the user is already logged in and which profile/backend they are using.
If not authenticated:
- Recommend
veris loginfor browser auth - Use API-key login only if the user explicitly prefers it
Do not proceed to veris env push until auth is working.
0.4 Verify or create .veris/
Inspect:
.veris/config.yaml.veris/veris.yaml.veris/Dockerfile.sandbox.veris/.dockerignore
If .veris/ does not exist, or it exists but has no environment binding: 1. Derive a candidate environment name from the repo directory. 2. Show the user the proposed name. 3. On approval, run veris env create --self-serve --name "<name>".
--self-serve (veris-cli >= 2.27.0) is the right mode for this skill's audience: you are authoring .veris/ yourself, and the env should be ready for veris env push immediately. Without it, env create defaults to managed-setup mode where the Veris team generates Dockerfile.sandbox + veris.yaml for the customer, and veris env push returns 409: Run veris env submit first until that setup completes. If veris env create --help doesn't list --self-serve, bump the CLI first using the install manager that owns veris (uv tool upgrade veris-cli or pip install -U veris-cli). For recovery from an env that was already created without --self-serve, see phases/troubleshooting.md.
Explain what veris env create gives them:
.veris/veris.yaml— Veris simulation config.veris/Dockerfile.sandbox— image build definition.veris/.dockerignore— build-context exclusions.veris/config.yaml— environment binding for this repo
0.5 Treat scaffolding as placeholders, not truth
The generated .veris/ files are just a starting point. They may use old defaults or generic placeholders. You are responsible for replacing them with the correct integration for this repo.
Proceed directly to Phase 1.
---
Phase 1: Discover The Repo And Current Runtime
[Phase 1/7]
Tell the user: "I'm going to inventory how this repo currently runs, what it depends on, and how users interact with it."
1.1 Existing Veris state
If .veris/ already exists, read all existing Veris files first. Call out anything that looks stale or legacy:
persona.modalityemail_address- old service names like
crm,calendar,oracle - missing
.veris/config.yamlenv binding - assumptions that conflict with the current docs
1.2 Infrastructure files
Read and summarize any of:
docker-compose.yml,docker-compose.yaml,compose.ymlDockerfile,Dockerfile.*Procfilesupervisord.conf,supervisord.inivercel.json,serverless.yml,netlify.toml- Kubernetes manifests
Identify:
- which process is the user-facing agent
- what other services exist
- how the system currently starts
1.3 Environment and secrets
Read:
.env.example,.env.sample,.env.template- config/settings modules
- secret or vault references
Collect every env var the agent reads, and note which are:
- stable non-secrets
- secrets
- service endpoints
- optional or debug-only
1.4 Dependencies
Read the package manifests for the repo’s language/runtime and identify:
- package manager
- framework
- Python/Node runtime assumptions
- SDKs for external services
1.5 Source-code entrypoints
Find the actual code path that handles incoming user work:
- app/server entrypoint
- chat/message handler
- config/settings module
- request routing
- any background worker or webhook listener that matters during a user conversation
1.5a Platform-hosted agents (config-only repos)
If the repo has no traditional application entrypoint — no main.py, app.py, server.js, index.ts — check whether it is a platform-hosted agent: a repo of config files that runs on an installed framework (CrewAI, LangServe, AutoGen, Dify, n8n, Flowise, or similar).
Signs:
- Primary files are YAML/JSON config, prompt templates, and tool definitions
pyproject.tomlorpackage.jsonlists a framework as the main dependency- No substantial application logic beyond small tool/hook files
- README instructions say "install [framework], then run [framework command]"
If this is the case:
- The framework is the runtime — it will be installed in the Dockerfile, not built from source
- The entry point is the framework's CLI or server command
- See Pattern 8 in
reference/infrastructure-patterns.mdfor the full restructuring approach - Watch for source-tree compile errors if you attempt
pip install .on these repos
1.6 Determine the integration interface
This is critical. Determine how the simulated actor should talk to the agent.
Look for four classes of interfaces:
HTTP
- Chat endpoint
- Request/response body shape
- Session or conversation field
- JSON or SSE response style
WebSocket
- WS route
- Message framing
- Session handling
- Inbox address
- Polling or webhook flow
Function
- A clean Python callable the agent already exposes as part of its public API
- Existing
handle_message-style functions the agent's own documentation treats as an entry point
Do not invent a function interface by wrapping a CLI or a server. If the agent is CLI-only in production, the integration is CLI-driven — surface that and find the right Veris channel for it, or log it as a platform gap. A function channel is only correct when the repo already ships a callable as its primary or documented interface.
If both network and function modes are viable, use the repo's real product interface. That is what runs in production; that is what we test.
Tell the user exactly what you found and confirm the likely best integration path before continuing.
Proceed to Phase 2.
---
Phase 2: Analyze Dependencies And Service Strategy
[Phase 2/7]
Tell the user: "I'm now classifying each dependency into mock, bundle, external, or skip."
Read:
- reference/service-mapping.md
- reference/env-var-overrides.md
- reference/bundling-recipes.md
For every dependency, classify it as one of:
1. Mock with Veris 2. Bundle inside the container 3. Use an external endpoint 4. Skip entirely 5. Needs discussion 6. Allow real egress — the agent must reach the real internet (e.g., web search, URL scraping, live API with no mock). Results will be nondeterministic across simulation runs.
Classification rules
- Always read the source code before deciding. Do not infer importance from service names alone.
- Show evidence when you decide something is skippable.
- Surface bundle cost when it matters, especially for heavy services like Elasticsearch or LocalStack.
- Prefer mock services when the dependency maps cleanly to Veris.
- Prefer env-var overrides over code changes whenever possible.
Special cases
Postgres
- Decide whether to use Veris
postgresor an external DB - If using Veris
postgres, find the schema artifact or migration source and determine the best copy path
LLM providers
- No Veris service entry is needed
- The LLM proxy intercepts supported domains automatically
- If the actor uses an email channel, note that the Veris email service is injected automatically
Auth helpers
- Google/Microsoft/Atlassian/Intuit auth helpers are platform-level helpers, not services you should normally add manually
Web search and scraping
- If the agent calls search APIs (Google, Bing, SerpAPI, Tavily, Brave Search, DuckDuckGo) or fetches live URLs, these cannot be mocked
- Classify as "Allow real egress"
- Warn the user: live internet calls make simulation results nondeterministic — the same scenario may produce different outputs on different runs
- If the search is truly optional (e.g., a fallback when the knowledge base has no answer), consider disabling it via env var for deterministic simulations
Real internet egress (general)
- Some agents need to hit arbitrary external endpoints that Veris cannot mock (webhooks to third-party services, real-time data feeds, public REST APIs without a Veris service)
- These also classify as "Allow real egress"
- The Veris container allows outbound internet by default for non-intercepted domains
- Surface the nondeterminism tradeoff to the user
Checkpoint
Walk through your dependency analysis with the user before moving on. The user should understand:
- what will be mocked
- what will be bundled
- what stays external
- what gets skipped
- what still needs a decision
Wait for approval before proceeding.
---
Phase 3: Choose Integration Mode And Container Architecture
[Phase 3/7]
Tell the user: "I'm locking down how this agent will run inside the Veris container and how the actor will talk to it."
Read reference/infrastructure-patterns.md.
3.1 Choose the channel strategy
Pick one of:
- HTTP — preferred when the product is already an HTTP chat API
- WebSocket — preferred when real-time stateful messaging is core
- Email — preferred when the product is genuinely email-driven
- Function — preferred when the repo has a clean callable path or should be treated as a one-shot request/response agent
3.2 Function-channel rules
If you choose a function channel:
- The callable path must be something the agent repo already exposes as a public interface (documented, referenced in its README, or otherwise part of its contract)
- Do not create a wrapper file to conjure a callable out of a CLI or server — if the repo doesn't already expose one, function is the wrong channel
- Omit
agent.entry_pointandagent.portinveris.yaml - If the callable is one-shot and stateless, set
actor.config.MAX_TURNS: 1
3.3 Network-channel rules
If you choose HTTP / WS / email:
- determine the exact request and response mappings
- determine the startup command
- choose a non-reserved port
- decide whether
start.shis needed for bundled infra or multiple processes
3.4 Container layout
Determine:
- what gets copied into
/agent - which files should stay out of the image
- whether a
start.shis required to bundle multiple processes (this is container orchestration, not agent modification)
Do not plan "which code changes are necessary." The target is zero code changes to the agent. If an env-var override isn't enough and the agent genuinely can't run as-shipped, that's a finding — escalate it rather than patching the source.
Checkpoint
Explain:
- how the actor will communicate with the agent (using the agent's real production interface)
- how the agent will start inside the container (its real production start command)
- what files will be copied
If you believe any agent-side code change is needed, flag it here and stop. The default answer is zero code changes. If you can't see a way forward without one, it's probably a Veris platform gap, not an integration step.
The single pre-sanctioned exception: a client-tool voice agent (ElevenLabs Conversational AI, OpenAI Realtime, Gemini Live, …) needs the agent_tool_call reporting hook so the grader can see its tools — see the Core framing exception and reference/voice-channels.md. That one is expected; add it without escalating. Everything else still stops here.
Wait for approval before proceeding.
---
Phase 4: Generate .veris/veris.yaml
[Phase 4/7]
Tell the user: "I'm generating the final Veris configuration in the current preferred schema."
Read:
- reference/veris-yaml-schema.md
- templates/veris-yaml.md
Rules
- Use
actor.channels, notpersona.modality - Use canonical service names from
reference/service-mapping.md - Use
agent_inbox, notemail_address - Only set
actor.config.MAX_TURNSwhen there is a concrete reason, usually a one-shot function integration - Do not add the
*_INTERVALknobs unless the user explicitly asks for advanced tuning - Keep secrets out of
veris.yaml - Put stable non-secret defaults in
agent.environment - Only use
${VAR}inagent.environmentwhen you need expansion/composition; if the agent can read a runtime env var directly, prefer setting it withveris env vars set
Channel-specific rules
HTTP / WS / Email
- include
agent.code_path - include
agent.entry_point - include
agent.port
Function
- include
agent.code_path - omit
agent.entry_point - omit
agent.port - set
actor.channels[0].type: function - set
callable: ...
Checkpoint
Show the complete veris.yaml, explain the sections, and get approval before writing or finalizing it.
---
Phase 5: Generate .veris/Dockerfile.sandbox And Supporting Files
[Phase 5/7]
Tell the user: "I'm generating the image build and any small support files needed for this integration."
Read:
- templates/dockerfile-sandbox.md
- templates/start-sh.md
5.1 Dockerfile rules
- Start with:
ARG GVISOR_BASE
FROM ${GVISOR_BASE}- Build context is the repo root
- Copy dependency manifests before source code
- Copy only what the agent actually needs
- End with
WORKDIR /app - Do not bake
veris.yamlinto the image
5.2 Runtime notes
- The current base image already includes Python,
uv, and Node.js - Only install extra runtimes or system packages when the repo truly needs them
- If using a function channel, you still package the agent code and dependencies normally; you just do not start a network server
5.3 Supporting files
Create only what is needed:
start.shfor bundling multiple processes (e.g., a database alongside the agent) — this is container orchestration, same as adocker-compose.yamlwould be.veris/.dockerignoreupdates if the repo has large directories the default ignore file misses
Do not create Python "wrapper modules" that expose the agent as a callable, translate Veris actor calls into the agent's native format, or otherwise insert themselves between the actor and the agent. Use the agent's real interface.
5.4 No code changes to the agent
The agent runs in Veris exactly as it runs in production. That means:
- No source-code patches to accommodate simulation
- No "simulation mode" flags or Veris-specific branches
- No forked copies of the agent with local modifications
If you find yourself wanting to change the agent's source, stop. Either:
- The change can be expressed as an env-var override (then do it that way, via
agent.environmentorveris env vars set), or - The change is a real issue in the agent (then it's the customer's responsibility to fix, and it would also affect production), or
- Veris can't accommodate the agent as-shipped (then it's a platform gap — escalate)
Unrelated refactors are obviously out.
The one sanctioned source addition is the client-tool reporting hook for voice agents (see the Core framing exception). It is the deliberate exception to "no code changes," not a counterexample to it: it's a no-op outside a simulation, it never alters the agent's behavior, and it exists solely so the grader can see client tools that otherwise never reach the trace. Add it for client-tool voice agents; don't generalize it into other source edits.
Proceed directly to Phase 6 once the files are in place.
---
Phase 6: Configure Runtime Env Vars, Validate, And Push
[Phase 6/7]
Tell the user: "I'm turning this into a pushable Veris environment."
Read:
- templates/env-vars.md
- phases/troubleshooting.md
6.1 Build the env-var plan
Classify env vars into:
1. Stable non-secret defaults → put in agent.environment 2. Secrets / per-environment values → set with veris env vars set 3. Local-only convenience values → optional root .env or shell exports for local smoke tests
Do not create .env.simulation.
6.2 Produce exact commands
Generate the exact veris env vars set commands the user needs.
If the user provides actual values and wants you to do it, run the commands for them.
Shell interpolation pitfall: when running veris env vars set KEY="$VAR" --secret with a shell variable, verify the source variable is actually set first (printenv VAR or test -n "$VAR"). An empty or unset variable expands to "" silently — the CLI will happily save an empty secret with no error, and the agent will fail at runtime with a confusing auth/provider error instead of a clear "missing key" message.
6.3 Validate push preconditions
Before pushing, verify:
verisis installed- auth/profile works
.veris/config.yamlhas an environment ID.veris/veris.yamlexists.veris/Dockerfile.sandboxexists
Optional but encouraged:
- run a local
docker build -f .veris/Dockerfile.sandbox .smoke test when that is likely to catch obvious breakage quickly
6.4 Push
If the user approves, run:
veris env pushOr with an explicit tag if the user wants one:
veris env push --tag <tag>If the push fails:
- diagnose the failing build step
- fix the integration
- retry
6.5 Final summary
Summarize:
- files created or modified
- integration mode chosen
- services mocked, bundled, external, or skipped
- env vars set vs left for the user
- whether
veris env pushsucceeded and which tag was created
Then suggest the next commands:
veris scenarios createveris simulations create
---
Phase 7: Smoke Validation
[Phase 7/7]
Tell the user: "I'm going to run a single scenario and simulation to verify the integration works end-to-end."
7.1 Create a smoke scenario
veris scenarios create --num 1The goal is a single short interaction that exercises the agent's primary interface.
7.2 Run a single simulation
veris simulations create --scenario-set-id <id>Wait for it to complete.
7.3 Check the results
Review the simulation for:
1. Agent responded with real content — not an error page, empty body, or exception traceback 2. Mock services were called — if the agent should call Slack, Salesforce, etc., confirm those calls appear 3. No startup crashes — the agent process stayed alive for the duration 4. Channel contract is correct — the actor's messages reached the agent and responses came back in the expected shape
7.4 Diagnose failures
If the smoke test fails:
- Check agent container logs for startup errors or missing env vars
- Verify
actor.channelsrequest/response mapping matches the actual API shape - Confirm mock service credentials and DNS aliases are correct
- Return to the relevant phase to fix and re-push
7.5 Sign off
If the smoke test passes, summarize:
- What the actor sent and what the agent responded
- Which services were exercised
- Confidence level that the integration is ready for full scenario generation
Then suggest full scenario generation (veris scenarios create --num N) and simulation as the next step.
---
Practical guidance
Prefer current conventions over stale scaffolding
If veris env create scaffolds old-looking placeholders, overwrite them with the current preferred shape from this skill.
Keep the skill honest about function channels
Use a function channel only when the agent already exposes a callable as part of its public interface. Do not force a networked product into a function callable just because it seems simpler, and never create a wrapper file to invent a callable the agent doesn't already have.
Keep the skill honest about one-shot agents
If the integrated agent is clearly one-shot/stateless, carry that through explicitly by setting actor.config.MAX_TURNS: 1.
Be explicit about what you did not automate
If login, secrets, or env-var values still require user action, say so plainly. The goal is to get as far as possible, not to hide blockers.
interface:
display_name: "Agent Integration"
short_description: "Integrate a raw repo with Veris end to end"
default_prompt: "Use $agent-integration to make this agent repo Veris-ready from scratch and push it with veris env push."
policy:
allow_implicit_invocation: true
Troubleshooting
Common integration issues organized by symptom.
veris-cli bootstrap problems
veris command not found
Install the CLI first:
uv tool install veris-cliFallback:
pip install veris-cliveris env push says no environment is configured
The repo is missing .veris/config.yaml with an environment binding. Run:
veris env create --self-serve --name "<env-name>"Use --self-serve for this skill's flow; plain veris env create can place the env in managed onboarding and block veris env push with a 409.
Auth problems
If the CLI is installed but backend calls fail, the user likely needs:
veris loginOr API-key login for headless contexts.
Agent fails to start
Common causes:
1. Missing required env var 2. Wrong entry_point 3. Wrong code_path 4. Missing dependency install in Dockerfile.sandbox 5. Bundled service not started before the agent
Check the agent logs first. The startup error usually points directly at the missing dependency or bad command.
Actor cannot reach the agent
For HTTP / WS / email integrations:
1. actor.channels[].url points at the wrong port or path 2. agent.port does not match the server’s listen port 3. The agent binds only to 127.0.0.1 instead of 0.0.0.0 4. The server takes too long to become healthy
For function integrations:
1. callable import path is wrong 2. Wrapper file was not copied into /agent 3. The callable returns a shape the driver cannot serialize cleanly
Agent runs but cannot reach mocked services
Common causes:
1. Missing or wrong services: entry 2. Wrong dns_aliases 3. Wrong env-var override, especially old docker-compose hostnames that should be localhost 4. Missing mock credentials
Check reference/service-mapping.md and reference/env-var-overrides.md.
Grader flags a voice agent for hallucinating tools it actually called
Symptom: a voice_ws / phone agent runs its tools correctly (the agent logs, and any mock-service logs, show the calls happening and the database changing), but the grader reports "no tool call," "fabricated," or "claimed an action without calling the tool."
Cause: the agent uses client tools (ElevenLabs Conversational AI, OpenAI Realtime, Gemini Live, …) that execute in-process and round-trip on the vendor WebSocket, so they never reach the spoken transcript the voice grader reads. Without an explicit report, the grader is blind to them. This is a grading-visibility issue, not an agent bug — the actions really happened.
Fix: emit an agent_tool_call event per tool call — see reference/voice-channels.md for the contract and copy-paste hook. If you already added the hook and still see this, check:
1. event_type is exactly agent_tool_call, and data.name / data.arguments are present (the renderer needs both). 2. SIMULATION_ID is set in the agent process — it's exported by the sandbox; if your hook silently no-ops, it isn't reading the env you think it is. 3. The POST is actually landing — a swallowed connection error logs could not report <tool> to engine. Confirm ENGINE_URL (default http://localhost:6100) is reachable from the agent.
Voice agent never answers under load (callee_no_answer)
Symptom: a LiveKit-based voice_ws agent connects fine in a single local smoke test, but under concurrent simulations a chunk of calls end in callee_no_answer — the actor connects, the room is created, but the agent never joins. The failure rate climbs with concurrency.
Cause: LiveKit's worker/auto-dispatch model has two race/throttle traps a plain WS server doesn't (see infrastructure-patterns.md → LiveKit dispatch gotchas):
1. Dispatch race. The worker registers with the SFU asynchronously, and auto-dispatch only fires for rooms created after registration. If the bridge accepts a caller before the worker registers (a fixed sleep instead of a real gate), the room exists but the agent is never dispatched into it. Under load, registration can take 10s+. 2. CPU self-throttle. A prod-mode AgentServer refuses dispatch when its CPU load function exceeds 0.7; with the SFU + worker + bridge + realtime session sharing one pod, that trips under load and the SFU reports "no workers with sufficient capacity."
Fix (both are agent-side; neither needs a veris-sandbox change):
1. Gate the bridge on the worker's registered worker log line (poll, ~60s ceiling), not a fixed sleep — see the start.sh in Pattern 9. 2. Disable the throttle: AgentServer(load_fnc=lambda *_: 0.0).
Vapi calls fail to connect under concurrency (ngrok contention)
Symptom: a Vapi-based voice_ws agent passes single smoke tests and small batches, but in a larger concurrent batch most calls end in callee_no_answer — the agent never finishes setting up the Vapi call. Agent logs show repeated ngrok spawn attempts ending in a session-limit error (e.g. ERR_NGROK_334).
Cause: Vapi delivers tool calls as HTTP webhooks to a public server.url, and the common integration spawns an in-pod ngrok tunnel to provide one. Free-tier ngrok allows one agent session per authtoken — every concurrent pod contends for it, and the losers retry with backoff, exhaust their attempts, and fail call setup. It looks like a flaky agent; it's the tunnel.
Fix, in increasing order of robustness:
1. Serialize — run one simulation at a time; each gets the single tunnel in turn. 2. Remove the limit — a paid ngrok plan or a (free) Cloudflare Tunnel allows concurrent tunnels. 3. Shared stable endpoint (production shape) — set PUBLIC_BASE_URL to one public webhook endpoint so pods skip in-pod tunnels entirely, and route inside it by call.id. Vapi correlates tool results by toolCallId, not by connection, so one stateless endpoint serves the whole fleet.
See voice-channels.md → Vapi.
Vapi agent acts like its tool returned nothing
Symptom: the agent's logs show the tool executed and the /tool webhook returned a result, but the model behaves as if it got no observation — it stalls, apologizes, or claims it couldn't complete the action. Vapi's call logs show "No result returned".
Cause: the webhook response didn't match Vapi's schema. The result field must be a JSON string (not a dict) and the response must be HTTP 200 — anything else is silently dropped and the model continues with no observation. Nothing hangs and nothing errors, so the failure is invisible in the agent's own logs.
Fix: wrap every successful result with json.dumps(output, default=str) (single-line), return failures as a string under the error key, and always return 200. See the tool-result pitfall.
Database connection fails
Common causes:
1. Old docker hostname (postgres, db) instead of localhost 2. Password mismatch between services[].config.POSTGRES_PASSWORD and DATABASE_URL 3. Schema file copied to the wrong path 4. Wrong database name instead of SIMULATION_ID
Bundled service fails
Common causes:
1. Service package not installed in Dockerfile.sandbox 2. Service not started or not health-checked in start.sh 3. Port conflict 4. Service is too heavy and should have stayed external
Build fails
Common causes:
1. Wrong build context 2. Bad COPY path 3. Dependency manifest copied incorrectly 4. Missing system package 5. WORKDIR /app omitted at the end
The correct local smoke-test command is:
docker build -f .veris/Dockerfile.sandbox .from the repo root.
Build fails due to source-tree compile errors
If pip install -e . or pip install . fails because the repo's own source files have syntax errors, broken imports, or missing type stubs:
1. Check whether the repo is a platform-hosted agent (config-only, framework-as-runtime). If so, do not install the repo as an editable package. 2. Instead, install the framework and its dependencies from published packages:
RUN pip install crewai langchain-openai # framework + plugins3. COPY only the config/prompt/tool files the framework needs — not the entire source tree as an installable package. 4. If the repo does contain real application code that must be installed, try pip install --no-build-isolation . or fix the specific compile errors. Common causes: missing build-system in pyproject.toml, Cython extensions without a C compiler, or Python version mismatch.
veris env create scaffold produces broken config
If veris env create succeeds but reports a non-fatal config upload error (typically a 422 on the services list), the scaffolded veris.yaml has fields the backend does not accept.
This is expected — the scaffolded config is a placeholder with commented-out examples. Fix it:
1. Regenerate .veris/veris.yaml using the current preferred shape from reference/veris-yaml-schema.md 2. Ensure services: is a valid YAML list (not commented-out blocks that parse as an empty mapping) 3. Re-push with veris env push
veris env push returns 409: managed onboarding
If veris env push returns [409] Run veris env submit first to complete managed onboarding, the env was created in managed-setup mode (the default for plain veris env create). Managed-setup envs do not accept image pushes until onboarding completes. -f / --force does not bypass this — it only affects the local config sync-guard, not the server-side gate.
For this skill's audience (self-authoring .veris/), --self-serve at create time is the right fix. Three recovery paths in increasing order of effort:
1. You forgot `--self-serve`: veris env delete <env-id>, then veris env create --self-serve --name <name>. Re-set any veris env vars set values on the new env, and update .veris/config.yaml if the env id changed. 2. Your CLI doesn't show `--self-serve`: upgrade to veris-cli 2.27.0 or newer using the install manager that owns veris (uv tool upgrade veris-cli or pip install -U veris-cli), then take path 1. 3. You actually want managed onboarding: run veris env submit, wait for the Veris team's email, then veris env config pull followed by veris env push.
Base image runtime version too old for agent
If the agent requires a newer Python or Node.js than the base image provides:
1. Check the agent's dependency manifests for explicit version constraints (python_requires >= "3.13", engines.node >= "20") 2. Add a runtime version override to Dockerfile.sandbox — see the "Runtime version override" section in templates/dockerfile-sandbox.md 3. For Python, install the newer version alongside the base and create a dedicated virtualenv 4. For Node.js, overlay the newer binary and it will replace the base version on PATH
Runtime env vars are missing
Common causes:
1. Secret was never set with veris env vars set 2. The value belongs in agent.environment but is missing there 3. The user expected .env.simulation, which is no longer the preferred flow 4. Secret was set via shell interpolation (veris env vars set KEY="$VAR" --secret) but $VAR was empty or unset — the CLI saved an empty string with no error
Fix:
- use
veris env vars setfor secrets and per-env overrides - use
agent.environmentfor stable non-secret defaults - optionally mirror local values in a root
.envfor local-only smoke tests - if a secret might have been set empty, verify with
printenv VARbefore setting, or re-set it with a literal value
Bundling Recipes
Step-by-step recipes for installing and starting common infrastructure services inside a veris-sandbox container.
The base image is Debian-based with Python 3.12 and apt-get available. When an agent depends on a service that veris does not mock (e.g., Redis, Elasticsearch), install it in Dockerfile.sandbox and start it via start.sh before the agent process starts.
---
Redis
Weight: Light — ~30 MB memory, ~5 MB image size increase.
Install (Dockerfile.sandbox):
RUN apt-get update && apt-get install -y redis-server && rm -rf /var/lib/apt/lists/*Start (start.sh):
redis-server --daemonize yes --maxmemory 128mb --maxmemory-policy allkeys-lruHealth check:
until redis-cli ping 2>/dev/null | grep -q PONG; do sleep 0.5; doneEnv var overrides:
REDIS_URL=redis://localhost:6379/0
REDIS_HOST=localhost
REDIS_PORT=6379
CELERY_BROKER_URL=redis://localhost:6379/0Memory: ~30 MB baseline. Set --maxmemory to 128-256 MB.
Notes: No password needed in sandbox. If the agent uses Redis as a Celery broker, the same URL works.
---
Elasticsearch (single-node)
Weight: Heavy — ~512 MB memory (256 MB heap minimum), ~500 MB image size increase. Confirm with user before bundling — external endpoint may be preferable.
Install (Dockerfile.sandbox):
RUN curl -fsSL https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.12.0-linux-x86_64.tar.gz | tar xz -C /opt/ && \
mv /opt/elasticsearch-8.12.0 /opt/elasticsearch && \
/opt/elasticsearch/bin/elasticsearch-plugin remove x-pack-ml 2>/dev/null || trueStart (start.sh):
ES_JAVA_OPTS="-Xms256m -Xmx256m" /opt/elasticsearch/bin/elasticsearch \
-d -p /tmp/es.pid \
-Ediscovery.type=single-node \
-Expack.security.enabled=false \
-Ecluster.routing.allocation.disk.threshold_enabled=falseHealth check:
until curl -sf http://localhost:9200/_cluster/health >/dev/null; do sleep 1; doneEnv var overrides:
ELASTICSEARCH_URL=http://localhost:9200
ES_HOST=localhost
ES_PORT=9200Memory: 256 MB heap minimum, ~512 MB total. Heaviest bundleable service.
Notes: Disable security (xpack.security.enabled=false) for sandbox. Single-node mode is required. If the agent creates indices at startup, they work against this instance.
---
RabbitMQ
Weight: Medium — ~80 MB memory, ~30 MB image size increase.
Install (Dockerfile.sandbox):
RUN apt-get update && apt-get install -y rabbitmq-server && rm -rf /var/lib/apt/lists/*Start (start.sh):
rabbitmq-server -detachedHealth check:
until rabbitmqctl status >/dev/null 2>&1; do sleep 1; doneEnv var overrides:
AMQP_URL=amqp://guest:guest@localhost:5672
RABBITMQ_URL=amqp://guest:guest@localhost:5672
RABBITMQ_HOST=localhostMemory: ~80 MB baseline.
Notes: Default guest/guest credentials. Management plugin on port 15672 is optional: rabbitmq-plugins enable rabbitmq_management.
---
MinIO (S3-compatible)
Weight: Light — ~50 MB memory, ~90 MB image size increase (single binary).
Install (Dockerfile.sandbox):
RUN curl -fsSL https://dl.min.io/server/minio/release/linux-amd64/minio -o /usr/local/bin/minio && \
chmod +x /usr/local/bin/minioStart (start.sh):
MINIO_ROOT_USER=minioadmin MINIO_ROOT_PASSWORD=minioadmin minio server /data/minio --console-address ":9001" &Health check:
until curl -sf http://localhost:9000/minio/health/live >/dev/null; do sleep 0.5; doneEnv var overrides:
AWS_ENDPOINT_URL=http://localhost:9000
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
S3_ENDPOINT=http://localhost:9000
MINIO_ENDPOINT=localhost:9000Memory: ~50 MB baseline.
Notes: Use minioadmin/minioadmin as default credentials. Agents using boto3 or any S3 SDK work with the AWS_ENDPOINT_URL override. To create buckets at startup, add to start.sh after the health check:
mc alias set local http://localhost:9000 minioadmin minioadmin && mc mb local/my-bucket---
SQLite
Weight: None — zero overhead, built into Python stdlib.
Start: No daemon. File-based.
Env var overrides:
DB_PATH=/tmp/agent.db
SQLITE_PATH=/tmp/agent.dbNotes: Simplest option. If the agent uses SQLite, just ensure the file path is writable. Works out of the box.
---
LocalStack (AWS SDK mock)
Weight: Heavy — ~200 MB memory, ~300 MB image size increase. Confirm with user before bundling — for just S3, prefer MinIO (much lighter).
Install (Dockerfile.sandbox):
RUN pip install localstack localstack-client awscli-localStart (start.sh):
localstack start -dHealth check:
until curl -sf http://localhost:4566/_localstack/health >/dev/null; do sleep 1; doneEnv var overrides:
AWS_ENDPOINT_URL=http://localhost:4566
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
AWS_DEFAULT_REGION=us-east-1Memory: ~200 MB. Heavy.
Notes: Mocks S3, SQS, DynamoDB, Lambda, and many other AWS services. Use if the agent relies on multiple AWS services. For just S3, prefer MinIO (lighter).
---
Memcached
Weight: Light — ~64 MB memory (configurable), ~2 MB image size increase.
Install (Dockerfile.sandbox):
RUN apt-get update && apt-get install -y memcached && rm -rf /var/lib/apt/lists/*Start (start.sh):
memcached -d -m 64 -p 11211 -u rootHealth check:
echo stats | nc localhost 11211 | grep -q pidEnv var overrides:
MEMCACHED_HOST=localhost
MEMCACHED_PORT=11211
MEMCACHED_URL=localhost:11211Memory: 64 MB (configurable via the -m flag).
---
Node.js Version Override (only when the repo needs a different Node.js version)
Weight: Light — ~0 MB extra memory (runtime only), ~80 MB image size increase.
Install (Dockerfile.sandbox):
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs && \
rm -rf /var/lib/apt/lists/*Notes: The current base image already includes Node.js. Use this only when the repo requires a different Node.js version than the base image ships with. This includes npm. For pnpm: RUN npm install -g pnpm. For yarn: RUN npm install -g yarn.
---
General Guidelines
- Clean apt lists: Always end
apt-get installlines withrm -rf /var/lib/apt/lists/*. - Background services: Start services in
start.shwith&(background) or daemon flags (--daemonize,-d,-detached). - Health checks before agent: Always add a health-check wait loop in
start.shafter starting each service and before starting the agent. - Memory budget: All bundled services plus the agent share one container. Keep total memory reasonable.
- Threshold: If total bundled service memory exceeds ~1 GB, consider using external endpoints instead of bundling.
Principle
Prefer environment-variable overrides over code changes whenever possible.
Use the current split:
- Stable non-secret defaults ->
agent.environmentinveris.yaml - Secrets and per-environment values ->
veris env vars set - Local-only convenience -> root
.envor shell exports when doing local smoke tests
If the agent already reads a secret directly by name (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.), prefer veris env vars set and do not duplicate that key in veris.yaml.
Docker hostnames -> localhost
When the repo currently relies on docker-compose service names, rewrite them to localhost within the Veris container:
| Original | Veris override |
|---|---|
REDIS_HOST=redis | REDIS_HOST=localhost |
REDIS_URL=redis://redis:6379/0 | REDIS_URL=redis://localhost:6379/0 |
DATABASE_URL=postgresql://user:pass@postgres:5432/mydb | DATABASE_URL=postgresql://postgres:postgres@localhost:5432/SIMULATION_ID |
ELASTICSEARCH_URL=http://elasticsearch:9200 | ELASTICSEARCH_URL=http://localhost:9200 |
AMQP_URL=amqp://guest:guest@rabbitmq:5672 | AMQP_URL=amqp://guest:guest@localhost:5672 |
KAFKA_BOOTSTRAP_SERVERS=kafka:9092 | KAFKA_BOOTSTRAP_SERVERS=localhost:9092 |
MINIO_ENDPOINT=minio:9000 | MINIO_ENDPOINT=localhost:9000 |
Mock-service credentials
Salesforce
Use:
SALESFORCE_DOMAIN=mock-salesforceSALESFORCE_USERNAME=mock_user@simulation.testSALESFORCE_CONSUMER_KEY=mock_consumer_key_12345SALESFORCE_CONSUMER_SECRET=mock_consumer_secret_67890
Google Calendar / Drive / Docs
Use:
GOOGLE_APPLICATION_CREDENTIALS=/certs/mock-service-account.json
Slack
Use:
SLACK_BOT_TOKEN=xoxb-mock-token-for-veris-simulationSLACK_SIGNING_SECRET=mock-signing-secret
Postgres
Use:
DATABASE_URL=postgresql://postgres:{POSTGRES_PASSWORD}@localhost:5432/SIMULATION_ID
Jira / Confluence
Usually keep the agent’s existing Atlassian base URL and let DNS interception route it:
https://mycompany.atlassian.net
LLM providers
The LLM proxy handles supported providers automatically.
Usually you only need to set the real provider key with:
veris env vars set OPENAI_API_KEY=sk-... --secretor:
veris env vars set ANTHROPIC_API_KEY=sk-ant-... --secretNo services: entry is needed for the provider itself.
Optional services
If the repo imports SDKs for observability or optional tooling that are not critical to user-facing behavior, prefer disable flags over code changes when possible:
DD_TRACE_ENABLED=falseSENTRY_DSN=""NEW_RELIC_LICENSE_KEY=""
If the agent still crashes without the service, then it is not optional and must be mocked, bundled, or kept external.
veris env vars set vs agent.environment
Use agent.environment for:
- stable local URLs
- non-secret defaults
- values that need
${VAR}or${SIMULATION_ID}expansion
Use veris env vars set for:
- secrets
- environment-specific URLs
- values that differ between dev/staging/prod
Platform env vars set with veris env vars set take precedence over agent.environment.
Special values in veris.yaml
${VAR_NAME}-> expanded from runtime env${SIMULATION_ID}-> expanded from the simulation context when supported in the valueSIMULATION_IDin database URLs is also commonly used literally
For local runs, a root .env file or exported shell variables can still be useful, but do not generate .env.simulation.
Infrastructure Patterns — Restructuring Guide
Reference for converting agent infrastructure into the veris-sandbox single-container setup. Covers 7 common architecture patterns.
Use this file for architecture shapes only. For canonical service names, current veris.yaml schema, and current env-var flow, rely on:
reference/service-mapping.mdreference/veris-yaml-schema.mdreference/env-var-overrides.md
Veris-Sandbox Container Model
Everything runs in ONE Docker container. The agent's code is COPY'd to /agent/ by the Dockerfile.sandbox. The simulation config (veris.yaml) is mounted at /config/veris.yaml by the CLI at runtime — it is NOT baked into the image. Veris provides mock services, an LLM proxy, an actor simulator, and a simulation engine — all as co-resident processes. The agent starts via a single entry_point command defined in veris.yaml.
At startup, veris's entrypoint.sh does cd $AGENT_CODE_PATH (from agent.code_path in veris.yaml, typically /agent), then runs the entry_point command. This means all entry_point paths are relative to code_path.
Reserved Veris ports (do NOT use for the agent): 6100-6299, 5432, 443. Recommended agent ports: 8080, 3000, 3001.
---
Pattern 1: Docker Compose — Single Agent + Infrastructure
Typical setup: FastAPI app + Postgres + Redis + Elasticsearch + Celery workers + nginx, all defined in docker-compose.yml.
Characteristics: One service is the agent (the thing a user talks to). Everything else is infrastructure the agent depends on.
How to identify the agent service
Look for these signals in docker-compose.yml:
- Has the main HTTP port mapping (
ports: "8000:8000") - Has a
command:likeuvicorn app.main:app --host 0.0.0.0 --port 8000 - Other services
depends_onit, or it depends on everything else - Named something like
api,app,web,server,agent
Restructuring steps
1. Copy only the agent's code:
# Dockerfile.sandbox
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
COPY requirements.txt /agent/
WORKDIR /agent
RUN pip install --no-cache-dir -r requirements.txt
COPY ./src /agent/src/
WORKDIR /appOnly copy the directory the agent service mounts or builds from. Do NOT copy Postgres data dirs, nginx configs, etc.
2. Set the entry point:
# veris.yaml
agent:
entry_point: uvicorn app.main:app --host 0.0.0.0 --port 8080
port: 8080
code_path: /agentMatch the command: from docker-compose, adjusting the port if needed. Entry point paths are relative to code_path (entrypoint.sh cd's there first).
3. Replace infrastructure hostnames with localhost:
Docker compose services communicate by service name (postgres, redis, elasticsearch). In veris, everything is localhost. Override via environment variables:
# veris.yaml
agent:
environment:
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/${SIMULATION_ID}"
REDIS_URL: "redis://localhost:6379"
ELASTICSEARCH_URL: "http://localhost:9200"4. Handle Celery workers:
- If Celery processes tasks triggered during a user conversation (e.g., async tool calls, webhook processing), they are needed. Add them to a
start.sh:
#!/bin/bash
# Start Celery worker in background
celery -A app.celery_app worker --loglevel=info &
# Start the agent in foreground
exec uvicorn app.main:app --host 0.0.0.0 --port 8080Note: start.sh runs from code_path (/agent), so no cd needed at the top.
- If Celery only runs scheduled/cron tasks (daily reports, batch jobs), it is likely not needed during simulation. Skip it.
5. Skip nginx:
Veris has its own nginx for TLS termination. The agent's nginx reverse proxy config is not needed.
6. Bundle or skip other services:
| Service | Decision |
|---|---|
| Redis | Bundle if agent uses it for caching/sessions during requests. Install in Dockerfile.sandbox: apt-get install -y redis-server, start in start.sh. |
| Elasticsearch | Heavy (~500MB). Use external endpoint if possible. Bundle only if agent queries ES on the critical path. Ask user first |
| MinIO/S3 | Lightweight. Bundle if agent stores/retrieves files during conversation. |
| Monitoring (Prometheus, Grafana) | Skip. Not needed for simulation. |
---
Pattern 2: Single Container + supervisord / Multi-Process
Typical setup: supervisord managing FastAPI + webhook listener + alert poller + log shipper, all in one container.
Characteristics: Already single-container. Multiple processes managed by supervisord or a process manager.
Restructuring steps
1. Copy the entire app directory:
# Dockerfile.sandbox
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
COPY requirements.txt /agent/
WORKDIR /agent
RUN pip install --no-cache-dir -r requirements.txt
COPY ./app /agent/app/
WORKDIR /app2. Replace supervisord with start.sh:
Supervisord adds unnecessary complexity for simulation. Replace it with a shell script that backgrounds processes.
Given a supervisord.conf like:
[program:api]
command=uvicorn app.main:app --port 8080
[program:webhook_listener]
command=python webhook_listener.py
[program:alert_poller]
command=python alert_poller.py
[program:fluent-bit]
command=/opt/fluent-bit/bin/fluent-bit -c /etc/fluent-bit.confCreate this start.sh:
#!/bin/bash
# Triage: which processes are needed for simulation?
# NEEDED — webhook listener (persona/services send webhooks to agent)
python webhook_listener.py &
# NEEDED — alert poller (triggers agent behavior during simulation)
python alert_poller.py &
# SKIP — fluent-bit is a log shipper sidecar, agent code doesn't import it
# FOREGROUND — main API server (always needed)
exec uvicorn app.main:app --host 0.0.0.0 --port 80803. Process triage rules:
| Process type | Needed? |
|---|---|
| Main API server | Always. Runs in foreground with exec. |
| Webhook listener | Yes, if persona or veris services send webhooks to the agent. |
| Poller/scheduler | Yes, if it triggers agent actions during simulation time window. |
| Log shipper (fluent-bit, fluentd) | No, unless agent code directly imports/queries it. |
| Health check sidecar | No. Veris has its own health checks. |
| Metrics exporter | No. Not needed for simulation. |
4. SQLite:
If the agent uses SQLite, it works out of the box (file-based, no install needed). Ensure the path is writable — use /agent/data/ or /tmp/.
5. Entry point:
# veris.yaml
agent:
entry_point: bash start.sh
port: 8080
code_path: /agentEntry point paths are relative to code_path (entrypoint.sh cd's there first).
---
Pattern 3: Cloud-Specific Services (Azure / AWS / GCP)
Typical setup: Azure OpenAI + Azurite + Cosmos DB emulator + Azure FHIR, or AWS services via LocalStack, or GCP BigQuery + Cloud SQL.
Characteristics: Heavy use of cloud-native services, sometimes with local emulators in docker-compose for development.
Restructuring steps
1. Classify each cloud service:
| Service | Veris equivalent | Action |
|---|---|---|
| Azure OpenAI, OpenAI, Anthropic | LLM proxy (port 443) | Automatic interception, no config needed |
| Google Calendar API | Veris google/calendar service | Use the canonical service name and override the SDK endpoint/env vars as needed |
| Salesforce API | Veris salesforce service | Use the canonical service name and override the SDK endpoint/env vars as needed |
| Postgres (RDS, Cloud SQL, Neon) | Veris postgres service (5432) | Change DATABASE_URL to localhost |
| Azure Blob (via Azurite) | Bundle Azurite or MinIO | Lightweight, works in-container |
| Cosmos DB emulator | External endpoint | Too heavy (~2GB) to bundle |
| LocalStack (full) | Evaluate per-service | ~200MB. Bundle only if agent uses multiple AWS services |
| LocalStack (just S3) | Bundle MinIO instead | MinIO is lighter and S3-compatible |
| FHIR, BigQuery, Snowflake | External endpoint | No veris mock. User provides staging URL + credentials. |
2. Cloud SDK endpoint overrides:
Most cloud SDKs read endpoint URLs from environment variables. No code changes needed — just override the env vars:
# veris.yaml
agent:
environment:
# Azure Blob → local Azurite
AZURE_STORAGE_CONNECTION_STRING: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=...;BlobEndpoint=http://localhost:10000/devstoreaccount1"
# AWS → local LocalStack or MinIO
AWS_ENDPOINT_URL: "http://localhost:4566"
AWS_ACCESS_KEY_ID: "test"
AWS_SECRET_ACCESS_KEY: "test"
# GCP → emulator
STORAGE_EMULATOR_HOST: "http://localhost:9023"3. LLM proxy — automatic interception:
Veris intercepts calls to api.openai.com, api.anthropic.com, and Azure OpenAI endpoints via DNS aliasing + TLS termination. The agent's LLM calls are proxied transparently. No environment variable changes needed for LLM endpoints.
4. Bundle lightweight emulators in Dockerfile.sandbox:
# Dockerfile.sandbox
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
# Install Azurite (Azure Blob emulator)
RUN npm install -g azurite
# Agent code
COPY requirements.txt /agent/
WORKDIR /agent
RUN pip install --no-cache-dir -r requirements.txt
COPY ./src /agent/src/
WORKDIR /appStart emulators in start.sh:
#!/bin/bash
# Start Azurite in background
azurite --silent --location /tmp/azurite --debug /tmp/azurite-debug.log &
# Start agent in foreground
exec uvicorn app.main:app --host 0.0.0.0 --port 80805. External endpoints for heavy/unmocked services:
For services without a veris mock or lightweight emulator, the user must provide an external endpoint:
# veris.yaml
agent:
environment:
COSMOS_DB_ENDPOINT: "https://staging-cosmos.documents.azure.com:443/"
COSMOS_DB_KEY: "<user-provided-key>"
BIGQUERY_PROJECT: "staging-project-id"---
Pattern 4: Serverless / No Docker (Vercel, Railway, Fly.io)
Typical setup: Next.js on Vercel + Neon Postgres + Upstash Redis + Pinecone. No Dockerfile exists.
Characteristics: Agent has never been containerized. Deployed to a PaaS. Uses managed/serverless databases.
Restructuring steps
1. Determine the runtime:
| Framework | Runtime | Install in Dockerfile.sandbox? |
|---|---|---|
| FastAPI, Flask, Django | Python | No (already in base image) |
| Next.js, Express, Hono | Node.js | No (already in base image) |
| Go (Gin, Echo) | Go | Yes |
| Rust (Actix, Axum) | Rust | Yes |
2. Install non-base runtimes:
# Dockerfile.sandbox — Node.js agent example
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
# Copy agent code and install dependencies
COPY package.json package-lock.json /agent/
WORKDIR /agent
RUN npm ci --production
COPY ./src /agent/src
COPY ./next.config.js /agent/
COPY ./tsconfig.json /agent/
# Build step (Next.js needs this)
RUN cd /agent && npm run build
# Prisma (if used)
COPY ./prisma /agent/prisma
RUN cd /agent && npx prisma generate
WORKDIR /app3. Map serverless databases to veris mocks:
| Serverless DB | Veris replacement |
|---|---|
| Neon Postgres | Postgres mock. DATABASE_URL=postgresql://postgres:postgres@localhost:5432/${SIMULATION_ID} |
| PlanetScale (MySQL) | No veris mock. Bundle MySQL or use external endpoint. |
| Upstash Redis | Bundle Redis. REDIS_URL=redis://localhost:6379 |
| Pinecone, Weaviate | No veris mock. Use external endpoint. |
| Supabase | Postgres mock for the DB. Auth/storage need external endpoint. |
4. Entry point:
# veris.yaml
agent:
entry_point: npx next start -p 8080
port: 8080
code_path: /agentFor Python serverless frameworks (FastAPI on Railway):
agent:
entry_point: uvicorn app.main:app --host 0.0.0.0 --port 8080
port: 8080
code_path: /agentEntry point paths are relative to code_path (entrypoint.sh cd's there first).
5. Handle Vercel/Railway-specific config:
vercel.json: Ignorecron,rewrites,regions,functions. The agent runs as a standard server.railway.toml/fly.toml: Extract thestart_command— that becomes your entry point.Procfile(Heroku/Railway): Theweb:line is the entry point.
6. Environment variables:
NEXT_PUBLIC_*vars must be set at build time (in the Dockerfile RUN build step), not just at runtime.- All other env vars go in
veris.yamlagent.environment. - Do NOT copy
.env.localor.env.productioninto the container. Declare all needed vars explicitly in veris.yaml.
7. API routes:
Next.js API routes (app/api/*/route.ts) work as-is — they are standard HTTP endpoints when running next start. No special handling needed.
---
Pattern 5: Multi-Agent System with Gateway
Typical setup: API gateway + flight-agent + hotel-agent + activity-agent + itinerary-agent + Kafka or NATS for inter-agent messaging.
Characteristics: Multiple separate agent services that coordinate to handle requests. A gateway routes to sub-agents.
Restructuring steps
1. Copy all agent code:
# Dockerfile.sandbox
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
# Install all dependencies from all sub-agents
COPY gateway/requirements.txt /tmp/gateway-req.txt
COPY flight-agent/requirements.txt /tmp/flight-req.txt
COPY hotel-agent/requirements.txt /tmp/hotel-req.txt
COPY itinerary-agent/requirements.txt /tmp/itinerary-req.txt
RUN pip install --no-cache-dir \
-r /tmp/gateway-req.txt \
-r /tmp/flight-req.txt \
-r /tmp/hotel-req.txt \
-r /tmp/itinerary-req.txt
# Copy each sub-agent
COPY ./gateway /agent/gateway/
COPY ./flight-agent /agent/flight-agent/
COPY ./hotel-agent /agent/hotel-agent/
COPY ./itinerary-agent /agent/itinerary-agent/
WORKDIR /appIf sub-agents share a requirements.txt, install it once. If they have conflicts, use separate virtualenvs (more complex — try to unify first).
2. Determine the persona-facing port:
The gateway is what the persona talks to. Its port goes in veris.yaml:
agent:
entry_point: bash start.sh
port: 8080
code_path: /agent3. Create start.sh with health-check waits:
#!/bin/bash
# Start sub-agents in background
(cd /agent/flight-agent && uvicorn main:app --host 0.0.0.0 --port 8081) &
(cd /agent/hotel-agent && uvicorn main:app --host 0.0.0.0 --port 8082) &
(cd /agent/itinerary-agent && uvicorn main:app --host 0.0.0.0 --port 8083) &
# Wait for sub-agents to be ready
for port in 8081 8082 8083; do
echo "Waiting for service on port $port..."
until curl -sf http://localhost:$port/health > /dev/null 2>&1; do sleep 1; done
echo "Service on port $port is ready."
done
# Start gateway in foreground
cd /agent/gateway
exec uvicorn main:app --host 0.0.0.0 --port 8080Veris already launches start.sh from agent.code_path. If you need to start background work from a subdirectory, use an explicit absolute-path subshell like (cd /agent/flight-agent && ...) &. If the foreground process lives in a subdirectory, cd there immediately before the final exec.
4. Override inter-agent communication URLs:
Docker compose hostnames become localhost:
# veris.yaml
agent:
environment:
FLIGHT_AGENT_URL: "http://localhost:8081"
HOTEL_AGENT_URL: "http://localhost:8082"
ITINERARY_AGENT_URL: "http://localhost:8083"5. Handle message queues (Kafka, NATS, RabbitMQ):
This is the biggest decision for multi-agent systems:
| Option | When to use |
|---|---|
| Bundle Kafka | Only if event streaming is core to agent logic (e.g., agents react to event streams, not just request-response). Very heavy (~500MB+ with Zookeeper). |
| Bundle RabbitMQ | If message routing/acknowledgment matters. Lighter than Kafka (~150MB). |
| Bundle Redis pub/sub | If the queue is just for async task dispatch. Lightest option. |
| External endpoint | If bundling is too heavy. User provides a managed Kafka/NATS URL and points the agent at it via env var. |
If bundling Kafka, add to start.sh:
# Start Zookeeper and Kafka
/opt/kafka/bin/zookeeper-server-start.sh -daemon /opt/kafka/config/zookeeper.properties
sleep 3
/opt/kafka/bin/kafka-server-start.sh -daemon /opt/kafka/config/server.properties
sleep 3
# Create required topics
/opt/kafka/bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --topic agent-events --partitions 1 --replication-factor 1 2>/dev/null || true6. gRPC between agents:
gRPC works on localhost. Just update the target addresses:
agent:
environment:
FLIGHT_AGENT_GRPC: "localhost:50051"
HOTEL_AGENT_GRPC: "localhost:50052"---
Pattern 6: Hybrid Frontend + Backend Workers
Typical setup: Next.js frontend + Python workers (content generator, image processor, scheduler) + RabbitMQ or Redis queue.
Characteristics: Mixed-language stack. Frontend handles user interaction; backend workers process tasks from a queue.
Restructuring steps
1. Identify the persona-facing process:
The persona interacts with the frontend (HTTP chat endpoint). The frontend port goes in veris.yaml.
2. Install both runtimes:
# Dockerfile.sandbox
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
# Install RabbitMQ (if needed as message broker)
RUN apt-get update && apt-get install -y rabbitmq-server && rm -rf /var/lib/apt/lists/*
# Copy and install frontend
COPY frontend/package.json frontend/package-lock.json /agent/frontend/
RUN cd /agent/frontend && npm ci && npm run build
COPY ./frontend /agent/frontend/
# Copy backend workers
COPY backend/requirements.txt /agent/backend/requirements.txt
RUN pip install --no-cache-dir -r /agent/backend/requirements.txt
COPY ./backend /agent/backend/
WORKDIR /app3. Create start.sh:
#!/bin/bash
# Start message broker
rabbitmq-server -detached
until rabbitmqctl status > /dev/null 2>&1; do sleep 1; done
echo "RabbitMQ is ready."
# Start Python workers in background
(cd /agent/backend/content-worker && python main.py) &
(cd /agent/backend/image-processor && python main.py) &
# Start frontend in foreground
cd /agent/frontend
exec npx next start -p 80804. veris.yaml:
agent:
entry_point: bash start.sh
port: 8080
code_path: /agent5. Queue alternatives:
If the original stack uses RabbitMQ, it can be bundled (~150MB). For lighter alternatives:
| Original | Lighter alternative | Trade-off |
|---|---|---|
| RabbitMQ | Redis with rq or celery[redis] | Simpler, less robust routing |
| Kafka | Redis Streams | Only works for simple pub/sub patterns |
| SQS (AWS) | Use real SQS via outbound egress, or keep SQS external with a managed endpoint | Don't patch the agent to swap SDKs — that diverges from how it runs in prod |
6. Skip monitoring and observability:
- Prometheus + Grafana: skip unless agent code queries the Prometheus metrics API at runtime.
- Jaeger/Zipkin tracing: skip. Tracing is for debugging, not simulation.
- ELK stack: skip. Logs go to stdout in the container.
7. Shared state between frontend and workers:
If frontend and workers share state via Redis or a database, ensure both point to the same instance:
agent:
environment:
REDIS_URL: "redis://localhost:6379"
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/${SIMULATION_ID}"
RABBITMQ_URL: "amqp://guest:guest@localhost:5672/"---
Pattern 7: Custom / Doesn't Match Above
When the agent architecture doesn't fit any of the patterns above, use this decision framework.
Questions to ask the user
1. Which process handles user/persona interaction? This is the HTTP endpoint where a user sends a chat message. It becomes the entry point and its port goes in veris.yaml. 2. What happens when a user sends a message? Trace the full request path: API server -> queue -> worker -> database -> response. Every service on this path must run in the container. 3. Which services are on the critical path for responding? If the agent can't respond without a service, it must be bundled or connected externally. 4. Are there background processes that MUST run during simulation? Pollers, schedulers, webhook listeners that trigger agent behavior.
Decision framework for each dependency
Is it a database?
├── Postgres → Use veris postgres mock (port 5432)
├── SQLite → Works as-is (file-based)
├── MongoDB → Bundle (apt-get install -y mongod) or external
└── Other → Bundle if lightweight, external if heavy
Is it a message queue?
├── Redis → Bundle (lightweight, ~50MB)
├── RabbitMQ → Bundle (~150MB) or replace with Redis
├── Kafka → External endpoint or replace with Redis Streams
└── NATS → Bundle (single binary, very lightweight)
Is it an API the agent calls?
├── OpenAI/Anthropic/Azure OpenAI → Automatic (Veris LLM proxy)
├── Google Calendar → Use Veris `google/calendar` when it maps cleanly
├── Salesforce → Use Veris `salesforce` when it maps cleanly
├── Stripe → Use Veris `stripe` when it maps cleanly
├── Jira → Use Veris `jira` when it maps cleanly
└── Other → External endpoint (user provides URL + credentials)
Is it a runtime/language?
├── Python → Already in base image
├── Node.js → Already in base image
├── Go → Install or compile binary in Dockerfile.sandbox
└── Other → Install in Dockerfile.sandbox
Is it infrastructure tooling?
├── nginx → Skip (veris has its own)
├── Monitoring (Prometheus, Grafana) → Skip
├── Log shipping (fluent-bit, fluentd) → Skip
├── Service mesh (Envoy, Istio) → Skip
└── Health check sidecar → SkipGeneral principles
- The entry point process runs in foreground (with
execin start.sh). - Supporting processes run in background (with
&in start.sh). - All inter-process communication uses localhost.
- Agent port = the port the persona sends requests to.
- When in doubt, start minimal and add services only when the agent fails without them.
---
Pattern 8: Platform-Hosted Agent (Framework-as-Runtime)
Typical setup: The repo contains configuration files, prompt templates, and tool definitions but not a standalone application. The agent runs on an installed framework CLI or server: LangServe, CrewAI, AutoGen, Dify, n8n, Flowise, or similar.
Characteristics: No main.py / index.js / application entrypoint. The "source code" is YAML/JSON config, prompt files, and possibly a few small Python/JS files that define tools or hooks. The framework is the runtime.
How to identify
- The repo has no traditional app entrypoint (
app.py,main.py,server.js,index.ts) - The primary files are configuration:
crew.yaml,agents.yaml,flows.json,docker-compose.ymlthat just runs a framework image pyproject.tomlorpackage.jsonlists the framework as a dependency but there is no substantial application code- The README says "install [framework], then run [framework CLI command]"
Restructuring steps
1. Install the framework in the Dockerfile:
The framework is installed from a package manager, not built from source:
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
# Install the framework
RUN pip install crewai # or: npm install -g langserve, etc.
# Copy config and tool definitions
COPY . /agent/
WORKDIR /appIf the repo has a pyproject.toml or requirements.txt that lists the framework plus tool dependencies, install from that:
COPY requirements.txt /agent/
WORKDIR /agent
RUN pip install --no-cache-dir -r requirements.txt
COPY . /agent/
WORKDIR /app2. Determine the entry point:
The entry point is the framework's CLI or server command:
# veris.yaml
agent:
entry_point: crewai run # or: langserve start, etc.
port: 8080
code_path: /agentCheck the framework's docs or the repo's README for the canonical start command. Common patterns:
- CrewAI:
crewai runorpython -m crewai run - LangServe:
langchain serve --port 8080 - Dify: container with built-in server
- n8n:
n8n start
3. Determine the channel interface:
Most frameworks expose an HTTP API. Check the framework's docs for:
- The chat/invoke endpoint path
- The request/response JSON shape
- Whether it streams (SSE) or returns a complete response
If the framework does not expose a network API and instead runs as a one-shot CLI, this is a platform gap, not a wrapper opportunity. Surface it to the user — the agent's production deployment model probably involves something driving that CLI (cron, a job runner, a shell script), and we want the simulation to drive it the same way, not via a Python adapter that fakes a callable. Do not create a wrapper Python file to paper over this.
4. Handle framework-specific config:
- Environment variables the framework reads (API keys, model names, etc.)
- Config file paths the framework expects (may need to be at a specific location)
- Tool/plugin registrations that reference local files
5. Watch for source-tree compile errors:
Platform-hosted repos sometimes have Python files with syntax errors or import failures because the developer only ever ran them through the framework (which may not import all files). If pip install -e . or the build fails due to bad source files, install the framework and config from published packages and COPY only the config files — do not install the repo as an editable package.
---
Pattern 9: Transport bridge
Typical setup: The agent framework's native transport doesn't match what the actor channel speaks on the wire. The most common case is voice agents whose framework uses WebRTC end-to-end (LiveKit Agents, Daily, anything SFU-based) but where the Veris actor expects raw PCM16 over a WebSocket (the `voice_ws` channel). It also covers agents wired to SIP/Twilio media streams, agents that speak a custom binary protocol, and any case where the framework's transport layer is fixed and not negotiable.
Characteristics: The agent ships, in production, against a transport you cannot bypass. Pointing the actor at the agent directly produces a protocol mismatch — the actor sends bytes the framework can't decode, or vice versa. The right answer is not to rewrite the agent's transport layer; it's to run a small bridge process inside the sandbox container that translates between the actor's channel format and whatever the framework already speaks.
How to identify
- The agent uses a framework with a fixed media transport (WebRTC SFU, SIP media, etc.) that doesn't downgrade to raw audio over WS.
- The actor channel you need is
voice_ws(or any channel whose wire format the framework can't speak directly). - The framework's published transport plugins don't include one for the actor's channel format. For voice agents specifically, the quick check is: can the framework serve a raw PCM16 WebSocket? If yes (Pipecat with
RawAudioFrameSerializer, most in-house frameworks), use that transport and skip this pattern — you don't need a bridge. If no (LiveKit Agents, Daily SDK, anything WebRTC-only), continue with this pattern.
Architecture
┌─────────────────┐ actor channel ┌────────────────────────┐
│ Veris actor │ ──── voice_ws ───▶ │ bridge process │
│ (in sandbox) │ ◀── PCM16 frames ─ │ (in sandbox container)│
└─────────────────┘ └───────────┬────────────┘
│
framework's native transport
(WebRTC room / SIP media /
custom envelope / etc.)
│
▼
┌────────────────────────┐
│ agent framework │
│ + your agent code │
│ (in sandbox container)│
└────────────────────────┘The bridge is the thin middle layer. It:
1. Accepts the actor's channel connection (a WebSocket on the port veris.yaml advertises). 2. Stands up the framework's native transport on the same host — for a WebRTC framework, that means joining a LiveKit room as a participant; for SIP, opening a media socket; for a custom envelope, opening the framework's WS and wrapping/unwrapping its envelope. 3. Pumps audio (or other media) in both directions, with whatever framing translation the wire formats require.
The agent code itself doesn't change. It participates in the framework's transport exactly the way it does in production — same room, same SDK, same tools, same prompt. The bridge is purely a sandbox-edge translator.
Restructuring steps
1. Identify the framework's native transport. What does the agent actually speak when deployed for real? Browser-to-LiveKit-room? SIP trunk? A proprietary WSS with a specific envelope? That's the transport the bridge has to terminate.
2. Decide what runs in the sandbox container. For an in-container WebRTC bridge you typically need three peer processes:
| Process | Purpose |
|---|---|
Framework's media server (e.g., livekit-server --dev) | Provides the rooms the agent and bridge will both join. Single binary, in-container. |
| Agent worker | The framework SDK auto-dispatches into every new room. Talks to LLM, runs tools, etc. |
| Bridge | FastAPI (or equivalent) listening on the actor's port. For each connection: join a fresh room as a participant, pump frames. |
For a SIP-style bridge it's typically two processes (the SIP stack and the agent), but the shape is the same — one in-container service plus a small translation layer.
3. Write the bridge as a normal piece of the repo, not in `.veris/`. Treat it as production code. If a future product surface needs to call the same agent over a raw WS (mobile app, kiosk, IVR vendor), the bridge ships with that product too — it's not Veris-specific. Put it next to the agent (app/bridge.py or similar), exercise it in your own tests, and keep .veris/ as pure config plus the multi-process start.sh (template 4).
4. Wire the entry point. The bridge listens on the port that veris.yaml advertises as agent.port. The agent worker and the framework's media server run as background siblings in start.sh:
#!/bin/bash
# .veris/start.sh — three peer processes with fail-fast
# Intentionally no `set -e` here — see Template 4 for the rationale.
wait_for_port() { # poll a TCP port until it accepts, or give up
local host="$1" port="$2"
for _ in $(seq 1 100); do
(exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null && { exec 3>&- 3<&-; return 0; }
sleep 0.2
done
return 1
}
# 1. Framework's media server — wait until it actually accepts before continuing.
/usr/local/bin/livekit-server --dev --bind 0.0.0.0 &
LK_PID=$!
wait_for_port localhost 7880 || { echo "livekit-server never came up" >&2; exit 1; }
# 2. Agent worker. It registers with the SFU *asynchronously*; capture its log
# so we can gate on real registration, and mirror it to stdout for agent.log.
uv run --no-sync python -m app.agent start > /tmp/worker.log 2>&1 &
AG_PID=$!
tail -f /tmp/worker.log & TAIL_PID=$!
# 3. GATE the bridge on the worker registering — NOT a fixed sleep. Auto-dispatch
# only fires for rooms created *after* the worker registers; under load that
# can take 10s+. Accept a caller too early and the agent never joins the room
# (the actor sees callee_no_answer). See "LiveKit dispatch gotchas" below.
for _ in $(seq 1 120); do
grep -q "registered worker" /tmp/worker.log 2>/dev/null && break
kill -0 "$AG_PID" 2>/dev/null || break
sleep 0.5
done
# 4. Bridge — listens on the actor's port.
uv run --no-sync uvicorn app.bridge:app --host 0.0.0.0 --port "${PORT:-8080}" &
BR_PID=$!
cleanup() { kill "$LK_PID" "$AG_PID" "$BR_PID" "$TAIL_PID" 2>/dev/null || true; }
trap 'cleanup; exit 143' TERM INT
# Fail-fast: when any peer dies, take down the rest so Veris restarts cleanly.
wait -n
status=$?
cleanup
wait || true
exit "$status"See start-sh.md Template 4 for the rationale around wait -n and why this shape avoids set -e.
5. Pull the framework's binary into the image. For LiveKit, the cleanest path is a multi-stage Dockerfile that copies the prebuilt static binary from the official image:
ARG GVISOR_BASE
FROM livekit/livekit-server:latest AS lk-stage
FROM ${GVISOR_BASE}
# Framework media server (Go static binary — works on the gVisor base).
COPY --from=lk-stage /livekit-server /usr/local/bin/livekit-server
# Normal agent install
COPY pyproject.toml /agent/
WORKDIR /agent
RUN uv sync --no-dev
COPY app /agent/app
COPY .veris/start.sh /agent/start.sh
RUN chmod +x /agent/start.sh
WORKDIR /appFor SIP or other media stacks, install the daemon from the distribution's package manager in the same Dockerfile.sandbox and skip the multi-stage copy.
Worked example: LiveKit Agents over voice_ws
For a LiveKit-based voice agent driven through Veris's voice_ws actor:
- The framework's media server is
livekit/livekit-serverin dev mode (devkey/secret). Self-contained, no external LiveKit Cloud account needed. - The agent worker uses
livekit-agentswith whatever realtime LLM you want —openai.realtime.RealtimeModel,google.realtime.RealtimeModel,aws.realtime.RealtimeModel.with_nova_sonic_2, or a chained STT/LLM/TTS pipeline. The worker is identical to the production worker; no Veris-specific code path. - The bridge accepts the
voice_wsconnection, mints a LiveKit access token (usinglivekit-apiand the in-container dev creds), joins a fresh room as a participant calledveris-actor, publishes incoming PCM16 frames viartc.AudioSource.capture_frame(AudioFrame(...)), and subscribes to the agent's audio track viartc.AudioStream(track, sample_rate=24000, num_channels=1), writing each frame back out asws.send_bytes(bytes(frame.data)).
The agent code (its Agent subclass, @function_tool() methods, prompt, DB-backed tool dispatch) is untouched. In production it joins a room driven by a browser client or SIP gateway; in Veris it joins a room driven by the bridge. Same agent, same surface.
LiveKit dispatch gotchas
LiveKit Agents' worker/auto-dispatch model has two failure modes a plain WS server doesn't, and both surface identically: the actor connects, nobody answers, and the sim ends in callee_no_answer. They only bite under concurrent cluster load — a single local smoke test passes and the problem appears at scale. Both fixes live in the agent's own code; neither needs a veris-sandbox change.
1. Gate the bridge on worker registration — never a fixed `sleep`. The worker registers with the SFU asynchronously, and the SFU only auto-dispatches into rooms created after registration. The bridge creates a room the instant the actor opens /voice; if that happens first, the worker is never dispatched into the room and the actor hears silence. A fixed sleep 5 races — under load registration can take 10s+. Gate on the real signal: tail the worker log and wait for the registered worker line (with a ceiling, ~60s) before the bridge starts accepting calls. This is the gate in the start.sh above — it replaces the naive sleep 1 that works locally and fails under load.
2. Disable the worker's CPU self-throttle. A start (prod-mode) AgentServer ships a CPU-based load function with a 0.7 threshold and refuses dispatch when the host is busy — the SFU then reports "no workers with sufficient capacity" and, again, the agent never joins. In the sandbox the SFU, worker, bridge, and the realtime session all share one CPU-bound pod, so under concurrent load that threshold trips constantly. This worker handles exactly one call per pod and must never refuse, so pin load to zero:
from livekit.agents import AgentServer
# Never self-throttle: one call per pod, must always accept dispatch.
server = AgentServer(load_fnc=lambda *_: 0.0)(dev mode already defaults the threshold to infinity for this reason; a prod-mode start worker does not.)
Symptom for both: intermittent callee_no_answer that worsens with concurrency. If a handful of parallel sims pass but a larger batch shows ~half failing to connect, suspect these before anything in the bridge or the audio path. See troubleshooting.md.
Hosted-runtime variant: Vapi (outbound bridge, inbound webhook)
When the framework is a hosted cloud runtime rather than an in-container stack, the bridge shape inverts. A Vapi agent needs no media server and no worker process — the agent process itself serves voice_ws and, per call, creates a Vapi call over the API and opens an outbound WSS to Vapi's cloud, pumping PCM16 both ways. One process in the container; no start.sh choreography.
What Vapi adds instead is an inbound requirement: tools are delivered as HTTP tool-calls webhooks from Vapi's cloud to a public server.url, so the pod must be publicly reachable. The two ways to provide that — an in-pod ngrok tunnel (with a hard free-tier concurrency limit: one agent session per authtoken) or a stable shared PUBLIC_BASE_URL endpoint — plus Vapi's silent string-result trap are covered in voice-channels.md → Vapi.
When not to use this pattern
- The framework can already serve the actor's channel directly. Most notably: Pipecat can serve
voice_wsnatively by configuringWebsocketServerTransportwith aRawAudioFrameSerializerinstead of the defaultProtobufFrameSerializer. If your framework has a transport plugin that emits/consumes bare PCM16 over WS, configure it and skip the bridge entirely — you keep one process in the container instead of three, and the failure modes are correspondingly simpler. (Pipecat's WS transports have one well-defined failure mode of their own — they don't honoraudio_out_auto_silence— so you still need a small silence-tail processor on the pipeline; see voice-channels.md for the recipe.) - The actor channel matches the framework's production transport. If the agent is a plain HTTP chat API and Veris is driving it over the
httpchannel, you're in Pattern 1, not 9. The bridge pattern only applies when the actor channel's wire format and the framework's transport differ. - You're tempted to "translate" semantic content (rewriting messages, adapting tool calls). That's a wrapper, not a bridge. A bridge translates transport, not behavior — the agent gets the same audio bytes it would get in production, just delivered via a different network path. If you find yourself reshaping tool-call JSON or normalizing speech-to-text output, stop; that's exactly the "no wrappers, no shims" rule from SKILL.md.
Cost
Adding a media-server peer process is real container weight — livekit-server is ~67 MB on disk, runs Go's network stack on top of gVisor, and binds a WebSocket plus a UDP/TCP RTC port internally. On a constrained sandbox, that's noticeable. The tradeoff is testing the agent against its production transport exactly as shipped. If the framework's transport can be swapped for a voice_ws-compatible one without lying about what production looks like (Pipecat's serializer config is the clean example), prefer that.
---
Common Rules Across All Patterns
These apply regardless of which pattern the agent matches.
File placement (inside the container at runtime)
| What | Where | How it gets there |
|---|---|---|
| Agent code | /agent/ | COPY'd by Dockerfile.sandbox. Matches code_path: /agent in veris.yaml |
| veris.yaml | /config/veris.yaml | Mounted by CLI at runtime (-v .veris/veris.yaml:/config/veris.yaml:ro). Do NOT COPY it. |
| start.sh (if needed) | /agent/start.sh | COPY'd by Dockerfile.sandbox. Referenced as entry_point: bash start.sh (relative to code_path) |
Dockerfile.sandbox requirements
Every Dockerfile.sandbox must end with:
WORKDIR /appThis is a veris requirement. The entrypoint.sh expects this working directory.
Port conflicts
Agent ports must NOT conflict with Veris service ports:
| Port range | Used by |
|---|---|
| 443 | Veris nginx (TLS termination, DNS interception) |
| 5432 | Veris Postgres service (native TCP) |
| 6100-6199 | Veris infrastructure services |
| 6200-6299 | Veris mock/application services |
Safe agent ports: 8080, 3000, 3001, 4000, 5000, 8008, 9000.
Hostname translation
Docker compose service names become localhost. Common translations:
# docker-compose hostnames → veris environment
postgres:5432 → localhost:5432
redis:6379 → localhost:6379
rabbitmq:5672 → localhost:5672
elasticsearch:9200 → localhost:9200
kafka:9092 → localhost:9092
api-gateway:8080 → localhost:8080start.sh template
When the agent needs multiple processes:
#!/bin/bash
set -e
# ---- Background services ----
# Start each supporting process and wait for readiness
some-service --daemon &
until some-health-check; do sleep 1; done
# ---- Background workers ----
(cd /agent/worker && python main.py) &
# ---- Foreground: main agent process ----
exec uvicorn app.main:app --host 0.0.0.0 --port 8080Rules for start.sh:
- Use
execfor the final (foreground) process so it receives signals correctly. - Use
&for background processes. - Add health-check waits (
until curl ...) before starting processes that depend on background services. - Use
set -eso the script fails fast if a critical setup step fails.
Veris Service Mapping Reference
Map real-world dependencies to the current canonical Veris service names.
Use this file when deciding what to put in services: and when migrating stale config that still uses old aliases like crm, calendar, or oracle.
Canonical service names
| Real service | Canonical Veris service | Common detection signals |
|---|---|---|
| Salesforce | salesforce | simple-salesforce, salesforce-bulk, SALESFORCE_*, SFDC_* |
| Google Calendar | google/calendar | google-api-python-client, calendar scopes, GOOGLE_CALENDAR_*, GOOGLE_APPLICATION_CREDENTIALS |
| PostgreSQL | postgres | psycopg2, asyncpg, sqlalchemy, pg, Prisma, DATABASE_URL, POSTGRES_* |
| Oracle Fusion Cloud | oracle/fscm | ORACLE_*, FUSION_* |
| Jira Cloud | atlassian/jira | jira, atlassian-python-api, jira-client, JIRA_*, ATLASSIAN_* |
| Confluence | atlassian/confluence | atlassian-python-api, CONFLUENCE_* |
| Stripe (MCP) | mcp/stripe | stripe, STRIPE_*, mcp.stripe.com |
| Shopify Storefront (MCP) | mcp/shopify-storefront | SHOPIFY_*, storefront APIs |
| Shopify Customer (MCP) | mcp/shopify-customer | customer account APIs, account.myshopify.com |
| Slack | slack | slack_sdk, slack_bolt, @slack/web-api, SLACK_* |
| Zendesk | zendesk-support | zendesk, ZENDESK_*, *.zendesk.com |
| Twilio | twilio | twilio, TWILIO_*, api.twilio.com |
| Microsoft Graph | microsoft/graph | msgraph, O365, GRAPH_*, graph.microsoft.com |
| Azure DevOps | microsoft/devops | azure-devops, AZDO_*, dev.azure.com |
| Google Drive | google/drive | drive scopes, drive.googleapis.com |
| HubSpot | hubspot | hubspot-api-client, HUBSPOT_*, api.hubapi.com |
| PagerDuty | pagerduty | pagerduty, PAGERDUTY_*, api.pagerduty.com |
| ServiceNow | servicenow | SERVICENOW_*, *.service-now.com |
| Epic / FHIR | epic/fhir | fhirclient, EPIC_*, FHIR_* |
| Splunk | splunk | splunk-sdk, SPLUNK_*, *.splunkcloud.com |
| Elastic / Elasticsearch | elastic | elasticsearch, elastic-transport, ELASTIC_*, *.elastic-cloud.com |
| Close CRM | close | closeio, CLOSE_*, api.close.com |
| SWIFT | swift | SWIFT_*, api.swift.com |
| OpenSanctions | opensanctions | opensanctions, api.opensanctions.org |
| You.com search | you | YOU_*, api.you.com, ydc-index.io |
| QuickBooks Online | intuit/quickbooks-online | quickbooks, INTUIT_*, quickbooks.api.intuit.com |
| Google Docs | google/docs | docs scopes, docs.googleapis.com |
| Zillow / Bridge | zillow | api.bridgedataoutput.com, real-estate MLS APIs |
Protocol and transport limitations
Some services support multiple protocol variants in production. The Veris mock may only support one. When integrating, confirm the agent uses the supported variant.
| Service | Supported protocol | Not supported | Migration notes |
|---|---|---|---|
| Slack | HTTP Events API (Web API calls over HTTPS) | Socket Mode (WebSocket via SLACK_APP_TOKEN) | Reconfigure the agent to HTTP mode. Set SLACK_SIGNING_SECRET instead of SLACK_APP_TOKEN. Handler code typically needs only minor changes (switch SocketModeHandler to HTTP adapter). |
| Twilio | REST API (HTTP) | WebSocket media streams | Agents processing real-time voice via Twilio WS streams cannot use the mock. Keep Twilio external or use a function channel to bypass the voice path. |
General rule: If a service has multiple transport modes (HTTP vs WebSocket, polling vs streaming, REST vs GraphQL), check which one the Veris mock implements. When the agent uses an unsupported mode, either reconfigure the agent to use the supported mode or classify the dependency as "external."
Legacy aliases to migrate away from
| Old alias | Current canonical name |
|---|---|
crm | salesforce |
calendar | google/calendar |
oracle | oracle/fscm |
If you see these old names in existing .veris/veris.yaml, migrate them to the current canonical names.
Auth helpers
These are platform-level helpers and usually should not be added manually:
google/authmicrosoft/authatlassian/authintuit/auth
If the main namespace service is active, Veris handles these helpers automatically where needed.
Email service
email is special:
- it is usually auto-injected when the actor uses an email channel
- you generally do not add it manually unless you have a specific reason
LLM providers
Do not add a service entry for OpenAI, Anthropic, Azure OpenAI, Google AI, Mistral, Groq, DeepSeek, Together, Fireworks, or Cohere.
The LLM proxy intercepts supported provider domains automatically.
Choosing between mock, bundle, and external
Use this mapping only to answer "does Veris have a mock for this?" It does not decide whether the service is:
- required on the critical path
- safe to skip
- better bundled locally
- better kept external
You still need to read the source code and classify the dependency honestly.
veris.yaml Reference For This Skill
Use this reference when generating or refreshing .veris/veris.yaml.
This skill targets the current preferred single-target shape used by veris env create. Do not generate legacy persona.modality config unless the user explicitly asks for compatibility.
Preferred shape
version: "1.0"
services:
- name: salesforce
dns_aliases:
- login.salesforce.com
actor:
init: # Optional pre-message setup call
type: http
method: POST
url: http://localhost:8080/api/session
channels:
- type: http # http | ws | email | function | voice_ws | browser-use
url: http://localhost:8080/chat
method: POST
headers:
Content-Type: application/json
request:
message_field: message
session_field: session_id
static_fields:
prompt_type: default
response:
type: json # json | sse
message_field: response
session_field: session_id
config:
MAX_TURNS: "1" # Optional; use only when needed
agent:
code_path: /agent
entry_point: python -m app.main
port: 8080
environment:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/SIMULATION_ID
LOG_LEVEL: infoCanonical choices
- Use
actor, notpersona - Use
channels, notmodality - Use
agent_inbox, notemail_address - Use canonical service names from
reference/service-mapping.md - Use uppercase env-style keys in
actor.config
Services
Each services[] entry may include:
services:
- name: postgres
dns_aliases: [] # Only for DNS-routed services
config:
POSTGRES_PASSWORD: postgres
SCHEMA_PATH: /agent/db/schema.sql
port: 5432 # Rarely needed
description: "..." # For generic services onlyNotes:
- Only add
dns_aliaseswhen the agent calls non-default domains - Only add
configwhen the service needs it - Do not add auth helper services (
google/auth,microsoft/auth, etc.) unless there is a specific reason
Actor channels
HTTP
actor:
channels:
- type: http
url: http://localhost:8080/chat
method: POST
request:
message_field: message
session_field: session_id
response:
type: json
message_field: responseWebSocket
actor:
channels:
- type: ws
url: ws://localhost:8080/ws
request:
message_field: message
session_field: session_id
response:
message_field: responseactor:
channels:
- type: email
agent_inbox: agent@email.test
poll_interval: 15poll_interval belongs on the email channel itself. Do not treat it as a general actor-global config knob.
Voice (voice_ws)
actor:
channels:
- type: voice_ws
url: ws://localhost:8080/voice
protocol: binary # "binary" (default) or "json"
language: en-US # optional, BCP-47
wait_for_callee_first: true # optional, default trueAudio is PCM16 / 24 kHz / mono. The protocol field selects the wire framing:
- `binary` — raw PCM16 bytes per WebSocket message; close = hangup. Matches Gemini Live, ElevenLabs, Vapi, AssemblyAI, Cartesia.
- `json` — JSON envelope
{"type":"audio","audio":"<b64 PCM16>"}plus{"type":"end"}for graceful close. Matches OpenAI Realtime, Twilio media streams, Deepgram.
See voice-channels.md for the full protocol, when to use a transport bridge for framework-native transports like LiveKit/WebRTC, and the trailing-silence convention that voice agents must follow for VAD-based end-of-turn detection.
Function
actor:
config:
MAX_TURNS: "1" # For one-shot/stateless callables
channels:
- type: function
callable: app.handlers:handle_messageFunction-channel rules:
- Omit
agent.entry_point - Omit
agent.port - Keep
agent.code_path - Use
MAX_TURNS: "1"when the callable is one-shot/stateless
Function callable contract
The callable specified in actor.channels[].callable must follow this signature:
Python (module:function notation):
def handle_message(message: str, session_id: str = "", **kwargs) -> str:
"""
Args:
message: The actor's message text.
session_id: A unique identifier for the simulation conversation.
Use to maintain state across turns if needed.
**kwargs: Reserved for future expansion. Accept and ignore
unknown keys.
Returns:
The agent's response as a plain string.
"""
...The callable path uses module.path:function_name notation (e.g., app.handlers:handle_message). The module must be importable from agent.code_path.
Key rules:
- The function is called once per actor turn
messageandsession_idare always provided as keyword arguments- Return a plain string (the actor receives it as the agent's reply)
- Raise an exception to signal failure; the simulation will log the error
- If the callable needs setup (DB connections, model loading), do it lazily on first call or in module-level init
- For one-shot/stateless callables, set
actor.config.MAX_TURNS: "1"
SSE responses
actor:
channels:
- type: http
url: http://localhost:8080/chat
request:
message_field: message
response:
type: sse
chunk_event: message
chunk_field: delta
chunk_filter_field: type
chunk_filter_equals: delta
done_data: "[DONE]"Use this when the agent streams user-visible content through SSE.
Agent section
agent:
name: Billing Assistant # Optional display name
code_path: /agent
entry_point: uv run --no-sync uvicorn app.main:app --host 0.0.0.0 --port 8080
port: 8080
environment:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/SIMULATION_ID
SERVICE_BASE_URL: http://localhost:9000If you reference service config artifacts like SCHEMA_PATH: /agent/db/schema.sql, make sure .veris/Dockerfile.sandbox copies that directory into the image.
Rules:
code_pathis usually/agententry_pointis required for non-function channelsportis required for non-function channels- Keep secrets out of
veris.yaml - Put stable non-secret defaults in
agent.environment - Use
veris env vars set --secretfor API keys and other sensitive values
Environment expansion
agent.environment supports shell-style expansion:
agent:
environment:
LOG_LEVEL: info
DB_NAME: app_${SIMULATION_ID}
API_BASE: ${API_BASE}Use ${VAR} only when you need expansion or composition. If the agent can simply read OPENAI_API_KEY or another runtime variable directly, prefer veris env vars set without duplicating that key in veris.yaml.
MAX_TURNS
Use actor.config.MAX_TURNS sparingly:
- Set it for one-shot or stateless agents that should stop after one actor turn
- Do not add it by default for conversational agents
- Do not add the
*_INTERVALactor tuning knobs unless the user explicitly asks for advanced harness tuning
Dependency Analysis — Presentation Format
When presenting dependency findings to the user, use this structure. Keep it conversational — explain what you found and what you recommend.
Agent Summary
- Name: {agent_name}
- Language/Runtime: {e.g., Python 3.12 / Node.js 20}
- Architecture: {single process / multi-process / micro-services / serverless}
- Entry point: {command or module path}
- Port: {port number}
- Package manager: {pip / uv / poetry / npm / pnpm / yarn}
Dependencies
For each dependency, explain naturally:
{Dependency Name}
- What it is: {DB / cache / queue / API / monitoring / etc.}
- How the agent uses it: {specific imports, function calls, config reads — cite files}
- Recommendation: {what to do and why}
- If veris mocks it: "Veris mocks this — your agent's API calls will be transparently intercepted. I'll add it to veris.yaml."
- If bundling: "I'd install this in the container. It adds ~{X}MB memory / ~{Y}MB to the image. Alternatively, you could point it at an external staging instance."
- If skipping: "I checked {files} and your agent doesn't import or call this service. It's only present as {reason}. Safe to leave out."
- If external: "This is too heavy to install locally / not mockable. You'd need to provide a staging URL and credentials."
- If unsure: "I'm not sure about this one. Here are the options: {A vs B}. What do you think?"
LLM Provider
- SDK: {OpenAI / Anthropic / LangChain / etc.}
- Note: Veris automatically intercepts LLM API calls through its proxy. No configuration needed for this.
Things I Want to Confirm
List any decisions where the user's input matters — bundling vs external, whether something is truly optional, etc.
Summary Table (optional, for quick reference)
| Dependency | What I Recommend | Notes |
|---|---|---|
| {name} | Veris mock / Install in container / Skip / External / Discuss | {brief reason} |
Dockerfile.sandbox Templates
Reference templates for building agent containers on the current Veris base image.
Current base-image assumptions
Use the current build-arg pattern:
ARG GVISOR_BASE
FROM ${GVISOR_BASE}The current base image already includes:
- Python 3.12
uv(latest at image build time)- Node.js 18.x (LTS) with npm
- nginx
- PostgreSQL 15
- Veris infrastructure and mock services
If the agent requires a newer Node.js or Python, see the "Runtime version override" section below. Do not re-install these runtimes unless the repo specifically requires a different version.
Template 1: Python agent with uv
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
COPY pyproject.toml uv.lock /agent/
WORKDIR /agent
RUN uv sync --frozen --no-dev
COPY app /agent/app
WORKDIR /appTemplate 2: Python agent with pip
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
COPY requirements.txt /agent/
WORKDIR /agent
RUN pip install --no-cache-dir -r requirements.txt
COPY app /agent/app
WORKDIR /appTemplate 3: Node.js agent
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
COPY package.json package-lock.json /agent/
WORKDIR /agent
RUN npm ci --omit=dev
COPY src /agent/src
COPY public /agent/public
WORKDIR /appIf the app needs a build step:
RUN npm run buildTemplate 4: Function-channel Python agent
Use Template 1 or Template 2, depending on whether the repo uses uv or pip.
A function-channel integration usually needs the same dependency install as any other Python repo. The difference is in veris.yaml: no entry_point and no port. See reference/veris-yaml-schema.md for the function-channel shape.
Template 5: Bundled Redis + start.sh
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
RUN apt-get update && \
apt-get install -y redis-server && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt /agent/
WORKDIR /agent
RUN pip install --no-cache-dir -r requirements.txt
COPY app /agent/app
COPY start.sh /agent/start.sh
RUN chmod +x /agent/start.sh
WORKDIR /appTemplate 6: Multi-process gateway
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
COPY gateway/requirements.txt /tmp/gateway-requirements.txt
COPY worker/requirements.txt /tmp/worker-requirements.txt
RUN pip install --no-cache-dir \
-r /tmp/gateway-requirements.txt \
-r /tmp/worker-requirements.txt
COPY gateway /agent/gateway
COPY worker /agent/worker
COPY start.sh /agent/start.sh
RUN chmod +x /agent/start.sh
WORKDIR /appTemplate 7: Platform-hosted agent (framework-as-runtime)
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
# Install the framework from a package manager
RUN pip install crewai # or: npm install -g @langchain/langserve
# Copy config files and tool definitions (not a full application)
COPY . /agent/
WORKDIR /appIf the repo has a dependency manifest that includes the framework:
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
COPY requirements.txt /agent/
WORKDIR /agent
RUN pip install --no-cache-dir -r requirements.txt
COPY . /agent/
WORKDIR /appUse this when the repo is primarily config files and the runtime is a globally-installed framework. See Pattern 8 in reference/infrastructure-patterns.md.
Runtime version override
The base image ships with Python 3.12 and Node.js 18.x. If the agent requires a newer version:
Node.js version upgrade:
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
ARG NODE_VERSION=22.14.0
RUN curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz \
| tar -xJ -C /usr/local --strip-components=1 --no-same-owner \
&& node --version
# ... rest of agent setupPython version upgrade (via deadsnakes PPA on Debian-based images):
ARG GVISOR_BASE
FROM ${GVISOR_BASE}
RUN apt-get update && \
apt-get install -y software-properties-common && \
add-apt-repository -y ppa:deadsnakes/ppa && \
apt-get update && \
apt-get install -y python3.13 python3.13-venv python3.13-dev && \
rm -rf /var/lib/apt/lists/*
RUN python3.13 -m venv /agent/.venv
ENV PATH="/agent/.venv/bin:$PATH"
# ... rest of agent setup (pip install, COPY, etc.)Only override runtimes when the agent genuinely requires a newer version. The base image versions work for the majority of agents.
Rules
- Build context is the repo root
- Copy dependency manifests before source code
- Copy only what the agent actually needs
- End with
WORKDIR /app - Do not copy
.veris/veris.yamlinto the image - Do not assume
.veris/is the build context - If you add a
start.shto bundle multiple processes (e.g., database alongside agent), copy it into/agent. Do not write agent-adapter wrappers — see SKILL.md core framing. - If the repo needs schemas, migrations, prompt assets, or static files at runtime, copy those explicitly
Runtime Env Var Template
Use this template when deciding what belongs in agent.environment versus veris env vars set.
Preferred split
1. Stable non-secret defaults -> agent.environment
Examples:
agent:
environment:
LOG_LEVEL: info
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/SIMULATION_ID
SALESFORCE_DOMAIN: mock-salesforce2. Secrets and per-environment values -> veris env vars set
Examples:
veris env vars set OPENAI_API_KEY=sk-... --secret
veris env vars set ANTHROPIC_API_KEY=sk-ant-... --secret
veris env vars set SALESFORCE_CONSUMER_KEY=... --secret
veris env vars set SALESFORCE_CONSUMER_SECRET=... --secret
veris env vars set LOG_LEVEL=debug3. Optional local-only convenience -> root .env
For local smoke tests or veris run local, it can still be useful to mirror values in a root .env file or export them in the shell. This is optional and should not replace the platform env-var flow.
Common categories
LLM provider keys
Usually set with:
veris env vars set OPENAI_API_KEY=sk-... --secretDo not generate a .env.simulation file for this.
Mock credentials
If the mock expects stable fake credentials, those are often fine in agent.environment.
Examples:
SALESFORCE_DOMAIN=mock-salesforceSLACK_BOT_TOKEN=xoxb-mock-token-for-veris-simulation
External endpoints
If the user keeps a dependency external, prefer:
veris env vars set PINECONE_API_KEY=... --secret
veris env vars set PINECONE_ENVIRONMENT=us-east-1
veris env vars set KAFKA_BOOTSTRAP_SERVERS=broker.example.com:9092Rule of thumb
- If the value is safe to commit and stable across environments, it can live in
agent.environment - If the value is sensitive or environment-specific, use
veris env vars set - If the app already reads a secret directly by name, do not duplicate it in
veris.yaml
start.sh Templates
Template 1: Bundled services + single agent process
#!/bin/bash
set -e
echo "Starting bundled services..."
# Start Redis
redis-server --daemonize yes --maxmemory 128mb --maxmemory-policy allkeys-lru
until redis-cli ping 2>/dev/null | grep -q PONG; do sleep 0.5; done
echo "Redis ready"
# Start agent (foreground — must be last, must use exec)
exec uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8080}Template 2: Multi-agent gateway + sub-agents
#!/bin/bash
set -e
echo "Starting sub-agents..."
# Start sub-agents in background
(cd /agent/flight-agent && uvicorn main:app --host 0.0.0.0 --port 8081) &
(cd /agent/hotel-agent && uvicorn main:app --host 0.0.0.0 --port 8082) &
# Wait for sub-agents to be ready
echo "Waiting for sub-agents..."
for port in 8081 8082; do
for i in $(seq 1 30); do
if curl -sf http://localhost:$port/health >/dev/null 2>&1; then
echo " Port $port ready"
break
fi
sleep 1
done
done
# Start gateway (foreground — must be last, must use exec)
cd /agent/gateway
exec uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080}Template 3: FastAPI + background workers
#!/bin/bash
set -e
echo "Starting background workers..."
# Start Celery worker (or other background processor)
python -m app.workers.celery_worker \
--concurrency 2 \
--loglevel info &
# Start webhook listener (if needed)
python -m app.webhook_listener &
# Start main API server (foreground — must be last, must use exec)
exec uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8080}Template 4: Peer processes with fail-fast
Use this when the container holds multiple peer processes that all must be alive for the agent to work, and no single one of them is naturally "the" foreground process. The canonical case is a transport bridge: an in-container media server, the agent worker (which auto-dispatches into the media server's rooms), and the bridge that accepts the actor's channel connection — none of them can be the lone exec'd foreground because all three are equally critical.
#!/bin/bash
# Multi-process container with fail-fast — if any peer dies, take down
# the rest so Veris restarts the container cleanly instead of leaving a
# partially-working stack serving broken responses.
# Peer 1: in-container service the agent depends on (media server, SIP daemon, etc.)
/usr/local/bin/some-server --bind 0.0.0.0 &
SVC_PID=$!
# Give the server a beat to bind sockets before the worker starts probing it.
sleep 1
# Peer 2: agent worker (registers against the in-container service)
uv run --no-sync python -m app.worker start &
WK_PID=$!
# Peer 3: bridge / API on the actor's port — also a peer, not the "main"
uv run --no-sync uvicorn app.bridge:app \
--host 0.0.0.0 --port "${PORT:-8080}" &
BR_PID=$!
# Clean shutdown. wait -n returns when the first child exits; cleanup
# kills the rest so the container exits as a unit and Veris restarts
# it instead of half-serving requests. NOTE: we intentionally avoid
# `set -e` here — with `set -e`, a non-zero exit from the first child
# would terminate the shell before the explicit kill block runs,
# leaving orphan peers behind.
cleanup() {
kill "$SVC_PID" "$WK_PID" "$BR_PID" 2>/dev/null || true
}
trap 'cleanup; exit 143' TERM INT
wait -n
status=$?
echo "[start] a peer process exited (status=$status) — shutting down siblings"
cleanup
wait || true
exit "$status"When to prefer this over Templates 1-3:
- Use Template 1 when there's one clear foreground process (the agent's HTTP server) and the rest are infrastructure it talks to (Redis, etc.). The agent process is the natural
exectarget; if Redis crashes, the agent's next request will fail loudly enough that you'll notice. - Use Template 4 when peer processes are equally critical to the agent's wire contract and a silent crash of any one of them leaves the container looking healthy from outside (port still open, /health still returns 200) while actually serving broken responses. The media-server-plus-worker-plus-bridge shape is the prototypical case; bash's
wait -nis the simplest way to make that container fail loudly.
Rules
- Templates 1–3: the LAST command must use
exec(replaces shell process, receives signals correctly). Background processes use&. A background process failing leaves the container running — that's intentional when the dependency isn't on the response path. - Template 4: the foreground
execrule does not apply — all peer processes run as backgrounds, and atrap+wait -n+ explicitcleanupbrings the container down as a unit when any peer dies. Use this only when the silent-crash failure mode of Templates 1-3 is unacceptable. - Always wait for bundled services to be healthy before starting the agent.
set -eat the top of Templates 1–3 is fine; do not combineset -ewith the Template 4wait -npattern — a non-zero child exit will kill the shell before cleanup runs, leaving orphan peers.- Use
${PORT:-8080}to respect veris port injection. - Never use supervisord in sandbox — start.sh is simpler and sufficient.
- Veris already starts
start.shfromagent.code_path, so do not add a redundantcd /agentat the top. - If you must launch background work from a subdirectory, use an explicit absolute-path subshell like
(cd /agent/worker && python main.py) &. - If the foreground process lives in a subdirectory,
cdthere immediately before the finalexec(Templates 1–3 only).
veris.yaml Annotated Examples
Use these as starting points when generating the final .veris/veris.yaml.
Example 1: HTTP agent with Postgres and Salesforce
version: "1.0"
services:
- name: postgres
config:
POSTGRES_PASSWORD: postgres
SCHEMA_PATH: /agent/db/schema.sql
- name: salesforce
dns_aliases:
- login.salesforce.com
- test.salesforce.com
- mock-salesforce.salesforce.com
actor:
channels:
- type: http
url: http://localhost:8080/api/chat
method: POST
request:
message_field: message
session_field: session_id
response:
type: json
message_field: response
agent:
code_path: /agent
entry_point: python -m app.main
port: 8080
environment:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/SIMULATION_ID
SALESFORCE_DOMAIN: mock-salesforce
LOG_LEVEL: infoIf you use SCHEMA_PATH: /agent/db/schema.sql, make sure .veris/Dockerfile.sandbox also copies db/ into /agent/db/.
Example 2: One-shot function agent
version: "1.0"
services:
- name: elastic
dns_aliases:
- siem-cluster.es.us-east-1.elastic-cloud.com
actor:
config:
MAX_TURNS: "1"
channels:
- type: function
callable: app.handlers:handle_message
agent:
code_path: /agent
environment:
ES_URL: https://siem-cluster.es.us-east-1.elastic-cloud.com
ES_INDEX: siem-eventsNotes:
- No
entry_point - No
port MAX_TURNS: "1"because the callable is one-shot/stateless
Example 3: Email-driven support agent
version: "1.0"
services:
- name: salesforce
dns_aliases:
- login.salesforce.com
- test.salesforce.com
actor:
channels:
- type: email
agent_inbox: support@email.test
poll_interval: 15
agent:
code_path: /agent
entry_point: uv run --no-sync uvicorn app.main:app --host 0.0.0.0 --port 8080
port: 8080
environment:
SALESFORCE_DOMAIN: mock-salesforceExample 4: Multi-process app with bundled Redis
version: "1.0"
services:
- name: atlassian/jira
dns_aliases:
- api.atlassian.com
- mycompany.atlassian.net
actor:
channels:
- type: http
url: http://localhost:8080/chat
agent:
code_path: /agent
entry_point: bash start.sh
port: 8080
environment:
REDIS_URL: redis://localhost:6379/0
JIRA_BASE_URL: https://mycompany.atlassian.netExample 5: Voice agent over voice_ws
version: "1.0"
actor:
channels:
- type: voice_ws
url: ws://localhost:8080/voice
language: en-US
wait_for_callee_first: true
agent:
code_path: /agent
entry_point: uv run --no-sync uvicorn app.main:app --host 0.0.0.0 --port 8080
port: 8080
environment:
LOG_LEVEL: infoThe agent's framework must speak PCM16/24 kHz/mono over a bare binary WebSocket. If the framework's native transport doesn't match that (e.g., LiveKit Agents, anything WebRTC-only), use entry_point: bash start.sh instead and bundle a transport bridge — see reference/voice-channels.md and Pattern 9.
Generation rules
- Use
actor.channels, notpersona.modality - Use
agent_inbox, notemail_address - Use canonical service names (
salesforce,google/calendar,oracle/fscm, etc.) - Only add
actor.config.MAX_TURNSwhen the interaction model requires it - Do not add
RESPONSE_INTERVAL,POLL_INTERVAL, orREFLECTION_INTERVALunless the user explicitly asks for advanced harness tuning - Omit
agent.entry_pointandagent.portfor function channels - Keep secrets out of
veris.yaml
Related skills
FAQ
How does Agent Integration install veris-cli?
Agent Integration installs veris-cli with uv tool install veris-cli first and falls back to pip install veris-cli when the veris command is not found on the system PATH.
What file does Agent Integration require for veris env push?
Agent Integration requires .veris/config.yaml with an environment binding created via veris env create --self-serve before veris env push can publish the agent environment.
Is Agent Integration safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.