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.
Files changed (117) hide show
  1. {witbitz_code-1.9.1/src/witbitz_code.egg-info → witbitz_code-1.9.3}/PKG-INFO +3 -3
  2. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/README.md +2 -2
  3. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/pyproject.toml +1 -1
  4. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/__init__.py +1 -1
  5. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/confidential.py +80 -2
  6. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/confidential_models.json +0 -11
  7. {witbitz_code-1.9.1 → witbitz_code-1.9.3/src/witbitz_code.egg-info}/PKG-INFO +3 -3
  8. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_confidential.py +69 -7
  9. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/LICENSE +0 -0
  10. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/setup.cfg +0 -0
  11. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/__main__.py +0 -0
  12. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/_js.py +0 -0
  13. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/account.py +0 -0
  14. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/asks.py +0 -0
  15. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/attachments.py +0 -0
  16. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/auto.py +0 -0
  17. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/auto_runner.py +0 -0
  18. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/browse.py +0 -0
  19. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/browsetree.py +0 -0
  20. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/cli.py +0 -0
  21. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/codekeys.py +0 -0
  22. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/codetools.py +0 -0
  23. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/compress.py +0 -0
  24. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/computers.py +0 -0
  25. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/connector.py +0 -0
  26. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/devicelink.py +0 -0
  27. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/dirs.py +0 -0
  28. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/folders.py +0 -0
  29. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/gateway.py +0 -0
  30. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
  31. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/icons/witbitz-code.ico +0 -0
  32. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/images.py +0 -0
  33. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/keys.py +0 -0
  34. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/link.py +0 -0
  35. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/mkdir.py +0 -0
  36. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/models.py +0 -0
  37. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/net.py +0 -0
  38. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/offered_models.json +0 -0
  39. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  40. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs +0 -0
  41. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs +0 -0
  42. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-publisher.mjs +0 -0
  43. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs +0 -0
  44. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-browser.js +0 -0
  45. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  46. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  47. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  48. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/outputs.py +0 -0
  49. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/pair.py +0 -0
  50. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/pairings.py +0 -0
  51. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/payers.py +0 -0
  52. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/plugins.py +0 -0
  53. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/policy.py +0 -0
  54. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/ports.py +0 -0
  55. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/qr.py +0 -0
  56. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/receipt.py +0 -0
  57. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/relay.py +0 -0
  58. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/remind.py +0 -0
  59. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/seccheck.py +0 -0
  60. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/seen.py +0 -0
  61. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/selfupdate.py +0 -0
  62. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/tinfoil_read.py +0 -0
  63. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/tools_catalog.json +0 -0
  64. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/tools_probe.py +0 -0
  65. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/toolsetup.py +0 -0
  66. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/ui.py +0 -0
  67. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/ui_static/app.css +0 -0
  68. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/ui_static/app.js +0 -0
  69. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/ui_static/index.html +0 -0
  70. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/uitools.py +0 -0
  71. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/uninstall.py +0 -0
  72. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/winagent.py +0 -0
  73. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/winbg.py +0 -0
  74. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/winconsole.py +0 -0
  75. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/wizard.py +0 -0
  76. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code/wslstart.py +0 -0
  77. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
  78. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  79. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  80. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code.egg-info/requires.txt +0 -0
  81. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/src/witbitz_code.egg-info/top_level.txt +0 -0
  82. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_asks.py +0 -0
  83. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_attachments.py +0 -0
  84. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_auto.py +0 -0
  85. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_browse.py +0 -0
  86. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_browsetree.py +0 -0
  87. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_cli.py +0 -0
  88. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_codekeys.py +0 -0
  89. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_codetools.py +0 -0
  90. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_connector_e2e.py +0 -0
  91. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_devicelink.py +0 -0
  92. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_dirs.py +0 -0
  93. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_files_and_compress.py +0 -0
  94. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_folders.py +0 -0
  95. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_images.py +0 -0
  96. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_outputs.py +0 -0
  97. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_pair_flows.py +0 -0
  98. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_plugins.py +0 -0
  99. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_policy.py +0 -0
  100. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_ports.py +0 -0
  101. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_ports_win.py +0 -0
  102. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_registry.py +0 -0
  103. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_relay_codec.py +0 -0
  104. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_remind.py +0 -0
  105. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_seccheck.py +0 -0
  106. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_selfupdate.py +0 -0
  107. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_setup.py +0 -0
  108. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_tools_probe.py +0 -0
  109. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_toolsetup.py +0 -0
  110. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_ui.py +0 -0
  111. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_uitools.py +0 -0
  112. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_uninstall.py +0 -0
  113. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_winagent.py +0 -0
  114. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_winbg.py +0 -0
  115. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_winconsole.py +0 -0
  116. {witbitz_code-1.9.1 → witbitz_code-1.9.3}/tests/test_windows_files.py +0 -0
  117. {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.1
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. Coding is by invitation for now — an account that is not
71
- invited gets no keys, and the pairing stops with a sentence saying so. A computer paired before 1.3 has no account keys:
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. Coding is by invitation for now — an account that is not
44
- invited gets no keys, and the pairing stops with a sentence saying so. A computer paired before 1.3 has no account keys:
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.1"
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"
@@ -4,4 +4,4 @@ The Python implementation of tools/witbitz-code.mjs (docs/opencode-relay.md phas
4
4
  vectors and cross-implementation tests.
5
5
  """
6
6
 
7
- __version__ = "1.9.1"
7
+ __version__ = "1.9.3"
@@ -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
- await res.end(dumps(completion_of(answer.events(), body["model"])))
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.1
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. Coding is by invitation for now — an account that is not
71
- invited gets no keys, and the pairing stops with a sentence saying so. A computer paired before 1.3 has no account keys:
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
- tampered.datas[1] = tampered.datas[1].replace(b'"OK"', b'"NO"')
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": 5, "stream": True}
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 (retired from the confidential tier since), and its receipt signs those
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(), "deepseek/deepseek-v4-flash": {"vision": False, "label": "DeepSeek V4 Flash"}}
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