diff --git a/cmd/cognitive-worker/fleet_status.go b/cmd/cognitive-worker/fleet_status.go index 004c8cb..15a3c45 100644 --- a/cmd/cognitive-worker/fleet_status.go +++ b/cmd/cognitive-worker/fleet_status.go @@ -17,6 +17,7 @@ type fleetEndpoint struct { ModelID string Tier capability.FleetTier URL string + Headers map[string]string } type fleetStatusResponse struct { @@ -37,6 +38,9 @@ func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier, } request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint.URL, nil) if err == nil { + for name, value := range endpoint.Headers { + request.Header.Set(name, value) + } response, requestErr := client.Do(request) if requestErr == nil { response.Body.Close() diff --git a/cmd/cognitive-worker/fleet_status_test.go b/cmd/cognitive-worker/fleet_status_test.go index 70d8571..c30d976 100644 --- a/cmd/cognitive-worker/fleet_status_test.go +++ b/cmd/cognitive-worker/fleet_status_test.go @@ -25,3 +25,22 @@ func TestFleetReadinessSelectsLifeboatWhenPrimaryTiersUnavailable(t *testing.T) t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } } + +func TestFleetReadinessSendsConfiguredProviderHeaders(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer fleet-key" { + t.Fatalf("authorization=%q", r.Header.Get("Authorization")) + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + handler := fleetReadinessHandler(server.Client(), capability.FleetTierFast, []fleetEndpoint{{ + ProviderID: "cpu", ModelID: "cpu-model", Tier: capability.FleetTierFast, URL: server.URL, + Headers: map[string]string{"Authorization": "Bearer fleet-key"}, + }}) + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil)) + if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"ready":true`) { + t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) + } +} diff --git a/cmd/cognitive-worker/main.go b/cmd/cognitive-worker/main.go index 2f68029..9b78b2d 100644 --- a/cmd/cognitive-worker/main.go +++ b/cmd/cognitive-worker/main.go @@ -94,8 +94,12 @@ func main() { }}) 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 + } 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"}, + {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}, {ProviderID: "local-gpu-opportunistic", ModelID: "gpu-opportunistic", Tier: capability.FleetTierHeavy, URL: strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/") + "/api/tags"}, {ProviderID: "cognitive-lifeboat", ModelID: "qwen3-8b-lifeboat", Tier: capability.FleetTierLifeboat, URL: lifeboatURL}, diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 511d7f3..f54a423 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000163-operations-ui-contract-refresh.json", - "task_id": "DEV-000163", + "task_file": "dev/tasks/DEV-000164-authorized-fleet-readiness.json", + "task_id": "DEV-000164", "task_revision": 1 } diff --git a/dev/tasks/DEV-000164-authorized-fleet-readiness.json b/dev/tasks/DEV-000164-authorized-fleet-readiness.json new file mode 100644 index 0000000..b468566 --- /dev/null +++ b/dev/tasks/DEV-000164-authorized-fleet-readiness.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000164", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "65a60d5f068e3d8f70733c4ee96bfbb9d9943d5e", + "risk_tier": "E", + "intent": "Make live model-fleet readiness use the existing file-backed CPU provider API key so FAST status reflects the authenticated provider path.", + "requirement_ids": ["WP-601", "Model fleet/failover"], + "allowed_change_scope": {"modules": ["cognitive-worker", "capability"], "paths": ["cmd/cognitive-worker/main.go", "cmd/cognitive-worker/fleet_status.go", "cmd/cognitive-worker/fleet_status_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000164-authorized-fleet-readiness.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 provider API key resolver", "fleet readiness response"], "may_change": ["fleet readiness probe request headers"]}, + "semantic_owner": "capability/cognitive-worker", + "canonical_state": [], + "state_transitions": ["authenticated provider probe -> truthful FAST readiness"], + "side_effects_and_privileges": ["uses existing in-memory resolved key only for the internal provider probe; no key logging or persistence"], + "persistence_and_migrations": ["none"], + "failure_retry_restart": ["401 remains a typed unavailable result when key is absent or rejected; GPU/Medium behavior unchanged"], + "assumptions": ["OpenAI-compatible local provider uses Bearer authentication as the existing model adapter does"], + "acceptance_scenarios": ["fleet readiness sends Bearer header when CPU key is configured", "FAST is ready when authenticated /models succeeds", "no Authorization header is sent without a key", "Lifeboat fallback remains unchanged"], + "required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production fleet readiness"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"] +}