Metadata-Version: 2.4
Name: witbitz-code
Version: 1.2.1
Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
Author: Witbitz
License-Expression: MIT
Project-URL: Documentation, https://witbitz.chat/docs/opencode.md
Project-URL: Repository, https://github.com/witbitzchat/witbitz-code
Keywords: witbitz,opencode,relay,e2ee
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Environment :: Console
Classifier: Topic :: Software Development
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=41
Requires-Dist: websockets>=13
Requires-Dist: httpx>=0.25
Requires-Dist: segno>=1.5
Requires-Dist: tinfoil<0.15,>=0.14
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Dynamic: license-file

# witbitz-code

Use the **Code** section of Witbitz Spaces, on any device you're signed in on, to reach
[OpenCode](https://opencode.ai) running on your computer. It's end-to-end encrypted, needs no open port, and doesn't need
Tailscale.

This is the Python build of the `witbitz-code` tool. It speaks the same wire protocol, uses the same pairing file and the
same account registry as the single-file Node download (`node witbitz-code.mjs`), so either one can pair a computer and
the other can serve or unpair it.

## Install

```sh
pipx install witbitz-code          # or: pip install witbitz-code
```

Python 3.10 or newer. Dependencies: `cryptography`, `websockets`, `httpx`, `segno`, and `tinfoil` (Tinfoil's own
verifier — attestation, Sigstore and TLS pinning — for the confidential models below).

## Use

```sh
witbitz-code setup
```

`setup` walks five steps and skips whatever is already done: it installs OpenCode if it is missing (it asks first), pairs
this computer (scan the QR code in Spaces → Settings → Back up & recovery → Add a device), asks for your **TrustedRouter**
key and an optional **Tinfoil** key (each checked with the provider before it is saved; it shows as `*****` while you
paste), then starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C stops it); next time,
`witbitz-code serve` is enough.

Your keys are yours: you pay TrustedRouter and Tinfoil directly, and your Witbitz account never carries them — so a new
computer asks for them once. The TrustedRouter key goes into OpenCode's own credentials (`$XDG_DATA_HOME/opencode/auth.json`,
the file `opencode auth login` writes), the Tinfoil key into `~/.opencode-server.env`. Without a TrustedRouter key,
OpenCode offers only its own free models.

The same thing one step at a time: install OpenCode (`curl -fsSL https://opencode.ai/install | bash`), then
`witbitz-code pair`, `witbitz-code trustedrouter-key`, `witbitz-code tinfoil-key` (optional) and `witbitz-code serve`.

| command | what it does |
|---|---|
| `setup [--port <n>]` | The walk-through above. With `--port`, a pairing made for another port moves to it (no new scan). |
| `trustedrouter-key` | Ask for a TrustedRouter key, check it, and store it in OpenCode's credentials. Restart `serve` to use it. |
| `tinfoil-key` | Ask for a Tinfoil key, check it, and store it. Used from the next message. |
| `pair [--name <name>] [--port <n> \| --opencode-url <url>]` | Show the QR code. The account that scans it gets this computer. `--name` sets the label your devices show (default: the hostname). |
| `serve [--port <n>] [--no-opencode]` | Start OpenCode on `127.0.0.1:<n>` (default 4096) if nothing is listening there, then connect the pairings whose OpenCode is on that port. One OpenCode per port, one `serve` per OpenCode. |
| `status` | List this computer's pairings: name, account, OpenCode address and relay. Secrets are never printed. |
| `rotate [--account <email>]` | Replace the pairing secret(s) without scanning again, then restart `serve`. Devices pick up the new secret on their next sync. |
| `unpair [--account <email>]` | Remove this computer from an account. Every device drops it on its next sync. |
| `version`, `--help` | |

`pair`, `rotate` and `unpair` also accept `--dry-run`.

**More than one account.** Each account that scans the QR code gets its own pairing, with its own secret and relay
channel. OpenCode has no users, so every paired account reaches the same sessions, files and shell. That's fine when
all the accounts are yours. If a second account belongs to **another person**, run a separate OpenCode for them
(another port, ideally another OS user) and pair that account with `--port`.

## How it works

```
 phone / desktop (Code page)                               this computer
 seal ▸ frames ◂ open  ── wss ─▶ code-relay.witbitz.chat ◀─ wss ──  witbitz-code serve ──▶ opencode (127.0.0.1)
```

- **Both ends dial out** to `wss://code-relay.witbitz.chat`. Nothing listens on your network, and OpenCode never leaves
  `127.0.0.1`.
- **Pairing** uses a device link: the QR code holds only an ephemeral public key. Your signed-in device seals the account
  pointer to that key. The computer then mints a random 32-byte **secret** for this pairing and does two things with it:
  - stores it in `~/.witbitz/code/pairings.json` (mode 0600);
  - publishes it into the account's sealed `computers` registry, which every signed-in device reads.
- **Keys:** the relay channel id and two direction keys (page→computer and computer→page, AES-256-GCM) are derived from
  the secret with HKDF-SHA256. A frame reflected back at its sender doesn't decrypt.
- **Every frame is sealed.** The relay forwards ciphertext it can't read, and the clear header (sender id, sequence
  number) is authenticated. Receivers drop replays. Large bodies are split into parts below the relay's message size cap.
- **Replays across restarts:** each time the connector's socket opens, it announces a fresh random nonce inside its
  sealed hello. Every request and subscription must carry that nonce, so a request recorded before a restart or
  reconnect is refused (409) and never reaches OpenCode.
- **The connector only forwards what the Code page itself calls:** list and read sessions, send a message, abort, answer
  a permission prompt, rename and delete. Every other request gets a 403 without touching OpenCode. OpenCode can run shell
  commands, so a leaked secret must not unlock more than the page can do. The live event stream is forwarded only while
  a device is watching.
- **The OpenCode password** lives in `~/.opencode-server.env` (created 0600 on first pair). The connector adds it to local
  calls, and it never leaves the computer.

**Confidential models** (the ones labelled "· confidential" in the model menu) are enforced on this computer, by a proxy
`serve` starts on `127.0.0.1:<port + 100>` and points OpenCode's TrustedRouter calls at — the same checks as the Node download:
- the request goes out only after **TrustedRouter's gateway proves** (Google Confidential Space attestation: Intel TDX,
  secure boot, debug off, the operator's own image) what it runs, and it carries the hard floor
  `provider.min_privacy = confidential`;
- the answer's words stream as they arrive, but **tool calls, the finish and usage wait for TrustedRouter's signed
  receipt** to verify for these exact request and response bytes (an attested key, a TEE-verified upstream under a known
  policy). A receipt that does not verify ends the step with an error, so no tool from that answer runs;
- an image or a document a text-only confidential model cannot read is read **as text inside Tinfoil's attested
  enclave** with your Tinfoil key — or refused, never sent anywhere less;
- every other model passes through untouched.

**Auto mode** (Manual · Accept edits · Plan · **Auto** in the Code composer's mode chip, per session) lets this computer answer OpenCode's permission
prompts while you are away:
- **Refused at once:** what is never safe, like `rm -rf ~`, `mkfs` or `dd` onto a disk. The agent is told why.
- **Allowed at once:** read-only commands that stay inside the project, and edits to ordinary files in it.
- **Everything else** goes to the session's own model, in a throw-away session that can use no tools. Only a clear,
  low-severity allow runs. A refusal, "ask", an unclear answer or a timeout leaves the prompt for you.
- **Never "always":** each answer covers that one call.

Each decision is logged to `~/.witbitz/code/auto-log.jsonl` as a digest, never the command. The rules are shared with the
Node connector and tested against the same cases, so both decide alike.

**What is encrypted, and what isn't.** Requests, responses and events travel end to end. The relay operator sees a
pseudonymous channel id, IP addresses, connection times, and frame sizes and timing. That's the same class of metadata
the Spaces room store sees. Anyone holding a pairing secret can drive OpenCode within the allowlist until you run
`rotate`.

Design: `docs/opencode-relay.md` in the Witbitz repository.

## Files and environment

| | default | override |
|---|---|---|
| pairings | `~/.witbitz/code/pairings.json` | `WITBITZ_CODE_PAIRINGS` |
| OpenCode password | `~/.opencode-server.env` (`OPENCODE_SERVER_PASSWORD=`) | `OPENCODE_ENV_FILE` |
| pairing QR, as SVG | `~/.witbitz-rc.link.svg` | |
| Auto mode state + decision log | `~/.witbitz/code/auto-<computer>.json`, `auto-log.jsonl` | `WITBITZ_CODE_AUTO_DIR` |
| account API | `https://api.witbitz.chat/v1/space` | `RC_BASE`, `RC_ORIGIN` |
| device-link origins | `https://spaces.witbitz.chat,https://witbitz-spaces.pages.dev` | `RC_LINK_ORIGIN` |

## Development

```sh
pip install -e '.[test]'
python -m pytest tests -q
```

The tests hold this package to the JavaScript reference implementation. They run the modules in `spaces/public/` and
`tools/` under `node` (22 or newer), and they cover:

- the pinned HKDF vectors;
- frames sealed on either side opening on the other;
- chunking through surrogate pairs;
- the device-link reply and the backup keys;
- the registry merge rules, compared byte for byte;
- the pairing file in both directions;
- the gzip wrap of account docs;
- a connector of each language driven by a client of the other, through a local fake relay and a fake OpenCode.

Without the repository checkout (`WITBITZ_REPO`) or `node`, the cross-implementation tests are skipped.
