Skip to content

RFC: Environment Services — brokered, budgeted access to external services (training, GPU jobs, evaluators, simulators) #1382

Open

Description

@rycerzes

Problem Statement

More and more environments give the agent real services, not just in-env tools. Examples: post-training tasks (PostTrain Arena #898, PostTrainBench, RSI-Index), kernel and compiler hill-climbing, Blender and Isaac simulation, and queued science solvers. #898 explicitly leaves "hosted-verifier, GPU/TPU-only, multi-agent, or simulated-user packages" out of scope "without a defined runner and policy". This RFC proposes that policy.

Today each env answers the same five questions ad hoc:

  1. Who holds the credential? Raw keys reach the agent environment. Examples: Harbor environment.env, Prime sandbox secrets, and OpenHands exporting secrets into the agent's shell.

  2. Who pays? Existing caps cover LLM tokens only and are checked after spending, so they overshoot (OpenHands max_budget_per_run, LiteLLM MCP cost, RSIGym). There's no budget per rollout, per group or per run.

  3. What happens to a job that outlives the episode? There's no job handle, no cancel on reset and no orphan handling. kernrl runs its GPU evaluator inside step.

  4. Can the agent reach the grader? Several frameworks grade inside the agent's sandbox:

    • ROLL and rLLM upload tests into the agent's sandbox after the episode.
    • In AReaL, a rollout's own key can set its reward.

    METR saw reward hacking in 30.4% of RE-Bench runs vs 0.7% on HCAST. The same concern drives Verify sandbox environments outside the agent's sandbox #1232 and Measure OpenEnv's reward-hack defenses and prototype a Karotte bridge #1357.

  5. What was called, and what did it produce? No framework links service calls to trajectory steps.

Proposed Solution

Add an Environment Services layer on openenvd (RFC 009, #1053). It has three rules: the agent never holds a real credential, never reaches the grader, and never spends without a reservation.

v1 scope: two profiles, batch job and evaluator, plus the per-rollout budget. That covers post-training and kernel optimisation. The other profiles are future work (see the appendix) until an env needs them.

agent sandbox ──MCP svc.* / jobs.* / artifacts.*──▶ openenvd (per env)
 (run token only)                                     ledger · audit · artifacts · token exchange
                                                          │ audience-bound token (RFC 8693)
                                                          ▼
                                    shared brokers: jobs (HF Jobs) · eval
grader runtime ◀── artifact handles + score endpoints ────┘

1. Service manifest. An env declares its services by profile:

services:
  - {name: jobs, profile: batch_job, surface: agent, flavors: [a10g-small], max_timeout: 2h, meter: [usd, gpu_s]}
  - {name: kernel_eval, profile: evaluator, surface: {agent: dev_evaluate, grader: score},
     hardware: {class: h100, exclusive: true}}
budget: {usd: 50, gpu_s: 7200, jobs: 10, scope: rollout}

2. Service profiles (v1). Each profile adds one requirement to the contract. Domains combine profiles.

Profile Examples Adds
batch job HF Jobs (v1); later Modal, Slurm, render farms job handle, worst-case reservation, cancel, orphans
evaluator kernel timing, held-out model eval dev_evaluate vs score, cache, exclusive hardware

Post-training uses batch job plus a grader-only evaluator: in v1 the agent trains by submitting scripts to HF Jobs. Kernel optimisation uses evaluator and batch job.

3. Budget ledger. It reserves before a call and settles after. A job's reservation is the hardware rate × the timeout. Budgets are scoped per rollout and allocated by the orchestrator, which keeps GRPO groups comparable. Exclusive GPUs are leases, separate from money. The agent sees its remaining budget as a fact, never as reward. Running out ends the episode with a typed reason.

Spend is published under a stable key. It is for accounting and for planning budgets and concurrency, never a sampling weight: dividing a task's sampling weight by its cost would starve expensive, hard tasks. RFC 013's sampler (#1256) never reads the env; the trainer reads this entry from the done observation and can report one value per rollout as costs to update(task, rewards, costs):

obs.metadata["service_spend"] = {
    "version": 1,
    "settled": {"usd": 0.62, "gpu_s": 410.0, "calls": 37},  # cumulative for the episode
    "reserved": {},   # open reservations; empty once every job has settled
    "final": True,    # false if a job ended orphaned or its cancel was unconfirmed
}

Units come from a fixed vocabulary that matches the manifest's meter field: usd, gpu_s, cpu_s, tokens, calls and jobs, always in base units. The manifest's meter list tells trainers which cost units to report (#1256, open question 6); spend is never a sampling weight.

4. Jobs. These are tools that return a handle: jobs.submit, status, wait and cancel. jobs.submit takes an idempotency key, so a retried submit never pays twice. States are queued → running → succeeded | failed | cancelled and cancelling → orphaned, each with a reason. done, reset and close cancel the episode's jobs, and an unconfirmed cancel blocks the next episode. Support for the MCP Tasks extension (SEP-2663) is opt-in, once /mcp speaks 2026-07-28 (decision 4).

Prior art: OpenMLE Sandbox (OpenRSI) already runs this job shape for MLE agents:

  • submit, status, logs and cancel endpoints;
  • states queued/running/completed/failed/cancelled, with typed result reasons;
  • idempotency keys;
  • CPU/GPU priority queues;
  • a router across several controllers.

It is also a backend a broker could front, but it scores on the same worker that ran the candidate, from a shared mount where the private answers are readable. Its README says this is "not a security boundary". The broker adds the missing layers: scoring in a fresh container, network policy, run tokens and the ledger.

5. Credentials. We follow MCP 2026-07-28 auth: audience-bound tokens and no passthrough.

  • The daemon injects credentials, using the OpenShell pattern, which is RFC 009's credential_isolation future work.
  • Brokers get short-lived tokens through RFC 8693 token exchange, with a hard cap on the provider side as a backstop (an HF Resource Group spend limit or a LiteLLM key budget).
  • The HF Jobs broker holds a fine-grained token with only job.write ("Start and manage Jobs"). It calls run_job/run_uv_job with explicit flavor, namespace and secrets, and never shells out to hf jobs uv run, whose script header can pull in the caller's HF_TOKEN and set the namespace.
  • The broker never passes its own token into a job: a job.write token inside a job can start further jobs that skip the broker's admission and budget checks. Checkpoint pushes get a separate repo-scoped token.
  • HF tokens carry no spend cap, so broker jobs are billed to a Resource Group with a Jobs spend limit (Enterprise) as the backstop.

6. Artifacts and audit. Artifacts stay in the daemon. The agent gets openenv-artifact://episode/job/name resource_links that carry a sha256 and size. Graders receive them through VerifyResult.artifacts. Checkpoints are signed with OpenSSF model signing v1.0. Every service call emits an audit record keyed to (episode, step, tool_call_id) as an OTel GenAI/MCP span, pinned to a specific commit.

7. Evaluator contract. The evaluator runs a cascade: admit (hash, dedupe, static scan), then validate, verify, measure on exclusive hardware, and grade.

  • dev_evaluate is rate-limited and budgeted. It returns at most 5 metrics, size-capped feedback, status, ci and evaluator_version.
  • score is grader-only. It uses hidden inputs, re-measures, and audits the trace.

Results are cached by candidate hash, evaluator version, input set, hardware class and toolchain. Each evaluator ships a red-team record; #1357's attack panel is a natural seed. Timing needs an exclusive lease: shared GPUs inflate kernel timings 1.5–3.2× (KREX), and MIG/MPS don't qualify.

Not allowed:

  • raw provider keys in the sandbox;
  • grading in the agent's sandbox;
  • reward writes the agent can reach;
  • passing the broker's own token into jobs;
  • judges reading unescaped trajectories;
  • building on MCP sampling or x402 (MCP has no official cost or payment extension).

Alignment with Principles

Principles Addressed

  • Minimize lifecycle deltas. Training and production eval use the same manifest and brokers. Only the budget values change.
  • Minimize human-agent divergence. Agents use the same scoped, budgeted tools a human operator would.
  • Be hands-on. This starts as an env-only prototype; see the implementation sketch.
  • Design for LLMs. Quota, denial and budget states are typed, and jobs return handles instead of blocking calls.

It keeps the key invariants:

  • Agents cannot reset. Approval, cancel and budget top-up are orchestrator-only.
  • Rewards inside environment. score runs in the env's grader zone.
  • Dual API boundary. svc.*, jobs.* and artifacts.* are agent tools, while the ledger and lifecycle stay on the infra side.

Trade-offs

Option Pros Cons
Proposed: profile manifest + openenvd custody + shared brokers One contract for every domain; no keys in the sandbox; budgets can't overshoot Needs openenvd; brokers are a new shared trust tier
Per-env ad hoc (status quo, e.g. kernrl) No core change Leaky credentials, no budgets, graders reachable by the agent
Give the agent raw provider tokens (e.g. HF Jobs) Simplest Tokens carry no spend cap, and a job given the token gets all its rights; even a job.write-only token lets a job start more jobs

Impact

Files/Systems Affected

  • src/openenv/core/env_server/mcp_types.py: RESERVED_TOOL_NAMES is exact-match only. It needs a prefix rule for svc.*, jobs.* and artifacts.*.
  • src/openenv/core/env_server/http_server.py: _cleanup_session_resources swallows errors, so job-cancel failures never surface.
  • src/openenv/core/env_server/types.py: the idle reaper should be on whenever services are declared.
  • src/openenv/core/harness/rollout.py: VerifyResult.artifacts should carry artifact handles.
  • src/openenv/core/env_server/http_server.py /mcp and pyproject.toml: speak MCP 2026-07-28 while still serving 2025 clients, which means bumping fastmcp to 4 (decision 4; later, via the RFC 003 amendment).
  • New client-safe models (manifest, ledger, job, artifact) that live outside server/.
  • openenvd (feat(openenvd): contract and pluggable enforcement backends (RFC 009, 1/2) #1354, feat(openenvd): daemon, surfaces and declared processes (RFC 009, 2/2) #1355): broker token exchange and the grader-side score service. A jobs observer stream needs its own justification, because RFC 009 rejected a resource stream.

Related RFCs

Implementation Sketch

  1. First PR: RFC doc plus env-only prototype, no core change. It adds rfcs/014-environment-services.md and a posttrain_env whose server holds an HF Jobs broker and an evaluator, the way kernrl holds its evaluator. The env runs PostTrain-style T0 tasks (Spec-agnostic task validation: openenv validate-task for external task.md packages (PostTrain Arena / BenchFlow) #898).
  2. Core types. Manifest, ledger, job and artifact models; the reserved-name prefix rule; cleanup semantics.
  3. openenvd integration. Credential custody, token exchange, the grader zone, and MCP Tasks.
job = await env.call_tool("jobs.submit", script="train.py", flavor="a10g-small", timeout="1h")
# -> {"job_id": "...", "reserved": {"usd": 1.05}, "budget_remaining": {"usd": 48.95}}
res = await env.call_tool("jobs.wait", job_id=job["job_id"], timeout_s=600)
# -> {"state": "succeeded", "artifacts": [{"uri": "openenv-artifact://ep/j1/adapter", "sha256": "..."}],
#     "settled": {"usd": 0.62}}

Decisions

  1. Long-running services and GRPO: a service runs live at train time only if it finishes within the trainer's staleness window. For example, TRL's experimental AsyncGRPOTrainer drops samples more than max_staleness weight updates old (default 4). Slower services declare a stand-in in the manifest, and the real service runs only at eval.

  2. HF Spaces: no services on Spaces for now. A Space gives no isolation guarantee, so it must not advertise services that hold credentials.

  3. Shared brokers are operator-run services outside the env. They have the same standing as RFC 009's backend gateways, such as the OpenShell gateway. RFC 009's "one daemon per env" still holds: the broker serves many tenants, the daemon doesn't. This follows existing practice:

    • RSIGym puts training, inference, eval and sandbox services behind one auth server with per-run budgeted keys.
    • LiteLLM gives each team or run a virtual key with its own budget.
    • OpenMLE Sandbox routes jobs to shared controllers by API key.

    Concretely:

    • Trust: the orchestrator's run config lists each broker's URL and expected TLS identity. That registration is the trust anchor; hardware attestation is out of scope.
    • Tokens: openenvd holds the broker credential as a backend credential and exchanges it for per-rollout tokens bound to one broker (RFC 8707). Brokers act as MCP resource servers (RFC 9728 metadata).
    • Budget: each broker enforces a hard cap per token and reports settled amounts. The daemon's per-rollout ledger stays authoritative.
  4. Transport: target MCP 2026-07-28. The revision removes protocol sessions. Servers that need cross-call state use server-minted handles passed as arguments (SEP-2567).

    • No conflict with today's /mcp. It is a custom JSON-RPC handler that serves only tools/list and tools/call, with no initialize. openenv/session/create already returns a server-minted id that clients pass explicitly.
    • What the upgrade adds: server/discover, the per-request _meta protocol version, resultType, the Mcp-Method/Mcp-Name headers, and cacheable tools/list results.
    • Libraries: MCP Python SDK v2 and FastMCP 4 serve 2025- and 2026-era clients from one server, and FastMCP 4 ships the Tasks extension (fastmcp-tasks). OpenEnv pins fastmcp>=3,<4, so this includes a major bump.
    • Jobs: they stay as tools that return handles, which is the revision's own pattern for cross-call state. MCP Tasks is opt-in on top.
    • Timing: the upgrade comes later, as a backward-compatible RFC 003 amendment, so existing envs need no changes. It isn't part of this RFC's first PR, and v1 jobs work without it.

Appendix: future service profiles

Each is added when an env needs it.

Profile Examples Adds
model call inference, LLM/VLM judge token metering
training session Tinker-compatible APIs session state, checkpoints as artifacts
interactive simulator Blender, Isaac, browser, VM lease, snapshot/fork, hardware capability
sub-sandbox E2B, Daytona nested isolation, per-service network policy
data access search, paper/chem DBs licensing, contamination audit, caching
licensed tool FlexLM EDA, quantum queues non-money leases (seats, daily quotas)
real-world actuation cloud lab, robot, deploy irreversible spend, orchestrator approval
human in the loop expert review hours-to-days latency, scarcity
artifact registry HF Hub push write-scoped credentials, provenance
peer agent A2A budget passed down to the peer (#779)

Activity

  1. rycerzes commented on Oct 9, 2026

    @rycerzes
    ContributorAuthor

    cc @burtenshaw @adithya-s-k @sergiopaniego @thegovind @zkwentz for feedback.

  2. tiagoperrupato commented on Oct 12, 2026

    @tiagoperrupato
    Contributor

    Thanks for linking RFC 013. One note on how the two fit: in #1256 the sampler does not read the environment directly. The trainer reports each group through update(task, rewards, costs), with one cost per rollout, in a unit the trainer chooses. So spend published in Observation.metadata can be one more cost source: the trainer reads it and forwards it as costs. For that to work across environments, the trainer needs a stable key and unit for the settled amount.

  3. rycerzes commented on Oct 12, 2026

    @rycerzes
    ContributorAuthor

    Thanks @tiagoperrupato, agreed: I've updated the RFC so the trainer reads a stable metadata["service_spend"] entry (fixed units, cumulative per episode) from the done observation and forwards it as costs.

  4. sergiopaniego commented on Oct 15, 2026

    @sergiopaniego
    Collaborator

    thanks for the proposal @rycerzes! It makes sense and there's a real gap :)

    • +1 to the env-only posttrain_env prototype first. let's mark it as the next step as part of a new PR, wdyt?
    • I'd narrow the scope to what that prototype uses. Batch job (HF Jobs), evaluator (dev_evaluate vs score) and the per-rollout budget cover post-training and kernel optimization. The other profiles (licensed tools, actuation, human in the loop, peer agents...) could move to future work until an env needs them. If we see that this is needed when we are generating the prototype, we can still add them but I feel like that'd be too much.
    • Episode length vs GRPO (your "multi-day services" open question). TRL's AsyncGRPOTrainer already decouples rollouts from training. Samples older than max_staleness weight updates are still dropped though, so very long jobs would need a stand-in at train time. Is this what you are looking for?
    • HF Jobs broker: +1 to run_job with an explicit secrets allowlist. Could you add sources for the two HF claims, that HF tokens have no spend cap and that hf jobs uv run can pull HF_TOKEN into the job through the script header? Fine-grained tokens and resource groups may already cover part of this, and if it's a real issue we should report it to huggingface_hub too.
    • Open question 4: I'd keep services out of Spaces for now. A Space gives no isolation guarantee, so it shouldn't advertise services that hold credentials.
  5. rycerzes commented on Oct 15, 2026

    @rycerzes
    ContributorAuthor

    Thanks @sergiopaniego! I've updated the RFC:

    • Prototype and scope: agreed. posttrain_env is next, as its own PR, and the RFC doc (rfcs/014-environment-services.md) will land in that same PR. v1 covers batch job, evaluator and the per-rollout budget, and the other profiles are now in a future-work appendix.
    • Episode length: yes. Since max_staleness drops stale samples, a service runs live at train time only if it finishes within that window. Slower ones use a stand-in, with the real service at eval only.
    • HF claims (checked in code and with test jobs):
      • hf jobs uv run fills a script header's secrets = ["HF_TOKEN"] from the caller's login and lets the header set namespace (jobs.py#L309-L345). It's documented and warns, so it isn't a huggingface_hub bug. The broker just uses run_job, which ignores the header.
      • You're right on both counts. Tokens carry no spend cap, but Resource Group Jobs limits cover that. A fine-grained job.write-only token works and can't touch repos. One catch: if that token is passed into a job, the job can start further jobs that skip the broker's budget checks. So the broker never passes its token into jobs.
    • Spaces: agreed, no services there for now.
    • also settled the other two open questions. Shared brokers are operator-run services, as in RSIGym and LiteLLM, and the transport targets MCP 2026-07-28 through a later, backward-compatible RFC 003 amendment.
  6. carrollco commented on Oct 17, 2026

    @carrollco

    Thanks for writing this up. One data point for the posttrain_env prototype, from a small private OpenEnv environment I built along the lines of your evaluator profile (OpenEnv 0.9.0, TRL 1.16.0).

    The task is a cart-pole swing-up with one hidden plant parameter per episode. The server keeps the simulator, the hidden parameter, the held-out scenarios and the grader. The model has probe to measure the hidden plant (at a small cost to the reward), simulate for practice inputs it chooses, and submit for held-out evaluation; the server computes the held-out score, roughly your dev_evaluate / score split. Every controller runs in plimsoll, a sandbox service I maintain (gVisor, no network, one signed record per run), and TRL's environment_factory drove the episodes.

    What I saw:

    • The sandbox was not the bottleneck here: a judged 20 s simulated trial takes about 1.5 s under gVisor on a laptop CPU, and plimsoll's runs added up to 14.5 s of a 557 s three-step GRPO run (Qwen3-0.6B on a laptop GPU). This measures one local workload; it does not estimate HF Jobs or GPU evaluation costs.
    • The base Qwen3-0.6B, not trained on this task, scored zero on every episode, so GRPO had no signal. After fine-tuning on 200 episodes from a scripted teacher, it obtained the same mean reward as the teacher on 16 held-out tasks not among those 200: 0.836, with one seed. Three GRPO steps after that were too few to say anything about RL.
    • Three targeted checks passed: a hidden-value search (the controller printed everything it could reach; the hidden values never appeared), trajectory replacement (rejected by the fingerprint check), and files left for a later run (every run started clean). The scenario-hiding check is a test anyone can run: https://github.com/plimsollmark/plimsoll/blob/main/sandbox/docker_judge_scenario_test.go

    plimsoll only runs the code; it is not a jobs broker or a budget ledger.

    Happy to share details or help with the prototype.

  7. burtenshaw commented on Oct 26, 2026

    @burtenshaw
    Collaborator

    Please link the env-only posttrain_env prototype PR when available and identify any remaining blockers to that agreed first slice.

  8. rycerzes commented on Oct 27, 2026

    @rycerzes
    ContributorAuthor

    The prototype PR is up: #1500. It covers only the agreed slice: batch jobs on HF Jobs, dev_evaluate vs score, and the per-rollout budget. RFC 014 is narrowed to match, and the live runs are in the PR.

    No blockers remain for that slice. Two things worth a look in review:

    • Agent jobs run without network. HF Jobs has no egress option, so the broker wraps each script in a small seccomp launcher. A native no-network flag on Jobs would let us drop it. Could HF add one?
    • A score job that fails ends the episode with reward=None. That follows RFC 013 (RFC 013: Task sampling and in-training evaluation #1256), which is still in review.

    cc @sergiopaniego

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions