Skip to content

Latest commit

 

History

History
122 lines (90 loc) · 5.51 KB

File metadata and controls

122 lines (90 loc) · 5.51 KB

Web UI Port — Configuration and Conflict Handling

How OpenProgram chooses, configures, and defends the port its web UI runs on: the configuration surface (openprogram ports) and what happens when the port is occupied. The runtime that makes one port sufficient is described in single-port.md.

The port at a glance

Default Serves Configured by
18100 FastAPI /api/*, /ws, /healthz, and the static web UI export ports --port, OPENPROGRAM_WEB_PORT, ui.web_port

The browser, the TUI and the CLI all talk to this one port. There is no proxy hop and no second process.

Why 18100

A fixed, uncommon, 5-digit value chosen so it almost never collides with something already running:

  • In the registered-port range (< 49152), so it never clashes with the OS ephemeral range the kernel hands out to outbound sockets.
  • The 18xxx block is rarely used by mainstream dev tooling — unlike 3000 / 5173 / 8000 / 8080, which any other project may already hold.

A fixed port also means a stable, bookmarkable URL and a browser session (localStorage, service worker scope) that survives restarts.

Configuration surface

explicit flag / arg  >  environment variable  >  stored pref  >  built-in default

openprogram ports

openprogram ports                 # show the current port
openprogram ports --port 9100     # set + persist

Writes to ~/.openprogram/config.json under ui.web_port. Nothing live is rebound — the change takes effect on the next openprogram web / openprogram worker start.

openprogram setup ui

The interactive wizard asks for the port (and the auto-open-browser pref) and validates range 1–65535.

Environment override (single run, not persisted)

  • OPENPROGRAM_WEB_PORT — the port for this process.

Per-launch flag

openprogram web --web-port <p> overrides for that run without persisting.

Where each entry point reads from

Entry point Port
openprogram web (cli/commands/web.py:_cmd_web) --web-portresolve_worker_port()
openprogram worker (worker/runner.py) resolve_worker_port()

resolve_worker_port() in openprogram/worker/lifecycle.py is the one resolution path: OPENPROGRAM_WEB_PORT → pref ui.web_port → 18100. read_ui_prefs() / set_ui_ports() in openprogram/setup.py are the one read/write path for the persisted pref.

Conflict handling

The port is pinned on purpose — a stable UI URL is worth more than "start no matter what". So the policy is reuse if it's ours, report and refuse if it's not — never kill the holder, never silently drift to a random port. This mirrors openclaw. All probing lives in one module, openprogram/_ports.py:

  • livenessport_in_use(port): a bare TCP connect.
  • identitybackend_is_ours(port) first checks the managed worker PID and port files plus the active profile's owner-only web/access.json snapshot, then sends a fresh random nonce to /api/auth/challenge and verifies the returned token-HMAC proof locally. The probe never sends the owner token or a Bearer header to the listener; a foreign process on the port cannot obtain the credential. An optional expected_revision binds the proof to the revision served by an upgrade target.
  • ownershipdescribe_port_owner(port) / port_owner_hint(port): lsof / netstat + /proc / ps / wmic to name the holding PID and command line, classified ours-vs-foreign. This is what lets a "port in use" error say who holds it.

Behavior by case

The fixed port is… openprogram web openprogram worker
free binds, starts binds, starts
held by our instance reuse it, point the browser at the UI the worker lock already prevents a second worker
held by a foreign program refuse; print who holds it (PID + cmdline) + how to free it or change the port; do not open a browser at it name the holder, then fall back to a free port and report it (the UI URL tracks it) — the worker also hosts channels, so it must still come up
recently-exited (TIME_WAIT) uvicorn's SO_REUSEADDR rebinds it _port_available uses SO_REUSEADDR, so a quick self-restart does not drift

The one deliberate asymmetry: openprogram web is a foreground UI command, so a foreign squatter is a hard stop. The worker is a long-running host for channels and the webui, so it stays up, falling back to another port with a diagnostic message, rather than refusing entirely.

Relationship to openclaw

openclaw pins its gateway to 18789 and handles conflicts in three layers; OpenProgram's equivalents:

openclaw layer openclaw source OpenProgram equivalent
single-instance lock (pid + start-time + argv) src/infra/gateway-lock.ts worker.lock (fcntl) + worker.pid (with start-time) + _process_alive
EADDRINUSE retry to ride out TIME_WAIT src/gateway/server/http-listen.ts SO_REUSEADDR on the bind (no retry loop needed)
name the holder via lsof src/infra/ports.ts _ports.describe_port_owner / port_owner_hint, wired into every "port in use" message

Notably, openclaw's lsof diagnostic is not on its main gateway-start path (only on the SSH-tunnel path), so its gateway-start "port in use" error can't name the holder. OpenProgram wires the owner diagnostic into the actual start path.