witbitz-code 1.7.0__tar.gz → 1.7.1__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.7.0/src/witbitz_code.egg-info → witbitz_code-1.7.1}/PKG-INFO +1 -1
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/pyproject.toml +1 -1
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/auto_runner.py +68 -2
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/cli.py +2 -1
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/connector.py +7 -4
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/uninstall.py +1 -1
- {witbitz_code-1.7.0 → witbitz_code-1.7.1/src/witbitz_code.egg-info}/PKG-INFO +1 -1
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_auto.py +111 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_uninstall.py +1 -1
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/LICENSE +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/README.md +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/setup.cfg +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/confidential.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/dirs.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/icons/witbitz-code.ico +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/images.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/mkdir.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/seccheck.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/tools_catalog.json +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/toolsetup.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/ui.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/ui_static/app.css +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/ui_static/app.js +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/ui_static/index.html +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/uitools.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/winagent.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/winbg.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/wizard.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/wslstart.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_asks.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_attachments.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_cli.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_codetools.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_confidential.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_dirs.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_folders.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_images.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_outputs.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_plugins.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_policy.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_ports.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_ports_win.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_registry.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_seccheck.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_setup.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_toolsetup.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_ui.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_uitools.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_winagent.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_winbg.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_windows_files.py +0 -0
- {witbitz_code-1.7.0 → witbitz_code-1.7.1}/tests/test_wslstart.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.7.
|
|
7
|
+
version = "1.7.1"
|
|
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"
|
|
@@ -22,6 +22,7 @@ import asyncio
|
|
|
22
22
|
import math
|
|
23
23
|
import os
|
|
24
24
|
import re
|
|
25
|
+
import secrets
|
|
25
26
|
import stat
|
|
26
27
|
import time
|
|
27
28
|
from collections.abc import Awaitable, Callable
|
|
@@ -42,6 +43,15 @@ MAX_USER_MESSAGES = 6
|
|
|
42
43
|
MAX_DETAIL = 300
|
|
43
44
|
PARENT_TTL_MS = 10 * 60_000
|
|
44
45
|
AGENTS_TTL_MS = 60_000
|
|
46
|
+
# THE REVIEWER'S MODEL (the owner, 2026-10-13: "I am not sure if the auto mode classifier is working correctly"). Reviewed as
|
|
47
|
+
# an OpenCode session with the session's own model, a review carried OpenCode's whole agent prompt (~35k chars) ahead of ours
|
|
48
|
+
# and the model's full reasoning: DeepSeek V4.1 Flash went from a 5 s median to 30 s timeouts, and every ask fell to the
|
|
49
|
+
# person. Where the computer runs the model proxy (`model_url`), the policy is now asked DIRECTLY, of a fixed fast confidential
|
|
50
|
+
# model, with a backup. Measured on 8 cases: GPT-OSS 120B 8/8 in 2.5–3.5 s, DeepSeek V4.1 Flash 8/8 in 5–16 s. Any failure is
|
|
51
|
+
# still the person's. Held equal to tools/code-auto-runner.mjs.
|
|
52
|
+
REVIEW_MODELS = ("openai/gpt-oss-120b", "deepseek/deepseek-v4.1-flash")
|
|
53
|
+
REVIEW_TIMEOUT_MS = 45_000 # the whole review, backup included
|
|
54
|
+
FIRST_TRY_MS = 20_000 # the first model's share, so a hung one leaves the backup time to answer
|
|
45
55
|
MAX_DEPTH = 4 # parent links followed looking for the session in Auto
|
|
46
56
|
REFUSAL_SUFFIX = "(Witbitz Auto mode refused this — try a narrower or safer step.)"
|
|
47
57
|
_UNKNOWN = object() # OpenCode could not say who a session's parent is (not cached)
|
|
@@ -107,11 +117,15 @@ def scratch_on_disk(paths: list[str], directory: str = SCRATCH_DIR) -> bool:
|
|
|
107
117
|
|
|
108
118
|
class AutoRunner:
|
|
109
119
|
def __init__(self, *, base: str, auth: Callable[[], dict], client: httpx.AsyncClient, state_path: Path | None,
|
|
110
|
-
log_path: Path | None, poll_ms: float = 1000, review_timeout_ms: float =
|
|
120
|
+
log_path: Path | None, poll_ms: float = 1000, review_timeout_ms: float = REVIEW_TIMEOUT_MS, home: str | None = None,
|
|
111
121
|
on_verdict: Callable[[dict], Any] = lambda v: None, log: Callable[[str], Any] = lambda m: None,
|
|
112
122
|
now: Callable[[], int] = _now_ms, scratch_check: Callable[[list[str]], bool] = scratch_on_disk,
|
|
113
123
|
list_asks: Callable[[str], Awaitable[list | None]] | None = None,
|
|
114
|
-
on_review: Callable[[str, bool], Any] | None = None
|
|
124
|
+
on_review: Callable[[str, bool], Any] | None = None, model_url: str = "",
|
|
125
|
+
review_models: tuple[str, ...] | list[str] = REVIEW_MODELS, first_try_ms: float = FIRST_TRY_MS) -> None:
|
|
126
|
+
self._model_url = re.sub(r"/+\Z", "", model_url or "")
|
|
127
|
+
self._review_models = list(review_models)
|
|
128
|
+
self._first_try_ms = first_try_ms
|
|
115
129
|
self._root = re.sub(r"/+\Z", "", base or "")
|
|
116
130
|
# The review session's model calls are billed to the account that turned Auto on (payers.py): told when the
|
|
117
131
|
# review opens and closes, since nothing from the Spaces app ever posts into it.
|
|
@@ -291,8 +305,60 @@ class AutoRunner:
|
|
|
291
305
|
return []
|
|
292
306
|
return allowed_folders(data.get("permission")) if ok and isinstance(data, dict) else []
|
|
293
307
|
|
|
308
|
+
async def _ask_model(self, m: str, prompt: dict, rid: str) -> tuple[dict | None, str]:
|
|
309
|
+
body = {"model": m, "messages": [{"role": "system", "content": prompt["system"]}, {"role": "user", "content": prompt["text"]}],
|
|
310
|
+
"max_tokens": 1500, "reasoning_effort": "low", "stream": False}
|
|
311
|
+
r = await self._client.post(f"{self._model_url}/chat/completions", headers={"content-type": "application/json", "x-session-id": rid},
|
|
312
|
+
content=_js.utf8(_js.stringify(body)))
|
|
313
|
+
try:
|
|
314
|
+
data = _js.parse(r.text)
|
|
315
|
+
except ValueError:
|
|
316
|
+
data = None
|
|
317
|
+
choices = data.get("choices") if isinstance(data, dict) and isinstance(data.get("choices"), list) else []
|
|
318
|
+
msg = choices[0].get("message") if choices and isinstance(choices[0], dict) and isinstance(choices[0].get("message"), dict) else {}
|
|
319
|
+
text = msg.get("content") if 200 <= r.status_code < 300 and isinstance(msg.get("content"), str) else ""
|
|
320
|
+
verdict = parse_verdict(text) if text else None
|
|
321
|
+
if verdict:
|
|
322
|
+
return verdict, ""
|
|
323
|
+
return None, f"answered {r.status_code}" if not 200 <= r.status_code < 300 else "no verdict in its answer" if text else "no answer"
|
|
324
|
+
|
|
325
|
+
async def _review_direct(self, prompt: dict) -> tuple[dict | None, str, str]:
|
|
326
|
+
"""Ask the policy of the review models in turn, through the computer's model proxy (billed like the session: on_review)."""
|
|
327
|
+
rid = "ses_review_" + secrets.token_urlsafe(9)
|
|
328
|
+
until = time.monotonic() + self._review_timeout_ms / 1000
|
|
329
|
+
notes: list[str] = []
|
|
330
|
+
if self._on_review:
|
|
331
|
+
self._on_review(rid, True)
|
|
332
|
+
try:
|
|
333
|
+
for i, m in enumerate(self._review_models):
|
|
334
|
+
left = until - time.monotonic()
|
|
335
|
+
if left <= 0:
|
|
336
|
+
break
|
|
337
|
+
limit = left if i == len(self._review_models) - 1 else min(left, self._first_try_ms / 1000)
|
|
338
|
+
try:
|
|
339
|
+
verdict, why = await asyncio.wait_for(self._ask_model(m, prompt, rid), limit)
|
|
340
|
+
except asyncio.TimeoutError:
|
|
341
|
+
verdict, why = None, "did not answer in time"
|
|
342
|
+
except asyncio.CancelledError:
|
|
343
|
+
raise
|
|
344
|
+
except Exception:
|
|
345
|
+
verdict, why = None, "could not be reached"
|
|
346
|
+
if verdict:
|
|
347
|
+
return verdict, f"trustedrouter/{m}", ""
|
|
348
|
+
notes.append(f"{m}: {why}")
|
|
349
|
+
finally:
|
|
350
|
+
if self._on_review:
|
|
351
|
+
self._on_review(rid, False)
|
|
352
|
+
first = self._review_models[0] if self._review_models else ""
|
|
353
|
+
secs = math.floor(self._review_timeout_ms / 1000 + 0.5) or 1
|
|
354
|
+
return None, f"trustedrouter/{first}", f"the reviewer did not answer ({'; '.join(notes) or f'nothing within {secs} s'})"
|
|
355
|
+
|
|
294
356
|
async def _review(self, req: dict, directory: str, owner: str | None = None, folders: list[str] | None = None) -> tuple[dict | None, str, str]:
|
|
295
357
|
model, user_messages, tools = await self._session_context(owner or req["sessionID"], directory)
|
|
358
|
+
if self._model_url and self._review_models:
|
|
359
|
+
call = req.get("tool") if isinstance(req.get("tool"), dict) else {}
|
|
360
|
+
tool = tools.get(call["callID"], "") if isinstance(call.get("callID"), str) else ""
|
|
361
|
+
return await self._review_direct(reviewer_prompt(req, directory, user_messages, tool, folders or []))
|
|
296
362
|
if not model:
|
|
297
363
|
return None, "", "the session has no model to review with yet"
|
|
298
364
|
label = f"{_js.js_string(model['providerID'])}/{_js.js_string(model['modelID'])}"
|
|
@@ -261,7 +261,8 @@ async def serve(args: list[str], *, stop: Any = None, live_out: dict | None = No
|
|
|
261
261
|
return 0
|
|
262
262
|
if child is not None:
|
|
263
263
|
_err("witbitz-code: model calls are paid by each account's own coding keys; confidential models are enforced (min_privacy + verified receipts)")
|
|
264
|
-
|
|
264
|
+
# Auto mode's reviewer asks the policy of the model proxy directly (auto_runner.REVIEW_MODELS), billed like the session.
|
|
265
|
+
connector = await start_connector(pairings, payers=payers, model_url=f"http://127.0.0.1:{proxy.port}/v1" if proxy else "")
|
|
265
266
|
live["status"] = asyncio.ensure_future(_check_keys_every(live_keys, keys_http))
|
|
266
267
|
live["connector"] = connector
|
|
267
268
|
if live_out is not None:
|
|
@@ -157,7 +157,8 @@ class PairingServer:
|
|
|
157
157
|
max_response_bytes: int = MAX_RESPONSE, auto_dir: Path | None = None, auto_poll_ms: float = 1000,
|
|
158
158
|
attach_root: Path | None = None, attach_max_file_bytes: int = MAX_FILE_BYTES,
|
|
159
159
|
asks_reconcile_ms: float = ASKS_RECONCILE_MS, notes_root: str | None = None,
|
|
160
|
-
notes_plugin_path: Path | None = None, payers: SessionPayers | None = None
|
|
160
|
+
notes_plugin_path: Path | None = None, payers: SessionPayers | None = None,
|
|
161
|
+
model_url: str = "") -> None:
|
|
161
162
|
self.pairing = pairing
|
|
162
163
|
self._security: seccheck.SecurityCheck | None = None # read on the first ask (seccheck.py)
|
|
163
164
|
# WHO PAYS (payers.py): this pairing's account owns the turns it starts; the proxy bills each call to its owner.
|
|
@@ -212,7 +213,8 @@ class PairingServer:
|
|
|
212
213
|
self.auto = AutoRunner(base=self.base, auth=self._auth, client=client, poll_ms=auto_poll_ms, log=log,
|
|
213
214
|
state_path=auto_dir / f"auto-{safe_id}.json", log_path=auto_dir / "auto-log.jsonl",
|
|
214
215
|
on_verdict=lambda v: self.peer.send({"t": "autoverdict", **v, "ts": int(time.time() * 1000)}),
|
|
215
|
-
list_asks=self._pending_asks, on_review=self._own_review
|
|
216
|
+
list_asks=self._pending_asks, on_review=self._own_review,
|
|
217
|
+
model_url=model_url) # the reviewer asks the model proxy directly (auto_runner.REVIEW_MODELS)
|
|
216
218
|
|
|
217
219
|
# ── lifecycle ─────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
218
220
|
async def start(self) -> None:
|
|
@@ -880,7 +882,8 @@ async def start_connector(pairings: list[dict], *, flush_ms: float = 120, log: C
|
|
|
880
882
|
max_response_bytes: int = MAX_RESPONSE, auto_dir: Path | None = None, auto_poll_ms: float = 1000,
|
|
881
883
|
attach_root: Path | None = None, attach_max_file_bytes: int = MAX_FILE_BYTES,
|
|
882
884
|
asks_reconcile_ms: float = ASKS_RECONCILE_MS, notes_root: str | None = None,
|
|
883
|
-
notes_plugin_path: Path | None = None, payers: SessionPayers | None = None
|
|
885
|
+
notes_plugin_path: Path | None = None, payers: SessionPayers | None = None,
|
|
886
|
+
model_url: str = "") -> Connector:
|
|
884
887
|
"""Serve the given pairings until stop()/aclose(). Options exist for tests: flush_ms, log, client, connect,
|
|
885
888
|
request_timeout_ms, max_senders, max_response_bytes, auto_dir, auto_poll_ms."""
|
|
886
889
|
client = client or local_client()
|
|
@@ -895,7 +898,7 @@ async def start_connector(pairings: list[dict], *, flush_ms: float = 120, log: C
|
|
|
895
898
|
s = PairingServer(p, client=client, flush_ms=flush_ms, log=log, connect=connect, request_timeout_ms=request_timeout_ms,
|
|
896
899
|
max_senders=max_senders, max_response_bytes=max_response_bytes, auto_dir=auto_dir, auto_poll_ms=auto_poll_ms,
|
|
897
900
|
attach_root=attach_root, attach_max_file_bytes=attach_max_file_bytes, asks_reconcile_ms=asks_reconcile_ms,
|
|
898
|
-
notes_root=notes_root, notes_plugin_path=notes_plugin_path, payers=payers)
|
|
901
|
+
notes_root=notes_root, notes_plugin_path=notes_plugin_path, payers=payers, model_url=model_url)
|
|
899
902
|
await s.start()
|
|
900
903
|
servers.append(s)
|
|
901
904
|
return Connector(servers, client, payers)
|
|
@@ -302,7 +302,7 @@ def remove_opencode_program(info: dict) -> dict:
|
|
|
302
302
|
return {"ok": False, "why": f"{info['path']} is still there", "said": said} if os.path.exists(info["path"]) else {"ok": True, "said": said}
|
|
303
303
|
|
|
304
304
|
|
|
305
|
-
WINDOWS_REINSTALL = "
|
|
305
|
+
WINDOWS_REINSTALL = "the install lines at https://witbitz.chat/code (or Code in your Spaces app)" # app-neutral: each app serves its own code.ps1
|
|
306
306
|
|
|
307
307
|
|
|
308
308
|
def windows_installer_root() -> str:
|
|
@@ -700,3 +700,114 @@ def test_auto_switches_on_for_a_network_share(tmp_path):
|
|
|
700
700
|
async def body(oc, runner, verdicts, make, client):
|
|
701
701
|
assert runner.set_auto("ses_main", _UNC, True)
|
|
702
702
|
with_runner(tmp_path, body)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
# ── THE DIRECT REVIEWER (2026-10-13; = tools/code-auto-runner.test.mjs): with the computer's model proxy, the policy is asked
|
|
706
|
+
# of a fixed confidential model, then a backup — never through an OpenCode session.
|
|
707
|
+
ALLOW = '{"decision":"allow","severity":10,"rule":"allow:tests","reason":"the user asked for tests"}'
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
class FakeProxy:
|
|
711
|
+
def __init__(self, answers: dict) -> None:
|
|
712
|
+
self.seen: list[dict] = []
|
|
713
|
+
outer = self
|
|
714
|
+
|
|
715
|
+
class H(BaseHTTPRequestHandler):
|
|
716
|
+
def log_message(self, *a: Any) -> None:
|
|
717
|
+
pass
|
|
718
|
+
|
|
719
|
+
def do_POST(self) -> None:
|
|
720
|
+
b = json.loads(self.rfile.read(int(self.headers.get("content-length") or 0)))
|
|
721
|
+
outer.seen.append({"path": self.path, "sid": self.headers.get("x-session-id"), "body": b})
|
|
722
|
+
a = answers.get(b["model"], {"status": 404})
|
|
723
|
+
if a.get("delay"):
|
|
724
|
+
time.sleep(a["delay"])
|
|
725
|
+
data = json.dumps({"choices": [{"message": {"role": "assistant", "content": a["text"]}}]} if "text" in a
|
|
726
|
+
else {"error": {"message": "provider error"}}).encode()
|
|
727
|
+
try:
|
|
728
|
+
self.send_response(a.get("status", 200))
|
|
729
|
+
self.send_header("content-type", "application/json")
|
|
730
|
+
self.send_header("content-length", str(len(data)))
|
|
731
|
+
self.end_headers()
|
|
732
|
+
self.wfile.write(data)
|
|
733
|
+
except OSError:
|
|
734
|
+
pass
|
|
735
|
+
|
|
736
|
+
self.httpd = ThreadingHTTPServer(("127.0.0.1", 0), H)
|
|
737
|
+
self.httpd.daemon_threads = True
|
|
738
|
+
threading.Thread(target=self.httpd.serve_forever, daemon=True).start()
|
|
739
|
+
self.url = f"http://127.0.0.1:{self.httpd.server_address[1]}/v1"
|
|
740
|
+
|
|
741
|
+
def close(self) -> None:
|
|
742
|
+
self.httpd.shutdown()
|
|
743
|
+
self.httpd.server_close()
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def direct_world(tmp_path: Path, answers: dict, check, **extra: Any) -> None:
|
|
747
|
+
px = FakeProxy(answers)
|
|
748
|
+
oc = LoopOpenCode()
|
|
749
|
+
verdicts: list[dict] = []
|
|
750
|
+
billed: list = []
|
|
751
|
+
|
|
752
|
+
async def go():
|
|
753
|
+
async with httpx.AsyncClient(trust_env=False) as client:
|
|
754
|
+
runner = AutoRunner(base=oc.url, auth=lambda: {"authorization": "Basic x"}, client=client, state_path=tmp_path / "auto.json",
|
|
755
|
+
log_path=tmp_path / "auto-log.jsonl", poll_ms=30, home="/home/u", on_verdict=verdicts.append,
|
|
756
|
+
on_review=lambda rid, start: billed.append((rid, start)), model_url=px.url, **extra)
|
|
757
|
+
runner.start()
|
|
758
|
+
try:
|
|
759
|
+
runner.set_auto("ses_main", DIR, True)
|
|
760
|
+
oc.ask(permission="bash", patterns=["npm test"], metadata={"command": "npm test"})
|
|
761
|
+
await check(oc, px, verdicts, billed)
|
|
762
|
+
finally:
|
|
763
|
+
runner.stop()
|
|
764
|
+
|
|
765
|
+
try:
|
|
766
|
+
run(go(), timeout=60)
|
|
767
|
+
finally:
|
|
768
|
+
oc.close()
|
|
769
|
+
px.close()
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def test_direct_gpt_oss_is_asked_directly_no_opencode_session_low_effort_billed_like_the_session(tmp_path):
|
|
773
|
+
async def check(oc, px, verdicts, billed):
|
|
774
|
+
assert await until(lambda: oc.replies(), 6)
|
|
775
|
+
assert oc.replies()[0]["body"] == {"reply": "once"}
|
|
776
|
+
assert oc.calls("POST", "/session") == [], "no review session"
|
|
777
|
+
c = px.seen[0]
|
|
778
|
+
assert c["path"] == "/v1/chat/completions"
|
|
779
|
+
assert (c["body"]["model"], c["body"]["reasoning_effort"], c["body"]["stream"]) == ("openai/gpt-oss-120b", "low", False)
|
|
780
|
+
assert "HARD DENY" in c["body"]["messages"][0]["content"]
|
|
781
|
+
assert "npm test" in c["body"]["messages"][1]["content"] and "please run the test suite" in c["body"]["messages"][1]["content"]
|
|
782
|
+
assert c["sid"].startswith("ses_review_")
|
|
783
|
+
assert billed == [(c["sid"], True), (c["sid"], False)]
|
|
784
|
+
assert await until(lambda: verdicts, 2)
|
|
785
|
+
assert verdicts[0]["stage"] == "reviewer"
|
|
786
|
+
direct_world(tmp_path, {"openai/gpt-oss-120b": {"text": ALLOW}}, check)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def test_direct_the_backup_answers_when_gpt_oss_fails_and_the_log_names_it(tmp_path):
|
|
790
|
+
async def check(oc, px, verdicts, billed):
|
|
791
|
+
assert await until(lambda: oc.replies(), 6)
|
|
792
|
+
assert [s["body"]["model"] for s in px.seen] == ["openai/gpt-oss-120b", "deepseek/deepseek-v4.1-flash"]
|
|
793
|
+
assert await until(lambda: log_lines(tmp_path), 2)
|
|
794
|
+
assert log_lines(tmp_path)[0]["model"] == "trustedrouter/deepseek/deepseek-v4.1-flash"
|
|
795
|
+
direct_world(tmp_path, {"openai/gpt-oss-120b": {"status": 502}, "deepseek/deepseek-v4.1-flash": {"text": ALLOW}}, check)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def test_direct_a_first_model_that_hangs_leaves_the_backup_its_time(tmp_path):
|
|
799
|
+
async def check(oc, px, verdicts, billed):
|
|
800
|
+
assert await until(lambda: oc.replies(), 6)
|
|
801
|
+
assert oc.replies()[0]["body"] == {"reply": "once"} and len(px.seen) == 2
|
|
802
|
+
direct_world(tmp_path, {"openai/gpt-oss-120b": {"text": ALLOW, "delay": 2}, "deepseek/deepseek-v4.1-flash": {"text": ALLOW}}, check,
|
|
803
|
+
first_try_ms=150, review_timeout_ms=1500)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def test_direct_both_failing_leaves_it_for_the_person_saying_why(tmp_path):
|
|
807
|
+
async def check(oc, px, verdicts, billed):
|
|
808
|
+
assert await until(lambda: verdicts, 6)
|
|
809
|
+
assert verdicts[0]["action"] == "ask"
|
|
810
|
+
assert "gpt-oss-120b: answered 502; deepseek/deepseek-v4.1-flash: no verdict in its answer" in verdicts[0]["reason"]
|
|
811
|
+
await asyncio.sleep(0.2)
|
|
812
|
+
assert oc.replies() == []
|
|
813
|
+
direct_world(tmp_path, {"openai/gpt-oss-120b": {"status": 502}, "deepseek/deepseek-v4.1-flash": {"text": "sure, fine"}}, check)
|
|
@@ -223,7 +223,7 @@ def test_under_the_windows_installer_the_summary_says_what_that_uninstaller_does
|
|
|
223
223
|
assert r["done"]
|
|
224
224
|
for wrong in ("pip uninstall", "pipx", "OpenCode is still installed", "still running in a terminal window"):
|
|
225
225
|
assert wrong not in said, f"{wrong!r} does not apply to the Windows installer:\n{said}"
|
|
226
|
-
assert "
|
|
226
|
+
assert "https://witbitz.chat/code" in said and "app.witbitz.chat" not in said, said
|
|
227
227
|
assert ("package",) not in f.done, "the Windows uninstaller removes the program itself"
|
|
228
228
|
|
|
229
229
|
|
|
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.7.0 → witbitz_code-1.7.1}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.7.0 → witbitz_code-1.7.1}/src/witbitz_code/opencode_plugins/witbitz-notes.js
RENAMED
|
File without changes
|
{witbitz_code-1.7.0 → witbitz_code-1.7.1}/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
|