Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/ARCH-000009-openai-advisory-binding-proposal.json",
|
||||
"task_id": "ARCH-000009",
|
||||
"task_file": "dev/tasks/DEV-000050-openai-advisory-adapter.json",
|
||||
"task_id": "DEV-000050",
|
||||
"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"]
|
||||
}
|
||||
@@ -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,6 @@ 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.
|
||||
|
||||
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