"""Official OpenCode UI/client; inference remains owned by llama-server.""" from __future__ import annotations import hashlib import json import os import shutil import subprocess import sys import tarfile import time from pathlib import Path import httpx from opencode_history import HistoryStore from opencode_mcp_auth import ensure_pc_mcp_auth OPENCODE_VERSION = "1.18.35" OPENCODE_ARCHIVE_URL = ( "https://github.com/anomalyco/opencode/releases/download/" f"v{OPENCODE_VERSION}/opencode-linux-x64.tar.gz" ) OPENCODE_ARCHIVE_SHA256 = "c8f888b451f5494a18f858fffb0e0b68f4e4baa9c241761c5f206884f0fa640d" PROVIDER_ID = "llamacpp" ZEN_FREE_PROVIDER = "opencode" # Free-only mode (SKIP_LLAMA): OpenCode's own Zen models, key from the # OPENCODE_ZEN_API_KEY space secret (alias of OPENCODE_API_KEY). Curated free # IDs below were live on the Zen catalog at the Oct 10 audit; the picker # resyncs when the catalog moves. # Proven live Oct 14 with real tool calls through this exact stack; big-pickle # role-plays tool calls as text instead of invoking them, so it stays out. ZEN_DEFAULT_MODEL = "nemotron-3-ultra-free" ZEN_SMALL_MODEL = "nemotron-3-ultra-free" def ensure_opencode(root: Path) -> Path: """Install only the pinned official client, checking its release digest.""" binary = root / "bin" / "opencode" manifest = root / "verified-binary.json" if binary.exists(): try: saved = json.loads(manifest.read_text(encoding="utf-8")) with binary.open("rb") as cached: actual = hashlib.file_digest(cached, "sha256").hexdigest() except (OSError, ValueError) as exc: raise RuntimeError("Cached OpenCode has no readable integrity record") from exc if saved.get("archive_sha256") != OPENCODE_ARCHIVE_SHA256 or saved.get("binary_sha256") != actual: raise RuntimeError("Cached OpenCode integrity mismatch; refusing to execute") if not binary.exists(): binary.parent.mkdir(parents=True, exist_ok=True) archive = root / "opencode.tar.gz.part" pending = binary.with_suffix(".part") digest = hashlib.sha256() print(f"[opencode] downloading official client v{OPENCODE_VERSION}", flush=True) try: with httpx.stream("GET", OPENCODE_ARCHIVE_URL, follow_redirects=True, timeout=90) as response: response.raise_for_status() size = 0 with archive.open("wb") as output: for chunk in response.iter_bytes(): size += len(chunk) if size > 150_000_000: raise RuntimeError("OpenCode archive exceeds the expected size limit") digest.update(chunk) output.write(chunk) if digest.hexdigest() != OPENCODE_ARCHIVE_SHA256: raise RuntimeError("OpenCode release SHA-256 mismatch; refusing to execute") with tarfile.open(archive, "r:gz") as bundle: members = [m for m in bundle.getmembers() if m.name in {"opencode", "./opencode"}] if len(members) != 1 or not members[0].isfile(): raise RuntimeError("Official OpenCode archive has an unexpected layout") source = bundle.extractfile(members[0]) if source is None: raise RuntimeError("OpenCode executable missing from archive") with source, pending.open("wb") as output: shutil.copyfileobj(source, output) pending.chmod(0o755) pending.replace(binary) with binary.open("rb") as installed: binary_hash = hashlib.file_digest(installed, "sha256").hexdigest() manifest.write_text(json.dumps({"archive_sha256": OPENCODE_ARCHIVE_SHA256, "binary_sha256": binary_hash}), encoding="utf-8") finally: archive.unlink(missing_ok=True) pending.unlink(missing_ok=True) version = subprocess.check_output([str(binary), "--version"], text=True, timeout=30).strip() if version != OPENCODE_VERSION: raise RuntimeError(f"Unexpected OpenCode version: {version!r}") print(f"[opencode] official client version={version} archive_sha256={OPENCODE_ARCHIVE_SHA256}", flush=True) return binary def make_config(*, model_id: str | None, context: int, api_port: int, ui_port: int, bridge: Path, mcp_url: str, token_url: str, free_only: bool = False) -> dict: if free_only: enabled_providers = [ZEN_FREE_PROVIDER] selected = f"{ZEN_FREE_PROVIDER}/{ZEN_DEFAULT_MODEL}" small_selected = f"{ZEN_FREE_PROVIDER}/{ZEN_SMALL_MODEL}" providers: dict = { ZEN_FREE_PROVIDER: {}, } else: if not model_id or context < 8192: raise ValueError("Existing model ID and context are required") selected = f"{PROVIDER_ID}/{model_id}" enabled_providers = [PROVIDER_ID, ZEN_FREE_PROVIDER] providers = { PROVIDER_ID: { "npm": "@ai-sdk/openai-compatible", "name": "llama.cpp CPU-only", "whitelist": [model_id], "options": {"baseURL": f"http://127.0.0.1:{api_port}/v1", "timeout": 900000}, "models": { model_id: { "name": "Naldo Coder (CPU)", "tool_call": True, "reasoning": True, "limit": {"context": context, "output": 32768}, } }, }, ZEN_FREE_PROVIDER: {}, } return { "$schema": "https://opencode.ai/config.json", "autoupdate": False, "share": "disabled", "enabled_providers": enabled_providers, "model": selected, "small_model": small_selected if free_only else selected, "default_agent": "1agent", "server": {"hostname": "127.0.0.1", "port": ui_port}, "provider": providers, "mcp": { "naldo_pc": { "type": "remote", "url": "https://naldo-mcp-render.onrender.com/mcp", "enabled": True, "timeout": 180000, } }, "lsp": False, "formatter": False, "permission": { "*": "deny", "external_directory": "deny", "edit": "allow", "glob": "allow", "grep": "allow", "list": "allow", "invalid": "allow", "read": "allow", "skill": {"*": "ask", "1agent-policy": "allow"}, "bash": "allow", "naldo_pc_*": "allow", "websearch": "allow", "webfetch": "allow", "todowrite": "allow", "question": "allow", }, "agent": { "1agent": { "description": "Agente personalizado 1agent.md para testes privados", "mode": "primary", "prompt": "{file:./prompts/1agent.md}", "temperature": 0.0, }, "build": { "prompt": ( "Responda em portugues do Brasil. Voce e Naldo Agent. " "O computador Windows do usuario e acessado pelas ferramentas naldo_pc. " "O ambiente local deste servidor e o Space Linux, nao o PC do usuario. " "Use naldo_pc_start_process com PowerShell puro; ele ja usa powershell.exe. " "Quando receber um PID, acompanhe esse mesmo PID com read_process_output; " "nao repita start_process para consultar o mesmo comando. " "Use websearch para pesquisar e webfetch para ler URLs verificadas. " "Nao invente resultados nem exponha segredos. " "O modelo padrao e o llama.cpp CPU-only; alternativas Zen Free sao externas, nunca use a GPU do Space. " "Quando a ferramenta responder ao pedido, conclua sem repetir chamadas." ) } }, } def prepare_config(*, root: Path, expected_model: str, context: int, api_port: int, ui_port: int, bridge: Path, mcp_url: str, token_url: str, validate: bool = True, free_only: bool = False) -> tuple[Path, dict]: model_id: str | None = None if validate: response = httpx.get(f"http://127.0.0.1:{api_port}/v1/models", timeout=10) response.raise_for_status() ids = [m.get("id") for m in response.json().get("data", [])] if ids != [expected_model]: raise RuntimeError(f"Expected exactly the existing model {expected_model!r}; got {ids!r}") model_id = ids[0] else: print("[opencode] CONFIG=SKIP local model validation (SKIP_LLAMA free-only)", flush=True) config = make_config(model_id=model_id, context=context, api_port=api_port, ui_port=ui_port, bridge=bridge, mcp_url=mcp_url, token_url=token_url, free_only=free_only) root.mkdir(parents=True, exist_ok=True) source_prompt = Path(__file__).resolve().parent / "agent_prompts" / "1agent.md" source_data = source_prompt.read_bytes() if hashlib.sha256(source_data).hexdigest() != "2c28b84910f9f9262460655c13347fb578d32cd2e2fdf9e65de971805de7ce65": raise RuntimeError("Original 1agent.md integrity check failed") prompt_dir = root / "prompts" prompt_dir.mkdir(parents=True, exist_ok=True) destination = prompt_dir / "1agent.md" if destination.exists() and destination.read_bytes() != source_data: raise RuntimeError("Unexpected existing OpenCode agent prompt") if not destination.exists(): destination.write_bytes(source_data) # OPENCODE_CONFIG_CONTENT is parsed independently of OPENCODE_CONFIG: # relative {file:...} references resolve against request-specific directories # (for example /root/.local), not the source configuration file. # Absolute file reference keeps the original agent prompt unmodified. config["agent"]["1agent"]["prompt"] = "{file:" + str(destination.resolve()) + "}" config["instructions"] = [str(destination.resolve())] # V1 global AGENTS.md and an optional policy skill supplement instructions. # The canonical source remains the unchanged, SHA-256-verified 1agent.md. global_config = root / "config" / "opencode" global_config.mkdir(parents=True, exist_ok=True) global_rules = global_config / "AGENTS.md" global_rules_text = ( "# Global OpenCode instructions\n" "The canonical project policy is loaded directly through the global " "opencode.json instructions setting from the integrity-checked " "1agent.md file. Apply those project instructions in every session " "and when changing models or agents. Keep the original file intact " "and respect provider rules and existing tool permissions.\n" ) if global_rules.exists() and global_rules.read_text(encoding="utf-8") != global_rules_text: raise RuntimeError("Unexpected global AGENTS.md content") global_rules.write_text(global_rules_text, encoding="utf-8") policy_skill = global_config / "skills" / "1agent-policy" / "SKILL.md" policy_skill.parent.mkdir(parents=True, exist_ok=True) skill_text = ( "---\n" "name: 1agent-policy\n" "description: Verify and apply the canonical OpenCode 1agent.md project policy across agent and model workflows.\n" "---\n" "The original 1agent.md is already loaded via global instructions. " "Apply it to the current engineering task, without altering the original. " "Verify configuration and relevant logs; do not claim model obedience " "without a real provider-backed test.\n" ) if policy_skill.exists() and policy_skill.read_text(encoding="utf-8") != skill_text: raise RuntimeError("Unexpected policy SKILL.md content") policy_skill.write_text(skill_text, encoding="utf-8") target = root / "opencode.json" target.write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") target.chmod(0o600) print(f"[opencode] provider={config['provider'] and next(iter(config['provider']))} " f"model={config['model']} free_only={free_only}", flush=True) return target, config def refresh_zen_models_if_idle(*, root: Path, ui_port: int, auth) -> None: """Rebuild the OpenCode model picker only when free Zen catalog IDs change.""" cache_file = root / "cache" / "opencode" / "models.json" if not cache_file.is_file(): return try: catalog = json.loads(cache_file.read_text(encoding="utf-8")) zen = catalog.get(ZEN_FREE_PROVIDER, {}).get("models", {}) if not isinstance(zen, dict): return free_ids = { mid for mid, model in zen.items() if isinstance(model, dict) and model.get("cost", {}).get("input") == 0 and model.get("cost", {}).get("output") == 0 and model.get("status") != "deprecated" } if not free_ids: return base = f"http://127.0.0.1:{ui_port}" current = httpx.get(f"{base}/provider", auth=auth, timeout=10) current.raise_for_status() providers = current.json().get("all", []) existing = next((set(p.get("models", {})) for p in providers if p.get("id") == ZEN_FREE_PROVIDER), None) if existing == free_ids: return if existing is None: print("[opencode] ZEN_CATALOG_SYNC=SKIPPED provider_unavailable", flush=True) return status = httpx.get(f"{base}/session/status", auth=auth, timeout=10) status.raise_for_status() if status.json(): print("[opencode] ZEN_CATALOG_SYNC=DEFERRED active_session", flush=True) return result = httpx.post(f"{base}/instance/dispose", auth=auth, timeout=15) result.raise_for_status() verify = httpx.get(f"{base}/provider", auth=auth, timeout=15) verify.raise_for_status() updated = next((set(p.get("models", {})) for p in verify.json().get("all", []) if p.get("id") == ZEN_FREE_PROVIDER), set()) if updated == free_ids: print(f"[opencode] ZEN_CATALOG_SYNC=PASS old={len(existing)} new={len(updated)}", flush=True) else: print(f"[opencode] ZEN_CATALOG_SYNC=PENDING cached={len(updated)} catalog={len(free_ids)}", flush=True) except (OSError, ValueError, TypeError, KeyError, httpx.HTTPError) as error: print(f"[opencode] ZEN_CATALOG_SYNC=ERROR type={type(error).__name__}", flush=True) def stop_process(process: subprocess.Popen) -> None: if process.poll() is not None: return process.terminate() try: process.wait(timeout=15) except subprocess.TimeoutExpired: process.kill() process.wait(timeout=5) def run_opencode_ui(*, binary: Path, model_process: subprocess.Popen | None, root: Path, expected_model: str, context: int, api_port: int, ui_port: int, bridge: Path, mcp_url: str, token_url: str, validate: bool = True, free_only: bool = False) -> None: config_path, config = prepare_config(root=root, expected_model=expected_model, context=context, api_port=api_port, ui_port=ui_port, bridge=bridge, mcp_url=mcp_url, token_url=token_url, validate=validate, free_only=free_only) private_mount = Path("/persistent") if not private_mount.is_dir(): # Never silently create ephemeral projects that vanish at restart. raise RuntimeError("Private project bucket unavailable; refusing temporary workspace") persistence = HistoryStore(local_data=root / "data", mounted=private_mount) workspace = persistence.prepare() git_marker = workspace / ".git" / "HEAD" if not git_marker.is_file(): raise RuntimeError("Persistent Git workspace missing; refusing global temporary project") verify = subprocess.run(["git", "-c", f"safe.directory={workspace}", "rev-parse", "--verify", "HEAD"], cwd=workspace, capture_output=True, timeout=15, text=True) if verify.returncode != 0 or len(verify.stdout.strip()) != 40: print(f"[opencode] PROJECT_VCS_CHECK_FAIL rc={verify.returncode} " f"stdout_chars={len(verify.stdout.strip())} stderr={verify.stderr.strip()[:260]!r} " f"git_marker_bytes={git_marker.stat().st_size}", flush=True) raise RuntimeError("Persistent project Git revision invalid; refusing global fallback") print("[opencode] PROJECT_PERSISTENCE=PASS PRIVATE_BUCKET=ON", flush=True) print("[opencode] PROJECT_VCS=PASS", flush=True) # OPENCODE_CONFIG_CONTENT resolves {file:./...} relative to the process CWD. source_prompt = root / "prompts" / "1agent.md" target_prompt = workspace / "prompts" / "1agent.md" content = source_prompt.read_bytes() if hashlib.sha256(content).hexdigest() != "2c28b84910f9f9262460655c13347fb578d32cd2e2fdf9e65de971805de7ce65": raise RuntimeError("OpenCode agent prompt does not match original") target_prompt.parent.mkdir(parents=True, exist_ok=True) if target_prompt.exists() and target_prompt.read_bytes() != content: raise RuntimeError("Unexpected project 1agent.md content") if not target_prompt.exists(): target_prompt.write_bytes(content) env = os.environ.copy() # Zen reads OPENCODE_API_KEY, accepting OPENCODE_ZEN_API_KEY as an alias. # The key lives only in the Space secret and the process env, never in files. zen_key = env.get("OPENCODE_ZEN_API_KEY") or env.get("OPENCODE_API_KEY") if zen_key: env["OPENCODE_API_KEY"] = zen_key env["OPENCODE_ZEN_API_KEY"] = zen_key print("[opencode] ZEN_KEY=PRESENT (from secret, not logged)", flush=True) elif free_only: print("[opencode] ZEN_KEY=MISSING add OPENCODE_ZEN_API_KEY as a Space secret", flush=True) # Enable OpenCode Basic Auth from Hugging Face Space Secrets. if not env.get("OPENCODE_SERVER_USERNAME") or not env.get("OPENCODE_SERVER_PASSWORD"): raise RuntimeError("Missing OpenCode login secrets") env.pop("OPENCODE_DISABLE_MODELS_FETCH", None) env.update({ "OPENCODE_CONFIG": str(config_path), "OPENCODE_CONFIG_CONTENT": json.dumps(config), "OPENCODE_DISABLE_AUTOUPDATE": "1", "OPENCODE_DISABLE_PROJECT_CONFIG": "1", "OPENCODE_ENABLE_EXA": "1", "XDG_CONFIG_HOME": str(root / "config"), "XDG_DATA_HOME": str(root / "data"), "XDG_CACHE_HOME": str(root / "cache"), # Protected, narrow Git trust: only the user's private mounted workspace. "GIT_CONFIG_COUNT": "2", "GIT_CONFIG_KEY_0": "safe.directory", "GIT_CONFIG_VALUE_0": str(workspace), "GIT_CONFIG_KEY_1": "safe.directory", "GIT_CONFIG_VALUE_1": str(workspace) + "/*", "PYTHONIOENCODING": "utf-8", }) import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: try: probe.bind(("127.0.0.1", ui_port)) except OSError as error: raise RuntimeError(f"OpenCode port {ui_port} not available: {type(error).__name__} {error.errno}") from error print(f"[opencode] PORT_BIND_PROBE=PASS {ui_port}", flush=True) log_path = root / "opencode-serve.log" log_handle = log_path.open("ab", buffering=0) log_handle.write(f"\n=== boot {time.time()} free_only={free_only} ===\n".encode()) ui = subprocess.Popen([str(binary), "--print-logs", "--log-level", "INFO", "web", "--hostname", "127.0.0.1", "--port", str(ui_port)], cwd=workspace, env=env, stdout=log_handle, stderr=subprocess.STDOUT) print(f"[opencode] LOG={log_path}", flush=True) deadline = time.monotonic() + 120 ready = False last_snapshot = time.monotonic() last_catalog_sync = time.monotonic() - 120 try: while True: if model_process is not None and model_process.poll() is not None: raise RuntimeError(f"llama-server exited: {model_process.returncode}") if ui.poll() is not None: raise RuntimeError(f"OpenCode exited: {ui.returncode}") if not ready: try: password = env.get("OPENCODE_SERVER_PASSWORD") auth = (env.get("OPENCODE_SERVER_USERNAME", "opencode"), password) if password else None response = httpx.get(f"http://127.0.0.1:{ui_port}/global/health", timeout=2, auth=auth) ready = response.status_code == 200 and response.json().get("healthy") is True except (httpx.HTTPError, ValueError): pass if ready: try: mcp_ready = ensure_pc_mcp_auth( base_url=f"http://127.0.0.1:{ui_port}", data_home=root / "data", env=env) print(f"[opencode] MCP_AUTH={'CONNECTED' if mcp_ready else 'NEEDS_AUTH'} " "source=SpaceSecrets", flush=True) except (httpx.HTTPError, OSError, ValueError, RuntimeError) as error: print(f"[opencode] MCP_AUTH=ERROR type={type(error).__name__}", flush=True) print(f"[opencode] official Web UI ready on 0.0.0.0:{ui_port}; MCP=naldo_pc " "websearch=enabled webfetch=enabled", flush=True) elif time.monotonic() >= deadline: raise TimeoutError("OpenCode Web did not become healthy within 120 seconds") if ready and time.monotonic() - last_catalog_sync >= 120: password = env.get("OPENCODE_SERVER_PASSWORD") auth = (env.get("OPENCODE_SERVER_USERNAME", "opencode"), password) if password else None refresh_zen_models_if_idle(root=root, ui_port=ui_port, auth=auth) last_catalog_sync = time.monotonic() if ready and persistence and time.monotonic() - last_snapshot >= 90: try: persistence.snapshot() except (OSError, RuntimeError, ValueError) as error: print(f"[opencode] SESSION_SNAPSHOT=ERROR type={type(error).__name__}", flush=True) last_snapshot = time.monotonic() time.sleep(1) finally: stop_process(ui) try: log_handle.close() except OSError: pass if persistence: try: persistence.snapshot() except (OSError, RuntimeError, ValueError) as error: print(f"[opencode] FINAL_SESSION_SNAPSHOT=ERROR type={type(error).__name__}", flush=True)