witbitz-code 1.9.1__tar.gz → 1.9.3__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.9.1/src/witbitz_code.egg-info → witbitz_code-1.9.3}/PKG-INFO +3 -3
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/README.md +2 -2
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/pyproject.toml +1 -1
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/confidential.py +80 -2
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/confidential_models.json +0 -11
- {witbitz_code-1.9.1 → witbitz_code-1.9.3/src/witbitz_code.egg-info}/PKG-INFO +3 -3
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_confidential.py +69 -7
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/LICENSE +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/setup.cfg +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/browse.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/browsetree.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/cli.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/connector.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/dirs.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/icons/witbitz-code.ico +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/images.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/mkdir.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-publisher.mjs +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browser.js +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/remind.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/seccheck.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/selfupdate.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/tools_catalog.json +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/toolsetup.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/ui.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/ui_static/app.css +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/ui_static/app.js +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/ui_static/index.html +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/uitools.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/uninstall.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/winagent.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/winbg.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/wizard.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/wslstart.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_asks.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_attachments.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_auto.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_browse.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_browsetree.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_cli.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_codetools.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_dirs.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_folders.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_images.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_outputs.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_plugins.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_policy.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_ports.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_ports_win.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_registry.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_remind.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_seccheck.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_selfupdate.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_setup.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_toolsetup.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_ui.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_uitools.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_uninstall.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_winagent.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_winbg.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_windows_files.py +0 -0
- {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_wslstart.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: witbitz-code
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.3
|
|
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
|
|
@@ -67,8 +67,8 @@ opened (a one-time key traded for a private cookie; no other website can drive i
|
|
|
67
67
|
**No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
|
|
68
68
|
providers (today up to $50 of models and $5 of tools a month per computer). They are sent sealed to this computer, kept in
|
|
69
69
|
`~/.witbitz/code/account-keys.json` (0600) and never given to OpenCode: the local proxy puts them on each call. Model calls
|
|
70
|
-
still go from this computer straight to TrustedRouter and Tinfoil.
|
|
71
|
-
|
|
70
|
+
still go from this computer straight to TrustedRouter and Tinfoil. An account that is not invited gets the free allowance —
|
|
71
|
+
one computer, $1 of models and $0.25 of tools a month; when it is used up, Code says so and when it comes back. A computer paired before 1.3 has no account keys:
|
|
72
72
|
pair it once more (`witbitz-code pair`).
|
|
73
73
|
|
|
74
74
|
**Several accounts on one computer:** each pays for its own prompts. OpenCode names the conversation on every call it
|
|
@@ -40,8 +40,8 @@ opened (a one-time key traded for a private cookie; no other website can drive i
|
|
|
40
40
|
**No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
|
|
41
41
|
providers (today up to $50 of models and $5 of tools a month per computer). They are sent sealed to this computer, kept in
|
|
42
42
|
`~/.witbitz/code/account-keys.json` (0600) and never given to OpenCode: the local proxy puts them on each call. Model calls
|
|
43
|
-
still go from this computer straight to TrustedRouter and Tinfoil.
|
|
44
|
-
|
|
43
|
+
still go from this computer straight to TrustedRouter and Tinfoil. An account that is not invited gets the free allowance —
|
|
44
|
+
one computer, $1 of models and $0.25 of tools a month; when it is used up, Code says so and when it comes back. A computer paired before 1.3 has no account keys:
|
|
45
45
|
pair it once more (`witbitz-code pair`).
|
|
46
46
|
|
|
47
47
|
**Several accounts on one computer:** each pays for its own prompts. OpenCode names the conversation on every call it
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.9.
|
|
7
|
+
version = "1.9.3"
|
|
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"
|
|
@@ -45,6 +45,22 @@ _SUBJECT = re.compile(r'"(filePath|path|command|pattern|url)"\s*:\s*"((?:[^"\\]|
|
|
|
45
45
|
_DATA_URL = re.compile(r"^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$", re.S)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
# THE MONTH'S ALLOWANCE IS USED (the owner, 2026-10-20: the free Code allowance — "treat it like any other end of budget"). At a
|
|
49
|
+
# key's monthly cap TrustedRouter answers 429 "API key monthly spend limit exceeded; resets at 2026-11-01T00:00:00Z" (measured
|
|
50
|
+
# 2026-10-20). Passed on as a 429, OpenCode reads "rate limited" and RETRIES — the turn hangs. So it becomes a 402 with a
|
|
51
|
+
# sentence a person can act on. tools/code-confidential.mjs allowanceUsed says the same words.
|
|
52
|
+
PLAN_URL = "https://getspaces.witbitz.chat/account"
|
|
53
|
+
_MONTHS = ("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def allowance_used(status: int, text: str) -> "Refusal | None":
|
|
57
|
+
if status != 429 or not re.search(r"monthly spend limit exceeded", text or "", re.I):
|
|
58
|
+
return None
|
|
59
|
+
m = re.search(r"resets at (\d{4})-(\d{2})-(\d{2})", text or "")
|
|
60
|
+
day = f"{int(m.group(3))} {_MONTHS[int(m.group(2)) - 1]}" if m and 1 <= int(m.group(2)) <= 12 else "the 1st of next month"
|
|
61
|
+
return Refusal(402, f"This computer's Code allowance for the month is used up — it comes back on {day}. Your plan and use: {PLAN_URL}", "allowance_used")
|
|
62
|
+
|
|
63
|
+
|
|
48
64
|
class Refusal(Exception):
|
|
49
65
|
def __init__(self, status: int, message: str, kind: str = "confidential_refused") -> None:
|
|
50
66
|
super().__init__(message)
|
|
@@ -117,9 +133,14 @@ async def convert_unreadable(body: dict, label: str, to_text: Callable | None) -
|
|
|
117
133
|
return {**body, "messages": messages}
|
|
118
134
|
|
|
119
135
|
|
|
136
|
+
# Only the providers whose enclaves we verify (2026-10-20, ⇄ agent/providers/trustedrouter.mjs CONFIDENTIAL_PROVIDERS):
|
|
137
|
+
# TrustedRouter's min_privacy=confidential alone began answering some models from an ordinary server (receipt `tls-webpki`).
|
|
138
|
+
CONFIDENTIAL_PROVIDERS = ("tinfoil",)
|
|
139
|
+
|
|
140
|
+
|
|
120
141
|
def stamp_floor(req: dict) -> dict:
|
|
121
142
|
"""The confidential floor, as a confidential room stamps it. Key ORDER matters — the receipt signs these exact bytes."""
|
|
122
|
-
return {**req, "stream": True, "stream_options": {"include_usage": True}, "provider": {**(req.get("provider") or {}), "min_privacy": "confidential"}}
|
|
143
|
+
return {**req, "stream": True, "stream_options": {"include_usage": True}, "provider": {**(req.get("provider") or {}), "min_privacy": "confidential", "only": list(CONFIDENTIAL_PROVIDERS)}}
|
|
123
144
|
|
|
124
145
|
|
|
125
146
|
# ── the answer ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -240,6 +261,28 @@ def without_words(frame: str) -> str:
|
|
|
240
261
|
return f"data: {dumps({**ev, 'choices': [{**ch, 'delta': rest}, *ev['choices'][1:]]})}\n\n"
|
|
241
262
|
|
|
242
263
|
|
|
264
|
+
# The confidential default (⇄ tools/code-confidential.mjs CONFIDENTIAL_DEFAULT): what answers when a pick has no verified-enclave
|
|
265
|
+
# route right now. The note says so once per session — a Code task makes many calls.
|
|
266
|
+
CONFIDENTIAL_DEFAULT = "deepseek/deepseek-v4.1-flash"
|
|
267
|
+
_NOTED: dict = {}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def fallback_note(frm: str, by: str, session_id: str = "") -> str:
|
|
271
|
+
if not frm:
|
|
272
|
+
return ""
|
|
273
|
+
k = session_id + "\0" + frm
|
|
274
|
+
if k in _NOTED:
|
|
275
|
+
return ""
|
|
276
|
+
_NOTED[k] = True
|
|
277
|
+
if len(_NOTED) > 500:
|
|
278
|
+
_NOTED.pop(next(iter(_NOTED)))
|
|
279
|
+
return f"\n\n_Answered by {by} — {frm} isn't available confidentially right now._"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def note_frame(note: str, model: str) -> str:
|
|
283
|
+
return "data: " + dumps({"id": "witbitz-note", "object": "chat.completion.chunk", "model": model, "choices": [{"index": 0, "delta": {"content": note}}]}) + "\n\n"
|
|
284
|
+
|
|
285
|
+
|
|
243
286
|
def completion_of(events: list, model: str) -> dict:
|
|
244
287
|
"""The streamed events as one chat.completion — for a caller that asked without `stream`."""
|
|
245
288
|
content = reasoning = ""
|
|
@@ -596,6 +639,15 @@ class ConfidentialProxy:
|
|
|
596
639
|
async with self.client.stream(req.method, target, headers=_forward_headers(req.headers), content=None if req.method in ("GET", "HEAD") else out) as up:
|
|
597
640
|
headers = {k: v for k, v in up.headers.items() if k.lower() not in ("content-encoding", "content-length", "transfer-encoding", "connection")}
|
|
598
641
|
self._key_refused(up.status_code)
|
|
642
|
+
if up.status_code == 429: # the month's allowance, said plainly — never a "rate limit" OpenCode would retry
|
|
643
|
+
text = (await up.aread()).decode("utf-8", "replace")
|
|
644
|
+
used = allowance_used(up.status_code, text)
|
|
645
|
+
if used:
|
|
646
|
+
await self._fail(res, used, False)
|
|
647
|
+
return
|
|
648
|
+
await res.head(up.status_code, headers)
|
|
649
|
+
await res.end(text)
|
|
650
|
+
return
|
|
599
651
|
await res.head(up.status_code, headers)
|
|
600
652
|
async for c in up.aiter_bytes():
|
|
601
653
|
await res.write(c)
|
|
@@ -622,7 +674,10 @@ class ConfidentialProxy:
|
|
|
622
674
|
except Exception: # noqa: BLE001 — progress must never fail a call
|
|
623
675
|
pass
|
|
624
676
|
|
|
677
|
+
fell = {"from": ""} # the pick's label when TrustedRouter had no verified-enclave route for it and the default answered
|
|
678
|
+
|
|
625
679
|
async def once(words_shown: bool = False) -> None:
|
|
680
|
+
nonlocal body, conf, label
|
|
626
681
|
ends = self._proof_ends.get(body["model"])
|
|
627
682
|
hold = attempt["n"] > 1 or (ends is not None and self._now() / 1000 >= ends - HOLD_BEFORE_LAPSE_SEC)
|
|
628
683
|
n = self._nonce()
|
|
@@ -637,6 +692,19 @@ class ConfidentialProxy:
|
|
|
637
692
|
if not up.is_success:
|
|
638
693
|
self._key_refused(up.status_code)
|
|
639
694
|
text = (await up.aread()).decode("utf-8", "replace")
|
|
695
|
+
used = allowance_used(up.status_code, text)
|
|
696
|
+
if used:
|
|
697
|
+
raise used
|
|
698
|
+
# No verified-enclave route for the pick (2026-10-20, ⇄ tools/code-confidential.mjs): refused BEFORE any upstream
|
|
699
|
+
# saw the request — the same request goes again as the confidential default, attested and checked, once.
|
|
700
|
+
if (up.status_code == 400 and "no route candidates" in text.lower() and not fell["from"] and body.get("model") != CONFIDENTIAL_DEFAULT
|
|
701
|
+
and CONFIDENTIAL_DEFAULT in self.models and not res.headers_sent):
|
|
702
|
+
fell["from"] = conf["label"]
|
|
703
|
+
self.log(f"code-confidential: {body['model']} has no verified-enclave route now → {CONFIDENTIAL_DEFAULT} answers")
|
|
704
|
+
body = {**body, "model": CONFIDENTIAL_DEFAULT}
|
|
705
|
+
conf = self.models[CONFIDENTIAL_DEFAULT]
|
|
706
|
+
label = f"{conf['label']} (confidential)"
|
|
707
|
+
return await once(words_shown)
|
|
640
708
|
if res.headers_sent:
|
|
641
709
|
raise Refusal(502, f"{label}: TrustedRouter answered {up.status_code} when asked again ({text[:160]})", "upstream_error")
|
|
642
710
|
await res.head(up.status_code, {"content-type": up.headers.get("content-type", "application/json")})
|
|
@@ -677,15 +745,25 @@ class ConfidentialProxy:
|
|
|
677
745
|
exp = ((v.get("claims") or {}).get("upstream") or {}).get("verification_expires_at")
|
|
678
746
|
if isinstance(exp, (int, float)):
|
|
679
747
|
self._proof_ends[body["model"]] = exp
|
|
748
|
+
note = fallback_note(fell["from"], conf["label"], session_id)
|
|
680
749
|
if wants_stream:
|
|
750
|
+
noted = not note
|
|
681
751
|
for f in answer.held():
|
|
752
|
+
if not noted and re.search(r'"finish_reason":"|^data: \[DONE\]', f, re.M):
|
|
753
|
+
await res.write(note_frame(note, body["model"])) # the note goes in before the answer ends
|
|
754
|
+
noted = True
|
|
682
755
|
out = without_words(f) if words_shown else f
|
|
683
756
|
if out:
|
|
684
757
|
await res.write(out)
|
|
758
|
+
if not noted:
|
|
759
|
+
await res.write(note_frame(note, body["model"]))
|
|
685
760
|
await res.end()
|
|
686
761
|
return
|
|
687
762
|
await res.head(200, {"content-type": "application/json"})
|
|
688
|
-
|
|
763
|
+
done = completion_of(answer.events(), body["model"])
|
|
764
|
+
if note and done.get("choices") and done["choices"][0].get("message") is not None:
|
|
765
|
+
done["choices"][0]["message"]["content"] = (done["choices"][0]["message"].get("content") or "") + note
|
|
766
|
+
await res.end(dumps(done))
|
|
689
767
|
|
|
690
768
|
try:
|
|
691
769
|
if not conf.get("vision"):
|
|
@@ -11,17 +11,6 @@
|
|
|
11
11
|
],
|
|
12
12
|
"maxTokens": 24000
|
|
13
13
|
},
|
|
14
|
-
{
|
|
15
|
-
"model": "z-ai/glm-5.3-flash",
|
|
16
|
-
"label": "GLM 5.3 Flash",
|
|
17
|
-
"vision": true,
|
|
18
|
-
"efforts": [
|
|
19
|
-
"low",
|
|
20
|
-
"high",
|
|
21
|
-
"max"
|
|
22
|
-
],
|
|
23
|
-
"maxTokens": 16000
|
|
24
|
-
},
|
|
25
14
|
{
|
|
26
15
|
"model": "z-ai/glm-5.3",
|
|
27
16
|
"label": "GLM 5.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: witbitz-code
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.3
|
|
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
|
|
@@ -67,8 +67,8 @@ opened (a one-time key traded for a private cookie; no other website can drive i
|
|
|
67
67
|
**No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
|
|
68
68
|
providers (today up to $50 of models and $5 of tools a month per computer). They are sent sealed to this computer, kept in
|
|
69
69
|
`~/.witbitz/code/account-keys.json` (0600) and never given to OpenCode: the local proxy puts them on each call. Model calls
|
|
70
|
-
still go from this computer straight to TrustedRouter and Tinfoil.
|
|
71
|
-
|
|
70
|
+
still go from this computer straight to TrustedRouter and Tinfoil. An account that is not invited gets the free allowance —
|
|
71
|
+
one computer, $1 of models and $0.25 of tools a month; when it is used up, Code says so and when it comes back. A computer paired before 1.3 has no account keys:
|
|
72
72
|
pair it once more (`witbitz-code pair`).
|
|
73
73
|
|
|
74
74
|
**Several accounts on one computer:** each pays for its own prompts. OpenCode names the conversation on every call it
|
|
@@ -61,7 +61,8 @@ def test_any_change_to_what_was_sent_or_received_is_refused():
|
|
|
61
61
|
assert v(request_body=request + b" ")["error"] == "receipt_req_hash"
|
|
62
62
|
assert v(nonce="someone-elses-nonce")["error"] == "receipt_nonce_mismatch"
|
|
63
63
|
tampered = capture_of(stream)
|
|
64
|
-
|
|
64
|
+
at = next(i for i, d in enumerate(tampered.datas) if b'"OK"' in d) # wherever the captured answer says OK
|
|
65
|
+
tampered.datas[at] = tampered.datas[at].replace(b'"OK"', b'"NO"')
|
|
65
66
|
assert v(capture=tampered)["error"] == "receipt_resp_hash"
|
|
66
67
|
dropped = capture_of(stream)
|
|
67
68
|
dropped.datas.pop()
|
|
@@ -91,14 +92,14 @@ from conftest import requires_node, run_node # noqa: E402
|
|
|
91
92
|
|
|
92
93
|
from witbitz_code import confidential as cf, models as mdl # noqa: E402
|
|
93
94
|
|
|
94
|
-
SAY_OK = {"model": "deepseek/deepseek-v4-flash", "messages": [{"role": "user", "content": "Say OK."}], "max_tokens":
|
|
95
|
+
SAY_OK = {"model": "deepseek/deepseek-v4.1-flash", "messages": [{"role": "user", "content": "Say OK."}], "max_tokens": 200, "stream": True}
|
|
95
96
|
PNG = "data:image/png;base64,bm90IHJlYWxseSBhIHBuZw=="
|
|
96
97
|
|
|
97
98
|
|
|
98
99
|
def replay_models():
|
|
99
|
-
# The captured exchange is deepseek-v4-flash's (
|
|
100
|
+
# The captured exchange is deepseek-v4.1-flash's (recaptured 2026-10-20 with the Tinfoil-only floor), and its receipt signs those
|
|
100
101
|
# exact bytes — so the replay hands the proxy that model, as the Node test does.
|
|
101
|
-
return {**mdl.confidential_models()
|
|
102
|
+
return {**mdl.confidential_models()}
|
|
102
103
|
|
|
103
104
|
|
|
104
105
|
class FakeNet:
|
|
@@ -217,9 +218,9 @@ def test_a_lapsed_proof_before_anything_reached_opencode_is_asked_again_with_a_f
|
|
|
217
218
|
|
|
218
219
|
async def t(post, net, _p):
|
|
219
220
|
r = await post({**SAY_OK, "stream": False})
|
|
220
|
-
assert r.status_code == 200 and r.json()["choices"][0]["message"]["content"] == "OK
|
|
221
|
+
assert r.status_code == 200 and r.json()["choices"][0]["message"]["content"] == "OK"
|
|
221
222
|
assert len(net.calls) == 2 and seen == {"verify": 2, "nonce": 2}
|
|
222
|
-
assert "REFUSED deepseek/deepseek-v4-flash — receipt_verification_window" in "\n".join(logs) and "↻" in "\n".join(logs)
|
|
223
|
+
assert "REFUSED deepseek/deepseek-v4.1-flash — receipt_verification_window" in "\n".join(logs) and "↻" in "\n".join(logs)
|
|
223
224
|
with_proxy(t, verify=lapsing, nonce=nonce, log=logs.append)
|
|
224
225
|
|
|
225
226
|
|
|
@@ -246,7 +247,7 @@ def test_the_gateway_must_prove_its_attestation_before_anything_leaves():
|
|
|
246
247
|
|
|
247
248
|
@needs_fixture
|
|
248
249
|
def test_an_image_for_a_text_only_model_is_refused_without_a_tinfoil_key_and_read_with_one():
|
|
249
|
-
img = {**SAY_OK, "messages": [{"role": "user", "content": [{"type": "text", "text": "what is this?"}, {"type": "image_url", "image_url": {"url": PNG}}]}]}
|
|
250
|
+
img = {**SAY_OK, "model": "z-ai/glm-5.3", "messages": [{"role": "user", "content": [{"type": "text", "text": "what is this?"}, {"type": "image_url", "image_url": {"url": PNG}}]}]} # a TEXT-ONLY confidential model (as the Node test)
|
|
250
251
|
|
|
251
252
|
async def no_key(post, net, _p):
|
|
252
253
|
r = await post(img)
|
|
@@ -380,3 +381,64 @@ def test_opencode_starts_on_the_catalog_s_confidential_default():
|
|
|
380
381
|
entry = next(m for m in models.catalog() if m["model"] == models.DEFAULT_MODEL)
|
|
381
382
|
assert want in (entry.get("id"), entry["model"].split("/")[-1]), f"catalog default {want!r} vs {models.DEFAULT_MODEL!r}"
|
|
382
383
|
assert models.default_model_config() == {"model": "trustedrouter/deepseek/deepseek-v4.1-flash"}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# THE MONTH'S ALLOWANCE IS USED (the free Code allowance, 2026-10-20): TrustedRouter's answer at a key's cap, as measured.
|
|
387
|
+
SPEND_LIMIT = {"error": {"message": "API key monthly spend limit exceeded; resets at 2026-11-01T00:00:00Z", "source": "router", "status": 429}}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class LimitNet(FakeNet):
|
|
391
|
+
def handler(self, req: httpx.Request):
|
|
392
|
+
if "googleapis.com/service_accounts" in str(req.url):
|
|
393
|
+
return httpx.Response(200, json=self.meta["googleJwks"])
|
|
394
|
+
self.calls.append({"url": str(req.url)})
|
|
395
|
+
return httpx.Response(429, json=SPEND_LIMIT)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@needs_fixture
|
|
399
|
+
def test_the_months_allowance_used_is_a_402_that_says_so_never_a_429_opencode_would_retry():
|
|
400
|
+
meta, stream, _ = fixture()
|
|
401
|
+
models = replay_models()
|
|
402
|
+
for model in ["openai/gpt-6-sol", next(iter(models))]: # pass-through, and a confidential model
|
|
403
|
+
async def t(post, _net, _p, model=model):
|
|
404
|
+
r = await post({"model": model, "messages": [{"role": "user", "content": "hi"}], "stream": True})
|
|
405
|
+
assert r.status_code == 402, model
|
|
406
|
+
e = r.json()["error"]
|
|
407
|
+
assert e["type"] == "allowance_used", model
|
|
408
|
+
assert "allowance for the month is used up — it comes back on 1 November" in e["message"], e["message"]
|
|
409
|
+
assert "getspaces.witbitz.chat/account" in e["message"]
|
|
410
|
+
with_proxy(t, net=LimitNet(meta, stream.decode("utf-8")), models=models)
|
|
411
|
+
assert cf.allowance_used(429, '{"error":{"message":"rate limited"}}') is None, "a real rate limit stays a rate limit"
|
|
412
|
+
assert cf.allowance_used(400, json.dumps(SPEND_LIMIT)) is None
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class NoRouteThenReplay(FakeNet):
|
|
416
|
+
"""TrustedRouter refuses any model but the default ("No route candidates" — before any upstream sees it); the default
|
|
417
|
+
answers with the captured stream."""
|
|
418
|
+
|
|
419
|
+
def handler(self, req: httpx.Request):
|
|
420
|
+
if "googleapis.com/service_accounts" not in str(req.url) and json.loads(req.content.decode("utf-8")).get("model") != cf.CONFIDENTIAL_DEFAULT:
|
|
421
|
+
self.calls.append({"url": str(req.url), "headers": dict(req.headers), "raw": list(req.headers.raw), "body": req.content.decode("utf-8")})
|
|
422
|
+
return httpx.Response(400, json={"error": {"message": "No route candidates match the requested provider filters"}})
|
|
423
|
+
return super().handler(req)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@needs_fixture
|
|
427
|
+
def test_a_pick_with_no_verified_enclave_route_is_answered_by_the_default_with_one_note_per_session():
|
|
428
|
+
meta, stream, request = fixture()
|
|
429
|
+
net = NoRouteThenReplay(meta, stream.decode("utf-8"))
|
|
430
|
+
logs = []
|
|
431
|
+
glm = {**SAY_OK, "model": "z-ai/glm-5.3"}
|
|
432
|
+
|
|
433
|
+
async def t(post, net_, _p):
|
|
434
|
+
r = await post(glm, {"x-session-id": "ses_fb"})
|
|
435
|
+
text = r.text
|
|
436
|
+
assert [json.loads(c["body"])["model"] for c in net.calls] == ["z-ai/glm-5.3", "deepseek/deepseek-v4.1-flash"], "the pick, then the default — once"
|
|
437
|
+
assert net.calls[1]["body"].encode() == request, "the same request, only the model changed — the receipt signs it"
|
|
438
|
+
assert '"content":"OK"' in text
|
|
439
|
+
assert "Answered by DeepSeek V4.1 Flash — GLM 5.3 isn't available confidentially right now." in text
|
|
440
|
+
assert text.index("Answered by") < text.index("[DONE]")
|
|
441
|
+
again = (await post(glm, {"x-session-id": "ses_fb"})).text
|
|
442
|
+
assert "Answered by" not in again and '"content":"OK"' in again, "said once per session"
|
|
443
|
+
assert any("has no verified-enclave route now" in line for line in logs)
|
|
444
|
+
with_proxy(t, net=net, log=logs.append)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs
RENAMED
|
File without changes
|
|
File without changes
|
{witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browser.js
RENAMED
|
File without changes
|
{witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-notes.js
RENAMED
|
File without changes
|
{witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-progress.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|