witbitz-code 1.9.0__tar.gz → 1.9.2__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.0/src/witbitz_code.egg-info → witbitz_code-1.9.2}/PKG-INFO +3 -3
  2. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/README.md +2 -2
  3. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/pyproject.toml +1 -1
  4. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/__init__.py +1 -1
  5. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/confidential.py +34 -1
  6. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/confidential_models.json +0 -11
  7. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-publisher.mjs +66 -7
  8. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs +5 -5
  9. {witbitz_code-1.9.0 → witbitz_code-1.9.2/src/witbitz_code.egg-info}/PKG-INFO +3 -3
  10. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_confidential.py +37 -7
  11. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/LICENSE +0 -0
  12. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/setup.cfg +0 -0
  13. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/__main__.py +0 -0
  14. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/_js.py +0 -0
  15. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/account.py +0 -0
  16. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/asks.py +0 -0
  17. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/attachments.py +0 -0
  18. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/auto.py +0 -0
  19. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/auto_runner.py +0 -0
  20. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/browse.py +0 -0
  21. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/browsetree.py +0 -0
  22. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/cli.py +0 -0
  23. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/codekeys.py +0 -0
  24. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/codetools.py +0 -0
  25. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/compress.py +0 -0
  26. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/computers.py +0 -0
  27. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/connector.py +0 -0
  28. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/devicelink.py +0 -0
  29. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/dirs.py +0 -0
  30. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/folders.py +0 -0
  31. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/gateway.py +0 -0
  32. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
  33. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/icons/witbitz-code.ico +0 -0
  34. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/images.py +0 -0
  35. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/keys.py +0 -0
  36. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/link.py +0 -0
  37. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/mkdir.py +0 -0
  38. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/models.py +0 -0
  39. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/net.py +0 -0
  40. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/offered_models.json +0 -0
  41. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  42. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs +0 -0
  43. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs +0 -0
  44. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browser.js +0 -0
  45. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  46. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  47. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  48. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/outputs.py +0 -0
  49. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/pair.py +0 -0
  50. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/pairings.py +0 -0
  51. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/payers.py +0 -0
  52. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/plugins.py +0 -0
  53. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/policy.py +0 -0
  54. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/ports.py +0 -0
  55. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/qr.py +0 -0
  56. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/receipt.py +0 -0
  57. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/relay.py +0 -0
  58. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/remind.py +0 -0
  59. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/seccheck.py +0 -0
  60. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/seen.py +0 -0
  61. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/selfupdate.py +0 -0
  62. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/tinfoil_read.py +0 -0
  63. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/tools_catalog.json +0 -0
  64. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/tools_probe.py +0 -0
  65. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/toolsetup.py +0 -0
  66. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/ui.py +0 -0
  67. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/ui_static/app.css +0 -0
  68. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/ui_static/app.js +0 -0
  69. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/ui_static/index.html +0 -0
  70. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/uitools.py +0 -0
  71. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/uninstall.py +0 -0
  72. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/winagent.py +0 -0
  73. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/winbg.py +0 -0
  74. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/winconsole.py +0 -0
  75. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/wizard.py +0 -0
  76. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/wslstart.py +0 -0
  77. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
  78. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  79. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  80. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/requires.txt +0 -0
  81. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/top_level.txt +0 -0
  82. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_asks.py +0 -0
  83. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_attachments.py +0 -0
  84. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_auto.py +0 -0
  85. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_browse.py +0 -0
  86. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_browsetree.py +0 -0
  87. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_cli.py +0 -0
  88. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_codekeys.py +0 -0
  89. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_codetools.py +0 -0
  90. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_connector_e2e.py +0 -0
  91. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_devicelink.py +0 -0
  92. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_dirs.py +0 -0
  93. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_files_and_compress.py +0 -0
  94. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_folders.py +0 -0
  95. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_images.py +0 -0
  96. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_outputs.py +0 -0
  97. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_pair_flows.py +0 -0
  98. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_plugins.py +0 -0
  99. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_policy.py +0 -0
  100. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_ports.py +0 -0
  101. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_ports_win.py +0 -0
  102. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_registry.py +0 -0
  103. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_relay_codec.py +0 -0
  104. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_remind.py +0 -0
  105. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_seccheck.py +0 -0
  106. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_selfupdate.py +0 -0
  107. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_setup.py +0 -0
  108. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_tools_probe.py +0 -0
  109. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_toolsetup.py +0 -0
  110. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_ui.py +0 -0
  111. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_uitools.py +0 -0
  112. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_uninstall.py +0 -0
  113. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_winagent.py +0 -0
  114. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_winbg.py +0 -0
  115. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_winconsole.py +0 -0
  116. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_windows_files.py +0 -0
  117. {witbitz_code-1.9.0 → witbitz_code-1.9.2}/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.0
3
+ Version: 1.9.2
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.0"
7
+ version = "1.9.2"
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.0"
7
+ __version__ = "1.9.2"
@@ -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 ───────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -596,6 +617,15 @@ class ConfidentialProxy:
596
617
  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
618
  headers = {k: v for k, v in up.headers.items() if k.lower() not in ("content-encoding", "content-length", "transfer-encoding", "connection")}
598
619
  self._key_refused(up.status_code)
620
+ if up.status_code == 429: # the month's allowance, said plainly — never a "rate limit" OpenCode would retry
621
+ text = (await up.aread()).decode("utf-8", "replace")
622
+ used = allowance_used(up.status_code, text)
623
+ if used:
624
+ await self._fail(res, used, False)
625
+ return
626
+ await res.head(up.status_code, headers)
627
+ await res.end(text)
628
+ return
599
629
  await res.head(up.status_code, headers)
600
630
  async for c in up.aiter_bytes():
601
631
  await res.write(c)
@@ -637,6 +667,9 @@ class ConfidentialProxy:
637
667
  if not up.is_success:
638
668
  self._key_refused(up.status_code)
639
669
  text = (await up.aread()).decode("utf-8", "replace")
670
+ used = allowance_used(up.status_code, text)
671
+ if used:
672
+ raise used
640
673
  if res.headers_sent:
641
674
  raise Refusal(502, f"{label}: TrustedRouter answered {up.status_code} when asked again ({text[:160]})", "upstream_error")
642
675
  await res.head(up.status_code, {"content-type": up.headers.get("content-type", "application/json")})
@@ -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",
@@ -258,16 +258,28 @@ function createCast({ cdpHttp, onFrame, onState = () => {
258
258
  const sh = c.h + BARS(c.h);
259
259
  await onPage("Emulation.setDeviceMetricsOverride", { width: c.w, height: pageFull ? sh : c.h, deviceScaleFactor: c.dpr, mobile, screenWidth: c.w, screenHeight: sh });
260
260
  await onPage("Emulation.setTouchEmulationEnabled", mobile ? { enabled: true, maxTouchPoints: 5 } : { enabled: false });
261
- if (desktopUA) await onPage("Emulation.setUserAgentOverride", { userAgent: mobile ? phoneUA(desktopUA) : desktopUA });
261
+ if (desktopUA) await onPage("Emulation.setUserAgentOverride", uaOverride(desktopUA, await realHints(), mobile));
262
262
  } else {
263
263
  await onPage("Emulation.clearDeviceMetricsOverride").catch(() => {
264
264
  });
265
265
  await onPage("Emulation.setTouchEmulationEnabled", { enabled: false }).catch(() => {
266
266
  });
267
- if (desktopUA) await onPage("Emulation.setUserAgentOverride", { userAgent: desktopUA }).catch(() => {
267
+ if (desktopUA) await onPage("Emulation.setUserAgentOverride", uaOverride(desktopUA, await realHints(), false)).catch(() => {
268
268
  });
269
269
  }
270
270
  }
271
+ let hintsOf = null;
272
+ async function realHints() {
273
+ if (hintsOf) return hintsOf;
274
+ const r = await onPage("Runtime.evaluate", { expression: HINTS_NOW, returnByValue: true, awaitPromise: true }).catch(() => null);
275
+ const v = r && r.result && r.result.value;
276
+ if (v && Array.isArray(v.brands) && v.brands.length) hintsOf = v;
277
+ if (!hintsOf) {
278
+ const b = await send("Browser.getVersion").catch(() => null);
279
+ hintsOf = hintsFromVersion(b && b.product, desktopUA);
280
+ }
281
+ return hintsOf;
282
+ }
271
283
  async function hookFullscreen() {
272
284
  await onPage("Runtime.enable");
273
285
  await onPage("Runtime.addBinding", { name: FS_BINDING });
@@ -467,6 +479,20 @@ function norm(v) {
467
479
  return { w, h, dpr: dpr >= 1 && dpr <= 4 ? dpr : 1, mobile: !!v.mobile };
468
480
  }
469
481
  var PHONE = { w: 412, h: 915, dpr: 2, mobile: true };
482
+ var HINTS_NOW = `(async () => { const d = navigator.userAgentData; if (!d) return null; let h = {}; try { h = await d.getHighEntropyValues(['platformVersion', 'architecture', 'bitness', 'model', 'fullVersionList', 'wow64']) } catch (e) {} return { brands: d.brands, fullVersionList: h.fullVersionList || d.brands, platform: d.platform, platformVersion: h.platformVersion || '', architecture: h.architecture || '', bitness: h.bitness || '', model: h.model || '', wow64: !!h.wow64 } })()`;
483
+ function hintsFromVersion(product, ua = "") {
484
+ const full = (/\/(\d+(?:\.\d+){1,3})/.exec(String(product || "")) || [])[1];
485
+ if (!full) return null;
486
+ const major = full.split(".")[0];
487
+ const platform = /Windows/.test(ua) ? "Windows" : /Mac OS X/.test(ua) ? "macOS" : /CrOS/.test(ua) ? "Chrome OS" : "Linux";
488
+ return { brands: [{ brand: "Chromium", version: major }, { brand: "Not)A;Brand", version: "99" }], fullVersionList: [{ brand: "Chromium", version: full }, { brand: "Not)A;Brand", version: "99.0.0.0" }], platform, platformVersion: "", architecture: /arm|aarch64/i.test(ua) ? "arm" : "x86", bitness: "64", model: "", wow64: false };
489
+ }
490
+ function uaOverride(desktop, real, mobile) {
491
+ const userAgent = mobile ? phoneUA(desktop) : desktop;
492
+ if (!real || !Array.isArray(real.brands) || !real.brands.length) return { userAgent };
493
+ const meta = { brands: real.brands, fullVersionList: real.fullVersionList || real.brands, platform: real.platform || "", platformVersion: real.platformVersion || "", architecture: real.architecture || "", model: real.model || "", mobile: false, bitness: real.bitness || "", wow64: !!real.wow64 };
494
+ return mobile ? { userAgent, platform: "Linux armv8l", userAgentMetadata: { ...meta, platform: "Android", platformVersion: "14.0.0", architecture: "", bitness: "", model: "Pixel 8", mobile: true, wow64: false } } : { userAgent, userAgentMetadata: meta };
495
+ }
470
496
  function phoneUA(desktop) {
471
497
  return desktop.replace(/\([^)]*\)/, "(Linux; Android 14; Pixel 8)").replace(/ Safari\//, " Mobile Safari/");
472
498
  }
@@ -476,6 +502,7 @@ var BINDING = "__wbm";
476
502
  var MAX_BATCH = 8 << 20;
477
503
  var MAX_SINCE_SNAPSHOT = 48 << 20;
478
504
  var ASSET_CACHE = 96 << 20;
505
+ var MAX_ANSWER = 2900 << 10;
479
506
  var MAX_SIDE2 = 1600;
480
507
  var STILL_MS = 500;
481
508
  var STILL_SIDE = 480;
@@ -607,12 +634,19 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
607
634
  for (const h of c.handlers) h(m);
608
635
  };
609
636
  ws.onclose = () => {
637
+ c.closed = true;
638
+ log2("mirror: the CDP connection to", http, "closed");
610
639
  for (const p of c.pending.values()) p.reject(new Error("cdp: closed"));
611
640
  c.pending.clear();
612
641
  };
613
642
  return c;
614
643
  }
615
644
  async function attachLive() {
645
+ if (live && live.closed) {
646
+ live = null;
647
+ liveSid = null;
648
+ liveTargetId = null;
649
+ }
616
650
  const t = liveTarget();
617
651
  if (!t || t === liveTargetId) return;
618
652
  if (!live) {
@@ -698,8 +732,14 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
698
732
  body: body ? Buffer.from(body).toString("base64") : ""
699
733
  }, tab.sid).catch(() => {
700
734
  });
735
+ const tooBig = (b) => b && Buffer.byteLength(b) > MAX_ANSWER;
736
+ const send = (status, headers, body) => tooBig(body) ? (tab.refused++, log2("mirror: not sent,", Math.round(Buffer.byteLength(body) / 1024), "KB \u2014", url.slice(0, 100)), done(404, {}, "")) : done(status, headers, body);
701
737
  if (url === "http://mirror.invalid/") return done(200, { "content-type": "text/html; charset=utf-8" }, shell);
702
738
  if (p.request.method !== "GET" || !/^https?:\/\//.test(url)) return done(404, {}, "");
739
+ if (p.resourceType === "Media") {
740
+ tab.refused++;
741
+ return done(404, {}, "");
742
+ }
703
743
  if (p.resourceType === "Document") {
704
744
  let host = "";
705
745
  try {
@@ -714,14 +754,14 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
714
754
  try {
715
755
  const r = await live.send("Network.getResponseBody", { requestId: known.requestId }, liveSid);
716
756
  tab.served++;
717
- return done(known.status || 200, { "content-type": known.mime || "application/octet-stream" }, r.base64Encoded ? Buffer.from(r.body, "base64") : r.body);
757
+ return send(known.status || 200, { "content-type": known.mime || "application/octet-stream" }, r.base64Encoded ? Buffer.from(r.body, "base64") : r.body);
718
758
  } catch {
719
759
  }
720
760
  }
721
761
  if (assets.has(url)) {
722
762
  tab.served++;
723
763
  const a = assets.get(url);
724
- return done(a.status, a.headers, a.body);
764
+ return send(a.status, a.headers, a.body);
725
765
  }
726
766
  if (!STATIC.has(p.resourceType) || !liveSid) {
727
767
  tab.refused++;
@@ -730,7 +770,7 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
730
770
  try {
731
771
  const a = await loadCredentialless(url);
732
772
  tab.loaded++;
733
- return done(a.status, a.headers, a.body);
773
+ return send(a.status, a.headers, a.body);
734
774
  } catch (e) {
735
775
  tab.refused++;
736
776
  log2("mirror: could not load", url.slice(0, 100), "\u2014", String(e.message || e).slice(0, 120));
@@ -816,7 +856,7 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
816
856
  });
817
857
  }
818
858
  async function open2(stream, shape) {
819
- if (!mirror) {
859
+ if (!mirror || mirror.closed) {
820
860
  mirror = await connect(mirrorHttp);
821
861
  mirror.handlers.push(onMirror);
822
862
  }
@@ -881,6 +921,10 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
881
921
  /** Make the set of mirrors equal `wanted` (stream → shape). Opens, reshapes and closes as needed. */
882
922
  async set(wanted) {
883
923
  await attachLive();
924
+ if (mirror && mirror.closed) {
925
+ mirror = null;
926
+ tabs.clear();
927
+ }
884
928
  for (const [stream] of tabs) if (!wanted.has(stream)) await close(stream);
885
929
  for (const [stream, shape] of wanted) {
886
930
  const tab = tabs.get(stream);
@@ -1225,7 +1269,22 @@ async function assignNow() {
1225
1269
  } catch {
1226
1270
  }
1227
1271
  }
1228
- if (mirrors) await mirrors.set(wanted);
1272
+ if (!mirrors) return;
1273
+ try {
1274
+ await mirrors.set(wanted);
1275
+ } catch (e) {
1276
+ const why = "mirror could not open: " + String(e.message || e).slice(0, 80);
1277
+ for (const [vid, s] of assigned) {
1278
+ if (!wanted.has(s)) continue;
1279
+ assigned.set(vid, "live");
1280
+ if (relay && relay.readyState === 1) relay.send(await sealCtl(key, { wb: "stream", vid, stream: "live", why }));
1281
+ }
1282
+ const l = cast && cast.last();
1283
+ if (l) outOrder(async () => {
1284
+ if (relay && relay.readyState === 1) relay.send(await seal(key, KIND_FRAME, packFrame(l.header, l.jpeg)));
1285
+ });
1286
+ throw e;
1287
+ }
1229
1288
  }
1230
1289
  async function startCast() {
1231
1290
  const c = ensureCast();
@@ -4,14 +4,14 @@
4
4
  // tools/browse-tree/pin.json
5
5
  var pin_default = {
6
6
  _: "THE CLOUD COMPUTER'S BROWSER FOR LINUX that this release installs (tools/browse-tree; make.sh writes it). The files are public at <base><tag>/; both sha256s are checked before anything is unpacked.",
7
- tag: "p2v",
7
+ tag: "p2x",
8
8
  base: "https://drwev5vxt2t2e.cloudfront.net/browse-tree/",
9
- pcr0: "87162d8648af94daec37f9d022c43546f6dc5c9018b7225ee5ccaf3cecd1c7924c7681f0ca48fddb84f5d0a2f987f719",
10
- commit: "e303d54e2295c148c5177b2216eca21de6b6f3a3",
9
+ pcr0: "050541844610fd5ed4b586259906d9219edbabc4445ed7943885ac737ee4918e144af2e49838a8313f5823184bc503cf",
10
+ commit: "235ef2b815ef74b46e954f48f860197d8d2b612c",
11
11
  tree: {
12
12
  file: "tree.tar.zst",
13
- sha256: "1fceee1d2ddbda8a6ec9f792755a960a877af6342645b8a29fbbf94b28b9536e",
14
- size: 664712218
13
+ sha256: "839b7cc06c70946a4912578115e6cd053cb6b2811cc458457e012ece885b36b6",
14
+ size: 664701891
15
15
  },
16
16
  lab: {
17
17
  file: "ccvm-lab.bin",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.9.0
3
+ Version: 1.9.2
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,32 @@ 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
File without changes
File without changes