Harden Halibut polling deployment
409 on name conflict, body-code va…idation, install-script probe, K8s runbook
Closes the root causes and guardrails for the 2026-04-18 production incident where a Tentacle registered successfully (HTTP 200) but polling loop-failed with opaque EOF errors because:
- server returned HTTP 200 + body{code:500} on duplicate-machine-name, which the client mistook for success;
- the generated install script embedded a polling URL that was externally unreachable (SLB health check mis-configured + node security group missing 100.64.0.0/10 ingress rule);
- no documentation / runbook existed to tell operators which network preconditions Halibut polling actually requires.
Code changes (non-breaking — MachineNameConflictException derives from InvalidOperationException so existing catch sites keep working):
• MachineNameConflictException with MachineName + SpaceId context
• EnsureUniqueNameAsync throws the new exception instead of raw
InvalidOperationException
• GlobalExceptionFilter maps MachineNameConflictException → HTTP 409
• TentacleRegistrationClient now validates body code (not only HTTP
status), plus enforces MachineId > 0 && ServerThumbprint != empty
before declaring "Registration successful"
• Optional HttpMessageHandlerFactory test seam on
TentacleRegistrationClientOptions
• New ITentacleCommsUrlProbe + TentacleCommsUrlProbe: TCP+TLS probe with
actionable remediation hints baked into Detail
• MachineScriptService.GenerateTentacleInstallScriptAsync runs the probe
and attaches result to response; UI can now render a warning banner
instead of silently handing out a broken script
• GenerateTentacleInstallScriptData carries new TentacleCommsProbeInfo
Docs: • CLAUDE.md adds "Kubernetes Deployment — Exposing Halibut Polling" section with full yaml, SG rule, diagnostic commands, failure matrix • deploy/k8s-network-requirements.md runbook for operators (independent of developer-facing CLAUDE.md), covering all 5 debugging steps used in the 2026-04-18 incident
Tests (all green: UnitTests 4052, Tentacle.Tests 1002, Calamari 115, Watchdog 17 = 5186 total): • RegisterSsh + RegisterTentaclePolling duplicate-name → 409 regression tests • TentacleRegistrationClient: Http200+bodyCode500, Http409, Http200+ emptyData, Http200+goodData, 4xx-not-retried • TentacleCommsUrlProbe: empty URL, malformed URL, unreachable host (asserts remediation hint mentions 100.64.0.0/10), in-proc TLS server with matching + mismatching thumbprint • MachineInstallScriptService: polling-mode invokes probe, listening- mode skips, unreachable probe surfaces to response