RL Sandbox
Use Veris as the environment layer for an RL training run you operate. Your policy and rollout framework keep their existing tool schemas, message formatting, and training loop. The tools call the same vendor SDKs they use in production, but those SDKs point at isolated Veris services instead of live vendors.
This integration works with SkyRL, NeMo Gym and NeMo RL, Tinker Cookbook, or a custom rollout system. It does not require a framework-specific Veris SDK.
This page is for teams bringing their own training stack. To run a Veris-managed GRPO training job, see Reinforcement Learning (GRPO).
Where Veris sits
policy output
↓
your framework step / agent loop
↓
your tool implementation
↓
Stripe, Gmail, Salesforce, or another vendor SDK
↓ base URL changed
Veris sandboxVeris sits at the vendor HTTP boundary. It does not implement a generic step() endpoint.
| Your training stack owns | Veris owns |
|---|---|
| Sampling, tokenization, chat templates, and loss masks | Isolated vendor-compatible services |
| Parsing model output and dispatching tool calls | Stateful, inspectable vendor data |
| Rollout grouping, retries, and asynchronous scheduling | Reusable starting worlds and deterministic IDs |
| Episode termination and advantage calculation | Virtual time, request traces, and failure injection |
| Reward composition | Ground-truth state for reward predicates |
A tool can make several calls across several vendors, and the meaning of one model action differs between agents and frameworks. Keeping step() in the rollout framework preserves those semantics and avoids an extra network hop around every action.
Create a reusable starting world
An environment names the services in a task and points to a durable baseline. A sandbox is one disposable, mutable copy of that environment.
Create the baseline once, then start every rollout from it:
Create an environment
curl -X POST https://api.veris.ai/v1/environments \
-H "Authorization: Bearer $VERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "billing-operations-rl",
"services": ["stripe", "google-gmail"]
}'Save the returned id as VERIS_ENVIRONMENT_ID.
Create a curator sandbox
curl -X POST \
"https://api.veris.ai/v1/environments/$VERIS_ENVIRONMENT_ID/sandboxes" \
-H "Authorization: Bearer $VERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ttl_minutes": 120}'Poll the returned sandbox until its status is ready. The response contains a stable url, control_url, and env_hint for every service.
Arrange the starting state
Use vendor APIs to build the world exactly as a real integration would, or write setup data through the service control plane:
curl -X POST "$STRIPE_CONTROL_URL/veris/data" \
-H "Content-Type: application/json" \
-d '{
"data": {
"customers": [{"id": "cus_training", "email": "ops@acme.test"}]
}
}'Use GET {control_url}/veris/schema to discover valid entity types and fields. Application and agent code should use only the vendor API; /veris/* is for environment setup, inspection, and reward code.
Promote the world
curl -X POST \
"https://api.veris.ai/v1/environments/$VERIS_ENVIRONMENT_ID/sandboxes/$CURATOR_SANDBOX_ID/promote" \
-H "Authorization: Bearer $VERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"clock_restore": "frozen"}'Promotion captures the service state, shared virtual clock, deterministic ID positions, and the optional Veris Postgres database into one pinned environment revision. Future sandboxes boot that revision. Other running sandboxes are not changed; treat the curator sandbox as capture-only and delete it after promotion.
Use clock_restore: "frozen" when identical logical time is part of the task. Frozen time also pauses scheduled callback delivery. Use "rebase" when the episode requires time to advance or webhooks to dispatch: the sandbox starts at the captured instant and then advances normally.
Run an episode
The following helper contains the complete framework-neutral integration. Create one instance for each active trajectory.
import os
import time
import requests
VERIS_API = os.getenv("VERIS_API_URL", "https://api.veris.ai")
VERIS_KEY = os.environ["VERIS_API_KEY"]
ENV_ID = os.environ["VERIS_ENVIRONMENT_ID"]
AUTH = {"Authorization": f"Bearer {VERIS_KEY}"}
class VerisRollout:
def __init__(self, sandbox):
self.id = sandbox["id"]
self.services = {service["name"]: service for service in sandbox["services"]}
@classmethod
def start(cls, ttl_minutes=120, timeout_seconds=180):
response = requests.post(
f"{VERIS_API}/v1/environments/{ENV_ID}/sandboxes",
headers=AUTH,
json={"ttl_minutes": ttl_minutes},
timeout=30,
)
response.raise_for_status()
sandbox = response.json()
try:
deadline = time.monotonic() + timeout_seconds
while sandbox["status"] != "ready":
if sandbox["status"] == "failed":
raise RuntimeError(sandbox.get("failure_reason") or "sandbox failed")
if time.monotonic() >= deadline:
raise TimeoutError(f"sandbox {sandbox['id']} did not become ready")
time.sleep(1)
response = requests.get(
f"{VERIS_API}/v1/environments/{ENV_ID}/sandboxes/{sandbox['id']}",
headers=AUTH,
timeout=30,
)
response.raise_for_status()
sandbox = response.json()
except Exception:
requests.delete(
f"{VERIS_API}/v1/environments/{ENV_ID}/sandboxes/{sandbox['id']}",
headers=AUTH,
timeout=30,
)
raise
return cls(sandbox)
def url(self, service):
return self.services[service]["url"]
def snapshot(self, service):
control_url = self.services[service]["control_url"]
response = requests.post(f"{control_url}/veris/snapshot", timeout=30)
response.raise_for_status()
return response.json()
def requests(self, service, limit=1000):
control_url = self.services[service]["control_url"]
response = requests.get(
f"{control_url}/veris/requests",
params={"limit": limit},
timeout=30,
)
response.raise_for_status()
return response.json()["requests"]
def close(self):
response = requests.delete(
f"{VERIS_API}/v1/environments/{ENV_ID}/sandboxes/{self.id}",
headers=AUTH,
timeout=30,
)
if response.status_code not in (204, 404):
response.raise_for_status()Point the existing SDK at the URL returned for that rollout:
import stripe
def configure_stripe(rollout):
stripe.api_base = rollout.url("stripe")
stripe.api_key = "sk_test_veris"
def refund_charge(charge_id: str):
return stripe.Refund.create(charge=charge_id)Any well-formed sandbox credential works in the default permissive auth mode, so your integration can usually keep the credential shape it already uses. Enable strict auth in the starting world when credential behavior is part of the task.
At episode end, read vendor-side state and compute a reward:
def score_refund(rollout, target_charge_id: str) -> float:
state = rollout.snapshot("stripe")["state"]
charge = next(c for c in state["charges"] if c["id"] == target_charge_id)
refunds = [r for r in state["refunds"] if r["charge_id"] == target_charge_id]
outcome = float(charge["refunded"] and len(refunds) == 1)
# Optional process reward: discourage avoidable vendor errors and retries.
trace = rollout.requests("stripe")
avoidable_errors = sum(1 for request in trace if (request["status"] or 599) >= 400)
return max(0.0, outcome - 0.05 * avoidable_errors)POST /veris/snapshot is a state inspection endpoint, not the durable environment baseline. The promoted environment revision is the reusable starting point; a snapshot is the current state of one service in one live sandbox.
Framework adapters
The lifecycle is the same in every framework: create a sandbox at episode initialization, let your existing tool dispatcher call its service URLs, read state for reward, and delete the sandbox in a finally block.
RL framework interfaces change frequently. Pin the framework version used by your training job and adapt constructor or return types as needed. The Veris lifecycle and service URLs stay outside tokenization and model-specific code.
SkyRL
SkyRL environments expose the right boundary through init() and step(). Keep action parsing and tool dispatch in the SkyRL environment; Veris only supplies the service URL and ground truth.
import json
from skyrl_gym.envs.base_text_env import BaseTextEnv, BaseTextEnvStepOutput
from reward import score_refund
from veris_rollout import VerisRollout
class StripeBillingEnv(BaseTextEnv):
def __init__(self, env_config, extras):
super().__init__()
self.task = extras["task"]
self.rollout = None
def init(self, prompt):
self.rollout = VerisRollout.start()
configure_tools(stripe_base_url=self.rollout.url("stripe"))
return prompt, {}
def step(self, action: str) -> BaseTextEnvStepOutput:
call = parse_tool_call(action)
result = TOOLS[call.name](**call.args)
reward = score_refund(self.rollout, self.task["target_charge_id"])
done = task_is_complete(self.rollout, self.task)
return BaseTextEnvStepOutput(
observations=[{"role": "tool", "content": json.dumps(result)}],
reward=reward,
done=done,
metadata={"sandbox_id": self.rollout.id},
)
def close(self):
if self.rollout:
self.rollout.close()Call close() from the rollout worker’s cleanup path, including failed and truncated trajectories.
Parallel rollouts
Allocate one sandbox per active trajectory. Never let two trajectories mutate the same sandbox.
| Sampling pattern | Sandbox allocation |
|---|---|
| Independent rollouts | One sandbox per rollout |
GRPO group of G completions | G sandboxes from the same environment baseline |
Batched environment with B active slots | One sandbox per active slot |
| Retried trajectory | A new sandbox from the same baseline |
For a promoted environment, restoring the episode means deleting the used sandbox and creating another one. POST .../sandboxes/{id}/reset is reserved for profile-backed environments and returns 409 for a promoted baseline so it cannot silently replace the curated world with default seed data.
Do not promote or roll back an environment while a training run is active. Record the environment’s baseline.revision_id with the run configuration so the starting world is auditable alongside the model checkpoint.
Reward signals
Veris supports two complementary reward sources:
- Outcome reward from
POST {control_url}/veris/snapshotor paginatedGET {control_url}/veris/data?entity_type=.... - Process reward from
GET {control_url}/veris/requests, including the vendor route, status, request and response bodies, and state version after each call.
For multi-service tasks, read each relevant service after the final tool call and combine the predicates in your reward code. Keep reward reads outside the agent’s tool surface so the policy cannot inspect ground truth directly.
Evidence for process graders
Veris records the vendor-side evidence from every rollout. Combine it with the trajectory captured by your training framework to evaluate how the agent reached an outcome.
evidence = {
# Captured by SkyRL, NeMo RL, Tinker, or your rollout loop
"trajectory": framework_trajectory,
# Captured by Veris
"environment": {
"stripe": {
"requests": rollout.requests("stripe"),
"final_state": rollout.snapshot("stripe")["state"],
}
},
}
result = your_behavior_judge(
behavior="Verify the customer before issuing a refund",
evidence=evidence,
)
# {"result": "pass", "reason": "Identity was verified before the refund call."}Process graders can check whether the agent verified identity before a destructive action, recovered appropriately from a vendor error, or claimed success after the vendor rejected a request.
Veris does not run the judge or define the reward. It supplies deterministic state and request evidence; your training framework owns the behavior specification, judge model, and scoring.
Operational notes
- Sandboxes are TTL-scoped. Delete them explicitly; TTL cleanup is a fallback.
- Treat
failedprovisioning as terminal and preservefailure_reasonin rollout metadata. - Use
control_urlfor/veris/*. For HTTP services it equalsurl; for PostgreSQL,urlis a database DSN andcontrol_urlis the HTTP inspection surface. - A frozen baseline keeps vendor time fixed and pauses callbacks. Advance or rebase time only when the task defines how time-bearing client protocols are handled.
- Veris resets the dependency world. Your harness must separately reset the agent process, memory, caches, filesystem, and any database not hosted in the Veris sandbox.
API reference
| Endpoint | Purpose |
|---|---|
POST /v1/environments | Define a reusable set of services |
POST /v1/environments/{env}/sandboxes | Create an isolated sandbox from the environment baseline |
GET /v1/environments/{env}/sandboxes/{id} | Poll readiness and read service URLs |
POST /v1/environments/{env}/sandboxes/{id}/promote | Capture a curated sandbox as the environment baseline |
DELETE /v1/environments/{env}/sandboxes/{id} | Tear down one rollout sandbox |
POST {control_url}/veris/snapshot | Read one service’s current ground-truth state |
GET {control_url}/veris/data | Read counts or paginated rows |
GET {control_url}/veris/requests | Read the vendor request trace |
GET {control_url}/veris/schema | Inspect entity and field definitions for reward code |