From f3ddb45fe50859459050ff60f61bddf767b0f1fa Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 23 Aug 2026 19:01:18 +0300 Subject: [PATCH] fix(cognitive): resolve providers by stable instance identity --- cmd/cognitive-worker/gateway.go | 11 ++++++++- cmd/cognitive-worker/gateway_registry_test.go | 13 +++++++++++ dev/ACTIVE_TASK.json | 4 ++-- ...DEV-000239-stable-provider-resolution.json | 23 +++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 dev/tasks/DEV-000239-stable-provider-resolution.json diff --git a/cmd/cognitive-worker/gateway.go b/cmd/cognitive-worker/gateway.go index 26731c3..af1644f 100644 --- a/cmd/cognitive-worker/gateway.go +++ b/cmd/cognitive-worker/gateway.go @@ -298,12 +298,21 @@ func fallbackEligible(reason cognitive.FinishReason) bool { } func (g cognitiveGateway) provider(selected capability.ResolvedCognitiveBinding) advisoryProvider { + var byStableID advisoryProvider for _, registration := range g.providers[selected.Provider.Kind] { if registration.EndpointRef == selected.Provider.EndpointRef && registration.CredentialRef == selected.Provider.CredentialRef { return registration.Provider } + // Capability revisions can legitimately retain an older credential + // reference while the protected runtime has rotated or reloaded the + // current credential. ProviderInstanceID is the stable owner identity; + // use it after exact metadata matching so stale non-secret metadata does + // not force an unnecessary builtin fallback. + if byStableID == nil && registration.ProviderInstanceID == selected.Provider.ID && registration.Kind == selected.Provider.Kind { + byStableID = registration.Provider + } } - return nil + return byStableID } func writeGatewayResponse(w http.ResponseWriter, response cognitive.GatewayAdvisoryResponse) { diff --git a/cmd/cognitive-worker/gateway_registry_test.go b/cmd/cognitive-worker/gateway_registry_test.go index e351ba4..eeb5e65 100644 --- a/cmd/cognitive-worker/gateway_registry_test.go +++ b/cmd/cognitive-worker/gateway_registry_test.go @@ -88,6 +88,19 @@ func TestActiveRegistryBindingReachesProviderAdapter(t *testing.T) { } } +func TestProviderUsesStableInstanceWhenCredentialMetadataIsStale(t *testing.T) { + provider := &stubAdvisoryProvider{} + gateway := cognitiveGateway{providers: map[capability.ProviderKind][]providerRegistration{ + "provider-kind-1": {{ProviderInstanceID: "provider-1", Kind: "provider-kind-1", EndpointRef: "current-endpoint", CredentialRef: "current-credential", Provider: provider}}, + }} + selected := gatewaySelection() + selected.Provider.EndpointRef = "old-endpoint" + selected.Provider.CredentialRef = "" + if got := gateway.provider(selected); got != provider { + t.Fatalf("stable provider identity was not used for stale metadata") + } +} + func TestCanaryFlagIsForwardedToCapabilityResolver(t *testing.T) { registry := ®istryStub{err: errors.New("stop after resolve")} gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{}, now: time.Now, allowCanary: true} diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index eb785bf..a3e0ab6 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000238-local-fallback-evidence.json", - "task_id": "DEV-000238", + "task_file": "dev/tasks/DEV-000239-stable-provider-resolution.json", + "task_id": "DEV-000239", "task_revision": 1 } diff --git a/dev/tasks/DEV-000239-stable-provider-resolution.json b/dev/tasks/DEV-000239-stable-provider-resolution.json new file mode 100644 index 0000000..5bd432f --- /dev/null +++ b/dev/tasks/DEV-000239-stable-provider-resolution.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000239", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "6061eb5c13e46cec1e60fc104b022394f0d94dee", + "risk_tier": "E", + "intent": "Resolve a registered provider by its stable instance identity when a persisted Capability revision contains stale non-secret endpoint credential metadata, while preserving exact metadata matching first.", + "requirement_ids": ["model fleet/failover", "credential rotation compatibility", "local fallback attribution"], + "allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/gateway_registry_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000239-stable-provider-resolution.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**", "contracts/**"]}, + "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": ["Capability ResolvedCognitiveBinding", "protected provider registration"], "may_change": ["internal provider lookup fallback order"]}, + "semantic_owner": "Cognitive worker provider adapter boundary", + "canonical_state": ["no new durable state"], + "state_transitions": ["exact provider metadata match -> stable provider identity match -> no provider"], + "side_effects_and_privileges": ["provider lookup only; secret values remain in protected runtime"], + "persistence_and_migrations": ["none"], + "failure_retry_restart": ["missing provider remains explicit and uses existing builtin fallback"], + "assumptions": ["ProviderInstanceID is the stable Capability-owned provider identity"], + "acceptance_scenarios": ["stale credential metadata still reaches current registered adapter", "exact metadata match retains priority", "no provider remains nil"], + "required_tests": ["go test ./cmd/cognitive-worker/...", "go vet ./cmd/cognitive-worker/...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "git diff --check", "independent", "CI", "production health smoke"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"] +} -- 2.54.0