> **BETA. USE AT YOUR OWN RISK. SOV is provided "as is", with no warranty of any kind. It runs AI agents that execute commands, edit files and make network calls on your machine. Install it only on a machine, and with accounts, you can afford to lose. You are responsible for your AI vendor accounts, their terms of service, and every charge they bill you. Versions may change, break or lose data without notice. Do not use it for production, regulated or sensitive data. By installing, you accept these terms.**

# Linux lab: one OpenRouter v1 lane

This is the shortest **catalog and caller** path supported by the current source. A fully public, self-serve
Linux run is still blocked by the Jobs installation and worker publication gaps listed below. Do not send the
metered POST until those gaps are filled and a capped prepaid OpenRouter key is installed. This document
does not contain a key; its paid POST is an example only. (`sov_agent/v1/service.py:serve`,
`sov_agent/worker_bootstrap.py:_call_worker_from_environment`)

## 1. Prerequisites and package installation

Use Python 3.12+, Docker, `curl`, this source checkout, a SOV-JOBS v2 installation, and an OpenCode binary.
No Claude, Codex, Cursor, Grok CLI, or subscription login is involved. The workspace-less OpenRouter call
ultimately uses a direct HTTP executor, although the catalog still pins the OpenCode executable at claim.
The package requires Python >=3.12; its `sov-src` dependency is Git pinned, and the repository does not
publish a Linux release or public Jobs installation procedure. Use an approved, hash-bound wheelhouse and
normal dependency resolution when those artifacts are made available; do not treat `pip install .` as a
documented public installation. (`pyproject.toml:[project]`,
`sov_agent/v1/worker.py:CallWorker._engine_executor`, `sov_agent/v1/cli_pins.py:check`)

## 2. Start a throwaway PostgreSQL database

These commands create one disposable container and a separate Agent database. Keep the password local to
this lab. Agent expects the `postgresql+psycopg://` SQLAlchemy URL and creates its own tables on startup.
(`sov_agent/v1/service.py:serve`, `sov_agent/v1/store.py:V1Store.initialize`)

```sh
docker run --rm -d --name sov-agent-lab-pg \
  -e POSTGRES_PASSWORD=lab-only-password \
  -p 127.0.0.1:15433:5432 postgres:16
until docker exec sov-agent-lab-pg pg_isready -U postgres >/dev/null 2>&1; do sleep 1; done
docker exec sov-agent-lab-pg psql -U postgres -d postgres -c 'CREATE DATABASE agent_lab;'
export SOV_AGENT_DATABASE_URL='postgresql+psycopg://postgres:lab-only-password@127.0.0.1:15433/agent_lab'
```

## 3. Fill the catalog's host values

Copy `examples/catalog-openrouter-min.json` to a local working file. Replace every `REPLACE-WITH-...` value:
`clients.opencode.host_id` if needed; `jobs.url`, `jobs.service_id`, `jobs.program` to match the actual Jobs
registration; `engine.provider_binding.executable` and `sha256` with the absolute OpenCode path and its
SHA-256; the Python and credential-free broker script paths and hashes; and `credential_sha256` with the
SHA-256 of the **real key**, never the key. `engine.platform` is `linux`, `family` is `opencode-jsonl`, and
`engine.schema_version` is 2. The example's model and ratings follow `qa/e2e_v1_calls.py:CLIENTS`,
`EXTRA_LANES`, and `VENDORS`; they are example ratings, not measured promises. The validator checks only
the engine schema version, so passing validation does not verify these pins or Jobs availability.
(`sov_agent/v1/contract.py:_validate_client`, `validate_catalog`,
`qa/e2e_worker_once.py:opencode_binding`, `sov_agent/v1/cli_pins.py:check`)

```sh
cp examples/catalog-openrouter-min.json catalog.local.json
# Edit catalog.local.json with the local paths, hashes, Jobs endpoint and key digest above.
sha256sum /absolute/path/to/opencode /absolute/path/to/python3.12 /absolute/path/to/broker-script
```

The `broker_script` in the E2E harness is `tests/native_test_broker.py`; it is a **test** broker. A
production, public credential-free broker script and its installation instructions are still missing.
(`qa/e2e_worker_once.py:opencode_binding`)

## 4. Register the matching SOV-JOBS definition

The catalog's `jobs` object must point to an existing Jobs service, program and URL. Register one
`agent.call` definition bound to this client's `host_id`, a submit principal with `observe` and `submit`,
and a separate worker principal with `observe` and `worker`. The definition and its revision come from
`call_descriptor(host_id)` and `call_definition(host_id)`; the worker installation uses the same host ID.
The E2E source builds exactly that bundle and calls
`python -m sov_jobs.v2 install-bundle --bundle-json jobs-bundle.json` before `sov_jobs.v2 serve`.
(`qa/e2e_v1_calls.py:main`, lines 594-632; `sov_agent/v1/contract.py:call_descriptor`, `call_definition`)

This repository does **not** provide the SOV-JOBS package, a public install URL, or a public lab
registration guide. `qa/e2e_v1_calls.py:main` lines 594-632 assumes a separate `--jobs-src` and
`--jobs-python`; `docs/deploy/loop-driver/LOOP-DRIVER-RUNBOOK.md` lines 89-92 points at a private Windows
register checkout. A Jobs maintainer must supply the installation and service bootstrap details before
this step can be copied into a plain Linux terminal. Do not guess a bundle or service identifier.

## 5. Install the catalog and register the caller

After Jobs is available, set `SOV_JOBS_TOKEN` to its **64-character submit credential**. The broker needs
only `SOV_AGENT_DATABASE_URL` and that token; caller administration needs the database URL. Generate a
distinct caller token of at least 32 characters in an environment variable; it is read from that variable,
never from the command line. `--overflow` is required even for an explicit metered lane, and `--max-usd`
must be positive. `--allow-uncapped-metered-cli` is only for workspace calls, where the CLI's dollar cap
cannot be enforced before the request; leave it off for this no-workspace read.
(`sov_agent/v1/service.py:serve`, `sov_agent/v1/admin.py:catalog_install`, `caller_put`,
`sov_agent/cli.py:_parser`, `sov_agent/v1/router.py:metered_refusal`)

```sh
# Supply the submit credential issued by SOV-JOBS; do not reuse its worker credential.
export SOV_JOBS_TOKEN='REPLACE-WITH-64-CHARACTER-JOBS-SUBMIT-TOKEN'
export LAB_CALLER_TOKEN="$(python3.12 -c 'import secrets; print(secrets.token_hex(32))')"
sov-agent v1-catalog-install --catalog-json catalog.local.json
sov-agent v1-caller-put --caller-id lab --token-env LAB_CALLER_TOKEN \
  --allow opencode --max-usd 0.05 --overflow
```

Metered admission also requires a shared spend ledger with a real prepaid cap and a per-call limit; with
`SOV_AGENT_SPEND_LEDGER` unset or an unknown cap, it refuses with `prepaid_unknown`. The operator must
configure that ledger from the actual capped OpenRouter key balance, then pass its path to broker and
worker. The worker also needs its **distinct** `SOV_JOBS_WORKER_TOKEN`, `SOV_AGENT_CLIENT_ID=opencode`,
`SOV_HOME`, the same database URL, and `OPENROUTER_API_KEY`; it never falls back to the submit token.
(`sov_agent/v1/spend.py:ledger_from_env`, `SpendLedger.set_limits`,
`sov_agent/v1/service.py:CallService.admit`,
`sov_agent/worker_bootstrap.py:_call_worker_from_environment`)

```sh
# Run only after Jobs, key, spend ledger, catalog pins, and worker have been configured.
sov-agent serve-v1 --host 127.0.0.1 --port 8771
```

## 6. Check the caller, then submit one read-only `agent_ask`

Run these from a second shell with the caller token. `GET /v1/me` reports the caller's usable models,
`overflow_allowed`, and spend ceiling. The POST is the HTTP form of a read-only `agent_ask`: it has no
workspace, an explicit fitted model, `spend.max_usd: 0.05`, and `spend.overflow: false` to opt out of
automatic fallback. A `202` response means the run is open; poll the returned call ID.
(`docs/v1-CALLER-GUIDE.md` sections 2, 3.1, 3.2, 3.7 and 6.1;
`sov_agent/v1/contract.py:validate_call_request`)

```sh
curl -sS -H "Authorization: Bearer $LAB_CALLER_TOKEN" http://127.0.0.1:8771/v1/me
curl -sS -H "Authorization: Bearer $LAB_CALLER_TOKEN" \
  -H 'Content-Type: application/json' http://127.0.0.1:8771/v1/calls -d '{
    "schema_version": 1,
    "idempotency_key": "linux-openrouter-ask-001",
    "select": {"model": "openrouter/deepseek/deepseek-v4-pro"},
    "prompt": "Explain optimistic locking in three factual bullet points.",
    "workspace": null,
    "spend": {"max_usd": 0.05, "overflow": false},
    "deadline_s": 300,
    "wait_s": 60
  }'
# Then: curl -H "Authorization: Bearer $LAB_CALLER_TOKEN" \
#   'http://127.0.0.1:8771/v1/calls/CALL_ID?wait_s=60'
```

## What still needs a human

- Supply a public Linux SOV-AGENT artifact and its selected wheelhouse/manifest. `pyproject.toml` pins
  `sov-src` through Git; a plain install cannot be shown as an offline public command. (`pyproject.toml:[project]`)
- Supply public SOV-JOBS installation, bundle registration, and startup instructions. The E2E harness uses a
  separate checkout; the Windows runbook uses a private registration path. (`qa/e2e_v1_calls.py:main`,
  lines 594-632; `docs/deploy/loop-driver/LOOP-DRIVER-RUNBOOK.md`, lines 89-92)
- Publish a supported Linux v1 worker startup path. The source has `worker-run` and a POSIX bootstrap, but
  the current public worker profile is a selected Windows one-shot coding worker; this document cannot
  promise Linux execution from the supported public surface. (`sov_agent/cli.py:_parser`,
  `sov_agent/worker_bootstrap.py:_call_worker_from_environment`)
- Provide an OpenRouter key with a real prepaid limit, the key's digest for the catalog, a matching spend
  ledger, and the host-specific OpenCode/Python/broker pins. No paid request was made for this example.
  (`sov_agent/v1/spend.py:SpendLedger.set_limits`, `sov_agent/native_openrouter_http.py:OpenRouterHttpExecutor`)
- Improve opaque validator errors separately: `catalog shape`, `services/clients`, `client <id> engine`,
  and `lane shape` do not name the missing or extra nested field. No validator behavior changed here.
  (`sov_agent/v1/contract.py:validate_catalog`, `_validate_client`)

The existing caller guide is stale in four relevant places: it calls v1 subscription-only, says the
no-workspace worker drives a CLI, calls `spend.max_usd` only a gate, and says only Claude may read a
workspace. The current OpenRouter path is metered and direct HTTP without a workspace; it reserves a
prepaid cap; and `READ_WORKSPACE_FAMILIES` includes all families unless strict mode is enabled.
(`docs/v1-CALLER-GUIDE.md` sections 1, 3.5 and 3.7;
`sov_agent/v1/worker.py:CallWorker._engine_executor`,
`sov_agent/v1/service.py:CallService.admit`, `sov_agent/v1/contract.py:READ_WORKSPACE_FAMILIES`)

The guide's request table also says prompts stop at 256 KiB, deadlines at 3600 seconds, and
`spend.max_usd` at 100. The validator currently accepts up to 4 MiB of prompt, a 4-hour deadline,
and any finite nonnegative request spend; the caller grant and ledger still constrain actual metered
admission. (`docs/v1-CALLER-GUIDE.md` section 3.1;
`sov_agent/v1/contract.py:validate_call_request`,
`sov_agent/v1/spend.py:SpendLedger.reserve`)
