zerochat 7.10.0__tar.gz → 7.12.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zerochat
3
- Version: 7.10.0
3
+ Version: 7.12.0
4
4
  Summary: Private local web client for AI models, tools and MCP services
5
5
  Author: Alberto Alday
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zerochat"
7
- version = "7.10.0"
7
+ version = "7.12.0"
8
8
  description = "Private local web client for AI models, tools and MCP services"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zerochat
3
- Version: 7.10.0
3
+ Version: 7.12.0
4
4
  Summary: Private local web client for AI models, tools and MCP services
5
5
  Author: Alberto Alday
6
6
  License-Expression: MIT
@@ -14,8 +14,10 @@ from __future__ import annotations
14
14
  import argparse
15
15
  import ast
16
16
  import atexit
17
+ import base64
17
18
  import datetime
18
19
  import fnmatch
20
+ import hashlib
19
21
  import hmac
20
22
  import importlib.metadata
21
23
  import json
@@ -40,7 +42,7 @@ import webbrowser
40
42
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
41
43
  from pathlib import Path
42
44
 
43
- SOURCE_BACKEND_VERSION = "7.10.0"
45
+ SOURCE_BACKEND_VERSION = "7.12.0"
44
46
 
45
47
  def _read_source_version(filename: str) -> str | None:
46
48
  """Lee la versión de un archivo del repositorio cuando se ejecuta desde fuentes."""
@@ -96,7 +98,8 @@ PYPI_VERSION_URL = "https://pypi.org/pypi/zerochat/json"
96
98
  REMOTE_SCRIPT_URL = "https://raw.githubusercontent.com/albalday/zerochat/master/zerochat.py"
97
99
  CONSOLE_STATUS_IDLE_SECONDS = 8.0
98
100
  CONSOLE_CONTROL = None
99
-
101
+ NOTICES: list[str] = []
102
+ NOTICES_LOCK = threading.Lock()
100
103
 
101
104
  def format_uptime(seconds: float) -> str:
102
105
  """Devuelve una duración breve y estable para la línea de estado de consola."""
@@ -108,9 +111,10 @@ def format_uptime(seconds: float) -> str:
108
111
 
109
112
  class ConsoleControl:
110
113
  """Atajos de consola y línea de estado, solo para terminales interactivos."""
111
- def __init__(self, server: ThreadingHTTPServer, parser: argparse.ArgumentParser):
114
+ def __init__(self, server: ThreadingHTTPServer, parser: argparse.ArgumentParser, target_url: str | None = None):
112
115
  self.server = server
113
116
  self.parser = parser
117
+ self.target_url = target_url
114
118
  self.started_at = time.monotonic()
115
119
  self.last_activity = self.started_at
116
120
  self.stop_event = threading.Event()
@@ -190,10 +194,27 @@ class ConsoleControl:
190
194
  if self.status_visible:
191
195
  sys.stdout.write("\r\033[2K")
192
196
  self.status_visible = False
193
- print("\nComandos de consola: [h] ayuda · [x] salir ordenadamente\n", flush=True)
197
+ commands = "[h] ayuda · [n] Navegador"
198
+ if get_notices():
199
+ commands += " · [i] información"
200
+ print(f"\nComandos de consola: {commands} · [x] salir ordenadamente\n", flush=True)
194
201
  print(self.parser.format_help().rstrip(), flush=True)
195
202
  self.last_activity = time.monotonic()
196
203
 
204
+ def show_notices(self):
205
+ notices = get_notices()
206
+ if not notices:
207
+ return
208
+ with self.lock:
209
+ if self.status_visible:
210
+ sys.stdout.write("\r\033[2K")
211
+ self.status_visible = False
212
+ print("\nInformación:", flush=True)
213
+ for notice in notices:
214
+ print(f"- {notice}", flush=True)
215
+ print(flush=True)
216
+ self.last_activity = time.monotonic()
217
+
197
218
  def _render_status(self):
198
219
  if not self.enabled:
199
220
  return
@@ -201,7 +222,10 @@ class ConsoleControl:
201
222
  with self.lock:
202
223
  if time.monotonic() - self.last_activity < CONSOLE_STATUS_IDLE_SECONDS:
203
224
  return
204
- sys.stdout.write(f"\r\033[2KZeroChat activo {uptime} · [h] ayuda · [x] salir")
225
+ commands = "[h] ayuda · [n] Navegador"
226
+ if get_notices():
227
+ commands += " · [i] información"
228
+ sys.stdout.write(f"\r\033[2KZeroChat activo {uptime} · {commands} · [x] salir")
205
229
  sys.stdout.flush()
206
230
  self.status_visible = True
207
231
 
@@ -212,6 +236,11 @@ class ConsoleControl:
212
236
  def _handle_key(self, key: str):
213
237
  if key.lower() == "h":
214
238
  self.show_help()
239
+ elif key.lower() == "n":
240
+ if self.target_url:
241
+ launch_browser(self.target_url)
242
+ elif key.lower() == "i":
243
+ self.show_notices()
215
244
  elif key.lower() == "x":
216
245
  self.log(f"[{time.strftime('%H:%M:%S')}] Deteniendo servidor ZeroChat...")
217
246
  stop_zerochat_server(self.server)
@@ -278,6 +307,26 @@ def get_venv_dir() -> Path:
278
307
  return get_data_dir() / ".venv"
279
308
 
280
309
 
310
+ def reset_notices():
311
+ """Reinicia los avisos transitorios de la ejecución actual."""
312
+ with NOTICES_LOCK:
313
+ NOTICES.clear()
314
+
315
+
316
+ def add_notice(message: str):
317
+ """Añade un aviso para la consola interactiva de la ejecución actual."""
318
+ if not isinstance(message, str) or not message.strip():
319
+ raise ValueError("El aviso debe ser texto no vacío")
320
+ with NOTICES_LOCK:
321
+ NOTICES.append(message.strip())
322
+
323
+
324
+ def get_notices() -> tuple[str, ...]:
325
+ """Devuelve una instantánea inmutable de los avisos actuales."""
326
+ with NOTICES_LOCK:
327
+ return tuple(NOTICES)
328
+
329
+
281
330
  def get_daily_token() -> str:
282
331
  """Devuelve un token de sesión diario persistido en ~/zerochat/config/token.json."""
283
332
  config_dir = get_data_dir() / "config"
@@ -306,6 +355,16 @@ SESSION_TOKEN = get_daily_token()
306
355
  ACTIVE_PORT = DEFAULT_PORT
307
356
  ACTIVE_HOST = DEFAULT_HOST
308
357
 
358
+ # Kept only for this Python process: binds an approved browser tool call to
359
+ # the exact JSON-RPC bytes sent to the local host.
360
+ TOOL_AUTH_VERSION = "zerochat-tool-auth-v1"
361
+ TOOL_AUTH_TTL_MS = 30_000
362
+ TOOL_AUTH_MAX_NONCES = 10_000
363
+ TOOL_AUTH_KEY = secrets.token_bytes(32)
364
+ TOOL_AUTH_SESSION_ID = secrets.token_urlsafe(18)
365
+ TOOL_AUTH_NONCES: dict[str, int] = {}
366
+ TOOL_AUTH_NONCES_LOCK = threading.Lock()
367
+
309
368
  DETECTED_OS = "windows" if sys.platform.startswith("win") else ("android" if "ANDROID_ROOT" in os.environ else "linux")
310
369
 
311
370
 
@@ -377,16 +436,15 @@ def check_version():
377
436
  installed = is_installed_runtime()
378
437
  remote_ver = _read_remote_version(PYPI_VERSION_URL if installed else REMOTE_VERSION_URL)
379
438
  if remote_ver and re.match(r"^\d+(\.\d+)+", remote_ver) and has_new_backend_version(remote_ver):
380
- console_log(f"[{time.strftime('%H:%M:%S')}] [zerochat] Nueva versión del servidor disponible (Local: {VERSION}, Remota: {compatibility_version(remote_ver)})", flush=True)
439
+ notice = f"Nueva versión del servidor disponible (local: {VERSION}, remota: {compatibility_version(remote_ver)})."
381
440
  if installed:
382
- console_log(f"[{time.strftime('%H:%M:%S')}] [zerochat] Actualiza cuando quieras con: {sys.executable} -m pip install --upgrade --no-cache-dir zerochat", flush=True)
441
+ notice += f" Actualiza cuando quieras con: {sys.executable} -m pip install --upgrade --no-cache-dir zerochat"
383
442
  else:
384
- console_log(f"[{time.strftime('%H:%M:%S')}] [zerochat] Actualiza con: curl -sSL {REMOTE_SCRIPT_URL} -o zerochat.py", flush=True)
443
+ notice += f" Actualiza con: curl -sSL {REMOTE_SCRIPT_URL} -o zerochat.py"
444
+ add_notice(notice)
385
445
  except Exception:
386
446
  # Modo offline o timeout ignorado de forma segura
387
447
  pass
388
-
389
-
390
448
  # ==============================================================================
391
449
  # Herramientas Locales Core
392
450
  # ==============================================================================
@@ -1159,6 +1217,38 @@ try {
1159
1217
  _BROWSER_SESSION = PersistentBrowserSession()
1160
1218
 
1161
1219
 
1220
+ def browser_action_availability() -> dict:
1221
+ """Comprueba requisitos locales sin lanzar Node.js ni Chromium."""
1222
+ if not shutil.which("node"):
1223
+ return {"available": False, "error": "Node.js is not installed or not found in system PATH."}
1224
+ playwright_locations = (
1225
+ Path.cwd() / "node_modules" / "playwright",
1226
+ get_data_dir() / "services" / "playwright" / "node_modules" / "playwright"
1227
+ )
1228
+ if not any(location.is_dir() for location in playwright_locations):
1229
+ return {"available": False, "error": "Playwright is not available."}
1230
+ browser_commands = (
1231
+ "google-chrome", "google-chrome-stable", "chromium", "chromium-browser",
1232
+ "msedge", "msedge.exe", "chrome", "chrome.exe"
1233
+ )
1234
+ if sys.platform == "darwin":
1235
+ playwright_cache = Path.home() / "Library" / "Caches" / "ms-playwright"
1236
+ elif sys.platform.startswith("win"):
1237
+ playwright_cache = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local")) / "ms-playwright"
1238
+ else:
1239
+ playwright_cache = Path.home() / ".cache" / "ms-playwright"
1240
+ try:
1241
+ has_playwright_browser = playwright_cache.is_dir() and any(
1242
+ child.is_dir() and child.name.startswith("chromium")
1243
+ for child in playwright_cache.iterdir()
1244
+ )
1245
+ except OSError:
1246
+ has_playwright_browser = False
1247
+ if has_playwright_browser or any(shutil.which(command) for command in browser_commands):
1248
+ return {"available": True}
1249
+ return {"available": False, "error": "Playwright Chromium is not installed."}
1250
+
1251
+
1162
1252
  def browser_action(action: str, url: str | None = None, selector: str | None = None, value: str | None = None) -> str:
1163
1253
  """Control a headless browser for UI testing and visual inspection."""
1164
1254
  try:
@@ -1317,7 +1407,6 @@ LOCAL_TOOL_HANDLERS = {
1317
1407
  "get_diagnostics": get_diagnostics,
1318
1408
  "browser_action": browser_action
1319
1409
  }
1320
-
1321
1410
  # ==============================================================================
1322
1411
  # Servidor HTTP JSON-RPC 2.0 y SSE con Autenticación por Token
1323
1412
  # ==============================================================================
@@ -2198,7 +2287,7 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2198
2287
  return
2199
2288
  self.send_header("Access-Control-Allow-Origin", origin if origin else "*")
2200
2289
  self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
2201
- self.send_header("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, X-ZeroChat-Token, X-ZeroChat-Client")
2290
+ self.send_header("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, X-ZeroChat-Token, X-ZeroChat-Client, X-ZeroChat-Tool-Session, X-ZeroChat-Tool-Expires, X-ZeroChat-Tool-Nonce, X-ZeroChat-Tool-Signature")
2202
2291
  self.send_header("Access-Control-Allow-Private-Network", "true")
2203
2292
 
2204
2293
  def verify_token(self) -> bool:
@@ -2217,6 +2306,42 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2217
2306
  mark_browser_active()
2218
2307
  return is_valid
2219
2308
 
2309
+ def verify_tool_authorization(self, body: bytes, request_path: str) -> str | None:
2310
+ """Validate and atomically consume an authorization for one tool call."""
2311
+ session_id = self.headers.get("X-ZeroChat-Tool-Session", "")
2312
+ expires_raw = self.headers.get("X-ZeroChat-Tool-Expires", "")
2313
+ nonce = self.headers.get("X-ZeroChat-Tool-Nonce", "")
2314
+ signature = self.headers.get("X-ZeroChat-Tool-Signature", "")
2315
+ if (not isinstance(session_id, str) or len(session_id) > 128 or
2316
+ not isinstance(nonce, str) or not re.fullmatch(r"[A-Za-z0-9_-]{22,128}", nonce) or
2317
+ not isinstance(signature, str) or not re.fullmatch(r"[A-Za-z0-9_-]{43,128}", signature)):
2318
+ return "Missing or invalid tool authorization"
2319
+ try:
2320
+ expires_at = int(expires_raw)
2321
+ except (TypeError, ValueError):
2322
+ return "Missing or invalid tool authorization"
2323
+ now_ms = int(time.time() * 1000)
2324
+ if session_id != TOOL_AUTH_SESSION_ID or expires_at < now_ms or expires_at > now_ms + TOOL_AUTH_TTL_MS:
2325
+ return "Expired or invalid tool authorization"
2326
+ body_hash = hashlib.sha256(body).hexdigest()
2327
+ signature_base = json.dumps(
2328
+ [TOOL_AUTH_VERSION, session_id, "POST", request_path or "/", expires_at, nonce, body_hash],
2329
+ separators=(",", ":"), ensure_ascii=True
2330
+ ).encode("utf-8")
2331
+ expected = base64.urlsafe_b64encode(hmac.new(TOOL_AUTH_KEY, signature_base, hashlib.sha256).digest()).rstrip(b"=").decode("ascii")
2332
+ if not hmac.compare_digest(signature, expected):
2333
+ return "Invalid tool authorization signature"
2334
+ with TOOL_AUTH_NONCES_LOCK:
2335
+ for value, expiry in list(TOOL_AUTH_NONCES.items()):
2336
+ if expiry < now_ms:
2337
+ del TOOL_AUTH_NONCES[value]
2338
+ if nonce in TOOL_AUTH_NONCES:
2339
+ return "Tool authorization was already used"
2340
+ if len(TOOL_AUTH_NONCES) >= TOOL_AUTH_MAX_NONCES:
2341
+ return "Tool authorization cache is full"
2342
+ TOOL_AUTH_NONCES[nonce] = expires_at
2343
+ return None
2344
+
2220
2345
  def do_OPTIONS(self):
2221
2346
  t0 = time.monotonic()
2222
2347
  safe_path = sanitize_log_path(self.path)
@@ -2266,6 +2391,13 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2266
2391
  self._log_res(200, safe_path, (time.monotonic() - t0) * 1000)
2267
2392
  return
2268
2393
 
2394
+ # Browsers request this optional public resource without API credentials.
2395
+ if path_clean == "/favicon.ico":
2396
+ self.send_response(204)
2397
+ self.send_header("Content-Length", "0")
2398
+ self.end_headers()
2399
+ return
2400
+
2269
2401
  if not is_heartbeat:
2270
2402
  self._log_req("GET", safe_path)
2271
2403
 
@@ -2299,12 +2431,13 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2299
2431
  self._log_res(200, f"{safe_path} [SSE canal activo]", (time.monotonic() - t0) * 1000)
2300
2432
  return
2301
2433
 
2302
- # Status general
2303
2434
  res_data = json.dumps({
2304
2435
  "status": "active",
2305
2436
  "server": "ZeroChat Local Server",
2306
2437
  "version": VERSION,
2438
+ "cwd": str(Path.cwd().resolve()),
2307
2439
  "tools_count": len(LOCAL_TOOLS_DEFINITIONS),
2440
+ "browser_action": browser_action_availability(),
2308
2441
  "os": DETECTED_OS
2309
2442
  }, ensure_ascii=False, indent=2).encode("utf-8")
2310
2443
 
@@ -2427,6 +2560,11 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2427
2560
  self._send_json_response(400, {"error": tool_args_error})
2428
2561
  self._log_res(400, safe_path, (time.monotonic() - t0) * 1000, tool_args_error)
2429
2562
  return
2563
+ auth_error = self.verify_tool_authorization(post_data, self.path.split("?", 1)[0].rstrip("/") or "/")
2564
+ if auth_error:
2565
+ self._send_json_response(403, {"error": auth_error})
2566
+ self._log_res(403, safe_path, (time.monotonic() - t0) * 1000, auth_error)
2567
+ return
2430
2568
 
2431
2569
  req_path = self.path.split("?", 1)[0].rstrip("/")
2432
2570
 
@@ -2465,6 +2603,12 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2465
2603
  },
2466
2604
  "capabilities": {
2467
2605
  "tools": {"listChanged": True}
2606
+ },
2607
+ "toolAuthorization": {
2608
+ "version": TOOL_AUTH_VERSION,
2609
+ "sessionId": TOOL_AUTH_SESSION_ID,
2610
+ "key": base64.urlsafe_b64encode(TOOL_AUTH_KEY).rstrip(b"=").decode("ascii"),
2611
+ "ttlMs": TOOL_AUTH_TTL_MS
2468
2612
  }
2469
2613
  }
2470
2614
  elif method == "tools/list":
@@ -2494,14 +2638,34 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2494
2638
  "protocolVersion": "2024-11-05",
2495
2639
  "serverInfo": {
2496
2640
  "name": "ZeroChat Local Server",
2497
- "version": VERSION
2641
+ "version": VERSION,
2642
+ "cwd": str(Path.cwd().resolve())
2498
2643
  },
2499
2644
  "capabilities": {
2500
2645
  "tools": {"listChanged": True}
2646
+ },
2647
+ "toolAuthorization": {
2648
+ "version": TOOL_AUTH_VERSION,
2649
+ "sessionId": TOOL_AUTH_SESSION_ID,
2650
+ "key": base64.urlsafe_b64encode(TOOL_AUTH_KEY).rstrip(b"=").decode("ascii"),
2651
+ "ttlMs": TOOL_AUTH_TTL_MS
2501
2652
  }
2502
2653
  }
2503
2654
  elif method == "tools/list":
2504
- result = {"tools": list(LOCAL_TOOLS_DEFINITIONS)}
2655
+ browser_availability = browser_action_availability()
2656
+ tools = []
2657
+ for definition in LOCAL_TOOLS_DEFINITIONS:
2658
+ item = dict(definition)
2659
+ if item.get("name") == "browser_action":
2660
+ item["availability"] = browser_availability
2661
+ tools.append(item)
2662
+ result = {"tools": tools}
2663
+ elif method == "tools/availability":
2664
+ requested_name = params.get("name", "") if isinstance(params, dict) else ""
2665
+ if requested_name != "browser_action":
2666
+ error = {"code": -32602, "message": "Herramienta no compatible con comprobación de disponibilidad."}
2667
+ else:
2668
+ result = browser_action_availability()
2505
2669
  elif method == "tools/call":
2506
2670
  tool_name = params.get("name", "") if isinstance(params, dict) else ""
2507
2671
  tool_args = params.get("arguments", {}) if isinstance(params, dict) else {}
@@ -2591,7 +2755,6 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2591
2755
  def log_message(self, format, *args):
2592
2756
  # Silenciar logs ruidosos por defecto
2593
2757
  pass
2594
-
2595
2758
  def is_termux_environment() -> bool:
2596
2759
  """Devuelve si el proceso se ejecuta dentro de la instalación de Termux."""
2597
2760
  termux_prefix = "/data/data/com.termux/files/usr"
@@ -2720,12 +2883,35 @@ def open_browser(url: str) -> bool:
2720
2883
  )
2721
2884
 
2722
2885
 
2886
+ def launch_browser(url: str) -> bool:
2887
+ """Abre el navegador en la URL de la sesión y muestra información o diagnóstico en consola."""
2888
+ termux_detected = is_termux_environment()
2889
+ if termux_detected:
2890
+ console_log(f"[{time.strftime('%H:%M:%S')}] Termux detectado; se abrirá mediante termux-open-url.", flush=True)
2891
+ console_log(f"[{time.strftime('%H:%M:%S')}] Abriendo navegador en la interfaz configurada...", flush=True)
2892
+ try:
2893
+ if not open_browser(url):
2894
+ raise RuntimeError("El lanzador de navegador devolvió un resultado sin éxito.")
2895
+ return True
2896
+ except Exception as e:
2897
+ console_log(f"[{time.strftime('%H:%M:%S')}] No se pudo abrir el navegador automáticamente: {e}", flush=True)
2898
+ console_log(" Traza de diagnóstico:", flush=True)
2899
+ traceback.print_exc()
2900
+ manual_command = get_manual_browser_command(url)
2901
+ if manual_command:
2902
+ console_log(" Termux detectado. Prueba este comando exacto:", flush=True)
2903
+ console_log(f" {manual_command}", flush=True)
2904
+ return False
2905
+
2906
+
2907
+
2723
2908
  # ==============================================================================
2724
2909
  # Punto de Entrada Principal (CLI)
2725
2910
  # ==============================================================================
2726
2911
 
2727
2912
  def main():
2728
2913
  global ACTIVE_PORT, ACTIVE_HOST, SESSION_TOKEN, CONSOLE_CONTROL
2914
+ global TOOL_AUTH_KEY, TOOL_AUTH_SESSION_ID, TOOL_AUTH_NONCES
2729
2915
 
2730
2916
  parser = argparse.ArgumentParser(description=f"ZeroChat Local Server v{VERSION}")
2731
2917
  parser.add_argument("--port", type=int, default=int(os.environ.get("ZEROCHAT_PORT", DEFAULT_PORT)), help=f"Puerto de escucha (default: {DEFAULT_PORT})")
@@ -2739,6 +2925,9 @@ def main():
2739
2925
  parser.add_argument("--version", action="version", version=f"ZeroChat {VERSION}")
2740
2926
  args = parser.parse_args()
2741
2927
 
2928
+ # Los avisos no persisten entre ejecuciones del servidor.
2929
+ reset_notices()
2930
+
2742
2931
  if args.test:
2743
2932
  print(f"[{time.strftime('%H:%M:%S')}] TEST list_directory {'ok' if json.loads(list_directory('.'))['success'] else 'error'}")
2744
2933
  print(f"[{time.strftime('%H:%M:%S')}] TEST read_file {'ok' if json.loads(read_file('package.json', max_lines=5))['success'] else 'error'}")
@@ -2750,6 +2939,14 @@ def main():
2750
2939
  if not args.no_venv:
2751
2940
  ensure_virtual_environment()
2752
2941
 
2942
+ browser_availability = browser_action_availability()
2943
+ if not browser_availability.get("available"):
2944
+ add_notice(
2945
+ "browser_action no está disponible y permanecerá desactivada. "
2946
+ "Instala Node.js, Playwright y Chromium: "
2947
+ "https://albalday.github.io/zerochat/help/browser-action.html"
2948
+ )
2949
+
2753
2950
  # 2. Detectar entorno de desarrollo y resolver URL de destino
2754
2951
  dev_root = get_dev_root()
2755
2952
  static_root = get_static_root()
@@ -2766,6 +2963,11 @@ def main():
2766
2963
  SESSION_TOKEN = args.token
2767
2964
  else:
2768
2965
  SESSION_TOKEN = get_daily_token()
2966
+ # A restart deliberately invalidates every browser-side signing credential.
2967
+ TOOL_AUTH_KEY = secrets.token_bytes(32)
2968
+ TOOL_AUTH_SESSION_ID = secrets.token_urlsafe(18)
2969
+ with TOOL_AUTH_NONCES_LOCK:
2970
+ TOOL_AUTH_NONCES = {}
2769
2971
 
2770
2972
  server = ThreadingHTTPServer((ACTIVE_HOST, ACTIVE_PORT), ZeroChatServerHandler)
2771
2973
 
@@ -2800,25 +3002,11 @@ def main():
2800
3002
  print(f" Auto-cierre : Desactivado")
2801
3003
  print("=" * 64, flush=True)
2802
3004
 
2803
- CONSOLE_CONTROL = ConsoleControl(server, parser)
3005
+ CONSOLE_CONTROL = ConsoleControl(server, parser, target_url=target_url)
2804
3006
  CONSOLE_CONTROL.start()
2805
3007
 
2806
3008
  if not args.no_browser:
2807
- termux_detected = is_termux_environment()
2808
- if termux_detected:
2809
- console_log(f"[{time.strftime('%H:%M:%S')}] Termux detectado; se abrirá mediante termux-open-url.", flush=True)
2810
- console_log(f"[{time.strftime('%H:%M:%S')}] Abriendo navegador en la interfaz configurada...", flush=True)
2811
- try:
2812
- if not open_browser(target_url):
2813
- raise RuntimeError("El lanzador de navegador devolvió un resultado sin éxito.")
2814
- except Exception as e:
2815
- console_log(f"[{time.strftime('%H:%M:%S')}] No se pudo abrir el navegador automáticamente: {e}", flush=True)
2816
- console_log(" Traza de diagnóstico:", flush=True)
2817
- traceback.print_exc()
2818
- manual_command = get_manual_browser_command(target_url)
2819
- if manual_command:
2820
- console_log(" Termux detectado. Prueba este comando exacto:", flush=True)
2821
- console_log(f" {manual_command}", flush=True)
3009
+ launch_browser(target_url)
2822
3010
 
2823
3011
  if exit_on_close:
2824
3012
  require_initial = not args.no_browser
File without changes
File without changes
File without changes