diff --git a/cmd/cognitive-worker/gateway.go b/cmd/cognitive-worker/gateway.go index fc96ba7..6504b5b 100644 --- a/cmd/cognitive-worker/gateway.go +++ b/cmd/cognitive-worker/gateway.go @@ -34,9 +34,11 @@ type providerRegistration struct { } type cognitiveGateway struct { - resolver bindingResolver - providers map[capability.ProviderKind][]providerRegistration - now func() time.Time + resolver bindingResolver + providers map[capability.ProviderKind][]providerRegistration + now func() time.Time + allowCanary bool + canaryMaxOutputBytes int } func (g cognitiveGateway) policyHandler(w http.ResponseWriter, r *http.Request) { @@ -44,7 +46,7 @@ func (g cognitiveGateway) policyHandler(w http.ResponseWriter, r *http.Request) if !decodeJSON(w, r, 8<<10, &request) || request.PrincipalID == "" { return } - requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, MaxOutputBytes: 1} + requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, MaxOutputBytes: 1} selected, err := g.resolver.Resolve(r.Context(), requirement) policy := cognitive.GatewayAdvisoryPolicy{} if err == nil && selected.Binding.DataPolicy != capability.DataPolicyLocalOnly { @@ -65,7 +67,11 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request http.Error(w, "invalid advisory request", http.StatusBadRequest) return } - requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(request.MaxOutputBytes)} + maxOutputBytes := request.MaxOutputBytes + if g.allowCanary && g.canaryMaxOutputBytes > 0 && maxOutputBytes > g.canaryMaxOutputBytes { + maxOutputBytes = g.canaryMaxOutputBytes + } + requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes)} selected, err := g.resolver.Resolve(r.Context(), requirement) if err != nil { writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable}) @@ -80,7 +86,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable}) return } - invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: request.MaxOutputBytes, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID} + invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID} started := g.now() response, invokeErr := provider.Advise(r.Context(), invocation) if invokeErr != nil { diff --git a/cmd/cognitive-worker/gateway_registry_test.go b/cmd/cognitive-worker/gateway_registry_test.go index 23595ee..e2aeb74 100644 --- a/cmd/cognitive-worker/gateway_registry_test.go +++ b/cmd/cognitive-worker/gateway_registry_test.go @@ -87,6 +87,33 @@ func TestActiveRegistryBindingReachesProviderAdapter(t *testing.T) { } } +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} + request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())) + gateway.advisoryHandler(httptest.NewRecorder(), request) + if !registry.requirement.AllowCanary { + t.Fatal("server-side canary flag was not forwarded") + } + gateway.allowCanary = false + gateway.advisoryHandler(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))) + if registry.requirement.AllowCanary { + t.Fatal("canary was allowed by default") + } +} + +func TestCanaryOutputLimitIsClamped(t *testing.T) { + selected := gatewaySelection() + selected.Binding.DataPolicy = capability.DataPolicyLocalOnly + registry := ®istryStub{selected: selected} + provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, AdvisoryText: "ok", FinishReason: cognitive.FinishCompleted}} + gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"provider-kind-1": {{EndpointRef: "endpoint-1", CredentialRef: "credential-1", Provider: provider}}}, now: time.Now, allowCanary: true, canaryMaxOutputBytes: 4096} + gateway.advisoryHandler(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))) + if registry.requirement.MaxOutputBytes != 4096 || provider.request.MaxOutputBytes != 4096 { + t.Fatalf("canary limit not clamped: requirement=%d invocation=%d", registry.requirement.MaxOutputBytes, provider.request.MaxOutputBytes) + } +} + func TestNoActiveBindingKeepsGatewayAvailableWithoutInvocation(t *testing.T) { registry := ®istryStub{err: errors.New("no active binding")} provider := &stubAdvisoryProvider{} diff --git a/cmd/cognitive-worker/main.go b/cmd/cognitive-worker/main.go index c9a2313..e155671 100644 --- a/cmd/cognitive-worker/main.go +++ b/cmd/cognitive-worker/main.go @@ -58,7 +58,7 @@ func main() { kind := capability.ProviderKind(envOr("OPENAI_PROVIDER_KIND", "openai-responses-v1")) providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), Provider: openAIProvider}) } - gateway := cognitiveGateway{resolver: resolver, providers: providers, now: time.Now} + gateway := cognitiveGateway{resolver: resolver, providers: providers, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096} mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1}) mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal) mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler) diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index 7c5eb6e..36b0dc4 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -118,6 +118,7 @@ services: LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1 LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434 COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal + COGNITIVE_ALLOW_CANARY: "true" depends_on: [core-runtime] creator-web: diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 8772d3f..4c4e428 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000058-local-cognitive-canary.json", - "task_id": "DEV-000058", + "task_file": "dev/tasks/DEV-000059-local-cognitive-canary-observation.json", + "task_id": "DEV-000059", "task_revision": 1 } diff --git a/dev/tasks/DEV-000059-local-cognitive-canary-observation.json b/dev/tasks/DEV-000059-local-cognitive-canary-observation.json new file mode 100644 index 0000000..adaa6b5 --- /dev/null +++ b/dev/tasks/DEV-000059-local-cognitive-canary-observation.json @@ -0,0 +1,17 @@ +{ + "task_id":"DEV-000059","task_revision":1,"architecture_revision":"ADR-0008","base_repo_revision":"2a0aee007352e8427619b458ab907ac945f19a64","risk_tier":"E", + "intent":"Expose the exact validated local CPU canary to authenticated Creator advisory traffic behind a fail-closed server-side flag, collect typed usage/reliability observations, and retain immediate disable semantics without promoting the binding to ACTIVE.", + "requirement_ids":["INV-OWN-001","INV-EPI-001","ADR-0006","ADR-0008","LOCAL-COGNITIVE-CANARY-OBSERVATION"], + "allowed_change_scope":{"modules":["capability"],"paths":["cmd/cognitive-worker/**","deploy/**","tests/cognitive_canary/**","docs/evidence/**","docs/PROJECT_STATUS.md","dev/ACTIVE_TASK.json","dev/tasks/DEV-000059-local-cognitive-canary-observation.json"]}, + "forbidden_change_scope":{"paths":["architecture/**","contracts/**","modules/**","adapters/**","database/**","cmd/core-runtime/**","cmd/creator-web/**","security/**","ci/**",".gitea/**","cmd/devctl/**"]}, + "allowed_architecture_delta":{"new_cross_module_dependencies":0,"new_canonical_owners":0,"new_privileges":0,"new_third_party_dependencies":0,"new_provider_adapters":0}, + "contracts":{"consume":["creator-advisory-v1","CognitiveRequirement.AllowCanary","CognitiveUsageObservation"],"may_change":[]}, + "semantic_owner":"capability","canonical_state":["existing CognitiveUsageObservation only"],"state_transitions":["none; exact binding remains CANARY"], + "side_effects_and_privileges":["bounded local CPU advisory inference for authenticated Creator","no OpenAI","no tools","no external mutation"], + "persistence_and_migrations":["existing usage observation table only","immutable observation artifact","no migration","no event"], + "failure_retry_restart":["flag defaults false","disable by removing flag and recreating cognitive worker","no automatic retry beyond existing single bounded fallback","provider loss returns typed failure"], + "assumptions":["Creator continuation authorizes bounded canary exposure","docker-test remains prohibited"], + "acceptance_scenarios":["flag absent excludes canary","flag true permits exact canary","usage observations bind exact revision","all requests remain advisory-only","binding remains CANARY","OpenAI usage remains zero"], + "required_tests":["gateway flag unit tests","production authenticated smoke","usage DB evidence","disable path","go test ./...","go vet ./...","architecture sentinel","exact diff scope"], + "terminal_gap_states":["BLOCKED_BY_ARCHITECTURE_GAP","BLOCKED_BY_SPEC_GAP","BLOCKED_BY_MISSING_CAPABILITY","BLOCKED_BY_CONFLICT"] +} diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 7087695..8260204 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # Assistant project status -Audit date: 2026-08-17. Active implementation: DEV-000058 revision 1. +Audit date: 2026-08-17. Active implementation: DEV-000059 revision 1. ## Current milestone @@ -12,6 +12,7 @@ Audit date: 2026-08-17. Active implementation: DEV-000058 revision 1. - Discovery never activates a provider, model or binding. Until a later benchmark and explicit promotion task, all discovered bindings remain `INACTIVE` and production remains fully LLM-free. - DEV-000057 executed a bounded synthetic qualification corpus against all 11 current local model revisions. Four exact binding revisions demonstrated the minimum advisory threshold; seven remain unsupported. Qualification evidence does not change lifecycle or activation. - DEV-000058 promoted the exact CPU `Qwen2.5-3B-Instruct-Q4_K_M` lineage to `VALIDATED/CANARY` through an authenticated, evidence-gated, append-only CAS transition. Ordinary routing still excludes it; only an explicit `AllowCanary` request can select it. All other current bindings remain `INACTIVE`. +- DEV-000059 enables that exact canary behind a fail-closed cognitive-worker flag and a 4 KiB output clamp. Five authenticated synthetic Creator requests completed through the full production path with typed usage observations, zero failures and zero API cost. The binding remains `CANARY`, not `ACTIVE`. ## Delivered diff --git a/docs/evidence/local-cpu-canary-observation-2026-08-17-v1.json b/docs/evidence/local-cpu-canary-observation-2026-08-17-v1.json new file mode 100644 index 0000000..7521f8f --- /dev/null +++ b/docs/evidence/local-cpu-canary-observation-2026-08-17-v1.json @@ -0,0 +1,16 @@ +{ + "artifact_id":"local-cpu-canary-observation-2026-08-17-v1", + "binding_id":"binding-9e5e9779cd10b907c3695374", + "binding_revision":2865230063392044815, + "provider_model_id":"Qwen2.5-3B-Instruct-Q4_K_M", + "path":"authenticated Creator Web to Cognitive Gateway to local CPU adapter to Capability usage owner", + "synthetic_requests":5, + "completed":5, + "failed":0, + "latency_millis":{"minimum":2347,"maximum":4603,"average":3646}, + "usage":{"input_units":225,"output_units":271,"actual_api_cost_micros":0}, + "canary_max_output_bytes":4096, + "automatic_retry":false, + "openai_requests":0, + "activation":"CANARY" +}