Metadata-Version: 2.5
Name: antioch-sim
Version: 0.3.32
Summary: Antioch simulation SDK: write typed Isaac Sim scenarios locally, run them on warm GPU machines.
Project-URL: Homepage, https://antioch.com
Project-URL: Documentation, https://console.poc.antioch.com/docs
Author: Antioch Robotics
License: Proprietary
Keywords: gpu,isaac-sim,rerun,robotics,simulation,telemetry,usd
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Typing :: Typed
Requires-Python: <3.13,>=3.12
Provides-Extra: isaac-601-ga
Provides-Extra: isaac-lab-30b2
Description-Content-Type: text/markdown
Requires-Dist: click>=8.1
Requires-Dist: cryptography<50,>=46
Requires-Dist: docker<8,>=7.2
Requires-Dist: httpx>=0.28
Requires-Dist: ipykernel<8,>=7.3
Requires-Dist: jupyter-client<9,>=8.6
Requires-Dist: jupyter-server<3,>=2.17
Requires-Dist: jupyterlab<5,>=4.5
Requires-Dist: nest-asyncio<2,>=1.6
Requires-Dist: paramiko<4,>=3.5
Requires-Dist: pathspec<2,>=0.12
Requires-Dist: pydantic<3,>=2.11.10
Requires-Dist: pyyaml<7,>=6.0.3
Requires-Dist: rerun-sdk==0.36.0
Requires-Dist: rich<15,>=14
Requires-Dist: tornado<7,>=6.5
Requires-Dist: traitlets<6,>=5.14
Requires-Dist: watchfiles<2,>=0.24
Requires-Dist: websocket-client>=1.9

# Antioch simulation SDK

`antioch-sim` is the typed Python SDK and command-line interface for running
Isaac simulations on warm Antioch GPU machines. Write ordinary Python on your
computer. The remote simulation image supplies Isaac, so no local simulator or
GPU is required.

The public wheel contains the `antioch` API, the CLI, shared models, and the
supported Isaac typing surfaces. The public preview uses the Antioch staging
service and requires Python 3.12.

## Quick start

Install [uv](https://docs.astral.sh/uv/getting-started/installation/) if it is
not already available:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

In an empty directory, create a Python 3.12 project and choose one engine:

```bash
uv init --bare --python '>=3.12,<3.13'
uv python pin 3.12
uv add --compile-bytecode "antioch-sim[isaac-601-ga]"
export ANTIOCH_ENV=staging
uv run antioch auth login
uv run antioch init
uv run antioch run src/main.py
```

The login command prints a short code and a URL to open in a browser. The run
command allocates or reuses a GPU machine, starts the generated Isaac Sim 6.0.1
example, relays its output, and prints a Mission Control link for its stream.

Use `antioch-sim[isaac-lab-30b2]` instead to create an Isaac Lab 3.0.0-beta2
project. Choose exactly one engine extra per project. In an existing Python
project, skip `uv init` and `uv python pin`.

## Install the SDK

The engine extra selects the image and examples that `antioch init` writes.
The wheel already contains every supported typing surface. `uv.lock` pins the
SDK and its third-party dependencies for repeatable installs. Isaac itself is
not a Python dependency and is not installed on your computer.

`antioch init` creates local files only. It does not allocate a machine,
register a remote project, or replace existing source. If a package manager
does not save the extra in `pyproject.toml`, select the engine explicitly:

```bash
uv run antioch init --engine isaac-601-ga
```

Activate `.venv` if you want to omit `uv run`:

```bash
source .venv/bin/activate
antioch --help
```

Mission Control already includes the authenticated CLI and example projects,
so its hosted workspace does not need a local SDK installation.

## Update a project

Refresh the SDK version in the lock file and reinstall the environment:

```bash
uv lock --upgrade-package antioch-sim
uv sync --compile-bytecode
uv run antioch --version
```

For an existing project, also change `services.sim.image` in `antioch.yaml` to
the engine and SDK version that the new lock selected. For example:

```yaml
services:
  sim:
    image: antioch-sim/isaac-601-ga:0.3.32
```

If the project has a Dockerfile, update its matching `FROM` coordinate. The SDK
and remote image must use the same version. Do not run `antioch init` again over
an existing project.

## Remove the SDK

```bash
uv remove antioch-sim
```

This removes the dependency from the Python project. It does not delete source,
`antioch.yaml`, recorded scenarios, or remote assets. Run `antioch auth logout`
before removal if you also want to remove the local login.

## Complete commands in your shell

Every command, subcommand, option, and enumerated value completes on Tab.
Add the line for your shell to its startup file:

```bash
# ~/.bashrc
eval "$(_ANTIOCH_COMPLETE=bash_source antioch)"
# ~/.zshrc
eval "$(_ANTIOCH_COMPLETE=zsh_source antioch)"
# ~/.config/fish/completions/antioch.fish
_ANTIOCH_COMPLETE=fish_source antioch | source
```

Completion resolves the one command being typed, so it stays as fast as the
CLI itself.

## Sign in

Sign in with `antioch auth login`. The CLI prints a short code and confirmation
URL that you can open in any browser, so the flow also works over SSH. Signing
in again replaces the active session. `antioch auth whoami` displays the active
user and organization, while `antioch auth switch` selects another available
organization. The CLI stores the login securely under `~/.config/antioch` and
shares it across projects. `antioch auth logout` removes the local login and
machine access from this computer.

## Your first script

`antioch init` creates local files only; it does not register a remote project,
allocate a machine, or replace existing source. It creates a src-layout project with a visible example simulation,
repeatable scenarios, and smoke and sweep suites. For Isaac Sim, generated
`src/main.py` rains colored cubes onto a ground plane; for Isaac Lab it drives
a cartpole. Both step physics and render for 30 seconds by default. Run the
local file on a remote machine with:

```bash
antioch run src/main.py
```

The generated simulation keeps the Isaac viewport responsive long enough to
watch the motion. Streaming is on by default; pass `--no-stream` for a headless
run. `antioch.boot()` consumes that process-scoped declaration
wherever it is called in the program. Before Isaac starts, the command prints a
Mission Control link for each machine so you can watch its livestream. The target
remains ordinary Python: it can use native Isaac APIs, parse arguments, and
control when Isaac starts. Arguments after `--` reach the script unchanged;
stdout, stderr, and the exit code remain the process's own.

`run` starts one finite process on one machine; `--machine` pins which one.
Ctrl-C signals that exact process gracefully first, and a second Ctrl-C
force-kills only it. Native programs can tune render fidelity directly:

```python
import antioch

antioch.boot(render_quality="quality", viewport=(1920, 1080))
```

`render_quality` picks a DLSS fidelity preset from `"performance"` through
`"ultra"`; `viewport` sets the main non-streamed render target. Leave either
unset to keep its default. Streaming stays a launch concern: it is on by
default, while `--no-stream` opts out; the runtime chooses a safe initial
encoder target, and the
connected browser takes over dynamic sizing. Simulation code does not
configure transport policy.

Antioch does not override Isaac or Kit logging by default, so output matches
the selected engine's native behavior. Set a level only when you deliberately
want to change that behavior:

```python
antioch.boot(log_level="error")  # or "fatal" / "warning" / "verbose"
```

For decorated scenarios, `antioch scenario run` keeps its concise live board while
`antioch scenario run --verbose` relays the informational process output.

## Your first scenario

A scenario turns a script into a named, repeatable simulation: decorate a
function with `@antioch.scenario` and Antioch can collect it, set its
parameters, run it across many machines, and save its source hash, parameters,
results, and telemetry. `antioch scenario run` collects every scenario under the manifest's
explicit `scenario_paths`, or the sim build-context discovery scope when that
field is omitted, and runs the selection on one project machine by default.
Pass `--machines N` or repeat `--machine` to fan out explicitly:

When `scenario_paths` is omitted, the sim build context is scanned recursively.
The default boundary skips hidden directories, virtualenvs (directories with
`pyvenv.cfg`), `node_modules`, common cache/build directories, and paths matched
by the context's `.gitignore` or `.dockerignore`. Use `scenario_paths` when a
project needs a narrower or different source scope.

```bash
antioch scenario run                        # every scenario in the project
antioch scenario run --scenario falling_cube --set drop_height=4
antioch scenario run --scenario falling_cube --case resting
antioch scenario run --scenario falling_cube -t smoke
antioch scenario run --scenario falling_cube --scenario cartpole_balance
antioch scenario run -t physics --exclude-tag slow
antioch scenario run -t physics -t nightly      # repeat -t for a union of tags
antioch scenario run --path src/physics     # discover somewhere other than the manifest
antioch suite run nightly        # named suite from antioch.yaml
antioch scenario run                        # livestream by default
antioch scenario run --no-stream            # headless run
```

The CLI prints the console URL to watch each machine on before it dispatches,
because the window worth watching is the run itself. The stream
server needs roughly fifteen seconds of engine boot before it accepts a
viewer, so a scenario that finishes in under half a minute is largely over by
the time the first frame arrives — give a run you intend to watch something to
do.

A scenario receives its run as the first argument. Use that run to save
results and artifacts, and to report the outcome. Annotating it is what makes the
handle discoverable in an editor; the SDK supplies that one name when it
reads your signature, so a module with `from __future__ import annotations`
can leave `ScenarioRun` behind `if TYPE_CHECKING:` rather than importing it
at runtime.

```python
import antioch

logger = antioch.Logger("cube")


@antioch.scenario(tags=["physics"], sim=antioch.BootProfile(physics_dt=1 / 240), cases=[antioch.case({"drop_height": 0.0}, id="resting", tags=["edge"])])
def falling_cube(run: antioch.ScenarioRun, drop_height: float = 2.0) -> None:
    world = antioch.world()
    ...
    logger.scalar("height", z)  # -> entity path "cube/height"
    run.add_result("final_z", z)
    run.add_artifact(output / "trace.csv")
    run.check("came to rest", z < 0.4, detail=f"rested at {z:.3f} m")
```

`sim=` declares what the scenario needs to be correct — physics and render
steps, render fidelity, Isaac log severity, the PhysX/Newton backend, and native Kit arguments. How you
*watch* a run (streaming is default; `--no-stream` and `--verbose` stay on the CLI). `--verbose`
changes whether process bytes are relayed live; it never changes which bytes
Isaac emits.

For a supported frame from the active viewport, aim Kit's USD camera with
`set_camera_view(...)`, step the world, and call `antioch.capture_viewport()`.
It returns an RGB-compatible array or `None` when no active viewport exists.
The legacy `isaacsim.sensors.camera.Camera` path is blocked on Isaac Sim 6.0.1
because its second render product can hang `World.step(render=True)`; use the
active viewport helper or the experimental RTX camera APIs instead. Newton is
opt-in with `antioch.BootProfile(physics_engine="newton")` and is not
available on Isaac Lab images.

Omit `sim` to use the default Kit profile. Pass `sim=None` when the scenario
does not need a simulator at all; the runner still records its result and
telemetry, but it does not boot Kit. Use `--no-stream` for these runs because
there is no simulator viewport to stream.

A `Logger` is a channel prefix and nothing else, created once at module
scope like `logging.getLogger` — it captures no run, so the same instance
serves every run the process executes. Its text always reaches the terminal
and additionally reaches the recording whenever a run is active, so a helper
shared between a scenario and a standalone script logs correctly in both.
Results belong to the run, not the logger.
`antioch.current_scenario_run()` reaches the run from an imported helper
that cannot take it as an argument.

`run.check(criterion, passed, detail=...)` is how a scenario says what its
task was and whether it was done. Every check is recorded and measured — a
failing one does not stop the body — and a run that fails any check finishes
`FAILED`, so the outcome is the task's outcome rather than a proxy for whether
the process survived. Reach for `assert` or `run.fail` only where the run
genuinely cannot continue.

The `ScenarioRun` handed to a decorated scenario is the whole authoring
surface: it saves what that scenario produced and cannot start, stop, or
finalize unrelated processes. Raw `antioch run`, direct `exec`, and notebook
processes do not save structured results; author a scenario when results must
be retained and reviewed.

### Durable outputs for long runs

Plain `antioch run` output and files under `/workspace/output` are machine-local
scratch. They are not a durable process record and disappear when the machine
is released. For training or dataset generation, make the work a scenario and
register each checkpoint with the existing scenario artifact seam:

```python
from pathlib import Path

import antioch


@antioch.scenario(tags=["training"])
def train(run: antioch.ScenarioRun, steps: int = 10_000) -> None:
    """Train a model and retain checkpoints as scenario artifacts."""

    output = Path("/workspace/output/checkpoints")
    output.mkdir(parents=True, exist_ok=True)
    for step in range(steps):
        # Run one training step and write a complete checkpoint atomically.
        if step % 100 == 0:
            checkpoint = output / f"step-{step:06d}.ckpt"
            checkpoint.write_bytes(b"checkpoint bytes")
            run.add_artifact(checkpoint, name=f"checkpoints/{checkpoint.name}")
```

Run and retrieve it with `antioch scenario run --scenario train`, then
`antioch scenario download SCENARIO_RUN_ID`. Each successful
`run.add_artifact(...)` upload is retained in the scenario run even if the
machine is later killed or reaped. A direct `antioch services cp
sim:/workspace/output/FILE ./FILE` is an immediate copy for a plain run; it
does not replace scenario artifact registration.

Scalars you log land in the scenario run's Rerun recording. Managed scenarios
and native `ScenarioSession` runs also sample Kit's existing active viewport
every 0.5 seconds of simulation time, logging JPEG frames to
`/antioch/viewport` at up to 640 pixels wide and capped at 600 frames. An
authored camera can keep using `/viewport`; the platform read-back has its own
path so the two writers never interleave. Capture does not create or frame a
camera; set the active viewport camera in simulation code when composition
matters. A run that never steps a simulator simply has no platform viewport
entity. Use `@antioch.scenario(capture=False)` or
`ScenarioSession(..., capture=False)` to opt out; batch suites can disable the
default globally with `ANTIOCH_TELEMETRY_CAPTURE=0`.

The recording is finalized with an automatic Rerun blueprint:
`/antioch/viewport` is shown in a spatial view, each logged scalar gets a
time-series view, and three-dimensional entities get a spatial view. To choose the layout yourself,
call the one canonical setter while the run is active; it suppresses the
automatic layout and is stored in the downloaded RRD as well as the live
viewer:

```python
import rerun.blueprint as rrb


@antioch.scenario()
def falling_cube(run: antioch.ScenarioRun) -> None:
    run.set_blueprint(rrb.Blueprint(rrb.TimeSeriesView(origin="/cube/height")))
    ...
```

To inspect the installed 0.36.0 reader's blueprint store in a downloaded RRD,
use `rerun.experimental.RrdReader("run.rrd").blueprints()`; the same reader's
`stream(store=...)` exposes the logged entities and their time columns.

The webapp renders that recording on the scenario-run detail page, where
results, telemetry, logs, and artifacts remain grouped under the authored run.

Every reusable parameter variation is an addressable case, and
`antioch.case(...)` is its only spelling. One declaration can be a singleton,
a Cartesian grid, or correlated combinations; all three expand locally into
the same flat case catalog:

```python
@antioch.scenario(
    cases=[
        antioch.case(id="default", tags=["smoke"]),
        antioch.case({"drop_height": 0.0}, id="resting", tags=["edge"]),
        antioch.case({"capture_batches": 100}, grid={"seed": range(50), "friction": [0.3, 0.8]}, id="seed-{seed}-mu-{friction}", tags=["production"]),
        antioch.case(combinations=[{"mass": 1.0, "speed": 2.0}, {"mass": 4.0, "speed": 0.5}], id="mass-{mass}-speed-{speed}"),
    ],
)
def falling_cube(...): ...
```

The positional mapping holds one case's values, or values shared beneath a
grid/combinations expansion. Grid axes follow scenario-signature order and
authored value order. An omitted id derives from the explicit values; an id on
any case is a format template over resolved parameters, and one without braces
is unchanged. A scenario with no
`cases=` contributes one implicit default run. Once it declares cases, it runs
exactly those cases—add `antioch.case(id="default")` when defaults should remain
in the catalog. Select one or more with repeated `--case`, a family with `-t edge`, and
see the expansion with `antioch scenario collect --json`.

Suites are immutable unions of exact selector clauses. Clauses are ORed; fields
inside one clause are ANDed; values in `paths`, `scenarios`, and `cases` are
exact alternatives. Required and excluded tags read the union of scenario and
case tags:

```yaml
suites:
  acceptance:
    description: "Warehouse acceptance"
    select:
      - paths: ["scenarios/safety"]
        tags: ["smoke"]
        exclude_tags: ["slow"]
      - scenarios: ["falling_cube"]
        cases: ["resting", "extreme"]
```

Overlapping clauses produce one scenario run at the first matching position, and
each clause must match at least one case so a typo cannot silently shrink a
suite.

`antioch scenario collect` inspects authored definitions entirely locally, and
`antioch suite collect` expands every declared suite to the exact scenario
files and cases it would run.
`scenario list`, `show`, `logs`, `delete`, and `suggest` use the scenario
surface; `suite run`, `list`, `summary`, `show`, and `cancel` use the distinct suite
surface. If a deployment includes runs from an earlier Antioch version, the
same commands include those runs and identify them as legacy. Use **Settings**
in the webapp to show or hide legacy data for the account. Each run shows only
the actions that it supports, such as deletion, rerun, or output retrieval. Scenario
decorators
can override catalog names and descriptions, carry a Rerun blueprint, opt out
of default viewport capture with `capture=False`, and declare validated case
catalogs. Prefer `run.set_blueprint(...)` when a layout depends on what the
scenario actually logged. Scalar parameters support descriptions
and inclusive numeric bounds through `antioch.param`; `Literal[...]`
annotations become closed CLI choices. A plain scenario selection expands
every declared case. `--case` selects one or more declared cases by id; repeat
the option to select several. `--set`
with an exact scenario creates one ad-hoc run from its defaults. The two
modes cannot be combined. Named suites remain immutable.

The history commands keep the webapp's full operational query surface while
adding script-oriented filters. Both scenario and suite histories filter by
project, member, phase, outcome, time, and interactive or queued dispatch;
scenario history additionally filters exact scenario and invocation ids,
suite membership, tags, params, and results. Repeat `--phase` or `--outcome`
on `scenario list` to select a union of display states. Every list cursor pins
that exact query, so continuation commands pass only `--cursor` and `--limit`.

`antioch scenario show SCENARIO_RUN_ID` prints results and every named output.
Retrieve one output or the complete set directly from object storage with:

```bash
antioch scenario download SCENARIO_RUN_ID --artifact telemetry
antioch scenario download SCENARIO_RUN_ID --artifact metrics/probe.csv -o output/
antioch scenario download SCENARIO_RUN_ID -o output/  # every artifact
```

Legacy outputs use the same command even when the older stack recorded no size
or digest. A legacy suite invocation id is only unique inside its suite, so
open it with `antioch suite show SUITE_RUN_ID --suite NAME`.

## Queued runs

`--queue` builds or resolves the selected Docker-backed services on the
submitter's assigned machine, adds the current project source to the simulation
image, and pushes the digest-pinned environment to your organization's private registry.
`antioch suite run NAME --queue` stores one suite run plus all of its scenario
children, while `antioch scenario run --queue` stores standalone scenario runs.
Queued fan-out draws on the same per-user machine quota as interactive work,
and Antioch distributes children fairly across eligible warm machines:

```bash
antioch suite run nightly --queue
antioch suite show SUITE_RUN_ID --follow
antioch suite cancel SUITE_RUN_ID
antioch suite rerun SUITE_RUN_ID
antioch scenario run --tag smoke --queue
antioch scenario cancel SCENARIO_RUN_ID
antioch scenario rerun SCENARIO_RUN_ID
```

Queued runs save their exact digest-pinned environment before Antioch
distributes them. For a single-machine interactive run, Antioch captures the
admitted `sim` container's project workspace at dispatch and then attempts to
save the exact images and source the run used. When that capture and publish
succeeds, `scenario rerun` and `suite rerun` create fresh queued runs with the
same environment, parameters, and cases. They do not change the original run.
Multi-machine interactive runs are not currently rerunnable. Repeat the
original command or use `--queue` when the result must be rerunnable. Antioch
explains when an older run or a failed source capture or publish leaves the
environment unavailable.

`--follow` reports the state it finds on connect and then every change,
so a suite waiting on capacity says so instead of going quiet. There is no estimated
finish time, because the fleet cannot honestly predict one. A followed
suite is a stream rather than a document, so `--follow --json` emits
line-delimited frames: one `state` frame per observed state and a final
`completed` frame carrying the suite-run view. Without `--follow`,
`--json` remains the single indented document it has always been.

Every finite history command supports `--json`. Every paged response — each
`list` and `suite summary` — uses the same
`{"items": [...], "next_cursor": "..."}` shape; pass `next_cursor` back with
`--cursor` and do not repeat filters, because the opaque token carries the
original query. Every `*_at` field in JSON is a Unix timestamp in microseconds;
humanized clock labels stay in human tables. Repeatable filters are unions,
while repeating a scalar option is a usage error. JSON failures are written to stderr as one `error` object with
a stable type, exit code, HTTP status when the server answered, and `retryable`.
Interactive terminals keep raw stdout/stderr or print destination paths, and
their help names that exception. Finite file transfers accept `--json` and
return a manifest with the destination, byte count, checksum, and replacement
fact.
Scenario and suite history commands use a customer-facing projection. Their
JSON keeps run identity, suite and project links, dispatch provenance, authored
inputs, lifecycle state, outcomes, timings, results, logs, artifacts, and
capability flags. It omits tenant keys, raw user subjects, queue claims,
assignment and process identities, attempt counters, and generation data.
See the [Antioch agent plugin JSON reference](https://github.com/antioch-robotics/antioch-agent-plugin/blob/main/skills/antioch-platform/references/json.md)
for the shared contract; use each command's `--help` for its current flags and
payload.

## Versioned assets

Assets are immutable named file revisions shared across the active organization.
`antioch assets push`, `list`, `show`, `pull`, and `repair` expose the asset library in the CLI.
Authenticated Python tools can use the same signed direct-storage path:

```python
from antioch import load_asset, save_asset

asset = save_asset("robots/commissioning-arm", version="v1", source="robot.usda", description="Validated commissioning geometry")
prim = load_asset(asset.name, prim_path="/World/CommissioningArm", version="v1")
```

Saves always land in the current organization's store. Loads resolve against
one asset library — organization assets, Antioch's shared assets, and the
legacy stack, with a current asset shadowing a legacy one of the same
name. Antioch transfers asset bytes directly to object storage, and the client verifies the stored
size and SHA-256 before publishing the local destination atomically. A
legacy revision has neither, because the legacy stack recorded neither, so its
bytes are checked against the provider's declared length alone.

## Choosing an engine

A project selects its engine with an image coordinate:
`antioch-sim/<engine>:<sim-version>`. `antioch init` detects the installed
engine extra and stamps that coordinate into an image-first `antioch.yaml`;
edit it to upgrade. The platform reads runtime engine identity and SDK version
from the image's labels. Add a Dockerfile only for custom dependencies and use
the same coordinate in its `FROM` line; saved run environments pin the resulting image
by digest.

The public `antioch-sim` wheel includes the supported engine typing surfaces.
The declared extra selects the initial scaffold and is locked in `uv.lock`:

```toml
dependencies = ["antioch-sim[isaac-lab-30b2]"]
```

Antioch's own building blocks state which engines they support, because the
engines genuinely differ: some are Isaac Lab APIs with no Isaac Sim
equivalent, and some need a minimum release. Using one outside its support
says so directly rather than failing somewhere deep inside Isaac:

```text
wrap_task_env requires Isaac Lab 3.0+ but this image runs Isaac Sim 6.0.1
('isaac-601-ga')
```

Support only widens: a component that works on an engine keeps working on
later releases of it, and new engines are added without changing your code.

### Base image inventory

The published engine image is the base for a project Dockerfile. The
`isaac-601-ga` core starts from Ubuntu 24.04 and includes the Isaac Sim 6.0.1
runtime, Python 3 with pip, venv, and development headers, `uv`, `git`, and
`git-lfs`. It also includes the runtime graphics and audio libraries that Kit
needs: Vulkan and `vulkan-tools`, GL/EGL/GLES/GLVND, X11, and audio support.
The Isaac Sim layer carries the in-process ROS 2 Jazzy Python stack. The
`isaac-lab-30b2` layer adds Isaac Lab 3.0.0b2 and its pinned framework extras.

This is a runtime inventory, not a promise of a general build workstation.
The base does not include `gcc`/`g++` or `build-essential`, the FFmpeg command
line tool, graphics development headers, the full ROS 2 command-line and
message-build toolchain, or project-specific system packages. Install those
in the project Dockerfile when the project needs them. Verify an image that
you changed with `antioch services exec sim command -v TOOL` before dispatch.

The `FROM antioch-sim/<engine>:<sim-version>` line is the cache boundary. A
coordinate change invalidates the engine and every project layer. A changed
`apt-get` or `uv` instruction rebuilds that instruction and the layers after
it; a changed source `COPY` rebuilds only the later project layers. Keep
slow, stable system dependencies before source copies. Watch sync updates
source without rebuilding, while a queued or saved run freezes the final
image by digest.

## Configure project services

`antioch.yaml` owns the complete project stack: one required `services.sim` entry
and auxiliary services in that same mapping; a top-level `sim` is not valid.
`schema_version` is optional and defaults to the latest schema, and streaming is
a runtime choice rather than a manifest `stream` key. The sim image coordinate
pins the engine and SDK version; edit it to upgrade. Add `build` only when a
Dockerfile is needed for custom dependencies, and make its `FROM` line that
coordinate. Declare dependencies, environment, health
checks, watch rules, and authenticated `ports` on those service blocks. Docker
runs underneath Antioch through the Engine API. The CLI supplies GPU access,
init, restart policy `no`, and the output bind; host networking and IPC default
to host mode, with explicit supported `network_mode` and `ipc` values available
as opt-outs. See the [Compose file reference](https://docs.docker.com/reference/compose-file/)
for the kept field vocabulary.

Start a foreground development session with `antioch services up --watch`. It builds,
health-gates, watches, and keeps declared ports reachable at localhost while the
stack is up. Ctrl-C ends the watch session but leaves containers and declared
ports running; use `antioch services down` to stop everything. A bare
`antioch services up` builds, health-gates the stack, opens its declared ports,
then returns. Every finite dispatch reconciles the source it will run
once before starting; it never silently executes stale source. A running
watcher keeps the project services current for ordinary `run`,
scenario, and suite dispatch; all share one stack even when that watcher is
live. Whenever Antioch recreates a container, it reapplies the declared sync
rules, so `services up`, `services restart`, an image change,
and `run --restart` never leave `/workspace/project` empty while waiting for
a watch session. `services ps`, `services logs`, and `services down` resolve
the existing project and never allocate a machine. Development watch and
tunnels are stripped when a scenario or suite enters the queue with an
immutable, digest-pinned environment.

Private `image:` services use the local Docker config for an interactive pull.
For queued work, the submitter pulls with that credential, pushes a digest-pinned
copy into your organization's private registry, and saves that reference in the
queued environment; workers and reruns need no third-party registry login. The staged
sim image also bakes the exact submitted project tree at `/workspace/project`,
because queued children run with no watch session to publish source.

## Local runs, Jupyter, and machines

`antioch run FILE` is the local-first Python workflow. It prepares the healthy
sim service when needed and starts one remote process with faithful output and
exit status. `antioch run` executes your file inside the running sim container,
next to whatever `command:` the service declares. A decorated scenario or
suite saves results, telemetry, and artifacts. Use `antioch services exec sim python` for a raw diagnostic,
`antioch services ssh` for a human shell, and `antioch services cp
sim:/workspace/output/result.png ./result.png` for an explicit container transfer; all three resolve an
existing assignment rather than allocating one. `antioch services exec SERVICE CMD` relays piped stdin
to the selected service; use `antioch services ssh` for an interactive shell. Use `antioch machine ssh` for
the VM shell. Docker runs underneath, so `antioch machine ssh` followed by raw `docker ps`, `docker
logs`, or `docker exec` is supported for host-level diagnosis. The shell has no
process identity, so work started there is not tracked or timed.

`services cp` follows Docker cp destination rules. A destination ending in `/`
is a directory, and the source basename is placed under it; for example,
`antioch services cp ./payload sim:/workspace/output/` writes
`/workspace/output/payload/...`. Without a trailing slash, the destination
path receives the source contents, so `./payload` copied to
`sim:/workspace/output/payload` writes directly under that path. The service
directory named by a trailing-slash destination must already exist.

`antioch machine status` prints the direct machine URL and, when available, the
stream address. It also names `Mission Control workspace` when a workspace is
holding the assignment. `antioch machine list` shows the same holder in its
`HOLDER` column. This is an assignment hold, not a running-process count: the
hosted Lab's cell-aware idle policy is deliberate, and its machine stays held
until the workspace assignment is released. For captured stack output, use a focused
service view such as:

```bash
antioch scenario show SCENARIO_RUN_ID --logs --service sim
```

`antioch jupyter` is the interactive surface, for people and for agents alike.
One kernel-only Jupyter server runs in the project's sim container and every
subcommand reaches it through one SSH forward, so a notebook and a coding agent
drive the same warm Isaac. `jupyter lab` runs JupyterLab locally in gateway
mode — notebooks, the editor, and Lab's terminal stay on your laptop while
kernels and kernelspecs come from the machine. `jupyter cell [CODE]` runs one
cell and exits, with `--json` for a structured result and a non-zero status when
the cell fails. `jupyter start`, `jupyter kernels`, `jupyter show`, `jupyter
stop`, `jupyter stream`, and `jupyter unstream` open a kernel and print its
identity, list the machine's kernels, inspect one, end one, declare the
one-per-machine livestream, and release that livestream. Declare a livestream
before calling `antioch.boot()`; stopping that exact kernel releases its
livestream. Repeating the same stream claim is idempotent. Sharing is explicit
both ways: a command that finds several kernels refuses to guess, so a session
that wants its own opens it with `start` and passes `--kernel` from then on.

Kernels outlive the command that made them, so the next cell lands on an Isaac
that is already booted; an unattended idle kernel is culled and the machine then
idles out normally. Restart replaces only that kernel and preserves the local
notebook.

The machine lifecycle surface is `list`, `status`, `checkout`, `release`, and
`ssh`. Container connections are under `antioch services`: `services ssh`
opens `/workspace/project` in `sim` by default and `services cp` is the explicit
transfer path. Use the machine lifecycle commands to choose or return
assignments; they do not replace the service stack.

A project may hold several machines at once, and `antioch machine checkout
MACHINE` says which of them single-machine commands use — the same idea as a
checked-out git branch, and `machine list` marks it with `*`. Commands resolve
an explicit `--machine` first, then the checked-out machine, then the project's
sole assignment; holding several with none checked out is the one case they
refuse to guess at. `antioch machine checkout --none` clears the choice, and
releasing a machine clears it too.

Local source remains authoritative throughout the watch session. Write
temporary checkpoints, frames, and debug files to `$ANTIOCH_OUTPUT_DIR`
(`/workspace/output`), which is outside the project tree and disappears when
the ephemeral VM is recycled. Retrieve a plain-run file before release with
`antioch services cp sim:/workspace/output/FILE ./FILE`; that copy is not a
durable record. Use the scenario artifact path above when output must survive
machine loss. See `antioch --help` for the public command contract and the
generated project for small authoring examples.

## Troubleshooting

- **`antioch` is not found:** use `uv run antioch` from the project, or activate
  `.venv` with `source .venv/bin/activate`.
- **Authentication is required or expired:** set `ANTIOCH_ENV=staging` and run
  `uv run antioch auth login` again. Use `uv run antioch auth whoami` to check
  the active user and organization.
- **The project cannot select an engine:** declare exactly one supported extra
  in `pyproject.toml`, then run `uv sync`. For a new project, you can also pass
  `antioch init --engine isaac-601-ga` explicitly.
- **The remote image version does not match the SDK:** update
  `services.sim.image` in `antioch.yaml` and any matching Dockerfile `FROM`
  line to the version in the project lock.
- **A stream is not ready immediately:** engine startup takes about 15 seconds.
  Use `--no-stream` for a short or headless run.

The CLI prints value-specific errors and recovery commands. Use
`uv run antioch scenario run --help` for one example of the current command
options.
