Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
417641dbf4 | ||
|
|
0ee9e4a8a1 | ||
|
|
3b009033d6 | ||
|
|
00965f20e0 | ||
|
|
b61612f543 | ||
|
|
835c62cc1d |
@@ -0,0 +1,269 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
)
|
||||
|
||||
const (
|
||||
OpenAIAdvisoryBindingID capability.CapabilityBindingID = "openai-responses-gpt-5.4-mini-2026-03-17"
|
||||
OpenAIAdvisoryBindingRevision int64 = 1
|
||||
openAIResponsesURL = "https://api.openai.com/v1/responses"
|
||||
openAIModel = "gpt-5.4-mini-2026-03-17"
|
||||
openAIMaxOutputTokens = 4096
|
||||
openAIResponseLimitBytes = 1 << 20
|
||||
openAITimeout = 30 * time.Second
|
||||
openAIInstructions = "Provide non-canonical advisory text only. State uncertainty when appropriate. Never claim that an action occurred, never request or expose secrets, and never emit tool or executable instructions."
|
||||
)
|
||||
|
||||
type OpenAIAdvisoryAdapter struct {
|
||||
apiKey []byte
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewOpenAIAdvisoryAdapter(apiKey []byte, transport http.RoundTripper) (*OpenAIAdvisoryAdapter, error) {
|
||||
if len(apiKey) == 0 || bytes.IndexAny(apiKey, "\r\n") >= 0 {
|
||||
return nil, errors.New("invalid OpenAI credential")
|
||||
}
|
||||
if transport == nil {
|
||||
base, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
return nil, errors.New("default HTTP transport is unavailable")
|
||||
}
|
||||
cloned := base.Clone()
|
||||
cloned.Proxy = nil
|
||||
transport = cloned
|
||||
}
|
||||
return &OpenAIAdvisoryAdapter{
|
||||
apiKey: append([]byte(nil), apiKey...),
|
||||
client: &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: openAITimeout,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type openAIRequest struct {
|
||||
Model string `json:"model"`
|
||||
Instructions string `json:"instructions"`
|
||||
Input string `json:"input"`
|
||||
Store bool `json:"store"`
|
||||
Reasoning openAIReasoning `json:"reasoning"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
}
|
||||
|
||||
type openAIReasoning struct {
|
||||
Effort string `json:"effort"`
|
||||
}
|
||||
|
||||
type openAIResponse struct {
|
||||
Status string `json:"status"`
|
||||
Model string `json:"model"`
|
||||
Store *bool `json:"store"`
|
||||
Output []json.RawMessage `json:"output"`
|
||||
Usage *openAIUsage `json:"usage"`
|
||||
IncompleteDetails *openAIIncompleteDetails `json:"incomplete_details"`
|
||||
}
|
||||
|
||||
type openAIIncompleteDetails struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type openAIUsage struct {
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
}
|
||||
|
||||
type openAIOutputHeader struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type openAIMessage struct {
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
Content []json.RawMessage `json:"content"`
|
||||
}
|
||||
|
||||
type openAIContentHeader struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type openAIOutputText struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
Annotations []json.RawMessage `json:"annotations"`
|
||||
}
|
||||
|
||||
type openAIRefusal struct {
|
||||
Type string `json:"type"`
|
||||
Refusal string `json:"refusal"`
|
||||
}
|
||||
|
||||
type openAIReasoningItem struct {
|
||||
Type string `json:"type"`
|
||||
Summary []json.RawMessage `json:"summary"`
|
||||
}
|
||||
|
||||
func (a *OpenAIAdvisoryAdapter) Advise(ctx context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
|
||||
if err := ValidateAdvisoryRequest(request); err != nil {
|
||||
return cognitive.AdvisoryResponse{}, err
|
||||
}
|
||||
if request.ProviderBindingID != OpenAIAdvisoryBindingID || request.ProviderBindingRevision != OpenAIAdvisoryBindingRevision || len(request.Context) != 0 {
|
||||
return cognitive.AdvisoryResponse{}, fmt.Errorf("%w: OpenAI Mode A binding", ErrInvalidAdvisory)
|
||||
}
|
||||
payload, err := json.Marshal(openAIRequest{
|
||||
Model: openAIModel,
|
||||
Instructions: openAIInstructions,
|
||||
Input: request.Message,
|
||||
Store: false,
|
||||
Reasoning: openAIReasoning{Effort: "low"},
|
||||
MaxOutputTokens: openAIMaxOutputTokens,
|
||||
})
|
||||
if err != nil {
|
||||
return cognitive.AdvisoryResponse{}, err
|
||||
}
|
||||
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, openAIResponsesURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return cognitive.AdvisoryResponse{}, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", "Bearer "+string(a.apiKey))
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResponse, err := a.client.Do(httpRequest)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return advisoryFailure(request, cognitive.FinishTimeout), nil
|
||||
}
|
||||
var networkError net.Error
|
||||
if errors.As(err, &networkError) && networkError.Timeout() {
|
||||
return advisoryFailure(request, cognitive.FinishTimeout), nil
|
||||
}
|
||||
return advisoryFailure(request, cognitive.FinishProviderUnavailable), nil
|
||||
}
|
||||
defer httpResponse.Body.Close()
|
||||
if httpResponse.StatusCode != http.StatusOK {
|
||||
return advisoryFailure(request, cognitive.FinishProviderUnavailable), nil
|
||||
}
|
||||
if contentType := httpResponse.Header.Get("Content-Type"); contentType != "" && !strings.HasPrefix(strings.ToLower(contentType), "application/json") {
|
||||
return advisoryFailure(request, cognitive.FinishInvalidProviderResponse), nil
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(httpResponse.Body, openAIResponseLimitBytes+1))
|
||||
if err != nil || len(body) > openAIResponseLimitBytes {
|
||||
return advisoryFailure(request, cognitive.FinishInvalidProviderResponse), nil
|
||||
}
|
||||
var providerResponse openAIResponse
|
||||
if err := json.Unmarshal(body, &providerResponse); err != nil || providerResponse.Model != openAIModel || (providerResponse.Store != nil && *providerResponse.Store) || providerResponse.Usage == nil || providerResponse.Usage.InputTokens < 0 || providerResponse.Usage.OutputTokens < 0 {
|
||||
return advisoryFailure(request, cognitive.FinishInvalidProviderResponse), nil
|
||||
}
|
||||
if providerResponse.Status == "incomplete" && providerResponse.IncompleteDetails != nil && providerResponse.IncompleteDetails.Reason == "max_output_tokens" {
|
||||
return advisoryFailure(request, cognitive.FinishLengthLimit), nil
|
||||
}
|
||||
if providerResponse.Status != "completed" || providerResponse.IncompleteDetails != nil {
|
||||
return advisoryFailure(request, cognitive.FinishInvalidProviderResponse), nil
|
||||
}
|
||||
|
||||
text, refused, ok := extractOpenAIText(providerResponse.Output)
|
||||
if !ok {
|
||||
return advisoryFailure(request, cognitive.FinishInvalidProviderResponse), nil
|
||||
}
|
||||
if refused {
|
||||
return advisoryFailure(request, cognitive.FinishContentRejected), nil
|
||||
}
|
||||
response := cognitive.AdvisoryResponse{
|
||||
Version: cognitive.CreatorAdvisoryV1,
|
||||
RequestID: request.RequestID,
|
||||
ProviderBindingID: request.ProviderBindingID,
|
||||
ProviderBindingRevision: request.ProviderBindingRevision,
|
||||
AdvisoryText: text,
|
||||
FinishReason: cognitive.FinishCompleted,
|
||||
Usage: &cognitive.ProviderUsage{
|
||||
InputUnits: providerResponse.Usage.InputTokens,
|
||||
OutputUnits: providerResponse.Usage.OutputTokens,
|
||||
},
|
||||
}
|
||||
if err := ValidateAdvisoryResponse(request, response); err != nil {
|
||||
return advisoryFailure(request, cognitive.FinishInvalidProviderResponse), nil
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func extractOpenAIText(output []json.RawMessage) (string, bool, bool) {
|
||||
var text string
|
||||
refused := false
|
||||
messages := 0
|
||||
for _, raw := range output {
|
||||
var header openAIOutputHeader
|
||||
if json.Unmarshal(raw, &header) != nil {
|
||||
return "", false, false
|
||||
}
|
||||
switch header.Type {
|
||||
case "reasoning":
|
||||
var item openAIReasoningItem
|
||||
if json.Unmarshal(raw, &item) != nil || len(item.Summary) != 0 {
|
||||
return "", false, false
|
||||
}
|
||||
case "message":
|
||||
messages++
|
||||
if messages != 1 {
|
||||
return "", false, false
|
||||
}
|
||||
var message openAIMessage
|
||||
if json.Unmarshal(raw, &message) != nil || message.Role != "assistant" || message.Status != "completed" || len(message.Content) != 1 {
|
||||
return "", false, false
|
||||
}
|
||||
var contentHeader openAIContentHeader
|
||||
if json.Unmarshal(message.Content[0], &contentHeader) != nil {
|
||||
return "", false, false
|
||||
}
|
||||
switch contentHeader.Type {
|
||||
case "output_text":
|
||||
var content openAIOutputText
|
||||
if json.Unmarshal(message.Content[0], &content) != nil || content.Text == "" || !utf8.ValidString(content.Text) || len(content.Annotations) != 0 {
|
||||
return "", false, false
|
||||
}
|
||||
text = content.Text
|
||||
case "refusal":
|
||||
var content openAIRefusal
|
||||
if json.Unmarshal(message.Content[0], &content) != nil || content.Refusal == "" {
|
||||
return "", false, false
|
||||
}
|
||||
refused = true
|
||||
default:
|
||||
return "", false, false
|
||||
}
|
||||
default:
|
||||
return "", false, false
|
||||
}
|
||||
}
|
||||
if messages != 1 || (refused && text != "") || (!refused && text == "") {
|
||||
return "", false, false
|
||||
}
|
||||
return text, refused, true
|
||||
}
|
||||
|
||||
func advisoryFailure(request cognitive.AdvisoryRequest, reason cognitive.FinishReason) cognitive.AdvisoryResponse {
|
||||
return cognitive.AdvisoryResponse{
|
||||
Version: cognitive.CreatorAdvisoryV1,
|
||||
RequestID: request.RequestID,
|
||||
ProviderBindingID: request.ProviderBindingID,
|
||||
ProviderBindingRevision: request.ProviderBindingRevision,
|
||||
FinishReason: reason,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) }
|
||||
|
||||
func validOpenAIAdvisoryRequest() cognitive.AdvisoryRequest {
|
||||
return cognitive.AdvisoryRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1,
|
||||
RequestID: "request-1",
|
||||
ActorID: identity.ActorID("actor-private"),
|
||||
PrincipalID: identity.PrincipalID("principal-private"),
|
||||
ScopeID: authority.ScopeID("scope-private"),
|
||||
Message: "What should I verify?",
|
||||
Locale: "en",
|
||||
MaxOutputBytes: MaxAdvisoryResponseBytes,
|
||||
ProviderBindingID: OpenAIAdvisoryBindingID,
|
||||
ProviderBindingRevision: OpenAIAdvisoryBindingRevision,
|
||||
}
|
||||
}
|
||||
|
||||
func providerHTTPResponse(status int, body string) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: status,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
}
|
||||
}
|
||||
|
||||
func validOpenAIResponse(text string) string {
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"status": "completed",
|
||||
"model": openAIModel,
|
||||
"store": false,
|
||||
"output": []any{
|
||||
map[string]any{"type": "reasoning", "summary": []any{}},
|
||||
map[string]any{"type": "message", "role": "assistant", "status": "completed", "content": []any{
|
||||
map[string]any{"type": "output_text", "text": text, "annotations": []any{}},
|
||||
}},
|
||||
},
|
||||
"usage": map[string]any{"input_tokens": 12, "output_tokens": 7},
|
||||
})
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func TestOpenAIAdvisorySendsOnlyModeAPayload(t *testing.T) {
|
||||
calls := 0
|
||||
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
||||
calls++
|
||||
if request.Method != http.MethodPost || request.URL.String() != openAIResponsesURL || request.Host != "api.openai.com" {
|
||||
t.Fatalf("unexpected target: %s %s host=%s", request.Method, request.URL, request.Host)
|
||||
}
|
||||
if request.Header.Get("Authorization") != "Bearer test-key" || request.Header.Get("Content-Type") != "application/json" {
|
||||
t.Fatal("provider authentication or content type is missing")
|
||||
}
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(payload) != 6 || payload["model"] != openAIModel || payload["input"] != "What should I verify?" || payload["store"] != false || payload["instructions"] != openAIInstructions || payload["max_output_tokens"] != float64(openAIMaxOutputTokens) {
|
||||
t.Fatalf("unexpected provider payload keys/values: %v", payload)
|
||||
}
|
||||
reasoning, ok := payload["reasoning"].(map[string]any)
|
||||
if !ok || len(reasoning) != 1 || reasoning["effort"] != "low" {
|
||||
t.Fatalf("unexpected reasoning payload: %v", payload["reasoning"])
|
||||
}
|
||||
serialized := string(body)
|
||||
for _, forbidden := range []string{"actor-private", "principal-private", "scope-private", "request-1", "tools", "previous_response_id", "conversation", "background"} {
|
||||
if strings.Contains(serialized, forbidden) {
|
||||
t.Fatalf("provider payload leaked forbidden field %q", forbidden)
|
||||
}
|
||||
}
|
||||
return providerHTTPResponse(http.StatusOK, validOpenAIResponse("Check the current evidence.")), nil
|
||||
})
|
||||
adapter, err := NewOpenAIAdvisoryAdapter([]byte("test-key"), transport)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err := adapter.Advise(context.Background(), validOpenAIAdvisoryRequest())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if calls != 1 || response.FinishReason != cognitive.FinishCompleted || response.AdvisoryText != "Check the current evidence." || response.Usage == nil || response.Usage.InputUnits != 12 || response.Usage.OutputUnits != 7 {
|
||||
t.Fatalf("unexpected response: calls=%d response=%+v", calls, response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIAdvisoryRejectsContextBeforeTransport(t *testing.T) {
|
||||
called := false
|
||||
adapter, _ := NewOpenAIAdvisoryAdapter([]byte("test-key"), roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
called = true
|
||||
return nil, errors.New("must not call")
|
||||
}))
|
||||
request := validOpenAIAdvisoryRequest()
|
||||
request.Context = []cognitive.ContextExcerpt{{ID: "excerpt", SourceKind: "task", SourceID: "task", Revision: 1, Sensitivity: cognitive.SensitivityPublic, Text: "public"}}
|
||||
if _, err := adapter.Advise(context.Background(), request); err == nil || called {
|
||||
t.Fatalf("error=%v called=%t", err, called)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIAdvisoryDoesNotFollowRedirectOrRetry(t *testing.T) {
|
||||
calls := 0
|
||||
adapter, _ := NewOpenAIAdvisoryAdapter([]byte("test-key"), roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
calls++
|
||||
response := providerHTTPResponse(http.StatusFound, "{}")
|
||||
response.Header.Set("Location", "https://attacker.invalid/steal")
|
||||
return response, nil
|
||||
}))
|
||||
response, err := adapter.Advise(context.Background(), validOpenAIAdvisoryRequest())
|
||||
if err != nil || calls != 1 || response.FinishReason != cognitive.FinishProviderUnavailable {
|
||||
t.Fatalf("error=%v calls=%d response=%+v", err, calls, response)
|
||||
}
|
||||
}
|
||||
|
||||
type timeoutError struct{}
|
||||
|
||||
func (timeoutError) Error() string { return "timeout" }
|
||||
func (timeoutError) Timeout() bool { return true }
|
||||
func (timeoutError) Temporary() bool { return true }
|
||||
|
||||
func TestOpenAIAdvisoryMapsTimeoutAndDoesNotRetry(t *testing.T) {
|
||||
calls := 0
|
||||
adapter, _ := NewOpenAIAdvisoryAdapter([]byte("test-key"), roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
calls++
|
||||
return nil, timeoutError{}
|
||||
}))
|
||||
response, err := adapter.Advise(context.Background(), validOpenAIAdvisoryRequest())
|
||||
if err != nil || calls != 1 || response.FinishReason != cognitive.FinishTimeout {
|
||||
t.Fatalf("error=%v calls=%d response=%+v", err, calls, response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIAdvisoryMapsRefusal(t *testing.T) {
|
||||
body := `{"status":"completed","model":"gpt-5.4-mini-2026-03-17","store":false,"output":[{"type":"message","role":"assistant","status":"completed","content":[{"type":"refusal","refusal":"cannot comply"}]}],"usage":{"input_tokens":3,"output_tokens":0}}`
|
||||
adapter, _ := NewOpenAIAdvisoryAdapter([]byte("test-key"), roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return providerHTTPResponse(http.StatusOK, body), nil
|
||||
}))
|
||||
response, err := adapter.Advise(context.Background(), validOpenAIAdvisoryRequest())
|
||||
if err != nil || response.FinishReason != cognitive.FinishContentRejected || response.AdvisoryText != "" {
|
||||
t.Fatalf("error=%v response=%+v", err, response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIAdvisoryMapsOutputLimit(t *testing.T) {
|
||||
body := `{"status":"incomplete","model":"gpt-5.4-mini-2026-03-17","store":false,"incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":3,"output_tokens":4096}}`
|
||||
adapter, _ := NewOpenAIAdvisoryAdapter([]byte("test-key"), roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return providerHTTPResponse(http.StatusOK, body), nil
|
||||
}))
|
||||
response, err := adapter.Advise(context.Background(), validOpenAIAdvisoryRequest())
|
||||
if err != nil || response.FinishReason != cognitive.FinishLengthLimit || response.AdvisoryText != "" {
|
||||
t.Fatalf("error=%v response=%+v", err, response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIAdvisoryRejectsExecutableOrUntrustedOutput(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"tool call": `{"status":"completed","model":"gpt-5.4-mini-2026-03-17","store":false,"output":[{"type":"function_call","name":"mutate"}],"usage":{"input_tokens":1,"output_tokens":1}}`,
|
||||
"citation": `{"status":"completed","model":"gpt-5.4-mini-2026-03-17","store":false,"output":[{"type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"answer","annotations":[{"type":"url_citation"}]}]}],"usage":{"input_tokens":1,"output_tokens":1}}`,
|
||||
"wrong model": strings.Replace(validOpenAIResponse("answer"), openAIModel, "moving-alias", 1),
|
||||
"oversize": validOpenAIResponse(strings.Repeat("a", MaxAdvisoryResponseBytes+1)),
|
||||
"malformed": `{`,
|
||||
}
|
||||
for name, body := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
adapter, _ := NewOpenAIAdvisoryAdapter([]byte("test-key"), roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return providerHTTPResponse(http.StatusOK, body), nil
|
||||
}))
|
||||
response, err := adapter.Advise(context.Background(), validOpenAIAdvisoryRequest())
|
||||
if err != nil || response.FinishReason != cognitive.FinishInvalidProviderResponse || response.AdvisoryText != "" {
|
||||
t.Fatalf("error=%v response=%+v", err, response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIAdvisoryRejectsInvalidCredential(t *testing.T) {
|
||||
for _, key := range [][]byte{nil, []byte("line\nbreak")} {
|
||||
if _, err := NewOpenAIAdvisoryAdapter(key, nil); err == nil {
|
||||
t.Fatalf("credential %q was accepted", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# ADR-0007 — OpenAI Creator advisory binding
|
||||
|
||||
Status: Accepted
|
||||
Date: 2026-08-16
|
||||
Accepted under: ARCH-000010
|
||||
|
||||
## Decision
|
||||
|
||||
1. The first real provider class for `creator-advisory-v1` is OpenAI at the fixed HTTPS origin `https://api.openai.com`, using `POST /v1/responses`.
|
||||
2. Binding revision 1 targets the immutable model snapshot `gpt-5.4-mini-2026-03-17` with reasoning effort `low`. An alias or another snapshot is a different binding revision and receives no inherited promotion.
|
||||
3. Requests set `store: false`, are synchronous and single-turn, and contain no tools, functions, conversations, background mode, streaming, files, images, web/file search, MCP, hosted execution, `previous_response_id` or provider memory.
|
||||
4. Mode A permits only the Creator message explicitly entered for that request plus a fixed data-free system instruction. Automatic excerpts and `PRIVATE_ALLOWED` context are prohibited until a separately evidenced ZDR/MAM binding revision is accepted and promoted.
|
||||
5. Before each Mode A request the Creator UI must disclose that the message leaves the private network and provider abuse-monitoring retention may be up to 30 days. Submission acknowledges that request only and is not durable blanket consent.
|
||||
6. `SECRET`, `UNKNOWN`, credentials, authorization/routing identifiers and ambient context never enter the provider payload. Trusted Actor, Principal, Scope, session, workspace, task and binding identifiers remain local authorization/routing data and are not model context.
|
||||
7. The API key belongs to a dedicated OpenAI project and may be mounted read-only only into `cognitive-worker` as a secret file. It is absent from Git, databases, images, environment variables, logs, browser state and other containers.
|
||||
8. Adapter implementation must use the standard library, fixed origin, normal TLS verification, disabled redirects and ambient proxies, strict request/response types, a 30-second deadline, no automatic retry and no request/response body logging.
|
||||
9. Provider output is advisory text only. Tool/executable/unknown output items, invalid JSON, origin changes, oversize output or malformed text fail closed and are never partially displayed.
|
||||
10. Initial canary limits are one in-flight request, five accepted requests per minute and fifty per day for the single Creator. A provider project spend limit and alert are required before any production key is mounted.
|
||||
11. Production activation requires adversarial fake-server tests, a synthetic/public evaluation record, shadow evidence and explicit promotion of the exact binding revision. This ADR alone does not authorize a real key, network egress or production call.
|
||||
|
||||
## Rationale
|
||||
|
||||
The existing provider-neutral boundary makes cognition replaceable and non-canonical. A dated snapshot preserves reproducibility and prevents silent model drift. Mode A enables a narrow explicit advisory use while making the provider's default retention boundary visible and forbidding automatic private context. Isolation and fail-closed mapping preserve the Assistant when cognition is absent or malformed.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A provider adapter may be implemented under `adapters/models` and exercised only against adversarial local fake servers before promotion.
|
||||
- The cognitive worker still has no canonical owner, database role, Commit/Executor credential or external mutation path.
|
||||
- Existing `creator-advisory-v1` contracts do not change.
|
||||
- The deterministic fake remains the only production binding until a separate evaluation/credential/promotion task succeeds.
|
||||
- A ChatGPT or Codex subscription does not substitute for a dedicated API project, API billing controls or API key.
|
||||
|
||||
## Rollback
|
||||
|
||||
Disable or retire the OpenAI binding and resolve all requests to `deterministic-fake`, returning `PROVIDER_UNAVAILABLE`. No canonical repair or migration is needed because advisory request/response state is not persisted.
|
||||
|
||||
## Revisit triggers
|
||||
|
||||
- OpenAI publishes or withdraws a model snapshot.
|
||||
- ZDR/MAM is approved for the exact API project.
|
||||
- Context excerpts, conversation persistence, tools, streaming or multimodal input are requested.
|
||||
- Evaluation shows the snapshot, effort, limits, latency or cost are unsuitable.
|
||||
- Provider policy, retention, region, endpoint or origin changes.
|
||||
@@ -4,4 +4,4 @@ Accepted architecture decisions live here. Normal implementation tasks may read
|
||||
|
||||
Use `ADR-XXXX-title.md` with: problem, evidence, alternatives, decision, rationale, consequences, validity conditions, revisit triggers, migration/rollback, status.
|
||||
|
||||
Current decisions include `ADR-0006-creator-advisory-boundary.md`, the fail-closed provider-neutral advisory boundary.
|
||||
Current decisions include `ADR-0006-creator-advisory-boundary.md`, the fail-closed provider-neutral advisory boundary, and `ADR-0007-openai-advisory-binding.md`, the accepted but not yet production-enabled OpenAI binding class.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/ARCH-000009-openai-advisory-binding-proposal.json",
|
||||
"task_id": "ARCH-000009",
|
||||
"task_file": "dev/tasks/DEV-000051-openai-advisory-runtime-gate.json",
|
||||
"task_id": "DEV-000051",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "ARCH-000010",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0006",
|
||||
"base_repo_revision": "835c62cc1d63348b11e0035ceda68965be0d760a",
|
||||
"risk_tier": "E",
|
||||
"intent": "Accept the ARCH-000009 recommendation as the first real Creator advisory provider binding class: OpenAI Responses API, immutable gpt-5.4-mini snapshot and Mode A per-request disclosure, while keeping production egress and credential activation subject to separate implementation, evaluation and promotion tasks.",
|
||||
"requirement_ids": ["ADR-0006", "ARCH-000009", "INV-EPI-001"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["architecture/adrs/ADR-0007-openai-advisory-binding.md", "architecture/adrs/README.md", "docs/OPENAI_ADVISORY_BINDING_PROPOSAL.md", "dev/ACTIVE_TASK.json", "dev/tasks/ARCH-000010-accept-openai-advisory-binding.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/CORE_SEMANTIC_MODEL_V1.md", "architecture/architecture-registry.*", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/**", "database/**", "deploy/**", "security/**", "tests/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0},
|
||||
"contracts": {"consume": ["creator-advisory-v1", "ADR-0006", "ARCH-000009 proposal"], "may_change": []},
|
||||
"semantic_owner": "none-architecture-decision",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [],
|
||||
"side_effects_and_privileges": ["accept ADR-0007", "authorize a separately scoped fake-server adapter implementation", "no API key, provider call, egress or production activation"],
|
||||
"persistence_and_migrations": [],
|
||||
"failure_retry_restart": ["deterministic fake remains production fallback", "no automatic provider retry", "invalid or unavailable provider response fails closed"],
|
||||
"assumptions": ["the Creator's continuation after the explicit recommendation accepts OpenAI, the fixed snapshot and Mode A retention disclosure", "a later production promotion still requires a dedicated OpenAI project/key and spend controls", "docker-test remains prohibited"],
|
||||
"acceptance_scenarios": ["ADR fixes provider origin, endpoint, model snapshot and Mode A data policy", "tools, provider memory and automatic context remain forbidden", "implementation may use only adversarial local fake transport until a later promotion task", "real key and production egress remain disabled", "rollback preserves deterministic PROVIDER_UNAVAILABLE"],
|
||||
"required_tests": ["architecture preflight", "exact diff scope", "CI", "cold-readable ADR review"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000050",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0007",
|
||||
"base_repo_revision": "00965f20e0c9a0e15a9444c72c4208f328834e08",
|
||||
"risk_tier": "E",
|
||||
"intent": "Implement the fixed OpenAI Responses API Creator advisory adapter with standard-library HTTP and adversarial fake-transport tests, without wiring runtime configuration, adding a real credential, enabling external egress or changing the provider-neutral contract.",
|
||||
"requirement_ids": ["ADR-0006", "ADR-0007", "INV-EPI-001"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["adapters/models/**", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000050-openai-advisory-adapter.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "ci/**", ".gitea/**", "contracts/**", "modules/**", "cmd/**", "database/**", "deploy/**", "security/**", "tests/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0},
|
||||
"contracts": {"consume": ["creator-advisory-v1", "ADR-0007"], "may_change": []},
|
||||
"semantic_owner": "none-provider-adapter",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [],
|
||||
"side_effects_and_privileges": ["adapter can construct one fixed-origin HTTPS request when invoked by a later authorized runtime task", "this task invokes only local fake transports and grants no runtime/provider credential or production egress"],
|
||||
"persistence_and_migrations": [],
|
||||
"failure_retry_restart": ["one call and no retry", "deadline maps to TIMEOUT", "transport/status failure maps to PROVIDER_UNAVAILABLE", "refusal maps to CONTENT_REJECTED", "invalid or executable provider response maps to INVALID_PROVIDER_RESPONSE", "no request or response is persisted"],
|
||||
"assumptions": ["Mode A permits message-only payload and forbids all context excerpts", "fixed binding is openai-responses-gpt-5.4-mini-2026-03-17 revision 1", "docker-test remains prohibited"],
|
||||
"acceptance_scenarios": ["request uses only fixed origin/path/model/instructions/message/store=false/reasoning low and output limit", "authority/routing fields and context are absent from provider JSON", "redirects and ambient proxies are disabled", "unknown/tool output, citation annotations, wrong model and oversize/malformed responses fail closed", "refusal, timeout and unavailable mappings are explicit", "tests prove no automatic retry and no body logging", "no SDK or third-party dependency is added", "runtime remains deterministic fake only"],
|
||||
"required_tests": ["unit", "adversarial fake transport", "go test ./...", "go vet ./...", "architecture preflight", "exact diff scope", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000051",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0007",
|
||||
"base_repo_revision": "0ee9e4a8a11e4bf216007d288b07d74b4049cac5",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add a fail-closed optional OpenAI advisory endpoint to cognitive-worker that can only initialize from a bounded secret file and remains unavailable by default, with no production compose, credential, egress or active Creator Web binding change.",
|
||||
"requirement_ids": ["ADR-0006", "ADR-0007", "DEV-000050", "INV-EPI-001"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["cmd/cognitive-worker/**", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000051-openai-advisory-runtime-gate.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/core-runtime/**", "cmd/creator-web/**", "cmd/commit-runtime/**", "cmd/continuity-runtime/**", "database/**", "deploy/**", "security/**", "tests/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0},
|
||||
"contracts": {"consume": ["creator-advisory-v1", "OpenAIAdvisoryAdapter", "ADR-0007"], "may_change": []},
|
||||
"semantic_owner": "none-cognitive-runtime-gate",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [],
|
||||
"side_effects_and_privileges": ["cognitive-worker code may read one explicitly configured OpenAI secret file and invoke the fixed adapter when a later deployment mounts it", "current production compose and Creator Web binding remain unchanged, so no provider call is activated"],
|
||||
"persistence_and_migrations": [],
|
||||
"failure_retry_restart": ["missing configuration keeps endpoint unavailable", "configured unreadable/empty/oversized secret fails startup", "invalid request fails before provider", "provider failures remain typed and are not retried", "restart loses no advisory state"],
|
||||
"assumptions": ["the OpenAI secret file is absent from current production", "Creator Web continues to call deterministic endpoint", "docker-test remains prohibited"],
|
||||
"acceptance_scenarios": ["default startup has no provider and deterministic fake remains usable", "OpenAI endpoint returns 503 before parsing/requesting when provider is disabled", "only a bounded secret file can initialize the adapter", "direct environment credential is unsupported", "strict request decoding and provider-neutral response encoding are preserved", "health/log responses expose no secret", "no deploy or Creator Web path changes activate egress"],
|
||||
"required_tests": ["unit", "handler adversarial", "secret-file boundary", "go test ./...", "go vet ./...", "architecture preflight", "exact diff scope", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
# OpenAI advisory binding proposal V1
|
||||
|
||||
Status: **PROPOSED — not accepted and not enabled**
|
||||
Status: **ACCEPTED by ADR-0007 — not production-enabled**
|
||||
|
||||
Task: `ARCH-000009` revision 1
|
||||
Parent boundary: `ADR-0006`
|
||||
|
||||
|
||||
@@ -30,4 +30,8 @@ ADR-0006 and DEV-000046 establish the provider-neutral, non-canonical Creator ad
|
||||
|
||||
DEV-000049 adds and executes a repeatable production audit for that boundary. It proves that an authenticated 16 KiB message reaches only the deterministic unavailable binding, unauthenticated and browser-supplied authority fields fail closed, the request sentinel is absent from cognitive-worker logs, and the cognitive worker has no database, Commit/Executor, Creator or provider credential environment/mount.
|
||||
|
||||
ADR-0007 accepts OpenAI Responses API with immutable snapshot `gpt-5.4-mini-2026-03-17` and Mode A per-request retention disclosure as the first real provider binding class. DEV-000050 implements its standard-library adapter and adversarial fake-transport tests: fixed origin/payload, message-only egress, no tools or provider memory, redirect/proxy denial, no retry, strict output mapping and fail-closed timeout/refusal/unavailable/invalid states. The adapter is not wired into the runtime; production remains on `deterministic-fake` with no provider credential or egress.
|
||||
|
||||
DEV-000051 adds an optional cognitive-worker runtime gate for that adapter. It is disabled when `OPENAI_API_KEY_FILE` is absent, rejects direct environment credentials, bounds and validates the secret file, and returns 503 before parsing an OpenAI request while disabled. Production configuration and Creator Web routing are unchanged, so the gate is not active and no provider egress occurs.
|
||||
|
||||
These are explicit scope decisions, not hidden incomplete steps in DEV-000043. No production secret is stored in the repository.
|
||||
|
||||
Reference in New Issue
Block a user