feat: gate OpenAI advisory runtime
CI / test (push) Successful in 11s
CI / test (pull_request) Successful in 11s

This commit is contained in:
m
2026-08-16 20:47:01 +03:00
parent 0ee9e4a8a1
commit 417641dbf4
5 changed files with 220 additions and 12 deletions
+104
View File
@@ -1,15 +1,32 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/cognitive"
)
type stubAdvisoryProvider struct {
called bool
request cognitive.AdvisoryRequest
response cognitive.AdvisoryResponse
err error
}
func (s *stubAdvisoryProvider) Advise(_ context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
s.called = true
s.request = request
return s.response, s.err
}
func TestDeterministicAdvisoryRejectsUnknownJSONField(t *testing.T) {
body := `{"Version":"creator-advisory-v1","Unexpected":"provider override"}`
request := httptest.NewRequest(http.MethodPost, "/v1/advisories/deterministic", strings.NewReader(body))
@@ -36,3 +53,90 @@ func TestDeterministicAdvisoryReturnsUnavailableContract(t *testing.T) {
t.Fatalf("unexpected response: %+v", advisory)
}
}
func TestOpenAIAdvisoryIsUnavailableBeforeBodyParsingWhenDisabled(t *testing.T) {
request := httptest.NewRequest(http.MethodPost, "/v1/advisories/openai", strings.NewReader(`not-json`))
response := httptest.NewRecorder()
openAIAdvisory(nil)(response, request)
if response.Code != http.StatusServiceUnavailable {
t.Fatalf("status=%d, want 503", response.Code)
}
}
func TestOpenAIAdvisoryStrictlyDecodesBeforeProvider(t *testing.T) {
provider := &stubAdvisoryProvider{}
request := httptest.NewRequest(http.MethodPost, "/v1/advisories/openai", strings.NewReader(`{"Version":"creator-advisory-v1","Unexpected":"provider override"}`))
response := httptest.NewRecorder()
openAIAdvisory(provider)(response, request)
if response.Code != http.StatusBadRequest || provider.called {
t.Fatalf("status=%d provider_called=%t", response.Code, provider.called)
}
}
func TestOpenAIAdvisoryReturnsProviderNeutralResponse(t *testing.T) {
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{
Version: cognitive.CreatorAdvisoryV1,
RequestID: "request-1",
ProviderBindingID: models.OpenAIAdvisoryBindingID,
ProviderBindingRevision: models.OpenAIAdvisoryBindingRevision,
AdvisoryText: "advice",
FinishReason: cognitive.FinishCompleted,
}}
body := `{"Version":"creator-advisory-v1","RequestID":"request-1","ActorID":"actor","PrincipalID":"principal","ScopeID":"scope","Message":"help","Context":[],"Locale":"en","MaxOutputBytes":4096,"ProviderBindingID":"openai-responses-gpt-5.4-mini-2026-03-17","ProviderBindingRevision":1}`
request := httptest.NewRequest(http.MethodPost, "/v1/advisories/openai", strings.NewReader(body))
response := httptest.NewRecorder()
openAIAdvisory(provider)(response, request)
if response.Code != http.StatusOK || !provider.called || provider.request.Message != "help" {
t.Fatalf("status=%d provider_called=%t", response.Code, provider.called)
}
var advisory cognitive.AdvisoryResponse
if err := json.NewDecoder(response.Body).Decode(&advisory); err != nil {
t.Fatal(err)
}
if advisory.AdvisoryText != "advice" || advisory.FinishReason != cognitive.FinishCompleted {
t.Fatalf("unexpected response: %+v", advisory)
}
}
func TestLoadOpenAIAdvisoryProviderDefaultsDisabled(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "")
provider, err := loadOpenAIAdvisoryProvider("")
if err != nil || provider != nil {
t.Fatalf("provider=%v error=%v", provider, err)
}
}
func TestLoadOpenAIAdvisoryProviderRejectsDirectEnvironmentCredential(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "must-not-be-read")
if _, err := loadOpenAIAdvisoryProvider(""); err == nil || strings.Contains(err.Error(), "must-not-be-read") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoadOpenAIAdvisoryProviderUsesBoundedSecretFile(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "")
directory := t.TempDir()
validPath := filepath.Join(directory, "valid")
if err := os.WriteFile(validPath, []byte("test-project-key\n"), 0o600); err != nil {
t.Fatal(err)
}
provider, err := loadOpenAIAdvisoryProvider(validPath)
if err != nil || provider == nil {
t.Fatalf("provider=%v error=%v", provider, err)
}
for name, content := range map[string][]byte{
"empty": nil,
"embedded whitespace": []byte("invalid key"),
"oversized": []byte(strings.Repeat("x", maxOpenAISecretBytes+1)),
} {
t.Run(name, func(t *testing.T) {
path := filepath.Join(directory, name)
if err := os.WriteFile(path, content, 0o600); err != nil {
t.Fatal(err)
}
if _, err := loadOpenAIAdvisoryProvider(path); err == nil {
t.Fatal("invalid secret file was accepted")
}
})
}
}
+89 -10
View File
@@ -1,7 +1,11 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/runtime/health"
@@ -11,15 +15,26 @@ import (
"os"
)
const maxOpenAISecretBytes = 8 << 10
type advisoryProvider interface {
Advise(context.Context, cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error)
}
func main() {
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8083"
}
openAIProvider, err := loadOpenAIAdvisoryProvider(os.Getenv("OPENAI_API_KEY_FILE"))
if err != nil {
log.Fatal(err)
}
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)
mux.HandleFunc("POST /v1/advisories/openai", openAIAdvisory(openAIProvider))
log.Printf("cognitive-worker listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
@@ -27,16 +42,8 @@ func main() {
}
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)
request, ok := decodeAdvisoryRequest(w, r)
if !ok {
return
}
response, err := models.DeterministicAdvisoryUnavailable(request)
@@ -48,6 +55,78 @@ func deterministicAdvisory(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(response)
}
func openAIAdvisory(provider advisoryProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if provider == nil {
http.Error(w, "provider unavailable", http.StatusServiceUnavailable)
return
}
request, ok := decodeAdvisoryRequest(w, r)
if !ok {
return
}
response, err := provider.Advise(r.Context(), 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 decodeAdvisoryRequest(w http.ResponseWriter, r *http.Request) (cognitive.AdvisoryRequest, bool) {
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 cognitive.AdvisoryRequest{}, false
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
http.Error(w, "invalid advisory request", http.StatusBadRequest)
return cognitive.AdvisoryRequest{}, false
}
return request, true
}
func loadOpenAIAdvisoryProvider(secretPath string) (advisoryProvider, error) {
if os.Getenv("OPENAI_API_KEY") != "" {
return nil, errors.New("direct OpenAI credential environment is forbidden; use OPENAI_API_KEY_FILE")
}
if secretPath == "" {
return nil, nil
}
file, err := os.Open(secretPath)
if err != nil {
return nil, fmt.Errorf("open OpenAI credential file: %w", err)
}
defer file.Close()
raw, err := io.ReadAll(io.LimitReader(file, maxOpenAISecretBytes+1))
if err != nil {
return nil, fmt.Errorf("read OpenAI credential file: %w", err)
}
defer clear(raw)
if len(raw) > maxOpenAISecretBytes {
return nil, errors.New("OpenAI credential file is oversized")
}
key := bytes.TrimSpace(raw)
if len(key) == 0 {
return nil, errors.New("OpenAI credential file is empty")
}
for _, character := range key {
if character < 0x21 || character > 0x7e {
return nil, errors.New("OpenAI credential file contains invalid characters")
}
}
provider, err := models.NewOpenAIAdvisoryAdapter(key, nil)
if err != nil {
return nil, errors.New("OpenAI credential file is invalid")
}
return provider, nil
}
func deterministicProposal(w http.ResponseWriter, r *http.Request) {
var request models.DeterministicRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {