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.
- {witbitz_code-1.2.0/src/witbitz_code.egg-info → witbitz_code-1.2.1}/PKG-INFO +33 -7
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/README.md +31 -6
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/pyproject.toml +5 -2
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/cli.py +79 -8
- witbitz_code-1.2.1/src/witbitz_code/confidential.py +679 -0
- witbitz_code-1.2.1/src/witbitz_code/confidential_models.json +69 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/connector.py +20 -0
- witbitz_code-1.2.1/src/witbitz_code/gateway.py +147 -0
- witbitz_code-1.2.1/src/witbitz_code/keys.py +212 -0
- witbitz_code-1.2.1/src/witbitz_code/models.py +73 -0
- witbitz_code-1.2.1/src/witbitz_code/receipt.py +153 -0
- witbitz_code-1.2.1/src/witbitz_code/tinfoil_read.py +122 -0
- witbitz_code-1.2.1/src/witbitz_code/wizard.py +202 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1/src/witbitz_code.egg-info}/PKG-INFO +33 -7
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code.egg-info/SOURCES.txt +10 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code.egg-info/requires.txt +1 -0
- witbitz_code-1.2.1/tests/test_confidential.py +355 -0
- witbitz_code-1.2.1/tests/test_setup.py +175 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/LICENSE +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/setup.cfg +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_asks.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_attachments.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_auto.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_cli.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_codetools.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_folders.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_outputs.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_plugins.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_policy.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_registry.py +0 -0
- {witbitz_code-1.2.0 → witbitz_code-1.2.1}/tests/test_relay_codec.py +0 -0
- {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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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"]
|
|
@@ -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
|
|
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
|
|
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 =
|
|
187
|
+
exe = wizard.find_opencode()
|
|
167
188
|
if not exe:
|
|
168
|
-
_err("witbitz-code: OpenCode is not installed (or not on PATH).
|
|
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":
|