From 010c7f08eaf322004824c2d9fdde5088db20add4 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 03:59:49 +0700 Subject: [PATCH 01/17] feat(evaos): vendor agent tools runtime receipt route --- .../evaos-beta/bridge/agent-tools/SOURCE.json | 26 + .../agent-tools/hermes-adapter/README.md | 73 + .../bin/evaos-desktop-bridge-command | 684 +++++++ .../agent-tools/openclaw-plugin/README.md | 175 ++ .../agent-tools/openclaw-plugin/dist/index.js | 800 ++++++++ .../openclaw-plugin/dist/src/bridge.js | 1541 ++++++++++++++ .../openclaw-plugin/dist/src/firewall.js | 172 ++ .../dist/src/runtimeReceipt.js | 442 ++++ .../agent-tools/openclaw-plugin/index.ts | 833 ++++++++ .../openclaw-plugin/openclaw.plugin.json | 89 + .../agent-tools/openclaw-plugin/package.json | 115 ++ .../openclaw-plugin/scripts/qa-run-bridge.mjs | 119 ++ .../agent-tools/openclaw-plugin/src/bridge.ts | 1823 +++++++++++++++++ .../openclaw-plugin/src/firewall.ts | 203 ++ .../openclaw-plugin/src/runtimeReceipt.ts | 575 ++++++ .../openclaw-plugin/src/types.d.ts | 56 + .../tests/runtimeReceipt.test.mjs | 304 +++ .../agent-tools/openclaw-plugin/tsconfig.json | 13 + .../src/evaos_desktop_bridge/qa_canary.py | 19 +- 19 files changed, 8058 insertions(+), 4 deletions(-) create mode 100644 resources/evaos-beta/bridge/agent-tools/SOURCE.json create mode 100644 resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md create mode 100755 resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/firewall.js create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/openclaw.plugin.json create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package.json create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/qa-run-bridge.mjs create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/firewall.ts create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/types.d.ts create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tsconfig.json diff --git a/resources/evaos-beta/bridge/agent-tools/SOURCE.json b/resources/evaos-beta/bridge/agent-tools/SOURCE.json new file mode 100644 index 0000000000..d4436bf22b --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/SOURCE.json @@ -0,0 +1,26 @@ +{ + "schema": "evaos-workbench-agent-tools-source/v1", + "owner": "100yenadmin/evaOS-GUI", + "version": "0.2.0", + "status": "vendored", + "releaseTagNamespace": "evaos-desktop-bridge-openclaw-vX.Y.Z", + "importedFrom": "electricsheephq/evaos-desktop-bridge", + "importedCommit": "b3e8da7ae90c86b7a999b6a0ef5d7e4bcb83b2c8", + "importedAt": "2026-07-15", + "sourceDigests": { + "hermes-adapter/README.md": "sha256:0077be1d91ac82fad35c6c916cba3f780088f8a4b3d80bd69dea93819bb9003a", + "hermes-adapter/bin/evaos-desktop-bridge-command": "sha256:d3d1870e9625ecf69787d6a536054340bd0c7aec862ff34a0be4ec86bc56ada3", + "openclaw-plugin/README.md": "sha256:18ba84670be254004a093f613edc783ac9fd2523174968b1f853a3abb59a2156", + "openclaw-plugin/dist/index.js": "sha256:ffdacb32f8e5e378df1352ac8b07ea9a152a3e4b25200428512b8e667de91adc", + "openclaw-plugin/dist/src/bridge.js": "sha256:d3bb9ff47b6cbd01b548baac7fd211051a595ef522abe913c22ec44e167dad7e", + "openclaw-plugin/dist/src/firewall.js": "sha256:60c2c0f5e5319cb77ef6d4c4b4d4b30b46f4dddafb7390230ddd3fbfe03dd993", + "openclaw-plugin/index.ts": "sha256:5ea45783093fe60b30c2bb8cf93343a069751004789a9dcdd202be21ea88e26d", + "openclaw-plugin/openclaw.plugin.json": "sha256:4e5c50b2bb31a31fc9756171679a7d2139e88503b304734eeae9093bd25095e1", + "openclaw-plugin/package.json": "sha256:c27ab5217832722473709611ddc8d6308a22a62de7a24e66b5952a4f8fde5c41", + "openclaw-plugin/scripts/qa-run-bridge.mjs": "sha256:db40c073164440a60f068153f5a21d5d39b04e479ad7bc2bbcfdea2468dd54da", + "openclaw-plugin/src/bridge.ts": "sha256:2695a25bc6710c5ac5f95682e5ee2c1849a708b3ce00d9a44c924efd75a088e2", + "openclaw-plugin/src/firewall.ts": "sha256:2b21f94e43184c9d9507d5bef91f057949e30c266b81fe4d3005a14f9f966a5c", + "openclaw-plugin/src/types.d.ts": "sha256:6645c7b2f7808c88ee84fb95163eefcdec1c69cf678737f217bca6e1c973cb9e", + "openclaw-plugin/tsconfig.json": "sha256:2d137f90e1b16119cb1837e7cc61122c1c298b7e77f8f20a497e98d03bcba23a" + } +} diff --git a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md new file mode 100644 index 0000000000..85f695332d --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md @@ -0,0 +1,73 @@ +# evaOS Desktop Bridge Hermes Adapter + +Hermes uses the same customer Mac connector contract as OpenClaw. This adapter +is intentionally tiny: it does not create a second control backend, and it does +not expose generic shell, hidden AppleScript, public Mac ports, or app-server +mutation. + +## Runtime contract + +Set these on the customer VM after the Mac is paired. The wrapper automatically +sources `/root/.openclaw/evaos-desktop-bridge.env` when the variables are not +already present, so OpenClaw, Hermes, and direct support smokes all use the same +connector contract. + +```bash +export EVAOS_DESKTOP_BRIDGE_URL="http://:8765" +export EVAOS_DESKTOP_BRIDGE_TOKEN="" +``` + +Connector material must come from the selected Workbench runtime launch. The +adapter exposes no legacy pairing-code or connector-claim command. + +Hermes tools should call `bin/evaos-desktop-bridge-command` with one of the +fixed connector command names supported by `/v1/commands`, for example: + +```bash +hermes-adapter/bin/evaos-desktop-bridge-command customerMacStatus '{}' +hermes-adapter/bin/evaos-desktop-bridge-command customerMacControlStart '{"mode":"full-access","agent_label":"Hermes"}' +hermes-adapter/bin/evaos-desktop-bridge-command desktopSee '{}' +hermes-adapter/bin/evaos-desktop-bridge-command desktopClick '{"target_label":"Continue","dry_run":false}' +hermes-adapter/bin/evaos-desktop-bridge-command customerMacIphoneMirroringStatus '{}' +hermes-adapter/bin/evaos-desktop-bridge-command iphoneSwipe '{"direction":"up","dry_run":false}' +hermes-adapter/bin/evaos-desktop-bridge-command evaosProviderProfiles '{}' +hermes-adapter/bin/evaos-desktop-bridge-command evaosProviderCompleteAuth '{"identity":"admin@100yen.org"}' +hermes-adapter/bin/evaos-desktop-bridge-command evaosSharedBrowserGuidance '{}' +``` + +Provider/Auth Hub and Shared Browser guidance commands read optional +`EVAOS_PROVIDER_PROFILES_JSON`, `EVAOS_PROVIDER_GRANTS_JSON`, +`EVAOS_ACTIVE_PROVIDER_KEY`, `EVAOS_SHARED_BROWSER_STATUS_JSON`, and +`EVAOS_CUSTOMER_ID` environment values. They return metadata and opaque grant +handles only, never raw provider credentials. + +`EVAOS_SHARED_BROWSER_STATUS_JSON` should use the same +`evaos.browser_status.v1` shape as Workbench and OpenClaw, including +`customer_id`, `room_id`, optional `session_id`, sanitized `current_url`, +auth/CAPTCHA flags, allowed `actions`, `source_pointer`, and `audit_id`. + +Provider auth completion uses the dashboard broker endpoint from +`EVAOS_PROVIDER_DISCOVERY_URL` or `EVAOS_DESKTOP_RUNTIME_SESSION_URL`, signs +metadata proof with `EVAOS_PROVIDER_AUTH_PROOF_SECRET`, and sends only identity, +scopes, expiry, and `EVAOS_PROVIDER_SERVER_SECRET_REF`. When the broker mints a +Hermes grant, the wrapper caches that opaque handle in +`EVAOS_PROVIDER_GRANT_CACHE_FILE` or `~/.openclaw/evaos-provider-grants.json` +so later provider discovery works without pasting raw provider secrets. + +Full Access mode allows live desktop/iPhone commands without per-action +approval. Ask Permission mode gates risky clicks, taps, hotkeys, typing, +sends, and other high-impact actions with +`{"dry_run":false,"approval_audit_id":"..."}`. The kill switch blocks future +live connector commands immediately. + +The wrapper returns connector JSON on stdout even for structured denials such as +blocked sensitive apps or missing approval ids. Network failures and malformed +responses still fail as hard command errors. + +## Boundary + +- OpenClaw remains the first native plugin path. +- Hermes uses this command wrapper or an MCP/tool config that shells to it. +- The command wrapper only posts fixed JSON to the paired connector URL. +- Customer-facing Mac/iPhone control uses the same Full Access / Ask Permission + session contract as OpenClaw. diff --git a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command new file mode 100755 index 0000000000..e62d871391 --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command @@ -0,0 +1,684 @@ +#!/usr/bin/env bash +set -euo pipefail + +command_name="${1:-}" +if [[ $# -ge 2 ]]; then + params_json="$2" +else + params_json="{}" +fi +if [[ "${params_json}" == "-" ]]; then + params_json="$(cat)" +fi + +if [[ -z "${command_name}" ]]; then + echo "usage: evaos-desktop-bridge-command [params-json]" >&2 + exit 2 +fi + +if [[ -z "${EVAOS_DESKTOP_BRIDGE_URL:-}" || -z "${EVAOS_DESKTOP_BRIDGE_TOKEN:-}" ]]; then + env_file="${EVAOS_DESKTOP_BRIDGE_ENV_FILE:-/root/.openclaw/evaos-desktop-bridge.env}" + if [[ -f "${env_file}" ]]; then + set -a + # shellcheck source=/dev/null + . "${env_file}" + set +a + fi +fi + +python3 - "$command_name" "$params_json" <<'PY' +import base64 +import datetime +import hashlib +import hmac +import http.client +import json +import os +import pathlib +import socket +import sys +import urllib.parse +import urllib.request +import uuid + +command = sys.argv[1] +try: + params = json.loads(sys.argv[2] or "{}") +except json.JSONDecodeError as exc: + raise SystemExit(f"invalid params JSON: {exc}") from exc + +def scrub_secrets(value): + if isinstance(value, list): + return [scrub_secrets(item) for item in value] + if isinstance(value, dict): + redacted = {} + for key, nested in value.items(): + if is_secret_metadata_key(str(key)): + redacted[key] = "[redacted]" + else: + redacted[key] = scrub_secrets(nested) + return redacted + return value + + +def is_secret_metadata_key(key): + compact_key = "".join(ch for ch in key.lower() if ch.isalnum()) + return ( + compact_key == "token" + or compact_key.endswith("token") + or compact_key == "authorization" + or compact_key.endswith("authorization") + or compact_key in {"header", "headers"} + or compact_key.endswith("header") + or compact_key.endswith("headers") + or compact_key == "password" + or compact_key.endswith("password") + or compact_key in {"credential", "credentials"} + or compact_key.endswith("credential") + or compact_key.endswith("credentials") + or compact_key == "secret" + or compact_key.endswith("secret") + or "apikey" in compact_key + or "clientsecret" in compact_key + or "privatekey" in compact_key + or "secretkey" in compact_key + or "accesskey" in compact_key + ) + + +def find_visual_image(value): + if isinstance(value, dict): + if isinstance(value.get("bytes_base64"), str) or isinstance(value.get("artifact_url"), str): + return value + for nested in value.values(): + found = find_visual_image(nested) + if found: + return found + elif isinstance(value, list): + for nested in value: + found = find_visual_image(nested) + if found: + return found + return None + + +def safe_filename(value): + text = "".join(ch if ch.isalnum() or ch in "._-" else "-" for ch in str(value or "visual")) + return text[:96] or "visual" + + +def materialize_visual_evidence(payload): + artifact_dir = os.environ.get("EVAOS_DESKTOP_BRIDGE_ARTIFACT_DIR") + if not artifact_dir or not isinstance(payload, dict): + return payload + data = payload.get("data") + if not isinstance(data, dict): + return payload + image = find_visual_image(data) + if not image: + return payload + content = None + if isinstance(image.get("bytes_base64"), str): + try: + content = base64.b64decode(image["bytes_base64"]) + except Exception: + content = None + artifact_url = image.get("artifact_url") + if content is None and isinstance(artifact_url, str): + parsed_base = urllib.parse.urlparse(os.environ.get("EVAOS_DESKTOP_BRIDGE_URL", "")) + endpoint = urllib.parse.urljoin(os.environ.get("EVAOS_DESKTOP_BRIDGE_URL", "").rstrip("/") + "/", artifact_url) + parsed_endpoint = urllib.parse.urlparse(endpoint) + if parsed_base.netloc == parsed_endpoint.netloc and parsed_endpoint.path.startswith("/v1/artifacts/"): + request = urllib.request.Request(endpoint, method="GET", headers={"Authorization": f"Bearer {os.environ.get('EVAOS_DESKTOP_BRIDGE_TOKEN', '')}"}) + try: + with urllib.request.urlopen(request, timeout=15) as response: + content = response.read() + except Exception as exc: + payload.setdefault("warnings", []).append(f"Unable to fetch connector artifact: {exc}") + if content is None: + return payload + snapshot_id = ( + data.get("snapshot_id") + or image.get("snapshot_id") + or image.get("artifact_id") + or pathlib.Path(str(artifact_url or "visual")).stem + ) + evidence_dir = pathlib.Path(artifact_dir) / "evidence" + evidence_dir.mkdir(parents=True, exist_ok=True) + output_path = evidence_dir / f"{safe_filename(snapshot_id)}.png" + output_path.write_bytes(content) + data["vm_visual_artifact_path"] = str(output_path) + image["vm_artifact_path"] = str(output_path) + return payload + + +def timeout_for_command(command): + if command in { + "customerMacControlStart", + "desktop_control_start", + }: + return 30 + if command in { + "desktopSee", + "desktop_see", + "iphoneSee", + "iphone_see", + "customerMacSnapshot", + "customer_mac_snapshot", + "customerMacAxTree", + "customer_mac_ax_tree", + }: + return 60 + if command in { + "desktopDrag", + "desktop_drag", + "desktopScroll", + "desktop_scroll", + "iphoneSwipe", + "iphone_swipe", + "customerMacIphoneMirroringScroll", + "customer_mac_iphone_mirroring_scroll", + "customerMacIphoneMirroringSwipeLeft", + "customer_mac_iphone_mirroring_swipe_left", + "customerMacIphoneMirroringSwipeRight", + "customer_mac_iphone_mirroring_swipe_right", + "customerMacIphoneMirroringSwipeUp", + "customer_mac_iphone_mirroring_swipe_up", + "customerMacIphoneMirroringSwipeDown", + "customer_mac_iphone_mirroring_swipe_down", + "desktopMenu", + "desktop_menu", + "desktopWindow", + "desktop_window", + "desktopBrowserAction", + "desktop_browser_action", + "desktopFocusApp", + "desktop_focus_app", + "customerMacIphoneMirroringOpenApp", + "customer_mac_iphone_mirroring_open_app", + }: + return 20 + if command in { + "desktopClick", + "desktop_click", + "iphoneTap", + "iphone_tap", + }: + return 30 + if command in { + "desktopType", + "desktop_type", + "desktopHotkey", + "desktop_hotkey", + "iphoneType", + "iphone_type", + "customerMacIphoneMirroringTypeApprovedText", + "customer_mac_iphone_mirroring_type_approved_text", + "customerMacIphoneMirroringSendApprovedMessage", + "customer_mac_iphone_mirroring_send_approved_message", + }: + return 15 + return 10 + + +def normalize_control_mode(value): + text = str(value or "").strip().lower().replace("-", "_") + if text in {"full_access", "fullaccess"}: + return "full_access" + if text in {"ask_permission", "askpermission"}: + return "ask_permission" + return text + + +def requested_control_mode(params): + return normalize_control_mode(params.get("mode") or "full-access") + + +def status_matches_control_start(payload, mode): + if not isinstance(payload, dict) or not payload.get("ok"): + return False + data = payload.get("data") + if not isinstance(data, dict): + return False + session = data.get("session") if isinstance(data.get("session"), dict) else {} + active = data.get("active") + kill_switch = data.get("kill_switch") + actual_mode = data.get("mode") + if active is None: + active = session.get("active") + if kill_switch is None: + kill_switch = session.get("kill_switch") + if actual_mode is None: + actual_mode = session.get("mode") + return active is True and kill_switch is False and normalize_control_mode(actual_mode) == mode + + +def reconcile_control_start_after_abort(original_error): + if command not in {"customerMacControlStart", "desktop_control_start"}: + return None + status_body = json.dumps({"command": "customerMacControlStatus", "params": {}}, separators=(",", ":")).encode() + status_request = urllib.request.Request(url, data=status_body, method="POST", headers=headers) + with urllib.request.urlopen(status_request, timeout=15) as response: + status_payload = json.loads(response.read().decode()) + mode = requested_control_mode(params) + if not status_matches_control_start(status_payload, mode): + return None + warnings = status_payload.get("warnings") if isinstance(status_payload.get("warnings"), list) else [] + data = status_payload.get("data") if isinstance(status_payload.get("data"), dict) else {} + data["control_start_reconciled"] = True + data["control_start_original_error"] = type(original_error).__name__ + status_payload["data"] = data + status_payload["warnings"] = [ + { + "code": "control_start_response_reconciled_after_abort", + "message": "Control start response was interrupted after the local session became active; reconciled from control status.", + }, + *warnings, + ] + return status_payload + + +def read_json_env(name): + raw = os.environ.get(name) + if not raw: + return None + try: + return json.loads(raw) + except json.JSONDecodeError: + return None + + +def provider_discovery_endpoint(): + explicit = os.environ.get("EVAOS_PROVIDER_DISCOVERY_URL", "").strip() + if explicit: + return explicit + broker = os.environ.get("EVAOS_DESKTOP_RUNTIME_SESSION_URL", "").strip() + if broker: + return broker + return None + + +def provider_grant_handle_for(agent_runtime): + direct = os.environ.get("EVAOS_PROVIDER_GRANT_HANDLE", "").strip() + if direct: + return direct + grants_payload = read_json_env("EVAOS_PROVIDER_GRANTS_JSON") + runtime_grant = grants_payload.get(agent_runtime) if isinstance(grants_payload, dict) else None + if isinstance(runtime_grant, dict) and isinstance(runtime_grant.get("grant_handle"), str): + return runtime_grant["grant_handle"].strip() or None + cache_payload = read_provider_grant_cache() + cached_runtime_grant = cache_payload.get(agent_runtime) if isinstance(cache_payload, dict) else None + if isinstance(cached_runtime_grant, dict) and isinstance(cached_runtime_grant.get("grant_handle"), str): + return cached_runtime_grant["grant_handle"].strip() or None + return None + + +def provider_grant_cache_path(): + explicit = os.environ.get("EVAOS_PROVIDER_GRANT_CACHE_FILE", "").strip() + if explicit: + return pathlib.Path(explicit) + home = os.environ.get("HOME", "").strip() + if not home: + return None + return pathlib.Path(home) / ".openclaw" / "evaos-provider-grants.json" + + +def read_provider_grant_cache(): + cache_path = provider_grant_cache_path() + if not cache_path: + return None + try: + return json.loads(cache_path.read_text(encoding="utf-8")) + except Exception: + return None + + +def cache_provider_grant_from_broker(agent_runtime, provider_key, payload): + if not isinstance(payload, dict) or not isinstance(payload.get("agent_grant"), dict): + return False + grant = payload["agent_grant"] + grant_handle = grant.get("grant_handle") + if ( + grant.get("provider_key") != provider_key + or grant.get("agent_runtime") != agent_runtime + or not isinstance(grant_handle, str) + or not grant_handle.strip() + ): + return False + cache_path = provider_grant_cache_path() + if not cache_path: + return False + existing = read_provider_grant_cache() + if not isinstance(existing, dict): + existing = {} + existing[agent_runtime] = { + "provider_key": provider_key, + "agent_runtime": agent_runtime, + "grant_handle": grant_handle.strip(), + "expires_at": grant.get("expires_at") if isinstance(grant.get("expires_at"), str) else None, + "cached_at": datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z"), + } + try: + cache_path.parent.mkdir(parents=True, exist_ok=True) + cache_path.write_text(json.dumps(existing, indent=2), encoding="utf-8") + try: + cache_path.chmod(0o600) + except OSError: + pass + return True + except Exception: + return False + + +def provider_agent_discovery_payload(agent_runtime): + endpoint = provider_discovery_endpoint() + customer_id = os.environ.get("EVAOS_CUSTOMER_ID", "").strip() + grant_handle = provider_grant_handle_for(agent_runtime) + if not endpoint or not customer_id or not grant_handle: + return None + + request = urllib.request.Request( + endpoint, + data=json.dumps({ + "action": "provider_agent_discovery", + "customer_id": customer_id, + "agent_runtime": agent_runtime, + }).encode("utf-8"), + headers={ + "Accept": "application/json", + "Content-Type": "application/json", + "X-Evaos-Provider-Grant": grant_handle, + }, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=10) as response: + payload = json.loads(response.read().decode("utf-8")) + except Exception: + return None + + if not isinstance(payload, dict): + return None + active_profile = payload.get("provider_profile") if isinstance(payload.get("provider_profile"), dict) else None + active_key = payload.get("active_provider_key") if isinstance(payload.get("active_provider_key"), str) else None + grant_status = payload.get("grant_status") if isinstance(payload.get("grant_status"), str) else "unknown" + grant_expires_at = payload.get("grant_expires_at") if isinstance(payload.get("grant_expires_at"), str) else None + provider_profiles = [active_profile] if active_profile else [] + profiles_payload = scrub_secrets({ + "ok": True, + "data": { + "customer_id": customer_id, + "provider_profiles": provider_profiles, + "provider_grants": { + agent_runtime: { + "grant_handle": grant_handle, + "status": grant_status, + "expires_at": grant_expires_at, + }, + }, + "active_provider_key": active_key, + "raw_secrets_available": False, + "raw_secrets_stored_in_workbench": False, + "raw_provider_token_returned": False, + "source": "broker", + }, + "warnings": [] if provider_profiles else ["No active provider profile is available from the broker."], + }) + active_payload = scrub_secrets({ + "ok": True, + "data": { + "customer_id": customer_id, + "active_provider_key": active_key, + "active_profile": active_profile, + "provider_identity": payload.get("provider_identity") if isinstance(payload.get("provider_identity"), str) else None, + "provider_scopes": [str(scope) for scope in payload.get("provider_scopes", [])] if isinstance(payload.get("provider_scopes"), list) else [], + "grant_status": grant_status, + "grant_expires_at": grant_expires_at, + "needs_reauth": bool(payload.get("reauth_needed")) or active_profile is None, + "raw_secrets_available": False, + "raw_provider_token_returned": False, + "source": "broker", + }, + "warnings": [] if active_profile else ["No verified active provider grant is available. Ask the customer to connect or re-auth the provider in evaOS Workbench."], + }) + return {"profiles": profiles_payload, "active": active_payload} + + +def provider_profiles_payload(): + broker_payload = provider_agent_discovery_payload("hermes") + if broker_payload: + return broker_payload["profiles"] + + profiles_payload = read_json_env("EVAOS_PROVIDER_PROFILES_JSON") + if isinstance(profiles_payload, list): + provider_profiles = profiles_payload + active_provider_key = os.environ.get("EVAOS_ACTIVE_PROVIDER_KEY") + elif isinstance(profiles_payload, dict): + provider_profiles = profiles_payload.get("provider_profiles") if isinstance(profiles_payload.get("provider_profiles"), list) else [] + active_provider_key = os.environ.get("EVAOS_ACTIVE_PROVIDER_KEY") or profiles_payload.get("active_provider_key") + else: + provider_profiles = [] + active_provider_key = os.environ.get("EVAOS_ACTIVE_PROVIDER_KEY") + payload = { + "ok": True, + "data": { + "customer_id": os.environ.get("EVAOS_CUSTOMER_ID"), + "provider_profiles": provider_profiles, + "provider_grants": read_json_env("EVAOS_PROVIDER_GRANTS_JSON"), + "active_provider_key": active_provider_key, + "raw_secrets_available": False, + "raw_secrets_stored_in_workbench": False, + }, + "warnings": [] if provider_profiles else ["Provider profiles are not configured on this VM yet."], + } + return scrub_secrets(payload) + + +def provider_active_profile_payload(): + broker_payload = provider_agent_discovery_payload("hermes") + if broker_payload: + return broker_payload["active"] + + payload = provider_profiles_payload() + data = payload.get("data", {}) if isinstance(payload, dict) else {} + profiles = data.get("provider_profiles", []) if isinstance(data, dict) else [] + active_key = data.get("active_provider_key") if isinstance(data, dict) else None + active_profile = next((profile for profile in profiles if isinstance(profile, dict) and profile.get("provider_key") == active_key), None) + grants = data.get("provider_grants", {}) if isinstance(data, dict) else {} + hermes_grant = grants.get("hermes") if isinstance(grants, dict) else None + openclaw_grant = grants.get("openclaw") if isinstance(grants, dict) else None + has_connection_proof = ( + isinstance(active_profile, dict) + and active_profile.get("status") == "connected" + and isinstance(active_profile.get("last_validated_at"), str) + and ( + (isinstance(hermes_grant, dict) and isinstance(hermes_grant.get("grant_handle"), str)) + or (isinstance(openclaw_grant, dict) and isinstance(openclaw_grant.get("grant_handle"), str)) + ) + ) + return scrub_secrets({ + "ok": True, + "data": { + "customer_id": os.environ.get("EVAOS_CUSTOMER_ID"), + "active_provider_key": active_key, + "active_profile": active_profile, + "needs_reauth": not has_connection_proof, + "raw_secrets_available": False, + }, + "warnings": [] if has_connection_proof else ["No verified active provider grant is available. Ask the customer to connect or re-auth the provider in evaOS Workbench."], + }) + + +def shared_browser_guidance_payload(): + status = read_json_env("EVAOS_SHARED_BROWSER_STATUS_JSON") + return scrub_secrets({ + "ok": True, + "data": { + "schema_version": "evaos.browser_status.v1", + "customer_id": os.environ.get("EVAOS_CUSTOMER_ID"), + "business_browser_preferred_for_cloud_web_tasks": True, + "shared_browser_preferred_for_cloud_web_tasks": True, + "instructions": "Use Business Browser for cloud web tasks that need a persistent VM browser, user auth/CAPTCHA handoff, or human-visible browsing state. Use local Mac browser tools only when the task explicitly needs the customer's Mac browser.", + "status": status, + }, + "warnings": [] if status else ["Business Browser live status is not configured in this VM environment yet."], + }) + + +def provider_complete_auth_payload(): + endpoint = provider_discovery_endpoint() + customer_id = os.environ.get("EVAOS_CUSTOMER_ID", "").strip() + proof_secret = (os.environ.get("EVAOS_PROVIDER_AUTH_PROOF_SECRET", "").strip() + or os.environ.get("EVAOS_PROVIDER_PROOF_SECRET", "").strip()) + identity = str(params.get("identity") or os.environ.get("EVAOS_PROVIDER_AUTH_IDENTITY") or os.environ.get("EVAOS_PROVIDER_IDENTITY") or "").strip() + scopes_param = params.get("scopes") + if isinstance(scopes_param, list): + scopes = [str(scope).strip() for scope in scopes_param if str(scope).strip()] + else: + scopes = [scope.strip() for scope in os.environ.get("EVAOS_PROVIDER_AUTH_SCOPES", "").split(",") if scope.strip()] + if not scopes: + scopes = ["codex", "offline_access"] + server_secret_ref = str(params.get("server_secret_ref") or os.environ.get("EVAOS_PROVIDER_SERVER_SECRET_REF") or "").strip() + if not server_secret_ref and customer_id: + server_secret_ref = "provider://openai_codex/" + urllib.parse.quote(customer_id, safe="") + "/hermes" + expires_at = str(params.get("expires_at") or os.environ.get("EVAOS_PROVIDER_AUTH_EXPIRES_AT") or "").strip() + if not expires_at: + expires_at = (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1)).isoformat().replace("+00:00", "Z") + + if not endpoint or not customer_id or not proof_secret or not identity or not server_secret_ref.startswith("provider://"): + return { + "ok": False, + "errors": [{ + "code": "provider_auth_proof_not_configured", + "message": "Provider auth proof completion requires broker endpoint, customer id, proof secret, identity, and server secret reference.", + "guidance": "Set EVAOS_PROVIDER_DISCOVERY_URL, EVAOS_CUSTOMER_ID, EVAOS_PROVIDER_AUTH_PROOF_SECRET, EVAOS_PROVIDER_AUTH_IDENTITY, and EVAOS_PROVIDER_SERVER_SECRET_REF on the VM.", + }], + } + + provider_key = "openai_codex" + proof_id = "eap_" + uuid.uuid4().hex + signed_payload = json.dumps({ + "customer_id": customer_id, + "provider_key": provider_key, + "purpose": "provider_auth_complete", + "agent_runtime": "hermes", + "proof_id": proof_id, + "identity": identity, + "scopes": scopes, + "expires_at": expires_at, + "server_secret_ref": server_secret_ref, + }, separators=(",", ":")) + signature = hmac.new(proof_secret.encode("utf-8"), signed_payload.encode("utf-8"), hashlib.sha256).hexdigest() + request_body = { + "action": "provider_auth_complete", + "customer_id": customer_id, + "provider_key": provider_key, + "agent_runtime": "hermes", + "provider_auth_proof": { + "purpose": "provider_auth_complete", + "agent_runtime": "hermes", + "proof_id": proof_id, + "identity": identity, + "scopes": scopes, + "expires_at": expires_at, + "server_secret_ref": server_secret_ref, + "signature": signature, + }, + } + request = urllib.request.Request( + endpoint, + data=json.dumps(request_body, separators=(",", ":")).encode("utf-8"), + headers={ + "Accept": "application/json", + "Content-Type": "application/json", + "X-Evaos-Provider-Proof": "signed-v1", + }, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=10) as response: + response_payload = json.loads(response.read().decode("utf-8")) + except Exception as exc: + return { + "ok": False, + "errors": [{ + "code": "provider_auth_complete_failed", + "message": str(exc), + }], + } + grant_cached = cache_provider_grant_from_broker("hermes", provider_key, response_payload) + return scrub_secrets({ + "ok": True, + "data": { + "connected": bool(response_payload.get("connected")) or response_payload.get("status") == "connected", + "provider_key": provider_key, + "grant_cached": grant_cached, + "response": response_payload, + "raw_provider_token_returned": False, + }, + }) + + +if command in {"evaosProviderProfiles", "providerProfiles"}: + sys.stdout.write(json.dumps(provider_profiles_payload(), separators=(",", ":")) + "\n") + raise SystemExit(0) +if command in {"evaosProviderActiveProfile", "providerActiveProfile"}: + sys.stdout.write(json.dumps(provider_active_profile_payload(), separators=(",", ":")) + "\n") + raise SystemExit(0) +if command in {"evaosProviderCompleteAuth", "providerCompleteAuth"}: + sys.stdout.write(json.dumps(provider_complete_auth_payload(), separators=(",", ":")) + "\n") + raise SystemExit(0) +if command in {"evaosSharedBrowserGuidance", "sharedBrowserGuidance"}: + sys.stdout.write(json.dumps(shared_browser_guidance_payload(), separators=(",", ":")) + "\n") + raise SystemExit(0) +if command == "completeEnrollment": + sys.stdout.write(json.dumps({ + "ok": False, + "errors": [{ + "code": "legacy_pairing_removed", + "message": "Legacy pairing-code enrollment is unavailable.", + "guidance": "Launch the selected customer runtime from evaOS Workbench.", + }], + }, separators=(",", ":")) + "\n") + raise SystemExit(0) + + +headers = { + "Content-Type": "application/json", + "Accept": "application/json", +} +if not os.environ.get("EVAOS_DESKTOP_BRIDGE_URL"): + raise SystemExit("EVAOS_DESKTOP_BRIDGE_URL is required") +if not os.environ.get("EVAOS_DESKTOP_BRIDGE_TOKEN"): + raise SystemExit("EVAOS_DESKTOP_BRIDGE_TOKEN is required") +url = os.environ["EVAOS_DESKTOP_BRIDGE_URL"].rstrip("/") + "/v1/commands" +headers["Authorization"] = f"Bearer {os.environ['EVAOS_DESKTOP_BRIDGE_TOKEN']}" +body = json.dumps({"command": command, "params": params}, separators=(",", ":")).encode() + +request = urllib.request.Request(url, data=body, method="POST", headers=headers) +try: + with urllib.request.urlopen(request, timeout=timeout_for_command(command)) as response: + response_body = response.read().decode() + try: + sys.stdout.write(json.dumps(scrub_secrets(materialize_visual_evidence(json.loads(response_body))), separators=(",", ":"))) + except json.JSONDecodeError: + sys.stdout.write(response_body) + sys.stdout.write("\n") +except urllib.error.HTTPError as exc: + error_body = exc.read().decode(errors="replace") + if error_body.strip().startswith("{"): + try: + sys.stdout.write(json.dumps(scrub_secrets(materialize_visual_evidence(json.loads(error_body))), separators=(",", ":"))) + except json.JSONDecodeError: + sys.stdout.write(error_body) + sys.stdout.write("\n") + raise SystemExit(0) from exc + sys.stderr.write(error_body + "\n") + raise SystemExit(exc.code) from exc +except (TimeoutError, socket.timeout, urllib.error.URLError, ConnectionError, http.client.HTTPException, OSError) as exc: + reconciled = reconcile_control_start_after_abort(exc) + if reconciled is not None: + sys.stdout.write(json.dumps(scrub_secrets(materialize_visual_evidence(reconciled)), separators=(",", ":")) + "\n") + raise SystemExit(0) from exc + raise +PY diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md new file mode 100644 index 0000000000..37f6f5871f --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md @@ -0,0 +1,175 @@ +# evaOS Desktop Bridge for OpenClaw + +OpenClaw plugin wrapper for `evaos-desktop-bridge`. Codex Desktop tools remain +read-oriented except the visible support fallback. Customer Mac/iPhone tools now +use the Workbench connector's customer-granted control session: + +This vendored `0.2.0` package is owned by `100yenadmin/evaOS-GUI`; release tags +continue to use the `evaos-desktop-bridge-openclaw-vX.Y.Z` namespace. + +- **Full Access**: live desktop and iPhone actions run continuously after the + customer starts the session in Workbench. +- **Ask Permission**: navigation stays continuous, but risky clicks, taps, + hotkeys, typing, sends, and other high-impact actions still require approval + evidence. +- The connector keeps the visible session state, audit log, and kill switch. + +## Exposed Tools + +- `desktop_bridge_status` +- `desktop_bridge_capabilities` +- `desktop_bridge_latest` +- `desktop_bridge_audit_tail` +- `desktop_bridge_queue_list` +- `desktop_bridge_queue_append` +- `desktop_bridge_codex_frontmost` +- `desktop_bridge_codex_windows` +- `desktop_bridge_codex_threads` +- `desktop_bridge_codex_thread_map` +- `desktop_bridge_codex_select_thread` +- `desktop_bridge_codex_send_visible_message` +- `desktop_bridge_codex_snapshot` +- `desktop_bridge_codex_inspect` +- `desktop_bridge_codex_ax_tree` +- `desktop_bridge_codex_app_server_status` +- `desktop_bridge_codex_app_server_remote_control_status` +- `desktop_bridge_codex_app_server_threads` +- `desktop_bridge_codex_connections_status` +- `desktop_bridge_codex_app_server_loaded_threads` +- `desktop_bridge_codex_live_status` +- `desktop_bridge_codex_continue_thread` +- `evaos_provider_profiles` +- `evaos_provider_active_profile` +- `evaos_provider_complete_auth` +- `evaos_shared_browser_guidance` +- `customer_mac_status` +- `desktop_control_status` +- `desktop_control_start` +- `desktop_control_stop` +- `desktop_kill_switch` +- `customer_mac_capabilities` +- `desktop_see` +- `desktop_click` +- `desktop_type` +- `desktop_set_value` +- `desktop_scroll` +- `desktop_drag` +- `desktop_hotkey` +- `desktop_focus_app` +- `desktop_window` +- `desktop_menu` +- `desktop_browser_action` +- `customer_mac_snapshot` +- `customer_mac_ax_tree` +- `customer_mac_app_focus` +- `customer_mac_local_site_open` +- `customer_mac_local_site_action` +- `iphone_see` +- `iphone_tap` +- `iphone_swipe` +- `iphone_type` +- `customer_mac_iphone_mirroring_status` +- `customer_mac_iphone_mirroring_focus` +- `customer_mac_iphone_mirroring_home` +- `customer_mac_iphone_mirroring_app_switcher` +- `customer_mac_iphone_mirroring_spotlight` +- `customer_mac_iphone_mirroring_type_spotlight` +- `customer_mac_iphone_mirroring_open_app` +- `customer_mac_iphone_mirroring_tap_named_target` +- `customer_mac_iphone_mirroring_scroll` +- `customer_mac_iphone_mirroring_swipe_left` +- `customer_mac_iphone_mirroring_swipe_right` +- `customer_mac_iphone_mirroring_swipe_up` +- `customer_mac_iphone_mirroring_swipe_down` +- `customer_mac_iphone_mirroring_type_approved_text` +- `customer_mac_iphone_mirroring_send_approved_message` +- `customer_mac_screen_sharing_status` + +The plugin exposes a real computer-use surface for the paired customer Mac and +iPhone Mirroring, plus named guarded Codex Desktop app-server controller tools, +but not a generic shell, hidden AppleScript passthrough, public VNC/SSH, +generic Codex app-server RPC passthrough, session database access, or Screen +Sharing enablement. Codex controller live mode requires `confirm:true` and a +`source_audit_id`; customer Mac/iPhone approval is enforced by connector control +mode, not by hardcoded per-action prompts. + +## Local Setup + +Install the bridge CLI first: + +```bash +python3 -m pip install -e . +evaos-desktop-bridge capabilities --json +``` + +If OpenClaw cannot find the executable, set: + +```bash +export EVAOS_DESKTOP_BRIDGE_BIN=/absolute/path/to/evaos-desktop-bridge +``` + +For a paired customer VM calling the customer's Mac over Headscale, point the +plugin at the connector server instead: + +```bash +export EVAOS_DESKTOP_BRIDGE_URL=http://:8765 +export EVAOS_DESKTOP_BRIDGE_TOKEN="$(cat "$HOME/Library/Application Support/evaos-desktop-bridge/connector.token")" +``` + +Provider/Auth Hub and Shared Browser discovery are metadata-only VM tools. +When the control plane has minted an opaque provider grant, the plugin can +discover the active provider profile from the broker; otherwise it falls back to +optional VM environment snapshots. It never exposes raw provider credentials: + +```bash +export EVAOS_CUSTOMER_ID="" +export EVAOS_PROVIDER_DISCOVERY_URL="https://.supabase.co/functions/v1/desktop-runtime-session" +export EVAOS_PROVIDER_GRANT_HANDLE="epg_..." +export EVAOS_PROVIDER_AUTH_PROOF_SECRET="" +export EVAOS_PROVIDER_SERVER_SECRET_REF="provider://openai_codex//openclaw" +export EVAOS_PROVIDER_AUTH_IDENTITY="user@example.com" +export EVAOS_PROVIDER_GRANT_CACHE_FILE="$HOME/.openclaw/evaos-provider-grants.json" +export EVAOS_PROVIDER_PROFILES_JSON='{"provider_profiles":[]}' +export EVAOS_PROVIDER_GRANTS_JSON='[]' +export EVAOS_SHARED_BROWSER_STATUS_JSON='{"schema_version":"evaos.browser_status.v1","customer_account_id":"acct_123","customer_id":"david-poku","runtime":"browser","status":"ready","room_id":"shared-browser:david-poku","session_id":"browser-session-123","owner":"david-poku","current_url":{"host":"accounts.google.com","path":"/signin","query_redacted":true},"last_activity_at":"2026-06-01T00:00:00Z","needs_auth":true,"needs_captcha":false,"actions":["start_attach","refresh_status","stop_browser"],"source_pointer":"broker:runtime_status:browser","audit_id":"audit_123"}' +``` + +`evaos_shared_browser_guidance` returns the same `evaos.browser_status.v1` +metadata shape that Workbench uses for Business Browser status. It is guidance +only: agents may prefer the brokered browser for auth/CAPTCHA and cloud web +tasks, but this does not add generic browser automation or raw cookie access. +`room_id` identifies the shared browser room; `session_id` identifies the +current browser session when broker evidence includes it. + +`evaos_provider_complete_auth` posts signed metadata proof for the VM-side +Codex/OpenAI readiness check. The proof includes identity, scopes, expiry, and a +server-side secret reference only; raw OpenAI/Codex cookies, access tokens, +refresh tokens, API keys, and authorization headers are never returned through +the plugin. When the broker returns an OpenClaw grant, the plugin stores only +that opaque grant handle in `EVAOS_PROVIDER_GRANT_CACHE_FILE` or +`~/.openclaw/evaos-provider-grants.json` so later discovery works without a +human manually injecting environment variables. + +Connector material must come from the selected Workbench runtime launch. The +plugin exposes no legacy pairing-code or connector-claim tool. + +## Safety Notes + +The plugin registers one exact gateway-authenticated release-canary route at +`POST /api/v1/evaos/mac-control/runtime-receipt`. Its public JSON body accepts +only a bounded random `challenge` and a nonsecret `runRef`. Connector address, +token, customer, VM, and selected-binding authority are accepted only from the +ws-proxy-owned headers. The signed execution context is verified with the +pinned `EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_KEY_ID` and raw 32-byte +`EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_PUBLIC_KEY_B64` before the connector can +be called. The route exposes no generic tool, proxy, action, or signing API. + +Local plugin tools call fixed bridge argv mappings with `shell: false`. Remote +plugin tools post fixed command keys to `/v1/commands` on the paired Mac +connector. Numeric caps are clamped. A `before_tool_call` firewall blocks common +shell/computer escape hatches that would bypass the bridge boundary and requests +approval for guarded Codex visible GUI message sends and legacy guarded customer Mac actions. Codex visible sends also accept `thread_id=current` for the already-open visible thread plus optional `wait_ms` and `poll_interval_ms` parameters for capped read-only post-send progress evidence. New `desktop_*` and +`iphone_*` actions are governed by the Workbench control session: Full Access +allows live action without `approval_audit_id`, Ask Permission gates risky +clicks, taps, hotkeys, typing, sends, and other high-impact actions, and the +kill switch blocks all live control immediately. diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js new file mode 100644 index 0000000000..3905f66c3d --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js @@ -0,0 +1,800 @@ +import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry'; +import { runBridge } from './src/bridge.js'; +import { desktopBridgeFirewall } from './src/firewall.js'; +import { registerMacControlRuntimeReceiptRoute } from './src/runtimeReceipt.js'; +const approvalAuditIdProperty = { + type: 'string', + minLength: 1, + maxLength: 160, + description: 'Required when dry_run is false; use the audit id from the approved dry-run/evidence step.', +}; +export default definePluginEntry({ + id: 'evaos-desktop-bridge', + name: 'EvaOS Desktop Bridge', + description: 'Guarded bridge from OpenClaw to Codex Desktop and paired customer Mac state.', + kind: 'tool', + register(api) { + registerMacControlRuntimeReceiptRoute(api); + for (const bridgeTool of readOnlyTools()) { + api.registerTool(bridgeTool); + } + api.registerHook?.('before_tool_call', desktopBridgeFirewall, { + name: 'evaos-desktop-bridge-firewall', + }); + }, +}); +function readOnlyTools() { + return [ + tool('desktop_bridge_status', 'Read Codex Desktop installation, running, permission, and safety status.', 'status'), + tool( + 'desktop_bridge_capabilities', + 'Read the bridge command capability surface and hard safety boundaries.', + 'capabilities' + ), + tool('desktop_bridge_latest', 'Read the last redacted bridge observation envelope from local state.', 'latest'), + tool('desktop_bridge_audit_tail', 'Read a redacted tail of the append-only local bridge audit log.', 'auditTail', { + type: 'object', + additionalProperties: false, + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 }, + }, + }), + tool( + 'desktop_bridge_codex_frontmost', + 'Read whether Codex Desktop is the current frontmost app.', + 'codexFrontmost' + ), + tool( + 'desktop_bridge_codex_windows', + 'Read visible Codex Desktop window metadata through Accessibility.', + 'codexWindows' + ), + tool('desktop_bridge_queue_list', 'Read capped Eva/OpenClaw announcement queue events.', 'queueList', { + type: 'object', + additionalProperties: false, + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 }, + }, + }), + tool( + 'desktop_bridge_queue_append', + 'Append a local Eva/OpenClaw announcement queue event with source audit provenance.', + 'queueAppend', + { + type: 'object', + additionalProperties: false, + required: ['kind', 'source_audit_id'], + properties: { + kind: { type: 'string', enum: ['idle', 'approval_needed', 'done', 'error', 'attention'] }, + source_audit_id: { type: 'string' }, + message: { type: 'string' }, + }, + } + ), + tool( + 'desktop_bridge_codex_threads', + 'Read visible Codex Desktop thread candidates from GUI state.', + 'codexThreads', + { + type: 'object', + additionalProperties: false, + properties: { + max_items: { type: 'integer', minimum: 1, maximum: 200, default: 50 }, + }, + } + ), + tool( + 'desktop_bridge_codex_thread_map', + 'Read a joined map of visible Codex Desktop thread candidates and saved app-server thread summaries.', + 'codexThreadMap', + { + type: 'object', + additionalProperties: false, + properties: { + max_items: { type: 'integer', minimum: 1, maximum: 200, default: 50 }, + }, + } + ), + tool( + 'desktop_bridge_codex_select_thread', + 'Guarded visible action: select an already-visible Codex thread by visible_id. Dry-run defaults on.', + 'codexSelectThread', + { + type: 'object', + additionalProperties: false, + required: ['thread_id'], + properties: { + thread_id: { type: 'string' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'desktop_bridge_codex_send_visible_message', + 'Live visible Codex GUI action: send an approved message through the frontmost Codex Desktop composer. Dry-run defaults on and live mode requires approval.', + 'codexSendVisibleMessage', + { + type: 'object', + additionalProperties: false, + required: ['thread_id', 'message'], + properties: { + thread_id: { type: 'string' }, + message: { type: 'string', minLength: 1, maxLength: 4000 }, + dry_run: { type: 'boolean', default: true }, + confirm: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + wait_ms: { type: 'integer', minimum: 0, maximum: 120000, default: 0 }, + poll_interval_ms: { type: 'integer', minimum: 250, maximum: 10000, default: 2000 }, + }, + } + ), + tool( + 'desktop_bridge_codex_continue_thread', + "Support-only fallback: select a visible Codex thread by title and submit the exact prompt 'continue'. Dry-run defaults on.", + 'codexContinueThread', + { + type: 'object', + additionalProperties: false, + required: ['title'], + properties: { + title: { type: 'string', minLength: 1, maxLength: 160 }, + prompt: { type: 'string', enum: ['continue'], default: 'continue' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'desktop_bridge_codex_snapshot', + 'Read a capped visible Codex Desktop snapshot; screenshots are skipped unless Codex is frontmost.', + 'codexSnapshot', + { + type: 'object', + additionalProperties: false, + properties: { + max_chars: { type: 'integer', minimum: 1, maximum: 20000, default: 4000 }, + }, + } + ), + tool( + 'desktop_bridge_codex_inspect', + 'Read a compact Codex Desktop page map with visible windows, controls, and text summaries.', + 'codexInspect', + { + type: 'object', + additionalProperties: false, + properties: { + max_nodes: { type: 'integer', minimum: 1, maximum: 1000, default: 120 }, + }, + } + ), + tool( + 'desktop_bridge_codex_ax_tree', + 'Read a capped Codex Desktop Accessibility tree summary with roles and names only.', + 'codexAxTree', + { + type: 'object', + additionalProperties: false, + properties: { + max_nodes: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }, + }, + } + ), + tool( + 'desktop_bridge_codex_app_server_status', + 'Read Codex app-server availability and read-only method allowlist.', + 'codexAppServerStatus' + ), + tool( + 'desktop_bridge_codex_connections_status', + 'Read Codex Desktop app-server, daemon, websocket, remote-control, and notification readiness.', + 'codexConnectionsStatus' + ), + tool( + 'desktop_bridge_codex_app_server_remote_control_status', + 'Read Codex native remote-control readiness without enabling or mutating it.', + 'codexAppServerRemoteControlStatus' + ), + tool( + 'desktop_bridge_codex_app_server_threads', + 'Read capped Codex thread summaries through the app-server read allowlist.', + 'codexAppServerThreads', + { + type: 'object', + additionalProperties: false, + properties: { + max_items: { type: 'integer', minimum: 1, maximum: 200, default: 50 }, + }, + } + ), + tool( + 'desktop_bridge_codex_app_server_loaded_threads', + 'Read loaded Codex app-server thread ids for readiness diagnostics.', + 'codexAppServerLoadedThreads', + { + type: 'object', + additionalProperties: false, + properties: { + max_items: { type: 'integer', minimum: 1, maximum: 200, default: 50 }, + }, + } + ), + tool( + 'desktop_bridge_codex_live_status', + 'Read buffered Codex app-server live notifications for one loaded thread.', + 'codexLiveStatus', + { + type: 'object', + additionalProperties: false, + required: ['thread_id'], + properties: { + thread_id: { type: 'string', minLength: 1, maxLength: 240 }, + duration_ms: { type: 'integer', minimum: 1, maximum: 30000, default: 1000 }, + }, + } + ), + tool( + 'evaos_provider_profiles', + 'Read active evaOS provider profile metadata and brokered grant readiness without raw provider secrets.', + 'evaosProviderProfiles' + ), + tool( + 'evaos_provider_active_profile', + 'Read the active provider profile and whether OpenClaw/Hermes should re-auth.', + 'evaosProviderActiveProfile' + ), + tool( + 'evaos_provider_complete_auth', + 'Complete OpenAI / Codex provider auth by sending signed metadata proof to the evaOS broker. Does not expose raw provider credentials.', + 'evaosProviderCompleteAuth', + { + type: 'object', + additionalProperties: false, + properties: { + identity: { type: 'string', minLength: 3, maxLength: 240 }, + scopes: { + type: 'array', + maxItems: 12, + items: { type: 'string', minLength: 1, maxLength: 80 }, + }, + expires_at: { type: 'string', minLength: 10, maxLength: 80 }, + server_secret_ref: { type: 'string', minLength: 16, maxLength: 240 }, + }, + } + ), + tool( + 'evaos_shared_browser_guidance', + 'Read Business Browser status/guidance so cloud agents default to the brokered VM browser for web tasks.', + 'evaosSharedBrowserGuidance' + ), + tool( + 'customer_mac_status', + 'Read paired customer Mac connector, iPhone Mirroring, and Screen Sharing readiness.', + 'customerMacStatus' + ), + tool( + 'desktop_control_status', + 'Read the customer-granted Full Access / Ask Permission control session state.', + 'customerMacControlStatus' + ), + tool( + 'desktop_control_start', + 'Start a customer-granted agent control session. Live actions wait for the 10-second operator takeover warning; Full Access then allows continuous desktop and iPhone actions without per-action prompts.', + 'customerMacControlStart', + { + type: 'object', + additionalProperties: false, + properties: { + mode: { type: 'string', enum: ['full-access', 'ask-permission'], default: 'full-access' }, + agent_label: { type: 'string', minLength: 1, maxLength: 160 }, + }, + } + ), + tool('desktop_control_stop', 'Stop the active customer-granted agent control session.', 'customerMacControlStop'), + tool( + 'desktop_kill_switch', + 'Immediately stop and block future customer Mac control commands until the customer starts a new session.', + 'customerMacControlKillSwitch' + ), + tool( + 'customer_mac_capabilities', + 'Read supported named customer Mac actions and hard safety boundaries.', + 'customerMacCapabilities' + ), + tool( + 'desktop_see', + "See the customer's Mac through Peekaboo or the built-in screen/Accessibility fallback.", + 'desktopSee', + { + type: 'object', + additionalProperties: false, + properties: { + max_chars: { type: 'integer', minimum: 1, maximum: 20000, default: 4000 }, + max_nodes: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }, + }, + } + ), + tool( + 'desktop_click', + "Click a visible target label or, when labels are unavailable, an x/y point on the customer's Mac.", + 'desktopClick', + { + type: 'object', + additionalProperties: false, + properties: { + target_label: { type: 'string', minLength: 1, maxLength: 200 }, + snapshot_id: { type: 'string', minLength: 1, maxLength: 120 }, + element_id: { type: 'string', minLength: 1, maxLength: 80 }, + x: { type: 'integer' }, + y: { type: 'integer' }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool('desktop_type', 'Type exact text into the focused Mac field.', 'desktopType', { + type: 'object', + additionalProperties: false, + required: ['text'], + properties: { + text: { type: 'string', minLength: 1, maxLength: 4000 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool( + 'desktop_set_value', + 'Set an AX-backed native text field from a fresh desktop snapshot element. Dry-run defaults on and live mode may require approval.', + 'desktopSetValue', + { + type: 'object', + additionalProperties: false, + required: ['snapshot_id', 'element_id', 'value'], + properties: { + snapshot_id: { type: 'string', minLength: 1, maxLength: 120 }, + element_id: { type: 'string', minLength: 1, maxLength: 80 }, + value: { type: 'string', minLength: 1, maxLength: 4000 }, + attribute: { type: 'string', enum: ['value', 'selected_text'], default: 'value' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool('desktop_scroll', 'Scroll the focused Mac surface.', 'desktopScroll', { + type: 'object', + additionalProperties: false, + properties: { + direction: { type: 'string', enum: ['up', 'down', 'left', 'right'], default: 'down' }, + amount: { type: 'integer', minimum: 1, maximum: 5000, default: 600 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('desktop_drag', "Drag from one point to another on the customer's Mac.", 'desktopDrag', { + type: 'object', + additionalProperties: false, + required: ['from_x', 'from_y', 'to_x', 'to_y'], + properties: { + from_x: { type: 'integer' }, + from_y: { type: 'integer' }, + to_x: { type: 'integer' }, + to_y: { type: 'integer' }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('desktop_hotkey', 'Press a Mac hotkey such as cmd+l, cmd+r, or cmd+shift+4.', 'desktopHotkey', { + type: 'object', + additionalProperties: false, + required: ['keys'], + properties: { + keys: { type: 'string', minLength: 1, maxLength: 80 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('desktop_focus_app', 'Focus or open a Mac app by name.', 'desktopFocusApp', { + type: 'object', + additionalProperties: false, + required: ['app_name'], + properties: { + app_name: { type: 'string', minLength: 1, maxLength: 120 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('desktop_window', 'Perform a focused-window action: focus, minimize, maximize, or close.', 'desktopWindow', { + type: 'object', + additionalProperties: false, + required: ['action'], + properties: { + action: { type: 'string', enum: ['focus', 'minimize', 'maximize', 'zoom', 'close'] }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('desktop_menu', 'Choose a visible menu path through Peekaboo, for example File > New Tab.', 'desktopMenu', { + type: 'object', + additionalProperties: false, + required: ['menu_path'], + properties: { + menu_path: { type: 'string', minLength: 1, maxLength: 240 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool( + 'desktop_browser_action', + 'Use the local Mac browser: reload, back, forward, new_tab, or open_url.', + 'desktopBrowserAction', + { + type: 'object', + additionalProperties: false, + required: ['action'], + properties: { + action: { type: 'string', enum: ['reload', 'back', 'forward', 'new_tab', 'open_url'] }, + url: { type: 'string', maxLength: 2048 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_snapshot', + 'Read a safe screenshot path for the frontmost non-sensitive app; sensitive apps are blocked.', + 'customerMacSnapshot', + { + type: 'object', + additionalProperties: false, + properties: { + max_chars: { type: 'integer', minimum: 1, maximum: 20000, default: 4000 }, + }, + } + ), + tool( + 'customer_mac_ax_tree', + 'Read a capped Accessibility tree for the frontmost non-sensitive app.', + 'customerMacAxTree', + { + type: 'object', + additionalProperties: false, + properties: { + max_nodes: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }, + }, + } + ), + tool( + 'customer_mac_app_focus', + 'Approval-gated named action: focus a non-sensitive customer Mac app by name.', + 'customerMacAppFocus', + { + type: 'object', + additionalProperties: false, + required: ['app_name'], + properties: { + app_name: { type: 'string' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_local_site_open', + 'Approval-gated named action: open a localhost, loopback, or .local website on the customer Mac.', + 'customerMacLocalSiteOpen', + { + type: 'object', + additionalProperties: false, + required: ['url'], + properties: { + url: { type: 'string' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_local_site_action', + 'Approval-gated named action: run reload, back, or forward in the frontmost supported browser.', + 'customerMacLocalSiteAction', + { + type: 'object', + additionalProperties: false, + required: ['action'], + properties: { + action: { type: 'string', enum: ['reload', 'back', 'forward'] }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool('iphone_see', 'See the visible iPhone Mirroring surface through the paired Mac.', 'iphoneSee', { + type: 'object', + additionalProperties: false, + properties: { + max_chars: { type: 'integer', minimum: 1, maximum: 20000, default: 4000 }, + max_nodes: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }, + }, + }), + tool( + 'iphone_tap', + 'Tap a visible iPhone target label or fallback x/y point inside iPhone Mirroring.', + 'iphoneTap', + { + type: 'object', + additionalProperties: false, + properties: { + target_label: { type: 'string', minLength: 1, maxLength: 200 }, + snapshot_id: { type: 'string', minLength: 1, maxLength: 120 }, + element_id: { type: 'string', minLength: 1, maxLength: 80 }, + x: { type: 'integer' }, + y: { type: 'integer' }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool('iphone_swipe', 'Swipe the focused iPhone Mirroring surface.', 'iphoneSwipe', { + type: 'object', + additionalProperties: false, + required: ['direction'], + properties: { + direction: { type: 'string', enum: ['left', 'right', 'up', 'down'] }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('iphone_type', 'Type exact text into the focused iPhone Mirroring field.', 'iphoneType', { + type: 'object', + additionalProperties: false, + required: ['text'], + properties: { + text: { type: 'string', minLength: 1, maxLength: 4000 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool( + 'customer_mac_iphone_mirroring_status', + 'Read iPhone Mirroring readiness and supported named actions.', + 'customerMacIphoneMirroringStatus' + ), + tool( + 'customer_mac_iphone_mirroring_focus', + 'Approval-gated named action: focus iPhone Mirroring.', + 'customerMacIphoneMirroringFocus', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_home', + 'Approval-gated named action: send Home to iPhone Mirroring.', + 'customerMacIphoneMirroringHome', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_app_switcher', + 'Approval-gated named action: open the iPhone Mirroring App Switcher.', + 'customerMacIphoneMirroringAppSwitcher', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_spotlight', + 'Approval-gated named action: open iPhone Spotlight through iPhone Mirroring.', + 'customerMacIphoneMirroringSpotlight', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_type_spotlight', + 'Approval-gated named action: type short disposable/search text into iPhone Spotlight.', + 'customerMacIphoneMirroringTypeSpotlight', + { + type: 'object', + additionalProperties: false, + required: ['text'], + properties: { + text: { type: 'string', minLength: 1, maxLength: 80 }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_iphone_mirroring_open_app', + 'Approval-gated named action: launch a non-sensitive iPhone app through Spotlight.', + 'customerMacIphoneMirroringOpenApp', + { + type: 'object', + additionalProperties: false, + required: ['app_name'], + properties: { + app_name: { type: 'string', minLength: 1, maxLength: 80 }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_iphone_mirroring_tap_named_target', + 'Approval-gated named action: press an exact visible iPhone Mirroring AX label; generic coordinates are blocked.', + 'customerMacIphoneMirroringTapNamedTarget', + { + type: 'object', + additionalProperties: false, + required: ['target_label'], + properties: { + target_label: { type: 'string', minLength: 1, maxLength: 80 }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_iphone_mirroring_scroll', + 'Approval-gated named action: scroll the focused iPhone Mirroring window by named direction.', + 'customerMacIphoneMirroringScroll', + { + type: 'object', + additionalProperties: false, + properties: { + direction: { type: 'string', enum: ['up', 'down'], default: 'down' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_iphone_mirroring_swipe_left', + 'Approval-gated named action: swipe left in iPhone Mirroring. Requires dry-run approval before live use.', + 'customerMacIphoneMirroringSwipeLeft', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_swipe_right', + 'Approval-gated named action: swipe right in iPhone Mirroring. Requires dry-run approval before live use.', + 'customerMacIphoneMirroringSwipeRight', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_swipe_up', + 'Approval-gated named action: swipe up in iPhone Mirroring. Requires dry-run approval before live use.', + 'customerMacIphoneMirroringSwipeUp', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_swipe_down', + 'Approval-gated named action: swipe down in iPhone Mirroring. Requires dry-run approval before live use.', + 'customerMacIphoneMirroringSwipeDown', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_type_approved_text', + 'Approval-gated named action: type exact same-turn-approved text in iPhone Mirroring.', + 'customerMacIphoneMirroringTypeApprovedText', + { + type: 'object', + additionalProperties: false, + required: ['text'], + properties: { + text: { type: 'string', minLength: 1, maxLength: 240 }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_iphone_mirroring_send_approved_message', + 'Full Access iPhone action: type and send one exact message through visible iPhone Mirroring; Ask Permission gates it.', + 'customerMacIphoneMirroringSendApprovedMessage', + { + type: 'object', + additionalProperties: false, + required: ['text', 'recipient_context'], + properties: { + text: { type: 'string', minLength: 1, maxLength: 240 }, + recipient_context: { type: 'string', minLength: 1, maxLength: 160 }, + target_label: { type: 'string', minLength: 1, maxLength: 80, default: 'Send' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_screen_sharing_status', + 'Read Screen Sharing/Remote Management status; this tool cannot enable it.', + 'customerMacScreenSharingStatus' + ), + ]; +} +function tool( + name, + description, + command, + parameters = { type: 'object', additionalProperties: false, properties: {}, required: [] } +) { + return { + name, + description, + parameters: normalizeToolParameters(parameters), + execute: async (_toolCallId, params = {}) => toToolResult(await runBridge(command, params)), + }; +} +function toToolResult(payload) { + if (isToolResultLike(payload)) { + return payload; + } + return { + content: [{ type: 'text', text: stringifyToolPayload(payload) }], + details: payload, + }; +} +function isToolResultLike(value) { + return isRecord(value) && Array.isArray(value.content); +} +function stringifyToolPayload(payload) { + if (typeof payload === 'string') { + return payload; + } + try { + const text = JSON.stringify(payload, null, 2); + if (typeof text === 'string') { + return text; + } + } catch { + // Fall through to String(payload). + } + return String(payload); +} +function isRecord(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} +function normalizeToolParameters(parameters) { + const properties = + parameters.properties && typeof parameters.properties === 'object' && !Array.isArray(parameters.properties) + ? parameters.properties + : {}; + const required = Array.isArray(parameters.required) + ? parameters.required + .filter((value) => typeof value === 'string' && value.length > 0) + .filter((value) => Object.prototype.hasOwnProperty.call(properties, value)) + : []; + return { + ...parameters, + type: 'object', + additionalProperties: parameters.additionalProperties === undefined ? false : parameters.additionalProperties, + properties, + required, + }; +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js new file mode 100644 index 0000000000..1bb80efafa --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js @@ -0,0 +1,1541 @@ +import { execFile } from 'node:child_process'; +import { createHmac, randomUUID } from 'node:crypto'; +import * as fs from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { promisify } from 'node:util'; +const execFileAsync = promisify(execFile); +const fsCompat = fs; +const FIXED_COMMANDS = { + status: ['status', '--json'], + capabilities: ['capabilities', '--json'], + latest: ['latest', '--json'], + codexFrontmost: ['codex', 'frontmost', '--json'], + codexWindows: ['codex', 'windows', '--json'], + codexConnectionsStatus: ['codex', 'connections', 'status', '--json'], + codexAppServerStatus: ['codex', 'app-server', 'status', '--json'], + codexAppServerRemoteControlStatus: ['codex', 'app-server', 'remote-control-status', '--json'], + customerMacStatus: ['customer-mac', 'status', '--json'], + customerMacCapabilities: ['customer-mac', 'capabilities', '--json'], + customerMacControlStatus: ['customer-mac', 'control', 'status', '--json'], + customerMacControlStop: ['customer-mac', 'control', 'stop', '--json'], + customerMacControlKillSwitch: ['customer-mac', 'control', 'kill-switch', '--json'], + customerMacIphoneMirroringStatus: ['customer-mac', 'iphone-mirroring', 'status', '--json'], + customerMacScreenSharingStatus: ['customer-mac', 'screen-sharing', 'status', '--json'], +}; +const CUSTOMER_MAC_REMOTE_COMMANDS = new Set([ + 'customerMacStatus', + 'customerMacCapabilities', + 'customerMacControlStatus', + 'customerMacControlStart', + 'customerMacControlStop', + 'customerMacControlKillSwitch', + 'desktopSee', + 'desktopClick', + 'desktopType', + 'desktopSetValue', + 'desktopScroll', + 'desktopDrag', + 'desktopHotkey', + 'desktopFocusApp', + 'desktopWindow', + 'desktopMenu', + 'desktopBrowserAction', + 'customerMacSnapshot', + 'customerMacAxTree', + 'customerMacAppFocus', + 'customerMacLocalSiteOpen', + 'customerMacLocalSiteAction', + 'customerMacIphoneMirroringStatus', + 'iphoneSee', + 'iphoneTap', + 'iphoneSwipe', + 'iphoneType', + 'customerMacIphoneMirroringFocus', + 'customerMacIphoneMirroringHome', + 'customerMacIphoneMirroringAppSwitcher', + 'customerMacIphoneMirroringSpotlight', + 'customerMacIphoneMirroringTypeSpotlight', + 'customerMacIphoneMirroringOpenApp', + 'customerMacIphoneMirroringTapNamedTarget', + 'customerMacIphoneMirroringScroll', + 'customerMacIphoneMirroringSwipeLeft', + 'customerMacIphoneMirroringSwipeRight', + 'customerMacIphoneMirroringSwipeUp', + 'customerMacIphoneMirroringSwipeDown', + 'customerMacIphoneMirroringTypeApprovedText', + 'customerMacIphoneMirroringSendApprovedMessage', + 'customerMacScreenSharingStatus', +]); +export function buildBridgeArgv(command, params = {}) { + if (command in FIXED_COMMANDS) { + return FIXED_COMMANDS[command]; + } + if (command === 'auditTail') { + return ['audit-tail', '--json', '--limit', String(clampInt(params.limit, 20, 1, 100))]; + } + if (command === 'queueList') { + return ['queue', 'list', '--json', '--limit', String(clampInt(params.limit, 20, 1, 100))]; + } + if (command === 'queueAppend') { + return [ + 'queue', + 'append', + '--json', + '--kind', + requiredString(params.kind, 'kind'), + '--source-audit-id', + requiredString(params.source_audit_id, 'source_audit_id'), + ...(params.message ? ['--message', String(params.message)] : []), + ]; + } + if (command === 'codexThreads') { + return ['codex', 'threads', '--json', '--max-items', String(clampInt(params.max_items, 50, 1, 200))]; + } + if (command === 'codexThreadMap') { + return ['codex', 'thread-map', '--json', '--max-items', String(clampInt(params.max_items, 50, 1, 200))]; + } + if (command === 'codexSelectThread') { + return [ + 'codex', + 'select-thread', + '--json', + '--thread-id', + requiredString(params.thread_id, 'thread_id'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'codexContinueThread') { + return [ + 'codex', + 'continue-thread', + '--json', + '--title', + requiredString(params.title, 'title'), + '--prompt', + String(params.prompt || 'continue'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'codexSendVisibleMessage') { + const messageFile = + typeof params.message_file === 'string' && params.message_file.trim() !== '' + ? params.message_file.trim() + : undefined; + return [ + 'codex', + 'send-visible-message', + '--json', + '--thread-id', + requiredString(params.thread_id, 'thread_id'), + ...(messageFile ? ['--message-file', messageFile] : ['--message', requiredString(params.message, 'message')]), + ...(params.dry_run !== false ? ['--dry-run'] : ['--live']), + ...(params.confirm === true ? ['--confirm'] : []), + ...guardedApprovalArg(params), + ...(params.wait_ms !== undefined ? ['--wait-ms', String(clampInt(params.wait_ms, 0, 0, 120000))] : []), + ...(params.poll_interval_ms !== undefined + ? ['--poll-interval-ms', String(clampInt(params.poll_interval_ms, 2000, 250, 10000))] + : []), + ]; + } + if (command === 'codexSnapshot') { + return ['codex', 'snapshot', '--json', '--max-chars', String(clampInt(params.max_chars, 4000, 1, 20000))]; + } + if (command === 'codexInspect') { + return ['codex', 'inspect', '--json', '--max-nodes', String(clampInt(params.max_nodes, 120, 1, 1000))]; + } + if (command === 'codexAxTree') { + return ['codex', 'ax-tree', '--json', '--max-nodes', String(clampInt(params.max_nodes, 200, 1, 1000))]; + } + if (command === 'codexAppServerThreads') { + return ['codex', 'app-server', 'threads', '--json', '--max-items', String(clampInt(params.max_items, 50, 1, 200))]; + } + if (command === 'codexAppServerLoadedThreads') { + return [ + 'codex', + 'app-server', + 'loaded-threads', + '--json', + '--max-items', + String(clampInt(params.max_items, 50, 1, 200)), + ]; + } + if (command === 'codexLiveStatus') { + return [ + 'codex', + 'app-server', + 'subscribe', + '--json', + '--thread-id', + requiredString(params.thread_id, 'thread_id'), + '--duration-ms', + String(clampInt(params.duration_ms, 1000, 1, 30000)), + ]; + } + if (command === 'customerMacSnapshot') { + return ['customer-mac', 'snapshot', '--json', '--max-chars', String(clampInt(params.max_chars, 4000, 1, 20000))]; + } + if (command === 'customerMacAxTree') { + return ['customer-mac', 'ax-tree', '--json', '--max-nodes', String(clampInt(params.max_nodes, 200, 1, 1000))]; + } + if (command === 'customerMacControlStart') { + return [ + 'customer-mac', + 'control', + 'start', + '--json', + '--mode', + String(params.mode || 'full-access'), + ...(params.agent_label ? ['--agent-label', String(params.agent_label)] : []), + ]; + } + if (command === 'desktopSee') { + return [ + 'customer-mac', + 'desktop', + 'see', + '--json', + '--max-chars', + String(clampInt(params.max_chars, 4000, 1, 20000)), + '--max-nodes', + String(clampInt(params.max_nodes, 200, 1, 1000)), + ]; + } + if (command === 'desktopClick') { + return [ + 'customer-mac', + 'desktop', + 'click', + '--json', + ...optionalStringArg(params.snapshot_id, '--snapshot-id'), + ...optionalStringArg(params.element_id, '--element-id'), + ...optionalStringArg(params.target_label, '--target-label'), + ...optionalNumberArg(params.x, '--x'), + ...optionalNumberArg(params.y, '--y'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopType') { + return [ + 'customer-mac', + 'desktop', + 'type', + '--json', + '--text', + requiredString(params.text, 'text'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopSetValue') { + const valueFile = + typeof params.value_file === 'string' && params.value_file.trim() !== '' ? params.value_file.trim() : undefined; + if (!valueFile) { + throw new Error('desktopSetValue value must be materialized before building CLI argv.'); + } + return [ + 'customer-mac', + 'desktop', + 'set-value', + '--json', + '--snapshot-id', + requiredString(params.snapshot_id, 'snapshot_id'), + '--element-id', + requiredString(params.element_id, 'element_id'), + '--value-file', + valueFile, + '--attribute', + String(params.attribute || 'value'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopScroll') { + return [ + 'customer-mac', + 'desktop', + 'scroll', + '--json', + '--direction', + String(params.direction || 'down'), + '--amount', + String(clampInt(params.amount, 600, 1, 5000)), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopDrag') { + return [ + 'customer-mac', + 'desktop', + 'drag', + '--json', + '--from-x', + requiredNumberString(params.from_x, 'from_x'), + '--from-y', + requiredNumberString(params.from_y, 'from_y'), + '--to-x', + requiredNumberString(params.to_x, 'to_x'), + '--to-y', + requiredNumberString(params.to_y, 'to_y'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopHotkey') { + return [ + 'customer-mac', + 'desktop', + 'hotkey', + '--json', + '--keys', + requiredString(params.keys, 'keys'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopFocusApp') { + return [ + 'customer-mac', + 'desktop', + 'focus-app', + '--json', + '--app-name', + requiredString(params.app_name, 'app_name'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopWindow') { + return [ + 'customer-mac', + 'desktop', + 'window', + '--json', + '--action', + requiredString(params.action, 'action'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopMenu') { + return [ + 'customer-mac', + 'desktop', + 'menu', + '--json', + '--menu-path', + requiredString(params.menu_path, 'menu_path'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopBrowserAction') { + return [ + 'customer-mac', + 'desktop', + 'browser-action', + '--json', + '--action', + requiredString(params.action, 'action'), + ...optionalStringArg(params.url, '--url'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacAppFocus') { + return [ + 'customer-mac', + 'app-focus', + '--json', + '--app-name', + requiredString(params.app_name, 'app_name'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacLocalSiteOpen') { + return [ + 'customer-mac', + 'local-site', + 'open', + '--json', + '--url', + requiredString(params.url, 'url'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacLocalSiteAction') { + return [ + 'customer-mac', + 'local-site', + 'action', + '--json', + '--action', + requiredString(params.action, 'action'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'iphoneSee') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'see', + '--json', + '--max-chars', + String(clampInt(params.max_chars, 4000, 1, 20000)), + '--max-nodes', + String(clampInt(params.max_nodes, 200, 1, 1000)), + ]; + } + if (command === 'iphoneTap') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'tap', + '--json', + ...optionalStringArg(params.snapshot_id, '--snapshot-id'), + ...optionalStringArg(params.element_id, '--element-id'), + ...optionalStringArg(params.target_label, '--target-label'), + ...optionalNumberArg(params.x, '--x'), + ...optionalNumberArg(params.y, '--y'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'iphoneSwipe') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'swipe', + '--json', + '--direction', + requiredString(params.direction, 'direction'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'iphoneType') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'type', + '--json', + '--text', + requiredString(params.text, 'text'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringFocus') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'focus', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringHome') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'home', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringAppSwitcher') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'app-switcher', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSpotlight') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'spotlight', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringTypeSpotlight') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'type-spotlight', + '--json', + '--text', + requiredString(params.text, 'text'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringOpenApp') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'open-app', + '--json', + '--app-name', + requiredString(params.app_name, 'app_name'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringTapNamedTarget') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'tap-named-target', + '--json', + '--target-label', + requiredString(params.target_label, 'target_label'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringScroll') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'scroll', + '--json', + '--direction', + String(params.direction || 'down'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSwipeLeft') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'swipe-left', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSwipeRight') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'swipe-right', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSwipeUp') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'swipe-up', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSwipeDown') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'swipe-down', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringTypeApprovedText') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'type-approved-text', + '--json', + '--text', + requiredString(params.text, 'text'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSendApprovedMessage') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'send-approved-message', + '--json', + '--text', + requiredString(params.text, 'text'), + '--recipient-context', + requiredString(params.recipient_context, 'recipient_context'), + '--target-label', + String(params.target_label || 'Send'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + throw new Error(`Unsupported bridge command key: ${String(command)}`); +} +function approvalArg(params) { + if (typeof params.approval_audit_id !== 'string' || params.approval_audit_id.trim() === '') { + return []; + } + return ['--approval-audit-id', params.approval_audit_id.trim()]; +} +function guardedApprovalArg(params) { + if (params.dry_run !== false) { + return []; + } + return approvalArg(params); +} +function requiredString(value, name) { + if (typeof value !== 'string' || value.trim() === '') { + throw new Error(`${name} is required`); + } + return value; +} +function requiredNumberString(value, name) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new Error(`${name} is required`); + } + return String(Math.trunc(value)); +} +function optionalNumberArg(value, flag) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return []; + } + return [flag, String(Math.trunc(value))]; +} +function optionalStringArg(value, flag) { + if (typeof value !== 'string' || value.trim() === '') { + return []; + } + return [flag, value.trim()]; +} +export async function runBridge(command, params = {}) { + if (String(command) === 'customerMacCompletePairing') { + return { + ok: false, + errors: [ + { + code: 'legacy_pairing_removed', + message: 'Legacy pairing-code enrollment is unavailable.', + guidance: 'Launch the selected customer runtime from evaOS Workbench.', + }, + ], + }; + } + if (command === 'evaosProviderProfiles') { + return await providerProfilesPayload(); + } + if (command === 'evaosProviderActiveProfile') { + return await providerActiveProfilePayload(); + } + if (command === 'evaosProviderCompleteAuth') { + return await providerCompleteAuthPayload(params); + } + if (command === 'evaosSharedBrowserGuidance') { + return sharedBrowserGuidancePayload(); + } + loadDesktopBridgeEnvFile(); + const remoteURL = process.env.EVAOS_DESKTOP_BRIDGE_URL?.trim(); + const remoteToken = process.env.EVAOS_DESKTOP_BRIDGE_TOKEN?.trim(); + if (isCustomerMacRemoteCommand(command)) { + const missing = missingRemoteConnectorMaterial(remoteURL, remoteToken); + if (missing.length > 0) { + return customerMacConnectorMaterialMissing(command, missing); + } + } + if (remoteURL) { + return runRemoteBridge(remoteURL, command, params); + } + return withLocalMessagePayload(command, params, async (safeParams) => { + const bin = process.env.EVAOS_DESKTOP_BRIDGE_BIN || 'evaos-desktop-bridge'; + const argv = buildBridgeArgv(command, safeParams); + try { + const { stdout } = await execFileAsync(bin, argv, { + shell: false, + timeout: timeoutForCommand(command), + maxBuffer: 8 * 1024 * 1024, + }); + return materializeVisualEvidence(command, JSON.parse(stdout)); + } catch (error) { + const err = error; + if (err.stdout) { + try { + return JSON.parse(err.stdout); + } catch { + // Fall through to structured wrapper below. + } + } + return { + ok: false, + errors: [ + { + code: 'bridge_cli_failed', + message: safeBridgeErrorMessage(command, err.message), + guidance: 'Install evaos-desktop-bridge locally and set EVAOS_DESKTOP_BRIDGE_BIN if it is not on PATH.', + }, + ], + }; + } + }); +} +function isCustomerMacRemoteCommand(command) { + return CUSTOMER_MAC_REMOTE_COMMANDS.has(command); +} +function missingRemoteConnectorMaterial(remoteURL, remoteToken) { + const missing = []; + if (!remoteURL) { + missing.push('EVAOS_DESKTOP_BRIDGE_URL'); + } + if (!remoteToken) { + missing.push('EVAOS_DESKTOP_BRIDGE_TOKEN'); + } + return missing; +} +function customerMacConnectorMaterialMissing(command, missing) { + return { + ok: false, + errors: [ + { + code: 'mac_connector_material_missing', + message: 'Customer Mac tools require broker/session-provided remote connector material.', + guidance: + 'Launch the runtime from Workbench for the selected customer so the brokered connector contract is present before using customer Mac tools.', + details: { + command, + missing, + required: ['EVAOS_DESKTOP_BRIDGE_URL', 'EVAOS_DESKTOP_BRIDGE_TOKEN'], + route: '/v1/commands', + env_file: displayDesktopBridgeEnvPath(desktopBridgeEnvPath()), + local_fallback: false, + }, + }, + ], + }; +} +async function withLocalMessagePayload(command, params, callback) { + const isCodexMessage = command === 'codexSendVisibleMessage' && typeof params.message === 'string'; + const isDesktopSetValue = command === 'desktopSetValue' && typeof params.value === 'string'; + if (!isCodexMessage && !isDesktopSetValue) { + return callback(params); + } + const dir = fsCompat.mkdtempSync( + path.join( + process.env.TMPDIR || '/tmp', + isCodexMessage ? 'evaos-codex-visible-message-' : 'evaos-desktop-set-value-' + ) + ); + const payloadFile = path.join(dir, isCodexMessage ? 'message.txt' : 'value.txt'); + try { + await writeFile(payloadFile, isCodexMessage ? String(params.message) : String(params.value), { + encoding: 'utf8', + mode: 0o600, + }); + try { + fsCompat.chmodSync(payloadFile, 0o600); + } catch { + // Best-effort on platforms that do not support chmod. + } + const safeParams = isCodexMessage + ? { ...params, message: undefined, message_file: payloadFile } + : { ...params, value: undefined, value_file: payloadFile }; + return await callback(safeParams); + } finally { + try { + fsCompat.rmSync(dir, { recursive: true, force: true }); + } catch { + // Best-effort cleanup; the file is 0600 and contains only the approved message. + } + } +} +function safeBridgeErrorMessage(command, message) { + if (command === 'codexSendVisibleMessage') { + return 'evaos-desktop-bridge guarded Codex visible message command failed'; + } + return message || 'evaos-desktop-bridge command failed'; +} +async function providerProfilesPayload() { + const brokerProfile = await providerAgentDiscoveryPayload('openclaw'); + if (brokerProfile) { + return brokerProfile.profilesPayload; + } + const profilesPayload = readJSONEnv('EVAOS_PROVIDER_PROFILES_JSON'); + const providerProfiles = Array.isArray(profilesPayload) + ? profilesPayload + : isRecord(profilesPayload) && Array.isArray(profilesPayload.provider_profiles) + ? profilesPayload.provider_profiles + : []; + const grantsPayload = readJSONEnv('EVAOS_PROVIDER_GRANTS_JSON'); + return redactConnectorSecrets({ + ok: true, + data: { + customer_id: process.env.EVAOS_CUSTOMER_ID || null, + provider_profiles: providerProfiles, + provider_grants: grantsPayload || null, + active_provider_key: + process.env.EVAOS_ACTIVE_PROVIDER_KEY || + (isRecord(profilesPayload) && typeof profilesPayload.active_provider_key === 'string' + ? profilesPayload.active_provider_key + : null), + raw_secrets_available: false, + raw_secrets_stored_in_workbench: false, + }, + warnings: providerProfiles.length === 0 ? ['Provider profiles are not configured on this VM yet.'] : [], + }); +} +async function providerCompleteAuthPayload(params) { + const endpoint = providerDiscoveryEndpoint(); + const customerID = process.env.EVAOS_CUSTOMER_ID?.trim(); + const proofSecret = + process.env.EVAOS_PROVIDER_AUTH_PROOF_SECRET?.trim() || process.env.EVAOS_PROVIDER_PROOF_SECRET?.trim(); + const identity = requiredProviderIdentity(params.identity); + const scopes = normalizeProviderScopes(params.scopes); + const expiresAt = normalizeProviderProofExpiry(params.expires_at); + const serverSecretRef = normalizeServerSecretRef(params.server_secret_ref, customerID); + if (!endpoint || !customerID || !proofSecret || !identity || !serverSecretRef) { + return { + ok: false, + errors: [ + { + code: 'provider_auth_proof_not_configured', + message: + 'Provider auth proof completion requires broker endpoint, customer id, proof secret, identity, and server secret reference.', + guidance: + 'Set EVAOS_PROVIDER_DISCOVERY_URL, EVAOS_CUSTOMER_ID, EVAOS_PROVIDER_AUTH_PROOF_SECRET, EVAOS_PROVIDER_AUTH_IDENTITY, and EVAOS_PROVIDER_SERVER_SECRET_REF on the VM.', + }, + ], + }; + } + const providerKey = 'openai_codex'; + const proofID = providerProofID(); + const proofPayload = { + customer_id: customerID, + provider_key: providerKey, + purpose: 'provider_auth_complete', + agent_runtime: 'openclaw', + proof_id: proofID, + identity, + scopes, + expires_at: expiresAt, + server_secret_ref: serverSecretRef, + }; + const signature = createHmac('sha256', proofSecret).update(JSON.stringify(proofPayload)).digest('hex'); + const requestBody = { + action: 'provider_auth_complete', + customer_id: customerID, + provider_key: providerKey, + agent_runtime: 'openclaw', + provider_auth_proof: { + purpose: 'provider_auth_complete', + agent_runtime: 'openclaw', + proof_id: proofID, + identity, + scopes, + expires_at: expiresAt, + server_secret_ref: serverSecretRef, + signature, + }, + }; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10_000); + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Evaos-Provider-Proof': 'signed-v1', + }, + body: JSON.stringify(requestBody), + signal: controller.signal, + }); + const payload = await response.json().catch(() => null); + if (!response.ok) { + return { + ok: false, + errors: [ + { + code: 'provider_auth_complete_failed', + message: + isRecord(payload) && typeof payload.error === 'string' + ? payload.error + : `Provider auth completion failed with HTTP ${response.status}.`, + }, + ], + }; + } + const cachedGrant = await cacheProviderGrantFromBroker('openclaw', providerKey, payload); + return redactConnectorSecrets({ + ok: true, + data: { + connected: isRecord(payload) ? payload.connected === true || payload.status === 'connected' : true, + provider_key: providerKey, + grant_cached: cachedGrant, + response: payload, + raw_provider_token_returned: false, + }, + }); + } catch (error) { + return { + ok: false, + errors: [ + { + code: 'provider_auth_complete_unreachable', + message: error instanceof Error ? error.message : 'Provider auth completion broker was unreachable.', + }, + ], + }; + } finally { + clearTimeout(timeout); + } +} +async function providerActiveProfilePayload() { + const brokerProfile = await providerAgentDiscoveryPayload('openclaw'); + if (brokerProfile) { + return brokerProfile.activePayload; + } + const profiles = await providerProfilesPayload(); + const data = isRecord(profiles.data) ? profiles.data : {}; + const providerProfiles = Array.isArray(data.provider_profiles) ? data.provider_profiles : []; + const activeProviderKey = typeof data.active_provider_key === 'string' ? data.active_provider_key : null; + const activeProfile = + providerProfiles.find((profile) => isRecord(profile) && profile.provider_key === activeProviderKey) ?? null; + const providerGrants = isRecord(data.provider_grants) ? data.provider_grants : {}; + const openClawGrant = isRecord(providerGrants.openclaw) ? providerGrants.openclaw : null; + const hasConnectionProof = + isRecord(activeProfile) && + activeProfile.status === 'connected' && + typeof activeProfile.last_validated_at === 'string' && + Boolean(openClawGrant && typeof openClawGrant.grant_handle === 'string'); + return redactConnectorSecrets({ + ok: true, + data: { + customer_id: process.env.EVAOS_CUSTOMER_ID || null, + active_provider_key: activeProviderKey, + active_profile: activeProfile, + needs_reauth: !hasConnectionProof, + raw_secrets_available: false, + }, + warnings: hasConnectionProof + ? [] + : [ + 'No verified active provider grant is available. Ask the customer to connect or re-auth the provider in evaOS Workbench.', + ], + }); +} +function requiredProviderIdentity(value) { + const fromParam = typeof value === 'string' ? value.trim() : ''; + if (fromParam) return fromParam; + const fromEnv = process.env.EVAOS_PROVIDER_AUTH_IDENTITY?.trim() || process.env.EVAOS_PROVIDER_IDENTITY?.trim() || ''; + return fromEnv || null; +} +function normalizeProviderScopes(value) { + const scopes = Array.isArray(value) ? value.map((scope) => String(scope).trim()).filter(Boolean) : []; + if (scopes.length > 0) return [...new Set(scopes)]; + const fromEnv = + process.env.EVAOS_PROVIDER_AUTH_SCOPES?.split(',') + .map((scope) => scope.trim()) + .filter(Boolean) ?? []; + return fromEnv.length > 0 ? [...new Set(fromEnv)] : ['codex', 'offline_access']; +} +function normalizeProviderProofExpiry(value) { + const candidate = typeof value === 'string' ? value.trim() : process.env.EVAOS_PROVIDER_AUTH_EXPIRES_AT?.trim() || ''; + if (candidate && Number.isFinite(Date.parse(candidate)) && Date.parse(candidate) > Date.now()) { + return new Date(candidate).toISOString(); + } + return new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); +} +function normalizeServerSecretRef(value, customerID) { + const candidate = + typeof value === 'string' ? value.trim() : process.env.EVAOS_PROVIDER_SERVER_SECRET_REF?.trim() || ''; + if (candidate.startsWith('provider://')) return candidate; + if (!customerID) return null; + return `provider://openai_codex/${encodeURIComponent(customerID)}/openclaw`; +} +function providerProofID() { + return `eap_${randomUUID().replace(/-/g, '')}`; +} +async function providerAgentDiscoveryPayload(agentRuntime) { + const endpoint = providerDiscoveryEndpoint(); + const customerID = process.env.EVAOS_CUSTOMER_ID?.trim(); + const grantHandle = providerGrantHandleFor(agentRuntime); + if (!endpoint || !customerID || !grantHandle) { + return null; + } + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10_000); + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-Evaos-Provider-Grant': grantHandle, + }, + body: JSON.stringify({ + action: 'provider_agent_discovery', + customer_id: customerID, + agent_runtime: agentRuntime, + }), + signal: controller.signal, + }); + const payload = await response.json().catch(() => null); + if (!response.ok || !isRecord(payload)) { + return null; + } + const activeProfile = isRecord(payload.provider_profile) ? payload.provider_profile : null; + const activeProviderKey = typeof payload.active_provider_key === 'string' ? payload.active_provider_key : null; + const providerProfiles = activeProfile ? [activeProfile] : []; + const grantStatus = typeof payload.grant_status === 'string' ? payload.grant_status : 'unknown'; + const grantExpiresAt = typeof payload.grant_expires_at === 'string' ? payload.grant_expires_at : null; + return { + profilesPayload: redactConnectorSecrets({ + ok: true, + data: { + customer_id: customerID, + provider_profiles: providerProfiles, + provider_grants: { + [agentRuntime]: { + grant_handle: grantHandle, + status: grantStatus, + expires_at: grantExpiresAt, + }, + }, + active_provider_key: activeProviderKey, + raw_secrets_available: false, + raw_secrets_stored_in_workbench: false, + raw_provider_token_returned: false, + source: 'broker', + }, + warnings: providerProfiles.length === 0 ? ['No active provider profile is available from the broker.'] : [], + }), + activePayload: redactConnectorSecrets({ + ok: true, + data: { + customer_id: customerID, + active_provider_key: activeProviderKey, + active_profile: activeProfile, + provider_identity: typeof payload.provider_identity === 'string' ? payload.provider_identity : null, + provider_scopes: Array.isArray(payload.provider_scopes) ? payload.provider_scopes.map(String) : [], + grant_status: grantStatus, + grant_expires_at: grantExpiresAt, + needs_reauth: payload.reauth_needed === true || !activeProfile, + raw_secrets_available: false, + raw_provider_token_returned: false, + source: 'broker', + }, + warnings: activeProfile + ? [] + : [ + 'No verified active provider grant is available. Ask the customer to connect or re-auth the provider in evaOS Workbench.', + ], + }), + }; + } catch { + return null; + } finally { + clearTimeout(timeout); + } +} +function providerDiscoveryEndpoint() { + const explicit = process.env.EVAOS_PROVIDER_DISCOVERY_URL?.trim(); + if (explicit) return explicit; + const broker = process.env.EVAOS_DESKTOP_RUNTIME_SESSION_URL?.trim(); + if (broker) return broker; + return null; +} +function desktopBridgeEnvPath() { + const explicit = process.env.EVAOS_DESKTOP_BRIDGE_ENV_FILE?.trim(); + if (explicit) return explicit; + const home = process.env.HOME?.trim(); + if (!home) return null; + return path.join(home, '.openclaw', 'evaos-desktop-bridge.env'); +} +function displayDesktopBridgeEnvPath(envPath) { + if (!envPath) return null; + const home = process.env.HOME?.trim(); + return home && envPath.startsWith(home) ? envPath.replace(home, '~') : envPath; +} +function parseShellEnvValue(value) { + const trimmed = value.trim(); + if (trimmed.startsWith("'") && trimmed.endsWith("'")) { + return trimmed.slice(1, -1).replace(/'"'"'/g, "'"); + } + if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + } + return trimmed; +} +function loadDesktopBridgeEnvFile() { + if (process.env.EVAOS_DESKTOP_BRIDGE_URL && process.env.EVAOS_DESKTOP_BRIDGE_TOKEN) return; + const envPath = desktopBridgeEnvPath(); + if (!envPath) return; + let text; + try { + text = fs.readFileSync(envPath, 'utf8'); + } catch { + return; + } + for (const line of text.split(/\r?\n/)) { + const match = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)=(.*)$/); + if (!match) continue; + const [, key, rawValue] = match; + if (!key.startsWith('EVAOS_')) continue; + if (process.env[key]) continue; + process.env[key] = parseShellEnvValue(rawValue); + } +} +function providerGrantHandleFor(agentRuntime) { + const direct = process.env.EVAOS_PROVIDER_GRANT_HANDLE?.trim(); + if (direct) return direct; + const grantsPayload = readJSONEnv('EVAOS_PROVIDER_GRANTS_JSON'); + const runtimeGrant = isRecord(grantsPayload) ? grantsPayload[agentRuntime] : null; + if ( + isRecord(runtimeGrant) && + typeof runtimeGrant.grant_handle === 'string' && + runtimeGrant.grant_handle.trim() !== '' + ) { + return runtimeGrant.grant_handle.trim(); + } + const cachePayload = readProviderGrantCache(); + const cachedRuntimeGrant = isRecord(cachePayload) ? cachePayload[agentRuntime] : null; + if ( + isRecord(cachedRuntimeGrant) && + typeof cachedRuntimeGrant.grant_handle === 'string' && + cachedRuntimeGrant.grant_handle.trim() !== '' + ) { + return cachedRuntimeGrant.grant_handle.trim(); + } + return null; +} +async function cacheProviderGrantFromBroker(agentRuntime, providerKey, payload) { + if (!isRecord(payload) || !isRecord(payload.agent_grant)) return false; + const grant = payload.agent_grant; + if ( + grant.provider_key !== providerKey || + grant.agent_runtime !== agentRuntime || + typeof grant.grant_handle !== 'string' || + !grant.grant_handle.trim() + ) { + return false; + } + const cachePath = providerGrantCachePath(); + if (!cachePath) return false; + const existing = isRecord(readProviderGrantCache()) ? readProviderGrantCache() : {}; + const next = { + ...existing, + [agentRuntime]: { + provider_key: providerKey, + agent_runtime: agentRuntime, + grant_handle: grant.grant_handle.trim(), + expires_at: typeof grant.expires_at === 'string' ? grant.expires_at : null, + cached_at: new Date().toISOString(), + }, + }; + try { + await mkdir(path.dirname(cachePath), { recursive: true }); + await writeFile(cachePath, JSON.stringify(next, null, 2), { encoding: 'utf8', mode: 0o600 }); + return true; + } catch { + return false; + } +} +function readProviderGrantCache() { + const cachePath = providerGrantCachePath(); + if (!cachePath) return null; + try { + return JSON.parse(fs.readFileSync(cachePath, 'utf8')); + } catch { + return null; + } +} +function providerGrantCachePath() { + const explicit = process.env.EVAOS_PROVIDER_GRANT_CACHE_FILE?.trim(); + if (explicit) return explicit; + const home = process.env.HOME?.trim(); + if (!home) return null; + return path.join(home, '.openclaw', 'evaos-provider-grants.json'); +} +function sharedBrowserGuidancePayload() { + const status = readJSONEnv('EVAOS_SHARED_BROWSER_STATUS_JSON'); + return redactConnectorSecrets({ + ok: true, + data: { + schema_version: 'evaos.browser_status.v1', + customer_id: process.env.EVAOS_CUSTOMER_ID || null, + business_browser_preferred_for_cloud_web_tasks: true, + shared_browser_preferred_for_cloud_web_tasks: true, + instructions: + "Use Business Browser for cloud web tasks that need a persistent VM browser, user auth/CAPTCHA handoff, or human-visible browsing state. Use local Mac browser tools only when the task explicitly needs the customer's Mac browser.", + status: status || null, + }, + warnings: status ? [] : ['Business Browser live status is not configured in this VM environment yet.'], + }); +} +function readJSONEnv(name) { + const raw = process.env[name]; + if (!raw) return null; + try { + return JSON.parse(raw); + } catch { + return null; + } +} +async function runRemoteBridge(remoteURL, command, params) { + const endpoint = new URL('/v1/commands', remoteURL); + const headers = { + 'Content-Type': 'application/json', + }; + const token = process.env.EVAOS_DESKTOP_BRIDGE_TOKEN?.trim(); + if (token) { + headers.Authorization = `Bearer ${token}`; + } + try { + return await postRemoteBridgeCommand(endpoint, headers, remoteURL, command, params, timeoutForCommand(command)); + } catch (error) { + if (command === 'customerMacControlStart' && isAbortLikeError(error)) { + const reconciled = await reconcileControlStartAfterAbort(endpoint, headers, remoteURL, params); + if (reconciled) { + return reconciled; + } + } + const err = error; + return { + ok: false, + errors: [ + { + code: 'bridge_connector_failed', + message: err.message || 'evaos-desktop-bridge connector request failed', + guidance: 'Verify Headscale reachability, EVAOS_DESKTOP_BRIDGE_URL, and EVAOS_DESKTOP_BRIDGE_TOKEN.', + }, + ], + }; + } +} +async function postRemoteBridgeCommand(endpoint, headers, remoteURL, command, params, timeoutMs) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(endpoint, { + method: 'POST', + headers, + body: JSON.stringify({ command, params }), + signal: controller.signal, + }); + const text = await response.text(); + try { + return await materializeVisualEvidence(command, JSON.parse(text), remoteURL, headers.Authorization); + } catch { + return { + ok: false, + errors: [ + { + code: 'bridge_connector_invalid_response', + message: text || `Connector returned HTTP ${response.status}`, + guidance: 'Check the paired Mac connector endpoint and token.', + }, + ], + }; + } + } finally { + clearTimeout(timeout); + } +} +async function reconcileControlStartAfterAbort(endpoint, headers, remoteURL, params) { + try { + const status = await postRemoteBridgeCommand(endpoint, headers, remoteURL, 'customerMacControlStatus', {}, 15_000); + if (!isControlSessionActive(status, params)) { + return null; + } + const payload = isRecord(status) ? { ...status } : { ok: true }; + const data = isRecord(payload.data) ? { ...payload.data } : {}; + payload.ok = true; + payload.data = { + ...data, + control_start_reconciled: true, + }; + payload.warnings = [ + ...(Array.isArray(payload.warnings) ? payload.warnings : []), + { + code: 'control_start_response_reconciled_after_abort', + message: + 'Mac control start timed out after the connector activated the control session; status was reconciled from the paired Mac.', + guidance: 'Continue with desktop_control_status and desktop_see before running live Mac-control actions.', + }, + ]; + return payload; + } catch { + return null; + } +} +function isAbortLikeError(error) { + const err = error; + return err.name === 'AbortError' || /abort/i.test(err.message || ''); +} +function isControlSessionActive(status, params) { + if (!isRecord(status) || status.ok !== true || !isRecord(status.data)) { + return false; + } + const data = status.data; + const session = isRecord(data.session) ? data.session : {}; + const active = data.active === true || session.active === true; + const killSwitch = + data.kill_switch === true || + data.killSwitch === true || + session.kill_switch === true || + session.killSwitch === true; + const mode = controlModeFromStatus(data.mode) ?? controlModeFromStatus(session.mode); + return active && !killSwitch && mode === requestedControlMode(params.mode); +} +function requestedControlMode(mode) { + return controlModeFromStatus(mode) ?? 'full_access'; +} +function controlModeFromStatus(mode) { + if (typeof mode !== 'string') { + return undefined; + } + const normalized = mode.trim().toLowerCase().replace(/-/g, '_'); + if (normalized === 'ask_permission') { + return 'ask_permission'; + } + if (normalized === 'full_access') { + return 'full_access'; + } + return undefined; +} +function timeoutForCommand(command) { + if (command === 'codexLiveStatus') { + return 35_000; + } + if (command === 'customerMacControlStart') { + return 30_000; + } + if ( + command === 'desktopSee' || + command === 'iphoneSee' || + command === 'customerMacSnapshot' || + command === 'customerMacAxTree' + ) { + return 60_000; + } + if ( + command === 'desktopDrag' || + command === 'desktopScroll' || + command === 'iphoneSwipe' || + command === 'customerMacIphoneMirroringScroll' || + command === 'customerMacIphoneMirroringSwipeLeft' || + command === 'customerMacIphoneMirroringSwipeRight' || + command === 'customerMacIphoneMirroringSwipeUp' || + command === 'customerMacIphoneMirroringSwipeDown' + ) { + return 20_000; + } + if ( + command === 'desktopMenu' || + command === 'desktopWindow' || + command === 'desktopBrowserAction' || + command === 'desktopFocusApp' || + command === 'customerMacIphoneMirroringOpenApp' + ) { + return 20_000; + } + if (command === 'desktopClick' || command === 'iphoneTap') { + return 30_000; + } + if ( + command === 'desktopSetValue' || + command === 'desktopType' || + command === 'desktopHotkey' || + command === 'iphoneType' || + command === 'customerMacIphoneMirroringTypeApprovedText' || + command === 'customerMacIphoneMirroringSendApprovedMessage' + ) { + return 15_000; + } + return 10_000; +} +async function materializeVisualEvidence(command, payload, remoteURL, authHeader) { + if (!isRecord(payload)) { + return payload; + } + const data = payload.data; + if (!isRecord(data)) { + return payload; + } + const image = findVisualImage(data); + if (!image) { + return payload; + } + let imageBytes; + let materializedFrom = 'inline'; + if (typeof image.bytes_base64 === 'string') { + imageBytes = Buffer.from(image.bytes_base64, 'base64'); + } else if (typeof image.artifact_url === 'string' && remoteURL) { + const fetched = await fetchVisualArtifact(remoteURL, image.artifact_url, authHeader); + if (!fetched.ok) { + const warnings = Array.isArray(payload.warnings) ? payload.warnings : []; + warnings.push(fetched.warning); + payload.warnings = warnings; + return payload; + } + imageBytes = fetched.bytes; + materializedFrom = 'connector_artifact'; + } else { + return payload; + } + const snapshotId = + (typeof data.snapshot_id === 'string' && data.snapshot_id) || + (isRecord(data.screenshot) && typeof data.screenshot.snapshot_id === 'string' && data.screenshot.snapshot_id) || + (typeof image.artifact_id === 'string' && image.artifact_id) || + (typeof payload.audit_id === 'string' && payload.audit_id) || + command; + const artifactDir = process.env.EVAOS_DESKTOP_BRIDGE_ARTIFACT_DIR || '/root/agent-files/downloads/desktop-bridge'; + const safeName = snapshotId.replace(/[^A-Za-z0-9_.-]+/g, '-').slice(0, 160); + const artifactPath = path.join(artifactDir, `${safeName}.png`); + try { + await mkdir(artifactDir, { recursive: true }); + await writeFile(artifactPath, imageBytes); + image.vm_artifact_path = artifactPath; + image.bytes_base64_present = true; + image.vm_artifact_source = materializedFrom; + delete image.bytes_base64; + data.vm_visual_artifact_path = artifactPath; + } catch (error) { + const warnings = Array.isArray(payload.warnings) ? payload.warnings : []; + warnings.push(`Unable to write VM visual artifact: ${error.message || 'unknown error'}`); + payload.warnings = warnings; + } + return payload; +} +function findVisualImage(data) { + const direct = data.image; + if (isRecord(direct) && hasImageMaterial(direct)) { + return direct; + } + const screenshot = data.screenshot; + if (isRecord(screenshot)) { + const screenshotRecord = screenshot; + if (hasImageMaterial(screenshotRecord)) { + return screenshotRecord; + } + const screenshotImage = screenshotRecord.screenshot; + if (isRecord(screenshotImage) && hasImageMaterial(screenshotImage)) { + return screenshotImage; + } + const image = screenshotRecord.image; + if (isRecord(image) && hasImageMaterial(image)) { + return image; + } + } + return undefined; +} +function hasImageMaterial(value) { + return typeof value.bytes_base64 === 'string' || typeof value.artifact_url === 'string'; +} +async function fetchVisualArtifact(remoteURL, artifactURL, authHeader) { + let endpoint; + try { + endpoint = new URL(artifactURL, remoteURL); + } catch { + return { ok: false, warning: 'Unable to fetch VM visual artifact: connector returned an invalid artifact URL' }; + } + const base = new URL(remoteURL); + if (endpoint.origin !== base.origin || !endpoint.pathname.startsWith('/v1/artifacts/')) { + return { + ok: false, + warning: 'Unable to fetch VM visual artifact: connector artifact URL was outside the paired connector', + }; + } + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 15_000); + try { + const headers = {}; + if (authHeader) { + headers.Authorization = authHeader; + } + const response = await fetch(endpoint, { + method: 'GET', + headers, + signal: controller.signal, + }); + if (!response.ok) { + return { ok: false, warning: `Unable to fetch VM visual artifact: connector returned HTTP ${response.status}` }; + } + return { ok: true, bytes: Buffer.from(await response.arrayBuffer()) }; + } catch (error) { + return { + ok: false, + warning: `Unable to fetch VM visual artifact: ${error.message || 'unknown error'}`, + }; + } finally { + clearTimeout(timeout); + } +} +function isRecord(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} +function redactConnectorSecrets(value) { + if (Array.isArray(value)) { + return value.map((item) => redactConnectorSecrets(item)); + } + if (!value || typeof value !== 'object') { + return value; + } + const redacted = {}; + for (const [key, nestedValue] of Object.entries(value)) { + if (isSecretMetadataKey(key)) { + redacted[key] = '[redacted]'; + } else { + redacted[key] = redactConnectorSecrets(nestedValue); + } + } + return redacted; +} +function isSecretMetadataKey(key) { + const compactKey = key.toLowerCase().replace(/[^a-z0-9]/g, ''); + return ( + compactKey === 'token' || + compactKey.endsWith('token') || + compactKey === 'authorization' || + compactKey.endsWith('authorization') || + compactKey === 'header' || + compactKey === 'headers' || + compactKey.endsWith('header') || + compactKey.endsWith('headers') || + compactKey === 'password' || + compactKey.endsWith('password') || + compactKey === 'credential' || + compactKey === 'credentials' || + compactKey.endsWith('credential') || + compactKey.endsWith('credentials') || + compactKey === 'secret' || + compactKey.endsWith('secret') || + compactKey.includes('apikey') || + compactKey.includes('clientsecret') || + compactKey.includes('privatekey') || + compactKey.includes('secretkey') || + compactKey.includes('accesskey') + ); +} +function clampInt(value, fallback, min, max) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return fallback; + } + return Math.min(max, Math.max(min, Math.trunc(value))); +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/firewall.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/firewall.js new file mode 100644 index 0000000000..cd946c85c5 --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/firewall.js @@ -0,0 +1,172 @@ +const SAFE_TOOL_PREFIXES = ['desktop_bridge_', 'customer_mac_', 'desktop_', 'iphone_', 'evaos_']; +const FULL_ACCESS_TOOL_PREFIXES = ['desktop_', 'iphone_', 'customer_mac_iphone_mirroring_']; +const APPROVAL_GATED_TOOL_PREFIXES = [ + 'desktop_bridge_codex_select_thread', + 'desktop_bridge_codex_continue_thread', + 'desktop_bridge_codex_send_visible_message', + 'customer_mac_app_focus', + 'customer_mac_local_site_', +]; +const IPHONE_GESTURE_TOOL_NAMES = new Set([ + 'customer_mac_iphone_mirroring_swipe_left', + 'customer_mac_iphone_mirroring_swipe_right', + 'customer_mac_iphone_mirroring_swipe_up', + 'customer_mac_iphone_mirroring_swipe_down', +]); +const IPHONE_GESTURE_ALLOWED_MATCHES = new Set(['swipe']); +const FULL_ACCESS_ALLOWED_MATCHES = new Set([ + 'generic coordinates', + 'coordinate', + 'mouseDown', + 'mouseUp', + 'drag', + 'swipe', + 'typewrite', + 'send_message', + 'submit_prompt', + 'messages', + 'call', + 'purchase', + 'camera', + 'microphone', +]); +const DANGEROUS_TOOL_NAMES = [ + 'exec', + 'shell', + 'bash', + 'terminal', + 'computer', + 'computer_use', + 'run_command', + 'write', + 'edit', +]; +const FORBIDDEN_ARGUMENT_PATTERNS = [ + 'osascript', + 'screencapture', + 'cliclick', + 'pyautogui', + 'pynput', + 'AXUIElement', + 'AXUIElementPerformAction', + 'AXUIElementSetAttributeValue', + 'AXPress', + 'AXSetValue', + 'AXShowMenu', + 'AXSelectedText', + 'System Events', + 'Codex.app', + 'open -a Codex', + 'codex app-server', + 'app-server', + 'internal mutation rpc', + 'turn/start', + 'turn/steer', + 'turn/interrupt', + 'thread/inject_items', + 'thread/start', + 'thread/resume', + 'thread/fork', + 'thread/rollback', + 'thread/compact/start', + 'command/exec', + 'fs/writeFile', + 'fs/remove', + 'config/value/write', + 'config/batchWrite', + 'plugin/install', + 'plugin/uninstall', + 'remoteControl/enable', + 'remoteControl/disable', + 'remoteControl/approve', + 'remoteControl/deny', + 'session.db', + 'state.db', + 'sqlite', + '.codex', + 'auth.json', + 'token', + 'Authorization', + 'Bearer ', + 'send_message', + 'submit_prompt', + 'typewrite', + 'generic coordinates', + 'coordinate', + 'mouseDown', + 'mouseUp', + 'drag', + 'swipe', + 'Screen Sharing enable', + 'Remote Management enable', + 'kickstart -activate', + 'messages', + 'call', + 'purchase', + 'camera', + 'microphone', +]; +export function desktopBridgeFirewall(event) { + const toolName = String(event.toolName || event.name || ''); + const haystack = JSON.stringify({ + toolName, + args: firewallPayload(toolName, event.args), + input: firewallPayload(toolName, event.input), + params: firewallPayload(toolName, event.params), + parameters: firewallPayload(toolName, event.parameters), + }).toLowerCase(); + const matchedPattern = FORBIDDEN_ARGUMENT_PATTERNS.find((pattern) => haystack.includes(pattern.toLowerCase())); + if (SAFE_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix))) { + const allowedFullAccessMatch = + matchedPattern !== undefined && + FULL_ACCESS_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix)) && + FULL_ACCESS_ALLOWED_MATCHES.has(matchedPattern); + const allowedSupportCanaryMatch = + matchedPattern !== undefined && + IPHONE_GESTURE_TOOL_NAMES.has(toolName) && + IPHONE_GESTURE_ALLOWED_MATCHES.has(matchedPattern); + if (matchedPattern && !allowedSupportCanaryMatch && !allowedFullAccessMatch) { + return { + block: true, + blockReason: `desktop-bridge firewall blocked ${toolName}: ${matchedPattern} must go through the connector's audited control contract.`, + }; + } + if (APPROVAL_GATED_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix))) { + const params = event.args || event.input || event.params || event.parameters || {}; + if (params.dry_run !== false) { + return undefined; + } + return { + requireApproval: { + title: 'Approve customer Mac action', + description: `${toolName} is a live customer Mac named action. Approval is required and the bridge will audit the command.`, + severity: 'warning', + timeoutBehavior: 'deny', + allowedDecisions: ['allow-once', 'deny'], + }, + }; + } + return undefined; + } + const suspiciousTool = DANGEROUS_TOOL_NAMES.some((name) => toolName.toLowerCase().includes(name)); + if (suspiciousTool && matchedPattern) { + return { + block: true, + blockReason: `desktop-bridge firewall blocked ${toolName}: ${matchedPattern} is outside the read-only passive observer boundary.`, + }; + } + return undefined; +} +function firewallPayload(toolName, value) { + if (toolName !== 'desktop_bridge_codex_send_visible_message' || !isRecord(value)) { + return value; + } + const clone = { ...value }; + if (typeof clone.message === 'string') { + clone.message = ''; + } + return clone; +} +function isRecord(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js new file mode 100644 index 0000000000..65f5b1195f --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js @@ -0,0 +1,442 @@ +import { createPublicKey, verify } from 'node:crypto'; +export const MAC_CONTROL_RUNTIME_RECEIPT_PATH = '/api/v1/evaos/mac-control/runtime-receipt'; +const CONTEXT_SCHEMA = 'evaos.mac_control_execution_context.v1'; +const CONTRACT_SCHEMA = 'evaos.mac_control_runtime_contract.v2'; +const CONNECTOR_REQUEST_SCHEMA = 'evaos.mac_control.canary_request.v1'; +const CONNECTOR_RESPONSE_SCHEMA = 'evaos.mac_control.runtime_receipt_envelope.v1'; +const RECEIPT_NAMESPACE = 'evaos-mac-control-receipt-v1'; +const CONTEXT_TTL_SECONDS = 60; +const CLOCK_SKEW_SECONDS = 5; +const MAX_REQUEST_BYTES = 4096; +const MAX_CONNECTOR_RESPONSE_BYTES = 65536; +const CONTEXT_FIELDS = [ + 'schema_version', + 'key_id', + 'runtime', + 'customer_id', + 'customer_vm_id', + 'binding_id', + 'binding_version', + 'issued_at', + 'expires_at', + 'context_id', +]; +const RESPONSE_FIELDS = ['schema', 'receiptBase64', 'signature', 'keyId', 'namespace']; +const HEADER = { + context: 'x-evaos-mac-control-execution-context', + contextSignature: 'x-evaos-mac-control-execution-context-signature', + contextKeyId: 'x-evaos-mac-control-execution-context-key-id', + contract: 'x-evaos-mac-control-contract', + customer: 'x-evaos-mac-control-customer', + grantState: 'x-evaos-mac-control-grant-state', + connectorUrl: 'x-evaos-desktop-bridge-url', + connectorToken: 'x-evaos-desktop-bridge-token', + connectorTokenLast4: 'x-evaos-desktop-bridge-token-last4', + bindingId: 'x-evaos-mac-control-binding-id', + bindingVersion: 'x-evaos-mac-control-binding-version', + bindingExpiresAt: 'x-evaos-mac-control-binding-expires-at', +}; +/** Register the one exact, gateway-authenticated Mac-control receipt route. */ +export function registerMacControlRuntimeReceiptRoute(api) { + api.registerHttpRoute({ + path: MAC_CONTROL_RUNTIME_RECEIPT_PATH, + auth: 'gateway', + match: 'exact', + handler: createMacControlRuntimeReceiptHandler(), + }); +} +/** Build the receipt route handler; dependency overrides exist only for focused tests. */ +export function createMacControlRuntimeReceiptHandler(options = {}) { + const env = options.env ?? process.env; + const fetchImpl = options.fetchImpl ?? globalThis.fetch; + const now = options.now ?? Date.now; + const replayCache = options.replayCache ?? new Map(); + return async (request, response) => { + if ((request.method ?? 'GET').toUpperCase() !== 'POST') { + response.setHeader('Allow', 'POST'); + sendError(response, 405, 'method_not_allowed'); + return true; + } + const publicRequest = await readPublicRequest(request); + if (!publicRequest.ok) { + sendError(response, publicRequest.status, 'invalid_request'); + return true; + } + const authority = verifyAuthority(request.headers, env, now()); + if (!authority.ok) { + sendError(response, authority.status, authority.code); + return true; + } + pruneReplayCache(replayCache, now()); + if (replayCache.has(authority.value.context.context_id)) { + sendError(response, 409, 'execution_context_replayed'); + return true; + } + replayCache.set(authority.value.context.context_id, authority.value.context.expires_at * 1000); + const connectorBody = { + schema: CONNECTOR_REQUEST_SCHEMA, + challenge: publicRequest.value.challenge, + runRef: publicRequest.value.runRef, + executionContext: { + payload: authority.value.contextPayload, + signature: authority.value.contextSignature, + keyId: authority.value.contextKeyId, + }, + binding: { + bindingId: authority.value.bindingId, + bindingVersion: authority.value.bindingVersion, + bindingExpiresAt: authority.value.bindingExpiresAt, + }, + }; + let connectorResponse; + try { + connectorResponse = await fetchImpl(`${authority.value.connectorUrl}/v1/canary/mac-control`, { + method: 'POST', + headers: { + Authorization: `Bearer ${authority.value.connectorToken}`, + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify(connectorBody), + }); + } catch { + sendError(response, 502, 'connector_unavailable'); + return true; + } + if (!connectorResponse.ok) { + sendError( + response, + connectorResponse.status >= 400 && connectorResponse.status < 500 ? 409 : 502, + 'connector_rejected_canary' + ); + return true; + } + let rawConnectorResponse; + try { + rawConnectorResponse = await connectorResponse.text(); + } catch { + sendError(response, 502, 'connector_response_invalid'); + return true; + } + const sanitized = validateConnectorEnvelope(rawConnectorResponse, authority.value); + if (!sanitized.ok) { + sendError(response, 502, 'connector_response_invalid'); + return true; + } + sendJson(response, 200, sanitized.value); + return true; + }; +} +async function readPublicRequest(request) { + const chunks = []; + let total = 0; + try { + for await (const chunk of request) { + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(typeof chunk === 'string' ? chunk : chunk); + total += buffer.byteLength; + if (total > MAX_REQUEST_BYTES) { + return { ok: false, status: 413 }; + } + chunks.push(buffer); + } + } catch { + return { ok: false, status: 400 }; + } + let body; + try { + body = JSON.parse(Buffer.concat(chunks).toString('utf8')); + } catch { + return { ok: false, status: 400 }; + } + if (!isRecord(body) || !hasExactKeys(body, ['challenge', 'runRef'])) { + return { ok: false, status: 400 }; + } + if ( + typeof body.challenge !== 'string' || + !/^[A-Za-z0-9_-]{32,128}$/.test(body.challenge) || + decodeBase64Url(body.challenge)?.byteLength === undefined || + typeof body.runRef !== 'string' || + !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(body.runRef) + ) { + return { ok: false, status: 400 }; + } + const challengeBytes = decodeBase64Url(body.challenge); + if (!challengeBytes || challengeBytes.byteLength < 24 || challengeBytes.byteLength > 96) { + return { ok: false, status: 400 }; + } + return { ok: true, value: { challenge: body.challenge, runRef: body.runRef } }; +} +function verifyAuthority(headers, env, nowMs) { + const expectedKeyId = env.EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_KEY_ID; + const publicKeyBase64 = env.EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_PUBLIC_KEY_B64; + if (!expectedKeyId || !publicKeyBase64 || !validKeyId(expectedKeyId)) { + return { ok: false, status: 503, code: 'execution_context_verifier_unavailable' }; + } + const rawPublicKey = decodeCanonicalBase64(publicKeyBase64); + if (!rawPublicKey || rawPublicKey.byteLength !== 32) { + return { ok: false, status: 503, code: 'execution_context_verifier_unavailable' }; + } + const contextPayload = readHeader(headers, HEADER.context); + const contextSignature = readHeader(headers, HEADER.contextSignature); + const contextKeyId = readHeader(headers, HEADER.contextKeyId); + if (!contextPayload || !contextSignature || !contextKeyId) { + return { ok: false, status: 401, code: 'execution_context_missing' }; + } + if (contextKeyId !== expectedKeyId) { + return { ok: false, status: 401, code: 'execution_context_key_unknown' }; + } + const payloadBytes = decodeBase64Url(contextPayload); + const signatureBytes = decodeBase64Url(contextSignature); + if (!payloadBytes || !signatureBytes || signatureBytes.byteLength !== 64) { + return { ok: false, status: 401, code: 'execution_context_malformed' }; + } + try { + const spkiPrefix = Buffer.from('302a300506032b6570032100', 'hex'); + const publicKey = createPublicKey({ + key: Buffer.concat([spkiPrefix, rawPublicKey]), + format: 'der', + type: 'spki', + }); + if (!verify(null, payloadBytes, publicKey, signatureBytes)) { + return { ok: false, status: 401, code: 'execution_context_signature_invalid' }; + } + } catch { + return { ok: false, status: 401, code: 'execution_context_signature_invalid' }; + } + let parsed; + try { + parsed = JSON.parse(payloadBytes.toString('utf8')); + } catch { + return { ok: false, status: 401, code: 'execution_context_malformed' }; + } + if (!isRecord(parsed) || !hasExactKeys(parsed, CONTEXT_FIELDS)) { + return { ok: false, status: 401, code: 'execution_context_malformed' }; + } + const context = parsed; + if ( + context.schema_version !== CONTEXT_SCHEMA || + context.key_id !== expectedKeyId || + context.key_id !== contextKeyId || + context.runtime !== 'openclaw' || + !validOpaqueId(context.customer_id, 160) || + !validOpaqueId(context.customer_vm_id, 160) || + !validOpaqueId(context.binding_id, 160) || + !validBindingVersion(context.binding_version) || + !Number.isInteger(context.issued_at) || + !Number.isInteger(context.expires_at) || + !validContextId(context.context_id) + ) { + return { ok: false, status: 401, code: 'execution_context_malformed' }; + } + const nowSeconds = Math.floor(nowMs / 1000); + if ( + context.issued_at > nowSeconds + CLOCK_SKEW_SECONDS || + context.issued_at < nowSeconds - CONTEXT_TTL_SECONDS || + context.expires_at <= nowSeconds || + context.expires_at <= context.issued_at || + context.expires_at > context.issued_at + CONTEXT_TTL_SECONDS + ) { + return { ok: false, status: 401, code: 'execution_context_expired' }; + } + const contract = readHeader(headers, HEADER.contract); + const customer = readHeader(headers, HEADER.customer); + const grantState = readHeader(headers, HEADER.grantState); + const connectorUrlHeader = readHeader(headers, HEADER.connectorUrl); + const connectorToken = readHeader(headers, HEADER.connectorToken); + const connectorTokenLast4 = readHeader(headers, HEADER.connectorTokenLast4); + const bindingId = readHeader(headers, HEADER.bindingId); + const bindingVersion = readHeader(headers, HEADER.bindingVersion); + const bindingExpiresAt = readHeader(headers, HEADER.bindingExpiresAt); + const connectorUrl = connectorUrlHeader ? validConnectorUrl(connectorUrlHeader) : undefined; + const bindingExpiryMs = bindingExpiresAt ? Date.parse(bindingExpiresAt) : Number.NaN; + if ( + contract !== CONTRACT_SCHEMA || + grantState !== 'active' || + customer !== context.customer_id || + bindingId !== context.binding_id || + bindingVersion !== context.binding_version || + !bindingExpiresAt || + !Number.isFinite(bindingExpiryMs) || + context.expires_at * 1000 > bindingExpiryMs || + !connectorUrl || + !connectorToken || + connectorToken.length < 24 || + connectorToken.length > 512 || + connectorToken.trim() !== connectorToken || + !connectorTokenLast4 || + connectorTokenLast4 !== connectorToken.slice(-4) + ) { + return { ok: false, status: 401, code: 'execution_context_scope_mismatch' }; + } + return { + ok: true, + value: { + context, + contextPayload, + contextSignature, + contextKeyId, + connectorUrl, + connectorToken, + bindingId, + bindingVersion, + bindingExpiresAt, + }, + }; +} +function validateConnectorEnvelope(raw, authority) { + if (Buffer.from(raw).byteLength > MAX_CONNECTOR_RESPONSE_BYTES) { + return { ok: false }; + } + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { ok: false }; + } + if (!isRecord(parsed) || !hasExactKeys(parsed, RESPONSE_FIELDS)) { + return { ok: false }; + } + if ( + parsed.schema !== CONNECTOR_RESPONSE_SCHEMA || + parsed.namespace !== RECEIPT_NAMESPACE || + typeof parsed.receiptBase64 !== 'string' || + parsed.receiptBase64.length < 32 || + parsed.receiptBase64.length > 32768 || + typeof parsed.signature !== 'string' || + parsed.signature.length > 8192 || + typeof parsed.keyId !== 'string' || + !validKeyId(parsed.keyId) + ) { + return { ok: false }; + } + const receiptBytes = decodeBase64Url(parsed.receiptBase64); + if (!receiptBytes || receiptBytes.byteLength > 24576 || !validSshSignature(parsed.signature)) { + return { ok: false }; + } + const receiptText = receiptBytes.toString('utf8'); + const forbiddenValues = [ + authority.connectorToken, + authority.connectorUrl, + authority.context.customer_id, + authority.context.customer_vm_id, + authority.context.binding_id, + authority.contextPayload, + authority.contextSignature, + ]; + if ( + forbiddenValues.some((value) => value.length > 0 && receiptText.includes(value)) || + /https?:\/\//i.test(receiptText) || + /\b100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])(?:\.\d{1,3}){2}\b/.test(receiptText) || + /(?:\/tmp\/|\/private\/var\/folders\/)/i.test(receiptText) || + /"(?:token|secret|authorization|connector_url|customer_id|customer_vm_id|binding_id)"\s*:/i.test(receiptText) + ) { + return { ok: false }; + } + return { ok: true, value: parsed }; +} +function validConnectorUrl(value) { + let parsed; + try { + parsed = new URL(value); + } catch { + return undefined; + } + if ( + parsed.protocol !== 'http:' || + parsed.port !== '8765' || + parsed.username || + parsed.password || + parsed.pathname !== '/' || + parsed.search || + parsed.hash || + !isTailnetIpv4(parsed.hostname) + ) { + return undefined; + } + return parsed.origin; +} +function isTailnetIpv4(value) { + const parts = value.split('.'); + if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/.test(part) || Number(part) > 255)) { + return false; + } + return Number(parts[0]) === 100 && Number(parts[1]) >= 64 && Number(parts[1]) <= 127; +} +function readHeader(headers, name) { + const value = headers[name] ?? headers[canonicalHeaderName(name)]; + if (Array.isArray(value)) { + return value.length === 1 ? strictString(value[0]) : undefined; + } + return strictString(value); +} +function canonicalHeaderName(name) { + return name + .split('-') + .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`) + .join('-'); +} +function strictString(value) { + return typeof value === 'string' && value.length > 0 && value.trim() === value && !value.includes(',') + ? value + : undefined; +} +function validOpaqueId(value, maxLength) { + return typeof value === 'string' && new RegExp(`^[A-Za-z0-9][A-Za-z0-9._:@-]{0,${maxLength - 1}}$`).test(value); +} +function validBindingVersion(value) { + return typeof value === 'string' && /^[1-9][0-9]{0,18}$/.test(value); +} +function validKeyId(value) { + return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value); +} +function validContextId(value) { + return typeof value === 'string' && decodeBase64Url(value)?.byteLength === 16; +} +function decodeBase64Url(value) { + if (!/^[A-Za-z0-9_-]+$/.test(value)) { + return undefined; + } + try { + const decoded = Buffer.from(value, 'base64url'); + return decoded.toString('base64url') === value ? decoded : undefined; + } catch { + return undefined; + } +} +function decodeCanonicalBase64(value) { + if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) { + return undefined; + } + try { + const decoded = Buffer.from(value, 'base64'); + return decoded.toString('base64') === value ? decoded : undefined; + } catch { + return undefined; + } +} +function validSshSignature(value) { + return /^-----BEGIN SSH SIGNATURE-----\n(?:[A-Za-z0-9+/=]{1,76}\n)+-----END SSH SIGNATURE-----\n?$/.test(value); +} +function hasExactKeys(value, keys) { + const actual = Object.keys(value); + return actual.length === keys.length && actual.every((key, index) => key === keys[index]); +} +function isRecord(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} +function pruneReplayCache(cache, nowMs) { + for (const [contextId, expiresAt] of cache) { + if (expiresAt <= nowMs) { + cache.delete(contextId); + } + } +} +function sendJson(response, status, body) { + response.statusCode = status; + response.setHeader('Cache-Control', 'no-store'); + response.setHeader('Content-Type', 'application/json; charset=utf-8'); + response.end(JSON.stringify(body)); +} +function sendError(response, status, code) { + sendJson(response, status, { ok: false, error: { code } }); +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts new file mode 100644 index 0000000000..e115b9ed99 --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts @@ -0,0 +1,833 @@ +import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry'; + +import { type BridgeCommandKey, type BridgeParams, runBridge } from './src/bridge.js'; +import { desktopBridgeFirewall } from './src/firewall.js'; +import { registerMacControlRuntimeReceiptRoute } from './src/runtimeReceipt.js'; + +type ToolDefinition = { + name: string; + description: string; + parameters: Record; + execute: (toolCallId: string, params: BridgeParams) => Promise; +}; + +type ToolTextContent = { + type: 'text'; + text: string; +}; + +type ToolResult = { + content: ToolTextContent[]; + details: unknown; +}; + +const approvalAuditIdProperty = { + type: 'string', + minLength: 1, + maxLength: 160, + description: 'Required when dry_run is false; use the audit id from the approved dry-run/evidence step.', +}; + +export default definePluginEntry({ + id: 'evaos-desktop-bridge', + name: 'EvaOS Desktop Bridge', + description: 'Guarded bridge from OpenClaw to Codex Desktop and paired customer Mac state.', + kind: 'tool', + register(api: any) { + registerMacControlRuntimeReceiptRoute(api); + + for (const bridgeTool of readOnlyTools()) { + api.registerTool(bridgeTool); + } + + api.registerHook?.('before_tool_call', desktopBridgeFirewall, { + name: 'evaos-desktop-bridge-firewall', + }); + }, +}); + +function readOnlyTools(): ToolDefinition[] { + return [ + tool('desktop_bridge_status', 'Read Codex Desktop installation, running, permission, and safety status.', 'status'), + tool( + 'desktop_bridge_capabilities', + 'Read the bridge command capability surface and hard safety boundaries.', + 'capabilities' + ), + tool('desktop_bridge_latest', 'Read the last redacted bridge observation envelope from local state.', 'latest'), + tool('desktop_bridge_audit_tail', 'Read a redacted tail of the append-only local bridge audit log.', 'auditTail', { + type: 'object', + additionalProperties: false, + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 }, + }, + }), + tool( + 'desktop_bridge_codex_frontmost', + 'Read whether Codex Desktop is the current frontmost app.', + 'codexFrontmost' + ), + tool( + 'desktop_bridge_codex_windows', + 'Read visible Codex Desktop window metadata through Accessibility.', + 'codexWindows' + ), + tool('desktop_bridge_queue_list', 'Read capped Eva/OpenClaw announcement queue events.', 'queueList', { + type: 'object', + additionalProperties: false, + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 }, + }, + }), + tool( + 'desktop_bridge_queue_append', + 'Append a local Eva/OpenClaw announcement queue event with source audit provenance.', + 'queueAppend', + { + type: 'object', + additionalProperties: false, + required: ['kind', 'source_audit_id'], + properties: { + kind: { type: 'string', enum: ['idle', 'approval_needed', 'done', 'error', 'attention'] }, + source_audit_id: { type: 'string' }, + message: { type: 'string' }, + }, + } + ), + tool( + 'desktop_bridge_codex_threads', + 'Read visible Codex Desktop thread candidates from GUI state.', + 'codexThreads', + { + type: 'object', + additionalProperties: false, + properties: { + max_items: { type: 'integer', minimum: 1, maximum: 200, default: 50 }, + }, + } + ), + tool( + 'desktop_bridge_codex_thread_map', + 'Read a joined map of visible Codex Desktop thread candidates and saved app-server thread summaries.', + 'codexThreadMap', + { + type: 'object', + additionalProperties: false, + properties: { + max_items: { type: 'integer', minimum: 1, maximum: 200, default: 50 }, + }, + } + ), + tool( + 'desktop_bridge_codex_select_thread', + 'Guarded visible action: select an already-visible Codex thread by visible_id. Dry-run defaults on.', + 'codexSelectThread', + { + type: 'object', + additionalProperties: false, + required: ['thread_id'], + properties: { + thread_id: { type: 'string' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'desktop_bridge_codex_send_visible_message', + 'Live visible Codex GUI action: send an approved message through the frontmost Codex Desktop composer. Dry-run defaults on and live mode requires approval.', + 'codexSendVisibleMessage', + { + type: 'object', + additionalProperties: false, + required: ['thread_id', 'message'], + properties: { + thread_id: { type: 'string' }, + message: { type: 'string', minLength: 1, maxLength: 4000 }, + dry_run: { type: 'boolean', default: true }, + confirm: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + wait_ms: { type: 'integer', minimum: 0, maximum: 120000, default: 0 }, + poll_interval_ms: { type: 'integer', minimum: 250, maximum: 10000, default: 2000 }, + }, + } + ), + tool( + 'desktop_bridge_codex_continue_thread', + "Support-only fallback: select a visible Codex thread by title and submit the exact prompt 'continue'. Dry-run defaults on.", + 'codexContinueThread', + { + type: 'object', + additionalProperties: false, + required: ['title'], + properties: { + title: { type: 'string', minLength: 1, maxLength: 160 }, + prompt: { type: 'string', enum: ['continue'], default: 'continue' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'desktop_bridge_codex_snapshot', + 'Read a capped visible Codex Desktop snapshot; screenshots are skipped unless Codex is frontmost.', + 'codexSnapshot', + { + type: 'object', + additionalProperties: false, + properties: { + max_chars: { type: 'integer', minimum: 1, maximum: 20000, default: 4000 }, + }, + } + ), + tool( + 'desktop_bridge_codex_inspect', + 'Read a compact Codex Desktop page map with visible windows, controls, and text summaries.', + 'codexInspect', + { + type: 'object', + additionalProperties: false, + properties: { + max_nodes: { type: 'integer', minimum: 1, maximum: 1000, default: 120 }, + }, + } + ), + tool( + 'desktop_bridge_codex_ax_tree', + 'Read a capped Codex Desktop Accessibility tree summary with roles and names only.', + 'codexAxTree', + { + type: 'object', + additionalProperties: false, + properties: { + max_nodes: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }, + }, + } + ), + tool( + 'desktop_bridge_codex_app_server_status', + 'Read Codex app-server availability and read-only method allowlist.', + 'codexAppServerStatus' + ), + tool( + 'desktop_bridge_codex_connections_status', + 'Read Codex Desktop app-server, daemon, websocket, remote-control, and notification readiness.', + 'codexConnectionsStatus' + ), + tool( + 'desktop_bridge_codex_app_server_remote_control_status', + 'Read Codex native remote-control readiness without enabling or mutating it.', + 'codexAppServerRemoteControlStatus' + ), + tool( + 'desktop_bridge_codex_app_server_threads', + 'Read capped Codex thread summaries through the app-server read allowlist.', + 'codexAppServerThreads', + { + type: 'object', + additionalProperties: false, + properties: { + max_items: { type: 'integer', minimum: 1, maximum: 200, default: 50 }, + }, + } + ), + tool( + 'desktop_bridge_codex_app_server_loaded_threads', + 'Read loaded Codex app-server thread ids for readiness diagnostics.', + 'codexAppServerLoadedThreads', + { + type: 'object', + additionalProperties: false, + properties: { + max_items: { type: 'integer', minimum: 1, maximum: 200, default: 50 }, + }, + } + ), + tool( + 'desktop_bridge_codex_live_status', + 'Read buffered Codex app-server live notifications for one loaded thread.', + 'codexLiveStatus', + { + type: 'object', + additionalProperties: false, + required: ['thread_id'], + properties: { + thread_id: { type: 'string', minLength: 1, maxLength: 240 }, + duration_ms: { type: 'integer', minimum: 1, maximum: 30000, default: 1000 }, + }, + } + ), + tool( + 'evaos_provider_profiles', + 'Read active evaOS provider profile metadata and brokered grant readiness without raw provider secrets.', + 'evaosProviderProfiles' + ), + tool( + 'evaos_provider_active_profile', + 'Read the active provider profile and whether OpenClaw/Hermes should re-auth.', + 'evaosProviderActiveProfile' + ), + tool( + 'evaos_provider_complete_auth', + 'Complete OpenAI / Codex provider auth by sending signed metadata proof to the evaOS broker. Does not expose raw provider credentials.', + 'evaosProviderCompleteAuth', + { + type: 'object', + additionalProperties: false, + properties: { + identity: { type: 'string', minLength: 3, maxLength: 240 }, + scopes: { + type: 'array', + maxItems: 12, + items: { type: 'string', minLength: 1, maxLength: 80 }, + }, + expires_at: { type: 'string', minLength: 10, maxLength: 80 }, + server_secret_ref: { type: 'string', minLength: 16, maxLength: 240 }, + }, + } + ), + tool( + 'evaos_shared_browser_guidance', + 'Read Business Browser status/guidance so cloud agents default to the brokered VM browser for web tasks.', + 'evaosSharedBrowserGuidance' + ), + tool( + 'customer_mac_status', + 'Read paired customer Mac connector, iPhone Mirroring, and Screen Sharing readiness.', + 'customerMacStatus' + ), + tool( + 'desktop_control_status', + 'Read the customer-granted Full Access / Ask Permission control session state.', + 'customerMacControlStatus' + ), + tool( + 'desktop_control_start', + 'Start a customer-granted agent control session. Live actions wait for the 10-second operator takeover warning; Full Access then allows continuous desktop and iPhone actions without per-action prompts.', + 'customerMacControlStart', + { + type: 'object', + additionalProperties: false, + properties: { + mode: { type: 'string', enum: ['full-access', 'ask-permission'], default: 'full-access' }, + agent_label: { type: 'string', minLength: 1, maxLength: 160 }, + }, + } + ), + tool('desktop_control_stop', 'Stop the active customer-granted agent control session.', 'customerMacControlStop'), + tool( + 'desktop_kill_switch', + 'Immediately stop and block future customer Mac control commands until the customer starts a new session.', + 'customerMacControlKillSwitch' + ), + tool( + 'customer_mac_capabilities', + 'Read supported named customer Mac actions and hard safety boundaries.', + 'customerMacCapabilities' + ), + tool( + 'desktop_see', + "See the customer's Mac through Peekaboo or the built-in screen/Accessibility fallback.", + 'desktopSee', + { + type: 'object', + additionalProperties: false, + properties: { + max_chars: { type: 'integer', minimum: 1, maximum: 20000, default: 4000 }, + max_nodes: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }, + }, + } + ), + tool( + 'desktop_click', + "Click a visible target label or, when labels are unavailable, an x/y point on the customer's Mac.", + 'desktopClick', + { + type: 'object', + additionalProperties: false, + properties: { + target_label: { type: 'string', minLength: 1, maxLength: 200 }, + snapshot_id: { type: 'string', minLength: 1, maxLength: 120 }, + element_id: { type: 'string', minLength: 1, maxLength: 80 }, + x: { type: 'integer' }, + y: { type: 'integer' }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool('desktop_type', 'Type exact text into the focused Mac field.', 'desktopType', { + type: 'object', + additionalProperties: false, + required: ['text'], + properties: { + text: { type: 'string', minLength: 1, maxLength: 4000 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool( + 'desktop_set_value', + 'Set an AX-backed native text field from a fresh desktop snapshot element. Dry-run defaults on and live mode may require approval.', + 'desktopSetValue', + { + type: 'object', + additionalProperties: false, + required: ['snapshot_id', 'element_id', 'value'], + properties: { + snapshot_id: { type: 'string', minLength: 1, maxLength: 120 }, + element_id: { type: 'string', minLength: 1, maxLength: 80 }, + value: { type: 'string', minLength: 1, maxLength: 4000 }, + attribute: { type: 'string', enum: ['value', 'selected_text'], default: 'value' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool('desktop_scroll', 'Scroll the focused Mac surface.', 'desktopScroll', { + type: 'object', + additionalProperties: false, + properties: { + direction: { type: 'string', enum: ['up', 'down', 'left', 'right'], default: 'down' }, + amount: { type: 'integer', minimum: 1, maximum: 5000, default: 600 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('desktop_drag', "Drag from one point to another on the customer's Mac.", 'desktopDrag', { + type: 'object', + additionalProperties: false, + required: ['from_x', 'from_y', 'to_x', 'to_y'], + properties: { + from_x: { type: 'integer' }, + from_y: { type: 'integer' }, + to_x: { type: 'integer' }, + to_y: { type: 'integer' }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('desktop_hotkey', 'Press a Mac hotkey such as cmd+l, cmd+r, or cmd+shift+4.', 'desktopHotkey', { + type: 'object', + additionalProperties: false, + required: ['keys'], + properties: { + keys: { type: 'string', minLength: 1, maxLength: 80 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('desktop_focus_app', 'Focus or open a Mac app by name.', 'desktopFocusApp', { + type: 'object', + additionalProperties: false, + required: ['app_name'], + properties: { + app_name: { type: 'string', minLength: 1, maxLength: 120 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('desktop_window', 'Perform a focused-window action: focus, minimize, maximize, or close.', 'desktopWindow', { + type: 'object', + additionalProperties: false, + required: ['action'], + properties: { + action: { type: 'string', enum: ['focus', 'minimize', 'maximize', 'zoom', 'close'] }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('desktop_menu', 'Choose a visible menu path through Peekaboo, for example File > New Tab.', 'desktopMenu', { + type: 'object', + additionalProperties: false, + required: ['menu_path'], + properties: { + menu_path: { type: 'string', minLength: 1, maxLength: 240 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool( + 'desktop_browser_action', + 'Use the local Mac browser: reload, back, forward, new_tab, or open_url.', + 'desktopBrowserAction', + { + type: 'object', + additionalProperties: false, + required: ['action'], + properties: { + action: { type: 'string', enum: ['reload', 'back', 'forward', 'new_tab', 'open_url'] }, + url: { type: 'string', maxLength: 2048 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_snapshot', + 'Read a safe screenshot path for the frontmost non-sensitive app; sensitive apps are blocked.', + 'customerMacSnapshot', + { + type: 'object', + additionalProperties: false, + properties: { + max_chars: { type: 'integer', minimum: 1, maximum: 20000, default: 4000 }, + }, + } + ), + tool( + 'customer_mac_ax_tree', + 'Read a capped Accessibility tree for the frontmost non-sensitive app.', + 'customerMacAxTree', + { + type: 'object', + additionalProperties: false, + properties: { + max_nodes: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }, + }, + } + ), + tool( + 'customer_mac_app_focus', + 'Approval-gated named action: focus a non-sensitive customer Mac app by name.', + 'customerMacAppFocus', + { + type: 'object', + additionalProperties: false, + required: ['app_name'], + properties: { + app_name: { type: 'string' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_local_site_open', + 'Approval-gated named action: open a localhost, loopback, or .local website on the customer Mac.', + 'customerMacLocalSiteOpen', + { + type: 'object', + additionalProperties: false, + required: ['url'], + properties: { + url: { type: 'string' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_local_site_action', + 'Approval-gated named action: run reload, back, or forward in the frontmost supported browser.', + 'customerMacLocalSiteAction', + { + type: 'object', + additionalProperties: false, + required: ['action'], + properties: { + action: { type: 'string', enum: ['reload', 'back', 'forward'] }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool('iphone_see', 'See the visible iPhone Mirroring surface through the paired Mac.', 'iphoneSee', { + type: 'object', + additionalProperties: false, + properties: { + max_chars: { type: 'integer', minimum: 1, maximum: 20000, default: 4000 }, + max_nodes: { type: 'integer', minimum: 1, maximum: 1000, default: 200 }, + }, + }), + tool( + 'iphone_tap', + 'Tap a visible iPhone target label or fallback x/y point inside iPhone Mirroring.', + 'iphoneTap', + { + type: 'object', + additionalProperties: false, + properties: { + target_label: { type: 'string', minLength: 1, maxLength: 200 }, + snapshot_id: { type: 'string', minLength: 1, maxLength: 120 }, + element_id: { type: 'string', minLength: 1, maxLength: 80 }, + x: { type: 'integer' }, + y: { type: 'integer' }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool('iphone_swipe', 'Swipe the focused iPhone Mirroring surface.', 'iphoneSwipe', { + type: 'object', + additionalProperties: false, + required: ['direction'], + properties: { + direction: { type: 'string', enum: ['left', 'right', 'up', 'down'] }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool('iphone_type', 'Type exact text into the focused iPhone Mirroring field.', 'iphoneType', { + type: 'object', + additionalProperties: false, + required: ['text'], + properties: { + text: { type: 'string', minLength: 1, maxLength: 4000 }, + dry_run: { type: 'boolean', default: false }, + approval_audit_id: approvalAuditIdProperty, + }, + }), + tool( + 'customer_mac_iphone_mirroring_status', + 'Read iPhone Mirroring readiness and supported named actions.', + 'customerMacIphoneMirroringStatus' + ), + tool( + 'customer_mac_iphone_mirroring_focus', + 'Approval-gated named action: focus iPhone Mirroring.', + 'customerMacIphoneMirroringFocus', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_home', + 'Approval-gated named action: send Home to iPhone Mirroring.', + 'customerMacIphoneMirroringHome', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_app_switcher', + 'Approval-gated named action: open the iPhone Mirroring App Switcher.', + 'customerMacIphoneMirroringAppSwitcher', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_spotlight', + 'Approval-gated named action: open iPhone Spotlight through iPhone Mirroring.', + 'customerMacIphoneMirroringSpotlight', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_type_spotlight', + 'Approval-gated named action: type short disposable/search text into iPhone Spotlight.', + 'customerMacIphoneMirroringTypeSpotlight', + { + type: 'object', + additionalProperties: false, + required: ['text'], + properties: { + text: { type: 'string', minLength: 1, maxLength: 80 }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_iphone_mirroring_open_app', + 'Approval-gated named action: launch a non-sensitive iPhone app through Spotlight.', + 'customerMacIphoneMirroringOpenApp', + { + type: 'object', + additionalProperties: false, + required: ['app_name'], + properties: { + app_name: { type: 'string', minLength: 1, maxLength: 80 }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_iphone_mirroring_tap_named_target', + 'Approval-gated named action: press an exact visible iPhone Mirroring AX label; generic coordinates are blocked.', + 'customerMacIphoneMirroringTapNamedTarget', + { + type: 'object', + additionalProperties: false, + required: ['target_label'], + properties: { + target_label: { type: 'string', minLength: 1, maxLength: 80 }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_iphone_mirroring_scroll', + 'Approval-gated named action: scroll the focused iPhone Mirroring window by named direction.', + 'customerMacIphoneMirroringScroll', + { + type: 'object', + additionalProperties: false, + properties: { + direction: { type: 'string', enum: ['up', 'down'], default: 'down' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_iphone_mirroring_swipe_left', + 'Approval-gated named action: swipe left in iPhone Mirroring. Requires dry-run approval before live use.', + 'customerMacIphoneMirroringSwipeLeft', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_swipe_right', + 'Approval-gated named action: swipe right in iPhone Mirroring. Requires dry-run approval before live use.', + 'customerMacIphoneMirroringSwipeRight', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_swipe_up', + 'Approval-gated named action: swipe up in iPhone Mirroring. Requires dry-run approval before live use.', + 'customerMacIphoneMirroringSwipeUp', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_swipe_down', + 'Approval-gated named action: swipe down in iPhone Mirroring. Requires dry-run approval before live use.', + 'customerMacIphoneMirroringSwipeDown', + { + type: 'object', + additionalProperties: false, + properties: { dry_run: { type: 'boolean', default: true }, approval_audit_id: approvalAuditIdProperty }, + } + ), + tool( + 'customer_mac_iphone_mirroring_type_approved_text', + 'Approval-gated named action: type exact same-turn-approved text in iPhone Mirroring.', + 'customerMacIphoneMirroringTypeApprovedText', + { + type: 'object', + additionalProperties: false, + required: ['text'], + properties: { + text: { type: 'string', minLength: 1, maxLength: 240 }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_iphone_mirroring_send_approved_message', + 'Full Access iPhone action: type and send one exact message through visible iPhone Mirroring; Ask Permission gates it.', + 'customerMacIphoneMirroringSendApprovedMessage', + { + type: 'object', + additionalProperties: false, + required: ['text', 'recipient_context'], + properties: { + text: { type: 'string', minLength: 1, maxLength: 240 }, + recipient_context: { type: 'string', minLength: 1, maxLength: 160 }, + target_label: { type: 'string', minLength: 1, maxLength: 80, default: 'Send' }, + dry_run: { type: 'boolean', default: true }, + approval_audit_id: approvalAuditIdProperty, + }, + } + ), + tool( + 'customer_mac_screen_sharing_status', + 'Read Screen Sharing/Remote Management status; this tool cannot enable it.', + 'customerMacScreenSharingStatus' + ), + ]; +} + +function tool( + name: string, + description: string, + command: BridgeCommandKey, + parameters: Record = { type: 'object', additionalProperties: false, properties: {}, required: [] } +): ToolDefinition { + return { + name, + description, + parameters: normalizeToolParameters(parameters), + execute: async (_toolCallId: string, params: BridgeParams = {}) => toToolResult(await runBridge(command, params)), + }; +} + +function toToolResult(payload: unknown): ToolResult { + if (isToolResultLike(payload)) { + return payload; + } + + return { + content: [{ type: 'text', text: stringifyToolPayload(payload) }], + details: payload, + }; +} + +function isToolResultLike(value: unknown): value is ToolResult { + return isRecord(value) && Array.isArray(value.content); +} + +function stringifyToolPayload(payload: unknown): string { + if (typeof payload === 'string') { + return payload; + } + + try { + const text = JSON.stringify(payload, null, 2); + if (typeof text === 'string') { + return text; + } + } catch { + // Fall through to String(payload). + } + + return String(payload); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function normalizeToolParameters(parameters: Record): Record { + const properties = + parameters.properties && typeof parameters.properties === 'object' && !Array.isArray(parameters.properties) + ? (parameters.properties as Record) + : {}; + const required = Array.isArray(parameters.required) + ? parameters.required + .filter((value): value is string => typeof value === 'string' && value.length > 0) + .filter((value) => Object.prototype.hasOwnProperty.call(properties, value)) + : []; + + return { + ...parameters, + type: 'object', + additionalProperties: parameters.additionalProperties === undefined ? false : parameters.additionalProperties, + properties, + required, + }; +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/openclaw.plugin.json b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/openclaw.plugin.json new file mode 100644 index 0000000000..a8099a497b --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/openclaw.plugin.json @@ -0,0 +1,89 @@ +{ + "id": "evaos-desktop-bridge", + "name": "EvaOS Desktop Bridge", + "description": "Guarded OpenClaw tools for paired customer Mac, iPhone Mirroring, and Codex Desktop readiness.", + "version": "0.2.0", + "owner": "100yenadmin/evaOS-GUI", + "kind": "tool", + "main": "dist/index.js", + "activation": { + "onStartup": true + }, + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "contracts": { + "tools": [ + "desktop_bridge_status", + "desktop_bridge_capabilities", + "desktop_bridge_latest", + "desktop_bridge_audit_tail", + "desktop_bridge_queue_list", + "desktop_bridge_queue_append", + "desktop_bridge_codex_frontmost", + "desktop_bridge_codex_windows", + "desktop_bridge_codex_threads", + "desktop_bridge_codex_thread_map", + "desktop_bridge_codex_send_visible_message", + "desktop_bridge_codex_continue_thread", + "desktop_bridge_codex_select_thread", + "desktop_bridge_codex_snapshot", + "desktop_bridge_codex_inspect", + "desktop_bridge_codex_ax_tree", + "desktop_bridge_codex_app_server_status", + "desktop_bridge_codex_app_server_remote_control_status", + "desktop_bridge_codex_app_server_threads", + "desktop_bridge_codex_connections_status", + "desktop_bridge_codex_app_server_loaded_threads", + "desktop_bridge_codex_live_status", + "evaos_provider_profiles", + "evaos_provider_active_profile", + "evaos_provider_complete_auth", + "evaos_shared_browser_guidance", + "customer_mac_status", + "desktop_control_status", + "desktop_control_start", + "desktop_control_stop", + "desktop_kill_switch", + "customer_mac_capabilities", + "desktop_see", + "desktop_click", + "desktop_type", + "desktop_set_value", + "desktop_scroll", + "desktop_drag", + "desktop_hotkey", + "desktop_focus_app", + "desktop_window", + "desktop_menu", + "desktop_browser_action", + "customer_mac_snapshot", + "customer_mac_ax_tree", + "customer_mac_app_focus", + "customer_mac_local_site_open", + "customer_mac_local_site_action", + "iphone_see", + "iphone_tap", + "iphone_swipe", + "iphone_type", + "customer_mac_iphone_mirroring_status", + "customer_mac_iphone_mirroring_focus", + "customer_mac_iphone_mirroring_home", + "customer_mac_iphone_mirroring_app_switcher", + "customer_mac_iphone_mirroring_spotlight", + "customer_mac_iphone_mirroring_type_spotlight", + "customer_mac_iphone_mirroring_open_app", + "customer_mac_iphone_mirroring_tap_named_target", + "customer_mac_iphone_mirroring_scroll", + "customer_mac_iphone_mirroring_swipe_left", + "customer_mac_iphone_mirroring_swipe_right", + "customer_mac_iphone_mirroring_swipe_up", + "customer_mac_iphone_mirroring_swipe_down", + "customer_mac_iphone_mirroring_type_approved_text", + "customer_mac_iphone_mirroring_send_approved_message", + "customer_mac_screen_sharing_status" + ] + } +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package.json b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package.json new file mode 100644 index 0000000000..537ffc4822 --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package.json @@ -0,0 +1,115 @@ +{ + "name": "@100yenadmin/evaos-desktop-bridge-openclaw", + "version": "0.2.0", + "private": true, + "description": "Guarded OpenClaw plugin wrapper for evaos-desktop-bridge.", + "author": "100yenadmin/evaOS-GUI", + "repository": { + "type": "git", + "url": "git+https://github.com/100yenadmin/evaOS-GUI.git", + "directory": "resources/evaos-beta/bridge/agent-tools/openclaw-plugin" + }, + "files": [ + "dist", + "scripts", + "README.md", + "openclaw.plugin.json", + "package.json" + ], + "type": "module", + "exports": { + ".": "./dist/index.js" + }, + "scripts": { + "build": "tsc -p tsconfig.json && oxfmt dist", + "test": "npm run build && node --test tests/*.test.mjs" + }, + "devDependencies": { + "oxfmt": "0.41.0", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "openclaw": ">=0.0.0" + }, + "evaos": { + "owner": "100yenadmin/evaOS-GUI", + "releaseTagNamespace": "evaos-desktop-bridge-openclaw-vX.Y.Z" + }, + "openclaw": { + "extensions": [ + "./dist/index.js" + ], + "contracts": { + "tools": [ + "desktop_bridge_status", + "desktop_bridge_capabilities", + "desktop_bridge_latest", + "desktop_bridge_audit_tail", + "desktop_bridge_queue_list", + "desktop_bridge_queue_append", + "desktop_bridge_codex_frontmost", + "desktop_bridge_codex_windows", + "desktop_bridge_codex_threads", + "desktop_bridge_codex_thread_map", + "desktop_bridge_codex_send_visible_message", + "desktop_bridge_codex_continue_thread", + "desktop_bridge_codex_select_thread", + "desktop_bridge_codex_snapshot", + "desktop_bridge_codex_inspect", + "desktop_bridge_codex_ax_tree", + "desktop_bridge_codex_app_server_status", + "desktop_bridge_codex_app_server_remote_control_status", + "desktop_bridge_codex_app_server_threads", + "desktop_bridge_codex_connections_status", + "desktop_bridge_codex_app_server_loaded_threads", + "desktop_bridge_codex_live_status", + "evaos_provider_profiles", + "evaos_provider_active_profile", + "evaos_provider_complete_auth", + "evaos_shared_browser_guidance", + "customer_mac_status", + "desktop_control_status", + "desktop_control_start", + "desktop_control_stop", + "desktop_kill_switch", + "customer_mac_capabilities", + "desktop_see", + "desktop_click", + "desktop_type", + "desktop_set_value", + "desktop_scroll", + "desktop_drag", + "desktop_hotkey", + "desktop_focus_app", + "desktop_window", + "desktop_menu", + "desktop_browser_action", + "customer_mac_snapshot", + "customer_mac_ax_tree", + "customer_mac_app_focus", + "customer_mac_local_site_open", + "customer_mac_local_site_action", + "iphone_see", + "iphone_tap", + "iphone_swipe", + "iphone_type", + "customer_mac_iphone_mirroring_status", + "customer_mac_iphone_mirroring_focus", + "customer_mac_iphone_mirroring_home", + "customer_mac_iphone_mirroring_app_switcher", + "customer_mac_iphone_mirroring_spotlight", + "customer_mac_iphone_mirroring_type_spotlight", + "customer_mac_iphone_mirroring_open_app", + "customer_mac_iphone_mirroring_tap_named_target", + "customer_mac_iphone_mirroring_scroll", + "customer_mac_iphone_mirroring_swipe_left", + "customer_mac_iphone_mirroring_swipe_right", + "customer_mac_iphone_mirroring_swipe_up", + "customer_mac_iphone_mirroring_swipe_down", + "customer_mac_iphone_mirroring_type_approved_text", + "customer_mac_iphone_mirroring_send_approved_message", + "customer_mac_screen_sharing_status" + ] + } + } +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/qa-run-bridge.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/qa-run-bridge.mjs new file mode 100644 index 0000000000..c134d517ad --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/qa-run-bridge.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { runBridge } from '../dist/src/bridge.js'; +import { desktopBridgeFirewall } from '../dist/src/firewall.js'; + +const [, , command, paramsArgument = '{}'] = process.argv; + +if (!command) { + console.error('usage: qa-run-bridge.mjs [params-json]'); + process.exit(2); +} + +let params; +try { + const paramsJSON = paramsArgument === '-' ? await readStdin() : paramsArgument; + params = JSON.parse(paramsJSON); +} catch (error) { + console.error(`invalid params JSON: ${error instanceof Error ? error.message : String(error)}`); + process.exit(2); +} + +const toolMap = await loadRegisteredToolMap(); +const bridgeCommand = toolMap.get(command) || command; +const firewallDecision = desktopBridgeFirewall({ + toolName: command, + args: params, +}); + +if (firewallDecision?.block) { + console.log( + JSON.stringify({ + ok: false, + errors: [ + { + code: 'qa_openclaw_firewall_blocked', + message: firewallDecision.blockReason || 'OpenClaw desktop bridge firewall blocked this tool call.', + guidance: 'Use only the registered desktop bridge tools and audited connector command contract.', + }, + ], + }) + ); + process.exit(0); +} + +if (firewallDecision?.requireApproval) { + console.log( + JSON.stringify({ + ok: false, + errors: [ + { + code: 'qa_openclaw_firewall_approval_required', + message: firewallDecision.requireApproval.description, + guidance: 'Run the dry-run first, collect approval, then rerun with matching approval evidence.', + }, + ], + }) + ); + process.exit(0); +} + +if (!toolMap.has(command) && command.includes('_')) { + console.log( + JSON.stringify({ + ok: false, + errors: [ + { + code: 'qa_openclaw_tool_not_registered', + message: `${command} is not registered by the built OpenClaw plugin entrypoint.`, + guidance: 'Rebuild the OpenClaw plugin and verify openclaw-plugin/dist/index.js.', + }, + ], + }) + ); + process.exit(0); +} + +try { + const result = await runBridge(bridgeCommand, params); + console.log(JSON.stringify(result)); +} catch (error) { + console.log( + JSON.stringify({ + ok: false, + errors: [ + { + code: 'qa_run_bridge_failed', + message: error instanceof Error ? error.message : String(error), + guidance: 'Build the OpenClaw plugin and verify the bridge command shape.', + }, + ], + }) + ); + process.exit(0); +} + +async function readStdin() { + const chunks = []; + for await (const chunk of process.stdin) { + chunks.push(Buffer.from(chunk)); + } + return Buffer.concat(chunks).toString('utf8') || '{}'; +} + +async function loadRegisteredToolMap() { + const scriptDir = dirname(fileURLToPath(import.meta.url)); + const distIndex = join(scriptDir, '../dist/index.js'); + const source = await readFile(distIndex, 'utf8'); + const map = new Map(); + const toolCallPattern = /tool\(\s*"([^"]+)"[\s\S]*?,\s*"([A-Za-z0-9]+)"(?:\s*,|\s*\))/g; + let match; + while ((match = toolCallPattern.exec(source)) !== null) { + map.set(match[1], match[2]); + } + return map; +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts new file mode 100644 index 0000000000..66d26c179f --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts @@ -0,0 +1,1823 @@ +import { execFile } from 'node:child_process'; +import { createHmac, randomUUID } from 'node:crypto'; +import * as fs from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { promisify } from 'node:util'; + +const execFileAsync = promisify(execFile) as ( + file: string, + args: string[], + options: Record +) => Promise<{ stdout: string }>; +const fsCompat = fs as unknown as { + mkdtempSync: (prefix: string) => string; + chmodSync: (path: string, mode: number) => void; + rmSync: (path: string, options: { recursive: boolean; force: boolean }) => void; +}; + +export type BridgeCommandKey = + | 'status' + | 'capabilities' + | 'latest' + | 'auditTail' + | 'queueList' + | 'queueAppend' + | 'codexFrontmost' + | 'codexWindows' + | 'codexThreads' + | 'codexThreadMap' + | 'codexSendVisibleMessage' + | 'codexContinueThread' + | 'codexSelectThread' + | 'codexSnapshot' + | 'codexInspect' + | 'codexAxTree' + | 'codexConnectionsStatus' + | 'codexAppServerStatus' + | 'codexAppServerThreads' + | 'codexAppServerLoadedThreads' + | 'codexLiveStatus' + | 'codexAppServerRemoteControlStatus' + | 'evaosProviderProfiles' + | 'evaosProviderActiveProfile' + | 'evaosProviderCompleteAuth' + | 'evaosSharedBrowserGuidance' + | 'customerMacStatus' + | 'customerMacCapabilities' + | 'customerMacControlStatus' + | 'customerMacControlStart' + | 'customerMacControlStop' + | 'customerMacControlKillSwitch' + | 'desktopSee' + | 'desktopClick' + | 'desktopType' + | 'desktopSetValue' + | 'desktopScroll' + | 'desktopDrag' + | 'desktopHotkey' + | 'desktopFocusApp' + | 'desktopWindow' + | 'desktopMenu' + | 'desktopBrowserAction' + | 'customerMacSnapshot' + | 'customerMacAxTree' + | 'customerMacAppFocus' + | 'customerMacLocalSiteOpen' + | 'customerMacLocalSiteAction' + | 'customerMacIphoneMirroringStatus' + | 'iphoneSee' + | 'iphoneTap' + | 'iphoneSwipe' + | 'iphoneType' + | 'customerMacIphoneMirroringFocus' + | 'customerMacIphoneMirroringHome' + | 'customerMacIphoneMirroringAppSwitcher' + | 'customerMacIphoneMirroringSpotlight' + | 'customerMacIphoneMirroringTypeSpotlight' + | 'customerMacIphoneMirroringOpenApp' + | 'customerMacIphoneMirroringTapNamedTarget' + | 'customerMacIphoneMirroringScroll' + | 'customerMacIphoneMirroringSwipeLeft' + | 'customerMacIphoneMirroringSwipeRight' + | 'customerMacIphoneMirroringSwipeUp' + | 'customerMacIphoneMirroringSwipeDown' + | 'customerMacIphoneMirroringTypeApprovedText' + | 'customerMacIphoneMirroringSendApprovedMessage' + | 'customerMacScreenSharingStatus'; + +export type BridgeParams = { + max_chars?: number; + max_nodes?: number; + max_items?: number; + limit?: number; + kind?: string; + source_audit_id?: string; + message?: string; + message_file?: string; + value_file?: string; + thread_id?: string; + turn_id?: string; + title?: string; + prompt?: string; + dry_run?: boolean; + confirm?: boolean; + duration_ms?: number; + wait_ms?: number; + poll_interval_ms?: number; + app_name?: string; + url?: string; + action?: string; + text?: string; + value?: string; + attribute?: string; + direction?: string; + recipient_context?: string; + target_label?: string; + snapshot_id?: string; + element_id?: string; + approval_audit_id?: string; + mode?: string; + agent_label?: string; + x?: number; + y?: number; + from_x?: number; + from_y?: number; + to_x?: number; + to_y?: number; + amount?: number; + keys?: string; + menu_path?: string; + identity?: string; + scopes?: string[]; + expires_at?: string; + server_secret_ref?: string; +}; + +const FIXED_COMMANDS: Record< + Exclude< + BridgeCommandKey, + | 'codexSnapshot' + | 'codexInspect' + | 'codexAxTree' + | 'auditTail' + | 'queueList' + | 'queueAppend' + | 'codexThreads' + | 'codexThreadMap' + | 'codexSendVisibleMessage' + | 'codexContinueThread' + | 'codexSelectThread' + | 'codexAppServerThreads' + | 'codexAppServerLoadedThreads' + | 'codexLiveStatus' + | 'evaosProviderProfiles' + | 'evaosProviderActiveProfile' + | 'evaosProviderCompleteAuth' + | 'evaosSharedBrowserGuidance' + | 'customerMacSnapshot' + | 'customerMacControlStart' + | 'desktopSee' + | 'desktopClick' + | 'desktopType' + | 'desktopSetValue' + | 'desktopScroll' + | 'desktopDrag' + | 'desktopHotkey' + | 'desktopFocusApp' + | 'desktopWindow' + | 'desktopMenu' + | 'desktopBrowserAction' + | 'customerMacAxTree' + | 'customerMacAppFocus' + | 'customerMacLocalSiteOpen' + | 'customerMacLocalSiteAction' + | 'iphoneSee' + | 'iphoneTap' + | 'iphoneSwipe' + | 'iphoneType' + | 'customerMacIphoneMirroringFocus' + | 'customerMacIphoneMirroringHome' + | 'customerMacIphoneMirroringAppSwitcher' + | 'customerMacIphoneMirroringSpotlight' + | 'customerMacIphoneMirroringTypeSpotlight' + | 'customerMacIphoneMirroringOpenApp' + | 'customerMacIphoneMirroringTapNamedTarget' + | 'customerMacIphoneMirroringScroll' + | 'customerMacIphoneMirroringSwipeLeft' + | 'customerMacIphoneMirroringSwipeRight' + | 'customerMacIphoneMirroringSwipeUp' + | 'customerMacIphoneMirroringSwipeDown' + | 'customerMacIphoneMirroringTypeApprovedText' + | 'customerMacIphoneMirroringSendApprovedMessage' + >, + string[] +> = { + status: ['status', '--json'], + capabilities: ['capabilities', '--json'], + latest: ['latest', '--json'], + codexFrontmost: ['codex', 'frontmost', '--json'], + codexWindows: ['codex', 'windows', '--json'], + codexConnectionsStatus: ['codex', 'connections', 'status', '--json'], + codexAppServerStatus: ['codex', 'app-server', 'status', '--json'], + codexAppServerRemoteControlStatus: ['codex', 'app-server', 'remote-control-status', '--json'], + customerMacStatus: ['customer-mac', 'status', '--json'], + customerMacCapabilities: ['customer-mac', 'capabilities', '--json'], + customerMacControlStatus: ['customer-mac', 'control', 'status', '--json'], + customerMacControlStop: ['customer-mac', 'control', 'stop', '--json'], + customerMacControlKillSwitch: ['customer-mac', 'control', 'kill-switch', '--json'], + customerMacIphoneMirroringStatus: ['customer-mac', 'iphone-mirroring', 'status', '--json'], + customerMacScreenSharingStatus: ['customer-mac', 'screen-sharing', 'status', '--json'], +}; + +const CUSTOMER_MAC_REMOTE_COMMANDS = new Set([ + 'customerMacStatus', + 'customerMacCapabilities', + 'customerMacControlStatus', + 'customerMacControlStart', + 'customerMacControlStop', + 'customerMacControlKillSwitch', + 'desktopSee', + 'desktopClick', + 'desktopType', + 'desktopSetValue', + 'desktopScroll', + 'desktopDrag', + 'desktopHotkey', + 'desktopFocusApp', + 'desktopWindow', + 'desktopMenu', + 'desktopBrowserAction', + 'customerMacSnapshot', + 'customerMacAxTree', + 'customerMacAppFocus', + 'customerMacLocalSiteOpen', + 'customerMacLocalSiteAction', + 'customerMacIphoneMirroringStatus', + 'iphoneSee', + 'iphoneTap', + 'iphoneSwipe', + 'iphoneType', + 'customerMacIphoneMirroringFocus', + 'customerMacIphoneMirroringHome', + 'customerMacIphoneMirroringAppSwitcher', + 'customerMacIphoneMirroringSpotlight', + 'customerMacIphoneMirroringTypeSpotlight', + 'customerMacIphoneMirroringOpenApp', + 'customerMacIphoneMirroringTapNamedTarget', + 'customerMacIphoneMirroringScroll', + 'customerMacIphoneMirroringSwipeLeft', + 'customerMacIphoneMirroringSwipeRight', + 'customerMacIphoneMirroringSwipeUp', + 'customerMacIphoneMirroringSwipeDown', + 'customerMacIphoneMirroringTypeApprovedText', + 'customerMacIphoneMirroringSendApprovedMessage', + 'customerMacScreenSharingStatus', +]); + +export function buildBridgeArgv(command: BridgeCommandKey, params: BridgeParams = {}): string[] { + if (command in FIXED_COMMANDS) { + return FIXED_COMMANDS[command as keyof typeof FIXED_COMMANDS]; + } + if (command === 'auditTail') { + return ['audit-tail', '--json', '--limit', String(clampInt(params.limit, 20, 1, 100))]; + } + if (command === 'queueList') { + return ['queue', 'list', '--json', '--limit', String(clampInt(params.limit, 20, 1, 100))]; + } + if (command === 'queueAppend') { + return [ + 'queue', + 'append', + '--json', + '--kind', + requiredString(params.kind, 'kind'), + '--source-audit-id', + requiredString(params.source_audit_id, 'source_audit_id'), + ...(params.message ? ['--message', String(params.message)] : []), + ]; + } + if (command === 'codexThreads') { + return ['codex', 'threads', '--json', '--max-items', String(clampInt(params.max_items, 50, 1, 200))]; + } + if (command === 'codexThreadMap') { + return ['codex', 'thread-map', '--json', '--max-items', String(clampInt(params.max_items, 50, 1, 200))]; + } + if (command === 'codexSelectThread') { + return [ + 'codex', + 'select-thread', + '--json', + '--thread-id', + requiredString(params.thread_id, 'thread_id'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'codexContinueThread') { + return [ + 'codex', + 'continue-thread', + '--json', + '--title', + requiredString(params.title, 'title'), + '--prompt', + String(params.prompt || 'continue'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'codexSendVisibleMessage') { + const messageFile = + typeof params.message_file === 'string' && params.message_file.trim() !== '' + ? params.message_file.trim() + : undefined; + return [ + 'codex', + 'send-visible-message', + '--json', + '--thread-id', + requiredString(params.thread_id, 'thread_id'), + ...(messageFile ? ['--message-file', messageFile] : ['--message', requiredString(params.message, 'message')]), + ...(params.dry_run !== false ? ['--dry-run'] : ['--live']), + ...(params.confirm === true ? ['--confirm'] : []), + ...guardedApprovalArg(params), + ...(params.wait_ms !== undefined ? ['--wait-ms', String(clampInt(params.wait_ms, 0, 0, 120000))] : []), + ...(params.poll_interval_ms !== undefined + ? ['--poll-interval-ms', String(clampInt(params.poll_interval_ms, 2000, 250, 10000))] + : []), + ]; + } + if (command === 'codexSnapshot') { + return ['codex', 'snapshot', '--json', '--max-chars', String(clampInt(params.max_chars, 4000, 1, 20000))]; + } + if (command === 'codexInspect') { + return ['codex', 'inspect', '--json', '--max-nodes', String(clampInt(params.max_nodes, 120, 1, 1000))]; + } + if (command === 'codexAxTree') { + return ['codex', 'ax-tree', '--json', '--max-nodes', String(clampInt(params.max_nodes, 200, 1, 1000))]; + } + if (command === 'codexAppServerThreads') { + return ['codex', 'app-server', 'threads', '--json', '--max-items', String(clampInt(params.max_items, 50, 1, 200))]; + } + if (command === 'codexAppServerLoadedThreads') { + return [ + 'codex', + 'app-server', + 'loaded-threads', + '--json', + '--max-items', + String(clampInt(params.max_items, 50, 1, 200)), + ]; + } + if (command === 'codexLiveStatus') { + return [ + 'codex', + 'app-server', + 'subscribe', + '--json', + '--thread-id', + requiredString(params.thread_id, 'thread_id'), + '--duration-ms', + String(clampInt(params.duration_ms, 1000, 1, 30000)), + ]; + } + if (command === 'customerMacSnapshot') { + return ['customer-mac', 'snapshot', '--json', '--max-chars', String(clampInt(params.max_chars, 4000, 1, 20000))]; + } + if (command === 'customerMacAxTree') { + return ['customer-mac', 'ax-tree', '--json', '--max-nodes', String(clampInt(params.max_nodes, 200, 1, 1000))]; + } + if (command === 'customerMacControlStart') { + return [ + 'customer-mac', + 'control', + 'start', + '--json', + '--mode', + String(params.mode || 'full-access'), + ...(params.agent_label ? ['--agent-label', String(params.agent_label)] : []), + ]; + } + if (command === 'desktopSee') { + return [ + 'customer-mac', + 'desktop', + 'see', + '--json', + '--max-chars', + String(clampInt(params.max_chars, 4000, 1, 20000)), + '--max-nodes', + String(clampInt(params.max_nodes, 200, 1, 1000)), + ]; + } + if (command === 'desktopClick') { + return [ + 'customer-mac', + 'desktop', + 'click', + '--json', + ...optionalStringArg(params.snapshot_id, '--snapshot-id'), + ...optionalStringArg(params.element_id, '--element-id'), + ...optionalStringArg(params.target_label, '--target-label'), + ...optionalNumberArg(params.x, '--x'), + ...optionalNumberArg(params.y, '--y'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopType') { + return [ + 'customer-mac', + 'desktop', + 'type', + '--json', + '--text', + requiredString(params.text, 'text'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopSetValue') { + const valueFile = + typeof params.value_file === 'string' && params.value_file.trim() !== '' ? params.value_file.trim() : undefined; + if (!valueFile) { + throw new Error('desktopSetValue value must be materialized before building CLI argv.'); + } + return [ + 'customer-mac', + 'desktop', + 'set-value', + '--json', + '--snapshot-id', + requiredString(params.snapshot_id, 'snapshot_id'), + '--element-id', + requiredString(params.element_id, 'element_id'), + '--value-file', + valueFile, + '--attribute', + String(params.attribute || 'value'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopScroll') { + return [ + 'customer-mac', + 'desktop', + 'scroll', + '--json', + '--direction', + String(params.direction || 'down'), + '--amount', + String(clampInt(params.amount, 600, 1, 5000)), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopDrag') { + return [ + 'customer-mac', + 'desktop', + 'drag', + '--json', + '--from-x', + requiredNumberString(params.from_x, 'from_x'), + '--from-y', + requiredNumberString(params.from_y, 'from_y'), + '--to-x', + requiredNumberString(params.to_x, 'to_x'), + '--to-y', + requiredNumberString(params.to_y, 'to_y'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopHotkey') { + return [ + 'customer-mac', + 'desktop', + 'hotkey', + '--json', + '--keys', + requiredString(params.keys, 'keys'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopFocusApp') { + return [ + 'customer-mac', + 'desktop', + 'focus-app', + '--json', + '--app-name', + requiredString(params.app_name, 'app_name'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopWindow') { + return [ + 'customer-mac', + 'desktop', + 'window', + '--json', + '--action', + requiredString(params.action, 'action'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopMenu') { + return [ + 'customer-mac', + 'desktop', + 'menu', + '--json', + '--menu-path', + requiredString(params.menu_path, 'menu_path'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'desktopBrowserAction') { + return [ + 'customer-mac', + 'desktop', + 'browser-action', + '--json', + '--action', + requiredString(params.action, 'action'), + ...optionalStringArg(params.url, '--url'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacAppFocus') { + return [ + 'customer-mac', + 'app-focus', + '--json', + '--app-name', + requiredString(params.app_name, 'app_name'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacLocalSiteOpen') { + return [ + 'customer-mac', + 'local-site', + 'open', + '--json', + '--url', + requiredString(params.url, 'url'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacLocalSiteAction') { + return [ + 'customer-mac', + 'local-site', + 'action', + '--json', + '--action', + requiredString(params.action, 'action'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'iphoneSee') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'see', + '--json', + '--max-chars', + String(clampInt(params.max_chars, 4000, 1, 20000)), + '--max-nodes', + String(clampInt(params.max_nodes, 200, 1, 1000)), + ]; + } + if (command === 'iphoneTap') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'tap', + '--json', + ...optionalStringArg(params.snapshot_id, '--snapshot-id'), + ...optionalStringArg(params.element_id, '--element-id'), + ...optionalStringArg(params.target_label, '--target-label'), + ...optionalNumberArg(params.x, '--x'), + ...optionalNumberArg(params.y, '--y'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'iphoneSwipe') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'swipe', + '--json', + '--direction', + requiredString(params.direction, 'direction'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'iphoneType') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'type', + '--json', + '--text', + requiredString(params.text, 'text'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringFocus') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'focus', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringHome') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'home', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringAppSwitcher') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'app-switcher', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSpotlight') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'spotlight', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringTypeSpotlight') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'type-spotlight', + '--json', + '--text', + requiredString(params.text, 'text'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringOpenApp') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'open-app', + '--json', + '--app-name', + requiredString(params.app_name, 'app_name'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringTapNamedTarget') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'tap-named-target', + '--json', + '--target-label', + requiredString(params.target_label, 'target_label'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringScroll') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'scroll', + '--json', + '--direction', + String(params.direction || 'down'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSwipeLeft') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'swipe-left', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSwipeRight') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'swipe-right', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSwipeUp') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'swipe-up', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSwipeDown') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'swipe-down', + '--json', + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringTypeApprovedText') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'type-approved-text', + '--json', + '--text', + requiredString(params.text, 'text'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + if (command === 'customerMacIphoneMirroringSendApprovedMessage') { + return [ + 'customer-mac', + 'iphone-mirroring', + 'send-approved-message', + '--json', + '--text', + requiredString(params.text, 'text'), + '--recipient-context', + requiredString(params.recipient_context, 'recipient_context'), + '--target-label', + String(params.target_label || 'Send'), + ...(params.dry_run !== false ? ['--dry-run'] : []), + ...guardedApprovalArg(params), + ]; + } + throw new Error(`Unsupported bridge command key: ${String(command)}`); +} + +function approvalArg(params: BridgeParams): string[] { + if (typeof params.approval_audit_id !== 'string' || params.approval_audit_id.trim() === '') { + return []; + } + return ['--approval-audit-id', params.approval_audit_id.trim()]; +} + +function guardedApprovalArg(params: BridgeParams): string[] { + if (params.dry_run !== false) { + return []; + } + return approvalArg(params); +} + +function requiredString(value: unknown, name: string): string { + if (typeof value !== 'string' || value.trim() === '') { + throw new Error(`${name} is required`); + } + return value; +} + +function requiredNumberString(value: unknown, name: string): string { + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new Error(`${name} is required`); + } + return String(Math.trunc(value)); +} + +function optionalNumberArg(value: unknown, flag: string): string[] { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return []; + } + return [flag, String(Math.trunc(value))]; +} + +function optionalStringArg(value: unknown, flag: string): string[] { + if (typeof value !== 'string' || value.trim() === '') { + return []; + } + return [flag, value.trim()]; +} + +export async function runBridge(command: BridgeCommandKey, params: BridgeParams = {}): Promise { + if (String(command) === 'customerMacCompletePairing') { + return { + ok: false, + errors: [ + { + code: 'legacy_pairing_removed', + message: 'Legacy pairing-code enrollment is unavailable.', + guidance: 'Launch the selected customer runtime from evaOS Workbench.', + }, + ], + }; + } + if (command === 'evaosProviderProfiles') { + return await providerProfilesPayload(); + } + if (command === 'evaosProviderActiveProfile') { + return await providerActiveProfilePayload(); + } + if (command === 'evaosProviderCompleteAuth') { + return await providerCompleteAuthPayload(params); + } + if (command === 'evaosSharedBrowserGuidance') { + return sharedBrowserGuidancePayload(); + } + loadDesktopBridgeEnvFile(); + const remoteURL = process.env.EVAOS_DESKTOP_BRIDGE_URL?.trim(); + const remoteToken = process.env.EVAOS_DESKTOP_BRIDGE_TOKEN?.trim(); + if (isCustomerMacRemoteCommand(command)) { + const missing = missingRemoteConnectorMaterial(remoteURL, remoteToken); + if (missing.length > 0) { + return customerMacConnectorMaterialMissing(command, missing); + } + } + if (remoteURL) { + return runRemoteBridge(remoteURL, command, params); + } + + return withLocalMessagePayload(command, params, async (safeParams) => { + const bin = process.env.EVAOS_DESKTOP_BRIDGE_BIN || 'evaos-desktop-bridge'; + const argv = buildBridgeArgv(command, safeParams); + try { + const { stdout } = await execFileAsync(bin, argv, { + shell: false, + timeout: timeoutForCommand(command), + maxBuffer: 8 * 1024 * 1024, + }); + return materializeVisualEvidence(command, JSON.parse(stdout)); + } catch (error: unknown) { + const err = error as { stdout?: string; message?: string }; + if (err.stdout) { + try { + return JSON.parse(err.stdout); + } catch { + // Fall through to structured wrapper below. + } + } + return { + ok: false, + errors: [ + { + code: 'bridge_cli_failed', + message: safeBridgeErrorMessage(command, err.message), + guidance: 'Install evaos-desktop-bridge locally and set EVAOS_DESKTOP_BRIDGE_BIN if it is not on PATH.', + }, + ], + }; + } + }); +} + +function isCustomerMacRemoteCommand(command: BridgeCommandKey): boolean { + return CUSTOMER_MAC_REMOTE_COMMANDS.has(command); +} + +function missingRemoteConnectorMaterial(remoteURL: string | undefined, remoteToken: string | undefined): string[] { + const missing: string[] = []; + if (!remoteURL) { + missing.push('EVAOS_DESKTOP_BRIDGE_URL'); + } + if (!remoteToken) { + missing.push('EVAOS_DESKTOP_BRIDGE_TOKEN'); + } + return missing; +} + +function customerMacConnectorMaterialMissing(command: BridgeCommandKey, missing: string[]): unknown { + return { + ok: false, + errors: [ + { + code: 'mac_connector_material_missing', + message: 'Customer Mac tools require broker/session-provided remote connector material.', + guidance: + 'Launch the runtime from Workbench for the selected customer so the brokered connector contract is present before using customer Mac tools.', + details: { + command, + missing, + required: ['EVAOS_DESKTOP_BRIDGE_URL', 'EVAOS_DESKTOP_BRIDGE_TOKEN'], + route: '/v1/commands', + env_file: displayDesktopBridgeEnvPath(desktopBridgeEnvPath()), + local_fallback: false, + }, + }, + ], + }; +} + +async function withLocalMessagePayload( + command: BridgeCommandKey, + params: BridgeParams, + callback: (params: BridgeParams) => Promise +): Promise { + const isCodexMessage = command === 'codexSendVisibleMessage' && typeof params.message === 'string'; + const isDesktopSetValue = command === 'desktopSetValue' && typeof params.value === 'string'; + if (!isCodexMessage && !isDesktopSetValue) { + return callback(params); + } + const dir = fsCompat.mkdtempSync( + path.join( + process.env.TMPDIR || '/tmp', + isCodexMessage ? 'evaos-codex-visible-message-' : 'evaos-desktop-set-value-' + ) + ); + const payloadFile = path.join(dir, isCodexMessage ? 'message.txt' : 'value.txt'); + try { + await writeFile(payloadFile, isCodexMessage ? String(params.message) : String(params.value), { + encoding: 'utf8', + mode: 0o600, + }); + try { + fsCompat.chmodSync(payloadFile, 0o600); + } catch { + // Best-effort on platforms that do not support chmod. + } + const safeParams = isCodexMessage + ? { ...params, message: undefined, message_file: payloadFile } + : { ...params, value: undefined, value_file: payloadFile }; + return await callback(safeParams); + } finally { + try { + fsCompat.rmSync(dir, { recursive: true, force: true }); + } catch { + // Best-effort cleanup; the file is 0600 and contains only the approved message. + } + } +} + +function safeBridgeErrorMessage(command: BridgeCommandKey, message?: string): string { + if (command === 'codexSendVisibleMessage') { + return 'evaos-desktop-bridge guarded Codex visible message command failed'; + } + return message || 'evaos-desktop-bridge command failed'; +} + +async function providerProfilesPayload(): Promise { + const brokerProfile = await providerAgentDiscoveryPayload('openclaw'); + if (brokerProfile) { + return brokerProfile.profilesPayload; + } + + const profilesPayload = readJSONEnv('EVAOS_PROVIDER_PROFILES_JSON'); + const providerProfiles = Array.isArray(profilesPayload) + ? profilesPayload + : isRecord(profilesPayload) && Array.isArray(profilesPayload.provider_profiles) + ? profilesPayload.provider_profiles + : []; + const grantsPayload = readJSONEnv('EVAOS_PROVIDER_GRANTS_JSON'); + return redactConnectorSecrets({ + ok: true, + data: { + customer_id: process.env.EVAOS_CUSTOMER_ID || null, + provider_profiles: providerProfiles, + provider_grants: grantsPayload || null, + active_provider_key: + process.env.EVAOS_ACTIVE_PROVIDER_KEY || + (isRecord(profilesPayload) && typeof profilesPayload.active_provider_key === 'string' + ? profilesPayload.active_provider_key + : null), + raw_secrets_available: false, + raw_secrets_stored_in_workbench: false, + }, + warnings: providerProfiles.length === 0 ? ['Provider profiles are not configured on this VM yet.'] : [], + }); +} + +async function providerCompleteAuthPayload(params: BridgeParams): Promise { + const endpoint = providerDiscoveryEndpoint(); + const customerID = process.env.EVAOS_CUSTOMER_ID?.trim(); + const proofSecret = + process.env.EVAOS_PROVIDER_AUTH_PROOF_SECRET?.trim() || process.env.EVAOS_PROVIDER_PROOF_SECRET?.trim(); + const identity = requiredProviderIdentity(params.identity); + const scopes = normalizeProviderScopes(params.scopes); + const expiresAt = normalizeProviderProofExpiry(params.expires_at); + const serverSecretRef = normalizeServerSecretRef(params.server_secret_ref, customerID); + + if (!endpoint || !customerID || !proofSecret || !identity || !serverSecretRef) { + return { + ok: false, + errors: [ + { + code: 'provider_auth_proof_not_configured', + message: + 'Provider auth proof completion requires broker endpoint, customer id, proof secret, identity, and server secret reference.', + guidance: + 'Set EVAOS_PROVIDER_DISCOVERY_URL, EVAOS_CUSTOMER_ID, EVAOS_PROVIDER_AUTH_PROOF_SECRET, EVAOS_PROVIDER_AUTH_IDENTITY, and EVAOS_PROVIDER_SERVER_SECRET_REF on the VM.', + }, + ], + }; + } + + const providerKey = 'openai_codex'; + const proofID = providerProofID(); + const proofPayload = { + customer_id: customerID, + provider_key: providerKey, + purpose: 'provider_auth_complete', + agent_runtime: 'openclaw', + proof_id: proofID, + identity, + scopes, + expires_at: expiresAt, + server_secret_ref: serverSecretRef, + }; + const signature = createHmac('sha256', proofSecret).update(JSON.stringify(proofPayload)).digest('hex'); + const requestBody = { + action: 'provider_auth_complete', + customer_id: customerID, + provider_key: providerKey, + agent_runtime: 'openclaw', + provider_auth_proof: { + purpose: 'provider_auth_complete', + agent_runtime: 'openclaw', + proof_id: proofID, + identity, + scopes, + expires_at: expiresAt, + server_secret_ref: serverSecretRef, + signature, + }, + }; + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10_000); + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Evaos-Provider-Proof': 'signed-v1', + }, + body: JSON.stringify(requestBody), + signal: controller.signal, + }); + const payload = await response.json().catch(() => null); + if (!response.ok) { + return { + ok: false, + errors: [ + { + code: 'provider_auth_complete_failed', + message: + isRecord(payload) && typeof payload.error === 'string' + ? payload.error + : `Provider auth completion failed with HTTP ${response.status}.`, + }, + ], + }; + } + const cachedGrant = await cacheProviderGrantFromBroker('openclaw', providerKey, payload); + return redactConnectorSecrets({ + ok: true, + data: { + connected: isRecord(payload) ? payload.connected === true || payload.status === 'connected' : true, + provider_key: providerKey, + grant_cached: cachedGrant, + response: payload, + raw_provider_token_returned: false, + }, + }); + } catch (error: unknown) { + return { + ok: false, + errors: [ + { + code: 'provider_auth_complete_unreachable', + message: error instanceof Error ? error.message : 'Provider auth completion broker was unreachable.', + }, + ], + }; + } finally { + clearTimeout(timeout); + } +} + +async function providerActiveProfilePayload(): Promise { + const brokerProfile = await providerAgentDiscoveryPayload('openclaw'); + if (brokerProfile) { + return brokerProfile.activePayload; + } + + const profiles = (await providerProfilesPayload()) as Record; + const data = isRecord(profiles.data) ? profiles.data : {}; + const providerProfiles = Array.isArray(data.provider_profiles) ? data.provider_profiles : []; + const activeProviderKey = typeof data.active_provider_key === 'string' ? data.active_provider_key : null; + const activeProfile = + providerProfiles.find((profile) => isRecord(profile) && profile.provider_key === activeProviderKey) ?? null; + const providerGrants = isRecord(data.provider_grants) ? data.provider_grants : {}; + const openClawGrant = isRecord(providerGrants.openclaw) ? providerGrants.openclaw : null; + const hasConnectionProof = + isRecord(activeProfile) && + activeProfile.status === 'connected' && + typeof activeProfile.last_validated_at === 'string' && + Boolean(openClawGrant && typeof openClawGrant.grant_handle === 'string'); + return redactConnectorSecrets({ + ok: true, + data: { + customer_id: process.env.EVAOS_CUSTOMER_ID || null, + active_provider_key: activeProviderKey, + active_profile: activeProfile, + needs_reauth: !hasConnectionProof, + raw_secrets_available: false, + }, + warnings: hasConnectionProof + ? [] + : [ + 'No verified active provider grant is available. Ask the customer to connect or re-auth the provider in evaOS Workbench.', + ], + }); +} + +function requiredProviderIdentity(value: unknown): string | null { + const fromParam = typeof value === 'string' ? value.trim() : ''; + if (fromParam) return fromParam; + const fromEnv = process.env.EVAOS_PROVIDER_AUTH_IDENTITY?.trim() || process.env.EVAOS_PROVIDER_IDENTITY?.trim() || ''; + return fromEnv || null; +} + +function normalizeProviderScopes(value: unknown): string[] { + const scopes = Array.isArray(value) ? value.map((scope) => String(scope).trim()).filter(Boolean) : []; + if (scopes.length > 0) return [...new Set(scopes)]; + const fromEnv = + process.env.EVAOS_PROVIDER_AUTH_SCOPES?.split(',') + .map((scope) => scope.trim()) + .filter(Boolean) ?? []; + return fromEnv.length > 0 ? [...new Set(fromEnv)] : ['codex', 'offline_access']; +} + +function normalizeProviderProofExpiry(value: unknown): string { + const candidate = typeof value === 'string' ? value.trim() : process.env.EVAOS_PROVIDER_AUTH_EXPIRES_AT?.trim() || ''; + if (candidate && Number.isFinite(Date.parse(candidate)) && Date.parse(candidate) > Date.now()) { + return new Date(candidate).toISOString(); + } + return new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); +} + +function normalizeServerSecretRef(value: unknown, customerID: string | undefined): string | null { + const candidate = + typeof value === 'string' ? value.trim() : process.env.EVAOS_PROVIDER_SERVER_SECRET_REF?.trim() || ''; + if (candidate.startsWith('provider://')) return candidate; + if (!customerID) return null; + return `provider://openai_codex/${encodeURIComponent(customerID)}/openclaw`; +} + +function providerProofID(): string { + return `eap_${randomUUID().replace(/-/g, '')}`; +} + +type ProviderDiscoveryPayload = { + profilesPayload: unknown; + activePayload: unknown; +}; + +async function providerAgentDiscoveryPayload( + agentRuntime: 'openclaw' | 'hermes' +): Promise { + const endpoint = providerDiscoveryEndpoint(); + const customerID = process.env.EVAOS_CUSTOMER_ID?.trim(); + const grantHandle = providerGrantHandleFor(agentRuntime); + if (!endpoint || !customerID || !grantHandle) { + return null; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10_000); + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-Evaos-Provider-Grant': grantHandle, + }, + body: JSON.stringify({ + action: 'provider_agent_discovery', + customer_id: customerID, + agent_runtime: agentRuntime, + }), + signal: controller.signal, + }); + const payload = await response.json().catch(() => null); + if (!response.ok || !isRecord(payload)) { + return null; + } + const activeProfile = isRecord(payload.provider_profile) ? payload.provider_profile : null; + const activeProviderKey = typeof payload.active_provider_key === 'string' ? payload.active_provider_key : null; + const providerProfiles = activeProfile ? [activeProfile] : []; + const grantStatus = typeof payload.grant_status === 'string' ? payload.grant_status : 'unknown'; + const grantExpiresAt = typeof payload.grant_expires_at === 'string' ? payload.grant_expires_at : null; + + return { + profilesPayload: redactConnectorSecrets({ + ok: true, + data: { + customer_id: customerID, + provider_profiles: providerProfiles, + provider_grants: { + [agentRuntime]: { + grant_handle: grantHandle, + status: grantStatus, + expires_at: grantExpiresAt, + }, + }, + active_provider_key: activeProviderKey, + raw_secrets_available: false, + raw_secrets_stored_in_workbench: false, + raw_provider_token_returned: false, + source: 'broker', + }, + warnings: providerProfiles.length === 0 ? ['No active provider profile is available from the broker.'] : [], + }), + activePayload: redactConnectorSecrets({ + ok: true, + data: { + customer_id: customerID, + active_provider_key: activeProviderKey, + active_profile: activeProfile, + provider_identity: typeof payload.provider_identity === 'string' ? payload.provider_identity : null, + provider_scopes: Array.isArray(payload.provider_scopes) ? payload.provider_scopes.map(String) : [], + grant_status: grantStatus, + grant_expires_at: grantExpiresAt, + needs_reauth: payload.reauth_needed === true || !activeProfile, + raw_secrets_available: false, + raw_provider_token_returned: false, + source: 'broker', + }, + warnings: activeProfile + ? [] + : [ + 'No verified active provider grant is available. Ask the customer to connect or re-auth the provider in evaOS Workbench.', + ], + }), + }; + } catch { + return null; + } finally { + clearTimeout(timeout); + } +} + +function providerDiscoveryEndpoint(): string | null { + const explicit = process.env.EVAOS_PROVIDER_DISCOVERY_URL?.trim(); + if (explicit) return explicit; + const broker = process.env.EVAOS_DESKTOP_RUNTIME_SESSION_URL?.trim(); + if (broker) return broker; + return null; +} + +function desktopBridgeEnvPath(): string | null { + const explicit = process.env.EVAOS_DESKTOP_BRIDGE_ENV_FILE?.trim(); + if (explicit) return explicit; + const home = process.env.HOME?.trim(); + if (!home) return null; + return path.join(home, '.openclaw', 'evaos-desktop-bridge.env'); +} + +function displayDesktopBridgeEnvPath(envPath: string | null): string | null { + if (!envPath) return null; + const home = process.env.HOME?.trim(); + return home && envPath.startsWith(home) ? envPath.replace(home, '~') : envPath; +} + +function parseShellEnvValue(value: string): string { + const trimmed = value.trim(); + if (trimmed.startsWith("'") && trimmed.endsWith("'")) { + return trimmed.slice(1, -1).replace(/'"'"'/g, "'"); + } + if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + } + return trimmed; +} + +function loadDesktopBridgeEnvFile(): void { + if (process.env.EVAOS_DESKTOP_BRIDGE_URL && process.env.EVAOS_DESKTOP_BRIDGE_TOKEN) return; + const envPath = desktopBridgeEnvPath(); + if (!envPath) return; + let text: string; + try { + text = fs.readFileSync(envPath, 'utf8'); + } catch { + return; + } + for (const line of text.split(/\r?\n/)) { + const match = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)=(.*)$/); + if (!match) continue; + const [, key, rawValue] = match; + if (!key.startsWith('EVAOS_')) continue; + if (process.env[key]) continue; + process.env[key] = parseShellEnvValue(rawValue); + } +} + +function providerGrantHandleFor(agentRuntime: 'openclaw' | 'hermes'): string | null { + const direct = process.env.EVAOS_PROVIDER_GRANT_HANDLE?.trim(); + if (direct) return direct; + const grantsPayload = readJSONEnv('EVAOS_PROVIDER_GRANTS_JSON'); + const runtimeGrant = isRecord(grantsPayload) ? grantsPayload[agentRuntime] : null; + if ( + isRecord(runtimeGrant) && + typeof runtimeGrant.grant_handle === 'string' && + runtimeGrant.grant_handle.trim() !== '' + ) { + return runtimeGrant.grant_handle.trim(); + } + const cachePayload = readProviderGrantCache(); + const cachedRuntimeGrant = isRecord(cachePayload) ? cachePayload[agentRuntime] : null; + if ( + isRecord(cachedRuntimeGrant) && + typeof cachedRuntimeGrant.grant_handle === 'string' && + cachedRuntimeGrant.grant_handle.trim() !== '' + ) { + return cachedRuntimeGrant.grant_handle.trim(); + } + return null; +} + +async function cacheProviderGrantFromBroker( + agentRuntime: 'openclaw' | 'hermes', + providerKey: string, + payload: unknown +): Promise { + if (!isRecord(payload) || !isRecord(payload.agent_grant)) return false; + const grant = payload.agent_grant; + if ( + grant.provider_key !== providerKey || + grant.agent_runtime !== agentRuntime || + typeof grant.grant_handle !== 'string' || + !grant.grant_handle.trim() + ) { + return false; + } + const cachePath = providerGrantCachePath(); + if (!cachePath) return false; + const existing = isRecord(readProviderGrantCache()) ? (readProviderGrantCache() as Record) : {}; + const next = { + ...existing, + [agentRuntime]: { + provider_key: providerKey, + agent_runtime: agentRuntime, + grant_handle: grant.grant_handle.trim(), + expires_at: typeof grant.expires_at === 'string' ? grant.expires_at : null, + cached_at: new Date().toISOString(), + }, + }; + try { + await mkdir(path.dirname(cachePath), { recursive: true }); + await writeFile(cachePath, JSON.stringify(next, null, 2), { encoding: 'utf8', mode: 0o600 }); + return true; + } catch { + return false; + } +} + +function readProviderGrantCache(): unknown { + const cachePath = providerGrantCachePath(); + if (!cachePath) return null; + try { + return JSON.parse(fs.readFileSync(cachePath, 'utf8')); + } catch { + return null; + } +} + +function providerGrantCachePath(): string | null { + const explicit = process.env.EVAOS_PROVIDER_GRANT_CACHE_FILE?.trim(); + if (explicit) return explicit; + const home = process.env.HOME?.trim(); + if (!home) return null; + return path.join(home, '.openclaw', 'evaos-provider-grants.json'); +} + +function sharedBrowserGuidancePayload(): unknown { + const status = readJSONEnv('EVAOS_SHARED_BROWSER_STATUS_JSON'); + return redactConnectorSecrets({ + ok: true, + data: { + schema_version: 'evaos.browser_status.v1', + customer_id: process.env.EVAOS_CUSTOMER_ID || null, + business_browser_preferred_for_cloud_web_tasks: true, + shared_browser_preferred_for_cloud_web_tasks: true, + instructions: + "Use Business Browser for cloud web tasks that need a persistent VM browser, user auth/CAPTCHA handoff, or human-visible browsing state. Use local Mac browser tools only when the task explicitly needs the customer's Mac browser.", + status: status || null, + }, + warnings: status ? [] : ['Business Browser live status is not configured in this VM environment yet.'], + }); +} + +function readJSONEnv(name: string): unknown { + const raw = process.env[name]; + if (!raw) return null; + try { + return JSON.parse(raw); + } catch { + return null; + } +} + +async function runRemoteBridge(remoteURL: string, command: BridgeCommandKey, params: BridgeParams): Promise { + const endpoint = new URL('/v1/commands', remoteURL); + const headers: Record = { + 'Content-Type': 'application/json', + }; + const token = process.env.EVAOS_DESKTOP_BRIDGE_TOKEN?.trim(); + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + try { + return await postRemoteBridgeCommand(endpoint, headers, remoteURL, command, params, timeoutForCommand(command)); + } catch (error: unknown) { + if (command === 'customerMacControlStart' && isAbortLikeError(error)) { + const reconciled = await reconcileControlStartAfterAbort(endpoint, headers, remoteURL, params); + if (reconciled) { + return reconciled; + } + } + const err = error as { message?: string }; + return { + ok: false, + errors: [ + { + code: 'bridge_connector_failed', + message: err.message || 'evaos-desktop-bridge connector request failed', + guidance: 'Verify Headscale reachability, EVAOS_DESKTOP_BRIDGE_URL, and EVAOS_DESKTOP_BRIDGE_TOKEN.', + }, + ], + }; + } +} + +async function postRemoteBridgeCommand( + endpoint: URL, + headers: Record, + remoteURL: string, + command: BridgeCommandKey, + params: BridgeParams, + timeoutMs: number +): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(endpoint, { + method: 'POST', + headers, + body: JSON.stringify({ command, params }), + signal: controller.signal, + }); + const text = await response.text(); + try { + return await materializeVisualEvidence(command, JSON.parse(text), remoteURL, headers.Authorization); + } catch { + return { + ok: false, + errors: [ + { + code: 'bridge_connector_invalid_response', + message: text || `Connector returned HTTP ${response.status}`, + guidance: 'Check the paired Mac connector endpoint and token.', + }, + ], + }; + } + } finally { + clearTimeout(timeout); + } +} + +async function reconcileControlStartAfterAbort( + endpoint: URL, + headers: Record, + remoteURL: string, + params: BridgeParams +): Promise { + try { + const status = await postRemoteBridgeCommand(endpoint, headers, remoteURL, 'customerMacControlStatus', {}, 15_000); + if (!isControlSessionActive(status, params)) { + return null; + } + const payload: Record = isRecord(status) ? { ...status } : { ok: true }; + const data = isRecord(payload.data) ? { ...payload.data } : {}; + payload.ok = true; + payload.data = { + ...data, + control_start_reconciled: true, + }; + payload.warnings = [ + ...(Array.isArray(payload.warnings) ? payload.warnings : []), + { + code: 'control_start_response_reconciled_after_abort', + message: + 'Mac control start timed out after the connector activated the control session; status was reconciled from the paired Mac.', + guidance: 'Continue with desktop_control_status and desktop_see before running live Mac-control actions.', + }, + ]; + return payload; + } catch { + return null; + } +} + +function isAbortLikeError(error: unknown): boolean { + const err = error as { name?: string; message?: string }; + return err.name === 'AbortError' || /abort/i.test(err.message || ''); +} + +function isControlSessionActive(status: unknown, params: BridgeParams): boolean { + if (!isRecord(status) || status.ok !== true || !isRecord(status.data)) { + return false; + } + const data = status.data; + const session = isRecord(data.session) ? data.session : {}; + const active = data.active === true || session.active === true; + const killSwitch = + data.kill_switch === true || + data.killSwitch === true || + session.kill_switch === true || + session.killSwitch === true; + const mode = controlModeFromStatus(data.mode) ?? controlModeFromStatus(session.mode); + return active && !killSwitch && mode === requestedControlMode(params.mode); +} + +function requestedControlMode(mode: unknown): 'full_access' | 'ask_permission' { + return controlModeFromStatus(mode) ?? 'full_access'; +} + +function controlModeFromStatus(mode: unknown): 'full_access' | 'ask_permission' | undefined { + if (typeof mode !== 'string') { + return undefined; + } + const normalized = mode.trim().toLowerCase().replace(/-/g, '_'); + if (normalized === 'ask_permission') { + return 'ask_permission'; + } + if (normalized === 'full_access') { + return 'full_access'; + } + return undefined; +} + +function timeoutForCommand(command: BridgeCommandKey): number { + if (command === 'codexLiveStatus') { + return 35_000; + } + if (command === 'customerMacControlStart') { + return 30_000; + } + if ( + command === 'desktopSee' || + command === 'iphoneSee' || + command === 'customerMacSnapshot' || + command === 'customerMacAxTree' + ) { + return 60_000; + } + if ( + command === 'desktopDrag' || + command === 'desktopScroll' || + command === 'iphoneSwipe' || + command === 'customerMacIphoneMirroringScroll' || + command === 'customerMacIphoneMirroringSwipeLeft' || + command === 'customerMacIphoneMirroringSwipeRight' || + command === 'customerMacIphoneMirroringSwipeUp' || + command === 'customerMacIphoneMirroringSwipeDown' + ) { + return 20_000; + } + if ( + command === 'desktopMenu' || + command === 'desktopWindow' || + command === 'desktopBrowserAction' || + command === 'desktopFocusApp' || + command === 'customerMacIphoneMirroringOpenApp' + ) { + return 20_000; + } + if (command === 'desktopClick' || command === 'iphoneTap') { + return 30_000; + } + if ( + command === 'desktopSetValue' || + command === 'desktopType' || + command === 'desktopHotkey' || + command === 'iphoneType' || + command === 'customerMacIphoneMirroringTypeApprovedText' || + command === 'customerMacIphoneMirroringSendApprovedMessage' + ) { + return 15_000; + } + return 10_000; +} + +async function materializeVisualEvidence( + command: BridgeCommandKey, + payload: unknown, + remoteURL?: string, + authHeader?: string +): Promise { + if (!isRecord(payload)) { + return payload; + } + const data = payload.data; + if (!isRecord(data)) { + return payload; + } + const image = findVisualImage(data); + if (!image) { + return payload; + } + let imageBytes: unknown; + let materializedFrom = 'inline'; + if (typeof image.bytes_base64 === 'string') { + imageBytes = Buffer.from(image.bytes_base64, 'base64'); + } else if (typeof image.artifact_url === 'string' && remoteURL) { + const fetched = await fetchVisualArtifact(remoteURL, image.artifact_url, authHeader); + if (!fetched.ok) { + const warnings = Array.isArray(payload.warnings) ? payload.warnings : []; + warnings.push(fetched.warning); + payload.warnings = warnings; + return payload; + } + imageBytes = fetched.bytes; + materializedFrom = 'connector_artifact'; + } else { + return payload; + } + const snapshotId = + (typeof data.snapshot_id === 'string' && data.snapshot_id) || + (isRecord(data.screenshot) && typeof data.screenshot.snapshot_id === 'string' && data.screenshot.snapshot_id) || + (typeof image.artifact_id === 'string' && image.artifact_id) || + (typeof payload.audit_id === 'string' && payload.audit_id) || + command; + const artifactDir = process.env.EVAOS_DESKTOP_BRIDGE_ARTIFACT_DIR || '/root/agent-files/downloads/desktop-bridge'; + const safeName = snapshotId.replace(/[^A-Za-z0-9_.-]+/g, '-').slice(0, 160); + const artifactPath = path.join(artifactDir, `${safeName}.png`); + try { + await mkdir(artifactDir, { recursive: true }); + await writeFile(artifactPath, imageBytes); + image.vm_artifact_path = artifactPath; + image.bytes_base64_present = true; + image.vm_artifact_source = materializedFrom; + delete image.bytes_base64; + data.vm_visual_artifact_path = artifactPath; + } catch (error: unknown) { + const warnings = Array.isArray(payload.warnings) ? payload.warnings : []; + warnings.push(`Unable to write VM visual artifact: ${(error as { message?: string }).message || 'unknown error'}`); + payload.warnings = warnings; + } + return payload; +} + +function findVisualImage(data: Record): Record | undefined { + const direct = data.image; + if (isRecord(direct) && hasImageMaterial(direct)) { + return direct; + } + const screenshot = data.screenshot; + if (isRecord(screenshot)) { + const screenshotRecord: Record = screenshot; + if (hasImageMaterial(screenshotRecord)) { + return screenshotRecord; + } + const screenshotImage = screenshotRecord.screenshot; + if (isRecord(screenshotImage) && hasImageMaterial(screenshotImage)) { + return screenshotImage; + } + const image = screenshotRecord.image; + if (isRecord(image) && hasImageMaterial(image)) { + return image; + } + } + return undefined; +} + +function hasImageMaterial(value: Record): boolean { + return typeof value.bytes_base64 === 'string' || typeof value.artifact_url === 'string'; +} + +async function fetchVisualArtifact( + remoteURL: string, + artifactURL: string, + authHeader?: string +): Promise<{ ok: true; bytes: unknown } | { ok: false; warning: string }> { + let endpoint: URL; + try { + endpoint = new URL(artifactURL, remoteURL); + } catch { + return { ok: false, warning: 'Unable to fetch VM visual artifact: connector returned an invalid artifact URL' }; + } + const base = new URL(remoteURL); + if (endpoint.origin !== base.origin || !endpoint.pathname.startsWith('/v1/artifacts/')) { + return { + ok: false, + warning: 'Unable to fetch VM visual artifact: connector artifact URL was outside the paired connector', + }; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 15_000); + try { + const headers: Record = {}; + if (authHeader) { + headers.Authorization = authHeader; + } + const response = await fetch(endpoint, { + method: 'GET', + headers, + signal: controller.signal, + }); + if (!response.ok) { + return { ok: false, warning: `Unable to fetch VM visual artifact: connector returned HTTP ${response.status}` }; + } + return { ok: true, bytes: Buffer.from(await response.arrayBuffer()) }; + } catch (error: unknown) { + return { + ok: false, + warning: `Unable to fetch VM visual artifact: ${(error as { message?: string }).message || 'unknown error'}`, + }; + } finally { + clearTimeout(timeout); + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function redactConnectorSecrets(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map((item) => redactConnectorSecrets(item)); + } + if (!value || typeof value !== 'object') { + return value; + } + const redacted: Record = {}; + for (const [key, nestedValue] of Object.entries(value)) { + if (isSecretMetadataKey(key)) { + redacted[key] = '[redacted]'; + } else { + redacted[key] = redactConnectorSecrets(nestedValue); + } + } + return redacted; +} + +function isSecretMetadataKey(key: string): boolean { + const compactKey = key.toLowerCase().replace(/[^a-z0-9]/g, ''); + return ( + compactKey === 'token' || + compactKey.endsWith('token') || + compactKey === 'authorization' || + compactKey.endsWith('authorization') || + compactKey === 'header' || + compactKey === 'headers' || + compactKey.endsWith('header') || + compactKey.endsWith('headers') || + compactKey === 'password' || + compactKey.endsWith('password') || + compactKey === 'credential' || + compactKey === 'credentials' || + compactKey.endsWith('credential') || + compactKey.endsWith('credentials') || + compactKey === 'secret' || + compactKey.endsWith('secret') || + compactKey.includes('apikey') || + compactKey.includes('clientsecret') || + compactKey.includes('privatekey') || + compactKey.includes('secretkey') || + compactKey.includes('accesskey') + ); +} + +function clampInt(value: unknown, fallback: number, min: number, max: number): number { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return fallback; + } + return Math.min(max, Math.max(min, Math.trunc(value))); +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/firewall.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/firewall.ts new file mode 100644 index 0000000000..181c2a4b2b --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/firewall.ts @@ -0,0 +1,203 @@ +type HookEvent = { + toolName?: string; + name?: string; + args?: unknown; + input?: unknown; + params?: unknown; + parameters?: unknown; +}; + +type HookDecision = void | { + block?: boolean; + blockReason?: string; + requireApproval?: { + title: string; + description: string; + severity?: 'info' | 'warning' | 'critical'; + timeoutBehavior?: 'allow' | 'deny'; + allowedDecisions?: Array<'allow-once' | 'allow-always' | 'deny'>; + }; +}; + +const SAFE_TOOL_PREFIXES = ['desktop_bridge_', 'customer_mac_', 'desktop_', 'iphone_', 'evaos_']; +const FULL_ACCESS_TOOL_PREFIXES = ['desktop_', 'iphone_', 'customer_mac_iphone_mirroring_']; +const APPROVAL_GATED_TOOL_PREFIXES = [ + 'desktop_bridge_codex_select_thread', + 'desktop_bridge_codex_continue_thread', + 'desktop_bridge_codex_send_visible_message', + 'customer_mac_app_focus', + 'customer_mac_local_site_', +]; + +const IPHONE_GESTURE_TOOL_NAMES = new Set([ + 'customer_mac_iphone_mirroring_swipe_left', + 'customer_mac_iphone_mirroring_swipe_right', + 'customer_mac_iphone_mirroring_swipe_up', + 'customer_mac_iphone_mirroring_swipe_down', +]); + +const IPHONE_GESTURE_ALLOWED_MATCHES = new Set(['swipe']); +const FULL_ACCESS_ALLOWED_MATCHES = new Set([ + 'generic coordinates', + 'coordinate', + 'mouseDown', + 'mouseUp', + 'drag', + 'swipe', + 'typewrite', + 'send_message', + 'submit_prompt', + 'messages', + 'call', + 'purchase', + 'camera', + 'microphone', +]); + +const DANGEROUS_TOOL_NAMES = [ + 'exec', + 'shell', + 'bash', + 'terminal', + 'computer', + 'computer_use', + 'run_command', + 'write', + 'edit', +]; + +const FORBIDDEN_ARGUMENT_PATTERNS = [ + 'osascript', + 'screencapture', + 'cliclick', + 'pyautogui', + 'pynput', + 'AXUIElement', + 'AXUIElementPerformAction', + 'AXUIElementSetAttributeValue', + 'AXPress', + 'AXSetValue', + 'AXShowMenu', + 'AXSelectedText', + 'System Events', + 'Codex.app', + 'open -a Codex', + 'codex app-server', + 'app-server', + 'internal mutation rpc', + 'turn/start', + 'turn/steer', + 'turn/interrupt', + 'thread/inject_items', + 'thread/start', + 'thread/resume', + 'thread/fork', + 'thread/rollback', + 'thread/compact/start', + 'command/exec', + 'fs/writeFile', + 'fs/remove', + 'config/value/write', + 'config/batchWrite', + 'plugin/install', + 'plugin/uninstall', + 'remoteControl/enable', + 'remoteControl/disable', + 'remoteControl/approve', + 'remoteControl/deny', + 'session.db', + 'state.db', + 'sqlite', + '.codex', + 'auth.json', + 'token', + 'Authorization', + 'Bearer ', + 'send_message', + 'submit_prompt', + 'typewrite', + 'generic coordinates', + 'coordinate', + 'mouseDown', + 'mouseUp', + 'drag', + 'swipe', + 'Screen Sharing enable', + 'Remote Management enable', + 'kickstart -activate', + 'messages', + 'call', + 'purchase', + 'camera', + 'microphone', +]; + +export function desktopBridgeFirewall(event: HookEvent): HookDecision { + const toolName = String(event.toolName || event.name || ''); + const haystack = JSON.stringify({ + toolName, + args: firewallPayload(toolName, event.args), + input: firewallPayload(toolName, event.input), + params: firewallPayload(toolName, event.params), + parameters: firewallPayload(toolName, event.parameters), + }).toLowerCase(); + const matchedPattern = FORBIDDEN_ARGUMENT_PATTERNS.find((pattern) => haystack.includes(pattern.toLowerCase())); + if (SAFE_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix))) { + const allowedFullAccessMatch = + matchedPattern !== undefined && + FULL_ACCESS_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix)) && + FULL_ACCESS_ALLOWED_MATCHES.has(matchedPattern); + const allowedSupportCanaryMatch = + matchedPattern !== undefined && + IPHONE_GESTURE_TOOL_NAMES.has(toolName) && + IPHONE_GESTURE_ALLOWED_MATCHES.has(matchedPattern); + if (matchedPattern && !allowedSupportCanaryMatch && !allowedFullAccessMatch) { + return { + block: true, + blockReason: `desktop-bridge firewall blocked ${toolName}: ${matchedPattern} must go through the connector's audited control contract.`, + }; + } + if (APPROVAL_GATED_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix))) { + const params = (event.args || event.input || event.params || event.parameters || {}) as Record; + if (params.dry_run !== false) { + return undefined; + } + return { + requireApproval: { + title: 'Approve customer Mac action', + description: `${toolName} is a live customer Mac named action. Approval is required and the bridge will audit the command.`, + severity: 'warning', + timeoutBehavior: 'deny', + allowedDecisions: ['allow-once', 'deny'], + }, + }; + } + return undefined; + } + + const suspiciousTool = DANGEROUS_TOOL_NAMES.some((name) => toolName.toLowerCase().includes(name)); + + if (suspiciousTool && matchedPattern) { + return { + block: true, + blockReason: `desktop-bridge firewall blocked ${toolName}: ${matchedPattern} is outside the read-only passive observer boundary.`, + }; + } + + return undefined; +} + +function firewallPayload(toolName: string, value: unknown): unknown { + if (toolName !== 'desktop_bridge_codex_send_visible_message' || !isRecord(value)) { + return value; + } + const clone = { ...value }; + if (typeof clone.message === 'string') { + clone.message = ''; + } + return clone; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts new file mode 100644 index 0000000000..81169cd80f --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts @@ -0,0 +1,575 @@ +import { createPublicKey, verify } from 'node:crypto'; + +export const MAC_CONTROL_RUNTIME_RECEIPT_PATH = '/api/v1/evaos/mac-control/runtime-receipt'; + +const CONTEXT_SCHEMA = 'evaos.mac_control_execution_context.v1'; +const CONTRACT_SCHEMA = 'evaos.mac_control_runtime_contract.v2'; +const CONNECTOR_REQUEST_SCHEMA = 'evaos.mac_control.canary_request.v1'; +const CONNECTOR_RESPONSE_SCHEMA = 'evaos.mac_control.runtime_receipt_envelope.v1'; +const RECEIPT_NAMESPACE = 'evaos-mac-control-receipt-v1'; +const CONTEXT_TTL_SECONDS = 60; +const CLOCK_SKEW_SECONDS = 5; +const MAX_REQUEST_BYTES = 4096; +const MAX_CONNECTOR_RESPONSE_BYTES = 65536; +const CONTEXT_FIELDS = [ + 'schema_version', + 'key_id', + 'runtime', + 'customer_id', + 'customer_vm_id', + 'binding_id', + 'binding_version', + 'issued_at', + 'expires_at', + 'context_id', +] as const; +const RESPONSE_FIELDS = ['schema', 'receiptBase64', 'signature', 'keyId', 'namespace'] as const; + +const HEADER = { + context: 'x-evaos-mac-control-execution-context', + contextSignature: 'x-evaos-mac-control-execution-context-signature', + contextKeyId: 'x-evaos-mac-control-execution-context-key-id', + contract: 'x-evaos-mac-control-contract', + customer: 'x-evaos-mac-control-customer', + grantState: 'x-evaos-mac-control-grant-state', + connectorUrl: 'x-evaos-desktop-bridge-url', + connectorToken: 'x-evaos-desktop-bridge-token', + connectorTokenLast4: 'x-evaos-desktop-bridge-token-last4', + bindingId: 'x-evaos-mac-control-binding-id', + bindingVersion: 'x-evaos-mac-control-binding-version', + bindingExpiresAt: 'x-evaos-mac-control-binding-expires-at', +} as const; + +type ByteBuffer = { + byteLength: number; + subarray(start?: number, end?: number): Uint8Array; + toString(encoding?: string): string; +}; + +type RequestLike = AsyncIterable & { + method?: string; + headers: Record; +}; + +type ResponseLike = { + statusCode: number; + setHeader(name: string, value: string): void; + end(body?: string): void; +}; + +type FetchResponseLike = { + ok: boolean; + status: number; + text(): Promise; +}; + +type FetchLike = ( + input: string, + init: { method: string; headers: Record; body: string; signal?: unknown } +) => Promise; + +type ExecutionContext = { + schema_version: string; + key_id: string; + runtime: string; + customer_id: string; + customer_vm_id: string; + binding_id: string; + binding_version: string; + issued_at: number; + expires_at: number; + context_id: string; +}; + +type RuntimeReceiptOptions = { + env?: Record; + fetchImpl?: FetchLike; + now?: () => number; + replayCache?: Map; +}; + +type VerifiedAuthority = { + context: ExecutionContext; + contextPayload: string; + contextSignature: string; + contextKeyId: string; + connectorUrl: string; + connectorToken: string; + bindingId: string; + bindingVersion: string; + bindingExpiresAt: string; +}; + +type PublicRequest = { + challenge: string; + runRef: string; +}; + +type ConnectorEnvelope = { + schema: string; + receiptBase64: string; + signature: string; + keyId: string; + namespace: string; +}; + +type PluginHttpApi = { + registerHttpRoute(route: { + path: string; + auth: 'gateway'; + match: 'exact'; + handler: (request: RequestLike, response: ResponseLike) => Promise; + }): void; +}; + +/** Register the one exact, gateway-authenticated Mac-control receipt route. */ +export function registerMacControlRuntimeReceiptRoute(api: PluginHttpApi): void { + api.registerHttpRoute({ + path: MAC_CONTROL_RUNTIME_RECEIPT_PATH, + auth: 'gateway', + match: 'exact', + handler: createMacControlRuntimeReceiptHandler(), + }); +} + +/** Build the receipt route handler; dependency overrides exist only for focused tests. */ +export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOptions = {}) { + const env = options.env ?? process.env; + const fetchImpl = options.fetchImpl ?? (globalThis.fetch as unknown as FetchLike); + const now = options.now ?? Date.now; + const replayCache = options.replayCache ?? new Map(); + + return async (request: RequestLike, response: ResponseLike): Promise => { + if ((request.method ?? 'GET').toUpperCase() !== 'POST') { + response.setHeader('Allow', 'POST'); + sendError(response, 405, 'method_not_allowed'); + return true; + } + + const publicRequest = await readPublicRequest(request); + if (!publicRequest.ok) { + sendError(response, publicRequest.status, 'invalid_request'); + return true; + } + + const authority = verifyAuthority(request.headers, env, now()); + if (!authority.ok) { + sendError(response, authority.status, authority.code); + return true; + } + + pruneReplayCache(replayCache, now()); + if (replayCache.has(authority.value.context.context_id)) { + sendError(response, 409, 'execution_context_replayed'); + return true; + } + replayCache.set(authority.value.context.context_id, authority.value.context.expires_at * 1000); + + const connectorBody = { + schema: CONNECTOR_REQUEST_SCHEMA, + challenge: publicRequest.value.challenge, + runRef: publicRequest.value.runRef, + executionContext: { + payload: authority.value.contextPayload, + signature: authority.value.contextSignature, + keyId: authority.value.contextKeyId, + }, + binding: { + bindingId: authority.value.bindingId, + bindingVersion: authority.value.bindingVersion, + bindingExpiresAt: authority.value.bindingExpiresAt, + }, + }; + + let connectorResponse: FetchResponseLike; + try { + connectorResponse = await fetchImpl(`${authority.value.connectorUrl}/v1/canary/mac-control`, { + method: 'POST', + headers: { + Authorization: `Bearer ${authority.value.connectorToken}`, + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify(connectorBody), + }); + } catch { + sendError(response, 502, 'connector_unavailable'); + return true; + } + + if (!connectorResponse.ok) { + sendError( + response, + connectorResponse.status >= 400 && connectorResponse.status < 500 ? 409 : 502, + 'connector_rejected_canary' + ); + return true; + } + + let rawConnectorResponse: string; + try { + rawConnectorResponse = await connectorResponse.text(); + } catch { + sendError(response, 502, 'connector_response_invalid'); + return true; + } + const sanitized = validateConnectorEnvelope(rawConnectorResponse, authority.value); + if (!sanitized.ok) { + sendError(response, 502, 'connector_response_invalid'); + return true; + } + + sendJson(response, 200, sanitized.value); + return true; + }; +} + +async function readPublicRequest( + request: RequestLike +): Promise<{ ok: true; value: PublicRequest } | { ok: false; status: number }> { + const chunks: ByteBuffer[] = []; + let total = 0; + try { + for await (const chunk of request) { + const buffer = Buffer.isBuffer(chunk) + ? (chunk as ByteBuffer) + : Buffer.from(typeof chunk === 'string' ? chunk : (chunk as Uint8Array)); + total += buffer.byteLength; + if (total > MAX_REQUEST_BYTES) { + return { ok: false, status: 413 }; + } + chunks.push(buffer); + } + } catch { + return { ok: false, status: 400 }; + } + + let body: unknown; + try { + body = JSON.parse(Buffer.concat(chunks).toString('utf8')); + } catch { + return { ok: false, status: 400 }; + } + if (!isRecord(body) || !hasExactKeys(body, ['challenge', 'runRef'])) { + return { ok: false, status: 400 }; + } + if ( + typeof body.challenge !== 'string' || + !/^[A-Za-z0-9_-]{32,128}$/.test(body.challenge) || + decodeBase64Url(body.challenge)?.byteLength === undefined || + typeof body.runRef !== 'string' || + !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(body.runRef) + ) { + return { ok: false, status: 400 }; + } + const challengeBytes = decodeBase64Url(body.challenge); + if (!challengeBytes || challengeBytes.byteLength < 24 || challengeBytes.byteLength > 96) { + return { ok: false, status: 400 }; + } + return { ok: true, value: { challenge: body.challenge, runRef: body.runRef } }; +} + +function verifyAuthority( + headers: RequestLike['headers'], + env: Record, + nowMs: number +): { ok: true; value: VerifiedAuthority } | { ok: false; status: number; code: string } { + const expectedKeyId = env.EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_KEY_ID; + const publicKeyBase64 = env.EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_PUBLIC_KEY_B64; + if (!expectedKeyId || !publicKeyBase64 || !validKeyId(expectedKeyId)) { + return { ok: false, status: 503, code: 'execution_context_verifier_unavailable' }; + } + const rawPublicKey = decodeCanonicalBase64(publicKeyBase64); + if (!rawPublicKey || rawPublicKey.byteLength !== 32) { + return { ok: false, status: 503, code: 'execution_context_verifier_unavailable' }; + } + + const contextPayload = readHeader(headers, HEADER.context); + const contextSignature = readHeader(headers, HEADER.contextSignature); + const contextKeyId = readHeader(headers, HEADER.contextKeyId); + if (!contextPayload || !contextSignature || !contextKeyId) { + return { ok: false, status: 401, code: 'execution_context_missing' }; + } + if (contextKeyId !== expectedKeyId) { + return { ok: false, status: 401, code: 'execution_context_key_unknown' }; + } + + const payloadBytes = decodeBase64Url(contextPayload); + const signatureBytes = decodeBase64Url(contextSignature); + if (!payloadBytes || !signatureBytes || signatureBytes.byteLength !== 64) { + return { ok: false, status: 401, code: 'execution_context_malformed' }; + } + try { + const spkiPrefix = Buffer.from('302a300506032b6570032100', 'hex'); + const publicKey = createPublicKey({ + key: Buffer.concat([spkiPrefix, rawPublicKey]), + format: 'der', + type: 'spki', + }); + if (!verify(null, payloadBytes, publicKey, signatureBytes)) { + return { ok: false, status: 401, code: 'execution_context_signature_invalid' }; + } + } catch { + return { ok: false, status: 401, code: 'execution_context_signature_invalid' }; + } + + let parsed: unknown; + try { + parsed = JSON.parse(payloadBytes.toString('utf8')); + } catch { + return { ok: false, status: 401, code: 'execution_context_malformed' }; + } + if (!isRecord(parsed) || !hasExactKeys(parsed, CONTEXT_FIELDS)) { + return { ok: false, status: 401, code: 'execution_context_malformed' }; + } + const context = parsed as ExecutionContext; + if ( + context.schema_version !== CONTEXT_SCHEMA || + context.key_id !== expectedKeyId || + context.key_id !== contextKeyId || + context.runtime !== 'openclaw' || + !validOpaqueId(context.customer_id, 160) || + !validOpaqueId(context.customer_vm_id, 160) || + !validOpaqueId(context.binding_id, 160) || + !validBindingVersion(context.binding_version) || + !Number.isInteger(context.issued_at) || + !Number.isInteger(context.expires_at) || + !validContextId(context.context_id) + ) { + return { ok: false, status: 401, code: 'execution_context_malformed' }; + } + const nowSeconds = Math.floor(nowMs / 1000); + if ( + context.issued_at > nowSeconds + CLOCK_SKEW_SECONDS || + context.issued_at < nowSeconds - CONTEXT_TTL_SECONDS || + context.expires_at <= nowSeconds || + context.expires_at <= context.issued_at || + context.expires_at > context.issued_at + CONTEXT_TTL_SECONDS + ) { + return { ok: false, status: 401, code: 'execution_context_expired' }; + } + + const contract = readHeader(headers, HEADER.contract); + const customer = readHeader(headers, HEADER.customer); + const grantState = readHeader(headers, HEADER.grantState); + const connectorUrlHeader = readHeader(headers, HEADER.connectorUrl); + const connectorToken = readHeader(headers, HEADER.connectorToken); + const connectorTokenLast4 = readHeader(headers, HEADER.connectorTokenLast4); + const bindingId = readHeader(headers, HEADER.bindingId); + const bindingVersion = readHeader(headers, HEADER.bindingVersion); + const bindingExpiresAt = readHeader(headers, HEADER.bindingExpiresAt); + const connectorUrl = connectorUrlHeader ? validConnectorUrl(connectorUrlHeader) : undefined; + const bindingExpiryMs = bindingExpiresAt ? Date.parse(bindingExpiresAt) : Number.NaN; + if ( + contract !== CONTRACT_SCHEMA || + grantState !== 'active' || + customer !== context.customer_id || + bindingId !== context.binding_id || + bindingVersion !== context.binding_version || + !bindingExpiresAt || + !Number.isFinite(bindingExpiryMs) || + context.expires_at * 1000 > bindingExpiryMs || + !connectorUrl || + !connectorToken || + connectorToken.length < 24 || + connectorToken.length > 512 || + connectorToken.trim() !== connectorToken || + !connectorTokenLast4 || + connectorTokenLast4 !== connectorToken.slice(-4) + ) { + return { ok: false, status: 401, code: 'execution_context_scope_mismatch' }; + } + + return { + ok: true, + value: { + context, + contextPayload, + contextSignature, + contextKeyId, + connectorUrl, + connectorToken, + bindingId, + bindingVersion, + bindingExpiresAt, + }, + }; +} + +function validateConnectorEnvelope( + raw: string, + authority: VerifiedAuthority +): { ok: true; value: ConnectorEnvelope } | { ok: false } { + if (Buffer.from(raw).byteLength > MAX_CONNECTOR_RESPONSE_BYTES) { + return { ok: false }; + } + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return { ok: false }; + } + if (!isRecord(parsed) || !hasExactKeys(parsed, RESPONSE_FIELDS)) { + return { ok: false }; + } + if ( + parsed.schema !== CONNECTOR_RESPONSE_SCHEMA || + parsed.namespace !== RECEIPT_NAMESPACE || + typeof parsed.receiptBase64 !== 'string' || + parsed.receiptBase64.length < 32 || + parsed.receiptBase64.length > 32768 || + typeof parsed.signature !== 'string' || + parsed.signature.length > 8192 || + typeof parsed.keyId !== 'string' || + !validKeyId(parsed.keyId) + ) { + return { ok: false }; + } + const receiptBytes = decodeBase64Url(parsed.receiptBase64); + if (!receiptBytes || receiptBytes.byteLength > 24576 || !validSshSignature(parsed.signature)) { + return { ok: false }; + } + const receiptText = receiptBytes.toString('utf8'); + const forbiddenValues = [ + authority.connectorToken, + authority.connectorUrl, + authority.context.customer_id, + authority.context.customer_vm_id, + authority.context.binding_id, + authority.contextPayload, + authority.contextSignature, + ]; + if ( + forbiddenValues.some((value) => value.length > 0 && receiptText.includes(value)) || + /https?:\/\//i.test(receiptText) || + /\b100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])(?:\.\d{1,3}){2}\b/.test(receiptText) || + /(?:\/tmp\/|\/private\/var\/folders\/)/i.test(receiptText) || + /"(?:token|secret|authorization|connector_url|customer_id|customer_vm_id|binding_id)"\s*:/i.test(receiptText) + ) { + return { ok: false }; + } + return { ok: true, value: parsed as ConnectorEnvelope }; +} + +function validConnectorUrl(value: string): string | undefined { + let parsed: URL; + try { + parsed = new URL(value); + } catch { + return undefined; + } + if ( + parsed.protocol !== 'http:' || + parsed.port !== '8765' || + parsed.username || + parsed.password || + parsed.pathname !== '/' || + parsed.search || + parsed.hash || + !isTailnetIpv4(parsed.hostname) + ) { + return undefined; + } + return parsed.origin; +} + +function isTailnetIpv4(value: string): boolean { + const parts = value.split('.'); + if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/.test(part) || Number(part) > 255)) { + return false; + } + return Number(parts[0]) === 100 && Number(parts[1]) >= 64 && Number(parts[1]) <= 127; +} + +function readHeader(headers: RequestLike['headers'], name: string): string | undefined { + const value = headers[name] ?? headers[canonicalHeaderName(name)]; + if (Array.isArray(value)) { + return value.length === 1 ? strictString(value[0]) : undefined; + } + return strictString(value); +} + +function canonicalHeaderName(name: string): string { + return name + .split('-') + .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`) + .join('-'); +} + +function strictString(value: unknown): string | undefined { + return typeof value === 'string' && value.length > 0 && value.trim() === value && !value.includes(',') + ? value + : undefined; +} + +function validOpaqueId(value: unknown, maxLength: number): value is string { + return typeof value === 'string' && new RegExp(`^[A-Za-z0-9][A-Za-z0-9._:@-]{0,${maxLength - 1}}$`).test(value); +} + +function validBindingVersion(value: unknown): value is string { + return typeof value === 'string' && /^[1-9][0-9]{0,18}$/.test(value); +} + +function validKeyId(value: string): boolean { + return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value); +} + +function validContextId(value: unknown): value is string { + return typeof value === 'string' && decodeBase64Url(value)?.byteLength === 16; +} + +function decodeBase64Url(value: string): ByteBuffer | undefined { + if (!/^[A-Za-z0-9_-]+$/.test(value)) { + return undefined; + } + try { + const decoded = Buffer.from(value, 'base64url'); + return decoded.toString('base64url') === value ? decoded : undefined; + } catch { + return undefined; + } +} + +function decodeCanonicalBase64(value: string): ByteBuffer | undefined { + if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) { + return undefined; + } + try { + const decoded = Buffer.from(value, 'base64'); + return decoded.toString('base64') === value ? decoded : undefined; + } catch { + return undefined; + } +} + +function validSshSignature(value: string): boolean { + return /^-----BEGIN SSH SIGNATURE-----\n(?:[A-Za-z0-9+/=]{1,76}\n)+-----END SSH SIGNATURE-----\n?$/.test(value); +} + +function hasExactKeys(value: Record, keys: readonly string[]): boolean { + const actual = Object.keys(value); + return actual.length === keys.length && actual.every((key, index) => key === keys[index]); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function pruneReplayCache(cache: Map, nowMs: number): void { + for (const [contextId, expiresAt] of cache) { + if (expiresAt <= nowMs) { + cache.delete(contextId); + } + } +} + +function sendJson(response: ResponseLike, status: number, body: unknown): void { + response.statusCode = status; + response.setHeader('Cache-Control', 'no-store'); + response.setHeader('Content-Type', 'application/json; charset=utf-8'); + response.end(JSON.stringify(body)); +} + +function sendError(response: ResponseLike, status: number, code: string): void { + sendJson(response, status, { ok: false, error: { code } }); +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/types.d.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/types.d.ts new file mode 100644 index 0000000000..2e669a7913 --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/types.d.ts @@ -0,0 +1,56 @@ +declare module 'openclaw/plugin-sdk/plugin-entry' { + export function definePluginEntry(entry: unknown): unknown; +} + +declare module 'node:child_process' { + export function execFile(...args: unknown[]): unknown; +} + +declare module 'node:crypto' { + export function createPublicKey(options: unknown): unknown; + export function createHmac(...args: unknown[]): { update(value: unknown): { digest(encoding: string): string } }; + export function randomUUID(): string; + export function verify(algorithm: null, data: unknown, key: unknown, signature: unknown): boolean; +} + +declare module 'node:fs' { + export function readFileSync(path: string, encoding: string): string; +} + +declare module 'node:util' { + export function promisify(fn: unknown): (...args: unknown[]) => Promise; +} + +declare module 'node:fs/promises' { + export function mkdir(path: string, options?: { recursive?: boolean }): Promise; + export function writeFile(path: string, data: unknown, options?: { encoding?: string; mode?: number }): Promise; +} + +declare module 'node:path' { + const path: { + join(...parts: string[]): string; + dirname(value: string): string; + }; + export default path; +} + +declare const process: { + env: Record; +}; + +declare const Buffer: { + from( + value: string | ArrayBuffer | Uint8Array, + encoding?: string + ): { + byteLength: number; + subarray(start?: number, end?: number): Uint8Array; + toString(encoding?: string): string; + }; + concat(values: unknown[]): { + byteLength: number; + subarray(start?: number, end?: number): Uint8Array; + toString(encoding?: string): string; + }; + isBuffer(value: unknown): boolean; +}; diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs new file mode 100644 index 0000000000..66ffbb4c4e --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs @@ -0,0 +1,304 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { generateKeyPairSync, sign } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { Readable } from 'node:stream'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { runBridge } from '../dist/src/bridge.js'; +import { + MAC_CONTROL_RUNTIME_RECEIPT_PATH, + createMacControlRuntimeReceiptHandler, + registerMacControlRuntimeReceiptRoute, +} from '../dist/src/runtimeReceipt.js'; + +const NOW_MS = Date.parse('2026-07-15T00:00:00Z'); +const KEY_ID = 'mac-context-test-2026-07'; +const CONNECTOR_TOKEN = 'connector-token-value-at-least-24'; +const { privateKey, publicKey } = generateKeyPairSync('ed25519'); +const rawPublicKey = publicKey.export({ format: 'der', type: 'spki' }).subarray(-32); +const env = { + EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_KEY_ID: KEY_ID, + EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_PUBLIC_KEY_B64: rawPublicKey.toString('base64'), +}; + +test('registers one exact gateway-authenticated runtime receipt route', () => { + const routes = []; + registerMacControlRuntimeReceiptRoute({ registerHttpRoute: (route) => routes.push(route) }); + assert.equal(routes.length, 1); + assert.equal(routes[0].path, MAC_CONTROL_RUNTIME_RECEIPT_PATH); + assert.equal(routes[0].auth, 'gateway'); + assert.equal(routes[0].match, 'exact'); +}); + +test('does not expose or execute the removed legacy pairing-code surface', async () => { + const packageManifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); + const pluginManifest = JSON.parse(readFileSync(new URL('../openclaw.plugin.json', import.meta.url), 'utf8')); + assert.equal(packageManifest.openclaw.contracts.tools.includes('customer_mac_complete_pairing'), false); + assert.equal(pluginManifest.contracts.tools.includes('customer_mac_complete_pairing'), false); + + const openClawResult = await runBridge('customerMacCompletePairing', { + enrollment_code: 'must-not-be-used', + }); + assert.equal(openClawResult.ok, false); + assert.equal(openClawResult.errors[0].code, 'legacy_pairing_removed'); + + const hermes = spawnSync( + fileURLToPath(new URL('../../hermes-adapter/bin/evaos-desktop-bridge-command', import.meta.url)), + ['completeEnrollment', '{"enrollment_code":"must-not-be-used"}'], + { encoding: 'utf8', env: { PATH: process.env.PATH } } + ); + assert.equal(hermes.status, 0); + assert.equal(JSON.parse(hermes.stdout).errors[0].code, 'legacy_pairing_removed'); +}); + +test('verifies server authority and forwards only the fixed connector canary contract', async () => { + const challenge = Buffer.alloc(32, 7).toString('base64url'); + const runRef = 'rc-2.1.36-arm64'; + const authority = signedAuthority(); + const receipt = Buffer.from( + JSON.stringify({ + schema: 'evaos.mac_control.runtime_receipt.v1', + challenge, + run_ref: runRef, + binding_ref: 'sha256:binding-proof', + customer_ref: 'sha256:customer-proof', + }) + ).toString('base64url'); + const connectorEnvelope = { + schema: 'evaos.mac_control.runtime_receipt_envelope.v1', + receiptBase64: receipt, + signature: '-----BEGIN SSH SIGNATURE-----\nAAAA\n-----END SSH SIGNATURE-----\n', + keyId: 'connector-receipt-test-2026-07', + namespace: 'evaos-mac-control-receipt-v1', + }; + let connectorCalls = 0; + const handler = createMacControlRuntimeReceiptHandler({ + env, + now: () => NOW_MS, + fetchImpl: async (url, init) => { + connectorCalls += 1; + assert.equal(url, 'http://100.64.10.12:8765/v1/canary/mac-control'); + assert.equal(init.method, 'POST'); + assert.equal(init.headers.Authorization, `Bearer ${CONNECTOR_TOKEN}`); + const body = JSON.parse(init.body); + assert.deepEqual(body, { + schema: 'evaos.mac_control.canary_request.v1', + challenge, + runRef, + executionContext: { + payload: authority.payload, + signature: authority.signature, + keyId: KEY_ID, + }, + binding: { + bindingId: 'binding-1', + bindingVersion: '7', + bindingExpiresAt: '2026-07-15T00:02:00.000Z', + }, + }); + assert.equal('action' in body, false); + assert.equal(init.body.includes(CONNECTOR_TOKEN), false); + return fetchResponse(200, connectorEnvelope); + }, + }); + const response = await invoke(handler, { challenge, runRef }, authority.headers); + assert.equal(response.statusCode, 200); + assert.deepEqual(JSON.parse(response.body), connectorEnvelope); + assert.equal(response.body.includes(CONNECTOR_TOKEN), false); + assert.equal(response.body.includes('customer-1'), false); + assert.equal(connectorCalls, 1); +}); + +test('rejects methods and caller-supplied authority or action fields before connector access', async () => { + let connectorCalls = 0; + const handler = createMacControlRuntimeReceiptHandler({ + env, + now: () => NOW_MS, + fetchImpl: async () => { + connectorCalls += 1; + return fetchResponse(500, {}); + }, + }); + const authority = signedAuthority(); + const getResponse = await invoke(handler, {}, authority.headers, 'GET'); + assert.equal(getResponse.statusCode, 405); + + const injected = await invoke( + handler, + { + challenge: Buffer.alloc(32, 1).toString('base64url'), + runRef: 'run-1', + action: 'customer_mac.desktop_type', + }, + authority.headers + ); + assert.equal(injected.statusCode, 400); + assert.equal(connectorCalls, 0); +}); + +test('fails closed for missing, forged, unknown, malformed, expired, and cross-binding contexts', async (t) => { + const challenge = Buffer.alloc(32, 2).toString('base64url'); + const cases = [ + ['missing', () => ({ ...signedAuthority().headers, 'x-evaos-mac-control-execution-context': undefined })], + [ + 'forged', + () => ({ + ...signedAuthority().headers, + 'x-evaos-mac-control-execution-context-signature': Buffer.alloc(64, 9).toString('base64url'), + }), + ], + [ + 'unknown key', + () => signedAuthority().headers, + { ...env, EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_KEY_ID: 'unknown-key' }, + ], + ['malformed', () => signedAuthority({ extra: 'not-allowed' }).headers], + [ + 'expired', + () => + signedAuthority({ issued_at: Math.floor(NOW_MS / 1000) - 70, expires_at: Math.floor(NOW_MS / 1000) - 10 }) + .headers, + ], + ['cross binding', () => ({ ...signedAuthority().headers, 'x-evaos-mac-control-binding-id': 'binding-2' })], + ]; + + for (const [name, makeHeaders, caseEnv = env] of cases) { + await t.test(name, async () => { + let connectorCalls = 0; + const handler = createMacControlRuntimeReceiptHandler({ + env: caseEnv, + now: () => NOW_MS, + fetchImpl: async () => { + connectorCalls += 1; + return fetchResponse(500, {}); + }, + }); + const response = await invoke(handler, { challenge, runRef: `run-${name.replaceAll(' ', '-')}` }, makeHeaders()); + assert.notEqual(response.statusCode, 200); + assert.equal(connectorCalls, 0); + assert.equal(response.body.includes(CONNECTOR_TOKEN), false); + }); + } +}); + +test('burns a verified execution context before connector IO and rejects replay', async () => { + const authority = signedAuthority(); + let connectorCalls = 0; + const envelope = { + schema: 'evaos.mac_control.runtime_receipt_envelope.v1', + receiptBase64: Buffer.from('{"schema":"evaos.mac_control.runtime_receipt.v1","binding_ref":"sha256:ok"}').toString( + 'base64url' + ), + signature: '-----BEGIN SSH SIGNATURE-----\nAAAA\n-----END SSH SIGNATURE-----\n', + keyId: 'connector-receipt-test-2026-07', + namespace: 'evaos-mac-control-receipt-v1', + }; + const handler = createMacControlRuntimeReceiptHandler({ + env, + now: () => NOW_MS, + fetchImpl: async () => { + connectorCalls += 1; + return fetchResponse(200, envelope); + }, + }); + const body = { challenge: Buffer.alloc(32, 3).toString('base64url'), runRef: 'replay-proof' }; + assert.equal((await invoke(handler, body, authority.headers)).statusCode, 200); + const replay = await invoke(handler, body, authority.headers); + assert.equal(replay.statusCode, 409); + assert.equal(JSON.parse(replay.body).error.code, 'execution_context_replayed'); + assert.equal(connectorCalls, 1); +}); + +test('rejects a connector receipt that contains raw authority without reflecting it', async () => { + const authority = signedAuthority(); + const leakingReceipt = Buffer.from( + JSON.stringify({ schema: 'evaos.mac_control.runtime_receipt.v1', customer_id: 'customer-1' }) + ).toString('base64url'); + const handler = createMacControlRuntimeReceiptHandler({ + env, + now: () => NOW_MS, + fetchImpl: async () => + fetchResponse(200, { + schema: 'evaos.mac_control.runtime_receipt_envelope.v1', + receiptBase64: leakingReceipt, + signature: '-----BEGIN SSH SIGNATURE-----\nAAAA\n-----END SSH SIGNATURE-----\n', + keyId: 'connector-receipt-test-2026-07', + namespace: 'evaos-mac-control-receipt-v1', + }), + }); + const response = await invoke( + handler, + { challenge: Buffer.alloc(32, 4).toString('base64url'), runRef: 'secret-scan' }, + authority.headers + ); + assert.equal(response.statusCode, 502); + assert.equal(response.body.includes('customer-1'), false); + assert.equal(response.body.includes(CONNECTOR_TOKEN), false); +}); + +function signedAuthority(overrides = {}) { + const now = Math.floor(NOW_MS / 1000); + const context = { + schema_version: 'evaos.mac_control_execution_context.v1', + key_id: KEY_ID, + runtime: 'openclaw', + customer_id: 'customer-1', + customer_vm_id: '82000000-0000-4000-8000-000000000001', + binding_id: 'binding-1', + binding_version: '7', + issued_at: now, + expires_at: now + 50, + context_id: Buffer.alloc(16, 5).toString('base64url'), + ...overrides, + }; + const payloadBytes = Buffer.from(JSON.stringify(context)); + const payload = payloadBytes.toString('base64url'); + const signature = sign(null, payloadBytes, privateKey).toString('base64url'); + return { + payload, + signature, + headers: { + 'x-evaos-mac-control-execution-context': payload, + 'x-evaos-mac-control-execution-context-signature': signature, + 'x-evaos-mac-control-execution-context-key-id': KEY_ID, + 'x-evaos-mac-control-contract': 'evaos.mac_control_runtime_contract.v2', + 'x-evaos-mac-control-customer': 'customer-1', + 'x-evaos-mac-control-grant-state': 'active', + 'x-evaos-desktop-bridge-url': 'http://100.64.10.12:8765', + 'x-evaos-desktop-bridge-token': CONNECTOR_TOKEN, + 'x-evaos-desktop-bridge-token-last4': CONNECTOR_TOKEN.slice(-4), + 'x-evaos-mac-control-binding-id': 'binding-1', + 'x-evaos-mac-control-binding-version': '7', + 'x-evaos-mac-control-binding-expires-at': '2026-07-15T00:02:00.000Z', + }, + }; +} + +async function invoke(handler, body, headers, method = 'POST') { + const request = Readable.from([JSON.stringify(body)]); + request.method = method; + request.headers = headers; + const response = { + statusCode: 0, + headers: {}, + body: '', + setHeader(name, value) { + this.headers[name.toLowerCase()] = value; + }, + end(value = '') { + this.body = value; + }, + }; + await handler(request, response); + return response; +} + +function fetchResponse(status, body) { + return { + ok: status >= 200 && status < 300, + status, + text: async () => JSON.stringify(body), + }; +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tsconfig.json b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tsconfig.json new file mode 100644 index 0000000000..bb115e1a45 --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": ".", + "strict": true, + "noEmitOnError": false, + "skipLibCheck": true + }, + "include": ["index.ts", "src/**/*.ts", "src/**/*.d.ts"] +} diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py index 0f63105d12..46fa92f350 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py @@ -798,7 +798,7 @@ def __init__(self, *, connector_url: str, token: str, artifact_dir: Path, repo_r self.repo_root = repo_root or _resolve_repo_root() def run(self, command: str, params: dict[str, Any]) -> SurfaceResponse: - helper = self.repo_root / "openclaw-plugin" / "scripts" / "qa-run-bridge.mjs" + helper = _agent_tools_root(self.repo_root) / "openclaw-plugin" / "scripts" / "qa-run-bridge.mjs" env = os.environ.copy() env.update( { @@ -839,7 +839,7 @@ def run(self, command: str, params: dict[str, Any]) -> SurfaceResponse: "EVAOS_DESKTOP_BRIDGE_ARTIFACT_DIR": str(self.artifact_dir), } ) - adapter = self.repo_root / "hermes-adapter" / "bin" / "evaos-desktop-bridge-command" + adapter = _agent_tools_root(self.repo_root) / "hermes-adapter" / "bin" / "evaos-desktop-bridge-command" params_json = json.dumps(params, separators=(",", ":")) completed = subprocess.run( [str(adapter), command, "-"], @@ -1143,7 +1143,11 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--allow-real-world-actions", action="store_true") parser.add_argument("--operator-ack-live-control", action="store_true", help="Required for suites that may move the mouse, keyboard, or iPhone Mirroring.") parser.add_argument("--allow-skips", action="store_true", help="Exit 0 when required suites contain skipped rows; release certification should not use this.") - parser.add_argument("--repo-root", type=Path, help="Repository root containing openclaw-plugin/ and hermes-adapter/ for adapter surfaces.") + parser.add_argument( + "--repo-root", + type=Path, + help="evaOS-GUI root containing resources/evaos-beta/bridge/agent-tools for adapter surfaces.", + ) parser.add_argument("--version-under-test", default="local-dev") parser.add_argument("--build-under-test", default="") parser.add_argument("--source-commit-under-test", default="") @@ -1905,11 +1909,18 @@ def _resolve_repo_root() -> Path: module_path = Path(__file__).resolve() candidates.extend(module_path.parents) for candidate in candidates: - if (candidate / "openclaw-plugin" / "dist" / "index.js").exists() and (candidate / "hermes-adapter" / "bin" / "evaos-desktop-bridge-command").exists(): + agent_tools = _agent_tools_root(candidate) + if (agent_tools / "openclaw-plugin" / "dist" / "index.js").exists() and ( + agent_tools / "hermes-adapter" / "bin" / "evaos-desktop-bridge-command" + ).exists(): return candidate return Path.cwd() +def _agent_tools_root(repo_root: Path) -> Path: + return repo_root / "resources" / "evaos-beta" / "bridge" / "agent-tools" + + def _secret_values() -> list[str]: values = [] for key, value in os.environ.items(): From 9985c99ba62b5112483cb3437eb372ee985d1424 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 04:08:18 +0700 Subject: [PATCH 02/17] feat(evaos): add signed Mac-control runtime receipts --- .../workflows/workbench-functional-smoke.yml | 4 + .../bridge/native/EvaOSEd25519Verify.swift | 36 + .../evaos_desktop_bridge/connector_server.py | 852 +++++++++++++++--- .../evaos_desktop_bridge/receipt_canary.py | 687 ++++++++++++++ scripts/afterSign.js | 1 + scripts/create-mock-release-artifacts.sh | 13 +- scripts/evaosBetaReleaseGate.js | 25 +- scripts/evaosInstalledAppProductProof.js | 41 + scripts/prepareEvaosDesktopBridgeResource.js | 61 ++ .../evaos/evaosDesktopBridgeReceipt.test.ts | 288 ++++++ .../evaosInstalledAppProductProof.test.ts | 24 + .../unit/process/evaosBetaReleaseGate.test.ts | 18 +- .../prepareEvaosDesktopBridgeResource.test.ts | 69 +- 13 files changed, 2005 insertions(+), 114 deletions(-) create mode 100644 resources/evaos-beta/bridge/native/EvaOSEd25519Verify.swift create mode 100644 resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py create mode 100644 tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts diff --git a/.github/workflows/workbench-functional-smoke.yml b/.github/workflows/workbench-functional-smoke.yml index 69f6eef6a8..48792297d0 100644 --- a/.github/workflows/workbench-functional-smoke.yml +++ b/.github/workflows/workbench-functional-smoke.yml @@ -199,6 +199,7 @@ jobs: BRIDGE="$APP_PATH/Contents/Resources/Bridge/evaos-desktop-bridge" BRIDGE_PEEKABOO="$APP_PATH/Contents/Resources/Bridge/bin/peekaboo" BRIDGE_HELPER="$APP_PATH/Contents/Resources/Bridge/bin/evaos-connector-helper" + BRIDGE_RECEIPT_VERIFIER="$APP_PATH/Contents/Resources/Bridge/bin/evaos-ed25519-verify" BRIDGE_PEEKABOO_LICENSE="$APP_PATH/Contents/Resources/Bridge/licenses/Peekaboo-LICENSE.txt" BRIDGE_MANIFEST="$APP_PATH/Contents/Resources/Bridge/manifest.json" BRIDGE_PYTHON="$APP_PATH/Contents/Resources/Bridge/python/bin/python3" @@ -209,6 +210,7 @@ jobs: test -x "$BRIDGE" test -x "$BRIDGE_PEEKABOO" test -x "$BRIDGE_HELPER" + test -x "$BRIDGE_RECEIPT_VERIFIER" test -f "$BRIDGE_PEEKABOO_LICENSE" test -f "$BRIDGE_MANIFEST" test -x "$BRIDGE_PYTHON" @@ -245,6 +247,8 @@ jobs: ;; esac codesign --verify --strict "$BRIDGE_PEEKABOO" + codesign --verify --strict "$BRIDGE_RECEIPT_VERIFIER" + test "$(lipo -archs "$BRIDGE_RECEIPT_VERIFIER")" = arm64 if [ "$BUNDLED_PEEKABOO_SOURCE_SHA256" != "$PEEKABOO_BINARY_SHA256" ]; then echo "::error::Bundled Peekaboo source digest does not match the pinned release binary." exit 1 diff --git a/resources/evaos-beta/bridge/native/EvaOSEd25519Verify.swift b/resources/evaos-beta/bridge/native/EvaOSEd25519Verify.swift new file mode 100644 index 0000000000..23bd1a4eb0 --- /dev/null +++ b/resources/evaos-beta/bridge/native/EvaOSEd25519Verify.swift @@ -0,0 +1,36 @@ +import CryptoKit +import Foundation + +private struct VerificationRequest: Decodable { + let publicKey: String + let message: String + let signature: String +} + +private let maximumRequestBytes = 96 * 1024 +private let maximumMessageBytes = 64 * 1024 + +private func fail(_ status: Int32) -> Never { + Foundation.exit(status) +} + +let input = FileHandle.standardInput.readDataToEndOfFile() +guard !input.isEmpty, input.count <= maximumRequestBytes else { + fail(2) +} + +let decoder = JSONDecoder() +guard + let request = try? decoder.decode(VerificationRequest.self, from: input), + let publicKey = Data(base64Encoded: request.publicKey), + let message = Data(base64Encoded: request.message), + let signature = Data(base64Encoded: request.signature), + publicKey.count == 32, + message.count <= maximumMessageBytes, + signature.count == 64, + let verifier = try? Curve25519.Signing.PublicKey(rawRepresentation: publicKey) +else { + fail(2) +} + +fail(verifier.isValidSignature(signature, for: message) ? 0 : 3) diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py index 7d7edb1ee5..c753e05dfa 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py @@ -13,26 +13,50 @@ import time import urllib.request from collections import deque +from datetime import datetime, timezone from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any, Callable from urllib.parse import urlparse from .candidate_identity import public_packaged_bridge_candidate +from .receipt_canary import ( + CanaryError, + build_receipt, + burn_replay_token, + candidate_snapshot, + default_process_identity, + load_canary_config, + receipt_envelope, + require_canary_control_state, + sign_receipt, + validate_action_audit, + validate_canary_request, + validate_receipt_signer_key, +) from .audit import default_state_dir from .schema import build_envelope, make_error -from .state import approval_audit_freshness_error, control_session_transaction, read_audit_record, read_control_session +from .state import ( + approval_audit_freshness_error, + control_session_transaction, + read_audit_record, + read_control_session, +) CommandRunner = Callable[[list[str]], tuple[int, str]] OwnerProvider = Callable[[], dict[str, Any] | None] +CandidateProvider = Callable[[], dict[str, Any]] +ProcessProvider = Callable[[], dict[str, Any]] DIAGNOSTICS_SCHEMA = "evaos.desktop_bridge.diagnostics.v1" READY_SCHEMA = "evaos.desktop_bridge.ready.v1" SERVICE_EVENTS_FILE = "connector-service-events.jsonl" MAX_SERVICE_EVENTS = 40 URL_PATTERN = re.compile(r"https?://[^\s)>\"]+", re.IGNORECASE) -AUTH_HEADER_PATTERN = re.compile(r"(?i)(authorization\s*:\s*)(?:bearer\s+|basic\s+)?[A-Za-z0-9._~+/=-]+") +AUTH_HEADER_PATTERN = re.compile( + r"(?i)(authorization\s*:\s*)(?:bearer\s+|basic\s+)?[A-Za-z0-9._~+/=-]+" +) BEARER_TOKEN_PATTERN = re.compile(r"(?i)(bearer\s+)[A-Za-z0-9._~+/=-]{8,}") SECRET_WORD_PATTERN = re.compile( r"(?i)\b(?:api[_-]?key|auth|authorization|password|secret|token)[A-Za-z0-9_.-]*(?:\s*[:=]\s*|\s+)[A-Za-z0-9._~+/=-]{8,}" @@ -245,26 +269,53 @@ def normalize_connector_command(command: str) -> str: CONNECTOR_COMMAND_APPROVAL: dict[str, tuple[str, tuple[str, ...]]] = { "codexSelectThread": ("codex.select_thread", ("thread_id_hash",)), - "codexSendVisibleMessage": ("codex.send_visible_message", ("thread_id_hash", "message_hash")), + "codexSendVisibleMessage": ( + "codex.send_visible_message", + ("thread_id_hash", "message_hash"), + ), "codexContinueThread": ("codex.continue_thread", ("title_hash", "prompt_hash")), "customerMacAppFocus": ("customer_mac.app_focus", ("app_name",)), "customerMacLocalSiteOpen": ("customer_mac.local_site_open", ("url_hash",)), "customerMacLocalSiteAction": ("customer_mac.local_site_action", ("action",)), "customerMacIphoneMirroringFocus": ("customer_mac.iphone_mirroring_focus", ()), "customerMacIphoneMirroringHome": ("customer_mac.iphone_mirroring_home", ()), - "customerMacIphoneMirroringAppSwitcher": ("customer_mac.iphone_mirroring_app_switcher", ()), - "customerMacIphoneMirroringSpotlight": ("customer_mac.iphone_mirroring_spotlight", ()), - "customerMacIphoneMirroringTypeSpotlight": ("customer_mac.iphone_mirroring_type_spotlight", ("text_hash",)), - "customerMacIphoneMirroringOpenApp": ("customer_mac.iphone_mirroring_open_app", ("app_name",)), + "customerMacIphoneMirroringAppSwitcher": ( + "customer_mac.iphone_mirroring_app_switcher", + (), + ), + "customerMacIphoneMirroringSpotlight": ( + "customer_mac.iphone_mirroring_spotlight", + (), + ), + "customerMacIphoneMirroringTypeSpotlight": ( + "customer_mac.iphone_mirroring_type_spotlight", + ("text_hash",), + ), + "customerMacIphoneMirroringOpenApp": ( + "customer_mac.iphone_mirroring_open_app", + ("app_name",), + ), "customerMacIphoneMirroringTapNamedTarget": ( "customer_mac.iphone_mirroring_tap_named_target", ("target_label_hash",), ), - "customerMacIphoneMirroringScroll": ("customer_mac.iphone_mirroring_scroll", ("direction",)), - "customerMacIphoneMirroringSwipeLeft": ("customer_mac.iphone_mirroring_swipe_left", ()), - "customerMacIphoneMirroringSwipeRight": ("customer_mac.iphone_mirroring_swipe_right", ()), + "customerMacIphoneMirroringScroll": ( + "customer_mac.iphone_mirroring_scroll", + ("direction",), + ), + "customerMacIphoneMirroringSwipeLeft": ( + "customer_mac.iphone_mirroring_swipe_left", + (), + ), + "customerMacIphoneMirroringSwipeRight": ( + "customer_mac.iphone_mirroring_swipe_right", + (), + ), "customerMacIphoneMirroringSwipeUp": ("customer_mac.iphone_mirroring_swipe_up", ()), - "customerMacIphoneMirroringSwipeDown": ("customer_mac.iphone_mirroring_swipe_down", ()), + "customerMacIphoneMirroringSwipeDown": ( + "customer_mac.iphone_mirroring_swipe_down", + (), + ), "customerMacIphoneMirroringTypeApprovedText": ( "customer_mac.iphone_mirroring_type_approved_text", ("text_hash",), @@ -278,14 +329,20 @@ def normalize_connector_command(command: str) -> str: ("snapshot_id", "element_id", "target_label_hash", "x", "y"), ), "desktopType": ("customer_mac.desktop_type", ("text_hash",)), - "desktopSetValue": ("customer_mac.desktop_set_value", ("snapshot_id", "element_id", "attribute", "value_hash")), + "desktopSetValue": ( + "customer_mac.desktop_set_value", + ("snapshot_id", "element_id", "attribute", "value_hash"), + ), "desktopScroll": ("customer_mac.desktop_scroll", ("direction", "amount")), "desktopDrag": ("customer_mac.desktop_drag", ("from_x", "from_y", "to_x", "to_y")), "desktopHotkey": ("customer_mac.desktop_hotkey", ("keys",)), "desktopFocusApp": ("customer_mac.desktop_focus_app", ("app_name",)), "desktopWindow": ("customer_mac.desktop_window", ("action",)), "desktopMenu": ("customer_mac.desktop_menu", ("menu_path",)), - "desktopBrowserAction": ("customer_mac.desktop_browser_action", ("action", "url_hash")), + "desktopBrowserAction": ( + "customer_mac.desktop_browser_action", + ("action", "url_hash"), + ), "iphoneTap": ( "customer_mac.iphone_tap", ("snapshot_id", "element_id", "target_label_hash", "x", "y"), @@ -306,21 +363,52 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis "codexWindows": ["codex", "windows", "--json"], "codexConnectionsStatus": ["codex", "connections", "status", "--json"], "codexAppServerStatus": ["codex", "app-server", "status", "--json"], - "codexAppServerRemoteControlStatus": ["codex", "app-server", "remote-control-status", "--json"], + "codexAppServerRemoteControlStatus": [ + "codex", + "app-server", + "remote-control-status", + "--json", + ], "customerMacStatus": ["customer-mac", "status", "--json"], "customerMacCapabilities": ["customer-mac", "capabilities", "--json"], "customerMacControlStatus": ["customer-mac", "control", "status", "--json"], "customerMacControlStop": ["customer-mac", "control", "stop", "--json"], - "customerMacControlKillSwitch": ["customer-mac", "control", "kill-switch", "--json"], - "customerMacIphoneMirroringStatus": ["customer-mac", "iphone-mirroring", "status", "--json"], - "customerMacScreenSharingStatus": ["customer-mac", "screen-sharing", "status", "--json"], + "customerMacControlKillSwitch": [ + "customer-mac", + "control", + "kill-switch", + "--json", + ], + "customerMacIphoneMirroringStatus": [ + "customer-mac", + "iphone-mirroring", + "status", + "--json", + ], + "customerMacScreenSharingStatus": [ + "customer-mac", + "screen-sharing", + "status", + "--json", + ], } if command in fixed: return fixed[command] if command == "auditTail": - return ["audit-tail", "--json", "--limit", str(_clamp_int(params.get("limit"), 20, 1, 100))] + return [ + "audit-tail", + "--json", + "--limit", + str(_clamp_int(params.get("limit"), 20, 1, 100)), + ] if command == "queueList": - return ["queue", "list", "--json", "--limit", str(_clamp_int(params.get("limit"), 20, 1, 100))] + return [ + "queue", + "list", + "--json", + "--limit", + str(_clamp_int(params.get("limit"), 20, 1, 100)), + ] if command == "queueAppend": argv = [ "queue", @@ -335,9 +423,21 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis argv.extend(["--message", str(params["message"])]) return argv if command == "codexThreads": - return ["codex", "threads", "--json", "--max-items", str(_clamp_int(params.get("max_items"), 50, 1, 200))] + return [ + "codex", + "threads", + "--json", + "--max-items", + str(_clamp_int(params.get("max_items"), 50, 1, 200)), + ] if command == "codexThreadMap": - return ["codex", "thread-map", "--json", "--max-items", str(_clamp_int(params.get("max_items"), 50, 1, 200))] + return [ + "codex", + "thread-map", + "--json", + "--max-items", + str(_clamp_int(params.get("max_items"), 50, 1, 200)), + ] if command == "codexSelectThread": return [ "codex", @@ -371,7 +471,9 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis message_file = params.get("message_file") if isinstance(message_file, str) and message_file.strip(): if params.get("_prepared_message_file") is not True: - raise ValueError("message_file is reserved for connector internals; provide message.") + raise ValueError( + "message_file is reserved for connector internals; provide message." + ) argv.extend(["--message-file", message_file.strip()]) else: argv.extend(["--message", _required_string(params, "message")]) @@ -383,20 +485,59 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis argv.append("--dry-run") argv.extend(_approval_arg(params)) if params.get("wait_ms") is not None: - argv.extend(["--wait-ms", str(_clamp_int(params.get("wait_ms"), 0, 0, 120_000))]) + argv.extend( + ["--wait-ms", str(_clamp_int(params.get("wait_ms"), 0, 0, 120_000))] + ) if params.get("poll_interval_ms") is not None: - argv.extend(["--poll-interval-ms", str(_clamp_int(params.get("poll_interval_ms"), 2000, 250, 10_000))]) + argv.extend( + [ + "--poll-interval-ms", + str(_clamp_int(params.get("poll_interval_ms"), 2000, 250, 10_000)), + ] + ) return argv if command == "codexSnapshot": - return ["codex", "snapshot", "--json", "--max-chars", str(_clamp_int(params.get("max_chars"), 4000, 1, 20000))] + return [ + "codex", + "snapshot", + "--json", + "--max-chars", + str(_clamp_int(params.get("max_chars"), 4000, 1, 20000)), + ] if command == "codexInspect": - return ["codex", "inspect", "--json", "--max-nodes", str(_clamp_int(params.get("max_nodes"), 120, 1, 1000))] + return [ + "codex", + "inspect", + "--json", + "--max-nodes", + str(_clamp_int(params.get("max_nodes"), 120, 1, 1000)), + ] if command == "codexAxTree": - return ["codex", "ax-tree", "--json", "--max-nodes", str(_clamp_int(params.get("max_nodes"), 200, 1, 1000))] + return [ + "codex", + "ax-tree", + "--json", + "--max-nodes", + str(_clamp_int(params.get("max_nodes"), 200, 1, 1000)), + ] if command == "codexAppServerThreads": - return ["codex", "app-server", "threads", "--json", "--max-items", str(_clamp_int(params.get("max_items"), 50, 1, 200))] + return [ + "codex", + "app-server", + "threads", + "--json", + "--max-items", + str(_clamp_int(params.get("max_items"), 50, 1, 200)), + ] if command == "codexAppServerLoadedThreads": - return ["codex", "app-server", "loaded-threads", "--json", "--max-items", str(_clamp_int(params.get("max_items"), 50, 1, 200))] + return [ + "codex", + "app-server", + "loaded-threads", + "--json", + "--max-items", + str(_clamp_int(params.get("max_items"), 50, 1, 200)), + ] if command == "codexLiveStatus": return [ "codex", @@ -409,11 +550,31 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis str(_clamp_int(params.get("duration_ms"), 1000, 1, 30000)), ] if command == "customerMacSnapshot": - return ["customer-mac", "snapshot", "--json", "--max-chars", str(_clamp_int(params.get("max_chars"), 4000, 1, 20000))] + return [ + "customer-mac", + "snapshot", + "--json", + "--max-chars", + str(_clamp_int(params.get("max_chars"), 4000, 1, 20000)), + ] if command == "customerMacAxTree": - return ["customer-mac", "ax-tree", "--json", "--max-nodes", str(_clamp_int(params.get("max_nodes"), 200, 1, 1000))] + return [ + "customer-mac", + "ax-tree", + "--json", + "--max-nodes", + str(_clamp_int(params.get("max_nodes"), 200, 1, 1000)), + ] if command == "customerMacControlStart": - return ["customer-mac", "control", "start", "--json", "--mode", str(params.get("mode") or "full-access"), *(_optional_string_arg(params, "agent_label", "--agent-label"))] + return [ + "customer-mac", + "control", + "start", + "--json", + "--mode", + str(params.get("mode") or "full-access"), + *(_optional_string_arg(params, "agent_label", "--agent-label")), + ] if command == "desktopSee": return [ "customer-mac", @@ -426,7 +587,14 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis str(_clamp_int(params.get("max_nodes"), 200, 1, 1000)), ] if command == "desktopClick": - argv = ["customer-mac", "desktop", "click", "--json", *_dry_run_arg(params), *_approval_arg(params)] + argv = [ + "customer-mac", + "desktop", + "click", + "--json", + *_dry_run_arg(params), + *_approval_arg(params), + ] argv.extend(_optional_string_arg(params, "snapshot_id", "--snapshot-id")) argv.extend(_optional_string_arg(params, "element_id", "--element-id")) argv.extend(_optional_string_arg(params, "target_label", "--target-label")) @@ -434,7 +602,16 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis argv.extend(_optional_int_arg(params, "y", "--y")) return argv if command == "desktopType": - return ["customer-mac", "desktop", "type", "--json", "--text", _required_string(params, "text"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "desktop", + "type", + "--json", + "--text", + _required_string(params, "text"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "desktopSetValue": argv = [ "customer-mac", @@ -449,16 +626,31 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis value_file = params.get("value_file") if isinstance(value_file, str) and value_file.strip(): if params.get("_prepared_value_file") is not True: - raise ValueError("value_file is reserved for connector internals; provide value.") + raise ValueError( + "value_file is reserved for connector internals; provide value." + ) argv.extend(["--value-file", value_file.strip()]) else: - raise ValueError("desktopSetValue value must be materialized before building CLI argv.") + raise ValueError( + "desktopSetValue value must be materialized before building CLI argv." + ) argv.extend(["--attribute", str(params.get("attribute") or "value")]) argv.extend(_dry_run_arg(params)) argv.extend(_approval_arg(params)) return argv if command == "desktopScroll": - return ["customer-mac", "desktop", "scroll", "--json", "--direction", str(params.get("direction") or "down"), "--amount", str(_clamp_int(params.get("amount"), 600, 1, 5000)), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "desktop", + "scroll", + "--json", + "--direction", + str(params.get("direction") or "down"), + "--amount", + str(_clamp_int(params.get("amount"), 600, 1, 5000)), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "desktopDrag": return [ "customer-mac", @@ -477,25 +669,103 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis *_approval_arg(params), ] if command == "desktopHotkey": - return ["customer-mac", "desktop", "hotkey", "--json", "--keys", _required_string(params, "keys"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "desktop", + "hotkey", + "--json", + "--keys", + _required_string(params, "keys"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "desktopFocusApp": - return ["customer-mac", "desktop", "focus-app", "--json", "--app-name", _required_string(params, "app_name"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "desktop", + "focus-app", + "--json", + "--app-name", + _required_string(params, "app_name"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "desktopWindow": - return ["customer-mac", "desktop", "window", "--json", "--action", _required_string(params, "action"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "desktop", + "window", + "--json", + "--action", + _required_string(params, "action"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "desktopMenu": - return ["customer-mac", "desktop", "menu", "--json", "--menu-path", _required_string(params, "menu_path"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "desktop", + "menu", + "--json", + "--menu-path", + _required_string(params, "menu_path"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "desktopBrowserAction": - argv = ["customer-mac", "desktop", "browser-action", "--json", "--action", _required_string(params, "action"), *_dry_run_arg(params), *_approval_arg(params)] + argv = [ + "customer-mac", + "desktop", + "browser-action", + "--json", + "--action", + _required_string(params, "action"), + *_dry_run_arg(params), + *_approval_arg(params), + ] argv.extend(_optional_string_arg(params, "url", "--url")) return argv if command == "customerMacAppFocus": - return ["customer-mac", "app-focus", "--json", "--app-name", _required_string(params, "app_name"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "app-focus", + "--json", + "--app-name", + _required_string(params, "app_name"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacLocalSiteOpen": - return ["customer-mac", "local-site", "open", "--json", "--url", _required_string(params, "url"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "local-site", + "open", + "--json", + "--url", + _required_string(params, "url"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacLocalSiteAction": - return ["customer-mac", "local-site", "action", "--json", "--action", _required_string(params, "action"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "local-site", + "action", + "--json", + "--action", + _required_string(params, "action"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringFocus": - return ["customer-mac", "iphone-mirroring", "focus", "--json", *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "focus", + "--json", + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "iphoneSee": return [ "customer-mac", @@ -508,7 +778,14 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis str(_clamp_int(params.get("max_nodes"), 200, 1, 1000)), ] if command == "iphoneTap": - argv = ["customer-mac", "iphone-mirroring", "tap", "--json", *_dry_run_arg(params), *_approval_arg(params)] + argv = [ + "customer-mac", + "iphone-mirroring", + "tap", + "--json", + *_dry_run_arg(params), + *_approval_arg(params), + ] argv.extend(_optional_string_arg(params, "snapshot_id", "--snapshot-id")) argv.extend(_optional_string_arg(params, "element_id", "--element-id")) argv.extend(_optional_string_arg(params, "target_label", "--target-label")) @@ -516,19 +793,76 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis argv.extend(_optional_int_arg(params, "y", "--y")) return argv if command == "iphoneSwipe": - return ["customer-mac", "iphone-mirroring", "swipe", "--json", "--direction", _required_string(params, "direction"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "swipe", + "--json", + "--direction", + _required_string(params, "direction"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "iphoneType": - return ["customer-mac", "iphone-mirroring", "type", "--json", "--text", _required_string(params, "text"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "type", + "--json", + "--text", + _required_string(params, "text"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringHome": - return ["customer-mac", "iphone-mirroring", "home", "--json", *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "home", + "--json", + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringAppSwitcher": - return ["customer-mac", "iphone-mirroring", "app-switcher", "--json", *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "app-switcher", + "--json", + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringSpotlight": - return ["customer-mac", "iphone-mirroring", "spotlight", "--json", *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "spotlight", + "--json", + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringTypeSpotlight": - return ["customer-mac", "iphone-mirroring", "type-spotlight", "--json", "--text", _required_string(params, "text"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "type-spotlight", + "--json", + "--text", + _required_string(params, "text"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringOpenApp": - return ["customer-mac", "iphone-mirroring", "open-app", "--json", "--app-name", _required_string(params, "app_name"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "open-app", + "--json", + "--app-name", + _required_string(params, "app_name"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringTapNamedTarget": return [ "customer-mac", @@ -541,17 +875,63 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis *_approval_arg(params), ] if command == "customerMacIphoneMirroringScroll": - return ["customer-mac", "iphone-mirroring", "scroll", "--json", "--direction", str(params.get("direction") or "down"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "scroll", + "--json", + "--direction", + str(params.get("direction") or "down"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringSwipeLeft": - return ["customer-mac", "iphone-mirroring", "swipe-left", "--json", *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "swipe-left", + "--json", + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringSwipeRight": - return ["customer-mac", "iphone-mirroring", "swipe-right", "--json", *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "swipe-right", + "--json", + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringSwipeUp": - return ["customer-mac", "iphone-mirroring", "swipe-up", "--json", *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "swipe-up", + "--json", + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringSwipeDown": - return ["customer-mac", "iphone-mirroring", "swipe-down", "--json", *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "swipe-down", + "--json", + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringTypeApprovedText": - return ["customer-mac", "iphone-mirroring", "type-approved-text", "--json", "--text", _required_string(params, "text"), *_dry_run_arg(params), *_approval_arg(params)] + return [ + "customer-mac", + "iphone-mirroring", + "type-approved-text", + "--json", + "--text", + _required_string(params, "text"), + *_dry_run_arg(params), + *_approval_arg(params), + ] if command == "customerMacIphoneMirroringSendApprovedMessage": return [ "customer-mac", @@ -570,13 +950,20 @@ def build_bridge_argv(command: str, params: dict[str, Any] | None = None) -> lis raise ValueError(f"Unsupported connector command: {command}") -def _prepare_connector_params(command: str, params: dict[str, Any], *, state_dir: Path | None) -> tuple[dict[str, Any], list[Path]]: +def _prepare_connector_params( + command: str, params: dict[str, Any], *, state_dir: Path | None +) -> tuple[dict[str, Any], list[Path]]: command = normalize_connector_command(command) if command not in {"codexSendVisibleMessage", "desktopSetValue"}: return params, [] if command == "codexSendVisibleMessage": - if isinstance(params.get("message_file"), str) and params.get("message_file", "").strip(): - raise ValueError("message_file is reserved for connector internals; provide message.") + if ( + isinstance(params.get("message_file"), str) + and params.get("message_file", "").strip() + ): + raise ValueError( + "message_file is reserved for connector internals; provide message." + ) if not isinstance(params.get("message"), str): raise ValueError("message is required") payload_key = "message" @@ -584,8 +971,13 @@ def _prepare_connector_params(command: str, params: dict[str, Any], *, state_dir prepared_flag = "_prepared_message_file" prefix = "codex-visible-message-" else: - if isinstance(params.get("value_file"), str) and params.get("value_file", "").strip(): - raise ValueError("value_file is reserved for connector internals; provide value.") + if ( + isinstance(params.get("value_file"), str) + and params.get("value_file", "").strip() + ): + raise ValueError( + "value_file is reserved for connector internals; provide value." + ) if not isinstance(params.get("value"), str): raise ValueError("value is required") payload_key = "value" @@ -594,7 +986,9 @@ def _prepare_connector_params(command: str, params: dict[str, Any], *, state_dir prefix = "desktop-set-value-" root = (state_dir or default_state_dir()) / "tmp" root.mkdir(parents=True, exist_ok=True) - fd, path_text = tempfile.mkstemp(prefix=prefix, suffix=".txt", dir=str(root), text=True) + fd, path_text = tempfile.mkstemp( + prefix=prefix, suffix=".txt", dir=str(root), text=True + ) path = Path(path_text) try: try: @@ -636,7 +1030,12 @@ def run_connector_server( state_dir=state_dir, details={"host": host, "port": port}, ) - handler = _make_handler(token=token, command_runner=command_runner, state_dir=state_dir, owner_provider=owner_provider) + handler = _make_handler( + token=token, + command_runner=command_runner, + state_dir=state_dir, + owner_provider=owner_provider, + ) try: server = ThreadingHTTPServer((host, port), handler) except OSError as exc: @@ -659,7 +1058,9 @@ def run_connector_server( server.serve_forever() -def read_token(path: str | None, *, state_dir: Path | None = None, auto_create: bool = False) -> str | None: +def read_token( + path: str | None, *, state_dir: Path | None = None, auto_create: bool = False +) -> str | None: if not path: token_path = (state_dir or default_state_dir()) / "connector.token" else: @@ -682,7 +1083,9 @@ def read_token(path: str | None, *, state_dir: Path | None = None, auto_create: return token -def build_ready_payload(*, token: str | None, state_dir: Path | None = None) -> dict[str, Any]: +def build_ready_payload( + *, token: str | None, state_dir: Path | None = None +) -> dict[str, Any]: blockers = [] if not token: blockers.append( @@ -704,7 +1107,12 @@ def build_ready_payload(*, token: str | None, state_dir: Path | None = None) -> } -def build_diagnostics_payload(*, token: str | None, state_dir: Path | None = None, owner: dict[str, Any] | None = None) -> dict[str, Any]: +def build_diagnostics_payload( + *, + token: str | None, + state_dir: Path | None = None, + owner: dict[str, Any] | None = None, +) -> dict[str, Any]: root = state_dir or default_state_dir() connector: dict[str, Any] = { "token_state": _token_state(token), @@ -724,12 +1132,16 @@ def build_diagnostics_payload(*, token: str | None, state_dir: Path | None = Non }, "bridge": { "version": _bridge_version(), - "mode": _sanitize_public_text(os.environ.get("EVAOS_DESKTOP_BRIDGE_MODE") or "unknown"), + "mode": _sanitize_public_text( + os.environ.get("EVAOS_DESKTOP_BRIDGE_MODE") or "unknown" + ), "candidate": public_packaged_bridge_candidate(module_file=__file__), }, "connector": connector, "control_session": _public_control_session(state_dir), - "service_events": read_service_events(state_dir=state_dir, limit=MAX_SERVICE_EVENTS), + "service_events": read_service_events( + state_dir=state_dir, limit=MAX_SERVICE_EVENTS + ), "redaction": { "tokens": "redacted", "urls": "redacted", @@ -758,13 +1170,17 @@ def record_service_event( try: root.mkdir(parents=True, exist_ok=True) with (root / SERVICE_EVENTS_FILE).open("a", encoding="utf-8") as handle: - handle.write(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n") + handle.write( + json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n" + ) except Exception: pass return event -def read_service_events(*, state_dir: Path | None = None, limit: int = MAX_SERVICE_EVENTS) -> list[dict[str, Any]]: +def read_service_events( + *, state_dir: Path | None = None, limit: int = MAX_SERVICE_EVENTS +) -> list[dict[str, Any]]: path = (state_dir or default_state_dir()) / SERVICE_EVENTS_FILE if not path.exists(): return [] @@ -918,7 +1334,7 @@ def _public_path(path: str | Path | None) -> str | None: if text == home: return "~" if text.startswith(home + os.sep): - return "~" + text[len(home):] + return "~" + text[len(home) :] except Exception: pass return text @@ -934,6 +1350,8 @@ def _make_handler( command_runner: CommandRunner, state_dir: Path | None = None, owner_provider: OwnerProvider | None = None, + candidate_provider: CandidateProvider | None = None, + process_provider: ProcessProvider | None = None, ) -> type[BaseHTTPRequestHandler]: class ConnectorHandler(BaseHTTPRequestHandler): server_version = "evaos-desktop-bridge-connector/0.1" @@ -941,7 +1359,9 @@ class ConnectorHandler(BaseHTTPRequestHandler): def do_GET(self) -> None: # noqa: N802 parsed = urlparse(self.path) if parsed.path == "/health": - self._write_json(200, {"ok": True, "service": "evaos-desktop-bridge-connector"}) + self._write_json( + 200, {"ok": True, "service": "evaos-desktop-bridge-connector"} + ) return if parsed.path == "/ready": payload = build_ready_payload(token=token, state_dir=state_dir) @@ -949,12 +1369,31 @@ def do_GET(self) -> None: # noqa: N802 return if parsed.path == "/v1/diagnostics": if not token: - self._write_json(503, {"ok": False, "error": "token_missing", "schema": DIAGNOSTICS_SCHEMA}) + self._write_json( + 503, + { + "ok": False, + "error": "token_missing", + "schema": DIAGNOSTICS_SCHEMA, + }, + ) return if not self._authorized(): - self._write_json(401, {"ok": False, "error": "connector_unauthorized", "schema": DIAGNOSTICS_SCHEMA}) + self._write_json( + 401, + { + "ok": False, + "error": "connector_unauthorized", + "schema": DIAGNOSTICS_SCHEMA, + }, + ) return - self._write_json(200, build_diagnostics_payload(token=token, state_dir=state_dir, owner=self._owner_summary())) + self._write_json( + 200, + build_diagnostics_payload( + token=token, state_dir=state_dir, owner=self._owner_summary() + ), + ) return if parsed.path.startswith("/v1/artifacts/"): self._serve_artifact(parsed.path.removeprefix("/v1/artifacts/")) @@ -966,16 +1405,31 @@ def do_POST(self) -> None: # noqa: N802 if parsed.path == "/v1/enrollment/complete": self._complete_enrollment() return + if parsed.path == "/v1/canary/mac-control": + self._mac_control_canary() + return if parsed.path != "/v1/commands": self._write_json(404, {"ok": False, "error": "not_found"}) return if not self._authorized(): - self._write_json(401, _error_envelope("connector.unauthorized", "connector", "connector_unauthorized", "Missing or invalid connector token.")) + self._write_json( + 401, + _error_envelope( + "connector.unauthorized", + "connector", + "connector_unauthorized", + "Missing or invalid connector token.", + ), + ) return try: payload = self._read_json() command = normalize_connector_command(str(payload.get("command") or "")) - params = payload.get("params") if isinstance(payload.get("params"), dict) else {} + params = ( + payload.get("params") + if isinstance(payload.get("params"), dict) + else {} + ) if command == "customerMacControlStart": self._write_json( 403, @@ -990,7 +1444,9 @@ def do_POST(self) -> None: # noqa: N802 temp_paths: list[Path] = [] try: with control_session_transaction(state_dir): - kill_switch_error = _remote_kill_switch_error(command, state_dir=state_dir) + kill_switch_error = _remote_kill_switch_error( + command, state_dir=state_dir + ) if kill_switch_error is not None: self._write_json( 403, @@ -1002,7 +1458,9 @@ def do_POST(self) -> None: # noqa: N802 ), ) return - approval_error = _live_guarded_approval_error(command, params, state_dir=state_dir) + approval_error = _live_guarded_approval_error( + command, params, state_dir=state_dir + ) if approval_error is not None: if "kill switch" in approval_error.lower(): error_code = "control_kill_switch_active" @@ -1020,11 +1478,15 @@ def do_POST(self) -> None: # noqa: N802 ), ) return - prepared_params, temp_paths = _prepare_connector_params(command, params, state_dir=state_dir) + prepared_params, temp_paths = _prepare_connector_params( + command, params, state_dir=state_dir + ) argv = build_bridge_argv(command, prepared_params) if command in TAKEOVER_WARNING_REMOTE_COMMANDS and params.get("dry_run") is False: session = read_control_session(state_dir) - argv = _with_remote_control_generation(argv, session.get("generation")) + argv = _with_remote_control_generation( + argv, session.get("generation") + ) exit_code, output = command_runner(argv) finally: for temp_path in temp_paths: @@ -1035,41 +1497,170 @@ def do_POST(self) -> None: # noqa: N802 try: response = json.loads(output) except json.JSONDecodeError: - response = _error_envelope(command, "desktop", "bridge_output_invalid", output[:500]) + response = _error_envelope( + command, "desktop", "bridge_output_invalid", output[:500] + ) response = _candidate_bound_command_response(command, response) status = 200 if exit_code == 0 else 422 self._write_json(status, response) except Exception as exc: - self._write_json(400, _error_envelope("connector.command", "desktop", "connector_bad_request", str(exc))) + self._write_json( + 400, + _error_envelope( + "connector.command", + "desktop", + "connector_bad_request", + str(exc), + ), + ) + + def _mac_control_canary(self) -> None: + if not token: + self._write_json( + 503, {"ok": False, "error": "connector_token_unavailable"} + ) + return + if not self._authorized(): + self._write_json(401, {"ok": False, "error": "connector_unauthorized"}) + return + try: + payload = self._read_json() + config = load_canary_config() + validate_receipt_signer_key(config) + candidate = ( + candidate_provider() + if candidate_provider is not None + else public_packaged_bridge_candidate(module_file=__file__) + ) + owner = self._owner_summary() + process = ( + process_provider() + if process_provider is not None + else default_process_identity() + ) + candidate_snapshot(candidate, owner=owner, process=process) + context, raw_context, context_signature = validate_canary_request( + payload, config + ) + challenge = str(payload["challenge"]) + run_ref = str(payload["runRef"]) + with control_session_transaction(state_dir): + before = require_canary_control_state( + read_control_session(state_dir) + ) + burn_replay_token( + raw_context, + context_signature, + str(context["context_id"]), + state_dir=state_dir, + ) + generation = before["generation"] + argv = _with_remote_control_generation( + build_bridge_argv( + "desktopHotkey", {"keys": "escape", "dry_run": False} + ), + generation, + ) + action_started_at = datetime.now(timezone.utc) + exit_code, output = command_runner(argv) + try: + response = json.loads(output) + except json.JSONDecodeError as exc: + raise CanaryError("canary_action_failed", status=422) from exc + if exit_code != 0 or not isinstance(response, dict): + raise CanaryError("canary_action_failed", status=422) + with control_session_transaction(state_dir): + after = require_canary_control_state( + read_control_session(state_dir) + ) + if before != after: + raise CanaryError("canary_control_state_changed", status=409) + audit_record, audit_digest = validate_action_audit( + response, + state_dir=state_dir, + action_started_at=action_started_at, + ) + receipt = build_receipt( + config=config, + context=context, + raw_context=raw_context, + challenge=challenge, + run_ref=run_ref, + candidate=candidate, + owner=owner, + process=process, + before=before, + after=after, + audit_record=audit_record, + audit_digest=audit_digest, + executed_at=datetime.now(timezone.utc) + .replace(microsecond=0) + .isoformat() + .replace("+00:00", "Z"), + binding_expires_at=str(payload["binding"]["bindingExpiresAt"]), + ) + signature = sign_receipt(receipt, config) + self._write_json(200, receipt_envelope(receipt, signature, config)) + except CanaryError as exc: + self._write_json(exc.status, {"ok": False, "error": exc.code}) + except Exception: + self._write_json(500, {"ok": False, "error": "canary_internal_error"}) def _complete_enrollment(self) -> None: try: payload = self._read_json() enrollment_code = str(payload.get("enrollment_code") or "").strip() if not enrollment_code: - self._write_json(400, {"ok": False, "error": "missing_enrollment_code"}) + self._write_json( + 400, {"ok": False, "error": "missing_enrollment_code"} + ) return if not token: - self._write_json(503, {"ok": False, "error": "connector_token_unavailable"}) + self._write_json( + 503, {"ok": False, "error": "connector_token_unavailable"} + ) return connector_url = _connector_url_from_request(self) response = complete_enrollment_via_control( enrollment_code=enrollment_code, connector_url=connector_url, connector_token=token, - device_name=str(payload.get("device_name") or socket.gethostname() or "Customer Mac"), + device_name=str( + payload.get("device_name") + or socket.gethostname() + or "Customer Mac" + ), device_identifier=str(payload.get("device_identifier") or ""), ) self._write_json(200, {"ok": True, "data": response}) except Exception as exc: - self._write_json(400, {"ok": False, "error": "enrollment_complete_failed", "message": str(exc)}) + self._write_json( + 400, + { + "ok": False, + "error": "enrollment_complete_failed", + "message": str(exc), + }, + ) def _serve_artifact(self, artifact_name: str) -> None: if not self._authorized(): - self._write_json(401, _error_envelope("connector.artifact", "customer_mac", "connector_unauthorized", "Missing or invalid connector token.")) + self._write_json( + 401, + _error_envelope( + "connector.artifact", + "customer_mac", + "connector_unauthorized", + "Missing or invalid connector token.", + ), + ) return artifact_id = artifact_name.removesuffix(".png") - if not artifact_id.startswith("snap-") or "/" in artifact_id or ".." in artifact_id: + if ( + not artifact_id.startswith("snap-") + or "/" in artifact_id + or ".." in artifact_id + ): self._write_json(404, {"ok": False, "error": "artifact_not_found"}) return root = state_dir or default_state_dir() @@ -1127,7 +1718,12 @@ def _write_json(self, status: int, payload: dict[str, Any]) -> None: def _candidate_bound_command_response(command: str, response: Any) -> dict[str, Any]: if not isinstance(response, dict): - response = _error_envelope(command, "desktop", "bridge_output_invalid", "Bridge response must be a JSON object.") + response = _error_envelope( + command, + "desktop", + "bridge_output_invalid", + "Bridge response must be a JSON object.", + ) if command == "status": return { **response, @@ -1212,25 +1808,47 @@ def _live_guarded_without_approval(command: str, params: dict[str, Any]) -> bool return False if command in CODEX_REMOTE_CONTROL_COMMANDS: source = params.get("source_audit_id") - return params.get("confirm") is not True or not isinstance(source, str) or not source.strip().startswith("audit-") + return ( + params.get("confirm") is not True + or not isinstance(source, str) + or not source.strip().startswith("audit-") + ) approval = params.get("approval_audit_id") return not isinstance(approval, str) or not approval.strip() -def _live_guarded_approval_error(command: str, params: dict[str, Any], *, state_dir: Path | None, require_lookup: bool = True) -> str | None: +def _live_guarded_approval_error( + command: str, + params: dict[str, Any], + *, + state_dir: Path | None, + require_lookup: bool = True, +) -> str | None: command = normalize_connector_command(command) if command in TAKEOVER_WARNING_REMOTE_COMMANDS and params.get("dry_run") is False: session = read_control_session(state_dir) if session.get("kill_switch") is True: return "The customer Mac kill switch is active; live agent control commands are blocked." - warning = session.get("takeover_warning") if isinstance(session.get("takeover_warning"), dict) else {} + warning = ( + session.get("takeover_warning") + if isinstance(session.get("takeover_warning"), dict) + else {} + ) if session.get("active") is True and warning.get("active") is True: - seconds = warning.get("seconds") if isinstance(warning.get("seconds"), int) else 10 + seconds = ( + warning.get("seconds") + if isinstance(warning.get("seconds"), int) + else 10 + ) return f"Agent control is starting; live actions are blocked until the {seconds}-second takeover warning finishes." if command in CONTROLLED_REMOTE_COMMANDS and session.get("active") is True: if session.get("mode") == "full_access": return None - if session.get("mode") == "ask_permission" and not _ask_permission_requires_approval(command, params): + if session.get( + "mode" + ) == "ask_permission" and not _ask_permission_requires_approval( + command, params + ): return None if command not in GUARDED_REMOTE_COMMANDS: @@ -1263,7 +1881,9 @@ def _live_guarded_approval_error(command: str, params: dict[str, Any], *, state_ return None approval = params.get("approval_audit_id") if not isinstance(approval, str) or not approval.strip(): - return "Live remote control actions require a prior dry-run and approval_audit_id." + return ( + "Live remote control actions require a prior dry-run and approval_audit_id." + ) if not require_lookup: return None command_id, fields = CONNECTOR_COMMAND_APPROVAL[command] @@ -1306,7 +1926,13 @@ def _ask_permission_requires_approval(command: str, params: dict[str, Any]) -> b return True return _contains_risk_word(label) if command == "desktopHotkey": - keys = str(params.get("keys") or "").strip().lower().replace("command", "cmd").replace(" ", "") + keys = ( + str(params.get("keys") or "") + .strip() + .lower() + .replace("command", "cmd") + .replace(" ", "") + ) return keys not in ASK_PERMISSION_SAFE_HOTKEYS if command == "desktopWindow": return str(params.get("action") or "").strip().lower() == "close" @@ -1329,7 +1955,10 @@ def _approval_field_value(command: str, params: dict[str, Any], field: str) -> A value = params.get(source_field) if source_field == "prompt" and command == "codexContinueThread": value = value or "continue" - if source_field == "target_label" and command == "customerMacIphoneMirroringSendApprovedMessage": + if ( + source_field == "target_label" + and command == "customerMacIphoneMirroringSendApprovedMessage" + ): value = value or "Send" if value is None: return None @@ -1403,19 +2032,32 @@ def _required_string(params: dict[str, Any], name: str) -> str: def _with_remote_control_generation(argv: list[str], generation: Any) -> list[str]: - if not argv or argv[0] != "customer-mac" or not isinstance(generation, int) or generation < 0: + if ( + not argv + or argv[0] != "customer-mac" + or not isinstance(generation, int) + or generation < 0 + ): raise ValueError("remote control generation is unavailable") return [argv[0], "--remote-control-generation", str(generation), *argv[1:]] -def _error_envelope(command: str, target: str, code: str, message: str) -> dict[str, Any]: +def _error_envelope( + command: str, target: str, code: str, message: str +) -> dict[str, Any]: return build_envelope( command=command, target=target, ok=False, data={}, warnings=[], - errors=[make_error(code=code, message=message, guidance="Check connector pairing, command shape, and approval state.")], + errors=[ + make_error( + code=code, + message=message, + guidance="Check connector pairing, command shape, and approval state.", + ) + ], audit_id="connector-rejected", ) diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py new file mode 100644 index 0000000000..533d492aa8 --- /dev/null +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py @@ -0,0 +1,687 @@ +from __future__ import annotations + +import base64 +import binascii +import hashlib +import json +import os +import re +import stat +import subprocess +import sys +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Callable + +from .audit import default_state_dir +from .state import read_audit_record + +REQUEST_SCHEMA = "evaos.mac_control.canary_request.v1" +CONTEXT_SCHEMA = "evaos.mac_control_execution_context.v1" +RECEIPT_SCHEMA = "evaos.mac_control.runtime_receipt.v1" +RECEIPT_ENVELOPE_SCHEMA = "evaos.mac_control.runtime_receipt_envelope.v1" +RECEIPT_NAMESPACE = "evaos-mac-control-receipt-v1" +NATIVE_VERIFIER_NAME = "evaos-ed25519-verify" +REPLAY_FILE = "mac-control-canary-replay.jsonl" +MAX_CONTEXT_BYTES = 64 * 1024 +MAX_CONTEXT_AGE_SECONDS = 120 +MAX_FUTURE_SKEW_SECONDS = 15 + +_TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$") +_CHALLENGE_RE = re.compile(r"^[A-Za-z0-9_-]{32,128}$") +_COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_AUDIT_ID_RE = re.compile(r"^audit-[0-9A-Za-z_-]{8,128}$") +_CONTEXT_FIELDS = frozenset( + { + "schema_version", + "key_id", + "runtime", + "customer_id", + "customer_vm_id", + "binding_id", + "binding_version", + "issued_at", + "expires_at", + "context_id", + } +) +_REQUEST_FIELDS = frozenset( + {"schema", "challenge", "runRef", "executionContext", "binding"} +) +_SIGNED_CONTEXT_FIELDS = frozenset({"payload", "signature", "keyId"}) +_BINDING_FIELDS = frozenset({"bindingId", "bindingVersion", "bindingExpiresAt"}) + + +class CanaryError(RuntimeError): + def __init__(self, code: str, *, status: int = 400) -> None: + super().__init__(code) + self.code = code + self.status = status + + +@dataclass(frozen=True) +class CanaryConfig: + context_key_id: str + context_public_key: bytes + receipt_key_id: str + receipt_private_key: Path + + +def load_canary_config(env: dict[str, str] | None = None) -> CanaryConfig: + source = os.environ if env is None else env + + def required(name: str) -> str: + value = str(source.get(name) or "").strip() + if not value: + raise CanaryError("canary_config_unavailable", status=503) + return value + + context_key_id = required("EVAOS_MAC_CONTROL_CONTEXT_KEY_ID") + receipt_key_id = required("EVAOS_MAC_CONTROL_RECEIPT_KEY_ID") + if not _TOKEN_RE.fullmatch(context_key_id) or not _TOKEN_RE.fullmatch( + receipt_key_id + ): + raise CanaryError("canary_config_invalid", status=503) + try: + public_key = _base64url_decode(required("EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY")) + except CanaryError as exc: + raise CanaryError("canary_config_invalid", status=503) from exc + if len(public_key) != 32: + raise CanaryError("canary_config_invalid", status=503) + return CanaryConfig( + context_key_id=context_key_id, + context_public_key=public_key, + receipt_key_id=receipt_key_id, + receipt_private_key=Path( + required("EVAOS_MAC_CONTROL_RECEIPT_PRIVATE_KEY_PATH") + ), + ) + + +def native_verifier_path(*, module_file: str | Path = __file__) -> Path: + package_dir = Path(module_file).resolve().parent + return package_dir.parents[1] / "bin" / NATIVE_VERIFIER_NAME + + +def verify_execution_context_signature( + *, + public_key: bytes, + message: bytes, + signature: bytes, + module_file: str | Path = __file__, + run_process: Callable[..., subprocess.CompletedProcess[bytes]] = subprocess.run, +) -> None: + if ( + len(public_key) != 32 + or len(message) > MAX_CONTEXT_BYTES + or len(signature) != 64 + ): + raise CanaryError("execution_context_invalid") + helper = native_verifier_path(module_file=module_file) + try: + metadata = helper.lstat() + except OSError as exc: + raise CanaryError("execution_context_verifier_unavailable", status=503) from exc + if ( + helper.is_symlink() + or not stat.S_ISREG(metadata.st_mode) + or not os.access(helper, os.X_OK) + ): + raise CanaryError("execution_context_verifier_unavailable", status=503) + request = json.dumps( + { + "publicKey": base64.b64encode(public_key).decode("ascii"), + "message": base64.b64encode(message).decode("ascii"), + "signature": base64.b64encode(signature).decode("ascii"), + }, + sort_keys=True, + separators=(",", ":"), + ).encode("ascii") + try: + result = run_process( + [str(helper)], + input=request, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=3, + check=False, + env={"PATH": "/usr/bin:/bin", "LANG": "C", "LC_ALL": "C"}, + close_fds=True, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + raise CanaryError("execution_context_verifier_unavailable", status=503) from exc + if result.returncode == 0: + return + if result.returncode == 3: + raise CanaryError("execution_context_signature_invalid", status=403) + if result.returncode == 2: + raise CanaryError("execution_context_invalid") + raise CanaryError("execution_context_verifier_unavailable", status=503) + + +def validate_canary_request( + payload: dict[str, Any], + config: CanaryConfig, + *, + now: datetime | None = None, + signature_verifier: Callable[..., None] = verify_execution_context_signature, +) -> tuple[dict[str, Any], bytes, bytes]: + if set(payload) != _REQUEST_FIELDS or payload.get("schema") != REQUEST_SCHEMA: + raise CanaryError("canary_request_invalid") + _bounded_token(payload.get("challenge"), _CHALLENGE_RE, "canary_challenge_invalid") + _bounded_token(payload.get("runRef"), _TOKEN_RE, "canary_run_ref_invalid") + signed = payload.get("executionContext") + binding = payload.get("binding") + if not isinstance(signed, dict) or set(signed) != _SIGNED_CONTEXT_FIELDS: + raise CanaryError("execution_context_invalid") + if not isinstance(binding, dict) or set(binding) != _BINDING_FIELDS: + raise CanaryError("canary_binding_invalid", status=403) + if signed.get("keyId") != config.context_key_id: + raise CanaryError("execution_context_key_mismatch", status=403) + try: + raw_context = _base64url_decode(str(signed.get("payload") or "")) + signature = _base64url_decode(str(signed.get("signature") or "")) + except CanaryError as exc: + raise CanaryError("execution_context_invalid") from exc + if not raw_context or len(raw_context) > MAX_CONTEXT_BYTES or len(signature) != 64: + raise CanaryError("execution_context_invalid") + signature_verifier( + public_key=config.context_public_key, message=raw_context, signature=signature + ) + try: + context = json.loads(raw_context.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise CanaryError("execution_context_invalid") from exc + if not isinstance(context, dict) or set(context) != _CONTEXT_FIELDS: + raise CanaryError("execution_context_invalid") + if ( + context.get("schema_version") != CONTEXT_SCHEMA + or context.get("key_id") != config.context_key_id + or context.get("runtime") != "openclaw" + ): + raise CanaryError("execution_context_mismatch", status=403) + for field in ( + "customer_id", + "customer_vm_id", + "binding_id", + "binding_version", + "context_id", + ): + _bounded_identifier(context.get(field), "execution_context_invalid", 256) + binding_expires_at = _parse_timestamp( + binding.get("bindingExpiresAt"), "canary_binding_invalid", status=403 + ) + if binding.get("bindingId") != context.get("binding_id") or binding.get( + "bindingVersion" + ) != context.get("binding_version"): + raise CanaryError("execution_context_binding_mismatch", status=403) + current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc) + issued_at = _parse_timestamp(context.get("issued_at"), "execution_context_invalid") + expires_at = _parse_timestamp( + context.get("expires_at"), "execution_context_invalid" + ) + age = (current - issued_at).total_seconds() + if age < -MAX_FUTURE_SKEW_SECONDS or age > MAX_CONTEXT_AGE_SECONDS: + raise CanaryError("execution_context_expired", status=403) + if ( + expires_at <= current + or expires_at <= issued_at + or binding_expires_at <= current + or expires_at > binding_expires_at + ): + raise CanaryError("execution_context_expired", status=403) + if (expires_at - issued_at).total_seconds() > MAX_CONTEXT_AGE_SECONDS: + raise CanaryError("execution_context_expired", status=403) + return context, raw_context, signature + + +def replay_digest(raw_context: bytes, signature: bytes) -> str: + return hashlib.sha256( + b"evaos-mac-control-context-v1\0" + raw_context + b"\0" + signature + ).hexdigest() + + +def burn_replay_token( + raw_context: bytes, + signature: bytes, + context_id: str, + *, + state_dir: Path | None = None, +) -> None: + root = state_dir or default_state_dir() + root.mkdir(parents=True, exist_ok=True) + replay_path = root / REPLAY_FILE + digest = replay_digest(raw_context, signature) + context_id_digest = hashlib.sha256(context_id.encode("utf-8")).hexdigest() + if replay_path.exists(): + metadata = replay_path.lstat() + if replay_path.is_symlink() or not stat.S_ISREG(metadata.st_mode): + raise CanaryError("canary_replay_store_unavailable", status=503) + try: + if metadata.st_size > 4 * 1024 * 1024: + raise CanaryError("canary_replay_store_unavailable", status=503) + for line in replay_path.read_text(encoding="utf-8").splitlines(): + record = json.loads(line) + if isinstance(record, dict) and ( + record.get("digest") == digest + or record.get("contextIdDigest") == context_id_digest + ): + raise CanaryError("execution_context_replayed", status=409) + except CanaryError: + raise + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise CanaryError("canary_replay_store_unavailable", status=503) from exc + flags = ( + os.O_APPEND + | os.O_CREAT + | os.O_WRONLY + | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_NOFOLLOW", 0) + ) + try: + descriptor = os.open(replay_path, flags, 0o600) + os.fchmod(descriptor, 0o600) + with os.fdopen(descriptor, "a", encoding="utf-8") as handle: + handle.write( + json.dumps( + {"contextIdDigest": context_id_digest, "digest": digest}, + sort_keys=True, + separators=(",", ":"), + ) + + "\n" + ) + handle.flush() + os.fsync(handle.fileno()) + except OSError as exc: + raise CanaryError("canary_replay_store_unavailable", status=503) from exc + + +def stable_control_state(session: dict[str, Any]) -> dict[str, Any]: + warning = ( + session.get("takeover_warning") + if isinstance(session.get("takeover_warning"), dict) + else {} + ) + generation = session.get("generation") + return { + "active": session.get("active") is True, + "generation": generation + if isinstance(generation, int) and generation >= 0 + else None, + "killSwitch": session.get("kill_switch") is True, + "mode": session.get("mode"), + "ready": session.get("ready") is True, + "takeoverActive": warning.get("active") is True, + } + + +def require_canary_control_state(session: dict[str, Any]) -> dict[str, Any]: + stable = stable_control_state(session) + if ( + stable["active"] is not True + or stable["ready"] is not True + or stable["mode"] != "full_access" + or stable["killSwitch"] is True + or stable["takeoverActive"] is True + or stable["generation"] is None + ): + raise CanaryError("canary_control_state_not_ready", status=403) + return stable + + +def validate_action_audit( + response: dict[str, Any], + *, + state_dir: Path | None, + action_started_at: datetime, +) -> tuple[dict[str, Any], str]: + audit_id = response.get("audit_id") + if ( + response.get("ok") is not True + or not isinstance(audit_id, str) + or not _AUDIT_ID_RE.fullmatch(audit_id) + ): + raise CanaryError("canary_action_failed", status=422) + record = read_audit_record(audit_id, state_dir=state_dir) + if not isinstance(record, dict): + raise CanaryError("canary_audit_missing", status=422) + args = record.get("args") + expected_args = { + "approval_audit_id": None, + "customer_mac_command": "desktop", + "desktop_command": "hotkey", + "dry_run": False, + "json": True, + "keys": "escape", + "scope": "customer-mac", + } + if ( + record.get("audit_id") != audit_id + or record.get("command") != "customer_mac.desktop_hotkey" + or record.get("target") != "customer_mac" + or record.get("ok") is not True + or args != expected_args + ): + raise CanaryError("canary_audit_mismatch", status=422) + timestamp = _parse_timestamp( + record.get("timestamp"), "canary_audit_mismatch", status=422 + ) + current = datetime.now(timezone.utc) + if timestamp < action_started_at - _seconds(5) or timestamp > current + _seconds( + MAX_FUTURE_SKEW_SECONDS + ): + raise CanaryError("canary_audit_mismatch", status=422) + digest = hashlib.sha256(_canonical_json(record)).hexdigest() + return record, digest + + +def build_receipt( + *, + config: CanaryConfig, + context: dict[str, Any], + raw_context: bytes, + challenge: str, + run_ref: str, + candidate: dict[str, Any], + owner: dict[str, Any] | None, + process: dict[str, Any], + before: dict[str, Any], + after: dict[str, Any], + audit_record: dict[str, Any], + audit_digest: str, + executed_at: str, + binding_expires_at: str, +) -> dict[str, Any]: + packaged_candidate = candidate_snapshot(candidate, owner=owner, process=process) + before_digest = hashlib.sha256(_canonical_json(before)).hexdigest() + after_digest = hashlib.sha256(_canonical_json(after)).hexdigest() + if before != after or before_digest != after_digest: + raise CanaryError("canary_control_state_changed", status=409) + binding_id = str(context["binding_id"]) + session_seed = f"{binding_id}\0{before['generation']}" + action_args = {"keys": "escape", "dryRun": False} + action = {"command": "customer_mac.desktop_hotkey", "args": action_args} + return { + "schema": RECEIPT_SCHEMA, + "keyId": config.receipt_key_id, + "namespace": RECEIPT_NAMESPACE, + "executedAt": executed_at, + "runtime": "openclaw", + "challenge": challenge, + "runRef": run_ref, + "contextKeyId": config.context_key_id, + "executionContextDigest": _salted_hash(challenge, raw_context), + "contextRef": _salted_hash(challenge, str(context["context_id"])), + "contextIssuedAt": context["issued_at"], + "contextExpiresAt": context["expires_at"], + "customerRef": _salted_hash(challenge, str(context["customer_id"])), + "vmRef": _salted_hash(challenge, str(context["customer_vm_id"])), + "bindingRef": _salted_hash(challenge, binding_id), + "bindingVersion": context["binding_version"], + "bindingExpiresAt": binding_expires_at, + "sessionRef": _salted_hash(challenge, session_seed), + "controlStateBefore": before, + "controlStateAfter": after, + "controlStateBeforeDigest": before_digest, + "controlStateAfterDigest": after_digest, + "candidate": packaged_candidate, + "action": action, + "actionArgsDigest": hashlib.sha256(_canonical_json(action_args)).hexdigest(), + "auditId": audit_record["audit_id"], + "auditTimestamp": audit_record.get("timestamp"), + "auditRecordDigest": audit_digest, + } + + +def sign_receipt( + receipt: dict[str, Any], + config: CanaryConfig, + *, + run_process: Callable[..., subprocess.CompletedProcess[bytes]] = subprocess.run, +) -> str: + key_path, before = validate_receipt_signer_key(config) + message = _canonical_json(receipt) + try: + result = run_process( + [ + "/usr/bin/ssh-keygen", + "-Y", + "sign", + "-q", + "-f", + str(key_path), + "-n", + RECEIPT_NAMESPACE, + "-", + ], + input=message, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + timeout=4, + check=False, + env={"PATH": "/usr/bin:/bin", "LANG": "C", "LC_ALL": "C"}, + close_fds=True, + ) + after = key_path.lstat() + except (OSError, subprocess.TimeoutExpired) as exc: + raise CanaryError("receipt_signer_unavailable", status=503) from exc + if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) != ( + after.st_dev, + after.st_ino, + after.st_size, + after.st_mtime_ns, + ): + raise CanaryError("receipt_signer_unavailable", status=503) + try: + signature = result.stdout.decode("ascii") + except UnicodeDecodeError as exc: + raise CanaryError("receipt_signer_unavailable", status=503) from exc + if ( + result.returncode != 0 + or len(signature) > 8192 + or not signature.startswith("-----BEGIN SSH SIGNATURE-----\n") + or not signature.endswith("-----END SSH SIGNATURE-----\n") + ): + raise CanaryError("receipt_signer_unavailable", status=503) + return signature + + +def validate_receipt_signer_key(config: CanaryConfig) -> tuple[Path, os.stat_result]: + key_path = config.receipt_private_key + if not key_path.is_absolute(): + raise CanaryError("receipt_signer_unavailable", status=503) + try: + parent = key_path.parent.lstat() + before = key_path.lstat() + except OSError as exc: + raise CanaryError("receipt_signer_unavailable", status=503) from exc + if ( + key_path.is_symlink() + or key_path.parent.is_symlink() + or not stat.S_ISREG(before.st_mode) + or not stat.S_ISDIR(parent.st_mode) + or before.st_uid != os.getuid() + or parent.st_uid != os.getuid() + or stat.S_IMODE(before.st_mode) != 0o600 + or stat.S_IMODE(parent.st_mode) & 0o077 + ): + raise CanaryError("receipt_signer_unavailable", status=503) + return key_path, before + + +def receipt_envelope( + receipt: dict[str, Any], sshsig: str, config: CanaryConfig +) -> dict[str, Any]: + return { + "ok": True, + "schema": RECEIPT_ENVELOPE_SCHEMA, + "receiptBase64": _base64url_encode(_canonical_json(receipt)), + "signature": sshsig, + "keyId": config.receipt_key_id, + "namespace": RECEIPT_NAMESPACE, + } + + +def candidate_snapshot( + candidate: dict[str, Any], *, owner: dict[str, Any] | None, process: dict[str, Any] +) -> dict[str, Any]: + required_candidate = { + "sourceCommit": candidate.get("source_commit"), + "sourceSha256": candidate.get("source_sha256"), + "sourcePath": candidate.get("source_path"), + "sourceOwner": candidate.get("owner"), + "status": candidate.get("status"), + "appPath": candidate.get("app_path"), + "appVersion": candidate.get("app_version"), + "appBuild": candidate.get("app_build"), + "appBundleId": candidate.get("app_bundle_id"), + "appName": candidate.get("app_name"), + } + owner = owner if isinstance(owner, dict) else {} + owner_program = ( + owner.get("program_path") if isinstance(owner.get("program_path"), dict) else {} + ) + owner_app = owner.get("app_path") if isinstance(owner.get("app_path"), dict) else {} + owner_manifest = ( + owner.get("manifest_path") + if isinstance(owner.get("manifest_path"), dict) + else {} + ) + owner_plist = ( + owner.get("plist_path") if isinstance(owner.get("plist_path"), dict) else {} + ) + executable = process.get("executable") + argv0 = process.get("argv0") + if ( + candidate.get("ok") is not True + or not _COMMIT_RE.fullmatch(str(required_candidate["sourceCommit"] or "")) + or not _SHA256_RE.fullmatch(str(required_candidate["sourceSha256"] or "")) + or required_candidate["sourcePath"] != "resources/evaos-beta/bridge" + or required_candidate["sourceOwner"] != "100yenadmin/evaOS-GUI" + or required_candidate["status"] != "vendored" + or required_candidate["appPath"] != "/Applications/evaOS Workbench.app" + or required_candidate["appBundleId"] != "com.evaos.workbench" + or required_candidate["appName"] != "evaOS Workbench" + or owner.get("label") != "com.electricsheep.evaos-desktop-bridge" + or owner.get("classification") != "workbench_bundle" + or owner.get("bundle_id") != "com.evaos.workbench" + or owner.get("source_commit") != required_candidate["sourceCommit"] + or owner_program + != { + "kind": "path", + "value": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge", + } + or owner_app != {"kind": "path", "value": "/Applications/evaOS Workbench.app"} + or owner_manifest + != { + "kind": "path", + "value": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/manifest.json", + } + or owner_plist.get("kind") != "path" + or not isinstance(owner_plist.get("value"), str) + or not str(owner_plist["value"]).endswith( + "/Library/LaunchAgents/com.electricsheep.evaos-desktop-bridge.plist" + ) + or not isinstance(executable, str) + or not re.fullmatch( + r"/Applications/evaOS Workbench\.app/Contents/Resources/Bridge/python/bin/python3(?:\.12)?", + executable, + ) + or argv0 + != "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py" + ): + raise CanaryError("canary_candidate_identity_invalid", status=503) + return { + **required_candidate, + "executable": executable, + "argv0": argv0, + "owner": { + "label": owner.get("label"), + "classification": owner.get("classification"), + "bundleId": owner.get("bundle_id"), + "sourceCommit": owner.get("source_commit"), + "programPath": owner_program, + "appPath": owner_app, + "manifestPath": owner_manifest, + "plistPath": owner_plist, + }, + } + + +def _canonical_json(value: Any) -> bytes: + return json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + + +def _salted_hash(challenge: str, value: str | bytes) -> str: + raw = value if isinstance(value, bytes) else value.encode("utf-8") + return hashlib.sha256(challenge.encode("ascii") + b"\0" + raw).hexdigest() + + +def _base64url_decode(value: str) -> bytes: + if not value or "=" in value or re.fullmatch(r"[A-Za-z0-9_-]+", value) is None: + raise CanaryError("base64url_invalid") + try: + return base64.b64decode( + value + "=" * (-len(value) % 4), altchars=b"-_", validate=True + ) + except (ValueError, binascii.Error) as exc: + raise CanaryError("base64url_invalid") from exc + + +def _base64url_encode(value: bytes) -> str: + return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=") + + +def _bounded_token(value: Any, pattern: re.Pattern[str], code: str) -> str: + if not isinstance(value, str) or not pattern.fullmatch(value): + raise CanaryError(code) + return value + + +def _bounded_identifier(value: Any, code: str, maximum: int) -> str: + if ( + not isinstance(value, str) + or not value + or len(value.encode("utf-8")) > maximum + or any(ord(char) < 32 for char in value) + ): + raise CanaryError(code) + return value + + +def _parse_timestamp(value: Any, code: str, *, status: int = 400) -> datetime: + if not isinstance(value, str) or not value.endswith("Z"): + raise CanaryError(code, status=status) + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise CanaryError(code, status=status) from exc + if parsed.tzinfo is None: + raise CanaryError(code, status=status) + return parsed.astimezone(timezone.utc) + + +def _timestamp_now() -> str: + return ( + datetime.now(timezone.utc) + .replace(microsecond=0) + .isoformat() + .replace("+00:00", "Z") + ) + + +def _seconds(value: int): + from datetime import timedelta + + return timedelta(seconds=value) + + +def default_process_identity() -> dict[str, Any]: + return { + "executable": str(Path(sys.executable).resolve()), + "argv0": str(Path(sys.argv[0]).resolve()) if sys.argv else None, + } diff --git a/scripts/afterSign.js b/scripts/afterSign.js index b49f91b8bb..1d45ea9b45 100644 --- a/scripts/afterSign.js +++ b/scripts/afterSign.js @@ -37,6 +37,7 @@ const MACHO_MAGICS = new Set([ const MAC_CONTROL_HELPER_RELATIVE_PATHS = [ path.join('Contents', 'Resources', 'Bridge', 'bin', 'peekaboo'), path.join('Contents', 'Resources', 'Bridge', 'bin', 'evaos-connector-helper'), + path.join('Contents', 'Resources', 'Bridge', 'bin', 'evaos-ed25519-verify'), ]; function getAppleIdNotarizationOptions(env) { diff --git a/scripts/create-mock-release-artifacts.sh b/scripts/create-mock-release-artifacts.sh index b3a7e067ae..1cb85a418b 100644 --- a/scripts/create-mock-release-artifacts.sh +++ b/scripts/create-mock-release-artifacts.sh @@ -66,7 +66,8 @@ NODE "$tmp_dir/committed-source/resources/evaos-beta/bridge/SOURCE.json" \ "$MOCK_SOURCE_COMMIT" \ "$VERSION" \ - "$PRODUCT_NAME" <<'PY' + "$PRODUCT_NAME" \ + "$tmp_dir/committed-source/resources/evaos-beta/bridge/native/EvaOSEd25519Verify.swift" <<'PY' import hashlib import json import pathlib @@ -83,6 +84,7 @@ source_provenance_path = pathlib.Path(sys.argv[5]) source_commit = sys.argv[6] app_version = sys.argv[7] product_name = sys.argv[8] +ed25519_verifier_source_path = pathlib.Path(sys.argv[9]) with (bridge.parents[1] / "Info.plist").open("wb") as info_stream: plistlib.dump( { @@ -118,8 +120,10 @@ source_provenance = json.loads(source_provenance_path.read_text(encoding="utf-8" source_provenance["sourceSha256"] = bridge_source_sha256 (bridge / "bin" / "peekaboo").write_bytes(macho) (bridge / "bin" / "evaos-connector-helper").write_bytes(macho) +(bridge / "bin" / "evaos-ed25519-verify").write_bytes(python_header) (bridge / "bin" / "peekaboo").chmod(0o755) (bridge / "bin" / "evaos-connector-helper").chmod(0o755) +(bridge / "bin" / "evaos-ed25519-verify").chmod(0o755) (bridge / "licenses" / "Peekaboo-LICENSE.txt").write_bytes(license_bytes) (bridge / "licenses" / "CPython-LICENSE.txt").write_bytes(python_license_bytes) python_bin = bridge / "python" / "bin" @@ -196,6 +200,13 @@ manifest = { "sourceBranch": "mock-release-fixture", "sourceProvenance": source_provenance, "bundledTools": { + "ed25519Verifier": { + "schema": "evaos-workbench-ed25519-verifier/v1", + "path": "bin/evaos-ed25519-verify", + "architecture": architecture, + "minimumMacOS": "15.0", + "sourceSha256": hashlib.sha256(ed25519_verifier_source_path.read_bytes()).hexdigest(), + }, "peekaboo": { "version": "3.8.0", "sourceSha256": "4a5c7e28c263c84e406aa1853ef62cad3042b13f40a7a9e044ec74ec42933383", diff --git a/scripts/evaosBetaReleaseGate.js b/scripts/evaosBetaReleaseGate.js index 95ee912a56..c9468a65de 100644 --- a/scripts/evaosBetaReleaseGate.js +++ b/scripts/evaosBetaReleaseGate.js @@ -13,7 +13,7 @@ const committedBridgeSourceIdentityCache = new Map(); const TRUTHY_VALUES = new Set(['1', 'true', 'yes', 'on', 'evaos-beta']); const LIVE_CANARY_VERIFIER_SHA256 = '692d88c72217b44f7957d78228748991ff65a12afda253c03b365a30b63e6127'; -const FUNCTIONAL_SMOKE_SHAPE_RUN_SHA256 = '427e7ad95cf5d10399620463f3b789d2ef62f7f30e0c49ae14d68c10de0663a6'; +const FUNCTIONAL_SMOKE_SHAPE_RUN_SHA256 = '7d3bc23e52e3e342782b2903664572b15754782db4e67155cdb869c4c8d93d3b'; const REQUIRED_PUBLIC_BETA_CODE_SIGNING_ENV = [ { @@ -2503,6 +2503,13 @@ function committedBridgeSourceIdentity(expectedSourceCommit, runGit = execFileSy } function inspectMacosZipBridgePayload(zipPath, expectedSourceCommit, committedSourceIdentity, expectedAppVersion) { + const expectedEd25519VerifierSourceSha256 = createHash('sha256') + .update( + fs.readFileSync( + path.join(PROJECT_ROOT, 'resources', 'evaos-beta', 'bridge', 'native', 'EvaOSEd25519Verify.swift') + ) + ) + .digest('hex'); const script = [ 'import hashlib', 'import json', @@ -2528,6 +2535,7 @@ function inspectMacosZipBridgePayload(zipPath, expectedSourceCommit, committedSo `expected_bridge_source_sha256 = ${JSON.stringify(committedSourceIdentity.sourceSha256)}`, `expected_bridge_source_paths = ${JSON.stringify(committedSourceIdentity.sourcePaths)}`, `expected_app_version = ${JSON.stringify(String(expectedAppVersion || ''))}`, + `expected_ed25519_verifier_source_sha256 = ${JSON.stringify(expectedEd25519VerifierSourceSha256)}`, 'macho_magics = {"feedface", "feedfacf", "cefaedfe", "cffaedfe", "cafebabe", "cafebabf", "bebafeca", "bfbafeca"}', 'expected_cpu_types = {"arm64": 0x0100000c, "x64": 0x01000007}', 'def thin_macho_cpu(data):', @@ -2596,7 +2604,7 @@ function inspectMacosZipBridgePayload(zipPath, expectedSourceCommit, committedSo ' return False', ' resolved = posixpath.normpath(posixpath.join(posixpath.dirname(relative_path), target))', ' return resolved != ".." and not resolved.startswith("../")', - 'result = {"zipLayoutValid": False, "singleAppRoot": False, "infoPlistValid": False, "bundleIdentifierValid": False, "productNameValid": False, "shortVersionValid": False, "bundleVersionValid": False, "hasBridgeExecutable": False, "hasBridgeManifest": False, "hasPeekaboo": False, "hasConnectorHelper": False, "hasPeekabooLicense": False, "executableModesValid": False, "bridgeWrapperValid": False, "bridgeSourceTreeExact": False, "bridgeSourceDigestValid": False, "bridgeCommitBindingValid": False, "peekabooMachO": False, "connectorHelperMachO": False, "manifestPlaceholderFalse": False, "manifestSourceDigestValid": False, "manifestLicenseMetadataValid": False, "licenseDigestValid": False, "licenseNoticeValid": False, "hasPythonRuntime": False, "hasPythonLauncher": False, "pythonLauncherValid": False, "pythonRuntimeMachO": False, "pythonRuntimeArchValid": False, "hasPythonLicense": False, "pythonManifestValid": False, "pythonLicenseDigestValid": False, "hasPythonControlModules": False, "pythonObjcArchValid": False, "pythonInventoryValid": False, "hasPythonStdlibSentinel": False, "hasPythonNativeSentinels": False, "pythonNativeSentinelsExecutable": False}', + 'result = {"zipLayoutValid": False, "singleAppRoot": False, "infoPlistValid": False, "bundleIdentifierValid": False, "productNameValid": False, "shortVersionValid": False, "bundleVersionValid": False, "hasBridgeExecutable": False, "hasBridgeManifest": False, "hasPeekaboo": False, "hasConnectorHelper": False, "hasEd25519Verifier": False, "hasPeekabooLicense": False, "executableModesValid": False, "bridgeWrapperValid": False, "bridgeSourceTreeExact": False, "bridgeSourceDigestValid": False, "bridgeCommitBindingValid": False, "peekabooMachO": False, "connectorHelperMachO": False, "ed25519VerifierMachO": False, "ed25519VerifierArchValid": False, "ed25519VerifierManifestValid": False, "manifestPlaceholderFalse": False, "manifestSourceDigestValid": False, "manifestLicenseMetadataValid": False, "licenseDigestValid": False, "licenseNoticeValid": False, "hasPythonRuntime": False, "hasPythonLauncher": False, "pythonLauncherValid": False, "pythonRuntimeMachO": False, "pythonRuntimeArchValid": False, "hasPythonLicense": False, "pythonManifestValid": False, "pythonLicenseDigestValid": False, "hasPythonControlModules": False, "pythonObjcArchValid": False, "pythonInventoryValid": False, "hasPythonStdlibSentinel": False, "hasPythonNativeSentinels": False, "pythonNativeSentinelsExecutable": False}', 'with zipfile.ZipFile(path) as archive:', ' infos = archive.infolist()', ' names = [info.filename for info in infos]', @@ -2631,11 +2639,12 @@ function inspectMacosZipBridgePayload(zipPath, expectedSourceCommit, committedSo ' result["hasBridgeManifest"] = "manifest.json" in entries', ' result["hasPeekaboo"] = "bin/peekaboo" in entries', ' result["hasConnectorHelper"] = "bin/evaos-connector-helper" in entries', + ' result["hasEd25519Verifier"] = "bin/evaos-ed25519-verify" in entries', ' result["hasPeekabooLicense"] = expected_license_path in entries', ' result["hasPythonRuntime"] = "python/bin/python3.12" in entries', ' result["hasPythonLauncher"] = "python/bin/python3" in entries', ' result["hasPythonLicense"] = expected_python_license_path in entries', - ' executable_paths = ["evaos-desktop-bridge", "bin/peekaboo", "bin/evaos-connector-helper", "python/bin/python3.12"]', + ' executable_paths = ["evaos-desktop-bridge", "bin/peekaboo", "bin/evaos-connector-helper", "bin/evaos-ed25519-verify", "python/bin/python3.12"]', ' result["executableModesValid"] = all(name in entries and regular_executable(entries[name]) for name in executable_paths)', ' control_module_paths = {"python/lib/python3.12/site-packages/ApplicationServices/__init__.py", "python/lib/python3.12/site-packages/Cocoa/__init__.py", "python/lib/python3.12/site-packages/CoreText/__init__.py", "python/lib/python3.12/site-packages/Quartz/__init__.py", "python/lib/python3.12/site-packages/objc/__init__.py"}', ' result["hasPythonControlModules"] = control_module_paths.issubset(entries)', @@ -2648,6 +2657,7 @@ function inspectMacosZipBridgePayload(zipPath, expectedSourceCommit, committedSo ' peekaboo = manifest.get("bundledTools", {}).get("peekaboo", {}) if isinstance(manifest, dict) else {}', ' python_runtime = manifest.get("bundledTools", {}).get("python", {}) if isinstance(manifest, dict) else {}', ' bridge_wrapper = manifest.get("bundledTools", {}).get("bridgeWrapper", {}) if isinstance(manifest, dict) else {}', + ' ed25519_verifier = manifest.get("bundledTools", {}).get("ed25519Verifier", {}) if isinstance(manifest, dict) else {}', ' source_provenance = manifest.get("sourceProvenance", {}) if isinstance(manifest, dict) else {}', ' result["manifestPlaceholderFalse"] = manifest.get("placeholder") is False if isinstance(manifest, dict) else False', ' imported_commit = str(source_provenance.get("importedCommit", ""))', @@ -2678,12 +2688,17 @@ function inspectMacosZipBridgePayload(zipPath, expectedSourceCommit, committedSo ' packaged_bridge_source_sha256 = bridge_source_hash.hexdigest()', ' result["bridgeSourceDigestValid"] = source_provenance.get("sourceSha256") == packaged_bridge_source_sha256 == expected_bridge_source_sha256', ' result["manifestSourceDigestValid"] = peekaboo.get("version") == expected_version and peekaboo.get("sourceSha256") == expected_source_sha256', + ' result["ed25519VerifierManifestValid"] = ed25519_verifier.get("schema") == "evaos-workbench-ed25519-verifier/v1" and ed25519_verifier.get("path") == "bin/evaos-ed25519-verify" and ed25519_verifier.get("architecture") == expected_python_arch and ed25519_verifier.get("minimumMacOS") == "15.0" and ed25519_verifier.get("sourceSha256") == expected_ed25519_verifier_source_sha256', ' result["manifestLicenseMetadataValid"] = peekaboo.get("license") == "MIT" and peekaboo.get("licensePath") == expected_license_path', ' result["pythonManifestValid"] = python_runtime.get("version") == expected_python_version and python_runtime.get("architecture") == expected_python_arch and python_runtime.get("sourceSha256") == expected_python_source_sha256[expected_python_arch] and python_runtime.get("sourceUrl") == expected_python_source_url[expected_python_arch] and python_runtime.get("license") == "Python-2.0" and python_runtime.get("licensePath") == expected_python_license_path and python_runtime.get("licenseSha256") == expected_python_license_sha256 and python_runtime.get("packages") == expected_python_packages', ' if result["hasPeekaboo"]:', ' result["peekabooMachO"] = archive.read(entries["bin/peekaboo"])[:4].hex() in macho_magics', ' if result["hasConnectorHelper"]:', ' result["connectorHelperMachO"] = archive.read(entries["bin/evaos-connector-helper"])[:4].hex() in macho_magics', + ' if result["hasEd25519Verifier"]:', + ' ed25519_verifier_bytes = archive.read(entries["bin/evaos-ed25519-verify"])', + ' result["ed25519VerifierMachO"] = ed25519_verifier_bytes[:4].hex() in macho_magics', + ' result["ed25519VerifierArchValid"] = macho_has_arch(ed25519_verifier_bytes, expected_python_arch)', ' if result["hasPythonRuntime"]:', ' python_bytes = archive.read(entries["python/bin/python3.12"])', ' result["pythonRuntimeMachO"] = python_bytes[:4].hex() in macho_magics', @@ -2839,6 +2854,10 @@ function assertMacosZipBridgePayload(outputDir, releaseTargetPlatforms, expected assertZipBridgeProbe(probe, 'hasConnectorHelper', zipName, 'connector helper'); assertZipBridgeProbe(probe, 'executableModesValid', zipName, 'executable ZIP mode'); assertZipBridgeProbe(probe, 'connectorHelperMachO', zipName, 'connector helper Mach-O shape'); + assertZipBridgeProbe(probe, 'hasEd25519Verifier', zipName, 'Ed25519 verifier'); + assertZipBridgeProbe(probe, 'ed25519VerifierMachO', zipName, 'Ed25519 verifier Mach-O shape'); + assertZipBridgeProbe(probe, 'ed25519VerifierArchValid', zipName, 'Ed25519 verifier architecture'); + assertZipBridgeProbe(probe, 'ed25519VerifierManifestValid', zipName, 'Ed25519 verifier manifest identity'); assertZipBridgeProbe(probe, 'hasPeekabooLicense', zipName, 'Peekaboo license'); assertZipBridgeProbe(probe, 'manifestPlaceholderFalse', zipName, 'non-placeholder manifest'); assertZipBridgeProbe(probe, 'manifestSourceDigestValid', zipName, 'Peekaboo source digest'); diff --git a/scripts/evaosInstalledAppProductProof.js b/scripts/evaosInstalledAppProductProof.js index afd1c35a25..07542cb0c7 100644 --- a/scripts/evaosInstalledAppProductProof.js +++ b/scripts/evaosInstalledAppProductProof.js @@ -540,6 +540,22 @@ function collectRelativeFiles(directory, rootPath, matches) { } function inspectInstalledAppTrustState(appPath = DEFAULT_APP_PATH, execFileSyncImpl = execFileSync) { + const receiptVerifierPath = path.join(appPath, 'Contents', 'Resources', 'Bridge', 'bin', 'evaos-ed25519-verify'); + let receiptVerifierPresent = false; + let receiptVerifierNative = false; + try { + const metadata = fs.lstatSync(receiptVerifierPath); + const header = fs.readFileSync(receiptVerifierPath).subarray(0, 4).toString('hex'); + receiptVerifierPresent = metadata.isFile() && !metadata.isSymbolicLink() && Boolean(metadata.mode & 0o111); + receiptVerifierNative = + receiptVerifierPresent && + new Set(['feedface', 'feedfacf', 'cefaedfe', 'cffaedfe', 'cafebabe', 'bebafeca', 'cafebabf', 'bfbafeca']).has( + header + ); + } catch { + receiptVerifierPresent = false; + receiptVerifierNative = false; + } return { codesign: runTrustCommand('/usr/bin/codesign', ['--verify', '--deep', '--strict', appPath], execFileSyncImpl), spctl: runTrustCommand( @@ -548,6 +564,17 @@ function inspectInstalledAppTrustState(appPath = DEFAULT_APP_PATH, execFileSyncI execFileSyncImpl ), pythonCacheFiles: findPythonCacheFiles(path.join(appPath, 'Contents', 'Resources', 'Bridge')), + receiptVerifier: { + path: receiptVerifierPath, + present: receiptVerifierPresent, + native: receiptVerifierNative, + codesign: receiptVerifierPresent + ? runTrustCommand('/usr/bin/codesign', ['--verify', '--strict', receiptVerifierPath], execFileSyncImpl) + : { ok: false }, + architecture: receiptVerifierPresent + ? runTrustCommand('/usr/bin/lipo', ['-archs', receiptVerifierPath], execFileSyncImpl) + : { ok: false }, + }, }; } @@ -569,6 +596,20 @@ function assertInstalledAppTrustStateClean(state) { `Installed app Gatekeeper assessment failed. ${state.spctl?.stderr || state.spctl?.error || ''}`.trim() ); } + const expectedArchitecture = process.arch === 'x64' ? 'x86_64' : process.arch; + if ( + state.receiptVerifier?.present !== true || + state.receiptVerifier?.native !== true || + state.receiptVerifier?.codesign?.ok !== true || + state.receiptVerifier?.architecture?.ok !== true || + state.receiptVerifier?.architecture?.output !== expectedArchitecture + ) { + throw new Error( + `Installed app receipt verifier is missing, non-native, unsigned, or built for the wrong architecture: ${ + state.receiptVerifier?.path || 'unknown path' + }.` + ); + } } function inspectDesktopProofState(appPath = DEFAULT_APP_PATH, execFileSyncImpl = execFileSync) { diff --git a/scripts/prepareEvaosDesktopBridgeResource.js b/scripts/prepareEvaosDesktopBridgeResource.js index 9dac4e4886..d23fd0c5bf 100644 --- a/scripts/prepareEvaosDesktopBridgeResource.js +++ b/scripts/prepareEvaosDesktopBridgeResource.js @@ -17,6 +17,9 @@ const PYTHON_LICENSE_RELATIVE_PATH = 'licenses/CPython-LICENSE.txt'; const PYTHON_RUNTIME_INVENTORY_RELATIVE_PATH = 'python-runtime-inventory.json'; const PYTHON_RUNTIME_INVENTORY_SCHEMA = 'evaos-python-runtime-inventory/v1'; const BRIDGE_WRAPPER_METADATA_SCHEMA = 'evaos-workbench-bridge-wrapper/v1'; +const ED25519_VERIFIER_METADATA_SCHEMA = 'evaos-workbench-ed25519-verifier/v1'; +const ED25519_VERIFIER_NAME = 'evaos-ed25519-verify'; +const ED25519_VERIFIER_SOURCE = path.join(vendoredBridgeSourceDir, 'native', 'EvaOSEd25519Verify.swift'); const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on', 'evaos-beta']); const MACHO_MAGICS = new Set([ @@ -379,6 +382,60 @@ function requireMachOReleaseBinary(filePath, description) { ); } +function ed25519VerifierBuildArgs(sourcePath, outputPath, architecture) { + const targetArchitecture = architecture === 'x64' ? 'x86_64' : architecture; + if (!['arm64', 'x86_64'].includes(targetArchitecture)) { + throw new Error(`Unsupported evaOS Ed25519 verifier architecture: ${architecture}`); + } + return [ + 'swiftc', + '-O', + '-whole-module-optimization', + '-target', + `${targetArchitecture}-apple-macos15.0`, + '-o', + outputPath, + sourcePath, + ]; +} + +function buildEd25519Verifier({ + sourcePath = ED25519_VERIFIER_SOURCE, + targetDir = path.join(bridgeResourceDir, 'bin'), + architecture = process.env.EVAOS_REQUIRED_PYTHON_RUNTIME_ARCH || process.arch, + runCommand = execFileSync, +} = {}) { + if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) { + throw new Error('The evaOS Ed25519 verifier Swift source is missing.'); + } + if (process.platform !== 'darwin') { + if (shouldRequireRealBridge()) { + throw new Error('Release builds require macOS to compile the bundled Ed25519 verifier.'); + } + return undefined; + } + fs.mkdirSync(targetDir, { recursive: true }); + const outputPath = path.join(targetDir, ED25519_VERIFIER_NAME); + const args = ed25519VerifierBuildArgs(sourcePath, outputPath, architecture); + runCommand('xcrun', args, { stdio: ['ignore', 'pipe', 'pipe'] }); + fs.chmodSync(outputPath, 0o755); + requireMachOReleaseBinary(outputPath, 'bundled Ed25519 verifier'); + const expectedArch = architecture === 'x64' ? 'x86_64' : architecture; + const actualArch = String( + runCommand('lipo', ['-archs', outputPath], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) + ).trim(); + if (actualArch !== expectedArch) { + throw new Error(`Bundled Ed25519 verifier architecture ${actualArch} does not match ${expectedArch}.`); + } + return { + schema: ED25519_VERIFIER_METADATA_SCHEMA, + path: `bin/${ED25519_VERIFIER_NAME}`, + architecture, + minimumMacOS: '15.0', + sourceSha256: sha256File(sourcePath), + }; +} + function writeConnectorHelperWrapper(targetDir) { const helperPath = path.join(targetDir, 'evaos-connector-helper'); fs.writeFileSync( @@ -786,6 +843,7 @@ function main() { removePycache(bridgeResourceDir); const pythonRuntime = installPythonRuntime(); const bridgeExecutable = writeBridgeExecutable(); + const ed25519Verifier = buildEd25519Verifier({ targetDir: bridgeBinDir }); const peekabooBinary = copyOptionalBinary('peekaboo', bridgeBinDir); const helperPath = path.join(bridgeBinDir, 'evaos-connector-helper'); if (peekabooBinary && shouldRequireRealBridge()) { @@ -810,6 +868,7 @@ function main() { bridgeWrapper: bridgeWrapperMetadata(bridgeExecutable), }; if (pythonRuntime) bundledTools.python = pythonRuntime; + if (ed25519Verifier) bundledTools.ed25519Verifier = ed25519Verifier; const manifest = bridgeManifest({ requestedSourceRef, @@ -839,6 +898,8 @@ module.exports = { bridgeManifest, bridgeWrapperMetadata, bridgeWrapperScript, + buildEd25519Verifier, + ed25519VerifierBuildArgs, installPythonRuntime, installPeekabooLicense, isMachOExecutable, diff --git a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts new file mode 100644 index 0000000000..87b7da9165 --- /dev/null +++ b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts @@ -0,0 +1,288 @@ +import { execFileSync } from 'node:child_process'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const repoRoot = process.cwd(); +const bridgeSource = join(repoRoot, 'resources', 'evaos-beta', 'bridge', 'src'); + +describe('evaOS signed Mac-control receipt connector', () => { + it('fails closed around the fixed authenticated canary route and emits a verifiable sanitized receipt', () => { + const script = String.raw` +import base64 +import json +import os +import subprocess +import tempfile +import threading +import urllib.error +import urllib.request +from datetime import datetime, timedelta, timezone +from http.server import ThreadingHTTPServer +from pathlib import Path + +from evaos_desktop_bridge import connector_server, receipt_canary +from evaos_desktop_bridge.audit import append_audit + +root = Path(tempfile.mkdtemp(prefix="evaos-receipt-http-")) +key_dir = root / "signer" +key_dir.mkdir(mode=0o700) +key_path = key_dir / "receipt-key" +subprocess.run(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key_path)], check=True) +os.chmod(key_path, 0o600) + +def b64url(value): + return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=") + +os.environ.update({ + "EVAOS_MAC_CONTROL_CONTEXT_KEY_ID": "ws-proxy-context-v1", + "EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY": b64url(b"p" * 32), + "EVAOS_MAC_CONTROL_RECEIPT_KEY_ID": "staging-connector-receipt-v1", + "EVAOS_MAC_CONTROL_RECEIPT_PRIVATE_KEY_PATH": str(key_path), +}) + +control = { + "active": True, + "generation": 7, + "mode": "full_access", + "agent_label": "staging-canary", + "started_at": "2026-07-14T00:00:00Z", + "stopped_at": None, + "kill_switch": False, + "takeover_warning_started_at": None, + "takeover_warning_until": None, + "takeover_warning_seconds": 10, +} +(root / "control-session.json").write_text(json.dumps(control), encoding="utf-8") +os.chmod(root / "control-session.json", 0o600) + +signature_mode = {"forged": False} +original_validate = receipt_canary.validate_canary_request +def validate_request(payload, config): + def verifier(**kwargs): + if signature_mode["forged"]: + raise receipt_canary.CanaryError("execution_context_signature_invalid", status=403) + return original_validate(payload, config, signature_verifier=verifier) +connector_server.validate_canary_request = validate_request + +candidate = { + "ok": True, + "source_commit": "a" * 40, + "source_sha256": "b" * 64, + "source_path": "resources/evaos-beta/bridge", + "owner": "100yenadmin/evaOS-GUI", + "status": "vendored", + "app_path": "/Applications/evaOS Workbench.app", + "app_version": "2.1.36", + "app_build": "2.1.36", + "app_bundle_id": "com.evaos.workbench", + "app_name": "evaOS Workbench", +} +owner = { + "label": "com.electricsheep.evaos-desktop-bridge", + "classification": "workbench_bundle", + "bundle_id": "com.evaos.workbench", + "source_commit": "a" * 40, + "program_path": {"kind": "path", "value": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge"}, + "app_path": {"kind": "path", "value": "/Applications/evaOS Workbench.app"}, + "manifest_path": {"kind": "path", "value": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/manifest.json"}, + "plist_path": {"kind": "path", "value": "~/Library/LaunchAgents/com.electricsheep.evaos-desktop-bridge.plist"}, +} +process = { + "executable": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/python/bin/python3.12", + "argv0": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py", +} +action_calls = [] +tamper_audit = {"enabled": False} +def runner(argv): + action_calls.append(list(argv)) + args = { + "approval_audit_id": None, + "customer_mac_command": "desktop", + "desktop_command": "hotkey", + "dry_run": False, + "json": True, + "keys": "cmd+l" if tamper_audit["enabled"] else "escape", + "scope": "customer-mac", + } + audit_id = append_audit( + command="customer_mac.desktop_hotkey", + target="customer_mac", + args=args, + ok=True, + warnings=[], + errors=[], + state_dir=root, + ) + return 0, json.dumps({"ok": True, "audit_id": audit_id}) + +handler = connector_server._make_handler( + token="connector-test-token", + command_runner=runner, + state_dir=root, + owner_provider=lambda: owner, + candidate_provider=lambda: candidate, + process_provider=lambda: process, +) +server = ThreadingHTTPServer(("127.0.0.1", 0), handler) +thread = threading.Thread(target=server.serve_forever, daemon=True) +thread.start() +endpoint = f"http://127.0.0.1:{server.server_address[1]}/v1/canary/mac-control" + +counter = 0 +def payload(*, expired=False, cross_binding=False): + global counter + counter += 1 + now = datetime.now(timezone.utc).replace(microsecond=0) + context = { + "schema_version": "evaos.mac_control_execution_context.v1", + "key_id": "ws-proxy-context-v1", + "runtime": "openclaw", + "customer_id": "customer-sensitive-id", + "customer_vm_id": "vm-sensitive-id", + "binding_id": "binding-sensitive-id", + "binding_version": "42", + "issued_at": (now - timedelta(seconds=180 if expired else 1)).isoformat().replace("+00:00", "Z"), + "expires_at": (now - timedelta(seconds=1) if expired else now + timedelta(seconds=60)).isoformat().replace("+00:00", "Z"), + "context_id": f"context-sensitive-{counter}", + } + raw = json.dumps(context, sort_keys=True, separators=(",", ":")).encode() + return { + "schema": "evaos.mac_control.canary_request.v1", + "challenge": "C" * 32, + "runRef": f"run-{counter}", + "executionContext": { + "payload": b64url(raw), + "signature": b64url(b"s" * 64), + "keyId": "ws-proxy-context-v1", + }, + "binding": { + "bindingId": "other-binding" if cross_binding else "binding-sensitive-id", + "bindingVersion": "42", + "bindingExpiresAt": (now + timedelta(seconds=300)).isoformat().replace("+00:00", "Z"), + }, + } + +def post(body, token="connector-test-token"): + request = urllib.request.Request( + endpoint, + data=json.dumps(body).encode(), + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=8) as response: + return response.status, json.loads(response.read()) + except urllib.error.HTTPError as error: + return error.code, json.loads(error.read()) + +try: + valid = payload() + status, envelope = post(valid) + assert status == 200, (status, envelope) + assert action_calls == [["customer-mac", "--remote-control-generation", "7", "desktop", "hotkey", "--json", "--keys", "escape"]] + assert envelope["schema"] == "evaos.mac_control.runtime_receipt_envelope.v1" + assert envelope["namespace"] == "evaos-mac-control-receipt-v1" + receipt_bytes = base64.urlsafe_b64decode(envelope["receiptBase64"] + "=" * (-len(envelope["receiptBase64"]) % 4)) + receipt = json.loads(receipt_bytes) + assert receipt_bytes == json.dumps(receipt, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() + assert receipt["action"] == {"command": "customer_mac.desktop_hotkey", "args": {"dryRun": False, "keys": "escape"}} + assert receipt["controlStateBefore"] == receipt["controlStateAfter"] + assert receipt["candidate"]["sourceCommit"] == "a" * 40 + serialized = json.dumps(envelope, sort_keys=True) + for forbidden in [ + "customer-sensitive-id", + "vm-sensitive-id", + "binding-sensitive-id", + "context-sensitive-1", + "connector-test-token", + str(key_path), + "http://", + "127.0.0.1", + ]: + assert forbidden not in serialized, forbidden + + allowed = root / "allowed_signers" + public = (key_path.with_suffix(".pub")).read_text(encoding="utf-8").strip() + allowed.write_text(f"evaos {public}\n", encoding="utf-8") + signature_path = root / "receipt.sshsig" + signature_path.write_text(envelope["signature"], encoding="ascii") + verified = subprocess.run( + ["/usr/bin/ssh-keygen", "-Y", "verify", "-f", str(allowed), "-I", "evaos", "-n", envelope["namespace"], "-s", str(signature_path)], + input=receipt_bytes, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + assert verified.returncode == 0 + + count = len(action_calls) + assert post(valid)[0] == 409 + assert len(action_calls) == count + assert post({"schema": "evaos.mac_control.canary_request.v1"})[0] == 400 + assert len(action_calls) == count + assert post(payload(expired=True))[0] == 403 + assert len(action_calls) == count + assert post(payload(cross_binding=True))[0] == 403 + assert len(action_calls) == count + signature_mode["forged"] = True + assert post(payload())[0] == 403 + signature_mode["forged"] = False + assert len(action_calls) == count + + control["mode"] = "ask_permission" + (root / "control-session.json").write_text(json.dumps(control), encoding="utf-8") + assert post(payload())[0] == 403 + assert len(action_calls) == count + control["mode"] = "full_access" + (root / "control-session.json").write_text(json.dumps(control), encoding="utf-8") + + os.chmod(key_path, 0o644) + assert post(payload())[0] == 503 + assert len(action_calls) == count + os.chmod(key_path, 0o600) + + real_key_path = str(key_path) + symlink_path = key_dir / "receipt-key-link" + symlink_path.symlink_to(key_path) + os.environ["EVAOS_MAC_CONTROL_RECEIPT_PRIVATE_KEY_PATH"] = str(symlink_path) + assert post(payload())[0] == 503 + assert len(action_calls) == count + os.environ["EVAOS_MAC_CONTROL_RECEIPT_PRIVATE_KEY_PATH"] = real_key_path + + tamper_audit["enabled"] = True + state_before = (root / "control-session.json").read_text(encoding="utf-8") + status, error = post(payload()) + assert status == 422 and error["error"] == "canary_audit_mismatch" + assert len(action_calls) == count + 1 + assert (root / "control-session.json").read_text(encoding="utf-8") == state_before + + try: + receipt_canary.verify_execution_context_signature( + public_key=b"p" * 32, + message=b"message", + signature=b"s" * 64, + module_file=root / "missing" / "src" / "evaos_desktop_bridge" / "receipt_canary.py", + ) + except receipt_canary.CanaryError as error: + assert error.code == "execution_context_verifier_unavailable" + else: + raise AssertionError("missing native verifier did not fail closed") +finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) +`; + + expect(() => + execFileSync('python3', ['-c', script], { + cwd: repoRoot, + env: { + ...process.env, + PYTHONPATH: bridgeSource, + PYTHONDONTWRITEBYTECODE: '1', + }, + stdio: ['ignore', 'pipe', 'pipe'], + timeout: 30_000, + }) + ).not.toThrow(); + }, 40_000); +}); diff --git a/tests/unit/evaos/evaosInstalledAppProductProof.test.ts b/tests/unit/evaos/evaosInstalledAppProductProof.test.ts index 690de084a0..0e49ca9613 100644 --- a/tests/unit/evaos/evaosInstalledAppProductProof.test.ts +++ b/tests/unit/evaos/evaosInstalledAppProductProof.test.ts @@ -61,6 +61,13 @@ const installedAppProof = require('../../../scripts/evaosInstalledAppProductProo codesign: { ok: boolean; stderr?: string; error?: string }; spctl: { ok: boolean; stderr?: string; error?: string }; pythonCacheFiles: string[]; + receiptVerifier: { + path: string; + present: boolean; + native: boolean; + codesign: { ok: boolean }; + architecture: { ok: boolean; output?: string }; + }; }) => void; assertExpectedBundle: (bundleInfo: { bundleId: string; @@ -179,6 +186,13 @@ const installedAppProof = require('../../../scripts/evaosInstalledAppProductProo codesign: { ok: boolean; command: string; output?: string; stderr?: string; error?: string }; spctl: { ok: boolean; command: string; output?: string; stderr?: string; error?: string }; pythonCacheFiles: string[]; + receiptVerifier: { + path: string; + present: boolean; + native: boolean; + codesign: { ok: boolean }; + architecture: { ok: boolean; output?: string }; + }; }; parseAppBundlePaths: (output: string) => string[]; parseBridgeListenerPids: (output: string) => string[]; @@ -1124,6 +1138,16 @@ describe('evaOS installed app product proof', () => { expect(() => installedAppProof.assertInstalledAppTrustStateClean(trust)).toThrow(/codesign verification failed/); }); + it('fails installed-app trust when the signed native receipt verifier is absent', () => { + const tempApp = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-app-receipt-verifier-')); + fs.mkdirSync(path.join(tempApp, 'Contents/Resources/Bridge'), { recursive: true }); + + const trust = installedAppProof.inspectInstalledAppTrustState(tempApp, () => ''); + + expect(trust.receiptVerifier.present).toBe(false); + expect(() => installedAppProof.assertInstalledAppTrustStateClean(trust)).toThrow(/receipt verifier is missing/); + }); + it('parses LaunchServices protocol handler ownership for the beta scheme', () => { const dump = [ '---------------------------------------------------------------------------------', diff --git a/tests/unit/process/evaosBetaReleaseGate.test.ts b/tests/unit/process/evaosBetaReleaseGate.test.ts index da68a720bc..10acf9bb6a 100644 --- a/tests/unit/process/evaosBetaReleaseGate.test.ts +++ b/tests/unit/process/evaosBetaReleaseGate.test.ts @@ -1,5 +1,6 @@ import { createRequire } from 'node:module'; import { execFileSync, spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -361,7 +362,7 @@ function writeMacosBridgeZip( inventoryDirectoryArchiveMode?: number; inventoryFileArchiveMode?: number; wrongPythonSourceUrl?: boolean; - nonExecutablePayload?: 'bridge' | 'peekaboo' | 'helper' | 'python'; + nonExecutablePayload?: 'bridge' | 'peekaboo' | 'helper' | 'verifier' | 'python'; omitFoundationNative?: boolean; omitInventoriedRuntimeFile?: boolean; omitStdlibSentinel?: boolean; @@ -382,6 +383,13 @@ function writeMacosBridgeZip( } = {} ) { const bridgeWrapperBase64 = Buffer.from(bridgeResource.bridgeWrapperScript()).toString('base64'); + const ed25519VerifierSourceSha256 = createHash('sha256') + .update( + fs.readFileSync( + path.join(process.cwd(), 'resources', 'evaos-beta', 'bridge', 'native', 'EvaOSEd25519Verify.swift') + ) + ) + .digest('hex'); const script = [ 'import base64', 'import hashlib', @@ -436,6 +444,7 @@ function writeMacosBridgeZip( 'wrong_short_version = sys.argv[42] == "1"', 'wrong_bundle_version = sys.argv[43] == "1"', `bridge_wrapper_bytes = base64.b64decode("${bridgeWrapperBase64}")`, + `ed25519_verifier_source_sha256 = "${ed25519VerifierSourceSha256}"`, 'app_root = "Wrong Workbench.app" if wrong_app_root else "evaOS Workbench.app"', 'info_plist = {"CFBundleIdentifier": "com.example.wrong" if wrong_bundle_identifier else "com.evaos.workbench", "CFBundleName": "Wrong Workbench" if wrong_product_name else "evaOS Workbench", "CFBundleShortVersionString": "9.9.9" if wrong_short_version else "2.1.10", "CFBundleVersion": "999" if wrong_bundle_version else "2.1.10"}', 'info_plist_bytes = b"not-a-plist" if malformed_info_plist else plistlib.dumps(info_plist)', @@ -518,7 +527,7 @@ function writeMacosBridgeZip( ' bridge_source_hash.update(b"\\0")', 'bridge_wrapper_metadata = {"schema": "evaos-workbench-bridge-wrapper/v1", "path": "evaos-desktop-bridge", "sourceSha256": hashlib.sha256(bridge_wrapper_bytes).hexdigest()}', 'source_provenance = {"schema": "evaos-workbench-vendored-bridge-source/v1", "owner": "100yenadmin/evaOS-GUI", "status": "vendored", "importedCommit": "908e3cad8c5f11dca739bbfc2c697c3e6d52f79e", "sourceSha256": bridge_source_hash.hexdigest()}', - 'manifest = {"placeholder": False, "requestedSourceRef": bridge_source_commit, "sourcePath": "resources/evaos-beta/bridge", "sourceCommit": bridge_source_commit, "sourceProvenance": source_provenance, "bundledTools": {"bridgeWrapper": bridge_wrapper_metadata, "peekaboo": {"version": "3.8.0", "sourceSha256": source_sha256, "license": "MIT", "licensePath": "licenses/Peekaboo-LICENSE.txt", "licenseSha256": license_sha256}, "python": python_metadata}}', + 'manifest = {"placeholder": False, "requestedSourceRef": bridge_source_commit, "sourcePath": "resources/evaos-beta/bridge", "sourceCommit": bridge_source_commit, "sourceProvenance": source_provenance, "bundledTools": {"bridgeWrapper": bridge_wrapper_metadata, "ed25519Verifier": {"schema": "evaos-workbench-ed25519-verifier/v1", "path": "bin/evaos-ed25519-verify", "architecture": python_arch, "minimumMacOS": "15.0", "sourceSha256": ed25519_verifier_source_sha256}, "peekaboo": {"version": "3.8.0", "sourceSha256": source_sha256, "license": "MIT", "licensePath": "licenses/Peekaboo-LICENSE.txt", "licenseSha256": license_sha256}, "python": python_metadata}}', 'def write_regular(archive, name, data, mode=0o644):', ' info = zipfile.ZipInfo(name)', ' info.create_system = 3', @@ -551,6 +560,7 @@ function writeMacosBridgeZip( ' if not omit_peekaboo:', ' write_regular(archive, f"{bridge_prefix}/bin/peekaboo", bytes.fromhex("cafebabe00000000"), 0o644 if non_executable_payload == "peekaboo" else 0o755)', ' write_regular(archive, f"{bridge_prefix}/bin/evaos-connector-helper", bytes.fromhex("cafebabe00000000"), 0o644 if non_executable_payload == "helper" else 0o755)', + ' write_regular(archive, f"{bridge_prefix}/bin/evaos-ed25519-verify", python_header, 0o644 if non_executable_payload == "verifier" else 0o755)', ' write_directory(archive, f"{bridge_prefix}/python", 0o600 if non_traversable_python_root else 0o755)', ' if normalized_python_entry_collision:', ' write_regular(archive, f"{bridge_prefix}/python/bin", b"shadowed runtime entry")', @@ -1338,6 +1348,7 @@ printf '%s\\n' ok const helperDir = path.join(appPath, 'Contents', 'Resources', 'Bridge', 'bin'); const peekabooPath = path.join(helperDir, 'peekaboo'); const connectorHelperPath = path.join(helperDir, 'evaos-connector-helper'); + const verifierPath = path.join(helperDir, 'evaos-ed25519-verify'); const pythonPath = path.join(appPath, 'Contents', 'Resources', 'Bridge', 'python', 'bin', 'python3.12'); const pythonDylibPath = path.join( appPath, @@ -1364,6 +1375,7 @@ printf '%s\\n' ok try { writeMachOFixture(peekabooPath); writeMachOFixture(connectorHelperPath); + writeMachOFixture(verifierPath); writeMachOFixture(pythonPath); fs.mkdirSync(path.dirname(pythonDylibPath), { recursive: true }); fs.writeFileSync(pythonDylibPath, Buffer.from('cffaedfe0c000001', 'hex')); @@ -2947,7 +2959,7 @@ printf '%s\\n' ok options: { wrongBundleVersion: true }, expected: /tag-bound bundle version/, }, - ...(['bridge', 'peekaboo', 'helper', 'python'] as const).map((payload) => ({ + ...(['bridge', 'peekaboo', 'helper', 'verifier', 'python'] as const).map((payload) => ({ name: `non-executable ${payload}`, options: { nonExecutablePayload: payload }, expected: /executable ZIP mode/, diff --git a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts index 03b23444bc..9d661e8419 100644 --- a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts +++ b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts @@ -8,8 +8,8 @@ import { symlinkSync, writeFileSync, } from 'node:fs'; -import { createHash } from 'node:crypto'; -import { execFileSync } from 'node:child_process'; +import { createHash, generateKeyPairSync, sign } from 'node:crypto'; +import { execFileSync, spawnSync } from 'node:child_process'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; @@ -59,6 +59,12 @@ const bridgeResource = require('../../../scripts/prepareEvaosDesktopBridgeResour }) => Record; bridgeWrapperMetadata: (filePath: string) => { schema: string; path: string; sourceSha256: string }; bridgeWrapperScript: () => string; + buildEd25519Verifier: (options?: { + sourcePath?: string; + targetDir?: string; + architecture?: string; + }) => { path: string; architecture: string; minimumMacOS: string; sourceSha256: string } | undefined; + ed25519VerifierBuildArgs: (sourcePath: string, outputPath: string, architecture: string) => string[]; installPythonRuntime: (sourcePath?: string, resourceDir?: string) => PythonRuntimeMetadata | undefined; writePythonRuntimeInventory: (resourceDir: string) => { inventoryPath: string; @@ -90,6 +96,65 @@ const { copyDir } = require('builder-util/out/fs') as { }; describe('prepareEvaosDesktopBridgeResource', () => { + it('pins the native verifier to the selected architecture and macOS 15', () => { + expect(bridgeResource.ed25519VerifierBuildArgs('/source.swift', '/output', 'arm64')).toEqual([ + 'swiftc', + '-O', + '-whole-module-optimization', + '-target', + 'arm64-apple-macos15.0', + '-o', + '/output', + '/source.swift', + ]); + expect(bridgeResource.ed25519VerifierBuildArgs('/source.swift', '/output', 'x64')).toContain( + 'x86_64-apple-macos15.0' + ); + expect(() => bridgeResource.ed25519VerifierBuildArgs('/source.swift', '/output', 'universal')).toThrow( + /Unsupported evaOS Ed25519 verifier architecture/ + ); + }); + + it.skipIf(process.platform !== 'darwin')( + 'builds a native verifier that accepts valid Ed25519 vectors only', + () => { + const dir = mkdtempSync(join(tmpdir(), 'evaos-ed25519-verifier-')); + try { + const metadata = bridgeResource.buildEd25519Verifier({ + targetDir: dir, + architecture: process.arch, + }); + expect(metadata).toMatchObject({ + path: 'bin/evaos-ed25519-verify', + architecture: process.arch, + minimumMacOS: '15.0', + }); + const executable = join(dir, 'evaos-ed25519-verify'); + const { privateKey, publicKey } = generateKeyPairSync('ed25519'); + const message = Buffer.from('evaos-native-ed25519-vector'); + const signature = sign(null, message, privateKey); + const publicKeyRaw = publicKey.export({ format: 'der', type: 'spki' }).subarray(-32); + const request = (signatureBytes: Buffer) => + Buffer.from( + JSON.stringify({ + publicKey: publicKeyRaw.toString('base64'), + message: message.toString('base64'), + signature: signatureBytes.toString('base64'), + }) + ); + + expect(spawnSync(executable, { input: request(signature) }).status).toBe(0); + const forged = Buffer.from(signature); + forged[0] ^= 1; + expect(spawnSync(executable, { input: request(forged) }).status).toBe(3); + expect(spawnSync(executable, { input: Buffer.from('{}') }).status).toBe(2); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, + 30_000 + ); + it('isolates the packaged desktop bridge wrapper from ambient Python paths', () => { const wrapper = bridgeResource.bridgeWrapperScript(); From 3e39c6dfabd4a7dbf296c2cb32cfd36875b1c2a0 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 06:16:27 +0700 Subject: [PATCH 03/17] fix(evaos): require signed Mac-control runtime proof --- .github/workflows/evaos-live-canary-proof.yml | 73 +-- .../common/evaos/nativeCompanionBoundary.ts | 2 +- .../services/evaosNativeCompanionStatus.ts | 4 + .../agent-tools/hermes-adapter/README.md | 1 - .../bin/evaos-desktop-bridge-command | 78 +-- .../agent-tools/openclaw-plugin/README.md | 16 +- .../agent-tools/openclaw-plugin/dist/index.js | 13 - .../openclaw-plugin/dist/src/bridge.js | 95 +-- .../dist/src/runtimeReceipt.js | 479 ++++++++++++++- .../agent-tools/openclaw-plugin/index.ts | 13 - .../openclaw-plugin/openclaw.plugin.json | 1 - .../agent-tools/openclaw-plugin/package.json | 4 +- .../runtime-receipt-negative-proof.mjs | 243 ++++++++ .../agent-tools/openclaw-plugin/src/bridge.ts | 109 +--- .../openclaw-plugin/src/runtimeReceipt.ts | 557 +++++++++++++++++- .../openclaw-plugin/src/types.d.ts | 29 +- .../tests/runtimeReceipt.test.mjs | 434 ++++++++++++-- .../evaos_desktop_bridge/connector_server.py | 12 +- .../src/evaos_desktop_bridge/qa_canary.py | 109 ++-- .../evaos_desktop_bridge/receipt_canary.py | 50 +- scripts/evaosBetaReleaseGate.js | 153 +++-- scripts/evaosBrokerLiveCanary.js | 237 +++++++- scripts/evaosScanMacControlProofs.js | 111 ++++ .../common-adapter/evaosIpcBridge.dom.test.ts | 61 ++ .../unit/evaos/evaosBrokerLiveCanary.test.ts | 206 +++++-- .../evaos/evaosDesktopBridgeReceipt.test.ts | 24 +- .../evaos/evaosLiveCanaryWorkflow.test.ts | 53 +- .../evaos/evaosNativeCompanionStatus.test.ts | 22 + .../unit/process/evaosBetaReleaseGate.test.ts | 208 ++++++- .../prepareEvaosDesktopBridgeResource.test.ts | 2 +- 30 files changed, 2770 insertions(+), 629 deletions(-) create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs create mode 100644 scripts/evaosScanMacControlProofs.js diff --git a/.github/workflows/evaos-live-canary-proof.yml b/.github/workflows/evaos-live-canary-proof.yml index b5356b6d5a..b2d9da4636 100644 --- a/.github/workflows/evaos-live-canary-proof.yml +++ b/.github/workflows/evaos-live-canary-proof.yml @@ -227,6 +227,14 @@ jobs: set -euo pipefail node scripts/evaosBrokerLiveCanary.js --mac-control > "$PROOF_DIR/mac-control-runtime.json" + - name: Prove Mac-control runtime-receipt negative boundaries + if: github.event.inputs.run_live_canaries == 'true' && github.event.inputs.run_mac_control_canary == 'true' + run: | + set -euo pipefail + npm --prefix resources/evaos-beta/bridge/agent-tools/openclaw-plugin install --ignore-scripts --omit=peer --no-audit --no-fund + npm --prefix resources/evaos-beta/bridge/agent-tools/openclaw-plugin run proof:runtime-receipt-negative -- \ + "$PROOF_DIR/mac-control-runtime-negative.json" "$GITHUB_SHA" "$GITHUB_RUN_ID" + - name: Run follow-up live canaries if: github.event.inputs.run_live_canaries == 'true' && github.event.inputs.run_followup_canaries == 'true' run: | @@ -260,70 +268,7 @@ jobs: if: always() && github.event.inputs.run_mac_control_canary == 'true' run: | set -euo pipefail - node - "$PROOF_DIR" <<'NODE' - const fs = require('fs'); - const path = require('path'); - - const proofDir = process.argv[2]; - const proofNames = [ - 'mac-control-runtime.json', - 'mac-control-session-provisioning.json', - 'mac-control-session-provisioning.stdout.json', - 'mac-control-session-cleanup.json', - 'mac-control-session-cleanup.stdout.json', - ]; - const forbiddenFields = new Set([ - 'accountEmail', - 'bindingId', - 'customerId', - 'customer_id', - 'desktopSession', - 'endpoint', - 'launchUrl', - 'session', - ]); - const forbiddenValue = /\beds_[A-Za-z0-9_-]{8,}\b|\bBearer\s+|\beyJ[A-Za-z0-9_-]{8,}\.|https?:\/\/|[?&]session=|\S+@\S+/i; - - function scan(value, location) { - if (typeof value === 'string' && forbiddenValue.test(value)) { - throw new Error(`Mac-control proof contains forbidden sensitive material at ${location}.`); - } - if (Array.isArray(value)) { - value.forEach((entry, index) => scan(entry, `${location}[${index}]`)); - return; - } - if (!value || typeof value !== 'object') return; - for (const [key, entry] of Object.entries(value)) { - if (forbiddenFields.has(key)) { - throw new Error(`Mac-control proof contains forbidden field ${key}.`); - } - scan(entry, `${location}.${key}`); - } - } - - let scanned = 0; - for (const proofName of proofNames) { - const proofPath = path.join(proofDir, proofName); - if (!fs.existsSync(proofPath)) continue; - scan(JSON.parse(fs.readFileSync(proofPath, 'utf8')), proofName); - scanned += 1; - } - if (scanned === 0) { - throw new Error('Mac-control secret/redaction scan found no proof artifacts.'); - } - const cleanupPath = path.join(proofDir, 'mac-control-session-cleanup.json'); - if (!fs.existsSync(cleanupPath)) { - throw new Error('Mac-control proof is missing sanitized cleanup evidence.'); - } - const cleanupProof = JSON.parse(fs.readFileSync(cleanupPath, 'utf8')); - if ( - cleanupProof.schema !== 'evaos-mac-control-canary-session-cleanup/v1' || - cleanupProof.sessionRevoked !== true || - cleanupProof.sensitiveOutput !== 'passed' - ) { - throw new Error('Mac-control proof did not prove temporary session revocation.'); - } - NODE + node scripts/evaosScanMacControlProofs.js "$PROOF_DIR" - name: Write summary if: always() diff --git a/packages/desktop/src/common/evaos/nativeCompanionBoundary.ts b/packages/desktop/src/common/evaos/nativeCompanionBoundary.ts index e3f9de5e82..b4f259a833 100644 --- a/packages/desktop/src/common/evaos/nativeCompanionBoundary.ts +++ b/packages/desktop/src/common/evaos/nativeCompanionBoundary.ts @@ -70,7 +70,7 @@ const EVAOS_EXPECTED_WORKBENCH_BUILD_ARG = const EVAOS_EXPECTED_SOURCE_COMMIT_ARG = '"${EVAOS_WORKBENCH_EXPECTED_SOURCE_COMMIT:?Set EVAOS_WORKBENCH_EXPECTED_SOURCE_COMMIT to the exact 40-character evaOS-GUI commit}"'; const EVAOS_SELECTED_BINDING_PROOF_ARG = - '"${EVAOS_MAC_CONTROL_LIVE_CANARY_PROOF:?Set EVAOS_MAC_CONTROL_LIVE_CANARY_PROOF to the sanitized selected-binding callback proof}"'; + '"${EVAOS_MAC_CONTROL_LIVE_CANARY_PROOF:?Set EVAOS_MAC_CONTROL_LIVE_CANARY_PROOF to the sanitized selected-binding runtime-receipt proof}"'; const EVAOS_SELECTED_BINDING_PROOF_RUN_ID_ARG = '"${EVAOS_MAC_CONTROL_LIVE_CANARY_RUN_ID:?Set EVAOS_MAC_CONTROL_LIVE_CANARY_RUN_ID to the exact proof workflow run id}"'; diff --git a/packages/desktop/src/process/services/evaosNativeCompanionStatus.ts b/packages/desktop/src/process/services/evaosNativeCompanionStatus.ts index 2df1b8ce51..0aec12f6d2 100644 --- a/packages/desktop/src/process/services/evaosNativeCompanionStatus.ts +++ b/packages/desktop/src/process/services/evaosNativeCompanionStatus.ts @@ -94,6 +94,7 @@ const SECURE_NETWORK_ENROLL_SETTLE_COMMAND_TIMEOUT_MS = 2000; let secureNetworkEnrollmentInFlight = false; export type EvaosNativeCompanionDiagnosticEventCode = + | 'secure_network_enrollment_provider_received' | 'secure_network_enrollment_action_started' | 'secure_network_enrollment_preflight_failed' | 'secure_network_enrollment_broker_request_started' @@ -2140,6 +2141,9 @@ export async function runNativeCompanionAction( request: IEvaosNativeCompanionActionRequest, deps: EvaosNativeCompanionStatusDeps = {} ): Promise { + if (request.action === 'secure_network_enroll') { + recordNativeCompanionDiagnosticEvent(deps, 'secure_network_enrollment_provider_received'); + } const existsSync = deps.existsSync ?? fs.existsSync; const bridgePath = resolveBridgeExecutable( deps.bridgePaths ?? defaultBridgePaths(deps.env, nativeCompanionIsPackaged(deps)), diff --git a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md index 85f695332d..05088a0193 100644 --- a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md +++ b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md @@ -25,7 +25,6 @@ fixed connector command names supported by `/v1/commands`, for example: ```bash hermes-adapter/bin/evaos-desktop-bridge-command customerMacStatus '{}' -hermes-adapter/bin/evaos-desktop-bridge-command customerMacControlStart '{"mode":"full-access","agent_label":"Hermes"}' hermes-adapter/bin/evaos-desktop-bridge-command desktopSee '{}' hermes-adapter/bin/evaos-desktop-bridge-command desktopClick '{"target_label":"Continue","dry_run":false}' hermes-adapter/bin/evaos-desktop-bridge-command customerMacIphoneMirroringStatus '{}' diff --git a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command index e62d871391..d1aab5a885 100755 --- a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command +++ b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command @@ -153,11 +153,6 @@ def materialize_visual_evidence(payload): def timeout_for_command(command): - if command in { - "customerMacControlStart", - "desktop_control_start", - }: - return 30 if command in { "desktopSee", "desktop_see", @@ -221,63 +216,6 @@ def timeout_for_command(command): return 10 -def normalize_control_mode(value): - text = str(value or "").strip().lower().replace("-", "_") - if text in {"full_access", "fullaccess"}: - return "full_access" - if text in {"ask_permission", "askpermission"}: - return "ask_permission" - return text - - -def requested_control_mode(params): - return normalize_control_mode(params.get("mode") or "full-access") - - -def status_matches_control_start(payload, mode): - if not isinstance(payload, dict) or not payload.get("ok"): - return False - data = payload.get("data") - if not isinstance(data, dict): - return False - session = data.get("session") if isinstance(data.get("session"), dict) else {} - active = data.get("active") - kill_switch = data.get("kill_switch") - actual_mode = data.get("mode") - if active is None: - active = session.get("active") - if kill_switch is None: - kill_switch = session.get("kill_switch") - if actual_mode is None: - actual_mode = session.get("mode") - return active is True and kill_switch is False and normalize_control_mode(actual_mode) == mode - - -def reconcile_control_start_after_abort(original_error): - if command not in {"customerMacControlStart", "desktop_control_start"}: - return None - status_body = json.dumps({"command": "customerMacControlStatus", "params": {}}, separators=(",", ":")).encode() - status_request = urllib.request.Request(url, data=status_body, method="POST", headers=headers) - with urllib.request.urlopen(status_request, timeout=15) as response: - status_payload = json.loads(response.read().decode()) - mode = requested_control_mode(params) - if not status_matches_control_start(status_payload, mode): - return None - warnings = status_payload.get("warnings") if isinstance(status_payload.get("warnings"), list) else [] - data = status_payload.get("data") if isinstance(status_payload.get("data"), dict) else {} - data["control_start_reconciled"] = True - data["control_start_original_error"] = type(original_error).__name__ - status_payload["data"] = data - status_payload["warnings"] = [ - { - "code": "control_start_response_reconciled_after_abort", - "message": "Control start response was interrupted after the local session became active; reconciled from control status.", - }, - *warnings, - ] - return status_payload - - def read_json_env(name): raw = os.environ.get(name) if not raw: @@ -641,6 +579,16 @@ if command == "completeEnrollment": }], }, separators=(",", ":")) + "\n") raise SystemExit(0) +if command in {"customerMacControlStart", "desktop_control_start"}: + sys.stdout.write(json.dumps({ + "ok": False, + "errors": [{ + "code": "control_start_local_only", + "message": "Remote agent tools cannot start or restart Mac control.", + "guidance": "Use the local evaOS Workbench app on the customer Mac.", + }], + }, separators=(",", ":")) + "\n") + raise SystemExit(0) headers = { @@ -675,10 +623,6 @@ except urllib.error.HTTPError as exc: raise SystemExit(0) from exc sys.stderr.write(error_body + "\n") raise SystemExit(exc.code) from exc -except (TimeoutError, socket.timeout, urllib.error.URLError, ConnectionError, http.client.HTTPException, OSError) as exc: - reconciled = reconcile_control_start_after_abort(exc) - if reconciled is not None: - sys.stdout.write(json.dumps(scrub_secrets(materialize_visual_evidence(reconciled)), separators=(",", ":")) + "\n") - raise SystemExit(0) from exc +except (TimeoutError, socket.timeout, urllib.error.URLError, ConnectionError, http.client.HTTPException, OSError): raise PY diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md index 37f6f5871f..f7812106ed 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md @@ -44,7 +44,6 @@ continue to use the `evaos-desktop-bridge-openclaw-vX.Y.Z` namespace. - `evaos_shared_browser_guidance` - `customer_mac_status` - `desktop_control_status` -- `desktop_control_start` - `desktop_control_stop` - `desktop_kill_switch` - `customer_mac_capabilities` @@ -160,9 +159,14 @@ The plugin registers one exact gateway-authenticated release-canary route at only a bounded random `challenge` and a nonsecret `runRef`. Connector address, token, customer, VM, and selected-binding authority are accepted only from the ws-proxy-owned headers. The signed execution context is verified with the -pinned `EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_KEY_ID` and raw 32-byte -`EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_PUBLIC_KEY_B64` before the connector can -be called. The route exposes no generic tool, proxy, action, or signing API. +pinned `EVAOS_MAC_CONTROL_CONTEXT_KEY_ID` and unpadded base64url raw 32-byte +`EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY`. Before the connector can be called, the +route also requires a pinned `EVAOS_MAC_CONTROL_RECEIPT_KEY_ID`, raw 32-byte +`EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY`, and exact expected source commit, +source digest, app version, and build. It verifies the connector SSHSIG and +every selected-scope, action, state, audit, expiry, and candidate claim, then +returns only the allowlisted `evaos.mac_control.runtime_proof.v2` view. The +route exposes no generic tool, proxy, action, raw receipt, or signing API. Local plugin tools call fixed bridge argv mappings with `shell: false`. Remote plugin tools post fixed command keys to `/v1/commands` on the paired Mac @@ -173,3 +177,7 @@ approval for guarded Codex visible GUI message sends and legacy guarded customer allows live action without `approval_audit_id`, Ask Permission gates risky clicks, taps, hotkeys, typing, sends, and other high-impact actions, and the kill switch blocks all live control immediately. + +Starting or restarting Mac control is local-only in evaOS Workbench. OpenClaw +and Hermes do not advertise a remote control-start tool; status, stop, and the +kill switch remain available remotely. diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js index 3905f66c3d..8817d8a3f0 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js @@ -278,19 +278,6 @@ function readOnlyTools() { 'Read the customer-granted Full Access / Ask Permission control session state.', 'customerMacControlStatus' ), - tool( - 'desktop_control_start', - 'Start a customer-granted agent control session. Live actions wait for the 10-second operator takeover warning; Full Access then allows continuous desktop and iPhone actions without per-action prompts.', - 'customerMacControlStart', - { - type: 'object', - additionalProperties: false, - properties: { - mode: { type: 'string', enum: ['full-access', 'ask-permission'], default: 'full-access' }, - agent_label: { type: 'string', minLength: 1, maxLength: 160 }, - }, - } - ), tool('desktop_control_stop', 'Stop the active customer-granted agent control session.', 'customerMacControlStop'), tool( 'desktop_kill_switch', diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js index 1bb80efafa..f73eb97e98 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js @@ -27,7 +27,6 @@ const CUSTOMER_MAC_REMOTE_COMMANDS = new Set([ 'customerMacStatus', 'customerMacCapabilities', 'customerMacControlStatus', - 'customerMacControlStart', 'customerMacControlStop', 'customerMacControlKillSwitch', 'desktopSee', @@ -180,17 +179,6 @@ export function buildBridgeArgv(command, params = {}) { if (command === 'customerMacAxTree') { return ['customer-mac', 'ax-tree', '--json', '--max-nodes', String(clampInt(params.max_nodes, 200, 1, 1000))]; } - if (command === 'customerMacControlStart') { - return [ - 'customer-mac', - 'control', - 'start', - '--json', - '--mode', - String(params.mode || 'full-access'), - ...(params.agent_label ? ['--agent-label', String(params.agent_label)] : []), - ]; - } if (command === 'desktopSee') { return [ 'customer-mac', @@ -627,6 +615,18 @@ function optionalStringArg(value, flag) { return [flag, value.trim()]; } export async function runBridge(command, params = {}) { + if (String(command) === 'customerMacControlStart' || String(command) === 'desktop_control_start') { + return { + ok: false, + errors: [ + { + code: 'control_start_local_only', + message: 'Remote agent tools cannot start or restart Mac control.', + guidance: 'Use the local evaOS Workbench app on the customer Mac.', + }, + ], + }; + } if (String(command) === 'customerMacCompletePairing') { return { ok: false, @@ -1209,12 +1209,6 @@ async function runRemoteBridge(remoteURL, command, params) { try { return await postRemoteBridgeCommand(endpoint, headers, remoteURL, command, params, timeoutForCommand(command)); } catch (error) { - if (command === 'customerMacControlStart' && isAbortLikeError(error)) { - const reconciled = await reconcileControlStartAfterAbort(endpoint, headers, remoteURL, params); - if (reconciled) { - return reconciled; - } - } const err = error; return { ok: false, @@ -1257,75 +1251,10 @@ async function postRemoteBridgeCommand(endpoint, headers, remoteURL, command, pa clearTimeout(timeout); } } -async function reconcileControlStartAfterAbort(endpoint, headers, remoteURL, params) { - try { - const status = await postRemoteBridgeCommand(endpoint, headers, remoteURL, 'customerMacControlStatus', {}, 15_000); - if (!isControlSessionActive(status, params)) { - return null; - } - const payload = isRecord(status) ? { ...status } : { ok: true }; - const data = isRecord(payload.data) ? { ...payload.data } : {}; - payload.ok = true; - payload.data = { - ...data, - control_start_reconciled: true, - }; - payload.warnings = [ - ...(Array.isArray(payload.warnings) ? payload.warnings : []), - { - code: 'control_start_response_reconciled_after_abort', - message: - 'Mac control start timed out after the connector activated the control session; status was reconciled from the paired Mac.', - guidance: 'Continue with desktop_control_status and desktop_see before running live Mac-control actions.', - }, - ]; - return payload; - } catch { - return null; - } -} -function isAbortLikeError(error) { - const err = error; - return err.name === 'AbortError' || /abort/i.test(err.message || ''); -} -function isControlSessionActive(status, params) { - if (!isRecord(status) || status.ok !== true || !isRecord(status.data)) { - return false; - } - const data = status.data; - const session = isRecord(data.session) ? data.session : {}; - const active = data.active === true || session.active === true; - const killSwitch = - data.kill_switch === true || - data.killSwitch === true || - session.kill_switch === true || - session.killSwitch === true; - const mode = controlModeFromStatus(data.mode) ?? controlModeFromStatus(session.mode); - return active && !killSwitch && mode === requestedControlMode(params.mode); -} -function requestedControlMode(mode) { - return controlModeFromStatus(mode) ?? 'full_access'; -} -function controlModeFromStatus(mode) { - if (typeof mode !== 'string') { - return undefined; - } - const normalized = mode.trim().toLowerCase().replace(/-/g, '_'); - if (normalized === 'ask_permission') { - return 'ask_permission'; - } - if (normalized === 'full_access') { - return 'full_access'; - } - return undefined; -} function timeoutForCommand(command) { if (command === 'codexLiveStatus') { return 35_000; } - if (command === 'customerMacControlStart') { - return 30_000; - } if ( command === 'desktopSee' || command === 'iphoneSee' || diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js index 65f5b1195f..3eadf0eae5 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js @@ -1,12 +1,18 @@ -import { createPublicKey, verify } from 'node:crypto'; +import { createHash, createPublicKey, timingSafeEqual, verify } from 'node:crypto'; export const MAC_CONTROL_RUNTIME_RECEIPT_PATH = '/api/v1/evaos/mac-control/runtime-receipt'; const CONTEXT_SCHEMA = 'evaos.mac_control_execution_context.v1'; const CONTRACT_SCHEMA = 'evaos.mac_control_runtime_contract.v2'; const CONNECTOR_REQUEST_SCHEMA = 'evaos.mac_control.canary_request.v1'; const CONNECTOR_RESPONSE_SCHEMA = 'evaos.mac_control.runtime_receipt_envelope.v1'; +const RECEIPT_SCHEMA = 'evaos.mac_control.runtime_receipt.v1'; const RECEIPT_NAMESPACE = 'evaos-mac-control-receipt-v1'; +const PUBLIC_PROOF_SCHEMA = 'evaos.mac_control.runtime_proof.v2'; +const PUBLIC_PROOF_KIND = 'selected_binding_direct_mac_control'; +const PUBLIC_PROOF_SOURCE = 'evaos-desktop-bridge:runtime-receipt'; const CONTEXT_TTL_SECONDS = 60; const CLOCK_SKEW_SECONDS = 5; +const DEFAULT_CONNECTOR_TIMEOUT_MS = 10_000; +const AUTHORITY_DEADLINE_SAFETY_MS = 250; const MAX_REQUEST_BYTES = 4096; const MAX_CONNECTOR_RESPONSE_BYTES = 65536; const CONTEXT_FIELDS = [ @@ -22,6 +28,65 @@ const CONTEXT_FIELDS = [ 'context_id', ]; const RESPONSE_FIELDS = ['schema', 'receiptBase64', 'signature', 'keyId', 'namespace']; +const RECEIPT_FIELDS = [ + 'schema', + 'keyId', + 'namespace', + 'executedAt', + 'runtime', + 'challenge', + 'runRef', + 'contextKeyId', + 'executionContextDigest', + 'contextRef', + 'contextIssuedAt', + 'contextExpiresAt', + 'customerRef', + 'vmRef', + 'bindingRef', + 'bindingVersion', + 'bindingExpiresAt', + 'sessionRef', + 'controlStateBefore', + 'controlStateAfter', + 'controlStateBeforeDigest', + 'controlStateAfterDigest', + 'candidate', + 'action', + 'actionArgsDigest', + 'auditId', + 'auditTimestamp', + 'auditRecordDigest', +]; +const CONTROL_STATE_FIELDS = ['active', 'generation', 'killSwitch', 'mode', 'ready', 'takeoverActive']; +const CANDIDATE_FIELDS = [ + 'sourceCommit', + 'sourceSha256', + 'sourcePath', + 'sourceOwner', + 'status', + 'appPath', + 'appVersion', + 'appBuild', + 'appBundleId', + 'appName', + 'executable', + 'argv0', + 'owner', +]; +const OWNER_FIELDS = [ + 'label', + 'classification', + 'bundleId', + 'sourceCommit', + 'programPath', + 'appPath', + 'manifestPath', + 'plistPath', +]; +const PATH_FIELDS = ['kind', 'value']; +const ACTION_FIELDS = ['command', 'args']; +const ACTION_ARGS_FIELDS = ['keys', 'dryRun']; const HEADER = { context: 'x-evaos-mac-control-execution-context', contextSignature: 'x-evaos-mac-control-execution-context-signature', @@ -51,6 +116,7 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { const fetchImpl = options.fetchImpl ?? globalThis.fetch; const now = options.now ?? Date.now; const replayCache = options.replayCache ?? new Map(); + const configuredConnectorTimeoutMs = options.connectorTimeoutMs ?? DEFAULT_CONNECTOR_TIMEOUT_MS; return async (request, response) => { if ((request.method ?? 'GET').toUpperCase() !== 'POST') { response.setHeader('Allow', 'POST'); @@ -67,6 +133,20 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { sendError(response, authority.status, authority.code); return true; } + const receiptVerifier = loadReceiptVerifierConfig(env); + if (!receiptVerifier.ok) { + sendError(response, 503, 'receipt_verifier_unavailable'); + return true; + } + const remainingAuthorityMs = + Math.min(authority.value.context.expires_at * 1000, Date.parse(authority.value.bindingExpiresAt)) - + now() - + AUTHORITY_DEADLINE_SAFETY_MS; + if (remainingAuthorityMs <= 0) { + sendError(response, 401, 'execution_context_expired'); + return true; + } + const connectorTimeoutMs = Math.max(1, Math.min(configuredConnectorTimeoutMs, remainingAuthorityMs)); pruneReplayCache(replayCache, now()); if (replayCache.has(authority.value.context.context_id)) { sendError(response, 409, 'execution_context_replayed'); @@ -89,6 +169,8 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { }, }; let connectorResponse; + const controller = new AbortController(); + const connectorTimeout = setTimeout(() => controller.abort(), connectorTimeoutMs); try { connectorResponse = await fetchImpl(`${authority.value.connectorUrl}/v1/canary/mac-control`, { method: 'POST', @@ -98,10 +180,13 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify(connectorBody), + signal: controller.signal, }); } catch { sendError(response, 502, 'connector_unavailable'); return true; + } finally { + clearTimeout(connectorTimeout); } if (!connectorResponse.ok) { sendError( @@ -118,7 +203,12 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { sendError(response, 502, 'connector_response_invalid'); return true; } - const sanitized = validateConnectorEnvelope(rawConnectorResponse, authority.value); + const sanitized = validateConnectorEnvelope( + rawConnectorResponse, + authority.value, + publicRequest.value, + receiptVerifier.value + ); if (!sanitized.ok) { sendError(response, 502, 'connector_response_invalid'); return true; @@ -167,12 +257,12 @@ async function readPublicRequest(request) { return { ok: true, value: { challenge: body.challenge, runRef: body.runRef } }; } function verifyAuthority(headers, env, nowMs) { - const expectedKeyId = env.EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_KEY_ID; - const publicKeyBase64 = env.EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_PUBLIC_KEY_B64; + const expectedKeyId = env.EVAOS_MAC_CONTROL_CONTEXT_KEY_ID; + const publicKeyBase64 = env.EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY; if (!expectedKeyId || !publicKeyBase64 || !validKeyId(expectedKeyId)) { return { ok: false, status: 503, code: 'execution_context_verifier_unavailable' }; } - const rawPublicKey = decodeCanonicalBase64(publicKeyBase64); + const rawPublicKey = decodeBase64Url(publicKeyBase64); if (!rawPublicKey || rawPublicKey.byteLength !== 32) { return { ok: false, status: 503, code: 'execution_context_verifier_unavailable' }; } @@ -283,7 +373,45 @@ function verifyAuthority(headers, env, nowMs) { }, }; } -function validateConnectorEnvelope(raw, authority) { +function loadReceiptVerifierConfig(env) { + const keyId = env.EVAOS_MAC_CONTROL_RECEIPT_KEY_ID; + const encodedPublicKey = env.EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY; + const expectedSourceCommit = env.EVAOS_MAC_CONTROL_EXPECTED_SOURCE_COMMIT; + const expectedSourceSha256 = env.EVAOS_MAC_CONTROL_EXPECTED_SOURCE_SHA256; + const expectedAppVersion = env.EVAOS_MAC_CONTROL_EXPECTED_APP_VERSION; + const expectedAppBuild = env.EVAOS_MAC_CONTROL_EXPECTED_APP_BUILD; + if ( + !keyId || + !validKeyId(keyId) || + !encodedPublicKey || + !expectedSourceCommit || + !/^[0-9a-f]{40}$/.test(expectedSourceCommit) || + !expectedSourceSha256 || + !validSha256(expectedSourceSha256) || + !expectedAppVersion || + !validReleaseValue(expectedAppVersion) || + !expectedAppBuild || + !validReleaseValue(expectedAppBuild) + ) { + return { ok: false }; + } + const publicKey = decodeBase64Url(encodedPublicKey); + if (!publicKey || publicKey.byteLength !== 32) { + return { ok: false }; + } + return { + ok: true, + value: { + keyId, + publicKey, + expectedSourceCommit, + expectedSourceSha256, + expectedAppVersion, + expectedAppBuild, + }, + }; +} +function validateConnectorEnvelope(raw, authority, request, verifier) { if (Buffer.from(raw).byteLength > MAX_CONNECTOR_RESPONSE_BYTES) { return { ok: false }; } @@ -305,7 +433,7 @@ function validateConnectorEnvelope(raw, authority) { typeof parsed.signature !== 'string' || parsed.signature.length > 8192 || typeof parsed.keyId !== 'string' || - !validKeyId(parsed.keyId) + parsed.keyId !== verifier.keyId ) { return { ok: false }; } @@ -332,7 +460,339 @@ function validateConnectorEnvelope(raw, authority) { ) { return { ok: false }; } - return { ok: true, value: parsed }; + if (!verifySshSignature(receiptBytes, parsed.signature, verifier.publicKey)) { + return { ok: false }; + } + return validateReceipt(receiptBytes, authority, request, verifier); +} +function validateReceipt(receiptBytes, authority, request, verifier) { + let parsed; + try { + parsed = JSON.parse(receiptBytes.toString('utf8')); + } catch { + return { ok: false }; + } + if (!isRecord(parsed) || !hasExactKeys(parsed, RECEIPT_FIELDS)) { + return { ok: false }; + } + const canonical = canonicalJson(parsed); + if (!canonical || !timingSafeBufferEqual(Buffer.from(receiptBytes), Buffer.from(canonical, 'utf8'))) { + return { ok: false }; + } + const contextPayload = decodeBase64Url(authority.contextPayload); + const before = parsed.controlStateBefore; + const after = parsed.controlStateAfter; + const candidate = parsed.candidate; + const action = parsed.action; + if ( + !contextPayload || + parsed.schema !== RECEIPT_SCHEMA || + parsed.keyId !== verifier.keyId || + parsed.namespace !== RECEIPT_NAMESPACE || + parsed.runtime !== 'openclaw' || + parsed.challenge !== request.challenge || + parsed.runRef !== request.runRef || + parsed.contextKeyId !== authority.contextKeyId || + parsed.contextIssuedAt !== authority.context.issued_at || + parsed.contextExpiresAt !== authority.context.expires_at || + parsed.bindingVersion !== authority.context.binding_version || + parsed.bindingExpiresAt !== authority.bindingExpiresAt || + parsed.executionContextDigest !== saltedHash(request.challenge, contextPayload) || + parsed.contextRef !== saltedHash(request.challenge, authority.context.context_id) || + parsed.customerRef !== saltedHash(request.challenge, authority.context.customer_id) || + parsed.vmRef !== saltedHash(request.challenge, authority.context.customer_vm_id) || + parsed.bindingRef !== saltedHash(request.challenge, authority.context.binding_id) || + !isRecord(before) || + !isRecord(after) || + !validControlState(before) || + !validControlState(after) || + canonicalJson(before) !== canonicalJson(after) || + parsed.sessionRef !== + saltedHash(request.challenge, `${authority.context.binding_id}\0${String(before.generation)}`) || + parsed.controlStateBeforeDigest !== sha256Hex(canonicalJsonBytes(before)) || + parsed.controlStateAfterDigest !== sha256Hex(canonicalJsonBytes(after)) || + !validCandidate(candidate, verifier) || + !validReceiptAction(action) || + parsed.actionArgsDigest !== sha256Hex(canonicalJsonBytes(action.args)) || + typeof parsed.auditId !== 'string' || + !/^audit-[0-9A-Za-z_-]{8,128}$/.test(parsed.auditId) || + typeof parsed.auditRecordDigest !== 'string' || + !validSha256(parsed.auditRecordDigest) + ) { + return { ok: false }; + } + const executedAt = parseUtcTimestamp(parsed.executedAt); + const auditTimestamp = parseUtcTimestamp(parsed.auditTimestamp); + const bindingExpiresAt = parseUtcTimestamp(parsed.bindingExpiresAt); + if ( + executedAt === undefined || + auditTimestamp === undefined || + bindingExpiresAt === undefined || + executedAt < (authority.context.issued_at - CLOCK_SKEW_SECONDS) * 1000 || + executedAt > authority.context.expires_at * 1000 || + auditTimestamp < (authority.context.issued_at - CLOCK_SKEW_SECONDS) * 1000 || + auditTimestamp > executedAt + CLOCK_SKEW_SECONDS * 1000 || + authority.context.expires_at * 1000 > bindingExpiresAt + ) { + return { ok: false }; + } + const typedCandidate = candidate; + return { + ok: true, + value: { + ok: true, + schema: PUBLIC_PROOF_SCHEMA, + proofKind: PUBLIC_PROOF_KIND, + tool: 'customer_mac.desktop_hotkey', + outcome: 'succeeded', + runRef: request.runRef, + executedAt: parsed.executedAt, + bindingRef: parsed.bindingRef, + bindingVersion: authority.context.binding_version, + sessionRef: parsed.sessionRef, + expiresAt: authority.context.expires_at, + auditRef: saltedHash(request.challenge, parsed.auditId), + sourcePointer: PUBLIC_PROOF_SOURCE, + candidate: { + sourceCommit: typedCandidate.sourceCommit, + sourceSha256: typedCandidate.sourceSha256, + appVersion: typedCandidate.appVersion, + appBuild: typedCandidate.appBuild, + }, + }, + }; +} +function validControlState(value) { + return ( + hasExactKeys(value, CONTROL_STATE_FIELDS) && + value.active === true && + Number.isInteger(value.generation) && + value.generation >= 0 && + value.killSwitch === false && + value.mode === 'full_access' && + value.ready === true && + value.takeoverActive === false + ); +} +function validCandidate(value, verifier) { + if (!isRecord(value) || !hasExactKeys(value, CANDIDATE_FIELDS)) { + return false; + } + const owner = value.owner; + if (!isRecord(owner) || !hasExactKeys(owner, OWNER_FIELDS)) { + return false; + } + return ( + value.sourceCommit === verifier.expectedSourceCommit && + value.sourceSha256 === verifier.expectedSourceSha256 && + value.sourcePath === 'resources/evaos-beta/bridge' && + value.sourceOwner === '100yenadmin/evaOS-GUI' && + value.status === 'vendored' && + value.appPath === '/Applications/evaOS Workbench.app' && + value.appVersion === verifier.expectedAppVersion && + value.appBuild === verifier.expectedAppBuild && + value.appBundleId === 'com.evaos.workbench' && + value.appName === 'evaOS Workbench' && + typeof value.executable === 'string' && + /^\/Applications\/evaOS Workbench\.app\/Contents\/Resources\/Bridge\/python\/bin\/python3(?:\.12)?$/.test( + value.executable + ) && + value.argv0 === '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py' && + owner.label === 'com.electricsheep.evaos-desktop-bridge' && + owner.classification === 'workbench_bundle' && + owner.bundleId === 'com.evaos.workbench' && + owner.sourceCommit === verifier.expectedSourceCommit && + validPathClaim( + owner.programPath, + '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge' + ) && + validPathClaim(owner.appPath, '/Applications/evaOS Workbench.app') && + validPathClaim(owner.manifestPath, '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/manifest.json') && + isRecord(owner.plistPath) && + hasExactKeys(owner.plistPath, PATH_FIELDS) && + owner.plistPath.kind === 'path' && + typeof owner.plistPath.value === 'string' && + owner.plistPath.value.endsWith('/Library/LaunchAgents/com.electricsheep.evaos-desktop-bridge.plist') + ); +} +function validPathClaim(value, expected) { + return isRecord(value) && hasExactKeys(value, PATH_FIELDS) && value.kind === 'path' && value.value === expected; +} +function validReceiptAction(value) { + return ( + isRecord(value) && + hasExactKeys(value, ACTION_FIELDS) && + value.command === 'customer_mac.desktop_hotkey' && + isRecord(value.args) && + hasExactKeys(value.args, ACTION_ARGS_FIELDS) && + value.args.keys === 'escape' && + value.args.dryRun === false + ); +} +function verifySshSignature(message, armor, pinnedPublicKey) { + const decoded = decodeSshSignatureArmor(armor); + if (!decoded) { + return false; + } + try { + const reader = sshReader(decoded); + if (reader.readRaw(6).toString('ascii') !== 'SSHSIG' || reader.readUInt32() !== 1) { + return false; + } + const publicKeyBlob = reader.readString(); + const namespace = reader.readString().toString('utf8'); + const reserved = reader.readString(); + const hashAlgorithm = reader.readString().toString('ascii'); + const signatureBlob = reader.readString(); + if (!reader.done() || namespace !== RECEIPT_NAMESPACE || reserved.byteLength !== 0) { + return false; + } + const publicKeyReader = sshReader(publicKeyBlob); + const publicKeyAlgorithm = publicKeyReader.readString().toString('ascii'); + const embeddedPublicKey = publicKeyReader.readString(); + const signatureReader = sshReader(signatureBlob); + const signatureAlgorithm = signatureReader.readString().toString('ascii'); + const signature = signatureReader.readString(); + if ( + !publicKeyReader.done() || + !signatureReader.done() || + publicKeyAlgorithm !== 'ssh-ed25519' || + signatureAlgorithm !== 'ssh-ed25519' || + embeddedPublicKey.byteLength !== 32 || + signature.byteLength !== 64 || + !timingSafeBufferEqual(embeddedPublicKey, Buffer.from(pinnedPublicKey)) || + (hashAlgorithm !== 'sha256' && hashAlgorithm !== 'sha512') + ) { + return false; + } + const digest = createHash(hashAlgorithm).update(Buffer.from(message)).digest(); + const signedData = Buffer.concat([ + Buffer.from('SSHSIG', 'ascii'), + encodeSshString(Buffer.from(namespace, 'utf8')), + encodeSshString(Buffer.alloc(0)), + encodeSshString(Buffer.from(hashAlgorithm, 'ascii')), + encodeSshString(digest), + ]); + const spkiPrefix = Buffer.from('302a300506032b6570032100', 'hex'); + const publicKey = createPublicKey({ + key: Buffer.concat([spkiPrefix, embeddedPublicKey]), + format: 'der', + type: 'spki', + }); + return verify(null, signedData, publicKey, signature); + } catch { + return false; + } +} +function decodeSshSignatureArmor(value) { + const match = /^-----BEGIN SSH SIGNATURE-----\n([A-Za-z0-9+/=\n]+)-----END SSH SIGNATURE-----\n?$/.exec(value); + if (!match) { + return undefined; + } + const lines = match[1].split('\n').filter((line) => line.length > 0); + if (lines.length === 0 || lines.some((line) => line.length > 76 || !/^[A-Za-z0-9+/=]+$/.test(line))) { + return undefined; + } + const decoded = decodeCanonicalBase64(lines.join('')); + return decoded ? Buffer.from(decoded) : undefined; +} +function sshReader(value) { + const buffer = Buffer.from(value); + let offset = 0; + return { + readRaw(length) { + if (!Number.isInteger(length) || length < 0 || offset + length > buffer.byteLength) { + throw new Error('invalid SSH signature field'); + } + const result = buffer.subarray(offset, offset + length); + offset += length; + return result; + }, + readUInt32() { + if (offset + 4 > buffer.byteLength) { + throw new Error('invalid SSH signature integer'); + } + const result = buffer.readUInt32BE(offset); + offset += 4; + return result; + }, + readString() { + const length = this.readUInt32(); + if (length > MAX_CONNECTOR_RESPONSE_BYTES) { + throw new Error('oversized SSH signature field'); + } + return this.readRaw(length); + }, + done() { + return offset === buffer.byteLength; + }, + }; +} +function encodeUInt32(value) { + const output = Buffer.alloc(4); + output.writeUInt32BE(value, 0); + return output; +} +function encodeSshString(value) { + const buffer = Buffer.from(value); + return Buffer.concat([encodeUInt32(buffer.byteLength), buffer]); +} +function timingSafeBufferEqual(left, right) { + const leftBuffer = Buffer.from(left); + const rightBuffer = Buffer.from(right); + return leftBuffer.byteLength === rightBuffer.byteLength && timingSafeEqual(leftBuffer, rightBuffer); +} +function canonicalJson(value) { + try { + return JSON.stringify(sortJson(value)); + } catch { + return undefined; + } +} +function canonicalJsonBytes(value) { + const encoded = canonicalJson(value); + if (encoded === undefined) { + throw new Error('value is not canonical JSON'); + } + return Buffer.from(encoded, 'utf8'); +} +function sortJson(value) { + if (Array.isArray(value)) { + return value.map(sortJson); + } + if (!isRecord(value)) { + return value; + } + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, sortJson(value[key])]) + ); +} +function sha256Hex(value) { + return createHash('sha256').update(Buffer.from(value)).digest('hex'); +} +function saltedHash(challenge, value) { + return sha256Hex( + Buffer.concat([ + Buffer.from(challenge, 'ascii'), + Buffer.from([0]), + typeof value === 'string' ? Buffer.from(value, 'utf8') : Buffer.from(value), + ]) + ); +} +function parseUtcTimestamp(value) { + if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(value)) { + return undefined; + } + const parsed = Date.parse(value); + return Number.isFinite(parsed) ? parsed : undefined; +} +function validSha256(value) { + return /^[0-9a-f]{64}$/.test(value); +} +function validReleaseValue(value) { + return /^[0-9A-Za-z][0-9A-Za-z._+-]{0,63}$/.test(value); } function validConnectorUrl(value) { let parsed; @@ -418,8 +878,9 @@ function validSshSignature(value) { return /^-----BEGIN SSH SIGNATURE-----\n(?:[A-Za-z0-9+/=]{1,76}\n)+-----END SSH SIGNATURE-----\n?$/.test(value); } function hasExactKeys(value, keys) { + const expected = new Set(keys); const actual = Object.keys(value); - return actual.length === keys.length && actual.every((key, index) => key === keys[index]); + return actual.length === expected.size && actual.every((key) => expected.has(key)); } function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts index e115b9ed99..6dd4fee154 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts @@ -301,19 +301,6 @@ function readOnlyTools(): ToolDefinition[] { 'Read the customer-granted Full Access / Ask Permission control session state.', 'customerMacControlStatus' ), - tool( - 'desktop_control_start', - 'Start a customer-granted agent control session. Live actions wait for the 10-second operator takeover warning; Full Access then allows continuous desktop and iPhone actions without per-action prompts.', - 'customerMacControlStart', - { - type: 'object', - additionalProperties: false, - properties: { - mode: { type: 'string', enum: ['full-access', 'ask-permission'], default: 'full-access' }, - agent_label: { type: 'string', minLength: 1, maxLength: 160 }, - }, - } - ), tool('desktop_control_stop', 'Stop the active customer-granted agent control session.', 'customerMacControlStop'), tool( 'desktop_kill_switch', diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/openclaw.plugin.json b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/openclaw.plugin.json index a8099a497b..4a56c9fee9 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/openclaw.plugin.json +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/openclaw.plugin.json @@ -44,7 +44,6 @@ "evaos_shared_browser_guidance", "customer_mac_status", "desktop_control_status", - "desktop_control_start", "desktop_control_stop", "desktop_kill_switch", "customer_mac_capabilities", diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package.json b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package.json index 537ffc4822..21ae577cef 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package.json +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package.json @@ -22,7 +22,8 @@ }, "scripts": { "build": "tsc -p tsconfig.json && oxfmt dist", - "test": "npm run build && node --test tests/*.test.mjs" + "test": "npm run build && node --test tests/*.test.mjs", + "proof:runtime-receipt-negative": "npm run build && node scripts/runtime-receipt-negative-proof.mjs" }, "devDependencies": { "oxfmt": "0.41.0", @@ -69,7 +70,6 @@ "evaos_shared_browser_guidance", "customer_mac_status", "desktop_control_status", - "desktop_control_start", "desktop_control_stop", "desktop_kill_switch", "customer_mac_capabilities", diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs new file mode 100644 index 0000000000..4fbff1a29b --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs @@ -0,0 +1,243 @@ +import assert from 'node:assert/strict'; +import { generateKeyPairSync, sign } from 'node:crypto'; +import { writeFileSync } from 'node:fs'; +import { Readable } from 'node:stream'; + +import { createMacControlRuntimeReceiptHandler } from '../dist/src/runtimeReceipt.js'; + +const [outputPath, sourceHeadSha, sourceRunId] = process.argv.slice(2); +if (!outputPath || !/^[0-9a-f]{40}$/i.test(sourceHeadSha || '') || !/^\d+$/.test(sourceRunId || '')) { + throw new Error('Runtime-receipt negative proof requires output path and exact GitHub source provenance.'); +} + +const nowMs = Date.now(); +const nowSeconds = Math.floor(nowMs / 1000); +const contextKeyId = 'runtime-receipt-negative-context'; +const receiptKeyId = 'runtime-receipt-negative-receipt'; +const connectorToken = 'negative-proof-connector-token-value'; +const customerId = 'negative-proof-customer'; +const customerVmId = 'negative-proof-vm'; +const bindingId = 'negative-proof-binding'; +const bindingVersion = '7'; +const connectorUrl = 'http://100.64.10.12:8765'; +const { privateKey: contextPrivateKey, publicKey: contextPublicKey } = generateKeyPairSync('ed25519'); +const { publicKey: receiptPublicKey } = generateKeyPairSync('ed25519'); +const rawContextPublicKey = contextPublicKey.export({ format: 'der', type: 'spki' }).subarray(-32); +const rawReceiptPublicKey = receiptPublicKey.export({ format: 'der', type: 'spki' }).subarray(-32); +const verifierEnv = { + EVAOS_MAC_CONTROL_CONTEXT_KEY_ID: contextKeyId, + EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY: rawContextPublicKey.toString('base64url'), + EVAOS_MAC_CONTROL_RECEIPT_KEY_ID: receiptKeyId, + EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY: rawReceiptPublicKey.toString('base64url'), + EVAOS_MAC_CONTROL_EXPECTED_SOURCE_COMMIT: sourceHeadSha.toLowerCase(), + EVAOS_MAC_CONTROL_EXPECTED_SOURCE_SHA256: 'b'.repeat(64), + EVAOS_MAC_CONTROL_EXPECTED_APP_VERSION: '2.1.36', + EVAOS_MAC_CONTROL_EXPECTED_APP_BUILD: '2.1.36', +}; + +const assertions = { + forgedContextRejected: await proveForgedContextRejected(), + expiredContextRejected: await proveExpiredContextRejected(), + replayRejected: await proveReplayRejected(), + authorityRedacted: await proveAuthorityRedacted(), +}; +assert.deepEqual(assertions, { + forgedContextRejected: true, + expiredContextRejected: true, + replayRejected: true, + authorityRedacted: true, +}); + +writeFileSync( + outputPath, + `${JSON.stringify( + { + schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', + sourceHeadSha: sourceHeadSha.toLowerCase(), + sourceRunId, + assertions, + }, + null, + 2 + )}\n`, + { encoding: 'utf8', mode: 0o600, flag: 'wx' } +); + +async function proveForgedContextRejected() { + let connectorCalls = 0; + const authority = signedAuthority({ contextIdByte: 1 }); + authority.headers['x-evaos-mac-control-execution-context-signature'] = Buffer.alloc(64, 9).toString('base64url'); + const handler = createMacControlRuntimeReceiptHandler({ + env: verifierEnv, + now: () => nowMs, + fetchImpl: async () => { + connectorCalls += 1; + return fetchResponse(500, {}); + }, + }); + const response = await invoke(handler, requestBody(1), authority.headers); + assert.equal(response.statusCode, 401); + assert.equal(JSON.parse(response.body).error.code, 'execution_context_signature_invalid'); + assert.equal(connectorCalls, 0); + assertAuthorityRedacted(response.body, authority); + return true; +} + +async function proveExpiredContextRejected() { + let connectorCalls = 0; + const authority = signedAuthority({ + issuedAt: nowSeconds - 70, + expiresAt: nowSeconds - 10, + contextIdByte: 2, + }); + const handler = createMacControlRuntimeReceiptHandler({ + env: verifierEnv, + now: () => nowMs, + fetchImpl: async () => { + connectorCalls += 1; + return fetchResponse(500, {}); + }, + }); + const response = await invoke(handler, requestBody(2), authority.headers); + assert.equal(response.statusCode, 401); + assert.equal(JSON.parse(response.body).error.code, 'execution_context_expired'); + assert.equal(connectorCalls, 0); + assertAuthorityRedacted(response.body, authority); + return true; +} + +async function proveReplayRejected() { + let connectorCalls = 0; + const authority = signedAuthority({ contextIdByte: 3 }); + const handler = createMacControlRuntimeReceiptHandler({ + env: verifierEnv, + now: () => nowMs, + fetchImpl: async () => { + connectorCalls += 1; + return fetchResponse(503, {}); + }, + }); + const body = requestBody(3); + const first = await invoke(handler, body, authority.headers); + const replay = await invoke(handler, body, authority.headers); + assert.equal(first.statusCode, 502); + assert.equal(JSON.parse(first.body).error.code, 'connector_rejected_canary'); + assert.equal(replay.statusCode, 409); + assert.equal(JSON.parse(replay.body).error.code, 'execution_context_replayed'); + assert.equal(connectorCalls, 1); + assertAuthorityRedacted(first.body, authority); + assertAuthorityRedacted(replay.body, authority); + return true; +} + +async function proveAuthorityRedacted() { + const authority = signedAuthority({ contextIdByte: 4 }); + const leakingReceipt = Buffer.from( + JSON.stringify({ + schema: 'evaos.mac_control.runtime_receipt.v1', + customer_id: customerId, + connector_token: connectorToken, + }) + ); + const handler = createMacControlRuntimeReceiptHandler({ + env: verifierEnv, + now: () => nowMs, + fetchImpl: async () => + fetchResponse(200, { + schema: 'evaos.mac_control.runtime_receipt_envelope.v1', + receiptBase64: leakingReceipt.toString('base64url'), + signature: '-----BEGIN SSH SIGNATURE-----\nQUFBQQ==\n-----END SSH SIGNATURE-----\n', + keyId: receiptKeyId, + namespace: 'evaos-mac-control-receipt-v1', + }), + }); + const response = await invoke(handler, requestBody(4), authority.headers); + assert.equal(response.statusCode, 502); + assert.equal(JSON.parse(response.body).error.code, 'connector_response_invalid'); + assertAuthorityRedacted(response.body, authority); + return true; +} + +function signedAuthority({ issuedAt = nowSeconds, expiresAt = nowSeconds + 50, contextIdByte }) { + const context = { + schema_version: 'evaos.mac_control_execution_context.v1', + key_id: contextKeyId, + runtime: 'openclaw', + customer_id: customerId, + customer_vm_id: customerVmId, + binding_id: bindingId, + binding_version: bindingVersion, + issued_at: issuedAt, + expires_at: expiresAt, + context_id: Buffer.alloc(16, contextIdByte).toString('base64url'), + }; + const payloadBytes = Buffer.from(JSON.stringify(context)); + const payload = payloadBytes.toString('base64url'); + const signature = sign(null, payloadBytes, contextPrivateKey).toString('base64url'); + return { + payload, + signature, + headers: { + 'x-evaos-mac-control-execution-context': payload, + 'x-evaos-mac-control-execution-context-signature': signature, + 'x-evaos-mac-control-execution-context-key-id': contextKeyId, + 'x-evaos-mac-control-contract': 'evaos.mac_control_runtime_contract.v2', + 'x-evaos-mac-control-customer': customerId, + 'x-evaos-mac-control-grant-state': 'active', + 'x-evaos-desktop-bridge-url': connectorUrl, + 'x-evaos-desktop-bridge-token': connectorToken, + 'x-evaos-desktop-bridge-token-last4': connectorToken.slice(-4), + 'x-evaos-mac-control-binding-id': bindingId, + 'x-evaos-mac-control-binding-version': bindingVersion, + 'x-evaos-mac-control-binding-expires-at': new Date(nowMs + 120_000).toISOString(), + }, + }; +} + +function requestBody(fill) { + return { + challenge: Buffer.alloc(32, fill).toString('base64url'), + runRef: `gha:${sourceRunId}:${Buffer.alloc(12, fill).toString('hex')}`, + }; +} + +function assertAuthorityRedacted(body, authority) { + for (const forbidden of [ + connectorToken, + connectorUrl, + customerId, + customerVmId, + bindingId, + authority.payload, + authority.signature, + ]) { + assert.equal(body.includes(forbidden), false); + } +} + +async function invoke(handler, body, headers) { + const request = Readable.from([JSON.stringify(body)]); + request.method = 'POST'; + request.headers = headers; + const response = { + statusCode: 0, + headers: {}, + body: '', + setHeader(name, value) { + this.headers[name.toLowerCase()] = value; + }, + end(value = '') { + this.body = value; + }, + }; + await handler(request, response); + return response; +} + +function fetchResponse(status, body) { + return { + ok: status >= 200 && status < 300, + status, + text: async () => JSON.stringify(body), + }; +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts index 66d26c179f..9f2a83c006 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts @@ -46,7 +46,6 @@ export type BridgeCommandKey = | 'customerMacStatus' | 'customerMacCapabilities' | 'customerMacControlStatus' - | 'customerMacControlStart' | 'customerMacControlStop' | 'customerMacControlKillSwitch' | 'desktopSee' @@ -117,8 +116,6 @@ export type BridgeParams = { snapshot_id?: string; element_id?: string; approval_audit_id?: string; - mode?: string; - agent_label?: string; x?: number; y?: number; from_x?: number; @@ -156,7 +153,6 @@ const FIXED_COMMANDS: Record< | 'evaosProviderCompleteAuth' | 'evaosSharedBrowserGuidance' | 'customerMacSnapshot' - | 'customerMacControlStart' | 'desktopSee' | 'desktopClick' | 'desktopType' @@ -214,7 +210,6 @@ const CUSTOMER_MAC_REMOTE_COMMANDS = new Set([ 'customerMacStatus', 'customerMacCapabilities', 'customerMacControlStatus', - 'customerMacControlStart', 'customerMacControlStop', 'customerMacControlKillSwitch', 'desktopSee', @@ -368,17 +363,6 @@ export function buildBridgeArgv(command: BridgeCommandKey, params: BridgeParams if (command === 'customerMacAxTree') { return ['customer-mac', 'ax-tree', '--json', '--max-nodes', String(clampInt(params.max_nodes, 200, 1, 1000))]; } - if (command === 'customerMacControlStart') { - return [ - 'customer-mac', - 'control', - 'start', - '--json', - '--mode', - String(params.mode || 'full-access'), - ...(params.agent_label ? ['--agent-label', String(params.agent_label)] : []), - ]; - } if (command === 'desktopSee') { return [ 'customer-mac', @@ -822,6 +806,18 @@ function optionalStringArg(value: unknown, flag: string): string[] { } export async function runBridge(command: BridgeCommandKey, params: BridgeParams = {}): Promise { + if (String(command) === 'customerMacControlStart' || String(command) === 'desktop_control_start') { + return { + ok: false, + errors: [ + { + code: 'control_start_local_only', + message: 'Remote agent tools cannot start or restart Mac control.', + guidance: 'Use the local evaOS Workbench app on the customer Mac.', + }, + ], + }; + } if (String(command) === 'customerMacCompletePairing') { return { ok: false, @@ -1454,12 +1450,6 @@ async function runRemoteBridge(remoteURL: string, command: BridgeCommandKey, par try { return await postRemoteBridgeCommand(endpoint, headers, remoteURL, command, params, timeoutForCommand(command)); } catch (error: unknown) { - if (command === 'customerMacControlStart' && isAbortLikeError(error)) { - const reconciled = await reconcileControlStartAfterAbort(endpoint, headers, remoteURL, params); - if (reconciled) { - return reconciled; - } - } const err = error as { message?: string }; return { ok: false, @@ -1511,85 +1501,10 @@ async function postRemoteBridgeCommand( } } -async function reconcileControlStartAfterAbort( - endpoint: URL, - headers: Record, - remoteURL: string, - params: BridgeParams -): Promise { - try { - const status = await postRemoteBridgeCommand(endpoint, headers, remoteURL, 'customerMacControlStatus', {}, 15_000); - if (!isControlSessionActive(status, params)) { - return null; - } - const payload: Record = isRecord(status) ? { ...status } : { ok: true }; - const data = isRecord(payload.data) ? { ...payload.data } : {}; - payload.ok = true; - payload.data = { - ...data, - control_start_reconciled: true, - }; - payload.warnings = [ - ...(Array.isArray(payload.warnings) ? payload.warnings : []), - { - code: 'control_start_response_reconciled_after_abort', - message: - 'Mac control start timed out after the connector activated the control session; status was reconciled from the paired Mac.', - guidance: 'Continue with desktop_control_status and desktop_see before running live Mac-control actions.', - }, - ]; - return payload; - } catch { - return null; - } -} - -function isAbortLikeError(error: unknown): boolean { - const err = error as { name?: string; message?: string }; - return err.name === 'AbortError' || /abort/i.test(err.message || ''); -} - -function isControlSessionActive(status: unknown, params: BridgeParams): boolean { - if (!isRecord(status) || status.ok !== true || !isRecord(status.data)) { - return false; - } - const data = status.data; - const session = isRecord(data.session) ? data.session : {}; - const active = data.active === true || session.active === true; - const killSwitch = - data.kill_switch === true || - data.killSwitch === true || - session.kill_switch === true || - session.killSwitch === true; - const mode = controlModeFromStatus(data.mode) ?? controlModeFromStatus(session.mode); - return active && !killSwitch && mode === requestedControlMode(params.mode); -} - -function requestedControlMode(mode: unknown): 'full_access' | 'ask_permission' { - return controlModeFromStatus(mode) ?? 'full_access'; -} - -function controlModeFromStatus(mode: unknown): 'full_access' | 'ask_permission' | undefined { - if (typeof mode !== 'string') { - return undefined; - } - const normalized = mode.trim().toLowerCase().replace(/-/g, '_'); - if (normalized === 'ask_permission') { - return 'ask_permission'; - } - if (normalized === 'full_access') { - return 'full_access'; - } - return undefined; -} - function timeoutForCommand(command: BridgeCommandKey): number { if (command === 'codexLiveStatus') { return 35_000; } - if (command === 'customerMacControlStart') { - return 30_000; - } if ( command === 'desktopSee' || command === 'iphoneSee' || diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts index 81169cd80f..d7b9e9ac95 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts @@ -1,4 +1,4 @@ -import { createPublicKey, verify } from 'node:crypto'; +import { createHash, createPublicKey, timingSafeEqual, verify } from 'node:crypto'; export const MAC_CONTROL_RUNTIME_RECEIPT_PATH = '/api/v1/evaos/mac-control/runtime-receipt'; @@ -6,9 +6,15 @@ const CONTEXT_SCHEMA = 'evaos.mac_control_execution_context.v1'; const CONTRACT_SCHEMA = 'evaos.mac_control_runtime_contract.v2'; const CONNECTOR_REQUEST_SCHEMA = 'evaos.mac_control.canary_request.v1'; const CONNECTOR_RESPONSE_SCHEMA = 'evaos.mac_control.runtime_receipt_envelope.v1'; +const RECEIPT_SCHEMA = 'evaos.mac_control.runtime_receipt.v1'; const RECEIPT_NAMESPACE = 'evaos-mac-control-receipt-v1'; +const PUBLIC_PROOF_SCHEMA = 'evaos.mac_control.runtime_proof.v2'; +const PUBLIC_PROOF_KIND = 'selected_binding_direct_mac_control'; +const PUBLIC_PROOF_SOURCE = 'evaos-desktop-bridge:runtime-receipt'; const CONTEXT_TTL_SECONDS = 60; const CLOCK_SKEW_SECONDS = 5; +const DEFAULT_CONNECTOR_TIMEOUT_MS = 10_000; +const AUTHORITY_DEADLINE_SAFETY_MS = 250; const MAX_REQUEST_BYTES = 4096; const MAX_CONNECTOR_RESPONSE_BYTES = 65536; const CONTEXT_FIELDS = [ @@ -24,6 +30,65 @@ const CONTEXT_FIELDS = [ 'context_id', ] as const; const RESPONSE_FIELDS = ['schema', 'receiptBase64', 'signature', 'keyId', 'namespace'] as const; +const RECEIPT_FIELDS = [ + 'schema', + 'keyId', + 'namespace', + 'executedAt', + 'runtime', + 'challenge', + 'runRef', + 'contextKeyId', + 'executionContextDigest', + 'contextRef', + 'contextIssuedAt', + 'contextExpiresAt', + 'customerRef', + 'vmRef', + 'bindingRef', + 'bindingVersion', + 'bindingExpiresAt', + 'sessionRef', + 'controlStateBefore', + 'controlStateAfter', + 'controlStateBeforeDigest', + 'controlStateAfterDigest', + 'candidate', + 'action', + 'actionArgsDigest', + 'auditId', + 'auditTimestamp', + 'auditRecordDigest', +] as const; +const CONTROL_STATE_FIELDS = ['active', 'generation', 'killSwitch', 'mode', 'ready', 'takeoverActive'] as const; +const CANDIDATE_FIELDS = [ + 'sourceCommit', + 'sourceSha256', + 'sourcePath', + 'sourceOwner', + 'status', + 'appPath', + 'appVersion', + 'appBuild', + 'appBundleId', + 'appName', + 'executable', + 'argv0', + 'owner', +] as const; +const OWNER_FIELDS = [ + 'label', + 'classification', + 'bundleId', + 'sourceCommit', + 'programPath', + 'appPath', + 'manifestPath', + 'plistPath', +] as const; +const PATH_FIELDS = ['kind', 'value'] as const; +const ACTION_FIELDS = ['command', 'args'] as const; +const ACTION_ARGS_FIELDS = ['keys', 'dryRun'] as const; const HEADER = { context: 'x-evaos-mac-control-execution-context', @@ -40,9 +105,9 @@ const HEADER = { bindingExpiresAt: 'x-evaos-mac-control-binding-expires-at', } as const; -type ByteBuffer = { - byteLength: number; - subarray(start?: number, end?: number): Uint8Array; +type ByteBuffer = Uint8Array & { + readUInt32BE?(offset: number): number; + writeUInt32BE?(value: number, offset: number): number; toString(encoding?: string): string; }; @@ -86,6 +151,7 @@ type RuntimeReceiptOptions = { fetchImpl?: FetchLike; now?: () => number; replayCache?: Map; + connectorTimeoutMs?: number; }; type VerifiedAuthority = { @@ -105,12 +171,35 @@ type PublicRequest = { runRef: string; }; -type ConnectorEnvelope = { - schema: string; - receiptBase64: string; - signature: string; +type ReceiptVerifierConfig = { keyId: string; - namespace: string; + publicKey: ByteBuffer; + expectedSourceCommit: string; + expectedSourceSha256: string; + expectedAppVersion: string; + expectedAppBuild: string; +}; + +type PublicRuntimeProof = { + ok: true; + schema: typeof PUBLIC_PROOF_SCHEMA; + proofKind: typeof PUBLIC_PROOF_KIND; + tool: 'customer_mac.desktop_hotkey'; + outcome: 'succeeded'; + runRef: string; + executedAt: string; + bindingRef: string; + bindingVersion: string; + sessionRef: string; + expiresAt: number; + auditRef: string; + sourcePointer: typeof PUBLIC_PROOF_SOURCE; + candidate: { + sourceCommit: string; + sourceSha256: string; + appVersion: string; + appBuild: string; + }; }; type PluginHttpApi = { @@ -138,6 +227,7 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt const fetchImpl = options.fetchImpl ?? (globalThis.fetch as unknown as FetchLike); const now = options.now ?? Date.now; const replayCache = options.replayCache ?? new Map(); + const configuredConnectorTimeoutMs = options.connectorTimeoutMs ?? DEFAULT_CONNECTOR_TIMEOUT_MS; return async (request: RequestLike, response: ResponseLike): Promise => { if ((request.method ?? 'GET').toUpperCase() !== 'POST') { @@ -158,6 +248,22 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt return true; } + const receiptVerifier = loadReceiptVerifierConfig(env); + if (!receiptVerifier.ok) { + sendError(response, 503, 'receipt_verifier_unavailable'); + return true; + } + + const remainingAuthorityMs = + Math.min(authority.value.context.expires_at * 1000, Date.parse(authority.value.bindingExpiresAt)) - + now() - + AUTHORITY_DEADLINE_SAFETY_MS; + if (remainingAuthorityMs <= 0) { + sendError(response, 401, 'execution_context_expired'); + return true; + } + const connectorTimeoutMs = Math.max(1, Math.min(configuredConnectorTimeoutMs, remainingAuthorityMs)); + pruneReplayCache(replayCache, now()); if (replayCache.has(authority.value.context.context_id)) { sendError(response, 409, 'execution_context_replayed'); @@ -182,6 +288,8 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt }; let connectorResponse: FetchResponseLike; + const controller = new AbortController(); + const connectorTimeout = setTimeout(() => controller.abort(), connectorTimeoutMs); try { connectorResponse = await fetchImpl(`${authority.value.connectorUrl}/v1/canary/mac-control`, { method: 'POST', @@ -191,10 +299,13 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify(connectorBody), + signal: controller.signal, }); } catch { sendError(response, 502, 'connector_unavailable'); return true; + } finally { + clearTimeout(connectorTimeout); } if (!connectorResponse.ok) { @@ -213,7 +324,12 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt sendError(response, 502, 'connector_response_invalid'); return true; } - const sanitized = validateConnectorEnvelope(rawConnectorResponse, authority.value); + const sanitized = validateConnectorEnvelope( + rawConnectorResponse, + authority.value, + publicRequest.value, + receiptVerifier.value + ); if (!sanitized.ok) { sendError(response, 502, 'connector_response_invalid'); return true; @@ -274,12 +390,12 @@ function verifyAuthority( env: Record, nowMs: number ): { ok: true; value: VerifiedAuthority } | { ok: false; status: number; code: string } { - const expectedKeyId = env.EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_KEY_ID; - const publicKeyBase64 = env.EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_PUBLIC_KEY_B64; + const expectedKeyId = env.EVAOS_MAC_CONTROL_CONTEXT_KEY_ID; + const publicKeyBase64 = env.EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY; if (!expectedKeyId || !publicKeyBase64 || !validKeyId(expectedKeyId)) { return { ok: false, status: 503, code: 'execution_context_verifier_unavailable' }; } - const rawPublicKey = decodeCanonicalBase64(publicKeyBase64); + const rawPublicKey = decodeBase64Url(publicKeyBase64); if (!rawPublicKey || rawPublicKey.byteLength !== 32) { return { ok: false, status: 503, code: 'execution_context_verifier_unavailable' }; } @@ -396,10 +512,53 @@ function verifyAuthority( }; } +function loadReceiptVerifierConfig( + env: Record +): { ok: true; value: ReceiptVerifierConfig } | { ok: false } { + const keyId = env.EVAOS_MAC_CONTROL_RECEIPT_KEY_ID; + const encodedPublicKey = env.EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY; + const expectedSourceCommit = env.EVAOS_MAC_CONTROL_EXPECTED_SOURCE_COMMIT; + const expectedSourceSha256 = env.EVAOS_MAC_CONTROL_EXPECTED_SOURCE_SHA256; + const expectedAppVersion = env.EVAOS_MAC_CONTROL_EXPECTED_APP_VERSION; + const expectedAppBuild = env.EVAOS_MAC_CONTROL_EXPECTED_APP_BUILD; + if ( + !keyId || + !validKeyId(keyId) || + !encodedPublicKey || + !expectedSourceCommit || + !/^[0-9a-f]{40}$/.test(expectedSourceCommit) || + !expectedSourceSha256 || + !validSha256(expectedSourceSha256) || + !expectedAppVersion || + !validReleaseValue(expectedAppVersion) || + !expectedAppBuild || + !validReleaseValue(expectedAppBuild) + ) { + return { ok: false }; + } + const publicKey = decodeBase64Url(encodedPublicKey); + if (!publicKey || publicKey.byteLength !== 32) { + return { ok: false }; + } + return { + ok: true, + value: { + keyId, + publicKey, + expectedSourceCommit, + expectedSourceSha256, + expectedAppVersion, + expectedAppBuild, + }, + }; +} + function validateConnectorEnvelope( raw: string, - authority: VerifiedAuthority -): { ok: true; value: ConnectorEnvelope } | { ok: false } { + authority: VerifiedAuthority, + request: PublicRequest, + verifier: ReceiptVerifierConfig +): { ok: true; value: PublicRuntimeProof } | { ok: false } { if (Buffer.from(raw).byteLength > MAX_CONNECTOR_RESPONSE_BYTES) { return { ok: false }; } @@ -421,7 +580,7 @@ function validateConnectorEnvelope( typeof parsed.signature !== 'string' || parsed.signature.length > 8192 || typeof parsed.keyId !== 'string' || - !validKeyId(parsed.keyId) + parsed.keyId !== verifier.keyId ) { return { ok: false }; } @@ -448,7 +607,368 @@ function validateConnectorEnvelope( ) { return { ok: false }; } - return { ok: true, value: parsed as ConnectorEnvelope }; + if (!verifySshSignature(receiptBytes, parsed.signature, verifier.publicKey)) { + return { ok: false }; + } + return validateReceipt(receiptBytes, authority, request, verifier); +} + +function validateReceipt( + receiptBytes: ByteBuffer, + authority: VerifiedAuthority, + request: PublicRequest, + verifier: ReceiptVerifierConfig +): { ok: true; value: PublicRuntimeProof } | { ok: false } { + let parsed: unknown; + try { + parsed = JSON.parse(receiptBytes.toString('utf8')); + } catch { + return { ok: false }; + } + if (!isRecord(parsed) || !hasExactKeys(parsed, RECEIPT_FIELDS)) { + return { ok: false }; + } + const canonical = canonicalJson(parsed); + if (!canonical || !timingSafeBufferEqual(Buffer.from(receiptBytes), Buffer.from(canonical, 'utf8'))) { + return { ok: false }; + } + + const contextPayload = decodeBase64Url(authority.contextPayload); + const before = parsed.controlStateBefore; + const after = parsed.controlStateAfter; + const candidate = parsed.candidate; + const action = parsed.action; + if ( + !contextPayload || + parsed.schema !== RECEIPT_SCHEMA || + parsed.keyId !== verifier.keyId || + parsed.namespace !== RECEIPT_NAMESPACE || + parsed.runtime !== 'openclaw' || + parsed.challenge !== request.challenge || + parsed.runRef !== request.runRef || + parsed.contextKeyId !== authority.contextKeyId || + parsed.contextIssuedAt !== authority.context.issued_at || + parsed.contextExpiresAt !== authority.context.expires_at || + parsed.bindingVersion !== authority.context.binding_version || + parsed.bindingExpiresAt !== authority.bindingExpiresAt || + parsed.executionContextDigest !== saltedHash(request.challenge, contextPayload) || + parsed.contextRef !== saltedHash(request.challenge, authority.context.context_id) || + parsed.customerRef !== saltedHash(request.challenge, authority.context.customer_id) || + parsed.vmRef !== saltedHash(request.challenge, authority.context.customer_vm_id) || + parsed.bindingRef !== saltedHash(request.challenge, authority.context.binding_id) || + !isRecord(before) || + !isRecord(after) || + !validControlState(before) || + !validControlState(after) || + canonicalJson(before) !== canonicalJson(after) || + parsed.sessionRef !== + saltedHash(request.challenge, `${authority.context.binding_id}\0${String(before.generation)}`) || + parsed.controlStateBeforeDigest !== sha256Hex(canonicalJsonBytes(before)) || + parsed.controlStateAfterDigest !== sha256Hex(canonicalJsonBytes(after)) || + !validCandidate(candidate, verifier) || + !validReceiptAction(action) || + parsed.actionArgsDigest !== sha256Hex(canonicalJsonBytes(action.args)) || + typeof parsed.auditId !== 'string' || + !/^audit-[0-9A-Za-z_-]{8,128}$/.test(parsed.auditId) || + typeof parsed.auditRecordDigest !== 'string' || + !validSha256(parsed.auditRecordDigest) + ) { + return { ok: false }; + } + + const executedAt = parseUtcTimestamp(parsed.executedAt); + const auditTimestamp = parseUtcTimestamp(parsed.auditTimestamp); + const bindingExpiresAt = parseUtcTimestamp(parsed.bindingExpiresAt); + if ( + executedAt === undefined || + auditTimestamp === undefined || + bindingExpiresAt === undefined || + executedAt < (authority.context.issued_at - CLOCK_SKEW_SECONDS) * 1000 || + executedAt > authority.context.expires_at * 1000 || + auditTimestamp < (authority.context.issued_at - CLOCK_SKEW_SECONDS) * 1000 || + auditTimestamp > executedAt + CLOCK_SKEW_SECONDS * 1000 || + authority.context.expires_at * 1000 > bindingExpiresAt + ) { + return { ok: false }; + } + + const typedCandidate = candidate as Record; + return { + ok: true, + value: { + ok: true, + schema: PUBLIC_PROOF_SCHEMA, + proofKind: PUBLIC_PROOF_KIND, + tool: 'customer_mac.desktop_hotkey', + outcome: 'succeeded', + runRef: request.runRef, + executedAt: parsed.executedAt as string, + bindingRef: parsed.bindingRef as string, + bindingVersion: authority.context.binding_version, + sessionRef: parsed.sessionRef as string, + expiresAt: authority.context.expires_at, + auditRef: saltedHash(request.challenge, parsed.auditId), + sourcePointer: PUBLIC_PROOF_SOURCE, + candidate: { + sourceCommit: typedCandidate.sourceCommit as string, + sourceSha256: typedCandidate.sourceSha256 as string, + appVersion: typedCandidate.appVersion as string, + appBuild: typedCandidate.appBuild as string, + }, + }, + }; +} + +function validControlState(value: Record): boolean { + return ( + hasExactKeys(value, CONTROL_STATE_FIELDS) && + value.active === true && + Number.isInteger(value.generation) && + (value.generation as number) >= 0 && + value.killSwitch === false && + value.mode === 'full_access' && + value.ready === true && + value.takeoverActive === false + ); +} + +function validCandidate(value: unknown, verifier: ReceiptVerifierConfig): value is Record { + if (!isRecord(value) || !hasExactKeys(value, CANDIDATE_FIELDS)) { + return false; + } + const owner = value.owner; + if (!isRecord(owner) || !hasExactKeys(owner, OWNER_FIELDS)) { + return false; + } + return ( + value.sourceCommit === verifier.expectedSourceCommit && + value.sourceSha256 === verifier.expectedSourceSha256 && + value.sourcePath === 'resources/evaos-beta/bridge' && + value.sourceOwner === '100yenadmin/evaOS-GUI' && + value.status === 'vendored' && + value.appPath === '/Applications/evaOS Workbench.app' && + value.appVersion === verifier.expectedAppVersion && + value.appBuild === verifier.expectedAppBuild && + value.appBundleId === 'com.evaos.workbench' && + value.appName === 'evaOS Workbench' && + typeof value.executable === 'string' && + /^\/Applications\/evaOS Workbench\.app\/Contents\/Resources\/Bridge\/python\/bin\/python3(?:\.12)?$/.test( + value.executable + ) && + value.argv0 === '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py' && + owner.label === 'com.electricsheep.evaos-desktop-bridge' && + owner.classification === 'workbench_bundle' && + owner.bundleId === 'com.evaos.workbench' && + owner.sourceCommit === verifier.expectedSourceCommit && + validPathClaim( + owner.programPath, + '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge' + ) && + validPathClaim(owner.appPath, '/Applications/evaOS Workbench.app') && + validPathClaim(owner.manifestPath, '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/manifest.json') && + isRecord(owner.plistPath) && + hasExactKeys(owner.plistPath, PATH_FIELDS) && + owner.plistPath.kind === 'path' && + typeof owner.plistPath.value === 'string' && + owner.plistPath.value.endsWith('/Library/LaunchAgents/com.electricsheep.evaos-desktop-bridge.plist') + ); +} + +function validPathClaim(value: unknown, expected: string): boolean { + return isRecord(value) && hasExactKeys(value, PATH_FIELDS) && value.kind === 'path' && value.value === expected; +} + +function validReceiptAction(value: unknown): value is { command: string; args: Record } { + return ( + isRecord(value) && + hasExactKeys(value, ACTION_FIELDS) && + value.command === 'customer_mac.desktop_hotkey' && + isRecord(value.args) && + hasExactKeys(value.args, ACTION_ARGS_FIELDS) && + value.args.keys === 'escape' && + value.args.dryRun === false + ); +} + +function verifySshSignature(message: ByteBuffer, armor: string, pinnedPublicKey: ByteBuffer): boolean { + const decoded = decodeSshSignatureArmor(armor); + if (!decoded) { + return false; + } + try { + const reader = sshReader(decoded); + if (reader.readRaw(6).toString('ascii') !== 'SSHSIG' || reader.readUInt32() !== 1) { + return false; + } + const publicKeyBlob = reader.readString(); + const namespace = reader.readString().toString('utf8'); + const reserved = reader.readString(); + const hashAlgorithm = reader.readString().toString('ascii'); + const signatureBlob = reader.readString(); + if (!reader.done() || namespace !== RECEIPT_NAMESPACE || reserved.byteLength !== 0) { + return false; + } + + const publicKeyReader = sshReader(publicKeyBlob); + const publicKeyAlgorithm = publicKeyReader.readString().toString('ascii'); + const embeddedPublicKey = publicKeyReader.readString(); + const signatureReader = sshReader(signatureBlob); + const signatureAlgorithm = signatureReader.readString().toString('ascii'); + const signature = signatureReader.readString(); + if ( + !publicKeyReader.done() || + !signatureReader.done() || + publicKeyAlgorithm !== 'ssh-ed25519' || + signatureAlgorithm !== 'ssh-ed25519' || + embeddedPublicKey.byteLength !== 32 || + signature.byteLength !== 64 || + !timingSafeBufferEqual(embeddedPublicKey, Buffer.from(pinnedPublicKey)) || + (hashAlgorithm !== 'sha256' && hashAlgorithm !== 'sha512') + ) { + return false; + } + + const digest = createHash(hashAlgorithm).update(Buffer.from(message)).digest(); + const signedData = Buffer.concat([ + Buffer.from('SSHSIG', 'ascii'), + encodeSshString(Buffer.from(namespace, 'utf8')), + encodeSshString(Buffer.alloc(0)), + encodeSshString(Buffer.from(hashAlgorithm, 'ascii')), + encodeSshString(digest), + ]); + const spkiPrefix = Buffer.from('302a300506032b6570032100', 'hex'); + const publicKey = createPublicKey({ + key: Buffer.concat([spkiPrefix, embeddedPublicKey]), + format: 'der', + type: 'spki', + }); + return verify(null, signedData, publicKey, signature); + } catch { + return false; + } +} + +function decodeSshSignatureArmor(value: string): ByteBuffer | undefined { + const match = /^-----BEGIN SSH SIGNATURE-----\n([A-Za-z0-9+/=\n]+)-----END SSH SIGNATURE-----\n?$/.exec(value); + if (!match) { + return undefined; + } + const lines = match[1].split('\n').filter((line) => line.length > 0); + if (lines.length === 0 || lines.some((line) => line.length > 76 || !/^[A-Za-z0-9+/=]+$/.test(line))) { + return undefined; + } + const decoded = decodeCanonicalBase64(lines.join('')); + return decoded ? Buffer.from(decoded) : undefined; +} + +function sshReader(value: ByteBuffer) { + const buffer = Buffer.from(value); + let offset = 0; + return { + readRaw(length: number): ByteBuffer { + if (!Number.isInteger(length) || length < 0 || offset + length > buffer.byteLength) { + throw new Error('invalid SSH signature field'); + } + const result = buffer.subarray(offset, offset + length); + offset += length; + return result; + }, + readUInt32(): number { + if (offset + 4 > buffer.byteLength) { + throw new Error('invalid SSH signature integer'); + } + const result = buffer.readUInt32BE(offset); + offset += 4; + return result; + }, + readString(): ByteBuffer { + const length = this.readUInt32(); + if (length > MAX_CONNECTOR_RESPONSE_BYTES) { + throw new Error('oversized SSH signature field'); + } + return this.readRaw(length); + }, + done(): boolean { + return offset === buffer.byteLength; + }, + }; +} + +function encodeUInt32(value: number): ByteBuffer { + const output = Buffer.alloc(4); + output.writeUInt32BE(value, 0); + return output; +} + +function encodeSshString(value: ByteBuffer): ByteBuffer { + const buffer = Buffer.from(value); + return Buffer.concat([encodeUInt32(buffer.byteLength), buffer]); +} + +function timingSafeBufferEqual(left: ByteBuffer, right: ByteBuffer): boolean { + const leftBuffer = Buffer.from(left); + const rightBuffer = Buffer.from(right); + return leftBuffer.byteLength === rightBuffer.byteLength && timingSafeEqual(leftBuffer, rightBuffer); +} + +function canonicalJson(value: unknown): string | undefined { + try { + return JSON.stringify(sortJson(value)); + } catch { + return undefined; + } +} + +function canonicalJsonBytes(value: unknown): ByteBuffer { + const encoded = canonicalJson(value); + if (encoded === undefined) { + throw new Error('value is not canonical JSON'); + } + return Buffer.from(encoded, 'utf8'); +} + +function sortJson(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(sortJson); + } + if (!isRecord(value)) { + return value; + } + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, sortJson(value[key])]) + ); +} + +function sha256Hex(value: ByteBuffer): string { + return createHash('sha256').update(Buffer.from(value)).digest('hex'); +} + +function saltedHash(challenge: string, value: string | ByteBuffer): string { + return sha256Hex( + Buffer.concat([ + Buffer.from(challenge, 'ascii'), + Buffer.from([0]), + typeof value === 'string' ? Buffer.from(value, 'utf8') : Buffer.from(value), + ]) + ); +} + +function parseUtcTimestamp(value: unknown): number | undefined { + if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(value)) { + return undefined; + } + const parsed = Date.parse(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function validSha256(value: string): boolean { + return /^[0-9a-f]{64}$/.test(value); +} + +function validReleaseValue(value: string): boolean { + return /^[0-9A-Za-z][0-9A-Za-z._+-]{0,63}$/.test(value); } function validConnectorUrl(value: string): string | undefined { @@ -547,8 +1067,9 @@ function validSshSignature(value: string): boolean { } function hasExactKeys(value: Record, keys: readonly string[]): boolean { + const expected = new Set(keys); const actual = Object.keys(value); - return actual.length === keys.length && actual.every((key, index) => key === keys[index]); + return actual.length === expected.size && actual.every((key) => expected.has(key)); } function isRecord(value: unknown): value is Record { diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/types.d.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/types.d.ts index 2e669a7913..6a2f9b8d1f 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/types.d.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/types.d.ts @@ -7,9 +7,16 @@ declare module 'node:child_process' { } declare module 'node:crypto' { + export function createHash(algorithm: string): { + update(value: unknown): { + digest(): EvaosBuffer; + digest(encoding: string): string; + }; + }; export function createPublicKey(options: unknown): unknown; export function createHmac(...args: unknown[]): { update(value: unknown): { digest(encoding: string): string } }; export function randomUUID(): string; + export function timingSafeEqual(left: Uint8Array, right: Uint8Array): boolean; export function verify(algorithm: null, data: unknown, key: unknown, signature: unknown): boolean; } @@ -38,19 +45,15 @@ declare const process: { env: Record; }; +type EvaosBuffer = Uint8Array & { + readUInt32BE(offset: number): number; + writeUInt32BE(value: number, offset: number): number; + toString(encoding?: string): string; +}; + declare const Buffer: { - from( - value: string | ArrayBuffer | Uint8Array, - encoding?: string - ): { - byteLength: number; - subarray(start?: number, end?: number): Uint8Array; - toString(encoding?: string): string; - }; - concat(values: unknown[]): { - byteLength: number; - subarray(start?: number, end?: number): Uint8Array; - toString(encoding?: string): string; - }; + from(value: string | ArrayBuffer | Uint8Array | readonly number[], encoding?: string): EvaosBuffer; + alloc(size: number): EvaosBuffer; + concat(values: readonly Uint8Array[]): EvaosBuffer; isBuffer(value: unknown): boolean; }; diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs index 66ffbb4c4e..d6bdf50eb3 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs @@ -1,7 +1,9 @@ import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { generateKeyPairSync, sign } from 'node:crypto'; -import { readFileSync } from 'node:fs'; +import { createHash, generateKeyPairSync, sign } from 'node:crypto'; +import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { Readable } from 'node:stream'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; @@ -15,12 +17,24 @@ import { const NOW_MS = Date.parse('2026-07-15T00:00:00Z'); const KEY_ID = 'mac-context-test-2026-07'; +const RECEIPT_KEY_ID = 'connector-receipt-test-2026-07'; +const RECEIPT_NAMESPACE = 'evaos-mac-control-receipt-v1'; const CONNECTOR_TOKEN = 'connector-token-value-at-least-24'; +const EXPECTED_COMMIT = 'a'.repeat(40); +const EXPECTED_SOURCE_SHA256 = 'b'.repeat(64); const { privateKey, publicKey } = generateKeyPairSync('ed25519'); const rawPublicKey = publicKey.export({ format: 'der', type: 'spki' }).subarray(-32); +const { privateKey: receiptPrivateKey, publicKey: receiptPublicKey } = generateKeyPairSync('ed25519'); +const rawReceiptPublicKey = receiptPublicKey.export({ format: 'der', type: 'spki' }).subarray(-32); const env = { - EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_KEY_ID: KEY_ID, - EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_PUBLIC_KEY_B64: rawPublicKey.toString('base64'), + EVAOS_MAC_CONTROL_CONTEXT_KEY_ID: KEY_ID, + EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY: rawPublicKey.toString('base64url'), + EVAOS_MAC_CONTROL_RECEIPT_KEY_ID: RECEIPT_KEY_ID, + EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY: rawReceiptPublicKey.toString('base64url'), + EVAOS_MAC_CONTROL_EXPECTED_SOURCE_COMMIT: EXPECTED_COMMIT, + EVAOS_MAC_CONTROL_EXPECTED_SOURCE_SHA256: EXPECTED_SOURCE_SHA256, + EVAOS_MAC_CONTROL_EXPECTED_APP_VERSION: '2.1.36', + EVAOS_MAC_CONTROL_EXPECTED_APP_BUILD: '2.1.36', }; test('registers one exact gateway-authenticated runtime receipt route', () => { @@ -53,26 +67,31 @@ test('does not expose or execute the removed legacy pairing-code surface', async assert.equal(JSON.parse(hermes.stdout).errors[0].code, 'legacy_pairing_removed'); }); +test('does not advertise or execute remote Mac-control start', async () => { + const packageManifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); + const pluginManifest = JSON.parse(readFileSync(new URL('../openclaw.plugin.json', import.meta.url), 'utf8')); + assert.equal(packageManifest.openclaw.contracts.tools.includes('desktop_control_start'), false); + assert.equal(pluginManifest.contracts.tools.includes('desktop_control_start'), false); + + const openClawResult = await runBridge('customerMacControlStart', { mode: 'full-access' }); + assert.equal(openClawResult.ok, false); + assert.equal(openClawResult.errors[0].code, 'control_start_local_only'); + + const hermes = spawnSync( + fileURLToPath(new URL('../../hermes-adapter/bin/evaos-desktop-bridge-command', import.meta.url)), + ['customerMacControlStart', '{"mode":"full-access"}'], + { encoding: 'utf8', env: { PATH: process.env.PATH } } + ); + assert.equal(hermes.status, 0); + assert.equal(JSON.parse(hermes.stdout).errors[0].code, 'control_start_local_only'); +}); + test('verifies server authority and forwards only the fixed connector canary contract', async () => { const challenge = Buffer.alloc(32, 7).toString('base64url'); const runRef = 'rc-2.1.36-arm64'; const authority = signedAuthority(); - const receipt = Buffer.from( - JSON.stringify({ - schema: 'evaos.mac_control.runtime_receipt.v1', - challenge, - run_ref: runRef, - binding_ref: 'sha256:binding-proof', - customer_ref: 'sha256:customer-proof', - }) - ).toString('base64url'); - const connectorEnvelope = { - schema: 'evaos.mac_control.runtime_receipt_envelope.v1', - receiptBase64: receipt, - signature: '-----BEGIN SSH SIGNATURE-----\nAAAA\n-----END SSH SIGNATURE-----\n', - keyId: 'connector-receipt-test-2026-07', - namespace: 'evaos-mac-control-receipt-v1', - }; + const receipt = validReceipt(authority, challenge, runRef); + const connectorEnvelope = signedConnectorEnvelope(receipt); let connectorCalls = 0; const handler = createMacControlRuntimeReceiptHandler({ env, @@ -105,7 +124,30 @@ test('verifies server authority and forwards only the fixed connector canary con }); const response = await invoke(handler, { challenge, runRef }, authority.headers); assert.equal(response.statusCode, 200); - assert.deepEqual(JSON.parse(response.body), connectorEnvelope); + const proof = JSON.parse(response.body); + assert.deepEqual(proof, { + ok: true, + schema: 'evaos.mac_control.runtime_proof.v2', + proofKind: 'selected_binding_direct_mac_control', + tool: 'customer_mac.desktop_hotkey', + outcome: 'succeeded', + runRef, + executedAt: '2026-07-15T00:00:10Z', + bindingRef: receipt.bindingRef, + bindingVersion: '7', + sessionRef: receipt.sessionRef, + expiresAt: Math.floor(NOW_MS / 1000) + 50, + auditRef: saltedHash(challenge, receipt.auditId), + sourcePointer: 'evaos-desktop-bridge:runtime-receipt', + candidate: { + sourceCommit: EXPECTED_COMMIT, + sourceSha256: EXPECTED_SOURCE_SHA256, + appVersion: '2.1.36', + appBuild: '2.1.36', + }, + }); + assert.equal(response.body.includes('receiptBase64'), false); + assert.equal(response.body.includes('SSH SIGNATURE'), false); assert.equal(response.body.includes(CONNECTOR_TOKEN), false); assert.equal(response.body.includes('customer-1'), false); assert.equal(connectorCalls, 1); @@ -138,6 +180,133 @@ test('rejects methods and caller-supplied authority or action fields before conn assert.equal(connectorCalls, 0); }); +test('preflights the pinned receipt verifier before connector execution', async () => { + let connectorCalls = 0; + const { EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY: _missing, ...incompleteEnv } = env; + const handler = createMacControlRuntimeReceiptHandler({ + env: incompleteEnv, + now: () => NOW_MS, + fetchImpl: async () => { + connectorCalls += 1; + return fetchResponse(500, {}); + }, + }); + const response = await invoke( + handler, + { challenge: Buffer.alloc(32, 8).toString('base64url'), runRef: 'missing-receipt-verifier' }, + signedAuthority().headers + ); + assert.equal(response.statusCode, 503); + assert.equal(JSON.parse(response.body).error.code, 'receipt_verifier_unavailable'); + assert.equal(connectorCalls, 0); +}); + +test('bounds connector IO by a sanitized deadline before signed authority expires', async () => { + const authority = signedAuthority(); + let connectorCalls = 0; + const handler = createMacControlRuntimeReceiptHandler({ + env, + now: () => NOW_MS, + connectorTimeoutMs: 20, + fetchImpl: async (_url, init) => { + connectorCalls += 1; + return await new Promise((_resolve, reject) => { + init.signal.addEventListener('abort', () => reject(new Error('must not escape')), { once: true }); + }); + }, + }); + const response = await invoke( + handler, + { challenge: Buffer.alloc(32, 12).toString('base64url'), runRef: 'connector-timeout' }, + authority.headers + ); + assert.equal(response.statusCode, 502); + assert.equal(JSON.parse(response.body).error.code, 'connector_unavailable'); + assert.equal(response.body.includes('must not escape'), false); + assert.equal(response.body.includes(CONNECTOR_TOKEN), false); + assert.equal(connectorCalls, 1); +}); + +test('rejects forged signatures and re-signed wrong release claims', async (t) => { + const challenge = Buffer.alloc(32, 9).toString('base64url'); + const runRef = 'receipt-tamper'; + const authority = signedAuthority(); + const valid = validReceipt(authority, challenge, runRef); + const cases = [ + [ + 'forged signature', + () => { + const envelope = signedConnectorEnvelope(valid); + envelope.signature = sshSignature(canonicalBytes({ ...valid, runRef: 'other-run' })); + return envelope; + }, + ], + [ + 'wrong release candidate', + () => + signedConnectorEnvelope({ + ...valid, + candidate: { ...valid.candidate, sourceCommit: 'd'.repeat(40) }, + }), + ], + ]; + for (const [name, envelope] of cases) { + await t.test(name, async () => { + const handler = createMacControlRuntimeReceiptHandler({ + env, + now: () => NOW_MS, + fetchImpl: async () => fetchResponse(200, envelope()), + }); + const response = await invoke(handler, { challenge, runRef }, authority.headers); + assert.equal(response.statusCode, 502); + assert.equal(JSON.parse(response.body).error.code, 'connector_response_invalid'); + }); + } +}); + +test('verifies a real OpenSSH SSHSIG over the exact canonical receipt bytes', async () => { + const root = mkdtempSync(join(tmpdir(), 'evaos-runtime-receipt-')); + try { + const keyPath = join(root, 'receipt-key'); + const keygen = spawnSync('/usr/bin/ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-f', keyPath], { + encoding: 'utf8', + }); + assert.equal(keygen.status, 0, keygen.stderr); + const publicLine = readFileSync(`${keyPath}.pub`, 'utf8').trim().split(/\s+/); + assert.equal(publicLine[0], 'ssh-ed25519'); + const publicBlob = Buffer.from(publicLine[1], 'base64'); + const actualPublicKey = publicBlob.subarray(-32); + assert.equal(actualPublicKey.length, 32); + + const challenge = Buffer.alloc(32, 10).toString('base64url'); + const runRef = 'real-openssh-signature'; + const authority = signedAuthority(); + const receipt = validReceipt(authority, challenge, runRef); + const receiptBytes = canonicalBytes(receipt); + const signed = spawnSync('/usr/bin/ssh-keygen', ['-Y', 'sign', '-q', '-f', keyPath, '-n', RECEIPT_NAMESPACE, '-'], { + input: receiptBytes, + }); + assert.equal(signed.status, 0, signed.stderr.toString('utf8')); + const envelope = { + schema: 'evaos.mac_control.runtime_receipt_envelope.v1', + receiptBase64: receiptBytes.toString('base64url'), + signature: signed.stdout.toString('ascii'), + keyId: RECEIPT_KEY_ID, + namespace: RECEIPT_NAMESPACE, + }; + const handler = createMacControlRuntimeReceiptHandler({ + env: { ...env, EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY: actualPublicKey.toString('base64url') }, + now: () => NOW_MS, + fetchImpl: async () => fetchResponse(200, envelope), + }); + const response = await invoke(handler, { challenge, runRef }, authority.headers); + assert.equal(response.statusCode, 200, response.body); + assert.equal(JSON.parse(response.body).schema, 'evaos.mac_control.runtime_proof.v2'); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test('fails closed for missing, forged, unknown, malformed, expired, and cross-binding contexts', async (t) => { const challenge = Buffer.alloc(32, 2).toString('base64url'); const cases = [ @@ -149,11 +318,7 @@ test('fails closed for missing, forged, unknown, malformed, expired, and cross-b 'x-evaos-mac-control-execution-context-signature': Buffer.alloc(64, 9).toString('base64url'), }), ], - [ - 'unknown key', - () => signedAuthority().headers, - { ...env, EVAOS_MAC_CONTROL_EXECUTION_CONTEXT_KEY_ID: 'unknown-key' }, - ], + ['unknown key', () => signedAuthority().headers, { ...env, EVAOS_MAC_CONTROL_CONTEXT_KEY_ID: 'unknown-key' }], ['malformed', () => signedAuthority({ extra: 'not-allowed' }).headers], [ 'expired', @@ -185,16 +350,9 @@ test('fails closed for missing, forged, unknown, malformed, expired, and cross-b test('burns a verified execution context before connector IO and rejects replay', async () => { const authority = signedAuthority(); + const body = { challenge: Buffer.alloc(32, 3).toString('base64url'), runRef: 'replay-proof' }; let connectorCalls = 0; - const envelope = { - schema: 'evaos.mac_control.runtime_receipt_envelope.v1', - receiptBase64: Buffer.from('{"schema":"evaos.mac_control.runtime_receipt.v1","binding_ref":"sha256:ok"}').toString( - 'base64url' - ), - signature: '-----BEGIN SSH SIGNATURE-----\nAAAA\n-----END SSH SIGNATURE-----\n', - keyId: 'connector-receipt-test-2026-07', - namespace: 'evaos-mac-control-receipt-v1', - }; + const envelope = signedConnectorEnvelope(validReceipt(authority, body.challenge, body.runRef)); const handler = createMacControlRuntimeReceiptHandler({ env, now: () => NOW_MS, @@ -203,7 +361,6 @@ test('burns a verified execution context before connector IO and rejects replay' return fetchResponse(200, envelope); }, }); - const body = { challenge: Buffer.alloc(32, 3).toString('base64url'), runRef: 'replay-proof' }; assert.equal((await invoke(handler, body, authority.headers)).statusCode, 200); const replay = await invoke(handler, body, authority.headers); assert.equal(replay.statusCode, 409); @@ -213,20 +370,11 @@ test('burns a verified execution context before connector IO and rejects replay' test('rejects a connector receipt that contains raw authority without reflecting it', async () => { const authority = signedAuthority(); - const leakingReceipt = Buffer.from( - JSON.stringify({ schema: 'evaos.mac_control.runtime_receipt.v1', customer_id: 'customer-1' }) - ).toString('base64url'); + const leakingReceipt = { schema: 'evaos.mac_control.runtime_receipt.v1', customer_id: 'customer-1' }; const handler = createMacControlRuntimeReceiptHandler({ env, now: () => NOW_MS, - fetchImpl: async () => - fetchResponse(200, { - schema: 'evaos.mac_control.runtime_receipt_envelope.v1', - receiptBase64: leakingReceipt, - signature: '-----BEGIN SSH SIGNATURE-----\nAAAA\n-----END SSH SIGNATURE-----\n', - keyId: 'connector-receipt-test-2026-07', - namespace: 'evaos-mac-control-receipt-v1', - }), + fetchImpl: async () => fetchResponse(200, signedConnectorEnvelope(leakingReceipt)), }); const response = await invoke( handler, @@ -238,6 +386,41 @@ test('rejects a connector receipt that contains raw authority without reflecting assert.equal(response.body.includes(CONNECTOR_TOKEN), false); }); +test('dedicated negative-proof runner emits only mechanically observed assertions', () => { + const root = mkdtempSync(join(tmpdir(), 'evaos-runtime-receipt-negative-')); + try { + const outputPath = join(root, 'negative-proof.json'); + const sourceHeadSha = 'd'.repeat(40); + const sourceRunId = '123456789'; + const completed = spawnSync( + process.execPath, + [ + fileURLToPath(new URL('../scripts/runtime-receipt-negative-proof.mjs', import.meta.url)), + outputPath, + sourceHeadSha, + sourceRunId, + ], + { encoding: 'utf8' } + ); + assert.equal(completed.status, 0, completed.stderr); + assert.equal(completed.stdout, ''); + assert.deepEqual(JSON.parse(readFileSync(outputPath, 'utf8')), { + schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', + sourceHeadSha, + sourceRunId, + assertions: { + forgedContextRejected: true, + expiredContextRejected: true, + replayRejected: true, + authorityRedacted: true, + }, + }); + assert.equal(statSync(outputPath).mode & 0o777, 0o600); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + function signedAuthority(overrides = {}) { const now = Math.floor(NOW_MS / 1000); const context = { @@ -257,6 +440,7 @@ function signedAuthority(overrides = {}) { const payload = payloadBytes.toString('base64url'); const signature = sign(null, payloadBytes, privateKey).toString('base64url'); return { + context, payload, signature, headers: { @@ -276,6 +460,162 @@ function signedAuthority(overrides = {}) { }; } +function validReceipt(authority, challenge, runRef, overrides = {}) { + const controlState = { + active: true, + generation: 7, + killSwitch: false, + mode: 'full_access', + ready: true, + takeoverActive: false, + }; + const actionArgs = { keys: 'escape', dryRun: false }; + const auditId = 'audit-runtime-receipt-1234'; + return { + schema: 'evaos.mac_control.runtime_receipt.v1', + keyId: RECEIPT_KEY_ID, + namespace: RECEIPT_NAMESPACE, + executedAt: '2026-07-15T00:00:10Z', + runtime: 'openclaw', + challenge, + runRef, + contextKeyId: KEY_ID, + executionContextDigest: saltedHash(challenge, Buffer.from(authority.payload, 'base64url')), + contextRef: saltedHash(challenge, authority.context.context_id), + contextIssuedAt: authority.context.issued_at, + contextExpiresAt: authority.context.expires_at, + customerRef: saltedHash(challenge, authority.context.customer_id), + vmRef: saltedHash(challenge, authority.context.customer_vm_id), + bindingRef: saltedHash(challenge, authority.context.binding_id), + bindingVersion: authority.context.binding_version, + bindingExpiresAt: authority.headers['x-evaos-mac-control-binding-expires-at'], + sessionRef: saltedHash(challenge, `${authority.context.binding_id}\0${controlState.generation}`), + controlStateBefore: controlState, + controlStateAfter: controlState, + controlStateBeforeDigest: sha256(canonicalBytes(controlState)), + controlStateAfterDigest: sha256(canonicalBytes(controlState)), + candidate: { + sourceCommit: EXPECTED_COMMIT, + sourceSha256: EXPECTED_SOURCE_SHA256, + sourcePath: 'resources/evaos-beta/bridge', + sourceOwner: '100yenadmin/evaOS-GUI', + status: 'vendored', + appPath: '/Applications/evaOS Workbench.app', + appVersion: '2.1.36', + appBuild: '2.1.36', + appBundleId: 'com.evaos.workbench', + appName: 'evaOS Workbench', + executable: '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/python/bin/python3.12', + argv0: '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py', + owner: { + label: 'com.electricsheep.evaos-desktop-bridge', + classification: 'workbench_bundle', + bundleId: 'com.evaos.workbench', + sourceCommit: EXPECTED_COMMIT, + programPath: { + kind: 'path', + value: '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge', + }, + appPath: { kind: 'path', value: '/Applications/evaOS Workbench.app' }, + manifestPath: { + kind: 'path', + value: '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/manifest.json', + }, + plistPath: { + kind: 'path', + value: '/Users/test/Library/LaunchAgents/com.electricsheep.evaos-desktop-bridge.plist', + }, + }, + }, + action: { command: 'customer_mac.desktop_hotkey', args: actionArgs }, + actionArgsDigest: sha256(canonicalBytes(actionArgs)), + auditId, + auditTimestamp: '2026-07-15T00:00:09Z', + auditRecordDigest: 'c'.repeat(64), + ...overrides, + }; +} + +function signedConnectorEnvelope(receipt) { + const receiptBytes = canonicalBytes(receipt); + return { + schema: 'evaos.mac_control.runtime_receipt_envelope.v1', + receiptBase64: receiptBytes.toString('base64url'), + signature: sshSignature(receiptBytes), + keyId: RECEIPT_KEY_ID, + namespace: RECEIPT_NAMESPACE, + }; +} + +function sshSignature(message) { + const algorithm = Buffer.from('ssh-ed25519'); + const namespace = Buffer.from(RECEIPT_NAMESPACE); + const hashAlgorithm = Buffer.from('sha512'); + const digest = createHash('sha512').update(message).digest(); + const signedData = Buffer.concat([ + Buffer.from('SSHSIG'), + sshString(namespace), + sshString(Buffer.alloc(0)), + sshString(hashAlgorithm), + sshString(digest), + ]); + const signature = sign(null, signedData, receiptPrivateKey); + const publicKeyBlob = Buffer.concat([sshString(algorithm), sshString(rawReceiptPublicKey)]); + const signatureBlob = Buffer.concat([sshString(algorithm), sshString(signature)]); + const binary = Buffer.concat([ + Buffer.from('SSHSIG'), + uint32(1), + sshString(publicKeyBlob), + sshString(namespace), + sshString(Buffer.alloc(0)), + sshString(hashAlgorithm), + sshString(signatureBlob), + ]); + const encoded = binary + .toString('base64') + .match(/.{1,70}/g) + .join('\n'); + return `-----BEGIN SSH SIGNATURE-----\n${encoded}\n-----END SSH SIGNATURE-----\n`; +} + +function uint32(value) { + const buffer = Buffer.alloc(4); + buffer.writeUInt32BE(value, 0); + return buffer; +} + +function sshString(value) { + return Buffer.concat([uint32(value.length), value]); +} + +function canonicalBytes(value) { + return Buffer.from(JSON.stringify(sortJson(value))); +} + +function sortJson(value) { + if (Array.isArray(value)) return value.map(sortJson); + if (value === null || typeof value !== 'object') return value; + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, sortJson(value[key])]) + ); +} + +function sha256(value) { + return createHash('sha256').update(value).digest('hex'); +} + +function saltedHash(challenge, value) { + return sha256( + Buffer.concat([ + Buffer.from(challenge, 'ascii'), + Buffer.from([0]), + typeof value === 'string' ? Buffer.from(value, 'utf8') : value, + ]) + ); +} + async function invoke(handler, body, headers, method = 'POST') { const request = Readable.from([JSON.stringify(body)]); request.method = method; diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py index c753e05dfa..e101bf5304 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py @@ -28,6 +28,7 @@ default_process_identity, load_canary_config, receipt_envelope, + require_canary_authority_fresh, require_canary_control_state, sign_receipt, validate_action_audit, @@ -1482,7 +1483,10 @@ def do_POST(self) -> None: # noqa: N802 command, params, state_dir=state_dir ) argv = build_bridge_argv(command, prepared_params) - if command in TAKEOVER_WARNING_REMOTE_COMMANDS and params.get("dry_run") is False: + if ( + command in TAKEOVER_WARNING_REMOTE_COMMANDS + and params.get("dry_run") is False + ): session = read_control_session(state_dir) argv = _with_remote_control_generation( argv, session.get("generation") @@ -1545,6 +1549,9 @@ def _mac_control_canary(self) -> None: challenge = str(payload["challenge"]) run_ref = str(payload["runRef"]) with control_session_transaction(state_dir): + require_canary_authority_fresh( + context, payload["binding"]["bindingExpiresAt"] + ) before = require_canary_control_state( read_control_session(state_dir) ) @@ -1561,6 +1568,9 @@ def _mac_control_canary(self) -> None: ), generation, ) + require_canary_authority_fresh( + context, payload["binding"]["bindingExpiresAt"] + ) action_started_at = datetime.now(timezone.utc) exit_code, output = command_runner(argv) try: diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py index 46fa92f350..15cff5191a 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py @@ -79,33 +79,27 @@ "artifact_not_found", "not_found", ) -SELECTED_BINDING_PROOF_ASSERTIONS = ( - "attached", - "toolsReady", - "activeGrant", - "requiredCapabilityGroups", - "bindingIdPresent", - "bindingIdMatched", - "bindingVersionPresent", - "bindingVersionMatched", - "bindingExpiryPresent", - "bindingExpiryMatched", - "bindingExpiryValid", - "expectedLaunchTarget", - "callbackAccepted", - "proxySessionAccepted", -) SELECTED_BINDING_PROOF_FIELDS = { - "schema", "ok", - "runtime", - "launchMode", - "reason", - "httpStatus", - "sourceHeadSha", - "sourceRunId", - "assertions", - "secretScan", + "schema", + "proofKind", + "tool", + "outcome", + "runRef", + "executedAt", + "bindingRef", + "bindingVersion", + "sessionRef", + "expiresAt", + "auditRef", + "sourcePointer", + "candidate", +} +SELECTED_BINDING_CANDIDATE_FIELDS = { + "sourceCommit", + "sourceSha256", + "appVersion", + "appBuild", } LOCAL_WORKBENCH_CONTROL_START = "local_workbench_control_start" INSTALLED_WORKBENCH_BRIDGE_CLI = Path( @@ -762,28 +756,61 @@ def selected_binding_proof_binding( if not isinstance(proof, dict) or set(proof) != SELECTED_BINDING_PROOF_FIELDS: result["reason"] = "selected_binding_proof_shape_invalid" return result - assertions = proof.get("assertions") if isinstance(proof.get("assertions"), dict) else {} + candidate = ( + proof.get("candidate") if isinstance(proof.get("candidate"), dict) else {} + ) + executed_at_text = proof.get("executedAt") + try: + executed_at = datetime.fromisoformat( + str(executed_at_text or "").replace("Z", "+00:00") + ) + except ValueError: + executed_at = None + expires_at = proof.get("expiresAt") result.update( { "schema": proof.get("schema"), - "source_head_sha": proof.get("sourceHeadSha"), - "source_run_id": str(proof.get("sourceRunId") or ""), - "assertions_verified": sorted(assertions) if isinstance(assertions, dict) else [], + "runtime_receipt_verified": proof.get("schema") + == "evaos.mac_control.runtime_proof.v2", + "direct_mac_control_succeeded": proof.get("outcome") == "succeeded", + "candidate": { + "source_commit": candidate.get("sourceCommit"), + "source_sha256": candidate.get("sourceSha256"), + "app_version": candidate.get("appVersion"), + "app_build": candidate.get("appBuild"), + }, } ) result["ok"] = ( - proof.get("schema") == "evaos-mac-control-live-canary/v1" - and proof.get("ok") is True - and proof.get("runtime") == "openclaw" - and proof.get("launchMode") == "mac_control_tools" - and proof.get("reason") == "ready" - and proof.get("httpStatus") == 302 - and proof.get("sourceHeadSha") == expected_source_commit + proof.get("ok") is True + and proof.get("schema") == "evaos.mac_control.runtime_proof.v2" + and proof.get("proofKind") == "selected_binding_direct_mac_control" + and proof.get("tool") == "customer_mac.desktop_hotkey" + and proof.get("outcome") == "succeeded" and re.fullmatch(r"\d+", expected_source_run_id) is not None - and str(proof.get("sourceRunId") or "") == expected_source_run_id - and proof.get("secretScan") == "passed" - and set(assertions) == set(SELECTED_BINDING_PROOF_ASSERTIONS) - and all(assertions.get(assertion) is True for assertion in SELECTED_BINDING_PROOF_ASSERTIONS) + and re.fullmatch( + rf"gha:{re.escape(expected_source_run_id)}:[0-9a-f]{{24}}", + str(proof.get("runRef") or ""), + ) + is not None + and executed_at is not None + and executed_at.tzinfo is not None + and type(expires_at) is int + and int(executed_at.timestamp()) <= expires_at + and re.fullmatch(r"[0-9a-f]{64}", str(proof.get("bindingRef") or "")) + is not None + and re.fullmatch(r"[1-9][0-9]{0,18}", str(proof.get("bindingVersion") or "")) + is not None + and re.fullmatch(r"[0-9a-f]{64}", str(proof.get("sessionRef") or "")) + is not None + and re.fullmatch(r"[0-9a-f]{64}", str(proof.get("auditRef") or "")) + is not None + and proof.get("sourcePointer") == "evaos-desktop-bridge:runtime-receipt" + and set(candidate) == SELECTED_BINDING_CANDIDATE_FIELDS + and candidate.get("sourceCommit") == expected_source_commit + and candidate.get("sourceSha256") == expected_source_sha256 + and candidate.get("appVersion") == expected_version + and candidate.get("appBuild") == expected_build ) if result["ok"] is not True: result["reason"] = "selected_binding_proof_mismatch" @@ -1154,7 +1181,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument( "--selected-binding-proof", type=Path, - help="Sanitized mac-control-runtime.json proof from the exact selected-binding callback canary.", + help="Sanitized mac-control-runtime.json proof from the verified selected-binding runtime receipt.", ) parser.add_argument( "--selected-binding-proof-run-id", diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py index 533d492aa8..26fbd38871 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py @@ -25,8 +25,8 @@ NATIVE_VERIFIER_NAME = "evaos-ed25519-verify" REPLAY_FILE = "mac-control-canary-replay.jsonl" MAX_CONTEXT_BYTES = 64 * 1024 -MAX_CONTEXT_AGE_SECONDS = 120 -MAX_FUTURE_SKEW_SECONDS = 15 +MAX_CONTEXT_AGE_SECONDS = 60 +MAX_FUTURE_SKEW_SECONDS = 5 _TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$") _CHALLENGE_RE = re.compile(r"^[A-Za-z0-9_-]{32,128}$") @@ -217,26 +217,53 @@ def validate_canary_request( "bindingVersion" ) != context.get("binding_version"): raise CanaryError("execution_context_binding_mismatch", status=403) - current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc) - issued_at = _parse_timestamp(context.get("issued_at"), "execution_context_invalid") - expires_at = _parse_timestamp( - context.get("expires_at"), "execution_context_invalid" - ) - age = (current - issued_at).total_seconds() + current = int((now or datetime.now(timezone.utc)).timestamp()) + issued_at = context.get("issued_at") + expires_at = context.get("expires_at") + if ( + isinstance(issued_at, bool) + or not isinstance(issued_at, int) + or isinstance(expires_at, bool) + or not isinstance(expires_at, int) + ): + raise CanaryError("execution_context_invalid") + age = current - issued_at if age < -MAX_FUTURE_SKEW_SECONDS or age > MAX_CONTEXT_AGE_SECONDS: raise CanaryError("execution_context_expired", status=403) if ( expires_at <= current or expires_at <= issued_at - or binding_expires_at <= current - or expires_at > binding_expires_at + or binding_expires_at.timestamp() <= current + or expires_at > int(binding_expires_at.timestamp()) ): raise CanaryError("execution_context_expired", status=403) - if (expires_at - issued_at).total_seconds() > MAX_CONTEXT_AGE_SECONDS: + if expires_at - issued_at > MAX_CONTEXT_AGE_SECONDS: raise CanaryError("execution_context_expired", status=403) return context, raw_context, signature +def require_canary_authority_fresh( + context: dict[str, Any], + binding_expires_at: Any, + *, + now: datetime | None = None, +) -> None: + """Recheck short-lived authority immediately before a live Mac mutation.""" + current = int((now or datetime.now(timezone.utc)).timestamp()) + expires_at = context.get("expires_at") + binding_expiry = _parse_timestamp( + binding_expires_at, "canary_binding_invalid", status=403 + ) + if ( + isinstance(expires_at, bool) + or not isinstance(expires_at, int) + or expires_at <= current + or binding_expiry.timestamp() <= current + or expires_at > int(binding_expiry.timestamp()) + ): + raise CanaryError("execution_context_expired", status=403) + + def replay_digest(raw_context: bytes, signature: bytes) -> str: return hashlib.sha256( b"evaos-mac-control-context-v1\0" + raw_context + b"\0" + signature @@ -515,7 +542,6 @@ def receipt_envelope( receipt: dict[str, Any], sshsig: str, config: CanaryConfig ) -> dict[str, Any]: return { - "ok": True, "schema": RECEIPT_ENVELOPE_SCHEMA, "receiptBase64": _base64url_encode(_canonical_json(receipt)), "signature": sshsig, diff --git a/scripts/evaosBetaReleaseGate.js b/scripts/evaosBetaReleaseGate.js index c9468a65de..3b13a38f14 100644 --- a/scripts/evaosBetaReleaseGate.js +++ b/scripts/evaosBetaReleaseGate.js @@ -92,6 +92,7 @@ const RC_PROOF_MANIFEST_NAME = 'evaos-beta-rc-proof.json'; const BROKER_LIVE_CANARY_PROOF_NAME = 'broker-runtime-status.json'; const BUSINESS_BROWSER_LIVE_CANARY_PROOF_NAME = 'business-browser.json'; const MAC_CONTROL_LIVE_CANARY_PROOF_NAME = 'mac-control-runtime.json'; +const MAC_CONTROL_NEGATIVE_PROOF_NAME = 'mac-control-runtime-negative.json'; const MAC_CONTROL_PROVISION_PROOF_NAME = 'mac-control-session-provisioning.json'; const MAC_CONTROL_CLEANUP_PROOF_NAME = 'mac-control-session-cleanup.json'; const RELEASE_ASSET_EXTS = new Set(['.exe', '.msi', '.dmg', '.deb', '.zip', '.yml']); @@ -279,21 +280,21 @@ const LIVE_CANARY_SECRET_VALUE_PATTERNS = [ /\bBearer\s+[A-Za-z0-9._-]{8,}\b/i, /[?&#](?:access[_-]?token|refresh[_-]?token|desktop[_-]?session|provider[_-]?grant|grant[_-]?handle|api[_-]?key|service[_-]?role|token|secret|password|credential)=/i, ]; -const MAC_CONTROL_LIVE_CANARY_ASSERTIONS = Object.freeze([ - 'attached', - 'toolsReady', - 'activeGrant', - 'requiredCapabilityGroups', - 'bindingIdPresent', - 'bindingIdMatched', - 'bindingVersionPresent', - 'bindingVersionMatched', - 'bindingExpiryPresent', - 'bindingExpiryMatched', - 'bindingExpiryValid', - 'expectedLaunchTarget', - 'callbackAccepted', - 'proxySessionAccepted', +const MAC_CONTROL_RUNTIME_PROOF_FIELDS = Object.freeze([ + 'ok', + 'schema', + 'proofKind', + 'tool', + 'outcome', + 'runRef', + 'executedAt', + 'bindingRef', + 'bindingVersion', + 'sessionRef', + 'expiresAt', + 'auditRef', + 'sourcePointer', + 'candidate', ]); function normalizeBoolean(value) { @@ -3261,13 +3262,13 @@ function verifyBrokerLiveCanaryProof(proofDir, env = process.env) { } if (normalizeBoolean(env.EVAOS_REQUIRE_MAC_CONTROL_LIVE_CANARY_PROOF)) { - verifyMacControlLiveCanaryProof(proofDir, env); + verifyMacControlLiveCanaryProof(proofDir, env, options); } return true; } -function verifyMacControlLiveCanaryProof(proofDir, env = process.env) { +function verifyMacControlLiveCanaryProof(proofDir, env = process.env, verificationOptions = {}) { const proofPath = requireExistingRelativeFile( proofDir, MAC_CONTROL_LIVE_CANARY_PROOF_NAME, @@ -3276,18 +3277,7 @@ function verifyMacControlLiveCanaryProof(proofDir, env = process.env) { const proof = readManifestFile(proofPath); assertLiveCanaryPlainObject(proof, 'Mac-control live canary proof'); - const allowedTopLevelFields = new Set([ - 'schema', - 'ok', - 'runtime', - 'launchMode', - 'reason', - 'httpStatus', - 'sourceHeadSha', - 'sourceRunId', - 'assertions', - 'secretScan', - ]); + const allowedTopLevelFields = new Set(MAC_CONTROL_RUNTIME_PROOF_FIELDS); for (const field of Object.keys(proof)) { if (!allowedTopLevelFields.has(field)) { throw new Error(`Mac-control live canary proof contains forbidden field: ${field}.`); @@ -3295,20 +3285,20 @@ function verifyMacControlLiveCanaryProof(proofDir, env = process.env) { } assertLiveCanaryNoSecretMaterial(proof, 'macControl'); - if (proof.schema !== 'evaos-mac-control-live-canary/v1') { + if (Object.keys(proof).length !== allowedTopLevelFields.size) { + throw new Error('Mac-control live canary proof is missing required runtime-receipt fields.'); + } + if (proof.schema !== 'evaos.mac_control.runtime_proof.v2') { throw new Error(`Unexpected Mac-control live canary proof schema: ${proof.schema}`); } if ( proof.ok !== true || - proof.runtime !== 'openclaw' || - proof.launchMode !== 'mac_control_tools' || - proof.reason !== 'ready' || - proof.httpStatus !== 302 + proof.proofKind !== 'selected_binding_direct_mac_control' || + proof.tool !== 'customer_mac.desktop_hotkey' || + proof.outcome !== 'succeeded' || + proof.sourcePointer !== 'evaos-desktop-bridge:runtime-receipt' ) { - throw new Error('Mac-control live canary release gate requires a successful ready proof.'); - } - if (proof.secretScan !== 'passed') { - throw new Error('Mac-control live canary proof must pass secret scanning.'); + throw new Error('Mac-control live canary release gate requires a successful direct-control runtime receipt.'); } const expectedHeadSha = String(env.EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA || '').trim(); @@ -3319,25 +3309,86 @@ function verifyMacControlLiveCanaryProof(proofDir, env = process.env) { if (!/^\d+$/.test(expectedRunId)) { throw new Error('Mac-control live canary proof requires EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID.'); } - if (proof.sourceHeadSha !== expectedHeadSha) { - throw new Error('Mac-control live canary proof source head does not match the release commit.'); + if (!new RegExp(`^gha:${expectedRunId}:[0-9a-f]{24}$`).test(String(proof.runRef || ''))) { + throw new Error('Mac-control live canary proof run does not match the selected proof run.'); } - if (String(proof.sourceRunId || '') !== expectedRunId) { - throw new Error('Mac-control live canary proof source run does not match the selected proof run.'); + + const executedAtText = String(proof.executedAt || ''); + const executedAt = Date.parse(executedAtText); + const expiresAtMs = Number.isSafeInteger(proof.expiresAt) ? proof.expiresAt * 1000 : Number.NaN; + const maxAgeRaw = String(env.EVAOS_LIVE_CANARY_MAX_PROOF_AGE_HOURS || '24').trim(); + const configuredMaxAgeHours = Number.parseFloat(maxAgeRaw); + const maxAgeHours = + Number.isFinite(verificationOptions.maxAgeHours) && verificationOptions.maxAgeHours > 0 + ? verificationOptions.maxAgeHours + : Number.isFinite(configuredMaxAgeHours) && configuredMaxAgeHours > 0 + ? configuredMaxAgeHours + : 24; + const configuredVerificationNow = + verificationOptions.now instanceof Date ? verificationOptions.now.getTime() : Number.NaN; + const verificationNow = Number.isFinite(configuredVerificationNow) ? configuredVerificationNow : Date.now(); + const receiptAgeMs = verificationNow - executedAt; + if ( + !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(executedAtText) || + !Number.isFinite(executedAt) || + !Number.isFinite(expiresAtMs) || + receiptAgeMs < -5_000 || + receiptAgeMs > maxAgeHours * 60 * 60 * 1000 || + executedAt > expiresAtMs || + expiresAtMs - executedAt > 66_000 || + !/^[0-9a-f]{64}$/.test(String(proof.bindingRef || '')) || + !/^[1-9][0-9]{0,18}$/.test(String(proof.bindingVersion || '')) || + !/^[0-9a-f]{64}$/.test(String(proof.sessionRef || '')) || + !/^[0-9a-f]{64}$/.test(String(proof.auditRef || '')) + ) { + throw new Error('Mac-control live canary runtime receipt fields are invalid.'); } - assertLiveCanaryPlainObject(proof.assertions, 'Mac-control live canary assertions'); - const assertionKeys = Object.keys(proof.assertions); + assertLiveCanaryPlainObject(proof.candidate, 'Mac-control live canary candidate'); + const candidateFields = ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild']; if ( - assertionKeys.length !== MAC_CONTROL_LIVE_CANARY_ASSERTIONS.length || - assertionKeys.some((assertion) => !MAC_CONTROL_LIVE_CANARY_ASSERTIONS.includes(assertion)) + Object.keys(proof.candidate).length !== candidateFields.length || + Object.keys(proof.candidate).some((field) => !candidateFields.includes(field)) ) { - throw new Error('Mac-control live canary proof assertions do not match the required release contract.'); + throw new Error('Mac-control live canary candidate fields do not match the required release contract.'); } - for (const assertion of MAC_CONTROL_LIVE_CANARY_ASSERTIONS) { - if (proof.assertions[assertion] !== true) { - throw new Error(`Mac-control live canary proof assertion ${assertion} must be true.`); - } + const expectedSourceSha256 = committedBridgeSourceIdentity(expectedHeadSha).sourceSha256; + const expectedVersion = packageVersionAtCommit(expectedHeadSha); + if ( + proof.candidate.sourceCommit !== expectedHeadSha || + proof.candidate.sourceSha256 !== expectedSourceSha256 || + proof.candidate.appVersion !== expectedVersion || + proof.candidate.appBuild !== expectedVersion + ) { + throw new Error('Mac-control live canary candidate does not match the exact release commit.'); + } + + const negativePath = requireExistingRelativeFile( + proofDir, + MAC_CONTROL_NEGATIVE_PROOF_NAME, + 'Mac-control runtime-receipt negative proof' + ); + const negativeProof = readManifestFile(negativePath); + assertLiveCanaryPlainObject(negativeProof, 'Mac-control runtime-receipt negative proof'); + assertLiveCanaryNoSecretMaterial(negativeProof, 'macControlNegative'); + const negativeFields = ['schema', 'sourceHeadSha', 'sourceRunId', 'assertions']; + if ( + Object.keys(negativeProof).length !== negativeFields.length || + Object.keys(negativeProof).some((field) => !negativeFields.includes(field)) || + negativeProof.schema !== 'evaos.mac_control.runtime_receipt_negative_proof.v1' || + negativeProof.sourceHeadSha !== expectedHeadSha || + String(negativeProof.sourceRunId || '') !== expectedRunId + ) { + throw new Error('Mac-control runtime-receipt negative proof does not match the exact release run.'); + } + assertLiveCanaryPlainObject(negativeProof.assertions, 'Mac-control runtime-receipt negative assertions'); + const negativeAssertions = ['forgedContextRejected', 'expiredContextRejected', 'replayRejected', 'authorityRedacted']; + if ( + Object.keys(negativeProof.assertions).length !== negativeAssertions.length || + Object.keys(negativeProof.assertions).some((field) => !negativeAssertions.includes(field)) || + negativeAssertions.some((field) => negativeProof.assertions[field] !== true) + ) { + throw new Error('Mac-control runtime-receipt negative assertions are incomplete.'); } const provisionPath = requireExistingRelativeFile( diff --git a/scripts/evaosBrokerLiveCanary.js b/scripts/evaosBrokerLiveCanary.js index 2b80d0241f..b7ce18bddf 100644 --- a/scripts/evaosBrokerLiveCanary.js +++ b/scripts/evaosBrokerLiveCanary.js @@ -1,5 +1,10 @@ #!/usr/bin/env node +const { createHash, randomBytes } = require('node:crypto'); + +const { committedBridgeSourceIdentity } = require('./evaosBetaReleaseGate.js'); +const { version: WORKBENCH_VERSION } = require('../package.json'); + const DEFAULT_ENDPOINT = 'https://rhfojelkgtwcxnrfhtlj.supabase.co/functions/v1/desktop-runtime-session'; const REQUIRED_BROKER_SURFACES = Object.freeze([ Object.freeze({ surface: 'evaos', runtime: 'openclaw' }), @@ -10,6 +15,7 @@ const REQUIRED_BROKER_SURFACES = Object.freeze([ ]); const MAC_CONTROL_CANARY_ACK = 'evaos-mac-control-canary'; const MAC_CONTROL_RUNTIME = 'openclaw'; +const MAC_CONTROL_RUNTIME_RECEIPT_PATH = '/api/v1/evaos/mac-control/runtime-receipt'; const MAC_CONTROL_LAUNCH_PRIVATE = Symbol('mac-control-launch-private'); const MAC_CONTROL_REQUIRED_CAPABILITY_GROUPS = Object.freeze([ Object.freeze(['customer_mac_status']), @@ -35,6 +41,7 @@ const MAC_CONTROL_SAFE_REASON_CODES = new Set([ 'mac_node_unpaired', 'missing_required_capability', 'runtime_launch_blocked', + 'runtime_receipt_rejected', 'selected_customer_scope_mismatch', 'selected_scope_not_ready', ]); @@ -513,7 +520,12 @@ function sanitizeMacControlRuntimeLaunchCanaryResponse(raw, request, now = Date. }, }; Object.defineProperty(sanitized, MAC_CONTROL_LAUNCH_PRIVATE, { - value: Object.freeze({ launchUrl: launchUrl.toString(), bindingExpiry }), + value: Object.freeze({ + launchUrl: launchUrl.toString(), + bindingId, + bindingVersion, + bindingExpiry, + }), enumerable: false, }); return sanitized; @@ -813,7 +825,7 @@ function responseSetCookieHeaders(headers) { return combined ? splitCombinedSetCookie(combined) : []; } -function hasLiveProxySessionCookie(headers, now) { +function liveProxySessionCookie(headers, now) { for (const header of responseSetCookieHeaders(headers)) { const parts = String(header) .split(';') @@ -822,7 +834,9 @@ function hasLiveProxySessionCookie(headers, now) { if (separator < 1 || parts[0].slice(0, separator).trim() !== 'evaos_session') continue; let value = parts[0].slice(separator + 1).trim(); if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1); - if (!value || value.toLowerCase() === 'deleted') continue; + if (!/^[A-Za-z0-9._~+/=-]{16,4096}$/.test(value) || value.toLowerCase() === 'deleted') { + continue; + } const attributes = new Map(); let duplicateAttribute = false; @@ -855,9 +869,139 @@ function hasLiveProxySessionCookie(headers, now) { const expiresAt = Date.parse(attributes.get('expires')); if (!Number.isFinite(expiresAt) || expiresAt <= now) continue; } - return true; + return `evaos_session=${value}`; + } + return undefined; +} + +function hasLiveProxySessionCookie(headers, now) { + return Boolean(liveProxySessionCookie(headers, now)); +} + +function expectedMacControlCandidate(env) { + const sourceCommit = envText(env, 'GITHUB_SHA'); + if (!/^[0-9a-f]{40}$/i.test(sourceCommit)) { + throw macControlFailure( + 'invalid_configuration', + 'Mac-control runtime receipt proof requires the exact source head.' + ); + } + const sourceSha256 = committedBridgeSourceIdentity(sourceCommit).sourceSha256; + return { + sourceCommit, + sourceSha256, + appVersion: WORKBENCH_VERSION, + appBuild: WORKBENCH_VERSION, + }; +} + +function macControlSaltedReference(challenge, value) { + return createHash('sha256') + .update(Buffer.from(challenge, 'ascii')) + .update(Buffer.from([0])) + .update(Buffer.from(value, 'utf8')) + .digest('hex'); +} + +function sanitizeMacControlRuntimeProof( + raw, + { + challenge, + runRef, + expectedCandidate, + selectedBindingId, + selectedBindingVersion, + bindingExpiry, + requestStartedAt, + now, + } +) { + const proof = asPlainRecord(raw); + const candidate = asPlainRecord(proof?.candidate); + const exactProofFields = new Set([ + 'ok', + 'schema', + 'proofKind', + 'tool', + 'outcome', + 'runRef', + 'executedAt', + 'bindingRef', + 'bindingVersion', + 'sessionRef', + 'expiresAt', + 'auditRef', + 'sourcePointer', + 'candidate', + ]); + const exactCandidateFields = new Set(['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild']); + try { + assertNoSecretMaterial(proof); + } catch { + throw macControlFailure('runtime_receipt_rejected', 'Mac-control runtime receipt contained forbidden material.'); + } + const executedAtText = String(proof?.executedAt || ''); + const executedAt = Date.parse(executedAtText); + const expiresAtMs = Number.isSafeInteger(proof?.expiresAt) ? proof.expiresAt * 1000 : Number.NaN; + const expectedBindingRef = macControlSaltedReference(challenge, selectedBindingId); + if ( + !proof || + !candidate || + Object.keys(proof).length !== exactProofFields.size || + Object.keys(proof).some((field) => !exactProofFields.has(field)) || + Object.keys(candidate).length !== exactCandidateFields.size || + Object.keys(candidate).some((field) => !exactCandidateFields.has(field)) || + proof.ok !== true || + proof.schema !== 'evaos.mac_control.runtime_proof.v2' || + proof.proofKind !== 'selected_binding_direct_mac_control' || + proof.tool !== 'customer_mac.desktop_hotkey' || + proof.outcome !== 'succeeded' || + proof.runRef !== runRef || + !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(executedAtText) || + !Number.isFinite(executedAt) || + executedAt < requestStartedAt - 5_000 || + executedAt > now + 5_000 || + !Number.isFinite(expiresAtMs) || + executedAt > expiresAtMs || + expiresAtMs - executedAt > 66_000 || + proof.bindingRef !== expectedBindingRef || + proof.bindingVersion !== selectedBindingVersion || + !/^[0-9a-f]{64}$/.test(String(proof.sessionRef || '')) || + expiresAtMs <= now || + expiresAtMs > bindingExpiry || + !/^[0-9a-f]{64}$/.test(String(proof.auditRef || '')) || + proof.sourcePointer !== 'evaos-desktop-bridge:runtime-receipt' || + candidate.sourceCommit !== expectedCandidate.sourceCommit || + candidate.sourceSha256 !== expectedCandidate.sourceSha256 || + candidate.appVersion !== expectedCandidate.appVersion || + candidate.appBuild !== expectedCandidate.appBuild + ) { + throw macControlFailure( + 'runtime_receipt_rejected', + 'Mac-control runtime receipt did not match the exact signed candidate.' + ); } - return false; + return { + ok: true, + schema: proof.schema, + proofKind: proof.proofKind, + tool: proof.tool, + outcome: proof.outcome, + runRef: proof.runRef, + executedAt: proof.executedAt, + bindingRef: proof.bindingRef, + bindingVersion: proof.bindingVersion, + sessionRef: proof.sessionRef, + expiresAt: proof.expiresAt, + auditRef: proof.auditRef, + sourcePointer: proof.sourcePointer, + candidate: { + sourceCommit: candidate.sourceCommit, + sourceSha256: candidate.sourceSha256, + appVersion: candidate.appVersion, + appBuild: candidate.appBuild, + }, + }; } function failedMacControlProof(env, reason, extras = {}) { @@ -907,6 +1051,20 @@ async function postMacControlRuntimeLaunch(fetchImpl, config) { return response.json(); } +async function postMacControlRuntimeReceipt(fetchImpl, launchUrl, sessionCookie, challenge, runRef) { + const endpoint = new URL(MAC_CONTROL_RUNTIME_RECEIPT_PATH, launchUrl); + return fetchImpl(endpoint.toString(), { + method: 'POST', + redirect: 'manual', + headers: { + Cookie: sessionCookie, + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ challenge, runRef }), + }); +} + async function runMacControlLiveCanary(options = {}) { const env = options.env ?? process.env; const fetchImpl = options.fetchImpl ?? fetch; @@ -928,7 +1086,8 @@ async function runMacControlLiveCanary(options = {}) { const callback = await fetchImpl(launchPrivate.launchUrl, { method: 'GET', redirect: 'manual' }); const postCallbackNow = now(); const callbackAccepted = callback.status === 302 && callback.headers.get('location') === '/ui/'; - const proxySessionAccepted = hasLiveProxySessionCookie(callback.headers, postCallbackNow); + const proxySessionCookie = liveProxySessionCookie(callback.headers, postCallbackNow); + const proxySessionAccepted = Boolean(proxySessionCookie); if (!callbackAccepted || !proxySessionAccepted) { throw macControlFailure('callback_rejected', 'Mac-control proxy callback did not accept the staged session.', { httpStatus: callback.status, @@ -940,21 +1099,55 @@ async function runMacControlLiveCanary(options = {}) { }); } - return { - schema: 'evaos-mac-control-live-canary/v1', - ok: true, - runtime: MAC_CONTROL_RUNTIME, - launchMode: 'mac_control_tools', - reason: 'ready', - httpStatus: callback.status, - ...macControlProofSource(env), - assertions: { - ...launch.assertions, - callbackAccepted: true, - proxySessionAccepted: true, - }, - secretScan: 'passed', - }; + const expectedCandidate = expectedMacControlCandidate(env); + const randomBytesImpl = typeof options.randomBytes === 'function' ? options.randomBytes : randomBytes; + const challengeBytes = randomBytesImpl(32); + if (!(challengeBytes instanceof Uint8Array) || challengeBytes.byteLength !== 32) { + throw macControlFailure('invalid_configuration', 'Mac-control runtime receipt challenge generation failed.'); + } + const runId = macControlProofSource(env).sourceRunId; + const runNonceBytes = randomBytesImpl(12); + if (!runId || !(runNonceBytes instanceof Uint8Array) || runNonceBytes.byteLength !== 12) { + throw macControlFailure('invalid_configuration', 'Mac-control runtime receipt run provenance is unavailable.'); + } + const challenge = Buffer.from(challengeBytes).toString('base64url'); + const runRef = `gha:${runId}:${Buffer.from(runNonceBytes).toString('hex')}`; + const receiptRequestStartedAt = now(); + const runtimeReceiptResponse = await postMacControlRuntimeReceipt( + fetchImpl, + launchPrivate.launchUrl, + proxySessionCookie, + challenge, + runRef + ); + if (!runtimeReceiptResponse.ok) { + throw macControlFailure( + 'runtime_receipt_rejected', + 'Mac-control runtime receipt endpoint rejected the direct-control proof.', + { httpStatus: runtimeReceiptResponse.status } + ); + } + let rawRuntimeProof; + try { + rawRuntimeProof = await runtimeReceiptResponse.json(); + } catch { + throw macControlFailure( + 'runtime_receipt_rejected', + 'Mac-control runtime receipt endpoint returned an invalid response.' + ); + } + const runtimeProof = sanitizeMacControlRuntimeProof(rawRuntimeProof, { + runRef, + expectedCandidate, + challenge, + selectedBindingId: launchPrivate.bindingId, + selectedBindingVersion: launchPrivate.bindingVersion, + bindingExpiry: launchPrivate.bindingExpiry, + requestStartedAt: receiptRequestStartedAt, + now: now(), + }); + + return runtimeProof; } catch (error) { if (error instanceof MacControlCanaryError) throw error; const reason = macControlReason(error?.reason, config ? 'invalid_response' : 'invalid_configuration'); @@ -1050,6 +1243,8 @@ module.exports = { sanitizeBrokerRuntimeCanaryResponse, sanitizeBrokerRuntimeLaunchCanaryResponse, sanitizeMacControlRuntimeLaunchCanaryResponse, + sanitizeMacControlRuntimeProof, + expectedMacControlCandidate, resolveBrokerCanaryCredentials, resolveMacControlCanaryConfig, nonOkResponseShapeSummary, diff --git a/scripts/evaosScanMacControlProofs.js b/scripts/evaosScanMacControlProofs.js new file mode 100644 index 0000000000..55e75f60ae --- /dev/null +++ b/scripts/evaosScanMacControlProofs.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +const fs = require('node:fs'); +const path = require('node:path'); + +const MAC_CONTROL_PROOF_NAMES = Object.freeze([ + 'mac-control-runtime.json', + 'mac-control-runtime-negative.json', + 'mac-control-session-provisioning.json', + 'mac-control-session-provisioning.stdout.json', + 'mac-control-session-cleanup.json', + 'mac-control-session-cleanup.stdout.json', +]); +const FORBIDDEN_NORMALIZED_FIELDS = new Set([ + 'accountemail', + 'accesstoken', + 'apikey', + 'authorization', + 'authkey', + 'bindingid', + 'challenge', + 'connectorurl', + 'connectortoken', + 'contextpayload', + 'contextsignature', + 'cookie', + 'credential', + 'customerid', + 'desktopsession', + 'endpoint', + 'executioncontext', + 'launchurl', + 'password', + 'privatekey', + 'receiptbase64', + 'refreshtoken', + 'servicerolekey', + 'session', + 'signature', +]); +const FORBIDDEN_VALUE = + /\beds_[A-Za-z0-9_-]{8,}\b|\bBearer\s+|\beyJ[A-Za-z0-9_-]{8,}\.|https?:\/\/|[?&]session=|\S+@\S+|-----BEGIN [A-Z0-9 ]*(?:PRIVATE KEY|SSH SIGNATURE)-----/i; + +function normalizedProofFieldName(value) { + return String(value || '') + .toLowerCase() + .replace(/[_-]/g, ''); +} + +function assertMacControlProofSanitized(value, location = '$') { + if (typeof value === 'string' && FORBIDDEN_VALUE.test(value)) { + throw new Error(`Mac-control proof contains forbidden sensitive material at ${location}.`); + } + if (Array.isArray(value)) { + value.forEach((entry, index) => assertMacControlProofSanitized(entry, `${location}[${index}]`)); + return; + } + if (!value || typeof value !== 'object') return; + for (const [key, entry] of Object.entries(value)) { + const normalizedKey = normalizedProofFieldName(key); + if ( + FORBIDDEN_NORMALIZED_FIELDS.has(normalizedKey) || + normalizedKey.includes('privatekey') || + normalizedKey.includes('keymaterial') + ) { + throw new Error(`Mac-control proof contains forbidden field ${key} at ${location}.`); + } + assertMacControlProofSanitized(entry, `${location}.${key}`); + } +} + +function scanMacControlProofDirectory(proofDir) { + const resolvedProofDir = path.resolve(String(proofDir || '')); + let scanned = 0; + for (const proofName of MAC_CONTROL_PROOF_NAMES) { + const proofPath = path.join(resolvedProofDir, proofName); + if (!fs.existsSync(proofPath)) continue; + const proof = JSON.parse(fs.readFileSync(proofPath, 'utf8')); + assertMacControlProofSanitized(proof, proofName); + scanned += 1; + } + if (scanned === 0) { + throw new Error('Mac-control secret/redaction scan found no proof artifacts.'); + } + const cleanupPath = path.join(resolvedProofDir, 'mac-control-session-cleanup.json'); + if (!fs.existsSync(cleanupPath)) { + throw new Error('Mac-control proof is missing sanitized cleanup evidence.'); + } + const cleanupProof = JSON.parse(fs.readFileSync(cleanupPath, 'utf8')); + if ( + cleanupProof.schema !== 'evaos-mac-control-canary-session-cleanup/v1' || + cleanupProof.sessionRevoked !== true || + cleanupProof.sensitiveOutput !== 'passed' + ) { + throw new Error('Mac-control proof did not prove temporary session revocation.'); + } + return { ok: true, scanned }; +} + +if (require.main === module) { + const proofDir = process.argv[2]; + if (!proofDir) throw new Error('Mac-control proof directory is required.'); + scanMacControlProofDirectory(proofDir); +} + +module.exports = { + MAC_CONTROL_PROOF_NAMES, + assertMacControlProofSanitized, + normalizedProofFieldName, + scanMacControlProofDirectory, +}; diff --git a/tests/unit/common-adapter/evaosIpcBridge.dom.test.ts b/tests/unit/common-adapter/evaosIpcBridge.dom.test.ts index 40f56325f9..af140b822a 100644 --- a/tests/unit/common-adapter/evaosIpcBridge.dom.test.ts +++ b/tests/unit/common-adapter/evaosIpcBridge.dom.test.ts @@ -138,6 +138,67 @@ describe('evaOS IPC bridge provider wrapper', () => { expect(outcome).toBe('Timed out waiting for evaOS provider response: evaos.native-companion.run-action'); }); + it('emits one exact selected-customer enrollment request and resolves only its matching native callback', async () => { + const emit = vi.fn(); + (window as typeof window & { electronAPI?: unknown }).electronAPI = { + on: vi.fn((callback: (event: { value: string }) => void) => { + callbacks.push(callback); + }), + emit, + }; + const { evaosNativeCompanion } = await import('@/common/adapter/ipcBridge'); + const request = { + action: 'secure_network_enroll' as const, + customerId: 'customer-selected-test', + agentLabel: 'evaOS Workbench', + }; + + const pending = evaosNativeCompanion.runAction.invoke(request); + + expect(emit).toHaveBeenCalledTimes(1); + expect(emit).toHaveBeenCalledWith('subscribe-evaos.native-companion.run-action', { + id: expect.stringMatching(/^evaos-/), + data: request, + }); + const emitted = emit.mock.calls[0][1] as { id: string }; + let settled = false; + void pending.finally(() => { + settled = true; + }); + callbacks[0]({ + value: JSON.stringify({ + name: 'subscribe.callback-evaos.native-companion.run-actionwrong-request', + data: { success: false, msg: 'must be ignored' }, + }), + }); + await Promise.resolve(); + expect(settled).toBe(false); + callbacks[0]({ + value: JSON.stringify({ + name: `subscribe.callback-evaos.native-companion.run-action${emitted.id}`, + data: { + success: true, + data: { + action: 'secure_network_enroll', + status: 'succeeded', + message: 'Enrollment submitted.', + sourcePointer: 'native-companion:secure-network-enrollment-submitted', + auditIds: [], + refreshRecommended: true, + }, + }, + }), + }); + + await expect(pending).resolves.toMatchObject({ + success: true, + data: { + action: 'secure_network_enroll', + sourcePointer: 'native-companion:secure-network-enrollment-submitted', + }, + }); + }); + it('keeps the default deadline for non-enrollment native-companion actions', async () => { vi.useFakeTimers(); (window as typeof window & { electronAPI?: unknown }).electronAPI = { diff --git a/tests/unit/evaos/evaosBrokerLiveCanary.test.ts b/tests/unit/evaos/evaosBrokerLiveCanary.test.ts index 34af185281..1784f2f6fa 100644 --- a/tests/unit/evaos/evaosBrokerLiveCanary.test.ts +++ b/tests/unit/evaos/evaosBrokerLiveCanary.test.ts @@ -7,6 +7,7 @@ */ import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; import fs from 'node:fs'; import { createRequire } from 'node:module'; import os from 'node:os'; @@ -15,7 +16,11 @@ import { describe, expect, it, vi } from 'vitest'; const require = createRequire(import.meta.url); const releaseGate = require('../../../scripts/evaosBetaReleaseGate.js') as { - verifyMacControlLiveCanaryProof: (proofDir: string, env: Record) => boolean; + verifyMacControlLiveCanaryProof: ( + proofDir: string, + env: Record, + options?: { now?: Date; maxAgeHours?: number } + ) => boolean; }; const liveCanary = require('../../../scripts/evaosBrokerLiveCanary.js') as { REQUIRED_BROKER_SURFACES: Array<{ surface: string; runtime: string }>; @@ -46,7 +51,27 @@ const liveCanary = require('../../../scripts/evaosBrokerLiveCanary.js') as { env: Record; fetchImpl: typeof fetch; now?: () => number; + randomBytes?: (size: number) => Uint8Array; }) => Promise>; + expectedMacControlCandidate: (env: Record) => { + sourceCommit: string; + sourceSha256: string; + appVersion: string; + appBuild: string; + }; + sanitizeMacControlRuntimeProof: ( + raw: unknown, + options: { + challenge: string; + runRef: string; + expectedCandidate: Record; + selectedBindingId: string; + selectedBindingVersion: string; + bindingExpiry: number; + requestStartedAt: number; + now: number; + } + ) => Record; sanitizeMacControlRuntimeLaunchCanaryResponse: ( raw: unknown, request: { customerId: string; runtime: string; expectedCallbackHost: string }, @@ -62,6 +87,14 @@ function jsonResponse(body: unknown, init: ResponseInit = {}): Response { }); } +function saltedReference(challenge: string, value: string): string { + return createHash('sha256') + .update(Buffer.from(challenge, 'ascii')) + .update(Buffer.from([0])) + .update(Buffer.from(value, 'utf8')) + .digest('hex'); +} + describe('evaOS broker live canary', () => { it('returns a sanitized runtime proof summary for clean broker responses', () => { const proof = liveCanary.sanitizeBrokerRuntimeCanaryResponse( @@ -583,6 +616,15 @@ describe('evaOS broker live canary', () => { it('proves selected-binding Mac-control launch and callback acceptance without persisting private identifiers', async () => { const now = Date.parse('2026-07-14T05:00:00.000Z'); + const sourceHeadSha = execFileSync('git', ['rev-parse', 'HEAD'], { + cwd: process.cwd(), + encoding: 'utf8', + }).trim(); + const challengeBytes = Buffer.alloc(32, 13); + const runNonceBytes = Buffer.alloc(12, 14); + const challenge = challengeBytes.toString('base64url'); + const runRef = `gha:123456789:${runNonceBytes.toString('hex')}`; + const expectedCandidate = liveCanary.expectedMacControlCandidate({ GITHUB_SHA: sourceHeadSha }); const bindingExpiresAt = new Date(now + 20_000).toISOString(); const binding = { schema_version: 'evaos.mac_control_runtime_readiness.v1', @@ -626,6 +668,24 @@ describe('evaOS broker live canary', () => { 'Set-Cookie': 'evaos_session=proxy_session_secret_for_test; Path=/; Max-Age=300; Secure; HttpOnly', }, }) + ) + .mockResolvedValueOnce( + jsonResponse({ + ok: true, + schema: 'evaos.mac_control.runtime_proof.v2', + proofKind: 'selected_binding_direct_mac_control', + tool: 'customer_mac.desktop_hotkey', + outcome: 'succeeded', + runRef, + executedAt: new Date(now).toISOString(), + bindingRef: saltedReference(challenge, binding.binding_id), + bindingVersion: '7', + sessionRef: 'b'.repeat(64), + expiresAt: Math.floor(now / 1000) + 15, + auditRef: 'c'.repeat(64), + sourcePointer: 'evaos-desktop-bridge:runtime-receipt', + candidate: expectedCandidate, + }) ); const proof = await liveCanary.runMacControlLiveCanary({ @@ -635,14 +695,15 @@ describe('evaOS broker live canary', () => { AIONUI_EVAOS_MAC_CONTROL_CANARY_CUSTOMER_ID: 'staging-mac-owner', AIONUI_EVAOS_MAC_CONTROL_CANARY_ENDPOINT: 'https://dashboard-staging.example.test/runtime', AIONUI_EVAOS_MAC_CONTROL_CANARY_EXPECTED_CALLBACK_HOST: 'openclaw-staging.example.test', - GITHUB_SHA: '4192aadf6b45bf1c6535f209fcc96f2be806863e', + GITHUB_SHA: sourceHeadSha, GITHUB_RUN_ID: '123456789', }, fetchImpl, now: () => now, + randomBytes: (size: number) => (size === 32 ? challengeBytes : runNonceBytes), }); - expect(fetchImpl).toHaveBeenCalledTimes(2); + expect(fetchImpl).toHaveBeenCalledTimes(3); expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toEqual({ action: 'runtime_launch', customer_id: 'staging-mac-owner', @@ -650,32 +711,34 @@ describe('evaOS broker live canary', () => { launch_mode: 'mac_control_tools', }); expect(fetchImpl.mock.calls[1][1]).toMatchObject({ method: 'GET', redirect: 'manual' }); - expect(proof).toMatchObject({ - schema: 'evaos-mac-control-live-canary/v1', - ok: true, - runtime: 'openclaw', - launchMode: 'mac_control_tools', - reason: 'ready', - httpStatus: 302, - sourceHeadSha: '4192aadf6b45bf1c6535f209fcc96f2be806863e', - sourceRunId: '123456789', - assertions: { - attached: true, - toolsReady: true, - activeGrant: true, - requiredCapabilityGroups: true, - bindingIdPresent: true, - bindingIdMatched: true, - bindingVersionPresent: true, - bindingVersionMatched: true, - bindingExpiryPresent: true, - bindingExpiryMatched: true, - bindingExpiryValid: true, - expectedLaunchTarget: true, - callbackAccepted: true, - proxySessionAccepted: true, + expect(fetchImpl.mock.calls[2][0]).toBe( + 'https://openclaw-staging.example.test/api/v1/evaos/mac-control/runtime-receipt' + ); + expect(fetchImpl.mock.calls[2][1]).toMatchObject({ + method: 'POST', + redirect: 'manual', + headers: { + Cookie: 'evaos_session=proxy_session_secret_for_test', + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json', }, - secretScan: 'passed', + }); + expect(JSON.parse(String(fetchImpl.mock.calls[2][1]?.body))).toEqual({ challenge, runRef }); + expect(proof).toEqual({ + ok: true, + schema: 'evaos.mac_control.runtime_proof.v2', + proofKind: 'selected_binding_direct_mac_control', + tool: 'customer_mac.desktop_hotkey', + outcome: 'succeeded', + runRef, + executedAt: new Date(now).toISOString(), + bindingRef: saltedReference(challenge, binding.binding_id), + bindingVersion: '7', + sessionRef: 'b'.repeat(64), + expiresAt: Math.floor(now / 1000) + 15, + auditRef: 'c'.repeat(64), + sourcePointer: 'evaos-desktop-bridge:runtime-receipt', + candidate: expectedCandidate, }); expect(JSON.stringify(proof)).not.toMatch( /staging-mac-owner|11111111-1111-4111-8111-111111111111|binding_version|binding_expires_at|callback_secret|proxy_session_secret|example\.test|eds_/ @@ -706,12 +769,30 @@ describe('evaOS broker live canary', () => { sensitiveOutput: 'passed', })}\n` ); + fs.writeFileSync( + path.join(proofDir, 'mac-control-runtime-negative.json'), + `${JSON.stringify({ + schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', + sourceHeadSha, + sourceRunId: '123456789', + assertions: { + forgedContextRejected: true, + expiredContextRejected: true, + replayRejected: true, + authorityRedacted: true, + }, + })}\n` + ); expect( - releaseGate.verifyMacControlLiveCanaryProof(proofDir, { - EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: '4192aadf6b45bf1c6535f209fcc96f2be806863e', - EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '123456789', - }) + releaseGate.verifyMacControlLiveCanaryProof( + proofDir, + { + EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: sourceHeadSha, + EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '123456789', + }, + { now: new Date(now), maxAgeHours: 24 } + ) ).toBe(true); const pythonSource = [ @@ -720,9 +801,9 @@ describe('evaOS broker live canary', () => { 'from evaos_desktop_bridge import qa_canary', 'result = qa_canary.selected_binding_proof_binding(', ' Path(sys.argv[1]),', - ' expected_source_commit="4192aadf6b45bf1c6535f209fcc96f2be806863e",', + ` expected_source_commit=${JSON.stringify(sourceHeadSha)},`, ' expected_source_run_id="123456789",', - ' expected_source_sha256="0" * 64,', + ` expected_source_sha256=${JSON.stringify(expectedCandidate.sourceSha256)},`, ' expected_version="2.1.36",', ' expected_build="2.1.36",', ')', @@ -744,6 +825,63 @@ describe('evaOS broker live canary', () => { } }); + it('rejects runtime receipts that are stale, overlong, future-dated, or cross-bound', () => { + const now = Date.parse('2026-07-15T00:00:20.000Z'); + const challenge = Buffer.alloc(32, 15).toString('base64url'); + const selectedBindingId = 'binding-selected-test'; + const selectedBindingVersion = '7'; + const runRef = 'gha:123456789:111111111111111111111111'; + const expectedCandidate = { + sourceCommit: 'a'.repeat(40), + sourceSha256: 'b'.repeat(64), + appVersion: '2.1.36', + appBuild: '2.1.36', + }; + const baseProof = { + ok: true, + schema: 'evaos.mac_control.runtime_proof.v2', + proofKind: 'selected_binding_direct_mac_control', + tool: 'customer_mac.desktop_hotkey', + outcome: 'succeeded', + runRef, + executedAt: '2026-07-15T00:00:20.000Z', + bindingRef: saltedReference(challenge, selectedBindingId), + bindingVersion: selectedBindingVersion, + sessionRef: 'b'.repeat(64), + expiresAt: Math.floor(now / 1000) + 60, + auditRef: 'c'.repeat(64), + sourcePointer: 'evaos-desktop-bridge:runtime-receipt', + candidate: expectedCandidate, + }; + const options = { + challenge, + runRef, + expectedCandidate, + selectedBindingId, + selectedBindingVersion, + bindingExpiry: now + 120_000, + requestStartedAt: now, + now, + }; + + for (const mutation of [ + { schema: 'evaos.mac_control.runtime_proof.v1' }, + { unexpected: true }, + { runRef: 'gha:123456789:222222222222222222222222' }, + { bindingRef: 'd'.repeat(64) }, + { bindingVersion: '999' }, + { candidate: { ...expectedCandidate, sourceCommit: 'd'.repeat(40) } }, + { executedAt: '2026-07-15T00:00:14.999Z' }, + { executedAt: '2026-07-15T00:00:25.001Z' }, + { executedAt: '2026-07-15 00:00:20Z' }, + { expiresAt: Math.floor(now / 1000) + 67 }, + ]) { + expect(() => liveCanary.sanitizeMacControlRuntimeProof({ ...baseProof, ...mutation }, options)).toThrow( + /runtime receipt did not match/i + ); + } + }); + it('rejects an incomplete top-level capability set even when runtime status is complete', () => { const now = Date.parse('2026-07-14T05:00:00.000Z'); const binding = { diff --git a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts index 87b7da9165..93a29f292c 100644 --- a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts +++ b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts @@ -14,6 +14,7 @@ import os import subprocess import tempfile import threading +import time import urllib.error import urllib.request from datetime import datetime, timedelta, timezone @@ -51,6 +52,7 @@ control = { "takeover_warning_started_at": None, "takeover_warning_until": None, "takeover_warning_seconds": 10, + "takeover_alert_signal_status": {}, } (root / "control-session.json").write_text(json.dumps(control), encoding="utf-8") os.chmod(root / "control-session.json", 0o600) @@ -129,7 +131,7 @@ thread.start() endpoint = f"http://127.0.0.1:{server.server_address[1]}/v1/canary/mac-control" counter = 0 -def payload(*, expired=False, cross_binding=False): +def payload(*, expired=False, cross_binding=False, expires_in=59): global counter counter += 1 now = datetime.now(timezone.utc).replace(microsecond=0) @@ -141,9 +143,9 @@ def payload(*, expired=False, cross_binding=False): "customer_vm_id": "vm-sensitive-id", "binding_id": "binding-sensitive-id", "binding_version": "42", - "issued_at": (now - timedelta(seconds=180 if expired else 1)).isoformat().replace("+00:00", "Z"), - "expires_at": (now - timedelta(seconds=1) if expired else now + timedelta(seconds=60)).isoformat().replace("+00:00", "Z"), - "context_id": f"context-sensitive-{counter}", + "issued_at": int((now - timedelta(seconds=180 if expired else 1)).timestamp()), + "expires_at": int((now - timedelta(seconds=1) if expired else now + timedelta(seconds=expires_in)).timestamp()), + "context_id": b64url(counter.to_bytes(16, "big")), } raw = json.dumps(context, sort_keys=True, separators=(",", ":")).encode() return { @@ -221,6 +223,20 @@ try: assert len(action_calls) == count assert post(payload(expired=True))[0] == 403 assert len(action_calls) == count + + blocked_result = [] + with connector_server.control_session_transaction(root): + blocked_request = payload(expires_in=1) + blocked_thread = threading.Thread( + target=lambda: blocked_result.append(post(blocked_request)), daemon=True + ) + blocked_thread.start() + time.sleep(1.5) + blocked_thread.join(timeout=5) + assert not blocked_thread.is_alive() + assert blocked_result == [(403, {"ok": False, "error": "execution_context_expired"})] + assert len(action_calls) == count + assert post(payload(cross_binding=True))[0] == 403 assert len(action_calls) == count signature_mode["forged"] = True diff --git a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts index d013cd5303..23eeded1b6 100644 --- a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts +++ b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts @@ -1,7 +1,14 @@ import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; import { describe, expect, it } from 'vitest'; const WORKFLOW_PATH = '.github/workflows/evaos-live-canary-proof.yml'; +const require = createRequire(import.meta.url); +const proofScanner = require('../../../scripts/evaosScanMacControlProofs.js') as { + scanMacControlProofDirectory: (proofDir: string) => { ok: boolean; scanned: number }; +}; function readWorkflow(): string { return fs.readFileSync(WORKFLOW_PATH, 'utf8'); @@ -127,6 +134,19 @@ describe('evaOS live canary proof workflow', () => { expect(workflow).toContain('node scripts/evaosProvisionLiveCanaryFixtures.js provision-mac-control'); expect(workflow).toContain('node scripts/evaosBrokerLiveCanary.js --mac-control'); expect(workflow).toContain('> "$PROOF_DIR/mac-control-runtime.json"'); + expect(workflow).toContain('Prove Mac-control runtime-receipt negative boundaries'); + const negativeStep = workflow.slice( + workflow.indexOf('- name: Prove Mac-control runtime-receipt negative boundaries'), + workflow.indexOf('- name: Run follow-up live canaries') + ); + expect(negativeStep).toContain( + "if: github.event.inputs.run_live_canaries == 'true' && github.event.inputs.run_mac_control_canary == 'true'" + ); + expect(negativeStep).toContain('run proof:runtime-receipt-negative --'); + expect(negativeStep).toContain('"$PROOF_DIR/mac-control-runtime-negative.json" "$GITHUB_SHA" "$GITHUB_RUN_ID"'); + expect(negativeStep).not.toContain('continue-on-error'); + expect(negativeStep).not.toContain('forgedContextRejected: true'); + expect(workflow).toContain('mac-control-runtime-negative.json'); expect(workflow).toContain('node scripts/evaosProvisionLiveCanaryFixtures.js cleanup-mac-control'); expect(workflow).toMatch( /- name: Cleanup Mac-control canary session[\s\S]*if: always\(\)[\s\S]*cleanup-mac-control/ @@ -134,10 +154,39 @@ describe('evaOS live canary proof workflow', () => { expect(workflow).toContain('mac-control-runtime.json'); expect(workflow).toContain('secret/redaction scan'); expect(workflow).toMatch(/- name: Run Mac-control proof secret\/redaction scan\n\s+id: mac-control-proof-scan/); - expect(workflow).toContain("cleanupProof.schema !== 'evaos-mac-control-canary-session-cleanup/v1'"); - expect(workflow).toContain('cleanupProof.sessionRevoked !== true'); + expect(workflow).toContain('node scripts/evaosScanMacControlProofs.js "$PROOF_DIR"'); expect(workflow).toContain( "if: always() && (github.event.inputs.run_mac_control_canary != 'true' || steps.mac-control-proof-scan.outcome == 'success')" ); }); + + it('executes a case-normalized Mac-control artifact scanner', () => { + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-proof-scan-')); + try { + fs.writeFileSync( + path.join(proofDir, 'mac-control-session-cleanup.json'), + `${JSON.stringify({ + schema: 'evaos-mac-control-canary-session-cleanup/v1', + sessionRevoked: true, + sensitiveOutput: 'passed', + })}\n` + ); + expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 1 }); + + for (const unsafe of [ + { Cookie: 'opaque-cookie-value-123456' }, + { Authorization: 'opaque-auth-value-123456' }, + { context_signature: 'opaque-signature-value-123456' }, + { receipt_base64: 'opaque-receipt-value-123456' }, + { connector_token: 'opaque-token-value-123456' }, + { receiptPrivateKeyPath: '/safe-looking/path' }, + { note: '-----BEGIN OPENSSH PRIVATE KEY-----' }, + ]) { + fs.writeFileSync(path.join(proofDir, 'mac-control-runtime.json'), `${JSON.stringify(unsafe)}\n`); + expect(() => proofScanner.scanMacControlProofDirectory(proofDir)).toThrow(/forbidden/i); + } + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } + }); }); diff --git a/tests/unit/evaos/evaosNativeCompanionStatus.test.ts b/tests/unit/evaos/evaosNativeCompanionStatus.test.ts index ccd8cd757e..695071cd45 100644 --- a/tests/unit/evaos/evaosNativeCompanionStatus.test.ts +++ b/tests/unit/evaos/evaosNativeCompanionStatus.test.ts @@ -5067,6 +5067,26 @@ describe('evaosNativeCompanionStatus', () => { expect(openExternal).not.toHaveBeenCalled(); }); + it('records receipt of an enrollment action before bridge executable lookup', async () => { + const diagnosticEvents: string[] = []; + + const result = await runNativeCompanionAction( + { action: 'secure_network_enroll', customerId: 'customer-selected-test' }, + { + bridgePaths: ['/missing/evaos-desktop-bridge'], + existsSync: () => false, + recordDiagnosticEvent: (eventCode) => diagnosticEvents.push(eventCode), + } + ); + + expect(result).toMatchObject({ + action: 'secure_network_enroll', + status: 'repair_required', + sourcePointer: 'native-companion:bridge-cli-missing', + }); + expect(diagnosticEvents).toEqual(['secure_network_enrollment_provider_received']); + }); + it('enrolls an unenrolled signed Tailscale client with file-backed one-use material', async () => { const authKey = 'one-use-private-network-key-for-test'; const createPrivateNetworkEnrollment = vi.fn(async () => ({ @@ -5451,6 +5471,7 @@ describe('evaosNativeCompanionStatus', () => { expect(JSON.stringify(result)).not.toContain('evaos-private-network-'); expect(result.enrollmentDiagnostic?.message).toContain('control server rejected enrollment'); expect(diagnosticEvents).toEqual([ + 'secure_network_enrollment_provider_received', 'secure_network_enrollment_action_started', 'secure_network_enrollment_broker_request_started', 'secure_network_enrollment_cli_started', @@ -5654,6 +5675,7 @@ describe('evaosNativeCompanionStatus', () => { sourcePointer: 'native-companion:secure-network-enrollment-cancel-unconfirmed', }); expect(diagnosticEvents).toEqual([ + 'secure_network_enrollment_provider_received', 'secure_network_enrollment_action_started', 'secure_network_enrollment_broker_request_started', 'secure_network_enrollment_cli_started', diff --git a/tests/unit/process/evaosBetaReleaseGate.test.ts b/tests/unit/process/evaosBetaReleaseGate.test.ts index 10acf9bb6a..67e730b349 100644 --- a/tests/unit/process/evaosBetaReleaseGate.test.ts +++ b/tests/unit/process/evaosBetaReleaseGate.test.ts @@ -39,7 +39,11 @@ const releaseGate = require('../../../scripts/evaosBetaReleaseGate.js') as { normalizeBoolean: (value: unknown) => boolean; requiresMacControlLiveCanaryProof: (tagOrVersion: string) => boolean; verifyBrokerLiveCanaryProof: (proofDir: string, env?: Record) => boolean; - verifyMacControlLiveCanaryProof: (proofDir: string, env?: Record) => boolean; + verifyMacControlLiveCanaryProof: ( + proofDir: string, + env?: Record, + options?: { now?: Date; maxAgeHours?: number } + ) => boolean; verifyReleaseManifest: (outputDir: string, tag: string, env: Record) => boolean; verifyRcProof: (proofDir: string, tag: string, env: Record) => boolean; writeRcProofTemplate: (proofDir: string, tag: string) => unknown; @@ -816,6 +820,8 @@ function mutateBrokerLiveCanaryProof(proofDir: string, mutator: (proof: Record = {}) { fs.mkdirSync(proofDir, { recursive: true }); + const sourceSha256 = releaseGate.committedBridgeSourceIdentity(fixtureReleaseCommit).sourceSha256; + const executedAt = Date.now(); fs.writeFileSync( path.join(proofDir, 'mac-control-session-provisioning.json'), `${JSON.stringify( @@ -837,32 +843,44 @@ function writeMacControlLiveCanaryProof(proofDir: string, overrides: Record { proof.ok = false; - proof.reason = 'binding_missing'; + proof.outcome = 'failed'; }, - error: /successful ready proof/, + error: /successful direct-control runtime receipt/, }, { name: 'incomplete', mutate: (proof) => { - const assertions = proof.assertions as Record; - assertions.bindingExpiryValid = false; + proof.bindingRef = 'not-a-reference'; }, - error: /bindingExpiryValid/, + error: /runtime receipt fields/i, }, { name: 'unsafe', @@ -2449,9 +2466,10 @@ printf '%s\\n' ok { name: 'wrong-head', mutate: (proof) => { - proof.sourceHeadSha = 'b'.repeat(40); + const candidate = proof.candidate as Record; + candidate.sourceCommit = 'b'.repeat(40); }, - error: /source head/i, + error: /candidate.*release commit/i, }, ]; @@ -2470,6 +2488,138 @@ printf '%s\\n' ok } }); + it('requires a fresh, canonical, bounded historical runtime receipt', () => { + const verificationNow = Date.parse('2026-07-15T12:00:00.000Z'); + const releaseEnv = { + EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: fixtureReleaseCommit, + EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '12345', + EVAOS_LIVE_CANARY_MAX_PROOF_AGE_HOURS: '24', + }; + const validExecutedAt = verificationNow - 60 * 60 * 1000; + const validDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-live-mac-control-historical-')); + try { + writeMacControlLiveCanaryProof(validDir, { + executedAt: new Date(validExecutedAt).toISOString(), + expiresAt: Math.floor(validExecutedAt / 1000) + 60, + }); + expect( + releaseGate.verifyMacControlLiveCanaryProof(validDir, releaseEnv, { + now: new Date(verificationNow), + maxAgeHours: 24, + }) + ).toBe(true); + } finally { + fs.rmSync(validDir, { recursive: true, force: true }); + } + + const cases = [ + { + name: 'future', + executedAt: verificationNow + 5_001, + expiresAt: Math.floor((verificationNow + 5_001) / 1000) + 60, + }, + { + name: 'stale', + executedAt: verificationNow - 25 * 60 * 60 * 1000, + expiresAt: Math.floor((verificationNow - 25 * 60 * 60 * 1000) / 1000) + 60, + }, + { + name: 'overlong', + executedAt: verificationNow - 1_000, + expiresAt: Math.floor((verificationNow - 1_000) / 1000) + 67, + }, + ]; + for (const testCase of cases) { + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), `evaos-live-mac-control-${testCase.name}-`)); + try { + writeMacControlLiveCanaryProof(proofDir, { + executedAt: new Date(testCase.executedAt).toISOString(), + expiresAt: testCase.expiresAt, + }); + expect(() => + releaseGate.verifyMacControlLiveCanaryProof(proofDir, releaseEnv, { + now: new Date(verificationNow), + maxAgeHours: 24, + }) + ).toThrow(/runtime receipt fields/i); + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } + } + + const noncanonicalDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-live-mac-control-noncanonical-')); + try { + writeMacControlLiveCanaryProof(noncanonicalDir, { + executedAt: '2026-07-15 11:59:59Z', + expiresAt: Math.floor(verificationNow / 1000) + 59, + }); + expect(() => + releaseGate.verifyMacControlLiveCanaryProof(noncanonicalDir, releaseEnv, { + now: new Date(verificationNow), + maxAgeHours: 24, + }) + ).toThrow(/runtime receipt fields/i); + } finally { + fs.rmSync(noncanonicalDir, { recursive: true, force: true }); + } + }); + + it('rejects missing, false, mismatched, or extended runtime-receipt negative proof', () => { + const releaseEnv = { + EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: fixtureReleaseCommit, + EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '12345', + }; + const cases: Array<{ + name: string; + mutate: (proofDir: string, proof: Record) => void; + }> = [ + { + name: 'missing', + mutate: (proofDir) => fs.rmSync(path.join(proofDir, 'mac-control-runtime-negative.json')), + }, + { + name: 'false-assertion', + mutate: (_proofDir, proof) => { + (proof.assertions as Record).replayRejected = false; + }, + }, + { + name: 'wrong-head', + mutate: (_proofDir, proof) => { + proof.sourceHeadSha = 'e'.repeat(40); + }, + }, + { + name: 'wrong-run', + mutate: (_proofDir, proof) => { + proof.sourceRunId = '99999'; + }, + }, + { + name: 'extra-field', + mutate: (_proofDir, proof) => { + proof.note = 'must not be accepted'; + }, + }, + ]; + + for (const testCase of cases) { + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), `evaos-live-mac-control-negative-${testCase.name}-`)); + try { + writeMacControlLiveCanaryProof(proofDir); + const proofPath = path.join(proofDir, 'mac-control-runtime-negative.json'); + const proof = JSON.parse(fs.readFileSync(proofPath, 'utf8')) as Record; + testCase.mutate(proofDir, proof); + if (fs.existsSync(proofPath)) fs.writeFileSync(proofPath, `${JSON.stringify(proof, null, 2)}\n`); + expect(() => releaseGate.verifyMacControlLiveCanaryProof(proofDir, releaseEnv)).toThrow( + /negative proof|negative assertions/i + ); + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } + } + }); + it('uses the broker canary customer as the expected live broker proof customer when configured', () => { const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-live-broker-proof-broker-customer-')); try { diff --git a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts index 9d661e8419..089a5fafdd 100644 --- a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts +++ b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts @@ -699,7 +699,7 @@ describe('prepareEvaosDesktopBridgeResource', () => { ' assert_connector_rejected(lambda payload: payload["connector"]["owner"].__setitem__("app_path", {"kind": "path", "value": "/Applications/evaOS Workbench.app/../Other.app"}))', ' assert_connector_rejected(lambda payload: payload["connector"]["owner"].__setitem__("manifest_path", {"kind": "path", "value": "/tmp/manifest.json"}))', ' selected_proof_path = Path(root) / "mac-control-runtime.json"', - ' selected_proof = {"schema": "evaos-mac-control-live-canary/v1", "ok": True, "runtime": "openclaw", "launchMode": "mac_control_tools", "reason": "ready", "httpStatus": 302, "sourceHeadSha": commit, "sourceRunId": "12345", "assertions": {name: True for name in qa_canary.SELECTED_BINDING_PROOF_ASSERTIONS}, "secretScan": "passed"}', + ' selected_proof = {"ok": True, "schema": "evaos.mac_control.runtime_proof.v2", "proofKind": "selected_binding_direct_mac_control", "tool": "customer_mac.desktop_hotkey", "outcome": "succeeded", "runRef": "gha:12345:" + "1" * 24, "executedAt": "2026-07-15T00:00:00Z", "bindingRef": "a" * 64, "bindingVersion": "7", "sessionRef": "b" * 64, "expiresAt": 1784073660, "auditRef": "c" * 64, "sourcePointer": "evaos-desktop-bridge:runtime-receipt", "candidate": {"sourceCommit": commit, "sourceSha256": source_sha256, "appVersion": "2.1.36", "appBuild": "2.1.36"}}', ' selected_proof_path.write_text(json.dumps(selected_proof), encoding="utf-8")', ' selected_binding = qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="12345", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36")', ' assert selected_binding["ok"] is True, selected_binding', From d8c86df0cf855854831a40003efd0ad6e9890878 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 06:21:09 +0700 Subject: [PATCH 04/17] fix(evaos): reject decorated proof secrets --- scripts/evaosScanMacControlProofs.js | 20 +++++++++++++++++-- .../evaos/evaosLiveCanaryWorkflow.test.ts | 19 ++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/scripts/evaosScanMacControlProofs.js b/scripts/evaosScanMacControlProofs.js index 55e75f60ae..9db00a42bb 100644 --- a/scripts/evaosScanMacControlProofs.js +++ b/scripts/evaosScanMacControlProofs.js @@ -40,6 +40,22 @@ const FORBIDDEN_NORMALIZED_FIELDS = new Set([ ]); const FORBIDDEN_VALUE = /\beds_[A-Za-z0-9_-]{8,}\b|\bBearer\s+|\beyJ[A-Za-z0-9_-]{8,}\.|https?:\/\/|[?&]session=|\S+@\S+|-----BEGIN [A-Z0-9 ]*(?:PRIVATE KEY|SSH SIGNATURE)-----/i; +const FORBIDDEN_NORMALIZED_FIELD_FRAGMENTS = Object.freeze([ + 'authorization', + 'cookie', + 'signature', + 'token', + 'credential', + 'password', + 'secret', + 'keymaterial', + 'receiptbase64', + 'contextpayload', + 'executioncontext', + 'connectorurl', + 'desktopsession', + 'launchurl', +]); function normalizedProofFieldName(value) { return String(value || '') @@ -60,8 +76,8 @@ function assertMacControlProofSanitized(value, location = '$') { const normalizedKey = normalizedProofFieldName(key); if ( FORBIDDEN_NORMALIZED_FIELDS.has(normalizedKey) || - normalizedKey.includes('privatekey') || - normalizedKey.includes('keymaterial') + FORBIDDEN_NORMALIZED_FIELD_FRAGMENTS.some((fragment) => normalizedKey.includes(fragment)) || + /private.*key/.test(normalizedKey) ) { throw new Error(`Mac-control proof contains forbidden field ${key} at ${location}.`); } diff --git a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts index 23eeded1b6..22cf4f5d9e 100644 --- a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts +++ b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts @@ -175,16 +175,35 @@ describe('evaOS live canary proof workflow', () => { for (const unsafe of [ { Cookie: 'opaque-cookie-value-123456' }, + { AuthorizationHeader: 'opaque-auth-value-123456' }, { Authorization: 'opaque-auth-value-123456' }, + { cookie_value: 'opaque-cookie-value-123456' }, { context_signature: 'opaque-signature-value-123456' }, + { receipt_signature_value: 'opaque-signature-value-123456' }, { receipt_base64: 'opaque-receipt-value-123456' }, { connector_token: 'opaque-token-value-123456' }, + { connector_token_value: 'opaque-token-value-123456' }, + { providerCredentialsBundle: 'opaque-credential-value-123456' }, + { password_hint: 'opaque-password-value-123456' }, { receiptPrivateKeyPath: '/safe-looking/path' }, + { privateSigningKey: 'opaque-key-value-123456' }, + { connectorKeyMaterial: 'opaque-key-material-value-123456' }, { note: '-----BEGIN OPENSSH PRIVATE KEY-----' }, ]) { fs.writeFileSync(path.join(proofDir, 'mac-control-runtime.json'), `${JSON.stringify(unsafe)}\n`); expect(() => proofScanner.scanMacControlProofDirectory(proofDir)).toThrow(/forbidden/i); } + + fs.writeFileSync( + path.join(proofDir, 'mac-control-runtime.json'), + `${JSON.stringify({ + bindingRef: 'a'.repeat(64), + sessionRef: 'b'.repeat(64), + auditRef: 'c'.repeat(64), + receiptKeyId: 'public-receipt-key-id', + })}\n` + ); + expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 2 }); } finally { fs.rmSync(proofDir, { recursive: true, force: true }); } From 009c6f480a0e04e25907e94da6e0862b46fe0fa4 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 06:25:26 +0700 Subject: [PATCH 05/17] fix(evaos): enforce exact proof artifact schemas --- scripts/evaosScanMacControlProofs.js | 101 ++++++++++++++++++ .../evaos/evaosLiveCanaryWorkflow.test.ts | 51 +++++++-- 2 files changed, 145 insertions(+), 7 deletions(-) diff --git a/scripts/evaosScanMacControlProofs.js b/scripts/evaosScanMacControlProofs.js index 9db00a42bb..a73602b3df 100644 --- a/scripts/evaosScanMacControlProofs.js +++ b/scripts/evaosScanMacControlProofs.js @@ -11,6 +11,75 @@ const MAC_CONTROL_PROOF_NAMES = Object.freeze([ 'mac-control-session-cleanup.json', 'mac-control-session-cleanup.stdout.json', ]); +const MAC_CONTROL_PROOF_CONTRACTS = Object.freeze({ + 'mac-control-runtime.json': { + schema: 'evaos.mac_control.runtime_proof.v2', + fields: [ + 'ok', + 'schema', + 'proofKind', + 'tool', + 'outcome', + 'runRef', + 'executedAt', + 'bindingRef', + 'bindingVersion', + 'sessionRef', + 'expiresAt', + 'auditRef', + 'sourcePointer', + 'candidate', + ], + nested: { + candidate: { + fields: ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild'], + }, + }, + }, + 'mac-control-runtime-negative.json': { + schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', + fields: ['schema', 'sourceHeadSha', 'sourceRunId', 'assertions'], + nested: { + assertions: { + fields: ['forgedContextRejected', 'expiredContextRejected', 'replayRejected', 'authorityRedacted'], + }, + }, + }, + 'mac-control-session-provisioning.json': { + schema: 'evaos-mac-control-canary-session-provision/v1', + fields: [ + 'schema', + 'accountConfigured', + 'customerConfigured', + 'activeMembershipVerified', + 'stagingMarkerVerified', + 'sessionMinted', + 'sessionExpiryPresent', + 'sensitiveOutput', + ], + }, + 'mac-control-session-provisioning.stdout.json': { + schema: 'evaos-mac-control-canary-session-provision/v1', + fields: [ + 'schema', + 'accountConfigured', + 'customerConfigured', + 'activeMembershipVerified', + 'stagingMarkerVerified', + 'sessionMinted', + 'sessionExpiryPresent', + 'sensitiveOutput', + ], + }, + 'mac-control-session-cleanup.json': { + schema: 'evaos-mac-control-canary-session-cleanup/v1', + fields: ['schema', 'sessionRevoked', 'sensitiveOutput'], + }, + 'mac-control-session-cleanup.stdout.json': { + schema: 'evaos-mac-control-canary-session-cleanup/v1', + fields: ['schema', 'sessionRevoked', 'sensitiveOutput'], + }, +}); const FORBIDDEN_NORMALIZED_FIELDS = new Set([ 'accountemail', 'accesstoken', @@ -56,6 +125,7 @@ const FORBIDDEN_NORMALIZED_FIELD_FRAGMENTS = Object.freeze([ 'desktopsession', 'launchurl', ]); +const ALLOWED_PUBLIC_KEY_IDENTIFIER_FIELDS = new Set(['contextkeyid', 'keyid', 'publickeyid', 'receiptkeyid']); function normalizedProofFieldName(value) { return String(value || '') @@ -77,6 +147,7 @@ function assertMacControlProofSanitized(value, location = '$') { if ( FORBIDDEN_NORMALIZED_FIELDS.has(normalizedKey) || FORBIDDEN_NORMALIZED_FIELD_FRAGMENTS.some((fragment) => normalizedKey.includes(fragment)) || + (normalizedKey.includes('key') && !ALLOWED_PUBLIC_KEY_IDENTIFIER_FIELDS.has(normalizedKey)) || /private.*key/.test(normalizedKey) ) { throw new Error(`Mac-control proof contains forbidden field ${key} at ${location}.`); @@ -85,6 +156,31 @@ function assertMacControlProofSanitized(value, location = '$') { } } +function assertExactProofContract(value, contract, location) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`Mac-control proof contract requires an object at ${location}.`); + } + const allowedFields = new Set(contract.fields); + for (const key of Object.keys(value)) { + if (!allowedFields.has(key)) { + throw new Error(`Mac-control proof contains forbidden unexpected field ${key} at ${location}.`); + } + } + for (const key of contract.fields) { + if (!Object.hasOwn(value, key)) { + throw new Error(`Mac-control proof contract is missing field ${key} at ${location}.`); + } + } + for (const [key, entry] of Object.entries(value)) { + const nestedContract = contract.nested?.[key]; + if (nestedContract) { + assertExactProofContract(entry, nestedContract, `${location}.${key}`); + } else if (entry && typeof entry === 'object') { + throw new Error(`Mac-control proof contains forbidden structured field ${key} at ${location}.`); + } + } +} + function scanMacControlProofDirectory(proofDir) { const resolvedProofDir = path.resolve(String(proofDir || '')); let scanned = 0; @@ -93,6 +189,11 @@ function scanMacControlProofDirectory(proofDir) { if (!fs.existsSync(proofPath)) continue; const proof = JSON.parse(fs.readFileSync(proofPath, 'utf8')); assertMacControlProofSanitized(proof, proofName); + const contract = MAC_CONTROL_PROOF_CONTRACTS[proofName]; + if (!contract || proof.schema !== contract.schema) { + throw new Error(`Mac-control proof has an unexpected schema in ${proofName}.`); + } + assertExactProofContract(proof, contract, proofName); scanned += 1; } if (scanned === 0) { diff --git a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts index 22cf4f5d9e..c887687a1d 100644 --- a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts +++ b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts @@ -7,6 +7,7 @@ import { describe, expect, it } from 'vitest'; const WORKFLOW_PATH = '.github/workflows/evaos-live-canary-proof.yml'; const require = createRequire(import.meta.url); const proofScanner = require('../../../scripts/evaosScanMacControlProofs.js') as { + assertMacControlProofSanitized: (proof: unknown) => void; scanMacControlProofDirectory: (proofDir: string) => { ok: boolean; scanned: number }; }; @@ -14,6 +15,30 @@ function readWorkflow(): string { return fs.readFileSync(WORKFLOW_PATH, 'utf8'); } +function validMacControlRuntimeProof(): Record { + return { + ok: true, + schema: 'evaos.mac_control.runtime_proof.v2', + proofKind: 'selected_binding_direct_mac_control', + tool: 'customer_mac.desktop_hotkey', + outcome: 'succeeded', + runRef: 'gha:12345:111111111111111111111111', + executedAt: '2026-07-15T00:00:00.000Z', + bindingRef: 'a'.repeat(64), + bindingVersion: '7', + sessionRef: 'b'.repeat(64), + expiresAt: 1784073660, + auditRef: 'c'.repeat(64), + sourcePointer: 'evaos-desktop-bridge:runtime-receipt', + candidate: { + sourceCommit: 'd'.repeat(40), + sourceSha256: 'e'.repeat(64), + appVersion: '2.1.36', + appBuild: '2.1.36', + }, + }; +} + describe('evaOS live canary proof workflow', () => { it('is a manual staging proof workflow with explicit acknowledgement', () => { const workflow = readWorkflow(); @@ -185,25 +210,37 @@ describe('evaOS live canary proof workflow', () => { { connector_token_value: 'opaque-token-value-123456' }, { providerCredentialsBundle: 'opaque-credential-value-123456' }, { password_hint: 'opaque-password-value-123456' }, + { apiKeyValue: 'opaque-api-key-value-123456' }, + { auth_key_value: 'opaque-auth-key-value-123456' }, + { serviceRoleKeyValue: 'opaque-service-role-key-value-123456' }, + { signingKeyHandle: 'opaque-signing-key-value-123456' }, + { sessionValue: 'opaque-session-value-123456' }, + { bindingIdValue: 'opaque-binding-value-123456' }, + { challengeCopy: 'opaque-challenge-value-123456' }, { receiptPrivateKeyPath: '/safe-looking/path' }, { privateSigningKey: 'opaque-key-value-123456' }, { connectorKeyMaterial: 'opaque-key-material-value-123456' }, { note: '-----BEGIN OPENSSH PRIVATE KEY-----' }, ]) { - fs.writeFileSync(path.join(proofDir, 'mac-control-runtime.json'), `${JSON.stringify(unsafe)}\n`); + fs.writeFileSync( + path.join(proofDir, 'mac-control-runtime.json'), + `${JSON.stringify({ ...validMacControlRuntimeProof(), ...unsafe })}\n` + ); expect(() => proofScanner.scanMacControlProofDirectory(proofDir)).toThrow(/forbidden/i); } fs.writeFileSync( path.join(proofDir, 'mac-control-runtime.json'), - `${JSON.stringify({ - bindingRef: 'a'.repeat(64), - sessionRef: 'b'.repeat(64), - auditRef: 'c'.repeat(64), - receiptKeyId: 'public-receipt-key-id', - })}\n` + `${JSON.stringify(validMacControlRuntimeProof())}\n` ); expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 2 }); + expect(() => + proofScanner.assertMacControlProofSanitized({ + keyId: 'public-key-id', + contextKeyId: 'public-context-key-id', + receiptKeyId: 'public-receipt-key-id', + }) + ).not.toThrow(); } finally { fs.rmSync(proofDir, { recursive: true, force: true }); } From efb53fef9054b72064ec86a61dda86bace496571 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 06:30:44 +0700 Subject: [PATCH 06/17] fix(evaos): require complete Mac control proof packets --- scripts/evaosScanMacControlProofs.js | 10 ++-- .../evaos/evaosLiveCanaryWorkflow.test.ts | 47 ++++++++++++++++++- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/scripts/evaosScanMacControlProofs.js b/scripts/evaosScanMacControlProofs.js index a73602b3df..187c031c7d 100644 --- a/scripts/evaosScanMacControlProofs.js +++ b/scripts/evaosScanMacControlProofs.js @@ -186,7 +186,9 @@ function scanMacControlProofDirectory(proofDir) { let scanned = 0; for (const proofName of MAC_CONTROL_PROOF_NAMES) { const proofPath = path.join(resolvedProofDir, proofName); - if (!fs.existsSync(proofPath)) continue; + if (!fs.existsSync(proofPath)) { + throw new Error(`Mac-control proof is missing required artifact ${proofName}.`); + } const proof = JSON.parse(fs.readFileSync(proofPath, 'utf8')); assertMacControlProofSanitized(proof, proofName); const contract = MAC_CONTROL_PROOF_CONTRACTS[proofName]; @@ -196,13 +198,7 @@ function scanMacControlProofDirectory(proofDir) { assertExactProofContract(proof, contract, proofName); scanned += 1; } - if (scanned === 0) { - throw new Error('Mac-control secret/redaction scan found no proof artifacts.'); - } const cleanupPath = path.join(resolvedProofDir, 'mac-control-session-cleanup.json'); - if (!fs.existsSync(cleanupPath)) { - throw new Error('Mac-control proof is missing sanitized cleanup evidence.'); - } const cleanupProof = JSON.parse(fs.readFileSync(cleanupPath, 'utf8')); if ( cleanupProof.schema !== 'evaos-mac-control-canary-session-cleanup/v1' || diff --git a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts index c887687a1d..70e8f75eb6 100644 --- a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts +++ b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts @@ -39,6 +39,46 @@ function validMacControlRuntimeProof(): Record { }; } +function writeCompleteMacControlProofSet(proofDir: string): void { + const provision = { + schema: 'evaos-mac-control-canary-session-provision/v1', + accountConfigured: true, + customerConfigured: true, + activeMembershipVerified: true, + stagingMarkerVerified: true, + sessionMinted: true, + sessionExpiryPresent: true, + sensitiveOutput: 'passed', + }; + const cleanup = { + schema: 'evaos-mac-control-canary-session-cleanup/v1', + sessionRevoked: true, + sensitiveOutput: 'passed', + }; + const negative = { + schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', + sourceHeadSha: 'd'.repeat(40), + sourceRunId: '12345', + assertions: { + forgedContextRejected: true, + expiredContextRejected: true, + replayRejected: true, + authorityRedacted: true, + }, + }; + const proofs = { + 'mac-control-runtime.json': validMacControlRuntimeProof(), + 'mac-control-runtime-negative.json': negative, + 'mac-control-session-provisioning.json': provision, + 'mac-control-session-provisioning.stdout.json': provision, + 'mac-control-session-cleanup.json': cleanup, + 'mac-control-session-cleanup.stdout.json': cleanup, + }; + for (const [name, proof] of Object.entries(proofs)) { + fs.writeFileSync(path.join(proofDir, name), `${JSON.stringify(proof)}\n`); + } +} + describe('evaOS live canary proof workflow', () => { it('is a manual staging proof workflow with explicit acknowledgement', () => { const workflow = readWorkflow(); @@ -196,7 +236,10 @@ describe('evaOS live canary proof workflow', () => { sensitiveOutput: 'passed', })}\n` ); - expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 1 }); + expect(() => proofScanner.scanMacControlProofDirectory(proofDir)).toThrow(/missing required artifact/i); + + writeCompleteMacControlProofSet(proofDir); + expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 6 }); for (const unsafe of [ { Cookie: 'opaque-cookie-value-123456' }, @@ -233,7 +276,7 @@ describe('evaOS live canary proof workflow', () => { path.join(proofDir, 'mac-control-runtime.json'), `${JSON.stringify(validMacControlRuntimeProof())}\n` ); - expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 2 }); + expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 6 }); expect(() => proofScanner.assertMacControlProofSanitized({ keyId: 'public-key-id', From 029476730d855e8e0826a5d7b3e6fdbcbe1e61a1 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 07:19:26 +0700 Subject: [PATCH 07/17] feat: sign Mac-control release attestations --- .github/workflows/evaos-live-canary-proof.yml | 7 +- .github/workflows/pr-checks.yml | 33 + .github/workflows/release-distribute.yml | 3 + .gitignore | 1 + CHANGELOG.md | 15 + .../evaos-beta/bridge/agent-tools/SOURCE.json | 33 +- .../agent-tools/openclaw-plugin/README.md | 12 +- .../dist/src/runtimeReceipt.js | 156 +- .../openclaw-plugin/package-lock.json | 4345 +++++++++++++++++ .../runtime-receipt-negative-proof.mjs | 20 +- .../openclaw-plugin/src/runtimeReceipt.ts | 201 +- .../tests/runtimeReceipt.test.mjs | 140 +- .../evaos_desktop_bridge/connector_server.py | 19 +- .../src/evaos_desktop_bridge/qa_canary.py | 1393 +++++- .../evaos_desktop_bridge/receipt_canary.py | 104 +- scripts/evaosBetaReleaseGate.js | 152 +- scripts/evaosBrokerLiveCanary.js | 297 +- scripts/evaosLiveCanaryEnvInventory.js | 29 + scripts/evaosMacControlSignedProof.js | 246 + scripts/evaosScanMacControlProofs.js | 114 +- scripts/evaosValidateLiveCanaryProofRun.sh | 9 + .../unit/evaos/evaosBrokerLiveCanary.test.ts | 196 +- .../evaos/evaosDesktopBridgeReceipt.test.ts | 39 +- .../evaos/evaosLiveCanaryEnvInventory.test.ts | 11 +- .../evaos/evaosLiveCanaryWorkflow.test.ts | 47 +- .../fixtures/signedMacControlAttestation.ts | 126 + .../unit/process/evaosBetaReleaseGate.test.ts | 264 +- .../prepareEvaosDesktopBridgeResource.test.ts | 21 +- 28 files changed, 7389 insertions(+), 644 deletions(-) create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package-lock.json create mode 100644 scripts/evaosMacControlSignedProof.js create mode 100644 tests/unit/evaos/fixtures/signedMacControlAttestation.ts diff --git a/.github/workflows/evaos-live-canary-proof.yml b/.github/workflows/evaos-live-canary-proof.yml index b2d9da4636..d62991e31c 100644 --- a/.github/workflows/evaos-live-canary-proof.yml +++ b/.github/workflows/evaos-live-canary-proof.yml @@ -108,6 +108,10 @@ jobs: AIONUI_EVAOS_MAC_CONTROL_CANARY_SUPABASE_URL: ${{ secrets.AIONUI_EVAOS_MAC_CONTROL_CANARY_SUPABASE_URL }} AIONUI_EVAOS_MAC_CONTROL_CANARY_SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.AIONUI_EVAOS_MAC_CONTROL_CANARY_SUPABASE_SERVICE_ROLE_KEY }} AIONUI_EVAOS_MAC_CONTROL_CANARY_TTL_MINUTES: '10' + AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_PROBE_OUTPUT: live-canary-proof/mac-control-deployed-route.json + EVAOS_LIVE_CANARY_CONTEXT_KEY_ID: ${{ vars.EVAOS_MAC_CONTROL_CONTEXT_KEY_ID }} + EVAOS_LIVE_CANARY_RECEIPT_KEY_ID: ${{ vars.EVAOS_MAC_CONTROL_RECEIPT_KEY_ID }} + EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY: ${{ vars.EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY }} steps: - name: Validate proof inputs env: @@ -231,7 +235,8 @@ jobs: if: github.event.inputs.run_live_canaries == 'true' && github.event.inputs.run_mac_control_canary == 'true' run: | set -euo pipefail - npm --prefix resources/evaos-beta/bridge/agent-tools/openclaw-plugin install --ignore-scripts --omit=peer --no-audit --no-fund + test -f resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package-lock.json + npm --prefix resources/evaos-beta/bridge/agent-tools/openclaw-plugin ci --ignore-scripts --omit=peer --no-audit --no-fund npm --prefix resources/evaos-beta/bridge/agent-tools/openclaw-plugin run proof:runtime-receipt-negative -- \ "$PROOF_DIR/mac-control-runtime-negative.json" "$GITHUB_SHA" "$GITHUB_RUN_ID" diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 3cfc8f0071..653ec99f79 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -183,6 +183,39 @@ jobs: - name: Run extension system tests run: bunx vitest run + openclaw-plugin-contract: + name: OpenClaw Plugin Contract + if: github.event_name == 'workflow_dispatch' || (github.event.action != 'closed' && github.event.pull_request.draft == false) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Resolve PR context + uses: ./.github/actions/checkout-pr + with: + pr_number: ${{ inputs.pr_number }} + github_token: ${{ github.token }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package-lock.json + + - name: Install locked plugin dependencies + working-directory: resources/evaos-beta/bridge/agent-tools/openclaw-plugin + run: npm ci --ignore-scripts --omit=peer --no-audit --no-fund + + - name: Run complete plugin contract suite + working-directory: resources/evaos-beta/bridge/agent-tools/openclaw-plugin + run: npm test + + - name: Reject generated plugin drift + run: git diff --exit-code -- resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist + windows-unit-tests: name: Unit Tests (windows-2022) needs: pr-check-plan diff --git a/.github/workflows/release-distribute.yml b/.github/workflows/release-distribute.yml index 1b3f0e1672..d68c6dd4b0 100644 --- a/.github/workflows/release-distribute.yml +++ b/.github/workflows/release-distribute.yml @@ -329,6 +329,9 @@ jobs: EVAOS_LIVE_CANARY_MAX_PROOF_AGE_HOURS: '24' EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: ${{ steps.provenance.outputs.tag_commit }} EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: ${{ github.event.inputs.live_canary_proof_run_id }} + EVAOS_LIVE_CANARY_CONTEXT_KEY_ID: ${{ vars.EVAOS_MAC_CONTROL_CONTEXT_KEY_ID }} + EVAOS_LIVE_CANARY_RECEIPT_KEY_ID: ${{ vars.EVAOS_MAC_CONTROL_RECEIPT_KEY_ID }} + EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY: ${{ vars.EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY }} run: | /bin/bash scripts/evaosValidateLiveCanaryProofRun.sh diff --git a/.gitignore b/.gitignore index 62e3c99532..0e9a6ce214 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ jspm_packages/ # Lock files for other package managers (project uses bun.lock) package-lock.json +!resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package-lock.json yarn.lock # TypeScript v1 declaration files diff --git a/CHANGELOG.md b/CHANGELOG.md index e6c39e08c8..a8d28b7f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,12 @@ bounded deadline so a healthy Mac is not misreported as unauthenticated. - Binds the packaged bridge source digest and ownership provenance to the exact evaOS-GUI release commit before signing. +- Treats Tailscale `NeedsLogin` as an installed, running client that still + needs enrollment, immediately shows progress for the single allowed + `Connect this Mac` action, keeps the renderer IPC alive for the bounded + enrollment window, and surfaces a strictly sanitized rejection instead of + leaving the button apparently inert. Duplicate enrollment clicks remain + disabled while the first action is in flight. ### Selected-Binding Mac-control Canary @@ -47,6 +53,15 @@ compensates failed persistence by revoking the exact temporary session. - Requires matching normalized capability sets, a secure host-only browser session cookie, and a still-live selected binding after callback completion. +- Cross-binds the connector's private SSHSIG receipt to a separately signed, + minimal public runtime attestation. OpenClaw verifies both signatures and + returns the public envelope unchanged; the live canary and distribution gate + independently verify it against protected external key configuration and + reject legacy unsigned, forged, replayed, stale, wrong-run, or wrong-head + evidence. +- Adds deployed-route behavior probes for gateway authentication, POST-only + routing, exact path matching, strict body validation, and rejection of + caller-supplied authority before Mac-control proof can authorize publication. ### Release Authorization And Bundle Integrity diff --git a/resources/evaos-beta/bridge/agent-tools/SOURCE.json b/resources/evaos-beta/bridge/agent-tools/SOURCE.json index d4436bf22b..9631ac6663 100644 --- a/resources/evaos-beta/bridge/agent-tools/SOURCE.json +++ b/resources/evaos-beta/bridge/agent-tools/SOURCE.json @@ -8,19 +8,24 @@ "importedCommit": "b3e8da7ae90c86b7a999b6a0ef5d7e4bcb83b2c8", "importedAt": "2026-07-15", "sourceDigests": { - "hermes-adapter/README.md": "sha256:0077be1d91ac82fad35c6c916cba3f780088f8a4b3d80bd69dea93819bb9003a", - "hermes-adapter/bin/evaos-desktop-bridge-command": "sha256:d3d1870e9625ecf69787d6a536054340bd0c7aec862ff34a0be4ec86bc56ada3", - "openclaw-plugin/README.md": "sha256:18ba84670be254004a093f613edc783ac9fd2523174968b1f853a3abb59a2156", - "openclaw-plugin/dist/index.js": "sha256:ffdacb32f8e5e378df1352ac8b07ea9a152a3e4b25200428512b8e667de91adc", - "openclaw-plugin/dist/src/bridge.js": "sha256:d3bb9ff47b6cbd01b548baac7fd211051a595ef522abe913c22ec44e167dad7e", - "openclaw-plugin/dist/src/firewall.js": "sha256:60c2c0f5e5319cb77ef6d4c4b4d4b30b46f4dddafb7390230ddd3fbfe03dd993", - "openclaw-plugin/index.ts": "sha256:5ea45783093fe60b30c2bb8cf93343a069751004789a9dcdd202be21ea88e26d", - "openclaw-plugin/openclaw.plugin.json": "sha256:4e5c50b2bb31a31fc9756171679a7d2139e88503b304734eeae9093bd25095e1", - "openclaw-plugin/package.json": "sha256:c27ab5217832722473709611ddc8d6308a22a62de7a24e66b5952a4f8fde5c41", - "openclaw-plugin/scripts/qa-run-bridge.mjs": "sha256:db40c073164440a60f068153f5a21d5d39b04e479ad7bc2bbcfdea2468dd54da", - "openclaw-plugin/src/bridge.ts": "sha256:2695a25bc6710c5ac5f95682e5ee2c1849a708b3ce00d9a44c924efd75a088e2", - "openclaw-plugin/src/firewall.ts": "sha256:2b21f94e43184c9d9507d5bef91f057949e30c266b81fe4d3005a14f9f966a5c", - "openclaw-plugin/src/types.d.ts": "sha256:6645c7b2f7808c88ee84fb95163eefcdec1c69cf678737f217bca6e1c973cb9e", - "openclaw-plugin/tsconfig.json": "sha256:2d137f90e1b16119cb1837e7cc61122c1c298b7e77f8f20a497e98d03bcba23a" + "hermes-adapter/README.md": "sha256:28517b43d41370c8481f92936a1a4156ce5827110122876e9b09373e7c94e9f4", + "hermes-adapter/bin/evaos-desktop-bridge-command": "sha256:71b6dde4fe392a2a4ce2854aae8935dd905701ea07da244afbb37cc05be44232", + "openclaw-plugin/README.md": "sha256:ce371b7ed539092b7cad9245883f8fb5e91c0c8f56ff094164a0635716128737", + "openclaw-plugin/dist/index.js": "sha256:5634b0a23dfc08ba8640343cd4b3bf9a2fa77e496884924d39069fc97138ab91", + "openclaw-plugin/dist/src/bridge.js": "sha256:a057903f228906b5206f7dd9cf2b43c9fb721cae8c1fa3822c7eb84f7434a66f", + "openclaw-plugin/dist/src/firewall.js": "sha256:939cdd1070bf02aade901d899a67e81e52ef2c6f554171f2039ba8419ffc630a", + "openclaw-plugin/dist/src/runtimeReceipt.js": "sha256:23cfb4db488e255ba5384c2f42429f12ad077dcd004a70f2e941984f3f3dd8b0", + "openclaw-plugin/index.ts": "sha256:a3d54d4ef87fca3435ae0c3ff86f3947950131b6d29c8cfa66944c4dd034bc12", + "openclaw-plugin/openclaw.plugin.json": "sha256:b145a479aad1209f528a2d1c69e2cf99ec8d2906957f668e3e124113a7276e79", + "openclaw-plugin/package-lock.json": "sha256:e0f8d437d5a51f6fcb16f173550b5774022a0201944e5fb1eb5fc0158e7f7d52", + "openclaw-plugin/package.json": "sha256:78ac262042b80c509a11419a0a60ce48db24054dad043f4de37a3ab7540b02ff", + "openclaw-plugin/scripts/qa-run-bridge.mjs": "sha256:648b24a86431254c25907ecf6906f1b0070b8701165fa1cb92dc9d9068e67dd2", + "openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs": "sha256:6a91ee0fc8ba7269ee5ff8ca3a79d7bfadb9139ed1eb629642c9c2a688130062", + "openclaw-plugin/src/bridge.ts": "sha256:b03486bb226e3a2aca65a70074d82822bf510291295f858ad44dcd5e26a8c896", + "openclaw-plugin/src/firewall.ts": "sha256:85e638930cd5a412fc059e4cd2e2c8db41d29102319fe047bfc88601c7911668", + "openclaw-plugin/src/runtimeReceipt.ts": "sha256:e88ba64b2113d0f63897ff8c599d5dc9dafe28a631c24a454039395967a2ecf3", + "openclaw-plugin/src/types.d.ts": "sha256:6144e747e97c133ddaefe37af986865d6b87d842b922026a2af14b010c946bfc", + "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:455b5e1a4cec736a718a8ee58b27cefacab15d923433d20f20a41aed3ea2d6a2", + "openclaw-plugin/tsconfig.json": "sha256:d5ceeadf205f97973f4d2a1e13ef20f9af854c1fbc01823af60b02eed5d5b34b" } } diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md index f7812106ed..659e139c1f 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/README.md @@ -163,10 +163,14 @@ pinned `EVAOS_MAC_CONTROL_CONTEXT_KEY_ID` and unpadded base64url raw 32-byte `EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY`. Before the connector can be called, the route also requires a pinned `EVAOS_MAC_CONTROL_RECEIPT_KEY_ID`, raw 32-byte `EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY`, and exact expected source commit, -source digest, app version, and build. It verifies the connector SSHSIG and -every selected-scope, action, state, audit, expiry, and candidate claim, then -returns only the allowlisted `evaos.mac_control.runtime_proof.v2` view. The -route exposes no generic tool, proxy, action, raw receipt, or signing API. +source digest, app version, and build. It verifies the private connector SSHSIG +and every selected-scope, action, state, audit, expiry, and candidate claim, +cross-checks the separately signed minimal public attestation against that +private receipt, then returns the unchanged +`evaos.mac_control.public_runtime_attestation_envelope.v1` envelope. Release +gates verify that public SSHSIG again against an externally configured key; the +artifact never supplies its own trust key. The route exposes no generic tool, +proxy, action, raw private receipt, or signing API. Local plugin tools call fixed bridge argv mappings with `shell: false`. Remote plugin tools post fixed command keys to `/v1/commands` on the paired Mac diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js index 3eadf0eae5..453185979a 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js @@ -3,12 +3,14 @@ export const MAC_CONTROL_RUNTIME_RECEIPT_PATH = '/api/v1/evaos/mac-control/runti const CONTEXT_SCHEMA = 'evaos.mac_control_execution_context.v1'; const CONTRACT_SCHEMA = 'evaos.mac_control_runtime_contract.v2'; const CONNECTOR_REQUEST_SCHEMA = 'evaos.mac_control.canary_request.v1'; -const CONNECTOR_RESPONSE_SCHEMA = 'evaos.mac_control.runtime_receipt_envelope.v1'; +const CONNECTOR_RESPONSE_SCHEMA = 'evaos.mac_control.runtime_receipt_bundle.v2'; +const PRIVATE_RECEIPT_ENVELOPE_SCHEMA = 'evaos.mac_control.runtime_receipt_envelope.v1'; const RECEIPT_SCHEMA = 'evaos.mac_control.runtime_receipt.v1'; const RECEIPT_NAMESPACE = 'evaos-mac-control-receipt-v1'; -const PUBLIC_PROOF_SCHEMA = 'evaos.mac_control.runtime_proof.v2'; +const PUBLIC_ATTESTATION_SCHEMA = 'evaos.mac_control.public_runtime_attestation.v1'; +const PUBLIC_ATTESTATION_ENVELOPE_SCHEMA = 'evaos.mac_control.public_runtime_attestation_envelope.v1'; +const PUBLIC_ATTESTATION_NAMESPACE = 'evaos-mac-control-public-attestation-v1'; const PUBLIC_PROOF_KIND = 'selected_binding_direct_mac_control'; -const PUBLIC_PROOF_SOURCE = 'evaos-desktop-bridge:runtime-receipt'; const CONTEXT_TTL_SECONDS = 60; const CLOCK_SKEW_SECONDS = 5; const DEFAULT_CONNECTOR_TIMEOUT_MS = 10_000; @@ -27,7 +29,9 @@ const CONTEXT_FIELDS = [ 'expires_at', 'context_id', ]; -const RESPONSE_FIELDS = ['schema', 'receiptBase64', 'signature', 'keyId', 'namespace']; +const RESPONSE_FIELDS = ['schema', 'privateReceipt', 'publicAttestation']; +const PRIVATE_RECEIPT_ENVELOPE_FIELDS = ['schema', 'receiptBase64', 'signature', 'keyId', 'namespace']; +const PUBLIC_ATTESTATION_ENVELOPE_FIELDS = ['schema', 'attestationBase64', 'signature', 'keyId', 'namespace']; const RECEIPT_FIELDS = [ 'schema', 'keyId', @@ -87,6 +91,25 @@ const OWNER_FIELDS = [ const PATH_FIELDS = ['kind', 'value']; const ACTION_FIELDS = ['command', 'args']; const ACTION_ARGS_FIELDS = ['keys', 'dryRun']; +const PUBLIC_ATTESTATION_FIELDS = [ + 'schema', + 'keyId', + 'namespace', + 'proofKind', + 'runtime', + 'tool', + 'outcome', + 'runRef', + 'executedAt', + 'authorityIssuedAt', + 'authorityExpiresAt', + 'contextKeyId', + 'controlState', + 'auditRecorded', + 'privateReceiptSha256', + 'connectorCandidate', +]; +const PUBLIC_CANDIDATE_FIELDS = ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild']; const HEADER = { context: 'x-evaos-mac-control-execution-context', contextSignature: 'x-evaos-mac-control-execution-context-signature', @@ -424,21 +447,27 @@ function validateConnectorEnvelope(raw, authority, request, verifier) { if (!isRecord(parsed) || !hasExactKeys(parsed, RESPONSE_FIELDS)) { return { ok: false }; } + const privateReceipt = parsed.privateReceipt; + const publicAttestation = parsed.publicAttestation; if ( parsed.schema !== CONNECTOR_RESPONSE_SCHEMA || - parsed.namespace !== RECEIPT_NAMESPACE || - typeof parsed.receiptBase64 !== 'string' || - parsed.receiptBase64.length < 32 || - parsed.receiptBase64.length > 32768 || - typeof parsed.signature !== 'string' || - parsed.signature.length > 8192 || - typeof parsed.keyId !== 'string' || - parsed.keyId !== verifier.keyId + !isRecord(privateReceipt) || + !hasExactKeys(privateReceipt, PRIVATE_RECEIPT_ENVELOPE_FIELDS) || + !isRecord(publicAttestation) || + !hasExactKeys(publicAttestation, PUBLIC_ATTESTATION_ENVELOPE_FIELDS) || + privateReceipt.schema !== PRIVATE_RECEIPT_ENVELOPE_SCHEMA || + privateReceipt.namespace !== RECEIPT_NAMESPACE || + typeof privateReceipt.receiptBase64 !== 'string' || + privateReceipt.receiptBase64.length < 32 || + privateReceipt.receiptBase64.length > 32768 || + typeof privateReceipt.signature !== 'string' || + privateReceipt.signature.length > 8192 || + privateReceipt.keyId !== verifier.keyId ) { return { ok: false }; } - const receiptBytes = decodeBase64Url(parsed.receiptBase64); - if (!receiptBytes || receiptBytes.byteLength > 24576 || !validSshSignature(parsed.signature)) { + const receiptBytes = decodeBase64Url(privateReceipt.receiptBase64); + if (!receiptBytes || receiptBytes.byteLength > 24576 || !validSshSignature(privateReceipt.signature)) { return { ok: false }; } const receiptText = receiptBytes.toString('utf8'); @@ -460,10 +489,14 @@ function validateConnectorEnvelope(raw, authority, request, verifier) { ) { return { ok: false }; } - if (!verifySshSignature(receiptBytes, parsed.signature, verifier.publicKey)) { + if (!verifySshSignature(receiptBytes, privateReceipt.signature, verifier.publicKey, RECEIPT_NAMESPACE)) { + return { ok: false }; + } + const expectedAttestation = validateReceipt(receiptBytes, authority, request, verifier); + if (!expectedAttestation.ok) { return { ok: false }; } - return validateReceipt(receiptBytes, authority, request, verifier); + return validatePublicAttestationEnvelope(publicAttestation, expectedAttestation.value, authority, verifier); } function validateReceipt(receiptBytes, authority, request, verifier) { let parsed; @@ -540,20 +573,22 @@ function validateReceipt(receiptBytes, authority, request, verifier) { return { ok: true, value: { - ok: true, - schema: PUBLIC_PROOF_SCHEMA, + schema: PUBLIC_ATTESTATION_SCHEMA, + keyId: verifier.keyId, + namespace: PUBLIC_ATTESTATION_NAMESPACE, proofKind: PUBLIC_PROOF_KIND, + runtime: 'openclaw', tool: 'customer_mac.desktop_hotkey', outcome: 'succeeded', runRef: request.runRef, executedAt: parsed.executedAt, - bindingRef: parsed.bindingRef, - bindingVersion: authority.context.binding_version, - sessionRef: parsed.sessionRef, - expiresAt: authority.context.expires_at, - auditRef: saltedHash(request.challenge, parsed.auditId), - sourcePointer: PUBLIC_PROOF_SOURCE, - candidate: { + authorityIssuedAt: authority.context.issued_at, + authorityExpiresAt: authority.context.expires_at, + contextKeyId: authority.contextKeyId, + controlState: 'ready_unchanged', + auditRecorded: true, + privateReceiptSha256: sha256Hex(receiptBytes), + connectorCandidate: { sourceCommit: typedCandidate.sourceCommit, sourceSha256: typedCandidate.sourceSha256, appVersion: typedCandidate.appVersion, @@ -562,6 +597,73 @@ function validateReceipt(receiptBytes, authority, request, verifier) { }, }; } +function validatePublicAttestationEnvelope(value, expected, authority, verifier) { + if ( + value.schema !== PUBLIC_ATTESTATION_ENVELOPE_SCHEMA || + value.namespace !== PUBLIC_ATTESTATION_NAMESPACE || + value.keyId !== verifier.keyId || + typeof value.attestationBase64 !== 'string' || + value.attestationBase64.length < 32 || + value.attestationBase64.length > 24576 || + typeof value.signature !== 'string' || + value.signature.length > 8192 || + !validSshSignature(value.signature) + ) { + return { ok: false }; + } + const attestationBytes = decodeBase64Url(value.attestationBase64); + if (!attestationBytes || attestationBytes.byteLength > 16384) { + return { ok: false }; + } + const attestationText = attestationBytes.toString('utf8'); + const forbiddenValues = [ + authority.connectorToken, + authority.connectorUrl, + authority.context.customer_id, + authority.context.customer_vm_id, + authority.context.binding_id, + authority.contextPayload, + authority.contextSignature, + ]; + if ( + forbiddenValues.some((forbidden) => forbidden.length > 0 && attestationText.includes(forbidden)) || + /https?:\/\//i.test(attestationText) || + /\b100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])(?:\.\d{1,3}){2}\b/.test(attestationText) || + /(?:\/tmp\/|\/private\/var\/folders\/)/i.test(attestationText) || + /"(?:challenge|token|secret|authorization|connector_url|customer_id|customer_vm_id|binding_id|bindingRef|sessionRef|auditRef)"\s*:/i.test( + attestationText + ) + ) { + return { ok: false }; + } + let attestation; + try { + attestation = JSON.parse(attestationText); + } catch { + return { ok: false }; + } + if ( + !isRecord(attestation) || + !hasExactKeys(attestation, PUBLIC_ATTESTATION_FIELDS) || + !isRecord(attestation.connectorCandidate) || + !hasExactKeys(attestation.connectorCandidate, PUBLIC_CANDIDATE_FIELDS) || + canonicalJson(attestation) !== attestationText || + canonicalJson(attestation) !== canonicalJson(expected) || + !verifySshSignature(attestationBytes, value.signature, verifier.publicKey, PUBLIC_ATTESTATION_NAMESPACE) + ) { + return { ok: false }; + } + return { + ok: true, + value: { + schema: PUBLIC_ATTESTATION_ENVELOPE_SCHEMA, + attestationBase64: value.attestationBase64, + signature: value.signature, + keyId: verifier.keyId, + namespace: PUBLIC_ATTESTATION_NAMESPACE, + }, + }; +} function validControlState(value) { return ( hasExactKeys(value, CONTROL_STATE_FIELDS) && @@ -629,7 +731,7 @@ function validReceiptAction(value) { value.args.dryRun === false ); } -function verifySshSignature(message, armor, pinnedPublicKey) { +function verifySshSignature(message, armor, pinnedPublicKey, expectedNamespace) { const decoded = decodeSshSignatureArmor(armor); if (!decoded) { return false; @@ -644,7 +746,7 @@ function verifySshSignature(message, armor, pinnedPublicKey) { const reserved = reader.readString(); const hashAlgorithm = reader.readString().toString('ascii'); const signatureBlob = reader.readString(); - if (!reader.done() || namespace !== RECEIPT_NAMESPACE || reserved.byteLength !== 0) { + if (!reader.done() || namespace !== expectedNamespace || reserved.byteLength !== 0) { return false; } const publicKeyReader = sshReader(publicKeyBlob); diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package-lock.json b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package-lock.json new file mode 100644 index 0000000000..aa1487cac1 --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package-lock.json @@ -0,0 +1,4345 @@ +{ + "name": "@100yenadmin/evaos-desktop-bridge-openclaw", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@100yenadmin/evaos-desktop-bridge-openclaw", + "version": "0.2.0", + "devDependencies": { + "oxfmt": "0.41.0", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "openclaw": ">=0.0.0" + } + }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.41.0.tgz", + "integrity": "sha512-REfrqeMKGkfMP+m/ScX4f5jJBSmVNYcpoDF8vP8f8eYPDuPGZmzp56NIUsYmx3h7f6NzC6cE3gqh8GDWrJHCKw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.41.0.tgz", + "integrity": "sha512-s0b1dxNgb2KomspFV2LfogC2XtSJB42POXF4bMCLJyvQmAGos4ZtjGPfQreToQEaY0FQFjz3030ggI36rF1q5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.41.0.tgz", + "integrity": "sha512-EGXGualADbv/ZmamE7/2DbsrYmjoPlAmHEpTL4vapLF4EfVD6fr8/uQDFnPJkUBjiSWFJZtFNsGeN1B6V3owmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.41.0.tgz", + "integrity": "sha512-WxySJEvdQQYMmyvISH3qDpTvoS0ebnIP63IMxLLWowJyPp/AAH0hdWtlo+iGNK5y3eVfa5jZguwNaQkDKWpGSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.41.0.tgz", + "integrity": "sha512-Y2kzMkv3U3oyuYaR4wTfGjOTYTXiFC/hXmG0yVASKkbh02BJkvD98Ij8bIevr45hNZ0DmZEgqiXF+9buD4yMYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.41.0.tgz", + "integrity": "sha512-ptazDjdUyhket01IjPTT6ULS1KFuBfTUU97osTP96X5y/0oso+AgAaJzuH81oP0+XXyrWIHbRzozSAuQm4p48g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.41.0.tgz", + "integrity": "sha512-UkoL2OKxFD+56bPEBcdGn+4juTW4HRv/T6w1dIDLnvKKWr6DbarB/mtHXlADKlFiJubJz8pRkttOR7qjYR6lTA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.41.0.tgz", + "integrity": "sha512-gofu0PuumSOHYczD8p62CPY4UF6ee+rSLZJdUXkpwxg6pILiwSDBIouPskjF/5nF3A7QZTz2O9KFNkNxxFN9tA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.41.0.tgz", + "integrity": "sha512-VfVZxL0+6RU86T8F8vKiDBa+iHsr8PAjQmKGBzSCAX70b6x+UOMFl+2dNihmKmUwqkCazCPfYjt6SuAPOeQJ3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.41.0.tgz", + "integrity": "sha512-bwzokz2eGvdfJbc0i+zXMJ4BBjQPqg13jyWpEEZDOrBCQ91r8KeY2Mi2kUeuMTZNFXju+jcAbAbpyJxRGla0eg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.41.0.tgz", + "integrity": "sha512-POLM//PCH9uqDeNDwWL3b3DkMmI3oI2cU6hwc2lnztD1o7dzrQs3R9nq555BZ6wI7t2lyhT9CS+CRaz5X0XqLA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.41.0.tgz", + "integrity": "sha512-NNK7PzhFqLUwx/G12Xtm6scGv7UITvyGdAR5Y+TlqsG+essnuRWR4jRNODWRjzLZod0T3SayRbnkSIWMBov33w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.41.0.tgz", + "integrity": "sha512-qVf/zDC5cN9eKe4qI/O/m445er1IRl6swsSl7jHkqmOSVfknwCe5JXitYjZca+V/cNJSU/xPlC5EFMabMMFDpw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.41.0.tgz", + "integrity": "sha512-ojxYWu7vUb6ysYqVCPHuAPVZHAI40gfZ0PDtZAMwVmh2f0V8ExpPIKoAKr7/8sNbAXJBBpZhs2coypIo2jJX4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.41.0.tgz", + "integrity": "sha512-O2exZLBxoCMIv2vlvcbkdedazJPTdG0VSup+0QUCfYQtx751zCZNboX2ZUOiQ/gDTdhtXvSiot0h6GEGkOyalA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.41.0.tgz", + "integrity": "sha512-N+31/VoL+z+NNBt8viy3I4NaIdPbiYeOnB884LKqvXldaE2dRztdPv3q5ipfZYv0RwFp7JfqS4I27K/DSHCakg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.41.0.tgz", + "integrity": "sha512-Z7NAtu/RN8kjCQ1y5oDD0nTAeRswh3GJ93qwcW51srmidP7XPBmZbLlwERu1W5veCevQJtPS9xmkpcDTYsGIwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.41.0.tgz", + "integrity": "sha512-uNxxP3l4bJ6VyzIeRqCmBU2Q0SkCFgIhvx9/9dJ9V8t/v+jP1IBsuaLwCXGR8JPHtkj4tFp+RHtUmU2ZYAUpMA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.41.0.tgz", + "integrity": "sha512-49ZSpbZ1noozyPapE8SUOSm3IN0Ze4b5nkO+4+7fq6oEYQQJFhE0saj5k/Gg4oewVPdjn0L3ZFeWk2Vehjcw7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/openclaw": { + "version": "2026.7.1", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.7.1.tgz", + "integrity": "sha512-ge/Xss99CHAjPL/ikmH/UFoiOrjcxDB4sW3y9mhyCD+dYW3wzV7TKbAVdkrXFgAG2d2BjpJofP97zUZ+umxo8g==", + "hasInstallScript": true, + "hasShrinkwrap": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@agentclientprotocol/sdk": "1.1.0", + "@anthropic-ai/sdk": "0.109.1", + "@clack/core": "1.4.2", + "@clack/prompts": "1.6.0", + "@earendil-works/pi-tui": "0.80.3", + "@google/genai": "2.10.0", + "@grammyjs/runner": "2.0.3", + "@grammyjs/transformer-throttler": "1.2.1", + "@homebridge/ciao": "1.3.9", + "@lydell/node-pty": "1.2.0-beta.12", + "@mistralai/mistralai": "2.4.0", + "@modelcontextprotocol/sdk": "1.29.0", + "@mozilla/readability": "0.6.0", + "@openclaw/ai": "2026.7.1", + "@openclaw/fs-safe": "0.4.1", + "@openclaw/proxyline": "0.3.3", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "chokidar": "5.0.0", + "clawpdf": "0.3.0", + "commander": "15.0.0", + "croner": "10.0.1", + "diff": "9.0.0", + "dotenv": "17.4.2", + "express": "5.2.1", + "file-type": "22.0.1", + "glob": "13.0.6", + "grammy": "1.44.0", + "highlight.js": "11.11.1", + "hosted-git-info": "10.1.1", + "ignore": "7.0.5", + "jiti": "2.7.0", + "json5": "2.2.3", + "jszip": "3.10.1", + "kysely": "0.29.2", + "linkedom": "0.18.12", + "minimatch": "10.2.5", + "node-edge-tts": "1.2.10", + "openai": "6.45.0", + "partial-json": "0.1.7", + "playwright-core": "1.61.1", + "proper-lockfile": "4.1.2", + "qrcode": "1.5.4", + "quickjs-wasi": "3.0.2", + "rastermill": "0.3.1", + "tar": "7.5.19", + "tree-sitter-bash": "0.25.1", + "tslog": "4.10.2", + "typebox": "1.3.3", + "typescript": "6.0.3", + "undici": "8.5.0", + "web-push": "3.6.7", + "web-tree-sitter": "0.26.10", + "ws": "8.21.0", + "yaml": "2.9.0", + "zod": "4.4.3" + }, + "bin": { + "openclaw": "openclaw.mjs" + }, + "engines": { + "node": ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0" + }, + "optionalDependencies": { + "sqlite-vec": "0.1.9" + } + }, + "node_modules/openclaw/node_modules/@agentclientprotocol/sdk": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.1.0.tgz", + "integrity": "sha512-NT2KqphUJ3w6EksUL51ZhJgIYgq/ZLGcBPkyMKgRSO5PMVwe9DnKKX+Htnvk6KHh6dUuh34UHK4gKp+4te1Mdg==", + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/openclaw/node_modules/@anthropic-ai/sdk": { + "version": "0.109.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.109.1.tgz", + "integrity": "sha512-q9OnEKLr5H9nxSuXdgDgJhxfYMiE+AaUEBze2Gk91UcaaLnsN+Lx5fbCYywiqurU/APLdwv23x03Wm6WN3EBsg==", + "license": "MIT", + "peer": true, + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/openclaw/node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/openclaw/node_modules/@clack/core": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.2.tgz", + "integrity": "sha512-0Ty/1Gfm+Kb07sXcuESjyKfwEhSy4Ns1AgeEisHb/bDY5fWme0tTeTkU14T1Gmcs17YIjB/teiDe4uaCghbYqQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/openclaw/node_modules/@clack/prompts": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.6.0.tgz", + "integrity": "sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@clack/core": "1.4.2", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/openclaw/node_modules/@earendil-works/pi-tui": { + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.3.tgz", + "integrity": "sha512-2BJI6qwRQfnM0Q7seL1+SbacU/jRRjBnN7Hu3n9BjAn7/s5FaBNnvdD1qBQYRsFTHfjqMaDsjYqanPyqwXj99w==", + "license": "MIT", + "peer": true, + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/openclaw/node_modules/@google/genai": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.10.0.tgz", + "integrity": "sha512-e4cFxj3tiuMtsgOT4G9c1hXyGJhg7/Buj7VVeBacRY3fRtkRZZ59Q3nuVp2xbq8BGQXLXCDB253qMhklMOeUDg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/@grammyjs/runner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@grammyjs/runner/-/runner-2.0.3.tgz", + "integrity": "sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==", + "license": "MIT", + "peer": true, + "dependencies": { + "abort-controller": "^3.0.0" + }, + "engines": { + "node": ">=12.20.0 || >=14.13.1" + }, + "peerDependencies": { + "grammy": "^1.13.1" + } + }, + "node_modules/openclaw/node_modules/@grammyjs/transformer-throttler": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@grammyjs/transformer-throttler/-/transformer-throttler-1.2.1.tgz", + "integrity": "sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==", + "license": "MIT", + "peer": true, + "dependencies": { + "bottleneck": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + }, + "peerDependencies": { + "grammy": "^1.0.0" + } + }, + "node_modules/openclaw/node_modules/@grammyjs/types": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.28.0.tgz", + "integrity": "sha512-4JvXCdxRZHCje0M4gHzLwtB4bLno3WD28xd8CNfk4POWIu73BFnSvGeW6OQ5gPem4eYTEwkD9yDaXssixl6tMQ==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/@homebridge/ciao": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.9.tgz", + "integrity": "sha512-TMy9zy173jDOpnFXDqL3BPIQn5lfcAkSsivYQatCCakoHk4fLGd7QjfAaNGYE3Ox+/ZI6Lq0e1gGcz1qdw/IbA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "fast-deep-equal": "^3.1.3", + "source-map-support": "^0.5.21", + "tslib": "^2.8.1" + }, + "bin": { + "ciao-bcs": "lib/bonjour-conformance-testing.js" + } + }, + "node_modules/openclaw/node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/openclaw/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/@lydell/node-pty": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.2.0-beta.12.tgz", + "integrity": "sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g==", + "license": "MIT", + "peer": true, + "optionalDependencies": { + "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", + "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", + "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", + "@lydell/node-pty-linux-x64": "1.2.0-beta.12", + "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", + "@lydell/node-pty-win32-x64": "1.2.0-beta.12" + } + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-darwin-arm64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.2.0-beta.12.tgz", + "integrity": "sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-darwin-x64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-x64/-/node-pty-darwin-x64-1.2.0-beta.12.tgz", + "integrity": "sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-linux-arm64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.2.0-beta.12.tgz", + "integrity": "sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-linux-x64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.2.0-beta.12.tgz", + "integrity": "sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-win32-arm64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-arm64/-/node-pty-win32-arm64-1.2.0-beta.12.tgz", + "integrity": "sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/@lydell/node-pty-win32-x64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-x64/-/node-pty-win32-x64-1.2.0-beta.12.tgz", + "integrity": "sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/@mistralai/mistralai": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.4.0.tgz", + "integrity": "sha512-t6hCx242MTGolB76CI+17jDtPIe/bzLsMdUTMMoMn9Qo1h02N2G5jQYHmKDGU3X//OgR2wvngTD7tO6tPp5poQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/openclaw/node_modules/@mozilla/readability": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz", + "integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/openclaw/node_modules/@openclaw/ai": { + "version": "2026.7.1", + "resolved": "https://registry.npmjs.org/@openclaw/ai/-/ai-2026.7.1.tgz", + "integrity": "sha512-FsKy5DXSHf4qyN8Huoz/10HZRgoEwLF4uk8UWaCafaIler+q5Fsl51HcrIqIrEe0S38OT7LOaxnR++MOshAlmw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@anthropic-ai/sdk": "0.109.1", + "@google/genai": "2.10.0", + "@mistralai/mistralai": "2.4.0", + "openai": "6.45.0", + "partial-json": "0.1.7", + "typebox": "1.3.3" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/openclaw/node_modules/@openclaw/fs-safe": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.4.1.tgz", + "integrity": "sha512-hQi+BxO10KdRFlYUot1syC+hTaUnGeQNdqX5kwkKJig8CFq1tKsYJLPm+zkiiGsSKOprPAquQl/txejEhpKPgg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22" + }, + "optionalDependencies": { + "jszip": "^3.10.1", + "tar": "7.5.19" + } + }, + "node_modules/openclaw/node_modules/@openclaw/proxyline": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@openclaw/proxyline/-/proxyline-0.3.3.tgz", + "integrity": "sha512-sftHnW69NHQqLjCxBTvQ8f/eQl+peZ5pHCBQtuTWBbeuYRHZ0/GXVTmw/O/YKsShMbqPWhJB0UYtPPdvCUSS8w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22.19.0" + }, + "peerDependencies": { + "undici": ">=8.3.0 <9" + } + }, + "node_modules/openclaw/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/openclaw/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/openclaw/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/openclaw/node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/openclaw/node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/openclaw/node_modules/@types/retry": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "peer": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/openclaw/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/openclaw/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/openclaw/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/openclaw/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/openclaw/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/openclaw/node_modules/bn.js": { + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.4.tgz", + "integrity": "sha512-njR1b+ixG2ufvL9Zn9JGneW+b5GV6jqpYyPPpg4QVt723b5kJPGUczkUyWEH9BwEA74UakJZ43I4FDLBF7ci0g==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/openclaw/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/openclaw/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/clawpdf": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/clawpdf/-/clawpdf-0.3.0.tgz", + "integrity": "sha512-41+3AnKk9yek2sm+/9XvUlDTN8Wi+ag7fmxZuqw+ySn4lqaf/fCgLeamqPLiXY4gVbizKEHGoTG/JrIIFNE2rw==", + "license": "MIT", + "peer": true, + "bin": { + "clawpdf": "dist/cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/openclaw/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/openclaw/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/openclaw/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/openclaw/node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/openclaw/node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/croner": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/croner/-/croner-10.0.1.tgz", + "integrity": "sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==", + "funding": [ + { + "type": "other", + "url": "https://paypal.me/hexagonpp" + }, + { + "type": "github", + "url": "https://github.com/sponsors/hexagon" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/openclaw/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/openclaw/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/openclaw/node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/openclaw/node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/openclaw/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openclaw/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/openclaw/node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/openclaw/node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/openclaw/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "peer": true, + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/openclaw/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/openclaw/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense", + "peer": true + }, + "node_modules/openclaw/node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/openclaw/node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/openclaw/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/openclaw/node_modules/file-type": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.1.tgz", + "integrity": "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/openclaw/node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/gaxios": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", + "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "peer": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/openclaw/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openclaw/node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/openclaw/node_modules/google-auth-library": { + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz", + "integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/openclaw/node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/grammy": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.44.0.tgz", + "integrity": "sha512-gGVykS5+c5f1tPV97LuU6IDRMawE2NzpwM9pNz58HQ35IZXDnYL3VOLvNzYognPSeBIOSzQXRu5w96V0aY8y8A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@grammyjs/types": "3.28.0", + "abort-controller": "^3.0.0", + "debug": "^4.4.3", + "node-fetch": "^2.7.0" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + } + }, + "node_modules/openclaw/node_modules/grammy/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/openclaw/node_modules/hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/openclaw/node_modules/hosted-git-info": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz", + "integrity": "sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/openclaw/node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/openclaw/node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/http_ece": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", + "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/openclaw/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/openclaw/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/openclaw/node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/openclaw/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/openclaw/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/openclaw/node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openclaw/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/openclaw/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/openclaw/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "peer": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/openclaw/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/openclaw/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "peer": true, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/openclaw/node_modules/kysely": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.2.tgz", + "integrity": "sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/openclaw/node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/openclaw/node_modules/linkedom": { + "version": "0.18.12", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz", + "integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==", + "license": "ISC", + "peer": true, + "dependencies": { + "css-select": "^5.1.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^10.0.0", + "uhyphen": "^0.2.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": ">= 2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/openclaw/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/openclaw/node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/openclaw/node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/openclaw/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openclaw/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/openclaw/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/openclaw/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/openclaw/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openclaw/node_modules/node-addon-api": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", + "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/openclaw/node_modules/node-domexception": { + "name": "@nolyfill/domexception", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@nolyfill/domexception/-/domexception-1.0.28.tgz", + "integrity": "sha512-tlc/FcYIv5i8RYsl2iDil4A0gOihaas1R5jPcIC4Zw3GhjKsVilw90aHcVlhZPTBLGBzd379S+VcnsDjd9ChiA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/openclaw/node_modules/node-edge-tts": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/node-edge-tts/-/node-edge-tts-1.2.10.tgz", + "integrity": "sha512-bV2i4XU54D45+US0Zm1HcJRkifuB3W438dWyuJEHLQdKxnuqlI1kim2MOvR6Q3XUQZvfF9PoDyR1Rt7aeXhPdQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "https-proxy-agent": "^7.0.1", + "ws": "^8.13.0", + "yargs": "^17.7.2" + }, + "bin": { + "node-edge-tts": "bin.js" + } + }, + "node_modules/openclaw/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/openclaw/node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "peer": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/openclaw/node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openclaw/node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openclaw/node_modules/openai": { + "version": "6.45.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.45.0.tgz", + "integrity": "sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==", + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openclaw/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)", + "peer": true + }, + "node_modules/openclaw/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/openclaw/node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/openclaw/node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/openclaw/node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/openclaw/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/openclaw/node_modules/protobufjs": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.3.tgz", + "integrity": "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/openclaw/node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/openclaw/node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "peer": true, + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/openclaw/node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/openclaw/node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/openclaw/node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/quickjs-wasi": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-3.0.2.tgz", + "integrity": "sha512-SyfPzlrfz67/kv0SogmQgW4c2I1klkLcbvj9Y2gc1h7+VylmvuGevFljLXibGKajKJKiJV29d4S6FQLA6Sc80A==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/rastermill": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/rastermill/-/rastermill-0.3.1.tgz", + "integrity": "sha512-CX4nij6+ZLHYIaojJNfLTr7W+AiH/IPJi6E9Aw1br2///1KZL2KBOHd68rkcLedc47MPvb4hhH+fzYeGFa4A/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@silvia-odwyer/photon-node": "0.3.4" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/openclaw/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/openclaw/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/openclaw/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/openclaw/node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openclaw/node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openclaw/node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/openclaw/node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/openclaw/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/openclaw/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openclaw/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/openclaw/node_modules/sqlite-vec": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.9.tgz", + "integrity": "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==", + "license": "MIT OR Apache", + "optional": true, + "peer": true, + "optionalDependencies": { + "sqlite-vec-darwin-arm64": "0.1.9", + "sqlite-vec-darwin-x64": "0.1.9", + "sqlite-vec-linux-arm64": "0.1.9", + "sqlite-vec-linux-x64": "0.1.9", + "sqlite-vec-windows-x64": "0.1.9" + } + }, + "node_modules/openclaw/node_modules/sqlite-vec-darwin-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.9.tgz", + "integrity": "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/sqlite-vec-darwin-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.9.tgz", + "integrity": "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/sqlite-vec-linux-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.9.tgz", + "integrity": "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/sqlite-vec-linux-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.9.tgz", + "integrity": "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/sqlite-vec-windows-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.9.tgz", + "integrity": "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/openclaw/node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/openclaw/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/openclaw/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openclaw/node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/openclaw/node_modules/tar": { + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/openclaw/node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "peer": true, + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/openclaw/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/tree-sitter-bash": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/tree-sitter-bash/-/tree-sitter-bash-0.25.1.tgz", + "integrity": "sha512-7hMytuYIMoXOq24yRulgIxthE9YmggZIOHCyPTTuJcu6EU54tYD+4G39cUb28kxC6jMf/AbPfWGLQtgPTdh3xw==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "node-addon-api": "^8.2.1", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.25.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, + "node_modules/openclaw/node_modules/tslog": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.10.2.tgz", + "integrity": "sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/fullstack-build/tslog?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/openclaw/node_modules/typebox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.3.tgz", + "integrity": "sha512-URXGUE31PJDQC+PtRMJeLdF4kmmOdFoVPikPCtV2oOIhUpNpppEdIz7W8bH8cFYPYHdDpaRvqwdegMTmHliudg==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/openclaw/node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openclaw/node_modules/undici": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/openclaw/node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openclaw/node_modules/web-push": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", + "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==", + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "asn1.js": "^5.3.0", + "http_ece": "1.2.0", + "https-proxy-agent": "^7.0.0", + "jws": "^4.0.0", + "minimist": "^1.2.5" + }, + "bin": { + "web-push": "src/cli.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/openclaw/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/openclaw/node_modules/web-tree-sitter": { + "version": "0.26.10", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.10.tgz", + "integrity": "sha512-vengBGYS7FpAerkR3o04oBL4L8MkVmjawK50AFBu7v0HZBkmF9ZavPGKoXLSSmRhp7T/YgsJ7joAS3yAxHPEqQ==", + "license": "MIT", + "peer": true + }, + "node_modules/openclaw/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/openclaw/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/openclaw/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/openclaw/node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/openclaw/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "peer": true + }, + "node_modules/openclaw/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/openclaw/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/openclaw/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/openclaw/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/openclaw/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/openclaw/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/openclaw/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/openclaw/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/oxfmt": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.41.0.tgz", + "integrity": "sha512-sKLdJZdQ3bw6x9qKiT7+eID4MNEXlDHf5ZacfIircrq6Qwjk0L6t2/JQlZZrVHTXJawK3KaMuBoJnEJPcqCEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinypool": "2.1.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.41.0", + "@oxfmt/binding-android-arm64": "0.41.0", + "@oxfmt/binding-darwin-arm64": "0.41.0", + "@oxfmt/binding-darwin-x64": "0.41.0", + "@oxfmt/binding-freebsd-x64": "0.41.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.41.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.41.0", + "@oxfmt/binding-linux-arm64-gnu": "0.41.0", + "@oxfmt/binding-linux-arm64-musl": "0.41.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.41.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.41.0", + "@oxfmt/binding-linux-riscv64-musl": "0.41.0", + "@oxfmt/binding-linux-s390x-gnu": "0.41.0", + "@oxfmt/binding-linux-x64-gnu": "0.41.0", + "@oxfmt/binding-linux-x64-musl": "0.41.0", + "@oxfmt/binding-openharmony-arm64": "0.41.0", + "@oxfmt/binding-win32-arm64-msvc": "0.41.0", + "@oxfmt/binding-win32-ia32-msvc": "0.41.0", + "@oxfmt/binding-win32-x64-msvc": "0.41.0" + } + }, + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs index 4fbff1a29b..4fe8699a0f 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs @@ -144,11 +144,21 @@ async function proveAuthorityRedacted() { now: () => nowMs, fetchImpl: async () => fetchResponse(200, { - schema: 'evaos.mac_control.runtime_receipt_envelope.v1', - receiptBase64: leakingReceipt.toString('base64url'), - signature: '-----BEGIN SSH SIGNATURE-----\nQUFBQQ==\n-----END SSH SIGNATURE-----\n', - keyId: receiptKeyId, - namespace: 'evaos-mac-control-receipt-v1', + schema: 'evaos.mac_control.runtime_receipt_bundle.v2', + privateReceipt: { + schema: 'evaos.mac_control.runtime_receipt_envelope.v1', + receiptBase64: leakingReceipt.toString('base64url'), + signature: '-----BEGIN SSH SIGNATURE-----\nQUFBQQ==\n-----END SSH SIGNATURE-----\n', + keyId: receiptKeyId, + namespace: 'evaos-mac-control-receipt-v1', + }, + publicAttestation: { + schema: 'evaos.mac_control.public_runtime_attestation_envelope.v1', + attestationBase64: Buffer.from('{}').toString('base64url'), + signature: '-----BEGIN SSH SIGNATURE-----\nQUFBQQ==\n-----END SSH SIGNATURE-----\n', + keyId: receiptKeyId, + namespace: 'evaos-mac-control-public-attestation-v1', + }, }), }); const response = await invoke(handler, requestBody(4), authority.headers); diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts index d7b9e9ac95..8e9fa9b54b 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts @@ -5,12 +5,14 @@ export const MAC_CONTROL_RUNTIME_RECEIPT_PATH = '/api/v1/evaos/mac-control/runti const CONTEXT_SCHEMA = 'evaos.mac_control_execution_context.v1'; const CONTRACT_SCHEMA = 'evaos.mac_control_runtime_contract.v2'; const CONNECTOR_REQUEST_SCHEMA = 'evaos.mac_control.canary_request.v1'; -const CONNECTOR_RESPONSE_SCHEMA = 'evaos.mac_control.runtime_receipt_envelope.v1'; +const CONNECTOR_RESPONSE_SCHEMA = 'evaos.mac_control.runtime_receipt_bundle.v2'; +const PRIVATE_RECEIPT_ENVELOPE_SCHEMA = 'evaos.mac_control.runtime_receipt_envelope.v1'; const RECEIPT_SCHEMA = 'evaos.mac_control.runtime_receipt.v1'; const RECEIPT_NAMESPACE = 'evaos-mac-control-receipt-v1'; -const PUBLIC_PROOF_SCHEMA = 'evaos.mac_control.runtime_proof.v2'; +const PUBLIC_ATTESTATION_SCHEMA = 'evaos.mac_control.public_runtime_attestation.v1'; +const PUBLIC_ATTESTATION_ENVELOPE_SCHEMA = 'evaos.mac_control.public_runtime_attestation_envelope.v1'; +const PUBLIC_ATTESTATION_NAMESPACE = 'evaos-mac-control-public-attestation-v1'; const PUBLIC_PROOF_KIND = 'selected_binding_direct_mac_control'; -const PUBLIC_PROOF_SOURCE = 'evaos-desktop-bridge:runtime-receipt'; const CONTEXT_TTL_SECONDS = 60; const CLOCK_SKEW_SECONDS = 5; const DEFAULT_CONNECTOR_TIMEOUT_MS = 10_000; @@ -29,7 +31,9 @@ const CONTEXT_FIELDS = [ 'expires_at', 'context_id', ] as const; -const RESPONSE_FIELDS = ['schema', 'receiptBase64', 'signature', 'keyId', 'namespace'] as const; +const RESPONSE_FIELDS = ['schema', 'privateReceipt', 'publicAttestation'] as const; +const PRIVATE_RECEIPT_ENVELOPE_FIELDS = ['schema', 'receiptBase64', 'signature', 'keyId', 'namespace'] as const; +const PUBLIC_ATTESTATION_ENVELOPE_FIELDS = ['schema', 'attestationBase64', 'signature', 'keyId', 'namespace'] as const; const RECEIPT_FIELDS = [ 'schema', 'keyId', @@ -89,6 +93,25 @@ const OWNER_FIELDS = [ const PATH_FIELDS = ['kind', 'value'] as const; const ACTION_FIELDS = ['command', 'args'] as const; const ACTION_ARGS_FIELDS = ['keys', 'dryRun'] as const; +const PUBLIC_ATTESTATION_FIELDS = [ + 'schema', + 'keyId', + 'namespace', + 'proofKind', + 'runtime', + 'tool', + 'outcome', + 'runRef', + 'executedAt', + 'authorityIssuedAt', + 'authorityExpiresAt', + 'contextKeyId', + 'controlState', + 'auditRecorded', + 'privateReceiptSha256', + 'connectorCandidate', +] as const; +const PUBLIC_CANDIDATE_FIELDS = ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild'] as const; const HEADER = { context: 'x-evaos-mac-control-execution-context', @@ -180,21 +203,23 @@ type ReceiptVerifierConfig = { expectedAppBuild: string; }; -type PublicRuntimeProof = { - ok: true; - schema: typeof PUBLIC_PROOF_SCHEMA; +type PublicRuntimeAttestation = { + schema: typeof PUBLIC_ATTESTATION_SCHEMA; + keyId: string; + namespace: typeof PUBLIC_ATTESTATION_NAMESPACE; proofKind: typeof PUBLIC_PROOF_KIND; + runtime: 'openclaw'; tool: 'customer_mac.desktop_hotkey'; outcome: 'succeeded'; runRef: string; executedAt: string; - bindingRef: string; - bindingVersion: string; - sessionRef: string; - expiresAt: number; - auditRef: string; - sourcePointer: typeof PUBLIC_PROOF_SOURCE; - candidate: { + authorityIssuedAt: number; + authorityExpiresAt: number; + contextKeyId: string; + controlState: 'ready_unchanged'; + auditRecorded: true; + privateReceiptSha256: string; + connectorCandidate: { sourceCommit: string; sourceSha256: string; appVersion: string; @@ -202,6 +227,14 @@ type PublicRuntimeProof = { }; }; +type PublicAttestationEnvelope = { + schema: typeof PUBLIC_ATTESTATION_ENVELOPE_SCHEMA; + attestationBase64: string; + signature: string; + keyId: string; + namespace: typeof PUBLIC_ATTESTATION_NAMESPACE; +}; + type PluginHttpApi = { registerHttpRoute(route: { path: string; @@ -558,7 +591,7 @@ function validateConnectorEnvelope( authority: VerifiedAuthority, request: PublicRequest, verifier: ReceiptVerifierConfig -): { ok: true; value: PublicRuntimeProof } | { ok: false } { +): { ok: true; value: PublicAttestationEnvelope } | { ok: false } { if (Buffer.from(raw).byteLength > MAX_CONNECTOR_RESPONSE_BYTES) { return { ok: false }; } @@ -571,21 +604,27 @@ function validateConnectorEnvelope( if (!isRecord(parsed) || !hasExactKeys(parsed, RESPONSE_FIELDS)) { return { ok: false }; } + const privateReceipt = parsed.privateReceipt; + const publicAttestation = parsed.publicAttestation; if ( parsed.schema !== CONNECTOR_RESPONSE_SCHEMA || - parsed.namespace !== RECEIPT_NAMESPACE || - typeof parsed.receiptBase64 !== 'string' || - parsed.receiptBase64.length < 32 || - parsed.receiptBase64.length > 32768 || - typeof parsed.signature !== 'string' || - parsed.signature.length > 8192 || - typeof parsed.keyId !== 'string' || - parsed.keyId !== verifier.keyId + !isRecord(privateReceipt) || + !hasExactKeys(privateReceipt, PRIVATE_RECEIPT_ENVELOPE_FIELDS) || + !isRecord(publicAttestation) || + !hasExactKeys(publicAttestation, PUBLIC_ATTESTATION_ENVELOPE_FIELDS) || + privateReceipt.schema !== PRIVATE_RECEIPT_ENVELOPE_SCHEMA || + privateReceipt.namespace !== RECEIPT_NAMESPACE || + typeof privateReceipt.receiptBase64 !== 'string' || + privateReceipt.receiptBase64.length < 32 || + privateReceipt.receiptBase64.length > 32768 || + typeof privateReceipt.signature !== 'string' || + privateReceipt.signature.length > 8192 || + privateReceipt.keyId !== verifier.keyId ) { return { ok: false }; } - const receiptBytes = decodeBase64Url(parsed.receiptBase64); - if (!receiptBytes || receiptBytes.byteLength > 24576 || !validSshSignature(parsed.signature)) { + const receiptBytes = decodeBase64Url(privateReceipt.receiptBase64); + if (!receiptBytes || receiptBytes.byteLength > 24576 || !validSshSignature(privateReceipt.signature)) { return { ok: false }; } const receiptText = receiptBytes.toString('utf8'); @@ -607,10 +646,14 @@ function validateConnectorEnvelope( ) { return { ok: false }; } - if (!verifySshSignature(receiptBytes, parsed.signature, verifier.publicKey)) { + if (!verifySshSignature(receiptBytes, privateReceipt.signature, verifier.publicKey, RECEIPT_NAMESPACE)) { + return { ok: false }; + } + const expectedAttestation = validateReceipt(receiptBytes, authority, request, verifier); + if (!expectedAttestation.ok) { return { ok: false }; } - return validateReceipt(receiptBytes, authority, request, verifier); + return validatePublicAttestationEnvelope(publicAttestation, expectedAttestation.value, authority, verifier); } function validateReceipt( @@ -618,7 +661,7 @@ function validateReceipt( authority: VerifiedAuthority, request: PublicRequest, verifier: ReceiptVerifierConfig -): { ok: true; value: PublicRuntimeProof } | { ok: false } { +): { ok: true; value: PublicRuntimeAttestation } | { ok: false } { let parsed: unknown; try { parsed = JSON.parse(receiptBytes.toString('utf8')); @@ -696,20 +739,22 @@ function validateReceipt( return { ok: true, value: { - ok: true, - schema: PUBLIC_PROOF_SCHEMA, + schema: PUBLIC_ATTESTATION_SCHEMA, + keyId: verifier.keyId, + namespace: PUBLIC_ATTESTATION_NAMESPACE, proofKind: PUBLIC_PROOF_KIND, + runtime: 'openclaw', tool: 'customer_mac.desktop_hotkey', outcome: 'succeeded', runRef: request.runRef, executedAt: parsed.executedAt as string, - bindingRef: parsed.bindingRef as string, - bindingVersion: authority.context.binding_version, - sessionRef: parsed.sessionRef as string, - expiresAt: authority.context.expires_at, - auditRef: saltedHash(request.challenge, parsed.auditId), - sourcePointer: PUBLIC_PROOF_SOURCE, - candidate: { + authorityIssuedAt: authority.context.issued_at, + authorityExpiresAt: authority.context.expires_at, + contextKeyId: authority.contextKeyId, + controlState: 'ready_unchanged', + auditRecorded: true, + privateReceiptSha256: sha256Hex(receiptBytes), + connectorCandidate: { sourceCommit: typedCandidate.sourceCommit as string, sourceSha256: typedCandidate.sourceSha256 as string, appVersion: typedCandidate.appVersion as string, @@ -719,6 +764,79 @@ function validateReceipt( }; } +function validatePublicAttestationEnvelope( + value: Record, + expected: PublicRuntimeAttestation, + authority: VerifiedAuthority, + verifier: ReceiptVerifierConfig +): { ok: true; value: PublicAttestationEnvelope } | { ok: false } { + if ( + value.schema !== PUBLIC_ATTESTATION_ENVELOPE_SCHEMA || + value.namespace !== PUBLIC_ATTESTATION_NAMESPACE || + value.keyId !== verifier.keyId || + typeof value.attestationBase64 !== 'string' || + value.attestationBase64.length < 32 || + value.attestationBase64.length > 24576 || + typeof value.signature !== 'string' || + value.signature.length > 8192 || + !validSshSignature(value.signature) + ) { + return { ok: false }; + } + const attestationBytes = decodeBase64Url(value.attestationBase64); + if (!attestationBytes || attestationBytes.byteLength > 16384) { + return { ok: false }; + } + const attestationText = attestationBytes.toString('utf8'); + const forbiddenValues = [ + authority.connectorToken, + authority.connectorUrl, + authority.context.customer_id, + authority.context.customer_vm_id, + authority.context.binding_id, + authority.contextPayload, + authority.contextSignature, + ]; + if ( + forbiddenValues.some((forbidden) => forbidden.length > 0 && attestationText.includes(forbidden)) || + /https?:\/\//i.test(attestationText) || + /\b100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])(?:\.\d{1,3}){2}\b/.test(attestationText) || + /(?:\/tmp\/|\/private\/var\/folders\/)/i.test(attestationText) || + /"(?:challenge|token|secret|authorization|connector_url|customer_id|customer_vm_id|binding_id|bindingRef|sessionRef|auditRef)"\s*:/i.test( + attestationText + ) + ) { + return { ok: false }; + } + let attestation: unknown; + try { + attestation = JSON.parse(attestationText); + } catch { + return { ok: false }; + } + if ( + !isRecord(attestation) || + !hasExactKeys(attestation, PUBLIC_ATTESTATION_FIELDS) || + !isRecord(attestation.connectorCandidate) || + !hasExactKeys(attestation.connectorCandidate, PUBLIC_CANDIDATE_FIELDS) || + canonicalJson(attestation) !== attestationText || + canonicalJson(attestation) !== canonicalJson(expected) || + !verifySshSignature(attestationBytes, value.signature, verifier.publicKey, PUBLIC_ATTESTATION_NAMESPACE) + ) { + return { ok: false }; + } + return { + ok: true, + value: { + schema: PUBLIC_ATTESTATION_ENVELOPE_SCHEMA, + attestationBase64: value.attestationBase64, + signature: value.signature, + keyId: verifier.keyId, + namespace: PUBLIC_ATTESTATION_NAMESPACE, + }, + }; +} + function validControlState(value: Record): boolean { return ( hasExactKeys(value, CONTROL_STATE_FIELDS) && @@ -790,7 +908,12 @@ function validReceiptAction(value: unknown): value is { command: string; args: R ); } -function verifySshSignature(message: ByteBuffer, armor: string, pinnedPublicKey: ByteBuffer): boolean { +function verifySshSignature( + message: ByteBuffer, + armor: string, + pinnedPublicKey: ByteBuffer, + expectedNamespace: string +): boolean { const decoded = decodeSshSignatureArmor(armor); if (!decoded) { return false; @@ -805,7 +928,7 @@ function verifySshSignature(message: ByteBuffer, armor: string, pinnedPublicKey: const reserved = reader.readString(); const hashAlgorithm = reader.readString().toString('ascii'); const signatureBlob = reader.readString(); - if (!reader.done() || namespace !== RECEIPT_NAMESPACE || reserved.byteLength !== 0) { + if (!reader.done() || namespace !== expectedNamespace || reserved.byteLength !== 0) { return false; } diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs index d6bdf50eb3..126a63efa3 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs @@ -19,6 +19,7 @@ const NOW_MS = Date.parse('2026-07-15T00:00:00Z'); const KEY_ID = 'mac-context-test-2026-07'; const RECEIPT_KEY_ID = 'connector-receipt-test-2026-07'; const RECEIPT_NAMESPACE = 'evaos-mac-control-receipt-v1'; +const PUBLIC_ATTESTATION_NAMESPACE = 'evaos-mac-control-public-attestation-v1'; const CONNECTOR_TOKEN = 'connector-token-value-at-least-24'; const EXPECTED_COMMIT = 'a'.repeat(40); const EXPECTED_SOURCE_SHA256 = 'b'.repeat(64); @@ -124,22 +125,26 @@ test('verifies server authority and forwards only the fixed connector canary con }); const response = await invoke(handler, { challenge, runRef }, authority.headers); assert.equal(response.statusCode, 200); - const proof = JSON.parse(response.body); - assert.deepEqual(proof, { - ok: true, - schema: 'evaos.mac_control.runtime_proof.v2', + const envelope = JSON.parse(response.body); + assert.deepEqual(envelope, connectorEnvelope.publicAttestation); + const attestation = JSON.parse(Buffer.from(envelope.attestationBase64, 'base64url')); + assert.deepEqual(attestation, { + schema: 'evaos.mac_control.public_runtime_attestation.v1', + keyId: RECEIPT_KEY_ID, + namespace: PUBLIC_ATTESTATION_NAMESPACE, proofKind: 'selected_binding_direct_mac_control', + runtime: 'openclaw', tool: 'customer_mac.desktop_hotkey', outcome: 'succeeded', runRef, executedAt: '2026-07-15T00:00:10Z', - bindingRef: receipt.bindingRef, - bindingVersion: '7', - sessionRef: receipt.sessionRef, - expiresAt: Math.floor(NOW_MS / 1000) + 50, - auditRef: saltedHash(challenge, receipt.auditId), - sourcePointer: 'evaos-desktop-bridge:runtime-receipt', - candidate: { + authorityIssuedAt: Math.floor(NOW_MS / 1000), + authorityExpiresAt: Math.floor(NOW_MS / 1000) + 50, + contextKeyId: KEY_ID, + controlState: 'ready_unchanged', + auditRecorded: true, + privateReceiptSha256: sha256(canonicalBytes(receipt)), + connectorCandidate: { sourceCommit: EXPECTED_COMMIT, sourceSha256: EXPECTED_SOURCE_SHA256, appVersion: '2.1.36', @@ -147,9 +152,12 @@ test('verifies server authority and forwards only the fixed connector canary con }, }); assert.equal(response.body.includes('receiptBase64'), false); - assert.equal(response.body.includes('SSH SIGNATURE'), false); + assert.equal(response.body.includes('SSH SIGNATURE'), true); assert.equal(response.body.includes(CONNECTOR_TOKEN), false); assert.equal(response.body.includes('customer-1'), false); + for (const field of ['challenge', 'bindingRef', 'sessionRef', 'auditRef', 'customerRef', 'vmRef']) { + assert.equal(field in attestation, false); + } assert.equal(connectorCalls, 1); }); @@ -237,7 +245,7 @@ test('rejects forged signatures and re-signed wrong release claims', async (t) = 'forged signature', () => { const envelope = signedConnectorEnvelope(valid); - envelope.signature = sshSignature(canonicalBytes({ ...valid, runRef: 'other-run' })); + envelope.privateReceipt.signature = sshSignature(canonicalBytes({ ...valid, runRef: 'other-run' })); return envelope; }, ], @@ -249,6 +257,28 @@ test('rejects forged signatures and re-signed wrong release claims', async (t) = candidate: { ...valid.candidate, sourceCommit: 'd'.repeat(40) }, }), ], + [ + 'forged public attestation', + () => { + const envelope = signedConnectorEnvelope(valid); + envelope.publicAttestation.signature = sshSignature( + canonicalBytes({ ...publicAttestation(valid), outcome: 'failed' }), + PUBLIC_ATTESTATION_NAMESPACE + ); + return envelope; + }, + ], + [ + 'public attestation paired with another private receipt', + () => { + const envelope = signedConnectorEnvelope(valid); + envelope.publicAttestation = signedConnectorEnvelope({ + ...valid, + auditRecordDigest: 'd'.repeat(64), + }).publicAttestation; + return envelope; + }, + ], ]; for (const [name, envelope] of cases) { await t.test(name, async () => { @@ -287,12 +317,30 @@ test('verifies a real OpenSSH SSHSIG over the exact canonical receipt bytes', as input: receiptBytes, }); assert.equal(signed.status, 0, signed.stderr.toString('utf8')); + const attestation = publicAttestation(receipt); + const attestationBytes = canonicalBytes(attestation); + const publicSigned = spawnSync( + '/usr/bin/ssh-keygen', + ['-Y', 'sign', '-q', '-f', keyPath, '-n', PUBLIC_ATTESTATION_NAMESPACE, '-'], + { input: attestationBytes } + ); + assert.equal(publicSigned.status, 0, publicSigned.stderr.toString('utf8')); const envelope = { - schema: 'evaos.mac_control.runtime_receipt_envelope.v1', - receiptBase64: receiptBytes.toString('base64url'), - signature: signed.stdout.toString('ascii'), - keyId: RECEIPT_KEY_ID, - namespace: RECEIPT_NAMESPACE, + schema: 'evaos.mac_control.runtime_receipt_bundle.v2', + privateReceipt: { + schema: 'evaos.mac_control.runtime_receipt_envelope.v1', + receiptBase64: receiptBytes.toString('base64url'), + signature: signed.stdout.toString('ascii'), + keyId: RECEIPT_KEY_ID, + namespace: RECEIPT_NAMESPACE, + }, + publicAttestation: { + schema: 'evaos.mac_control.public_runtime_attestation_envelope.v1', + attestationBase64: attestationBytes.toString('base64url'), + signature: publicSigned.stdout.toString('ascii'), + keyId: RECEIPT_KEY_ID, + namespace: PUBLIC_ATTESTATION_NAMESPACE, + }, }; const handler = createMacControlRuntimeReceiptHandler({ env: { ...env, EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY: actualPublicKey.toString('base64url') }, @@ -301,7 +349,7 @@ test('verifies a real OpenSSH SSHSIG over the exact canonical receipt bytes', as }); const response = await invoke(handler, { challenge, runRef }, authority.headers); assert.equal(response.statusCode, 200, response.body); - assert.equal(JSON.parse(response.body).schema, 'evaos.mac_control.runtime_proof.v2'); + assert.equal(JSON.parse(response.body).schema, 'evaos.mac_control.public_runtime_attestation_envelope.v1'); } finally { rmSync(root, { recursive: true, force: true }); } @@ -538,23 +586,61 @@ function validReceipt(authority, challenge, runRef, overrides = {}) { function signedConnectorEnvelope(receipt) { const receiptBytes = canonicalBytes(receipt); + const attestation = publicAttestation(receipt); + const attestationBytes = canonicalBytes(attestation); return { - schema: 'evaos.mac_control.runtime_receipt_envelope.v1', - receiptBase64: receiptBytes.toString('base64url'), - signature: sshSignature(receiptBytes), + schema: 'evaos.mac_control.runtime_receipt_bundle.v2', + privateReceipt: { + schema: 'evaos.mac_control.runtime_receipt_envelope.v1', + receiptBase64: receiptBytes.toString('base64url'), + signature: sshSignature(receiptBytes, RECEIPT_NAMESPACE), + keyId: RECEIPT_KEY_ID, + namespace: RECEIPT_NAMESPACE, + }, + publicAttestation: { + schema: 'evaos.mac_control.public_runtime_attestation_envelope.v1', + attestationBase64: attestationBytes.toString('base64url'), + signature: sshSignature(attestationBytes, PUBLIC_ATTESTATION_NAMESPACE), + keyId: RECEIPT_KEY_ID, + namespace: PUBLIC_ATTESTATION_NAMESPACE, + }, + }; +} + +function publicAttestation(receipt) { + return { + schema: 'evaos.mac_control.public_runtime_attestation.v1', keyId: RECEIPT_KEY_ID, - namespace: RECEIPT_NAMESPACE, + namespace: PUBLIC_ATTESTATION_NAMESPACE, + proofKind: 'selected_binding_direct_mac_control', + runtime: 'openclaw', + tool: 'customer_mac.desktop_hotkey', + outcome: 'succeeded', + runRef: receipt.runRef, + executedAt: receipt.executedAt, + authorityIssuedAt: receipt.contextIssuedAt, + authorityExpiresAt: receipt.contextExpiresAt, + contextKeyId: receipt.contextKeyId, + controlState: 'ready_unchanged', + auditRecorded: true, + privateReceiptSha256: sha256(canonicalBytes(receipt)), + connectorCandidate: { + sourceCommit: receipt.candidate.sourceCommit, + sourceSha256: receipt.candidate.sourceSha256, + appVersion: receipt.candidate.appVersion, + appBuild: receipt.candidate.appBuild, + }, }; } -function sshSignature(message) { +function sshSignature(message, namespace = RECEIPT_NAMESPACE) { const algorithm = Buffer.from('ssh-ed25519'); - const namespace = Buffer.from(RECEIPT_NAMESPACE); + const namespaceBytes = Buffer.from(namespace); const hashAlgorithm = Buffer.from('sha512'); const digest = createHash('sha512').update(message).digest(); const signedData = Buffer.concat([ Buffer.from('SSHSIG'), - sshString(namespace), + sshString(namespaceBytes), sshString(Buffer.alloc(0)), sshString(hashAlgorithm), sshString(digest), @@ -566,7 +652,7 @@ function sshSignature(message) { Buffer.from('SSHSIG'), uint32(1), sshString(publicKeyBlob), - sshString(namespace), + sshString(namespaceBytes), sshString(Buffer.alloc(0)), sshString(hashAlgorithm), sshString(signatureBlob), diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py index e101bf5304..dcdd04db85 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py @@ -23,14 +23,16 @@ from .receipt_canary import ( CanaryError, build_receipt, + build_public_attestation, burn_replay_token, candidate_snapshot, default_process_identity, load_canary_config, - receipt_envelope, + receipt_bundle, require_canary_authority_fresh, require_canary_control_state, sign_receipt, + sign_public_attestation, validate_action_audit, validate_canary_request, validate_receipt_signer_key, @@ -1609,8 +1611,19 @@ def _mac_control_canary(self) -> None: .replace("+00:00", "Z"), binding_expires_at=str(payload["binding"]["bindingExpiresAt"]), ) - signature = sign_receipt(receipt, config) - self._write_json(200, receipt_envelope(receipt, signature, config)) + receipt_signature = sign_receipt(receipt, config) + public_attestation = build_public_attestation(receipt, config) + public_signature = sign_public_attestation(public_attestation, config) + self._write_json( + 200, + receipt_bundle( + receipt=receipt, + receipt_signature=receipt_signature, + public_attestation=public_attestation, + public_signature=public_signature, + config=config, + ), + ) except CanaryError as exc: self._write_json(exc.status, {"ok": False, "error": exc.code}) except Exception: diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py index 15cff5191a..b75d31e8d0 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import base64 import json import os import posixpath @@ -9,6 +10,7 @@ import subprocess import sys import time +import tempfile import uuid import urllib.error import urllib.parse @@ -80,20 +82,11 @@ "not_found", ) SELECTED_BINDING_PROOF_FIELDS = { - "ok", "schema", - "proofKind", - "tool", - "outcome", - "runRef", - "executedAt", - "bindingRef", - "bindingVersion", - "sessionRef", - "expiresAt", - "auditRef", - "sourcePointer", - "candidate", + "attestationBase64", + "signature", + "keyId", + "namespace", } SELECTED_BINDING_CANDIDATE_FIELDS = { "sourceCommit", @@ -101,6 +94,24 @@ "appVersion", "appBuild", } +SELECTED_BINDING_ATTESTATION_FIELDS = { + "schema", + "keyId", + "namespace", + "proofKind", + "runtime", + "tool", + "outcome", + "runRef", + "executedAt", + "authorityIssuedAt", + "authorityExpiresAt", + "contextKeyId", + "controlState", + "auditRecorded", + "privateReceiptSha256", + "connectorCandidate", +} LOCAL_WORKBENCH_CONTROL_START = "local_workbench_control_start" INSTALLED_WORKBENCH_BRIDGE_CLI = Path( "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge" @@ -139,18 +150,51 @@ class SurfaceResponse: warnings: list[Any] @classmethod - def from_payload(cls, payload: dict[str, Any], artifact_path: str | None = None) -> "SurfaceResponse": + def from_payload( + cls, payload: dict[str, Any], artifact_path: str | None = None + ) -> "SurfaceResponse": data = payload.get("data") if isinstance(payload.get("data"), dict) else {} - errors = payload.get("errors") if isinstance(payload.get("errors"), list) else [] - warnings = payload.get("warnings") if isinstance(payload.get("warnings"), list) else [] + errors = ( + payload.get("errors") if isinstance(payload.get("errors"), list) else [] + ) + warnings = ( + payload.get("warnings") if isinstance(payload.get("warnings"), list) else [] + ) return cls( payload=payload, ok=payload.get("ok") is True, - audit_id=payload.get("audit_id") if isinstance(payload.get("audit_id"), str) else None, - engine=_extract_string(data, ("engine", "screenshot.engine", "screenshot.screenshot.engine", "image.engine")), - snapshot_id=_extract_string(data, ("snapshot_id", "screenshot.snapshot_id", "screenshot.screenshot.snapshot_id", "image.snapshot_id", "screenshot.image.snapshot_id")), + audit_id=payload.get("audit_id") + if isinstance(payload.get("audit_id"), str) + else None, + engine=_extract_string( + data, + ( + "engine", + "screenshot.engine", + "screenshot.screenshot.engine", + "image.engine", + ), + ), + snapshot_id=_extract_string( + data, + ( + "snapshot_id", + "screenshot.snapshot_id", + "screenshot.screenshot.snapshot_id", + "image.snapshot_id", + "screenshot.image.snapshot_id", + ), + ), artifact_path=artifact_path - or _extract_string(data, ("vm_visual_artifact_path", "screenshot.vm_artifact_path", "screenshot.screenshot.vm_artifact_path", "image.vm_artifact_path")), + or _extract_string( + data, + ( + "vm_visual_artifact_path", + "screenshot.vm_artifact_path", + "screenshot.screenshot.vm_artifact_path", + "image.vm_artifact_path", + ), + ), errors=[error for error in errors if isinstance(error, dict)], warnings=warnings, ) @@ -194,8 +238,7 @@ def to_report_dict(self) -> dict[str, Any]: class CanarySurface(Protocol): - def run(self, command: str, params: dict[str, Any]) -> SurfaceResponse: - ... + def run(self, command: str, params: dict[str, Any]) -> SurfaceResponse: ... class ConnectorSurface: @@ -205,7 +248,9 @@ def __init__(self, *, connector_url: str, token: str, artifact_dir: Path) -> Non self.artifact_dir = artifact_dir def run(self, command: str, params: dict[str, Any]) -> SurfaceResponse: - payload = self._post_json("/v1/commands", {"command": command, "params": params}) + payload = self._post_json( + "/v1/commands", {"command": command, "params": params} + ) artifact_path = self._materialize_visual_artifact(payload) return SurfaceResponse.from_payload(payload, artifact_path=artifact_path) @@ -222,36 +267,75 @@ def _post_json(self, path: str, body: dict[str, Any]) -> dict[str, Any]: ) command = str(body.get("command") or "connector.command") try: - with urllib.request.urlopen(request, timeout=timeout_for_command(command) + 5) as response: # noqa: S310 - operator-supplied private connector URL. + with urllib.request.urlopen( + request, timeout=timeout_for_command(command) + 5 + ) as response: # noqa: S310 - operator-supplied private connector URL. return _loads_json_response(response.read()) except urllib.error.HTTPError as exc: body_bytes = exc.read() try: return _loads_json_response(body_bytes) except ValueError: - return _error_payload(command=command, code="connector_http_error", message=body_bytes.decode("utf-8", errors="replace") or f"HTTP {exc.code}") + return _error_payload( + command=command, + code="connector_http_error", + message=body_bytes.decode("utf-8", errors="replace") + or f"HTTP {exc.code}", + ) except TimeoutError: - return _error_payload(command=command, code="connector_timeout", message=f"{command} timed out after {timeout_for_command(command)} seconds.") + return _error_payload( + command=command, + code="connector_timeout", + message=f"{command} timed out after {timeout_for_command(command)} seconds.", + ) def _materialize_visual_artifact(self, payload: dict[str, Any]) -> str | None: data = payload.get("data") if isinstance(payload.get("data"), dict) else {} - artifact_url = _extract_string(data, ("image.artifact_url", "screenshot.artifact_url", "screenshot.screenshot.artifact_url", "screenshot.image.artifact_url")) + artifact_url = _extract_string( + data, + ( + "image.artifact_url", + "screenshot.artifact_url", + "screenshot.screenshot.artifact_url", + "screenshot.image.artifact_url", + ), + ) if not artifact_url: return None endpoint = urllib.parse.urljoin(self.connector_url + "/", artifact_url) parsed_endpoint = urllib.parse.urlparse(endpoint) parsed_connector = urllib.parse.urlparse(self.connector_url) - if parsed_endpoint.netloc != parsed_connector.netloc or not parsed_endpoint.path.startswith("/v1/artifacts/"): - payload.setdefault("warnings", []).append("Connector returned an artifact URL outside the paired connector.") + if ( + parsed_endpoint.netloc != parsed_connector.netloc + or not parsed_endpoint.path.startswith("/v1/artifacts/") + ): + payload.setdefault("warnings", []).append( + "Connector returned an artifact URL outside the paired connector." + ) return None - request = urllib.request.Request(endpoint, method="GET", headers={"Authorization": f"Bearer {self.token}"}) + request = urllib.request.Request( + endpoint, method="GET", headers={"Authorization": f"Bearer {self.token}"} + ) try: with urllib.request.urlopen(request, timeout=15) as response: # noqa: S310 - validated same-origin connector artifact URL. content = response.read() except Exception as exc: # noqa: BLE001 - report evidence fetch failure without hiding command result. - payload.setdefault("warnings", []).append(f"Unable to fetch connector artifact: {exc}") + payload.setdefault("warnings", []).append( + f"Unable to fetch connector artifact: {exc}" + ) return None - snapshot_id = _extract_string(data, ("snapshot_id", "screenshot.snapshot_id", "screenshot.screenshot.snapshot_id", "image.artifact_id")) or Path(parsed_endpoint.path).stem + snapshot_id = ( + _extract_string( + data, + ( + "snapshot_id", + "screenshot.snapshot_id", + "screenshot.screenshot.snapshot_id", + "image.artifact_id", + ), + ) + or Path(parsed_endpoint.path).stem + ) output_dir = self.artifact_dir / "evidence" output_dir.mkdir(parents=True, exist_ok=True) output_path = output_dir / f"{_safe_filename(snapshot_id)}.png" @@ -336,7 +420,9 @@ def _start_local_control(self, params: dict[str, Any]) -> dict[str, Any]: code="local_control_agent_label_invalid", message="Local Workbench control agent label is invalid.", ) - label_prefix = agent_label.strip() if isinstance(agent_label, str) else "evaOS QA Canary" + label_prefix = ( + agent_label.strip() if isinstance(agent_label, str) else "evaOS QA Canary" + ) binding_nonce = uuid.uuid4().hex bound_agent_label = f"{label_prefix[:120]} [{binding_nonce}]" generation = self.session_reader().get("generation") @@ -503,7 +589,9 @@ def _start_local_control(self, params: dict[str, Any]) -> dict[str, Any]: if remote_status is not None and remote_status.ok is True and isinstance(remote_status.payload.get("data"), dict) - and isinstance(remote_status.payload.get("data", {}).get("session"), dict) + and isinstance( + remote_status.payload.get("data", {}).get("session"), dict + ) else None ) if not ( @@ -601,10 +689,16 @@ def evaluate_connector_candidate_identity( "expected_build": expected_build, } bridge = payload.get("bridge") if isinstance(payload.get("bridge"), dict) else {} - candidate = bridge.get("candidate") if isinstance(bridge.get("candidate"), dict) else {} + candidate = ( + bridge.get("candidate") if isinstance(bridge.get("candidate"), dict) else {} + ) process = payload.get("process") if isinstance(payload.get("process"), dict) else {} - connector = payload.get("connector") if isinstance(payload.get("connector"), dict) else {} - connector_owner = connector.get("owner") if isinstance(connector.get("owner"), dict) else {} + connector = ( + payload.get("connector") if isinstance(payload.get("connector"), dict) else {} + ) + connector_owner = ( + connector.get("owner") if isinstance(connector.get("owner"), dict) else {} + ) expected_app_path = "/Applications/evaOS Workbench.app" expected_bridge_root = expected_app_path + "/Contents/Resources/Bridge" expected_argv0 = expected_bridge_root + "/src/evaos_desktop_bridge/cli.py" @@ -645,7 +739,9 @@ def evaluate_connector_candidate_identity( and candidate.get("app_build") == expected_build and candidate.get("app_bundle_id") == "com.evaos.workbench" and candidate.get("app_name") == "evaOS Workbench" - and _bundled_python_executable(process_executable, bridge_root=expected_bridge_root) + and _bundled_python_executable( + process_executable, bridge_root=expected_bridge_root + ) and _exact_absolute_path(process_argv0, expected_argv0) and connector_owner.get("label") == "com.electricsheep.evaos-desktop-bridge" and connector_owner.get("classification") == "workbench_bundle" @@ -653,7 +749,9 @@ def evaluate_connector_candidate_identity( and connector_owner.get("source_commit") == expected_source_commit and _typed_exact_path(connector_owner.get("program_path"), expected_argv0) and _typed_exact_path(connector_owner.get("app_path"), expected_app_path) - and _typed_exact_path(connector_owner.get("manifest_path"), expected_manifest_path) + and _typed_exact_path( + connector_owner.get("manifest_path"), expected_manifest_path + ) ) if result["ok"] is not True: result["reason"] = "connector_candidate_identity_mismatch" @@ -661,7 +759,11 @@ def evaluate_connector_candidate_identity( def _exact_absolute_path(value: object, expected: str) -> bool: - if not isinstance(value, str) or not value.startswith("/") or ".." in Path(value).parts: + if ( + not isinstance(value, str) + or not value.startswith("/") + or ".." in Path(value).parts + ): return False return posixpath.normpath(value) == value == expected @@ -675,12 +777,19 @@ def _typed_exact_path(value: object, expected: str) -> bool: def _bundled_python_executable(value: object, *, bridge_root: str) -> bool: - if not isinstance(value, str) or not value.startswith("/") or ".." in Path(value).parts: + if ( + not isinstance(value, str) + or not value.startswith("/") + or ".." in Path(value).parts + ): return False if posixpath.normpath(value) != value: return False parent, name = posixpath.split(value) - return parent == bridge_root + "/python/bin" and re.fullmatch(r"python3(?:\.\d+)?", name) is not None + return ( + parent == bridge_root + "/python/bin" + and re.fullmatch(r"python3(?:\.\d+)?", name) is not None + ) def connector_candidate_binding( @@ -705,7 +814,9 @@ def connector_candidate_binding( if not re.fullmatch(r"[0-9a-fA-F]{64}", expected_source_sha256): result["reason"] = "expected_source_sha256_invalid" return result - if not re.fullmatch(r"\d+\.\d+\.\d+", expected_version) or not re.fullmatch(r"\d+(?:\.\d+){0,2}", expected_build): + if not re.fullmatch(r"\d+\.\d+\.\d+", expected_version) or not re.fullmatch( + r"\d+(?:\.\d+){0,2}", expected_build + ): result["reason"] = "expected_candidate_version_invalid" return result request = urllib.request.Request( @@ -756,23 +867,120 @@ def selected_binding_proof_binding( if not isinstance(proof, dict) or set(proof) != SELECTED_BINDING_PROOF_FIELDS: result["reason"] = "selected_binding_proof_shape_invalid" return result + receipt_key_id = str( + os.environ.get("EVAOS_LIVE_CANARY_RECEIPT_KEY_ID") or "" + ).strip() + receipt_public_key_text = str( + os.environ.get("EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY") or "" + ).strip() + expected_context_key_id = str( + os.environ.get("EVAOS_LIVE_CANARY_CONTEXT_KEY_ID") or "" + ).strip() + try: + padding = "=" * (-len(str(proof.get("attestationBase64") or "")) % 4) + attestation_bytes = base64.urlsafe_b64decode( + str(proof.get("attestationBase64") or "") + padding + ) + attestation = json.loads(attestation_bytes.decode("utf-8")) + public_padding = "=" * (-len(receipt_public_key_text) % 4) + receipt_public_key = base64.urlsafe_b64decode( + receipt_public_key_text + public_padding + ) + except (ValueError, UnicodeError, json.JSONDecodeError): + result["reason"] = "selected_binding_proof_shape_invalid" + return result + canonical_attestation = json.dumps( + attestation, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + if ( + proof.get("schema") + != "evaos.mac_control.public_runtime_attestation_envelope.v1" + or proof.get("namespace") != "evaos-mac-control-public-attestation-v1" + or proof.get("keyId") != receipt_key_id + or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}", receipt_key_id) + or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}", expected_context_key_id) + or len(receipt_public_key) != 32 + or base64.urlsafe_b64encode(receipt_public_key).decode("ascii").rstrip("=") + != receipt_public_key_text + or base64.urlsafe_b64encode(attestation_bytes).decode("ascii").rstrip("=") + != proof.get("attestationBase64") + or canonical_attestation != attestation_bytes + or not isinstance(attestation, dict) + or set(attestation) != SELECTED_BINDING_ATTESTATION_FIELDS + ): + result["reason"] = "selected_binding_proof_shape_invalid" + return result candidate = ( - proof.get("candidate") if isinstance(proof.get("candidate"), dict) else {} + attestation.get("connectorCandidate") + if isinstance(attestation.get("connectorCandidate"), dict) + else {} ) - executed_at_text = proof.get("executedAt") + signature = str(proof.get("signature") or "") + signature_verified = False + if ( + signature.startswith("-----BEGIN SSH SIGNATURE-----\n") + and signature.endswith("-----END SSH SIGNATURE-----\n") + and len(signature) <= 8192 + ): + algorithm = b"ssh-ed25519" + public_blob = ( + struct.pack(">I", len(algorithm)) + + algorithm + + struct.pack(">I", len(receipt_public_key)) + + receipt_public_key + ) + with tempfile.TemporaryDirectory(prefix="evaos-proof-verify-") as directory: + allowed_path = Path(directory) / "allowed_signers" + signature_path = Path(directory) / "attestation.sshsig" + allowed_path.write_text( + "evaos ssh-ed25519 " + + base64.b64encode(public_blob).decode("ascii") + + "\n", + encoding="ascii", + ) + signature_path.write_text(signature, encoding="ascii") + os.chmod(allowed_path, 0o600) + os.chmod(signature_path, 0o600) + try: + verified = subprocess.run( + [ + "/usr/bin/ssh-keygen", + "-Y", + "verify", + "-f", + str(allowed_path), + "-I", + "evaos", + "-n", + "evaos-mac-control-public-attestation-v1", + "-s", + str(signature_path), + ], + input=attestation_bytes, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=4, + check=False, + env={"PATH": "/usr/bin:/bin", "LANG": "C", "LC_ALL": "C"}, + close_fds=True, + ) + signature_verified = verified.returncode == 0 + except (OSError, subprocess.TimeoutExpired): + signature_verified = False + executed_at_text = attestation.get("executedAt") try: executed_at = datetime.fromisoformat( str(executed_at_text or "").replace("Z", "+00:00") ) except ValueError: executed_at = None - expires_at = proof.get("expiresAt") + issued_at = attestation.get("authorityIssuedAt") + expires_at = attestation.get("authorityExpiresAt") result.update( { "schema": proof.get("schema"), - "runtime_receipt_verified": proof.get("schema") - == "evaos.mac_control.runtime_proof.v2", - "direct_mac_control_succeeded": proof.get("outcome") == "succeeded", + "runtime_receipt_verified": signature_verified, + "direct_mac_control_succeeded": attestation.get("outcome") == "succeeded", "candidate": { "source_commit": candidate.get("sourceCommit"), "source_sha256": candidate.get("sourceSha256"), @@ -782,30 +990,35 @@ def selected_binding_proof_binding( } ) result["ok"] = ( - proof.get("ok") is True - and proof.get("schema") == "evaos.mac_control.runtime_proof.v2" - and proof.get("proofKind") == "selected_binding_direct_mac_control" - and proof.get("tool") == "customer_mac.desktop_hotkey" - and proof.get("outcome") == "succeeded" + signature_verified + and attestation.get("schema") + == "evaos.mac_control.public_runtime_attestation.v1" + and attestation.get("keyId") == receipt_key_id + and attestation.get("namespace") == "evaos-mac-control-public-attestation-v1" + and attestation.get("proofKind") == "selected_binding_direct_mac_control" + and attestation.get("runtime") == "openclaw" + and attestation.get("tool") == "customer_mac.desktop_hotkey" + and attestation.get("outcome") == "succeeded" + and attestation.get("contextKeyId") == expected_context_key_id + and attestation.get("controlState") == "ready_unchanged" + and attestation.get("auditRecorded") is True + and re.fullmatch( + r"[0-9a-f]{64}", str(attestation.get("privateReceiptSha256") or "") + ) + is not None and re.fullmatch(r"\d+", expected_source_run_id) is not None and re.fullmatch( rf"gha:{re.escape(expected_source_run_id)}:[0-9a-f]{{24}}", - str(proof.get("runRef") or ""), + str(attestation.get("runRef") or ""), ) is not None and executed_at is not None and executed_at.tzinfo is not None + and type(issued_at) is int and type(expires_at) is int + and issued_at < expires_at <= issued_at + 60 + and executed_at.timestamp() >= issued_at - 5 and int(executed_at.timestamp()) <= expires_at - and re.fullmatch(r"[0-9a-f]{64}", str(proof.get("bindingRef") or "")) - is not None - and re.fullmatch(r"[1-9][0-9]{0,18}", str(proof.get("bindingVersion") or "")) - is not None - and re.fullmatch(r"[0-9a-f]{64}", str(proof.get("sessionRef") or "")) - is not None - and re.fullmatch(r"[0-9a-f]{64}", str(proof.get("auditRef") or "")) - is not None - and proof.get("sourcePointer") == "evaos-desktop-bridge:runtime-receipt" and set(candidate) == SELECTED_BINDING_CANDIDATE_FIELDS and candidate.get("sourceCommit") == expected_source_commit and candidate.get("sourceSha256") == expected_source_sha256 @@ -818,14 +1031,26 @@ def selected_binding_proof_binding( class OpenClawSurface: - def __init__(self, *, connector_url: str, token: str, artifact_dir: Path, repo_root: Path | None = None) -> None: + def __init__( + self, + *, + connector_url: str, + token: str, + artifact_dir: Path, + repo_root: Path | None = None, + ) -> None: self.connector_url = connector_url self.token = token self.artifact_dir = artifact_dir self.repo_root = repo_root or _resolve_repo_root() def run(self, command: str, params: dict[str, Any]) -> SurfaceResponse: - helper = _agent_tools_root(self.repo_root) / "openclaw-plugin" / "scripts" / "qa-run-bridge.mjs" + helper = ( + _agent_tools_root(self.repo_root) + / "openclaw-plugin" + / "scripts" + / "qa-run-bridge.mjs" + ) env = os.environ.copy() env.update( { @@ -851,7 +1076,14 @@ def run(self, command: str, params: dict[str, Any]) -> SurfaceResponse: class HermesSurface: - def __init__(self, *, connector_url: str, token: str, artifact_dir: Path, repo_root: Path | None = None) -> None: + def __init__( + self, + *, + connector_url: str, + token: str, + artifact_dir: Path, + repo_root: Path | None = None, + ) -> None: self.connector_url = connector_url self.token = token self.artifact_dir = artifact_dir @@ -866,7 +1098,12 @@ def run(self, command: str, params: dict[str, Any]) -> SurfaceResponse: "EVAOS_DESKTOP_BRIDGE_ARTIFACT_DIR": str(self.artifact_dir), } ) - adapter = _agent_tools_root(self.repo_root) / "hermes-adapter" / "bin" / "evaos-desktop-bridge-command" + adapter = ( + _agent_tools_root(self.repo_root) + / "hermes-adapter" + / "bin" + / "evaos-desktop-bridge-command" + ) params_json = json.dumps(params, separators=(",", ":")) completed = subprocess.run( [str(adapter), command, "-"], @@ -889,20 +1126,28 @@ def run_steps(steps: list[CanaryStep], surface: CanarySurface) -> list[CanaryRes for step in steps: missing_env = [key for key in step.env_required if not os.environ.get(key)] if missing_env: - result = _skipped_result(step, f"Missing local-only QA env: {', '.join(missing_env)}") + result = _skipped_result( + step, f"Missing local-only QA env: {', '.join(missing_env)}" + ) results.append(result) context[step.id] = result continue try: params = _resolve_params(step.params, context) except _UnresolvedPlaceholder as exc: - result = _skipped_result(step, str(exc)) if step.skip_if_unresolved else _failed_result(step, str(exc)) + result = ( + _skipped_result(step, str(exc)) + if step.skip_if_unresolved + else _failed_result(step, str(exc)) + ) results.append(result) context[step.id] = result continue dependency_error = _scenario_dependency_error(step, context) if dependency_error: - result = _failed_result(step, dependency_error, code="qa_required_visual_state_failed") + result = _failed_result( + step, dependency_error, code="qa_required_visual_state_failed" + ) results.append(result) context[step.id] = result continue @@ -924,7 +1169,9 @@ def run_steps(steps: list[CanaryStep], surface: CanarySurface) -> list[CanaryRes time.sleep(step.visual_retry_delay_seconds) previous_snapshot_id = response.snapshot_id response = surface.run(step.command, params) - status, errors, warnings = _classify_step_response(response, step, context) + status, errors, warnings = _classify_step_response( + response, step, context + ) warnings = [ f"Retried transient visual assertion mismatch ({retry_attempt}/{step.visual_assert_retries}); previous snapshot: {previous_snapshot_id or 'none'}." ] + warnings @@ -961,7 +1208,13 @@ def run_steps(steps: list[CanaryStep], surface: CanarySurface) -> list[CanaryRes snapshot_id=None, artifact_path=None, duration_ms=duration_ms, - errors=[{"code": "qa_step_exception", "message": str(exc), "guidance": "Inspect the QA canary report and connector logs."}], + errors=[ + { + "code": "qa_step_exception", + "message": str(exc), + "guidance": "Inspect the QA canary report and connector logs.", + } + ], warnings=[], payload={}, ) @@ -970,11 +1223,22 @@ def run_steps(steps: list[CanaryStep], surface: CanarySurface) -> list[CanaryRes return results -def classify_status(payload: dict[str, Any], *, skip_on_unavailable: bool = False, expect_error_code: str | None = None) -> str: +def classify_status( + payload: dict[str, Any], + *, + skip_on_unavailable: bool = False, + expect_error_code: str | None = None, +) -> str: errors = payload.get("errors") if isinstance(payload.get("errors"), list) else [] - error_codes = {str(error.get("code") or "") for error in errors if isinstance(error, dict)} + error_codes = { + str(error.get("code") or "") for error in errors if isinstance(error, dict) + } if expect_error_code: - return "passed" if payload.get("ok") is not True and expect_error_code in error_codes else "failed" + return ( + "passed" + if payload.get("ok") is not True and expect_error_code in error_codes + else "failed" + ) if payload.get("ok") is True: return "passed" if skip_on_unavailable and (error_codes & set(UNAVAILABLE_CODES)): @@ -982,11 +1246,21 @@ def classify_status(payload: dict[str, Any], *, skip_on_unavailable: bool = Fals return "failed" -def _classify_step_response(response: SurfaceResponse, step: CanaryStep, context: dict[str, CanaryResult]) -> tuple[str, list[dict[str, Any]], list[Any]]: - status = classify_status(response.payload, skip_on_unavailable=step.skip_on_unavailable, expect_error_code=step.expect_error_code) +def _classify_step_response( + response: SurfaceResponse, step: CanaryStep, context: dict[str, CanaryResult] +) -> tuple[str, list[dict[str, Any]], list[Any]]: + status = classify_status( + response.payload, + skip_on_unavailable=step.skip_on_unavailable, + expect_error_code=step.expect_error_code, + ) errors = list(response.errors) warnings = list(response.warnings) - if status == "passed" and step.requires_visual_evidence and (not response.snapshot_id or not response.artifact_path): + if ( + status == "passed" + and step.requires_visual_evidence + and (not response.snapshot_id or not response.artifact_path) + ): status = "failed" errors.append( { @@ -996,7 +1270,11 @@ def _classify_step_response(response: SurfaceResponse, step: CanaryStep, context } ) if status == "passed" and step.visual_assert: - assertion_errors = _visual_assertion_errors(response.payload, _resolve_params(step.visual_assert, context), artifact_path=response.artifact_path) + assertion_errors = _visual_assertion_errors( + response.payload, + _resolve_params(step.visual_assert, context), + artifact_path=response.artifact_path, + ) if assertion_errors: status = "failed" errors.extend(assertion_errors) @@ -1004,20 +1282,40 @@ def _classify_step_response(response: SurfaceResponse, step: CanaryStep, context def _has_visual_assertion_failure(errors: list[dict[str, Any]]) -> bool: - return any(error.get("code") == "qa_visual_assertion_failed" for error in errors if isinstance(error, dict)) + return any( + error.get("code") == "qa_visual_assertion_failed" + for error in errors + if isinstance(error, dict) + ) def timeout_for_command(command: str) -> int: """Per-primitive timeout in seconds. Scenario suites may run many primitives.""" if command == LOCAL_WORKBENCH_CONTROL_START: return 30 - if command in {"desktop_see", "iphone_see", "customer_mac_snapshot", "customer_mac_ax_tree"}: + if command in { + "desktop_see", + "iphone_see", + "customer_mac_snapshot", + "customer_mac_ax_tree", + }: return 60 if command in {"desktop_click", "iphone_tap"}: return 30 - if command in {"desktop_drag", "desktop_scroll", "iphone_swipe", "customer_mac_iphone_mirroring_scroll"}: + if command in { + "desktop_drag", + "desktop_scroll", + "iphone_swipe", + "customer_mac_iphone_mirroring_scroll", + }: return 20 - if command in {"desktop_menu", "desktop_window", "desktop_browser_action", "desktop_focus_app", "customer_mac_iphone_mirroring_open_app"}: + if command in { + "desktop_menu", + "desktop_window", + "desktop_browser_action", + "desktop_focus_app", + "customer_mac_iphone_mirroring_open_app", + }: return 20 if command in { "desktop_type", @@ -1037,7 +1335,13 @@ def build_scenarios(suite: str, *, allow_real_world_actions: bool) -> list[Canar if normalized == "iphone": normalized = "iphone_scenario" suites: dict[str, list[CanaryStep]] = { - "candidate": [CanaryStep(id="candidate.bridge_status", suite="candidate", command="desktop_bridge_status")], + "candidate": [ + CanaryStep( + id="candidate.bridge_status", + suite="candidate", + command="desktop_bridge_status", + ) + ], "control_start": _control_start_steps(), "readiness": _readiness_steps(), "codex": _codex_steps(), @@ -1050,7 +1354,15 @@ def build_scenarios(suite: str, *, allow_real_world_actions: bool) -> list[Canar "real_world_optional": _real_world_steps() if allow_real_world_actions else [], } if normalized == "all": - ordered = ["readiness", "codex", "full_access", "primitive", "desktop_scenario", "iphone_scenario", "ask_permission"] + ordered = [ + "readiness", + "codex", + "full_access", + "primitive", + "desktop_scenario", + "iphone_scenario", + "ask_permission", + ] if allow_real_world_actions: ordered.append("real_world_optional") return [step for name in ordered for step in suites[name]] @@ -1088,8 +1400,13 @@ def write_reports( } json_path = artifact_dir / "qa-report.json" markdown_path = artifact_dir / "qa-report.md" - json_path.write_text(json.dumps(redact_for_report(report), indent=2, sort_keys=True) + "\n", encoding="utf-8") - markdown_path.write_text(_markdown_report(redact_for_report(report)), encoding="utf-8") + json_path.write_text( + json.dumps(redact_for_report(report), indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + markdown_path.write_text( + _markdown_report(redact_for_report(report)), encoding="utf-8" + ) _sanitize_artifact_files(artifact_dir) return {"json": json_path, "markdown": markdown_path} @@ -1100,7 +1417,11 @@ def redact_for_report(value: Any) -> Any: redacted: dict[str, Any] = {} for key, nested in value.items(): lowered = str(key).lower() - if "token" in lowered or "secret" in lowered or lowered in {"authorization", "recipient_context", "text"}: + if ( + "token" in lowered + or "secret" in lowered + or lowered in {"authorization", "recipient_context", "text"} + ): redacted[key] = "[redacted]" elif lowered.endswith("url") and isinstance(nested, str): redacted[key] = redact_connector_url(nested) @@ -1157,19 +1478,47 @@ def _sanitize_artifact_files(artifact_dir: Path) -> None: def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="Run evaOS Workbench connector QA canaries.") + parser = argparse.ArgumentParser( + description="Run evaOS Workbench connector QA canaries." + ) parser.add_argument("--connector-url", required=True) parser.add_argument("--token-env", default="EVAOS_DESKTOP_BRIDGE_TOKEN") - parser.add_argument("--surface", choices=("connector", "openclaw", "hermes"), default="connector") + parser.add_argument( + "--surface", choices=("connector", "openclaw", "hermes"), default="connector" + ) parser.add_argument( "--suite", - choices=("candidate", "control_start", "readiness", "codex", "desktop", "iphone", "primitive", "desktop_scenario", "iphone_scenario", "full", "full_access", "ask_permission", "kill_switch", "real_world_optional", "all"), + choices=( + "candidate", + "control_start", + "readiness", + "codex", + "desktop", + "iphone", + "primitive", + "desktop_scenario", + "iphone_scenario", + "full", + "full_access", + "ask_permission", + "kill_switch", + "real_world_optional", + "all", + ), default="readiness", ) parser.add_argument("--artifact-dir", type=Path) parser.add_argument("--allow-real-world-actions", action="store_true") - parser.add_argument("--operator-ack-live-control", action="store_true", help="Required for suites that may move the mouse, keyboard, or iPhone Mirroring.") - parser.add_argument("--allow-skips", action="store_true", help="Exit 0 when required suites contain skipped rows; release certification should not use this.") + parser.add_argument( + "--operator-ack-live-control", + action="store_true", + help="Required for suites that may move the mouse, keyboard, or iPhone Mirroring.", + ) + parser.add_argument( + "--allow-skips", + action="store_true", + help="Exit 0 when required suites contain skipped rows; release certification should not use this.", + ) parser.add_argument( "--repo-root", type=Path, @@ -1193,29 +1542,45 @@ def main(argv: list[str] | None = None) -> int: token = os.environ.get(args.token_env) if not token: parser.error(f"{args.token_env} is required") - if _suite_requires_operator_ack(args.suite, allow_real_world_actions=args.allow_real_world_actions) and not args.operator_ack_live_control: + if ( + _suite_requires_operator_ack( + args.suite, allow_real_world_actions=args.allow_real_world_actions + ) + and not args.operator_ack_live_control + ): print( "Refusing to run live-control QA without operator acknowledgement. " "Re-run with --operator-ack-live-control when the Mac/iPhone can be controlled.", file=sys.stderr, ) return 2 - if _suite_requires_operator_ack(args.suite, allow_real_world_actions=args.allow_real_world_actions): + if _suite_requires_operator_ack( + args.suite, allow_real_world_actions=args.allow_real_world_actions + ): print( "evaOS QA live-control warning: this suite may move the mouse, type, click, scroll, or operate iPhone Mirroring.", file=sys.stderr, ) run_id = f"qa-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}-{uuid.uuid4().hex[:8]}" artifact_dir = args.artifact_dir or DEFAULT_RUN_ROOT / run_id - started_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + started_at = ( + datetime.now(timezone.utc) + .replace(microsecond=0) + .isoformat() + .replace("+00:00", "Z") + ) if args.source_commit_under_test: - local_candidate_binding = packaged_bridge_source_binding(args.source_commit_under_test, module_file=__file__) + local_candidate_binding = packaged_bridge_source_binding( + args.source_commit_under_test, module_file=__file__ + ) connector_binding = ( connector_candidate_binding( connector_url=args.connector_url, token=token, expected_source_commit=args.source_commit_under_test, - expected_source_sha256=str(local_candidate_binding.get("actual_source_sha256") or ""), + expected_source_sha256=str( + local_candidate_binding.get("actual_source_sha256") or "" + ), expected_version=args.version_under_test, expected_build=args.build_under_test, ) @@ -1231,7 +1596,9 @@ def main(argv: list[str] | None = None) -> int: args.selected_binding_proof, expected_source_commit=args.source_commit_under_test, expected_source_run_id=args.selected_binding_proof_run_id, - expected_source_sha256=str(local_candidate_binding.get("actual_source_sha256") or ""), + expected_source_sha256=str( + local_candidate_binding.get("actual_source_sha256") or "" + ), expected_version=args.version_under_test, expected_build=args.build_under_test, ) @@ -1242,7 +1609,9 @@ def main(argv: list[str] | None = None) -> int: "ok": ( local_candidate_binding.get("ok") is True and connector_binding.get("ok") is True - and (not selected_binding_required or selected_binding.get("ok") is True) + and ( + not selected_binding_required or selected_binding.get("ok") is True + ) ), "local": local_candidate_binding, "connector": connector_binding, @@ -1278,12 +1647,19 @@ def main(argv: list[str] | None = None) -> int: return 2 if args.repo_root: os.environ["EVAOS_DESKTOP_BRIDGE_QA_REPO_ROOT"] = str(args.repo_root) - remote_surface = _surface_for_name(args.surface, connector_url=args.connector_url, token=token, artifact_dir=artifact_dir) + remote_surface = _surface_for_name( + args.surface, + connector_url=args.connector_url, + token=token, + artifact_dir=artifact_dir, + ) surface = OperatorAcknowledgedLocalControlSurface( remote_surface, operator_ack_live_control=args.operator_ack_live_control, ) - steps = build_scenarios(args.suite, allow_real_world_actions=args.allow_real_world_actions) + steps = build_scenarios( + args.suite, allow_real_world_actions=args.allow_real_world_actions + ) results = run_steps(steps, surface) report_paths = write_reports( artifact_dir=artifact_dir, @@ -1298,17 +1674,37 @@ def main(argv: list[str] | None = None) -> int: results=results, ) ok = _run_successful(results, allow_skips=args.allow_skips) - print(json.dumps({"ok": ok, "run_id": run_id, "artifact_dir": str(artifact_dir), "reports": {key: str(path) for key, path in report_paths.items()}, "summary": _summary(results), "candidate_binding": candidate_binding}, sort_keys=True)) + print( + json.dumps( + { + "ok": ok, + "run_id": run_id, + "artifact_dir": str(artifact_dir), + "reports": {key: str(path) for key, path in report_paths.items()}, + "summary": _summary(results), + "candidate_binding": candidate_binding, + }, + sort_keys=True, + ) + ) return 0 if ok else 1 -def _surface_for_name(name: str, *, connector_url: str, token: str, artifact_dir: Path) -> CanarySurface: +def _surface_for_name( + name: str, *, connector_url: str, token: str, artifact_dir: Path +) -> CanarySurface: if name == "connector": - return ConnectorSurface(connector_url=connector_url, token=token, artifact_dir=artifact_dir) + return ConnectorSurface( + connector_url=connector_url, token=token, artifact_dir=artifact_dir + ) if name == "openclaw": - return OpenClawSurface(connector_url=connector_url, token=token, artifact_dir=artifact_dir) + return OpenClawSurface( + connector_url=connector_url, token=token, artifact_dir=artifact_dir + ) if name == "hermes": - return HermesSurface(connector_url=connector_url, token=token, artifact_dir=artifact_dir) + return HermesSurface( + connector_url=connector_url, token=token, artifact_dir=artifact_dir + ) raise ValueError(f"unknown surface: {name}") @@ -1335,18 +1731,48 @@ def _suite_requires_selected_binding_proof( def _readiness_steps() -> list[CanaryStep]: return [ - CanaryStep(id="readiness.bridge_status", suite="readiness", command="desktop_bridge_status"), - CanaryStep(id="readiness.customer_mac_status", suite="readiness", command="customer_mac_status"), - CanaryStep(id="readiness.customer_mac_capabilities", suite="readiness", command="customer_mac_capabilities"), - CanaryStep(id="readiness.control_status", suite="readiness", command="desktop_control_status"), - CanaryStep(id="readiness.audit_tail", suite="readiness", command="desktop_bridge_audit_tail", params={"limit": 20}), - CanaryStep(id="readiness.iphone_status", suite="readiness", command="customer_mac_iphone_mirroring_status", skip_on_unavailable=True), + CanaryStep( + id="readiness.bridge_status", + suite="readiness", + command="desktop_bridge_status", + ), + CanaryStep( + id="readiness.customer_mac_status", + suite="readiness", + command="customer_mac_status", + ), + CanaryStep( + id="readiness.customer_mac_capabilities", + suite="readiness", + command="customer_mac_capabilities", + ), + CanaryStep( + id="readiness.control_status", + suite="readiness", + command="desktop_control_status", + ), + CanaryStep( + id="readiness.audit_tail", + suite="readiness", + command="desktop_bridge_audit_tail", + params={"limit": 20}, + ), + CanaryStep( + id="readiness.iphone_status", + suite="readiness", + command="customer_mac_iphone_mirroring_status", + skip_on_unavailable=True, + ), ] def _control_start_steps() -> list[CanaryStep]: return [ - CanaryStep(id="control_start.bridge_status", suite="control_start", command="desktop_bridge_status"), + CanaryStep( + id="control_start.bridge_status", + suite="control_start", + command="desktop_bridge_status", + ), CanaryStep( id="control_start.full_access", suite="control_start", @@ -1359,112 +1785,510 @@ def _control_start_steps() -> list[CanaryStep]: command=LOCAL_WORKBENCH_CONTROL_START, params={"mode": "ask-permission", "agent_label": "evaOS RC Canary"}, ), - CanaryStep(id="control_start.stop", suite="control_start", command="desktop_control_stop"), - CanaryStep(id="control_start.kill_switch", suite="control_start", command="desktop_kill_switch"), + CanaryStep( + id="control_start.stop", + suite="control_start", + command="desktop_control_stop", + ), + CanaryStep( + id="control_start.kill_switch", + suite="control_start", + command="desktop_kill_switch", + ), ] def _codex_steps() -> list[CanaryStep]: return [ - CanaryStep(id="codex.frontmost", suite="codex", command="desktop_bridge_codex_frontmost", skip_on_unavailable=True), - CanaryStep(id="codex.windows", suite="codex", command="desktop_bridge_codex_windows", skip_on_unavailable=True), - CanaryStep(id="codex.threads", suite="codex", command="desktop_bridge_codex_threads", params={"max_items": 20}, skip_on_unavailable=True), - CanaryStep(id="codex.connections_status", suite="codex", command="desktop_bridge_codex_connections_status", skip_on_unavailable=True), - CanaryStep(id="codex.app_server_status", suite="codex", command="desktop_bridge_codex_app_server_status", skip_on_unavailable=True), - CanaryStep(id="codex.loaded_threads", suite="codex", command="desktop_bridge_codex_app_server_loaded_threads", params={"max_items": 20}, skip_on_unavailable=True), - CanaryStep(id="codex.remote_control_status", suite="codex", command="desktop_bridge_codex_app_server_remote_control_status", skip_on_unavailable=True), + CanaryStep( + id="codex.frontmost", + suite="codex", + command="desktop_bridge_codex_frontmost", + skip_on_unavailable=True, + ), + CanaryStep( + id="codex.windows", + suite="codex", + command="desktop_bridge_codex_windows", + skip_on_unavailable=True, + ), + CanaryStep( + id="codex.threads", + suite="codex", + command="desktop_bridge_codex_threads", + params={"max_items": 20}, + skip_on_unavailable=True, + ), + CanaryStep( + id="codex.connections_status", + suite="codex", + command="desktop_bridge_codex_connections_status", + skip_on_unavailable=True, + ), + CanaryStep( + id="codex.app_server_status", + suite="codex", + command="desktop_bridge_codex_app_server_status", + skip_on_unavailable=True, + ), + CanaryStep( + id="codex.loaded_threads", + suite="codex", + command="desktop_bridge_codex_app_server_loaded_threads", + params={"max_items": 20}, + skip_on_unavailable=True, + ), + CanaryStep( + id="codex.remote_control_status", + suite="codex", + command="desktop_bridge_codex_app_server_remote_control_status", + skip_on_unavailable=True, + ), ] def _full_access_steps() -> list[CanaryStep]: return [ - CanaryStep(id="full.start", suite="full_access", command=LOCAL_WORKBENCH_CONTROL_START, params={"mode": "full-access", "agent_label": "evaOS QA Canary"}), - CanaryStep(id="full.status", suite="full_access", command="desktop_control_status"), - CanaryStep(id="full.scroll_no_approval", suite="full_access", command="desktop_scroll", params={"direction": "down", "amount": 200, "dry_run": False}, delay_before_seconds=10.5), - CanaryStep(id="full.hotkey_no_approval", suite="full_access", command="desktop_hotkey", params={"keys": "escape", "dry_run": False}), + CanaryStep( + id="full.start", + suite="full_access", + command=LOCAL_WORKBENCH_CONTROL_START, + params={"mode": "full-access", "agent_label": "evaOS QA Canary"}, + ), + CanaryStep( + id="full.status", suite="full_access", command="desktop_control_status" + ), + CanaryStep( + id="full.scroll_no_approval", + suite="full_access", + command="desktop_scroll", + params={"direction": "down", "amount": 200, "dry_run": False}, + delay_before_seconds=10.5, + ), + CanaryStep( + id="full.hotkey_no_approval", + suite="full_access", + command="desktop_hotkey", + params={"keys": "escape", "dry_run": False}, + ), ] def _primitive_steps() -> list[CanaryStep]: return [ - CanaryStep(id="primitive.desktop_see", suite="primitive", lane="primitive", command="desktop_see", params={"max_chars": 4000, "max_nodes": 200}, requires_visual_evidence=True), + CanaryStep( + id="primitive.desktop_see", + suite="primitive", + lane="primitive", + command="desktop_see", + params={"max_chars": 4000, "max_nodes": 200}, + requires_visual_evidence=True, + ), CanaryStep( id="primitive.desktop_click_element", suite="primitive", lane="primitive", command="desktop_click", - params={"snapshot_id": "${primitive.desktop_see.snapshot_id}", "element_id": "${primitive.desktop_see.first_element_id}", "dry_run": False}, + params={ + "snapshot_id": "${primitive.desktop_see.snapshot_id}", + "element_id": "${primitive.desktop_see.first_element_id}", + "dry_run": False, + }, + skip_on_unavailable=True, + ), + CanaryStep( + id="primitive.desktop_click_coordinates", + suite="primitive", + lane="primitive", + command="desktop_click", + params={ + "snapshot_id": "${primitive.desktop_see.snapshot_id}", + "x": 700, + "y": 15, + "dry_run": False, + }, + ), + CanaryStep( + id="primitive.desktop_type", + suite="primitive", + lane="primitive", + command="desktop_type", + params={"text": "evaOS QA smoke", "dry_run": False}, + ), + CanaryStep( + id="primitive.desktop_scroll", + suite="primitive", + lane="primitive", + command="desktop_scroll", + params={"direction": "down", "amount": 400, "dry_run": False}, + ), + CanaryStep( + id="primitive.desktop_drag", + suite="primitive", + lane="primitive", + command="desktop_drag", + params={ + "from_x": 180, + "from_y": 180, + "to_x": 260, + "to_y": 260, + "dry_run": False, + }, + ), + CanaryStep( + id="primitive.desktop_hotkey", + suite="primitive", + lane="primitive", + command="desktop_hotkey", + params={"keys": "escape", "dry_run": False}, + ), + CanaryStep( + id="primitive.iphone_focus", + suite="primitive", + lane="primitive", + command="customer_mac_iphone_mirroring_focus", + params={"dry_run": False}, + skip_on_unavailable=True, + ), + CanaryStep( + id="primitive.iphone_open_calculator", + suite="primitive", + lane="primitive", + command="customer_mac_iphone_mirroring_open_app", + params={"app_name": "Calculator", "dry_run": False}, + skip_on_unavailable=True, + ), + CanaryStep( + id="primitive.iphone_see", + suite="primitive", + lane="primitive", + command="iphone_see", + params={"max_chars": 4000, "max_nodes": 200}, + skip_on_unavailable=True, + requires_visual_evidence=True, + ), + CanaryStep( + id="primitive.iphone_tap_coordinates", + suite="primitive", + lane="primitive", + command="iphone_tap", + params={ + "snapshot_id": "${primitive.iphone_see.snapshot_id}", + "x": 140, + "y": 140, + "dry_run": False, + }, + skip_on_unavailable=True, + ), + CanaryStep( + id="primitive.iphone_type", + suite="primitive", + lane="primitive", + command="iphone_type", + params={"text": "evaOS QA", "dry_run": False}, skip_on_unavailable=True, ), - CanaryStep(id="primitive.desktop_click_coordinates", suite="primitive", lane="primitive", command="desktop_click", params={"snapshot_id": "${primitive.desktop_see.snapshot_id}", "x": 700, "y": 15, "dry_run": False}), - CanaryStep(id="primitive.desktop_type", suite="primitive", lane="primitive", command="desktop_type", params={"text": "evaOS QA smoke", "dry_run": False}), - CanaryStep(id="primitive.desktop_scroll", suite="primitive", lane="primitive", command="desktop_scroll", params={"direction": "down", "amount": 400, "dry_run": False}), - CanaryStep(id="primitive.desktop_drag", suite="primitive", lane="primitive", command="desktop_drag", params={"from_x": 180, "from_y": 180, "to_x": 260, "to_y": 260, "dry_run": False}), - CanaryStep(id="primitive.desktop_hotkey", suite="primitive", lane="primitive", command="desktop_hotkey", params={"keys": "escape", "dry_run": False}), - CanaryStep(id="primitive.iphone_focus", suite="primitive", lane="primitive", command="customer_mac_iphone_mirroring_focus", params={"dry_run": False}, skip_on_unavailable=True), - CanaryStep(id="primitive.iphone_open_calculator", suite="primitive", lane="primitive", command="customer_mac_iphone_mirroring_open_app", params={"app_name": "Calculator", "dry_run": False}, skip_on_unavailable=True), - CanaryStep(id="primitive.iphone_see", suite="primitive", lane="primitive", command="iphone_see", params={"max_chars": 4000, "max_nodes": 200}, skip_on_unavailable=True, requires_visual_evidence=True), - CanaryStep(id="primitive.iphone_tap_coordinates", suite="primitive", lane="primitive", command="iphone_tap", params={"snapshot_id": "${primitive.iphone_see.snapshot_id}", "x": 140, "y": 140, "dry_run": False}, skip_on_unavailable=True), - CanaryStep(id="primitive.iphone_type", suite="primitive", lane="primitive", command="iphone_type", params={"text": "evaOS QA", "dry_run": False}, skip_on_unavailable=True), ] def _desktop_scenario_steps() -> list[CanaryStep]: return [ - CanaryStep(id="desktop_scenario.initial_see", suite="desktop_scenario", lane="scenario", command="desktop_see", params={"max_chars": 4000, "max_nodes": 200}, requires_visual_evidence=True), - CanaryStep(id="desktop_scenario.browser_open", suite="desktop_scenario", lane="scenario", command="desktop_browser_action", params={"action": "open_url", "url": "https://example.com", "dry_run": False}, assert_from_step="desktop_scenario.initial_see"), - CanaryStep(id="desktop_scenario.see_browser", suite="desktop_scenario", lane="scenario", command="desktop_see", params={"max_chars": 4000, "max_nodes": 200}, requires_visual_evidence=True, visual_assert={"expected_visible_text": "Example"}), - CanaryStep(id="desktop_scenario.escape", suite="desktop_scenario", lane="scenario", command="desktop_hotkey", params={"keys": "escape", "dry_run": False}, assert_from_step="desktop_scenario.see_browser"), - CanaryStep(id="desktop_scenario.menu_probe", suite="desktop_scenario", lane="scenario", command="desktop_menu", params={"menu_path": "Window", "dry_run": True}, skip_on_unavailable=True, assert_from_step="desktop_scenario.see_browser"), + CanaryStep( + id="desktop_scenario.initial_see", + suite="desktop_scenario", + lane="scenario", + command="desktop_see", + params={"max_chars": 4000, "max_nodes": 200}, + requires_visual_evidence=True, + ), + CanaryStep( + id="desktop_scenario.browser_open", + suite="desktop_scenario", + lane="scenario", + command="desktop_browser_action", + params={ + "action": "open_url", + "url": "https://example.com", + "dry_run": False, + }, + assert_from_step="desktop_scenario.initial_see", + ), + CanaryStep( + id="desktop_scenario.see_browser", + suite="desktop_scenario", + lane="scenario", + command="desktop_see", + params={"max_chars": 4000, "max_nodes": 200}, + requires_visual_evidence=True, + visual_assert={"expected_visible_text": "Example"}, + ), + CanaryStep( + id="desktop_scenario.escape", + suite="desktop_scenario", + lane="scenario", + command="desktop_hotkey", + params={"keys": "escape", "dry_run": False}, + assert_from_step="desktop_scenario.see_browser", + ), + CanaryStep( + id="desktop_scenario.menu_probe", + suite="desktop_scenario", + lane="scenario", + command="desktop_menu", + params={"menu_path": "Window", "dry_run": True}, + skip_on_unavailable=True, + assert_from_step="desktop_scenario.see_browser", + ), ] def _iphone_scenario_steps() -> list[CanaryStep]: return [ - CanaryStep(id="iphone_scenario.focus", suite="iphone_scenario", lane="scenario", command="customer_mac_iphone_mirroring_focus", params={"dry_run": False}, skip_on_unavailable=True), - CanaryStep(id="iphone_scenario.pre_open_state", suite="iphone_scenario", lane="scenario", command="iphone_see", params={"max_chars": 4000, "max_nodes": 200}, skip_on_unavailable=True, requires_visual_evidence=True), - CanaryStep(id="iphone_scenario.open_calculator", suite="iphone_scenario", lane="scenario", command="customer_mac_iphone_mirroring_open_app", params={"app_name": "Calculator", "dry_run": False}, skip_on_unavailable=True, assert_from_step="iphone_scenario.pre_open_state"), - CanaryStep(id="iphone_scenario.see_calculator", suite="iphone_scenario", lane="scenario", command="iphone_see", params={"max_chars": 4000, "max_nodes": 200}, skip_on_unavailable=True, requires_visual_evidence=True, visual_assert={"expected_visible_text": "Calculator", "expected_image_state": "iphone_calculator"}, visual_assert_retries=2, visual_retry_delay_seconds=1.0), - CanaryStep(id="iphone_scenario.calculator_entry", suite="iphone_scenario", lane="scenario", command="iphone_type", params={"text": "1+1+1=", "dry_run": False}, skip_on_unavailable=True, assert_from_step="iphone_scenario.see_calculator"), - CanaryStep(id="iphone_scenario.see_result", suite="iphone_scenario", lane="scenario", command="iphone_see", params={"max_chars": 4000, "max_nodes": 200}, skip_on_unavailable=True, requires_visual_evidence=True, visual_assert={"expected_image_state": "iphone_calculator", "allowed_states": ["Calculator", "3"]}, visual_assert_retries=2, visual_retry_delay_seconds=1.0), - CanaryStep(id="iphone_scenario.home", suite="iphone_scenario", lane="scenario", command="customer_mac_iphone_mirroring_home", params={"dry_run": False}, skip_on_unavailable=True, assert_from_step="iphone_scenario.see_result"), - CanaryStep(id="iphone_scenario.see_home", suite="iphone_scenario", lane="scenario", command="iphone_see", params={"max_chars": 4000, "max_nodes": 200}, skip_on_unavailable=True, requires_visual_evidence=True), - CanaryStep(id="iphone_scenario.spotlight", suite="iphone_scenario", lane="scenario", command="customer_mac_iphone_mirroring_spotlight", params={"dry_run": False}, skip_on_unavailable=True, assert_from_step="iphone_scenario.see_home"), - CanaryStep(id="iphone_scenario.app_switcher", suite="iphone_scenario", lane="scenario", command="customer_mac_iphone_mirroring_app_switcher", params={"dry_run": False}, skip_on_unavailable=True, assert_from_step="iphone_scenario.see_home"), + CanaryStep( + id="iphone_scenario.focus", + suite="iphone_scenario", + lane="scenario", + command="customer_mac_iphone_mirroring_focus", + params={"dry_run": False}, + skip_on_unavailable=True, + ), + CanaryStep( + id="iphone_scenario.pre_open_state", + suite="iphone_scenario", + lane="scenario", + command="iphone_see", + params={"max_chars": 4000, "max_nodes": 200}, + skip_on_unavailable=True, + requires_visual_evidence=True, + ), + CanaryStep( + id="iphone_scenario.open_calculator", + suite="iphone_scenario", + lane="scenario", + command="customer_mac_iphone_mirroring_open_app", + params={"app_name": "Calculator", "dry_run": False}, + skip_on_unavailable=True, + assert_from_step="iphone_scenario.pre_open_state", + ), + CanaryStep( + id="iphone_scenario.see_calculator", + suite="iphone_scenario", + lane="scenario", + command="iphone_see", + params={"max_chars": 4000, "max_nodes": 200}, + skip_on_unavailable=True, + requires_visual_evidence=True, + visual_assert={ + "expected_visible_text": "Calculator", + "expected_image_state": "iphone_calculator", + }, + visual_assert_retries=2, + visual_retry_delay_seconds=1.0, + ), + CanaryStep( + id="iphone_scenario.calculator_entry", + suite="iphone_scenario", + lane="scenario", + command="iphone_type", + params={"text": "1+1+1=", "dry_run": False}, + skip_on_unavailable=True, + assert_from_step="iphone_scenario.see_calculator", + ), + CanaryStep( + id="iphone_scenario.see_result", + suite="iphone_scenario", + lane="scenario", + command="iphone_see", + params={"max_chars": 4000, "max_nodes": 200}, + skip_on_unavailable=True, + requires_visual_evidence=True, + visual_assert={ + "expected_image_state": "iphone_calculator", + "allowed_states": ["Calculator", "3"], + }, + visual_assert_retries=2, + visual_retry_delay_seconds=1.0, + ), + CanaryStep( + id="iphone_scenario.home", + suite="iphone_scenario", + lane="scenario", + command="customer_mac_iphone_mirroring_home", + params={"dry_run": False}, + skip_on_unavailable=True, + assert_from_step="iphone_scenario.see_result", + ), + CanaryStep( + id="iphone_scenario.see_home", + suite="iphone_scenario", + lane="scenario", + command="iphone_see", + params={"max_chars": 4000, "max_nodes": 200}, + skip_on_unavailable=True, + requires_visual_evidence=True, + ), + CanaryStep( + id="iphone_scenario.spotlight", + suite="iphone_scenario", + lane="scenario", + command="customer_mac_iphone_mirroring_spotlight", + params={"dry_run": False}, + skip_on_unavailable=True, + assert_from_step="iphone_scenario.see_home", + ), + CanaryStep( + id="iphone_scenario.app_switcher", + suite="iphone_scenario", + lane="scenario", + command="customer_mac_iphone_mirroring_app_switcher", + params={"dry_run": False}, + skip_on_unavailable=True, + assert_from_step="iphone_scenario.see_home", + ), ] def _ask_permission_steps() -> list[CanaryStep]: return [ - CanaryStep(id="ask.start", suite="ask_permission", command=LOCAL_WORKBENCH_CONTROL_START, params={"mode": "ask-permission", "agent_label": "evaOS QA Canary"}), - CanaryStep(id="ask.high_impact_denied", suite="ask_permission", command="desktop_type", params={"text": "evaOS QA ask permission", "dry_run": False}, expect_error_code="approval_audit_required", delay_before_seconds=10.5), - CanaryStep(id="ask.high_impact_dry_run", suite="ask_permission", command="desktop_type", params={"text": "evaOS QA ask permission", "dry_run": True}), - CanaryStep(id="ask.high_impact_approved", suite="ask_permission", command="desktop_type", params={"text": "evaOS QA ask permission", "dry_run": False, "approval_audit_id": "${ask.high_impact_dry_run.audit_id}"}, skip_if_unresolved=True), + CanaryStep( + id="ask.start", + suite="ask_permission", + command=LOCAL_WORKBENCH_CONTROL_START, + params={"mode": "ask-permission", "agent_label": "evaOS QA Canary"}, + ), + CanaryStep( + id="ask.high_impact_denied", + suite="ask_permission", + command="desktop_type", + params={"text": "evaOS QA ask permission", "dry_run": False}, + expect_error_code="approval_audit_required", + delay_before_seconds=10.5, + ), + CanaryStep( + id="ask.high_impact_dry_run", + suite="ask_permission", + command="desktop_type", + params={"text": "evaOS QA ask permission", "dry_run": True}, + ), + CanaryStep( + id="ask.high_impact_approved", + suite="ask_permission", + command="desktop_type", + params={ + "text": "evaOS QA ask permission", + "dry_run": False, + "approval_audit_id": "${ask.high_impact_dry_run.audit_id}", + }, + skip_if_unresolved=True, + ), ] def _kill_switch_steps() -> list[CanaryStep]: return [ - CanaryStep(id="kill.activate", suite="kill_switch", command="desktop_kill_switch"), - CanaryStep(id="kill.status", suite="kill_switch", command="desktop_control_status"), - CanaryStep(id="kill.blocks_control", suite="kill_switch", command="desktop_scroll", params={"direction": "down", "amount": 100, "dry_run": False}, expect_error_code="control_kill_switch_active"), + CanaryStep( + id="kill.activate", suite="kill_switch", command="desktop_kill_switch" + ), + CanaryStep( + id="kill.status", suite="kill_switch", command="desktop_control_status" + ), + CanaryStep( + id="kill.blocks_control", + suite="kill_switch", + command="desktop_scroll", + params={"direction": "down", "amount": 100, "dry_run": False}, + expect_error_code="control_kill_switch_active", + ), ] def _real_world_steps() -> list[CanaryStep]: return [ - CanaryStep(id="real.bumble_open", suite="real_world_optional", lane="real_world", command="customer_mac_iphone_mirroring_open_app", params={"app_name": "Bumble", "dry_run": False}, env_required=("QA_BUMBLE_TEXT",), skip_on_unavailable=True), - CanaryStep(id="real.bumble_see", suite="real_world_optional", lane="real_world", command="iphone_see", params={"max_chars": 4000, "max_nodes": 200}, env_required=("QA_BUMBLE_TEXT",), skip_on_unavailable=True, requires_visual_evidence=True, visual_assert={"expected_visible_text": "Bumble"}), - CanaryStep(id="real.bumble_swipe_left", suite="real_world_optional", lane="real_world", command="iphone_swipe", params={"direction": "left", "dry_run": False}, env_required=("QA_BUMBLE_TEXT",), skip_on_unavailable=True, assert_from_step="real.bumble_see"), - CanaryStep(id="real.bumble_text", suite="real_world_optional", lane="real_world", command="iphone_type", params={"text": "${env.QA_BUMBLE_TEXT}", "dry_run": False}, env_required=("QA_BUMBLE_TEXT",), skip_on_unavailable=True, assert_from_step="real.bumble_see"), - CanaryStep(id="real.sms_text", suite="real_world_optional", lane="real_world", command="iphone_type", params={"text": "${env.QA_SMS_TEXT}", "dry_run": False}, env_required=("QA_SMS_CONTACT", "QA_SMS_TEXT"), skip_on_unavailable=True), - CanaryStep(id="real.social_open", suite="real_world_optional", lane="real_world", command="customer_mac_iphone_mirroring_open_app", params={"app_name": "${env.QA_SOCIAL_APP}", "dry_run": False}, env_required=("QA_SOCIAL_APP", "QA_SOCIAL_TEXT"), skip_on_unavailable=True), - CanaryStep(id="real.social_see", suite="real_world_optional", lane="real_world", command="iphone_see", params={"max_chars": 4000, "max_nodes": 200}, env_required=("QA_SOCIAL_APP", "QA_SOCIAL_TEXT"), skip_on_unavailable=True, requires_visual_evidence=True, visual_assert={"expected_visible_text": "${env.QA_SOCIAL_APP}"}), - CanaryStep(id="real.social_text", suite="real_world_optional", lane="real_world", command="iphone_type", params={"text": "${env.QA_SOCIAL_TEXT}", "dry_run": False}, env_required=("QA_SOCIAL_APP", "QA_SOCIAL_TEXT"), skip_on_unavailable=True, assert_from_step="real.social_see"), + CanaryStep( + id="real.bumble_open", + suite="real_world_optional", + lane="real_world", + command="customer_mac_iphone_mirroring_open_app", + params={"app_name": "Bumble", "dry_run": False}, + env_required=("QA_BUMBLE_TEXT",), + skip_on_unavailable=True, + ), + CanaryStep( + id="real.bumble_see", + suite="real_world_optional", + lane="real_world", + command="iphone_see", + params={"max_chars": 4000, "max_nodes": 200}, + env_required=("QA_BUMBLE_TEXT",), + skip_on_unavailable=True, + requires_visual_evidence=True, + visual_assert={"expected_visible_text": "Bumble"}, + ), + CanaryStep( + id="real.bumble_swipe_left", + suite="real_world_optional", + lane="real_world", + command="iphone_swipe", + params={"direction": "left", "dry_run": False}, + env_required=("QA_BUMBLE_TEXT",), + skip_on_unavailable=True, + assert_from_step="real.bumble_see", + ), + CanaryStep( + id="real.bumble_text", + suite="real_world_optional", + lane="real_world", + command="iphone_type", + params={"text": "${env.QA_BUMBLE_TEXT}", "dry_run": False}, + env_required=("QA_BUMBLE_TEXT",), + skip_on_unavailable=True, + assert_from_step="real.bumble_see", + ), + CanaryStep( + id="real.sms_text", + suite="real_world_optional", + lane="real_world", + command="iphone_type", + params={"text": "${env.QA_SMS_TEXT}", "dry_run": False}, + env_required=("QA_SMS_CONTACT", "QA_SMS_TEXT"), + skip_on_unavailable=True, + ), + CanaryStep( + id="real.social_open", + suite="real_world_optional", + lane="real_world", + command="customer_mac_iphone_mirroring_open_app", + params={"app_name": "${env.QA_SOCIAL_APP}", "dry_run": False}, + env_required=("QA_SOCIAL_APP", "QA_SOCIAL_TEXT"), + skip_on_unavailable=True, + ), + CanaryStep( + id="real.social_see", + suite="real_world_optional", + lane="real_world", + command="iphone_see", + params={"max_chars": 4000, "max_nodes": 200}, + env_required=("QA_SOCIAL_APP", "QA_SOCIAL_TEXT"), + skip_on_unavailable=True, + requires_visual_evidence=True, + visual_assert={"expected_visible_text": "${env.QA_SOCIAL_APP}"}, + ), + CanaryStep( + id="real.social_text", + suite="real_world_optional", + lane="real_world", + command="iphone_type", + params={"text": "${env.QA_SOCIAL_TEXT}", "dry_run": False}, + env_required=("QA_SOCIAL_APP", "QA_SOCIAL_TEXT"), + skip_on_unavailable=True, + assert_from_step="real.social_see", + ), ] -def _resolve_params(params: dict[str, Any], context: dict[str, CanaryResult]) -> dict[str, Any]: +def _resolve_params( + params: dict[str, Any], context: dict[str, CanaryResult] +) -> dict[str, Any]: return {key: _resolve_value(value, context) for key, value in params.items()} @@ -1473,7 +2297,11 @@ def _resolve_value(value: Any, context: dict[str, CanaryResult]) -> Any: return {key: _resolve_value(nested, context) for key, nested in value.items()} if isinstance(value, list): return [_resolve_value(item, context) for item in value] - if not isinstance(value, str) or not value.startswith("${") or not value.endswith("}"): + if ( + not isinstance(value, str) + or not value.startswith("${") + or not value.endswith("}") + ): return value expression = value[2:-1] if expression.startswith("env."): @@ -1527,8 +2355,13 @@ def _find_first_element_id(payload: dict[str, Any]) -> str | None: return None -def _scenario_dependency_error(step: CanaryStep, context: dict[str, CanaryResult]) -> str | None: - if step.lane not in {"scenario", "real_world"} or step.command not in ACTION_COMMANDS: +def _scenario_dependency_error( + step: CanaryStep, context: dict[str, CanaryResult] +) -> str | None: + if ( + step.lane not in {"scenario", "real_world"} + or step.command not in ACTION_COMMANDS + ): return None if not step.assert_from_step: return "Scenario live actions require assert_from_step so the harness acts from a verified visual state." @@ -1542,33 +2375,100 @@ def _scenario_dependency_error(step: CanaryStep, context: dict[str, CanaryResult return None -def _visual_assertion_errors(payload: dict[str, Any], assertion: dict[str, Any], *, artifact_path: str | None = None) -> list[dict[str, Any]]: +def _visual_assertion_errors( + payload: dict[str, Any], + assertion: dict[str, Any], + *, + artifact_path: str | None = None, +) -> list[dict[str, Any]]: data = payload.get("data") if isinstance(payload.get("data"), dict) else {} text = _visible_text(data).lower() - frontmost_app = str(data.get("frontmost_app") or data.get("app") or data.get("active_app") or "").lower() + frontmost_app = str( + data.get("frontmost_app") or data.get("app") or data.get("active_app") or "" + ).lower() image_states = _visual_artifact_states(artifact_path) errors: list[dict[str, Any]] = [] expected_app = assertion.get("expected_app") - if isinstance(expected_app, str) and expected_app.strip() and not _visual_state_matches(expected_app, text=text, frontmost_app=frontmost_app, image_states=image_states): - errors.append(_visual_assertion_error(f"Expected app/state containing '{expected_app}' but saw '{frontmost_app or text[:80]}'.")) + if ( + isinstance(expected_app, str) + and expected_app.strip() + and not _visual_state_matches( + expected_app, + text=text, + frontmost_app=frontmost_app, + image_states=image_states, + ) + ): + errors.append( + _visual_assertion_error( + f"Expected app/state containing '{expected_app}' but saw '{frontmost_app or text[:80]}'." + ) + ) expected_text = assertion.get("expected_visible_text") - if isinstance(expected_text, str) and expected_text.strip() and not _visual_state_matches(expected_text, text=text, frontmost_app=frontmost_app, image_states=image_states): - errors.append(_visual_assertion_error(f"Expected visible text '{expected_text}' was not found.")) + if ( + isinstance(expected_text, str) + and expected_text.strip() + and not _visual_state_matches( + expected_text, + text=text, + frontmost_app=frontmost_app, + image_states=image_states, + ) + ): + errors.append( + _visual_assertion_error( + f"Expected visible text '{expected_text}' was not found." + ) + ) expected_label = assertion.get("expected_label") - if isinstance(expected_label, str) and expected_label.strip() and not _visual_state_matches(expected_label, text=text, frontmost_app=frontmost_app, image_states=image_states): - errors.append(_visual_assertion_error(f"Expected visible label '{expected_label}' was not found.")) + if ( + isinstance(expected_label, str) + and expected_label.strip() + and not _visual_state_matches( + expected_label, + text=text, + frontmost_app=frontmost_app, + image_states=image_states, + ) + ): + errors.append( + _visual_assertion_error( + f"Expected visible label '{expected_label}' was not found." + ) + ) expected_image_state = assertion.get("expected_image_state") - if isinstance(expected_image_state, str) and expected_image_state.strip() and not _visual_image_state_matches(expected_image_state, image_states=image_states): - errors.append(_visual_assertion_error(f"Expected image state '{expected_image_state}' was not found in the materialized visual artifact.")) + if ( + isinstance(expected_image_state, str) + and expected_image_state.strip() + and not _visual_image_state_matches( + expected_image_state, image_states=image_states + ) + ): + errors.append( + _visual_assertion_error( + f"Expected image state '{expected_image_state}' was not found in the materialized visual artifact." + ) + ) allowed_states = assertion.get("allowed_states") if isinstance(allowed_states, list) and allowed_states: states = [str(state) for state in allowed_states if str(state).strip()] - if states and not any(_visual_state_matches(state, text=text, frontmost_app=frontmost_app, image_states=image_states) for state in states): - errors.append(_visual_assertion_error(f"None of the allowed states matched: {', '.join(str(state) for state in allowed_states)}.")) + if states and not any( + _visual_state_matches( + state, text=text, frontmost_app=frontmost_app, image_states=image_states + ) + for state in states + ): + errors.append( + _visual_assertion_error( + f"None of the allowed states matched: {', '.join(str(state) for state in allowed_states)}." + ) + ) return errors -def _visual_state_matches(expected: str, *, text: str, frontmost_app: str, image_states: set[str]) -> bool: +def _visual_state_matches( + expected: str, *, text: str, frontmost_app: str, image_states: set[str] +) -> bool: raw = expected.strip().lower() normalized = _normalize_visual_state(expected) normalized_text = _normalize_visual_state(text) @@ -1651,7 +2551,15 @@ def _image_color_ratios_from_png(artifact_path: str) -> dict[str, float] | None: if chunk_length != 13: return None try: - width, height, bit_depth, color_type, _compression, _filter, interlace = struct.unpack(">IIBBBBB", chunk_data) + ( + width, + height, + bit_depth, + color_type, + _compression, + _filter, + interlace, + ) = struct.unpack(">IIBBBBB", chunk_data) except struct.error: return None elif chunk_type == b"IDAT": @@ -1659,7 +2567,14 @@ def _image_color_ratios_from_png(artifact_path: str) -> dict[str, float] | None: elif chunk_type == b"IEND": break offset = chunk_data_end + 4 - if width <= 0 or height <= 0 or bit_depth != 8 or interlace != 0 or color_type not in {2, 6} or not idat: + if ( + width <= 0 + or height <= 0 + or bit_depth != 8 + or interlace != 0 + or color_type not in {2, 6} + or not idat + ): return None channels = 4 if color_type == 6 else 3 row_stride = width * channels @@ -1692,22 +2607,35 @@ def _image_color_ratios_from_png(artifact_path: str) -> dict[str, float] | None: return None for index in range(0, row_stride, channels): if pixel_index % sample_every == 0: - pixels.append((scanline[index], scanline[index + 1], scanline[index + 2])) + pixels.append( + (scanline[index], scanline[index + 1], scanline[index + 2]) + ) pixel_index += 1 previous = scanline return _color_ratios_from_pixels(pixels) -def _color_ratios_from_pixels(pixels: list[tuple[int, int, int]]) -> dict[str, float] | None: +def _color_ratios_from_pixels( + pixels: list[tuple[int, int, int]], +) -> dict[str, float] | None: total = len(pixels) if total == 0: return None - orange = sum(1 for red, green, blue in pixels if red > 190 and 95 < green < 190 and blue < 90 and red > green + 45) - dark = sum(1 for red, green, blue in pixels if red < 45 and green < 45 and blue < 45) + orange = sum( + 1 + for red, green, blue in pixels + if red > 190 and 95 < green < 190 and blue < 90 and red > green + 45 + ) + dark = sum( + 1 for red, green, blue in pixels if red < 45 and green < 45 and blue < 45 + ) gray = sum( 1 for red, green, blue in pixels - if 45 < red < 180 and 45 < green < 180 and 45 < blue < 180 and max(red, green, blue) - min(red, green, blue) < 35 + if 45 < red < 180 + and 45 < green < 180 + and 45 < blue < 180 + and max(red, green, blue) - min(red, green, blue) < 35 ) return {"orange": orange / total, "dark": dark / total, "gray": gray / total} @@ -1722,19 +2650,27 @@ def _png_unfilter_up(scanline: bytearray, previous: bytearray) -> None: scanline[index] = (scanline[index] + previous[index]) & 0xFF -def _png_unfilter_average(scanline: bytearray, previous: bytearray, bytes_per_pixel: int) -> None: +def _png_unfilter_average( + scanline: bytearray, previous: bytearray, bytes_per_pixel: int +) -> None: for index in range(len(scanline)): left = scanline[index - bytes_per_pixel] if index >= bytes_per_pixel else 0 up = previous[index] scanline[index] = (scanline[index] + ((left + up) // 2)) & 0xFF -def _png_unfilter_paeth(scanline: bytearray, previous: bytearray, bytes_per_pixel: int) -> None: +def _png_unfilter_paeth( + scanline: bytearray, previous: bytearray, bytes_per_pixel: int +) -> None: for index in range(len(scanline)): left = scanline[index - bytes_per_pixel] if index >= bytes_per_pixel else 0 up = previous[index] - upper_left = previous[index - bytes_per_pixel] if index >= bytes_per_pixel else 0 - scanline[index] = (scanline[index] + _paeth_predictor(left, up, upper_left)) & 0xFF + upper_left = ( + previous[index - bytes_per_pixel] if index >= bytes_per_pixel else 0 + ) + scanline[index] = ( + scanline[index] + _paeth_predictor(left, up, upper_left) + ) & 0xFF def _paeth_predictor(left: int, up: int, upper_left: int) -> int: @@ -1769,7 +2705,17 @@ def collect(value: Any) -> None: collect(item) return if isinstance(value, dict): - for key in ("label", "title", "text", "value", "name", "description", "frontmost_app", "app", "active_app"): + for key in ( + "label", + "title", + "text", + "value", + "name", + "description", + "frontmost_app", + "app", + "active_app", + ): nested = value.get(key) if isinstance(nested, str): chunks.append(nested) @@ -1805,7 +2751,9 @@ def _skipped_result(step: CanaryStep, message: str) -> CanaryResult: ) -def _failed_result(step: CanaryStep, message: str, *, code: str = "qa_placeholder_unresolved") -> CanaryResult: +def _failed_result( + step: CanaryStep, message: str, *, code: str = "qa_placeholder_unresolved" +) -> CanaryResult: return CanaryResult( id=step.id, suite=step.suite, @@ -1819,7 +2767,13 @@ def _failed_result(step: CanaryStep, message: str, *, code: str = "qa_placeholde snapshot_id=None, artifact_path=None, duration_ms=0, - errors=[{"code": code, "message": message, "guidance": "Inspect previous canary steps."}], + errors=[ + { + "code": code, + "message": message, + "guidance": "Inspect previous canary steps.", + } + ], warnings=[], payload={}, ) @@ -1839,7 +2793,10 @@ def _run_successful(results: list[CanaryResult], *, allow_skips: bool) -> bool: return False if allow_skips: return True - return not any(result.status == "skipped" and result.suite != "real_world_optional" for result in results) + return not any( + result.status == "skipped" and result.suite != "real_world_optional" + for result in results + ) def _markdown_report(report: dict[str, Any]) -> str: @@ -1875,7 +2832,9 @@ def _markdown_report(report: dict[str, Any]) -> str: return "\n".join(lines) -def _payload_from_completed_process(completed: subprocess.CompletedProcess[str], command: str) -> dict[str, Any]: +def _payload_from_completed_process( + completed: subprocess.CompletedProcess[str], command: str +) -> dict[str, Any]: stdout = (completed.stdout or "").strip() if stdout: try: @@ -1884,7 +2843,9 @@ def _payload_from_completed_process(completed: subprocess.CompletedProcess[str], return payload except json.JSONDecodeError: pass - message = (completed.stderr or stdout or f"adapter exited {completed.returncode}").strip() + message = ( + completed.stderr or stdout or f"adapter exited {completed.returncode}" + ).strip() return _error_payload(command=command, code="qa_adapter_failed", message=message) @@ -1900,11 +2861,20 @@ def _error_payload(*, command: str, code: str, message: str) -> dict[str, Any]: "schema_version": "2026-05-02.mvp1", "command": command, "target": "customer_mac", - "timestamp": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), + "timestamp": datetime.now(timezone.utc) + .replace(microsecond=0) + .isoformat() + .replace("+00:00", "Z"), "ok": False, "data": {}, "warnings": [], - "errors": [{"code": code, "message": message, "guidance": "Inspect the QA canary report and connector logs."}], + "errors": [ + { + "code": code, + "message": message, + "guidance": "Inspect the QA canary report and connector logs.", + } + ], "audit_id": "qa-adapter-failed", } @@ -1923,7 +2893,12 @@ def _extract_string(source: dict[str, Any], paths: tuple[str, ...]) -> str | Non def _safe_filename(value: str) -> str: - return "".join(char if char.isalnum() or char in "._-" else "-" for char in value)[:160] or "artifact" + return ( + "".join(char if char.isalnum() or char in "._-" else "-" for char in value)[ + :160 + ] + or "artifact" + ) def _resolve_repo_root() -> Path: @@ -1952,7 +2927,9 @@ def _secret_values() -> list[str]: values = [] for key, value in os.environ.items(): lowered = key.lower() - if value and ("token" in lowered or "secret" in lowered or key in REAL_WORLD_ENV_KEYS): + if value and ( + "token" in lowered or "secret" in lowered or key in REAL_WORLD_ENV_KEYS + ): values.append(value) return values diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py index 26fbd38871..2d131f05ca 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py @@ -22,6 +22,12 @@ RECEIPT_SCHEMA = "evaos.mac_control.runtime_receipt.v1" RECEIPT_ENVELOPE_SCHEMA = "evaos.mac_control.runtime_receipt_envelope.v1" RECEIPT_NAMESPACE = "evaos-mac-control-receipt-v1" +RECEIPT_BUNDLE_SCHEMA = "evaos.mac_control.runtime_receipt_bundle.v2" +PUBLIC_ATTESTATION_SCHEMA = "evaos.mac_control.public_runtime_attestation.v1" +PUBLIC_ATTESTATION_ENVELOPE_SCHEMA = ( + "evaos.mac_control.public_runtime_attestation_envelope.v1" +) +PUBLIC_ATTESTATION_NAMESPACE = "evaos-mac-control-public-attestation-v1" NATIVE_VERIFIER_NAME = "evaos-ed25519-verify" REPLAY_FILE = "mac-control-canary-replay.jsonl" MAX_CONTEXT_BYTES = 64 * 1024 @@ -462,14 +468,15 @@ def build_receipt( } -def sign_receipt( - receipt: dict[str, Any], +def sign_payload( + payload: dict[str, Any], config: CanaryConfig, *, + namespace: str, run_process: Callable[..., subprocess.CompletedProcess[bytes]] = subprocess.run, ) -> str: key_path, before = validate_receipt_signer_key(config) - message = _canonical_json(receipt) + message = _canonical_json(payload) try: result = run_process( [ @@ -480,7 +487,7 @@ def sign_receipt( "-f", str(key_path), "-n", - RECEIPT_NAMESPACE, + namespace, "-", ], input=message, @@ -515,6 +522,66 @@ def sign_receipt( return signature +def sign_receipt( + receipt: dict[str, Any], + config: CanaryConfig, + *, + run_process: Callable[..., subprocess.CompletedProcess[bytes]] = subprocess.run, +) -> str: + return sign_payload( + receipt, + config, + namespace=RECEIPT_NAMESPACE, + run_process=run_process, + ) + + +def build_public_attestation( + receipt: dict[str, Any], config: CanaryConfig +) -> dict[str, Any]: + candidate = receipt.get("candidate") + if not isinstance(candidate, dict): + raise CanaryError("receipt_public_attestation_invalid", status=503) + private_receipt = _canonical_json(receipt) + return { + "schema": PUBLIC_ATTESTATION_SCHEMA, + "keyId": config.receipt_key_id, + "namespace": PUBLIC_ATTESTATION_NAMESPACE, + "proofKind": "selected_binding_direct_mac_control", + "runtime": "openclaw", + "tool": "customer_mac.desktop_hotkey", + "outcome": "succeeded", + "runRef": receipt.get("runRef"), + "executedAt": receipt.get("executedAt"), + "authorityIssuedAt": receipt.get("contextIssuedAt"), + "authorityExpiresAt": receipt.get("contextExpiresAt"), + "contextKeyId": receipt.get("contextKeyId"), + "controlState": "ready_unchanged", + "auditRecorded": True, + "privateReceiptSha256": hashlib.sha256(private_receipt).hexdigest(), + "connectorCandidate": { + "sourceCommit": candidate.get("sourceCommit"), + "sourceSha256": candidate.get("sourceSha256"), + "appVersion": candidate.get("appVersion"), + "appBuild": candidate.get("appBuild"), + }, + } + + +def sign_public_attestation( + attestation: dict[str, Any], + config: CanaryConfig, + *, + run_process: Callable[..., subprocess.CompletedProcess[bytes]] = subprocess.run, +) -> str: + return sign_payload( + attestation, + config, + namespace=PUBLIC_ATTESTATION_NAMESPACE, + run_process=run_process, + ) + + def validate_receipt_signer_key(config: CanaryConfig) -> tuple[Path, os.stat_result]: key_path = config.receipt_private_key if not key_path.is_absolute(): @@ -550,6 +617,35 @@ def receipt_envelope( } +def public_attestation_envelope( + attestation: dict[str, Any], sshsig: str, config: CanaryConfig +) -> dict[str, Any]: + return { + "schema": PUBLIC_ATTESTATION_ENVELOPE_SCHEMA, + "attestationBase64": _base64url_encode(_canonical_json(attestation)), + "signature": sshsig, + "keyId": config.receipt_key_id, + "namespace": PUBLIC_ATTESTATION_NAMESPACE, + } + + +def receipt_bundle( + *, + receipt: dict[str, Any], + receipt_signature: str, + public_attestation: dict[str, Any], + public_signature: str, + config: CanaryConfig, +) -> dict[str, Any]: + return { + "schema": RECEIPT_BUNDLE_SCHEMA, + "privateReceipt": receipt_envelope(receipt, receipt_signature, config), + "publicAttestation": public_attestation_envelope( + public_attestation, public_signature, config + ), + } + + def candidate_snapshot( candidate: dict[str, Any], *, owner: dict[str, Any] | None, process: dict[str, Any] ) -> dict[str, Any]: diff --git a/scripts/evaosBetaReleaseGate.js b/scripts/evaosBetaReleaseGate.js index 3b13a38f14..6e781c2fd1 100644 --- a/scripts/evaosBetaReleaseGate.js +++ b/scripts/evaosBetaReleaseGate.js @@ -6,13 +6,17 @@ const path = require('path'); const { createHash } = require('crypto'); const { execFileSync } = require('child_process'); const { bridgeWrapperScript } = require('./prepareEvaosDesktopBridgeResource'); +const { + PUBLIC_ATTESTATION_ENVELOPE_FIELDS, + verifyMacControlPublicAttestation, +} = require('./evaosMacControlSignedProof'); const PROJECT_ROOT = path.resolve(__dirname, '..'); const WORKBENCH_BRIDGE_SOURCE_DIR = 'resources/evaos-beta/bridge/src/evaos_desktop_bridge'; const committedBridgeSourceIdentityCache = new Map(); const TRUTHY_VALUES = new Set(['1', 'true', 'yes', 'on', 'evaos-beta']); -const LIVE_CANARY_VERIFIER_SHA256 = '692d88c72217b44f7957d78228748991ff65a12afda253c03b365a30b63e6127'; +const LIVE_CANARY_VERIFIER_SHA256 = '701828332e3c35497294359980944e7021064384a6a0304157af7885897462bd'; const FUNCTIONAL_SMOKE_SHAPE_RUN_SHA256 = '7d3bc23e52e3e342782b2903664572b15754782db4e67155cdb869c4c8d93d3b'; const REQUIRED_PUBLIC_BETA_CODE_SIGNING_ENV = [ @@ -93,6 +97,7 @@ const BROKER_LIVE_CANARY_PROOF_NAME = 'broker-runtime-status.json'; const BUSINESS_BROWSER_LIVE_CANARY_PROOF_NAME = 'business-browser.json'; const MAC_CONTROL_LIVE_CANARY_PROOF_NAME = 'mac-control-runtime.json'; const MAC_CONTROL_NEGATIVE_PROOF_NAME = 'mac-control-runtime-negative.json'; +const MAC_CONTROL_DEPLOYED_PROBE_NAME = 'mac-control-deployed-route.json'; const MAC_CONTROL_PROVISION_PROOF_NAME = 'mac-control-session-provisioning.json'; const MAC_CONTROL_CLEANUP_PROOF_NAME = 'mac-control-session-cleanup.json'; const RELEASE_ASSET_EXTS = new Set(['.exe', '.msi', '.dmg', '.deb', '.zip', '.yml']); @@ -280,22 +285,7 @@ const LIVE_CANARY_SECRET_VALUE_PATTERNS = [ /\bBearer\s+[A-Za-z0-9._-]{8,}\b/i, /[?&#](?:access[_-]?token|refresh[_-]?token|desktop[_-]?session|provider[_-]?grant|grant[_-]?handle|api[_-]?key|service[_-]?role|token|secret|password|credential)=/i, ]; -const MAC_CONTROL_RUNTIME_PROOF_FIELDS = Object.freeze([ - 'ok', - 'schema', - 'proofKind', - 'tool', - 'outcome', - 'runRef', - 'executedAt', - 'bindingRef', - 'bindingVersion', - 'sessionRef', - 'expiresAt', - 'auditRef', - 'sourcePointer', - 'candidate', -]); +const MAC_CONTROL_RUNTIME_PROOF_FIELDS = PUBLIC_ATTESTATION_ENVELOPE_FIELDS; function normalizeBoolean(value) { return TRUTHY_VALUES.has( @@ -889,6 +879,9 @@ function collectReleaseDistributeWorkflowIssues(workflow) { 'EVAOS_LIVE_CANARY_MAX_PROOF_AGE_HOURS', 'EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA', 'EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID', + 'EVAOS_LIVE_CANARY_CONTEXT_KEY_ID', + 'EVAOS_LIVE_CANARY_RECEIPT_KEY_ID', + 'EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY', ]; if ( JSON.stringify(getWorkflowStepPropertyNames(steps[0])) !== JSON.stringify(['name', 'shell', 'env', 'run']) || @@ -910,6 +903,9 @@ function collectReleaseDistributeWorkflowIssues(workflow) { ['EVAOS_LIVE_CANARY_MAX_PROOF_AGE_HOURS', '24'], ['EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA', '${{ steps.provenance.outputs.tag_commit }}'], ['EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID', '${{ github.event.inputs.live_canary_proof_run_id }}'], + ['EVAOS_LIVE_CANARY_CONTEXT_KEY_ID', '${{ vars.EVAOS_MAC_CONTROL_CONTEXT_KEY_ID }}'], + ['EVAOS_LIVE_CANARY_RECEIPT_KEY_ID', '${{ vars.EVAOS_MAC_CONTROL_RECEIPT_KEY_ID }}'], + ['EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY', '${{ vars.EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY }}'], ]) { const values = getWorkflowStepEnvValues(steps[0], key); if (values.length !== 1 || values[0] !== expectedValue) { @@ -1032,6 +1028,7 @@ function runLiveCanaryVerifierBehaviorProbe(verifierPath, mode, bashPath) { ' mkdir -p "$output_dir/packet"', ' : > "$output_dir/packet/broker-runtime-status.json"', ' : > "$output_dir/packet/mac-control-runtime.json"', + ' : > "$output_dir/packet/mac-control-deployed-route.json"', ` printf '%s\\n' 'Run live canaries: true' 'Run follow-up canaries: none' 'Run Mac-control canary: true' > "$output_dir/packet/proof-run.md"`, ' exit 0', 'fi', @@ -3286,36 +3283,56 @@ function verifyMacControlLiveCanaryProof(proofDir, env = process.env, verificati assertLiveCanaryNoSecretMaterial(proof, 'macControl'); if (Object.keys(proof).length !== allowedTopLevelFields.size) { - throw new Error('Mac-control live canary proof is missing required runtime-receipt fields.'); - } - if (proof.schema !== 'evaos.mac_control.runtime_proof.v2') { - throw new Error(`Unexpected Mac-control live canary proof schema: ${proof.schema}`); - } - if ( - proof.ok !== true || - proof.proofKind !== 'selected_binding_direct_mac_control' || - proof.tool !== 'customer_mac.desktop_hotkey' || - proof.outcome !== 'succeeded' || - proof.sourcePointer !== 'evaos-desktop-bridge:runtime-receipt' - ) { - throw new Error('Mac-control live canary release gate requires a successful direct-control runtime receipt.'); + throw new Error('Mac-control live canary proof is missing required signed-attestation fields.'); } const expectedHeadSha = String(env.EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA || '').trim(); const expectedRunId = String(env.EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID || '').trim(); + const receiptKeyId = String(env.EVAOS_LIVE_CANARY_RECEIPT_KEY_ID || '').trim(); + const receiptPublicKey = String(env.EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY || '').trim(); + const expectedContextKeyId = String(env.EVAOS_LIVE_CANARY_CONTEXT_KEY_ID || '').trim(); if (!/^[0-9a-f]{40}$/i.test(expectedHeadSha)) { throw new Error('Mac-control live canary proof requires EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA.'); } if (!/^\d+$/.test(expectedRunId)) { throw new Error('Mac-control live canary proof requires EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID.'); } - if (!new RegExp(`^gha:${expectedRunId}:[0-9a-f]{24}$`).test(String(proof.runRef || ''))) { + if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(expectedContextKeyId)) { + throw new Error('Mac-control live canary proof requires EVAOS_LIVE_CANARY_CONTEXT_KEY_ID.'); + } + let attestation; + try { + attestation = verifyMacControlPublicAttestation(proof, { + keyId: receiptKeyId, + publicKey: receiptPublicKey, + }); + } catch { + throw new Error('Mac-control live canary public attestation signature is invalid.'); + } + assertLiveCanaryNoSecretMaterial(attestation, 'macControlAttestation'); + if ( + attestation.proofKind !== 'selected_binding_direct_mac_control' || + attestation.runtime !== 'openclaw' || + attestation.tool !== 'customer_mac.desktop_hotkey' || + attestation.outcome !== 'succeeded' || + attestation.controlState !== 'ready_unchanged' || + attestation.auditRecorded !== true || + attestation.contextKeyId !== expectedContextKeyId + ) { + throw new Error('Mac-control live canary release gate requires a successful signed direct-control attestation.'); + } + if (!new RegExp(`^gha:${expectedRunId}:[0-9a-f]{24}$`).test(String(attestation.runRef || ''))) { throw new Error('Mac-control live canary proof run does not match the selected proof run.'); } - const executedAtText = String(proof.executedAt || ''); + const executedAtText = String(attestation.executedAt || ''); const executedAt = Date.parse(executedAtText); - const expiresAtMs = Number.isSafeInteger(proof.expiresAt) ? proof.expiresAt * 1000 : Number.NaN; + const authorityIssuedAtMs = Number.isSafeInteger(attestation.authorityIssuedAt) + ? attestation.authorityIssuedAt * 1000 + : Number.NaN; + const authorityExpiresAtMs = Number.isSafeInteger(attestation.authorityExpiresAt) + ? attestation.authorityExpiresAt * 1000 + : Number.NaN; const maxAgeRaw = String(env.EVAOS_LIVE_CANARY_MAX_PROOF_AGE_HOURS || '24').trim(); const configuredMaxAgeHours = Number.parseFloat(maxAgeRaw); const maxAgeHours = @@ -3331,34 +3348,34 @@ function verifyMacControlLiveCanaryProof(proofDir, env = process.env, verificati if ( !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(executedAtText) || !Number.isFinite(executedAt) || - !Number.isFinite(expiresAtMs) || + !Number.isFinite(authorityIssuedAtMs) || + !Number.isFinite(authorityExpiresAtMs) || receiptAgeMs < -5_000 || receiptAgeMs > maxAgeHours * 60 * 60 * 1000 || - executedAt > expiresAtMs || - expiresAtMs - executedAt > 66_000 || - !/^[0-9a-f]{64}$/.test(String(proof.bindingRef || '')) || - !/^[1-9][0-9]{0,18}$/.test(String(proof.bindingVersion || '')) || - !/^[0-9a-f]{64}$/.test(String(proof.sessionRef || '')) || - !/^[0-9a-f]{64}$/.test(String(proof.auditRef || '')) + authorityExpiresAtMs <= authorityIssuedAtMs || + authorityExpiresAtMs - authorityIssuedAtMs > 60_000 || + executedAt < authorityIssuedAtMs - 5_000 || + executedAt > authorityExpiresAtMs || + !/^[0-9a-f]{64}$/.test(String(attestation.privateReceiptSha256 || '')) ) { - throw new Error('Mac-control live canary runtime receipt fields are invalid.'); + throw new Error('Mac-control live canary signed attestation fields are invalid.'); } - assertLiveCanaryPlainObject(proof.candidate, 'Mac-control live canary candidate'); + assertLiveCanaryPlainObject(attestation.connectorCandidate, 'Mac-control live canary candidate'); const candidateFields = ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild']; if ( - Object.keys(proof.candidate).length !== candidateFields.length || - Object.keys(proof.candidate).some((field) => !candidateFields.includes(field)) + Object.keys(attestation.connectorCandidate).length !== candidateFields.length || + Object.keys(attestation.connectorCandidate).some((field) => !candidateFields.includes(field)) ) { throw new Error('Mac-control live canary candidate fields do not match the required release contract.'); } const expectedSourceSha256 = committedBridgeSourceIdentity(expectedHeadSha).sourceSha256; const expectedVersion = packageVersionAtCommit(expectedHeadSha); if ( - proof.candidate.sourceCommit !== expectedHeadSha || - proof.candidate.sourceSha256 !== expectedSourceSha256 || - proof.candidate.appVersion !== expectedVersion || - proof.candidate.appBuild !== expectedVersion + attestation.connectorCandidate.sourceCommit !== expectedHeadSha || + attestation.connectorCandidate.sourceSha256 !== expectedSourceSha256 || + attestation.connectorCandidate.appVersion !== expectedVersion || + attestation.connectorCandidate.appBuild !== expectedVersion ) { throw new Error('Mac-control live canary candidate does not match the exact release commit.'); } @@ -3391,6 +3408,45 @@ function verifyMacControlLiveCanaryProof(proofDir, env = process.env, verificati throw new Error('Mac-control runtime-receipt negative assertions are incomplete.'); } + const deployedProbePath = requireExistingRelativeFile( + proofDir, + MAC_CONTROL_DEPLOYED_PROBE_NAME, + 'Mac-control deployed route probe' + ); + const deployedProbe = readManifestFile(deployedProbePath); + assertLiveCanaryPlainObject(deployedProbe, 'Mac-control deployed route probe'); + assertLiveCanaryNoSecretMaterial(deployedProbe, 'macControlDeployedRoute'); + const deployedProbeFields = ['schema', 'sourceHeadSha', 'sourceRunId', 'checkedAt', 'assertions']; + if ( + Object.keys(deployedProbe).length !== deployedProbeFields.length || + Object.keys(deployedProbe).some((field) => !deployedProbeFields.includes(field)) || + deployedProbe.schema !== 'evaos.mac_control.deployed_route_probe.v1' || + deployedProbe.sourceHeadSha !== expectedHeadSha || + String(deployedProbe.sourceRunId || '') !== expectedRunId + ) { + throw new Error('Mac-control deployed route probe does not match the exact release run.'); + } + assertLiveCanaryFresh(deployedProbe.checkedAt, 'macControlDeployedRoute.checkedAt', { + now: new Date(verificationNow), + maxAgeHours, + }); + assertLiveCanaryPlainObject(deployedProbe.assertions, 'Mac-control deployed route assertions'); + const deployedAssertions = [ + 'gatewayAuthRequired', + 'postOnly', + 'exactMatch', + 'strictBody', + 'callerAuthorityBodyRejected', + 'sensitiveOutputAbsent', + ]; + if ( + Object.keys(deployedProbe.assertions).length !== deployedAssertions.length || + Object.keys(deployedProbe.assertions).some((field) => !deployedAssertions.includes(field)) || + deployedAssertions.some((field) => deployedProbe.assertions[field] !== true) + ) { + throw new Error('Mac-control deployed route assertions are incomplete.'); + } + const provisionPath = requireExistingRelativeFile( proofDir, MAC_CONTROL_PROVISION_PROOF_NAME, diff --git a/scripts/evaosBrokerLiveCanary.js b/scripts/evaosBrokerLiveCanary.js index b7ce18bddf..a6326e3779 100644 --- a/scripts/evaosBrokerLiveCanary.js +++ b/scripts/evaosBrokerLiveCanary.js @@ -1,8 +1,10 @@ #!/usr/bin/env node -const { createHash, randomBytes } = require('node:crypto'); +const { randomBytes } = require('node:crypto'); +const fs = require('node:fs'); const { committedBridgeSourceIdentity } = require('./evaosBetaReleaseGate.js'); +const { verifyMacControlPublicAttestation } = require('./evaosMacControlSignedProof.js'); const { version: WORKBENCH_VERSION } = require('../package.json'); const DEFAULT_ENDPOINT = 'https://rhfojelkgtwcxnrfhtlj.supabase.co/functions/v1/desktop-runtime-session'; @@ -164,6 +166,9 @@ function resolveMacControlCanaryConfig(env) { 'AIONUI_EVAOS_MAC_CONTROL_CANARY_CUSTOMER_ID', 'AIONUI_EVAOS_MAC_CONTROL_CANARY_ENDPOINT', 'AIONUI_EVAOS_MAC_CONTROL_CANARY_EXPECTED_CALLBACK_HOST', + 'EVAOS_LIVE_CANARY_RECEIPT_KEY_ID', + 'EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY', + 'EVAOS_LIVE_CANARY_CONTEXT_KEY_ID', ]; const missing = names.filter((name) => !envText(env, name)); if (missing.length > 0) { @@ -204,12 +209,28 @@ function resolveMacControlCanaryConfig(env) { const expectedCallbackHost = new URL( `https://${callbackHostname}${callbackPort ? `:${callbackPort}` : ''}` ).host.toLowerCase(); + const receiptKeyId = envText(env, 'EVAOS_LIVE_CANARY_RECEIPT_KEY_ID'); + const receiptPublicKey = envText(env, 'EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY'); + const contextKeyId = envText(env, 'EVAOS_LIVE_CANARY_CONTEXT_KEY_ID'); + const decodedReceiptKey = Buffer.from(receiptPublicKey, 'base64url'); + if ( + !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(receiptKeyId) || + !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(contextKeyId) || + !/^[A-Za-z0-9_-]+$/.test(receiptPublicKey) || + decodedReceiptKey.byteLength !== 32 || + decodedReceiptKey.toString('base64url') !== receiptPublicKey + ) { + throw new Error('Dedicated Mac-control canary trust anchors are invalid.'); + } return { desktopSession: envText(env, 'AIONUI_EVAOS_MAC_CONTROL_CANARY_DESKTOP_SESSION'), customerId, endpoint: endpointUrl.toString(), expectedCallbackHost, + receiptKeyId, + receiptPublicKey, + contextKeyId, }; } @@ -895,82 +916,58 @@ function expectedMacControlCandidate(env) { }; } -function macControlSaltedReference(challenge, value) { - return createHash('sha256') - .update(Buffer.from(challenge, 'ascii')) - .update(Buffer.from([0])) - .update(Buffer.from(value, 'utf8')) - .digest('hex'); -} - function sanitizeMacControlRuntimeProof( raw, - { - challenge, - runRef, - expectedCandidate, - selectedBindingId, - selectedBindingVersion, - bindingExpiry, - requestStartedAt, - now, - } + { runRef, expectedCandidate, bindingExpiry, requestStartedAt, now, receiptKeyId, receiptPublicKey, contextKeyId } ) { - const proof = asPlainRecord(raw); - const candidate = asPlainRecord(proof?.candidate); - const exactProofFields = new Set([ - 'ok', - 'schema', - 'proofKind', - 'tool', - 'outcome', - 'runRef', - 'executedAt', - 'bindingRef', - 'bindingVersion', - 'sessionRef', - 'expiresAt', - 'auditRef', - 'sourcePointer', - 'candidate', - ]); - const exactCandidateFields = new Set(['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild']); + const envelope = asPlainRecord(raw); + let attestation; try { - assertNoSecretMaterial(proof); + assertNoSecretMaterial(envelope); + attestation = verifyMacControlPublicAttestation(envelope, { + keyId: receiptKeyId, + publicKey: receiptPublicKey, + }); + assertNoSecretMaterial(attestation); } catch { - throw macControlFailure('runtime_receipt_rejected', 'Mac-control runtime receipt contained forbidden material.'); + throw macControlFailure( + 'runtime_receipt_rejected', + 'Mac-control public runtime attestation could not be verified.' + ); } - const executedAtText = String(proof?.executedAt || ''); + const candidate = asPlainRecord(attestation?.connectorCandidate); + const executedAtText = String(attestation?.executedAt || ''); const executedAt = Date.parse(executedAtText); - const expiresAtMs = Number.isSafeInteger(proof?.expiresAt) ? proof.expiresAt * 1000 : Number.NaN; - const expectedBindingRef = macControlSaltedReference(challenge, selectedBindingId); + const authorityIssuedAtMs = Number.isSafeInteger(attestation?.authorityIssuedAt) + ? attestation.authorityIssuedAt * 1000 + : Number.NaN; + const authorityExpiresAtMs = Number.isSafeInteger(attestation?.authorityExpiresAt) + ? attestation.authorityExpiresAt * 1000 + : Number.NaN; if ( - !proof || + !envelope || !candidate || - Object.keys(proof).length !== exactProofFields.size || - Object.keys(proof).some((field) => !exactProofFields.has(field)) || - Object.keys(candidate).length !== exactCandidateFields.size || - Object.keys(candidate).some((field) => !exactCandidateFields.has(field)) || - proof.ok !== true || - proof.schema !== 'evaos.mac_control.runtime_proof.v2' || - proof.proofKind !== 'selected_binding_direct_mac_control' || - proof.tool !== 'customer_mac.desktop_hotkey' || - proof.outcome !== 'succeeded' || - proof.runRef !== runRef || + attestation.proofKind !== 'selected_binding_direct_mac_control' || + attestation.runtime !== 'openclaw' || + attestation.tool !== 'customer_mac.desktop_hotkey' || + attestation.outcome !== 'succeeded' || + attestation.runRef !== runRef || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(executedAtText) || !Number.isFinite(executedAt) || executedAt < requestStartedAt - 5_000 || executedAt > now + 5_000 || - !Number.isFinite(expiresAtMs) || - executedAt > expiresAtMs || - expiresAtMs - executedAt > 66_000 || - proof.bindingRef !== expectedBindingRef || - proof.bindingVersion !== selectedBindingVersion || - !/^[0-9a-f]{64}$/.test(String(proof.sessionRef || '')) || - expiresAtMs <= now || - expiresAtMs > bindingExpiry || - !/^[0-9a-f]{64}$/.test(String(proof.auditRef || '')) || - proof.sourcePointer !== 'evaos-desktop-bridge:runtime-receipt' || + !Number.isFinite(authorityIssuedAtMs) || + !Number.isFinite(authorityExpiresAtMs) || + authorityExpiresAtMs <= authorityIssuedAtMs || + authorityExpiresAtMs - authorityIssuedAtMs > 60_000 || + executedAt < authorityIssuedAtMs - 5_000 || + executedAt > authorityExpiresAtMs || + authorityExpiresAtMs <= now || + authorityExpiresAtMs > bindingExpiry || + attestation.contextKeyId !== contextKeyId || + attestation.controlState !== 'ready_unchanged' || + attestation.auditRecorded !== true || + !/^[0-9a-f]{64}$/.test(String(attestation.privateReceiptSha256 || '')) || candidate.sourceCommit !== expectedCandidate.sourceCommit || candidate.sourceSha256 !== expectedCandidate.sourceSha256 || candidate.appVersion !== expectedCandidate.appVersion || @@ -978,30 +975,10 @@ function sanitizeMacControlRuntimeProof( ) { throw macControlFailure( 'runtime_receipt_rejected', - 'Mac-control runtime receipt did not match the exact signed candidate.' + 'Mac-control public runtime attestation did not match the exact signed candidate.' ); } - return { - ok: true, - schema: proof.schema, - proofKind: proof.proofKind, - tool: proof.tool, - outcome: proof.outcome, - runRef: proof.runRef, - executedAt: proof.executedAt, - bindingRef: proof.bindingRef, - bindingVersion: proof.bindingVersion, - sessionRef: proof.sessionRef, - expiresAt: proof.expiresAt, - auditRef: proof.auditRef, - sourcePointer: proof.sourcePointer, - candidate: { - sourceCommit: candidate.sourceCommit, - sourceSha256: candidate.sourceSha256, - appVersion: candidate.appVersion, - appBuild: candidate.appBuild, - }, - }; + return envelope; } function failedMacControlProof(env, reason, extras = {}) { @@ -1065,6 +1042,111 @@ async function postMacControlRuntimeReceipt(fetchImpl, launchUrl, sessionCookie, }); } +async function safeMacControlProbeResponse(response) { + let text; + try { + text = await response.text(); + } catch { + throw macControlFailure('invalid_response', 'Mac-control deployed route probe response was unreadable.'); + } + if (Buffer.byteLength(text) > 4096) { + throw macControlFailure('invalid_response', 'Mac-control deployed route probe response was oversized.'); + } + let body; + try { + body = text ? JSON.parse(text) : {}; + } catch { + body = {}; + } + try { + assertNoSecretMaterial(body); + assertNoSecretMaterial(text); + } catch { + throw macControlFailure('invalid_response', 'Mac-control deployed route probe response was unsafe.'); + } + return asPlainRecord(body); +} + +function macControlProbeErrorCode(body) { + const nested = asPlainRecord(body?.error); + return String(nested?.code || body?.code || body?.error_code || ''); +} + +async function runMacControlDeployedRouteProbes({ fetchImpl, launchUrl, sessionCookie, env, now }) { + const endpoint = new URL(MAC_CONTROL_RUNTIME_RECEIPT_PATH, launchUrl); + const authenticatedHeaders = { + Cookie: sessionCookie, + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json', + }; + const unauthenticated = await fetchImpl(endpoint.toString(), { + method: 'POST', + redirect: 'manual', + headers: { 'Cache-Control': 'no-store', 'Content-Type': 'application/json' }, + body: '{}', + }); + await safeMacControlProbeResponse(unauthenticated); + + const method = await fetchImpl(endpoint.toString(), { + method: 'GET', + redirect: 'manual', + headers: { Cookie: sessionCookie, 'Cache-Control': 'no-store' }, + }); + const methodBody = await safeMacControlProbeResponse(method); + + const trailing = await fetchImpl(`${endpoint.toString()}/`, { + method: 'POST', + redirect: 'manual', + headers: authenticatedHeaders, + body: '{}', + }); + await safeMacControlProbeResponse(trailing); + + const empty = await fetchImpl(endpoint.toString(), { + method: 'POST', + redirect: 'manual', + headers: authenticatedHeaders, + body: '{}', + }); + const emptyBody = await safeMacControlProbeResponse(empty); + + const injected = await fetchImpl(endpoint.toString(), { + method: 'POST', + redirect: 'manual', + headers: authenticatedHeaders, + body: JSON.stringify({ + action: 'customer_mac.desktop_type', + binding: { bindingId: 'caller-supplied' }, + connector: { endpoint: 'caller-supplied' }, + executionContext: { payload: 'caller-supplied' }, + }), + }); + const injectedBody = await safeMacControlProbeResponse(injected); + const source = macControlProofSource(env); + const proof = { + schema: 'evaos.mac_control.deployed_route_probe.v1', + sourceHeadSha: source.sourceHeadSha, + sourceRunId: source.sourceRunId, + checkedAt: new Date(now).toISOString(), + assertions: { + gatewayAuthRequired: unauthenticated.status === 401 || unauthenticated.status === 403, + postOnly: + method.status === 405 && + method.headers.get('allow') === 'POST' && + macControlProbeErrorCode(methodBody) === 'method_not_allowed', + exactMatch: trailing.status === 404, + strictBody: empty.status === 400 && macControlProbeErrorCode(emptyBody) === 'invalid_request', + callerAuthorityBodyRejected: + injected.status === 400 && macControlProbeErrorCode(injectedBody) === 'invalid_request', + sensitiveOutputAbsent: true, + }, + }; + if (!proof.sourceHeadSha || !proof.sourceRunId || Object.values(proof.assertions).some((value) => value !== true)) { + throw macControlFailure('runtime_receipt_rejected', 'Mac-control deployed route boundary probes failed.'); + } + return proof; +} + async function runMacControlLiveCanary(options = {}) { const env = options.env ?? process.env; const fetchImpl = options.fetchImpl ?? fetch; @@ -1139,13 +1221,24 @@ async function runMacControlLiveCanary(options = {}) { const runtimeProof = sanitizeMacControlRuntimeProof(rawRuntimeProof, { runRef, expectedCandidate, - challenge, - selectedBindingId: launchPrivate.bindingId, - selectedBindingVersion: launchPrivate.bindingVersion, bindingExpiry: launchPrivate.bindingExpiry, requestStartedAt: receiptRequestStartedAt, now: now(), + receiptKeyId: config.receiptKeyId, + receiptPublicKey: config.receiptPublicKey, + contextKeyId: config.contextKeyId, + }); + + const deployedProbe = await runMacControlDeployedRouteProbes({ + fetchImpl, + launchUrl: launchPrivate.launchUrl, + sessionCookie: proxySessionCookie, + env, + now: now(), }); + if (typeof options.onDeployedProbe === 'function') { + await options.onDeployedProbe(deployedProbe); + } return runtimeProof; } catch (error) { @@ -1219,7 +1312,26 @@ async function runBrokerLiveCanary(options = {}) { } async function main() { - const result = process.argv.includes('--mac-control') ? await runMacControlLiveCanary() : await runBrokerLiveCanary(); + let result; + if (process.argv.includes('--mac-control')) { + const outputPath = envText(process.env, 'AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_PROBE_OUTPUT'); + if (!outputPath) { + throw new Error('AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_PROBE_OUTPUT is required for Mac-control proof.'); + } + result = await runMacControlLiveCanary({ + onDeployedProbe: (proof) => { + const descriptor = fs.openSync(outputPath, 'wx', 0o600); + try { + fs.writeFileSync(descriptor, `${JSON.stringify(proof, null, 2)}\n`, 'utf8'); + fs.fsyncSync(descriptor); + } finally { + fs.closeSync(descriptor); + } + }, + }); + } else { + result = await runBrokerLiveCanary(); + } console.log(JSON.stringify(result, null, 2)); } @@ -1228,7 +1340,9 @@ if (require.main === module) { if ((error instanceof BrokerCanaryError || error instanceof MacControlCanaryError) && error.proof) { console.log(JSON.stringify(error.proof, null, 2)); } - console.error(error instanceof Error ? error.message : String(error)); + const message = error instanceof Error ? error.message : String(error); + const safeReason = error instanceof MacControlCanaryError ? macControlReason(error.reason) : undefined; + console.error(safeReason ? `${message} Safe reason: ${safeReason}.` : message); process.exit(1); }); } @@ -1244,6 +1358,7 @@ module.exports = { sanitizeBrokerRuntimeLaunchCanaryResponse, sanitizeMacControlRuntimeLaunchCanaryResponse, sanitizeMacControlRuntimeProof, + runMacControlDeployedRouteProbes, expectedMacControlCandidate, resolveBrokerCanaryCredentials, resolveMacControlCanaryConfig, diff --git a/scripts/evaosLiveCanaryEnvInventory.js b/scripts/evaosLiveCanaryEnvInventory.js index 6fcddd539d..9c50a5ab38 100644 --- a/scripts/evaosLiveCanaryEnvInventory.js +++ b/scripts/evaosLiveCanaryEnvInventory.js @@ -127,6 +127,20 @@ const REQUIRED_MAC_CONTROL_SECRETS = [ reason: 'exact staging ws-proxy callback host expected from the launch response', }, ]; +const REQUIRED_MAC_CONTROL_VARIABLES = [ + { + name: 'EVAOS_MAC_CONTROL_CONTEXT_KEY_ID', + reason: 'public identifier for the staging ws-proxy execution-context signer', + }, + { + name: 'EVAOS_MAC_CONTROL_RECEIPT_KEY_ID', + reason: 'public identifier for the staging Workbench connector receipt signer', + }, + { + name: 'EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY', + reason: 'public Ed25519 trust anchor used to verify portable Mac-control attestations', + }, +]; function normalizeNames(values) { return new Set((values ?? []).map((value) => String(value ?? '').trim()).filter(Boolean)); @@ -194,6 +208,13 @@ function auditEnvironmentInventory(input, options = {}) { missing.push(fixture.name); } } + for (const fixture of REQUIRED_MAC_CONTROL_VARIABLES) { + if (inventory.variables.has(fixture.name)) { + satisfied.push(fixture.name); + } else { + missing.push(fixture.name); + } + } } const recommendedPresent = RECOMMENDED_VARIABLES.filter((name) => inventory.variables.has(name)); @@ -343,6 +364,13 @@ function renderProvisioningTemplate(options = {}) { lines.push(''); } + lines.push('', '## Optional Selected-Binding Mac-Control Canary Public Trust Variables', ''); + for (const fixture of REQUIRED_MAC_CONTROL_VARIABLES) { + lines.push(`# ${fixture.reason}`); + lines.push(variableCommand(fixture.name, repo, environment)); + lines.push(''); + } + lines.push( '', '## Verify Inventory', @@ -447,6 +475,7 @@ if (require.main === module) { module.exports = { REQUIRED_ONE_OF_FIXTURES, REQUIRED_MAC_CONTROL_SECRETS, + REQUIRED_MAC_CONTROL_VARIABLES, REQUIRED_PROVISIONED_SECRET_GROUPS, REQUIRED_SINGLE_FIXTURES, auditEnvironmentInventory, diff --git a/scripts/evaosMacControlSignedProof.js b/scripts/evaosMacControlSignedProof.js new file mode 100644 index 0000000000..3c97d128a5 --- /dev/null +++ b/scripts/evaosMacControlSignedProof.js @@ -0,0 +1,246 @@ +#!/usr/bin/env node + +const { createHash, createPublicKey, timingSafeEqual, verify } = require('node:crypto'); + +const PUBLIC_ATTESTATION_SCHEMA = 'evaos.mac_control.public_runtime_attestation.v1'; +const PUBLIC_ATTESTATION_ENVELOPE_SCHEMA = 'evaos.mac_control.public_runtime_attestation_envelope.v1'; +const PUBLIC_ATTESTATION_NAMESPACE = 'evaos-mac-control-public-attestation-v1'; +const PUBLIC_ATTESTATION_ENVELOPE_FIELDS = Object.freeze([ + 'schema', + 'attestationBase64', + 'signature', + 'keyId', + 'namespace', +]); +const PUBLIC_ATTESTATION_FIELDS = Object.freeze([ + 'schema', + 'keyId', + 'namespace', + 'proofKind', + 'runtime', + 'tool', + 'outcome', + 'runRef', + 'executedAt', + 'authorityIssuedAt', + 'authorityExpiresAt', + 'contextKeyId', + 'controlState', + 'auditRecorded', + 'privateReceiptSha256', + 'connectorCandidate', +]); +const CONNECTOR_CANDIDATE_FIELDS = Object.freeze(['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild']); +const MAX_ATTESTATION_BYTES = 16 * 1024; +const MAX_SIGNATURE_BYTES = 16 * 1024; + +function verifyMacControlPublicAttestation(envelope, options = {}) { + if (!isPlainObject(envelope) || !hasExactKeys(envelope, PUBLIC_ATTESTATION_ENVELOPE_FIELDS)) { + throw new Error('Mac-control public attestation envelope is invalid.'); + } + const expectedKeyId = String(options.keyId || '').trim(); + const encodedPublicKey = String(options.publicKey || '').trim(); + if ( + envelope.schema !== PUBLIC_ATTESTATION_ENVELOPE_SCHEMA || + envelope.namespace !== PUBLIC_ATTESTATION_NAMESPACE || + typeof envelope.keyId !== 'string' || + envelope.keyId !== expectedKeyId || + !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(expectedKeyId) || + typeof envelope.signature !== 'string' || + envelope.signature.length > MAX_SIGNATURE_BYTES + ) { + throw new Error('Mac-control public attestation trust anchor does not match.'); + } + + const publicKey = decodeCanonicalBase64Url(encodedPublicKey, 32); + const attestationBytes = decodeCanonicalBase64Url(envelope.attestationBase64, MAX_ATTESTATION_BYTES); + if (!publicKey || publicKey.byteLength !== 32 || !attestationBytes) { + throw new Error('Mac-control public attestation encoding is invalid.'); + } + + let attestation; + try { + attestation = JSON.parse(attestationBytes.toString('utf8')); + } catch { + throw new Error('Mac-control public attestation payload is invalid.'); + } + if ( + !isPlainObject(attestation) || + !hasExactKeys(attestation, PUBLIC_ATTESTATION_FIELDS) || + attestation.schema !== PUBLIC_ATTESTATION_SCHEMA || + attestation.keyId !== expectedKeyId || + attestation.namespace !== PUBLIC_ATTESTATION_NAMESPACE || + !isPlainObject(attestation.connectorCandidate) || + !hasExactKeys(attestation.connectorCandidate, CONNECTOR_CANDIDATE_FIELDS) + ) { + throw new Error('Mac-control public attestation contract is invalid.'); + } + const canonical = canonicalJson(attestation); + if (!canonical || !timingSafeBufferEqual(attestationBytes, Buffer.from(canonical, 'utf8'))) { + throw new Error('Mac-control public attestation payload is not canonical.'); + } + if (!verifySshSignature(attestationBytes, envelope.signature, publicKey, PUBLIC_ATTESTATION_NAMESPACE)) { + throw new Error('Mac-control public attestation signature is invalid.'); + } + return attestation; +} + +function verifySshSignature(message, armor, pinnedPublicKey, expectedNamespace) { + const decoded = decodeSshSignatureArmor(armor); + if (!decoded) return false; + try { + const reader = sshReader(decoded); + if (reader.readRaw(6).toString('ascii') !== 'SSHSIG' || reader.readUInt32() !== 1) return false; + const publicKeyBlob = reader.readString(); + const namespace = reader.readString().toString('utf8'); + const reserved = reader.readString(); + const hashAlgorithm = reader.readString().toString('ascii'); + const signatureBlob = reader.readString(); + if (!reader.done() || namespace !== expectedNamespace || reserved.byteLength !== 0) return false; + + const publicKeyReader = sshReader(publicKeyBlob); + const publicKeyAlgorithm = publicKeyReader.readString().toString('ascii'); + const embeddedPublicKey = publicKeyReader.readString(); + const signatureReader = sshReader(signatureBlob); + const signatureAlgorithm = signatureReader.readString().toString('ascii'); + const signature = signatureReader.readString(); + if ( + !publicKeyReader.done() || + !signatureReader.done() || + publicKeyAlgorithm !== 'ssh-ed25519' || + signatureAlgorithm !== 'ssh-ed25519' || + embeddedPublicKey.byteLength !== 32 || + signature.byteLength !== 64 || + !timingSafeBufferEqual(embeddedPublicKey, pinnedPublicKey) || + (hashAlgorithm !== 'sha256' && hashAlgorithm !== 'sha512') + ) { + return false; + } + + const digest = createHash(hashAlgorithm).update(message).digest(); + const signedData = Buffer.concat([ + Buffer.from('SSHSIG', 'ascii'), + encodeSshString(Buffer.from(namespace, 'utf8')), + encodeSshString(Buffer.alloc(0)), + encodeSshString(Buffer.from(hashAlgorithm, 'ascii')), + encodeSshString(digest), + ]); + const publicKey = createPublicKey({ + key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), embeddedPublicKey]), + format: 'der', + type: 'spki', + }); + return verify(null, signedData, publicKey, signature); + } catch { + return false; + } +} + +function decodeSshSignatureArmor(value) { + const match = /^-----BEGIN SSH SIGNATURE-----\n([A-Za-z0-9+/=\n]+)-----END SSH SIGNATURE-----\n?$/.exec(value); + if (!match) return undefined; + const lines = match[1].split('\n').filter(Boolean); + if (lines.length === 0 || lines.some((line) => line.length > 76 || !/^[A-Za-z0-9+/=]+$/.test(line))) { + return undefined; + } + const joined = lines.join(''); + const decoded = Buffer.from(joined, 'base64'); + return decoded.toString('base64') === joined ? decoded : undefined; +} + +function decodeCanonicalBase64Url(value, maxBytes) { + if ( + typeof value !== 'string' || + value.length === 0 || + value.length > maxBytes * 2 || + !/^[A-Za-z0-9_-]+$/.test(value) + ) { + return undefined; + } + const decoded = Buffer.from(value, 'base64url'); + return decoded.byteLength > 0 && decoded.byteLength <= maxBytes && decoded.toString('base64url') === value + ? decoded + : undefined; +} + +function sshReader(value) { + const buffer = Buffer.from(value); + let offset = 0; + return { + readRaw(length) { + if (!Number.isInteger(length) || length < 0 || offset + length > buffer.byteLength) { + throw new Error('invalid SSH signature field'); + } + const result = buffer.subarray(offset, offset + length); + offset += length; + return result; + }, + readUInt32() { + if (offset + 4 > buffer.byteLength) throw new Error('invalid SSH signature integer'); + const result = buffer.readUInt32BE(offset); + offset += 4; + return result; + }, + readString() { + const length = this.readUInt32(); + if (length > MAX_SIGNATURE_BYTES) throw new Error('oversized SSH signature field'); + return this.readRaw(length); + }, + done() { + return offset === buffer.byteLength; + }, + }; +} + +function encodeSshString(value) { + const buffer = Buffer.from(value); + const length = Buffer.alloc(4); + length.writeUInt32BE(buffer.byteLength, 0); + return Buffer.concat([length, buffer]); +} + +function timingSafeBufferEqual(left, right) { + const leftBuffer = Buffer.from(left); + const rightBuffer = Buffer.from(right); + return leftBuffer.byteLength === rightBuffer.byteLength && timingSafeEqual(leftBuffer, rightBuffer); +} + +function canonicalJson(value) { + try { + return JSON.stringify(sortJson(value)); + } catch { + return undefined; + } +} + +function sortJson(value) { + if (Array.isArray(value)) return value.map(sortJson); + if (!isPlainObject(value)) return value; + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, sortJson(value[key])]) + ); +} + +function isPlainObject(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function hasExactKeys(value, expected) { + const keys = Object.keys(value).sort(); + const required = [...expected].sort(); + return keys.length === required.length && keys.every((key, index) => key === required[index]); +} + +module.exports = { + CONNECTOR_CANDIDATE_FIELDS, + PUBLIC_ATTESTATION_ENVELOPE_FIELDS, + PUBLIC_ATTESTATION_ENVELOPE_SCHEMA, + PUBLIC_ATTESTATION_FIELDS, + PUBLIC_ATTESTATION_NAMESPACE, + PUBLIC_ATTESTATION_SCHEMA, + verifyMacControlPublicAttestation, +}; diff --git a/scripts/evaosScanMacControlProofs.js b/scripts/evaosScanMacControlProofs.js index 187c031c7d..0acaefa17a 100644 --- a/scripts/evaosScanMacControlProofs.js +++ b/scripts/evaosScanMacControlProofs.js @@ -6,6 +6,7 @@ const path = require('node:path'); const MAC_CONTROL_PROOF_NAMES = Object.freeze([ 'mac-control-runtime.json', 'mac-control-runtime-negative.json', + 'mac-control-deployed-route.json', 'mac-control-session-provisioning.json', 'mac-control-session-provisioning.stdout.json', 'mac-control-session-cleanup.json', @@ -13,28 +14,8 @@ const MAC_CONTROL_PROOF_NAMES = Object.freeze([ ]); const MAC_CONTROL_PROOF_CONTRACTS = Object.freeze({ 'mac-control-runtime.json': { - schema: 'evaos.mac_control.runtime_proof.v2', - fields: [ - 'ok', - 'schema', - 'proofKind', - 'tool', - 'outcome', - 'runRef', - 'executedAt', - 'bindingRef', - 'bindingVersion', - 'sessionRef', - 'expiresAt', - 'auditRef', - 'sourcePointer', - 'candidate', - ], - nested: { - candidate: { - fields: ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild'], - }, - }, + schema: 'evaos.mac_control.public_runtime_attestation_envelope.v1', + fields: ['schema', 'attestationBase64', 'signature', 'keyId', 'namespace'], }, 'mac-control-runtime-negative.json': { schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', @@ -45,6 +26,22 @@ const MAC_CONTROL_PROOF_CONTRACTS = Object.freeze({ }, }, }, + 'mac-control-deployed-route.json': { + schema: 'evaos.mac_control.deployed_route_probe.v1', + fields: ['schema', 'sourceHeadSha', 'sourceRunId', 'checkedAt', 'assertions'], + nested: { + assertions: { + fields: [ + 'gatewayAuthRequired', + 'postOnly', + 'exactMatch', + 'strictBody', + 'callerAuthorityBodyRejected', + 'sensitiveOutputAbsent', + ], + }, + }, + }, 'mac-control-session-provisioning.json': { schema: 'evaos-mac-control-canary-session-provision/v1', fields: [ @@ -181,6 +178,71 @@ function assertExactProofContract(value, contract, location) { } } +function assertPublicAttestationEnvelopeSanitized(value, location) { + const contract = MAC_CONTROL_PROOF_CONTRACTS['mac-control-runtime.json']; + assertExactProofContract(value, contract, location); + assertMacControlProofSanitized({ schema: value.schema, keyId: value.keyId, namespace: value.namespace }, location); + if ( + typeof value.attestationBase64 !== 'string' || + value.attestationBase64.length < 32 || + value.attestationBase64.length > 32768 || + !/^[A-Za-z0-9_-]+$/.test(value.attestationBase64) + ) { + throw new Error(`Mac-control proof contains invalid public attestation bytes at ${location}.`); + } + const attestationBytes = Buffer.from(value.attestationBase64, 'base64url'); + if ( + attestationBytes.byteLength === 0 || + attestationBytes.byteLength > 16384 || + attestationBytes.toString('base64url') !== value.attestationBase64 + ) { + throw new Error(`Mac-control proof contains noncanonical public attestation bytes at ${location}.`); + } + let attestation; + try { + attestation = JSON.parse(attestationBytes.toString('utf8')); + } catch { + throw new Error(`Mac-control proof contains invalid public attestation JSON at ${location}.`); + } + const attestationContract = { + fields: [ + 'schema', + 'keyId', + 'namespace', + 'proofKind', + 'runtime', + 'tool', + 'outcome', + 'runRef', + 'executedAt', + 'authorityIssuedAt', + 'authorityExpiresAt', + 'contextKeyId', + 'controlState', + 'auditRecorded', + 'privateReceiptSha256', + 'connectorCandidate', + ], + nested: { + connectorCandidate: { + fields: ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild'], + }, + }, + }; + assertExactProofContract(attestation, attestationContract, `${location}.decodedAttestation`); + if (attestation.schema !== 'evaos.mac_control.public_runtime_attestation.v1') { + throw new Error(`Mac-control proof contains an unexpected public attestation schema at ${location}.`); + } + assertMacControlProofSanitized(attestation, `${location}.decodedAttestation`); + if ( + typeof value.signature !== 'string' || + value.signature.length > 8192 || + !/^-----BEGIN SSH SIGNATURE-----\n(?:[A-Za-z0-9+/=]{1,76}\n)+-----END SSH SIGNATURE-----\n$/.test(value.signature) + ) { + throw new Error(`Mac-control proof contains an invalid public attestation signature at ${location}.`); + } +} + function scanMacControlProofDirectory(proofDir) { const resolvedProofDir = path.resolve(String(proofDir || '')); let scanned = 0; @@ -190,12 +252,16 @@ function scanMacControlProofDirectory(proofDir) { throw new Error(`Mac-control proof is missing required artifact ${proofName}.`); } const proof = JSON.parse(fs.readFileSync(proofPath, 'utf8')); - assertMacControlProofSanitized(proof, proofName); const contract = MAC_CONTROL_PROOF_CONTRACTS[proofName]; if (!contract || proof.schema !== contract.schema) { throw new Error(`Mac-control proof has an unexpected schema in ${proofName}.`); } - assertExactProofContract(proof, contract, proofName); + if (proofName === 'mac-control-runtime.json') { + assertPublicAttestationEnvelopeSanitized(proof, proofName); + } else { + assertMacControlProofSanitized(proof, proofName); + assertExactProofContract(proof, contract, proofName); + } scanned += 1; } const cleanupPath = path.join(resolvedProofDir, 'mac-control-session-cleanup.json'); diff --git a/scripts/evaosValidateLiveCanaryProofRun.sh b/scripts/evaosValidateLiveCanaryProofRun.sh index a7192c8a39..421b9656d9 100755 --- a/scripts/evaosValidateLiveCanaryProofRun.sh +++ b/scripts/evaosValidateLiveCanaryProofRun.sh @@ -57,6 +57,15 @@ if [ "$MAC_CONTROL_PROOF_REQUIRED" = "true" ]; then echo "::error::Broker and Mac-control live canary proofs must come from the same proof packet." exit 1 fi + mapfile -t MAC_CONTROL_DEPLOYED_PROOFS < <(find live-canary-proof-download -type f -name mac-control-deployed-route.json) + if [ "${#MAC_CONTROL_DEPLOYED_PROOFS[@]}" -ne 1 ]; then + echo "::error::Live canary artifact must contain exactly one mac-control-deployed-route.json, found ${#MAC_CONTROL_DEPLOYED_PROOFS[@]}." + exit 1 + fi + if [ "$(dirname "${MAC_CONTROL_DEPLOYED_PROOFS[0]}")" != "$PROOF_ROOT" ]; then + echo "::error::Broker, Mac-control, and deployed-route proofs must come from the same proof packet." + exit 1 + fi fi mkdir -p live-canary-proof cp -R "$PROOF_ROOT"/. live-canary-proof/ diff --git a/tests/unit/evaos/evaosBrokerLiveCanary.test.ts b/tests/unit/evaos/evaosBrokerLiveCanary.test.ts index 1784f2f6fa..a580e10136 100644 --- a/tests/unit/evaos/evaosBrokerLiveCanary.test.ts +++ b/tests/unit/evaos/evaosBrokerLiveCanary.test.ts @@ -7,12 +7,34 @@ */ import { execFileSync } from 'node:child_process'; -import { createHash } from 'node:crypto'; import fs from 'node:fs'; import { createRequire } from 'node:module'; import os from 'node:os'; import path from 'node:path'; import { describe, expect, it, vi } from 'vitest'; +import { + signedMacControlAttestation, + TEST_CONTEXT_KEY_ID, + TEST_RECEIPT_KEY_ID, +} from './fixtures/signedMacControlAttestation'; + +const TRUST_FIXTURE = signedMacControlAttestation({ + runRef: 'gha:12345:111111111111111111111111', + executedAt: '2026-07-15T00:00:00Z', + authorityIssuedAt: 1784073600, + authorityExpiresAt: 1784073660, + candidate: { + sourceCommit: 'a'.repeat(40), + sourceSha256: 'b'.repeat(64), + appVersion: '2.1.36', + appBuild: '2.1.36', + }, +}); +const TRUST_ENV = { + EVAOS_LIVE_CANARY_CONTEXT_KEY_ID: TRUST_FIXTURE.trust.contextKeyId, + EVAOS_LIVE_CANARY_RECEIPT_KEY_ID: TRUST_FIXTURE.trust.receiptKeyId, + EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY: TRUST_FIXTURE.trust.receiptPublicKey, +}; const require = createRequire(import.meta.url); const releaseGate = require('../../../scripts/evaosBetaReleaseGate.js') as { @@ -46,12 +68,16 @@ const liveCanary = require('../../../scripts/evaosBrokerLiveCanary.js') as { customerId: string; endpoint: string; expectedCallbackHost: string; + receiptKeyId: string; + receiptPublicKey: string; + contextKeyId: string; }; runMacControlLiveCanary: (options: { env: Record; fetchImpl: typeof fetch; now?: () => number; randomBytes?: (size: number) => Uint8Array; + onDeployedProbe?: (proof: Record) => void | Promise; }) => Promise>; expectedMacControlCandidate: (env: Record) => { sourceCommit: string; @@ -62,14 +88,14 @@ const liveCanary = require('../../../scripts/evaosBrokerLiveCanary.js') as { sanitizeMacControlRuntimeProof: ( raw: unknown, options: { - challenge: string; runRef: string; expectedCandidate: Record; - selectedBindingId: string; - selectedBindingVersion: string; bindingExpiry: number; requestStartedAt: number; now: number; + receiptKeyId: string; + receiptPublicKey: string; + contextKeyId: string; } ) => Record; sanitizeMacControlRuntimeLaunchCanaryResponse: ( @@ -87,14 +113,6 @@ function jsonResponse(body: unknown, init: ResponseInit = {}): Response { }); } -function saltedReference(challenge: string, value: string): string { - return createHash('sha256') - .update(Buffer.from(challenge, 'ascii')) - .update(Buffer.from([0])) - .update(Buffer.from(value, 'utf8')) - .digest('hex'); -} - describe('evaOS broker live canary', () => { it('returns a sanitized runtime proof summary for clean broker responses', () => { const proof = liveCanary.sanitizeBrokerRuntimeCanaryResponse( @@ -587,6 +605,7 @@ describe('evaOS broker live canary', () => { it('strictly validates and normalizes the dedicated callback host', () => { const env = { + ...TRUST_ENV, AIONUI_EVAOS_MAC_CONTROL_CANARY_ACK: 'evaos-mac-control-canary', AIONUI_EVAOS_MAC_CONTROL_CANARY_DESKTOP_SESSION: 'eds_mac_canary_session_for_test', AIONUI_EVAOS_MAC_CONTROL_CANARY_CUSTOMER_ID: 'staging-mac-owner', @@ -625,6 +644,16 @@ describe('evaOS broker live canary', () => { const challenge = challengeBytes.toString('base64url'); const runRef = `gha:123456789:${runNonceBytes.toString('hex')}`; const expectedCandidate = liveCanary.expectedMacControlCandidate({ GITHUB_SHA: sourceHeadSha }); + const signedProof = signedMacControlAttestation({ + runRef, + executedAt: new Date(now).toISOString(), + authorityIssuedAt: Math.floor(now / 1000) - 1, + authorityExpiresAt: Math.floor(now / 1000) + 15, + candidate: expectedCandidate, + keyId: TEST_RECEIPT_KEY_ID, + contextKeyId: TEST_CONTEXT_KEY_ID, + keyPair: TRUST_FIXTURE.keyPair, + }); const bindingExpiresAt = new Date(now + 20_000).toISOString(); const binding = { schema_version: 'evaos.mac_control_runtime_readiness.v1', @@ -669,24 +698,19 @@ describe('evaOS broker live canary', () => { }, }) ) + .mockResolvedValueOnce(jsonResponse(signedProof.envelope)) + .mockResolvedValueOnce(jsonResponse({ ok: false }, { status: 401 })) .mockResolvedValueOnce( - jsonResponse({ - ok: true, - schema: 'evaos.mac_control.runtime_proof.v2', - proofKind: 'selected_binding_direct_mac_control', - tool: 'customer_mac.desktop_hotkey', - outcome: 'succeeded', - runRef, - executedAt: new Date(now).toISOString(), - bindingRef: saltedReference(challenge, binding.binding_id), - bindingVersion: '7', - sessionRef: 'b'.repeat(64), - expiresAt: Math.floor(now / 1000) + 15, - auditRef: 'c'.repeat(64), - sourcePointer: 'evaos-desktop-bridge:runtime-receipt', - candidate: expectedCandidate, - }) - ); + jsonResponse( + { ok: false, error: { code: 'method_not_allowed' } }, + { status: 405, headers: { 'Content-Type': 'application/json', Allow: 'POST' } } + ) + ) + .mockResolvedValueOnce(jsonResponse({ ok: false }, { status: 404 })) + .mockResolvedValueOnce(jsonResponse({ ok: false, error: { code: 'invalid_request' } }, { status: 400 })) + .mockResolvedValueOnce(jsonResponse({ ok: false, error: { code: 'invalid_request' } }, { status: 400 })); + + let deployedProbe: Record | undefined; const proof = await liveCanary.runMacControlLiveCanary({ env: { @@ -695,15 +719,19 @@ describe('evaOS broker live canary', () => { AIONUI_EVAOS_MAC_CONTROL_CANARY_CUSTOMER_ID: 'staging-mac-owner', AIONUI_EVAOS_MAC_CONTROL_CANARY_ENDPOINT: 'https://dashboard-staging.example.test/runtime', AIONUI_EVAOS_MAC_CONTROL_CANARY_EXPECTED_CALLBACK_HOST: 'openclaw-staging.example.test', + ...TRUST_ENV, GITHUB_SHA: sourceHeadSha, GITHUB_RUN_ID: '123456789', }, fetchImpl, now: () => now, randomBytes: (size: number) => (size === 32 ? challengeBytes : runNonceBytes), + onDeployedProbe: (value) => { + deployedProbe = value; + }, }); - expect(fetchImpl).toHaveBeenCalledTimes(3); + expect(fetchImpl).toHaveBeenCalledTimes(8); expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toEqual({ action: 'runtime_launch', customer_id: 'staging-mac-owner', @@ -724,25 +752,28 @@ describe('evaOS broker live canary', () => { }, }); expect(JSON.parse(String(fetchImpl.mock.calls[2][1]?.body))).toEqual({ challenge, runRef }); - expect(proof).toEqual({ - ok: true, - schema: 'evaos.mac_control.runtime_proof.v2', - proofKind: 'selected_binding_direct_mac_control', - tool: 'customer_mac.desktop_hotkey', - outcome: 'succeeded', - runRef, - executedAt: new Date(now).toISOString(), - bindingRef: saltedReference(challenge, binding.binding_id), - bindingVersion: '7', - sessionRef: 'b'.repeat(64), - expiresAt: Math.floor(now / 1000) + 15, - auditRef: 'c'.repeat(64), - sourcePointer: 'evaos-desktop-bridge:runtime-receipt', - candidate: expectedCandidate, + expect(proof).toEqual(signedProof.envelope); + expect(deployedProbe).toMatchObject({ + schema: 'evaos.mac_control.deployed_route_probe.v1', + sourceHeadSha, + sourceRunId: '123456789', + assertions: { + gatewayAuthRequired: true, + postOnly: true, + exactMatch: true, + strictBody: true, + callerAuthorityBodyRejected: true, + sensitiveOutputAbsent: true, + }, }); + const publicAttestation = JSON.parse(Buffer.from(signedProof.envelope.attestationBase64, 'base64url').toString()); expect(JSON.stringify(proof)).not.toMatch( /staging-mac-owner|11111111-1111-4111-8111-111111111111|binding_version|binding_expires_at|callback_secret|proxy_session_secret|example\.test|eds_/ ); + expect(publicAttestation).not.toHaveProperty('challenge'); + expect(publicAttestation).not.toHaveProperty('bindingRef'); + expect(publicAttestation).not.toHaveProperty('sessionRef'); + expect(publicAttestation).not.toHaveProperty('auditRef'); const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-mac-control-contract-')); try { @@ -783,6 +814,7 @@ describe('evaOS broker live canary', () => { }, })}\n` ); + fs.writeFileSync(path.join(proofDir, 'mac-control-deployed-route.json'), `${JSON.stringify(deployedProbe)}\n`); expect( releaseGate.verifyMacControlLiveCanaryProof( @@ -790,6 +822,7 @@ describe('evaOS broker live canary', () => { { EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: sourceHeadSha, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '123456789', + ...TRUST_ENV, }, { now: new Date(now), maxAgeHours: 24 } ) @@ -817,6 +850,7 @@ describe('evaOS broker live canary', () => { ...process.env, PYTHONDONTWRITEBYTECODE: '1', PYTHONPATH: path.join(process.cwd(), 'resources', 'evaos-beta', 'bridge', 'src'), + ...TRUST_ENV, }, }).trim() ).toBe('ok'); @@ -825,11 +859,8 @@ describe('evaOS broker live canary', () => { } }); - it('rejects runtime receipts that are stale, overlong, future-dated, or cross-bound', () => { + it('rejects unsigned, forged, stale, overlong, future-dated, or cross-run attestations', () => { const now = Date.parse('2026-07-15T00:00:20.000Z'); - const challenge = Buffer.alloc(32, 15).toString('base64url'); - const selectedBindingId = 'binding-selected-test'; - const selectedBindingVersion = '7'; const runRef = 'gha:123456789:111111111111111111111111'; const expectedCandidate = { sourceCommit: 'a'.repeat(40), @@ -837,48 +868,52 @@ describe('evaOS broker live canary', () => { appVersion: '2.1.36', appBuild: '2.1.36', }; - const baseProof = { - ok: true, - schema: 'evaos.mac_control.runtime_proof.v2', - proofKind: 'selected_binding_direct_mac_control', - tool: 'customer_mac.desktop_hotkey', - outcome: 'succeeded', + const valid = signedMacControlAttestation({ runRef, executedAt: '2026-07-15T00:00:20.000Z', - bindingRef: saltedReference(challenge, selectedBindingId), - bindingVersion: selectedBindingVersion, - sessionRef: 'b'.repeat(64), - expiresAt: Math.floor(now / 1000) + 60, - auditRef: 'c'.repeat(64), - sourcePointer: 'evaos-desktop-bridge:runtime-receipt', + authorityIssuedAt: Math.floor(now / 1000) - 1, + authorityExpiresAt: Math.floor(now / 1000) + 59, candidate: expectedCandidate, - }; + keyId: TEST_RECEIPT_KEY_ID, + contextKeyId: TEST_CONTEXT_KEY_ID, + keyPair: TRUST_FIXTURE.keyPair, + }); const options = { - challenge, runRef, expectedCandidate, - selectedBindingId, - selectedBindingVersion, bindingExpiry: now + 120_000, requestStartedAt: now, now, + receiptKeyId: TRUST_ENV.EVAOS_LIVE_CANARY_RECEIPT_KEY_ID, + receiptPublicKey: TRUST_ENV.EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY, + contextKeyId: TRUST_ENV.EVAOS_LIVE_CANARY_CONTEXT_KEY_ID, }; - for (const mutation of [ - { schema: 'evaos.mac_control.runtime_proof.v1' }, - { unexpected: true }, - { runRef: 'gha:123456789:222222222222222222222222' }, - { bindingRef: 'd'.repeat(64) }, - { bindingVersion: '999' }, - { candidate: { ...expectedCandidate, sourceCommit: 'd'.repeat(40) } }, - { executedAt: '2026-07-15T00:00:14.999Z' }, - { executedAt: '2026-07-15T00:00:25.001Z' }, - { executedAt: '2026-07-15 00:00:20Z' }, - { expiresAt: Math.floor(now / 1000) + 67 }, + const resigned = (overrides: Partial[0]>) => + signedMacControlAttestation({ + runRef, + executedAt: '2026-07-15T00:00:20.000Z', + authorityIssuedAt: Math.floor(now / 1000) - 1, + authorityExpiresAt: Math.floor(now / 1000) + 59, + candidate: expectedCandidate, + keyId: TEST_RECEIPT_KEY_ID, + contextKeyId: TEST_CONTEXT_KEY_ID, + keyPair: TRUST_FIXTURE.keyPair, + ...overrides, + }).envelope; + for (const invalid of [ + { schema: 'evaos.mac_control.runtime_proof.v2' }, + { ...valid.envelope, unexpected: true }, + { ...valid.envelope, signature: valid.envelope.signature.replace('A', 'B') }, + resigned({ runRef: 'gha:123456789:222222222222222222222222' }), + resigned({ candidate: { ...expectedCandidate, sourceCommit: 'd'.repeat(40) } }), + resigned({ executedAt: '2026-07-15T00:00:14.999Z' }), + resigned({ executedAt: '2026-07-15T00:00:25.001Z' }), + resigned({ executedAt: '2026-07-15 00:00:20Z' }), + resigned({ authorityExpiresAt: Math.floor(now / 1000) + 60 }), + resigned({ contextKeyId: 'wrong-context-key' }), ]) { - expect(() => liveCanary.sanitizeMacControlRuntimeProof({ ...baseProof, ...mutation }, options)).toThrow( - /runtime receipt did not match/i - ); + expect(() => liveCanary.sanitizeMacControlRuntimeProof(invalid, options)).toThrow(/attestation/i); } }); @@ -1233,6 +1268,7 @@ describe('evaOS broker live canary', () => { AIONUI_EVAOS_MAC_CONTROL_CANARY_CUSTOMER_ID: 'staging-mac-owner', AIONUI_EVAOS_MAC_CONTROL_CANARY_ENDPOINT: 'https://dashboard-staging.example.test/runtime', AIONUI_EVAOS_MAC_CONTROL_CANARY_EXPECTED_CALLBACK_HOST: 'openclaw-staging.example.test', + ...TRUST_ENV, }, fetchImpl, now: () => now, @@ -1298,6 +1334,7 @@ describe('evaOS broker live canary', () => { AIONUI_EVAOS_MAC_CONTROL_CANARY_CUSTOMER_ID: 'staging-mac-owner', AIONUI_EVAOS_MAC_CONTROL_CANARY_ENDPOINT: 'https://dashboard-staging.example.test/runtime', AIONUI_EVAOS_MAC_CONTROL_CANARY_EXPECTED_CALLBACK_HOST: 'openclaw-staging.example.test', + ...TRUST_ENV, }, fetchImpl, now: () => times.shift() ?? callbackNow, @@ -1366,6 +1403,7 @@ describe('evaOS broker live canary', () => { AIONUI_EVAOS_MAC_CONTROL_CANARY_CUSTOMER_ID: 'staging-mac-owner', AIONUI_EVAOS_MAC_CONTROL_CANARY_ENDPOINT: 'https://dashboard-staging.example.test/runtime', AIONUI_EVAOS_MAC_CONTROL_CANARY_EXPECTED_CALLBACK_HOST: 'openclaw-staging.example.test', + ...TRUST_ENV, }, fetchImpl, now: () => now, diff --git a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts index 93a29f292c..37b51b199b 100644 --- a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts +++ b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts @@ -9,6 +9,7 @@ describe('evaOS signed Mac-control receipt connector', () => { it('fails closed around the fixed authenticated canary route and emits a verifiable sanitized receipt', () => { const script = String.raw` import base64 +import hashlib import json import os import subprocess @@ -182,9 +183,14 @@ try: status, envelope = post(valid) assert status == 200, (status, envelope) assert action_calls == [["customer-mac", "--remote-control-generation", "7", "desktop", "hotkey", "--json", "--keys", "escape"]] - assert envelope["schema"] == "evaos.mac_control.runtime_receipt_envelope.v1" - assert envelope["namespace"] == "evaos-mac-control-receipt-v1" - receipt_bytes = base64.urlsafe_b64decode(envelope["receiptBase64"] + "=" * (-len(envelope["receiptBase64"]) % 4)) + assert envelope["schema"] == "evaos.mac_control.runtime_receipt_bundle.v2" + private_envelope = envelope["privateReceipt"] + public_envelope = envelope["publicAttestation"] + assert private_envelope["schema"] == "evaos.mac_control.runtime_receipt_envelope.v1" + assert private_envelope["namespace"] == "evaos-mac-control-receipt-v1" + assert public_envelope["schema"] == "evaos.mac_control.public_runtime_attestation_envelope.v1" + assert public_envelope["namespace"] == "evaos-mac-control-public-attestation-v1" + receipt_bytes = base64.urlsafe_b64decode(private_envelope["receiptBase64"] + "=" * (-len(private_envelope["receiptBase64"]) % 4)) receipt = json.loads(receipt_bytes) assert receipt_bytes == json.dumps(receipt, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() assert receipt["action"] == {"command": "customer_mac.desktop_hotkey", "args": {"dryRun": False, "keys": "escape"}} @@ -207,15 +213,38 @@ try: public = (key_path.with_suffix(".pub")).read_text(encoding="utf-8").strip() allowed.write_text(f"evaos {public}\n", encoding="utf-8") signature_path = root / "receipt.sshsig" - signature_path.write_text(envelope["signature"], encoding="ascii") + signature_path.write_text(private_envelope["signature"], encoding="ascii") verified = subprocess.run( - ["/usr/bin/ssh-keygen", "-Y", "verify", "-f", str(allowed), "-I", "evaos", "-n", envelope["namespace"], "-s", str(signature_path)], + ["/usr/bin/ssh-keygen", "-Y", "verify", "-f", str(allowed), "-I", "evaos", "-n", private_envelope["namespace"], "-s", str(signature_path)], input=receipt_bytes, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) assert verified.returncode == 0 + attestation_bytes = base64.urlsafe_b64decode(public_envelope["attestationBase64"] + "=" * (-len(public_envelope["attestationBase64"]) % 4)) + attestation = json.loads(attestation_bytes) + assert attestation_bytes == json.dumps(attestation, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() + assert attestation["schema"] == "evaos.mac_control.public_runtime_attestation.v1" + assert attestation["privateReceiptSha256"] == hashlib.sha256(receipt_bytes).hexdigest() + assert attestation["connectorCandidate"] == { + "sourceCommit": "a" * 40, + "sourceSha256": "b" * 64, + "appVersion": "2.1.36", + "appBuild": "2.1.36", + } + for forbidden_field in ["challenge", "bindingRef", "sessionRef", "auditRef", "customerRef", "vmRef"]: + assert forbidden_field not in attestation + public_signature_path = root / "public-attestation.sshsig" + public_signature_path.write_text(public_envelope["signature"], encoding="ascii") + public_verified = subprocess.run( + ["/usr/bin/ssh-keygen", "-Y", "verify", "-f", str(allowed), "-I", "evaos", "-n", public_envelope["namespace"], "-s", str(public_signature_path)], + input=attestation_bytes, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + assert public_verified.returncode == 0 + count = len(action_calls) assert post(valid)[0] == 409 assert len(action_calls) == count diff --git a/tests/unit/evaos/evaosLiveCanaryEnvInventory.test.ts b/tests/unit/evaos/evaosLiveCanaryEnvInventory.test.ts index 828f58c5e1..265bcf8491 100644 --- a/tests/unit/evaos/evaosLiveCanaryEnvInventory.test.ts +++ b/tests/unit/evaos/evaosLiveCanaryEnvInventory.test.ts @@ -158,6 +158,9 @@ describe('evaOS live canary GitHub environment inventory', () => { 'AIONUI_EVAOS_MAC_CONTROL_CANARY_CUSTOMER_ID', 'AIONUI_EVAOS_MAC_CONTROL_CANARY_ENDPOINT', 'AIONUI_EVAOS_MAC_CONTROL_CANARY_EXPECTED_CALLBACK_HOST', + 'EVAOS_MAC_CONTROL_CONTEXT_KEY_ID', + 'EVAOS_MAC_CONTROL_RECEIPT_KEY_ID', + 'EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY', ]) ); @@ -177,6 +180,9 @@ describe('evaOS live canary GitHub environment inventory', () => { 'AIONUI_EVAOS_RELEASE_CANARY_CUSTOMER_ID', 'AIONUI_EVAOS_RELEASE_CANARY_TARGET_KIND', 'AIONUI_EVAOS_RELEASE_CANARY_TARGET_LABEL', + 'EVAOS_MAC_CONTROL_CONTEXT_KEY_ID', + 'EVAOS_MAC_CONTROL_RECEIPT_KEY_ID', + 'EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY', ], }, { provisioned: true, macControl: true } @@ -240,11 +246,14 @@ describe('evaOS live canary GitHub environment inventory', () => { expect(template).toContain('gh secret set AIONUI_EVAOS_MAC_CONTROL_CANARY_CUSTOMER_ID'); expect(template).toContain('gh secret set AIONUI_EVAOS_MAC_CONTROL_CANARY_ENDPOINT'); expect(template).toContain('gh secret set AIONUI_EVAOS_MAC_CONTROL_CANARY_EXPECTED_CALLBACK_HOST'); + expect(template).toContain('gh variable set EVAOS_MAC_CONTROL_CONTEXT_KEY_ID'); + expect(template).toContain('gh variable set EVAOS_MAC_CONTROL_RECEIPT_KEY_ID'); + expect(template).toContain('gh variable set EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY'); expect(template).toContain('-f run_mac_control_canary=true'); expect(template).toContain('-f mac_control_canary_ack=evaos-mac-control-canary'); expect(template).toContain("-f proof_ref='https://github.com/100yenadmin/AionUi/issues/41'"); expect(template).toContain( - "node scripts/evaosLiveCanaryEnvInventory.js --repo '100yenadmin/AionUi' --env 'evaos-staging' --strict --markdown --provisioned" + "node scripts/evaosLiveCanaryEnvInventory.js --repo '100yenadmin/AionUi' --env 'evaos-staging' --strict --markdown --provisioned --mac-control" ); expect(template).not.toContain('eds_live_session_for_test'); expect(template).not.toContain('https://workspace.example.test'); diff --git a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts index 70e8f75eb6..2cd8994f0a 100644 --- a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts +++ b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts @@ -3,6 +3,7 @@ import { createRequire } from 'node:module'; import os from 'node:os'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; +import { signedMacControlAttestation } from './fixtures/signedMacControlAttestation'; const WORKFLOW_PATH = '.github/workflows/evaos-live-canary-proof.yml'; const require = createRequire(import.meta.url); @@ -16,27 +17,18 @@ function readWorkflow(): string { } function validMacControlRuntimeProof(): Record { - return { - ok: true, - schema: 'evaos.mac_control.runtime_proof.v2', - proofKind: 'selected_binding_direct_mac_control', - tool: 'customer_mac.desktop_hotkey', - outcome: 'succeeded', + return signedMacControlAttestation({ runRef: 'gha:12345:111111111111111111111111', executedAt: '2026-07-15T00:00:00.000Z', - bindingRef: 'a'.repeat(64), - bindingVersion: '7', - sessionRef: 'b'.repeat(64), - expiresAt: 1784073660, - auditRef: 'c'.repeat(64), - sourcePointer: 'evaos-desktop-bridge:runtime-receipt', + authorityIssuedAt: 1784073599, + authorityExpiresAt: 1784073659, candidate: { sourceCommit: 'd'.repeat(40), sourceSha256: 'e'.repeat(64), appVersion: '2.1.36', appBuild: '2.1.36', }, - }; + }).envelope; } function writeCompleteMacControlProofSet(proofDir: string): void { @@ -66,9 +58,24 @@ function writeCompleteMacControlProofSet(proofDir: string): void { authorityRedacted: true, }, }; + const deployedRoute = { + schema: 'evaos.mac_control.deployed_route_probe.v1', + sourceHeadSha: 'd'.repeat(40), + sourceRunId: '12345', + checkedAt: '2026-07-15T00:00:01.000Z', + assertions: { + gatewayAuthRequired: true, + postOnly: true, + exactMatch: true, + strictBody: true, + callerAuthorityBodyRejected: true, + sensitiveOutputAbsent: true, + }, + }; const proofs = { 'mac-control-runtime.json': validMacControlRuntimeProof(), 'mac-control-runtime-negative.json': negative, + 'mac-control-deployed-route.json': deployedRoute, 'mac-control-session-provisioning.json': provision, 'mac-control-session-provisioning.stdout.json': provision, 'mac-control-session-cleanup.json': cleanup, @@ -199,6 +206,12 @@ describe('evaOS live canary proof workflow', () => { expect(workflow).toContain('node scripts/evaosProvisionLiveCanaryFixtures.js provision-mac-control'); expect(workflow).toContain('node scripts/evaosBrokerLiveCanary.js --mac-control'); expect(workflow).toContain('> "$PROOF_DIR/mac-control-runtime.json"'); + expect(workflow).toContain( + 'AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_PROBE_OUTPUT: live-canary-proof/mac-control-deployed-route.json' + ); + expect(workflow).toContain('vars.EVAOS_MAC_CONTROL_CONTEXT_KEY_ID'); + expect(workflow).toContain('vars.EVAOS_MAC_CONTROL_RECEIPT_KEY_ID'); + expect(workflow).toContain('vars.EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY'); expect(workflow).toContain('Prove Mac-control runtime-receipt negative boundaries'); const negativeStep = workflow.slice( workflow.indexOf('- name: Prove Mac-control runtime-receipt negative boundaries'), @@ -207,6 +220,10 @@ describe('evaOS live canary proof workflow', () => { expect(negativeStep).toContain( "if: github.event.inputs.run_live_canaries == 'true' && github.event.inputs.run_mac_control_canary == 'true'" ); + expect(negativeStep).toContain( + 'npm --prefix resources/evaos-beta/bridge/agent-tools/openclaw-plugin ci --ignore-scripts --omit=peer --no-audit --no-fund' + ); + expect(workflow).toContain('resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package-lock.json'); expect(negativeStep).toContain('run proof:runtime-receipt-negative --'); expect(negativeStep).toContain('"$PROOF_DIR/mac-control-runtime-negative.json" "$GITHUB_SHA" "$GITHUB_RUN_ID"'); expect(negativeStep).not.toContain('continue-on-error'); @@ -239,7 +256,7 @@ describe('evaOS live canary proof workflow', () => { expect(() => proofScanner.scanMacControlProofDirectory(proofDir)).toThrow(/missing required artifact/i); writeCompleteMacControlProofSet(proofDir); - expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 6 }); + expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 7 }); for (const unsafe of [ { Cookie: 'opaque-cookie-value-123456' }, @@ -276,7 +293,7 @@ describe('evaOS live canary proof workflow', () => { path.join(proofDir, 'mac-control-runtime.json'), `${JSON.stringify(validMacControlRuntimeProof())}\n` ); - expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 6 }); + expect(proofScanner.scanMacControlProofDirectory(proofDir)).toEqual({ ok: true, scanned: 7 }); expect(() => proofScanner.assertMacControlProofSanitized({ keyId: 'public-key-id', diff --git a/tests/unit/evaos/fixtures/signedMacControlAttestation.ts b/tests/unit/evaos/fixtures/signedMacControlAttestation.ts new file mode 100644 index 0000000000..0c3af8f5eb --- /dev/null +++ b/tests/unit/evaos/fixtures/signedMacControlAttestation.ts @@ -0,0 +1,126 @@ +import { createHash, generateKeyPairSync, sign, type KeyObject } from 'node:crypto'; + +export const TEST_RECEIPT_KEY_ID = 'staging-connector-receipt-v1'; +export const TEST_CONTEXT_KEY_ID = 'staging-ws-proxy-context-v1'; +export const PUBLIC_ATTESTATION_NAMESPACE = 'evaos-mac-control-public-attestation-v1'; + +type Candidate = { + sourceCommit: string; + sourceSha256: string; + appVersion: string; + appBuild: string; +}; + +type FixtureOptions = { + runRef: string; + executedAt: string; + authorityIssuedAt: number; + authorityExpiresAt: number; + candidate: Candidate; + privateReceiptSha256?: string; + keyId?: string; + contextKeyId?: string; + keyPair?: { privateKey: KeyObject; publicKey: KeyObject }; + attestationOverrides?: Record; + envelopeOverrides?: Record; +}; + +export function signedMacControlAttestation(options: FixtureOptions) { + const keyPair = options.keyPair ?? generateKeyPairSync('ed25519'); + const keyId = options.keyId ?? TEST_RECEIPT_KEY_ID; + const contextKeyId = options.contextKeyId ?? TEST_CONTEXT_KEY_ID; + const attestation = { + schema: 'evaos.mac_control.public_runtime_attestation.v1', + keyId, + namespace: PUBLIC_ATTESTATION_NAMESPACE, + proofKind: 'selected_binding_direct_mac_control', + runtime: 'openclaw', + tool: 'customer_mac.desktop_hotkey', + outcome: 'succeeded', + runRef: options.runRef, + executedAt: options.executedAt, + authorityIssuedAt: options.authorityIssuedAt, + authorityExpiresAt: options.authorityExpiresAt, + contextKeyId, + controlState: 'ready_unchanged', + auditRecorded: true, + privateReceiptSha256: options.privateReceiptSha256 ?? 'f'.repeat(64), + connectorCandidate: options.candidate, + ...options.attestationOverrides, + }; + const attestationBytes = canonicalBytes(attestation); + const rawPublicKey = keyPair.publicKey.export({ format: 'der', type: 'spki' }).subarray(-32); + return { + envelope: { + schema: 'evaos.mac_control.public_runtime_attestation_envelope.v1', + attestationBase64: attestationBytes.toString('base64url'), + signature: sshSignature(attestationBytes, keyPair.privateKey, rawPublicKey), + keyId, + namespace: PUBLIC_ATTESTATION_NAMESPACE, + ...options.envelopeOverrides, + }, + attestation, + keyPair, + trust: { + receiptKeyId: keyId, + receiptPublicKey: rawPublicKey.toString('base64url'), + contextKeyId, + }, + }; +} + +export function canonicalBytes(value: unknown): Buffer { + return Buffer.from(JSON.stringify(sortJson(value))); +} + +function sshSignature(message: Buffer, privateKey: KeyObject, rawPublicKey: Buffer): string { + const algorithm = Buffer.from('ssh-ed25519'); + const namespace = Buffer.from(PUBLIC_ATTESTATION_NAMESPACE); + const hashAlgorithm = Buffer.from('sha512'); + const digest = createHash('sha512').update(message).digest(); + const signedData = Buffer.concat([ + Buffer.from('SSHSIG'), + sshString(namespace), + sshString(Buffer.alloc(0)), + sshString(hashAlgorithm), + sshString(digest), + ]); + const signature = sign(null, signedData, privateKey); + const publicKeyBlob = Buffer.concat([sshString(algorithm), sshString(rawPublicKey)]); + const signatureBlob = Buffer.concat([sshString(algorithm), sshString(signature)]); + const binary = Buffer.concat([ + Buffer.from('SSHSIG'), + uint32(1), + sshString(publicKeyBlob), + sshString(namespace), + sshString(Buffer.alloc(0)), + sshString(hashAlgorithm), + sshString(signatureBlob), + ]); + const encoded = binary + .toString('base64') + .match(/.{1,70}/g)! + .join('\n'); + return `-----BEGIN SSH SIGNATURE-----\n${encoded}\n-----END SSH SIGNATURE-----\n`; +} + +function uint32(value: number): Buffer { + const buffer = Buffer.alloc(4); + buffer.writeUInt32BE(value, 0); + return buffer; +} + +function sshString(value: Buffer): Buffer { + return Buffer.concat([uint32(value.length), value]); +} + +function sortJson(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sortJson); + if (value === null || typeof value !== 'object') return value; + const record = value as Record; + return Object.fromEntries( + Object.keys(record) + .toSorted() + .map((key) => [key, sortJson(record[key])]) + ); +} diff --git a/tests/unit/process/evaosBetaReleaseGate.test.ts b/tests/unit/process/evaosBetaReleaseGate.test.ts index 67e730b349..3d720b1787 100644 --- a/tests/unit/process/evaosBetaReleaseGate.test.ts +++ b/tests/unit/process/evaosBetaReleaseGate.test.ts @@ -1,10 +1,15 @@ import { createRequire } from 'node:module'; import { execFileSync, spawnSync } from 'node:child_process'; -import { createHash } from 'node:crypto'; +import { createHash, generateKeyPairSync } from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { + signedMacControlAttestation, + TEST_CONTEXT_KEY_ID, + TEST_RECEIPT_KEY_ID, +} from '../evaos/fixtures/signedMacControlAttestation'; const require = createRequire(import.meta.url); const releaseGate = require('../../../scripts/evaosBetaReleaseGate.js') as { @@ -154,6 +159,15 @@ const liveCanaryProofEnv = { EVAOS_LIVE_CANARY_EXPECTED_CUSTOMER_ID: 'cus_123', EVAOS_LIVE_CANARY_MAX_PROOF_AGE_HOURS: '24', }; +const macControlTestKeyPair = generateKeyPairSync('ed25519'); +const macControlProofTrustEnv = { + EVAOS_LIVE_CANARY_CONTEXT_KEY_ID: TEST_CONTEXT_KEY_ID, + EVAOS_LIVE_CANARY_RECEIPT_KEY_ID: TEST_RECEIPT_KEY_ID, + EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY: macControlTestKeyPair.publicKey + .export({ format: 'der', type: 'spki' }) + .subarray(-32) + .toString('base64url'), +}; function writeArm64TrustEvidence(proofDir: string) { fs.writeFileSync(path.join(proofDir, 'codesign-dmg-macos-arm64.txt'), 'evaOS Workbench.dmg: valid on disk\n'); @@ -821,7 +835,29 @@ function mutateBrokerLiveCanaryProof(proofDir: string, mutator: (proof: Record = {}) { fs.mkdirSync(proofDir, { recursive: true }); const sourceSha256 = releaseGate.committedBridgeSourceIdentity(fixtureReleaseCommit).sourceSha256; - const executedAt = Date.now(); + const executedAtText = String(overrides.executedAt || new Date().toISOString()); + const executedAt = Date.parse(executedAtText); + const executedAtSeconds = Math.floor(executedAt / 1000); + const authorityIssuedAt = Number(overrides.authorityIssuedAt ?? executedAtSeconds); + const authorityExpiresAt = Number(overrides.authorityExpiresAt ?? overrides.expiresAt ?? executedAtSeconds + 59); + const candidateOverrides = (overrides.candidate as Record | undefined) || {}; + const candidate = { + sourceCommit: String(candidateOverrides.sourceCommit || fixtureReleaseCommit), + sourceSha256: String(candidateOverrides.sourceSha256 || sourceSha256), + appVersion: String(candidateOverrides.appVersion || '2.1.36'), + appBuild: String(candidateOverrides.appBuild || '2.1.36'), + }; + const signed = signedMacControlAttestation({ + runRef: String(overrides.runRef || 'gha:12345:111111111111111111111111'), + executedAt: executedAtText, + authorityIssuedAt, + authorityExpiresAt, + candidate, + privateReceiptSha256: String(overrides.privateReceiptSha256 || 'f'.repeat(64)), + keyPair: macControlTestKeyPair, + attestationOverrides: (overrides.attestationOverrides as Record | undefined) || {}, + envelopeOverrides: (overrides.envelopeOverrides as Record | undefined) || {}, + }); fs.writeFileSync( path.join(proofDir, 'mac-control-session-provisioning.json'), `${JSON.stringify( @@ -839,48 +875,42 @@ function writeMacControlLiveCanaryProof(proofDir: string, overrides: Record | undefined), }, null, 2 @@ -898,6 +928,7 @@ function writeMacControlLiveCanaryProof(proofDir: string, overrides: Record { @@ -2376,6 +2413,7 @@ printf '%s\\n' ok const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-live-mac-control-proof-')); const releaseEnv = { ...liveCanaryProofEnv, + ...macControlProofTrustEnv, EVAOS_REQUIRE_MAC_CONTROL_LIVE_CANARY_PROOF: 'true', EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: fixtureReleaseCommit, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '12345', @@ -2395,6 +2433,7 @@ printf '%s\\n' ok it('requires sanitized proof that the temporary Mac-control session was revoked', () => { const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-live-mac-control-cleanup-proof-')); const releaseEnv = { + ...macControlProofTrustEnv, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: fixtureReleaseCommit, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '12345', }; @@ -2421,6 +2460,7 @@ printf '%s\\n' ok it('requires sanitized proof of the database-backed staging marker', () => { const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-live-mac-control-staging-marker-')); const releaseEnv = { + ...macControlProofTrustEnv, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: fixtureReleaseCommit, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '12345', }; @@ -2435,26 +2475,27 @@ printf '%s\\n' ok } }); - it('rejects failed, incomplete, unsafe, or wrong-head Mac-control release proof', () => { + it('rejects failed, incomplete, unsafe, tampered, or wrong-head Mac-control release proof', () => { const releaseEnv = { + ...macControlProofTrustEnv, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: fixtureReleaseCommit, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '12345', }; - const cases: Array<{ name: string; mutate: (proof: Record) => void; error: RegExp }> = [ + const cases: Array<{ + name: string; + options?: Record; + mutate?: (proof: Record) => void; + error: RegExp; + }> = [ { name: 'failed', - mutate: (proof) => { - proof.ok = false; - proof.outcome = 'failed'; - }, - error: /successful direct-control runtime receipt/, + options: { attestationOverrides: { outcome: 'failed' } }, + error: /successful signed direct-control attestation/i, }, { name: 'incomplete', - mutate: (proof) => { - proof.bindingRef = 'not-a-reference'; - }, - error: /runtime receipt fields/i, + options: { runRef: 'not-a-run-reference' }, + error: /proof run/i, }, { name: 'unsafe', @@ -2464,11 +2505,15 @@ printf '%s\\n' ok error: /forbidden field|secret material/, }, { - name: 'wrong-head', + name: 'tampered-signature', mutate: (proof) => { - const candidate = proof.candidate as Record; - candidate.sourceCommit = 'b'.repeat(40); + proof.signature = String(proof.signature).replace('A', 'B'); }, + error: /signature is invalid/i, + }, + { + name: 'wrong-head', + options: { candidate: { sourceCommit: 'b'.repeat(40) } }, error: /candidate.*release commit/i, }, ]; @@ -2476,11 +2521,13 @@ printf '%s\\n' ok for (const testCase of cases) { const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), `evaos-live-mac-control-${testCase.name}-`)); try { - writeMacControlLiveCanaryProof(proofDir); + writeMacControlLiveCanaryProof(proofDir, testCase.options); const proofPath = path.join(proofDir, 'mac-control-runtime.json'); const proof = JSON.parse(fs.readFileSync(proofPath, 'utf8')) as Record; - testCase.mutate(proof); - fs.writeFileSync(proofPath, `${JSON.stringify(proof, null, 2)}\n`); + if (testCase.mutate) { + testCase.mutate(proof); + fs.writeFileSync(proofPath, `${JSON.stringify(proof, null, 2)}\n`); + } expect(() => releaseGate.verifyMacControlLiveCanaryProof(proofDir, releaseEnv)).toThrow(testCase.error); } finally { fs.rmSync(proofDir, { recursive: true, force: true }); @@ -2488,9 +2535,56 @@ printf '%s\\n' ok } }); + it('rejects legacy unsigned evidence, artifact-supplied trust, and the wrong external receipt key', () => { + const releaseEnv = { + ...macControlProofTrustEnv, + EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: fixtureReleaseCommit, + EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '12345', + }; + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-live-mac-control-trust-boundary-')); + try { + writeMacControlLiveCanaryProof(proofDir); + const proofPath = path.join(proofDir, 'mac-control-runtime.json'); + const envelope = JSON.parse(fs.readFileSync(proofPath, 'utf8')) as Record; + + fs.writeFileSync( + proofPath, + `${JSON.stringify({ + schema: 'evaos.mac_control.runtime_proof.v2', + ok: true, + outcome: 'succeeded', + candidate: { sourceCommit: fixtureReleaseCommit }, + })}\n` + ); + expect(() => releaseGate.verifyMacControlLiveCanaryProof(proofDir, releaseEnv)).toThrow( + /forbidden field|signed-attestation fields/i + ); + + fs.writeFileSync( + proofPath, + `${JSON.stringify({ ...envelope, publicKey: macControlProofTrustEnv.EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY })}\n` + ); + expect(() => releaseGate.verifyMacControlLiveCanaryProof(proofDir, releaseEnv)).toThrow(/forbidden field/i); + + fs.writeFileSync(proofPath, `${JSON.stringify(envelope)}\n`); + const wrongKeyPair = generateKeyPairSync('ed25519'); + const wrongKeyEnv = { + ...releaseEnv, + EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY: wrongKeyPair.publicKey + .export({ format: 'der', type: 'spki' }) + .subarray(-32) + .toString('base64url'), + }; + expect(() => releaseGate.verifyMacControlLiveCanaryProof(proofDir, wrongKeyEnv)).toThrow(/signature is invalid/i); + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } + }); + it('requires a fresh, canonical, bounded historical runtime receipt', () => { const verificationNow = Date.parse('2026-07-15T12:00:00.000Z'); const releaseEnv = { + ...macControlProofTrustEnv, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: fixtureReleaseCommit, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '12345', EVAOS_LIVE_CANARY_MAX_PROOF_AGE_HOURS: '24', @@ -2541,7 +2635,7 @@ printf '%s\\n' ok now: new Date(verificationNow), maxAgeHours: 24, }) - ).toThrow(/runtime receipt fields/i); + ).toThrow(/signed attestation fields/i); } finally { fs.rmSync(proofDir, { recursive: true, force: true }); } @@ -2558,7 +2652,7 @@ printf '%s\\n' ok now: new Date(verificationNow), maxAgeHours: 24, }) - ).toThrow(/runtime receipt fields/i); + ).toThrow(/signed attestation fields/i); } finally { fs.rmSync(noncanonicalDir, { recursive: true, force: true }); } @@ -2566,6 +2660,7 @@ printf '%s\\n' ok it('rejects missing, false, mismatched, or extended runtime-receipt negative proof', () => { const releaseEnv = { + ...macControlProofTrustEnv, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: fixtureReleaseCommit, EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '12345', }; @@ -2620,6 +2715,81 @@ printf '%s\\n' ok } }); + it('rejects missing, false, mismatched, stale, or extended deployed-route proof', () => { + const verificationNow = new Date('2026-07-15T12:00:00.000Z'); + const releaseEnv = { + ...macControlProofTrustEnv, + EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: fixtureReleaseCommit, + EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '12345', + EVAOS_LIVE_CANARY_MAX_PROOF_AGE_HOURS: '24', + }; + const cases: Array<{ + name: string; + mutate: (proofDir: string, proof: Record) => void; + error: RegExp; + }> = [ + { + name: 'missing', + mutate: (proofDir) => fs.rmSync(path.join(proofDir, 'mac-control-deployed-route.json')), + error: /deployed route probe/i, + }, + { + name: 'false-assertion', + mutate: (_proofDir, proof) => { + (proof.assertions as Record).strictBody = false; + }, + error: /deployed route assertions/i, + }, + { + name: 'wrong-head', + mutate: (_proofDir, proof) => { + proof.sourceHeadSha = 'e'.repeat(40); + }, + error: /exact release run/i, + }, + { + name: 'wrong-run', + mutate: (_proofDir, proof) => { + proof.sourceRunId = '99999'; + }, + error: /exact release run/i, + }, + { + name: 'stale', + mutate: (_proofDir, proof) => { + proof.checkedAt = '2026-07-13T11:59:59.000Z'; + }, + error: /stale/i, + }, + { + name: 'extra-field', + mutate: (_proofDir, proof) => { + proof.note = 'must not be accepted'; + }, + error: /exact release run/i, + }, + ]; + + for (const testCase of cases) { + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), `evaos-live-mac-control-route-${testCase.name}-`)); + try { + writeMacControlLiveCanaryProof(proofDir, { + executedAt: '2026-07-15T11:59:00.000Z', + authorityExpiresAt: Date.parse('2026-07-15T12:00:00.000Z') / 1000, + }); + const proofPath = path.join(proofDir, 'mac-control-deployed-route.json'); + const proof = JSON.parse(fs.readFileSync(proofPath, 'utf8')) as Record; + testCase.mutate(proofDir, proof); + if (fs.existsSync(proofPath)) fs.writeFileSync(proofPath, `${JSON.stringify(proof, null, 2)}\n`); + expect(() => + releaseGate.verifyMacControlLiveCanaryProof(proofDir, releaseEnv, { now: verificationNow, maxAgeHours: 24 }) + ).toThrow(testCase.error); + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } + } + }); + it('uses the broker canary customer as the expected live broker proof customer when configured', () => { const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-live-broker-proof-broker-customer-')); try { diff --git a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts index 089a5fafdd..e5b1a2538c 100644 --- a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts +++ b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts @@ -647,10 +647,13 @@ describe('prepareEvaosDesktopBridgeResource', () => { it('binds pre-canary and QA reports to the exact installed Workbench source manifest', () => { const sourceDir = join(process.cwd(), 'resources', 'evaos-beta', 'bridge', 'src'); const script = [ + 'import base64', 'import json', + 'import os', + 'import subprocess', 'from pathlib import Path', 'from tempfile import TemporaryDirectory', - 'from evaos_desktop_bridge import pre_canary, qa_canary', + 'from evaos_desktop_bridge import pre_canary, qa_canary, receipt_canary', 'from evaos_desktop_bridge.candidate_identity import source_tree_sha256', 'commit = "0123456789abcdef0123456789abcdef01234567"', 'with TemporaryDirectory() as root:', @@ -699,8 +702,22 @@ describe('prepareEvaosDesktopBridgeResource', () => { ' assert_connector_rejected(lambda payload: payload["connector"]["owner"].__setitem__("app_path", {"kind": "path", "value": "/Applications/evaOS Workbench.app/../Other.app"}))', ' assert_connector_rejected(lambda payload: payload["connector"]["owner"].__setitem__("manifest_path", {"kind": "path", "value": "/tmp/manifest.json"}))', ' selected_proof_path = Path(root) / "mac-control-runtime.json"', - ' selected_proof = {"ok": True, "schema": "evaos.mac_control.runtime_proof.v2", "proofKind": "selected_binding_direct_mac_control", "tool": "customer_mac.desktop_hotkey", "outcome": "succeeded", "runRef": "gha:12345:" + "1" * 24, "executedAt": "2026-07-15T00:00:00Z", "bindingRef": "a" * 64, "bindingVersion": "7", "sessionRef": "b" * 64, "expiresAt": 1784073660, "auditRef": "c" * 64, "sourcePointer": "evaos-desktop-bridge:runtime-receipt", "candidate": {"sourceCommit": commit, "sourceSha256": source_sha256, "appVersion": "2.1.36", "appBuild": "2.1.36"}}', + ' signer_dir = Path(root) / "signer"', + ' signer_dir.mkdir(mode=0o700)', + ' signer_path = signer_dir / "receipt_ed25519"', + ' subprocess.run(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(signer_path)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)', + ' os.chmod(signer_path, 0o600)', + ' public_blob = base64.b64decode(signer_path.with_suffix(".pub").read_text(encoding="ascii").split()[1])', + ' receipt_key_id = "staging-connector-receipt-v1"', + ' context_key_id = "staging-ws-proxy-context-v1"', + ' signer_config = receipt_canary.CanaryConfig(context_key_id=context_key_id, context_public_key=b"p" * 32, receipt_key_id=receipt_key_id, receipt_private_key=signer_path)', + ' private_receipt = {"runRef": "gha:12345:" + "1" * 24, "executedAt": "2026-07-15T00:00:00Z", "contextIssuedAt": 1784073600, "contextExpiresAt": 1784073660, "contextKeyId": context_key_id, "candidate": {"sourceCommit": commit, "sourceSha256": source_sha256, "appVersion": "2.1.36", "appBuild": "2.1.36"}}', + ' public_attestation = receipt_canary.build_public_attestation(private_receipt, signer_config)', + ' selected_proof = receipt_canary.public_attestation_envelope(public_attestation, receipt_canary.sign_public_attestation(public_attestation, signer_config), signer_config)', ' selected_proof_path.write_text(json.dumps(selected_proof), encoding="utf-8")', + ' os.environ["EVAOS_LIVE_CANARY_CONTEXT_KEY_ID"] = context_key_id', + ' os.environ["EVAOS_LIVE_CANARY_RECEIPT_KEY_ID"] = receipt_key_id', + ' os.environ["EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY"] = base64.urlsafe_b64encode(public_blob[-32:]).decode("ascii").rstrip("=")', ' selected_binding = qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="12345", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36")', ' assert selected_binding["ok"] is True, selected_binding', ' assert qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="54321", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36")["ok"] is False', From 560f3e3e305f7f0ea9cb777daad4c45a24e33731 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 07:29:48 +0700 Subject: [PATCH 08/17] fix(evaos): close Mac-control review gaps --- CHANGELOG.md | 5 ++ .../evaos-beta/bridge/agent-tools/SOURCE.json | 16 ++--- .../agent-tools/hermes-adapter/README.md | 28 +++++---- .../bin/evaos-desktop-bridge-command | 58 +++++++++++++----- .../openclaw-plugin/dist/src/bridge.js | 16 +++++ .../agent-tools/openclaw-plugin/src/bridge.ts | 16 +++++ .../tests/runtimeReceipt.test.mjs | 60 +++++++++++++++++-- .../src/evaos_desktop_bridge/qa_canary.py | 9 +++ .../unit/evaos/evaosBrokerLiveCanary.test.ts | 1 + .../prepareEvaosDesktopBridgeResource.test.ts | 5 +- 10 files changed, 173 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d28b7f61..f00978de10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ enrollment window, and surfaces a strictly sanitized rejection instead of leaving the button apparently inert. Duplicate enrollment clicks remain disabled while the first action is in flight. +- Makes the Hermes adapter parse only the two connector assignments instead of + executing an environment file, carries JSON parameters over standard input, + and rejects caller-selected local payload paths in the OpenClaw fallback. ### Selected-Binding Mac-control Canary @@ -62,6 +65,8 @@ - Adds deployed-route behavior probes for gateway authentication, POST-only routing, exact path matching, strict body validation, and rejection of caller-supplied authority before Mac-control proof can authorize publication. +- Requires the installed-app QA binding check to consume the signed selected- + binding attestation before its short-lived execution authority expires. ### Release Authorization And Bundle Integrity diff --git a/resources/evaos-beta/bridge/agent-tools/SOURCE.json b/resources/evaos-beta/bridge/agent-tools/SOURCE.json index 9631ac6663..d953ee68f7 100644 --- a/resources/evaos-beta/bridge/agent-tools/SOURCE.json +++ b/resources/evaos-beta/bridge/agent-tools/SOURCE.json @@ -4,15 +4,15 @@ "version": "0.2.0", "status": "vendored", "releaseTagNamespace": "evaos-desktop-bridge-openclaw-vX.Y.Z", - "importedFrom": "electricsheephq/evaos-desktop-bridge", - "importedCommit": "b3e8da7ae90c86b7a999b6a0ef5d7e4bcb83b2c8", - "importedAt": "2026-07-15", + "canonicalRepository": "100yenadmin/evaOS-GUI", + "sourcePath": "resources/evaos-beta/bridge/agent-tools", + "canonicalizedAt": "2026-07-15", "sourceDigests": { - "hermes-adapter/README.md": "sha256:28517b43d41370c8481f92936a1a4156ce5827110122876e9b09373e7c94e9f4", - "hermes-adapter/bin/evaos-desktop-bridge-command": "sha256:71b6dde4fe392a2a4ce2854aae8935dd905701ea07da244afbb37cc05be44232", + "hermes-adapter/README.md": "sha256:1bbef6050a512f42151dc5dc895474b2258ebf887192fe23796583e0b14b6a78", + "hermes-adapter/bin/evaos-desktop-bridge-command": "sha256:cf21cce6738b02047a506cb8809f70b9573a19877692ac466a03b57c4a3167ce", "openclaw-plugin/README.md": "sha256:ce371b7ed539092b7cad9245883f8fb5e91c0c8f56ff094164a0635716128737", "openclaw-plugin/dist/index.js": "sha256:5634b0a23dfc08ba8640343cd4b3bf9a2fa77e496884924d39069fc97138ab91", - "openclaw-plugin/dist/src/bridge.js": "sha256:a057903f228906b5206f7dd9cf2b43c9fb721cae8c1fa3822c7eb84f7434a66f", + "openclaw-plugin/dist/src/bridge.js": "sha256:625b964db6c16a3e7e574d4c86c757a1a11861af01daa6c4abb5c7cdcb947abf", "openclaw-plugin/dist/src/firewall.js": "sha256:939cdd1070bf02aade901d899a67e81e52ef2c6f554171f2039ba8419ffc630a", "openclaw-plugin/dist/src/runtimeReceipt.js": "sha256:23cfb4db488e255ba5384c2f42429f12ad077dcd004a70f2e941984f3f3dd8b0", "openclaw-plugin/index.ts": "sha256:a3d54d4ef87fca3435ae0c3ff86f3947950131b6d29c8cfa66944c4dd034bc12", @@ -21,11 +21,11 @@ "openclaw-plugin/package.json": "sha256:78ac262042b80c509a11419a0a60ce48db24054dad043f4de37a3ab7540b02ff", "openclaw-plugin/scripts/qa-run-bridge.mjs": "sha256:648b24a86431254c25907ecf6906f1b0070b8701165fa1cb92dc9d9068e67dd2", "openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs": "sha256:6a91ee0fc8ba7269ee5ff8ca3a79d7bfadb9139ed1eb629642c9c2a688130062", - "openclaw-plugin/src/bridge.ts": "sha256:b03486bb226e3a2aca65a70074d82822bf510291295f858ad44dcd5e26a8c896", + "openclaw-plugin/src/bridge.ts": "sha256:d0e0da280bd83dc71734579d12bbb8c37a50c5e8d746fb4ac967e7b69862a1f1", "openclaw-plugin/src/firewall.ts": "sha256:85e638930cd5a412fc059e4cd2e2c8db41d29102319fe047bfc88601c7911668", "openclaw-plugin/src/runtimeReceipt.ts": "sha256:e88ba64b2113d0f63897ff8c599d5dc9dafe28a631c24a454039395967a2ecf3", "openclaw-plugin/src/types.d.ts": "sha256:6144e747e97c133ddaefe37af986865d6b87d842b922026a2af14b010c946bfc", - "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:455b5e1a4cec736a718a8ee58b27cefacab15d923433d20f20a41aed3ea2d6a2", + "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:c2f6f475e7d5cb14a6c721d70cebb59bd35ee993845ac19b9c56ce9ef9b7441a", "openclaw-plugin/tsconfig.json": "sha256:d5ceeadf205f97973f4d2a1e13ef20f9af854c1fbc01823af60b02eed5d5b34b" } } diff --git a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md index 05088a0193..efc39a7863 100644 --- a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md +++ b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/README.md @@ -7,9 +7,10 @@ mutation. ## Runtime contract -Set these on the customer VM after the Mac is paired. The wrapper automatically -sources `/root/.openclaw/evaos-desktop-bridge.env` when the variables are not -already present, so OpenClaw, Hermes, and direct support smokes all use the same +Set these on the customer VM after the Mac is paired. When the variables are not +already present, the wrapper parses only the two expected assignments from +`/root/.openclaw/evaos-desktop-bridge.env`; it never executes the file as shell +code. OpenClaw, Hermes, and direct support smokes therefore use the same connector contract. ```bash @@ -24,16 +25,21 @@ Hermes tools should call `bin/evaos-desktop-bridge-command` with one of the fixed connector command names supported by `/v1/commands`, for example: ```bash -hermes-adapter/bin/evaos-desktop-bridge-command customerMacStatus '{}' -hermes-adapter/bin/evaos-desktop-bridge-command desktopSee '{}' -hermes-adapter/bin/evaos-desktop-bridge-command desktopClick '{"target_label":"Continue","dry_run":false}' -hermes-adapter/bin/evaos-desktop-bridge-command customerMacIphoneMirroringStatus '{}' -hermes-adapter/bin/evaos-desktop-bridge-command iphoneSwipe '{"direction":"up","dry_run":false}' -hermes-adapter/bin/evaos-desktop-bridge-command evaosProviderProfiles '{}' -hermes-adapter/bin/evaos-desktop-bridge-command evaosProviderCompleteAuth '{"identity":"admin@100yen.org"}' -hermes-adapter/bin/evaos-desktop-bridge-command evaosSharedBrowserGuidance '{}' +hermes-adapter/bin/evaos-desktop-bridge-command customerMacStatus +hermes-adapter/bin/evaos-desktop-bridge-command desktopSee +printf '%s' '{"target_label":"Continue","dry_run":false}' | hermes-adapter/bin/evaos-desktop-bridge-command desktopClick - +hermes-adapter/bin/evaos-desktop-bridge-command customerMacIphoneMirroringStatus +printf '%s' '{"direction":"up","dry_run":false}' | hermes-adapter/bin/evaos-desktop-bridge-command iphoneSwipe - +hermes-adapter/bin/evaos-desktop-bridge-command evaosProviderProfiles +printf '%s' '{"identity":"admin@100yen.org"}' | hermes-adapter/bin/evaos-desktop-bridge-command evaosProviderCompleteAuth - +hermes-adapter/bin/evaos-desktop-bridge-command evaosSharedBrowserGuidance ``` +The wrapper rejects JSON in process arguments. Non-empty parameter objects must +be supplied on standard input with the literal `-` marker so message text, +URLs, and other customer data do not appear in process listings or shell +history. + Provider/Auth Hub and Shared Browser guidance commands read optional `EVAOS_PROVIDER_PROFILES_JSON`, `EVAOS_PROVIDER_GRANTS_JSON`, `EVAOS_ACTIVE_PROVIDER_KEY`, `EVAOS_SHARED_BROWSER_STATUS_JSON`, and diff --git a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command index d1aab5a885..450c98d07d 100755 --- a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command +++ b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command @@ -2,31 +2,58 @@ set -euo pipefail command_name="${1:-}" -if [[ $# -ge 2 ]]; then - params_json="$2" -else - params_json="{}" +if [[ -z "${command_name}" || $# -gt 2 || ( $# -eq 2 && "$2" != "-" ) ]]; then + echo "usage: evaos-desktop-bridge-command [-]" >&2 + echo "Pass JSON parameters on stdin when the second argument is -." >&2 + exit 2 fi -if [[ "${params_json}" == "-" ]]; then + +params_json="{}" +if [[ $# -eq 2 ]]; then params_json="$(cat)" fi -if [[ -z "${command_name}" ]]; then - echo "usage: evaos-desktop-bridge-command [params-json]" >&2 - exit 2 -fi +read_connector_env_file() { + local env_file="$1" + local line key value + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%$'\r'}" + [[ -z "$line" || "$line" == \#* ]] && continue + [[ "$line" == *=* ]] || { + echo "invalid connector environment entry" >&2 + return 2 + } + key="${line%%=*}" + value="${line#*=}" + case "$key" in + EVAOS_DESKTOP_BRIDGE_URL) + EVAOS_DESKTOP_BRIDGE_URL="$value" + export EVAOS_DESKTOP_BRIDGE_URL + ;; + EVAOS_DESKTOP_BRIDGE_TOKEN) + EVAOS_DESKTOP_BRIDGE_TOKEN="$value" + export EVAOS_DESKTOP_BRIDGE_TOKEN + ;; + *) + echo "invalid connector environment key" >&2 + return 2 + ;; + esac + done < "$env_file" +} if [[ -z "${EVAOS_DESKTOP_BRIDGE_URL:-}" || -z "${EVAOS_DESKTOP_BRIDGE_TOKEN:-}" ]]; then env_file="${EVAOS_DESKTOP_BRIDGE_ENV_FILE:-/root/.openclaw/evaos-desktop-bridge.env}" if [[ -f "${env_file}" ]]; then - set -a - # shellcheck source=/dev/null - . "${env_file}" - set +a + [[ ! -L "${env_file}" ]] || { + echo "connector environment file must not be a symlink" >&2 + exit 2 + } + read_connector_env_file "${env_file}" fi fi -python3 - "$command_name" "$params_json" <<'PY' +python3 - "$command_name" 3<<<"$params_json" <<'PY' import base64 import datetime import hashlib @@ -43,7 +70,8 @@ import uuid command = sys.argv[1] try: - params = json.loads(sys.argv[2] or "{}") + with os.fdopen(3, "r", encoding="utf-8") as params_stream: + params = json.loads(params_stream.read() or "{}") except json.JSONDecodeError as exc: raise SystemExit(f"invalid params JSON: {exc}") from exc diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js index f73eb97e98..66f53f3c48 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/bridge.js @@ -639,6 +639,22 @@ export async function runBridge(command, params = {}) { ], }; } + if ( + Object.prototype.hasOwnProperty.call(params, 'message_file') || + Object.prototype.hasOwnProperty.call(params, 'value_file') + ) { + return { + ok: false, + errors: [ + { + code: 'payload_file_reserved', + message: 'Payload file paths are reserved for evaOS Desktop Bridge internals.', + guidance: + 'Provide message or value content directly; evaOS will materialize it in a protected temporary file.', + }, + ], + }; + } if (command === 'evaosProviderProfiles') { return await providerProfilesPayload(); } diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts index 9f2a83c006..fdf17db3a3 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/bridge.ts @@ -830,6 +830,22 @@ export async function runBridge(command: BridgeCommandKey, params: BridgeParams ], }; } + if ( + Object.prototype.hasOwnProperty.call(params, 'message_file') || + Object.prototype.hasOwnProperty.call(params, 'value_file') + ) { + return { + ok: false, + errors: [ + { + code: 'payload_file_reserved', + message: 'Payload file paths are reserved for evaOS Desktop Bridge internals.', + guidance: + 'Provide message or value content directly; evaOS will materialize it in a protected temporary file.', + }, + ], + }; + } if (command === 'evaosProviderProfiles') { return await providerProfilesPayload(); } diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs index 126a63efa3..1a6ae8521a 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { createHash, generateKeyPairSync, sign } from 'node:crypto'; -import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'; +import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Readable } from 'node:stream'; @@ -61,8 +61,8 @@ test('does not expose or execute the removed legacy pairing-code surface', async const hermes = spawnSync( fileURLToPath(new URL('../../hermes-adapter/bin/evaos-desktop-bridge-command', import.meta.url)), - ['completeEnrollment', '{"enrollment_code":"must-not-be-used"}'], - { encoding: 'utf8', env: { PATH: process.env.PATH } } + ['completeEnrollment', '-'], + { encoding: 'utf8', env: { PATH: process.env.PATH }, input: '{"enrollment_code":"must-not-be-used"}' } ); assert.equal(hermes.status, 0); assert.equal(JSON.parse(hermes.stdout).errors[0].code, 'legacy_pairing_removed'); @@ -80,13 +80,63 @@ test('does not advertise or execute remote Mac-control start', async () => { const hermes = spawnSync( fileURLToPath(new URL('../../hermes-adapter/bin/evaos-desktop-bridge-command', import.meta.url)), - ['customerMacControlStart', '{"mode":"full-access"}'], - { encoding: 'utf8', env: { PATH: process.env.PATH } } + ['customerMacControlStart', '-'], + { encoding: 'utf8', env: { PATH: process.env.PATH }, input: '{"mode":"full-access"}' } ); assert.equal(hermes.status, 0); assert.equal(JSON.parse(hermes.stdout).errors[0].code, 'control_start_local_only'); }); +test('Hermes reads params from stdin and never executes connector env files', () => { + const wrapper = fileURLToPath(new URL('../../hermes-adapter/bin/evaos-desktop-bridge-command', import.meta.url)); + const argvPayload = spawnSync(wrapper, ['customerMacStatus', '{}'], { + encoding: 'utf8', + env: { PATH: process.env.PATH }, + }); + assert.equal(argvPayload.status, 2); + assert.match(argvPayload.stderr, /standard input|Pass JSON parameters/); + + const directory = mkdtempSync(join(tmpdir(), 'evaos-hermes-env-')); + const envFile = join(directory, 'connector.env'); + const marker = join(directory, 'must-not-exist'); + try { + writeFileSync( + envFile, + `EVAOS_DESKTOP_BRIDGE_URL=$(touch ${marker})\nEVAOS_DESKTOP_BRIDGE_TOKEN=test-token-value-at-least-24\n`, + { encoding: 'utf8', mode: 0o600 } + ); + chmodSync(envFile, 0o600); + const result = spawnSync(wrapper, ['customerMacStatus'], { + encoding: 'utf8', + env: { + PATH: process.env.PATH, + EVAOS_DESKTOP_BRIDGE_ENV_FILE: envFile, + }, + }); + assert.notEqual(result.status, 0); + assert.equal(existsSync(marker), false); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +test('rejects caller-supplied local payload file paths', async () => { + const message = await runBridge('codexSendVisibleMessage', { + thread_id: 'thread-1', + message_file: '/etc/passwd', + }); + assert.equal(message.ok, false); + assert.equal(message.errors[0].code, 'payload_file_reserved'); + + const value = await runBridge('desktopSetValue', { + snapshot_id: 'snapshot-1', + element_id: 'element-1', + value_file: '/etc/passwd', + }); + assert.equal(value.ok, false); + assert.equal(value.errors[0].code, 'payload_file_reserved'); +}); + test('verifies server authority and forwards only the fixed connector canary contract', async () => { const challenge = Buffer.alloc(32, 7).toString('base64url'); const runRef = 'rc-2.1.36-arm64'; diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py index b75d31e8d0..f9ad45dd72 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py @@ -847,6 +847,7 @@ def selected_binding_proof_binding( expected_source_sha256: str, expected_version: str, expected_build: str, + verification_time_seconds: float | None = None, ) -> dict[str, Any]: result: dict[str, Any] = { "ok": False, @@ -976,6 +977,11 @@ def selected_binding_proof_binding( executed_at = None issued_at = attestation.get("authorityIssuedAt") expires_at = attestation.get("authorityExpiresAt") + verification_time = ( + time.time() + if verification_time_seconds is None + else float(verification_time_seconds) + ) result.update( { "schema": proof.get("schema"), @@ -1017,7 +1023,10 @@ def selected_binding_proof_binding( and type(issued_at) is int and type(expires_at) is int and issued_at < expires_at <= issued_at + 60 + and issued_at <= verification_time + 5 + and verification_time < expires_at and executed_at.timestamp() >= issued_at - 5 + and executed_at.timestamp() <= verification_time + 5 and int(executed_at.timestamp()) <= expires_at and set(candidate) == SELECTED_BINDING_CANDIDATE_FIELDS and candidate.get("sourceCommit") == expected_source_commit diff --git a/tests/unit/evaos/evaosBrokerLiveCanary.test.ts b/tests/unit/evaos/evaosBrokerLiveCanary.test.ts index a580e10136..dd9b461eae 100644 --- a/tests/unit/evaos/evaosBrokerLiveCanary.test.ts +++ b/tests/unit/evaos/evaosBrokerLiveCanary.test.ts @@ -839,6 +839,7 @@ describe('evaOS broker live canary', () => { ` expected_source_sha256=${JSON.stringify(expectedCandidate.sourceSha256)},`, ' expected_version="2.1.36",', ' expected_build="2.1.36",', + ` verification_time_seconds=${now / 1000},`, ')', 'assert result["ok"] is True, result', 'print("ok")', diff --git a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts index e5b1a2538c..dc50a43cbe 100644 --- a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts +++ b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts @@ -718,9 +718,10 @@ describe('prepareEvaosDesktopBridgeResource', () => { ' os.environ["EVAOS_LIVE_CANARY_CONTEXT_KEY_ID"] = context_key_id', ' os.environ["EVAOS_LIVE_CANARY_RECEIPT_KEY_ID"] = receipt_key_id', ' os.environ["EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY"] = base64.urlsafe_b64encode(public_blob[-32:]).decode("ascii").rstrip("=")', - ' selected_binding = qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="12345", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36")', + ' selected_binding = qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="12345", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36", verification_time_seconds=1784073630)', ' assert selected_binding["ok"] is True, selected_binding', - ' assert qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="54321", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36")["ok"] is False', + ' assert qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="54321", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36", verification_time_seconds=1784073630)["ok"] is False', + ' assert qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="12345", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36", verification_time_seconds=1784073660)["ok"] is False', 'print("ok")', ].join('\n'); From d103d385c5a6c8e5b08f85dfb39f53eb20dda680 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 07:44:59 +0700 Subject: [PATCH 09/17] fix(evaos): make runtime proof consumable --- CHANGELOG.md | 3 +- .../evaos-beta/bridge/agent-tools/SOURCE.json | 4 +- .../bin/evaos-desktop-bridge-command | 27 +++++++++-- .../tests/runtimeReceipt.test.mjs | 45 ++++++++++++++++++- .../src/evaos_desktop_bridge/qa_canary.py | 4 +- .../prepareEvaosDesktopBridgeResource.test.ts | 3 +- 6 files changed, 77 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f00978de10..da04453542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,8 @@ routing, exact path matching, strict body validation, and rejection of caller-supplied authority before Mac-control proof can authorize publication. - Requires the installed-app QA binding check to consume the signed selected- - binding attestation before its short-lived execution authority expires. + binding attestation within a bounded one-hour evidence window, while still + proving that its short-lived authority was valid at execution time. ### Release Authorization And Bundle Integrity diff --git a/resources/evaos-beta/bridge/agent-tools/SOURCE.json b/resources/evaos-beta/bridge/agent-tools/SOURCE.json index d953ee68f7..52b264c419 100644 --- a/resources/evaos-beta/bridge/agent-tools/SOURCE.json +++ b/resources/evaos-beta/bridge/agent-tools/SOURCE.json @@ -9,7 +9,7 @@ "canonicalizedAt": "2026-07-15", "sourceDigests": { "hermes-adapter/README.md": "sha256:1bbef6050a512f42151dc5dc895474b2258ebf887192fe23796583e0b14b6a78", - "hermes-adapter/bin/evaos-desktop-bridge-command": "sha256:cf21cce6738b02047a506cb8809f70b9573a19877692ac466a03b57c4a3167ce", + "hermes-adapter/bin/evaos-desktop-bridge-command": "sha256:d5985f9289b53a278c905278d31a7130392cf6e7b56a21b5e61210d43b19d49a", "openclaw-plugin/README.md": "sha256:ce371b7ed539092b7cad9245883f8fb5e91c0c8f56ff094164a0635716128737", "openclaw-plugin/dist/index.js": "sha256:5634b0a23dfc08ba8640343cd4b3bf9a2fa77e496884924d39069fc97138ab91", "openclaw-plugin/dist/src/bridge.js": "sha256:625b964db6c16a3e7e574d4c86c757a1a11861af01daa6c4abb5c7cdcb947abf", @@ -25,7 +25,7 @@ "openclaw-plugin/src/firewall.ts": "sha256:85e638930cd5a412fc059e4cd2e2c8db41d29102319fe047bfc88601c7911668", "openclaw-plugin/src/runtimeReceipt.ts": "sha256:e88ba64b2113d0f63897ff8c599d5dc9dafe28a631c24a454039395967a2ecf3", "openclaw-plugin/src/types.d.ts": "sha256:6144e747e97c133ddaefe37af986865d6b87d842b922026a2af14b010c946bfc", - "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:c2f6f475e7d5cb14a6c721d70cebb59bd35ee993845ac19b9c56ce9ef9b7441a", + "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:86e4574a958690fb102e36b2f6d46644ef7a1ec1286f9be08fbde83583db7d18", "openclaw-plugin/tsconfig.json": "sha256:d5ceeadf205f97973f4d2a1e13ef20f9af854c1fbc01823af60b02eed5d5b34b" } } diff --git a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command index 450c98d07d..1b7cbe0b5e 100755 --- a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command +++ b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command @@ -15,7 +15,9 @@ fi read_connector_env_file() { local env_file="$1" - local line key value + local line key value decoded_value + local seen_url=0 + local seen_token=0 while IFS= read -r line || [[ -n "$line" ]]; do line="${line%$'\r'}" [[ -z "$line" || "$line" == \#* ]] && continue @@ -25,13 +27,32 @@ read_connector_env_file() { } key="${line%%=*}" value="${line#*=}" + decoded_value="$(printf '%s' "$value" | python3 -c 'import shlex, sys +raw = sys.stdin.read() +parts = shlex.split(raw, comments=False, posix=True) +if len(parts) != 1: + raise SystemExit(2) +sys.stdout.write(parts[0])')" || { + echo "invalid connector environment value" >&2 + return 2 + } case "$key" in EVAOS_DESKTOP_BRIDGE_URL) - EVAOS_DESKTOP_BRIDGE_URL="$value" + [[ "$seen_url" == 0 ]] || { + echo "duplicate connector environment key" >&2 + return 2 + } + seen_url=1 + EVAOS_DESKTOP_BRIDGE_URL="$decoded_value" export EVAOS_DESKTOP_BRIDGE_URL ;; EVAOS_DESKTOP_BRIDGE_TOKEN) - EVAOS_DESKTOP_BRIDGE_TOKEN="$value" + [[ "$seen_token" == 0 ]] || { + echo "duplicate connector environment key" >&2 + return 2 + } + seen_token=1 + EVAOS_DESKTOP_BRIDGE_TOKEN="$decoded_value" export EVAOS_DESKTOP_BRIDGE_TOKEN ;; *) diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs index 1a6ae8521a..0813eea7e9 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs @@ -1,11 +1,13 @@ import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; +import { execFile, spawnSync } from 'node:child_process'; import { createHash, generateKeyPairSync, sign } from 'node:crypto'; import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; +import { createServer } from 'node:http'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Readable } from 'node:stream'; import test from 'node:test'; +import { promisify } from 'node:util'; import { fileURLToPath } from 'node:url'; import { runBridge } from '../dist/src/bridge.js'; @@ -16,6 +18,7 @@ import { } from '../dist/src/runtimeReceipt.js'; const NOW_MS = Date.parse('2026-07-15T00:00:00Z'); +const execFileAsync = promisify(execFile); const KEY_ID = 'mac-context-test-2026-07'; const RECEIPT_KEY_ID = 'connector-receipt-test-2026-07'; const RECEIPT_NAMESPACE = 'evaos-mac-control-receipt-v1'; @@ -120,6 +123,46 @@ test('Hermes reads params from stdin and never executes connector env files', () } }); +test('Hermes decodes inert shell-quoted connector values exactly', async () => { + const wrapper = fileURLToPath(new URL('../../hermes-adapter/bin/evaos-desktop-bridge-command', import.meta.url)); + const directory = mkdtempSync(join(tmpdir(), 'evaos-hermes-quoted-env-')); + const envFile = join(directory, 'connector.env'); + const token = 'quoted connector token at least 24'; + let authorization = ''; + const server = createServer((request, response) => { + authorization = String(request.headers.authorization || ''); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end('{"ok":true,"schema":"evaos.desktop_bridge.response.v1"}'); + }); + try { + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', resolve); + }); + const address = server.address(); + assert.equal(typeof address, 'object'); + writeFileSync( + envFile, + `EVAOS_DESKTOP_BRIDGE_URL='http://127.0.0.1:${address.port}'\nEVAOS_DESKTOP_BRIDGE_TOKEN='${token}'\n`, + { encoding: 'utf8', mode: 0o600 } + ); + chmodSync(envFile, 0o600); + const completed = await execFileAsync(wrapper, ['customerMacStatus'], { + encoding: 'utf8', + env: { + PATH: process.env.PATH, + EVAOS_DESKTOP_BRIDGE_ENV_FILE: envFile, + }, + timeout: 5_000, + }); + assert.equal(JSON.parse(completed.stdout).ok, true); + assert.equal(authorization, `Bearer ${token}`); + } finally { + await new Promise((resolve) => server.close(resolve)); + rmSync(directory, { recursive: true, force: true }); + } +}); + test('rejects caller-supplied local payload file paths', async () => { const message = await runBridge('codexSendVisibleMessage', { thread_id: 'thread-1', diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py index f9ad45dd72..99accdb514 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py @@ -112,6 +112,7 @@ "privateReceiptSha256", "connectorCandidate", } +SELECTED_BINDING_PROOF_MAX_AGE_SECONDS = 60 * 60 LOCAL_WORKBENCH_CONTROL_START = "local_workbench_control_start" INSTALLED_WORKBENCH_BRIDGE_CLI = Path( "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge" @@ -1024,9 +1025,10 @@ def selected_binding_proof_binding( and type(expires_at) is int and issued_at < expires_at <= issued_at + 60 and issued_at <= verification_time + 5 - and verification_time < expires_at and executed_at.timestamp() >= issued_at - 5 and executed_at.timestamp() <= verification_time + 5 + and verification_time - executed_at.timestamp() + <= SELECTED_BINDING_PROOF_MAX_AGE_SECONDS and int(executed_at.timestamp()) <= expires_at and set(candidate) == SELECTED_BINDING_CANDIDATE_FIELDS and candidate.get("sourceCommit") == expected_source_commit diff --git a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts index dc50a43cbe..37d51b1c87 100644 --- a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts +++ b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts @@ -721,7 +721,8 @@ describe('prepareEvaosDesktopBridgeResource', () => { ' selected_binding = qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="12345", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36", verification_time_seconds=1784073630)', ' assert selected_binding["ok"] is True, selected_binding', ' assert qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="54321", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36", verification_time_seconds=1784073630)["ok"] is False', - ' assert qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="12345", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36", verification_time_seconds=1784073660)["ok"] is False', + ' assert qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="12345", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36", verification_time_seconds=1784073660)["ok"] is True', + ' assert qa_canary.selected_binding_proof_binding(selected_proof_path, expected_source_commit=commit, expected_source_run_id="12345", expected_source_sha256=source_sha256, expected_version="2.1.36", expected_build="2.1.36", verification_time_seconds=1784077201)["ok"] is False', 'print("ok")', ].join('\n'); From 1cea61c7491ddb28b51c6bc31566c879df785b02 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 08:13:35 +0700 Subject: [PATCH 10/17] fix(evaos): make Mac control calls observable --- .../evaos-beta/bridge/agent-tools/SOURCE.json | 13 +++--- .../agent-tools/openclaw-plugin/dist/index.js | 7 ++- .../openclaw-plugin/dist/src/firewall.js | 26 +++++------ .../dist/src/toolParameters.js | 26 +++++++++++ .../agent-tools/openclaw-plugin/index.ts | 7 ++- .../openclaw-plugin/scripts/qa-run-bridge.mjs | 42 +++++++++++++++-- .../openclaw-plugin/src/firewall.ts | 26 +++++------ .../openclaw-plugin/src/toolParameters.ts | 37 +++++++++++++++ .../tests/pluginContract.test.mjs | 45 +++++++++++++++++++ .../src/evaos_desktop_bridge/qa_canary.py | 6 ++- .../unit/evaos/evaosBrokerLiveCanary.test.ts | 21 +++++++++ 11 files changed, 217 insertions(+), 39 deletions(-) create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/toolParameters.js create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/toolParameters.ts create mode 100644 resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/pluginContract.test.mjs diff --git a/resources/evaos-beta/bridge/agent-tools/SOURCE.json b/resources/evaos-beta/bridge/agent-tools/SOURCE.json index 52b264c419..ee453580fe 100644 --- a/resources/evaos-beta/bridge/agent-tools/SOURCE.json +++ b/resources/evaos-beta/bridge/agent-tools/SOURCE.json @@ -11,20 +11,23 @@ "hermes-adapter/README.md": "sha256:1bbef6050a512f42151dc5dc895474b2258ebf887192fe23796583e0b14b6a78", "hermes-adapter/bin/evaos-desktop-bridge-command": "sha256:d5985f9289b53a278c905278d31a7130392cf6e7b56a21b5e61210d43b19d49a", "openclaw-plugin/README.md": "sha256:ce371b7ed539092b7cad9245883f8fb5e91c0c8f56ff094164a0635716128737", - "openclaw-plugin/dist/index.js": "sha256:5634b0a23dfc08ba8640343cd4b3bf9a2fa77e496884924d39069fc97138ab91", + "openclaw-plugin/dist/index.js": "sha256:087cbabd678efa15d3657b7137c4da0c8af3b8a4f4b55eef0492e957c8bf62c9", "openclaw-plugin/dist/src/bridge.js": "sha256:625b964db6c16a3e7e574d4c86c757a1a11861af01daa6c4abb5c7cdcb947abf", - "openclaw-plugin/dist/src/firewall.js": "sha256:939cdd1070bf02aade901d899a67e81e52ef2c6f554171f2039ba8419ffc630a", + "openclaw-plugin/dist/src/firewall.js": "sha256:fc86631e8df350355f2084a5dac43ba81c58e32cf29d87aa610ac88268b19f6c", "openclaw-plugin/dist/src/runtimeReceipt.js": "sha256:23cfb4db488e255ba5384c2f42429f12ad077dcd004a70f2e941984f3f3dd8b0", - "openclaw-plugin/index.ts": "sha256:a3d54d4ef87fca3435ae0c3ff86f3947950131b6d29c8cfa66944c4dd034bc12", + "openclaw-plugin/dist/src/toolParameters.js": "sha256:a441d521980b64e571918ab4b769d13c764fdc05960303b4683ca5d8c59b5a73", + "openclaw-plugin/index.ts": "sha256:0c472c05dde30746e87b5c0b8276e13d0cf572bd47460c7d04cc0049f64ff579", "openclaw-plugin/openclaw.plugin.json": "sha256:b145a479aad1209f528a2d1c69e2cf99ec8d2906957f668e3e124113a7276e79", "openclaw-plugin/package-lock.json": "sha256:e0f8d437d5a51f6fcb16f173550b5774022a0201944e5fb1eb5fc0158e7f7d52", "openclaw-plugin/package.json": "sha256:78ac262042b80c509a11419a0a60ce48db24054dad043f4de37a3ab7540b02ff", - "openclaw-plugin/scripts/qa-run-bridge.mjs": "sha256:648b24a86431254c25907ecf6906f1b0070b8701165fa1cb92dc9d9068e67dd2", + "openclaw-plugin/scripts/qa-run-bridge.mjs": "sha256:39246ca0fb4dc66658abf6ac3695286333e68684e0ee545b7762c214be97cac6", "openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs": "sha256:6a91ee0fc8ba7269ee5ff8ca3a79d7bfadb9139ed1eb629642c9c2a688130062", "openclaw-plugin/src/bridge.ts": "sha256:d0e0da280bd83dc71734579d12bbb8c37a50c5e8d746fb4ac967e7b69862a1f1", - "openclaw-plugin/src/firewall.ts": "sha256:85e638930cd5a412fc059e4cd2e2c8db41d29102319fe047bfc88601c7911668", + "openclaw-plugin/src/firewall.ts": "sha256:e8ef55500e577f828cb9a1422d0496bec9e5b9f0bb5ac65e06c527220aa4cf22", "openclaw-plugin/src/runtimeReceipt.ts": "sha256:e88ba64b2113d0f63897ff8c599d5dc9dafe28a631c24a454039395967a2ecf3", + "openclaw-plugin/src/toolParameters.ts": "sha256:dcca4a1615f7c138a03b2ff0da5e7739b2d0f29bf25fd3ffa3c9bc85b8c61b7c", "openclaw-plugin/src/types.d.ts": "sha256:6144e747e97c133ddaefe37af986865d6b87d842b922026a2af14b010c946bfc", + "openclaw-plugin/tests/pluginContract.test.mjs": "sha256:33679173a252fdd97e7f9e43747fd65bce7d5a713b7f8dd7a904774d4a45a7ba", "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:86e4574a958690fb102e36b2f6d46644ef7a1ec1286f9be08fbde83583db7d18", "openclaw-plugin/tsconfig.json": "sha256:d5ceeadf205f97973f4d2a1e13ef20f9af854c1fbc01823af60b02eed5d5b34b" } diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js index 8817d8a3f0..9eaca7ce4f 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/index.js @@ -2,6 +2,7 @@ import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry'; import { runBridge } from './src/bridge.js'; import { desktopBridgeFirewall } from './src/firewall.js'; import { registerMacControlRuntimeReceiptRoute } from './src/runtimeReceipt.js'; +import { applyToolParameterDefaults } from './src/toolParameters.js'; const approvalAuditIdProperty = { type: 'string', minLength: 1, @@ -731,11 +732,13 @@ function tool( command, parameters = { type: 'object', additionalProperties: false, properties: {}, required: [] } ) { + const normalizedParameters = normalizeToolParameters(parameters); return { name, description, - parameters: normalizeToolParameters(parameters), - execute: async (_toolCallId, params = {}) => toToolResult(await runBridge(command, params)), + parameters: normalizedParameters, + execute: async (_toolCallId, params = {}) => + toToolResult(await runBridge(command, applyToolParameterDefaults(normalizedParameters, params))), }; } function toToolResult(payload) { diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/firewall.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/firewall.js index cd946c85c5..12d5a06332 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/firewall.js +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/firewall.js @@ -115,20 +115,20 @@ export function desktopBridgeFirewall(event) { params: firewallPayload(toolName, event.params), parameters: firewallPayload(toolName, event.parameters), }).toLowerCase(); - const matchedPattern = FORBIDDEN_ARGUMENT_PATTERNS.find((pattern) => haystack.includes(pattern.toLowerCase())); + const matchedPatterns = FORBIDDEN_ARGUMENT_PATTERNS.filter((pattern) => haystack.includes(pattern.toLowerCase())); if (SAFE_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix))) { - const allowedFullAccessMatch = - matchedPattern !== undefined && - FULL_ACCESS_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix)) && - FULL_ACCESS_ALLOWED_MATCHES.has(matchedPattern); - const allowedSupportCanaryMatch = - matchedPattern !== undefined && - IPHONE_GESTURE_TOOL_NAMES.has(toolName) && - IPHONE_GESTURE_ALLOWED_MATCHES.has(matchedPattern); - if (matchedPattern && !allowedSupportCanaryMatch && !allowedFullAccessMatch) { + const disallowedPattern = matchedPatterns.find((matchedPattern) => { + const allowedFullAccessMatch = + FULL_ACCESS_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix)) && + FULL_ACCESS_ALLOWED_MATCHES.has(matchedPattern); + const allowedSupportCanaryMatch = + IPHONE_GESTURE_TOOL_NAMES.has(toolName) && IPHONE_GESTURE_ALLOWED_MATCHES.has(matchedPattern); + return !allowedSupportCanaryMatch && !allowedFullAccessMatch; + }); + if (disallowedPattern) { return { block: true, - blockReason: `desktop-bridge firewall blocked ${toolName}: ${matchedPattern} must go through the connector's audited control contract.`, + blockReason: `desktop-bridge firewall blocked ${toolName}: ${disallowedPattern} must go through the connector's audited control contract.`, }; } if (APPROVAL_GATED_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix))) { @@ -149,10 +149,10 @@ export function desktopBridgeFirewall(event) { return undefined; } const suspiciousTool = DANGEROUS_TOOL_NAMES.some((name) => toolName.toLowerCase().includes(name)); - if (suspiciousTool && matchedPattern) { + if (suspiciousTool && matchedPatterns.length > 0) { return { block: true, - blockReason: `desktop-bridge firewall blocked ${toolName}: ${matchedPattern} is outside the read-only passive observer boundary.`, + blockReason: `desktop-bridge firewall blocked ${toolName}: ${matchedPatterns[0]} is outside the read-only passive observer boundary.`, }; } return undefined; diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/toolParameters.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/toolParameters.js new file mode 100644 index 0000000000..6f2d5d24f7 --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/toolParameters.js @@ -0,0 +1,26 @@ +export function applyToolParameterDefaults(parameters, params) { + const properties = isRecord(parameters.properties) ? parameters.properties : {}; + const normalized = { ...params }; + for (const [name, property] of Object.entries(properties)) { + if ( + normalized[name] === undefined && + isRecord(property) && + Object.prototype.hasOwnProperty.call(property, 'default') + ) { + normalized[name] = cloneDefault(property.default); + } + } + return normalized; +} +function cloneDefault(value) { + if (Array.isArray(value)) { + return value.map(cloneDefault); + } + if (isRecord(value)) { + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneDefault(entry)])); + } + return value; +} +function isRecord(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts index 6dd4fee154..d238bbdf8f 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/index.ts @@ -3,6 +3,7 @@ import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry'; import { type BridgeCommandKey, type BridgeParams, runBridge } from './src/bridge.js'; import { desktopBridgeFirewall } from './src/firewall.js'; import { registerMacControlRuntimeReceiptRoute } from './src/runtimeReceipt.js'; +import { applyToolParameterDefaults } from './src/toolParameters.js'; type ToolDefinition = { name: string; @@ -755,11 +756,13 @@ function tool( command: BridgeCommandKey, parameters: Record = { type: 'object', additionalProperties: false, properties: {}, required: [] } ): ToolDefinition { + const normalizedParameters = normalizeToolParameters(parameters); return { name, description, - parameters: normalizeToolParameters(parameters), - execute: async (_toolCallId: string, params: BridgeParams = {}) => toToolResult(await runBridge(command, params)), + parameters: normalizedParameters, + execute: async (_toolCallId: string, params: BridgeParams = {}) => + toToolResult(await runBridge(command, applyToolParameterDefaults(normalizedParameters, params))), }; } diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/qa-run-bridge.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/qa-run-bridge.mjs index c134d517ad..b41ca50cdf 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/qa-run-bridge.mjs +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/scripts/qa-run-bridge.mjs @@ -23,7 +23,24 @@ try { process.exit(2); } -const toolMap = await loadRegisteredToolMap(); +let toolMap; +try { + toolMap = await loadRegisteredToolMap(); +} catch { + console.log( + JSON.stringify({ + ok: false, + errors: [ + { + code: 'qa_openclaw_tool_registry_invalid', + message: 'The built OpenClaw plugin tool registry does not match its package contract.', + guidance: 'Rebuild the OpenClaw plugin and verify its exact registered tool contract.', + }, + ], + }) + ); + process.exit(0); +} const bridgeCommand = toolMap.get(command) || command; const firewallDecision = desktopBridgeFirewall({ toolName: command, @@ -108,12 +125,31 @@ async function readStdin() { async function loadRegisteredToolMap() { const scriptDir = dirname(fileURLToPath(import.meta.url)); const distIndex = join(scriptDir, '../dist/index.js'); + const packageManifestPath = join(scriptDir, '../package.json'); const source = await readFile(distIndex, 'utf8'); + const packageManifest = JSON.parse(await readFile(packageManifestPath, 'utf8')); + const expectedTools = packageManifest?.openclaw?.contracts?.tools; + if ( + !Array.isArray(expectedTools) || + expectedTools.length === 0 || + new Set(expectedTools).size !== expectedTools.length + ) { + throw new Error('invalid package tool contract'); + } const map = new Map(); - const toolCallPattern = /tool\(\s*"([^"]+)"[\s\S]*?,\s*"([A-Za-z0-9]+)"(?:\s*,|\s*\))/g; + const toolCallPattern = /tool\(\s*(['"])([^'"]+)\1\s*,[\s\S]*?,\s*(['"])([A-Za-z0-9]+)\3(?:\s*,|\s*\))/g; let match; + let matchCount = 0; while ((match = toolCallPattern.exec(source)) !== null) { - map.set(match[1], match[2]); + matchCount += 1; + map.set(match[2], match[4]); + } + if ( + matchCount !== expectedTools.length || + map.size !== expectedTools.length || + expectedTools.some((name) => typeof name !== 'string' || !map.has(name)) + ) { + throw new Error('built tool registry mismatch'); } return map; } diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/firewall.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/firewall.ts index 181c2a4b2b..22c06860ef 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/firewall.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/firewall.ts @@ -141,20 +141,20 @@ export function desktopBridgeFirewall(event: HookEvent): HookDecision { params: firewallPayload(toolName, event.params), parameters: firewallPayload(toolName, event.parameters), }).toLowerCase(); - const matchedPattern = FORBIDDEN_ARGUMENT_PATTERNS.find((pattern) => haystack.includes(pattern.toLowerCase())); + const matchedPatterns = FORBIDDEN_ARGUMENT_PATTERNS.filter((pattern) => haystack.includes(pattern.toLowerCase())); if (SAFE_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix))) { - const allowedFullAccessMatch = - matchedPattern !== undefined && - FULL_ACCESS_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix)) && - FULL_ACCESS_ALLOWED_MATCHES.has(matchedPattern); - const allowedSupportCanaryMatch = - matchedPattern !== undefined && - IPHONE_GESTURE_TOOL_NAMES.has(toolName) && - IPHONE_GESTURE_ALLOWED_MATCHES.has(matchedPattern); - if (matchedPattern && !allowedSupportCanaryMatch && !allowedFullAccessMatch) { + const disallowedPattern = matchedPatterns.find((matchedPattern) => { + const allowedFullAccessMatch = + FULL_ACCESS_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix)) && + FULL_ACCESS_ALLOWED_MATCHES.has(matchedPattern); + const allowedSupportCanaryMatch = + IPHONE_GESTURE_TOOL_NAMES.has(toolName) && IPHONE_GESTURE_ALLOWED_MATCHES.has(matchedPattern); + return !allowedSupportCanaryMatch && !allowedFullAccessMatch; + }); + if (disallowedPattern) { return { block: true, - blockReason: `desktop-bridge firewall blocked ${toolName}: ${matchedPattern} must go through the connector's audited control contract.`, + blockReason: `desktop-bridge firewall blocked ${toolName}: ${disallowedPattern} must go through the connector's audited control contract.`, }; } if (APPROVAL_GATED_TOOL_PREFIXES.some((prefix) => toolName.startsWith(prefix))) { @@ -177,10 +177,10 @@ export function desktopBridgeFirewall(event: HookEvent): HookDecision { const suspiciousTool = DANGEROUS_TOOL_NAMES.some((name) => toolName.toLowerCase().includes(name)); - if (suspiciousTool && matchedPattern) { + if (suspiciousTool && matchedPatterns.length > 0) { return { block: true, - blockReason: `desktop-bridge firewall blocked ${toolName}: ${matchedPattern} is outside the read-only passive observer boundary.`, + blockReason: `desktop-bridge firewall blocked ${toolName}: ${matchedPatterns[0]} is outside the read-only passive observer boundary.`, }; } diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/toolParameters.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/toolParameters.ts new file mode 100644 index 0000000000..67d1a0d8a5 --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/toolParameters.ts @@ -0,0 +1,37 @@ +type JsonSchemaProperty = { + default?: unknown; +}; + +export function applyToolParameterDefaults>( + parameters: Record, + params: T +): T { + const properties = isRecord(parameters.properties) ? parameters.properties : {}; + const normalized: Record = { ...params }; + + for (const [name, property] of Object.entries(properties)) { + if ( + normalized[name] === undefined && + isRecord(property) && + Object.prototype.hasOwnProperty.call(property, 'default') + ) { + normalized[name] = cloneDefault((property as JsonSchemaProperty).default); + } + } + + return normalized as T; +} + +function cloneDefault(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(cloneDefault); + } + if (isRecord(value)) { + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneDefault(entry)])); + } + return value; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/pluginContract.test.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/pluginContract.test.mjs new file mode 100644 index 0000000000..9adead1104 --- /dev/null +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/pluginContract.test.mjs @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; + +import { desktopBridgeFirewall } from '../dist/src/firewall.js'; +import { applyToolParameterDefaults } from '../dist/src/toolParameters.js'; + +test('applies advertised false and true defaults before bridge execution', () => { + const live = applyToolParameterDefaults( + { properties: { dry_run: { type: 'boolean', default: false }, amount: { type: 'integer', default: 600 } } }, + {} + ); + assert.deepEqual(live, { dry_run: false, amount: 600 }); + + const guarded = applyToolParameterDefaults({ properties: { dry_run: { type: 'boolean', default: true } } }, {}); + assert.deepEqual(guarded, { dry_run: true }); + assert.deepEqual(applyToolParameterDefaults({ properties: { dry_run: { default: false } } }, { dry_run: true }), { + dry_run: true, + }); +}); + +test('blocks a forbidden control-enablement phrase even after an allowed primitive match', () => { + for (const [toolName, text, forbidden] of [ + ['desktop_type', 'coordinate Screen Sharing enable', 'Screen Sharing enable'], + ['desktop_type', 'swipe Remote Management enable', 'Remote Management enable'], + ['desktop_type', 'drag kickstart -activate', 'kickstart -activate'], + ]) { + const decision = desktopBridgeFirewall({ toolName, args: { text } }); + assert.equal(decision?.block, true); + assert.match(decision?.blockReason || '', new RegExp(forbidden.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')); + } +}); + +test('built QA bridge recognizes the complete formatted plugin registry', () => { + const script = fileURLToPath(new URL('../scripts/qa-run-bridge.mjs', import.meta.url)); + const result = spawnSync(process.execPath, [script, 'desktop_bridge_status'], { + encoding: 'utf8', + env: { PATH: process.env.PATH }, + }); + assert.equal(result.status, 0); + const payload = JSON.parse(result.stdout); + assert.notEqual(payload?.errors?.[0]?.code, 'qa_openclaw_tool_not_registered'); + assert.notEqual(payload?.errors?.[0]?.code, 'qa_openclaw_tool_registry_invalid'); +}); diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py index 99accdb514..eba94d041c 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py @@ -919,11 +919,15 @@ def selected_binding_proof_binding( ) signature = str(proof.get("signature") or "") signature_verified = False - if ( + if not ( signature.startswith("-----BEGIN SSH SIGNATURE-----\n") and signature.endswith("-----END SSH SIGNATURE-----\n") and len(signature) <= 8192 + and signature.isascii() ): + result["reason"] = "selected_binding_proof_signature_invalid" + return result + if signature: algorithm = b"ssh-ed25519" public_blob = ( struct.pack(">I", len(algorithm)) diff --git a/tests/unit/evaos/evaosBrokerLiveCanary.test.ts b/tests/unit/evaos/evaosBrokerLiveCanary.test.ts index dd9b461eae..9523b606ec 100644 --- a/tests/unit/evaos/evaosBrokerLiveCanary.test.ts +++ b/tests/unit/evaos/evaosBrokerLiveCanary.test.ts @@ -855,6 +855,27 @@ describe('evaOS broker live canary', () => { }, }).trim() ).toBe('ok'); + + const nonAsciiProofPath = path.join(proofDir, 'mac-control-runtime-non-ascii-signature.json'); + fs.writeFileSync( + nonAsciiProofPath, + `${JSON.stringify({ ...proof, signature: `${proof.signature}\u2603` }, null, 2)}\n` + ); + const malformedPythonSource = pythonSource.replace( + 'assert result["ok"] is True, result', + 'assert result["ok"] is False and result["reason"] == "selected_binding_proof_signature_invalid", result' + ); + expect( + execFileSync('python3', ['-B', '-c', malformedPythonSource, nonAsciiProofPath], { + encoding: 'utf8', + env: { + ...process.env, + PYTHONDONTWRITEBYTECODE: '1', + PYTHONPATH: path.join(process.cwd(), 'resources', 'evaos-beta', 'bridge', 'src'), + ...TRUST_ENV, + }, + }).trim() + ).toBe('ok'); } finally { fs.rmSync(proofDir, { recursive: true, force: true }); } From 672789346a4cac29462f55b81d5bcbd6d6dc972a Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 08:57:51 +0700 Subject: [PATCH 11/17] fix: close v2.1.36 Mac control canary gaps --- .../evaos-beta/bridge/agent-tools/SOURCE.json | 6 +- .../dist/src/runtimeReceipt.js | 49 ++++-- .../openclaw-plugin/src/runtimeReceipt.ts | 57 +++--- .../tests/runtimeReceipt.test.mjs | 94 +++++++++- .../evaos_desktop_bridge/connector_server.py | 9 +- .../src/evaos_desktop_bridge/qa_canary.py | 74 +++++++- .../evaos_desktop_bridge/receipt_canary.py | 2 +- scripts/evaosBetaReleaseGate.js | 2 + .../evaosDesktopBridgeControlSafetyP0.test.ts | 99 +++++++++++ .../evaos/evaosDesktopBridgeReceipt.test.ts | 16 +- .../evaos/evaosQaCanaryLocalControl.test.ts | 162 +++++++++++++++++- .../unit/process/evaosBetaReleaseGate.test.ts | 11 +- 12 files changed, 532 insertions(+), 49 deletions(-) diff --git a/resources/evaos-beta/bridge/agent-tools/SOURCE.json b/resources/evaos-beta/bridge/agent-tools/SOURCE.json index ee453580fe..2d27345a6a 100644 --- a/resources/evaos-beta/bridge/agent-tools/SOURCE.json +++ b/resources/evaos-beta/bridge/agent-tools/SOURCE.json @@ -14,7 +14,7 @@ "openclaw-plugin/dist/index.js": "sha256:087cbabd678efa15d3657b7137c4da0c8af3b8a4f4b55eef0492e957c8bf62c9", "openclaw-plugin/dist/src/bridge.js": "sha256:625b964db6c16a3e7e574d4c86c757a1a11861af01daa6c4abb5c7cdcb947abf", "openclaw-plugin/dist/src/firewall.js": "sha256:fc86631e8df350355f2084a5dac43ba81c58e32cf29d87aa610ac88268b19f6c", - "openclaw-plugin/dist/src/runtimeReceipt.js": "sha256:23cfb4db488e255ba5384c2f42429f12ad077dcd004a70f2e941984f3f3dd8b0", + "openclaw-plugin/dist/src/runtimeReceipt.js": "sha256:53007de6bf12fe64997ac846802385f1201e3e1bf47ae49cfdaed54777ea1839", "openclaw-plugin/dist/src/toolParameters.js": "sha256:a441d521980b64e571918ab4b769d13c764fdc05960303b4683ca5d8c59b5a73", "openclaw-plugin/index.ts": "sha256:0c472c05dde30746e87b5c0b8276e13d0cf572bd47460c7d04cc0049f64ff579", "openclaw-plugin/openclaw.plugin.json": "sha256:b145a479aad1209f528a2d1c69e2cf99ec8d2906957f668e3e124113a7276e79", @@ -24,11 +24,11 @@ "openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs": "sha256:6a91ee0fc8ba7269ee5ff8ca3a79d7bfadb9139ed1eb629642c9c2a688130062", "openclaw-plugin/src/bridge.ts": "sha256:d0e0da280bd83dc71734579d12bbb8c37a50c5e8d746fb4ac967e7b69862a1f1", "openclaw-plugin/src/firewall.ts": "sha256:e8ef55500e577f828cb9a1422d0496bec9e5b9f0bb5ac65e06c527220aa4cf22", - "openclaw-plugin/src/runtimeReceipt.ts": "sha256:e88ba64b2113d0f63897ff8c599d5dc9dafe28a631c24a454039395967a2ecf3", + "openclaw-plugin/src/runtimeReceipt.ts": "sha256:c263ce591790d486243174a26aa0d654f19398c6d8b7da5fe1688d7860ea83ef", "openclaw-plugin/src/toolParameters.ts": "sha256:dcca4a1615f7c138a03b2ff0da5e7739b2d0f29bf25fd3ffa3c9bc85b8c61b7c", "openclaw-plugin/src/types.d.ts": "sha256:6144e747e97c133ddaefe37af986865d6b87d842b922026a2af14b010c946bfc", "openclaw-plugin/tests/pluginContract.test.mjs": "sha256:33679173a252fdd97e7f9e43747fd65bce7d5a713b7f8dd7a904774d4a45a7ba", - "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:86e4574a958690fb102e36b2f6d46644ef7a1ec1286f9be08fbde83583db7d18", + "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:37ac5d2ed14c5922a2ce90bb5152e1d79932b7e7ad51af7681401b71232beefc", "openclaw-plugin/tsconfig.json": "sha256:d5ceeadf205f97973f4d2a1e13ef20f9af854c1fbc01823af60b02eed5d5b34b" } } diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js index 453185979a..e39961f2e9 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js @@ -192,6 +192,7 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { }, }; let connectorResponse; + let rawConnectorResponse; const controller = new AbortController(); const connectorTimeout = setTimeout(() => controller.abort(), connectorTimeoutMs); try { @@ -205,27 +206,21 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { body: JSON.stringify(connectorBody), signal: controller.signal, }); + if (!connectorResponse.ok) { + sendError( + response, + connectorResponse.status >= 400 && connectorResponse.status < 500 ? 409 : 502, + 'connector_rejected_canary' + ); + return true; + } + rawConnectorResponse = await readBoundedConnectorResponse(connectorResponse); } catch { - sendError(response, 502, 'connector_unavailable'); + sendError(response, 502, connectorResponse ? 'connector_response_invalid' : 'connector_unavailable'); return true; } finally { clearTimeout(connectorTimeout); } - if (!connectorResponse.ok) { - sendError( - response, - connectorResponse.status >= 400 && connectorResponse.status < 500 ? 409 : 502, - 'connector_rejected_canary' - ); - return true; - } - let rawConnectorResponse; - try { - rawConnectorResponse = await connectorResponse.text(); - } catch { - sendError(response, 502, 'connector_response_invalid'); - return true; - } const sanitized = validateConnectorEnvelope( rawConnectorResponse, authority.value, @@ -498,6 +493,26 @@ function validateConnectorEnvelope(raw, authority, request, verifier) { } return validatePublicAttestationEnvelope(publicAttestation, expectedAttestation.value, authority, verifier); } +async function readBoundedConnectorResponse(response) { + if (response.body) { + const chunks = []; + let total = 0; + for await (const chunk of response.body) { + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(typeof chunk === 'string' ? chunk : chunk); + total += buffer.byteLength; + if (total > MAX_CONNECTOR_RESPONSE_BYTES) { + throw new Error('connector response exceeds the bounded receipt envelope size'); + } + chunks.push(buffer); + } + return Buffer.concat(chunks).toString('utf8'); + } + const raw = await response.text(); + if (Buffer.from(raw).byteLength > MAX_CONNECTOR_RESPONSE_BYTES) { + throw new Error('connector response exceeds the bounded receipt envelope size'); + } + return raw; +} function validateReceipt(receiptBytes, authority, request, verifier) { let parsed; try { @@ -706,7 +721,7 @@ function validCandidate(value, verifier) { owner.sourceCommit === verifier.expectedSourceCommit && validPathClaim( owner.programPath, - '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge' + '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py' ) && validPathClaim(owner.appPath, '/Applications/evaOS Workbench.app') && validPathClaim(owner.manifestPath, '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/manifest.json') && diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts index 8e9fa9b54b..d950675310 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts @@ -148,6 +148,7 @@ type ResponseLike = { type FetchResponseLike = { ok: boolean; status: number; + body?: AsyncIterable | null; text(): Promise; }; @@ -320,7 +321,8 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt }, }; - let connectorResponse: FetchResponseLike; + let connectorResponse: FetchResponseLike | undefined; + let rawConnectorResponse: string; const controller = new AbortController(); const connectorTimeout = setTimeout(() => controller.abort(), connectorTimeoutMs); try { @@ -334,29 +336,21 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt body: JSON.stringify(connectorBody), signal: controller.signal, }); + if (!connectorResponse.ok) { + sendError( + response, + connectorResponse.status >= 400 && connectorResponse.status < 500 ? 409 : 502, + 'connector_rejected_canary' + ); + return true; + } + rawConnectorResponse = await readBoundedConnectorResponse(connectorResponse); } catch { - sendError(response, 502, 'connector_unavailable'); + sendError(response, 502, connectorResponse ? 'connector_response_invalid' : 'connector_unavailable'); return true; } finally { clearTimeout(connectorTimeout); } - - if (!connectorResponse.ok) { - sendError( - response, - connectorResponse.status >= 400 && connectorResponse.status < 500 ? 409 : 502, - 'connector_rejected_canary' - ); - return true; - } - - let rawConnectorResponse: string; - try { - rawConnectorResponse = await connectorResponse.text(); - } catch { - sendError(response, 502, 'connector_response_invalid'); - return true; - } const sanitized = validateConnectorEnvelope( rawConnectorResponse, authority.value, @@ -656,6 +650,29 @@ function validateConnectorEnvelope( return validatePublicAttestationEnvelope(publicAttestation, expectedAttestation.value, authority, verifier); } +async function readBoundedConnectorResponse(response: FetchResponseLike): Promise { + if (response.body) { + const chunks: ByteBuffer[] = []; + let total = 0; + for await (const chunk of response.body) { + const buffer = Buffer.isBuffer(chunk) + ? (chunk as ByteBuffer) + : Buffer.from(typeof chunk === 'string' ? chunk : (chunk as Uint8Array)); + total += buffer.byteLength; + if (total > MAX_CONNECTOR_RESPONSE_BYTES) { + throw new Error('connector response exceeds the bounded receipt envelope size'); + } + chunks.push(buffer); + } + return Buffer.concat(chunks).toString('utf8'); + } + const raw = await response.text(); + if (Buffer.from(raw).byteLength > MAX_CONNECTOR_RESPONSE_BYTES) { + throw new Error('connector response exceeds the bounded receipt envelope size'); + } + return raw; +} + function validateReceipt( receiptBytes: ByteBuffer, authority: VerifiedAuthority, @@ -880,7 +897,7 @@ function validCandidate(value: unknown, verifier: ReceiptVerifierConfig): value owner.sourceCommit === verifier.expectedSourceCommit && validPathClaim( owner.programPath, - '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge' + '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py' ) && validPathClaim(owner.appPath, '/Applications/evaOS Workbench.app') && validPathClaim(owner.manifestPath, '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/manifest.json') && diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs index 0813eea7e9..246bcef913 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs @@ -328,6 +328,81 @@ test('bounds connector IO by a sanitized deadline before signed authority expire assert.equal(connectorCalls, 1); }); +test('keeps the connector deadline active through response body consumption', async () => { + const authority = signedAuthority(); + let connectorCalls = 0; + const handler = createMacControlRuntimeReceiptHandler({ + env, + now: () => NOW_MS, + connectorTimeoutMs: 20, + fetchImpl: async (_url, init) => { + connectorCalls += 1; + return { + ok: true, + status: 200, + text: async () => + await new Promise((_resolve, reject) => { + if (init.signal.aborted) { + reject(new Error('body deadline elapsed')); + return; + } + init.signal.addEventListener('abort', () => reject(new Error('body deadline elapsed')), { once: true }); + }), + }; + }, + }); + const outcome = await Promise.race([ + invoke( + handler, + { challenge: Buffer.alloc(32, 13).toString('base64url'), runRef: 'connector-body-timeout' }, + authority.headers + ), + new Promise((resolve) => setTimeout(() => resolve('handler_did_not_settle'), 250)), + ]); + assert.notEqual(outcome, 'handler_did_not_settle'); + assert.equal(outcome.statusCode, 502); + assert.equal(JSON.parse(outcome.body).error.code, 'connector_response_invalid'); + assert.equal(outcome.body.includes('body deadline elapsed'), false); + assert.equal(connectorCalls, 1); +}); + +test('rejects an oversized streaming connector body before reading beyond the cap', async () => { + const authority = signedAuthority(); + let textCalled = false; + let chunksRead = 0; + const handler = createMacControlRuntimeReceiptHandler({ + env, + now: () => NOW_MS, + fetchImpl: async () => ({ + ok: true, + status: 200, + body: { + async *[Symbol.asyncIterator]() { + chunksRead += 1; + yield Buffer.alloc(40_000, 0x61); + chunksRead += 1; + yield Buffer.alloc(30_000, 0x62); + throw new Error('must stop before a third chunk'); + }, + }, + text: async () => { + textCalled = true; + return ''; + }, + }), + }); + const response = await invoke( + handler, + { challenge: Buffer.alloc(32, 14).toString('base64url'), runRef: 'connector-body-oversized' }, + authority.headers + ); + assert.equal(response.statusCode, 502); + assert.equal(JSON.parse(response.body).error.code, 'connector_response_invalid'); + assert.equal(response.body.includes('third chunk'), false); + assert.equal(chunksRead, 2); + assert.equal(textCalled, false); +}); + test('rejects forged signatures and re-signed wrong release claims', async (t) => { const challenge = Buffer.alloc(32, 9).toString('base64url'); const runRef = 'receipt-tamper'; @@ -350,6 +425,23 @@ test('rejects forged signatures and re-signed wrong release claims', async (t) = candidate: { ...valid.candidate, sourceCommit: 'd'.repeat(40) }, }), ], + [ + 'launcher wrapper substituted for the active packaged connector program', + () => + signedConnectorEnvelope({ + ...valid, + candidate: { + ...valid.candidate, + owner: { + ...valid.candidate.owner, + programPath: { + kind: 'path', + value: '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge', + }, + }, + }, + }), + ], [ 'forged public attestation', () => { @@ -655,7 +747,7 @@ function validReceipt(authority, challenge, runRef, overrides = {}) { sourceCommit: EXPECTED_COMMIT, programPath: { kind: 'path', - value: '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge', + value: '/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py', }, appPath: { kind: 'path', value: '/Applications/evaOS Workbench.app' }, manifestPath: { diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py index dcdd04db85..742d7a70be 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/connector_server.py @@ -1987,8 +1987,15 @@ def _approval_field_value(command: str, params: dict[str, Any], field: str) -> A return None normalized = str(value).strip() if source_field == "message" else str(value) return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16] + if command == "desktopSetValue" and field == "attribute": + return str(params.get("attribute") or "value") + if command == "desktopScroll": + if field == "direction": + return str(params.get("direction") or "down") + if field == "amount": + return _clamp_int(params.get("amount"), 600, 1, 5000) if field == "direction" and command == "customerMacIphoneMirroringScroll": - return params.get("direction") or "down" + return str(params.get("direction") or "down") return params.get(field) diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py index eba94d041c..dbd9442084 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/qa_canary.py @@ -6,6 +6,7 @@ import os import posixpath import re +import signal import struct import subprocess import sys @@ -362,6 +363,7 @@ def __init__( self.session_reader = session_reader or read_control_session self.local_kill_switch = local_kill_switch or kill_control_session self.control_start_failed = False + self.local_control_cleanup_required = False def run(self, command: str, params: dict[str, Any]) -> SurfaceResponse: if command == LOCAL_WORKBENCH_CONTROL_START: @@ -445,6 +447,7 @@ def _start_local_control(self, params: dict[str, Any]) -> dict[str, Any]: str(generation), ] argv.extend(["--agent-label", bound_agent_label]) + self.local_control_cleanup_required = True try: exit_code, output = self.local_runner( INSTALLED_WORKBENCH_BRIDGE_CLI, @@ -620,16 +623,24 @@ def _start_local_control(self, params: dict[str, Any]) -> dict[str, Any]: ) return payload + def teardown_local_control(self) -> bool | None: + if not self.local_control_cleanup_required: + return None + return self._activate_local_kill_switch() + def _activate_local_kill_switch(self) -> bool: try: session = self.local_kill_switch() except Exception: # noqa: BLE001 - cleanup is reported only as a boolean. return False - return ( + confirmed = ( isinstance(session, dict) and session.get("active") is False and session.get("kill_switch") is True ) + if confirmed: + self.local_control_cleanup_required = False + return confirmed def _run_local_workbench_cli( @@ -1238,6 +1249,32 @@ def run_steps(steps: list[CanaryStep], surface: CanarySurface) -> list[CanaryRes return results +def run_steps_with_local_control_cleanup( + steps: list[CanaryStep], + surface: OperatorAcknowledgedLocalControlSurface, + *, + suite: str, +) -> list[CanaryResult]: + results: list[CanaryResult] = [] + cleanup_confirmed: bool | None = None + previous_sigterm = signal.getsignal(signal.SIGTERM) + + def orderly_sigterm(signum: int, _frame: Any) -> None: + raise SystemExit(128 + signum) + + signal.signal(signal.SIGTERM, orderly_sigterm) + try: + results = run_steps(steps, surface) + finally: + try: + cleanup_confirmed = surface.teardown_local_control() + finally: + signal.signal(signal.SIGTERM, previous_sigterm) + if cleanup_confirmed is not None: + results.append(_local_control_cleanup_result(suite, cleanup_confirmed)) + return results + + def classify_status( payload: dict[str, Any], *, @@ -1675,7 +1712,9 @@ def main(argv: list[str] | None = None) -> int: steps = build_scenarios( args.suite, allow_real_world_actions=args.allow_real_world_actions ) - results = run_steps(steps, surface) + results = run_steps_with_local_control_cleanup( + steps, surface, suite=args.suite + ) report_paths = write_reports( artifact_dir=artifact_dir, run_id=run_id, @@ -2794,6 +2833,37 @@ def _failed_result( ) +def _local_control_cleanup_result(suite: str, confirmed: bool) -> CanaryResult: + errors = ( + [] + if confirmed + else [ + { + "code": "local_control_teardown_unconfirmed", + "message": "The QA canary could not confirm its final local kill-switch cleanup.", + "guidance": "Use the installed Workbench to activate the kill switch before continuing release proof.", + } + ] + ) + return CanaryResult( + id="control_cleanup.local_kill_switch", + suite=suite, + lane="safety", + command="desktop_kill_switch", + params_redacted={}, + ok=confirmed, + status="passed" if confirmed else "failed", + audit_id=None, + engine=None, + snapshot_id=None, + artifact_path=None, + duration_ms=0, + errors=errors, + warnings=[], + payload={"ok": confirmed, "errors": errors}, + ) + + def _summary(results: list[CanaryResult]) -> dict[str, int]: return { "total": len(results), diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py index 2d131f05ca..9545539deb 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py @@ -693,7 +693,7 @@ def candidate_snapshot( or owner_program != { "kind": "path", - "value": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge", + "value": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py", } or owner_app != {"kind": "path", "value": "/Applications/evaOS Workbench.app"} or owner_manifest diff --git a/scripts/evaosBetaReleaseGate.js b/scripts/evaosBetaReleaseGate.js index 6e781c2fd1..bc7fb8a704 100644 --- a/scripts/evaosBetaReleaseGate.js +++ b/scripts/evaosBetaReleaseGate.js @@ -215,6 +215,7 @@ const REQUIRED_RC_PROOF_CHECKS = [ 'control_start.ask_permission', 'control_start.stop', 'control_start.kill_switch', + 'control_cleanup.local_kill_switch', '"source_commit_under_test"', '"candidate_binding"', ], @@ -3731,6 +3732,7 @@ function assertRcInstalledCandidateConnectorProof(proofPath, tag, releaseManifes { id: 'control_start.ask_permission', command: 'local_workbench_control_start', mode: 'ask-permission' }, { id: 'control_start.stop', command: 'desktop_control_stop' }, { id: 'control_start.kill_switch', command: 'desktop_kill_switch' }, + { id: 'control_cleanup.local_kill_switch', command: 'desktop_kill_switch' }, ]; const requiredStatuses = expectedResults.map((expected) => ({ expected, diff --git a/tests/unit/evaos/evaosDesktopBridgeControlSafetyP0.test.ts b/tests/unit/evaos/evaosDesktopBridgeControlSafetyP0.test.ts index 45d38d9582..e4a79e3e6a 100644 --- a/tests/unit/evaos/evaosDesktopBridgeControlSafetyP0.test.ts +++ b/tests/unit/evaos/evaosDesktopBridgeControlSafetyP0.test.ts @@ -417,6 +417,105 @@ with TemporaryDirectory() as temporary_root: server.shutdown() server.server_close() +print("ok") +`); + + expect(output).toBe('ok'); + }); + + it('replays Ask Permission dry runs when direct clients omit materialized defaults', () => { + const output = runPython(` +import hashlib +import json +import threading +import urllib.request +from http.server import ThreadingHTTPServer +from pathlib import Path +from tempfile import TemporaryDirectory + +from evaos_desktop_bridge.audit import append_audit +from evaos_desktop_bridge.connector_server import _approval_field_value, _make_handler +from evaos_desktop_bridge.state import start_control_session, write_control_session + +with TemporaryDirectory() as temporary_root: + root = Path(temporary_root) + session = start_control_session(mode="ask_permission", state_dir=root) + session["takeover_warning_started_at"] = None + session["takeover_warning_until"] = None + session = write_control_session(session, state_dir=root) + value = "approved replacement" + approval_id = append_audit( + command="customer_mac.desktop_set_value", + target="customer_mac", + args={ + "snapshot_id": "snapshot-1", + "element_id": "element-1", + "attribute": "value", + "value_hash": hashlib.sha256(value.encode("utf-8")).hexdigest()[:16], + "dry_run": True, + }, + ok=True, + warnings=[], + errors=[], + state_dir=root, + ) + captured = [] + + def runner(argv): + captured.append(list(argv)) + value_file_index = argv.index("--value-file") + 1 + assert Path(argv[value_file_index]).read_text(encoding="utf-8") == value + assert argv[argv.index("--attribute") + 1] == "value" + return 0, json.dumps({ + "schema_version": "2026-05-02.mvp1", + "command": "customer_mac.desktop_set_value", + "target": "customer_mac", + "timestamp": "2026-07-15T00:00:00Z", + "ok": True, + "data": {"set": True}, + "warnings": [], + "errors": [], + "audit_id": "audit-runner", + }) + + handler = _make_handler(token="connector-test-token", command_runner=runner, state_dir=root) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + endpoint = f"http://127.0.0.1:{server.server_address[1]}/v1/commands" + request = urllib.request.Request( + endpoint, + method="POST", + data=json.dumps({ + "command": "desktopSetValue", + "params": { + "snapshot_id": "snapshot-1", + "element_id": "element-1", + "value": value, + "dry_run": False, + "approval_audit_id": approval_id, + }, + }).encode("utf-8"), + headers={"Authorization": "Bearer connector-test-token", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(request, timeout=8) as response: + assert response.status == 200, response.read() + + assert len(captured) == 1, captured + assert captured[0][:3] == [ + "customer-mac", + "--remote-control-generation", + str(session["generation"]), + ] + assert _approval_field_value("desktopSetValue", {}, "attribute") == "value" + assert _approval_field_value("desktopScroll", {}, "direction") == "down" + assert _approval_field_value("desktopScroll", {}, "amount") == 600 + assert _approval_field_value("customerMacIphoneMirroringScroll", {}, "direction") == "down" + + server.shutdown() + server.server_close() + thread.join(timeout=5) + print("ok") `); diff --git a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts index 37b51b199b..7384a468ea 100644 --- a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts +++ b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts @@ -85,7 +85,7 @@ owner = { "classification": "workbench_bundle", "bundle_id": "com.evaos.workbench", "source_commit": "a" * 40, - "program_path": {"kind": "path", "value": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge"}, + "program_path": {"kind": "path", "value": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py"}, "app_path": {"kind": "path", "value": "/Applications/evaOS Workbench.app"}, "manifest_path": {"kind": "path", "value": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/manifest.json"}, "plist_path": {"kind": "path", "value": "~/Library/LaunchAgents/com.electricsheep.evaos-desktop-bridge.plist"}, @@ -94,6 +94,20 @@ process = { "executable": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/python/bin/python3.12", "argv0": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/src/evaos_desktop_bridge/cli.py", } +receipt_canary.candidate_snapshot(candidate, owner=owner, process=process) +wrong_owner = { + **owner, + "program_path": { + "kind": "path", + "value": "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge", + }, +} +try: + receipt_canary.candidate_snapshot(candidate, owner=wrong_owner, process=process) +except receipt_canary.CanaryError as error: + assert error.code == "canary_candidate_identity_invalid" +else: + raise AssertionError("launcher wrapper was accepted as the active packaged connector program") action_calls = [] tamper_audit = {"enabled": False} def runner(argv): diff --git a/tests/unit/evaos/evaosQaCanaryLocalControl.test.ts b/tests/unit/evaos/evaosQaCanaryLocalControl.test.ts index 75bc07aa4f..fa58309df2 100644 --- a/tests/unit/evaos/evaosQaCanaryLocalControl.test.ts +++ b/tests/unit/evaos/evaosQaCanaryLocalControl.test.ts @@ -59,7 +59,7 @@ from evaos_desktop_bridge.qa_canary import ( _suite_requires_selected_binding_proof, build_scenarios, ) -from evaos_desktop_bridge.state import read_control_session +from evaos_desktop_bridge.state import kill_control_session, read_control_session os.environ["EVAOS_DESKTOP_BRIDGE_DISABLE_TAKEOVER_WARNING_UI"] = "1" @@ -118,6 +118,7 @@ with TemporaryDirectory() as temporary_root: operator_ack_live_control=True, local_runner=local_runner, session_reader=lambda: read_control_session(root), + local_kill_switch=lambda: kill_control_session(root), ) full = surface.run( LOCAL_WORKBENCH_CONTROL_START, @@ -153,6 +154,10 @@ with TemporaryDirectory() as temporary_root: ("desktop_control_status", {}), ("desktop_control_status", {}), ] + assert surface.teardown_local_control() is True + final_state = read_control_session(root) + assert final_state["active"] is False and final_state["kill_switch"] is True + assert surface.teardown_local_control() is None print(json.dumps({"ok": True, "local_calls": len(local_argv), "remote_calls": len(remote.calls)})) `); @@ -160,6 +165,161 @@ print(json.dumps({"ok": True, "local_calls": len(local_argv), "remote_calls": le expect(JSON.parse(output)).toEqual({ ok: true, local_calls: 2, remote_calls: 3 }); }); + it('activates the local kill switch when an all-suite action is interrupted', () => { + const output = runPython(` +import os +from pathlib import Path +from tempfile import TemporaryDirectory + +from evaos_desktop_bridge.cli import _run_bridge_argv +from evaos_desktop_bridge.qa_canary import ( + CanaryStep, + LOCAL_WORKBENCH_CONTROL_START, + OperatorAcknowledgedLocalControlSurface, + SurfaceResponse, + run_steps_with_local_control_cleanup, +) +from evaos_desktop_bridge.state import kill_control_session, read_control_session + +os.environ["EVAOS_DESKTOP_BRIDGE_DISABLE_TAKEOVER_WARNING_UI"] = "1" + +class InterruptingSurface: + def __init__(self, state_reader): + self.state_reader = state_reader + + def run(self, command, params): + if command == "desktop_control_status": + return SurfaceResponse.from_payload({ + "ok": True, + "data": {"session": self.state_reader()}, + "warnings": [], + "errors": [], + "audit_id": "audit-status", + }) + raise KeyboardInterrupt("simulated operator cancellation") + +with TemporaryDirectory() as temporary_root: + root = Path(temporary_root) + cleanup_calls = [] + + def cleanup(): + cleanup_calls.append(True) + return kill_control_session(root) + + surface = OperatorAcknowledgedLocalControlSurface( + InterruptingSurface(lambda: read_control_session(root)), + operator_ack_live_control=True, + local_runner=lambda executable, argv, timeout_seconds: _run_bridge_argv(argv, state_dir=root), + session_reader=lambda: read_control_session(root), + local_kill_switch=cleanup, + ) + steps = [ + CanaryStep( + id="all.start", + suite="all", + command=LOCAL_WORKBENCH_CONTROL_START, + params={"mode": "ask-permission", "agent_label": "evaOS QA Canary"}, + ), + CanaryStep( + id="all.interrupted_action", + suite="all", + command="desktop_hotkey", + params={"keys": "escape", "dry_run": False}, + ), + ] + try: + run_steps_with_local_control_cleanup(steps, surface, suite="all") + except KeyboardInterrupt: + pass + else: + raise AssertionError("simulated operator cancellation did not propagate") + + final = read_control_session(root) + assert final["active"] is False and final["kill_switch"] is True + assert cleanup_calls == [True] + assert surface.local_control_cleanup_required is False + +print("ok") +`); + + expect(output).toBe('ok'); + }); + + it('converts SIGTERM job cancellation into local kill-switch cleanup', () => { + const output = runPython(` +import os +import signal +from pathlib import Path +from tempfile import TemporaryDirectory + +from evaos_desktop_bridge.cli import _run_bridge_argv +from evaos_desktop_bridge.qa_canary import ( + CanaryStep, + LOCAL_WORKBENCH_CONTROL_START, + OperatorAcknowledgedLocalControlSurface, + SurfaceResponse, + run_steps_with_local_control_cleanup, +) +from evaos_desktop_bridge.state import kill_control_session, read_control_session + +os.environ["EVAOS_DESKTOP_BRIDGE_DISABLE_TAKEOVER_WARNING_UI"] = "1" + +class TerminatingSurface: + def __init__(self, state_reader): + self.state_reader = state_reader + + def run(self, command, params): + if command == "desktop_control_status": + return SurfaceResponse.from_payload({ + "ok": True, + "data": {"session": self.state_reader()}, + "warnings": [], + "errors": [], + "audit_id": "audit-status", + }) + os.kill(os.getpid(), signal.SIGTERM) + raise AssertionError("SIGTERM did not stop the active canary step") + +with TemporaryDirectory() as temporary_root: + root = Path(temporary_root) + surface = OperatorAcknowledgedLocalControlSurface( + TerminatingSurface(lambda: read_control_session(root)), + operator_ack_live_control=True, + local_runner=lambda executable, argv, timeout_seconds: _run_bridge_argv(argv, state_dir=root), + session_reader=lambda: read_control_session(root), + local_kill_switch=lambda: kill_control_session(root), + ) + steps = [ + CanaryStep( + id="all.start", + suite="all", + command=LOCAL_WORKBENCH_CONTROL_START, + params={"mode": "full-access", "agent_label": "evaOS QA Canary"}, + ), + CanaryStep( + id="all.cancelled_action", + suite="all", + command="desktop_hotkey", + params={"keys": "escape", "dry_run": False}, + ), + ] + try: + run_steps_with_local_control_cleanup(steps, surface, suite="all") + except SystemExit as error: + assert error.code == 128 + signal.SIGTERM + else: + raise AssertionError("SIGTERM was not converted into an orderly canary exit") + + final = read_control_session(root) + assert final["active"] is False and final["kill_switch"] is True + assert signal.getsignal(signal.SIGTERM) == signal.SIG_DFL + +print("ok") +`); + + expect(output).toBe('ok'); + }); + it('fails closed without operator acknowledgement or with an inconsistent local CLI response', () => { const output = runPython(` import json diff --git a/tests/unit/process/evaosBetaReleaseGate.test.ts b/tests/unit/process/evaosBetaReleaseGate.test.ts index 3d720b1787..12808d3547 100644 --- a/tests/unit/process/evaosBetaReleaseGate.test.ts +++ b/tests/unit/process/evaosBetaReleaseGate.test.ts @@ -298,7 +298,7 @@ function writeArm64TrustEvidence(proofDir: string) { }, selected_binding: { ok: null, reason: 'selected_binding_proof_not_required_for_suite' }, }, - summary: { total: 5, passed: 5, failed: 0, skipped: 0 }, + summary: { total: 6, passed: 6, failed: 0, skipped: 0 }, results: [ { id: 'control_start.bridge_status', @@ -335,6 +335,13 @@ function writeArm64TrustEvidence(proofDir: string) { status: 'passed', params_redacted: {}, }, + { + id: 'control_cleanup.local_kill_switch', + command: 'desktop_kill_switch', + ok: true, + status: 'passed', + params_redacted: {}, + }, ], }, null, @@ -3707,7 +3714,7 @@ printf '%s\\n' ok connectorProof.results = connectorProof.results.filter( (result: { id?: string }) => result.id !== 'control_start.ask_permission' ); - connectorProof.summary = { total: 4, passed: 4, failed: 0, skipped: 0 }; + connectorProof.summary = { total: 5, passed: 5, failed: 0, skipped: 0 }; fs.writeFileSync(connectorProofPath, `${JSON.stringify(connectorProof, null, 2)}\n`); expect(() => releaseGate.verifyRcProof(proofDir, tag, { From 4898df661a8a2db611153d911663bb8550314b7b Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 14:14:35 +0700 Subject: [PATCH 12/17] fix: close v2.1.36 Mac control review blockers --- .../common/evaos/nativeCompanionBoundary.ts | 11 ++- .../evaos-beta/bridge/agent-tools/SOURCE.json | 8 +- .../bin/evaos-desktop-bridge-command | 15 ++- .../dist/src/runtimeReceipt.js | 8 ++ .../openclaw-plugin/src/runtimeReceipt.ts | 11 ++- .../tests/runtimeReceipt.test.mjs | 95 +++++++++++++++++++ .../evaos_desktop_bridge/receipt_canary.py | 50 +++++----- .../evaosDesktopBridgeControlSafetyP0.test.ts | 43 +++++++++ .../prepareEvaosDesktopBridgeResource.test.ts | 12 ++- 9 files changed, 216 insertions(+), 37 deletions(-) diff --git a/packages/desktop/src/common/evaos/nativeCompanionBoundary.ts b/packages/desktop/src/common/evaos/nativeCompanionBoundary.ts index b4f259a833..20f0581897 100644 --- a/packages/desktop/src/common/evaos/nativeCompanionBoundary.ts +++ b/packages/desktop/src/common/evaos/nativeCompanionBoundary.ts @@ -73,6 +73,13 @@ const EVAOS_SELECTED_BINDING_PROOF_ARG = '"${EVAOS_MAC_CONTROL_LIVE_CANARY_PROOF:?Set EVAOS_MAC_CONTROL_LIVE_CANARY_PROOF to the sanitized selected-binding runtime-receipt proof}"'; const EVAOS_SELECTED_BINDING_PROOF_RUN_ID_ARG = '"${EVAOS_MAC_CONTROL_LIVE_CANARY_RUN_ID:?Set EVAOS_MAC_CONTROL_LIVE_CANARY_RUN_ID to the exact proof workflow run id}"'; +const EVAOS_LIVE_CANARY_RECEIPT_KEY_ID_ENV = + 'EVAOS_LIVE_CANARY_RECEIPT_KEY_ID="${EVAOS_LIVE_CANARY_RECEIPT_KEY_ID:?Set EVAOS_LIVE_CANARY_RECEIPT_KEY_ID to the pinned receipt verifier key id}"'; +const EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY_ENV = + 'EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY="${EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY:?Set EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY to the pinned receipt verifier public key}"'; +const EVAOS_LIVE_CANARY_CONTEXT_KEY_ID_ENV = + 'EVAOS_LIVE_CANARY_CONTEXT_KEY_ID="${EVAOS_LIVE_CANARY_CONTEXT_KEY_ID:?Set EVAOS_LIVE_CANARY_CONTEXT_KEY_ID to the pinned execution-context key id}"'; +const EVAOS_SELECTED_BINDING_TRUST_ENV = `${EVAOS_LIVE_CANARY_RECEIPT_KEY_ID_ENV} ${EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY_ENV} ${EVAOS_LIVE_CANARY_CONTEXT_KEY_ID_ENV}`; export const EVAOS_NATIVE_COMPANION_CANARIES = [ { @@ -83,13 +90,13 @@ export const EVAOS_NATIVE_COMPANION_CANARIES = [ }, { id: 'connector-all', - command: `${EVAOS_PACKAGED_BRIDGE_COMMAND} qa-canary --connector-url ${EVAOS_CONNECTOR_URL_ARG} --artifact-dir ${EVAOS_CANARY_ARTIFACT_DIR_ARG} --version-under-test ${EVAOS_EXPECTED_WORKBENCH_VERSION_ARG} --build-under-test ${EVAOS_EXPECTED_WORKBENCH_BUILD_ARG} --source-commit-under-test ${EVAOS_EXPECTED_SOURCE_COMMIT_ARG} --selected-binding-proof ${EVAOS_SELECTED_BINDING_PROOF_ARG} --selected-binding-proof-run-id ${EVAOS_SELECTED_BINDING_PROOF_RUN_ID_ARG} --surface connector --suite all --operator-ack-live-control`, + command: `${EVAOS_SELECTED_BINDING_TRUST_ENV} ${EVAOS_PACKAGED_BRIDGE_COMMAND} qa-canary --connector-url ${EVAOS_CONNECTOR_URL_ARG} --artifact-dir ${EVAOS_CANARY_ARTIFACT_DIR_ARG} --version-under-test ${EVAOS_EXPECTED_WORKBENCH_VERSION_ARG} --build-under-test ${EVAOS_EXPECTED_WORKBENCH_BUILD_ARG} --source-commit-under-test ${EVAOS_EXPECTED_SOURCE_COMMIT_ARG} --selected-binding-proof ${EVAOS_SELECTED_BINDING_PROOF_ARG} --selected-binding-proof-run-id ${EVAOS_SELECTED_BINDING_PROOF_RUN_ID_ARG} --surface connector --suite all --operator-ack-live-control`, requiredArtifact: 'qa-report.json', forbidsSkips: true, }, { id: 'connector-kill-switch', - command: `${EVAOS_PACKAGED_BRIDGE_COMMAND} qa-canary --connector-url ${EVAOS_CONNECTOR_URL_ARG} --artifact-dir ${EVAOS_CANARY_ARTIFACT_DIR_ARG} --version-under-test ${EVAOS_EXPECTED_WORKBENCH_VERSION_ARG} --build-under-test ${EVAOS_EXPECTED_WORKBENCH_BUILD_ARG} --source-commit-under-test ${EVAOS_EXPECTED_SOURCE_COMMIT_ARG} --selected-binding-proof ${EVAOS_SELECTED_BINDING_PROOF_ARG} --selected-binding-proof-run-id ${EVAOS_SELECTED_BINDING_PROOF_RUN_ID_ARG} --surface connector --suite kill_switch --operator-ack-live-control`, + command: `${EVAOS_SELECTED_BINDING_TRUST_ENV} ${EVAOS_PACKAGED_BRIDGE_COMMAND} qa-canary --connector-url ${EVAOS_CONNECTOR_URL_ARG} --artifact-dir ${EVAOS_CANARY_ARTIFACT_DIR_ARG} --version-under-test ${EVAOS_EXPECTED_WORKBENCH_VERSION_ARG} --build-under-test ${EVAOS_EXPECTED_WORKBENCH_BUILD_ARG} --source-commit-under-test ${EVAOS_EXPECTED_SOURCE_COMMIT_ARG} --selected-binding-proof ${EVAOS_SELECTED_BINDING_PROOF_ARG} --selected-binding-proof-run-id ${EVAOS_SELECTED_BINDING_PROOF_RUN_ID_ARG} --surface connector --suite kill_switch --operator-ack-live-control`, requiredArtifact: 'qa-report.json', forbidsSkips: true, }, diff --git a/resources/evaos-beta/bridge/agent-tools/SOURCE.json b/resources/evaos-beta/bridge/agent-tools/SOURCE.json index 2d27345a6a..b69e9b0eee 100644 --- a/resources/evaos-beta/bridge/agent-tools/SOURCE.json +++ b/resources/evaos-beta/bridge/agent-tools/SOURCE.json @@ -9,12 +9,12 @@ "canonicalizedAt": "2026-07-15", "sourceDigests": { "hermes-adapter/README.md": "sha256:1bbef6050a512f42151dc5dc895474b2258ebf887192fe23796583e0b14b6a78", - "hermes-adapter/bin/evaos-desktop-bridge-command": "sha256:d5985f9289b53a278c905278d31a7130392cf6e7b56a21b5e61210d43b19d49a", + "hermes-adapter/bin/evaos-desktop-bridge-command": "sha256:1e9fd64dde0f90d0d88234ea375c30fa5f77c1a697bffcdb337d0908aab8a433", "openclaw-plugin/README.md": "sha256:ce371b7ed539092b7cad9245883f8fb5e91c0c8f56ff094164a0635716128737", "openclaw-plugin/dist/index.js": "sha256:087cbabd678efa15d3657b7137c4da0c8af3b8a4f4b55eef0492e957c8bf62c9", "openclaw-plugin/dist/src/bridge.js": "sha256:625b964db6c16a3e7e574d4c86c757a1a11861af01daa6c4abb5c7cdcb947abf", "openclaw-plugin/dist/src/firewall.js": "sha256:fc86631e8df350355f2084a5dac43ba81c58e32cf29d87aa610ac88268b19f6c", - "openclaw-plugin/dist/src/runtimeReceipt.js": "sha256:53007de6bf12fe64997ac846802385f1201e3e1bf47ae49cfdaed54777ea1839", + "openclaw-plugin/dist/src/runtimeReceipt.js": "sha256:31deeff5d80951ccb1ad429a42769e7b8ddd00610365bd9bbf84d32f902e0780", "openclaw-plugin/dist/src/toolParameters.js": "sha256:a441d521980b64e571918ab4b769d13c764fdc05960303b4683ca5d8c59b5a73", "openclaw-plugin/index.ts": "sha256:0c472c05dde30746e87b5c0b8276e13d0cf572bd47460c7d04cc0049f64ff579", "openclaw-plugin/openclaw.plugin.json": "sha256:b145a479aad1209f528a2d1c69e2cf99ec8d2906957f668e3e124113a7276e79", @@ -24,11 +24,11 @@ "openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs": "sha256:6a91ee0fc8ba7269ee5ff8ca3a79d7bfadb9139ed1eb629642c9c2a688130062", "openclaw-plugin/src/bridge.ts": "sha256:d0e0da280bd83dc71734579d12bbb8c37a50c5e8d746fb4ac967e7b69862a1f1", "openclaw-plugin/src/firewall.ts": "sha256:e8ef55500e577f828cb9a1422d0496bec9e5b9f0bb5ac65e06c527220aa4cf22", - "openclaw-plugin/src/runtimeReceipt.ts": "sha256:c263ce591790d486243174a26aa0d654f19398c6d8b7da5fe1688d7860ea83ef", + "openclaw-plugin/src/runtimeReceipt.ts": "sha256:89278b1aec300454bb011d2d9f1aaac85679219ce793a40d48e99c12bcd4eb0f", "openclaw-plugin/src/toolParameters.ts": "sha256:dcca4a1615f7c138a03b2ff0da5e7739b2d0f29bf25fd3ffa3c9bc85b8c61b7c", "openclaw-plugin/src/types.d.ts": "sha256:6144e747e97c133ddaefe37af986865d6b87d842b922026a2af14b010c946bfc", "openclaw-plugin/tests/pluginContract.test.mjs": "sha256:33679173a252fdd97e7f9e43747fd65bce7d5a713b7f8dd7a904774d4a45a7ba", - "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:37ac5d2ed14c5922a2ce90bb5152e1d79932b7e7ad51af7681401b71232beefc", + "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:205a3aaa858d4e717d1953d461f75ec5e3dfb9cee5eebf31e3b38fea1747ce16", "openclaw-plugin/tsconfig.json": "sha256:d5ceeadf205f97973f4d2a1e13ef20f9af854c1fbc01823af60b02eed5d5b34b" } } diff --git a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command index 1b7cbe0b5e..4e33c66045 100755 --- a/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command +++ b/resources/evaos-beta/bridge/agent-tools/hermes-adapter/bin/evaos-desktop-bridge-command @@ -26,6 +26,9 @@ read_connector_env_file() { return 2 } key="${line%%=*}" + if [[ "$key" == "export "* ]]; then + key="${key#export }" + fi value="${line#*=}" decoded_value="$(printf '%s' "$value" | python3 -c 'import shlex, sys raw = sys.stdin.read() @@ -43,8 +46,10 @@ sys.stdout.write(parts[0])')" || { return 2 } seen_url=1 - EVAOS_DESKTOP_BRIDGE_URL="$decoded_value" - export EVAOS_DESKTOP_BRIDGE_URL + if [[ -z "${EVAOS_DESKTOP_BRIDGE_URL:-}" ]]; then + EVAOS_DESKTOP_BRIDGE_URL="$decoded_value" + export EVAOS_DESKTOP_BRIDGE_URL + fi ;; EVAOS_DESKTOP_BRIDGE_TOKEN) [[ "$seen_token" == 0 ]] || { @@ -52,8 +57,10 @@ sys.stdout.write(parts[0])')" || { return 2 } seen_token=1 - EVAOS_DESKTOP_BRIDGE_TOKEN="$decoded_value" - export EVAOS_DESKTOP_BRIDGE_TOKEN + if [[ -z "${EVAOS_DESKTOP_BRIDGE_TOKEN:-}" ]]; then + EVAOS_DESKTOP_BRIDGE_TOKEN="$decoded_value" + export EVAOS_DESKTOP_BRIDGE_TOKEN + fi ;; *) echo "invalid connector environment key" >&2 diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js index e39961f2e9..0f0e2f6302 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js @@ -207,6 +207,7 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { signal: controller.signal, }); if (!connectorResponse.ok) { + await cancelConnectorResponseBody(connectorResponse); sendError( response, connectorResponse.status >= 400 && connectorResponse.status < 500 ? 409 : 502, @@ -235,6 +236,13 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { return true; }; } +async function cancelConnectorResponseBody(response) { + try { + await response.body?.cancel?.(); + } catch { + // Preserve the sanitized connector rejection even if stream cleanup fails. + } +} async function readPublicRequest(request) { const chunks = []; let total = 0; diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts index d950675310..5b19f51eb5 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts @@ -148,7 +148,7 @@ type ResponseLike = { type FetchResponseLike = { ok: boolean; status: number; - body?: AsyncIterable | null; + body?: (AsyncIterable & { cancel?: () => Promise | void }) | null; text(): Promise; }; @@ -337,6 +337,7 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt signal: controller.signal, }); if (!connectorResponse.ok) { + await cancelConnectorResponseBody(connectorResponse); sendError( response, connectorResponse.status >= 400 && connectorResponse.status < 500 ? 409 : 502, @@ -367,6 +368,14 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt }; } +async function cancelConnectorResponseBody(response: FetchResponseLike): Promise { + try { + await response.body?.cancel?.(); + } catch { + // Preserve the sanitized connector rejection even if stream cleanup fails. + } +} + async function readPublicRequest( request: RequestLike ): Promise<{ ok: true; value: PublicRequest } | { ok: false; status: number }> { diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs index 246bcef913..d0c454ef3b 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs @@ -163,6 +163,65 @@ test('Hermes decodes inert shell-quoted connector values exactly', async () => { } }); +test('Hermes accepts export-form env files without overriding explicit connector credentials', async () => { + const wrapper = fileURLToPath(new URL('../../hermes-adapter/bin/evaos-desktop-bridge-command', import.meta.url)); + const directory = mkdtempSync(join(tmpdir(), 'evaos-hermes-export-env-')); + const envFile = join(directory, 'connector.env'); + const fileToken = 'file connector token at least 24'; + const explicitToken = 'explicit connector token at least 24'; + const authorizations = []; + const server = createServer((request, response) => { + authorizations.push(String(request.headers.authorization || '')); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end('{"ok":true,"schema":"evaos.desktop_bridge.response.v1"}'); + }); + try { + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', resolve); + }); + const address = server.address(); + assert.equal(typeof address, 'object'); + const explicitUrl = `http://127.0.0.1:${address.port}`; + + writeFileSync( + envFile, + `export EVAOS_DESKTOP_BRIDGE_URL='http://127.0.0.1:1'\nexport EVAOS_DESKTOP_BRIDGE_TOKEN='${fileToken}'\n`, + { encoding: 'utf8', mode: 0o600 } + ); + const urlPreserved = await execFileAsync(wrapper, ['customerMacStatus'], { + encoding: 'utf8', + env: { + PATH: process.env.PATH, + EVAOS_DESKTOP_BRIDGE_ENV_FILE: envFile, + EVAOS_DESKTOP_BRIDGE_URL: explicitUrl, + }, + timeout: 5_000, + }); + assert.equal(JSON.parse(urlPreserved.stdout).ok, true); + + writeFileSync( + envFile, + `export EVAOS_DESKTOP_BRIDGE_URL='${explicitUrl}'\nexport EVAOS_DESKTOP_BRIDGE_TOKEN='${fileToken}'\n`, + { encoding: 'utf8', mode: 0o600 } + ); + const tokenPreserved = await execFileAsync(wrapper, ['customerMacStatus'], { + encoding: 'utf8', + env: { + PATH: process.env.PATH, + EVAOS_DESKTOP_BRIDGE_ENV_FILE: envFile, + EVAOS_DESKTOP_BRIDGE_TOKEN: explicitToken, + }, + timeout: 5_000, + }); + assert.equal(JSON.parse(tokenPreserved.stdout).ok, true); + assert.deepEqual(authorizations, [`Bearer ${fileToken}`, `Bearer ${explicitToken}`]); + } finally { + await new Promise((resolve) => server.close(resolve)); + rmSync(directory, { recursive: true, force: true }); + } +}); + test('rejects caller-supplied local payload file paths', async () => { const message = await runBridge('codexSendVisibleMessage', { thread_id: 'thread-1', @@ -366,6 +425,42 @@ test('keeps the connector deadline active through response body consumption', as assert.equal(connectorCalls, 1); }); +test('cancels a rejected streaming connector response before returning', async () => { + const authority = signedAuthority(); + let canceled = false; + let textCalled = false; + const handler = createMacControlRuntimeReceiptHandler({ + env, + now: () => NOW_MS, + fetchImpl: async () => ({ + ok: false, + status: 503, + body: { + async *[Symbol.asyncIterator]() { + throw new Error('rejected body must not be consumed'); + }, + async cancel() { + canceled = true; + }, + }, + text: async () => { + textCalled = true; + return 'must not be read'; + }, + }), + }); + const response = await invoke( + handler, + { challenge: Buffer.alloc(32, 15).toString('base64url'), runRef: 'connector-rejected-stream' }, + authority.headers + ); + assert.equal(response.statusCode, 502); + assert.equal(JSON.parse(response.body).error.code, 'connector_rejected_canary'); + assert.equal(canceled, true); + assert.equal(textCalled, false); + assert.equal(response.body.includes('must not'), false); +}); + test('rejects an oversized streaming connector body before reading beyond the cap', async () => { const authority = signedAuthority(); let textCalled = false; diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py index 9545539deb..7f5f457cc0 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/receipt_canary.py @@ -2,6 +2,7 @@ import base64 import binascii +import fcntl import hashlib import json import os @@ -288,35 +289,36 @@ def burn_replay_token( replay_path = root / REPLAY_FILE digest = replay_digest(raw_context, signature) context_id_digest = hashlib.sha256(context_id.encode("utf-8")).hexdigest() - if replay_path.exists(): - metadata = replay_path.lstat() - if replay_path.is_symlink() or not stat.S_ISREG(metadata.st_mode): - raise CanaryError("canary_replay_store_unavailable", status=503) - try: - if metadata.st_size > 4 * 1024 * 1024: - raise CanaryError("canary_replay_store_unavailable", status=503) - for line in replay_path.read_text(encoding="utf-8").splitlines(): - record = json.loads(line) - if isinstance(record, dict) and ( - record.get("digest") == digest - or record.get("contextIdDigest") == context_id_digest - ): - raise CanaryError("execution_context_replayed", status=409) - except CanaryError: - raise - except (OSError, UnicodeError, json.JSONDecodeError) as exc: - raise CanaryError("canary_replay_store_unavailable", status=503) from exc flags = ( - os.O_APPEND - | os.O_CREAT - | os.O_WRONLY + os.O_CREAT + | os.O_RDWR | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) ) try: descriptor = os.open(replay_path, flags, 0o600) os.fchmod(descriptor, 0o600) - with os.fdopen(descriptor, "a", encoding="utf-8") as handle: + with os.fdopen(descriptor, "r+", encoding="utf-8") as handle: + fcntl.flock(handle.fileno(), fcntl.LOCK_EX) + metadata = os.fstat(handle.fileno()) + path_metadata = replay_path.lstat() + if ( + not stat.S_ISREG(metadata.st_mode) + or not stat.S_ISREG(path_metadata.st_mode) + or (metadata.st_dev, metadata.st_ino) + != (path_metadata.st_dev, path_metadata.st_ino) + or metadata.st_size > 4 * 1024 * 1024 + ): + raise CanaryError("canary_replay_store_unavailable", status=503) + handle.seek(0) + for line in handle: + record = json.loads(line) + if isinstance(record, dict) and ( + record.get("digest") == digest + or record.get("contextIdDigest") == context_id_digest + ): + raise CanaryError("execution_context_replayed", status=409) + handle.seek(0, os.SEEK_END) handle.write( json.dumps( {"contextIdDigest": context_id_digest, "digest": digest}, @@ -327,7 +329,9 @@ def burn_replay_token( ) handle.flush() os.fsync(handle.fileno()) - except OSError as exc: + except CanaryError: + raise + except (OSError, UnicodeError, json.JSONDecodeError) as exc: raise CanaryError("canary_replay_store_unavailable", status=503) from exc diff --git a/tests/unit/evaos/evaosDesktopBridgeControlSafetyP0.test.ts b/tests/unit/evaos/evaosDesktopBridgeControlSafetyP0.test.ts index e4a79e3e6a..c45e432829 100644 --- a/tests/unit/evaos/evaosDesktopBridgeControlSafetyP0.test.ts +++ b/tests/unit/evaos/evaosDesktopBridgeControlSafetyP0.test.ts @@ -417,6 +417,49 @@ with TemporaryDirectory() as temporary_root: server.shutdown() server.server_close() +print("ok") +`); + + expect(output).toBe('ok'); + }); + + it('burns one execution context atomically across concurrent connector requests', () => { + const output = runPython(` +import concurrent.futures +import json +import threading +from pathlib import Path +from tempfile import TemporaryDirectory + +from evaos_desktop_bridge.receipt_canary import CanaryError, REPLAY_FILE, burn_replay_token + +worker_count = 8 +barrier = threading.Barrier(worker_count) + +with TemporaryDirectory() as temporary_root: + root = Path(temporary_root) + + def attempt_burn(_index): + barrier.wait(timeout=5) + try: + burn_replay_token( + b"signed-context-payload", + b"signed-context-signature", + "context-concurrency-proof", + state_dir=root, + ) + return "burned" + except CanaryError as exc: + return exc.code + + with concurrent.futures.ThreadPoolExecutor(max_workers=worker_count) as executor: + outcomes = list(executor.map(attempt_burn, range(worker_count))) + + assert outcomes.count("burned") == 1, outcomes + assert outcomes.count("execution_context_replayed") == worker_count - 1, outcomes + records = [json.loads(line) for line in (root / REPLAY_FILE).read_text(encoding="utf-8").splitlines()] + assert len(records) == 1, records + print("ok") `); diff --git a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts index 37d51b1c87..dfad127c37 100644 --- a/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts +++ b/tests/unit/process/prepareEvaosDesktopBridgeResource.test.ts @@ -235,15 +235,18 @@ describe('prepareEvaosDesktopBridgeResource', () => { 'connector-kill-switch', ]); for (const canary of EVAOS_NATIVE_COMPANION_CANARIES) { - expect(canary.command).toMatch( - /^"\/Applications\/evaOS Workbench\.app\/Contents\/Resources\/Bridge\/evaos-desktop-bridge" (pre-canary|qa-canary) / - ); expect(canary.command).toContain('--artifact-dir "${EVAOS_CANARY_ARTIFACT_DIR:'); expect(canary.command).toContain('${EVAOS_WORKBENCH_EXPECTED_VERSION:'); expect(canary.command).toContain('${EVAOS_WORKBENCH_EXPECTED_SOURCE_COMMIT:'); expect(canary.command).not.toContain('PYTHONPATH='); expect(canary.command).not.toMatch(/\bpython3\b/); if (canary.id !== 'pre-canary-bridge-peekaboo') { + expect(canary.command).toContain(`${EVAOS_PACKAGED_BRIDGE_COMMAND} qa-canary `); + expect(canary.command).toContain('EVAOS_LIVE_CANARY_RECEIPT_KEY_ID="${EVAOS_LIVE_CANARY_RECEIPT_KEY_ID:'); + expect(canary.command).toContain( + 'EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY="${EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY:' + ); + expect(canary.command).toContain('EVAOS_LIVE_CANARY_CONTEXT_KEY_ID="${EVAOS_LIVE_CANARY_CONTEXT_KEY_ID:'); expect(canary.command).toContain('--connector-url "${EVAOS_DESKTOP_BRIDGE_URL:'); expect(canary.command).toContain('--version-under-test'); expect(canary.command).toContain('--build-under-test'); @@ -252,6 +255,9 @@ describe('prepareEvaosDesktopBridgeResource', () => { expect(canary.command).toContain('--selected-binding-proof-run-id "${EVAOS_MAC_CONTROL_LIVE_CANARY_RUN_ID:'); expect(canary.command).toContain('${EVAOS_WORKBENCH_EXPECTED_BUILD:'); } else { + expect(canary.command).toMatch( + /^"\/Applications\/evaOS Workbench\.app\/Contents\/Resources\/Bridge\/evaos-desktop-bridge" pre-canary / + ); expect(canary.command).toContain('${EVAOS_WORKBENCH_EXPECTED_BUILD:'); expect(canary.command).toContain('--expected-source-commit'); } From 4e35e5edda4a74398d5d08aca70651e93125aee9 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 14:58:11 +0700 Subject: [PATCH 13/17] fix(evaos): carry signed canary config into connector --- .../bridge/src/evaos_desktop_bridge/cli.py | 36 +++++++-- .../evaos/evaosDesktopBridgeReceipt.test.ts | 76 +++++++++++++++++++ 2 files changed, 105 insertions(+), 7 deletions(-) diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/cli.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/cli.py index 68362fa820..952afdb3d4 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/cli.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/cli.py @@ -14,6 +14,7 @@ import subprocess import sys import time +from collections.abc import Mapping from pathlib import Path from typing import Any, Callable, TextIO @@ -1788,6 +1789,12 @@ def _connector_label_from_env(env: dict[str, str] | None = None) -> str: CONNECTOR_HTTP_RESPONSE_LIMIT_BYTES = 65536 CONNECTOR_SYSTEM_PLIST = Path(f"/Library/LaunchAgents/{CONNECTOR_LABEL}.plist") CONNECTOR_USER_PLIST = Path.home() / "Library" / "LaunchAgents" / f"{CONNECTOR_LABEL}.plist" +CONNECTOR_MAC_CONTROL_CANARY_ENV_KEYS = ( + "EVAOS_MAC_CONTROL_CONTEXT_KEY_ID", + "EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY", + "EVAOS_MAC_CONTROL_RECEIPT_KEY_ID", + "EVAOS_MAC_CONTROL_RECEIPT_PRIVATE_KEY_PATH", +) PEEKABOO_BIN_CANDIDATES = ( "peekaboo", "evaos-connector-helper", @@ -3275,10 +3282,11 @@ def _connector_plist_path() -> Path | None: return None -def _ensure_connector_user_plist() -> Path: - host = os.environ.get("EVAOS_DESKTOP_BRIDGE_CONNECTOR_HOST") or _tailscale_ip() or "127.0.0.1" +def _ensure_connector_user_plist(*, env: Mapping[str, str] | None = None) -> Path: + source_env = env if env is not None else os.environ + host = source_env.get("EVAOS_DESKTOP_BRIDGE_CONNECTOR_HOST") or _tailscale_ip() or "127.0.0.1" program = _connector_program_path() - payload = _connector_plist_payload(program=program, host=host) + payload = _connector_plist_payload(program=program, host=host, env=source_env) CONNECTOR_USER_PLIST.parent.mkdir(parents=True, exist_ok=True) current: dict[str, object] | None = None if CONNECTOR_USER_PLIST.exists(): @@ -3310,7 +3318,23 @@ def _connector_program_path() -> str: return "evaos-desktop-bridge" -def _connector_plist_payload(*, program: str, host: str) -> dict[str, object]: +def _connector_mac_control_canary_environment(env: Mapping[str, str] | None = None) -> dict[str, str]: + source_env = env if env is not None else os.environ + configured = {key: str(source_env.get(key) or "").strip() for key in CONNECTOR_MAC_CONTROL_CANARY_ENV_KEYS} + present = {key: value for key, value in configured.items() if value} + if present and len(present) != len(CONNECTOR_MAC_CONTROL_CANARY_ENV_KEYS): + missing = sorted(key for key, value in configured.items() if not value) + raise RuntimeError("Mac-control canary configuration is incomplete; missing " + ", ".join(missing)) + return present + + +def _connector_plist_payload( + *, program: str, host: str, env: Mapping[str, str] | None = None +) -> dict[str, object]: + environment_variables = { + "EVAOS_DESKTOP_BRIDGE_MODE": "customer-mac-connector", + **_connector_mac_control_canary_environment(env), + } return { "Label": CONNECTOR_LABEL, "ProgramArguments": [ @@ -3323,9 +3347,7 @@ def _connector_plist_payload(*, program: str, host: str) -> dict[str, object]: ], "RunAtLoad": True, "KeepAlive": True, - "EnvironmentVariables": { - "EVAOS_DESKTOP_BRIDGE_MODE": "customer-mac-connector", - }, + "EnvironmentVariables": environment_variables, } diff --git a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts index 7384a468ea..fb727da0a2 100644 --- a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts +++ b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts @@ -6,6 +6,82 @@ const repoRoot = process.cwd(); const bridgeSource = join(repoRoot, 'resources', 'evaos-beta', 'bridge', 'src'); describe('evaOS signed Mac-control receipt connector', () => { + it('carries only a complete signer configuration through the normal LaunchAgent start path', () => { + const script = String.raw` +import os +import plistlib +import tempfile +from pathlib import Path + +from evaos_desktop_bridge import cli + +root = Path(tempfile.mkdtemp(prefix="evaos-connector-plist-")) +cli.CONNECTOR_USER_PLIST = root / "connector.plist" +cli._connector_program_path = lambda: "/Applications/evaOS Workbench.app/Contents/Resources/Bridge/evaos-desktop-bridge" +cli._tailscale_ip = lambda: "127.0.0.1" +launchctl_calls = [] +cli._run_launchctl = lambda args: launchctl_calls.append(list(args)) or {"returncode": 0} + +signer_env = { + "EVAOS_MAC_CONTROL_CONTEXT_KEY_ID": "context-key-v1", + "EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY": "cHVibGljLWtleQ", + "EVAOS_MAC_CONTROL_RECEIPT_KEY_ID": "receipt-key-v1", + "EVAOS_MAC_CONTROL_RECEIPT_PRIVATE_KEY_PATH": str(root / "receipt-key"), +} +managed_keys = (*cli.CONNECTOR_MAC_CONTROL_CANARY_ENV_KEYS, "EVAOS_DESKTOP_BRIDGE_CONNECTOR_HOST") +previous = {key: os.environ.get(key) for key in managed_keys} +try: + for key in managed_keys: + os.environ.pop(key, None) + os.environ.update(signer_env) + cli._launchctl_start() + + payload = plistlib.loads(cli.CONNECTOR_USER_PLIST.read_bytes()) + assert payload["EnvironmentVariables"] == { + "EVAOS_DESKTOP_BRIDGE_MODE": "customer-mac-connector", + **signer_env, + } + assert len(launchctl_calls) == 3 + assert cli._connector_plist_payload(program="bridge", host="127.0.0.1", env={})[ + "EnvironmentVariables" + ] == {"EVAOS_DESKTOP_BRIDGE_MODE": "customer-mac-connector"} + + launchctl_calls.clear() + cli.CONNECTOR_USER_PLIST = root / "partial.plist" + for key in cli.CONNECTOR_MAC_CONTROL_CANARY_ENV_KEYS: + os.environ.pop(key, None) + os.environ["EVAOS_MAC_CONTROL_CONTEXT_KEY_ID"] = "context-key-v1" + try: + cli._launchctl_start() + except RuntimeError as error: + assert "configuration is incomplete" in str(error) + assert "context-key-v1" not in str(error) + else: + raise AssertionError("partial signer configuration was accepted") + assert not cli.CONNECTOR_USER_PLIST.exists() + assert launchctl_calls == [] +finally: + for key in managed_keys: + os.environ.pop(key, None) + for key, value in previous.items(): + if value is not None: + os.environ[key] = value +`; + + expect(() => + execFileSync('python3', ['-c', script], { + cwd: repoRoot, + env: { + ...process.env, + PYTHONPATH: bridgeSource, + PYTHONDONTWRITEBYTECODE: '1', + }, + stdio: ['ignore', 'pipe', 'pipe'], + timeout: 10_000, + }) + ).not.toThrow(); + }); + it('fails closed around the fixed authenticated canary route and emits a verifiable sanitized receipt', () => { const script = String.raw` import base64 From 9cd9bd1d2e84e18238d8c36123efac83760ca16f Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 15:12:58 +0700 Subject: [PATCH 14/17] fix(evaos): split Mac-control proof scan modes --- .github/workflows/evaos-live-canary-proof.yml | 13 +- scripts/evaosScanMacControlProofs.js | 95 +++++++--- .../evaos/evaosLiveCanaryWorkflow.test.ts | 170 +++++++++++++++++- 3 files changed, 245 insertions(+), 33 deletions(-) diff --git a/.github/workflows/evaos-live-canary-proof.yml b/.github/workflows/evaos-live-canary-proof.yml index d62991e31c..368b93f5ad 100644 --- a/.github/workflows/evaos-live-canary-proof.yml +++ b/.github/workflows/evaos-live-canary-proof.yml @@ -268,9 +268,16 @@ jobs: set -euo pipefail node scripts/evaosProvisionLiveCanaryFixtures.js cleanup-mac-control > "$PROOF_DIR/mac-control-session-cleanup.stdout.json" - - name: Run Mac-control proof secret/redaction scan - id: mac-control-proof-scan + - name: Scan existing Mac-control proofs for safe diagnostic upload + id: mac-control-partial-proof-scan if: always() && github.event.inputs.run_mac_control_canary == 'true' + run: | + set -euo pipefail + node scripts/evaosScanMacControlProofs.js --allow-partial "$PROOF_DIR" + + - name: Require complete Mac-control proof set + id: mac-control-complete-proof-scan + if: success() && github.event.inputs.run_live_canaries == 'true' && github.event.inputs.run_mac_control_canary == 'true' && github.event.inputs.provision_fixtures == 'true' && steps.mac-control-partial-proof-scan.outcome == 'success' run: | set -euo pipefail node scripts/evaosScanMacControlProofs.js "$PROOF_DIR" @@ -296,7 +303,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Upload live canary proof packet - if: always() && (github.event.inputs.run_mac_control_canary != 'true' || steps.mac-control-proof-scan.outcome == 'success') + if: always() && (github.event.inputs.run_mac_control_canary != 'true' || steps.mac-control-partial-proof-scan.outcome == 'success') uses: actions/upload-artifact@v4 with: name: evaos-live-canary-proof-${{ github.run_id }} diff --git a/scripts/evaosScanMacControlProofs.js b/scripts/evaosScanMacControlProofs.js index 0acaefa17a..f7c6b8d0df 100644 --- a/scripts/evaosScanMacControlProofs.js +++ b/scripts/evaosScanMacControlProofs.js @@ -12,20 +12,21 @@ const MAC_CONTROL_PROOF_NAMES = Object.freeze([ 'mac-control-session-cleanup.json', 'mac-control-session-cleanup.stdout.json', ]); +const MAC_CONTROL_RUNTIME_NEGATIVE_PROOF_CONTRACT = Object.freeze({ + schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', + fields: ['schema', 'sourceHeadSha', 'sourceRunId', 'assertions'], + nested: { + assertions: { + fields: ['forgedContextRejected', 'expiredContextRejected', 'replayRejected', 'authorityRedacted'], + }, + }, +}); const MAC_CONTROL_PROOF_CONTRACTS = Object.freeze({ 'mac-control-runtime.json': { schema: 'evaos.mac_control.public_runtime_attestation_envelope.v1', fields: ['schema', 'attestationBase64', 'signature', 'keyId', 'namespace'], }, - 'mac-control-runtime-negative.json': { - schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', - fields: ['schema', 'sourceHeadSha', 'sourceRunId', 'assertions'], - nested: { - assertions: { - fields: ['forgedContextRejected', 'expiredContextRejected', 'replayRejected', 'authorityRedacted'], - }, - }, - }, + 'mac-control-runtime-negative.json': MAC_CONTROL_RUNTIME_NEGATIVE_PROOF_CONTRACT, 'mac-control-deployed-route.json': { schema: 'evaos.mac_control.deployed_route_probe.v1', fields: ['schema', 'sourceHeadSha', 'sourceRunId', 'checkedAt', 'assertions'], @@ -178,6 +179,12 @@ function assertExactProofContract(value, contract, location) { } } +function assertSensitiveOutputPassed(value, contract, location) { + if (contract.fields.includes('sensitiveOutput') && value.sensitiveOutput !== 'passed') { + throw new Error(`Mac-control proof did not pass the sensitive-output contract in ${location}.`); + } +} + function assertPublicAttestationEnvelopeSanitized(value, location) { const contract = MAC_CONTROL_PROOF_CONTRACTS['mac-control-runtime.json']; assertExactProofContract(value, contract, location); @@ -243,14 +250,24 @@ function assertPublicAttestationEnvelopeSanitized(value, location) { } } -function scanMacControlProofDirectory(proofDir) { +function scanMacControlProofDirectory(proofDir, options = {}) { const resolvedProofDir = path.resolve(String(proofDir || '')); - let scanned = 0; + const allowPartial = options.allowPartial === true; + const missing = []; + const existing = []; + for (const proofName of MAC_CONTROL_PROOF_NAMES) { - const proofPath = path.join(resolvedProofDir, proofName); - if (!fs.existsSync(proofPath)) { - throw new Error(`Mac-control proof is missing required artifact ${proofName}.`); + if (fs.existsSync(path.join(resolvedProofDir, proofName))) { + existing.push(proofName); + } else { + missing.push(proofName); } + } + + let scanned = 0; + const parsedProofs = new Map(); + for (const proofName of existing) { + const proofPath = path.join(resolvedProofDir, proofName); const proof = JSON.parse(fs.readFileSync(proofPath, 'utf8')); const contract = MAC_CONTROL_PROOF_CONTRACTS[proofName]; if (!contract || proof.schema !== contract.schema) { @@ -262,28 +279,58 @@ function scanMacControlProofDirectory(proofDir) { assertMacControlProofSanitized(proof, proofName); assertExactProofContract(proof, contract, proofName); } + assertSensitiveOutputPassed(proof, contract, proofName); + parsedProofs.set(proofName, proof); scanned += 1; } - const cleanupPath = path.join(resolvedProofDir, 'mac-control-session-cleanup.json'); - const cleanupProof = JSON.parse(fs.readFileSync(cleanupPath, 'utf8')); - if ( - cleanupProof.schema !== 'evaos-mac-control-canary-session-cleanup/v1' || - cleanupProof.sessionRevoked !== true || - cleanupProof.sensitiveOutput !== 'passed' - ) { + + if (allowPartial) { + return { ok: true, scanned, missing }; + } + if (missing.length > 0) { + throw new Error(`Mac-control proof is missing required artifacts: ${missing.join(', ')}.`); + } + + const cleanupProof = parsedProofs.get('mac-control-session-cleanup.json'); + if (cleanupProof.sessionRevoked !== true) { throw new Error('Mac-control proof did not prove temporary session revocation.'); } return { ok: true, scanned }; } -if (require.main === module) { - const proofDir = process.argv[2]; +function parseScanArguments(args) { + let allowPartial = false; + let proofDir = ''; + for (const arg of args) { + if (arg === '--allow-partial') { + allowPartial = true; + } else if (arg.startsWith('--')) { + throw new Error(`Unknown Mac-control proof scanner option: ${arg}.`); + } else if (proofDir) { + throw new Error('Mac-control proof scanner accepts exactly one proof directory.'); + } else { + proofDir = arg; + } + } if (!proofDir) throw new Error('Mac-control proof directory is required.'); - scanMacControlProofDirectory(proofDir); + return { allowPartial, proofDir }; +} + +if (require.main === module) { + const { allowPartial, proofDir } = parseScanArguments(process.argv.slice(2)); + const result = scanMacControlProofDirectory(proofDir, { allowPartial }); + if (allowPartial) { + const missing = result.missing.length > 0 ? result.missing.join(', ') : '(none)'; + process.stdout.write( + `Mac-control partial proof scan inspected ${result.scanned} existing allowlisted artifact(s).\n` + + `Mac-control partial proof scan missing allowlisted artifacts: ${missing}.\n` + ); + } } module.exports = { MAC_CONTROL_PROOF_NAMES, + MAC_CONTROL_RUNTIME_NEGATIVE_PROOF_CONTRACT, assertMacControlProofSanitized, normalizedProofFieldName, scanMacControlProofDirectory, diff --git a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts index 2cd8994f0a..693dcbfd70 100644 --- a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts +++ b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts @@ -9,7 +9,10 @@ const WORKFLOW_PATH = '.github/workflows/evaos-live-canary-proof.yml'; const require = createRequire(import.meta.url); const proofScanner = require('../../../scripts/evaosScanMacControlProofs.js') as { assertMacControlProofSanitized: (proof: unknown) => void; - scanMacControlProofDirectory: (proofDir: string) => { ok: boolean; scanned: number }; + scanMacControlProofDirectory: ( + proofDir: string, + options?: { allowPartial?: boolean } + ) => { ok: boolean; scanned: number; missing?: string[] }; }; function readWorkflow(): string { @@ -234,12 +237,167 @@ describe('evaOS live canary proof workflow', () => { /- name: Cleanup Mac-control canary session[\s\S]*if: always\(\)[\s\S]*cleanup-mac-control/ ); expect(workflow).toContain('mac-control-runtime.json'); - expect(workflow).toContain('secret/redaction scan'); - expect(workflow).toMatch(/- name: Run Mac-control proof secret\/redaction scan\n\s+id: mac-control-proof-scan/); - expect(workflow).toContain('node scripts/evaosScanMacControlProofs.js "$PROOF_DIR"'); - expect(workflow).toContain( - "if: always() && (github.event.inputs.run_mac_control_canary != 'true' || steps.mac-control-proof-scan.outcome == 'success')" + expect(workflow).toContain('Scan existing Mac-control proofs for safe diagnostic upload'); + expect(workflow).toContain('Require complete Mac-control proof set'); + }); + + it('keeps producer failures terminal while scanning safe partial diagnostics before strict completeness', () => { + const workflow = readWorkflow(); + const producerStart = workflow.indexOf('- name: Provision Mac-control canary session'); + const partialStart = workflow.indexOf('- name: Scan existing Mac-control proofs for safe diagnostic upload'); + const strictStart = workflow.indexOf('- name: Require complete Mac-control proof set'); + const summaryStart = workflow.indexOf('- name: Write summary'); + + expect(producerStart).toBeGreaterThan(-1); + expect(partialStart).toBeGreaterThan(producerStart); + expect(strictStart).toBeGreaterThan(partialStart); + expect(summaryStart).toBeGreaterThan(strictStart); + + const producerBlock = workflow.slice(producerStart, partialStart); + const partialStep = workflow.slice(partialStart, strictStart); + const strictStep = workflow.slice(strictStart, summaryStart); + expect(producerBlock).not.toContain('continue-on-error'); + expect(partialStep).toContain('id: mac-control-partial-proof-scan'); + expect(partialStep).toContain("if: always() && github.event.inputs.run_mac_control_canary == 'true'"); + expect(partialStep).toContain('node scripts/evaosScanMacControlProofs.js --allow-partial "$PROOF_DIR"'); + expect(strictStep).toContain('id: mac-control-complete-proof-scan'); + expect(strictStep).toContain( + "if: success() && github.event.inputs.run_live_canaries == 'true' && github.event.inputs.run_mac_control_canary == 'true' && github.event.inputs.provision_fixtures == 'true' && steps.mac-control-partial-proof-scan.outcome == 'success'" + ); + expect(strictStep).toContain('node scripts/evaosScanMacControlProofs.js "$PROOF_DIR"'); + expect(strictStep).not.toContain('--allow-partial'); + }); + + it('uploads Mac-control diagnostics only after the partial safety scan passes', () => { + const workflow = readWorkflow(); + const uploadStep = workflow.slice(workflow.indexOf('- name: Upload live canary proof packet')); + + expect(uploadStep).toContain( + "if: always() && (github.event.inputs.run_mac_control_canary != 'true' || steps.mac-control-partial-proof-scan.outcome == 'success')" ); + expect(uploadStep).not.toContain('mac-control-complete-proof-scan.outcome'); + }); + + it('allows an empty partial scan and reports every missing allowlisted artifact', () => { + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-proof-partial-empty-')); + try { + expect(proofScanner.scanMacControlProofDirectory(proofDir, { allowPartial: true })).toEqual({ + ok: true, + scanned: 0, + missing: [ + 'mac-control-runtime.json', + 'mac-control-runtime-negative.json', + 'mac-control-deployed-route.json', + 'mac-control-session-provisioning.json', + 'mac-control-session-provisioning.stdout.json', + 'mac-control-session-cleanup.json', + 'mac-control-session-cleanup.stdout.json', + ], + }); + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } + }); + + it('scans a safe cleanup diagnostic without inferring successful revocation', () => { + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-proof-partial-cleanup-')); + try { + fs.writeFileSync( + path.join(proofDir, 'mac-control-session-cleanup.json'), + `${JSON.stringify({ + schema: 'evaos-mac-control-canary-session-cleanup/v1', + sessionRevoked: false, + sensitiveOutput: 'passed', + })}\n` + ); + + expect(proofScanner.scanMacControlProofDirectory(proofDir, { allowPartial: true })).toEqual({ + ok: true, + scanned: 1, + missing: [ + 'mac-control-runtime.json', + 'mac-control-runtime-negative.json', + 'mac-control-deployed-route.json', + 'mac-control-session-provisioning.json', + 'mac-control-session-provisioning.stdout.json', + 'mac-control-session-cleanup.stdout.json', + ], + }); + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } + }); + + it('rejects an unsafe existing artifact in partial mode', () => { + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-proof-partial-unsafe-')); + try { + fs.writeFileSync( + path.join(proofDir, 'mac-control-session-cleanup.json'), + `${JSON.stringify({ + schema: 'evaos-mac-control-canary-session-cleanup/v1', + sessionRevoked: false, + sensitiveOutput: 'passed', + connectorToken: 'opaque-token-value-123456', + })}\n` + ); + + expect(() => proofScanner.scanMacControlProofDirectory(proofDir, { allowPartial: true })).toThrow(/forbidden/i); + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } + }); + + it('rejects a failed sensitive-output claim in partial mode', () => { + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-proof-partial-sensitive-output-')); + try { + fs.writeFileSync( + path.join(proofDir, 'mac-control-session-cleanup.json'), + `${JSON.stringify({ + schema: 'evaos-mac-control-canary-session-cleanup/v1', + sessionRevoked: false, + sensitiveOutput: 'failed', + })}\n` + ); + + expect(() => proofScanner.scanMacControlProofDirectory(proofDir, { allowPartial: true })).toThrow( + /sensitive-output contract/i + ); + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } + }); + + it('fails a strict scan with the exact missing artifact name', () => { + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-proof-strict-missing-')); + try { + writeCompleteMacControlProofSet(proofDir); + fs.rmSync(path.join(proofDir, 'mac-control-runtime-negative.json')); + + expect(() => proofScanner.scanMacControlProofDirectory(proofDir)).toThrow( + 'Mac-control proof is missing required artifacts: mac-control-runtime-negative.json.' + ); + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } + }); + + it('requires successful session revocation in a strict complete-set scan', () => { + const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-proof-strict-cleanup-')); + try { + writeCompleteMacControlProofSet(proofDir); + fs.writeFileSync( + path.join(proofDir, 'mac-control-session-cleanup.json'), + `${JSON.stringify({ + schema: 'evaos-mac-control-canary-session-cleanup/v1', + sessionRevoked: false, + sensitiveOutput: 'passed', + })}\n` + ); + + expect(() => proofScanner.scanMacControlProofDirectory(proofDir)).toThrow(/temporary session revocation/i); + } finally { + fs.rmSync(proofDir, { recursive: true, force: true }); + } }); it('executes a case-normalized Mac-control artifact scanner', () => { From 4b92717b55bb9d202522b21b3648b0aa1ae60c0b Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 15:19:56 +0700 Subject: [PATCH 15/17] feat(evaos): prove deployed Mac-control negatives --- .../evaos-beta/bridge/agent-tools/SOURCE.json | 6 +- .../dist/src/runtimeReceipt.js | 176 +++++++++++++-- .../openclaw-plugin/src/runtimeReceipt.ts | 213 ++++++++++++++++-- .../tests/runtimeReceipt.test.mjs | 107 +++++++++ scripts/evaosBrokerLiveCanary.js | 170 +++++++++++++- .../unit/evaos/evaosBrokerLiveCanary.test.ts | 165 +++++++++----- 6 files changed, 722 insertions(+), 115 deletions(-) diff --git a/resources/evaos-beta/bridge/agent-tools/SOURCE.json b/resources/evaos-beta/bridge/agent-tools/SOURCE.json index b69e9b0eee..a6bbe59dd3 100644 --- a/resources/evaos-beta/bridge/agent-tools/SOURCE.json +++ b/resources/evaos-beta/bridge/agent-tools/SOURCE.json @@ -14,7 +14,7 @@ "openclaw-plugin/dist/index.js": "sha256:087cbabd678efa15d3657b7137c4da0c8af3b8a4f4b55eef0492e957c8bf62c9", "openclaw-plugin/dist/src/bridge.js": "sha256:625b964db6c16a3e7e574d4c86c757a1a11861af01daa6c4abb5c7cdcb947abf", "openclaw-plugin/dist/src/firewall.js": "sha256:fc86631e8df350355f2084a5dac43ba81c58e32cf29d87aa610ac88268b19f6c", - "openclaw-plugin/dist/src/runtimeReceipt.js": "sha256:31deeff5d80951ccb1ad429a42769e7b8ddd00610365bd9bbf84d32f902e0780", + "openclaw-plugin/dist/src/runtimeReceipt.js": "sha256:f91deaffa437de675c3dfa12279f8111b0738b545a31e398492b5d4e6ce99858", "openclaw-plugin/dist/src/toolParameters.js": "sha256:a441d521980b64e571918ab4b769d13c764fdc05960303b4683ca5d8c59b5a73", "openclaw-plugin/index.ts": "sha256:0c472c05dde30746e87b5c0b8276e13d0cf572bd47460c7d04cc0049f64ff579", "openclaw-plugin/openclaw.plugin.json": "sha256:b145a479aad1209f528a2d1c69e2cf99ec8d2906957f668e3e124113a7276e79", @@ -24,11 +24,11 @@ "openclaw-plugin/scripts/runtime-receipt-negative-proof.mjs": "sha256:6a91ee0fc8ba7269ee5ff8ca3a79d7bfadb9139ed1eb629642c9c2a688130062", "openclaw-plugin/src/bridge.ts": "sha256:d0e0da280bd83dc71734579d12bbb8c37a50c5e8d746fb4ac967e7b69862a1f1", "openclaw-plugin/src/firewall.ts": "sha256:e8ef55500e577f828cb9a1422d0496bec9e5b9f0bb5ac65e06c527220aa4cf22", - "openclaw-plugin/src/runtimeReceipt.ts": "sha256:89278b1aec300454bb011d2d9f1aaac85679219ce793a40d48e99c12bcd4eb0f", + "openclaw-plugin/src/runtimeReceipt.ts": "sha256:8f42f6b6c130cc198723a369ddc2d9ced9e5366e56be3597e46b197e7cd6dd5f", "openclaw-plugin/src/toolParameters.ts": "sha256:dcca4a1615f7c138a03b2ff0da5e7739b2d0f29bf25fd3ffa3c9bc85b8c61b7c", "openclaw-plugin/src/types.d.ts": "sha256:6144e747e97c133ddaefe37af986865d6b87d842b922026a2af14b010c946bfc", "openclaw-plugin/tests/pluginContract.test.mjs": "sha256:33679173a252fdd97e7f9e43747fd65bce7d5a713b7f8dd7a904774d4a45a7ba", - "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:205a3aaa858d4e717d1953d461f75ec5e3dfb9cee5eebf31e3b38fea1747ce16", + "openclaw-plugin/tests/runtimeReceipt.test.mjs": "sha256:b1142ba5d65c346fb914329b2f4796c6af98dee0151f4ea46c28452c5c91df59", "openclaw-plugin/tsconfig.json": "sha256:d5ceeadf205f97973f4d2a1e13ef20f9af854c1fbc01823af60b02eed5d5b34b" } } diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js index 0f0e2f6302..1f94f54ea3 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/dist/src/runtimeReceipt.js @@ -11,6 +11,9 @@ const PUBLIC_ATTESTATION_SCHEMA = 'evaos.mac_control.public_runtime_attestation. const PUBLIC_ATTESTATION_ENVELOPE_SCHEMA = 'evaos.mac_control.public_runtime_attestation_envelope.v1'; const PUBLIC_ATTESTATION_NAMESPACE = 'evaos-mac-control-public-attestation-v1'; const PUBLIC_PROOF_KIND = 'selected_binding_direct_mac_control'; +const DEPLOYED_NEGATIVE_PROBE_SCHEMA = 'evaos.mac_control.deployed_negative_probe.v1'; +const DEPLOYED_NEGATIVE_PROBE_MODE = 'deployed-staging'; +const DEPLOYED_NEGATIVE_PROBE_ENV_MODE = 'staging'; const CONTEXT_TTL_SECONDS = 60; const CLOCK_SKEW_SECONDS = 5; const DEFAULT_CONNECTOR_TIMEOUT_MS = 10_000; @@ -110,6 +113,7 @@ const PUBLIC_ATTESTATION_FIELDS = [ 'connectorCandidate', ]; const PUBLIC_CANDIDATE_FIELDS = ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild']; +const DEPLOYED_NEGATIVE_PROBE_REQUEST_FIELDS = ['proofMode', 'expectedCandidate']; const HEADER = { context: 'x-evaos-mac-control-execution-context', contextSignature: 'x-evaos-mac-control-execution-context-signature', @@ -146,9 +150,9 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { sendError(response, 405, 'method_not_allowed'); return true; } - const publicRequest = await readPublicRequest(request); - if (!publicRequest.ok) { - sendError(response, publicRequest.status, 'invalid_request'); + const parsedRequest = await readRuntimeReceiptRequest(request); + if (!parsedRequest.ok) { + sendError(response, parsedRequest.status, 'invalid_request'); return true; } const authority = verifyAuthority(request.headers, env, now()); @@ -156,30 +160,58 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { sendError(response, authority.status, authority.code); return true; } + if ( + parsedRequest.value.kind === 'deployed-negative-probe' && + env.EVAOS_MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_MODE !== DEPLOYED_NEGATIVE_PROBE_ENV_MODE + ) { + sendError(response, 403, 'deployed_negative_probe_disabled'); + return true; + } const receiptVerifier = loadReceiptVerifierConfig(env); if (!receiptVerifier.ok) { sendError(response, 503, 'receipt_verifier_unavailable'); return true; } + const deadlineNow = now(); const remainingAuthorityMs = Math.min(authority.value.context.expires_at * 1000, Date.parse(authority.value.bindingExpiresAt)) - - now() - + deadlineNow - AUTHORITY_DEADLINE_SAFETY_MS; if (remainingAuthorityMs <= 0) { sendError(response, 401, 'execution_context_expired'); return true; } + if (parsedRequest.value.kind === 'deployed-negative-probe') { + if (!candidateMatchesVerifier(parsedRequest.value.value.expectedCandidate, receiptVerifier.value)) { + sendError(response, 409, 'deployed_negative_probe_candidate_mismatch'); + return true; + } + const proof = createDeployedNegativeProbeProof( + request.headers, + authority.value, + receiptVerifier.value, + env, + replayCache, + deadlineNow + ); + if (!proof) { + sendError(response, 500, 'deployed_negative_probe_failed'); + return true; + } + sendJson(response, 200, proof); + return true; + } + const publicRequest = parsedRequest.value.value; const connectorTimeoutMs = Math.max(1, Math.min(configuredConnectorTimeoutMs, remainingAuthorityMs)); - pruneReplayCache(replayCache, now()); - if (replayCache.has(authority.value.context.context_id)) { - sendError(response, 409, 'execution_context_replayed'); + const claim = claimExecutionContext(replayCache, authority.value.context, now()); + if (!claim.ok) { + sendError(response, claim.status, claim.code); return true; } - replayCache.set(authority.value.context.context_id, authority.value.context.expires_at * 1000); const connectorBody = { schema: CONNECTOR_REQUEST_SCHEMA, - challenge: publicRequest.value.challenge, - runRef: publicRequest.value.runRef, + challenge: publicRequest.challenge, + runRef: publicRequest.runRef, executionContext: { payload: authority.value.contextPayload, signature: authority.value.contextSignature, @@ -225,7 +257,7 @@ export function createMacControlRuntimeReceiptHandler(options = {}) { const sanitized = validateConnectorEnvelope( rawConnectorResponse, authority.value, - publicRequest.value, + publicRequest, receiptVerifier.value ); if (!sanitized.ok) { @@ -243,7 +275,7 @@ async function cancelConnectorResponseBody(response) { // Preserve the sanitized connector rejection even if stream cleanup fails. } } -async function readPublicRequest(request) { +async function readRuntimeReceiptRequest(request) { const chunks = []; let total = 0; try { @@ -264,23 +296,115 @@ async function readPublicRequest(request) { } catch { return { ok: false, status: 400 }; } - if (!isRecord(body) || !hasExactKeys(body, ['challenge', 'runRef'])) { + if (!isRecord(body)) { return { ok: false, status: 400 }; } + if (hasExactKeys(body, ['challenge', 'runRef'])) { + if ( + typeof body.challenge !== 'string' || + !/^[A-Za-z0-9_-]{32,128}$/.test(body.challenge) || + decodeBase64Url(body.challenge)?.byteLength === undefined || + typeof body.runRef !== 'string' || + !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(body.runRef) + ) { + return { ok: false, status: 400 }; + } + const challengeBytes = decodeBase64Url(body.challenge); + if (!challengeBytes || challengeBytes.byteLength < 24 || challengeBytes.byteLength > 96) { + return { ok: false, status: 400 }; + } + return { ok: true, value: { kind: 'receipt', value: { challenge: body.challenge, runRef: body.runRef } } }; + } + if (!hasExactKeys(body, DEPLOYED_NEGATIVE_PROBE_REQUEST_FIELDS)) { + return { ok: false, status: 400 }; + } + const expectedCandidate = body.expectedCandidate; if ( - typeof body.challenge !== 'string' || - !/^[A-Za-z0-9_-]{32,128}$/.test(body.challenge) || - decodeBase64Url(body.challenge)?.byteLength === undefined || - typeof body.runRef !== 'string' || - !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(body.runRef) + body.proofMode !== DEPLOYED_NEGATIVE_PROBE_MODE || + !isRecord(expectedCandidate) || + !hasExactKeys(expectedCandidate, PUBLIC_CANDIDATE_FIELDS) || + typeof expectedCandidate.sourceCommit !== 'string' || + !/^[0-9a-f]{40}$/.test(expectedCandidate.sourceCommit) || + typeof expectedCandidate.sourceSha256 !== 'string' || + !validSha256(expectedCandidate.sourceSha256) || + typeof expectedCandidate.appVersion !== 'string' || + !validReleaseValue(expectedCandidate.appVersion) || + typeof expectedCandidate.appBuild !== 'string' || + !validReleaseValue(expectedCandidate.appBuild) ) { return { ok: false, status: 400 }; } - const challengeBytes = decodeBase64Url(body.challenge); - if (!challengeBytes || challengeBytes.byteLength < 24 || challengeBytes.byteLength > 96) { - return { ok: false, status: 400 }; + return { + ok: true, + value: { + kind: 'deployed-negative-probe', + value: { proofMode: DEPLOYED_NEGATIVE_PROBE_MODE, expectedCandidate: expectedCandidate }, + }, + }; +} +function candidateMatchesVerifier(candidate, verifier) { + return ( + candidate.sourceCommit === verifier.expectedSourceCommit && + candidate.sourceSha256 === verifier.expectedSourceSha256 && + candidate.appVersion === verifier.expectedAppVersion && + candidate.appBuild === verifier.expectedAppBuild + ); +} +function createDeployedNegativeProbeProof(headers, authority, verifier, env, replayCache, nowMs) { + const forgedSignature = Buffer.alloc(64).toString('base64url'); + const forgedHeaders = { + ...headers, + [HEADER.contextSignature]: forgedSignature, + [canonicalHeaderName(HEADER.contextSignature)]: forgedSignature, + }; + const forged = verifyAuthority(forgedHeaders, env, nowMs); + const expired = verifyAuthority(headers, env, authority.context.expires_at * 1000); + const firstClaim = claimExecutionContext(replayCache, authority.context, nowMs); + const replay = claimExecutionContext(replayCache, authority.context, nowMs); + if ( + forged.ok || + forged.status !== 401 || + forged.code !== 'execution_context_signature_invalid' || + expired.ok || + expired.status !== 401 || + expired.code !== 'execution_context_expired' || + !firstClaim.ok || + replay.ok || + replay.status !== 409 || + replay.code !== 'execution_context_replayed' + ) { + return undefined; } - return { ok: true, value: { challenge: body.challenge, runRef: body.runRef } }; + return { + schema: DEPLOYED_NEGATIVE_PROBE_SCHEMA, + proofMode: DEPLOYED_NEGATIVE_PROBE_MODE, + candidate: { + sourceCommit: verifier.expectedSourceCommit, + sourceSha256: verifier.expectedSourceSha256, + appVersion: verifier.expectedAppVersion, + appBuild: verifier.expectedAppBuild, + }, + classifications: { + forgedSignature: { + rejected: true, + httpStatus: forged.status, + code: forged.code, + }, + expiredContext: { + rejected: true, + httpStatus: expired.status, + code: expired.code, + }, + replay: { + firstAccepted: true, + secondRejected: true, + httpStatus: replay.status, + code: replay.code, + }, + }, + connectorActionAttempted: false, + sensitiveOutputAbsent: true, + }; } function verifyAuthority(headers, env, nowMs) { const expectedKeyId = env.EVAOS_MAC_CONTROL_CONTEXT_KEY_ID; @@ -1010,6 +1134,14 @@ function hasExactKeys(value, keys) { function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } +function claimExecutionContext(cache, context, nowMs) { + pruneReplayCache(cache, nowMs); + if (cache.has(context.context_id)) { + return { ok: false, status: 409, code: 'execution_context_replayed' }; + } + cache.set(context.context_id, context.expires_at * 1000); + return { ok: true }; +} function pruneReplayCache(cache, nowMs) { for (const [contextId, expiresAt] of cache) { if (expiresAt <= nowMs) { diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts index 5b19f51eb5..7e02a7fae8 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/src/runtimeReceipt.ts @@ -13,6 +13,9 @@ const PUBLIC_ATTESTATION_SCHEMA = 'evaos.mac_control.public_runtime_attestation. const PUBLIC_ATTESTATION_ENVELOPE_SCHEMA = 'evaos.mac_control.public_runtime_attestation_envelope.v1'; const PUBLIC_ATTESTATION_NAMESPACE = 'evaos-mac-control-public-attestation-v1'; const PUBLIC_PROOF_KIND = 'selected_binding_direct_mac_control'; +const DEPLOYED_NEGATIVE_PROBE_SCHEMA = 'evaos.mac_control.deployed_negative_probe.v1'; +const DEPLOYED_NEGATIVE_PROBE_MODE = 'deployed-staging'; +const DEPLOYED_NEGATIVE_PROBE_ENV_MODE = 'staging'; const CONTEXT_TTL_SECONDS = 60; const CLOCK_SKEW_SECONDS = 5; const DEFAULT_CONNECTOR_TIMEOUT_MS = 10_000; @@ -112,6 +115,7 @@ const PUBLIC_ATTESTATION_FIELDS = [ 'connectorCandidate', ] as const; const PUBLIC_CANDIDATE_FIELDS = ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild'] as const; +const DEPLOYED_NEGATIVE_PROBE_REQUEST_FIELDS = ['proofMode', 'expectedCandidate'] as const; const HEADER = { context: 'x-evaos-mac-control-execution-context', @@ -195,6 +199,22 @@ type PublicRequest = { runRef: string; }; +type PublicCandidate = { + sourceCommit: string; + sourceSha256: string; + appVersion: string; + appBuild: string; +}; + +type DeployedNegativeProbeRequest = { + proofMode: typeof DEPLOYED_NEGATIVE_PROBE_MODE; + expectedCandidate: PublicCandidate; +}; + +type RuntimeReceiptRequest = + | { kind: 'receipt'; value: PublicRequest } + | { kind: 'deployed-negative-probe'; value: DeployedNegativeProbeRequest }; + type ReceiptVerifierConfig = { keyId: string; publicKey: ByteBuffer; @@ -270,9 +290,9 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt return true; } - const publicRequest = await readPublicRequest(request); - if (!publicRequest.ok) { - sendError(response, publicRequest.status, 'invalid_request'); + const parsedRequest = await readRuntimeReceiptRequest(request); + if (!parsedRequest.ok) { + sendError(response, parsedRequest.status, 'invalid_request'); return true; } @@ -282,33 +302,64 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt return true; } + if ( + parsedRequest.value.kind === 'deployed-negative-probe' && + env.EVAOS_MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_MODE !== DEPLOYED_NEGATIVE_PROBE_ENV_MODE + ) { + sendError(response, 403, 'deployed_negative_probe_disabled'); + return true; + } + const receiptVerifier = loadReceiptVerifierConfig(env); if (!receiptVerifier.ok) { sendError(response, 503, 'receipt_verifier_unavailable'); return true; } + const deadlineNow = now(); const remainingAuthorityMs = Math.min(authority.value.context.expires_at * 1000, Date.parse(authority.value.bindingExpiresAt)) - - now() - + deadlineNow - AUTHORITY_DEADLINE_SAFETY_MS; if (remainingAuthorityMs <= 0) { sendError(response, 401, 'execution_context_expired'); return true; } + + if (parsedRequest.value.kind === 'deployed-negative-probe') { + if (!candidateMatchesVerifier(parsedRequest.value.value.expectedCandidate, receiptVerifier.value)) { + sendError(response, 409, 'deployed_negative_probe_candidate_mismatch'); + return true; + } + const proof = createDeployedNegativeProbeProof( + request.headers, + authority.value, + receiptVerifier.value, + env, + replayCache, + deadlineNow + ); + if (!proof) { + sendError(response, 500, 'deployed_negative_probe_failed'); + return true; + } + sendJson(response, 200, proof); + return true; + } + + const publicRequest = parsedRequest.value.value; const connectorTimeoutMs = Math.max(1, Math.min(configuredConnectorTimeoutMs, remainingAuthorityMs)); - pruneReplayCache(replayCache, now()); - if (replayCache.has(authority.value.context.context_id)) { - sendError(response, 409, 'execution_context_replayed'); + const claim = claimExecutionContext(replayCache, authority.value.context, now()); + if (!claim.ok) { + sendError(response, claim.status, claim.code); return true; } - replayCache.set(authority.value.context.context_id, authority.value.context.expires_at * 1000); const connectorBody = { schema: CONNECTOR_REQUEST_SCHEMA, - challenge: publicRequest.value.challenge, - runRef: publicRequest.value.runRef, + challenge: publicRequest.challenge, + runRef: publicRequest.runRef, executionContext: { payload: authority.value.contextPayload, signature: authority.value.contextSignature, @@ -355,7 +406,7 @@ export function createMacControlRuntimeReceiptHandler(options: RuntimeReceiptOpt const sanitized = validateConnectorEnvelope( rawConnectorResponse, authority.value, - publicRequest.value, + publicRequest, receiptVerifier.value ); if (!sanitized.ok) { @@ -376,9 +427,9 @@ async function cancelConnectorResponseBody(response: FetchResponseLike): Promise } } -async function readPublicRequest( +async function readRuntimeReceiptRequest( request: RequestLike -): Promise<{ ok: true; value: PublicRequest } | { ok: false; status: number }> { +): Promise<{ ok: true; value: RuntimeReceiptRequest } | { ok: false; status: number }> { const chunks: ByteBuffer[] = []; let total = 0; try { @@ -402,23 +453,126 @@ async function readPublicRequest( } catch { return { ok: false, status: 400 }; } - if (!isRecord(body) || !hasExactKeys(body, ['challenge', 'runRef'])) { + if (!isRecord(body)) { + return { ok: false, status: 400 }; + } + if (hasExactKeys(body, ['challenge', 'runRef'])) { + if ( + typeof body.challenge !== 'string' || + !/^[A-Za-z0-9_-]{32,128}$/.test(body.challenge) || + decodeBase64Url(body.challenge)?.byteLength === undefined || + typeof body.runRef !== 'string' || + !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(body.runRef) + ) { + return { ok: false, status: 400 }; + } + const challengeBytes = decodeBase64Url(body.challenge); + if (!challengeBytes || challengeBytes.byteLength < 24 || challengeBytes.byteLength > 96) { + return { ok: false, status: 400 }; + } + return { ok: true, value: { kind: 'receipt', value: { challenge: body.challenge, runRef: body.runRef } } }; + } + + if (!hasExactKeys(body, DEPLOYED_NEGATIVE_PROBE_REQUEST_FIELDS)) { return { ok: false, status: 400 }; } + const expectedCandidate = body.expectedCandidate; if ( - typeof body.challenge !== 'string' || - !/^[A-Za-z0-9_-]{32,128}$/.test(body.challenge) || - decodeBase64Url(body.challenge)?.byteLength === undefined || - typeof body.runRef !== 'string' || - !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(body.runRef) + body.proofMode !== DEPLOYED_NEGATIVE_PROBE_MODE || + !isRecord(expectedCandidate) || + !hasExactKeys(expectedCandidate, PUBLIC_CANDIDATE_FIELDS) || + typeof expectedCandidate.sourceCommit !== 'string' || + !/^[0-9a-f]{40}$/.test(expectedCandidate.sourceCommit) || + typeof expectedCandidate.sourceSha256 !== 'string' || + !validSha256(expectedCandidate.sourceSha256) || + typeof expectedCandidate.appVersion !== 'string' || + !validReleaseValue(expectedCandidate.appVersion) || + typeof expectedCandidate.appBuild !== 'string' || + !validReleaseValue(expectedCandidate.appBuild) ) { return { ok: false, status: 400 }; } - const challengeBytes = decodeBase64Url(body.challenge); - if (!challengeBytes || challengeBytes.byteLength < 24 || challengeBytes.byteLength > 96) { - return { ok: false, status: 400 }; + return { + ok: true, + value: { + kind: 'deployed-negative-probe', + value: { proofMode: DEPLOYED_NEGATIVE_PROBE_MODE, expectedCandidate: expectedCandidate as PublicCandidate }, + }, + }; +} + +function candidateMatchesVerifier(candidate: PublicCandidate, verifier: ReceiptVerifierConfig): boolean { + return ( + candidate.sourceCommit === verifier.expectedSourceCommit && + candidate.sourceSha256 === verifier.expectedSourceSha256 && + candidate.appVersion === verifier.expectedAppVersion && + candidate.appBuild === verifier.expectedAppBuild + ); +} + +function createDeployedNegativeProbeProof( + headers: RequestLike['headers'], + authority: VerifiedAuthority, + verifier: ReceiptVerifierConfig, + env: Record, + replayCache: Map, + nowMs: number +): Record | undefined { + const forgedSignature = Buffer.alloc(64).toString('base64url'); + const forgedHeaders = { + ...headers, + [HEADER.contextSignature]: forgedSignature, + [canonicalHeaderName(HEADER.contextSignature)]: forgedSignature, + }; + const forged = verifyAuthority(forgedHeaders, env, nowMs); + const expired = verifyAuthority(headers, env, authority.context.expires_at * 1000); + const firstClaim = claimExecutionContext(replayCache, authority.context, nowMs); + const replay = claimExecutionContext(replayCache, authority.context, nowMs); + if ( + forged.ok || + forged.status !== 401 || + forged.code !== 'execution_context_signature_invalid' || + expired.ok || + expired.status !== 401 || + expired.code !== 'execution_context_expired' || + !firstClaim.ok || + replay.ok || + replay.status !== 409 || + replay.code !== 'execution_context_replayed' + ) { + return undefined; } - return { ok: true, value: { challenge: body.challenge, runRef: body.runRef } }; + + return { + schema: DEPLOYED_NEGATIVE_PROBE_SCHEMA, + proofMode: DEPLOYED_NEGATIVE_PROBE_MODE, + candidate: { + sourceCommit: verifier.expectedSourceCommit, + sourceSha256: verifier.expectedSourceSha256, + appVersion: verifier.expectedAppVersion, + appBuild: verifier.expectedAppBuild, + }, + classifications: { + forgedSignature: { + rejected: true, + httpStatus: forged.status, + code: forged.code, + }, + expiredContext: { + rejected: true, + httpStatus: expired.status, + code: expired.code, + }, + replay: { + firstAccepted: true, + secondRejected: true, + httpStatus: replay.status, + code: replay.code, + }, + }, + connectorActionAttempted: false, + sensitiveOutputAbsent: true, + }; } function verifyAuthority( @@ -1225,6 +1379,19 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } +function claimExecutionContext( + cache: Map, + context: ExecutionContext, + nowMs: number +): { ok: true } | { ok: false; status: 409; code: 'execution_context_replayed' } { + pruneReplayCache(cache, nowMs); + if (cache.has(context.context_id)) { + return { ok: false, status: 409, code: 'execution_context_replayed' }; + } + cache.set(context.context_id, context.expires_at * 1000); + return { ok: true }; +} + function pruneReplayCache(cache: Map, nowMs: number): void { for (const [contextId, expiresAt] of cache) { if (expiresAt <= nowMs) { diff --git a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs index d0c454ef3b..176af34005 100644 --- a/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs +++ b/resources/evaos-beta/bridge/agent-tools/openclaw-plugin/tests/runtimeReceipt.test.mjs @@ -40,6 +40,12 @@ const env = { EVAOS_MAC_CONTROL_EXPECTED_APP_VERSION: '2.1.36', EVAOS_MAC_CONTROL_EXPECTED_APP_BUILD: '2.1.36', }; +const expectedCandidate = { + sourceCommit: EXPECTED_COMMIT, + sourceSha256: EXPECTED_SOURCE_SHA256, + appVersion: '2.1.36', + appBuild: '2.1.36', +}; test('registers one exact gateway-authenticated runtime receipt route', () => { const routes = []; @@ -676,6 +682,107 @@ test('fails closed for missing, forged, unknown, malformed, expired, and cross-b } }); +test('classifies deployed staging negatives from the actual signed authority without connector access', async () => { + const authority = signedAuthority(); + let connectorCalls = 0; + const handler = createMacControlRuntimeReceiptHandler({ + env: { ...env, EVAOS_MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_MODE: 'staging' }, + now: () => NOW_MS, + fetchImpl: async () => { + connectorCalls += 1; + return fetchResponse(500, {}); + }, + }); + const response = await invoke(handler, { proofMode: 'deployed-staging', expectedCandidate }, authority.headers); + + assert.equal(response.statusCode, 200, response.body); + assert.deepEqual(JSON.parse(response.body), { + schema: 'evaos.mac_control.deployed_negative_probe.v1', + proofMode: 'deployed-staging', + candidate: expectedCandidate, + classifications: { + forgedSignature: { + rejected: true, + httpStatus: 401, + code: 'execution_context_signature_invalid', + }, + expiredContext: { + rejected: true, + httpStatus: 401, + code: 'execution_context_expired', + }, + replay: { + firstAccepted: true, + secondRejected: true, + httpStatus: 409, + code: 'execution_context_replayed', + }, + }, + connectorActionAttempted: false, + sensitiveOutputAbsent: true, + }); + assert.equal(connectorCalls, 0); + for (const forbidden of [ + CONNECTOR_TOKEN, + authority.payload, + authority.signature, + authority.context.customer_id, + authority.context.customer_vm_id, + authority.context.binding_id, + authority.headers['x-evaos-desktop-bridge-url'], + ]) { + assert.equal(response.body.includes(forbidden), false); + } +}); + +test('rejects the deployed negative probe outside exact staging mode after validating authority', async () => { + let connectorCalls = 0; + const handler = createMacControlRuntimeReceiptHandler({ + env, + now: () => NOW_MS, + fetchImpl: async () => { + connectorCalls += 1; + return fetchResponse(500, {}); + }, + }); + const authority = signedAuthority(); + const body = { proofMode: 'deployed-staging', expectedCandidate }; + const disabled = await invoke(handler, body, authority.headers); + assert.equal(disabled.statusCode, 403); + assert.equal(JSON.parse(disabled.body).error.code, 'deployed_negative_probe_disabled'); + + const forged = await invoke(handler, body, { + ...authority.headers, + 'x-evaos-mac-control-execution-context-signature': Buffer.alloc(64, 9).toString('base64url'), + }); + assert.equal(forged.statusCode, 401); + assert.equal(JSON.parse(forged.body).error.code, 'execution_context_signature_invalid'); + assert.equal(connectorCalls, 0); +}); + +test('requires the caller candidate to match the verifier environment before the deployed probe runs', async () => { + let connectorCalls = 0; + const handler = createMacControlRuntimeReceiptHandler({ + env: { ...env, EVAOS_MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_MODE: 'staging' }, + now: () => NOW_MS, + fetchImpl: async () => { + connectorCalls += 1; + return fetchResponse(500, {}); + }, + }); + const response = await invoke( + handler, + { + proofMode: 'deployed-staging', + expectedCandidate: { ...expectedCandidate, sourceCommit: 'd'.repeat(40) }, + }, + signedAuthority().headers + ); + assert.equal(response.statusCode, 409); + assert.equal(JSON.parse(response.body).error.code, 'deployed_negative_probe_candidate_mismatch'); + assert.equal(connectorCalls, 0); +}); + test('burns a verified execution context before connector IO and rejects replay', async () => { const authority = signedAuthority(); const body = { challenge: Buffer.alloc(32, 3).toString('base64url'), runRef: 'replay-proof' }; diff --git a/scripts/evaosBrokerLiveCanary.js b/scripts/evaosBrokerLiveCanary.js index a6326e3779..b8b345608f 100644 --- a/scripts/evaosBrokerLiveCanary.js +++ b/scripts/evaosBrokerLiveCanary.js @@ -18,6 +18,8 @@ const REQUIRED_BROKER_SURFACES = Object.freeze([ const MAC_CONTROL_CANARY_ACK = 'evaos-mac-control-canary'; const MAC_CONTROL_RUNTIME = 'openclaw'; const MAC_CONTROL_RUNTIME_RECEIPT_PATH = '/api/v1/evaos/mac-control/runtime-receipt'; +const MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_SCHEMA = 'evaos.mac_control.deployed_negative_probe.v1'; +const MAC_CONTROL_DEPLOYED_NEGATIVE_PROOF_MODE = 'deployed-staging'; const MAC_CONTROL_LAUNCH_PRIVATE = Symbol('mac-control-launch-private'); const MAC_CONTROL_REQUIRED_CAPABILITY_GROUPS = Object.freeze([ Object.freeze(['customer_mac_status']), @@ -374,6 +376,13 @@ function asPlainRecord(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined; } +function hasExactRecordKeys(record, fields) { + if (!record) return false; + const expected = [...fields].sort(); + const actual = Object.keys(record).sort(); + return actual.length === expected.length && actual.every((key, index) => key === expected[index]); +} + function normalizeMacControlCapabilities(capabilities) { if (!Array.isArray(capabilities)) return undefined; const normalized = capabilities.map((value) => (typeof value === 'string' ? value.trim().toLowerCase() : '')); @@ -1147,6 +1156,121 @@ async function runMacControlDeployedRouteProbes({ fetchImpl, launchUrl, sessionC return proof; } +function sanitizeMacControlDeployedNegativeProbe(raw, { expectedCandidate, sourceRunId }) { + assertNoSecretMaterial(raw); + const record = asPlainRecord(raw); + const candidate = asPlainRecord(record?.candidate); + const classifications = asPlainRecord(record?.classifications); + const forgedSignature = asPlainRecord(classifications?.forgedSignature); + const expiredContext = asPlainRecord(classifications?.expiredContext); + const replay = asPlainRecord(classifications?.replay); + if ( + !/^\d+$/.test(sourceRunId) || + !hasExactRecordKeys(record, [ + 'schema', + 'proofMode', + 'candidate', + 'classifications', + 'connectorActionAttempted', + 'sensitiveOutputAbsent', + ]) || + record.schema !== MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_SCHEMA || + record.proofMode !== MAC_CONTROL_DEPLOYED_NEGATIVE_PROOF_MODE || + record.connectorActionAttempted !== false || + record.sensitiveOutputAbsent !== true || + !hasExactRecordKeys(candidate, ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild']) || + candidate.sourceCommit !== expectedCandidate.sourceCommit || + candidate.sourceSha256 !== expectedCandidate.sourceSha256 || + candidate.appVersion !== expectedCandidate.appVersion || + candidate.appBuild !== expectedCandidate.appBuild || + !hasExactRecordKeys(classifications, ['forgedSignature', 'expiredContext', 'replay']) || + !hasExactRecordKeys(forgedSignature, ['rejected', 'httpStatus', 'code']) || + forgedSignature.rejected !== true || + forgedSignature.httpStatus !== 401 || + forgedSignature.code !== 'execution_context_signature_invalid' || + !hasExactRecordKeys(expiredContext, ['rejected', 'httpStatus', 'code']) || + expiredContext.rejected !== true || + expiredContext.httpStatus !== 401 || + expiredContext.code !== 'execution_context_expired' || + !hasExactRecordKeys(replay, ['firstAccepted', 'secondRejected', 'httpStatus', 'code']) || + replay.firstAccepted !== true || + replay.secondRejected !== true || + replay.httpStatus !== 409 || + replay.code !== 'execution_context_replayed' + ) { + throw macControlFailure( + 'runtime_receipt_rejected', + 'Mac-control deployed negative probe did not match the strict staging contract.' + ); + } + + return { + schema: MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_SCHEMA, + proofMode: MAC_CONTROL_DEPLOYED_NEGATIVE_PROOF_MODE, + sourceRunId, + candidate: { + sourceCommit: expectedCandidate.sourceCommit, + sourceSha256: expectedCandidate.sourceSha256, + appVersion: expectedCandidate.appVersion, + appBuild: expectedCandidate.appBuild, + }, + classifications: { + forgedSignature: { + rejected: true, + httpStatus: 401, + code: 'execution_context_signature_invalid', + }, + expiredContext: { + rejected: true, + httpStatus: 401, + code: 'execution_context_expired', + }, + replay: { + firstAccepted: true, + secondRejected: true, + httpStatus: 409, + code: 'execution_context_replayed', + }, + }, + connectorActionAttempted: false, + sensitiveOutputAbsent: true, + }; +} + +async function runMacControlDeployedNegativeProbe({ fetchImpl, launchUrl, sessionCookie, expectedCandidate, env }) { + const source = macControlProofSource(env); + if (!source.sourceRunId || source.sourceHeadSha !== expectedCandidate.sourceCommit) { + throw macControlFailure( + 'invalid_configuration', + 'Mac-control deployed negative probe requires exact source provenance.' + ); + } + const endpoint = new URL(MAC_CONTROL_RUNTIME_RECEIPT_PATH, launchUrl); + const response = await fetchImpl(endpoint.toString(), { + method: 'POST', + redirect: 'manual', + headers: { + Cookie: sessionCookie, + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + proofMode: MAC_CONTROL_DEPLOYED_NEGATIVE_PROOF_MODE, + expectedCandidate, + }), + }); + const body = await safeMacControlProbeResponse(response); + if (response.status !== 200) { + throw macControlFailure('runtime_receipt_rejected', 'Mac-control deployed negative probe was rejected.', { + httpStatus: response.status, + }); + } + return sanitizeMacControlDeployedNegativeProbe(body, { + expectedCandidate, + sourceRunId: source.sourceRunId, + }); +} + async function runMacControlLiveCanary(options = {}) { const env = options.env ?? process.env; const fetchImpl = options.fetchImpl ?? fetch; @@ -1240,6 +1364,17 @@ async function runMacControlLiveCanary(options = {}) { await options.onDeployedProbe(deployedProbe); } + const deployedNegativeProbe = await runMacControlDeployedNegativeProbe({ + fetchImpl, + launchUrl: launchPrivate.launchUrl, + sessionCookie: proxySessionCookie, + expectedCandidate, + env, + }); + if (typeof options.onDeployedNegativeProbe === 'function') { + await options.onDeployedNegativeProbe(deployedNegativeProbe); + } + return runtimeProof; } catch (error) { if (error instanceof MacControlCanaryError) throw error; @@ -1314,19 +1449,22 @@ async function runBrokerLiveCanary(options = {}) { async function main() { let result; if (process.argv.includes('--mac-control')) { - const outputPath = envText(process.env, 'AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_PROBE_OUTPUT'); - if (!outputPath) { - throw new Error('AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_PROBE_OUTPUT is required for Mac-control proof.'); + const deployedProbeOutputPath = envText(process.env, 'AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_PROBE_OUTPUT'); + const deployedNegativeProbeOutputPath = envText( + process.env, + 'AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_OUTPUT' + ); + if (!deployedProbeOutputPath || !deployedNegativeProbeOutputPath) { + throw new Error( + 'AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_PROBE_OUTPUT and AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_OUTPUT are required for Mac-control proof.' + ); } result = await runMacControlLiveCanary({ onDeployedProbe: (proof) => { - const descriptor = fs.openSync(outputPath, 'wx', 0o600); - try { - fs.writeFileSync(descriptor, `${JSON.stringify(proof, null, 2)}\n`, 'utf8'); - fs.fsyncSync(descriptor); - } finally { - fs.closeSync(descriptor); - } + writeExclusiveProof(deployedProbeOutputPath, proof); + }, + onDeployedNegativeProbe: (proof) => { + writeExclusiveProof(deployedNegativeProbeOutputPath, proof); }, }); } else { @@ -1335,6 +1473,16 @@ async function main() { console.log(JSON.stringify(result, null, 2)); } +function writeExclusiveProof(outputPath, proof) { + const descriptor = fs.openSync(outputPath, 'wx', 0o600); + try { + fs.writeFileSync(descriptor, `${JSON.stringify(proof, null, 2)}\n`, 'utf8'); + fs.fsyncSync(descriptor); + } finally { + fs.closeSync(descriptor); + } +} + if (require.main === module) { main().catch((error) => { if ((error instanceof BrokerCanaryError || error instanceof MacControlCanaryError) && error.proof) { @@ -1358,7 +1506,9 @@ module.exports = { sanitizeBrokerRuntimeLaunchCanaryResponse, sanitizeMacControlRuntimeLaunchCanaryResponse, sanitizeMacControlRuntimeProof, + sanitizeMacControlDeployedNegativeProbe, runMacControlDeployedRouteProbes, + runMacControlDeployedNegativeProbe, expectedMacControlCandidate, resolveBrokerCanaryCredentials, resolveMacControlCanaryConfig, diff --git a/tests/unit/evaos/evaosBrokerLiveCanary.test.ts b/tests/unit/evaos/evaosBrokerLiveCanary.test.ts index 9523b606ec..083a7eb365 100644 --- a/tests/unit/evaos/evaosBrokerLiveCanary.test.ts +++ b/tests/unit/evaos/evaosBrokerLiveCanary.test.ts @@ -37,13 +37,6 @@ const TRUST_ENV = { }; const require = createRequire(import.meta.url); -const releaseGate = require('../../../scripts/evaosBetaReleaseGate.js') as { - verifyMacControlLiveCanaryProof: ( - proofDir: string, - env: Record, - options?: { now?: Date; maxAgeHours?: number } - ) => boolean; -}; const liveCanary = require('../../../scripts/evaosBrokerLiveCanary.js') as { REQUIRED_BROKER_SURFACES: Array<{ surface: string; runtime: string }>; runBrokerLiveCanary: (options: { @@ -78,6 +71,7 @@ const liveCanary = require('../../../scripts/evaosBrokerLiveCanary.js') as { now?: () => number; randomBytes?: (size: number) => Uint8Array; onDeployedProbe?: (proof: Record) => void | Promise; + onDeployedNegativeProbe?: (proof: Record) => void | Promise; }) => Promise>; expectedMacControlCandidate: (env: Record) => { sourceCommit: string; @@ -103,6 +97,10 @@ const liveCanary = require('../../../scripts/evaosBrokerLiveCanary.js') as { request: { customerId: string; runtime: string; expectedCallbackHost: string }, now?: number ) => Record; + sanitizeMacControlDeployedNegativeProbe: ( + raw: unknown, + options: { expectedCandidate: Record; sourceRunId: string } + ) => Record; }; function jsonResponse(body: unknown, init: ResponseInit = {}): Response { @@ -667,6 +665,31 @@ describe('evaOS broker live canary', () => { binding_expires_at: bindingExpiresAt, allowed_capabilities: ['customer_mac_status', 'desktop_see', 'desktop_control'], }; + const deployedNegativeResponse = { + schema: 'evaos.mac_control.deployed_negative_probe.v1', + proofMode: 'deployed-staging', + candidate: expectedCandidate, + classifications: { + forgedSignature: { + rejected: true, + httpStatus: 401, + code: 'execution_context_signature_invalid', + }, + expiredContext: { + rejected: true, + httpStatus: 401, + code: 'execution_context_expired', + }, + replay: { + firstAccepted: true, + secondRejected: true, + httpStatus: 409, + code: 'execution_context_replayed', + }, + }, + connectorActionAttempted: false, + sensitiveOutputAbsent: true, + }; const fetchImpl = vi .fn() .mockResolvedValueOnce( @@ -708,9 +731,11 @@ describe('evaOS broker live canary', () => { ) .mockResolvedValueOnce(jsonResponse({ ok: false }, { status: 404 })) .mockResolvedValueOnce(jsonResponse({ ok: false, error: { code: 'invalid_request' } }, { status: 400 })) - .mockResolvedValueOnce(jsonResponse({ ok: false, error: { code: 'invalid_request' } }, { status: 400 })); + .mockResolvedValueOnce(jsonResponse({ ok: false, error: { code: 'invalid_request' } }, { status: 400 })) + .mockResolvedValueOnce(jsonResponse(deployedNegativeResponse)); let deployedProbe: Record | undefined; + let deployedNegativeProbe: Record | undefined; const proof = await liveCanary.runMacControlLiveCanary({ env: { @@ -729,9 +754,12 @@ describe('evaOS broker live canary', () => { onDeployedProbe: (value) => { deployedProbe = value; }, + onDeployedNegativeProbe: (value) => { + deployedNegativeProbe = value; + }, }); - expect(fetchImpl).toHaveBeenCalledTimes(8); + expect(fetchImpl).toHaveBeenCalledTimes(9); expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toEqual({ action: 'runtime_launch', customer_id: 'staging-mac-owner', @@ -766,6 +794,14 @@ describe('evaOS broker live canary', () => { sensitiveOutputAbsent: true, }, }); + expect(JSON.parse(String(fetchImpl.mock.calls[8][1]?.body))).toEqual({ + proofMode: 'deployed-staging', + expectedCandidate, + }); + expect(deployedNegativeProbe).toEqual({ + ...deployedNegativeResponse, + sourceRunId: '123456789', + }); const publicAttestation = JSON.parse(Buffer.from(signedProof.envelope.attestationBase64, 'base64url').toString()); expect(JSON.stringify(proof)).not.toMatch( /staging-mac-owner|11111111-1111-4111-8111-111111111111|binding_version|binding_expires_at|callback_secret|proxy_session_secret|example\.test|eds_/ @@ -774,59 +810,14 @@ describe('evaOS broker live canary', () => { expect(publicAttestation).not.toHaveProperty('bindingRef'); expect(publicAttestation).not.toHaveProperty('sessionRef'); expect(publicAttestation).not.toHaveProperty('auditRef'); + expect(JSON.stringify(deployedNegativeProbe)).not.toMatch( + /staging-mac-owner|11111111-1111-4111-8111-111111111111|binding_version|binding_expires_at|callback_secret|proxy_session_secret|example\.test|eds_/ + ); const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evaos-mac-control-contract-')); try { const proofPath = path.join(proofDir, 'mac-control-runtime.json'); fs.writeFileSync(proofPath, `${JSON.stringify(proof, null, 2)}\n`); - fs.writeFileSync( - path.join(proofDir, 'mac-control-session-provisioning.json'), - `${JSON.stringify({ - schema: 'evaos-mac-control-canary-session-provision/v1', - accountConfigured: true, - customerConfigured: true, - activeMembershipVerified: true, - stagingMarkerVerified: true, - sessionMinted: true, - sessionExpiryPresent: true, - sensitiveOutput: 'passed', - })}\n` - ); - fs.writeFileSync( - path.join(proofDir, 'mac-control-session-cleanup.json'), - `${JSON.stringify({ - schema: 'evaos-mac-control-canary-session-cleanup/v1', - sessionRevoked: true, - sensitiveOutput: 'passed', - })}\n` - ); - fs.writeFileSync( - path.join(proofDir, 'mac-control-runtime-negative.json'), - `${JSON.stringify({ - schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', - sourceHeadSha, - sourceRunId: '123456789', - assertions: { - forgedContextRejected: true, - expiredContextRejected: true, - replayRejected: true, - authorityRedacted: true, - }, - })}\n` - ); - fs.writeFileSync(path.join(proofDir, 'mac-control-deployed-route.json'), `${JSON.stringify(deployedProbe)}\n`); - - expect( - releaseGate.verifyMacControlLiveCanaryProof( - proofDir, - { - EVAOS_LIVE_CANARY_EXPECTED_SOURCE_HEAD_SHA: sourceHeadSha, - EVAOS_LIVE_CANARY_EXPECTED_SOURCE_RUN_ID: '123456789', - ...TRUST_ENV, - }, - { now: new Date(now), maxAgeHours: 24 } - ) - ).toBe(true); const pythonSource = [ 'import sys', @@ -939,6 +930,66 @@ describe('evaOS broker live canary', () => { } }); + it('rejects any non-exact deployed negative classification or candidate binding', () => { + const expectedCandidate = { + sourceCommit: 'a'.repeat(40), + sourceSha256: 'b'.repeat(64), + appVersion: '2.1.36', + appBuild: '2.1.36', + }; + const valid = { + schema: 'evaos.mac_control.deployed_negative_probe.v1', + proofMode: 'deployed-staging', + candidate: expectedCandidate, + classifications: { + forgedSignature: { + rejected: true, + httpStatus: 401, + code: 'execution_context_signature_invalid', + }, + expiredContext: { + rejected: true, + httpStatus: 401, + code: 'execution_context_expired', + }, + replay: { + firstAccepted: true, + secondRejected: true, + httpStatus: 409, + code: 'execution_context_replayed', + }, + }, + connectorActionAttempted: false, + sensitiveOutputAbsent: true, + }; + const options = { expectedCandidate, sourceRunId: '123456789' }; + + expect(liveCanary.sanitizeMacControlDeployedNegativeProbe(valid, options)).toEqual({ + ...valid, + sourceRunId: '123456789', + }); + for (const invalid of [ + { ...valid, customerId: 'must-not-be-reflected' }, + { ...valid, proofMode: 'production' }, + { ...valid, candidate: { ...expectedCandidate, sourceCommit: 'd'.repeat(40) } }, + { + ...valid, + classifications: { + ...valid.classifications, + forgedSignature: { ...valid.classifications.forgedSignature, httpStatus: 200 }, + }, + }, + { ...valid, connectorActionAttempted: true }, + ]) { + expect(() => liveCanary.sanitizeMacControlDeployedNegativeProbe(invalid, options)).toThrow( + /strict staging contract/i + ); + } + expect(() => + liveCanary.sanitizeMacControlDeployedNegativeProbe(valid, { ...options, sourceRunId: 'not-a-run' }) + ).toThrow(/strict staging contract/i); + }); + it('rejects an incomplete top-level capability set even when runtime status is complete', () => { const now = Date.parse('2026-07-14T05:00:00.000Z'); const binding = { From 0241ee874095618c2195981692fda99ce8e4152a Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 15:25:48 +0700 Subject: [PATCH 16/17] fix(evaos): gate deployed Mac-control negatives --- .github/workflows/evaos-live-canary-proof.yml | 10 +-- scripts/evaosBetaReleaseGate.js | 70 +++++++++++++++---- scripts/evaosScanMacControlProofs.js | 39 +++++++++-- .../evaos/evaosLiveCanaryWorkflow.test.ts | 55 +++++++++------ .../unit/process/evaosBetaReleaseGate.test.ts | 36 +++++++--- 5 files changed, 151 insertions(+), 59 deletions(-) diff --git a/.github/workflows/evaos-live-canary-proof.yml b/.github/workflows/evaos-live-canary-proof.yml index 368b93f5ad..a5523ec9b5 100644 --- a/.github/workflows/evaos-live-canary-proof.yml +++ b/.github/workflows/evaos-live-canary-proof.yml @@ -109,6 +109,7 @@ jobs: AIONUI_EVAOS_MAC_CONTROL_CANARY_SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.AIONUI_EVAOS_MAC_CONTROL_CANARY_SUPABASE_SERVICE_ROLE_KEY }} AIONUI_EVAOS_MAC_CONTROL_CANARY_TTL_MINUTES: '10' AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_PROBE_OUTPUT: live-canary-proof/mac-control-deployed-route.json + AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_OUTPUT: live-canary-proof/mac-control-runtime-negative.json EVAOS_LIVE_CANARY_CONTEXT_KEY_ID: ${{ vars.EVAOS_MAC_CONTROL_CONTEXT_KEY_ID }} EVAOS_LIVE_CANARY_RECEIPT_KEY_ID: ${{ vars.EVAOS_MAC_CONTROL_RECEIPT_KEY_ID }} EVAOS_LIVE_CANARY_RECEIPT_PUBLIC_KEY: ${{ vars.EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY }} @@ -231,15 +232,6 @@ jobs: set -euo pipefail node scripts/evaosBrokerLiveCanary.js --mac-control > "$PROOF_DIR/mac-control-runtime.json" - - name: Prove Mac-control runtime-receipt negative boundaries - if: github.event.inputs.run_live_canaries == 'true' && github.event.inputs.run_mac_control_canary == 'true' - run: | - set -euo pipefail - test -f resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package-lock.json - npm --prefix resources/evaos-beta/bridge/agent-tools/openclaw-plugin ci --ignore-scripts --omit=peer --no-audit --no-fund - npm --prefix resources/evaos-beta/bridge/agent-tools/openclaw-plugin run proof:runtime-receipt-negative -- \ - "$PROOF_DIR/mac-control-runtime-negative.json" "$GITHUB_SHA" "$GITHUB_RUN_ID" - - name: Run follow-up live canaries if: github.event.inputs.run_live_canaries == 'true' && github.event.inputs.run_followup_canaries == 'true' run: | diff --git a/scripts/evaosBetaReleaseGate.js b/scripts/evaosBetaReleaseGate.js index bc7fb8a704..39724d948b 100644 --- a/scripts/evaosBetaReleaseGate.js +++ b/scripts/evaosBetaReleaseGate.js @@ -3019,6 +3019,11 @@ function assertLiveCanaryPlainObject(value, label) { return value; } +function hasExactLiveCanaryFields(value, fields) { + const keys = Object.keys(value); + return keys.length === fields.length && keys.every((field) => fields.includes(field)); +} + function optionalLiveCanarySafeText(value, label) { const text = String(value || '').trim(); if (!text) { @@ -3389,24 +3394,65 @@ function verifyMacControlLiveCanaryProof(proofDir, env = process.env, verificati const negativeProof = readManifestFile(negativePath); assertLiveCanaryPlainObject(negativeProof, 'Mac-control runtime-receipt negative proof'); assertLiveCanaryNoSecretMaterial(negativeProof, 'macControlNegative'); - const negativeFields = ['schema', 'sourceHeadSha', 'sourceRunId', 'assertions']; + const negativeFields = [ + 'schema', + 'proofMode', + 'sourceRunId', + 'candidate', + 'classifications', + 'connectorActionAttempted', + 'sensitiveOutputAbsent', + ]; if ( - Object.keys(negativeProof).length !== negativeFields.length || - Object.keys(negativeProof).some((field) => !negativeFields.includes(field)) || - negativeProof.schema !== 'evaos.mac_control.runtime_receipt_negative_proof.v1' || - negativeProof.sourceHeadSha !== expectedHeadSha || - String(negativeProof.sourceRunId || '') !== expectedRunId + !hasExactLiveCanaryFields(negativeProof, negativeFields) || + negativeProof.schema !== 'evaos.mac_control.deployed_negative_probe.v1' || + negativeProof.proofMode !== 'deployed-staging' || + String(negativeProof.sourceRunId || '') !== expectedRunId || + negativeProof.connectorActionAttempted !== false || + negativeProof.sensitiveOutputAbsent !== true ) { throw new Error('Mac-control runtime-receipt negative proof does not match the exact release run.'); } - assertLiveCanaryPlainObject(negativeProof.assertions, 'Mac-control runtime-receipt negative assertions'); - const negativeAssertions = ['forgedContextRejected', 'expiredContextRejected', 'replayRejected', 'authorityRedacted']; + assertLiveCanaryPlainObject(negativeProof.candidate, 'Mac-control runtime-receipt negative candidate'); + if ( + !hasExactLiveCanaryFields(negativeProof.candidate, candidateFields) || + negativeProof.candidate.sourceCommit !== expectedHeadSha || + negativeProof.candidate.sourceSha256 !== expectedSourceSha256 || + negativeProof.candidate.appVersion !== expectedVersion || + negativeProof.candidate.appBuild !== expectedVersion + ) { + throw new Error('Mac-control runtime-receipt negative candidate does not match the exact release commit.'); + } + assertLiveCanaryPlainObject(negativeProof.classifications, 'Mac-control runtime-receipt negative classifications'); + const negativeClassifications = ['forgedSignature', 'expiredContext', 'replay']; + if (!hasExactLiveCanaryFields(negativeProof.classifications, negativeClassifications)) { + throw new Error('Mac-control runtime-receipt negative classifications are incomplete.'); + } + const forgedSignature = assertLiveCanaryPlainObject( + negativeProof.classifications.forgedSignature, + 'Mac-control forged-signature classification' + ); + const expiredContext = assertLiveCanaryPlainObject( + negativeProof.classifications.expiredContext, + 'Mac-control expired-context classification' + ); + const replay = assertLiveCanaryPlainObject(negativeProof.classifications.replay, 'Mac-control replay classification'); if ( - Object.keys(negativeProof.assertions).length !== negativeAssertions.length || - Object.keys(negativeProof.assertions).some((field) => !negativeAssertions.includes(field)) || - negativeAssertions.some((field) => negativeProof.assertions[field] !== true) + !hasExactLiveCanaryFields(forgedSignature, ['rejected', 'httpStatus', 'code']) || + forgedSignature.rejected !== true || + forgedSignature.httpStatus !== 401 || + forgedSignature.code !== 'execution_context_signature_invalid' || + !hasExactLiveCanaryFields(expiredContext, ['rejected', 'httpStatus', 'code']) || + expiredContext.rejected !== true || + expiredContext.httpStatus !== 401 || + expiredContext.code !== 'execution_context_expired' || + !hasExactLiveCanaryFields(replay, ['firstAccepted', 'secondRejected', 'httpStatus', 'code']) || + replay.firstAccepted !== true || + replay.secondRejected !== true || + replay.httpStatus !== 409 || + replay.code !== 'execution_context_replayed' ) { - throw new Error('Mac-control runtime-receipt negative assertions are incomplete.'); + throw new Error('Mac-control runtime-receipt negative classifications are incomplete.'); } const deployedProbePath = requireExistingRelativeFile( diff --git a/scripts/evaosScanMacControlProofs.js b/scripts/evaosScanMacControlProofs.js index f7c6b8d0df..d2c24762f3 100644 --- a/scripts/evaosScanMacControlProofs.js +++ b/scripts/evaosScanMacControlProofs.js @@ -13,11 +13,33 @@ const MAC_CONTROL_PROOF_NAMES = Object.freeze([ 'mac-control-session-cleanup.stdout.json', ]); const MAC_CONTROL_RUNTIME_NEGATIVE_PROOF_CONTRACT = Object.freeze({ - schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', - fields: ['schema', 'sourceHeadSha', 'sourceRunId', 'assertions'], + schema: 'evaos.mac_control.deployed_negative_probe.v1', + fields: [ + 'schema', + 'proofMode', + 'sourceRunId', + 'candidate', + 'classifications', + 'connectorActionAttempted', + 'sensitiveOutputAbsent', + ], nested: { - assertions: { - fields: ['forgedContextRejected', 'expiredContextRejected', 'replayRejected', 'authorityRedacted'], + candidate: { + fields: ['sourceCommit', 'sourceSha256', 'appVersion', 'appBuild'], + }, + classifications: { + fields: ['forgedSignature', 'expiredContext', 'replay'], + nested: { + forgedSignature: { + fields: ['rejected', 'httpStatus', 'code'], + }, + expiredContext: { + fields: ['rejected', 'httpStatus', 'code'], + }, + replay: { + fields: ['firstAccepted', 'secondRejected', 'httpStatus', 'code'], + }, + }, }, }, }); @@ -142,9 +164,16 @@ function assertMacControlProofSanitized(value, location = '$') { if (!value || typeof value !== 'object') return; for (const [key, entry] of Object.entries(value)) { const normalizedKey = normalizedProofFieldName(key); + const safeClassificationLabel = + normalizedKey === 'forgedsignature' && + location.endsWith('.classifications') && + entry !== null && + typeof entry === 'object' && + !Array.isArray(entry); if ( FORBIDDEN_NORMALIZED_FIELDS.has(normalizedKey) || - FORBIDDEN_NORMALIZED_FIELD_FRAGMENTS.some((fragment) => normalizedKey.includes(fragment)) || + (!safeClassificationLabel && + FORBIDDEN_NORMALIZED_FIELD_FRAGMENTS.some((fragment) => normalizedKey.includes(fragment))) || (normalizedKey.includes('key') && !ALLOWED_PUBLIC_KEY_IDENTIFIER_FIELDS.has(normalizedKey)) || /private.*key/.test(normalizedKey) ) { diff --git a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts index 693dcbfd70..e48d1a7ac7 100644 --- a/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts +++ b/tests/unit/evaos/evaosLiveCanaryWorkflow.test.ts @@ -51,15 +51,35 @@ function writeCompleteMacControlProofSet(proofDir: string): void { sensitiveOutput: 'passed', }; const negative = { - schema: 'evaos.mac_control.runtime_receipt_negative_proof.v1', - sourceHeadSha: 'd'.repeat(40), + schema: 'evaos.mac_control.deployed_negative_probe.v1', + proofMode: 'deployed-staging', sourceRunId: '12345', - assertions: { - forgedContextRejected: true, - expiredContextRejected: true, - replayRejected: true, - authorityRedacted: true, + candidate: { + sourceCommit: 'd'.repeat(40), + sourceSha256: 'e'.repeat(64), + appVersion: '2.1.36', + appBuild: '2.1.36', }, + classifications: { + forgedSignature: { + rejected: true, + httpStatus: 401, + code: 'execution_context_signature_invalid', + }, + expiredContext: { + rejected: true, + httpStatus: 401, + code: 'execution_context_expired', + }, + replay: { + firstAccepted: true, + secondRejected: true, + httpStatus: 409, + code: 'execution_context_replayed', + }, + }, + connectorActionAttempted: false, + sensitiveOutputAbsent: true, }; const deployedRoute = { schema: 'evaos.mac_control.deployed_route_probe.v1', @@ -212,25 +232,14 @@ describe('evaOS live canary proof workflow', () => { expect(workflow).toContain( 'AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_PROBE_OUTPUT: live-canary-proof/mac-control-deployed-route.json' ); + expect(workflow).toContain( + 'AIONUI_EVAOS_MAC_CONTROL_DEPLOYED_NEGATIVE_PROBE_OUTPUT: live-canary-proof/mac-control-runtime-negative.json' + ); expect(workflow).toContain('vars.EVAOS_MAC_CONTROL_CONTEXT_KEY_ID'); expect(workflow).toContain('vars.EVAOS_MAC_CONTROL_RECEIPT_KEY_ID'); expect(workflow).toContain('vars.EVAOS_MAC_CONTROL_RECEIPT_PUBLIC_KEY'); - expect(workflow).toContain('Prove Mac-control runtime-receipt negative boundaries'); - const negativeStep = workflow.slice( - workflow.indexOf('- name: Prove Mac-control runtime-receipt negative boundaries'), - workflow.indexOf('- name: Run follow-up live canaries') - ); - expect(negativeStep).toContain( - "if: github.event.inputs.run_live_canaries == 'true' && github.event.inputs.run_mac_control_canary == 'true'" - ); - expect(negativeStep).toContain( - 'npm --prefix resources/evaos-beta/bridge/agent-tools/openclaw-plugin ci --ignore-scripts --omit=peer --no-audit --no-fund' - ); - expect(workflow).toContain('resources/evaos-beta/bridge/agent-tools/openclaw-plugin/package-lock.json'); - expect(negativeStep).toContain('run proof:runtime-receipt-negative --'); - expect(negativeStep).toContain('"$PROOF_DIR/mac-control-runtime-negative.json" "$GITHUB_SHA" "$GITHUB_RUN_ID"'); - expect(negativeStep).not.toContain('continue-on-error'); - expect(negativeStep).not.toContain('forgedContextRejected: true'); + expect(workflow).not.toContain('Prove Mac-control runtime-receipt negative boundaries'); + expect(workflow).not.toContain('proof:runtime-receipt-negative'); expect(workflow).toContain('mac-control-runtime-negative.json'); expect(workflow).toContain('node scripts/evaosProvisionLiveCanaryFixtures.js cleanup-mac-control'); expect(workflow).toMatch( diff --git a/tests/unit/process/evaosBetaReleaseGate.test.ts b/tests/unit/process/evaosBetaReleaseGate.test.ts index 12808d3547..4053559bc9 100644 --- a/tests/unit/process/evaosBetaReleaseGate.test.ts +++ b/tests/unit/process/evaosBetaReleaseGate.test.ts @@ -887,15 +887,30 @@ function writeMacControlLiveCanaryProof(proofDir: string, overrides: Record { - (proof.assertions as Record).replayRejected = false; + const classifications = proof.classifications as Record>; + classifications.replay.secondRejected = false; }, }, { name: 'wrong-head', mutate: (_proofDir, proof) => { - proof.sourceHeadSha = 'e'.repeat(40); + (proof.candidate as Record).sourceCommit = 'e'.repeat(40); }, }, { @@ -2714,7 +2730,7 @@ printf '%s\\n' ok testCase.mutate(proofDir, proof); if (fs.existsSync(proofPath)) fs.writeFileSync(proofPath, `${JSON.stringify(proof, null, 2)}\n`); expect(() => releaseGate.verifyMacControlLiveCanaryProof(proofDir, releaseEnv)).toThrow( - /negative proof|negative assertions/i + /negative proof|negative candidate|negative classifications/i ); } finally { fs.rmSync(proofDir, { recursive: true, force: true }); From 5b1308fadc481f83116c54de2b9713ab2363bed2 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 15 Jul 2026 15:50:52 +0700 Subject: [PATCH 17/17] fix: harden Mac enrollment startup --- CHANGELOG.md | 8 ++++++++ .../services/evaosNativeCompanionStatus.ts | 7 +++++-- .../bridge/src/evaos_desktop_bridge/cli.py | 5 +++++ .../evaos/evaosDesktopBridgeReceipt.test.ts | 19 +++++++++++++++++-- .../evaos/evaosNativeCompanionStatus.test.ts | 7 ++++++- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da04453542..cdd5f4250f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ enrollment window, and surfaces a strictly sanitized rejection instead of leaving the button apparently inert. Duplicate enrollment clicks remain disabled while the first action is in flight. +- Verifies genuine Mac App Store Tailscale with Apple's App Store signing + requirement while retaining the exact Tailscale team and bundle-identifier + checks; standalone Tailscale remains pinned to its Apple-anchored Developer + ID team requirement. - Makes the Hermes adapter parse only the two connector assignments instead of executing an environment file, carries JSON parameters over standard input, and rejects caller-selected local payload paths in the OpenClaw fallback. @@ -62,6 +66,10 @@ independently verify it against protected external key configuration and reject legacy unsigned, forged, replayed, stale, wrong-run, or wrong-head evidence. +- Carries connector receipt-signer settings into the LaunchAgent only for an + explicitly selected staging canary; ordinary customer starts ignore ambient + canary variables, while incomplete explicit staging configuration still + fails before writing or starting the service. - Adds deployed-route behavior probes for gateway authentication, POST-only routing, exact path matching, strict body validation, and rejection of caller-supplied authority before Mac-control proof can authorize publication. diff --git a/packages/desktop/src/process/services/evaosNativeCompanionStatus.ts b/packages/desktop/src/process/services/evaosNativeCompanionStatus.ts index 0aec12f6d2..c515713811 100644 --- a/packages/desktop/src/process/services/evaosNativeCompanionStatus.ts +++ b/packages/desktop/src/process/services/evaosNativeCompanionStatus.ts @@ -2321,8 +2321,11 @@ async function verifiedSecureNetworkClient( const clientVariant = identifier ? SECURE_NETWORK_APP_IDENTIFIERS[identifier] : undefined; if (teamIdentifier !== SECURE_NETWORK_APP_TEAM_ID || !clientVariant) continue; const requirement = - `anchor apple generic and certificate leaf[subject.OU] = "${SECURE_NETWORK_APP_TEAM_ID}" ` + - `and identifier "${identifier}"`; + clientVariant === 'tailscale_app_store' + ? `anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] exists ` + + `and identifier "${identifier}"` + : `anchor apple generic and certificate leaf[subject.OU] = "${SECURE_NETWORK_APP_TEAM_ID}" ` + + `and identifier "${identifier}"`; await execFile('/usr/bin/codesign', ['--verify', '--deep', '--strict', `-R=${requirement}`, appPath], { timeout: COMMAND_TIMEOUT_MS, }); diff --git a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/cli.py b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/cli.py index 952afdb3d4..c7d69b1eb0 100644 --- a/resources/evaos-beta/bridge/src/evaos_desktop_bridge/cli.py +++ b/resources/evaos-beta/bridge/src/evaos_desktop_bridge/cli.py @@ -1789,6 +1789,8 @@ def _connector_label_from_env(env: dict[str, str] | None = None) -> str: CONNECTOR_HTTP_RESPONSE_LIMIT_BYTES = 65536 CONNECTOR_SYSTEM_PLIST = Path(f"/Library/LaunchAgents/{CONNECTOR_LABEL}.plist") CONNECTOR_USER_PLIST = Path.home() / "Library" / "LaunchAgents" / f"{CONNECTOR_LABEL}.plist" +CONNECTOR_MAC_CONTROL_CANARY_MODE_ENV_KEY = "EVAOS_MAC_CONTROL_CANARY_MODE" +CONNECTOR_MAC_CONTROL_CANARY_MODE = "staging" CONNECTOR_MAC_CONTROL_CANARY_ENV_KEYS = ( "EVAOS_MAC_CONTROL_CONTEXT_KEY_ID", "EVAOS_MAC_CONTROL_CONTEXT_PUBLIC_KEY", @@ -3320,6 +3322,9 @@ def _connector_program_path() -> str: def _connector_mac_control_canary_environment(env: Mapping[str, str] | None = None) -> dict[str, str]: source_env = env if env is not None else os.environ + mode = str(source_env.get(CONNECTOR_MAC_CONTROL_CANARY_MODE_ENV_KEY) or "").strip() + if mode != CONNECTOR_MAC_CONTROL_CANARY_MODE: + return {} configured = {key: str(source_env.get(key) or "").strip() for key in CONNECTOR_MAC_CONTROL_CANARY_ENV_KEYS} present = {key: value for key, value in configured.items() if value} if present and len(present) != len(CONNECTOR_MAC_CONTROL_CANARY_ENV_KEYS): diff --git a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts index fb727da0a2..8297ae625c 100644 --- a/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts +++ b/tests/unit/evaos/evaosDesktopBridgeReceipt.test.ts @@ -6,7 +6,7 @@ const repoRoot = process.cwd(); const bridgeSource = join(repoRoot, 'resources', 'evaos-beta', 'bridge', 'src'); describe('evaOS signed Mac-control receipt connector', () => { - it('carries only a complete signer configuration through the normal LaunchAgent start path', () => { + it('isolates signer configuration to an explicit complete staging LaunchAgent start', () => { const script = String.raw` import os import plistlib @@ -28,7 +28,11 @@ signer_env = { "EVAOS_MAC_CONTROL_RECEIPT_KEY_ID": "receipt-key-v1", "EVAOS_MAC_CONTROL_RECEIPT_PRIVATE_KEY_PATH": str(root / "receipt-key"), } -managed_keys = (*cli.CONNECTOR_MAC_CONTROL_CANARY_ENV_KEYS, "EVAOS_DESKTOP_BRIDGE_CONNECTOR_HOST") +managed_keys = ( + *cli.CONNECTOR_MAC_CONTROL_CANARY_ENV_KEYS, + cli.CONNECTOR_MAC_CONTROL_CANARY_MODE_ENV_KEY, + "EVAOS_DESKTOP_BRIDGE_CONNECTOR_HOST", +) previous = {key: os.environ.get(key) for key in managed_keys} try: for key in managed_keys: @@ -36,6 +40,17 @@ try: os.environ.update(signer_env) cli._launchctl_start() + payload = plistlib.loads(cli.CONNECTOR_USER_PLIST.read_bytes()) + assert payload["EnvironmentVariables"] == { + "EVAOS_DESKTOP_BRIDGE_MODE": "customer-mac-connector", + } + assert len(launchctl_calls) == 3 + + launchctl_calls.clear() + cli.CONNECTOR_USER_PLIST = root / "staging.plist" + os.environ[cli.CONNECTOR_MAC_CONTROL_CANARY_MODE_ENV_KEY] = cli.CONNECTOR_MAC_CONTROL_CANARY_MODE + cli._launchctl_start() + payload = plistlib.loads(cli.CONNECTOR_USER_PLIST.read_bytes()) assert payload["EnvironmentVariables"] == { "EVAOS_DESKTOP_BRIDGE_MODE": "customer-mac-connector", diff --git a/tests/unit/evaos/evaosNativeCompanionStatus.test.ts b/tests/unit/evaos/evaosNativeCompanionStatus.test.ts index 695071cd45..b578865681 100644 --- a/tests/unit/evaos/evaosNativeCompanionStatus.test.ts +++ b/tests/unit/evaos/evaosNativeCompanionStatus.test.ts @@ -5128,9 +5128,10 @@ describe('evaosNativeCompanionStatus', () => { } if (file === '/usr/bin/codesign' && args[0] === '--verify') { expect(args.find((arg) => arg.startsWith('-R='))).toContain( - 'anchor apple generic and certificate leaf[subject.OU] = "W5364U7YZB"' + 'anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] exists' ); expect(args.find((arg) => arg.startsWith('-R='))).toContain('identifier "io.tailscale.ipn.macos"'); + expect(args.find((arg) => arg.startsWith('-R='))).not.toContain('certificate leaf[subject.OU]'); return { stdout: '', stderr: '' }; } if (file === '/usr/bin/codesign' && args[0] === '-dv') { @@ -5330,6 +5331,10 @@ describe('evaosNativeCompanionStatus', () => { return { stdout: '', stderr: 'Identifier=io.tailscale.ipn.macsys\nTeamIdentifier=W5364U7YZB\n' }; } if (file === '/usr/bin/codesign' && args[0] === '--verify') { + expect(args.find((arg) => arg.startsWith('-R='))).toContain( + 'anchor apple generic and certificate leaf[subject.OU] = "W5364U7YZB"' + ); + expect(args.find((arg) => arg.startsWith('-R='))).toContain('identifier "io.tailscale.ipn.macsys"'); const candidate = args.at(-1); if (candidate === systemApp) throw new Error('metadata-only self-signed app'); if (candidate === userApp) return { stdout: '', stderr: '' };