Metadata-Version: 2.5
Name: simhub
Version: 0.2.0
Summary: Define an environment, a policy and a task in Python; run them on hosted GPUs
Project-URL: Homepage, https://github.com/General-Deployment/sim
License: Proprietary
Keywords: evaluation,mujoco,reinforcement-learning,robotics,simulation
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Requires-Dist: numpy>=1.24
Requires-Dist: pydantic>=2.7
Requires-Dist: tomli>=2.0; python_version < '3.11'
Requires-Dist: typing-extensions>=4.12
Provides-Extra: all
Requires-Dist: huggingface-hub>=0.26; extra == 'all'
Requires-Dist: imageio-ffmpeg>=0.5; extra == 'all'
Requires-Dist: ipython>=8.0; extra == 'all'
Requires-Dist: matplotlib>=3.7; extra == 'all'
Requires-Dist: mujoco==3.11.*; (python_version >= '3.10') and extra == 'all'
Requires-Dist: pandas>=2.0; extra == 'all'
Requires-Dist: pillow>=10.0; extra == 'all'
Requires-Dist: pyarrow>=15.0; extra == 'all'
Provides-Extra: dataset
Requires-Dist: pyarrow>=15.0; extra == 'dataset'
Provides-Extra: mujoco
Requires-Dist: mujoco==3.11.*; (python_version >= '3.10') and extra == 'mujoco'
Provides-Extra: notebook
Requires-Dist: ipython>=8.0; extra == 'notebook'
Requires-Dist: matplotlib>=3.7; extra == 'notebook'
Requires-Dist: pandas>=2.0; extra == 'notebook'
Requires-Dist: pillow>=10.0; extra == 'notebook'
Provides-Extra: typing
Requires-Dist: datamodel-code-generator==0.35.0; extra == 'typing'
Provides-Extra: video
Requires-Dist: imageio-ffmpeg>=0.5; extra == 'video'
Requires-Dist: pillow>=10.0; extra == 'video'
Provides-Extra: weights
Requires-Dist: huggingface-hub>=0.26; extra == 'weights'
Description-Content-Type: text/markdown

# simhub

## Typed hosted workflows (0.2.0)

Define environments, policies, tasks, and reusable components as typed Python
classes. Pydantic validates their parameters; SimHub packages their source and
runs scene construction, physics, and requested video generation on the hosted
backend. Importing a definition never starts a simulator.

```python
import simhub as sh
from warehouse_barcode_scan.definitions import Warehouse, BarcodeController, ScanLabels

request = sh.RunRequest(
    environment=Warehouse(), policy=BarcodeController(), task=ScanLabels(),
    outputs=sh.Outputs(video=sh.VideoOutput()),
)
with sh.connect(hosted_only=True) as sim:
    run = sim.run(request, project="examples/warehouse_barcode_scan")
    run.wait(outputs=True)
    print(run.evaluation)
    print(run.share().url)
```

The three classes above are ordinary project source in
[`examples/warehouse_barcode_scan`](https://github.com/General-Deployment/sim/tree/main/examples/warehouse_barcode_scan).
`parameters.py` contains the inputs, `definitions.py` the public contracts,
`recipe.py` the scene composition, `parts.py` the reusable objects, and
`runtime.py` the environment and controller implementations. No private server
plugin or first-party warehouse component is required.

See [typed parameters and native authoring](https://github.com/General-Deployment/sim/blob/main/docs/SDK_TYPED_PARAMETERS.md)
and [bounded variation experiments](https://github.com/General-Deployment/sim/blob/main/docs/SDK_TYPED_EXPERIMENTS.md).
[Portable variation recipes](https://github.com/General-Deployment/sim/blob/main/docs/SDK_VARIATION_RECIPES.md)
add typed distributions, correlated choices, and constraints that can be saved as JSON.
[Named asset metadata](https://github.com/General-Deployment/sim/blob/main/docs/SDK_ASSET_METADATA.md)
describes physical links, joints, materials, dependencies, and semantic object roles.
Existing factory/decorator workflows below remain supported. Local engine APIs
are explicit; `hosted_only=True` refuses a client configured for a local backend.

Upload reusable robots with `simhub asset upload robot.urdf --env robots --name arm`,
then load them in hosted projects with `simhub.asset("arm").path`.
See [named URDF and mesh assets](docs/named-assets.md) for dependency packaging,
project aliases, and version pinning.

The [single-file house-ceiling example](../examples/asbestos_ceiling.py) defines a
photo-derived tracked robot base, two articulated tool arms, a domestic scene, a registered
policy, a frozen evaluation and the Cycles scene in one Python file:

```bash
PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py evaluate
PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py render
PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py export-model
```

The example now uses `simhub.particulate`: two-layer moisture penetration,
contact-work-driven detachment, finite shroud capture, three fibre size bins and
conservative 3D advection/diffusion/settling. All coating, head, particle and
ventilation assumptions remain in the single file. Mass is tracked in kg of the
constituent, including constituent still bound in fragments. Hosted delivery solves the room velocity with OpenFOAM; local material-only runs
use a prescribed velocity. Neither mode resolves shroud-scale flow or predicts
calibrated asbestos clearance.

```bash
PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py material-checks
PYTHONPATH=simhub/src:. python examples/asbestos_ceiling.py diagnostics
```

`material-checks` runs controlled parameter interventions and analytic/grid/time
checks; `diagnostics` renders the recorded state as a scientific animation and
requires Matplotlib plus ffmpeg. New outputs default to
`results/asbestos_ceiling_v3`; earlier outputs remain preserved.
Evaluation retains the original design targets and reports failures without
changing the physical parameters to obtain a pass.

For other scenes, `simhub.render_cycles(project_directory, trajectory_directory,
output_directory, program="scene.py")` invokes Blender locally. A scene program
defines `build_scene(recorded, config)` and returns a mapping of recorded body
names to unparented Blender objects plus an `update(index, fraction)` callback.
The callback can animate recorded fields and cameras; it must not rerun a policy.
Blender uses the best available Cycles GPU backend, with a CPU fallback.

MuJoCo, Cycles and hosted cinematic replay videos default to **60 fps**. A
20-second Blender replay renders 1,200 frames, with positions and rotations
interpolated from the recorded trajectory.
Use `fps=30` in `render_cycles`, or `--fps 30` in the replay and ceiling-example
CLIs, to choose another rate. Hosted cinematic requests accept `Render(fps=30)`;
MuJoCo and Cycles render jobs accept `params.fps`. MuJoCo interpolates joint motion while retaining
the source video's duration (rounded to the nearest output frame). Direct camera
recordings still retain the control rate and the exact images the policy saw.

`Recorder.channel(name, sampler)` records fixed-shape finite numeric fields
alongside body poses. `simhub.SurfaceContamination` and `simhub.CleaningTool`
provide a mass-conserving surface-transfer surrogate. The ceiling example records
remaining coating, surface and bond moisture, size-resolved airborne concentration
and seven mass inventories through this channel API, so the render and evaluation
read the same state. `AirTransport`, `CoatingParameters`, `ExtractionContact`,
`ParticleClass` and `WetCoating` are also exported directly from `simhub`. The hosted Cycles worker uses the same replay
entry point when supplied a Python bundle and `params.program` (use
`"__init__.py"` for a bundled single-file project). The worker must have Blender
and ffmpeg available; no cloud deployment is performed by the local commands.

Define an environment, a policy and a task in Python. Run one episode on your
laptop; run a hundred on GPUs from the same notebook, and get the videos and
the success rates back inline.

```bash
pip install simhub          # typed library and hosted client
pip install 'simhub[all]'   # + MuJoCo, video, plots, dataset export
```

The SDK connects to `https://api.g-deployment.com/` by default. Sign in to the
console, open **API keys**, create one, and set `SIMHUB_API_KEY` to that key.
To use another deployment, set `SIMHUB_API_URL` to its API address or pass
`url=` to `simhub.connect()`. An explicit `url=` takes precedence over
`SIMHUB_API_URL`.

## Pull, edit, and push an existing environment

With `SIMHUB_API_KEY` configured (and `SIMHUB_API_URL` for another deployment):

```bash
simhub pull warehouse-pick ./warehouse_pick
# Edit the downloaded source in ./warehouse_pick.
simhub push ./warehouse_pick --message "Update the picking scene"
```

`pull` accepts an environment name or ID and downloads its latest version.
Omit the directory to use the environment name, or pass `--version current`,
`--version 3`, or a version ID to inspect a specific revision. The destination
must be new or empty. Both commands print JSON with the environment and version
ID; push also reports whether that version is `current`.

The Python equivalent is:

```python
with simhub.connect() as sim:
    path = sim.pull("warehouse-pick", into="warehouse_pick")
    # Edit files, then load the updated project when ready to publish.
    pushed = sim.project(path).push("Update the picking scene")
```

Pull restores an agent workspace snapshot when available, otherwise the
published Python project bundle. It verifies downloaded checksums, preserves
the original Python package name, and materializes retained named assets.
It does not import source or install project dependencies. A version containing
only compiled artifacts has no editable source and returns an error; pull never
silently substitutes an older version.

Keep `.simhub-package` and `.simhub-checkout.json` with the local directory.
The checkout records its API URL, environment ID, and parent version, so changing
the project name does not redirect the next push. These local files are excluded
from uploads. Unchanged asset declarations retain their downloaded versions.
Typed projects also retain their published definitions and parameter values;
use a new typed run request to change those values.

Push refuses a checkout if a newer version already exists. Pull into another
directory and reapply your edits before retrying. A concurrent upload during
push can leave the new version as a branch (`current: false`); the server keeps
the existing current version. Source-only checkpoints can be downloaded for
repair and their pushed repairs likewise preserve this lineage.

## Agent skills

The SDK includes [15 portable agent skills](skills/README.md) for typed hosted
runs, scene composition, bounded variations, assets, project authoring, MuJoCo
and Blender, policies, tasks, local debugging, recording and dataset export.
Each skill describes
the public API, the expected workflow and current implementation limits.

Copy the skill directories into your agent's project skill folder using the
[installation instructions](skills/README.md#install-for-your-agent). They ship
inside the SDK wheel as well as this repository; installing the Python package
alone does not activate them. Start with `$simhub-typed-runs` for a new hosted
integration, or `$simhub-projects` for a runnable local factory example.

## The whole authoring surface

Three things, and none of them is a base class you have to inherit from.

```python
# warehouse_pick/__init__.py
import simhub

@simhub.environment("warehouse-pick")
def build(cell_seed: int = 7) -> simhub.Env:
    from .env import PickEnv          # heavy imports inside, always
    return PickEnv(cell_seed)

@simhub.policy("scripted")
def scripted(env):
    from .policies import ScriptedPick
    return ScriptedPick(env)
```

An **environment** is your own class with five members — `spec`, `reset`,
`step`, `observe`, `score`, and a `done` property. A **policy** has `spec`,
`reset` and `act`. A **task** is data: `simhub.Task(placed="eq:1",
collisions="eq:0")`, or a staged one that gives partial credit.

Beside the package, declare its dependencies and files:

```toml
# simhub.toml
name = "warehouse-pick"
image = "python-gpu"                        # python-cpu | python-gpu
requirements = ["mujoco==3.11.*", "gr00t"]  # installed at container start
scene_requirements = ["mujoco==3.11.*"]     # optional: scene compilation and previews
include = ["*.py", "assets/**"]
```

`scene_requirements` is the complete dependency list for building and previewing
the scene. Keep policy dependencies in `requirements`, which simulation runners
install. If omitted, the compiler installs `requirements` as before; `[]` means
the compiler needs no additional packages. Single-file projects can pass the same
option to `simhub.manifest(...)`. Keep policy imports inside their factories so
loading the project for a preview does not import the policy stack.

Already uploaded bundles keep their original manifests. To recover an older
version without republishing its source, submit `POST /api/jobs/compile` with
that version's `version_id`, its Python `source_artifact_id`, `target_format:
"glb"`, and `params` containing `env`, `seed`, and an explicit
`scene_requirements` array. This override applies only to Python scene previews
and is recorded in the job, derivation cache key, and preview context. Use `[]`
only when the compiler image already provides the scene's dependencies. Failed
preview jobs require an explicit retry; deploying a fix does not resubmit them.

No entrypoint key and no inventory key. The package directory *is* the
entrypoint, and what the project offers is generated by importing it — in your
interpreter, so a syntax error or a missing dependency fails on your machine
rather than ninety seconds into a container.

## One episode, here

```python
import simhub

simhub.run("warehouse-pick", "scripted", seed=3, out="out/one",
           record=simhub.Record(video="hero"))
```

This is the debug loop, not the product: same `rollout`, same output files, one
at a time and without a GPU. Use it to find out your scene works before you
spend money running a hundred of them.

## A hundred episodes, hosted

```python
sh = simhub.connect()                      # hosted API + SIMHUB_API_KEY
project = sh.project("./warehouse_pick")   # imports locally, builds the inventory

batch = project.launch(
    env="warehouse-pick",
    policies=["scripted", "groot-ft"],
    seeds=range(32),
    task=simhub.Stages(target_lifted="gte:1", target_placed="gte:1"),
    record=simhub.Record(cameras=["policy_ext", "wrist"], video="hero"),
    render="cycles",                       # photoreal replay, in the background
)

batch                       # a live table, refreshing
batch.wait()                # follows the server-sent events, per run; returns the batch
batch.dataframe()           # one row per run
batch.videos("hero")        # an inline grid of players, captioned with the verdict
batch.plot.success()        # rates with intervals, checked and claimed apart
batch.cancel()              # one call stops every child
```

The upload is invisible: your project is zipped deterministically and stored by
content hash, so re-launching unchanged code uploads nothing. **Nobody builds
or deploys a container image to add a new simulation.**

## Why hosted at all

Anybody can step MuJoCo in a loop for free. The reason to reach for this is
everything that is not the loop:

| what you want | why a laptop cannot |
| --- | --- |
| evaluate a VLA | GR00T N1.7 is 3B parameters on CUDA; pi0.5's checkpoint is gigabytes |
| a batch, not an episode | 192 episodes one after another against 16–64 at once |
| any video at all | 3.0 ms a frame on an A10G against 355 ms on CPU OSMesa |
| photoreal video | Cycles at 1080p is "under an hour" locally, minutes fanned over GPUs |
| fine-tune on what you recorded | never a laptop |
| a number worth citing next month | a result from somebody's machine has no digest, no row, and nothing to compare against |

## Evaluations

An eval is a **frozen** set of episodes, so two policies are scored on the same
problems rather than on different random draws. The suite's content digest *is*
its version.

```python
suite = simhub.Suite.grid("warehouse-pick/v1", cell_seed=range(8), target_seed=range(8))
ev = simhub.Eval(suite=suite, task=simhub.Task(target_placed="eq:1"),
                 primary="success", truncated_is="failure")

report = project.evaluate(ev, env="warehouse-pick",
                          policies=["scripted", "groot-ft"]).wait().report(ev)

report.table()                          # n, coverage, rate with its interval
report.compare("groot-ft", "scripted")  # paired: effect, interval, p
report.failures(policy="groot-ft")      # runs grouped by failure mode; join
                                        # to batch.videos() on run_id for clips
```

Five rules the report enforces, because each is a mistake that is easy to make
and expensive to publish:

1. **Checked and unchecked verdicts are never averaged.** A verdict a stored
   predicate computed and one a runner asserted are different kinds of number.
2. **Coverage is reported before the score.** A crashed run is a missing
   observation, not a policy failure. An eval at 80% coverage is not an eval.
3. **Intervals, not bare fractions.** 12/20 is 0.60 with a 95% Wilson interval
   of [0.39, 0.78]; "8/20 to 12/20" is inside that noise.
4. **Comparisons are paired and come back as an effect, not a ranking.** Two
   policies that ran different suites raise rather than being compared.
5. **Determinism is checked, not assumed** — `report.check_determinism()`
   re-runs one episode and asserts the metrics match.

## What every run produces

The same layout whether it finished or died halfway:

```
trajectory/000000.npz  ...  every body's pose and quaternion (wxyz), per step
trajectory/meta.json         body names, units, the quaternion convention, in words
video/hero.mp4               captioned with the instruction and the exact buffers
renders/<camera>/*.png       when you ask for frames
metrics.json                 what score() reported
result.json                  the verdict, on both axes
checkpoints/000200.npz       resume points; one is written on SIGTERM
```

One recording, three readers: the video, the evaluation and
`simhub.datasets.lerobot(batch, ...)` — which exports the same episodes in the
format GR00T and pi0.5 fine-tunes read.

## Costs, and the one line that controls them

Rendering is the entire cost of a batch: a physics step is 0.08 ms and a GPU
frame is 1.7–8.4 ms. So a policy declares what it reads:

```python
simhub.PolicySpec(action_dim=7, control_hz=15.0, expects=("observation/state",))
```

and the rollout renders exactly that. A scripted demonstrator that never looks
at an image renders **no frames at all**.

## Checking a scene you just generated

```python
report = simhub.check("./savant_lobby", env="savant-lobby")
report.ok, report.images   # links to pictures, not numbers
```

Five assertions: it imports and resolves; every policy camera actually sees
something; everything rests under gravity; `score()` reports every metric the
spec declares; one episode completes. Non-zero exit when it does not — which is
what an agent generating scenes can branch on.

## Modelling the scene in Blender

A project whose scene is easier to model than to write can build it with `bpy`
and hand simhub the result. Nothing about it is a special case: the contract is
still `write_mjcf(out, seed, **kwargs)`, and the same
`python_bundle -> mjcf_bundle` conversion turns it into a scene.

```python
from simhub import blender

def write_mjcf(out, seed=0, **kwargs):
    import bpy                                    # heavy import, inside

    bpy.ops.wm.read_factory_settings(use_empty=True)
    build_the_dock(seed)                          # your code, your bpy

    blender.static(bpy.data.objects["plinth"])    # welded to the world
    blender.body(crate, density=450, friction=0.9)  # falls, collides
    blender.body(ball, mass=0.35, collision=proxy)  # cheap collision stand-in
    blender.exclude(backdrop)                     # not in the simulation

    return blender.export(out, name="depot")
```

Cameras and lights come across on their own — a Blender camera and a MuJoCo
camera share a convention, which is the one thing here needing no conversion.
Marks are stored as custom properties, so a scene keeps its meaning through a
`.blend` round-trip.

Two things shape how you model. **Collision is convex**, because MuJoCo's is:
each object collides as the hull of each of its loose parts, so a room wants to
be several objects rather than one welded mesh, and `collision=` takes a
low-poly proxy when a hull is wrong. **Mass comes from the hull**, not the
visual mesh — a hollow shell and its hull weigh different amounts and only one
of them is the physics.

Articulation is not Blender's job. A robot's joints, actuators and limits
already have a good representation in MJCF, so the Blender program builds the
*set* and `write_mjcf` attaches the arm before returning the path.

### Or ship the `.blend` itself

A set modelled in Blender's UI rather than written as a program is a complete
input, because the marks live on the objects: `blender.body` and friends write
custom properties, so they survive a save.

```python
from pathlib import Path
from simhub import blender

def write_mjcf(out, seed=0, **kwargs):
    blender.open_file(Path(__file__).parent / "dock.blend")
    return blender.export(out, name="dock")
```

```toml
include = ["*.py", "*.blend"]   # the default list is text formats only
```

Nothing new happens to it. A `.blend` is a file in the project directory, so it
travels inside the ordinary `python_bundle` -- content-addressed, deduped, and
re-uploading nothing when it has not changed -- and the compile is the same
`python_bundle -> mjcf_bundle` conversion. There is no `blend` artifact format
and no second upload, deliberately: a fifth format would be a fifth thing to
keep in step with the other four, in exchange for behaviour that is already
here. [`examples/blend_file_dock`](../examples/blend_file_dock) is the worked
example.

One thing it is not: a `.blend` is one fixed layout, so it is a set rather than
a suite. Sixty-four seeds against it are sixty-four copies of one episode, and
a batch that says `randomised` is refused server-side for exactly that. Vary in
`reset`, or author the set as a program the way `blender_depot` does.

### Without Blender on your machine

You do not need `bpy` locally. Listing what a project offers never imports it --
every Blender call lives inside the scene program's body, the same rule every
project follows for MuJoCo and torch -- so `Project.load` works on any
interpreter and the wheel is only ever installed in the image that compiles:

```bash
export SIMHUB_API_KEY=sk_...
```

```python
sh = simhub.connect()
batch = sh.project("./my_scene").launch(
    env="depot-settle", policies=["sweep", "simhub:hold"], seeds=range(8),
    task=simhub.Conditions(settled="gte:4"),
    record=simhub.Record(video="hero"),
)
batch.wait(); batch.report().table(); batch.videos()
```

The deployment registers `python-blender` as its `python_bundle -> mjcf_bundle`
compiler, the compile runs in an image that has Blender, and the scene lands on
the version as an ordinary `mjcf_bundle`. Every simulate job after that gets it
on disk; an environment that wants it declares a `scene` parameter and the
runner fills it in.

That indirection is not fastidiousness: `bpy` publishes one wheel per CPython
release and skips 3.12 entirely, so "put Blender in the worker" is not something
a 3.12 deployment can do at any price. Behind the compiler protocol the image
picks its own interpreter and nothing else has to agree.

[`examples/blender_depot`](../examples/blender_depot) is the worked example, end
to end.


### Coupled room CFD and recorded-video artifacts

The `python-multiphysics` runtime adds distro OpenFOAM and official Blender to
MuJoCo on an A10G runner. Like other runtimes, it is declared by `runner.toml`
and registered by `.github/scripts/render_runners.py` for `deploy.yml`.
`simhub.manifest(image="python-multiphysics", ...)` selects it for a single-file project.

`simhub.openfoam.solve_room(...)` writes a reproducible room case, executes
`blockMesh`, `checkMesh` and transient `pimpleFoam`, and validates discrete
continuity. `apply_flow(transport, "velocity.npz")` transfers the solver's
oriented face fluxes to `AirTransport` without interpolating a nonconservative
cell-centred velocity. The current adapter supports a rectangular room with
no-slip walls, clean makeup air at y- and an outlet at y+. It does not resolve
the robot or the moving shroud; release, capture and aerosol mixing laws still
require calibration.

An environment can expose `finalize_run(out, result)` to produce additional
artifacts after its recorded rollout. Return file paths contained within `out`.
The hosted Python runner announces these using the normal artifact protocol
and remains running until the hook completes. A failed hook fails the run.
This supports evaluations and Cycles replay from the very trajectory that was
scored, without a separate manually uploaded demonstration video.

`examples/asbestos_ceiling.py` declares the scene, photo-derived robot,
controller, material assumptions, CFD inputs, evaluation suite and rendering
in one file. Dimensions and lift configuration are estimates from photographs;
the policy is scripted, and the material model is not a clearance prediction.

Public viewer links can open `/api/shared/view?token=...&run_id=...`. The page
shows the scoped run's execution status, task outcome, metrics and stored
video/artifacts without an account. The token is revocable and expires; never
substitute an organization API key in this URL.


## Run pages and evaluation output

Every run has an organization-scoped console URL:
`https://simhub-console.pages.dev/runs/<run_id>?org=<organization-slug>`.
Open a run card to watch its recording and inspect its metrics, timing, status,
and task evaluation. Copy link preserves this address through sign-in and
refresh. The recipient needs access to that organization; no API key is included.

`rollout()` writes and announces `evaluation.json` (`schema: simhub.evaluation/1`)
as an ordinary artifact. It adds display metadata and the local task verdict to
the existing flat `metrics.json` and `result.json` outputs. Custom metrics need
no console changes:

```python
spec = simhub.EnvSpec(
    id="pick-place",
    action_dim=7,
    metrics={
        "placement_error": simhub.MetricSpec(
            unit="m", better="lower", required=True,
            label="Placement error", description="Distance from object to target",
        ),
        "objects_placed": simhub.MetricSpec(label="Objects placed", unit="objects"),
    },
)
# Env.score() returns {"placement_error": 0.003, "objects_placed": 2}.
# simhub.rollout(...) records those values and their metadata automatically.

with simhub.connect() as sh:
    evaluation = sh.run_evaluation("run_...")
    print(evaluation["metrics"], evaluation["metric_specs"])
```

Reports include metrics for interrupted and failed episodes too. Nonfinite
numbers become JSON null and display as “Not reported”; a required metric that
is missing or null refuses a clean exit. Arrays and nested JSON statistics are
preserved. Task definitions and `local_verdict` describe evaluation inside the
runner. The hosted run's metrics, outcome, outcome source and checked flag remain
authoritative, including when a server task overrules the runner.

Older runs without this artifact still expose their recorded metrics. The SDK
raises a clear error for unsupported sidecar versions; the console reports that
error and keeps the server's metrics visible. Custom runner loops can build a
report with `simhub.evaluation.evaluation_report()` and publish it with
`Emitter.evaluation(report)` alongside their existing metric/result events.

## Hosted model credentials

Hosted SimHub configures credential storage automatically; the CLI and SDK do
not require a storage-provider setting.

`simhub model credentials set NAME` securely uploads an HF token via a hidden
prompt (or `--token-file` / `--stdin`). `simhub model register` creates a pinned
HF policy revision bound to that credential, and
`project.launch(policy_version_id=...)` records it on the runs. This requires
the matching hosted API/worker/runner deployment. See
[MODEL_CREDENTIALS.md](MODEL_CREDENTIALS.md) for the contract and deployment steps.

## Inspect a generated scene in the console

Publish a read-only, orbitable initial scene without running a policy:

```python
import simhub

with simhub.connect() as session:
    project = session.project("./warehouse_pick_cell")
    preview = project.preview(env="warehouse-pick", seed=7, env_kwargs={"cell_seed": 7})
    print(preview.get("console_url") or preview["artifact_id"])
```

The equivalent CLI entry point is `simhub launch ./warehouse_pick_cell --preview
--env warehouse-pick --seed 7`. Preview generation is an explicit hosted compile
job and can incur compute charges. The deployment must advertise a
`python_bundle -> glb` compiler with the project's dependencies. The operation
constructs the selected environment and calls `reset(seed)`; it never steps a
policy. Environments that declare a `scene` argument get their project's
`write_mjcf()` output before construction. `env_kwargs` go to the environment
factory, while `seed` controls reset and any required scene build.

The console's **Scenes** tab lists initial previews and compiled templates for
the active environment. New SDK runs with MuJoCo recorder bindings also publish
the scene immediately after reset: **Runs → View scene** opens that run's exact
snapshot, including its seed and attempt. An older run without a snapshot says
so. Merely opening a scene reads stored artifacts; it does not start compute.

Operators set `SIMHUB_CONSOLE_URL` to return a shareable organization-scoped
console link. The link contains no API key; viewers still need access to that
organization. Existing MJCF bundles have a limited browser fallback and an
explicit **Generate full preview** action when the compiler supports it.

The first version displays rigid MuJoCo geometry, authored colors, mesh normals,
UV-mapped 2D textures, body selection, bounds and named perspective cameras.
Procedural texture projections, heightfields, skins and deformables report
coverage warnings. It displays the physics scene rather than a separately
constructed Cycles set, and does not replay trajectories. GLB content is limited
to 64 MiB and manifests to 2 MiB. Preview failures do not change simulation
outcomes; recordings remain available from the run page.

For failed-stage diagnostics, saved receipts, cancellation, and independent rerendering,
see [hosted run recovery](https://github.com/General-Deployment/sim/blob/main/docs/SDK_HOSTED_RECOVERY.md).
