# Import from private storage

CI downloads your checkpoint with a scoped read credential and bakes it into an image — Roborama pulls the image, never the bucket.

*Provider workflow: Amazon S3, Google Cloud, Microsoft Azure.*

The API does not accept `s3://`, `gs://`, or `azure://` checkpoint
sources, and there are no storage-credential fields anywhere. The
supported workflow keeps credentials entirely on your side: CI
downloads the checkpoint with a scoped read credential and bakes it
into a container image at build time. Roborama pulls the image, never
the bucket — your cloud credentials never leave your CI.

## Prerequisites

- **The full checkpoint under one prefix** — weights, config, and
  normalization stats. A weights file alone does not identify
  preprocessing or inference settings; if `norm_stats.json` or the
  processor files aren't baked in alongside the weights, the policy
  runs confidently wrong.
- **A manifest, checked in CI.** Commit the expected file list with
  checksums next to the Dockerfile and verify with `sha256sum -c`. An
  incomplete download or a checksum mismatch fails *your* build, not
  the run — that is the point: fail before you buy episodes.
- **A scoped read credential in CI** — the per-provider minimum is in
  the table below. The credential only needs to live for the build, so
  prefer short-lived OIDC role assumption over stored keys.
- **A registry Roborama can pull from.** Today the image must be
  pullable without registry credentials. If that posture doesn't fit
  your weights,
  [Connect your endpoint](/docs/guides/connect-your-endpoint/) keeps
  them off our side entirely.

## Connect

The worked example bakes `s3://acme-checkpoints/skill-v4/` into
`ghcr.io/acme/skill:v4`.

### Commit the Dockerfile and a checksum manifest

```dockerfile
FROM pytorch/pytorch:2.4.0-cuda12.4-cudnn9-runtime
WORKDIR /policy
COPY checkpoint/ /policy/checkpoint/
COPY serve.py /policy/serve.py
CMD ["python", "serve.py", "--checkpoint", "checkpoint"]
```

Generate the manifest once, on the machine that trained the policy,
and commit it next to the Dockerfile:

```bash
sha256sum $(find checkpoint -type f) > checkpoint.manifest
```

### Download with a scoped role and verify

In GitHub Actions, assume a read-only role via OIDC — the job gets a
credential that lives minutes and is never stored:

```yaml
permissions:
  id-token: write
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/ckpt-read
      aws-region: us-east-1
```

Then download the prefix and check it against the manifest:

```bash
aws s3 cp --recursive \
  s3://acme-checkpoints/skill-v4/ ./checkpoint/
sha256sum -c checkpoint.manifest
```

A missing file or a mismatched hash stops the build here — before an
image exists, long before an episode is bought.

### Build, push, and hand the image to a run

```bash
docker build -t ghcr.io/acme/skill:v4 .
docker push ghcr.io/acme/skill:v4
```

From here the path is exactly
[Run your container](/docs/guides/run-your-container/):
`Policy.container(image="ghcr.io/acme/skill:v4")` with your declared
contracts. The canonical container run, `run_8842`, is this image on
`g1-edu-pro@fw2.3` in `kitchen-std@v1.2`.

### The same download on GCS and Azure

Only the download step changes; the manifest check, build, and push
are identical.

| Provider | Download | Minimum permission |
| --- | --- | --- |
| Amazon S3 | `aws s3 cp --recursive s3://acme-checkpoints/skill-v4/ ./checkpoint/` | `s3:GetObject` on the prefix |
| Google Cloud Storage | `gcloud storage cp -r gs://acme-checkpoints/skill-v4 ./checkpoint` | `roles/storage.objectViewer` on the bucket |
| Microsoft Azure | `az storage blob download` per blob (`download-batch` for a prefix) | `Storage Blob Data Reader` on the container |

Prefix, individual objects, or a tarball unpacked during the build —
whatever your CI downloads, Roborama only ever sees the finished image.

Weights baked into an image travel with it: the copy fetched for a run
is cached to schedule and execute that run, and is deleted when the
run's `data.retention` window closes — or immediately on
`roborama.data.purge(run_id)`, receipted. Emptying your bucket later
does not delete an already-fetched copy; purge does
([data contract](/docs/concepts/data-contract/)).

## Validate

Mock mode (`ROBORAMA_MOCK=1`, or `mock=True` on the client) dry-runs
quote → submit → result with no API key and no hardware. At submission,
admission checks the declared `action_space` and `observation_contract`
against the pinned robot and scene — a mismatch is rejected with
`contract_validation_failed`, free. Cell startup then pulls and starts
the image — a broken entrypoint surfaces there, after admission and
before episodes ([Bring your policy](/docs/guides/bring-your-policy/)
lays out the three layers).

*Example: the local dry-run*

**Python**

```python
import roborama

# Mock mode: no API key, no hardware. The whole loop runs
# offline against packaged fixtures — or set ROBORAMA_MOCK=1.
client = roborama.Client(mock=True)

quote = client.quote(
    robot="aloha2-pro",
    environment="cell-a",
    episodes=300,
)
print(quote)
# {robot_hours: 10.0, env_hours: 10.0, usd: 860, queue_eta: "2h"}

run = client.run(
    robot="aloha2-pro@fw1.1",
    environment="cell-a@v1.0",
    policy=roborama.Policy.checkpoint(
        hf="acme/act-so101-pick-v2",
        runtime="lerobot",
    ),
    task="pick_place@v1",
    episodes=300,
    max_budget_usd=1000,
)
print(run.result())
# n=300  rate=0.843  ci95=(0.797, 0.881)
```

**TypeScript**

```typescript
import Roborama from "@roborama/sdk";

// Mock mode: no API key, no hardware. The whole loop runs
// offline against packaged fixtures — or set ROBORAMA_MOCK=1.
const roborama = new Roborama({ mock: true });

const quote = await roborama.quote({
  robot: "aloha2-pro",
  environment: "cell-a",
  episodes: 300,
});
console.log(quote);
// { robot_hours: 10.0, env_hours: 10.0, usd: 860, queue_eta: "2h" }

const run = await roborama.runs.create({
  robot: "aloha2-pro@fw1.1",
  environment: "cell-a@v1.0",
  policy: {
    type: "checkpoint",
    hf: "acme/act-so101-pick-v2",
    runtime: "lerobot",
  },
  task: "pick_place@v1",
  episodes: 300,
  max_budget_usd: 1000,
});
console.log(await run.result());
// n=300  rate=0.843  ci95=(0.797, 0.881)
```

**cURL**

```bash
# Mock mode is an SDK feature — curl always talks to the live
# API. ROBORAMA_MOCK=1 flips any SDK entry point to packaged
# fixtures: no API key, no hardware, no spend.
ROBORAMA_MOCK=1 python - <<'PY'
import roborama

quote = roborama.quote(
    robot="aloha2-pro", environment="cell-a", episodes=300)
print(quote)

run = roborama.run(
    robot="aloha2-pro@fw1.1",
    environment="cell-a@v1.0",
    policy=roborama.Policy.checkpoint(
        hf="acme/act-so101-pick-v2", runtime="lerobot"),
    task="pick_place@v1",
    episodes=300,
    max_budget_usd=1000,
)
print(run.result())
PY
```

**Agent (tool-use payload)**

```json
{
  "type": "tool_use",
  "name": "roborama_quote",
  "input": {
    "robot": "aloha2-pro",
    "environment": "cell-a",
    "episodes": 300
  }
}
```

> **Fixture output:** Printed quotes and results in these snippets are packaged fixtures —
> exact shapes, illustrative numbers.

## Quote

Quotes are free and resolve both meters before you commit: 600
episodes of `g1-edu-pro` in `kitchen-std` — the worked example's setup
— prices at $3,740, queue ETA about 6 hours. Meters and hard stops:
[Billing & budgets](/docs/guides/billing-and-budgets/).

*Example: quote before you run*

**Python**

```python
import roborama  # reads ROBORAMA_API_KEY from the environment

quote = roborama.quote(
    robot="g1-edu-pro",
    environment="kitchen-std",
    episodes=600,
)
print(quote)
# {robot_hours: 20.0, env_hours: 20.0, usd: 3740, queue_eta: "6h"}
```

**TypeScript**

```typescript
import Roborama from "@roborama/sdk"; // reads ROBORAMA_API_KEY

const roborama = new Roborama();

const quote = await roborama.quote({
  robot: "g1-edu-pro",
  environment: "kitchen-std",
  episodes: 600,
});
console.log(quote);
// { robot_hours: 20.0, env_hours: 20.0, usd: 3740, queue_eta: "6h" }
```

**cURL**

```bash
curl https://api.roborama.com/v1/quote \
  -H "Authorization: Bearer $ROBORAMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "robot": "g1-edu-pro",
    "environment": "kitchen-std",
    "episodes": 600
  }'
```

**Agent (tool-use payload)**

```json
{
  "type": "tool_use",
  "name": "roborama_quote",
  "input": {
    "robot": "g1-edu-pro",
    "environment": "kitchen-std",
    "episodes": 600
  }
}
```

## Evaluate

Submit with [`run()`](/docs/primitives/run/), passing the
`Policy.container` built above; follow live with `run.watch()`.

## Inspect results

`run.result()` returns n, the success rate, and a Wilson ci95 — the
canonical run for this image, `run_8842`, reports n=612, rate=0.874,
ci95=(0.846, 0.898) at $3,812. Episodes carry video, MCAP telemetry,
and replay specs; `run.report(format="pdf")` is the citable record.
Field detail: [data contract](/docs/concepts/data-contract/).

## Compare versions

Bake `skill-v5` from its own prefix into its own image, keep every pin
and declaration the same, and [`compare()`](/docs/primitives/compare/)
reports the paired delta — same declared test, different weights.

## Troubleshoot

Download and checksum failures happen inside your CI — deliberately, no
Roborama error code exists for them. The codes below are the run path's.

| Code | Symptom | Fix |
| --- | --- | --- |
| `contract_validation_failed` | submission rejected — the baked policy declares contracts the pinned robot and scene don't provide | declare what the checkpoint was trained for; pin hardware that provides it |
| `firmware_pin_unavailable` | the requested `model@firmware` isn't installed on any cell | list installed firmwares with `robots.list()` and pin one of them |
| `task_unknown` | task id or version typo in the submission | check the task catalogue and pin an existing version |
| `budget_exceeded` | run halted at `max_budget_usd`; partial result kept | read the partial n and interval; resubmit with a higher cap if needed |
| `retention_expired` | artifacts fetched after the retention window, or after a purge | the data is gone by design — re-run for fresh episodes |

## Where next

- [Run your container](/docs/guides/run-your-container/) — the direct integration this workflow lands on.
- [Walkthrough: GitHub + S3](/docs/guides/walkthrough-github-storage/) — this workflow end to end, code and weights in different places.
- [Package your policy](/docs/guides/package-your-policy/) — the same CI bake, starting from a code repo.
- [Bring your policy](/docs/guides/bring-your-policy/) — all six paths side by side.
