diff --git a/cmd/cognitive-worker/main.go b/cmd/cognitive-worker/main.go index 9b78b2d..d70ff5c 100644 --- a/cmd/cognitive-worker/main.go +++ b/cmd/cognitive-worker/main.go @@ -86,18 +86,20 @@ func main() { mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1}) readinessClient := &http.Client{Timeout: 2 * time.Second} lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health" - mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: []health.Probe{ - health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"), - health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"), - health.HTTPProbe(readinessClient, lifeboatURL), - health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"), - }}) fleetClient := &http.Client{Timeout: 2 * time.Second} preferredFleet := capability.FleetTier(envOr("COGNITIVE_PREFERRED_FLEET_TIER", string(capability.FleetTierHeavy))) cpuFleetHeaders := map[string]string{} if cpuAPIKey != "" { cpuFleetHeaders["Authorization"] = "Bearer " + cpuAPIKey } + cpuHealthProbe := health.OptionalHTTPProbeWithHeaders(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models", cpuFleetHeaders) + readinessProbes := []health.Probe{ + health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"), + cpuHealthProbe, + health.HTTPProbe(readinessClient, lifeboatURL), + health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"), + } + mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: readinessProbes}) mux.Handle("/v1/fleet/readiness", fleetReadinessHandler(fleetClient, preferredFleet, []fleetEndpoint{ {ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/") + "/models", Headers: cpuFleetHeaders}, {ProviderID: "local-medium-unconfigured", ModelID: "medium-unconfigured", Tier: capability.FleetTierMedium}, diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index f54a423..b930a3c 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000164-authorized-fleet-readiness.json", - "task_id": "DEV-000164", + "task_file": "dev/tasks/DEV-000165-authorized-cpu-health-probe.json", + "task_id": "DEV-000165", "task_revision": 1 } diff --git a/dev/tasks/DEV-000165-authorized-cpu-health-probe.json b/dev/tasks/DEV-000165-authorized-cpu-health-probe.json new file mode 100644 index 0000000..b1de7f0 --- /dev/null +++ b/dev/tasks/DEV-000165-authorized-cpu-health-probe.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000165", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "5d5afc88883c7f79e3b051c790642e41386666ec", + "risk_tier": "E", + "intent": "Use the existing file-backed CPU provider API key in cognitive-worker readiness probes so /readyz and fleet readiness report consistent provider state.", + "requirement_ids": ["WP-601", "Model fleet/failover"], + "allowed_change_scope": {"modules": ["cognitive-worker", "platform-health"], "paths": ["runtime/health/server.go", "runtime/health/server_test.go", "cmd/cognitive-worker/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000165-authorized-cpu-health-probe.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "adapters/**", ".gitea/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["existing health Probe contract", "existing provider API key resolver"], "may_change": ["health probe header support"]}, + "semantic_owner": "cognitive-worker/platform health", + "canonical_state": [], + "state_transitions": ["authenticated CPU probe -> consistent readiness evidence"], + "side_effects_and_privileges": ["resolved key is used only in outbound probe header; never logged or persisted"], + "persistence_and_migrations": ["none"], + "failure_retry_restart": ["provider timeout or rejected key remains degraded; no fallback behavior changed"], + "assumptions": ["the local provider uses Bearer authentication"], + "acceptance_scenarios": ["health probe sends configured header", "cognitive /readyz uses authenticated CPU probe", "GPU remains optional", "no key is sent when absent"], + "required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"] +} diff --git a/runtime/health/server.go b/runtime/health/server.go index c25ceed..1f81555 100644 --- a/runtime/health/server.go +++ b/runtime/health/server.go @@ -82,11 +82,18 @@ func (h ReadinessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func HTTPProbe(client *http.Client, endpoint string) Probe { + return HTTPProbeWithHeaders(client, endpoint, nil) +} + +func HTTPProbeWithHeaders(client *http.Client, endpoint string, headers map[string]string) Probe { return Probe{Name: endpoint, Check: func(ctx context.Context) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return err } + for name, value := range headers { + req.Header.Set(name, value) + } response, err := client.Do(req) if err != nil { return err @@ -108,6 +115,12 @@ func OptionalHTTPProbe(client *http.Client, endpoint string) Probe { return probe } +func OptionalHTTPProbeWithHeaders(client *http.Client, endpoint string, headers map[string]string) Probe { + probe := HTTPProbeWithHeaders(client, endpoint, headers) + probe.Optional = true + return probe +} + func TCPProbe(name, host, port string) Probe { return Probe{Name: name, Check: func(ctx context.Context) error { connection, err := (&net.Dialer{}).DialContext(ctx, "tcp", net.JoinHostPort(host, port)) diff --git a/runtime/health/server_test.go b/runtime/health/server_test.go index e82f33a..2d8caaf 100644 --- a/runtime/health/server_test.go +++ b/runtime/health/server_test.go @@ -27,6 +27,20 @@ func TestReadinessReturns200WhenAllProbesPass(t *testing.T) { } } +func TestHTTPProbeWithHeadersSendsHeaders(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer health-key" { + t.Fatalf("authorization=%q", r.Header.Get("Authorization")) + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + probe := HTTPProbeWithHeaders(server.Client(), server.URL, map[string]string{"Authorization": "Bearer health-key"}) + if err := probe.Check(context.Background()); err != nil { + t.Fatal(err) + } +} + func TestReadinessStaysAvailableWithOptionalDependencyDegraded(t *testing.T) { handler := ReadinessHandler{Runtime: "test", Probes: []Probe{ {Name: "lifeboat", Check: func(context.Context) error { return nil }},