# OMNI Windows node gateway and desktop companion

This process combines three things on a Windows OMNI computer:

1. a small visible companion (edge glow, remote-use label, camera label and a
   separate cursor halo at about `1.25x` the Windows cursor size);
2. a grant-gated desktop node for screen, computer, files and webcam access;
3. an optional mTLS gateway that exposes the co-located Hermes/Qwen runtime to
   a paired conductor through the same HTTPS origin.

The safe default remains a loopback-only, visual/no-op sidecar at
`127.0.0.1:8765`. It neither starts nor downloads a model, and the default
driver never captures a screen or camera and never controls Windows.

## Local mode

Set a bearer credential of at least 16 characters and start the module:

```powershell
$env:OMNI_DESKTOP_TOKEN = "replace-with-a-long-random-secret"
python -m desktop.windows
```

The default `noop` driver only projects activity. `mock` records requests in
memory and is intended for offline tests. On non-Windows or a headless desktop,
the UI and global hotkey degrade to clean no-ops while the loopback API remains
usable.

## Real Windows backend (explicit opt-in)

The `windows` driver is implemented, but it is locked behind all three settings
below. Construction does not enumerate or capture anything; an effect starts
only after the API has validated the target node, permission and signed scope.

```powershell
$env:OMNI_DESKTOP_DRIVER = "windows"
$env:OMNI_DESKTOP_ENABLE_REAL_CONTROL = "1"
$env:OMNI_DESKTOP_ARTIFACT_DIR = "C:\OMNI\artifacts"
$env:OMNI_DESKTOP_GRANT_KEY = "a-separate-secret-with-at-least-32-bytes"
```

Implemented runtime paths:

| Resource | Backend | Optional runtime package |
| --- | --- | --- |
| Screen capture | `PIL.ImageGrab`, local PNG artifact | `Pillow` |
| Mouse, Unicode keyboard, hotkeys, clipboard | Win32 `ctypes` | none |
| File read/write | bounded stdlib I/O; atomic overwrite | none |
| Webcam start/frame/stop | DirectShow/OpenCV, local JPEG artifact | `opencv-python` |
| mDNS advertisement | `_omni-node._tcp.local.` | `zeroconf` |
| Offline certificate bootstrap | local ECDSA CA/node/client certificates | `cryptography` |

Optional packages are never installed automatically. Missing Pillow/OpenCV
causes only that requested capability to fail closed; the capability manifest
reports what is actually available. Offline tests use `mock` and do not call
any real OS backend.

Real webcam access additionally requires a running visible overlay. If the
overlay is headless, disabled or crashes, webcam start/frame fails closed. A
100 ms safety watchdog releases any already-open webcam if its indicator is no
longer running.

Screen and webcam results remain local artifact references. Raw screen/webcam
bytes are not put into audit events and are not forwarded between nodes. File
and clipboard content is returned only to the already authenticated, scoped
request that asked for it.

## Paired remote node and Hermes/Qwen gateway

A LAN listener is impossible by accident. It requires a concrete private IP
(never `0.0.0.0`/`::`), TLS 1.3 with mandatory client certificates, a local
fingerprint allowlist, a separate grant key and an append-only audit path:

```powershell
$env:OMNI_REMOTE_LISTENER_ENABLED = "1"
$env:OMNI_DESKTOP_HOST = "192.168.50.21"
$env:OMNI_MTLS_CA_PATH = "C:\OMNI\pki\ca.pem"
$env:OMNI_MTLS_CERT_PATH = "C:\OMNI\pki\node.pem"
$env:OMNI_MTLS_KEY_PATH = "C:\OMNI\pki\node-key.pem"
$env:OMNI_NODE_ALLOWLIST_PATH = "C:\OMNI\state\paired-nodes.json"
$env:OMNI_DESKTOP_GRANT_KEY = "a-separate-secret-with-at-least-32-bytes"
$env:OMNI_REMOTE_AUDIT_PATH = "C:\OMNI\state\remote-audit.jsonl"
python -m desktop.windows --host 192.168.50.21 --remote-listener
```

To make this computer a complete Hermes provider, point the gateway at an
already running, loopback-only Hermes service on this same machine:

```powershell
$env:OMNI_NODE_HERMES_PROXY_ENABLED = "1"
$env:OMNI_HERMES_BRIDGE_ENABLED = "1"
$env:OMNI_NODE_HERMES_URL = "http://127.0.0.1:8642"
$env:HERMES_API_KEY = "the-local-hermes-api-key"
$env:OMNI_HERMES_BRIDGE_TOKEN = "a-different-secret-with-at-least-32-bytes"
```

The gateway does not start Hermes, Qwen, MiniCPM or vLLM. It narrowly proxies
only create-run, run status/events, approval and stop under `/v1/runs`. Hermes
continues to route inference to the Qwen service configured on this same node;
the pairing receipt advertises `hermes.run`, `qwen.inference`, vision, desktop,
files, tools, webcam and search capability names used by the conductor's
provider selector.

The tool bridge listens separately on `127.0.0.1:8766`; it is never a LAN
service. A remote run receives a random run ticket inside `OMNI_CONTEXT_V1`.
The local MCP process also needs the separate bridge bearer, but that bearer
cannot mint a ticket. For every concrete MCP call, the bridge verifies the run
ticket, paired certificate identity and requested permission, then signs a new
grant valid for at most 60 seconds and scoped to only the exact path, device or
screen in that call. Stop, peer revoke and kill invalidate tickets.

Register the included dependency-free stdio MCP server in the Hermes
`~/.hermes/config.yaml` used by the API server:

```yaml
mcp_servers:
  omni_windows:
    command: python
    args: ["-m", "desktop.windows.hermes_mcp"]
    env:
      PYTHONPATH: "C:\path\to\OMNI"
      OMNI_HERMES_BRIDGE_URL: "http://127.0.0.1:8766/v1/hermes/node-actions"
      OMNI_HERMES_BRIDGE_TOKEN: "same-separate-bridge-secret"
    timeout: 120
    supports_parallel_tool_calls: false
```

Hermes discovers seven tools: screen capture, computer control, file read,
file write and webcam start/frame/stop. The generated dynamic toolset is
`mcp-omni_windows`; enable it for the Hermes API-server platform if that Hermes
version does not enable configured MCP tools automatically, then reload MCP or
restart Hermes.

There is deliberately no “local permission” fallback. The main computer uses
the same authenticated sidecar provider path as another paired node (it may
bind its sidecar to `127.0.0.1` with mTLS). Therefore local and incoming remote
runs both receive a run-bound ticket, and a remote run cannot escalate through
the MCP server's static environment.

### Offline certificate bootstrap

The helper creates a private local CA plus one server and one conductor client
certificate without any network activity. It refuses to overwrite a non-empty
directory. Install `cryptography` explicitly from your chosen trusted source,
then run:

```powershell
python -m desktop.windows.pki_bootstrap `
  --output C:\OMNI\pki `
  --node-id office-node `
  --conductor-id main-computer `
  --lan-ip 192.168.50.21
```

Keep `ca-key.pem` offline. Keep `node.pem`, `node-key.pem`, `ca.pem` and the
initial empty `paired-nodes.json` on the node. Transfer only `conductor.pem`,
`conductor-key.pem` and `ca.pem` to the main computer over a trusted channel.
`pairing-info.json` contains no secret: compare its node/client SHA-256
fingerprints and short authentication string on both machines.

Generate separate random bearer, grant, bridge and one-time-code values. For
example, repeat this for each secret instead of reusing one:

```powershell
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
$bytes = New-Object byte[] 32
$rng.GetBytes($bytes)
[Convert]::ToBase64String($bytes)
```

### One-time pairing

Pairing is optional and has a maximum 120-second window. The operator must
confirm the displayed SAS locally and provide a one-time code. The code is
constant-time checked, attempt-limited, consumed after the first successful
claim and never persisted or written to audit.

```powershell
$env:OMNI_PAIRING_ENABLED = "1"
$env:OMNI_PAIRING_CODE = "one-time-code-from-the-local-screen"
$env:OMNI_PAIRING_SAS_CONFIRMED = "1"
$env:OMNI_PAIRING_EXPECTED_CLIENT_FINGERPRINT_SHA256 = "<conductor fingerprint from pairing-info.json>"
$env:OMNI_PAIRING_WINDOW_SECONDS = "120"
```

`POST /v1/pairing/claim` still requires a CA-valid TLS client certificate, but
is the only remote route that may be used before its fingerprint is in the
allowlist. A successful claim writes that fingerprint and only the explicitly
granted permissions to `OMNI_NODE_ALLOWLIST_PATH`. Pairing never grants access
merely because a machine was discovered.

On the main computer, claim `https://192.168.50.21:8765` using `ca.pem`,
`conductor.pem` and `conductor-key.pem`, the one-time code, the expected node
fingerprint from `pairing-info.json`, and only the permissions the operator
approved. After the receipt is stored on both sides, clear the pairing code,
disable the pairing window and restart the node. Normal restarts then use the
persisted fingerprint allowlist; they never reopen pairing automatically.

### Discovery

Set `OMNI_MDNS_DISCOVERY_ENABLED=1` to advertise
`_omni-node._tcp.local.`. The TXT record intentionally contains only:

- `protocol_version`
- `node_id`
- `pairing_state`
- `api_port`

The advertisement is registered only after HTTPS is listening and is removed
before shutdown. Discovery is metadata, never trust. If `zeroconf` is not
installed or mDNS is blocked, enter the manual `https://<LAN-IP>:<port>`
endpoint and verify the server certificate SHA-256 fingerprint through a
separate trusted channel.

## Authentication split

| Surface | Required checks |
| --- | --- |
| Local routes | loopback bind + bearer token |
| Pairing claim | explicit pairing window + CA-valid client cert + code + local SAS |
| Remote health/capabilities/Hermes runs | TLS 1.3 + trusted client-cert fingerprint |
| Desktop/files/webcam actions | trusted client cert + bearer + signed scoped grant |

Grants are canonical JSON/HMAC-SHA256 values bound to grant ID, target node,
session, capability, issue/expiry time and exact path/screen/device scope.
Wrong-target, expired, out-of-scope and revoked grants fail closed. Pairing
revocation removes the fingerprint from the persistent allowlist. The issuing
CA should additionally revoke the client certificate when a peer is retired.

## API surface

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/health` or `/healthz` | Node, Hermes, overlay, trust and kill state |
| `POST` | `/v1/pairing/claim` | Bounded one-time mTLS pairing |
| `GET` | `/v1/capabilities` | Short-lived signed runtime manifest |
| `POST` | `/v1/runs` | Create a run on co-located Hermes/Qwen |
| `GET` | `/v1/runs/{id}` | Hermes run status |
| `GET` | `/v1/runs/{id}/events` | Hermes SSE event stream |
| `POST` | `/v1/runs/{id}/approval` | Submit an approval choice |
| `POST` | `/v1/runs/{id}/stop` | Stop a Hermes run |
| stdio MCP | `desktop.windows.hermes_mcp` | Actual Hermes screen/computer/files/webcam tools |
| `POST` | `/v1/node/actions` | Start a targeted, scoped resource action |
| `GET` | `/v1/node/actions/{action_id}` | Action status |
| `POST` | `/v1/node/actions/{action_id}/stop` | Idempotent action stop |
| `GET` | `/v1/camera/status` | Visible camera/capture state |
| `POST` | `/v1/revoke` | Revoke grant, session, peer or all grants |
| `POST` | `/v1/kill` | Sticky process emergency stop |
| `GET` | `/v1/audit?after_seq=N` | Redacted event-envelope replay |

Formal targeted actions are `desktop.screenshot`, `desktop.control`,
`files.read`, `files.write`, and `webcam.start|frame|stop`. The camera indicator
is enabled before the driver is allowed to open the device and remains visible
until stop, revoke or kill. A remote Hermes SSE run keeps the remote-use label
visible for the life of its stream.

`Ctrl+Alt+Shift+Esc` is a global physical kill switch. It releases active
camera handles, revokes all in-process grants, clears indicators, stops active
operations and permanently rejects new effects until this process is restarted.
