witbitz-code 1.2.0__tar.gz → 1.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {witbitz_code-1.2.0/src/witbitz_code.egg-info → witbitz_code-1.2.1}/PKG-INFO +33 -7
  2. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/README.md +31 -6
  3. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/pyproject.toml +5 -2
  4. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/__init__.py +1 -1
  5. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/cli.py +79 -8
  6. witbitz_code-1.2.1/src/witbitz_code/confidential.py +679 -0
  7. witbitz_code-1.2.1/src/witbitz_code/confidential_models.json +69 -0
  8. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/connector.py +20 -0
  9. witbitz_code-1.2.1/src/witbitz_code/gateway.py +147 -0
  10. witbitz_code-1.2.1/src/witbitz_code/keys.py +212 -0
  11. witbitz_code-1.2.1/src/witbitz_code/models.py +73 -0
  12. witbitz_code-1.2.1/src/witbitz_code/receipt.py +153 -0
  13. witbitz_code-1.2.1/src/witbitz_code/tinfoil_read.py +122 -0
  14. witbitz_code-1.2.1/src/witbitz_code/wizard.py +202 -0
  15. {witbitz_code-1.2.0 → witbitz_code-1.2.1/src/witbitz_code.egg-info}/PKG-INFO +33 -7
  16. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code.egg-info/SOURCES.txt +10 -0
  17. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code.egg-info/requires.txt +1 -0
  18. witbitz_code-1.2.1/tests/test_confidential.py +355 -0
  19. witbitz_code-1.2.1/tests/test_setup.py +175 -0
  20. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/LICENSE +0 -0
  21. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/setup.cfg +0 -0
  22. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/__main__.py +0 -0
  23. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/_js.py +0 -0
  24. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/account.py +0 -0
  25. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/asks.py +0 -0
  26. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/attachments.py +0 -0
  27. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/auto.py +0 -0
  28. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/auto_runner.py +0 -0
  29. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/codetools.py +0 -0
  30. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/compress.py +0 -0
  31. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/computers.py +0 -0
  32. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/devicelink.py +0 -0
  33. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/folders.py +0 -0
  34. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/link.py +0 -0
  35. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/net.py +0 -0
  36. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  37. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  38. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  39. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  40. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/outputs.py +0 -0
  41. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/pair.py +0 -0
  42. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/pairings.py +0 -0
  43. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/plugins.py +0 -0
  44. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/policy.py +0 -0
  45. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/qr.py +0 -0
  46. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/relay.py +0 -0
  47. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/seen.py +0 -0
  48. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/tools_probe.py +0 -0
  49. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  50. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  51. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code.egg-info/top_level.txt +0 -0
  52. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_asks.py +0 -0
  53. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_attachments.py +0 -0
  54. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_auto.py +0 -0
  55. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_cli.py +0 -0
  56. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_codetools.py +0 -0
  57. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_connector_e2e.py +0 -0
  58. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_devicelink.py +0 -0
  59. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_files_and_compress.py +0 -0
  60. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_folders.py +0 -0
  61. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_outputs.py +0 -0
  62. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_pair_flows.py +0 -0
  63. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_plugins.py +0 -0
  64. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_policy.py +0 -0
  65. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_registry.py +0 -0
  66. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_relay_codec.py +0 -0
  67. {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_tools_probe.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -18,6 +18,7 @@ Requires-Dist: cryptography>=41
18
18
  Requires-Dist: websockets>=13
19
19
  Requires-Dist: httpx>=0.25
20
20
  Requires-Dist: segno>=1.5
21
+ Requires-Dist: tinfoil<0.15,>=0.14
21
22
  Provides-Extra: test
22
23
  Requires-Dist: pytest>=8; extra == "test"
23
24
  Dynamic: license-file
@@ -38,21 +39,34 @@ the other can serve or unpair it.
38
39
  pipx install witbitz-code # or: pip install witbitz-code
39
40
  ```
40
41
 
41
- Python 3.10 or newer. Dependencies: `cryptography`, `websockets`, `httpx`, `segno`.
42
-
43
- You also need OpenCode itself: `npm install -g opencode-ai` or `curl -fsSL https://opencode.ai/install | bash`.
42
+ Python 3.10 or newer. Dependencies: `cryptography`, `websockets`, `httpx`, `segno`, and `tinfoil` (Tinfoil's own
43
+ verifier — attestation, Sigstore and TLS pinning — for the confidential models below).
44
44
 
45
45
  ## Use
46
46
 
47
47
  ```sh
48
- witbitz-code pair # shows a QR code: scan it in Spaces → Settings → Back up & recovery → Add a device
49
- witbitz-code serve # starts OpenCode on 127.0.0.1:4096 (unless it is running) and the connector
48
+ witbitz-code setup
50
49
  ```
51
50
 
52
- Then open Code in Spaces. Leave `serve` running (Ctrl-C stops it).
51
+ `setup` walks five steps and skips whatever is already done: it installs OpenCode if it is missing (it asks first), pairs
52
+ this computer (scan the QR code in Spaces → Settings → Back up & recovery → Add a device), asks for your **TrustedRouter**
53
+ key and an optional **Tinfoil** key (each checked with the provider before it is saved; it shows as `*****` while you
54
+ paste), then starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C stops it); next time,
55
+ `witbitz-code serve` is enough.
56
+
57
+ Your keys are yours: you pay TrustedRouter and Tinfoil directly, and your Witbitz account never carries them — so a new
58
+ computer asks for them once. The TrustedRouter key goes into OpenCode's own credentials (`$XDG_DATA_HOME/opencode/auth.json`,
59
+ the file `opencode auth login` writes), the Tinfoil key into `~/.opencode-server.env`. Without a TrustedRouter key,
60
+ OpenCode offers only its own free models.
61
+
62
+ The same thing one step at a time: install OpenCode (`curl -fsSL https://opencode.ai/install | bash`), then
63
+ `witbitz-code pair`, `witbitz-code trustedrouter-key`, `witbitz-code tinfoil-key` (optional) and `witbitz-code serve`.
53
64
 
54
65
  | command | what it does |
55
66
  |---|---|
67
+ | `setup [--port <n>]` | The walk-through above. With `--port`, a pairing made for another port moves to it (no new scan). |
68
+ | `trustedrouter-key` | Ask for a TrustedRouter key, check it, and store it in OpenCode's credentials. Restart `serve` to use it. |
69
+ | `tinfoil-key` | Ask for a Tinfoil key, check it, and store it. Used from the next message. |
56
70
  | `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). |
57
71
  | `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. |
58
72
  | `status` | List this computer's pairings: name, account, OpenCode address and relay. Secrets are never printed. |
@@ -94,6 +108,18 @@ all the accounts are yours. If a second account belongs to **another person**, r
94
108
  - **The OpenCode password** lives in `~/.opencode-server.env` (created 0600 on first pair). The connector adds it to local
95
109
  calls, and it never leaves the computer.
96
110
 
111
+ **Confidential models** (the ones labelled "· confidential" in the model menu) are enforced on this computer, by a proxy
112
+ `serve` starts on `127.0.0.1:<port + 100>` and points OpenCode's TrustedRouter calls at — the same checks as the Node download:
113
+ - the request goes out only after **TrustedRouter's gateway proves** (Google Confidential Space attestation: Intel TDX,
114
+ secure boot, debug off, the operator's own image) what it runs, and it carries the hard floor
115
+ `provider.min_privacy = confidential`;
116
+ - the answer's words stream as they arrive, but **tool calls, the finish and usage wait for TrustedRouter's signed
117
+ receipt** to verify for these exact request and response bytes (an attested key, a TEE-verified upstream under a known
118
+ policy). A receipt that does not verify ends the step with an error, so no tool from that answer runs;
119
+ - an image or a document a text-only confidential model cannot read is read **as text inside Tinfoil's attested
120
+ enclave** with your Tinfoil key — or refused, never sent anywhere less;
121
+ - every other model passes through untouched.
122
+
97
123
  **Auto mode** (Manual · Accept edits · Plan · **Auto** in the Code composer's mode chip, per session) lets this computer answer OpenCode's permission
98
124
  prompts while you are away:
99
125
  - **Refused at once:** what is never safe, like `rm -rf ~`, `mkfs` or `dd` onto a disk. The agent is told why.
@@ -14,21 +14,34 @@ the other can serve or unpair it.
14
14
  pipx install witbitz-code # or: pip install witbitz-code
15
15
  ```
16
16
 
17
- Python 3.10 or newer. Dependencies: `cryptography`, `websockets`, `httpx`, `segno`.
18
-
19
- You also need OpenCode itself: `npm install -g opencode-ai` or `curl -fsSL https://opencode.ai/install | bash`.
17
+ Python 3.10 or newer. Dependencies: `cryptography`, `websockets`, `httpx`, `segno`, and `tinfoil` (Tinfoil's own
18
+ verifier — attestation, Sigstore and TLS pinning — for the confidential models below).
20
19
 
21
20
  ## Use
22
21
 
23
22
  ```sh
24
- witbitz-code pair # shows a QR code: scan it in Spaces → Settings → Back up & recovery → Add a device
25
- witbitz-code serve # starts OpenCode on 127.0.0.1:4096 (unless it is running) and the connector
23
+ witbitz-code setup
26
24
  ```
27
25
 
28
- Then open Code in Spaces. Leave `serve` running (Ctrl-C stops it).
26
+ `setup` walks five steps and skips whatever is already done: it installs OpenCode if it is missing (it asks first), pairs
27
+ this computer (scan the QR code in Spaces → Settings → Back up & recovery → Add a device), asks for your **TrustedRouter**
28
+ key and an optional **Tinfoil** key (each checked with the provider before it is saved; it shows as `*****` while you
29
+ paste), then starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C stops it); next time,
30
+ `witbitz-code serve` is enough.
31
+
32
+ Your keys are yours: you pay TrustedRouter and Tinfoil directly, and your Witbitz account never carries them — so a new
33
+ computer asks for them once. The TrustedRouter key goes into OpenCode's own credentials (`$XDG_DATA_HOME/opencode/auth.json`,
34
+ the file `opencode auth login` writes), the Tinfoil key into `~/.opencode-server.env`. Without a TrustedRouter key,
35
+ OpenCode offers only its own free models.
36
+
37
+ The same thing one step at a time: install OpenCode (`curl -fsSL https://opencode.ai/install | bash`), then
38
+ `witbitz-code pair`, `witbitz-code trustedrouter-key`, `witbitz-code tinfoil-key` (optional) and `witbitz-code serve`.
29
39
 
30
40
  | command | what it does |
31
41
  |---|---|
42
+ | `setup [--port <n>]` | The walk-through above. With `--port`, a pairing made for another port moves to it (no new scan). |
43
+ | `trustedrouter-key` | Ask for a TrustedRouter key, check it, and store it in OpenCode's credentials. Restart `serve` to use it. |
44
+ | `tinfoil-key` | Ask for a Tinfoil key, check it, and store it. Used from the next message. |
32
45
  | `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). |
33
46
  | `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. |
34
47
  | `status` | List this computer's pairings: name, account, OpenCode address and relay. Secrets are never printed. |
@@ -70,6 +83,18 @@ all the accounts are yours. If a second account belongs to **another person**, r
70
83
  - **The OpenCode password** lives in `~/.opencode-server.env` (created 0600 on first pair). The connector adds it to local
71
84
  calls, and it never leaves the computer.
72
85
 
86
+ **Confidential models** (the ones labelled "· confidential" in the model menu) are enforced on this computer, by a proxy
87
+ `serve` starts on `127.0.0.1:<port + 100>` and points OpenCode's TrustedRouter calls at — the same checks as the Node download:
88
+ - the request goes out only after **TrustedRouter's gateway proves** (Google Confidential Space attestation: Intel TDX,
89
+ secure boot, debug off, the operator's own image) what it runs, and it carries the hard floor
90
+ `provider.min_privacy = confidential`;
91
+ - the answer's words stream as they arrive, but **tool calls, the finish and usage wait for TrustedRouter's signed
92
+ receipt** to verify for these exact request and response bytes (an attested key, a TEE-verified upstream under a known
93
+ policy). A receipt that does not verify ends the step with an error, so no tool from that answer runs;
94
+ - an image or a document a text-only confidential model cannot read is read **as text inside Tinfoil's attested
95
+ enclave** with your Tinfoil key — or refused, never sent anywhere less;
96
+ - every other model passes through untouched.
97
+
73
98
  **Auto mode** (Manual · Accept edits · Plan · **Auto** in the Code composer's mode chip, per session) lets this computer answer OpenCode's permission
74
99
  prompts while you are away:
75
100
  - **Refused at once:** what is never safe, like `rm -rf ~`, `mkfs` or `dd` onto a disk. The agent is told why.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "witbitz-code"
7
- version = "1.2.0"
7
+ version = "1.2.1"
8
8
  description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -23,6 +23,9 @@ dependencies = [
23
23
  "websockets>=13",
24
24
  "httpx>=0.25",
25
25
  "segno>=1.5",
26
+ # Tinfoil's own verifier (attestation + Sigstore + TLS pinning) — the confidential proxy reads images and files a
27
+ # text-only confidential model cannot, in Tinfoil's attested enclave (tinfoil_read.py). Pinned to a series on purpose.
28
+ "tinfoil>=0.14,<0.15",
26
29
  ]
27
30
 
28
31
  [project.optional-dependencies]
@@ -40,7 +43,7 @@ where = ["src"]
40
43
 
41
44
  # Our OpenCode plugins, installed by `serve` (plugins.py) — copies of tools/opencode-plugins, kept current by embed.mjs
42
45
  [tool.setuptools.package-data]
43
- witbitz_code = ["opencode_plugins/*"]
46
+ witbitz_code = ["opencode_plugins/*", "confidential_models.json"]
44
47
 
45
48
  [tool.pytest.ini_options]
46
49
  testpaths = ["tests"]
@@ -4,4 +4,4 @@ The Python implementation of tools/witbitz-code.mjs (docs/opencode-relay.md phas
4
4
  vectors and cross-implementation tests.
5
5
  """
6
6
 
7
- __version__ = "1.2.0"
7
+ __version__ = "1.2.1"
@@ -1,18 +1,20 @@
1
1
  """witbitz-code — connect the Spaces Code section to OpenCode on this computer (docs/opencode-relay.md). The same commands
2
2
  as the single-file Node download (tools/witbitz-code.mjs):
3
3
 
4
+ witbitz-code setup [--port 4096] the walk-through: OpenCode, pairing, the TrustedRouter and Tinfoil keys, then serve
4
5
  witbitz-code pair [--name desk] scan the QR with the Spaces app → this computer joins that account
5
6
  witbitz-code serve [--port 4096] start OpenCode (if it is not running) and the connector
6
7
  witbitz-code status what this computer is paired with
7
8
  witbitz-code rotate new secrets (no scan) — restart serve afterwards
8
9
  witbitz-code unpair [--account a@b] remove this computer from an account
10
+ witbitz-code trustedrouter-key store the TrustedRouter key in OpenCode's credentials (what `opencode auth login` does)
11
+ witbitz-code tinfoil-key store the Tinfoil key (confidential reading of images and files)
9
12
  """
10
13
 
11
14
  from __future__ import annotations
12
15
 
13
16
  import asyncio
14
17
  import os
15
- import shutil
16
18
  import signal
17
19
  import subprocess
18
20
  import sys
@@ -23,18 +25,22 @@ import httpx
23
25
  from . import __version__
24
26
  from . import pair as pair_cmd
25
27
  from . import pairings as pp
26
- from . import codetools, plugins, policy
28
+ from . import codetools, confidential, keys, models, plugins, policy, wizard
27
29
  from ._js import to_number, truthy
28
30
  from .connector import pairings_for_port, start_connector
29
31
 
30
32
  HELP = f"""witbitz-code {__version__} — reach OpenCode on this computer from the Spaces Code section, end-to-end encrypted.
31
33
 
34
+ setup [--port <n>] start here: installs OpenCode if needed, pairs this computer, asks for your TrustedRouter
35
+ and Tinfoil keys (each checked before it is saved), then serves
32
36
  pair [--name <name>] show a QR code; scan it in Spaces (Settings → Back up & recovery → Add a device)
33
37
  serve [--port <n>] [--no-opencode]
34
38
  start OpenCode on 127.0.0.1 (unless it is already running) and the connector
35
39
  status list this computer's pairings
36
40
  rotate [--account <email>] replace the pairing secret(s) without a scan, then restart serve
37
41
  unpair [--account <email>] remove this computer from an account
42
+ trustedrouter-key store your TrustedRouter API key in OpenCode's credentials (same as opencode auth login)
43
+ tinfoil-key store your Tinfoil API key (images and files a confidential model cannot read)
38
44
 
39
45
  Nothing listens on the network: OpenCode stays on 127.0.0.1 and the connector dials out to wss://code-relay.witbitz.chat.
40
46
  """
@@ -104,13 +110,17 @@ async def _wait_for_stop(stop: _Stop, child: asyncio.subprocess.Process | None)
104
110
  return 0
105
111
 
106
112
 
107
- async def _start_opencode(exe: str, port: int, media: dict | None = None, tools: tuple[dict, dict] | None = None) -> asyncio.subprocess.Process:
113
+ async def _start_opencode(exe: str, port: int, media: dict | None = None, tools: tuple[dict, dict] | None = None,
114
+ extra: dict | None = None) -> asyncio.subprocess.Process:
108
115
  password = pp.read_env_password()
109
116
  # Subagents ask like their parent, and a refused call does not end the turn (policy.py) — merged by OpenCode over the
110
117
  # user's own config, which is never written.
111
118
  tools_env, tools_config = tools or ({}, {}) # codetools.py: the lsp tool, ast-grep on PATH, the hint naming it
112
119
  env = {**os.environ, **tools_env, **({"OPENCODE_SERVER_PASSWORD": password} if password else {}),
113
- "OPENCODE_CONFIG_CONTENT": policy.config_content(os.environ.get("OPENCODE_CONFIG_CONTENT"), policy.merge_config(media or {}, tools_config))}
120
+ "OPENCODE_CONFIG_CONTENT": policy.config_content(os.environ.get("OPENCODE_CONFIG_CONTENT"), policy.merge_config(policy.merge_config(media or {}, tools_config), extra))}
121
+ # The Tinfoil key is witbitz-code's (the confidential proxy reads it). Handed to OpenCode it would be in every shell the
122
+ # agent runs, and OpenCode would list Tinfoil as a plain provider whose attestation nothing checks (as the Node one).
123
+ env.pop("TINFOIL_API_KEY", None)
114
124
  # The resolved path, not "opencode": on Windows the npm install is opencode.cmd, which a bare name does not find.
115
125
  # Its own process group there, so stopping it can take the whole tree (cmd.exe → node) with it.
116
126
  extra = {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP} if sys.platform == "win32" else {}
@@ -136,7 +146,7 @@ async def serve(args: list[str]) -> int:
136
146
  port = int(port_n) if truthy(port_n) and port_n == port_n and float(port_n).is_integer() else 4096
137
147
  everything = pp.load_pairings(log=lambda _m: None)
138
148
  if not everything:
139
- _err("witbitz-code: this computer is not paired yet — run: witbitz-code pair")
149
+ _err("witbitz-code: this computer is not paired yet — run: witbitz-code setup")
140
150
  return 1
141
151
  # One OpenCode per port, one connector per OpenCode: two `serve`s (one per account) must never both answer a channel.
142
152
  pairings = pairings_for_port(everything, port)
@@ -161,22 +171,42 @@ async def serve(args: list[str]) -> int:
161
171
  _err(note)
162
172
  stop = _Stop()
163
173
  child = None
174
+ proxy = None
175
+ live: dict = {"connector": None}
164
176
  try:
177
+ # THE CONFIDENTIAL-MODEL PROXY (confidential.py): started BEFORE OpenCode, so OpenCode is pointed at it only when it
178
+ # runs — and only where TrustedRouter is connected, or the confidential models would be on the menu of a computer
179
+ # that cannot call them.
180
+ pport = models.proxy_port_for(port)
181
+ try:
182
+ proxy = await confidential.start_confidential_proxy(port=pport, log=_err, on_progress=lambda ev: live["connector"] and live["connector"].progress(ev))
183
+ except OSError as e:
184
+ _err(f"witbitz-code: could not start the confidential-model proxy on 127.0.0.1:{pport} ({e}) — confidential models are not offered")
185
+ route = models.proxy_config(proxy.port) if proxy and keys.has_trustedrouter() else None
165
186
  if not await is_listening(port) and "--no-opencode" not in args:
166
- exe = shutil.which("opencode")
187
+ exe = wizard.find_opencode()
167
188
  if not exe:
168
- _err("witbitz-code: OpenCode is not installed (or not on PATH). Install it, then run serve again:")
189
+ _err("witbitz-code: OpenCode is not installed (or not on PATH). Run witbitz-code setup, or install it and run serve again:")
169
190
  _err(" npm install -g opencode-ai or curl -fsSL https://opencode.ai/install | bash")
170
191
  return 1
171
192
  _err(f"witbitz-code: starting OpenCode on 127.0.0.1:{port}")
172
- child = await _start_opencode(exe, port, media, (tools_env, tools_config))
193
+ child = await _start_opencode(exe, port, media, (tools_env, tools_config), route)
173
194
  for _ in range(40):
174
195
  if stop.event.is_set() or child.returncode is not None or await is_listening(port):
175
196
  break
176
197
  await asyncio.sleep(0.25)
177
198
  if stop.event.is_set():
178
199
  return 0
200
+ if proxy and child is None and "--no-opencode" not in args:
201
+ _err("witbitz-code: OpenCode was already running, so its TrustedRouter calls do not go through the confidential-model proxy — restart it with witbitz-code serve")
202
+ elif route and child is not None:
203
+ _err(f"witbitz-code: confidential models are enforced (min_privacy + verified receipts){' and read images through Tinfoil' if keys.tinfoil_key() else ' — add a Tinfoil key (witbitz-code tinfoil-key) to read images with text-only ones'}")
204
+ elif proxy and not keys.has_trustedrouter():
205
+ _err("witbitz-code: TrustedRouter is not connected, so Code offers only OpenCode's own models — run witbitz-code trustedrouter-key")
179
206
  connector = await start_connector(pairings)
207
+ live["connector"] = connector
208
+ if proxy:
209
+ connector.set_confidential_sink(proxy.add_confidential) # the page hands down what the account offers today
180
210
  served = ", ".join(f'"{p.get("name")}" → {p.get("account") or "account"}' for p in pairings)
181
211
  _err(f"witbitz-code: serving {served} through the sealed relay (Ctrl-C to stop)")
182
212
  try:
@@ -186,11 +216,52 @@ async def serve(args: list[str]) -> int:
186
216
  finally:
187
217
  if child is not None:
188
218
  await _stop_opencode(child)
219
+ if proxy is not None:
220
+ await proxy.close()
189
221
  stop.remove()
190
222
 
191
223
 
224
+ async def set_key(label: str, check, save, after: str) -> int:
225
+ """`trustedrouter-key` / `tinfoil-key`: ask (it shows as *****), check with the provider, save. A rejected key is not saved."""
226
+ key = keys.read_line(f"{label} API key (it shows as *****): ", hidden=True)
227
+ if not key:
228
+ _err("witbitz-code: no key entered — nothing changed")
229
+ return 1
230
+ if not keys.valid_key_shape(key):
231
+ _err("witbitz-code: that does not look like an API key (no spaces, 8–512 characters) — nothing changed")
232
+ return 1
233
+ v = await check(key)
234
+ if v.get("ok") is False:
235
+ _err(f"witbitz-code: {v.get('why')} — nothing changed")
236
+ return 1
237
+ where = save(key)
238
+ unchecked = f" without checking it — {v.get('why')}" if v.get("ok") is None else ""
239
+ _err(f"witbitz-code: saved in {where} (only you can read it){unchecked}. {after}")
240
+ return 0
241
+
242
+
243
+ async def setup(args: list[str]) -> int:
244
+ port_n = to_number(_flag(args, "--port", "4096"))
245
+ port = int(port_n) if truthy(port_n) and port_n == port_n and float(port_n).is_integer() else 4096
246
+ d = wizard.Deps(
247
+ say=_err, ask=lambda q: keys.read_line(q), secret=lambda q: keys.read_line(q, hidden=True),
248
+ port=port, port_explicit="--port" in args,
249
+ pair=lambda p: pair_cmd.main(["--port", str(p)]),
250
+ is_listening=is_listening, serve_here=lambda p: serve(["--port", str(p)]),
251
+ )
252
+ r = await wizard.run_setup(d)
253
+ return r.get("served", 0) if not r.get("stopped") else 1
254
+
255
+
192
256
  async def _main(argv: list[str]) -> int:
193
257
  cmd, rest = (argv[0], argv[1:]) if argv else ("", [])
258
+ if cmd == "setup":
259
+ return await setup(rest)
260
+ if cmd == "trustedrouter-key":
261
+ return await set_key("TrustedRouter", keys.check_trustedrouter_key, keys.save_trustedrouter_key,
262
+ "Restart witbitz-code serve so OpenCode picks it up.")
263
+ if cmd == "tinfoil-key":
264
+ return await set_key("Tinfoil", keys.check_tinfoil_key, keys.save_tinfoil_key, "It is used from the next message — no restart needed.")
194
265
  if cmd == "pair":
195
266
  return await pair_cmd.main(rest)
196
267
  if cmd == "serve":