witbitz-code 1.6.9__tar.gz → 1.7.0__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.6.9/src/witbitz_code.egg-info → witbitz_code-1.7.0}/PKG-INFO +2 -1
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/pyproject.toml +3 -1
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/confidential.py +7 -1
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/connector.py +11 -0
- witbitz_code-1.7.0/src/witbitz_code/images.py +125 -0
- witbitz_code-1.7.0/src/witbitz_code/seccheck.py +226 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/ui.py +6 -3
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/ui_static/app.css +8 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/ui_static/app.js +41 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/ui_static/index.html +8 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0/src/witbitz_code.egg-info}/PKG-INFO +2 -1
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code.egg-info/SOURCES.txt +4 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code.egg-info/requires.txt +1 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_connector_e2e.py +23 -0
- witbitz_code-1.7.0/tests/test_images.py +102 -0
- witbitz_code-1.7.0/tests/test_seccheck.py +143 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/LICENSE +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/README.md +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/setup.cfg +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/cli.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/dirs.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/icons/witbitz-code.ico +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/mkdir.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/tools_catalog.json +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/toolsetup.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/uitools.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/uninstall.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/winagent.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/winbg.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/wizard.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code/wslstart.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_asks.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_attachments.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_auto.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_cli.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_codetools.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_confidential.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_dirs.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_folders.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_outputs.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_plugins.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_policy.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_ports.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_ports_win.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_registry.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_setup.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_toolsetup.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_ui.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_uitools.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_uninstall.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_winagent.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_winbg.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_windows_files.py +0 -0
- {witbitz_code-1.6.9 → witbitz_code-1.7.0}/tests/test_wslstart.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: witbitz-code
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
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
|
|
@@ -19,6 +19,7 @@ Requires-Dist: websockets>=13
|
|
|
19
19
|
Requires-Dist: httpx>=0.25
|
|
20
20
|
Requires-Dist: segno>=1.5
|
|
21
21
|
Requires-Dist: tinfoil<0.15,>=0.14
|
|
22
|
+
Requires-Dist: pillow>=10
|
|
22
23
|
Provides-Extra: test
|
|
23
24
|
Requires-Dist: pytest>=8; extra == "test"
|
|
24
25
|
Requires-Dist: pyflakes>=3; extra == "test"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7.0"
|
|
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"
|
|
@@ -26,6 +26,8 @@ dependencies = [
|
|
|
26
26
|
# Tinfoil's own verifier (attestation + Sigstore + TLS pinning) — the confidential proxy reads images and files a
|
|
27
27
|
# text-only confidential model cannot, in Tinfoil's attested enclave (tinfoil_read.py). Pinned to a series on purpose.
|
|
28
28
|
"tinfoil>=0.14,<0.15",
|
|
29
|
+
# Older screenshots are re-encoded smaller before a request leaves (images.py): TrustedRouter drops a ~10 MB request.
|
|
30
|
+
"pillow>=10",
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
[project.optional-dependencies]
|
|
@@ -26,7 +26,7 @@ from typing import Any, Callable
|
|
|
26
26
|
|
|
27
27
|
import httpx
|
|
28
28
|
|
|
29
|
-
from . import keys, models as mdl, receipt as rcp, tinfoil_read
|
|
29
|
+
from . import images, keys, models as mdl, receipt as rcp, tinfoil_read
|
|
30
30
|
from .gateway import verify_gateway_attestation
|
|
31
31
|
|
|
32
32
|
TR_UPSTREAM = "https://api.trustedrouter.com"
|
|
@@ -577,6 +577,12 @@ class ConfidentialProxy:
|
|
|
577
577
|
return
|
|
578
578
|
req.headers = [(k, v) for k, v in req.headers if k.lower() not in ("authorization", "x-api-key")] + [("authorization", "Bearer " + cred["trustedrouter"])]
|
|
579
579
|
tinfoil = cred.get("tinfoil") or ""
|
|
580
|
+
if isinstance(body, dict):
|
|
581
|
+
shrunk, rep = await asyncio.to_thread(images.shrink_older_images, body)
|
|
582
|
+
if shrunk is not body:
|
|
583
|
+
body, req.body = shrunk, dumps(shrunk).encode("utf-8")
|
|
584
|
+
if rep["dropped"]:
|
|
585
|
+
self.log(f"code-confidential: the request was {rep['before'] // 1024} KB; {rep['dropped']} older screenshot(s) left out to send it")
|
|
580
586
|
conf = self.models.get(body.get("model")) if isinstance(body, dict) and isinstance(body.get("model"), str) else None
|
|
581
587
|
if not conf:
|
|
582
588
|
await self._pass_through(req, res, body, target)
|
|
@@ -41,6 +41,8 @@ from .plugins import notes_plugin_path as default_notes_plugin, notes_root as de
|
|
|
41
41
|
from .payers import SessionPayers
|
|
42
42
|
from .seen import SEEN_ROUTE, merge_seen, norm_seen
|
|
43
43
|
from .dirs import DIRS_ROUTE, list_dirs
|
|
44
|
+
from . import seccheck
|
|
45
|
+
from .seccheck import SECURITY_ROUTE
|
|
44
46
|
from .mkdir import MKDIR_ROUTE, mkdir_in
|
|
45
47
|
from .pairings import DEFAULT_OPENCODE_URL, hostname, read_env_password
|
|
46
48
|
from .relay import RELAY_URL, Connect, RelayPeer, allowed_event_path, allowed_request, project_response
|
|
@@ -157,6 +159,7 @@ class PairingServer:
|
|
|
157
159
|
asks_reconcile_ms: float = ASKS_RECONCILE_MS, notes_root: str | None = None,
|
|
158
160
|
notes_plugin_path: Path | None = None, payers: SessionPayers | None = None) -> None:
|
|
159
161
|
self.pairing = pairing
|
|
162
|
+
self._security: seccheck.SecurityCheck | None = None # read on the first ask (seccheck.py)
|
|
160
163
|
# WHO PAYS (payers.py): this pairing's account owns the turns it starts; the proxy bills each call to its owner.
|
|
161
164
|
self._payers = payers
|
|
162
165
|
self._cid = _js.js_string(pairing.get("computerId") or "")
|
|
@@ -407,6 +410,14 @@ class PairingServer:
|
|
|
407
410
|
st, b = list_dirs(path=q.get("path", ""))
|
|
408
411
|
await reply(st, b)
|
|
409
412
|
return
|
|
413
|
+
# "THIS COMPUTER'S SECURITY" (seccheck.py): the same check as the witbitz-code page, for the account's own Spaces app —
|
|
414
|
+
# read here, sealed through the relay like every answer, never written to the account's stores. Python only (the Node
|
|
415
|
+
# download answers 404 and the page says the check is not available there).
|
|
416
|
+
if bare == SECURITY_ROUTE and m.get("m") == "GET":
|
|
417
|
+
if self._security is None:
|
|
418
|
+
self._security = seccheck.SecurityCheck()
|
|
419
|
+
await reply(200, await asyncio.to_thread(self._security.status))
|
|
420
|
+
return
|
|
410
421
|
# "NEW FOLDER" (mkdir.py = tools/code-mkdir.mjs): one clean leaf under the folder the picker shows. Answered HERE.
|
|
411
422
|
if bare == MKDIR_ROUTE and m.get("m") == "POST":
|
|
412
423
|
q = dict(urllib.parse.parse_qsl(query, keep_blank_values=True))
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""OLDER IMAGES ARE SHRUNK BEFORE A REQUEST LEAVES THIS COMPUTER.
|
|
2
|
+
|
|
3
|
+
OpenCode sends the whole conversation on every call, and every screenshot in it rides along as a full base64 PNG. A model
|
|
4
|
+
reads an image as a fixed number of tokens whatever its bytes, so the context stays small while the REQUEST grows with each
|
|
5
|
+
screenshot — and TrustedRouter drops a request of about 10 MB without answering (measured 2026-10-12: 9 MB answered,
|
|
6
|
+
10 MB closed the connection; a session with 18 screenshots reached 11 MB and every message in it failed).
|
|
7
|
+
|
|
8
|
+
So: the newest KEEP_FULL images go as they are; every older one is re-encoded as a JPEG no larger than MAX_SIDE on its
|
|
9
|
+
longest side. A screenshot of ~1 MB becomes ~100–200 KB and still reads. The result is cached by content, so the same old
|
|
10
|
+
screenshot shrinks to the same bytes on every call (a provider's prompt cache keeps working from there on). If the request
|
|
11
|
+
is STILL over BUDGET, the oldest images are replaced by a one-line note, newest kept — a request that cannot be sent helps
|
|
12
|
+
no one. No image library, or an image it cannot decode: that image is left as it is (the budget rule still applies).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import hashlib
|
|
19
|
+
import io
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
from collections import OrderedDict
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
KEEP_FULL = 3
|
|
26
|
+
MAX_SIDE = 1280
|
|
27
|
+
JPEG_QUALITY = 70
|
|
28
|
+
BUDGET = 8 * 1024 * 1024 # bytes of request body; TrustedRouter's limit is ~10 MB
|
|
29
|
+
CACHE_MAX = 256
|
|
30
|
+
_DATA_URL = re.compile(r"^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$", re.S)
|
|
31
|
+
|
|
32
|
+
_cache: "OrderedDict[str, str | None]" = OrderedDict()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _image_parts(body: dict) -> list[tuple[int, int]]:
|
|
36
|
+
"""(message, part) of every inline image, oldest first."""
|
|
37
|
+
out = []
|
|
38
|
+
for mi, msg in enumerate(body.get("messages") or []):
|
|
39
|
+
content = msg.get("content") if isinstance(msg, dict) else None
|
|
40
|
+
if not isinstance(content, list):
|
|
41
|
+
continue
|
|
42
|
+
for pi, part in enumerate(content):
|
|
43
|
+
if isinstance(part, dict) and part.get("type") == "image_url" and _DATA_URL.match(str((part.get("image_url") or {}).get("url") or "")):
|
|
44
|
+
out.append((mi, pi))
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def shrink_data_url(url: str) -> str | None:
|
|
49
|
+
"""A smaller JPEG data URL for this image, or None (no library, cannot decode, or not smaller)."""
|
|
50
|
+
key = hashlib.sha256(url.encode("utf-8", "replace")).hexdigest()
|
|
51
|
+
if key in _cache:
|
|
52
|
+
_cache.move_to_end(key)
|
|
53
|
+
return _cache[key]
|
|
54
|
+
out = None
|
|
55
|
+
try:
|
|
56
|
+
from PIL import Image # optional: without it, images go as they are
|
|
57
|
+
m = _DATA_URL.match(url)
|
|
58
|
+
raw = base64.b64decode(m.group(2), validate=False)
|
|
59
|
+
with Image.open(io.BytesIO(raw)) as im:
|
|
60
|
+
im.load()
|
|
61
|
+
im = im.convert("RGBA") if im.mode in ("P", "LA") else im
|
|
62
|
+
if im.mode == "RGBA":
|
|
63
|
+
bg = Image.new("RGB", im.size, (255, 255, 255))
|
|
64
|
+
bg.paste(im, mask=im.split()[3])
|
|
65
|
+
im = bg
|
|
66
|
+
elif im.mode != "RGB":
|
|
67
|
+
im = im.convert("RGB")
|
|
68
|
+
if max(im.size) > MAX_SIDE:
|
|
69
|
+
im.thumbnail((MAX_SIDE, MAX_SIDE), Image.LANCZOS)
|
|
70
|
+
buf = io.BytesIO()
|
|
71
|
+
im.save(buf, "JPEG", quality=JPEG_QUALITY, optimize=True)
|
|
72
|
+
small = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode("ascii")
|
|
73
|
+
out = small if len(small) < len(url) else None
|
|
74
|
+
except Exception: # noqa: BLE001 — an image we cannot shrink is sent as it is
|
|
75
|
+
out = None
|
|
76
|
+
_cache[key] = out
|
|
77
|
+
while len(_cache) > CACHE_MAX:
|
|
78
|
+
_cache.popitem(last=False)
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _size(body: dict) -> int:
|
|
83
|
+
return len(json.dumps(body, separators=(",", ":"), ensure_ascii=False).encode("utf-8"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def shrink_older_images(body: Any, keep_full: int = KEEP_FULL, budget: int = BUDGET) -> tuple[Any, dict]:
|
|
87
|
+
"""(body, report). The body is a NEW dict when anything changed; the input is never mutated."""
|
|
88
|
+
report = {"shrunk": 0, "dropped": 0, "before": 0, "after": 0}
|
|
89
|
+
if not isinstance(body, dict):
|
|
90
|
+
return body, report
|
|
91
|
+
parts = _image_parts(body)
|
|
92
|
+
if not parts:
|
|
93
|
+
return body, report
|
|
94
|
+
messages = list(body["messages"])
|
|
95
|
+
touched: set[int] = set()
|
|
96
|
+
|
|
97
|
+
def put(mi: int, pi: int, part: dict) -> None:
|
|
98
|
+
if mi not in touched:
|
|
99
|
+
messages[mi] = {**messages[mi], "content": list(messages[mi]["content"])}
|
|
100
|
+
touched.add(mi)
|
|
101
|
+
messages[mi]["content"][pi] = part
|
|
102
|
+
|
|
103
|
+
older = parts[:-keep_full] if keep_full > 0 else parts
|
|
104
|
+
for mi, pi in older:
|
|
105
|
+
part = messages[mi]["content"][pi]
|
|
106
|
+
small = shrink_data_url(part["image_url"]["url"])
|
|
107
|
+
if small:
|
|
108
|
+
put(mi, pi, {**part, "image_url": {**part["image_url"], "url": small}})
|
|
109
|
+
report["shrunk"] += 1
|
|
110
|
+
out = {**body, "messages": messages} if touched else body
|
|
111
|
+
size = _size(out)
|
|
112
|
+
if size > budget:
|
|
113
|
+
report["before"] = size
|
|
114
|
+
for mi, pi in parts[:-1]: # the newest image is never dropped
|
|
115
|
+
if size <= budget:
|
|
116
|
+
break
|
|
117
|
+
part = messages[mi]["content"][pi]
|
|
118
|
+
was = len(json.dumps(part, separators=(",", ":"), ensure_ascii=False).encode("utf-8"))
|
|
119
|
+
note = {"type": "text", "text": "[An earlier screenshot was removed here to keep the request small enough to send.]"}
|
|
120
|
+
put(mi, pi, note)
|
|
121
|
+
size -= was - len(json.dumps(note, separators=(",", ":")).encode("utf-8"))
|
|
122
|
+
report["dropped"] += 1
|
|
123
|
+
out = {**body, "messages": messages}
|
|
124
|
+
report["after"] = size
|
|
125
|
+
return out, report
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""THIS COMPUTER'S SECURITY CHECK (the owner, 2026-10-12: infostealers on a desktop — "can the helper check the desktop status
|
|
2
|
+
and recommend what to do" → "Yes"). witbitz-code runs as an ordinary program on the computer, so it can read what a web page
|
|
3
|
+
never can: Windows' antivirus, its settings, the firewall, disk encryption, updates, the account's rights. It READS without
|
|
4
|
+
an administrator; a Fix that changes a setting goes through Windows' own admin prompt (UAC), and nothing changes silently.
|
|
5
|
+
A setting Windows wants changed in its own app (memory integrity: driver checks; BitLocker: a recovery key) opens that page.
|
|
6
|
+
|
|
7
|
+
It is ADVICE, not proof: malware already running as this user can fake anything a program running as this user reads.
|
|
8
|
+
It finds weak settings before something gets in; it cannot say that nothing has. The page and Spaces say so.
|
|
9
|
+
|
|
10
|
+
Windows only for now (also from WSL, where it reads the Windows that hosts it). Other systems: {"supported": False}.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from typing import Callable
|
|
23
|
+
|
|
24
|
+
SECURITY_ROUTE = "/witbitz/security" # the connector answers it for the paired Spaces app (connector.py)
|
|
25
|
+
|
|
26
|
+
# ONE PowerShell read. Every part is guarded: a part Windows refuses reads as null, never as a failure of the whole check.
|
|
27
|
+
PROBE_PS = r"""
|
|
28
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
29
|
+
$o = [ordered]@{}
|
|
30
|
+
$s = Get-MpComputerStatus
|
|
31
|
+
if ($s) { $o.defender = [ordered]@{ realtime = [bool]$s.RealTimeProtectionEnabled; behavior = [bool]$s.BehaviorMonitorEnabled;
|
|
32
|
+
tamper = [bool]$s.IsTamperProtected; sigAgeDays = [int]$s.AntivirusSignatureAge; mode = "$($s.AMRunningMode)" } }
|
|
33
|
+
$p = Get-MpPreference
|
|
34
|
+
if ($p) { $o.prefs = [ordered]@{ cloud = [int]$p.MAPSReporting; pua = [int]$p.PUAProtection; network = [int]$p.EnableNetworkProtection } }
|
|
35
|
+
$o.antivirus = @(Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct | ForEach-Object {
|
|
36
|
+
[ordered]@{ name = "$($_.displayName)"; on = ((($_.productState -shr 12) -band 0xF) -eq 1) } })
|
|
37
|
+
$dg = Get-CimInstance -Namespace root/Microsoft/Windows/DeviceGuard -ClassName Win32_DeviceGuard
|
|
38
|
+
if ($dg) { $o.memoryIntegrity = [bool](@($dg.SecurityServicesRunning) -contains 2) }
|
|
39
|
+
$o.firewall = @(Get-NetFirewallProfile | ForEach-Object { [ordered]@{ name = "$($_.Name)"; on = [bool]$_.Enabled } })
|
|
40
|
+
try { $bl = (New-Object -ComObject Shell.Application).NameSpace("$env:SystemDrive\").Self.ExtendedProperty('System.Volume.BitLockerProtection'); if ($bl -ne $null) { $o.bitlocker = [int]$bl } } catch {}
|
|
41
|
+
try { $d = (New-Object -ComObject Microsoft.Update.AutoUpdate).Results.LastInstallationSuccessDate; if ($d) { $o.updateDaysAgo = [int]((Get-Date) - [datetime]$d).TotalDays } } catch {}
|
|
42
|
+
$uac = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
|
43
|
+
if ($uac) { $o.uac = [ordered]@{ on = ([int]$uac.EnableLUA -eq 1); prompt = [int]$uac.ConsentPromptBehaviorAdmin } }
|
|
44
|
+
$o.admin = [bool]((whoami /groups /fo csv | Out-String) -match 'S-1-5-32-544')
|
|
45
|
+
$o.os = "$((Get-CimInstance Win32_OperatingSystem).Caption)"
|
|
46
|
+
$o | ConvertTo-Json -Depth 4 -Compress
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# The fixes. "ps" runs ELEVATED through Windows' admin prompt; "open" opens Windows' own page for a setting it checks itself.
|
|
50
|
+
FIXES = {
|
|
51
|
+
"pua": {"ps": "Set-MpPreference -PUAProtection Enabled"},
|
|
52
|
+
"network": {"ps": "Set-MpPreference -EnableNetworkProtection Enabled"},
|
|
53
|
+
"cloud": {"ps": "Set-MpPreference -MAPSReporting Advanced"},
|
|
54
|
+
"firewall": {"ps": "Set-NetFirewallProfile -All -Enabled True"},
|
|
55
|
+
"signatures": {"ps": "Update-MpSignature"},
|
|
56
|
+
"antivirus": {"open": "windowsdefender://threat"},
|
|
57
|
+
"memoryIntegrity": {"open": "windowsdefender://coreisolation"},
|
|
58
|
+
"tamper": {"open": "windowsdefender://threatsettings"},
|
|
59
|
+
"realtime": {"open": "windowsdefender://threatsettings"},
|
|
60
|
+
"bitlocker": {"open": "ms-settings:deviceencryption"},
|
|
61
|
+
"updates": {"open": "ms-settings:windowsupdate"},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def recommendations(s: dict | None) -> list[dict]:
|
|
66
|
+
"""[{id, ok, title, why, fix?}] — what this computer has and what it should change, plainly. Pure: tests feed it readings."""
|
|
67
|
+
if not s:
|
|
68
|
+
return []
|
|
69
|
+
out: list[dict] = []
|
|
70
|
+
|
|
71
|
+
def add(id_: str, ok: bool | None, title: str, why: str = "") -> None:
|
|
72
|
+
if ok is None:
|
|
73
|
+
return # Windows did not say: say nothing rather than guess
|
|
74
|
+
item = {"id": id_, "ok": bool(ok), "title": title, "why": "" if ok else why}
|
|
75
|
+
if not ok and id_ in FIXES:
|
|
76
|
+
item["fix"] = "admin" if "ps" in FIXES[id_] else "open"
|
|
77
|
+
out.append(item)
|
|
78
|
+
|
|
79
|
+
other_av = [a for a in (s.get("antivirus") or []) if a.get("on") and "defender" not in (a.get("name") or "").lower()]
|
|
80
|
+
d = s.get("defender")
|
|
81
|
+
if other_av:
|
|
82
|
+
out.append({"id": "antivirus", "ok": True, "title": f"Antivirus: {other_av[0]['name']}", "why": ""})
|
|
83
|
+
elif d:
|
|
84
|
+
add("realtime", d.get("realtime") and d.get("behavior"), "Microsoft Defender real-time protection",
|
|
85
|
+
"Real-time protection is off — malware can run without being stopped. Turn it on in Windows Security.")
|
|
86
|
+
add("tamper", d.get("tamper"), "Tamper protection",
|
|
87
|
+
"Tamper protection is off — malware can turn Defender off before it acts.")
|
|
88
|
+
age = d.get("sigAgeDays")
|
|
89
|
+
add("signatures", None if age is None else age <= 3, "Virus definitions",
|
|
90
|
+
f"Defender's definitions are {age} days old — it will miss newer malware.")
|
|
91
|
+
p = s.get("prefs") or {}
|
|
92
|
+
add("cloud", None if p.get("cloud") is None else p["cloud"] >= 1, "Cloud protection",
|
|
93
|
+
"Cloud protection is off — brand-new malware is recognised much later.")
|
|
94
|
+
add("pua", None if p.get("pua") is None else p["pua"] == 1, "Blocking unwanted apps",
|
|
95
|
+
"Bundled and unwanted apps are only logged, not blocked. Stealers often arrive inside them.")
|
|
96
|
+
add("network", None if p.get("network") is None else p["network"] == 1, "Blocking malicious sites in every app",
|
|
97
|
+
"Network protection is off — only Edge blocks known malicious sites; other apps and malware can still reach them.")
|
|
98
|
+
else:
|
|
99
|
+
out.append({"id": "antivirus", "ok": False, "title": "Antivirus", "why": "No antivirus answered. Open Windows Security and turn Microsoft Defender on.",
|
|
100
|
+
"fix": "open"})
|
|
101
|
+
add("memoryIntegrity", s.get("memoryIntegrity"), "Memory integrity (Core isolation)",
|
|
102
|
+
"Memory integrity is off — malware that reaches the system's core is much harder to stop. Turning it on needs a restart.")
|
|
103
|
+
fw = s.get("firewall") or []
|
|
104
|
+
add("firewall", None if not fw else all(f.get("on") for f in fw), "Firewall",
|
|
105
|
+
"The firewall is off for: " + ", ".join(f["name"] for f in fw if not f.get("on")) + ".")
|
|
106
|
+
bl = s.get("bitlocker")
|
|
107
|
+
add("bitlocker", None if bl is None else bl in (1, 3, 5), "Disk encryption (BitLocker)",
|
|
108
|
+
"The disk is not encrypted — anyone with the laptop can read it. Turn on device encryption and keep its recovery key safe.")
|
|
109
|
+
days = s.get("updateDaysAgo")
|
|
110
|
+
add("updates", None if days is None else days <= 21, "Windows updates",
|
|
111
|
+
f"Windows last installed updates {days} days ago. Updates close the holes malware uses.")
|
|
112
|
+
uac = s.get("uac") or {}
|
|
113
|
+
add("uac", None if not uac else (uac.get("on") and uac.get("prompt") != 0), "Admin prompt (UAC)",
|
|
114
|
+
"UAC is off or never asks — any program you run gets full control of the computer.")
|
|
115
|
+
if s.get("admin"):
|
|
116
|
+
out.append({"id": "adminAccount", "ok": False, "title": "Daily account",
|
|
117
|
+
"why": "You use an administrator account every day. A standard account for daily use, with the admin account kept for installs, "
|
|
118
|
+
"stops much malware from taking over the whole computer. (Optional, but the strongest single step.)"})
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def summary(items: list[dict]) -> dict:
|
|
123
|
+
todo = [i for i in items if not i["ok"]]
|
|
124
|
+
return {"checked": len(items), "todo": len(todo), "titles": [i["title"] for i in todo]}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ── running it ──────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
def _in_wsl() -> bool:
|
|
129
|
+
try:
|
|
130
|
+
with open("/proc/version", encoding="utf-8") as f:
|
|
131
|
+
return "microsoft" in f.read().lower()
|
|
132
|
+
except OSError:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def powershell() -> str | None:
|
|
137
|
+
"""The PowerShell that reads Windows: native, or the host's from WSL. None off Windows."""
|
|
138
|
+
if sys.platform == "win32":
|
|
139
|
+
return shutil.which("powershell.exe") or shutil.which("powershell") or os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
140
|
+
if _in_wsl():
|
|
141
|
+
return shutil.which("powershell.exe") or ("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" if os.path.exists("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe") else None)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _run_ps(ps: str, script: str, timeout: int = 60) -> subprocess.CompletedProcess:
|
|
146
|
+
flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
147
|
+
return subprocess.run([ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], # noqa: S603
|
|
148
|
+
check=False, stdin=subprocess.DEVNULL, capture_output=True, text=True, errors="replace",
|
|
149
|
+
timeout=timeout, creationflags=flags, cwd="/mnt/c" if _in_wsl() and os.path.isdir("/mnt/c") else None)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def read_status(run: Callable[[str, str], subprocess.CompletedProcess] = _run_ps, ps: str | None = None) -> dict:
|
|
153
|
+
ps = ps or powershell()
|
|
154
|
+
if not ps:
|
|
155
|
+
return {"supported": False}
|
|
156
|
+
r = run(ps, PROBE_PS)
|
|
157
|
+
out = (r.stdout or "").strip().splitlines()
|
|
158
|
+
try:
|
|
159
|
+
return {"supported": True, **json.loads(out[-1] if out else "")}
|
|
160
|
+
except (ValueError, IndexError):
|
|
161
|
+
return {"supported": True, "error": "Windows did not answer the check" + (f" ({(r.stderr or '').strip()[:120]})" if r.stderr else "")}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def fix(item_id: str, run: Callable[[str, str], subprocess.CompletedProcess] = _run_ps, ps: str | None = None) -> dict:
|
|
165
|
+
"""Apply one fix: an admin change through Windows' UAC prompt, or Windows' own page for it. → {ok, why?}"""
|
|
166
|
+
f = FIXES.get(item_id)
|
|
167
|
+
ps = ps or powershell()
|
|
168
|
+
if not f or not ps:
|
|
169
|
+
return {"ok": False, "why": "nothing here can change that"}
|
|
170
|
+
if "open" in f:
|
|
171
|
+
r = run(ps, f"Start-Process '{f['open']}'", 30)
|
|
172
|
+
return {"ok": r.returncode == 0, "opened": True, **({} if r.returncode == 0 else {"why": "Windows did not open the page"})}
|
|
173
|
+
# Elevated: Windows shows its admin prompt; -Wait so the check that follows reads the result. A No at the prompt fails Start-Process.
|
|
174
|
+
inner = f["ps"].replace("'", "''")
|
|
175
|
+
script = (f"try {{ $p = Start-Process powershell -Verb RunAs -Wait -PassThru -WindowStyle Hidden "
|
|
176
|
+
f"-ArgumentList '-NoProfile','-Command','{inner}'; exit $p.ExitCode }} catch {{ exit 1223 }}")
|
|
177
|
+
r = run(ps, script, 300)
|
|
178
|
+
if r.returncode == 1223:
|
|
179
|
+
return {"ok": False, "why": "the admin prompt was declined — nothing changed"}
|
|
180
|
+
return {"ok": r.returncode == 0, **({} if r.returncode == 0 else {"why": f"Windows refused it (code {r.returncode})"})}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class SecurityCheck:
|
|
184
|
+
"""{supported, items, summary, at, os, error?} for the page; read at most once a minute, again right after a fix."""
|
|
185
|
+
|
|
186
|
+
TTL = 60
|
|
187
|
+
|
|
188
|
+
def __init__(self, *, read: Callable[[], dict] | None = None, apply: Callable[[str], dict] | None = None,
|
|
189
|
+
now: Callable[[], float] = time.monotonic) -> None:
|
|
190
|
+
self._read = read or (lambda: read_status()) # looked up at call time
|
|
191
|
+
self._apply = apply or (lambda i: fix(i))
|
|
192
|
+
self._now = now
|
|
193
|
+
self._cache: tuple[float, dict] | None = None
|
|
194
|
+
self._task: asyncio.Task | None = None
|
|
195
|
+
|
|
196
|
+
def status(self, fresh: bool = False) -> dict:
|
|
197
|
+
if not fresh and self._cache and self._now() - self._cache[0] < self.TTL:
|
|
198
|
+
return self._cache[1]
|
|
199
|
+
try:
|
|
200
|
+
raw = self._read()
|
|
201
|
+
except Exception as e: # noqa: BLE001 — a reading, never a failure of the page
|
|
202
|
+
raw = {"supported": True, "error": f"could not check ({str(e)[:80]})"}
|
|
203
|
+
if not raw.get("supported"):
|
|
204
|
+
out = {"supported": False}
|
|
205
|
+
else:
|
|
206
|
+
items = recommendations(raw if not raw.get("error") else None)
|
|
207
|
+
out = {"supported": True, "items": items, "summary": summary(items), "os": raw.get("os") or "",
|
|
208
|
+
"at": int(time.time()), **({"error": raw["error"]} if raw.get("error") else {})}
|
|
209
|
+
self._cache = (self._now(), out)
|
|
210
|
+
return out
|
|
211
|
+
|
|
212
|
+
def peek(self) -> dict:
|
|
213
|
+
"""What the page shows NOW, never waiting on Windows (a read takes seconds): the last reading, and a fresh one started
|
|
214
|
+
in the background when it is stale. {"checking": True} before the first one lands."""
|
|
215
|
+
stale = not self._cache or self._now() - self._cache[0] >= self.TTL
|
|
216
|
+
if stale and not (self._task and not self._task.done()):
|
|
217
|
+
try:
|
|
218
|
+
self._task = asyncio.get_running_loop().create_task(asyncio.to_thread(self.status, True))
|
|
219
|
+
except RuntimeError:
|
|
220
|
+
pass
|
|
221
|
+
return self._cache[1] if self._cache else {"checking": True}
|
|
222
|
+
|
|
223
|
+
async def fix(self, item_id: str) -> dict:
|
|
224
|
+
r = await asyncio.to_thread(self._apply, item_id)
|
|
225
|
+
await asyncio.to_thread(self.status, True) # the card shows the result at once
|
|
226
|
+
return r
|
|
@@ -35,7 +35,7 @@ from importlib import resources
|
|
|
35
35
|
from pathlib import Path
|
|
36
36
|
from typing import Any, Awaitable, Callable
|
|
37
37
|
|
|
38
|
-
from . import __version__, devicelink, link, net, ports, qr, uitools, uninstall, winbg, wizard, wslstart
|
|
38
|
+
from . import __version__, devicelink, link, net, ports, qr, seccheck, uitools, uninstall, winbg, wizard, wslstart
|
|
39
39
|
from . import pair as pair_cmd
|
|
40
40
|
from . import pairings as pp
|
|
41
41
|
|
|
@@ -127,10 +127,12 @@ class Manager:
|
|
|
127
127
|
login_enable: Callable[[dict], tuple] = wslstart.startup_enable,
|
|
128
128
|
login_disable: Callable[[dict], bool] = wslstart.startup_disable,
|
|
129
129
|
port_owners: Callable[[int], list] | None = None, stop_pid: Callable[[int], bool] | None = None,
|
|
130
|
-
tools: uitools.Tools | None = None, label: dict | None = None, log: deque | None = None
|
|
130
|
+
tools: uitools.Tools | None = None, label: dict | None = None, log: deque | None = None,
|
|
131
|
+
security: seccheck.SecurityCheck | None = None) -> None:
|
|
131
132
|
self._serve, self._is_listening, self._pair, self._link = serve, is_listening, pair, link_node
|
|
132
133
|
self._load, self._keys_line = load_pairings, keys_line
|
|
133
134
|
self.tools = tools or uitools.Tools()
|
|
135
|
+
self.security = security or seccheck.SecurityCheck()
|
|
134
136
|
self._owners = port_owners or ports.port_owners
|
|
135
137
|
self._stop_pid = stop_pid or uninstall.stop_process
|
|
136
138
|
self._elsewhere_cache: tuple[float, dict | None] | None = None
|
|
@@ -214,7 +216,7 @@ class Manager:
|
|
|
214
216
|
serving = {"state": "elsewhere", "port": el["port"], "owners": el["owners"]}
|
|
215
217
|
return {"version": __version__, "computer": self.label, "paired": paired, "serving": serving, "pair": pair,
|
|
216
218
|
"login": self.login(), "busy": self.busy, "background": self.background,
|
|
217
|
-
"tools": await asyncio.to_thread(self.tools.status), "log": list(self.log)[-14:]}
|
|
219
|
+
"tools": await asyncio.to_thread(self.tools.status), "security": self.security.peek(), "log": list(self.log)[-14:]}
|
|
218
220
|
|
|
219
221
|
async def elsewhere(self, fresh: bool = False) -> dict | None:
|
|
220
222
|
"""{port, owners} when THIS person's other process holds OpenCode's port or the model proxy's (port + 100) while this
|
|
@@ -535,6 +537,7 @@ class Server:
|
|
|
535
537
|
"/api/login": lambda: self.m.set_login(bool(data.get("on"))),
|
|
536
538
|
"/api/quit": self.m.quit,
|
|
537
539
|
"/api/tools/install": self.m.tools.install,
|
|
540
|
+
"/api/security/fix": lambda: self.m.security.fix(str(data.get("id") or "")),
|
|
538
541
|
}
|
|
539
542
|
act = actions.get(path)
|
|
540
543
|
if not act:
|
|
@@ -51,3 +51,11 @@ footer { font-size: 13px; text-align: center; }
|
|
|
51
51
|
.actions:not(:has(button:not([hidden]))) { display: none; }
|
|
52
52
|
button.small { font-size: 13px; padding: 2px 10px; margin-left: 8px; }
|
|
53
53
|
pre.cmd { background: var(--off-bg); color: var(--text); padding: 8px 10px; border-radius: 8px; }
|
|
54
|
+
/* the security check (seccheck.py) */
|
|
55
|
+
p.small, div.small { font-size: 12.5px; }
|
|
56
|
+
.sec-list { list-style: none; padding: 0; margin: 8px 0; }
|
|
57
|
+
.sec-list li { padding: 8px 0; border-top: 1px solid var(--line, #e5e7eb); }
|
|
58
|
+
.sec-list li:first-child { border-top: 0; }
|
|
59
|
+
.sec-list li.todo .sec-head span { font-weight: 600; }
|
|
60
|
+
.sec-list li.ok { opacity: .7; }
|
|
61
|
+
.sec-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
@@ -97,6 +97,7 @@ function render(s) {
|
|
|
97
97
|
$('footerText').textContent = s.background ? 'runs in the background — Start menu → Witbitz Code opens this page' : 'keep its window open — closing it stops witbitz-code'
|
|
98
98
|
show($('quit'), !!s.background)
|
|
99
99
|
renderTools(s.tools, paired)
|
|
100
|
+
renderSecurity(s.security)
|
|
100
101
|
|
|
101
102
|
$('log').textContent = (s.log || []).join('\n')
|
|
102
103
|
}
|
|
@@ -161,6 +162,46 @@ function renderTools(t, paired) {
|
|
|
161
162
|
$('toolsLog').textContent = lines.join('\n')
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
// This computer's security check (seccheck.py): what Windows has, what to change, and a Fix for what can be fixed from here.
|
|
166
|
+
function renderSecurity(sec) {
|
|
167
|
+
const card = $('secCard')
|
|
168
|
+
if (!sec || sec.supported === false) return show(card, false)
|
|
169
|
+
show(card, true)
|
|
170
|
+
const items = sec.items || []
|
|
171
|
+
const todo = items.filter((i) => !i.ok)
|
|
172
|
+
$('secText').textContent = sec.checking ? 'Checking Windows…'
|
|
173
|
+
: sec.error ? sec.error
|
|
174
|
+
: todo.length ? `${todo.length} ${todo.length === 1 ? 'thing' : 'things'} to change` + (sec.os ? ` · ${sec.os}` : '')
|
|
175
|
+
: 'Everything checked here is set well ✓' + (sec.os ? ` · ${sec.os}` : '')
|
|
176
|
+
const list = $('secList')
|
|
177
|
+
const key = JSON.stringify(items) + '|' + !!(state && state.busy)
|
|
178
|
+
if (list.dataset.key === key) return
|
|
179
|
+
list.dataset.key = key
|
|
180
|
+
list.textContent = ''
|
|
181
|
+
for (const i of [...todo, ...items.filter((x) => x.ok)]) {
|
|
182
|
+
const li = document.createElement('li')
|
|
183
|
+
li.className = i.ok ? 'ok' : 'todo'
|
|
184
|
+
const head = document.createElement('div')
|
|
185
|
+
head.className = 'sec-head'
|
|
186
|
+
const t = document.createElement('span')
|
|
187
|
+
t.textContent = (i.ok ? '✓ ' : '! ') + i.title
|
|
188
|
+
head.append(t)
|
|
189
|
+
if (i.fix) {
|
|
190
|
+
const b = document.createElement('button')
|
|
191
|
+
b.type = 'button'
|
|
192
|
+
b.className = 'small'
|
|
193
|
+
b.textContent = i.fix === 'admin' ? 'Fix' : 'Open in Windows'
|
|
194
|
+
b.disabled = !!(state && state.busy)
|
|
195
|
+
b.onclick = () => act('/api/security/fix', { id: i.id },
|
|
196
|
+
i.fix === 'admin' ? `Change it now? Windows will ask for permission (an admin prompt) — say Yes there.` : '')
|
|
197
|
+
head.append(b)
|
|
198
|
+
}
|
|
199
|
+
li.append(head)
|
|
200
|
+
if (i.why) { const w = document.createElement('div'); w.className = 'muted small'; w.textContent = i.why; li.append(w) }
|
|
201
|
+
list.append(li)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
164
205
|
let quitting = false
|
|
165
206
|
async function quit() {
|
|
166
207
|
if (!window.confirm('Quit Witbitz Code? Code in Spaces will not reach this computer until you open Witbitz Code from the Start menu again.')) return
|
|
@@ -51,6 +51,14 @@
|
|
|
51
51
|
<pre id="toolsLog" class="cmd" hidden></pre>
|
|
52
52
|
</section>
|
|
53
53
|
|
|
54
|
+
<section class="card" id="secCard" hidden>
|
|
55
|
+
<h2>This computer's security</h2>
|
|
56
|
+
<p id="secText" class="muted"></p>
|
|
57
|
+
<ul id="secList" class="sec-list"></ul>
|
|
58
|
+
<p class="muted small">A check of Windows' settings, not a malware scan. It finds weak settings before something gets in;
|
|
59
|
+
malware already running here could hide from it.</p>
|
|
60
|
+
</section>
|
|
61
|
+
|
|
54
62
|
<section class="card" id="loginCard" hidden>
|
|
55
63
|
<label class="switch-row" for="login">
|
|
56
64
|
<span>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: witbitz-code
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
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
|
|
@@ -19,6 +19,7 @@ Requires-Dist: websockets>=13
|
|
|
19
19
|
Requires-Dist: httpx>=0.25
|
|
20
20
|
Requires-Dist: segno>=1.5
|
|
21
21
|
Requires-Dist: tinfoil<0.15,>=0.14
|
|
22
|
+
Requires-Dist: pillow>=10
|
|
22
23
|
Provides-Extra: test
|
|
23
24
|
Requires-Dist: pytest>=8; extra == "test"
|
|
24
25
|
Requires-Dist: pyflakes>=3; extra == "test"
|
|
@@ -21,6 +21,7 @@ src/witbitz_code/devicelink.py
|
|
|
21
21
|
src/witbitz_code/dirs.py
|
|
22
22
|
src/witbitz_code/folders.py
|
|
23
23
|
src/witbitz_code/gateway.py
|
|
24
|
+
src/witbitz_code/images.py
|
|
24
25
|
src/witbitz_code/keys.py
|
|
25
26
|
src/witbitz_code/link.py
|
|
26
27
|
src/witbitz_code/mkdir.py
|
|
@@ -37,6 +38,7 @@ src/witbitz_code/ports.py
|
|
|
37
38
|
src/witbitz_code/qr.py
|
|
38
39
|
src/witbitz_code/receipt.py
|
|
39
40
|
src/witbitz_code/relay.py
|
|
41
|
+
src/witbitz_code/seccheck.py
|
|
40
42
|
src/witbitz_code/seen.py
|
|
41
43
|
src/witbitz_code/tinfoil_read.py
|
|
42
44
|
src/witbitz_code/tools_catalog.json
|
|
@@ -77,6 +79,7 @@ tests/test_devicelink.py
|
|
|
77
79
|
tests/test_dirs.py
|
|
78
80
|
tests/test_files_and_compress.py
|
|
79
81
|
tests/test_folders.py
|
|
82
|
+
tests/test_images.py
|
|
80
83
|
tests/test_outputs.py
|
|
81
84
|
tests/test_pair_flows.py
|
|
82
85
|
tests/test_plugins.py
|
|
@@ -85,6 +88,7 @@ tests/test_ports.py
|
|
|
85
88
|
tests/test_ports_win.py
|
|
86
89
|
tests/test_registry.py
|
|
87
90
|
tests/test_relay_codec.py
|
|
91
|
+
tests/test_seccheck.py
|
|
88
92
|
tests/test_setup.py
|
|
89
93
|
tests/test_tools_probe.py
|
|
90
94
|
tests/test_toolsetup.py
|