feat: operator-approved port previews (#3) #21
No reviewers
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
#22 feat: agent instructions editor in Settings
lz/agent-nexus
Reference
lz/agent-nexus!21
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/port-previews"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements issue #3: operator-approved L4 TCP forwarders for per-session container ports, with an agent-callable registration helper and a per-session UI.
net.createServer+socket.pipeproxy inPreviewProxy; restored on Nexus boot.POST /api/agent/previewsis bridge-only (loopback excluded; source container IP must match session's worker).<Previews>mounted under each session row, including the workspace shell.notify-preview <port> [label]worker helper readsNEXUS_SESSION_IDinjected bytmux -eon session create/respawn (multi-pane coworking works because-escopes the env to the whole tmux window).preview_bind_interface(loopback|lan); rebinding all listeners synchronously on settings PUT.Spec:
docs/superpowers/specs/2026-05-24-operator-approved-port-previews-design.mdPlan:
docs/superpowers/plans/2026-05-24-port-previews.mdHow it hangs together
0007_previews.ts(FK to sessions on cascade, status/source CHECKs).singletons.tsconstructsPreviewProxyonce and stores it on theglobalThisslot so both esbuild and Rollup bundles see the same instance.cachedBindHostlives on that slot too.deleteSession,removeWorker,closeWorkspaceSession, andreconcileSessions(the 10s tmux-window reconciler) all callpreviewProxy.stopAllForSession(sessionId)before dropping the DB row, so the FK cascade can't orphan TCP listeners.preview_bind_interfacecallsrefreshBindHost+rebindAllPreviews— listeners are torn down and restarted on the new interface; any listener that can't rebind is demoted topending.Review status (transparency)
Per-task TDD + code-quality reviews were run during implementation. The pure helpers, proxy, service layer, singletons wiring, agent route, and session-injection task all got both spec and quality review with follow-up fix commits where reviewers flagged issues. Tasks 4 (1-line registry add), 8 (thin route wrappers), 10/12 (frontend types + UI), 11 (settings-side hook), 13/14 (worker helper + docs) skipped per-task quality review to keep the cadence — risk is low but it's not zero. A final cross-cutting review caught two listener-leak bugs in
reconcileSessionsandcloseWorkspaceSessionthat are now fixed in927647d.Test plan
pnpm --dir nexus testonnode:22-alpine— locally Windows sqlite tests auto-skip per AGENTS.md; CI exercises them in full (most importantlypreviews/service.test.ts).pnpm --dir nexus exec tsc --noEmit— passes locally; CI re-runs.notify-preview 8000 pyinside a session pane)netstat -an | grep <host_port>empty)Per code review: - torn flag in handleConnection so teardown is one-shot - guard before creating upstream in the resolveContainerIp .then() so a stop() racing the bridge-IP lookup can't leak an untracked upstream socket - stop(): destroy sockets first, then server.close(cb) — removes the TOCTOU gap between the .listening check and the once('close') registration - upstream-fail test uses a freshly-closed port instead of port 1 (firewall- agnostic ECONNREFUSED)Editable, defaults to 'loopback'. Validated to ('loopback' | 'lan'). Auto-listed by InstancePanel via existing settings/instance API.The agent inside the workspace knows whether its dev server speaks http or https — Nexus shouldn't guess. Add scheme as a first-class column on the previews table (default 'http', check-constrained to http|https). Threads through: - notify-preview.sh: new --scheme http|https flag (POSIX arg parser). - POST /api/agent/previews and POST .../sessions/:id/previews: optional body field 'scheme'. - DB row + DTO + service insertPending all carry scheme. - Server-side host_url uses the row's scheme (kept for non-browser consumers like notify-preview's 'already approved' message). UI changes in Previews.svelte: - Compose the displayed URL as ${p.scheme}://${window.location.hostname}:${p.host_port}. This makes the URL match how the operator reached Nexus — if they used http://devops.lan, the preview link is http://devops.lan:<port>, not http://<server-bind-ip>:<port>. - Multi-line row layout (URL on its own line, word-break: break-all) so long URLs don't overflow the workspace card. - Add http/https select in the manual-add form.