From c41634a0657739a18ad89bbe19e31017784f324e Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 23 Aug 2026 15:54:30 +0300 Subject: [PATCH] feat: expose controlled provider discovery status --- cmd/cognitive-worker/discovery.go | 44 ++++++++++++++++ cmd/cognitive-worker/discovery_test.go | 52 +++++++++++++++++++ cmd/cognitive-worker/main.go | 1 + dev/ACTIVE_TASK.json | 4 +- .../DEV-000228-controlled-discovery.json | 23 ++++++++ docs/PROJECT_STATUS.md | 2 +- 6 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 cmd/cognitive-worker/discovery_test.go create mode 100644 dev/tasks/DEV-000228-controlled-discovery.json diff --git a/cmd/cognitive-worker/discovery.go b/cmd/cognitive-worker/discovery.go index 9ca43fe..c873e35 100644 --- a/cmd/cognitive-worker/discovery.go +++ b/cmd/cognitive-worker/discovery.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "net/http" + "sort" "time" "git.cin.su/m/Assistant/contracts/capability" @@ -17,6 +18,10 @@ type discoveryService struct { } func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Path == "/v1/provider-discovery/status" { + s.statusHandler(w, r) + return + } registration, ok := s.providers[r.PathValue("provider")] if !ok || registration.Discovery == nil { http.Error(w, "provider discovery not found", http.StatusNotFound) @@ -35,3 +40,42 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(result) } + +type discoveryStatus struct { + Provider string `json:"provider"` + ProviderKind capability.ProviderKind `json:"provider_kind"` + Health capability.HealthState `json:"health"` + FailureClass capability.CognitiveFailureType `json:"failure_class,omitempty"` + ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"` + Models []discoveredModelStatus `json:"models"` +} + +type discoveredModelStatus struct { + ProviderModelID capability.ProviderModelID `json:"provider_model_id"` + Fingerprint capability.ModelFingerprint `json:"fingerprint,omitempty"` + Capabilities []capability.ModelCapability `json:"capabilities,omitempty"` +} + +func (s discoveryService) statusHandler(w http.ResponseWriter, r *http.Request) { + providers := make([]string, 0, len(s.providers)) + for name := range s.providers { + providers = append(providers, name) + } + sort.Strings(providers) + result := make([]discoveryStatus, 0, len(providers)) + for _, name := range providers { + registration := s.providers[name] + status := discoveryStatus{Provider: name, ProviderKind: registration.Kind, Health: capability.HealthUnknown, ObservedAtUnixMillis: s.now().UnixMilli(), Models: []discoveredModelStatus{}} + if registration.Discovery != nil { + models, probe := registration.Discovery.Discover(r.Context()) + status.Health = probe.Health + status.FailureClass = capability.CognitiveFailureType(probe.Failure) + for _, model := range models { + status.Models = append(status.Models, discoveredModelStatus{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, Capabilities: model.Capabilities}) + } + } + result = append(result, status) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(result) +} diff --git a/cmd/cognitive-worker/discovery_test.go b/cmd/cognitive-worker/discovery_test.go new file mode 100644 index 0000000..a960579 --- /dev/null +++ b/cmd/cognitive-worker/discovery_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "git.cin.su/m/Assistant/adapters/models" + "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/contracts/cognitive" + "git.cin.su/m/Assistant/contracts/identity" +) + +type discoveryTestProvider struct{} + +func (discoveryTestProvider) Probe(context.Context) models.ProviderProbe { + return models.ProviderProbe{Health: capability.HealthAvailable} +} +func (discoveryTestProvider) Discover(context.Context) ([]models.DiscoveredModel, models.ProviderProbe) { + return []models.DiscoveredModel{{ProviderModelID: "local-model", Fingerprint: "fingerprint-1", Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}}}, models.ProviderProbe{Health: capability.HealthAvailable} +} +func (discoveryTestProvider) Advise(context.Context, cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) { + return cognitive.AdvisoryResponse{}, nil +} + +func TestDiscoveryStatusIsReadOnlyAndDoesNotExposeCredentialMetadata(t *testing.T) { + service := discoveryService{ + providers: map[string]providerRegistration{"cpu": {ProviderInstanceID: "provider-1", Kind: "openai-compatible-chat-v1", CredentialRef: "credential://must-not-be-returned", Discovery: discoveryTestProvider{}}}, + owner: identity.PrincipalID("owner-1"), + now: func() time.Time { return time.UnixMilli(1234) }, + } + response := httptest.NewRecorder() + service.handler(response, httptest.NewRequest(http.MethodGet, "/v1/provider-discovery/status", nil)) + if response.Code != http.StatusOK { + t.Fatalf("status=%d", response.Code) + } + body := response.Body.String() + var result []discoveryStatus + if err := json.NewDecoder(strings.NewReader(body)).Decode(&result); err != nil { + t.Fatal(err) + } + if len(result) != 1 || result[0].Provider != "cpu" || result[0].Health != capability.HealthAvailable || len(result[0].Models) != 1 || result[0].ObservedAtUnixMillis != 1234 { + t.Fatalf("unexpected discovery status: %+v", result) + } + if strings.Contains(body, "must-not-be-returned") || strings.Contains(body, "credential://") { + t.Fatalf("credential metadata leaked in discovery status: %s", body) + } +} diff --git a/cmd/cognitive-worker/main.go b/cmd/cognitive-worker/main.go index b8fcf34..231eeec 100644 --- a/cmd/cognitive-worker/main.go +++ b/cmd/cognitive-worker/main.go @@ -169,6 +169,7 @@ func main() { mux.HandleFunc("POST /v1/task-evaluations/execute", gateway.taskEvaluationHandler) discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now} mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler) + mux.HandleFunc("GET /v1/provider-discovery/status", discovery.statusHandler) refreshProviderDiscoveries(resolver, discovery) log.Printf("cognitive-worker listening on %s", addr) if err := http.ListenAndServe(addr, mux); err != nil { diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index fa0f1e4..8f5a4ef 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000227-local-executor.json", - "task_id": "DEV-000227", + "task_file": "dev/tasks/DEV-000228-controlled-discovery.json", + "task_id": "DEV-000228", "task_revision": 1 } diff --git a/dev/tasks/DEV-000228-controlled-discovery.json b/dev/tasks/DEV-000228-controlled-discovery.json new file mode 100644 index 0000000..797a1f9 --- /dev/null +++ b/dev/tasks/DEV-000228-controlled-discovery.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000228", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "2609f31e59d3914cdf4adcf23ec618705277d12a", + "risk_tier": "E", + "intent": "Expose a read-only, provider-neutral discovery status view so Domovoy can inspect available optional providers and models before requesting qualification, without exposing credential metadata or activating anything.", + "requirement_ids": ["controlled discovery", "provider qualification visibility", "optional external tools"], + "allowed_change_scope": {"modules": ["cognitive-worker", "operations-status"], "paths": ["cmd/cognitive-worker/discovery.go", "cmd/cognitive-worker/discovery_test.go", "cmd/cognitive-worker/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000228-controlled-discovery.json", "docs/PROJECT_STATUS.md"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "contracts/**", "database/**", "deploy/**", "LLM-local/**"]}, + "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 LocalProvider discovery probes", "existing capability model metadata"], "may_change": ["read-only cognitive-worker discovery status projection"]}, + "semantic_owner": "cognitive-worker observation projection", + "canonical_state": ["existing Capability discovery registration remains the canonical persisted state"], + "state_transitions": ["provider probe -> observed discovery status", "discovery registration remains DISCOVERED/INACTIVE until qualification and promotion"], + "side_effects_and_privileges": ["read-only provider probe on explicit status request; no activation, mutation or credential disclosure"], + "persistence_and_migrations": ["none"], + "failure_retry_restart": ["unavailable provider is reported with health/failure state; persisted discovery remains available through existing owner"], + "assumptions": ["provider discovery adapters already enforce bounded requests", "qualification remains a separate task-evaluation and promotion step"], + "acceptance_scenarios": ["status lists registered providers deterministically", "model inventory and probe health are visible", "credential references are not returned", "status does not activate or persist a provider", "unknown provider remains absent or unavailable"], + "required_tests": ["go test ./cmd/cognitive-worker/...", "go vet ./cmd/cognitive-worker/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production status smoke"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"] +} diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 9cf0934..b32b40c 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -24,7 +24,7 @@ - `COMPLETION_MATRIX = docs/evidence/audit-completion-matrix-2026-08-23-v3.json`; all seven objective areas are mapped to current repository/production proof, with external bridge, GPU promotion/quality and deep-acceptance gaps explicit. -- `ACTIVE_TASK = dev/tasks/DEV-000227-local-executor.json revision 1`; the existing task-evaluation path is now being exposed as a bounded local executor while unsupported work remains durably waiting. +- `ACTIVE_TASK = dev/tasks/DEV-000228-controlled-discovery.json revision 1`; the cognitive worker now exposes a read-only provider/model discovery status view. Discovery does not qualify, activate or authorize a provider; those remain separate evidence-gated steps. - `LINEAGE_DECISION = docs/GOVERNANCE_LINEAGE_DECISION_2026-08-22.md`; historical packets remain provenance and are not current authority. - `REGISTRY_STATUS = YAML and JSON semantic mirrors agree` - `PRODUCTION_DOMAIN = docker.cin.su`; shared docker-test is not used for this project. -- 2.54.0