Merge pull request 'feat: expose controlled provider discovery status' (#205) from codex/dev-000228-controlled-discovery into main
CI / test (push) Successful in 1m9s
CI / test (push) Successful in 1m9s
feat: expose controlled provider discovery status
This commit was merged in pull request #205.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user