Hub runtime: allocate port 8788, add Go skeleton and Docker deployment

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-10 17:11:52 +11:00
parent 26cb4d324d
commit cd56e69a2f
10 changed files with 185 additions and 7 deletions

3
src/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module hub
go 1.25

132
src/main.go Normal file
View File

@@ -0,0 +1,132 @@
// Command hub is the Central AI Hub service.
//
// Skeleton (AUD-48): it serves GET /health only. POST /ask and POST /approve
// arrive in AUD-49. The wire format is frozen in docs/hub-api.md.
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
)
const (
serviceName = "hub"
serviceVersion = "1"
defaultPort = "8788"
)
var started = time.Now()
type healthResponse struct {
OK bool `json:"ok"`
Service string `json:"service"`
Version string `json:"version"`
UptimeS int64 `json:"uptime_s"`
Checks map[string]string `json:"checks"`
}
type errorBody struct {
Status string `json:"status"`
Error errorInfo `json:"error"`
}
type errorInfo struct {
Code string `json:"code"`
Message string `json:"message"`
}
// port reads HUB_PORT. It falls back to 8788.
func port() string {
if p := os.Getenv("HUB_PORT"); p != "" {
return p
}
return defaultPort
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, code int, errCode, message string) {
writeJSON(w, code, errorBody{
Status: "error",
Error: errorInfo{Code: errCode, Message: message},
})
}
// handleHealth is GET /health. Liveness only. No authentication.
func handleHealth(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "bad_request", "Use GET for this route.")
return
}
// OmniRoute and MQTT are wired in AUD-49, so they read "unknown" here.
writeJSON(w, http.StatusOK, healthResponse{
OK: true,
Service: serviceName,
Version: serviceVersion,
UptimeS: int64(time.Since(started).Seconds()),
Checks: map[string]string{
"omniroute": "unknown",
"mqtt": "unknown",
"log": "ok",
},
})
}
// handleNotFound answers an unknown path with the frozen error envelope.
func handleNotFound(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusNotFound, "not_found", "Unknown path.")
}
// healthcheck calls this process's own /health. It is the container health probe,
// because the runtime image has no shell and no curl.
func healthcheck() {
url := fmt.Sprintf("http://127.0.0.1:%s/health", port())
client := &http.Client{Timeout: 1 * time.Second}
resp, err := client.Get(url)
if err != nil {
log.Printf("healthcheck failed: %v", err)
os.Exit(1)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("healthcheck status: %d", resp.StatusCode)
os.Exit(1)
}
os.Exit(0)
}
func main() {
hc := flag.Bool("healthcheck", false, "Call this process's /health and exit.")
flag.Parse()
if *hc {
healthcheck()
return
}
mux := http.NewServeMux()
mux.HandleFunc("/health", handleHealth)
mux.HandleFunc("/", handleNotFound)
srv := &http.Server{
Addr: ":" + port(),
Handler: mux,
ReadHeaderTimeout: 1 * time.Second,
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Printf("%s %s listening on :%s", serviceName, serviceVersion, port())
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("%s stopped: %v", serviceName, err)
}
}