// The agent's wiring for the host half of the reverse channel (issue #53 / C6 // Step 3). The dial/serve/backoff machinery itself lives in // internal/tunnelclient — a leaf package, so the edge's acceptance test can // drive the SAME loop this agent runs (internal/host imports internal/edge, // so the edge tests can never import this package). What is left here is the // part that is genuinely agent-shaped: reading the env, and publishing the // state the status heartbeat reports. package host import ( "context" "log" "net/http" "os" "sync/atomic" "github.com/carlosframework/platform/internal/tunnelclient" ) // tunnelStateVal is "" (disabled/never started), "connected", or // "reconnecting" — read by statusLoop's heartbeat probe (probes must fail // toward honest values; "" means the loop isn't running at all). var tunnelStateVal atomic.Value func tunnelState() string { if v, ok := tunnelStateVal.Load().(string); ok { return v } return "" } // tunnelLoop dials CARLOS_TUNNEL_URL and serves handler over the inverted // conn, forever, with jittered capped backoff. Inert unless all three env // vars are set — the same convention as every other optional agent loop. func tunnelLoop(handler http.Handler) { rawURL := os.Getenv("CARLOS_TUNNEL_URL") token := os.Getenv("CARLOS_TUNNEL_TOKEN") name := os.Getenv("CARLOS_TUNNEL_NAME") if rawURL == "" || token == "" || name == "" { if rawURL != "" || token != "" || name != "" { log.Printf("agent: tunnel loop disabled: need all of CARLOS_TUNNEL_URL, CARLOS_TUNNEL_TOKEN, CARLOS_TUNNEL_NAME") } return } log.Printf("agent: tunnel loop armed (%s as %q)", rawURL, name) // context.Background(): the agent's tunnel runs for the life of the // process and is torn down with it. The ctx exists for callers that need // to stop the loop deterministically (the edge's e2e), not for this one. tunnelclient.Loop(context.Background(), tunnelclient.Config{ URL: rawURL, Name: name, Token: token, Handler: handler, OnState: func(s string) { tunnelStateVal.Store(s) }, // The "agent: " prefix is what operators grep the journal for, so it // is preserved here rather than baked into the leaf package. Logf: func(format string, args ...any) { log.Printf("agent: "+format, args...) }, }) }