Skip to main content
Implement an Environment provider through the shared session contract: build typed provider config from the resolved plan, create one task Environment, expose command and file primitives, and release the exact resource you created. The tutorial provider below wraps the public local-process session so every required method is executable without an external account. Replace each delegation with the official provider SDK when building a remote integration; do not add provider behavior to a Benchmark or Harness.

Record the Provider Contract

Use the provider’s official SDK and API documentation as the source of truth. Record authentication and account scope; mutually exclusive image, snapshot, or template selectors; workspace persistence; CPU, memory, disk, GPU, placement, and quotas; startup and deletion semantics; enforceable network modes; command, transfer, endpoint, cancellation, and error behavior; and async or thread-safety guarantees.

Create the Minimal File

Start with one provider module and one package export:
Implement example_local.py as follows:
This shows every abstract EnvironmentSession method and both abstract BaseEnvironment methods. There is no separate public EnvironmentPlan type: providers consume the Recipe-adjusted ExecutionPlan, and build_config(req, plan) reads plan.environment.params while validating the resolved network phases. The wrapper is only a contract exercise. A production provider should call its own SDK and return its own EnvironmentSession; it should not depend on HostProcessSession.

Export and Inspect the Registration

Add the import to src/agentcompass/environments/__init__.py:
Then inspect registry discovery and the live config schema:
The first command should contain example_local. The second should list workspace and default_workspace_root with their defaults and descriptions. If importing an optional provider SDK can fail, guard only its documented missing dependency in __init__.py; do not swallow unrelated exceptions or registration errors.

Run One Task

Use the companion Benchmark and Harness tutorial components to exercise provider open, session construction, and close without external credentials:
The terminal result should report one completed task and paths.run_info; its parent directory is the run directory. In run_info.json, confirm that requestenvironmentid is example_local and that resolved_execution_plans contains the same Environment ID for attempt 1. Also confirm that one details/*.json file and summary.md exist. The .agentcompass/environment-smoke directory confirms open() used the provider config; it is not a result directory. For a remote provider, add one session-level check that runs a list-form command, writes and reads UTF-8 text, uploads and downloads one file and one directory, and verifies cleanup in the provider console. A successful registry or local mock check does not prove remote lifecycle or network enforcement.

Map the Real Session Primitives

Implement the methods with these semantics: Normalize provider responses into ExecResult. A command’s nonzero return code is data, not a provider exception; raise only when transport or provider execution itself fails. Preserve timeout versus provider-error meaning. Use async SDK methods when available, and explicitly isolate blocking calls so high task concurrency does not block the event loop.

Own Open, Close, and Partial Cleanup

During open(), build and validate provider config from the resolved plan; resolve mutually exclusive selectors; apply resources, workspace, labels, and baseline network policy; create the sandbox within the startup timeout; and construct a session only after the provider reports a usable state. If any step fails, release every partially created resource before propagating the error. During close(), stop or delete the exact resource owned by that session. Make cleanup safe after partial startup and sufficiently idempotent for cancellation or repeated error handling. Never discover cleanup targets through broad names or unvalidated global searches. Declare supported_network_modes, supported_allowlist_entry_types, supports_network_target_ports, and supports_dynamic_network_policy from real enforcement capability. Fail closed when a mode, target type, or port restriction cannot be enforced. Do not advertise restrictions implemented only by prompts, environment variables, or best-effort agent instructions. Dynamic providers must switch from baseline to run policy and, for reused evaluation, directly from run to evaluation policy. Protect and redact proxy credentials, policy tokens, signed URLs, and generated endpoints, and remove temporary networks or policies after normal close and startup failure.

Preserve Config and Recipe Precedence

Define one typed config field for every public provider setting. Keep authentication, sandbox source, lifecycle timeouts, resources, workspace, and provider metadata distinct; use clear units, defaults, validation, and mutual-exclusion errors. Credentials must not enter logs or persisted plans. Environment code consumes the final plan while Recipes supply Benchmark-specific defaults. Both layers preserve:
Resolve the winning selector before removing incompatible fields. Apply resources field by field so explicit user values win while unspecified fields can inherit task hints. A Recipe copies the plan, stays narrow to a Benchmark/provider pair, and never calls the provider SDK. Respect the process-global provider-open limiter applied by BaseEnvironment, plus the provider’s SDK request limits, account quotas, and capacity. Log stable sandbox IDs, lifecycle phases, elapsed time, selected non-secret images, and actionable errors; never log full config dictionaries that may contain secrets.

Diagnose Failures by Stage

The simplest real reference is host_process.py. For image lifecycle, command execution, transfer, and enforceable network behavior in a container provider, compare docker.py.