Skip to content

netclaw init does not surface reverse proxy as an exposure mode #1114

@Aaronontheweb

Description

@Aaronontheweb

Problem

Daemon.ExposureMode = "reverse-proxy" is fully supported by the runtime — config schema, daemon binding, ForwardedHeaders middleware, startup validation (DaemonExposureValidator.Validate), and ExposureModeDoctorCheck all handle it correctly. But netclaw init only offers four of the five modes (Local, Tailscale Serve, Tailscale Funnel, Cloudflare Tunnel).

Operators who want to run Netclaw behind nginx / Caddy / Traefik / IIS / ALB currently have to either hand-edit netclaw.json after init or pick a mode they don't actually want. This is a UX gap, not a missing capability.

Proposed change

Add reverse proxy as the second option in the exposure-mode selection list (right after Local, ordered by deployment friction) and introduce one new sub-step in the ExposureModeStepViewModel to collect:

  • Bind address — text input, defaults to 0.0.0.0
  • Trusted proxies — multi-line text input, one IP/CIDR per line, ≥1 required

After collection, show a medium informational notice ("Got it — continue") with the exact serving URL the operator should configure their proxy to forward to (e.g. http://0.0.0.0:5199), the trusted proxies they just entered, and the operator responsibilities (TLS termination, firewalling, forwarded-header setup).

Validation policy

The wizard enforces only the same trusted-proxy minimum the runtime already enforces (≥1 entry), and blocks advance from the config sub-step until that's satisfied. This matches DaemonExposureValidator.Validate (src/Netclaw.Configuration/DaemonConfig.cs:140-155) and prevents the wizard from producing a config the daemon would refuse to start with.

Bind-address loopback / IP-format validation stays in DaemonExposureValidator and ExposureModeDoctorCheck — duplicating that logic in the TUI would drift over time. The help text mentions the loopback rule so operators don't trip on it unintentionally.

No deferrable / "we'll fix it later" path. Operators who don't yet know their proxy IP should pick Local and re-run netclaw init --resume once they do.

TUI flow (4 sub-steps for reverse proxy)

Sub-step 0:  Mode selection — Reverse Proxy added as 2nd option

Sub-step 1:  Configure reverse proxy
             ┌──────────────────────────────────────┐
             │ Bind address: [0.0.0.0            ]  │
             │ Trusted proxies (one per line, ≥1):  │
             │   [10.0.0.0/24                   ]   │
             │   [192.168.1.5                   ]   │
             └──────────────────────────────────────┘
             [Enter] blocked until ≥1 trusted proxy entered.

Sub-step 2:  Reverse proxy configured
             Daemon will listen on:  http://0.0.0.0:5199
             Trusted proxies:        10.0.0.0/24, 192.168.1.5
             You are responsible for: TLS termination, firewalling,
                                      X-Forwarded-* header setup.
             ▸ Got it — continue

Sub-step 3:  Webhook toggle (unchanged from today)

The wizard step-header chrome shows Network Exposure: Reverse Proxy → http://0.0.0.0:5199 through the remainder of the wizard, giving the operator a second sighting of the serving URL.

Files affected

  • src/Netclaw.Cli/Tui/Wizard/Steps/ExposureModeStepViewModel.cs — add Host + TrustedProxies fields, add 4th sub-step, update sub-step routing
  • src/Netclaw.Cli/Tui/Wizard/Steps/ExposureModeStepView.cs — add ReverseProxy to the selection list, add BuildReverseProxyConfig() with two TextInputNodes (pattern from IdentityStepView.cs:145-190), add BuildReverseProxyNotice()
  • src/Netclaw.Cli/Tui/Wizard/WizardConfigBuilder.cs — extend DaemonConfigSection (line 461) with Host + TrustedProxies, update the serializer at line 270 to emit them
  • src/Netclaw.Cli.Tests/Tui/Wizard/ExposureModeStepViewModelTests.cs — add reverse-proxy test cases
  • tests/smoke/tapes/init-wizard-reverse-proxy.tape (new) — drives the wizard through the reverse-proxy flow, anchored with Wait+Screen per tests/smoke/tapes/README.md
  • tests/smoke/assertions/init-wizard-reverse-proxy.sh (new) — jq-asserts Daemon.ExposureMode, Daemon.Host, Daemon.TrustedProxies[0]; runs netclaw doctor (exit 0 or 2)
  • feeds/skills/.system/files/netclaw-operations/SKILL.md — add reverse proxy to the exposure-mode guidance; bump metadata.version
  • docs/spec/SPEC-007-guided-onboarding.md — add reverse proxy to the wizard's exposure-mode list

No schema, validator, or daemon runtime changes needed. Those already do the right thing.

Acceptance criteria

  • netclaw init shows Reverse Proxy as the second option
  • Selecting it collects bind address and trusted proxies
  • Advance is blocked from the config sub-step until ≥1 trusted proxy is entered, with help text explaining why
  • The notice screen shows the serving URL http://{Host}:{Port} and operator responsibilities
  • The serving URL remains visible in the wizard step-header chrome through the rest of the wizard
  • Resulting netclaw.json contains Daemon.Host, Daemon.ExposureMode = "reverse-proxy", and Daemon.TrustedProxies
  • dotnet test src/Netclaw.Cli.Tests passes
  • ./scripts/smoke/run-smoke.sh light passes, including the new init-wizard-reverse-proxy tape
  • dotnet slopwatch analyze passes (no new violations)
  • ./scripts/Add-FileHeaders.ps1 -Verify passes
  • feeds/skills/.system/files/netclaw-operations/SKILL.md version bumped
  • docs/spec/SPEC-007-guided-onboarding.md updated

Non-goals

  • No changes to DaemonExposureValidator, ExposureModeDoctorCheck, the JSON schema, the daemon's ForwardedHeaders middleware, or any other runtime path — they already handle reverse proxy correctly
  • No automatic detection of common proxy IP ranges (Docker, k8s, etc.) — speculative
  • No port input in the wizard — port stays at the 5199 default; can be hand-edited in netclaw.json after init
  • No TLS / certificate management — TLS terminates at the proxy
  • No deferrable trusted-proxies path — the daemon requires them at start, so the wizard does too

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions