witbitz-code 1.2.1__tar.gz → 1.3.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.1/src/witbitz_code.egg-info → witbitz_code-1.3.1}/PKG-INFO +25 -17
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/README.md +25 -17
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/pyproject.toml +2 -2
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/account.py +5 -2
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/auto_runner.py +9 -1
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/cli.py +120 -48
- witbitz_code-1.3.1/src/witbitz_code/codekeys.py +285 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/confidential.py +27 -6
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/connector.py +30 -5
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/models.py +8 -0
- witbitz_code-1.3.1/src/witbitz_code/offered_models.json +32 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/pair.py +76 -6
- witbitz_code-1.3.1/src/witbitz_code/payers.py +111 -0
- witbitz_code-1.3.1/src/witbitz_code/uninstall.py +447 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/wizard.py +27 -70
- {witbitz_code-1.2.1 → witbitz_code-1.3.1/src/witbitz_code.egg-info}/PKG-INFO +25 -17
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code.egg-info/SOURCES.txt +7 -1
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_cli.py +23 -1
- witbitz_code-1.3.1/tests/test_codekeys.py +235 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_connector_e2e.py +30 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_pair_flows.py +61 -9
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_setup.py +33 -39
- witbitz_code-1.3.1/tests/test_uninstall.py +164 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/LICENSE +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/setup.cfg +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_asks.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_attachments.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_auto.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_codetools.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_confidential.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_folders.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_outputs.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_plugins.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_policy.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_registry.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.1}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.2.1 → witbitz_code-1.3.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.
|
|
3
|
+
Version: 1.3.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
|
|
@@ -48,28 +48,36 @@ verifier — attestation, Sigstore and TLS pinning — for the confidential mode
|
|
|
48
48
|
witbitz-code setup
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
`setup` walks
|
|
52
|
-
this computer (scan the QR code in Spaces → Settings → Back up & recovery → Add a device),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`witbitz-code serve` is enough.
|
|
51
|
+
`setup` walks four 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), which gives the computer **its
|
|
53
|
+
own keys from your Spaces account**, then starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C
|
|
54
|
+
stops it); next time, `witbitz-code serve` is enough.
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
**No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
|
|
57
|
+
providers (today up to $50 of models and $5 of tools a month per computer). They are sent sealed to this computer, kept in
|
|
58
|
+
`~/.witbitz/code/account-keys.json` (0600) and never given to OpenCode: the local proxy puts them on each call. Model calls
|
|
59
|
+
still go from this computer straight to TrustedRouter and Tinfoil. Coding is by invitation for now — an account that is not
|
|
60
|
+
invited gets no keys, and the pairing stops with a sentence saying so. A computer paired before 1.3 has no account keys:
|
|
61
|
+
pair it once more (`witbitz-code pair`).
|
|
62
|
+
|
|
63
|
+
**Several accounts on one computer:** each pays for its own prompts. OpenCode names the conversation on every call it
|
|
64
|
+
makes; the connector knows which account sent each prompt, and the proxy bills the call to that account. A prompt typed in
|
|
65
|
+
OpenCode's own window on this computer is refused ("coding runs from the Spaces app"), and while one account's reply is
|
|
66
|
+
being written another account cannot add to it. This splits the bill fairly between people who share a computer; it is not
|
|
67
|
+
a wall between them (they already share its files and shell).
|
|
68
|
+
|
|
69
|
+
To start over: `witbitz-code uninstall`, then `pipx install witbitz-code` and `witbitz-code setup`.
|
|
61
70
|
|
|
62
71
|
The same thing one step at a time: install OpenCode (`curl -fsSL https://opencode.ai/install | bash`), then
|
|
63
|
-
`witbitz-code pair
|
|
72
|
+
`witbitz-code pair` and `witbitz-code serve`.
|
|
64
73
|
|
|
65
74
|
| command | what it does |
|
|
66
75
|
|---|---|
|
|
67
76
|
| `setup [--port <n>]` | The walk-through above. With `--port`, a pairing made for another port moves to it (no new scan). |
|
|
68
|
-
| `
|
|
69
|
-
| `tinfoil-key` | Ask for a Tinfoil key, check it, and store it. Used from the next message. |
|
|
77
|
+
| `uninstall [--yes] [--remove-keys] [--remove-notes] [--remove-sessions] [--remove-opencode]` | Remove witbitz-code from this computer: its coding keys go back to each account, each account lets go of it, `~/.witbitz/code` and what `serve` added to OpenCode are deleted, and the pipx install removes itself. Asks first (Enter keeps) before your saved keys, project notes, OpenCode's sessions and OpenCode itself; never deletes sessions an OpenCode still has open. `--yes` skips the questions and keeps all four unless their flag is given. |
|
|
70
78
|
| `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). |
|
|
71
|
-
| `serve [--port <n>] [--no-opencode]` | Start OpenCode on `127.0.0.1:<n>` (default 4096)
|
|
72
|
-
| `status` | List this computer's pairings
|
|
79
|
+
| `serve [--port <n>] [--no-opencode]` | Start OpenCode on `127.0.0.1:<n>` (default 4096) with your account's keys, then connect the pairings whose OpenCode is on that port. An OpenCode already running there, started some other way, is not used (it cannot have the keys) — close it first. One OpenCode per port, one `serve` per OpenCode. |
|
|
80
|
+
| `status` | List this computer's pairings — name, account, OpenCode address, relay — and each one's coding keys: live, this month's model spend and the caps. Secrets are never printed. |
|
|
73
81
|
| `rotate [--account <email>]` | Replace the pairing secret(s) without scanning again, then restart `serve`. Devices pick up the new secret on their next sync. |
|
|
74
82
|
| `unpair [--account <email>]` | Remove this computer from an account. Every device drops it on its next sync. |
|
|
75
83
|
| `version`, `--help` | |
|
|
@@ -117,8 +125,8 @@ all the accounts are yours. If a second account belongs to **another person**, r
|
|
|
117
125
|
receipt** to verify for these exact request and response bytes (an attested key, a TEE-verified upstream under a known
|
|
118
126
|
policy). A receipt that does not verify ends the step with an error, so no tool from that answer runs;
|
|
119
127
|
- an image or a document a text-only confidential model cannot read is read **as text inside Tinfoil's attested
|
|
120
|
-
enclave** with
|
|
121
|
-
- every other model passes through
|
|
128
|
+
enclave** with the account's Tinfoil key — or refused, never sent anywhere less;
|
|
129
|
+
- every other model Witbitz offers passes through with the account's key; a model Witbitz does not offer is refused.
|
|
122
130
|
|
|
123
131
|
**Auto mode** (Manual · Accept edits · Plan · **Auto** in the Code composer's mode chip, per session) lets this computer answer OpenCode's permission
|
|
124
132
|
prompts while you are away:
|
|
@@ -23,28 +23,36 @@ verifier — attestation, Sigstore and TLS pinning — for the confidential mode
|
|
|
23
23
|
witbitz-code setup
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
`setup` walks
|
|
27
|
-
this computer (scan the QR code in Spaces → Settings → Back up & recovery → Add a device),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
`setup` walks four 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), which gives the computer **its
|
|
28
|
+
own keys from your Spaces account**, then starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C
|
|
29
|
+
stops it); next time, `witbitz-code serve` is enough.
|
|
30
|
+
|
|
31
|
+
**No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
|
|
32
|
+
providers (today up to $50 of models and $5 of tools a month per computer). They are sent sealed to this computer, kept in
|
|
33
|
+
`~/.witbitz/code/account-keys.json` (0600) and never given to OpenCode: the local proxy puts them on each call. Model calls
|
|
34
|
+
still go from this computer straight to TrustedRouter and Tinfoil. Coding is by invitation for now — an account that is not
|
|
35
|
+
invited gets no keys, and the pairing stops with a sentence saying so. A computer paired before 1.3 has no account keys:
|
|
36
|
+
pair it once more (`witbitz-code pair`).
|
|
37
|
+
|
|
38
|
+
**Several accounts on one computer:** each pays for its own prompts. OpenCode names the conversation on every call it
|
|
39
|
+
makes; the connector knows which account sent each prompt, and the proxy bills the call to that account. A prompt typed in
|
|
40
|
+
OpenCode's own window on this computer is refused ("coding runs from the Spaces app"), and while one account's reply is
|
|
41
|
+
being written another account cannot add to it. This splits the bill fairly between people who share a computer; it is not
|
|
42
|
+
a wall between them (they already share its files and shell).
|
|
43
|
+
|
|
44
|
+
To start over: `witbitz-code uninstall`, then `pipx install witbitz-code` and `witbitz-code setup`.
|
|
36
45
|
|
|
37
46
|
The same thing one step at a time: install OpenCode (`curl -fsSL https://opencode.ai/install | bash`), then
|
|
38
|
-
`witbitz-code pair
|
|
47
|
+
`witbitz-code pair` and `witbitz-code serve`.
|
|
39
48
|
|
|
40
49
|
| command | what it does |
|
|
41
50
|
|---|---|
|
|
42
51
|
| `setup [--port <n>]` | The walk-through above. With `--port`, a pairing made for another port moves to it (no new scan). |
|
|
43
|
-
| `
|
|
44
|
-
| `tinfoil-key` | Ask for a Tinfoil key, check it, and store it. Used from the next message. |
|
|
52
|
+
| `uninstall [--yes] [--remove-keys] [--remove-notes] [--remove-sessions] [--remove-opencode]` | Remove witbitz-code from this computer: its coding keys go back to each account, each account lets go of it, `~/.witbitz/code` and what `serve` added to OpenCode are deleted, and the pipx install removes itself. Asks first (Enter keeps) before your saved keys, project notes, OpenCode's sessions and OpenCode itself; never deletes sessions an OpenCode still has open. `--yes` skips the questions and keeps all four unless their flag is given. |
|
|
45
53
|
| `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). |
|
|
46
|
-
| `serve [--port <n>] [--no-opencode]` | Start OpenCode on `127.0.0.1:<n>` (default 4096)
|
|
47
|
-
| `status` | List this computer's pairings
|
|
54
|
+
| `serve [--port <n>] [--no-opencode]` | Start OpenCode on `127.0.0.1:<n>` (default 4096) with your account's keys, then connect the pairings whose OpenCode is on that port. An OpenCode already running there, started some other way, is not used (it cannot have the keys) — close it first. One OpenCode per port, one `serve` per OpenCode. |
|
|
55
|
+
| `status` | List this computer's pairings — name, account, OpenCode address, relay — and each one's coding keys: live, this month's model spend and the caps. Secrets are never printed. |
|
|
48
56
|
| `rotate [--account <email>]` | Replace the pairing secret(s) without scanning again, then restart `serve`. Devices pick up the new secret on their next sync. |
|
|
49
57
|
| `unpair [--account <email>]` | Remove this computer from an account. Every device drops it on its next sync. |
|
|
50
58
|
| `version`, `--help` | |
|
|
@@ -92,8 +100,8 @@ all the accounts are yours. If a second account belongs to **another person**, r
|
|
|
92
100
|
receipt** to verify for these exact request and response bytes (an attested key, a TEE-verified upstream under a known
|
|
93
101
|
policy). A receipt that does not verify ends the step with an error, so no tool from that answer runs;
|
|
94
102
|
- an image or a document a text-only confidential model cannot read is read **as text inside Tinfoil's attested
|
|
95
|
-
enclave** with
|
|
96
|
-
- every other model passes through
|
|
103
|
+
enclave** with the account's Tinfoil key — or refused, never sent anywhere less;
|
|
104
|
+
- every other model Witbitz offers passes through with the account's key; a model Witbitz does not offer is refused.
|
|
97
105
|
|
|
98
106
|
**Auto mode** (Manual · Accept edits · Plan · **Auto** in the Code composer's mode chip, per session) lets this computer answer OpenCode's permission
|
|
99
107
|
prompts while you are away:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.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"
|
|
@@ -43,7 +43,7 @@ where = ["src"]
|
|
|
43
43
|
|
|
44
44
|
# Our OpenCode plugins, installed by `serve` (plugins.py) — copies of tools/opencode-plugins, kept current by embed.mjs
|
|
45
45
|
[tool.setuptools.package-data]
|
|
46
|
-
witbitz_code = ["opencode_plugins/*", "confidential_models.json"]
|
|
46
|
+
witbitz_code = ["opencode_plugins/*", "confidential_models.json", "offered_models.json"]
|
|
47
47
|
|
|
48
48
|
[tool.pytest.ini_options]
|
|
49
49
|
testpaths = ["tests"]
|
|
@@ -73,13 +73,16 @@ async def publish_computer(*, call: ApiCall, idx: dict, computer_id: str, name:
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
async def unpublish_computer(*, call: ApiCall, idx: dict, computer_id: str, now: float | None = None) -> dict:
|
|
76
|
-
"""Tombstone this computer in the account's registry (its secret leaves the account).
|
|
76
|
+
"""Tombstone this computer in the account's registry (its secret leaves the account). Written even when this doc does not
|
|
77
|
+
list it: an app that seals the list keeps it in 'xcomputers', which this tool cannot read (it holds no account key), and
|
|
78
|
+
an older app emptied this doc — so "not listed" here says nothing about the list the apps show (tools/opencode-pair.mjs
|
|
79
|
+
unpublishComputer). Only a tombstone already here is left alone."""
|
|
77
80
|
now = _now_ms() if now is None else now
|
|
78
81
|
rd = await read_doc(call, idx, COMPUTERS_DOC)
|
|
79
82
|
if not rd["ok"]:
|
|
80
83
|
return rd
|
|
81
84
|
cur = norm_registry(rd["state"])["computers"].get(computer_id)
|
|
82
|
-
if
|
|
85
|
+
if cur and cur.get("removed"):
|
|
83
86
|
return {"ok": True, "noop": True}
|
|
84
87
|
nxt = without_computer(rd["state"], computer_id, now)
|
|
85
88
|
wr = await call(_write_body(idx, COMPUTERS_DOC, wrap_index_state({**nxt, "at": now}), "computer unpaired"))
|
|
@@ -110,8 +110,12 @@ class AutoRunner:
|
|
|
110
110
|
log_path: Path | None, poll_ms: float = 1000, review_timeout_ms: float = 30_000, home: str | None = None,
|
|
111
111
|
on_verdict: Callable[[dict], Any] = lambda v: None, log: Callable[[str], Any] = lambda m: None,
|
|
112
112
|
now: Callable[[], int] = _now_ms, scratch_check: Callable[[list[str]], bool] = scratch_on_disk,
|
|
113
|
-
list_asks: Callable[[str], Awaitable[list | None]] | None = None
|
|
113
|
+
list_asks: Callable[[str], Awaitable[list | None]] | None = None,
|
|
114
|
+
on_review: Callable[[str, bool], Any] | None = None) -> None:
|
|
114
115
|
self._root = re.sub(r"/+\Z", "", base or "")
|
|
116
|
+
# The review session's model calls are billed to the account that turned Auto on (payers.py): told when the
|
|
117
|
+
# review opens and closes, since nothing from the Spaces app ever posts into it.
|
|
118
|
+
self._on_review = on_review
|
|
115
119
|
self._auth = auth
|
|
116
120
|
self._client = client
|
|
117
121
|
self._state_path = Path(state_path) if state_path else None
|
|
@@ -303,6 +307,8 @@ class AutoRunner:
|
|
|
303
307
|
return None, label, "could not open a review session"
|
|
304
308
|
rid = _js.js_string(rid)
|
|
305
309
|
timed_out = False
|
|
310
|
+
if self._on_review:
|
|
311
|
+
self._on_review(rid, True)
|
|
306
312
|
try:
|
|
307
313
|
body = {"model": model, "system": prompt["system"], "tools": {"*": False}, "parts": [{"type": "text", "text": prompt["text"]}]}
|
|
308
314
|
try:
|
|
@@ -321,6 +327,8 @@ class AutoRunner:
|
|
|
321
327
|
secs = math.floor(self._review_timeout_ms / 1000 + 0.5) or 1 # Math.round(ms / 1000) || 1
|
|
322
328
|
return None, label, f"the reviewer did not answer within {secs} s" if timed_out else "the review failed"
|
|
323
329
|
finally:
|
|
330
|
+
if self._on_review:
|
|
331
|
+
self._on_review(rid, False)
|
|
324
332
|
if timed_out:
|
|
325
333
|
try:
|
|
326
334
|
await self._call("POST", f"/session/{_uri_component(rid)}/abort", directory)
|
|
@@ -1,14 +1,13 @@
|
|
|
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
|
|
4
|
+
witbitz-code setup [--port 4096] the walk-through: OpenCode, pairing (which brings this computer's keys), then serve
|
|
5
5
|
witbitz-code pair [--name desk] scan the QR with the Spaces app → this computer joins that account
|
|
6
6
|
witbitz-code serve [--port 4096] start OpenCode (if it is not running) and the connector
|
|
7
|
-
witbitz-code status what this computer is paired with
|
|
7
|
+
witbitz-code status what this computer is paired with, and its coding keys
|
|
8
8
|
witbitz-code rotate new secrets (no scan) — restart serve afterwards
|
|
9
9
|
witbitz-code unpair [--account a@b] remove this computer from an account
|
|
10
|
-
witbitz-code
|
|
11
|
-
witbitz-code tinfoil-key store the Tinfoil key (confidential reading of images and files)
|
|
10
|
+
witbitz-code uninstall remove witbitz-code from this computer (asks before your keys, notes and sessions)
|
|
12
11
|
"""
|
|
13
12
|
|
|
14
13
|
from __future__ import annotations
|
|
@@ -18,6 +17,7 @@ import os
|
|
|
18
17
|
import signal
|
|
19
18
|
import subprocess
|
|
20
19
|
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
21
|
from typing import Any
|
|
22
22
|
|
|
23
23
|
import httpx
|
|
@@ -25,24 +25,26 @@ import httpx
|
|
|
25
25
|
from . import __version__
|
|
26
26
|
from . import pair as pair_cmd
|
|
27
27
|
from . import pairings as pp
|
|
28
|
-
from . import codetools, confidential, keys, models, plugins, policy, wizard
|
|
28
|
+
from . import codekeys, codetools, confidential, keys, models, plugins, policy, uninstall as uninstall_mod, wizard
|
|
29
29
|
from ._js import to_number, truthy
|
|
30
30
|
from .connector import pairings_for_port, start_connector
|
|
31
|
+
from .payers import SessionPayers
|
|
31
32
|
|
|
32
33
|
HELP = f"""witbitz-code {__version__} — reach OpenCode on this computer from the Spaces Code section, end-to-end encrypted.
|
|
33
34
|
|
|
34
|
-
setup [--port <n>] start here: installs OpenCode if needed, pairs this computer
|
|
35
|
-
|
|
35
|
+
setup [--port <n>] start here: installs OpenCode if needed, pairs this computer — which gives it its own
|
|
36
|
+
keys from your Spaces account — then serves
|
|
36
37
|
pair [--name <name>] show a QR code; scan it in Spaces (Settings → Back up & recovery → Add a device)
|
|
37
38
|
serve [--port <n>] [--no-opencode]
|
|
38
|
-
start OpenCode on 127.0.0.1
|
|
39
|
-
status list this computer's pairings
|
|
39
|
+
start OpenCode on 127.0.0.1 with your account's keys, and the connector
|
|
40
|
+
status list this computer's pairings and their coding keys
|
|
40
41
|
rotate [--account <email>] replace the pairing secret(s) without a scan, then restart serve
|
|
41
42
|
unpair [--account <email>] remove this computer from an account
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
uninstall [--yes] [--remove-keys] [--remove-notes] [--remove-sessions] [--remove-opencode]
|
|
44
|
+
remove witbitz-code from this computer; asks before your keys, notes, sessions and OpenCode
|
|
44
45
|
|
|
45
46
|
Nothing listens on the network: OpenCode stays on 127.0.0.1 and the connector dials out to wss://code-relay.witbitz.chat.
|
|
47
|
+
Model calls go from this computer straight to TrustedRouter and Tinfoil, each billed to the account whose prompt it is.
|
|
46
48
|
"""
|
|
47
49
|
|
|
48
50
|
|
|
@@ -110,13 +112,18 @@ async def _wait_for_stop(stop: _Stop, child: asyncio.subprocess.Process | None)
|
|
|
110
112
|
return 0
|
|
111
113
|
|
|
112
114
|
|
|
115
|
+
# OpenCode needs SOME TrustedRouter key to offer the provider; the proxy replaces it on every call with the key of the
|
|
116
|
+
# account whose prompt it is (docs/code-account-keys.md), so OpenCode never holds a real one.
|
|
117
|
+
OPENCODE_TR_PLACEHOLDER = "witbitz-account-keys"
|
|
118
|
+
|
|
119
|
+
|
|
113
120
|
async def _start_opencode(exe: str, port: int, media: dict | None = None, tools: tuple[dict, dict] | None = None,
|
|
114
121
|
extra: dict | None = None) -> asyncio.subprocess.Process:
|
|
115
122
|
password = pp.read_env_password()
|
|
116
123
|
# Subagents ask like their parent, and a refused call does not end the turn (policy.py) — merged by OpenCode over the
|
|
117
124
|
# user's own config, which is never written.
|
|
118
125
|
tools_env, tools_config = tools or ({}, {}) # codetools.py: the lsp tool, ast-grep on PATH, the hint naming it
|
|
119
|
-
env = {**os.environ, **tools_env, **({"OPENCODE_SERVER_PASSWORD": password} if password else {}),
|
|
126
|
+
env = {**os.environ, **tools_env, **({"OPENCODE_SERVER_PASSWORD": password} if password else {}), "TRUSTEDROUTER_API_KEY": OPENCODE_TR_PLACEHOLDER,
|
|
120
127
|
"OPENCODE_CONFIG_CONTENT": policy.config_content(os.environ.get("OPENCODE_CONFIG_CONTENT"), policy.merge_config(policy.merge_config(media or {}, tools_config), extra))}
|
|
121
128
|
# The Tinfoil key is witbitz-code's (the confidential proxy reads it). Handed to OpenCode it would be in every shell the
|
|
122
129
|
# agent runs, and OpenCode would list Tinfoil as a plain provider whose attestation nothing checks (as the Node one).
|
|
@@ -169,21 +176,43 @@ async def serve(args: list[str]) -> int:
|
|
|
169
176
|
tools_env, tools_config, note = await asyncio.to_thread(codetools.code_tools, plugins.opencode_config_dir())
|
|
170
177
|
if note:
|
|
171
178
|
_err(note)
|
|
179
|
+
# THIS COMPUTER'S CODING KEYS (codekeys.py; docs/code-account-keys.md): the only model keys witbitz-code uses. A pairing
|
|
180
|
+
# made before them is not served — every call of its account would be refused.
|
|
181
|
+
unkeyed = [p for p in pairings if not codekeys.for_computer(p.get("computerId") or "")]
|
|
182
|
+
for p in unkeyed:
|
|
183
|
+
_err(f'witbitz-code: "{p.get("name")}" → {p.get("account") or "account"} has no coding keys — this computer must be paired again to use your Spaces account: witbitz-code pair')
|
|
184
|
+
pairings = [p for p in pairings if p not in unkeyed]
|
|
185
|
+
if not pairings:
|
|
186
|
+
return 1
|
|
187
|
+
live_keys = codekeys.LiveKeys([p["computerId"] for p in pairings])
|
|
188
|
+
payers = SessionPayers()
|
|
189
|
+
offered = models.offered()
|
|
190
|
+
|
|
191
|
+
def credential_for(req: Any) -> dict | None:
|
|
192
|
+
# Billed to the account whose prompt started the turn (payers.py); a call nobody here started is refused.
|
|
193
|
+
return live_keys.credential(payers.payer(req.header("x-session-id")[:100], req.header("x-parent-session-id")[:100]))
|
|
194
|
+
|
|
172
195
|
stop = _Stop()
|
|
173
196
|
child = None
|
|
174
197
|
proxy = None
|
|
175
198
|
live: dict = {"connector": None}
|
|
199
|
+
keys_http = httpx.AsyncClient(timeout=30)
|
|
176
200
|
try:
|
|
177
|
-
# THE
|
|
178
|
-
#
|
|
179
|
-
# that cannot call them.
|
|
201
|
+
# THE PROXY (confidential.py): started BEFORE OpenCode, which is always pointed at it — it puts each account's key on
|
|
202
|
+
# the calls of that account's prompts, enforces the confidential models and refuses a model Witbitz does not offer.
|
|
180
203
|
pport = models.proxy_port_for(port)
|
|
181
204
|
try:
|
|
182
|
-
proxy = await confidential.start_confidential_proxy(port=pport, log=_err, on_progress=lambda ev: live["connector"] and live["connector"].progress(ev)
|
|
205
|
+
proxy = await confidential.start_confidential_proxy(port=pport, log=_err, on_progress=lambda ev: live["connector"] and live["connector"].progress(ev),
|
|
206
|
+
credential_for=credential_for, offered=lambda m: m in offered)
|
|
183
207
|
except OSError as e:
|
|
184
|
-
_err(f"witbitz-code: could not start the
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
_err(f"witbitz-code: could not start the model proxy on 127.0.0.1:{pport} ({e}) — free that port and run serve again")
|
|
209
|
+
return 1
|
|
210
|
+
route = models.proxy_config(proxy.port)
|
|
211
|
+
if await is_listening(port) and "--no-opencode" not in args:
|
|
212
|
+
_err(f"witbitz-code: OpenCode is already running on 127.0.0.1:{port}, started some other way — it cannot use your account's keys.")
|
|
213
|
+
_err(" Close it, then run witbitz-code serve again (it starts OpenCode itself).")
|
|
214
|
+
return 1
|
|
215
|
+
if "--no-opencode" not in args:
|
|
187
216
|
exe = wizard.find_opencode()
|
|
188
217
|
if not exe:
|
|
189
218
|
_err("witbitz-code: OpenCode is not installed (or not on PATH). Run witbitz-code setup, or install it and run serve again:")
|
|
@@ -197,13 +226,10 @@ async def serve(args: list[str]) -> int:
|
|
|
197
226
|
await asyncio.sleep(0.25)
|
|
198
227
|
if stop.event.is_set():
|
|
199
228
|
return 0
|
|
200
|
-
if
|
|
201
|
-
_err("witbitz-code:
|
|
202
|
-
|
|
203
|
-
|
|
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")
|
|
206
|
-
connector = await start_connector(pairings)
|
|
229
|
+
if child is not None:
|
|
230
|
+
_err("witbitz-code: model calls are paid by each account's own coding keys; confidential models are enforced (min_privacy + verified receipts)")
|
|
231
|
+
connector = await start_connector(pairings, payers=payers)
|
|
232
|
+
live["status"] = asyncio.ensure_future(_check_keys_every(live_keys, keys_http))
|
|
207
233
|
live["connector"] = connector
|
|
208
234
|
if proxy:
|
|
209
235
|
connector.set_confidential_sink(proxy.add_confidential) # the page hands down what the account offers today
|
|
@@ -212,32 +238,26 @@ async def serve(args: list[str]) -> int:
|
|
|
212
238
|
try:
|
|
213
239
|
return await _wait_for_stop(stop, child)
|
|
214
240
|
finally:
|
|
241
|
+
live["status"].cancel()
|
|
215
242
|
await connector.aclose()
|
|
216
243
|
finally:
|
|
217
244
|
if child is not None:
|
|
218
245
|
await _stop_opencode(child)
|
|
219
246
|
if proxy is not None:
|
|
220
247
|
await proxy.close()
|
|
248
|
+
await keys_http.aclose()
|
|
221
249
|
stop.remove()
|
|
222
250
|
|
|
223
251
|
|
|
224
|
-
async def
|
|
225
|
-
"""
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
252
|
+
async def _check_keys_every(live_keys: codekeys.LiveKeys, http: httpx.AsyncClient) -> None:
|
|
253
|
+
"""At start and every few hours: are the keys live (revoked → refused, paused → refused until it is back on)? In the
|
|
254
|
+
background — the keys file is used meanwhile, and a slow server never holds up serving."""
|
|
255
|
+
while True:
|
|
256
|
+
try:
|
|
257
|
+
await live_keys.check(http, _err)
|
|
258
|
+
except Exception as e: # noqa: BLE001 — a reading that failed is asked again next time
|
|
259
|
+
_err(f"witbitz-code: could not check the coding keys ({str(e)[:80]})")
|
|
260
|
+
await asyncio.sleep(codekeys.STATUS_EVERY_S)
|
|
241
261
|
|
|
242
262
|
|
|
243
263
|
async def setup(args: list[str]) -> int:
|
|
@@ -253,15 +273,67 @@ async def setup(args: list[str]) -> int:
|
|
|
253
273
|
return r.get("served", 0) if not r.get("stopped") else 1
|
|
254
274
|
|
|
255
275
|
|
|
276
|
+
async def uninstall(args: list[str]) -> int:
|
|
277
|
+
yes = "--yes" in args
|
|
278
|
+
if not yes and not sys.stdin.isatty():
|
|
279
|
+
_err("witbitz-code: uninstall asks before it removes anything — run it in a terminal, or pass --yes")
|
|
280
|
+
return 2
|
|
281
|
+
from . import account as acct, net # noqa: PLC0415
|
|
282
|
+
|
|
283
|
+
client = net.new_client()
|
|
284
|
+
call = net.make_api_call(client)
|
|
285
|
+
closed = {"done": False}
|
|
286
|
+
|
|
287
|
+
async def close_client() -> None:
|
|
288
|
+
if not closed["done"]:
|
|
289
|
+
closed["done"] = True
|
|
290
|
+
await client.aclose()
|
|
291
|
+
|
|
292
|
+
async def remove_package(info: dict) -> dict:
|
|
293
|
+
await close_client() # before pipx deletes the files it would load
|
|
294
|
+
return uninstall_mod.remove_package(info)
|
|
295
|
+
|
|
296
|
+
async def unpair(p: dict) -> dict:
|
|
297
|
+
# This computer's coding keys go back first (codekeys.py) — the signing key that can return them is in the folder
|
|
298
|
+
# uninstall removes.
|
|
299
|
+
entry = codekeys.for_computer(p["computerId"])
|
|
300
|
+
if entry and not await codekeys.revoke(client, entry):
|
|
301
|
+
return {"ok": False, "why": "could not give this computer's coding keys back"}
|
|
302
|
+
if entry:
|
|
303
|
+
codekeys.forget(p["computerId"])
|
|
304
|
+
r = await acct.unpublish_computer(call=call, idx=p["idx"], computer_id=p["computerId"])
|
|
305
|
+
if r.get("ok"):
|
|
306
|
+
pp.write_pairings(pp.remove_pairings(pp.read_pairings(), p.get("account") or "")["doc"])
|
|
307
|
+
return r
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
r = await uninstall_mod.run_uninstall(uninstall_mod.Deps(
|
|
311
|
+
say=_err, ask=lambda q: keys.read_line(q), yes=yes,
|
|
312
|
+
remove_keys="--remove-keys" in args, remove_notes="--remove-notes" in args,
|
|
313
|
+
remove_sessions="--remove-sessions" in args, remove_opencode="--remove-opencode" in args,
|
|
314
|
+
unpair=unpair, has_keys=uninstall_mod.saved_keys, delete_keys=uninstall_mod.delete_saved_keys,
|
|
315
|
+
notes=uninstall_mod.notes_info, owned_files=uninstall_mod.owned_files, sessions=uninstall_mod.sessions_info,
|
|
316
|
+
delete_sessions=uninstall_mod.delete_sessions, remove_package=remove_package))
|
|
317
|
+
finally:
|
|
318
|
+
await close_client()
|
|
319
|
+
if r.get("done"):
|
|
320
|
+
for d in (pp.pairings_path().parent.parent, Path.home() / ".cache" / "witbitz"): # emptied by now, unless the person keeps something there
|
|
321
|
+
try:
|
|
322
|
+
d.rmdir()
|
|
323
|
+
except OSError:
|
|
324
|
+
pass
|
|
325
|
+
return 0 if r.get("done") else 1
|
|
326
|
+
|
|
327
|
+
|
|
256
328
|
async def _main(argv: list[str]) -> int:
|
|
257
329
|
cmd, rest = (argv[0], argv[1:]) if argv else ("", [])
|
|
330
|
+
if cmd == "uninstall":
|
|
331
|
+
return await uninstall(rest)
|
|
258
332
|
if cmd == "setup":
|
|
259
333
|
return await setup(rest)
|
|
260
|
-
if cmd
|
|
261
|
-
|
|
262
|
-
|
|
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.")
|
|
334
|
+
if cmd in ("trustedrouter-key", "tinfoil-key"):
|
|
335
|
+
_err("witbitz-code: keys now come from your Spaces account — this computer gets its own when you pair it: witbitz-code pair")
|
|
336
|
+
return 2
|
|
265
337
|
if cmd == "pair":
|
|
266
338
|
return await pair_cmd.main(rest)
|
|
267
339
|
if cmd == "serve":
|