Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
137ba04
feat(remote-control): replace remote access with outbound relay
pascalandr Sep 3, 2026
2fd73c6
feat(remote-control): harden encrypted relay transport
pascalandr Sep 4, 2026
e4ef184
test(server): isolate automation registry paths across platforms
pascalandr Sep 4, 2026
1468fe0
fix(packaging): stage workspace runtime artifacts safely
pascalandr Sep 4, 2026
096279e
chore(opencode): refresh V2 beta contract
pascalandr Sep 4, 2026
e334a6d
feat(remote-control): preserve direct access alongside encrypted relay
pascalandr Sep 5, 2026
86e34d8
fix(remote-control): close safety failures and bound browser network …
pascalandr Sep 5, 2026
18d474c
merge(dev): reconcile PR #668 with current V2 baseline
pascalandr Sep 6, 2026
8953542
merge(dev): resolve remote-control diagnostics and CI conflicts
pascalandr Sep 9, 2026
b309bae
merge(dev): refresh remote control relay branch
pascalandr Sep 10, 2026
1191056
merge(dev): preserve encrypted relay alongside the modern V2 integration
pascalandr Sep 16, 2026
1592047
merge(dev): preserve remote control with native automation
pascalandr Sep 19, 2026
56e8d73
Merge branch 'dev' into feat/remote-control-relay
pascalandr Sep 19, 2026
94c7f62
merge(dev): adopt bundled Node 24 runtime
pascalandr Sep 19, 2026
0af3b5f
fix(ci): prepare relay protocol for pruning UI
pascalandr Sep 19, 2026
0823ec1
merge(dev): adopt queued native event routing
pascalandr Sep 19, 2026
d716b77
merge(dev): retain relay with scrollbar cascade
pascalandr Sep 19, 2026
7d06a2d
fix(packaging): materialize workspace dependencies
pascalandr Sep 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ jobs:
node-version-file: .node-version
cache: npm
- run: npm ci --ignore-scripts
- name: Build Remote Control protocol for UI checks
run: npm run build --workspace @codenomad/remote-control-protocol
- name: Install browser for native pruning UI checks
run: npx --workspace @codenomad/ui playwright install --with-deps chromium
- name: Pack and install outside the checkout
Expand Down Expand Up @@ -220,9 +222,23 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Install Remote Control relay dependencies
run: npm ci --prefix packages/cloudflare

- name: Typecheck desktop clients
run: npm run typecheck

- name: Typecheck server and Remote Control relay
run: >-
npm run typecheck --workspace @neuralnomads/codenomad &&
npm run typecheck --prefix packages/cloudflare

- name: Test Remote Control protocol and relay
run: >-
npm test --workspace @codenomad/remote-control-protocol &&
npm test --prefix packages/cloudflare &&
npm run test:e2e --prefix packages/cloudflare

- name: Build bundled automation integration
run: npm run build:automation --workspace @neuralnomads/codenomad

Expand Down Expand Up @@ -262,6 +278,10 @@ jobs:
packages/ui/src/lib/model-visibility.test.ts
packages/ui/src/lib/native/browser.test.ts
packages/ui/src/lib/runtime-env.test.ts
packages/ui/src/lib/remote-control/bounded-body.test.ts
packages/ui/src/lib/remote-control/event-source.test.ts
packages/ui/src/lib/remote-control/tunnel.test.ts
packages/ui/src/lib/remote-control/web-socket.test.ts
packages/ui/src/lib/server-meta.test.ts
packages/ui/src/lib/theme-scheme.test.ts
packages/ui/src/lib/trailing-resync.test.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The backend delegates only the official `service start` command through its priv
- Tauri adapter: `packages/ui/src/lib/native/tauri/functions.ts`
- Desktop file drop: `packages/ui/src/lib/native/desktop-file-drop.ts`
- Client state: `packages/ui/src/lib/native/client-state.ts`
- Remote windows: `packages/ui/src/lib/native/remote-window.ts`
- Remote Control lifecycle: `packages/server/src/remote-control/manager.ts`
- Runtime detection: `packages/ui/src/lib/runtime-env.ts`

Use these abstractions instead of importing host APIs into feature components.
Expand Down
2 changes: 1 addition & 1 deletion MIGRATION_V2.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Git status is hybrid: native `vcs.status` is augmented with CodeNomad server det
- Translate host/WSL paths only after ownership validation.
- Strip CodeNomad cookies, browser authorization, forwarding headers, and incoming `x-opencode-*` headers; inject shared-service authentication server-side.
- Block upstream cookies and authentication challenges and avoid logging unredacted secret-bearing request bodies.
- Treat each unguessable preview token as a route-scoped capability. Loopback HTTP native previews use `<token>.preview.localhost` so applications retain normal root paths; HTTPS, LAN, and web clients use the equivalent capability path. SideCar/browser previews remain opaque-origin sandboxes without `allow-same-origin`, and element comments cross a source-checked message bridge.
- Treat each unguessable preview token as a route-scoped capability. Loopback HTTP native previews use `<token>.preview.localhost` so applications retain normal root paths; HTTPS and web clients use the equivalent capability path. SideCar/browser previews remain opaque-origin sandboxes without `allow-same-origin`, and element comments cross a source-checked message bridge.

## Desktop and Restore Restructuring

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ CodeNomad transforms OpenCode from a terminal tool into a **premium desktop work
## Features

- **🚀 Multi-Instance Workspace**
- **🌐 Remote Access**
- **🌐 Remote Control** through a secure outbound relay, one-time pairing links, and revocable devices
- **🧠 Session Management**
- **🎙️ Voice Input & Speech**
- **🌳 Git Worktrees**
Expand Down Expand Up @@ -56,7 +56,7 @@ npx @neuralnomads/codenomad --password <your-password> --launch

> **Self-signed certificate:** On first launch with HTTPS enabled (the default), your browser will show a "Your connection is not private" warning. This is expected — the server generates a local self-signed certificate automatically. Click **Advanced → Proceed to localhost** to continue. For local-only use without the warning, run with `--https=false --http=true`.

See [Server Documentation](packages/server/README.md) for flags, TLS, auth, and remote access.
See [Server Documentation](packages/server/README.md) for flags, TLS, authentication, and Remote Control.

### 🧪 Dev Releases

Expand All @@ -72,7 +72,7 @@ npx @neuralnomads/codenomad-dev --password <your-password> --launch

SideCars let you open local web tools inside CodeNomad as tabs.

Previews use token-scoped URLs inside opaque-origin sandboxes. Loopback HTTP native previews receive a dedicated `.preview.localhost` origin so root routes, POSTs, and live reload behave normally; HTTPS, LAN, and web hosts use the capability path. Element comments use a source-checked message bridge.
Previews use token-scoped URLs inside opaque-origin sandboxes. Loopback HTTP native previews receive a dedicated `.preview.localhost` origin so root routes, POSTs, and live reload behave normally; HTTPS and web clients use the capability path. Element comments use a source-checked message bridge.

<details>
<summary><strong>Configuration</strong></summary>
Expand Down
4 changes: 2 additions & 2 deletions dev-docs/DEVELOPER_MODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ Windows registrations are also discoverable by an OpenCode plugin running in WSL

## Target And Trust Boundaries

- The HTTP bridge and CDP endpoint use IPv4 loopback only. The bridge requires its random token. Raw CDP has no authentication and trusts local processes; default tool availability does not remove the bridge's authentication or its session/window selection checks.
- The native host selects the focused local window, or the most-recent local window when CodeNomad is not focused. A focused remote window is never selected.
- The HTTP bridge and CDP endpoint use IPv4 loopback only. The bridge requires its random token. Raw CDP has no authentication, can execute code in authenticated renderer pages, and therefore trusts local processes; default tool availability does not remove the bridge's authentication or its session/window selection checks.
- The native host selects the focused local window, or the most-recent local window when CodeNomad is not focused. Support windows such as Preferences and focused remote windows are never selected.
- CDP evaluates a bounded set of page targets and requires exactly the native window UUID, visible `data-instance-id`, and active `data-session-id`.
- The bridge resolves the OpenCode session through the shared service and verifies that the visible `data-instance-id` owns that session location.
- Operations for one native run are serialized. Click and type revalidate context immediately before input; inspection and screenshot revalidate after capture. Accessibility refs are invalidated by navigation, target replacement, context change, and restart.
Expand Down
17 changes: 16 additions & 1 deletion dev-docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Desktop host -> CodeNomad server -> one shared OpenCode service
^ |
| +-> CodeNomad /api/* and /api/events
+------ UI clients through /workspaces/:id/instance/api/*

Paired browser -> Cloudflare Worker/Durable Object <- outbound WebSocket <- CodeNomad server
```

There is no `@opencode-ai/sdk` integration and no legacy `packages/opencode-plugin` package. The narrow bundled `codenomad.automation` plugin is documented in [DEVELOPER_MODE.md](DEVELOPER_MODE.md) and [BROWSER_AUTOMATION.md](BROWSER_AUTOMATION.md); it does not own the OpenCode daemon or restore the V1 compatibility runtime.
Expand Down Expand Up @@ -36,6 +38,14 @@ OpenCode sessions and messages remain shared through the global daemon. Window m

Previews use unguessable capabilities for HTTP and WebSocket traffic. Electron and Windows Tauri local windows open HTTP(S) pages in hardened native child webviews with isolated storage; other clients use the existing capability-scoped iframe proxy. SideCar/browser iframes remain opaque-origin sandboxes without `allow-same-origin`; preview element comments use a source-checked message bridge instead of parent DOM access.

## Remote Control

CodeNomad listens only on `127.0.0.1`. Remote Control is an outbound-only connection from `packages/server/src/remote-control/` to the Cloudflare Worker and one `RemoteControlHost` Durable Object per random host ID. OpenCode is never exposed directly; relayed requests terminate at CodeNomad and continue through its existing authentication, folder, Git, Yolo, and proxy boundaries.

The persistent host identity and P-256 key pair are stored in `remote-control.json` with restricted permissions where supported; legacy identities gain a key pair without changing their host ID or relay secret. The connector authenticates with a bearer secret, while browsers pair through a fragment-token link that expires after ten minutes and pins the host public key. The relay stores only token hashes, issues secure host-scoped device cookies for 30 days, and supports revocation. Remote credentials are stripped only after host-side decryption; the local connector injects a dedicated internal CodeNomad session instead.

Protocol v2 carries HTTP streams and WebSocket messages inside an end-to-end encrypted browser-to-host tunnel. An ephemeral browser P-256 key, a fresh host challenge, ECDH, and HKDF-SHA-256 produce directional AES-256-GCM keys; authenticated counters reject tampering, reordering, and replay across the same or later tunnels. Cloudflare sees host/device routing, sizes, and timing, but receives neither application plaintext nor the host private key. This protects against an honest-but-curious relay and captured tunnel traffic, not an actively malicious Worker operator that replaces the browser bundle before it runs; reviewed releases and Cloudflare account security remain in the trust boundary. Host and remote-client sockets use the Durable Objects WebSocket Hibernation API, with attachment metadata sufficient to recover routing after an object is evicted. Connector heartbeats use Cloudflare's automatic WebSocket response path so idle hosts stay reachable without waking the object. The relay, browser, and connector bound clients, requests, sockets, devices, pairing links, bodies, frames, encrypted queues, unread response data, and outbound buffers; stream HTTP responses with idle timeouts; cancel abandoned work; and reject stale responses after a host reconnect. Electron and Tauri keep the backend alive after the final window closes only while Remote Control is enabled.

## API Boundaries

CodeNomad control APIs live under `/api/*`. Important routes include:
Expand All @@ -44,6 +54,7 @@ CodeNomad control APIs live under `/api/*`. Important routes include:
- `/api/workspaces/:id/worktrees/:slug/git-status|git-diff|git-stage|git-unstage|git-commit`
- `/api/events` and `/api/client-connections/pong`
- `/api/storage`, `/api/settings`, `/api/filesystem`, `/api/speech`
- `/api/remote-control/*`, restricted to local host UI requests
- `/api/opencode-plugin/automation`, authenticated by a per-process loopback token and restricted to CodeNomad-owned locations

Native OpenCode requests use `/workspaces/:id/instance/api/*`. The Fastify proxy exposes an explicit method/path allowlist, adds shared-service authorization, and rejects locations/directories outside the selected workspace or its worktrees. Session routes also verify `session.location.directory`. Upstream additions require an explicit proxy review and are not available automatically.
Expand Down Expand Up @@ -73,12 +84,13 @@ Current native events include session lifecycle/output events (`session.created`
| Browser SSE multiplexing | CodeNomad server |
| Desktop inspection and CDP feedback | Current CodeNomad desktop host and bundled automation plugin, available at normal startup |
| Autonomous browser previews | CodeNomad desktop browser controllers and the same bundled automation plugin, independent of Developer Mode |
| Remote Control relay, pairing, and device credentials | CodeNomad server plus Cloudflare Worker/Durable Object |

Session Shell remains separate from background Shell and PTY management. The Status panel lists location-scoped native background Shells, refreshes on Shell events/reconnect, displays native metadata, and allows ownership-checked removal. Output requests preserve native cursor pagination. Interactive PTYs remain separate. `packages/opencode-plugin` and the server plugin/background-process paths remain deleted and must not be restored; the narrow bundled automation plugin and session-pruning RPC use native V2 discovery and backend presence.

## Persistence

CodeNomad configuration resolves through `packages/server/src/config/location.ts`: `config.yaml`, `state.yaml`, and `instances/` under `~/.config/codenomad/`. `config.json` is migration input only.
CodeNomad configuration resolves through `packages/server/src/config/location.ts`: `config.yaml`, `state.yaml`, `remote-control.json`, and `instances/` under `~/.config/codenomad/`. `config.json` is migration input only.

## Key Files

Expand All @@ -90,6 +102,9 @@ CodeNomad configuration resolves through `packages/server/src/config/location.ts
- `packages/server/src/workspaces/git-mutations.ts`
- `packages/server/src/permissions/auto-accept-manager.ts`
- `packages/server/src/opencode/automation-plugin.ts`
- `packages/server/src/remote-control/manager.ts`
- `packages/cloudflare/src/remote-control/host-object.ts`
- `packages/remote-control-protocol/src/index.ts`
- `packages/ui/src/lib/sdk-manager.ts`
- `packages/ui/src/lib/api-client.ts`
- `packages/ui/src/stores/session-api.ts`
Expand Down
18 changes: 17 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@
"packages/server",
"packages/ui",
"packages/electron-app",
"packages/tauri-app"
"packages/tauri-app",
"packages/remote-control-protocol"
]
},
"scripts": {
"dev": "npm run dev --workspace @neuralnomads/codenomad-electron-app",
"dev:electron": "npm run dev --workspace @neuralnomads/codenomad-electron-app",
"dev:tauri": "npm run dev --workspace @codenomad/tauri-app",
"build": "npm run build --workspace @neuralnomads/codenomad-electron-app",
"build": "npm run build --workspace @codenomad/remote-control-protocol && npm run build --workspace @neuralnomads/codenomad-electron-app",
"build:tauri": "npm run build --workspace @codenomad/tauri-app",
"build:ui": "npm run build --workspace @codenomad/ui",
"build:mac-x64": "npm run build:mac-x64 --workspace @neuralnomads/codenomad-electron-app",
"build:binaries": "npm run build:binaries --workspace @neuralnomads/codenomad-electron-app",
"typecheck": "npm run typecheck --workspace @codenomad/ui && npm run typecheck --workspace @neuralnomads/codenomad-electron-app",
"typecheck": "npm run typecheck --workspace @codenomad/remote-control-protocol && npm run typecheck --workspace @codenomad/ui && npm run typecheck --workspace @neuralnomads/codenomad-electron-app",
"bumpVersion": "node ./scripts/bump-version.js"
},
"dependencies": {
Expand Down
Loading
Loading