fix(agent-url): decouple session LAB_URL from --base-url; harden labctl vs auth proxies (#30) #31

Merged
dominik.polakovics merged 1 commit from fix/agent-url-loopback into main 2026-07-07 23:16:42 +02:00

Summary

Fixes labctl breaking behind an SSO/auth proxy (nginx + Authelia). --base-url did double duty — the external browser origin and the LAB_URL handed to every session — so labctl's run-token /agent/v1 calls hairpinned out to the public FQDN, got 302'd to the Authelia login page, and (following the redirect) failed decoding the HTML as JSON (invalid character '<'). The agent API self-authenticates with run tokens and is mounted outside the operator auth/CSRF middleware, so machine traffic must never touch the proxy.

Implements approach A + C (decided with the maintainer in the issue).

A — dedicated session-facing URL, loopback by default

  • New --agent-url / LAB_AGENT_URL server knob (config.AgentURL), validated as an absolute http(s) URL exactly like --base-url (shared validateHTTPURL helper).
  • labURL() precedence is now agent-URL → base-URL → http://127.0.0.1:<port>.
  • NixOS module gains services.lab.agentUrl, defaulting to a loopback URL derived from listenAddr and passed as --agent-url — so a proxy-fronted deployment keeps machine traffic on loopback with zero operator config. null falls back to lab's own precedence.
  • --base-url keeps its Secure-cookie / CSRF-Origin role unchanged.

C — labctl fails loudly instead of parsing HTML as JSON

  • HTTP client sets CheckRedirect = ErrUseLastResponse — no more silent redirect-following.
  • A 3xx surfaces the redirect Location + an "LAB_URL may be behind an auth proxy" hint.
  • An HTML body (by Content-Type or a leading <) at any status surfaces the same hint — including 2xx, so a proxy answering 200 text/html to a mutating command (issue close/comment/label, which decode nothing) can never report a lost write as success.

Tests

  • Config: --agent-url precedence + validation (incl. bare host:port → actionable http(s) hint).
  • labURL() precedence (new cmd/lab/main_test.go).
  • labctl: 302 redirect, and 200/401/502 HTML across read and mutating commands.
  • flake.nix nixos-module check asserts the unit defaults --agent-url to loopback even with an external baseUrl.

Verification

  • go build, gofmt, go vet, full go test ./... — green.
  • nix build .#checks.x86_64-linux.nixos-module — exit 0 (real build).
  • 5-dimension adversarial review workflow ran; all 5 findings applied.

Out of scope

  • cloonar-nixos pinned-rev bump (utils/modules/coding-lab/default.nix) — follow-up once this lands.
  • The rejected nginx carve-out exposing /agent/v1/ unauthenticated.
  • Any change to run-token auth or --base-url's cookie/CSRF role.

Fixes #30

## Summary Fixes labctl breaking behind an SSO/auth proxy (nginx + Authelia). `--base-url` did double duty — the external browser origin **and** the `LAB_URL` handed to every session — so labctl's run-token `/agent/v1` calls hairpinned out to the public FQDN, got 302'd to the Authelia login page, and (following the redirect) failed decoding the HTML as JSON (`invalid character '<'`). The agent API self-authenticates with run tokens and is mounted outside the operator auth/CSRF middleware, so machine traffic must never touch the proxy. Implements **approach A + C** (decided with the maintainer in the issue). ### A — dedicated session-facing URL, loopback by default - New `--agent-url` / `LAB_AGENT_URL` server knob (`config.AgentURL`), validated as an absolute http(s) URL exactly like `--base-url` (shared `validateHTTPURL` helper). - `labURL()` precedence is now **agent-URL → base-URL → `http://127.0.0.1:<port>`**. - NixOS module gains `services.lab.agentUrl`, **defaulting to a loopback URL derived from `listenAddr`** and passed as `--agent-url` — so a proxy-fronted deployment keeps machine traffic on loopback with zero operator config. `null` falls back to lab's own precedence. - `--base-url` keeps its Secure-cookie / CSRF-Origin role unchanged. ### C — labctl fails loudly instead of parsing HTML as JSON - HTTP client sets `CheckRedirect = ErrUseLastResponse` — no more silent redirect-following. - A **3xx** surfaces the redirect `Location` + an "`LAB_URL` may be behind an auth proxy" hint. - An **HTML body** (by `Content-Type` or a leading `<`) at **any** status surfaces the same hint — including 2xx, so a proxy answering `200 text/html` to a *mutating* command (`issue close`/`comment`/label, which decode nothing) can never report a lost write as success. ## Tests - Config: `--agent-url` precedence + validation (incl. bare `host:port` → actionable http(s) hint). - `labURL()` precedence (new `cmd/lab/main_test.go`). - labctl: 302 redirect, and `200/401/502` HTML across read **and** mutating commands. - `flake.nix` `nixos-module` check asserts the unit defaults `--agent-url` to loopback even with an external `baseUrl`. ## Verification - `go build`, `gofmt`, `go vet`, full `go test ./...` — green. - `nix build .#checks.x86_64-linux.nixos-module` — exit 0 (real build). - 5-dimension adversarial review workflow ran; all 5 findings applied. ## Out of scope - cloonar-nixos pinned-rev bump (`utils/modules/coding-lab/default.nix`) — follow-up once this lands. - The rejected nginx carve-out exposing `/agent/v1/` unauthenticated. - Any change to run-token auth or `--base-url`'s cookie/CSRF role. Fixes #30
fix(agent-url): decouple session LAB_URL from --base-url; harden labctl vs auth proxies
All checks were successful
ci / flake-check (pull_request) Successful in 12m3s
fc6dba9a7a
Behind an SSO/auth proxy (nginx + Authelia), --base-url did double duty: the
external browser origin AND the LAB_URL handed to every session. labctl's
run-token calls to /agent/v1 hairpinned out to the proxy, got 302'd to the
login portal, and — following the redirect — failed decoding the HTML login
page as JSON. The run's only tracker surface (D10) was dead in that deployment.

Approach A + C (decided with the maintainer, issue #30):

A. New --agent-url / LAB_AGENT_URL server knob (validated as an absolute
   http(s) URL, like --base-url). Session LAB_URL precedence becomes
   agent-URL > base-URL > http://127.0.0.1:<port>. The NixOS module defaults
   services.lab.agentUrl to a loopback URL derived from listenAddr and passes
   --agent-url, so a proxy-fronted deployment keeps machine traffic on loopback
   with no operator config. --base-url keeps its cookie/CSRF role unchanged.

C. labctl no longer follows redirects (CheckRedirect = ErrUseLastResponse). A
   3xx surfaces the redirect target and an "LAB_URL may be behind an auth proxy"
   hint; an HTML body (by Content-Type or a leading '<') at any status — 2xx
   included, so mutating commands never report a lost write as success — surfaces
   the same hint instead of "invalid character '<'".

Tests cover config precedence + validation, the session-URL precedence, and
labctl's redirect / non-JSON handling across read and mutating commands. The
nixos-module flake check asserts the unit defaults --agent-url to loopback even
with an external baseUrl.

Fixes #30

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NP2Ai6CBkYY7Fj1fujtRDD
Author
Owner

This was generated by AI while landing a PR.

Landing audit — PASS

Verification signal relied on: CI ci / flake-check (pull_request) green (run #34, 12m3s) — real go test ./... + golangci-lint + nix build of the nixos-module. Not re-run.

Checked:

  • Mergeable, no conflicts — no resolution needed.
  • Title is Conventional Commits (fix(agent-url): …); no secrets.yaml / system.stateVersion edits; NixOS module changes are additive options.
  • Fixes #30 present → merge will auto-close the issue.
  • Diff vs. issue #30 acceptance criteria — all met:
    • --agent-url / LAB_AGENT_URL added, validated as an absolute http(s) URL via the shared validateHTTPURL (bare host:port folded into the same actionable message).
    • labURL() precedence is now agent-URL → base-URL → http://127.0.0.1:<port>.
    • labctl sets CheckRedirect: noRedirect; a 3xx surfaces the redirect Location + auth-proxy hint; an HTML body (Content-Type or leading <) is rejected at any status — including a 2xx on a mutating command, so a proxied login page can't report a lost write as success.
    • NixOS services.lab.agentUrl defaults to a loopback URL derived from listenAddr, passed as --agent-url; null falls back to lab's own precedence.
    • Tests cover config precedence/validation, labURL() precedence, and labctl 302 + 200/401/502 HTML across read and mutating commands; the flake nixos-module check asserts the loopback --agent-url default even with an external baseUrl.

Verdict: PASS — cleared for merge pending the maintainer's free-text go-ahead.

> *This was generated by AI while landing a PR.* **Landing audit — PASS** Verification signal relied on: CI `ci / flake-check (pull_request)` green (run #34, 12m3s) — real `go test ./...` + `golangci-lint` + `nix build` of the nixos-module. Not re-run. Checked: - **Mergeable**, no conflicts — no resolution needed. - **Title** is Conventional Commits (`fix(agent-url): …`); no `secrets.yaml` / `system.stateVersion` edits; NixOS module changes are additive options. - **`Fixes #30`** present → merge will auto-close the issue. - **Diff vs. issue #30 acceptance criteria — all met:** - `--agent-url` / `LAB_AGENT_URL` added, validated as an absolute http(s) URL via the shared `validateHTTPURL` (bare `host:port` folded into the same actionable message). - `labURL()` precedence is now agent-URL → base-URL → `http://127.0.0.1:<port>`. - labctl sets `CheckRedirect: noRedirect`; a 3xx surfaces the redirect `Location` + auth-proxy hint; an HTML body (Content-Type or leading `<`) is rejected at any status — including a 2xx on a *mutating* command, so a proxied login page can't report a lost write as success. - NixOS `services.lab.agentUrl` defaults to a loopback URL derived from `listenAddr`, passed as `--agent-url`; `null` falls back to lab's own precedence. - Tests cover config precedence/validation, `labURL()` precedence, and labctl 302 + 200/401/502 HTML across read and mutating commands; the flake nixos-module check asserts the loopback `--agent-url` default even with an external `baseUrl`. Verdict: **PASS** — cleared for merge pending the maintainer's free-text go-ahead.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!31
No description provided.