CodeNomad/packages/server
heunghingwan ca06bd99d7
feat(yolo): move permission auto-accept (Yolo) to the server (#561)
## Summary

Yolo (permission auto-accept) currently lives entirely in the UI. Each
browser keeps its own toggle in `localStorage` and auto-replies to
permission requests over a 4-hop path (`OpenCode → server SSE → UI →
server proxy → OpenCode`). The server — which already sits on the event
stream that carries every `permission.asked` — does none of the work.

This PR makes the **server authoritative**: it owns the toggle state,
resolves family-root inheritance, and performs the auto-reply in-process
via loopback using the same `"once"` semantics the UI used to send. The
UI becomes a pure view: it toggles via REST and mirrors state from a
`yolo.stateChanged` SSE event.

## Why

- **Correctness**: the server already consumes the instance SSE stream
(`InstanceEventBridge`); auto-accepting there is the natural choke point
instead of bouncing to the UI and back.
- **Multi-client**: previously each browser had independent
`localStorage` state and never synced. Toggles now broadcast to all
connected clients in real time.
- **Headless**: Yolo keeps auto-accepting even when no UI is connected
(useful for long autonomous runs).
- **Latency**: drops from 4 hops to a single in-process loopback call.

## What changed

**Server (new, authoritative)**
- `permissions/auto-accept-store.ts` — in-memory state keyed by family
root. Faithful port of `resolvePermissionAutoAcceptFamilyRoot`:
fork/`revert` sessions root at themselves; enabling any member enables
the whole family.
- `permissions/auto-accept-manager.ts` — subscribes to `instance.event`,
builds the session tree from `session.created/updated/deleted`
(`properties.info`), intercepts `permission.v2.asked` /
`permission.asked`, dedupes in-flight replies, emits `yolo.stateChanged`
/ `yolo.autoAccepted`, clears per-instance state on
`workspace.stopped/error`.
- `permissions/opencode-replier.ts` — default replier calling OpenCode
directly (`getInstancePort` + auth header), mirroring
`background-processes/manager.ts`.
- `server/routes/yolo.ts` — `GET/POST
/workspaces/:id/yolo/sessions/:sid[/toggle]`, following existing route
conventions.
- `api-types.ts` / `events/bus.ts` — `YoloStateResponse` + the two new
event types registered in `onEvent` so they flow over `/api/events`.

**UI (pure view)**
- `permission-auto-accept.ts` — removed `localStorage`, persistence, and
drain logic; now a runtime (non-persisted) projection.
`resolvePermissionAutoAcceptFamilyRoot` is **retained as a display aid**
so the badge still lights up for child/sub-sessions of an enabled family
(preserving the inheritance UX).
- `instances.ts` — toggle calls REST (optimistic, reconciled on success,
reverted on failure); subscribes to `yolo.stateChanged`;
`ensureYoloStateSynced` backfills the active session's state on first
connect (deduped per session, reset on SSE reconnect so it re-syncs
after a server restart).
- `session-events.ts` — removed the three
`drainAutoAcceptPermissionsForInstance` hooks (the server drains now).
- `api-client.ts` — `getYoloState` / `toggleYolo`.

## Behavior parity

| Aspect | Before | After |
|---|---|---|
| Reply semantics | `"once"` | `"once"` (unchanged) |
| Inheritance | whole family (root + non-fork descendants) | **same** |
| Fork isolation | `revert` session is its own root | **same** |
| Persistence | UI `localStorage` | none (intentional, see Notes) |
| Multi-client sync |  independent per browser |  real-time via SSE |
| Works with UI closed |  |  |
| Auto-reply path | 4 hops | 1 in-process hop |

## Testing

32 unit tests added (`node:test`), all passing. Server + UI typecheck
clean.

- **Store (16)**: inheritance (parent/child/sibling), fork isolation,
cyclic parent chains, late parent discovery, `revert` re-rooting,
per-instance independence, tree maintenance.
- **Manager (13)**: real `properties.info` event shapes, v2 vs legacy
reply, in-flight dedup + retry-after-resolve, `yolo.stateChanged`
emission, `session.deleted` keeps toggle, `workspace.stopped` clears
state, `stop()` unsubscribes.
- **UI (3)**: retained `resolvePermissionAutoAcceptFamilyRoot`
display-projection tests.

## Notes for reviewers

- **No persistence is intentional** for this milestone — server restart
resets all Yolo state (matches the agreed scope). The UI mirror
self-heals via SSE reconnect + `ensureYoloStateSynced`. Persistence can
be layered on later (e.g. into `~/.config/codenomad/config.json`)
without touching the manager.
- **Family-root resolution lives in two places on purpose**: the server
resolves it to decide whether to auto-reply; the UI resolves the same
pure function to render the badge instantly (synchronous memo). Both are
faithful ports; no network round-trip is added for display.
- **`properties.info` nesting**: OpenCode wraps session records under
`properties.info` for `session.*` events (permission events are flat).
The manager handles both and the tests use the real nested shape to
guard against regressions.
- `getYoloState` / `yolo.autoAccepted` are wired but `yolo.autoAccepted`
is not yet consumed by the UI — it's available on the wire for future
observability (e.g. an audit log / toast).

## Risk / rollback

The change is additive on the server and the UI gracefully degrades to
the SSE mirror. If the server lacks the new routes (mixed-version), the
UI's optimistic toggle still flips locally and `toggleYolo` failures are
logged + reverted, so no hard breakage.

---------

Co-authored-by: Pascal André <pascalandr@gmail.com>
2026-07-08 21:13:44 +02:00
..
scripts Package CodeNomad OpenCode plugin (#433) 2026-05-12 09:00:22 +01:00
src feat(yolo): move permission auto-accept (Yolo) to the server (#561) 2026-07-08 21:13:44 +02:00
.gitignore feat(release): add dev prereleases and update notices 2026-02-12 23:53:16 +00:00
.npmignore Rename CLI package to @neuralnomads/codenomad and bin codenomad 2025-11-20 23:51:44 +00:00
package-lock.json Bump version to 0.18.0 2026-06-19 13:55:00 +01:00
package.json feat(yolo): move permission auto-accept (Yolo) to the server (#561) 2026-07-08 21:13:44 +02:00
README.md docs: add auth requirement and self-signed cert warning to quick-start (#481) 2026-05-26 22:29:42 +02:00
tsconfig.json feat(yolo): move permission auto-accept (Yolo) to the server (#561) 2026-07-08 21:13:44 +02:00

CodeNomad Server

CodeNomad Server is the high-performance engine behind the CodeNomad cockpit. It transforms your machine into a robust development host, managing the lifecycle of multiple OpenCode instances and providing the low-latency data streams that long-haul builders demand. It bridges your local filesystem with the UI, ensuring that whether you are on localhost or a remote tunnel, you have the speed, clarity, and control of a native workspace.

Features & Capabilities

🌍 Deployment Freedom

  • Remote Access: Host CodeNomad on a powerful workstation and access it from your lightweight laptop.
  • Code Anywhere: Tunnel in via VPN or SSH to code securely from coffee shops or while traveling.
  • Multi-Device: The responsive web client works on tablets and iPads, turning any screen into a dev terminal.
  • Always-On: Run as a background service so your sessions are always ready when you connect.

Workspace Power

  • Multi-Instance: Juggle multiple OpenCode sessions side-by-side with per-instance tabs.
  • Long-Context Native: Scroll through massive transcripts without hitches.
  • Deep Task Awareness: Monitor background tasks and child sessions without losing your flow.
  • Command Palette: A single, global palette to jump tabs, launch tools, and fire shortcuts.

Prerequisites

  • OpenCode: opencode must be installed and configured on your system.
  • Node.js 18+ and npm (for running or building from source).
  • A workspace folder on disk you want to serve.
  • Optional: a Chromium-based browser if you want --launch to open the UI automatically.

Usage

You can run CodeNomad directly without installing it:

npx @neuralnomads/codenomad --password <your-password> --launch

Authentication required: The server requires a password. Pass it via --password, the CODENOMAD_SERVER_PASSWORD environment variable, or create an auth.json file (see Authentication below).

To list all CLI options:

npx @neuralnomads/codenomad --help

On startup, CodeNomad prints two URLs:

  • Local Connection URL : ... (used by desktop shells)
  • Remote Connection URL : ... (used by browsers/other machines when remote access is enabled)

Install Globally

Or install it globally to use the codenomad command:

npm install -g @neuralnomads/codenomad
codenomad --password <your-password> --launch

Install Locally (per-project)

If you prefer to install CodeNomad into a project and run the local binary:

npm install @neuralnomads/codenomad
npx codenomad --password <your-password> --launch

(npx codenomad ... will use ./node_modules/.bin/codenomad when present.)

Common Flags

You can configure the server using flags or environment variables:

Flag Env Variable Description
--https <enabled> CLI_HTTPS Enable HTTPS listener (default true)
--http <enabled> CLI_HTTP Enable HTTP listener (default false)
--https-port <number> CLI_HTTPS_PORT HTTPS port (default 9898, use 0 for auto)
--http-port <number> CLI_HTTP_PORT HTTP port (default 9899, use 0 for auto)
--tls-key <path> CLI_TLS_KEY TLS private key (PEM). Requires --tls-cert.
--tls-cert <path> CLI_TLS_CERT TLS certificate (PEM). Requires --tls-key.
--tls-ca <path> CLI_TLS_CA Optional CA chain/bundle (PEM)
--tlsSANs <list> CLI_TLS_SANS Additional TLS SANs (comma-separated)
--host <addr> CLI_HOST Interface to bind (default 127.0.0.1)
--workspace-root <path> CLI_WORKSPACE_ROOT Restricts the root path where new workspaces can be opened. Git worktrees are created in .codenomad/worktrees inside the project folder.
--unrestricted-root CLI_UNRESTRICTED_ROOT Allow full-filesystem browsing
--config <path> CLI_CONFIG Config file location
--launch CLI_LAUNCH Open the UI in a Chromium-based browser
--log-level <level> CLI_LOG_LEVEL Logging level (trace, debug, info, warn, error)
--log-destination <path> CLI_LOG_DESTINATION Log destination file (defaults to stdout)
--username <username> CODENOMAD_SERVER_USERNAME Username for CodeNomad's internal auth (default codenomad)
--password <password> CODENOMAD_SERVER_PASSWORD Password for CodeNomad's internal auth
--generate-token CODENOMAD_GENERATE_TOKEN Emit a one-time local bootstrap token for desktop flows
--dangerously-skip-auth CODENOMAD_SKIP_AUTH Disable CodeNomad's internal auth (use only behind a trusted perimeter)
--ui-dir <path> CLI_UI_DIR Directory containing the built UI bundle
--ui-dev-server <url> CLI_UI_DEV_SERVER Proxy UI requests to a running dev server (requires --https=false --http=true)
--ui-no-update CLI_UI_NO_UPDATE Disable remote UI updates
--ui-auto-update <enabled> CLI_UI_AUTO_UPDATE Enable remote UI updates (true
--ui-manifest-url <url> CLI_UI_MANIFEST_URL Remote UI manifest URL

Dev Releases (Advanced)

If you want the latest bleeding-edge builds (published as GitHub pre-releases), use the dev package:

npx @neuralnomads/codenomad-dev --password <your-password> --launch

These environment variables control how CodeNomad checks for dev updates:

Env Variable Description
CODENOMAD_UPDATE_CHANNEL Update channel (use dev to enable dev build update checks)
CODENOMAD_GITHUB_REPO GitHub repo used for dev release checks (default NeuralNomadsAI/CodeNomad)

HTTP vs HTTPS

  • Default: --https=true --http=false (HTTPS only).
  • To run plain HTTP only (useful for development):
codenomad --https=false --http=true
  • To run both HTTPS (for remote) and HTTP loopback (for desktop):
codenomad --https=true --http=true

Remote Access Binding Rules

  • When remote access is enabled (bind host is non-loopback, e.g. --host 0.0.0.0):
    • HTTP listens on 127.0.0.1 only.
    • HTTPS listens on --host (LAN/all interfaces).
  • When remote access is disabled (bind host is loopback, e.g. --host 127.0.0.1):
    • Both HTTP and HTTPS listen on 127.0.0.1.

Self-Signed Certificates

If --https=true and you do not provide --tls-key/--tls-cert, CodeNomad generates a local certificate automatically under your config directory:

  • ~/.config/codenomad/tls/ca-cert.pem
  • ~/.config/codenomad/tls/server-cert.pem

Certificates are valid for about 30 days and rotate automatically on startup when needed. You can add extra SANs via:

codenomad --tlsSANs "localhost,127.0.0.1,my-hostname,192.168.1.10"

Browser warning: Self-signed certificates trigger a "Your connection is not private" warning in browsers on first visit. This is expected and safe for local development (127.0.0.1 / localhost):

  1. Chrome/Brave/Edge: Click AdvancedProceed to 127.0.0.1 (unsafe)
  2. Firefox: Click AdvancedAccept the Risk and Continue
  3. Alternative: For local-only development without the warning, run with --https=false --http=true

Note: Only accept self-signed certificates for localhost/127.0.0.1 that you control. For remote hosts, use proper TLS certificates.

Authentication

  • Default behavior: CodeNomad requires a login (username/password) and stores a session cookie in the browser.
  • --dangerously-skip-auth / CODENOMAD_SKIP_AUTH=true disables the login prompt and treats all requests as authenticated. Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.). If you bind to 0.0.0.0 while skipping auth, anyone who can reach the port can access the API.

Setting a password

Practical setup options:

  1. Runtime password (every start): Use --password <your-password> or set CODENOMAD_SERVER_PASSWORD=<your-password> environment variable
  2. Persistent password (UI setup): Launch with --generate-token, complete the local bootstrap flow in your browser, then set a password through the UI settings

The --password flag and CODENOMAD_SERVER_PASSWORD env var are runtime credentials — they must be provided on every server start and are not persisted to disk.

Advanced: auth.json internals

The auth.json file (~/.config/codenomad/auth.json) is automatically created and managed by CodeNomad when you set a password through the UI. You generally don't need to edit this file manually. For reference, it uses the following scrypt-based schema:

{
  "version": 1,
  "username": "codenomad",
  "password": {
    "algorithm": "scrypt",
    "saltBase64": "<base64-salt>",
    "hashBase64": "<base64-hash>",
    "keyLength": 64,
    "params": {
      "N": 16384,
      "r": 8,
      "p": 1,
      "maxmem": 33554432
    }
  },
  "userProvided": true,
  "updatedAt": "2026-05-18T12:00:00.000Z"
}

Manual creation of this file is not recommended unless you have a helper to generate a valid scrypt PasswordHashRecord.

Progressive Web App (PWA)

When running as a server CodeNomad can also be installed as a PWA from any supported browser, giving you a native app experience just like the Electron installation but executing on the remote server instead.

  1. Open the CodeNomad UI in a Chromium-based browser (Chrome, Edge, Brave, etc.).
  2. Click the install icon in the address bar, or use the browser menu → "Install CodeNomad".
  3. The app will open in a standalone window and appear in your OS app list.

TLS requirement Browsers require a secure (https://) connection for PWA installation. If you host CodeNomad on a remote machine, use HTTPS. Self-signed certificates generally won't work unless they are explicitly trusted by the device/browser (e.g., via a custom CA).

Data Storage

  • Config: ~/.config/codenomad/config.json
  • Instance Data: ~/.config/codenomad/instances (chat history, etc.)