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.
- {witbitz_code-1.9.0/src/witbitz_code.egg-info → witbitz_code-1.9.2}/PKG-INFO +3 -3
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/README.md +2 -2
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/pyproject.toml +1 -1
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/confidential.py +34 -1
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/confidential_models.json +0 -11
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-publisher.mjs +66 -7
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs +5 -5
- {witbitz_code-1.9.0 → witbitz_code-1.9.2/src/witbitz_code.egg-info}/PKG-INFO +3 -3
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_confidential.py +37 -7
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/LICENSE +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/setup.cfg +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/browse.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/browsetree.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/cli.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/connector.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/dirs.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/icons/witbitz-code.ico +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/images.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/mkdir.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browser.js +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/remind.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/seccheck.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/selfupdate.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/tools_catalog.json +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/toolsetup.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/ui.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/ui_static/app.css +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/ui_static/app.js +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/ui_static/index.html +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/uitools.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/uninstall.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/winagent.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/winbg.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/wizard.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/wslstart.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_asks.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_attachments.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_auto.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_browse.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_browsetree.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_cli.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_codetools.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_dirs.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_folders.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_images.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_outputs.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_plugins.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_policy.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_ports.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_ports_win.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_registry.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_remind.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_seccheck.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_selfupdate.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_setup.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_toolsetup.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_ui.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_uitools.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_uninstall.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_winagent.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_winbg.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.9.0 → witbitz_code-1.9.2}/tests/test_windows_files.py +0 -0
- {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.
|
|
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.
|
|
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.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"
|
|
@@ -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",
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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();
|
{witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs
RENAMED
|
@@ -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: "
|
|
7
|
+
tag: "p2x",
|
|
8
8
|
base: "https://drwev5vxt2t2e.cloudfront.net/browse-tree/",
|
|
9
|
-
pcr0: "
|
|
10
|
-
commit: "
|
|
9
|
+
pcr0: "050541844610fd5ed4b586259906d9219edbabc4445ed7943885ac737ee4918e144af2e49838a8313f5823184bc503cf",
|
|
10
|
+
commit: "235ef2b815ef74b46e954f48f860197d8d2b612c",
|
|
11
11
|
tree: {
|
|
12
12
|
file: "tree.tar.zst",
|
|
13
|
-
sha256: "
|
|
14
|
-
size:
|
|
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.
|
|
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.
|
|
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,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
|
|
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.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browser.js
RENAMED
|
File without changes
|
{witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.9.0 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-notes.js
RENAMED
|
File without changes
|
{witbitz_code-1.9.0 → witbitz_code-1.9.2}/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
|