NVIDIA / NVIDIA/NemoClaw

[R0] Specify the v1-to-VoiceClaw onboarding handoff and scoped connection fixtures

Open
#11,747 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
22.5k
Forks
3.1k
Avg merge
1d 1h
Merged PRs (30d)
715

Description

# VoiceClaw / NemoClaw R0 wire agreement, revision 1

Profile: `nemoclaw-voice-r0/1`. This is the concrete implementation target for NemoClaw #11747/#11749/#11751 and VoiceClaw #8. It is an experimental connection-only subset, not full proposed v1alpha1 conformance or a supported release decision. The owning maintainers still accept implementation and qualification evidence.

## Scope and ownership

One v1 deployment, one real agent and one VoiceClaw connection. NemoClaw owns native adaptation and credentials. VoiceClaw owns its bootstrap, prompts, configuration, external processes and client interface. Only the semantic connection described here crosses their boundary. Main onboarding/export, prompt submission, task/results APIs, durable sessions, replay, automatic reconnect and offer/lease exchange are outside this revision.

`targetRef` is a NemoClaw-issued opaque public reference to the applied integration and actual agent generation. It is not a native agent/session ID. The credential is bound server-side to the same deployment, integration and generation. Names alone cannot authorize a replacement agent.

## HTTP protocol

R0 uses loopback HTTP on the Brev host. The endpoint argument is the full URL `http://127.0.0.1:/r0/connect` or its IPv6 loopback equivalent. Port is runtime-selected, not fixed in the contract. No redirects, userinfo, query, fragment, arbitrary hostname or proxy routing. Client access from another machine uses private forwarding to VoiceClaw; it does not expose the NemoClaw API publicly.

One request opens the connection:

```http
POST /r0/connect
Authorization: Bearer
Content-Type: application/json
Accept: application/x-ndjson

{"profile":"nemoclaw-voice-r0/1","targetRef":"target-r0-fixture"}
```

UTF-8 JSON; object key order is irrelevant. Reject unknown request keys, duplicate keys, missing/wrong-type values and malformed JSON. JSON media types may have `charset=utf-8`. Maximum request and individual response line: 4096 bytes including newline for lines. No native address, command or protocol option is accepted.

NemoClaw authenticates first, validates the request and target binding, and verifies the existing hosted native agent connection before sending HTTP 200. A container, open port, Fabric listener or generic HTTP health response alone is insufficient. This check need not invoke inference or submit a user prompt.

Success headers: `Content-Type: application/x-ndjson`, `Cache-Control: no-store`. One flushed JSON object per LF-terminated line:

```json
{"type":"ready","profile":"nemoclaw-voice-r0/1","targetRef":"target-r0-fixture","expiresAt":"2030-01-01T00:15:00Z"}
{"type":"heartbeat"}
{"type":"closed","reason":"credential_expired"}
```

The first record is exactly one `ready`. `expiresAt` is an RFC3339 UTC timestamp from the credential issuer, not a lease or new credential. Heartbeats occur at most five seconds apart. The first ready must arrive within ten seconds; subsequent silence exceeding fifteen seconds ends the client connection. Heartbeats are liveness records, not repeated assertions of native readiness. NemoClaw checks target identity/native readiness at least every five seconds while streaming and closes on loss. Stop by credential expiry even if a probe is pending.

After HTTP 200, terminal reasons are `credential_expired`, `agent_unavailable`, `target_replaced`, or `server_stopping`. Emit `closed` when possible, then end the stream. Abrupt EOF, malformed/oversized records, unknown types/profile, an unexpected target, or timeouts also end the VoiceClaw connection. There is no transparent retry, resume or credential renewal. VoiceClaw closes the HTTP stream when its client disconnects; this does not destroy the agent or cancel agent work. One active stream per integration; concurrent attempts return 409. No credential single-use/redemption ledger is required. A fresh credential for the next run is an operator workflow requirement, not a replay-proof claim.

Before HTTP 200, errors are `application/json`, `Cache-Control: no-store` and this shape:

```json
{"error":{"code":"agent_unavailable"}}
```

| HTTP | Code | Meaning |
| --- | --- | --- |
| 401 | authentication_failed | Missing, malformed or unknown bearer credential |
| 401 | credential_expired | Known credential at or after expiry |
| 400 | invalid_request | Invalid JSON/schema, path query or request fields |
| 409 | unsupported_profile | Well-formed but unsupported profile |
| 403 | target_not_authorized | Requested public target differs from authorized target |
| 409 | target_replaced | Authorized target generation has changed |
| 503 | agent_unavailable | Native readiness cannot be established, including probe timeout |
| 409 | connection_active | This integration already has an active stream |
| 413 | request_too_large | Body exceeds the bound |
| 415 | unsupported_media_type | Request content type is not JSON |

Unspecified routes/methods return 404/405 and never execute agent operations. VoiceClaw treats all non-200 statuses as failed connection, including unknown codes. No error, receipt or log includes credentials or native IDs. Fixtures isolate one failure per case; authentication takes precedence over target/readiness checks.

## Credential delivery

The operator-authorized v1 apply/connection path issues a cryptographically random bearer credential with at least 256 bits of entropy and a fifteen-minute lifetime, after VoiceClaw preparation. NemoClaw enforces expiry and target generation. The literal fixture credential below is synthetic and must never be accepted in production.

Pass the value as ASCII plus an optional final LF in an inherited read-only descriptor backed by an owner-only regular file. The file and descriptor belong to the invoking user. No secret in command arguments, environment variables, configuration, result files or logs. VoiceClaw reads at most 4096 bytes, validates ownership/mode, closes the descriptor and keeps the value only in gateway memory. The invoking owner unlinks the temporary credential file after handoff. Gateway exit clears its reference; server expiry/restart ends authority. Client-facing VoiceClaw access is separately VoiceClaw-owned.

## Onboarding ABI

The pinned VoiceClaw distribution supplies `bin/voiceclaw-nemoclaw-r0`. This is a new required entrypoint, not a claim that it exists today. An operator-approved absolute path identifies the trusted VoiceClaw bootstrap. NemoClaw does not download, interpret or execute commands from integration metadata. VoiceClaw's bootstrap acquires/verifies the rest of its distribution if absent.

After v1 verifies the agent, it calls these two stages of the same VoiceClaw-owned entrypoint. This avoids spending the credential lifetime on downloads and prompts:

```text
/bin/voiceclaw-nemoclaw-r0 prepare --profile nemoclaw-voice-r0/1 --result-file
/bin/voiceclaw-nemoclaw-r0 connect --profile nemoclaw-voice-r0/1 --endpoint http://127.0.0.1:/r0/connect --target-ref --credential-fd --result-file
```

`prepare` installs and configures VoiceClaw, collecting its own settings/credentials through the operator terminal. It receives no NemoClaw credential. Success writes `{"profile":"nemoclaw-voice-r0/1","status":"prepared"}` and exits 0. All internal configuration locations and setup mechanics are VoiceClaw-owned; both stages run under the same user and environment. No durable setup-operation journal is required.

After prepare succeeds, v1 rechecks the target, issues access and calls `connect`. VoiceClaw starts its independently owned gateway and establishes the HTTP stream. It writes success only after `ready`: `{"profile":"nemoclaw-voice-r0/1","status":"ready","targetRef":"target-r0-fixture","clientInstructions":"Use the VoiceClaw-owned client instructions at ."}` and exits 0. The string contains no credentials; the VoiceClaw-owned instructions explain its actual client interface and protected client access. #11752 proves that interface with an independent client. The gateway continues under VoiceClaw lifecycle ownership after the bootstrap exits; NemoClaw reports its processes as `not-managed`.

Both stages use a fresh result path in a caller-created owner-only directory, write bounded UTF-8 JSON atomically with mode 0600, and never follow existing symlinks. Maximum result: 4096 bytes. Failure exits nonzero (1 is sufficient) and, when possible, writes `{"profile":"nemoclaw-voice-r0/1","status":"failed","code":"setup_failed"}`; connect uses the relevant wire error code or `connection_lost`. Nonzero exit, missing/malformed result, unexpected status/profile/target or disagreement between exit and result is failure/inconclusive, never success. Prompts/diagnostics use the terminal, not the result JSON. v1 preserves the agent on failure. VoiceClaw cleans up only its own failed-start effects. No automatic setup retry or reconnection.

## Shared fixtures and acceptance

`fixtures.json` is the portable request/response and onboarding corpus. `fixture_server.py` is a standard-library mock of the NemoClaw side for VoiceClaw tests. It contains synthetic credentials and no native backend. Scenario selection is a fixture process argument, never a production request field. `test_fixture.py` checks its wire behavior.

NemoClaw consumes the same corpus against its implementation with controlled issuer clock, target identity and native-adapter test doubles. VoiceClaw consumes it through its actual semantic client and unpacked CI artifacts. Also test partial NDJSON chunks, unknown/malformed/oversized records, silent stream, abrupt EOF, active-connection rejection, and failure to write/read bootstrap results. These tests do not claim Brev or native-agent qualification.

Pin the SHA-256 of the exact `fixtures.json` bytes and profile in both repositories. Publish fixture changes before either implementation changes wire behavior. R0 acceptance remains the independent-client test against a real NemoClaw v1 deployment, not this mock.

## Delivery and validation

Both workstreams implement this revision. NemoClaw #11749 supplies the server; #11751 supplies invocation; VoiceClaw #8 supplies bootstrap/client and artifact proof. The issue remains open until implementations and shared tests provide evidence. This records the requested experimental interface, not maintainer product acceptance.

Fixture SHA-256: `20132310c2f14dd927b0ac50e30d5176a9e3c99b83d5890c9cf581e2320d3442`. Bundle SHA-256: `ddc17a6f78ee27ae01048c07e477a309798e4dc41cb89741ee2cd78e2d177e1d`.

Extract the JSON between the markers below, parse it, and write each `files` value verbatim under its filename. No install dependencies are needed. Run `python3 -m unittest discover -s . -p test_fixture.py -v`; run the mock with `python3 fixture_server.py --scenario ready --port 0`. Use the fixture clock for client expiry assertions. The mock never counts as live agent evidence.

Portable shared fixture bundle (revision 1)

```json
{
"profile": "nemoclaw-voice-r0/1",
"files": {
"fixtures.json": "{\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"clock\": \"2030-01-01T00:00:00Z\",\n \"syntheticCredential\": \"r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"targetRef\": \"target-r0-fixture\",\n \"notes\": \"Fixtures only. Ready scenario holds open and emits heartbeats. Errors are isolated scenarios. Production uses its issuer clock, native probe and identity binding.\",\n \"onboarding\": {\n \"prepared\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"status\": \"prepared\"\n },\n \"ready\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"status\": \"ready\",\n \"targetRef\": \"target-r0-fixture\",\n \"clientInstructions\": \"Use fixture client instructions.\"\n },\n \"failed\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"status\": \"failed\",\n \"code\": \"setup_failed\"\n }\n },\n \"cases\": [\n {\n \"name\": \"ready\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 200,\n \"contentType\": \"application/x-ndjson\",\n \"records\": [\n {\n \"type\": \"ready\",\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\",\n \"expiresAt\": \"2030-01-01T00:15:00Z\"\n },\n {\n \"type\": \"heartbeat\"\n }\n ]\n }\n },\n {\n \"name\": \"missing_auth\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 401,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"authentication_failed\"\n }\n }\n }\n },\n {\n \"name\": \"invalid_auth\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer invalid-fixture\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 401,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"authentication_failed\"\n }\n }\n }\n },\n {\n \"name\": \"expired\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 401,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"credential_expired\"\n }\n }\n }\n },\n {\n \"name\": \"wrong_target\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"another-target\"\n }\n },\n \"response\": {\n \"status\": 403,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"target_not_authorized\"\n }\n }\n }\n },\n {\n \"name\": \"replaced\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 409,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"target_replaced\"\n }\n }\n }\n },\n {\n \"name\": \"unavailable\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 503,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"agent_unavailable\"\n }\n }\n }\n },\n {\n \"name\": \"unsupported_profile\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"unsupported/0\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 409,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"unsupported_profile\"\n }\n }\n }\n },\n {\n \"name\": \"invalid_request\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\",\n \"extra\": true\n }\n },\n \"response\": {\n \"status\": 400,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"invalid_request\"\n }\n }\n }\n },\n {\n \"name\": \"active\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 409,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"connection_active\"\n }\n }\n }\n },\n {\n \"name\": \"oversized\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\",\n \"padding\": \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n }\n },\n \"response\": {\n \"status\": 413,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"request_too_large\"\n }\n }\n }\n },\n {\n \"name\": \"media_type\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"text/plain\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 415,\n \"contentType\": \"application/json\",\n \"body\": {\n \"error\": {\n \"code\": \"unsupported_media_type\"\n }\n }\n }\n },\n {\n \"name\": \"stream_credential_expired\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 200,\n \"contentType\": \"application/x-ndjson\",\n \"records\": [\n {\n \"type\": \"ready\",\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\",\n \"expiresAt\": \"2030-01-01T00:15:00Z\"\n },\n {\n \"type\": \"closed\",\n \"reason\": \"credential_expired\"\n }\n ]\n }\n },\n {\n \"name\": \"stream_agent_unavailable\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 200,\n \"contentType\": \"application/x-ndjson\",\n \"records\": [\n {\n \"type\": \"ready\",\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\",\n \"expiresAt\": \"2030-01-01T00:15:00Z\"\n },\n {\n \"type\": \"closed\",\n \"reason\": \"agent_unavailable\"\n }\n ]\n }\n },\n {\n \"name\": \"stream_target_replaced\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 200,\n \"contentType\": \"application/x-ndjson\",\n \"records\": [\n {\n \"type\": \"ready\",\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\",\n \"expiresAt\": \"2030-01-01T00:15:00Z\"\n },\n {\n \"type\": \"closed\",\n \"reason\": \"target_replaced\"\n }\n ]\n }\n },\n {\n \"name\": \"stream_server_stopping\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 200,\n \"contentType\": \"application/x-ndjson\",\n \"records\": [\n {\n \"type\": \"ready\",\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\",\n \"expiresAt\": \"2030-01-01T00:15:00Z\"\n },\n {\n \"type\": \"closed\",\n \"reason\": \"server_stopping\"\n }\n ]\n }\n },\n {\n \"name\": \"abrupt_eof\",\n \"request\": {\n \"method\": \"POST\",\n \"path\": \"/r0/connect\",\n \"headers\": {\n \"Authorization\": \"Bearer r0-fixture-only-0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/x-ndjson\"\n },\n \"body\": {\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\"\n }\n },\n \"response\": {\n \"status\": 200,\n \"contentType\": \"application/x-ndjson\",\n \"records\": [\n {\n \"type\": \"ready\",\n \"profile\": \"nemoclaw-voice-r0/1\",\n \"targetRef\": \"target-r0-fixture\",\n \"expiresAt\": \"2030-01-01T00:15:00Z\"\n }\n ]\n },\n \"clientOutcome\": \"disconnected\"\n }\n ]\n}\n",
"fixture_server.py": "\"\"\"Synthetic R0 wire fixture. Not an agent backend or production server.\"\"\"\nimport argparse\nimport json\nimport threading\nimport time\nfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\nfrom pathlib import Path\n\nCORPUS = json.loads(Path(__file__).with_name('fixtures.json').read_text())\n\n\ndef make_server(scenario='ready', port=0, heartbeat_seconds=5):\n case = next(c for c in CORPUS['cases'] if c['name'] == scenario)\n\n class Handler(BaseHTTPRequestHandler):\n protocol_version = 'HTTP/1.1'\n\n def log_message(self, *args):\n pass # Never log request headers or credentials.\n\n def json_error(self, status, code):\n payload = json.dumps({'error': {'code': code}}).encode()\n self.send_response(status)\n self.send_header('Content-Type', 'application/json')\n self.send_header('Cache-Control', 'no-store')\n self.send_header('Content-Length', str(len(payload)))\n self.send_header('Connection', 'close')\n self.end_headers()\n self.wfile.write(payload)\n self.close_connection = True\n\n def do_POST(self):\n expected = case['request']\n try:\n length = int(self.headers.get('Content-Length', '0'))\n if not 0 <= length <= 8192:\n return self.json_error(413, 'request_too_large')\n raw = self.rfile.read(length)\n body = json.loads(raw)\n except (ValueError, UnicodeError):\n return self.json_error(400, 'invalid_request')\n # Exact scenario inputs prevent a fixture from hiding client bugs.\n if (self.path != expected['path'] or body != expected['body']\n or self.headers.get('Authorization') != expected['headers'].get('Authorization')\n or self.headers.get('Content-Type', '').split(';')[0].strip().lower()\n != expected['headers']['Content-Type']\n or self.headers.get('Accept') != expected['headers']['Accept']):\n return self.json_error(400, 'fixture_request_mismatch')\n response = case['response']\n if response['status'] != 200:\n return self.json_error(response['status'], response['body']['error']['code'])\n self.send_response(200)\n self.send_header('Content-Type', 'application/x-ndjson')\n self.send_header('Cache-Control', 'no-store')\n self.send_header('Connection', 'close')\n self.end_headers()\n self.close_connection = True\n try:\n for record in response['records']:\n self.wfile.write((json.dumps(record) + '\\n').encode())\n self.wfile.flush()\n while scenario == 'ready' and not self.server.stopping.wait(heartbeat_seconds):\n self.wfile.write(b'{\"type\":\"heartbeat\"}\\n')\n self.wfile.flush()\n except (BrokenPipeError, ConnectionResetError):\n pass\n\n def do_GET(self):\n self.json_error(405, 'method_not_allowed')\n\n server = ThreadingHTTPServer(('127.0.0.1', port), Handler)\n server.stopping = threading.Event()\n return server\n\n\nif __name__ == '__main__':\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument('--scenario', choices=[c['name'] for c in CORPUS['cases']], default='ready')\n parser.add_argument('--port', type=int, default=0)\n args = parser.parse_args()\n server = make_server(args.scenario, args.port)\n print(json.dumps({'endpoint': f'http://127.0.0.1:{server.server_port}/r0/connect',\n 'scenario': args.scenario, 'synthetic': True}), flush=True)\n try:\n server.serve_forever()\n except KeyboardInterrupt:\n pass\n finally:\n server.stopping.set()\n server.server_close()\n",
"test_fixture.py": "\"\"\"Checks the fixture server against the portable corpus, not production conformance.\"\"\"\nimport http.client\nimport json\nimport threading\nimport unittest\nfrom fixture_server import CORPUS, make_server\n\n\nclass FixtureTest(unittest.TestCase):\n def test_all_wire_cases(self):\n for case in CORPUS['cases']:\n with self.subTest(case=case['name']):\n server = make_server(case['name'], heartbeat_seconds=0.02)\n thread = threading.Thread(target=server.serve_forever, daemon=True)\n thread.start()\n client = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=2)\n try:\n req = case['request']\n client.request(req['method'], req['path'], json.dumps(req['body']), req['headers'])\n response = client.getresponse()\n expected = case['response']\n self.assertEqual(response.status, expected['status'])\n self.assertEqual(response.getheader('Content-Type'), expected['contentType'])\n self.assertEqual(response.getheader('Cache-Control'), 'no-store')\n if response.status == 200:\n for record in expected['records']:\n self.assertEqual(json.loads(response.readline()), record)\n if case['name'] == 'ready':\n self.assertEqual(json.loads(response.readline()), {'type': 'heartbeat'})\n else:\n self.assertEqual(response.readline(), b'')\n else:\n self.assertEqual(json.loads(response.read()), expected['body'])\n response.close()\n finally:\n client.close()\n server.stopping.set()\n server.shutdown()\n server.server_close()\n thread.join()\n\n def test_fixture_does_not_accept_wrong_client_request(self):\n server = make_server()\n thread = threading.Thread(target=server.serve_forever, daemon=True)\n thread.start()\n client = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=2)\n try:\n client.request('POST', '/r0/connect', '{}', {'Content-Type': 'application/json'})\n response = client.getresponse()\n self.assertEqual(response.status, 400)\n self.assertEqual(json.loads(response.read())['error']['code'], 'fixture_request_mismatch')\n finally:\n client.close()\n server.stopping.set()\n server.shutdown()\n server.server_close()\n thread.join()\n\n\nif __name__ == '__main__':\n unittest.main()\n"
}
}
```

Contributor guide

Open the contributing guide

Research direction

Extract the embedded bundle into fixtures.json and the named fixture_server.py and test_fixture.py files, then run `python3 -m unittest discover -s . -p test_fixture.py -v` and the ready mock scenario. Compare the fixture corpus with the onboarding ABI and verify that prepare/connect results, NDJSON behavior, and failure cases match the stated acceptance conditions.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, typescript
Domain
backend-api-design, testing, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.