zerochat 7.11.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.
- {zerochat-7.11.0/zerochat.egg-info → zerochat-7.12.0}/PKG-INFO +1 -1
- {zerochat-7.11.0 → zerochat-7.12.0}/pyproject.toml +1 -1
- {zerochat-7.11.0 → zerochat-7.12.0/zerochat.egg-info}/PKG-INFO +1 -1
- {zerochat-7.11.0 → zerochat-7.12.0}/zerochat.py +191 -15
- {zerochat-7.11.0 → zerochat-7.12.0}/LICENSE +0 -0
- {zerochat-7.11.0 → zerochat-7.12.0}/README.md +0 -0
- {zerochat-7.11.0 → zerochat-7.12.0}/setup.cfg +0 -0
- {zerochat-7.11.0 → zerochat-7.12.0}/zerochat.egg-info/SOURCES.txt +0 -0
- {zerochat-7.11.0 → zerochat-7.12.0}/zerochat.egg-info/dependency_links.txt +0 -0
- {zerochat-7.11.0 → zerochat-7.12.0}/zerochat.egg-info/entry_points.txt +0 -0
- {zerochat-7.11.0 → zerochat-7.12.0}/zerochat.egg-info/top_level.txt +0 -0
|
@@ -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.
|
|
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."""
|
|
@@ -191,10 +194,27 @@ class ConsoleControl:
|
|
|
191
194
|
if self.status_visible:
|
|
192
195
|
sys.stdout.write("\r\033[2K")
|
|
193
196
|
self.status_visible = False
|
|
194
|
-
|
|
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)
|
|
195
201
|
print(self.parser.format_help().rstrip(), flush=True)
|
|
196
202
|
self.last_activity = time.monotonic()
|
|
197
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
|
+
|
|
198
218
|
def _render_status(self):
|
|
199
219
|
if not self.enabled:
|
|
200
220
|
return
|
|
@@ -202,7 +222,10 @@ class ConsoleControl:
|
|
|
202
222
|
with self.lock:
|
|
203
223
|
if time.monotonic() - self.last_activity < CONSOLE_STATUS_IDLE_SECONDS:
|
|
204
224
|
return
|
|
205
|
-
|
|
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")
|
|
206
229
|
sys.stdout.flush()
|
|
207
230
|
self.status_visible = True
|
|
208
231
|
|
|
@@ -216,6 +239,8 @@ class ConsoleControl:
|
|
|
216
239
|
elif key.lower() == "n":
|
|
217
240
|
if self.target_url:
|
|
218
241
|
launch_browser(self.target_url)
|
|
242
|
+
elif key.lower() == "i":
|
|
243
|
+
self.show_notices()
|
|
219
244
|
elif key.lower() == "x":
|
|
220
245
|
self.log(f"[{time.strftime('%H:%M:%S')}] Deteniendo servidor ZeroChat...")
|
|
221
246
|
stop_zerochat_server(self.server)
|
|
@@ -282,6 +307,26 @@ def get_venv_dir() -> Path:
|
|
|
282
307
|
return get_data_dir() / ".venv"
|
|
283
308
|
|
|
284
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
|
+
|
|
285
330
|
def get_daily_token() -> str:
|
|
286
331
|
"""Devuelve un token de sesión diario persistido en ~/zerochat/config/token.json."""
|
|
287
332
|
config_dir = get_data_dir() / "config"
|
|
@@ -310,6 +355,16 @@ SESSION_TOKEN = get_daily_token()
|
|
|
310
355
|
ACTIVE_PORT = DEFAULT_PORT
|
|
311
356
|
ACTIVE_HOST = DEFAULT_HOST
|
|
312
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
|
+
|
|
313
368
|
DETECTED_OS = "windows" if sys.platform.startswith("win") else ("android" if "ANDROID_ROOT" in os.environ else "linux")
|
|
314
369
|
|
|
315
370
|
|
|
@@ -381,16 +436,15 @@ def check_version():
|
|
|
381
436
|
installed = is_installed_runtime()
|
|
382
437
|
remote_ver = _read_remote_version(PYPI_VERSION_URL if installed else REMOTE_VERSION_URL)
|
|
383
438
|
if remote_ver and re.match(r"^\d+(\.\d+)+", remote_ver) and has_new_backend_version(remote_ver):
|
|
384
|
-
|
|
439
|
+
notice = f"Nueva versión del servidor disponible (local: {VERSION}, remota: {compatibility_version(remote_ver)})."
|
|
385
440
|
if installed:
|
|
386
|
-
|
|
441
|
+
notice += f" Actualiza cuando quieras con: {sys.executable} -m pip install --upgrade --no-cache-dir zerochat"
|
|
387
442
|
else:
|
|
388
|
-
|
|
443
|
+
notice += f" Actualiza con: curl -sSL {REMOTE_SCRIPT_URL} -o zerochat.py"
|
|
444
|
+
add_notice(notice)
|
|
389
445
|
except Exception:
|
|
390
446
|
# Modo offline o timeout ignorado de forma segura
|
|
391
447
|
pass
|
|
392
|
-
|
|
393
|
-
|
|
394
448
|
# ==============================================================================
|
|
395
449
|
# Herramientas Locales Core
|
|
396
450
|
# ==============================================================================
|
|
@@ -1163,6 +1217,38 @@ try {
|
|
|
1163
1217
|
_BROWSER_SESSION = PersistentBrowserSession()
|
|
1164
1218
|
|
|
1165
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
|
+
|
|
1166
1252
|
def browser_action(action: str, url: str | None = None, selector: str | None = None, value: str | None = None) -> str:
|
|
1167
1253
|
"""Control a headless browser for UI testing and visual inspection."""
|
|
1168
1254
|
try:
|
|
@@ -1321,7 +1407,6 @@ LOCAL_TOOL_HANDLERS = {
|
|
|
1321
1407
|
"get_diagnostics": get_diagnostics,
|
|
1322
1408
|
"browser_action": browser_action
|
|
1323
1409
|
}
|
|
1324
|
-
|
|
1325
1410
|
# ==============================================================================
|
|
1326
1411
|
# Servidor HTTP JSON-RPC 2.0 y SSE con Autenticación por Token
|
|
1327
1412
|
# ==============================================================================
|
|
@@ -2202,7 +2287,7 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2202
2287
|
return
|
|
2203
2288
|
self.send_header("Access-Control-Allow-Origin", origin if origin else "*")
|
|
2204
2289
|
self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
|
2205
|
-
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")
|
|
2206
2291
|
self.send_header("Access-Control-Allow-Private-Network", "true")
|
|
2207
2292
|
|
|
2208
2293
|
def verify_token(self) -> bool:
|
|
@@ -2221,6 +2306,42 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2221
2306
|
mark_browser_active()
|
|
2222
2307
|
return is_valid
|
|
2223
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
|
+
|
|
2224
2345
|
def do_OPTIONS(self):
|
|
2225
2346
|
t0 = time.monotonic()
|
|
2226
2347
|
safe_path = sanitize_log_path(self.path)
|
|
@@ -2270,6 +2391,13 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2270
2391
|
self._log_res(200, safe_path, (time.monotonic() - t0) * 1000)
|
|
2271
2392
|
return
|
|
2272
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
|
+
|
|
2273
2401
|
if not is_heartbeat:
|
|
2274
2402
|
self._log_req("GET", safe_path)
|
|
2275
2403
|
|
|
@@ -2303,12 +2431,13 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2303
2431
|
self._log_res(200, f"{safe_path} [SSE canal activo]", (time.monotonic() - t0) * 1000)
|
|
2304
2432
|
return
|
|
2305
2433
|
|
|
2306
|
-
# Status general
|
|
2307
2434
|
res_data = json.dumps({
|
|
2308
2435
|
"status": "active",
|
|
2309
2436
|
"server": "ZeroChat Local Server",
|
|
2310
2437
|
"version": VERSION,
|
|
2438
|
+
"cwd": str(Path.cwd().resolve()),
|
|
2311
2439
|
"tools_count": len(LOCAL_TOOLS_DEFINITIONS),
|
|
2440
|
+
"browser_action": browser_action_availability(),
|
|
2312
2441
|
"os": DETECTED_OS
|
|
2313
2442
|
}, ensure_ascii=False, indent=2).encode("utf-8")
|
|
2314
2443
|
|
|
@@ -2431,6 +2560,11 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2431
2560
|
self._send_json_response(400, {"error": tool_args_error})
|
|
2432
2561
|
self._log_res(400, safe_path, (time.monotonic() - t0) * 1000, tool_args_error)
|
|
2433
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
|
|
2434
2568
|
|
|
2435
2569
|
req_path = self.path.split("?", 1)[0].rstrip("/")
|
|
2436
2570
|
|
|
@@ -2469,6 +2603,12 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2469
2603
|
},
|
|
2470
2604
|
"capabilities": {
|
|
2471
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
|
|
2472
2612
|
}
|
|
2473
2613
|
}
|
|
2474
2614
|
elif method == "tools/list":
|
|
@@ -2498,14 +2638,34 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2498
2638
|
"protocolVersion": "2024-11-05",
|
|
2499
2639
|
"serverInfo": {
|
|
2500
2640
|
"name": "ZeroChat Local Server",
|
|
2501
|
-
"version": VERSION
|
|
2641
|
+
"version": VERSION,
|
|
2642
|
+
"cwd": str(Path.cwd().resolve())
|
|
2502
2643
|
},
|
|
2503
2644
|
"capabilities": {
|
|
2504
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
|
|
2505
2652
|
}
|
|
2506
2653
|
}
|
|
2507
2654
|
elif method == "tools/list":
|
|
2508
|
-
|
|
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()
|
|
2509
2669
|
elif method == "tools/call":
|
|
2510
2670
|
tool_name = params.get("name", "") if isinstance(params, dict) else ""
|
|
2511
2671
|
tool_args = params.get("arguments", {}) if isinstance(params, dict) else {}
|
|
@@ -2595,7 +2755,6 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2595
2755
|
def log_message(self, format, *args):
|
|
2596
2756
|
# Silenciar logs ruidosos por defecto
|
|
2597
2757
|
pass
|
|
2598
|
-
|
|
2599
2758
|
def is_termux_environment() -> bool:
|
|
2600
2759
|
"""Devuelve si el proceso se ejecuta dentro de la instalación de Termux."""
|
|
2601
2760
|
termux_prefix = "/data/data/com.termux/files/usr"
|
|
@@ -2752,6 +2911,7 @@ def launch_browser(url: str) -> bool:
|
|
|
2752
2911
|
|
|
2753
2912
|
def main():
|
|
2754
2913
|
global ACTIVE_PORT, ACTIVE_HOST, SESSION_TOKEN, CONSOLE_CONTROL
|
|
2914
|
+
global TOOL_AUTH_KEY, TOOL_AUTH_SESSION_ID, TOOL_AUTH_NONCES
|
|
2755
2915
|
|
|
2756
2916
|
parser = argparse.ArgumentParser(description=f"ZeroChat Local Server v{VERSION}")
|
|
2757
2917
|
parser.add_argument("--port", type=int, default=int(os.environ.get("ZEROCHAT_PORT", DEFAULT_PORT)), help=f"Puerto de escucha (default: {DEFAULT_PORT})")
|
|
@@ -2765,6 +2925,9 @@ def main():
|
|
|
2765
2925
|
parser.add_argument("--version", action="version", version=f"ZeroChat {VERSION}")
|
|
2766
2926
|
args = parser.parse_args()
|
|
2767
2927
|
|
|
2928
|
+
# Los avisos no persisten entre ejecuciones del servidor.
|
|
2929
|
+
reset_notices()
|
|
2930
|
+
|
|
2768
2931
|
if args.test:
|
|
2769
2932
|
print(f"[{time.strftime('%H:%M:%S')}] TEST list_directory {'ok' if json.loads(list_directory('.'))['success'] else 'error'}")
|
|
2770
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'}")
|
|
@@ -2776,6 +2939,14 @@ def main():
|
|
|
2776
2939
|
if not args.no_venv:
|
|
2777
2940
|
ensure_virtual_environment()
|
|
2778
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
|
+
|
|
2779
2950
|
# 2. Detectar entorno de desarrollo y resolver URL de destino
|
|
2780
2951
|
dev_root = get_dev_root()
|
|
2781
2952
|
static_root = get_static_root()
|
|
@@ -2792,6 +2963,11 @@ def main():
|
|
|
2792
2963
|
SESSION_TOKEN = args.token
|
|
2793
2964
|
else:
|
|
2794
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 = {}
|
|
2795
2971
|
|
|
2796
2972
|
server = ThreadingHTTPServer((ACTIVE_HOST, ACTIVE_PORT), ZeroChatServerHandler)
|
|
2797
2973
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|