# Deploy Temporal Cloud

> Split API and worker processes, GCS artifacts, Temporal TLS and API key, Modal tokens, and matching image digest plus task queue.

- Repository: mupt-ai/self-bench
- GitHub: https://github.com/mupt-ai/self-bench
- Human docs: https://grok-wiki.com/public/docs/mupt-ai-self-bench-62fff2be9bcc
- Complete Markdown: https://grok-wiki.com/public/docs/mupt-ai-self-bench-62fff2be9bcc/llms-full.txt

## Source Files

- `docs/operations.md`
- `Dockerfile`
- `src/api-main.ts`
- `src/temporal/worker-main.ts`
- `src/temporal/connection.ts`
- `src/config.ts`

---

---
title: "Deploy Temporal Cloud"
description: "Split API and worker processes, GCS artifacts, Temporal TLS and API key, Modal tokens, and matching image digest plus task queue."
---

Cloud deployments run two processes from the same `Dockerfile` image: `node dist/api-main.js` as a request-oriented HTTP service, and `node dist/temporal/worker-main.js` as a long-running Temporal worker. Both connect to Temporal Cloud with TLS plus `SELFBENCH_TEMPORAL_API_KEY`, share one `SELFBENCH_TASK_QUEUE`, and store run artifacts in GCS. The worker owns Modal, GitHub, model, and Harbor credentials and must stay up for the 14-day workflow execution timeout.

This repository defines the application boundary, not turnkey cloud infrastructure. Project, region, ingress, IAM, GCS, and Temporal provisioning stay deployment-specific.

```mermaid
flowchart LR
  subgraph Clients
    CLI["self-bench CLI"]
  end
  subgraph ApiProcess["API process"]
    API["node dist/api-main.js"]
  end
  subgraph WorkerProcess["Worker process"]
    Worker["node dist/temporal/worker-main.js"]
    Harbor["harbor --env modal"]
  end
  subgraph TemporalCloud["Temporal Cloud"]
    NS["namespace + TLS + API key"]
    TQ["SELFBENCH_TASK_QUEUE"]
  end
  subgraph Gcs["GCS"]
    Bucket["gs://bucket/prefix"]
  end
  subgraph Modal["Modal"]
    Sandboxes["Modal sandboxes"]
  end
  CLI -->|"Bearer SELFBENCH_API_TOKEN"| API
  API -->|"start / query / cancel"| NS
  API -->|"put / get"| Bucket
  Worker -->|"poll"| TQ
  Worker -->|"put / get"| Bucket
  Worker --> Harbor
  Worker --> Sandboxes
```

<Warning>
Do not run the worker on a scale-to-zero request platform. `self-bench run` starts `selfBenchRunWorkflow` with `workflowExecutionTimeout: "14 days"`. Work pauses when the worker stops; Temporal history remains until a matching worker polls again.
</Warning>

## What you provision

| Surface | Role | self-bench does not create |
| --- | --- | --- |
| Temporal Cloud namespace | Workflow history, visibility, task queue | Namespace, TLS, API key, gRPC address |
| GCS bucket + prefix | Provenance, checkpoints, exports | Bucket, IAM, lifecycle rules |
| API container | HTTP + Temporal client | Cloud Run (or equivalent) service, ingress |
| Worker container | Temporal poller + Modal + Harbor | Always-on compute |
| Modal account | Disposable sandboxes and Harbor `--env modal` | Tokens / workspace |
| Secrets | API token, Temporal API key, Modal tokens, `GH_TOKEN`, `OPENAI_API_KEY` | Secret manager |

The local analog of the process split is `compose.yaml`: same `selfbench:local` image, API default command, worker `command: ["node", "dist/temporal/worker-main.js"]`. Cloud replaces Compose Postgres/Temporal with Temporal Cloud and the named `artifacts` volume with GCS.

## Image and process split

Build `Dockerfile`. The image is `node:22-bookworm` with Harbor `harbor[modal]==0.20.1.dev202608040148`, `gh`, `uv`, and a Docker CLI (used only if a process is configured for Docker). Default `USER` is `node`. Default `CMD` is the API.

<CodeGroup>

```bash title="Build"
docker build -t selfbench:<tag> .
```

```bash title="API command"
node dist/api-main.js
# package.json: bun run start:api
```

```bash title="Worker command"
node dist/temporal/worker-main.js
# package.json: bun run start:worker
```

</CodeGroup>

| Process | Bind / poll | Credentials it should hold |
| --- | --- | --- |
| API | `SELFBENCH_API_HOST`:`SELFBENCH_API_PORT` (cloud: `0.0.0.0:8080`) | `SELFBENCH_API_TOKEN`, Temporal TLS + API key, GCS |
| Worker | Temporal namespace / task queue | Same Temporal + GCS, plus Modal, `GH_TOKEN`, `OPENAI_API_KEY` |

The API is a regular HTTP service suitable for Cloud Run. `src/api-main.ts` loads config, calls `startApi`, logs `SelfBench API listening on http://<host>:<port>`, and stops on `SIGINT` / `SIGTERM`.

The worker strips empty `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET` values, then fails startup with `docker info` and `docker compose version` if `SELFBENCH_EXECUTION_BACKEND` or `SELFBENCH_HARBOR_ENVIRONMENT` is `docker`. Cloud workers must set both to `modal` so they do not need a Docker socket. After connect, the worker logs:

```text
SelfBench worker polling <namespace>/<taskQueue> with activity concurrency <n>
```

<Note>
Keep API and worker on the **same image digest**. The API starts workflows; the worker loads `workflow.js` and activity implementations from that image. Divergent digests can desynchronize workflow code, Harbor version, and default sandbox image.
</Note>

## Temporal Cloud connection

Both processes call `Connection.connect` / `NativeConnection.connect` with `address`, `tls`, and optional `apiKey`. There is no client-certificate or custom CA field in config.

<ParamField body="SELFBENCH_TEMPORAL_ADDRESS" type="string" required>
gRPC address of the Temporal frontend. Default `127.0.0.1:7233` is the local Compose listener, not Temporal Cloud.
</ParamField>

<ParamField body="SELFBENCH_TEMPORAL_NAMESPACE" type="string" required>
Temporal namespace. Default `default`.
</ParamField>

<ParamField body="SELFBENCH_TEMPORAL_TLS" type="true | false">
Must be the string `true` or `false`. Default `false`. Temporal Cloud requires `true`.
</ParamField>

<ParamField body="SELFBENCH_TEMPORAL_API_KEY" type="string" required>
Passed through only when set. Omit locally; required for Temporal Cloud API-key auth.
</ParamField>

<ParamField body="SELFBENCH_TASK_QUEUE" type="string" required>
Queue the API uses in `client.workflow.start` and the worker polls. Default `selfbench-dev`.
</ParamField>

Set the **same** address, namespace, TLS flag, API key, and task queue on both processes. Use a dedicated cloud queue name (not the `selfbench-dev` default) so a local `self-bench up` worker cannot steal cloud tasks.

The API starts `selfBenchRunWorkflow` with `workflowId` equal to the run ID. `GET /v1/runs` lists executions with Temporal visibility query `WorkflowType = 'selfBenchRunWorkflow'`.

## GCS artifacts

Set `SELFBENCH_ARTIFACT_BACKEND=gcs` on both processes. `loadConfig` then requires `SELFBENCH_GCS_BUCKET` or throws `SELFBENCH_GCS_BUCKET is required for GCS`.

`GcsArtifactStore` constructs `@google-cloud/storage` `new Storage()` with no explicit credentials. The process identity must already be authorized (attached service account or Application Default Credentials). Object names are `<prefix>/<key>` after stripping leading and trailing slashes from the prefix.

| Key | Default | Constraint |
| --- | --- | --- |
| `SELFBENCH_ARTIFACT_BACKEND` | `local` | Cloud: `gcs` |
| `SELFBENCH_GCS_BUCKET` | unset | Required when backend is `gcs` |
| `SELFBENCH_GCS_PREFIX` | `selfbench` | App-level confinement; also grant IAM only to this prefix |

Writes use `ifGenerationMatch: 0` (create-if-absent). A second put of the same object is accepted only when the stored `sha256` metadata matches. Conflicting contents throw `artifact already exists with different contents: gs://<bucket>/<object>`.

Reads reject URIs outside the configured bucket or prefix (`artifact is outside configured bucket`). Keys may not be empty, start with `/`, or contain `..`.

:::files
gs://<bucket>/<prefix>/
└── runs/<runId>/
    ├── input/provenance.jsonl
    ├── discovery/wave-<n>/shard-<i>/...
    ├── validation/<taskId>/<sha12>/attempt-<n>/...
    └── export.tar.gz
:::

There is no HTTP delete route. Remove GCS prefixes with operator tooling.

<Warning>
Exports contain source snapshots, held-out tests, and reference solutions. They are sensitive and unencrypted. Artifact refs carry `sizeBytes` and SHA-256; `get` / `openRead` verify both.
</Warning>

## API environment

Bind beyond loopback only with a token. `SELFBENCH_API_HOST` values that skip the token check are `127.0.0.1`, `::1`, and `localhost`. `0.0.0.0` requires `SELFBENCH_API_TOKEN` or startup throws `SELFBENCH_API_TOKEN is required when the API listens beyond loopback`.

```text
SELFBENCH_API_HOST=0.0.0.0
SELFBENCH_API_PORT=8080
SELFBENCH_API_TOKEN=...
SELFBENCH_ARTIFACT_BACKEND=gcs
SELFBENCH_GCS_BUCKET=...
SELFBENCH_GCS_PREFIX=selfbench
SELFBENCH_TEMPORAL_ADDRESS=...
SELFBENCH_TEMPORAL_NAMESPACE=...
SELFBENCH_TEMPORAL_API_KEY=...
SELFBENCH_TEMPORAL_TLS=true
SELFBENCH_TASK_QUEUE=selfbench-prod
```

`GET /healthz` is unauthenticated and returns `{ "ok": true }`. Every other `/v1/*` route requires `Authorization: Bearer <token>` when the token is configured (timing-safe compare). `GET /` and `/assets/*` serve the bundled review UI without that check.

Optional: `SELFBENCH_BUILD_COMMIT` (40-char hex). When set, `POST /v1/runs` records it as `version.selfbenchCommit` instead of the CLI-submitted SHA.

Do not put `OPENAI_API_KEY`, `GH_TOKEN`, or Modal tokens on the API process.

## Worker environment

Share Temporal, task queue, and GCS with the API, then add execution and credentials:

```text
SELFBENCH_ARTIFACT_BACKEND=gcs
SELFBENCH_GCS_BUCKET=...
SELFBENCH_GCS_PREFIX=selfbench
SELFBENCH_TEMPORAL_ADDRESS=...
SELFBENCH_TEMPORAL_NAMESPACE=...
SELFBENCH_TEMPORAL_API_KEY=...
SELFBENCH_TEMPORAL_TLS=true
SELFBENCH_TASK_QUEUE=selfbench-prod
SELFBENCH_EXECUTION_BACKEND=modal
SELFBENCH_HARBOR_ENVIRONMENT=modal
SELFBENCH_ACTIVITY_CONCURRENCY=20
SELFBENCH_MODAL_APP=selfbench
MODAL_TOKEN_ID=...
MODAL_TOKEN_SECRET=...
GH_TOKEN=...
OPENAI_API_KEY=...
```

| Key | Default | Notes |
| --- | --- | --- |
| `SELFBENCH_EXECUTION_BACKEND` | `docker` | Cloud: `modal` |
| `SELFBENCH_HARBOR_ENVIRONMENT` | same as execution backend | Cloud: `modal` so Harbor runs `--env modal` |
| `SELFBENCH_ACTIVITY_CONCURRENCY` | `20` when Modal, else `1` | Integer 1–100; `maxConcurrentActivityTaskExecutions` |
| `SELFBENCH_MODAL_APP` | `selfbench` | `ModalClient.apps.fromName(..., { createIfMissing: true })` |
| `SELFBENCH_MODAL_ENVIRONMENT` | unset | Optional Modal environment name |
| `SELFBENCH_MODAL_IMAGE` | `node:22-bookworm` | Registry image the worker layers tools onto |
| `OPENAI_API_KEY` | unset | Preferred model auth; wins over subscription JSON |
| `SELFBENCH_PI_AUTH_JSON` / `SELFBENCH_CODEX_AUTH_JSON` | unset | Compatibility fallbacks only |
| `GH_TOKEN` | unset | Preferred GitHub read token; else `gh auth token` |

<Info>
`removeEmptyModalCredentialOverrides()` deletes `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET` when they are empty or whitespace so they cannot override a mounted `~/.modal.toml`. Cloud workers have no profile mount; set both tokens to non-empty secret values.
</Info>

Use separate least-privilege service accounts: API and worker need GCS object access under the configured prefix; only the worker needs Modal, GitHub, and model secrets.

## Matching digest and task queue

| Must match on API and worker | Why |
| --- | --- |
| Image digest | Workflow + activity code, Harbor version, default Modal image |
| `SELFBENCH_TASK_QUEUE` | API `workflow.start({ taskQueue })` vs worker `Worker.create({ taskQueue })` |
| Temporal address, namespace, TLS, API key | Both sides of the same Cloud namespace |
| GCS bucket + prefix | API writes provenance; worker reads it and writes exports |

A queue mismatch leaves new runs in Temporal with no poller. An image mismatch can break deterministic workflow replay. A GCS prefix mismatch makes the worker fail to read `runs/<runId>/input/provenance.jsonl`.

## Deploy

<Steps>
<Step title="Provision Temporal Cloud and GCS">
Create a TLS-enabled Temporal Cloud namespace and an API key. Create a GCS bucket and a prefix (default `selfbench`). Attach distinct service accounts that can only read/write that prefix.
</Step>

<Step title="Build one image">
Build `Dockerfile` and record the digest. Pass `SELFBENCH_BUILD_COMMIT=<40-char sha>` as a build-arg if you want that SHA baked into `dist/build-metadata.js` during `bun run build`.
</Step>

<Step title="Run the API">
Deploy the digest with default `CMD` (`node dist/api-main.js`), port `8080`, and the API environment above. Confirm `GET /healthz` returns `{ "ok": true }` and the process log includes `SelfBench API listening on http://0.0.0.0:8080`.
</Step>

<Step title="Run the worker">
Deploy the **same digest** with command `node dist/temporal/worker-main.js` on always-on compute. Confirm the poll log shows the intended namespace, queue, and concurrency. Do not scale this service to zero.
</Step>

<Step title="Point the CLI at the API">
From an operator machine that can reach the API:

```bash
export SELFBENCH_API_URL=https://<api-host>
export SELFBENCH_API_TOKEN=...
self-bench list
```

`SELFBENCH_API_URL` defaults to `http://127.0.0.1:8080`. The CLI sends `Authorization: Bearer $SELFBENCH_API_TOKEN` when that variable is set.
</Step>
</Steps>

<RequestExample>
```bash
curl -sS "$SELFBENCH_API_URL/healthz"
self-bench list
```
</RequestExample>

<ResponseExample>
```json
{
  "ok": true
}
```
```json
[]
```
</ResponseExample>

An empty `self-bench list` array is success: the API authenticated, connected to Temporal Cloud, and ran the visibility query.

## Verification signals

| Check | Expected |
| --- | --- |
| `GET /healthz` | `200` `{ "ok": true }` |
| API log | `SelfBench API listening on http://0.0.0.0:8080` |
| Worker log | `SelfBench worker polling <namespace>/<queue> with activity concurrency 20` |
| `self-bench list` with token | JSON array (possibly empty) |
| `self-bench list` without token | `unauthorized` when the API token is configured |
| Submit a run | `202` `{ "runId": "..." }`; worker activity starts; GCS objects appear under the prefix |

Closing the waiting CLI does not cancel the workflow. `self-bench cancel RUN_ID` requests Temporal cancellation through the API.

## Failure modes

| Symptom | Likely cause |
| --- | --- |
| Process exits: `SELFBENCH_API_TOKEN is required when the API listens beyond loopback` | `SELFBENCH_API_HOST=0.0.0.0` without a token |
| Process exits: `SELFBENCH_GCS_BUCKET is required for GCS` | Backend `gcs` and bucket unset |
| Worker exits on `docker info` | `SELFBENCH_EXECUTION_BACKEND` or `SELFBENCH_HARBOR_ENVIRONMENT` is `docker` |
| Runs stay queued / list shows `RUNNING` with no candidate progress | Queue mismatch, worker down, or worker still connecting |
| `artifact is outside configured bucket` | API and worker disagree on bucket or prefix |
| `artifact already exists with different contents` | Same object key, different SHA-256 |
| `401` `{ "error": "unauthorized" }` | Missing or wrong Bearer token |
| Modal auth falls through unexpectedly | Empty `MODAL_TOKEN_*` env vars were not replaced with real secrets |
| Export `409` `run export is not ready` | Workflow not in a terminal success path yet |

<Warning>
Sandboxes receive a source checkout plus a short-lived model credential. Use self-bench only with repositories you trust to execute. Modal sandboxes are disposable; the worker is not a malware-analysis service.
</Warning>

## Next

<CardGroup>
<Card title="Start the local stack" href="/start-local-stack">
Compose equivalent of the API/worker split, named volumes, and `--backend modal`.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Full `SELFBENCH_*` key list, defaults, loopback token rule, and Temporal fields.
</Card>
<Card title="Authentication" href="/authentication">
`OPENAI_API_KEY` precedence, GitHub token, API Bearer, Modal profile versus tokens.
</Card>
<Card title="Execution backends" href="/execution-backends">
Docker versus Modal concurrency, images, and Harbor environment pairing.
</Card>
<Card title="Manage runs" href="/manage-runs">
`list`, `status`, `cancel`, and SHA-256 download against `SELFBENCH_API_URL`.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Export-not-ready, integrity mismatches, and leftover execution resources.
</Card>
</CardGroup>
