← Về thư mục
📄 / / profiles / r-and-d / raw_evidence / scientific-agent-skills / skills / pufferlib / references / environments.md

Environment Contracts and Native Environments

Research snapshot: 2026-07-23.

Start from the Gymnasium contract

A current single-agent Gymnasium environment defines observation_space and action_space, then implements:

def reset(self, *, seed=None, options=None):
    super().reset(seed=seed)
    return observation, info

def step(self, action):
    return observation, reward, terminated, truncated, info

Contract requirements:

Do not collapse terminated and truncated during learning. A time-limit truncation can still permit value bootstrapping; a true terminal state does not.

Run the local contract tool before involving PufferLib:

python3 scripts/env_contract_validator.py

It validates only the bundled synthetic environment. It intentionally has no module-path option, so it cannot dynamically import an untrusted package.

Published PufferLib 3.0.0 native contract

For a native Python PufferEnv, assign these attributes before calling super().__init__(buf):

import gymnasium
import numpy as np
import pufferlib


class ReviewedEnv(pufferlib.PufferEnv):
    def __init__(self, buf=None, seed=0):
        self.single_observation_space = gymnasium.spaces.Box(
            low=-1.0, high=1.0, shape=(4,), dtype=np.float32
        )
        self.single_action_space = gymnasium.spaces.Discrete(3)
        self.num_agents = 2
        super().__init__(buf)

The stable base accepts a Box observation space and Discrete, MultiDiscrete, or Box action space. It allocates or attaches:

Native methods operate on those buffers:

def reset(self, seed=None):
    # update self.observations in place
    return self.observations, []

def step(self, actions):
    # update all buffers in place
    return (
        self.observations,
        self.rewards,
        self.terminals,
        self.truncations,
        [],
    )

The infos value for native Puffer environments is a list of dictionaries. PufferLib's native interface expects vector rows for agents, even when there is one agent. Native environments handle their own resets; clear rewards, terminals, truncations, masks, and partially written observations explicitly. Never leave a previous step's buffer values in place.

Native shape checklist

For A = num_agents and single observation shape S:

Validate the exact allocated action shape rather than assuming (A,), especially for MultiDiscrete and Box actions.

Stable Gymnasium and PettingZoo adaptation

PufferLib 3.0 uses explicit adapters:

import pufferlib.emulation

wrapped = pufferlib.emulation.GymnasiumPufferEnv(reviewed_gymnasium_instance)

or:

wrapped = pufferlib.emulation.PettingZooPufferEnv(reviewed_parallel_instance)

There is no supported 3.0 pufferlib.emulate(...) convenience function matching the old skill examples. Pass either an env instance or an env_creator callable according to the class signature; do not pass both.

The Gymnasium adapter:

The PettingZoo adapter:

Validate heterogeneous-agent spaces before use. The adapter derives its single spaces from the first possible agent, so environments with incompatible spaces need an explicit reviewed transformation.

Structured spaces

Stable emulation supports Box, Discrete, MultiDiscrete, Tuple, and Dict patterns through a packed NumPy dtype. This is byte-layout conversion, not semantic feature engineering. Check:

Current 4.0 Ocean contract

The 4.0 default branch focuses on first-party C environments. It no longer provides the 3.0 Python emulation/vector modules. The official starting points are:

A binding defines compile-time metadata such as:

#define OBS_SIZE 121
#define NUM_ATNS 1
#define ACT_SIZES {5}
#define OBS_TENSOR_T ByteTensor

#define Env Squared
#include "vecenv.h"

The environment struct must include pointers for observations, actions, rewards, and terminals, plus num_agents and a log struct. It implements c_reset, c_step, c_render, and c_close; binding.c supplies my_init and my_log.

Security and correctness rules:

  1. Treat the C environment and every linked library as native code.
  2. Verify repository/commit, license, asset rights, and checksums before build.
  3. Build only the selected environment in a disposable container or VM.
  4. Start with the local/address-sanitizer build described by upstream.
  5. Match OBS_SIZE, tensor dtype, action branch count/sizes, and actual writes.
  6. Bounds-check every index and allocation; use checked arithmetic for sizes.
  7. Initialize every output element each step. Reset reward/terminal buffers before early returns.
  8. Use an environment-owned RNG seeded per instance; do not use global RNG state for reproducibility.
  9. Free only memory owned by the environment. Do not free framework buffers.
  10. Fuzz reset/step/action boundaries before optimization.

c_step may reset immediately after marking a terminal. Record this autoreset behavior when interpreting terminal observations.

Environment provenance

An environment package may execute arbitrary Python/native code and may fetch assets at import, build, reset, or render time. Before execution:

An entry in Ocean/config is not a blanket security, quality, or licensing approval.

Testing ladder

  1. Built-in synthetic contract validator.
  2. One environment, one seed, serial, tens of steps.
  3. Boundary actions and intentionally invalid actions.
  4. Termination and time-limit truncation tests.
  5. Same-seed trace comparison.
  6. Independent-seed diversity check.
  7. Structured-space round trip.
  8. Multi-agent join/leave and mask tests.
  9. Serial versus vectorized trace equivalence where ordering permits.
  10. Bounded throughput benchmark only after correctness passes.

Sources