diff --git a/README.md b/README.md index 7bebc35..1ed9892 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ **A sovereign, local-first compute fabric for trusted devices.** -[![Tests](https://img.shields.io/badge/tests-185%20passing-00FF88?style=flat-square&labelColor=06090F)](./tests/test_sovereign_mesh.py) +[![Tests](https://img.shields.io/badge/tests-187%20passing-00FF88?style=flat-square&labelColor=06090F)](./tests/test_sovereign_mesh.py) [![Release](https://img.shields.io/badge/release-v0.1.4-F6C177?style=flat-square&labelColor=06090F)](./README.md#current-status) [![Version](https://img.shields.io/badge/wire%20version-sovereign--mesh%2Fv1-00D4FF?style=flat-square&labelColor=06090F)](./docs/OCP_STATUS.md) [![Status](https://img.shields.io/badge/status-active%20development-C8A96E?style=flat-square&labelColor=06090F)](./docs/OCP_MASTER_PLAN.md) @@ -131,7 +131,7 @@ Some devices are powerful. Some are private. Some are fragile. Some are approval | `server_control_page.py` | Extracted control-deck renderer for the advanced operator surface | | `server_http_handlers.py` | Grouped HTTP route handlers so `server.py` stays a thin transport host | | `docs/` | Protocol notes, status, and roadmap | -| `tests/test_sovereign_mesh.py` | Regression suite — 185 tests | +| `tests/test_sovereign_mesh.py` | Regression suite — 187 tests | **Key runtime concepts:** @@ -282,7 +282,7 @@ python3 -m unittest tests.test_sovereign_mesh python3 server.py --help ``` -Current baseline: **185 tests passing.** +Current baseline: **187 tests passing.** --- @@ -296,7 +296,7 @@ Current baseline: **185 tests passing.** - Private artifact content fetches now require operator auth unless the artifact policy is public. - Runtime execution now defaults to explicit environment inheritance, with `inherit_env_allowlist` for deliberate host env pass-through. - The signed envelope implementation now uses dependency-free Ed25519 helpers under `ed25519-sha512-v1`. -- The protocol-kernel refactor and mission-continuity/treaty foundation from v0.1.3 remain intact, with the full regression suite green at 185 tests. +- The protocol-kernel refactor and mission-continuity/treaty foundation from v0.1.3 remain intact, with the full regression suite green at 187 tests. **Implemented in the current runtime** diff --git a/docs/OCP_STATUS.md b/docs/OCP_STATUS.md index 29fc76b..fa103a3 100644 --- a/docs/OCP_STATUS.md +++ b/docs/OCP_STATUS.md @@ -280,4 +280,4 @@ python3 -m unittest tests.test_sovereign_mesh ``` Current standalone baseline: -- `tests.test_sovereign_mesh`: 185 tests passing +- `tests.test_sovereign_mesh`: 187 tests passing diff --git a/mesh_autonomy/service.py b/mesh_autonomy/service.py index 1f7e239..5aa8379 100644 --- a/mesh_autonomy/service.py +++ b/mesh_autonomy/service.py @@ -715,42 +715,42 @@ def activate( result["diagnostics"] = diagnostics self._action( actions, - "diagnostics", + "setup_checked", "ok", diagnostics.get("share_advice") or "Checked local IPs and shareable URLs.", details={"sharing_mode": diagnostics.get("sharing_mode"), "lan_urls": diagnostics.get("lan_urls") or []}, request_id=request_token, ) except Exception as exc: - self._action(actions, "diagnostics", "warning", f"Connectivity diagnostics failed: {exc}", details={"error": str(exc)}, request_id=request_token) + self._action(actions, "fix_needed", "warning", f"Connectivity diagnostics failed: {exc}", details={"error": str(exc)}, request_id=request_token) try: scan = self.mesh.scan_local_peers(timeout=scan_timeout, limit=limit, trust_tier="trusted") result["scan"] = scan self._action( actions, - "scan", + "peer_scan", "ok", f"Scanned nearby routes: {scan.get('reachable', scan.get('discovered', 0))} candidate(s) surfaced.", details={"discovered": scan.get("discovered"), "errors": scan.get("errors")}, request_id=request_token, ) except Exception as exc: - self._action(actions, "scan", "warning", f"Nearby scan could not complete: {exc}", details={"error": str(exc)}, request_id=request_token) + self._action(actions, "fix_needed", "warning", f"Nearby scan could not complete: {exc}", details={"error": str(exc)}, request_id=request_token) try: connected = self.mesh.connect_all_devices(timeout=timeout, scan_timeout=scan_timeout, limit=limit, trust_tier="trusted") result["connect"] = connected self._action( actions, - "connect", + "setup_checked", "ok", connected.get("operator_summary") or f"Connected {connected.get('connected', 0)} peer(s).", details={"connected": connected.get("connected"), "already_connected": connected.get("already_connected"), "errors": connected.get("errors")}, request_id=request_token, ) except Exception as exc: - self._action(actions, "connect", "warning", f"Connect pass had trouble: {exc}", details={"error": str(exc)}, request_id=request_token) + self._action(actions, "fix_needed", "warning", f"Connect pass had trouble: {exc}", details={"error": str(exc)}, request_id=request_token) peer_rows = list(self.mesh.list_peers(limit=max(24, int(limit or 24) * 2)).get("peers") or []) peer_ids = [str(peer.get("peer_id") or "").strip() for peer in peer_rows if str(peer.get("peer_id") or "").strip()] @@ -760,7 +760,7 @@ def activate( route_probes.append(probe) self._action( actions, - "route_probe", + "route_verified" if int(probe.get("reachable") or 0) else "fix_needed", "ok" if int(probe.get("reachable") or 0) else "warning", probe.get("operator_summary") or f"Probed routes for {peer_id}.", peer_id=peer_id, @@ -787,21 +787,21 @@ def activate( mission_status = str(mission.get("status") or proof.get("status") or "unknown") self._action( actions, - "whole_mesh_proof", + "proof_completed" if mission_status in {"completed", "planned", "accepted"} else "fix_needed", "ok" if mission_status in {"completed", "planned", "accepted"} else "warning", f"Whole-mesh proof launched with status {mission_status}.", details={"mission_id": mission.get("id"), "mission_status": mission_status}, request_id=request_token, ) if repair and self._proof_failed_due_transport(proof): - self._action(actions, "route_repair", "running", "Proof hit a transport timeout; probing routes once before retry.", request_id=request_token) + self._action(actions, "fix_needed", "running", "Proof hit a transport timeout; probing routes once before retry.", request_id=request_token) result["repairs"] = self._repair_routes(peer_ids, timeout=timeout, request_id=request_token, actions=actions) proof = self._run_whole_mesh_proof(include_local=True, limit=limit, request_id=f"{request_token}-proof-retry") result["proof_retry"] = proof retry_mission = dict(proof.get("mission") or {}) self._action( actions, - "whole_mesh_proof_retry", + "proof_completed", "ok", f"Retried whole-mesh proof with status {retry_mission.get('status') or proof.get('status') or 'unknown'}.", details={"mission_id": retry_mission.get("id"), "mission_status": retry_mission.get("status")}, @@ -809,7 +809,7 @@ def activate( ) except Exception as exc: result["proof_error"] = str(exc) - self._action(actions, "whole_mesh_proof", "warning", f"Whole-mesh proof needs attention: {exc}", details={"error": str(exc)}, request_id=request_token) + self._action(actions, "fix_needed", "warning", f"Whole-mesh proof needs attention: {exc}", details={"error": str(exc)}, request_id=request_token) status, summary = self._activation_outcome(result, actions) run = self._record_run( @@ -872,7 +872,7 @@ def _evaluate_and_enlist_helpers( limit=max(1, int(max_enlist or 2)) * 3, ) except Exception as exc: - self._action(actions, "helper_plan", "warning", f"Helper planning failed: {exc}", details={"error": str(exc)}, request_id=request_id) + self._action(actions, "fix_needed", "warning", f"Helper planning failed: {exc}", details={"error": str(exc)}, request_id=request_id) return {"status": "error", "error": str(exc), "plan": {}, "enlisted": [], "approvals": [], "skipped": []} enlisted = [] @@ -880,7 +880,7 @@ def _evaluate_and_enlist_helpers( skipped = [] self._action( actions, - "helper_plan", + "helper_ready", "ok", f"Evaluated {plan.get('candidate_count', 0)} helper candidate(s).", details={"candidate_count": plan.get("candidate_count")}, @@ -928,14 +928,14 @@ def _evaluate_and_enlist_helpers( peer = self.mesh._row_to_peer(self.mesh._get_peer_row(peer_id)) or {} if not self._route_is_usable(peer): skipped.append({"peer_id": peer_id, "reason": "route_not_usable"}) - self._action(actions, "helper_skipped", "warning", f"Did not enlist {peer_id} because no fresh working route is proven.", peer_id=peer_id, request_id=request_id) + self._action(actions, "fix_needed", "warning", f"Did not enlist {peer_id} because no fresh working route is proven.", peer_id=peer_id, request_id=request_id) continue trust = self._normalize_trust_tier(candidate.get("trust_tier") or "trusted") device_class = str(candidate.get("device_class") or "full").strip().lower() role = "gpu_helper" if dict(candidate.get("compute_profile") or {}).get("gpu_capable") else "helper" if str((candidate.get("enlistment") or {}).get("state") or "").strip().lower() == "enlisted": skipped.append({"peer_id": peer_id, "reason": "already_enlisted"}) - self._action(actions, "helper_reuse", "ok", f"{candidate.get('display_name') or peer_id} is already enlisted.", peer_id=peer_id, request_id=request_id) + self._action(actions, "helper_ready", "ok", f"{candidate.get('display_name') or peer_id} is already enlisted.", peer_id=peer_id, request_id=request_id) continue if len(enlisted) >= max(0, int(max_enlist or 0)): skipped.append({"peer_id": peer_id, "reason": "max_enlist_reached"}) @@ -944,10 +944,10 @@ def _evaluate_and_enlist_helpers( try: state = self.mesh.enlist_helper(peer_id, mode="on_demand", role=role, reason="autonomic_mesh_activation", source="autonomy") enlisted.append({"peer_id": peer_id, "state": state}) - self._action(actions, "helper_enlisted", "ok", f"Enlisted {candidate.get('display_name') or peer_id} as a safe helper.", peer_id=peer_id, details={"role": role}, request_id=request_id) + self._action(actions, "helper_ready", "ok", f"Enlisted {candidate.get('display_name') or peer_id} as a safe helper.", peer_id=peer_id, details={"role": role}, request_id=request_id) except Exception as exc: skipped.append({"peer_id": peer_id, "reason": str(exc)}) - self._action(actions, "helper_enlist_failed", "warning", f"Could not enlist {peer_id}: {exc}", peer_id=peer_id, details={"error": str(exc)}, request_id=request_id) + self._action(actions, "fix_needed", "warning", f"Could not enlist {peer_id}: {exc}", peer_id=peer_id, details={"error": str(exc)}, request_id=request_id) elif trust == "partner": approval = self.mesh.create_approval_request( title=f"Allow {candidate.get('display_name') or peer_id} to help this mesh?", @@ -973,10 +973,10 @@ def _evaluate_and_enlist_helpers( }, ) approvals.append(approval) - self._action(actions, "helper_approval_requested", "approval_required", f"Asked before using partner peer {candidate.get('display_name') or peer_id}.", peer_id=peer_id, details={"approval": approval}, request_id=request_id) + self._action(actions, "helper_ready", "approval_required", f"Asked before using partner peer {candidate.get('display_name') or peer_id}.", peer_id=peer_id, details={"approval": approval}, request_id=request_id) else: skipped.append({"peer_id": peer_id, "reason": f"trust_tier_{trust}_not_auto_enlisted"}) - self._action(actions, "helper_skipped", "blocked", f"Did not auto-enlist {peer_id} because trust tier is {trust}.", peer_id=peer_id, request_id=request_id) + self._action(actions, "fix_needed", "blocked", f"Did not auto-enlist {peer_id} because trust tier is {trust}.", peer_id=peer_id, request_id=request_id) return { "status": "ok", "plan": plan, diff --git a/mesh_protocol/conformance.py b/mesh_protocol/conformance.py index c9b344b..c4b75d3 100644 --- a/mesh_protocol/conformance.py +++ b/mesh_protocol/conformance.py @@ -214,6 +214,21 @@ def build_protocol_conformance_snapshot() -> dict[str, Any]: "healthy_routes": 1, "operator_summary": "Mesh is strong.", }, + "setup": { + "status": "strong", + "label": "Mesh strong", + "primary_action": "activate_mesh", + "bind_mode": "lan", + "phone_url": "http://192.168.1.10:8421/app", + "token_status": "configured", + "known_peer_count": 1, + "healthy_route_count": 1, + "route_count": 1, + "latest_proof_status": "completed", + "blocking_issue": "", + "next_fix": "No fix needed. The current mesh proof completed.", + "operator_summary": "Mesh is strong. Devices have proven routes and the latest proof completed.", + }, "autonomy": {"status": "ok", "mode": "assisted", "operator_summary": "Mesh is strong."}, "route_health": { "status": "ok", diff --git a/mesh_protocol/schemas.py b/mesh_protocol/schemas.py index 87079bd..46c66a6 100644 --- a/mesh_protocol/schemas.py +++ b/mesh_protocol/schemas.py @@ -412,7 +412,7 @@ "AppStatus": { "type": "object", "description": "Operator-facing compact status for the installable OCP app home.", - "required": ["status", "node", "app_urls", "mesh_quality", "next_actions"], + "required": ["status", "node", "app_urls", "mesh_quality", "setup", "next_actions"], "properties": { "status": {"type": "string"}, "node": {"type": "object"}, @@ -440,6 +440,24 @@ "operator_summary": {"type": "string"}, }, }, + "setup": { + "type": "object", + "properties": { + "status": {"type": "string"}, + "label": {"type": "string"}, + "primary_action": {"type": "string"}, + "bind_mode": {"type": "string"}, + "phone_url": {"type": "string"}, + "token_status": {"type": "string"}, + "known_peer_count": {"type": "integer"}, + "healthy_route_count": {"type": "integer"}, + "route_count": {"type": "integer"}, + "latest_proof_status": {"type": "string"}, + "blocking_issue": {"type": "string"}, + "next_fix": {"type": "string"}, + "operator_summary": {"type": "string"}, + }, + }, "autonomy": {"type": "object"}, "route_health": {"$ref": "#/schemas/RouteHealthList"}, "latest_proof": {"type": "object"}, diff --git a/ocp_desktop/launcher.py b/ocp_desktop/launcher.py index 67d7047..9a1bbc7 100644 --- a/ocp_desktop/launcher.py +++ b/ocp_desktop/launcher.py @@ -1,12 +1,12 @@ from __future__ import annotations import argparse +import json import os import secrets import subprocess import sys import urllib.error -import urllib.parse import urllib.request import webbrowser from dataclasses import dataclass @@ -87,14 +87,7 @@ def normalize_launcher_config(config: dict[str, Any]) -> dict[str, Any]: def operator_app_url(base_url: str, operator_token: str = "") -> str: - base = str(base_url or "").strip().rstrip("/") - if not base: - return "" - app_url = base if base.endswith("/app") else f"{base}/app" - token = str(operator_token or "").strip() - if not token: - return app_url - return f"{app_url}#ocp_operator_token={urllib.parse.quote(token, safe='')}" + return ocp_startup.operator_app_url(base_url, operator_token) def build_launch_plan( @@ -140,6 +133,37 @@ def server_is_alive(plan: LaunchPlan, *, timeout: float = 0.75) -> bool: return False +def fetch_app_status(plan: LaunchPlan, *, operator_token: str = "", timeout: float = 0.75) -> dict[str, Any]: + url = ocp_startup.build_open_url(plan.profile.host, plan.profile.port, "/mesh/app/status") + request = urllib.request.Request(url) + token = str(operator_token or "").strip() + if token: + request.add_header("X-OCP-Operator-Token", token) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + if response.status != 200: + return {} + return json.loads(response.read().decode("utf-8")) + except (OSError, urllib.error.URLError, json.JSONDecodeError): + return {} + + +def launcher_status_message(payload: dict[str, Any]) -> str: + setup = dict((payload or {}).get("setup") or {}) + status = str(setup.get("status") or "").strip().lower() + if status == "strong": + return "Mesh is strong. Latest proof completed." + if status == "proving": + return "OCP is proving the mesh now..." + if status == "ready": + return "OCP is ready for phone setup. Open the phone link below and press Activate Mesh." + if status == "local_only": + return "OCP is running local-only. Start Mesh Mode to use your phone or spare laptop." + if status == "needs_attention": + return "OCP needs attention: " + str(setup.get("next_fix") or setup.get("blocking_issue") or "open the app for details.") + return str(setup.get("operator_summary") or "OCP is running.") + + class OCPLauncherApp: def __init__(self, root, *, repo_root: Path, config_path: Path | None = None): import tkinter as tk @@ -297,9 +321,12 @@ def _app_link(self, plan: LaunchPlan, base_url: str) -> str: token = self._operator_token_for_mode(plan.mode) return operator_app_url(base_url, token) - def _render_links(self, plan: LaunchPlan) -> None: + def _render_links(self, plan: LaunchPlan, *, phone_url: str = "") -> None: rows = [f"App: {self._app_link(plan, plan.app_url)}"] - if plan.share_urls: + if phone_url: + rows.append("Phone/LAN:") + rows.append(f" {self._app_link(plan, phone_url)}") + elif plan.share_urls: rows.append("Phone/LAN:") rows.extend(f" {self._app_link(plan, url)}" for url in plan.share_urls) else: @@ -312,7 +339,14 @@ def _poll_status(self) -> None: plan = self.current_plan if plan and self.process and self.process.poll() is None: if server_is_alive(plan): - self.status.set(f"OCP is running. Open {plan.app_url} or use the phone link below.") + payload = fetch_app_status(plan, operator_token=str(self.config.get("operator_token") or "")) + if payload: + self.status.set(launcher_status_message(payload)) + setup = dict(payload.get("setup") or {}) + app_urls = dict(payload.get("app_urls") or {}) + self._render_links(plan, phone_url=str(setup.get("phone_url") or app_urls.get("phone_url") or "")) + else: + self.status.set(f"OCP is running. Open {plan.app_url} or use the phone link below.") else: self.status.set("OCP is starting...") elif self.process and self.process.poll() is not None: diff --git a/ocp_startup.py b/ocp_startup.py index c7104e2..1de29be 100644 --- a/ocp_startup.py +++ b/ocp_startup.py @@ -7,6 +7,7 @@ import sys import time import urllib.error +import urllib.parse import urllib.request from dataclasses import asdict, dataclass from pathlib import Path @@ -130,10 +131,34 @@ def build_open_url(host: str, port: int, path: str = "/") -> str: return f"http://{display_host_for_browser(host)}:{int(port)}{route}" +def operator_app_url(base_url: str, operator_token: str = "", *, path: str = "/app") -> str: + base = str(base_url or "").strip().rstrip("/") + if not base: + return "" + route = str(path or "/app").strip() or "/app" + route = route if route.startswith("/") else f"/{route}" + url = base if base.endswith(route) else f"{base}{route}" + token = str(operator_token or "").strip() + if not token: + return url + return f"{url}#ocp_operator_token={urllib.parse.quote(token, safe='')}" + + def health_url(host: str, port: int) -> str: return build_open_url(host, port, "/mesh/manifest") +def port_is_available(host: str, port: int) -> bool: + token = str(host or "").strip() or "127.0.0.1" + bind_host = "" if is_wildcard_host(token) else display_host_for_browser(token) + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind((bind_host, int(port))) + return True + except OSError: + return False + + def default_repo_state_dir(repo_root: Path) -> Path: return Path(repo_root) / ".local" / "ocp" @@ -306,6 +331,8 @@ def write_json_file(path: Path, payload: dict) -> None: "health_url", "is_loopback_host", "is_wildcard_host", + "operator_app_url", + "port_is_available", "profile_from_values", "read_json_file", "resolve_state_paths", diff --git a/scripts/start_ocp_easy.py b/scripts/start_ocp_easy.py index 89b7557..1d0223d 100755 --- a/scripts/start_ocp_easy.py +++ b/scripts/start_ocp_easy.py @@ -97,7 +97,10 @@ def main() -> int: args = parser.parse_args() repo_root = REPO_ROOT command = server_command(args, repo_root) + operator_token = (os.environ.get("OCP_OPERATOR_TOKEN") or os.environ.get("OCP_CONTROL_TOKEN") or "").strip() open_url = build_open_url(args.host, args.port, args.open_path) + if operator_token and args.open_path.strip("/") in {"", "app"}: + open_url = ocp_startup.operator_app_url(build_open_url(args.host, args.port, "/"), operator_token) profile = _profile_from_args(args, repo_root) print("Starting The Open Compute Protocol") @@ -110,21 +113,26 @@ def main() -> int: print(f" db: {profile.db_path}") print(f" identity: {profile.identity_dir}") print(f" workspace: {profile.workspace_root}") + if not ocp_startup.port_is_available(args.host, args.port): + print() + print(f"Port {args.port} is already in use on {args.host}.") + print("Stop the other OCP server or choose a different OCP_PORT.") + return 2 print() print("OCP app:") print(f" {open_url}") print() print("Easy setup module:") - print(f" {build_open_url(args.host, args.port, '/easy')}") + print(f" {ocp_startup.operator_app_url(build_open_url(args.host, args.port, '/'), operator_token, path='/easy') if operator_token else build_open_url(args.host, args.port, '/easy')}") print() print("Advanced control module:") - print(f" {build_open_url(args.host, args.port, '/control')}") + print(f" {ocp_startup.operator_app_url(build_open_url(args.host, args.port, '/'), operator_token, path='/control') if operator_token else build_open_url(args.host, args.port, '/control')}") share_urls = share_urls_for_host(args.host, args.port) if share_urls: print() print("LAN share URLs:") for url in share_urls: - print(f" {url}") + print(f" {ocp_startup.operator_app_url(url, operator_token) if operator_token else url}") elif discover_local_ipv4_addresses(bind_host=args.host) and is_loopback_host(args.host): print() print("Detected local network IPs, but this node is local-only right now:") diff --git a/server_app.py b/server_app.py index aeeeb39..ce3cf32 100644 --- a/server_app.py +++ b/server_app.py @@ -5,6 +5,7 @@ from typing import Any from mesh import SovereignMesh +from server_browser_client import build_browser_client_script def _node_summary(mesh: SovereignMesh) -> dict[str, Any]: @@ -47,6 +48,7 @@ def build_app_page(mesh: SovereignMesh) -> str: protocol = html.escape(str(summary.get("protocol_release") or "0.1")) version = html.escape(str(summary.get("protocol_version") or "")) version_label = f" / {version}" if version else "" + browser_client_js = build_browser_client_script() return f""" @@ -473,15 +475,15 @@ def build_app_page(mesh: SovereignMesh) -> str:
-

Autonomic Mesh

+

Setup Doctor

Local node ready

Loading local mesh status...

@@ -499,12 +501,12 @@ def build_app_page(mesh: SovereignMesh) -> str:
- + Inspect App Status