# @bellona/remote-host

Bellona remote host walking skeleton for Phase 180.

This host process provides local process health/readiness, strict configuration
loading, structured logging, graceful shutdown, a macOS Keychain-backed identity
store, a pairing client, an outbound gateway connection manager, a fake local
adapter registry, a host command executor, and Nx build, typecheck, lint, and
test targets. macOS LaunchAgent packaging is implemented by later `180.C.06`
tasks.

The local health server binds to `127.0.0.1` by default. It is intended for
developer diagnostics and future host supervisor checks, not as a public control
surface.

Host runtime logs use the shared Bellona remote structured logging fields from
`@bellona/remote-protocol`. Gateway-connection, command-execution, HTTP,
identity, and shutdown logs include `serviceRole`, `eventType`, and the
available correlation fields: `correlationId`, `sessionId`, `commandId`,
`deviceId`, `connectionId`, `actorParticipantId`, `idempotencyKey`, and command
target fields. Host logs avoid emitting signed host tokens or personal identity
file paths. Health/readiness payloads report only identity load status, device
id, and storage kind.

## Endpoints

- `GET /health`
- `GET /v1/health`
- `GET /ready`
- `GET /v1/ready`

## Configuration

- `BELLONA_REMOTE_HOST_HEALTH_HOST` or `HOST`, default `127.0.0.1`
- `BELLONA_REMOTE_HOST_HEALTH_PORT` or `PORT`, default `4071`
- `BELLONA_REMOTE_HOST_GATEWAY_URL`, default
  `ws://127.0.0.1:4070/v1/hosts/connect`
- `BELLONA_REMOTE_HOST_GATEWAY_RECONNECT_INTERVAL_MS`, default `1000`
- `BELLONA_REMOTE_HOST_GATEWAY_RECONNECT_MAX_ATTEMPTS`, default `10`
- `BELLONA_REMOTE_HOST_GATEWAY_RECONNECT_MAX_INTERVAL_MS`, default `10000`
- `BELLONA_REMOTE_HOST_IDENTITY_STORAGE`, default `macos-keychain`; set to
  `development-file` only for explicit local test fixtures
- `BELLONA_REMOTE_HOST_KEYCHAIN_ACCOUNT`, default `default`
- `BELLONA_REMOTE_HOST_KEYCHAIN_SERVICE`, default
  `com.oshun.bellona.remote-host.identity`
- `BELLONA_REMOTE_HOST_IDENTITY_FILE`, default
  `~/.oshun/bellona/remote-host.identity.dev.json` when
  `BELLONA_REMOTE_HOST_IDENTITY_STORAGE=development-file`
- `BELLONA_REMOTE_HOST_LOG_LEVEL`, default `info`
- `BELLONA_REMOTE_HOST_PAIRING_EXCHANGE_URL`, default derived from
  `BELLONA_REMOTE_HOST_GATEWAY_URL` as `/v1/pairing/exchange`
- `BELLONA_REMOTE_HOST_SHUTDOWN_GRACE_MS`, default `5000`
- `NODE_ENV`, default `development`

`BELLONA_REMOTE_HOST_GATEWAY_URL` must use `ws:` or `wss:`. The URL is validated
now so the later outbound connection task can fail fast on invalid host
configuration.

`BELLONA_REMOTE_HOST_PAIRING_EXCHANGE_URL` must use `http:` or `https:`. When it
is not provided, the host derives it from the configured gateway WebSocket URL.

## Identity Storage

`180.C.16.01` replaces the default development credential file with macOS
Keychain storage for the Remote Host device identity, `deviceKeyId`, token
expiry, and signed `hostToken`. The default store writes a single generic
password item under service `com.oshun.bellona.remote-host.identity` and account
`default`. The implementation invokes `/usr/bin/security` with `-w` as the final
argument and sends the serialized identity over stdin, so the signed token is
not placed in the process argument list.

Stored Keychain identities use this shape:

```json
{
  "schemaVersion": 1,
  "storage": "macos-keychain",
  "device": {
    "id": "device:studio-macbook-air",
    "displayName": "Studio MacBook Air"
  },
  "credentials": {
    "authScheme": "bellona-host-token-v1",
    "deviceKeyId": "device-key:studio-macbook-air-development",
    "expiresAt": "2026-07-21T00:00:00.000Z",
    "hostToken": "bellona-host-token-v1.<payload>.<signature>"
  },
  "createdAt": "2026-04-22T00:00:00.000Z",
  "updatedAt": "2026-04-22T00:00:00.000Z",
  "metadata": {
    "owner": "local-development"
  }
}
```

The `device` object above is abridged for readability. In tests and real host
Keychain records it must be a complete `RemoteDevice` from
`@bellona/remote-protocol`. The legacy `development-file` store remains
available only by setting
`BELLONA_REMOTE_HOST_IDENTITY_STORAGE=development-file` for deterministic local
fixtures; that schema still includes `keychainReplacementTask: "180.C.16.01"` so
old fixture files fail loudly if they drift.

## Pairing

The host exposes an internal pairing client through `app.pairingClient`. It
posts a one-time pairing code plus a complete `RemoteDevice` report to the
gateway pairing exchange endpoint, parses the returned signed host token
credentials, and writes the selected identity store. By default, paired
credentials are saved to macOS Keychain; explicit development-file fixtures
still write atomically to the configured file path. mTLS remains later transport
hardening.

Important behavior:

- gateway rejection does not write or partially update local credentials
- the selected identity store persists the complete paired `RemoteDevice`,
  `authScheme`, `deviceKeyId`, `expiresAt`, and signed `hostToken`
- the saved metadata records the pairing exchange URL for local diagnostics
- Keychain-backed identities overwrite stale credentials for the same
  service/account using the system Keychain update operation

## Gateway Connection

After loading identity, the host opens an outbound WebSocket connection to
`BELLONA_REMOTE_HOST_GATEWAY_URL`, sends a protocol-valid `host.handshake` with
the signed host token from the identity store, and auto-responds to gateway ping
heartbeats through the `ws` client. Unexpected disconnects schedule reconnect
attempts with bounded backoff. Explicit host shutdown closes the socket and does
not reconnect.

## Adapter Registry

The host registers an in-process fake Blender adapter for the walking skeleton:

- adapter id: `adapter:blender-local`
- capability id: `capability:blender-scene-query`
- routed command: `blender.scene.query`

The registry validates every adapter and capability with
`@bellona/remote-protocol`, reports adapter health in host health payloads, and
can route a protocol-valid `RemoteCommandEnvelope` to the fake handler. The fake
handler returns deterministic scene-query output and is intentionally limited to
test/development use.

The host also registers a dynamic state adapter matching the `180.C.03.15`
protocol contract:

- adapter id: `adapter:remote-host-state`
- capability ids: `capability:agent-readiness`, `capability:state-snapshot`
- routed commands: `agent.readiness`, `state.snapshot`

`state.snapshot` returns the current modal-dialog list, shader compile counts,
project-root writability, active/queued command jobs, and disk/memory pressure.
`agent.readiness` evaluates those same fields with adapter health and macOS TCC
state so agents can see blocking and degraded conditions before attempting a
command that would otherwise fail.

The host registers an in-process browser discovery adapter:

- adapter id: `adapter:browser-local`
- capability id: `capability:browser-managed-profile-discovery`
- routed command: `browser.adapter.discovery`

The browser discovery command reports Chrome/Chromium executable availability,
the Bellona-managed profile root, localhost-only CDP launch readiness, and
Playwright package/browser availability. Real profile access remains blocked;
managed browser commands only use Bellona-created isolated profile directories.
The detailed operator policy and future unlock criteria live in
`docs/domains/bellona/extras/remote-control/browser-profile-isolation.md`.

The host registers in-process Unreal discovery, mocked project-info, and mocked
world-query capabilities, plus the first safe actor-spawn mutation:

- adapter id: `adapter:unreal-local`
- capability ids: `capability:unreal-adapter-discovery`,
  `capability:unreal-project-info`, `capability:unreal-world-query`,
  `capability:unreal-actor-spawn`
- routed commands: `unreal.adapter.discovery`, `unreal.project.info`,
  `unreal.world.query`, `unreal.actor.spawn`

The Unreal discovery command reports discovered Unreal Engine installations,
selected engine version and executable path, configured `.uproject` metadata,
Bellona Unreal plugin descriptor/enabled state, editor process state, the
localhost-only Remote Control endpoint placeholder, authenticated Bellona plugin
readiness on `127.0.0.1:30180`, and the currently supported command list.
Discovery remains routable when Unreal, a project, or the plugin is missing so
agents receive structured blockers and remediation context instead of probing
direct editor APIs. Set `BELLONA_UNREAL_PLUGIN_SESSION_TOKEN` in the Remote Host
and the matching Unreal Editor plugin launch environment or pass
`pluginConnection.sessionToken` in tests to enable the ready probe. The host
rejects non-loopback Bellona plugin endpoints and treats missing/failed
authentication as degraded readiness, not live editor authority.
`unreal.project.info` validates the protocol-owned read-only argument and
response schema, then returns a mocked project report from local discovery data:
project path/name, engine version, plugins, modules, target platforms, content
roots, build target metadata, source-control state, config summary, and Bellona
plugin/Remote Control support. `unreal.world.query` validates a bounded
read-only schema and returns mocked world/map, persistent level, actor class
counts, actor sample, selection, shader compile, editor busy, and structured
blocker data. Neither command writes `.uproject`, config, generated project,
plugin, map, level, or actor state. `180.C.15.06` routes both read-only commands
through the gateway-host-adapter path with gateway audit entries and timeline
events before any safe mutation is enabled.

`180.C.15.07` enables `unreal.actor.spawn` as a schema-validated safe mutation.
The host parses the protocol-owned command envelope, advertises
`command.execute.safe-mutation` plus `project.write`, blocks observe-only
sessions in host preflight, and returns a protocol-valid spawn result with a
spawned actor summary, explicit object reference, committed `undo_stack`
transaction/undo checkpoint metadata, and a screenshot/evidence placeholder. The
default implementation remains mocked and does not write project files; live
plugin-backed spawn and screenshot capture remain hardware-smoke work.

`180.C.15.06` also adds a remote-path smoke command:

```bash
pnpm nx unreal:readonly-remote-smoke @bellona/remote-host
```

The smoke starts an ephemeral gateway, starts an outbound host with the Unreal
adapter registration, dispatches `unreal.project.info` and `unreal.world.query`
through the gateway command dispatcher, validates the protocol output schemas,
and prints timeline/audit evidence for both commands.

`180.C.15.09` adds an optional fixture-project smoke command for read-only query
and actor spawn:

```bash
pnpm nx unreal:actor-spawn-remote-smoke @bellona/remote-host
```

The command exits with a structured `skipped` summary unless
`BELLONA_UNREAL_HARDWARE_SMOKE=1` is set. When enabled, set
`BELLONA_UNREAL_PROJECT_PATH` to a fixture `.uproject`; optionally set
`BELLONA_UNREAL_PLUGIN_DESCRIPTOR_PATH`, `BELLONA_UNREAL_PLUGIN_SESSION_TOKEN`,
`BELLONA_UNREAL_PLUGIN_HOST`, `BELLONA_UNREAL_PLUGIN_PORT`, and
`BELLONA_UNREAL_REMOTE_CONTROL_PORT`. Use `-- --required` or
`BELLONA_UNREAL_HARDWARE_SMOKE_REQUIRED=1` to fail if Unreal Engine, the fixture
project, the Bellona plugin, or a running editor process is missing. The smoke
validates `unreal.project.info`, `unreal.world.query`, and `unreal.actor.spawn`
outputs and prints hardware evidence fields including project path, plugin
descriptor, editor process id, command ids, actor id, transaction id, undo
checkpoint id, and screenshot placeholder status.

`180.C.15.10` closes the Unreal read-only and safe mutation MVP in
[`unreal-readonly-safe-mutation-mvp-closure.md`](../../../docs/domains/bellona/extras/remote-control/unreal-readonly-safe-mutation-mvp-closure.md).
The closure records the deterministic protocol, host, gateway, policy, audit,
timeline, transaction, and evidence-placeholder coverage, and separates it from
live Unreal fixture evidence that must be collected with the optional hardware
smoke above.

`180.C.11.02` adds isolated managed profile directory primitives. Profiles are
created only under the Bellona-managed root, use a `bellona-profile-` directory
prefix, write a `bellona-profile.json` marker, and clean up with recursive
removal of the managed directory. The host rejects configured roots and cleanup
paths that point at Chrome/Chromium real profile locations such as `Default`,
`Profile N`, Google Chrome user-data roots, or Chromium user-data roots.

`180.C.11.04` through `180.C.11.06` add the managed browser session capability:

- capability id: `capability:browser-managed-session`
- routed commands: `browser.launch_managed`, `browser.navigate`,
  `browser.snapshot`, `browser.screenshot`, `browser.close`

`browser.launch_managed` creates an isolated managed profile, launches
Chrome/Chromium through Playwright, binds the Chromium remote-debugging endpoint
to `127.0.0.1`, validates the returned CDP WebSocket URL is loopback-only, and
returns a protocol-valid `RemoteBrowserManagedSession`. Launch failures close
any created browser context and recursively remove the managed profile.
`browser.close` closes the stored context and removes the managed profile. URL
navigation and snapshots are read-only commands that enforce blocked-domain
policy before Playwright touches the page. The default policy blocks
credentialed URLs, loopback/private/link-local IP ranges, `localhost`,
`.localhost`, and `.local` names; `BELLONA_BROWSER_BLOCKED_DOMAINS` or adapter
options can add exact or `*.example.test` domain rules. `browser.screenshot`
captures through Playwright, applies selector-mask redaction hooks for
`redactSelectors`, writes the image under `BELLONA_BROWSER_ARTIFACT_ROOT` or the
host default browser artifact directory, and returns protocol `RemoteArtifact`
and `RemoteAuditEvidence` references for gateway registration.

Browser mutating commands also carry compensating-action metadata. The protocol
catalog documents that `browser.launch_managed` is reversed by `browser.close`
with managed-profile cleanup, while `browser.close` is `manual_remediation_only`
because the isolated profile is intentionally removed. See
`docs/domains/bellona/extras/remote-control/browser-compensating-actions.md` for
the current catalog and future browser mutation requirements.

`180.C.11.10` adds a remote browser CLI smoke:

```bash
pnpm nx browser:managed-remote-smoke @bellona/remote-host
```

The smoke starts an ephemeral Remote Gateway and Remote Host, launches an
isolated managed browser session through the gateway-host path, navigates to
`https://example.com/`, captures a snapshot, captures a screenshot artifact,
closes the browser session, verifies managed profile cleanup, and prints audit
and timeline evidence. Add `-- --url https://example.com/` to use another
allowed public HTTPS page, `-- --keep-artifacts` to retain the temporary
screenshot artifact directory, or `-- --mock-browser` to use deterministic fake
browser handles while still exercising the gateway-host-adapter command path.

The host also registers an in-process diagnostics adapter:

- adapter id: `adapter:host-diagnostics`
- capability id: `capability:host-diagnostics-snapshot`
- routed command: `diagnostic.host.snapshot`

The diagnostics command returns host OS/version details, the current adapter and
capability inventory, macOS permission diagnostics, and resource placeholders.

## macOS Permission Diagnostics

On macOS hosts, the runtime probes the platform TCC state for Screen Recording
and Accessibility before advertising desktop fallback readiness. The detector
uses local Swift probes for `CGPreflightScreenCaptureAccess()` and
`AXIsProcessTrusted()`, maps Screen Recording to `desktop.observe`, maps
Accessibility to `desktop.input`, and reports each check as `granted`,
`missing`, or `unknown`.

Granted macOS TCC state does not grant command execution by itself. The
generated host permissions still use `requires-approval` so gateway policy and
operator approval remain in the command path. Missing or unknown macOS
permissions are advertised as denied host permissions with remediation text for
System Settings > Privacy & Security. The diagnostics adapter also includes the
same checks in `diagnostic.host.snapshot`, allowing operators and future support
bundles to see exactly which local permission blocks desktop fallback.

Non-macOS hosts report the checks as `not-applicable` and do not mutate the
device permission list.

## Desktop Screenshot Fallback

`180.C.13.02` adds the first desktop fallback command:

- adapter id: `adapter:desktop-fallback`
- capability id: `capability:desktop-screenshot`
- routed command: `desktop.screenshot`

The command is macOS-only and requires Screen Recording to be granted by the
local TCC permission detector. It captures a selected display through
`screencapture -D <display>` or a known window through
`screencapture -l <windowid>`, writes a retained PNG under
`BELLONA_DESKTOP_ARTIFACT_ROOT` or the host default DesktopArtifacts directory,
and returns protocol-valid `RemoteArtifact` and `RemoteAuditEvidence` records.

Redaction is fail-closed. Callers can provide blackout rectangles in screenshot
pixel coordinates; the host applies those blackout regions before writing the
retained artifact and records redaction hook placeholders, labels, and a regions
hash in artifact metadata. If a redaction hook is requested and cannot return
redacted output, the command fails instead of retaining an unredacted artifact.

Gateway artifact registration uses the existing command-result path: host
results include `artifact`, `artifacts`, `artifactIds`, `evidence`, and
`evidenceIds`, and the gateway registers screenshot artifacts from those
returned records.

## Desktop Window Fallback

`180.C.13.03` extends `adapter:desktop-fallback` with two macOS-only commands:

- `desktop.window.list` through `capability:desktop-window-list`
- `desktop.window.focus` through `capability:desktop-window-focus`

Window schemas live in `@bellona/remote-protocol`. The host implementation uses
a Swift CoreGraphics/AppKit/Accessibility helper so window inventory is
structured rather than parsed from AppleScript text. List results include window
id, app name, owner pid, title, bounds, display id/index, z-order, layer,
on-screen state, alpha, and best-effort focused state.

`desktop.window.list` requires Screen Recording because macOS may redact or hide
window metadata without that TCC grant. `desktop.window.focus` requires both
Screen Recording and Accessibility because the host first resolves the selected
window and then activates the owning app and raises the matching AX window.
Selectors can use a known `windowId`, `appName`, `title`, or a combination of
those fields. The focus result reports whether focus was verified, the matched
window, the verified post-focus window, and a human-readable reason.

## Desktop Coordinate Normalization

`180.C.13.04` adds protocol-owned coordinate schemas and a host-side pure
normalizer exported as `normalizeRemoteHostDesktopCoordinate()`. The normalizer
uses one canonical intermediate space, absolute macOS display points, and maps
between:

- display points
- display pixels with Retina scale factors
- window-relative points
- window-relative pixels
- screenshot pixels
- stream-normalized coordinates

The coordinate context carries display bounds/scale, optional selected-window
bounds, optional screenshot bounds and pixel size, and optional stream capture
region metadata. Callers can request clamping to the target coordinate space
before later desktop input commands use the normalized point. The helper is
intentionally pure and testable so `desktop.click` can audit both the original
coordinate and the normalized coordinate without invoking macOS input APIs.

## Desktop Click Fallback

`180.C.13.05` adds `desktop.click` through `capability:desktop-click`. The
command is macOS-only and fails closed unless all of these checks pass before
input dispatch:

- Screen Recording is granted so the host can resolve the selected window.
- Accessibility is granted so the host can post input through Core Graphics.
- The command carries an explicit `desktop.input` control-permission grant.
- The selected app/window is currently focused.
- The focused window matches the command allowlist by app name, title fragment,
  or window id.
- The requested coordinate normalizes inside that focused window.

The host resolves the current window with the existing `desktop.window.list`
backend, then normalizes the requested point with
`normalizeRemoteHostDesktopCoordinate()` before invoking the click backend. The
default backend posts a moved/down/up sequence with a Swift Core Graphics helper
using `CGEvent` and `cghidEventTap`. Results include the original allowlist, the
target focused window, the display point, the display-pixel point, and the
normalized coordinate record for audit.

`180.C.13.06` adds visual verification around `desktop.click`. By default the
desktop adapter captures a window screenshot immediately before the click and a
second window screenshot immediately after the click by reusing
`desktop.screenshot`. The click result returns the before/after screenshot
artifact ids and evidence ids under `visualVerification`, so the gateway can
register the images through the existing command-result artifact path and
operators can compare the click effect without trusting a bare input dispatch
acknowledgement.

`180.C.13.07` adds `desktop.emergency_stop` through
`capability:desktop-emergency-stop`. The host executor now serializes desktop
fallback commands behind a desktop-only queue, keeps `desktop.emergency_stop`
out of that queue, and cancels matching queued and running desktop fallback
commands for either the active session or the whole device. Running desktop
commands receive an `AbortSignal`; `desktop.screenshot` forwards it to
`screencapture`, and `desktop.click` checks it before screenshots and before
Core Graphics input dispatch. The stop result reports the interrupted queued and
running command ids for audit.

`180.C.13.12` adds `desktop.type` through `capability:desktop-type`. The command
requires the target app/window to be focused and allowlisted, verifies Screen
Recording and Accessibility grants, detects the active macOS keyboard input
source, reports QWERTY/AZERTY/Dvorak/Colemak family plus ANSI/ISO/JIS physical
keyboard shape when available, and emits Unicode keyboard events so
IME/composition and non-US layouts are not forced through US-QWERTY keycodes.

`180.C.13.13` adds stable multi-display targeting through
`desktop.display.list`. The host reports each macOS display with a Core-Graphics
UUID-backed `desktop-display:<uuid>` stable id, current `screencapture` index,
direct display id, name, bounds, pixel size, scale factor,
primary/built-in/mirrored flags, and rotation. `desktop.screenshot` display
targets may now use `stableId`, `uuid`, `directDisplayId`, or name selectors;
the host resolves that selector to the current display index immediately before
capture, so rearranging displays does not silently retarget a saved workflow.

`180.C.13.14` adds `desktop.clipboard.capture` through
`capability:desktop-clipboard-capture`. The host reads the macOS system
pasteboard with AppKit `NSPasteboard`, accepts plain text only, enforces
byte/character limits, runs builtin and command-supplied redaction patterns, and
writes only the redacted text as a `text/plain` audit artifact. The result
returns pasteboard `changeCount`, advertised pasteboard types, raw and redacted
SHA-256 hashes, redaction metadata, artifact/evidence references, and optional
redacted text for session notes. This gives operators a deterministic path for
copying visible error text into the session without treating OCR guesses as
facts.

Desktop mutating commands carry compensating-action metadata because macOS
desktop input has no native rollback. `desktop.window.focus`, `desktop.click`,
`desktop.type`, and `desktop.emergency_stop` declare protocol-owned
`compensating_action_id` metadata and currently use `manual_remediation_only`:
refocus the reviewed window, review click before/after evidence before applying
click-back or app-specific remediation, undo/remediate typed text through the
target app's reviewed path, and review interrupted session state after emergency
stop. See
[`docs/domains/bellona/extras/remote-control/desktop-compensating-actions.md`](../../../docs/domains/bellona/extras/remote-control/desktop-compensating-actions.md)
for the binding catalog and future drag/key requirements.

## Desktop Fallback Selection Policy

Desktop fallback is the last-resort control surface. Agents and operators must
prefer typed adapters, domain APIs, or high-level automation that can inspect
and act on structured state before using screenshot-and-input control.

Use desktop fallback only when all of these conditions are true:

- The requested work cannot be completed through an available Blender, Unreal,
  browser, file, process, or domain adapter.
- The target app/window is visible, selected explicitly, currently focused, and
  allowlisted by app name, title fragment, or window id.
- The command is bounded to a visible UI action such as observing a window,
  focusing a known window, clicking a known coordinate, or handling a modal that
  no typed adapter can reach.
- Required macOS TCC grants, Bellona control permissions, gateway policy,
  operator approvals, audit metadata, screenshot evidence, and emergency stop
  are in the command path.
- Coordinates come from a current screenshot, stream frame, or window inventory
  record and are normalized before input dispatch.

Use the Blender adapter instead of desktop fallback for Blender scene reads,
object creation, material/edit operations, imports, exports, render setup,
animation, file mutation, undo checkpoints, or any operation where the Blender
addon or bridge can report structured state. A Blender viewport being visible is
not enough reason to click through the UI when the adapter can perform or verify
the operation.

Use the Unreal adapter instead of desktop fallback for Unreal Editor project
state, map or actor queries, content import, placement, Sequencer,
play-in-editor state, renders, plugin diagnostics, builds, or editor mutations
once the Unreal plugin/adapter supports the action. Desktop fallback may only
cover visible UI gaps such as dismissing a non-scriptable dialog after the
Unreal route has exhausted structured options.

Use the browser adapter instead of desktop fallback for navigation, DOM or
accessibility snapshots, screenshots, downloads/uploads, form interactions, and
managed-profile lifecycle when Playwright/CDP can drive the page. Real browser
profiles, password-manager surfaces, credential prompts, and untrusted page
instructions remain denied unless a later policy explicitly unlocks them.

Desktop fallback must not be used for arbitrary shell execution, arbitrary
Blender Python, Unreal console commands, secret or credential entry, financial
or publishing actions, OS security prompts, real profile credential flows,
destructive project/file changes, or any hidden privileged path. If fallback is
used, the command reason must state why the typed adapter path was unavailable
and the result must include the relevant audit and visual evidence.

`180.C.13.10` adds a remote desktop fallback CLI smoke:

```bash
pnpm nx desktop:fallback-remote-smoke @bellona/remote-host
```

The smoke starts an ephemeral Remote Gateway and Remote Host, registers the
desktop fallback adapter, dispatches `desktop.window.focus`,
`desktop.screenshot`, and `desktop.click` through the gateway-host command path,
approves the required desktop fallback approval requests, verifies the click's
before/after screenshot references, registers screenshot and click verification
artifacts with the gateway, and prints audit and timeline evidence. The default
mode uses deterministic in-process desktop backends so CI can prove routing,
approvals, artifact registration, visual-verification references, and audit
behavior without injecting real macOS input. Pass `-- --live-macos` on a
configured MacBook with Screen Recording and Accessibility grants to exercise
the native Swift helpers against a real focused window.

## Blender Loopback Discovery

`180.C.09.01` adds a real Blender adapter registration helper alongside the
walking-skeleton fake adapter. `createRemoteHostBlenderAdapterRegistration()`
discovers the repository's existing local Blender control surfaces as loopback
candidates:

- `libs/bellona/blender/python/bellona_addon/server.py`, the Blender addon
  WebSocket server on `localhost:9876`
- `apps/bellona/bridge-blender/src/main.ts`, the bridge-blender loopback service
  on `localhost:9001/blender`

The helper validates the resulting adapter and capability with
`@bellona/remote-protocol` and only accepts `localhost`, `127.0.0.1`, or `::1`.
Candidates such as `0.0.0.0`, LAN addresses, or internet-routable hosts are
rejected before registration, so the remote host never exposes the Blender addon
or bridge as a public control port.

The discovered adapter exposes a host-side health command:

- capability id: `capability:blender-adapter-health`
- routed command: `blender.adapter.health`

The health command reports whether Blender is installed or missing, whether the
addon or bridge is reachable, Blender and addon versions when available,
connection state, endpoint metadata, and the active `.blend` file path when the
addon provides it. The scene-query capability remains `degraded` until the
read-only scene-info route is implemented, so discovery and health do not enable
scene control by themselves.

`180.C.09.04` adds the read-only scene-info route:

- capability id: `capability:blender-scene-info`
- routed command: `blender.scene.info`

When the local addon exposes `scene.info`, the host queries it over the
loopback-only WebSocket endpoint and validates the response with
`RemoteBlenderSceneInfoSchema` from `@bellona/remote-protocol`. When Blender or
the addon is unavailable, the route returns the protocol mock scene-info fixture
with fallback metadata instead of failing the command; later integration and
hardware smoke tasks replace that fallback with broader coverage.

`180.C.09.07` adds an optional local Blender smoke command:

```bash
pnpm nx blender:scene-info-smoke @bellona/remote-host
```

The smoke command looks for `BELLONA_BLENDER_EXECUTABLE`, then a standard macOS
Blender install, then `blender` on `PATH`. If Blender is present, it launches
Blender in background mode, creates and reopens a tiny fixture `.blend`, calls
the addon `scene.info` handler, and validates the JSON with
`RemoteBlenderSceneInfoSchema`. If Blender is absent, it exits successfully with
a skipped summary unless `--required` or `BELLONA_BLENDER_SMOKE_REQUIRED=1` is
set.

The two-MacBook development runbook for the read-only Blender MVP is
[`docs/domains/bellona/extras/remote-control/blender-read-only-mvp-second-macbook.md`](../../../docs/domains/bellona/extras/remote-control/blender-read-only-mvp-second-macbook.md).
Use that runbook when the gateway runs on the operator MacBook and the real
Blender adapter runs on the controlled MacBook.

`180.C.09.10` adds an agent/CLI smoke command for the full remote path:

```bash
pnpm nx blender:scene-info-remote-smoke @bellona/remote-host
```

The remote smoke starts an ephemeral gateway, starts an outbound host with the
real Blender adapter registration, dispatches `blender.scene.info` through the
gateway command dispatcher, validates `RemoteBlenderSceneInfoSchema`, and prints
timeline/audit evidence. It accepts `--require-live-addon` when the run should
fail instead of using the protocol fallback fixture.

`180.C.10.07` adds an optional local Blender mutation smoke command:

```bash
pnpm nx blender:create-primitive-smoke @bellona/remote-host
```

The primitive smoke uses the same Blender lookup order as the scene-info smoke.
When Blender is available, it launches background Blender, creates and reopens a
small fixture `.blend`, captures scene-info before the mutation, calls the addon
`object.create_primitive` handler, captures scene-info again, and verifies the
object count increased by one with the created object selected and linked to the
expected material. It also writes a rollback note: in `--background` mode
Blender cannot execute the editor undo operator, so the smoke restores the saved
fixture and verifies the created object is absent afterward. If Blender is
absent, it exits with a skipped summary unless `--required`,
`BELLONA_BLENDER_SMOKE_REQUIRED=1`, or
`BELLONA_BLENDER_MUTATION_SMOKE_REQUIRED=1` is set.

`180.C.10.08` adds a CLI smoke for mutation timeline visibility:

```bash
pnpm nx blender:create-primitive-remote-smoke @bellona/remote-host
```

The remote primitive smoke starts an ephemeral gateway and host, dispatches
`blender.object.create_primitive`, and prints the mutation result alongside the
gateway audit entry types and command timeline events. By default it uses a
deterministic in-process primitive provider so the timeline path can run without
a live Blender addon. Pass `--live-addon` to route the mutation through the
configured local addon endpoint instead.

`180.C.17.01` extends the same adapter path with protocol-owned Blender file
commands:

- `blender.file.open`
- `blender.file.save`
- `blender.file.save_as`
- `blender.file.pack_resources`
- `blender.file.recover_last`

These commands only run when the active discovery source is the local Bellona
Blender addon. The Remote Host resolves every command path against
`BELLONA_PROJECT_ROOT` or `process.cwd()`, refuses paths outside that root,
blocks `open` and `recover_last` when the current file is dirty unless the
command explicitly allows unsaved changes, and emits before/after file snapshot
evidence through `RemoteBlenderFileMutationResultSchema`. The addon now also
serves `file.get_path` and `file.is_modified` so the host can enforce those
checks without shell fallback.

`180.C.17.02` adds the first protocol-owned Blender object mutation family on
top of the same addon transport:

- `blender.object.delete`
- `blender.object.rename`
- `blender.object.duplicate`
- `blender.object.parent`
- `blender.object.set_transform`
- `blender.object.set_visibility`

These commands share a single object-mutation capability, run only against the
local Bellona Blender addon, and always wrap the addon mutation with
before/after `blender.scene.info` evidence. The addon must create an explicit
undo checkpoint before each mutation and return structured object identifiers,
relationship or transform deltas when relevant, and the committed undo metadata
that the host normalizes into `RemoteBlenderObjectMutationResultSchema`.

`180.C.17.03` extends the same host path with Blender collection coverage:

- `blender.collection.hierarchy`
- `blender.collection.create`
- `blender.collection.move_object`
- `blender.collection.set_visibility`
- `blender.collection.set_renderability`

The read-only hierarchy query normalizes addon collection trees into
`RemoteBlenderCollectionHierarchyResultSchema`, including parent-child
relationships, object membership, viewport visibility, and render visibility.
The four collection mutations share a collection-mutation capability, always
capture before/after `blender.scene.info` evidence, require addon-created undo
checkpoints, and normalize into `RemoteBlenderCollectionMutationResultSchema`.
The Blender scene client now reads collection hierarchy through this canonical
query instead of issuing unsupported collection detail commands.

`180.C.17.04` adds Blender material query and mutation coverage:

- `blender.material.semantic_search`
- `blender.material.create_principled`
- `blender.material.assign`
- `blender.material.set_parameter`
- `blender.material.bind_texture`

The read-only semantic-search path normalizes addon material matches into
`RemoteBlenderMaterialSemanticSearchResultSchema`, including deterministic
scores, matched terms/features, and assigned-object context. The four material
mutations share a material-mutation capability, always wrap the addon mutation
with before/after `blender.scene.info` evidence, require addon-created undo
checkpoints, and normalize into `RemoteBlenderMaterialMutationResultSchema`. The
Blender JS client now routes principled material creation, assignment, parameter
updates, and core texture bindings through these canonical commands instead of
relying on unsupported raw addon verbs.

`180.C.17.05` adds canonical Blender asset import coverage:

- `blender.asset.import`

The Remote Host only accepts allowlisted asset paths under
`BELLONA_PROJECT_ROOT` for this command, verifies the source extension matches
the requested format, records a hashed source-file provenance artifact, wraps
the addon mutation with before/after `blender.scene.info` evidence, and
normalizes the result into `RemoteBlenderAssetImportResultSchema`. The addon now
handles GLB/GLTF, FBX, OBJ, USD/USDZ, Alembic, image textures, and audio
references through `asset.import`, while the legacy raw `file.import` path
continues to delegate to the same implementation for bridge compatibility.

`180.C.17.06` adds canonical Blender asset export coverage:

- `blender.asset.export`

The Remote Host only accepts allowlisted export destinations under
`BELLONA_PROJECT_ROOT`, verifies the addon-reported output hashes against the
actual exported bytes, registers transfer-ready `audit-export` artifacts for
every exported file, wraps the addon export with before/after
`blender.scene.info` evidence, and normalizes the result into
`RemoteBlenderAssetExportResultSchema`. The addon now handles GLB/GLTF, FBX,
OBJ, USD, Alembic, image sequences, and packaged `.blend` exports through
`asset.export`, while the legacy raw `file.export` path delegates to the same
implementation for bridge compatibility.

`180.C.17.07` adds canonical Blender semantic selector coverage:

- `blender.semantic_selector.query`

The Remote Host routes this read-only selector query through the local Bellona
Blender addon and normalizes it into
`RemoteBlenderSemanticSelectorQueryResultSchema`. The addon resolves fixed
selector presets for emissive objects, selected rigs, large static props,
collection membership, and materials missing textures, while preserving
deterministic ranking and resolved collection metadata for downstream clients.

`180.C.17.08` adds deterministic golden-scene coverage in
`src/blender-adapter.test.ts` for exact `blender.scene.info`, object rename,
duplicate/undo-checkpoint, material assignment, collection move, asset import,
asset export, and file save-as round trips. The suite pins before/after scene
states, evidence-reference ids, transfer ids, file hashes, and file snapshot
metadata in normal CI without claiming live Blender editor signoff.

`180.C.17.09` documents the required hardware/headless verification split for
the broader Blender production command surface in
[`docs/domains/bellona/extras/remote-control/blender-production-command-hardware-verification.md`](../../../docs/domains/bellona/extras/remote-control/blender-production-command-hardware-verification.md).
Use that note to decide which commands can be proven in background Blender,
which still need a live editor session, and what evidence file must be recorded
before the stage closes.

`180.C.17.10` closes the Stage 17 gate with deterministic
gateway-host-adapter-audit coverage in `src/blender-adapter.test.ts` for
`blender.scene.info`, object rename, collection hierarchy and move, material
assign and semantic search, semantic selectors, `file.open`, `file.save`,
`file.save_as`, `file.pack_resources`, `file.recover_last`, asset import, and
asset export. The corresponding `MAC-HOST-01` evidence bundle is recorded in
[`docs/domains/bellona/extras/remote-control/evidence/180.C.17.09/2026-04-23-MAC-HOST-01-blender-production-commands.md`](../../../docs/domains/bellona/extras/remote-control/evidence/180.C.17.09/2026-04-23-MAC-HOST-01-blender-production-commands.md),
including headless `scene.info` and primitive smokes plus live remote
`--require-live-addon` / `--live-addon` proofs for the Bellona addon path.

`180.C.18.01` adds canonical Blender modifier-stack mutation coverage:

- `blender.modifier.add`
- `blender.modifier.configure`
- `blender.modifier.reorder`
- `blender.modifier.set_viewport_enabled`
- `blender.modifier.set_render_enabled`
- `blender.modifier.apply`

The Remote Host now advertises a dedicated modifier-mutation capability, wraps
every modifier mutation with before/after `blender.scene.info` evidence, and
records before/after modifier-stack snapshot artifacts so replay and audit
consumers can see exact stack order and visibility changes. The addon returns a
required rollback note plus an undo checkpoint for every modifier mutation, and
the JS Blender clients now route modifier configuration and viewport/render
toggles through these canonical command names instead of unsupported raw verbs.

`180.C.18.02` adds canonical Blender Geometry Nodes macro coverage:

- `blender.geometry_nodes.list_macros`
- `blender.geometry_nodes.apply_macro`

The Remote Host now advertises dedicated Geometry Nodes query and mutation
capabilities, exposes a deterministic macro catalog for scatter, cable runs,
kitbash arrays, modular facades, terrain dressing, and procedural prop layout,
and validates every compiled authoring plan with `@bellona/blender-agent` before
execution. The mutation path captures before/after `blender.scene.info`
evidence, before/after modifier-stack snapshots, per-plan audit artifacts, and
an undo-backed rollback note while the addon executes only the deterministic raw
`geometry_nodes.*` tree-edit and apply commands needed by the compiled plans.

`180.C.18.03` adds canonical Blender mesh edit macro coverage:

- `blender.mesh_edit.list_macros`
- `blender.mesh_edit.apply_macro`

The Remote Host now advertises dedicated mesh-edit query and mutation
capabilities, exposes a deterministic catalog for bevel, extrude, inset, merge
by distance, normal recalculation, UV unwrap, decimate, and triangulate, and
enforces per-macro selection-domain preconditions before normalizing results.
The mutation path captures before/after `blender.scene.info` evidence plus
before/after mesh-selection snapshot artifacts, while the addon executes only
the deterministic raw `mesh.*` edit commands required for each macro and returns
an undo-backed rollback note for every mutation.

Rollback and undo behavior for the primitive mutation MVP is documented in
[`docs/domains/bellona/extras/remote-control/blender-primitive-rollback-and-undo.md`](../../../docs/domains/bellona/extras/remote-control/blender-primitive-rollback-and-undo.md).
The `180.C.10` safe mutation closure evidence is recorded in
[`docs/domains/bellona/extras/remote-control/blender-safe-mutation-mvp-closure.md`](../../../docs/domains/bellona/extras/remote-control/blender-safe-mutation-mvp-closure.md).

## Command Execution

When the gateway sends a `command.dispatch` message, the host executor emits
protocol-valid progress events for accepted, started, running progress, and the
terminal lifecycle state. Completed, failed, cancelled, and timed-out commands
produce a protocol-valid `command.result` message back to the gateway.
Unexpected executor failures are reported as protocol-valid `command.error`
messages so the gateway can reject the host event without losing command
correlation. The health payload exposes `commands.activeCount` for local
diagnostics without exposing command arguments or signed host tokens.

## Policy Preflight Stub

Until the real policy package is added, the host runs a conservative local
preflight named `remote-host-known-command-policy-preflight-stub`. It only
allows commands advertised by the current adapter registry and denies unknown
command, adapter, or capability targets before adapter routing. Denials return
protocol-valid `command.result` messages with `status: "denied"` and
`error.code: "policy.denied"`.

## Shutdown Cleanup

Host shutdown marks readiness not-ready, cancels active commands, closes the
gateway connection, stops in-process adapters, closes the health server, emits
final lifecycle events, flushes logs, and then closes the logger. In-process
adapters are marked `blocked` after stop so local diagnostics cannot report them
as available during teardown.

## Local Startup

```bash
BELLONA_REMOTE_HOST_HEALTH_PORT=4107 BELLONA_REMOTE_HOST_LOG_LEVEL=debug pnpm nx start @bellona/remote-host
curl -fsS http://127.0.0.1:4107/health
curl -fsS http://127.0.0.1:4107/ready
```

Expected health output is a JSON payload with `status: "healthy"`. Expected
readiness output is a JSON payload with `status: "ready"` and `checks.config`,
`checks.adapters`, `checks.gatewayConfiguration`, `checks.gatewayConnection`,
`checks.identity`, and `checks.shuttingDown`. If the selected identity store is
missing or invalid, the adapter registry is empty, or the gateway connection is
not yet connected, the health server still starts for diagnostics, but readiness
returns `503`.

## Local Gateway Smoke

Use this flow from the repository root to run the real walking-skeleton host
against the local gateway.

Terminal 1 starts the gateway:

```bash
BELLONA_REMOTE_GATEWAY_PORT=4097 BELLONA_REMOTE_GATEWAY_LOG_LEVEL=debug pnpm nx start @bellona/remote-gateway
```

Terminal 2 creates an explicit development identity file fixture using the
protocol device fixture and the same development signing helper as the gateway,
then starts the host against that gateway:

```bash
export BELLONA_REMOTE_HOST_IDENTITY_FILE=/tmp/bellona-remote-host.identity.dev.json

pnpm exec tsx --eval "
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';
import { remoteDeviceFixture } from '@bellona/remote-protocol';
import { BELLONA_REMOTE_GATEWAY_DEVELOPMENT_HOST_TOKEN_SIGNING_SECRET } from './apps/bellona/remote-gateway/src/config.ts';
import { createHmacRemoteGatewayHostTokenAuthenticator } from './apps/bellona/remote-gateway/src/host-token.ts';

const credentials = createHmacRemoteGatewayHostTokenAuthenticator({
  now: () => new Date('2026-04-22T00:00:00.000Z'),
  signingSecret: BELLONA_REMOTE_GATEWAY_DEVELOPMENT_HOST_TOKEN_SIGNING_SECRET,
  tokenTtlMs: 90 * 24 * 60 * 60 * 1000
}).issueHostToken({
  device: remoteDeviceFixture,
  deviceKeyId: 'device-key:studio-macbook-air-development'
});

mkdirSync(dirname(process.env.BELLONA_REMOTE_HOST_IDENTITY_FILE), { recursive: true });
writeFileSync(
  process.env.BELLONA_REMOTE_HOST_IDENTITY_FILE,
  JSON.stringify(
    {
      schemaVersion: 1,
      storage: 'development-file',
      keychainReplacementTask: '180.C.16.01',
      device: remoteDeviceFixture,
      credentials,
      createdAt: '2026-04-22T00:00:00.000Z',
      updatedAt: '2026-04-22T00:00:00.000Z',
      metadata: { owner: 'local-development-smoke' }
    },
    null,
    2
  )
);
"

BELLONA_REMOTE_HOST_GATEWAY_URL=ws://127.0.0.1:4097/v1/hosts/connect \
BELLONA_REMOTE_HOST_HEALTH_PORT=4098 \
BELLONA_REMOTE_HOST_IDENTITY_STORAGE=development-file \
BELLONA_REMOTE_HOST_LOG_LEVEL=debug \
pnpm nx start @bellona/remote-host
```

Terminal 3 checks readiness:

```bash
curl -fsS http://127.0.0.1:4097/ready
curl -fsS http://127.0.0.1:4098/ready
curl -fsS http://127.0.0.1:4098/health
```

The gateway readiness payload should report `status: "ready"`. The host
readiness payload should report `status: "ready"`, `checks.identity: "loaded"`,
`checks.adapters: "ok"`, and `checks.gatewayConnection: "connected"`. The host
health payload should include two adapters, zero active commands, and the
connected gateway state.

Expected gateway logs include `remote gateway listening` and
`remote host handshake accepted`. Expected host logs include
`remote host health server listening`, `remote host identity loaded`, and
`remote host gateway handshake accepted`.

The gateway does not expose a public HTTP command-dispatch endpoint yet. The
executable command round trips for `blender.scene.query`, `blender.scene.info`,
and `diagnostic.host.snapshot` are covered by `src/gateway-connection.test.ts`,
which drives the same WebSocket `command.dispatch` path used by the local
gateway connection.

## Verification

```bash
pnpm nx test @bellona/remote-host --verbose
pnpm nx typecheck @bellona/remote-host --verbose
pnpm nx lint @bellona/remote-host --verbose
pnpm nx build @bellona/remote-host --skip-nx-cache --verbose
```

## macOS Packaging

`180.C.16.02` adds the initial signed/notarized packaging plan in
[`macos-packaging-and-notarization-plan.md`](../../../docs/domains/bellona/extras/remote-control/macos-packaging-and-notarization-plan.md)
and exposes the typed release contract through
`createRemoteHostMacOSPackagingPlan()`. The plan covers the Remote Host app
bundle, LaunchAgent plist, desktop helper tools, Blender add-on, Unreal plugin,
and final Developer ID Installer package. It is a release contract only; later
Stage 16 and Stage 26 work implements the actual app bundle, installer,
LaunchAgent registration, helper entitlements, updates, uninstall, and live
notarized hardware evidence.

`180.C.26.01`, `.02`, `.06`, `.09`, and `.14` now also expose the locally
testable macOS host contracts through `createMacOSLaunchAgentLifecyclePlan()`,
`deriveMacOSMenuBarModel()`, `evaluateMacOSSigningReadiness()`,
`createMacOSInstallQaMatrix()`, `evaluateMacOSInstallQaEvidence()`, and
`evaluateMacOSHostStageGate()`. These contracts cover the menu-bar status and
control model, LaunchAgent lifecycle commands, signing readiness, install QA
scenario coverage, canonical UTC millisecond per-scenario QA artifacts, and the
Stage 26 roll-up gate. The evaluators reject malformed Developer ID/notary/Team
ID placeholders, missing QA evidence kinds, incomplete, stale, or non-canonical
timestamp artifact metadata, QA artifacts that do not identify their
scenario/evidence-kind context, non-macOS host runner records, stale or
non-canonical QA run timestamps, generic install-QA operator identities,
artifact identifiers duplicated within or across QA/stage evidence records,
including case-variant duplicates, stale or non-canonical timestamp stage
evidence, stage artifacts that identify the checklist item but not the exact
evidence slot they prove, empty or placeholder macOS runner/operator/artifact
suffixes, generic install-QA summaries without complete per-scenario records or
captured before the scenario evidence they summarize, non-developer acceptance
without a non-developer operator identity or captured before prerequisite Stage
26 evidence, and bare Stage 26 booleans. They do not replace the required live
macOS app, launchd, Developer ID, notarization, install QA, and non-developer
acceptance evidence. See
[`macos-host-local-contracts.md`](../../../docs/domains/bellona/extras/remote-control/macos-host-local-contracts.md).

## Windows Packaging

`180.C.36.01` through `.10` expose the locally testable Windows host contracts
through `createWindowsRemoteHostPackagingPlan()`,
`evaluateWindowsSigningReadiness()`, `resolveWindowsDesktopStrategy()`,
`createRemoteHostWindowsPermissionProvider()`,
`buildWindowsManagedBrowserLaunchPlan()`, `windowsDefaultDiscoveryCandidates()`,
`createWindowsUpdateChannelPlan()`, `buildWindowsDiagnosticBundle()`,
`deriveWindowsHostRoles()`, `compareWindowsMacCapabilityParity()`, and
`evaluateWindowsHostStageGate()`. These contracts cover the signed MSI/Windows
Service plan, Authenticode environment validation, DXGI/UIAutomation/SendInput
strategy, Windows permission projection, managed Chrome/Edge launch policy,
managed-profile root/path escape rejection, explicit loopback CDP binding, TCP
port validation, fail-closed launch-time `initialUrl` rejection so navigation
uses the normal `browser.navigate` policy path, default DCC/browser discovery
paths, signed update rollback, diagnostic bundle shape, fleet role derivation,
macOS/Windows capability parity, and the Stage 36 evidence gate.

The evaluators reject malformed signing variables, placeholder publisher values,
missing Windows runner evidence, missing or Windows-reused macOS parity-runner
evidence, generic parity summaries without a passing structured comparator
result covering the browser, desktop, and Unreal MVP command anchors, and
missing final operator acceptance, including artifact identifiers duplicated
within or across stage evidence records, even when duplicate IDs differ only by
case. They also require Stage 36 artifacts to identify the checklist item, exact
evidence slot, and concrete non-placeholder run/artifact suffix they prove,
reject stale pre-contract stage evidence, require signing/update evidence from a
`WIN-SIGN-` runner with a concrete suffix, require host/probe/parity evidence
from `WIN-HOST-` runners with concrete suffixes, require all Stage 36 evidence
timestamps to use canonical UTC millisecond format, require parity evidence from
a `MAC-HOST-` runner with a concrete suffix, and require final acceptance from
an `operator:windows-acceptance:` identity with a concrete suffix captured after
every prerequisite Stage 36 evidence record. They also require signed update
rollback evidence to follow the signed MSI/service lifecycle record and parity
evidence to follow the Windows capability records it compares. They do not
replace the required live Windows MSI build/install, service lifecycle run,
DXGI/input probes, UAC/Defender/Smart App Control checks, real app discovery,
diagnostic collection, cross-host parity run, or final operator acceptance
evidence. See
[`windows-host-local-contracts.md`](../../../docs/domains/bellona/extras/remote-control/windows-host-local-contracts.md).

## Signed Updates

`180.C.16.03` adds the initial signed update channel and rollback contract in
[`remote-host-update-channel-and-rollback-plan.md`](../../../docs/domains/bellona/extras/remote-control/remote-host-update-channel-and-rollback-plan.md).
`createRemoteHostUpdatePlan()` documents the channel policy, while
`verifyRemoteHostSignedUpdateManifest()` verifies Ed25519 signatures, manifest
freshness, channel eligibility, required macOS artifacts, rollback metadata, and
adapter compatibility for the Remote Host package, Blender add-on, Unreal
plugin, and desktop helper. Live update delivery and rollback drills remain
blocked until the production macOS app shell, installer, LaunchAgent repair, and
Sparkle-compatible delivery work in later Stage 16 and Stage 26 tasks.
