DEV-000046: add cognitive advisory contract #18

Merged
m merged 1 commits from codex/cognitive-advisory-contract-v1 into main 2026-08-16 17:05:52 +00:00
8 changed files with 333 additions and 2 deletions
+105
View File
@@ -0,0 +1,105 @@
package models
import (
"errors"
"fmt"
"unicode/utf8"
"git.cin.su/m/Assistant/contracts/cognitive"
)
const (
MaxAdvisoryMessageBytes = 16 * 1024
MaxAdvisoryExcerpts = 8
MaxAdvisoryExcerptBytes = 8 * 1024
MaxAdvisoryContextBytes = 48 * 1024
MaxAdvisoryResponseBytes = 16 * 1024
)
var ErrInvalidAdvisory = errors.New("invalid creator advisory")
func ValidateAdvisoryRequest(request cognitive.AdvisoryRequest) error {
if request.Version != cognitive.CreatorAdvisoryV1 || request.RequestID == "" || request.ActorID == "" || request.PrincipalID == "" || request.ScopeID == "" || request.ProviderBindingID == "" || request.ProviderBindingRevision <= 0 {
return fmt.Errorf("%w: missing or unsupported identity/binding fields", ErrInvalidAdvisory)
}
if request.Message == "" || !utf8.ValidString(request.Message) || len(request.Message) > MaxAdvisoryMessageBytes {
return fmt.Errorf("%w: message", ErrInvalidAdvisory)
}
if request.Locale == "" || len(request.Locale) > 32 || !utf8.ValidString(request.Locale) || request.MaxOutputBytes <= 0 || request.MaxOutputBytes > MaxAdvisoryResponseBytes {
return fmt.Errorf("%w: locale or response limit", ErrInvalidAdvisory)
}
if len(request.Context) > MaxAdvisoryExcerpts {
return fmt.Errorf("%w: too many context excerpts", ErrInvalidAdvisory)
}
seen := make(map[cognitive.ContextExcerptID]struct{}, len(request.Context))
total := 0
for _, excerpt := range request.Context {
if excerpt.ID == "" || excerpt.SourceKind == "" || excerpt.SourceID == "" || excerpt.Revision <= 0 || excerpt.Text == "" || !utf8.ValidString(excerpt.Text) || len(excerpt.Text) > MaxAdvisoryExcerptBytes {
return fmt.Errorf("%w: context excerpt", ErrInvalidAdvisory)
}
if excerpt.Sensitivity != cognitive.SensitivityPublic && excerpt.Sensitivity != cognitive.SensitivityPrivateAllowed {
return fmt.Errorf("%w: context sensitivity", ErrInvalidAdvisory)
}
if _, exists := seen[excerpt.ID]; exists {
return fmt.Errorf("%w: duplicate context excerpt", ErrInvalidAdvisory)
}
seen[excerpt.ID] = struct{}{}
total += len(excerpt.Text)
}
if total > MaxAdvisoryContextBytes {
return fmt.Errorf("%w: total context", ErrInvalidAdvisory)
}
return nil
}
func ValidateAdvisoryResponse(request cognitive.AdvisoryRequest, response cognitive.AdvisoryResponse) error {
if response.Version != cognitive.CreatorAdvisoryV1 || response.RequestID != request.RequestID || response.ProviderBindingID != request.ProviderBindingID || response.ProviderBindingRevision != request.ProviderBindingRevision {
return fmt.Errorf("%w: response identity", ErrInvalidAdvisory)
}
if !validFinishReason(response.FinishReason) || !utf8.ValidString(response.AdvisoryText) || len(response.AdvisoryText) > request.MaxOutputBytes {
return fmt.Errorf("%w: response content", ErrInvalidAdvisory)
}
if response.FinishReason == cognitive.FinishCompleted && response.AdvisoryText == "" {
return fmt.Errorf("%w: completed response is empty", ErrInvalidAdvisory)
}
if response.FinishReason != cognitive.FinishCompleted && response.AdvisoryText != "" {
return fmt.Errorf("%w: failure response contains advisory text", ErrInvalidAdvisory)
}
if response.FinishReason != cognitive.FinishCompleted && len(response.ReferencedExcerptIDs) != 0 {
return fmt.Errorf("%w: failure response contains citations", ErrInvalidAdvisory)
}
allowed := make(map[cognitive.ContextExcerptID]struct{}, len(request.Context))
for _, excerpt := range request.Context {
allowed[excerpt.ID] = struct{}{}
}
seen := make(map[cognitive.ContextExcerptID]struct{}, len(response.ReferencedExcerptIDs))
for _, id := range response.ReferencedExcerptIDs {
if _, ok := allowed[id]; !ok {
return fmt.Errorf("%w: unknown citation", ErrInvalidAdvisory)
}
if _, ok := seen[id]; ok {
return fmt.Errorf("%w: duplicate citation", ErrInvalidAdvisory)
}
seen[id] = struct{}{}
}
if response.Usage != nil && (response.Usage.InputUnits < 0 || response.Usage.OutputUnits < 0) {
return fmt.Errorf("%w: negative usage", ErrInvalidAdvisory)
}
return nil
}
func DeterministicAdvisoryUnavailable(request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
if err := ValidateAdvisoryRequest(request); err != nil {
return cognitive.AdvisoryResponse{}, err
}
return cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, ProviderBindingID: request.ProviderBindingID, ProviderBindingRevision: request.ProviderBindingRevision, FinishReason: cognitive.FinishProviderUnavailable}, nil
}
func validFinishReason(reason cognitive.FinishReason) bool {
switch reason {
case cognitive.FinishCompleted, cognitive.FinishLengthLimit, cognitive.FinishContentRejected, cognitive.FinishProviderUnavailable, cognitive.FinishTimeout, cognitive.FinishInvalidProviderResponse:
return true
default:
return false
}
}
+38
View File
@@ -0,0 +1,38 @@
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/cognitive"
)
func TestDeterministicAdvisoryRejectsUnknownJSONField(t *testing.T) {
body := `{"Version":"creator-advisory-v1","Unexpected":"provider override"}`
request := httptest.NewRequest(http.MethodPost, "/v1/advisories/deterministic", strings.NewReader(body))
response := httptest.NewRecorder()
deterministicAdvisory(response, request)
if response.Code != http.StatusBadRequest {
t.Fatalf("status=%d, want 400", response.Code)
}
}
func TestDeterministicAdvisoryReturnsUnavailableContract(t *testing.T) {
body := `{"Version":"creator-advisory-v1","RequestID":"request-1","ActorID":"actor","PrincipalID":"principal","ScopeID":"scope","Message":"help","Context":[],"Locale":"en","MaxOutputBytes":4096,"ProviderBindingID":"fake-binding","ProviderBindingRevision":1}`
request := httptest.NewRequest(http.MethodPost, "/v1/advisories/deterministic", strings.NewReader(body))
response := httptest.NewRecorder()
deterministicAdvisory(response, request)
if response.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
var advisory cognitive.AdvisoryResponse
if err := json.NewDecoder(response.Body).Decode(&advisory); err != nil {
t.Fatal(err)
}
if advisory.FinishReason != cognitive.FinishProviderUnavailable || advisory.AdvisoryText != "" {
t.Fatalf("unexpected response: %+v", advisory)
}
}
+25
View File
@@ -3,7 +3,9 @@ package main
import (
"encoding/json"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/runtime/health"
"io"
"log"
"net/http"
"os"
@@ -17,12 +19,35 @@ func main() {
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
mux.HandleFunc("POST /v1/advisories/deterministic", deterministicAdvisory)
log.Printf("cognitive-worker listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
}
}
func deterministicAdvisory(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 80*1024)
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
var request cognitive.AdvisoryRequest
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid advisory request", http.StatusBadRequest)
return
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
http.Error(w, "invalid advisory request", http.StatusBadRequest)
return
}
response, err := models.DeterministicAdvisoryUnavailable(request)
if err != nil {
http.Error(w, "invalid advisory request", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response)
}
func deterministicProposal(w http.ResponseWriter, r *http.Request) {
var request models.DeterministicRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
+2
View File
@@ -0,0 +1,2 @@
// Package cognitive defines provider-neutral, non-canonical cognitive gateway contracts.
package cognitive
+71
View File
@@ -0,0 +1,71 @@
package cognitive
import (
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/ids"
)
type RequestID ids.ID
type ContextExcerptID ids.ID
type ContractVersion string
type Sensitivity string
type FinishReason string
const (
CreatorAdvisoryV1 ContractVersion = "creator-advisory-v1"
SensitivityPublic Sensitivity = "PUBLIC"
SensitivityPrivateAllowed Sensitivity = "PRIVATE_ALLOWED"
SensitivitySecret Sensitivity = "SECRET"
SensitivityUnknown Sensitivity = "UNKNOWN"
FinishCompleted FinishReason = "COMPLETED"
FinishLengthLimit FinishReason = "LENGTH_LIMIT"
FinishContentRejected FinishReason = "CONTENT_REJECTED"
FinishProviderUnavailable FinishReason = "PROVIDER_UNAVAILABLE"
FinishTimeout FinishReason = "TIMEOUT"
FinishInvalidProviderResponse FinishReason = "INVALID_PROVIDER_RESPONSE"
)
type ContextExcerpt struct {
ID ContextExcerptID
SourceKind string
SourceID string
Revision int64
Sensitivity Sensitivity
Text string
}
// AdvisoryRequest is assembled by a trusted gateway. Actor, Principal, Scope
// and Binding fields must never be accepted as browser-selected authority.
type AdvisoryRequest struct {
Version ContractVersion
RequestID RequestID
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
Message string
Context []ContextExcerpt
Locale string
MaxOutputBytes int
ProviderBindingID capability.CapabilityBindingID
ProviderBindingRevision int64
}
type ProviderUsage struct {
InputUnits int64
OutputUnits int64
}
type AdvisoryResponse struct {
Version ContractVersion
RequestID RequestID
ProviderBindingID capability.CapabilityBindingID
ProviderBindingRevision int64
AdvisoryText string
ReferencedExcerptIDs []ContextExcerptID
FinishReason FinishReason
Usage *ProviderUsage
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/ARCH-000008-accept-cognitive-advisory-boundary.json",
"task_id": "ARCH-000008",
"task_file": "dev/tasks/DEV-000046-cognitive-advisory-contract.json",
"task_id": "DEV-000046",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000046",
"task_revision": 1,
"architecture_revision": "ADR-0006",
"base_repo_revision": "b896d218f80af277ccf4e15d28fbffb1c7bad937",
"risk_tier": "E",
"intent": "Implement the provider-neutral creator-advisory-v1 contract, fail-closed validation, deterministic unavailable fake, and cognitive-worker HTTP contract without provider egress, persistence, tools or canonical authority.",
"requirement_ids": ["ADR-0006", "FD-001", "FD-009", "INV-EPI-001"],
"allowed_change_scope": {"modules": [], "paths": ["contracts/cognitive/**", "adapters/models/**", "cmd/cognitive-worker/**", "tests/cognitive_advisory/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000046-cognitive-advisory-contract.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "ci/**", ".gitea/**", "contracts/authority/**", "contracts/commit/**", "modules/**", "adapters/postgres/**", "database/**", "deploy/**", "security/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0},
"contracts": {"consume": ["contracts.identity@v1", "contracts.authority@v1", "contracts.capability@v1", "ADR-0006"], "may_change": ["new contracts.cognitive creator-advisory-v1"]},
"semantic_owner": "none-cognitive-gateway",
"canonical_state": [],
"state_transitions": [],
"side_effects_and_privileges": ["none; deterministic endpoint performs no network call or mutation"],
"persistence_and_migrations": [],
"failure_retry_restart": ["invalid or secret context rejected before adapter", "fake reports PROVIDER_UNAVAILABLE without fabricated text", "request loss creates no obligation and no retry"],
"assumptions": ["real provider remains unselected and outbound egress disabled", "docker-test remains prohibited"],
"acceptance_scenarios": ["valid typed request serializes without provider types", "secret/unknown/oversized/duplicate context is rejected", "response citations outside supplied excerpts are rejected", "unknown JSON fields are rejected", "fake returns explicit PROVIDER_UNAVAILABLE and no action/tool fields", "existing deterministic proposal fake remains compatible"],
"required_tests": ["unit", "HTTP contract", "adversarial validation", "go test", "go vet", "architecture preflight", "exact diff scope", "CI"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
+67
View File
@@ -0,0 +1,67 @@
package cognitive_advisory_test
import (
"encoding/json"
"strings"
"testing"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/ids"
)
func validRequest() cognitive.AdvisoryRequest {
return cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(ids.ID("request-1")), ActorID: identity.ActorID(ids.ID("actor")), PrincipalID: identity.PrincipalID(ids.ID("principal")), ScopeID: authority.ScopeID(ids.ID("scope")), Message: "What should I review?", Context: []cognitive.ContextExcerpt{{ID: cognitive.ContextExcerptID(ids.ID("excerpt-1")), SourceKind: "prospective-item", SourceID: "item-1", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: "Review tomorrow"}}, Locale: "en", MaxOutputBytes: 4096, ProviderBindingID: capability.CapabilityBindingID(ids.ID("fake-binding")), ProviderBindingRevision: 1}
}
func TestDeterministicAdvisoryIsExplicitlyUnavailable(t *testing.T) {
request := validRequest()
response, err := models.DeterministicAdvisoryUnavailable(request)
if err != nil {
t.Fatal(err)
}
if response.FinishReason != cognitive.FinishProviderUnavailable || response.AdvisoryText != "" {
t.Fatalf("unexpected fake response: %+v", response)
}
if err := models.ValidateAdvisoryResponse(request, response); err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(response)
if err != nil {
t.Fatal(err)
}
for _, forbidden := range []string{"ActionProposal", "ToolCall", "AuthorityGrant"} {
if strings.Contains(string(raw), forbidden) {
t.Fatalf("response contains forbidden field %s", forbidden)
}
}
}
func TestAdvisoryRejectsUnsafeContextAndCitations(t *testing.T) {
tests := []struct {
name string
mutate func(*cognitive.AdvisoryRequest)
}{
{"secret", func(r *cognitive.AdvisoryRequest) { r.Context[0].Sensitivity = cognitive.SensitivitySecret }},
{"unknown", func(r *cognitive.AdvisoryRequest) { r.Context[0].Sensitivity = cognitive.SensitivityUnknown }},
{"duplicate", func(r *cognitive.AdvisoryRequest) { r.Context = append(r.Context, r.Context[0]) }},
{"oversized-message", func(r *cognitive.AdvisoryRequest) { r.Message = strings.Repeat("x", models.MaxAdvisoryMessageBytes+1) }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
request := validRequest()
test.mutate(&request)
if err := models.ValidateAdvisoryRequest(request); err == nil {
t.Fatal("unsafe request accepted")
}
})
}
request := validRequest()
response := cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, ProviderBindingID: request.ProviderBindingID, ProviderBindingRevision: 1, AdvisoryText: "answer", ReferencedExcerptIDs: []cognitive.ContextExcerptID{cognitive.ContextExcerptID(ids.ID("not-supplied"))}, FinishReason: cognitive.FinishCompleted}
if err := models.ValidateAdvisoryResponse(request, response); err == nil {
t.Fatal("unknown citation accepted")
}
}