Redact direct API keys in request formatting

This commit is contained in:
2026-07-04 23:17:44 +00:00
parent 2df2f530b3
commit 7a8516b0c6
3 changed files with 129 additions and 2 deletions

View File

@@ -97,7 +97,9 @@ _ = result.Artifact
`RunResult` includes the run ID, output artifact, raw output, validation result, prompt/profile/model metadata, effective model parameters, input hashes, token/cache usage, and timing fields. Validation content failures return a successful `RunResult` with failed validation status. Runtime validation errors return `ErrValidation`.
For the public Go API, pass provider credentials with `RunRequest.APIKey`. The value is request-scoped, uses `json:"-"`, is preferred over profile `api_key_env` by the default OpenAI-compatible client, and is not included in `PreparedRun` or `RunResult` JSON. Do not store raw keys in config, prompt files, or profile YAML.
For the public Go API, pass provider credentials with `RunRequest.APIKey`. The value is request-scoped, uses `json:"-"`, is preferred over profile `api_key_env` by the default OpenAI-compatible client, and is not included in `PreparedRun` or `RunResult` JSON. Normal Go string formatting of `RunRequest` reports only whether a direct key is set. Do not store raw keys in config, prompt files, or profile YAML.
Avoid logging raw request structs with reflection-based debug dumpers; exported fields remain visible to tools that bypass `String` and `GoString` methods.
## Inject An LLM Client
@@ -116,7 +118,7 @@ func (fakeLLM) Generate(ctx context.Context, req scriptorium.GenerateRequest) (*
engine, err := scriptorium.NewEngine(cfg, scriptorium.WithLLMClient(fakeLLM{}))
```
The injected client receives the rendered prompt, effective execution target, target presence metadata for explicit numeric overrides, structured-output spec, and request API key when provided. `GenerateRequest.APIKey` also uses `json:"-"`; custom and fake clients should avoid logging or serializing it. `WithLLMClient(nil)` returns `ErrInvalidConfig`.
The injected client receives the rendered prompt, effective execution target, target presence metadata for explicit numeric overrides, structured-output spec, and request API key when provided. `GenerateRequest.APIKey` also uses `json:"-"`, and normal Go string formatting reports only whether a direct key is set. Custom and fake clients should avoid logging or serializing it. `WithLLMClient(nil)` returns `ErrInvalidConfig`.
## Request Overrides

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"net/http"
"net/http/httptest"
@@ -118,6 +119,79 @@ func TestPreparedRunJSONDoesNotExposeSecretOrTargetPresence(t *testing.T) {
}
}
func TestRunRequestFormattingRedactsDirectAPIKey(t *testing.T) {
const secret = "run-request-secret"
req := scriptorium.RunRequest{
PromptID: "generic.markdown_summary",
ProfileID: "local-fast",
APIKey: secret,
Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."),
},
}
for _, formatted := range []string{
fmt.Sprint(req),
fmt.Sprintf("%+v", req),
fmt.Sprintf("%#v", req),
} {
if strings.Contains(formatted, secret) {
t.Fatalf("formatted RunRequest leaked API key: %s", formatted)
}
if !strings.Contains(formatted, "APIKeySet:true") {
t.Fatalf("formatted RunRequest should indicate an API key is set, got %s", formatted)
}
}
payload, err := json.Marshal(req)
if err != nil {
t.Fatalf("expected RunRequest to marshal, got %v", err)
}
if strings.Contains(string(payload), secret) {
t.Fatalf("RunRequest JSON leaked API key: %s", payload)
}
}
func TestGenerateRequestFormattingRedactsDirectAPIKey(t *testing.T) {
const secret = "generate-request-secret"
req := scriptorium.GenerateRequest{
Prompt: scriptorium.RenderedPrompt{Messages: []scriptorium.RenderedMessage{
{Role: "user", Content: "secret prompt content"},
}},
Target: scriptorium.ExecutionTarget{
Model: "test-model",
ExtraParams: map[string]any{
"provider_option": "on",
},
},
APIKey: secret,
}
for _, formatted := range []string{
fmt.Sprint(req),
fmt.Sprintf("%+v", req),
fmt.Sprintf("%#v", req),
} {
if strings.Contains(formatted, secret) {
t.Fatalf("formatted GenerateRequest leaked API key: %s", formatted)
}
if strings.Contains(formatted, "secret prompt content") {
t.Fatalf("formatted GenerateRequest leaked prompt content: %s", formatted)
}
if !strings.Contains(formatted, "APIKeySet:true") {
t.Fatalf("formatted GenerateRequest should indicate an API key is set, got %s", formatted)
}
}
payload, err := json.Marshal(req)
if err != nil {
t.Fatalf("expected GenerateRequest to marshal, got %v", err)
}
if strings.Contains(string(payload), secret) {
t.Fatalf("GenerateRequest JSON leaked API key: %s", payload)
}
}
func TestPreparePreservesExplicitZeroExecutionOverrides(t *testing.T) {
engine := newExampleEngine(t)
zeroFloat := 0.0

51
formatting.go Normal file
View File

@@ -0,0 +1,51 @@
package scriptorium
import "fmt"
// String returns a concise request summary without exposing direct API keys.
func (r RunRequest) String() string {
return r.redactedString()
}
// GoString returns a concise request summary without exposing direct API keys.
func (r RunRequest) GoString() string {
return r.redactedString()
}
func (r RunRequest) redactedString() string {
return fmt.Sprintf(
"scriptorium.RunRequest{PromptID:%q PromptVersion:%q ProfileID:%q APIKeySet:%t Inputs:%d Vars:%d ExecutionSet:%t ValidationSet:%t Metadata:%d}",
r.PromptID,
r.PromptVersion,
r.ProfileID,
r.APIKey != "",
len(r.Inputs),
len(r.Vars),
r.Execution != nil,
r.Validation != nil,
len(r.Metadata),
)
}
// String returns a concise request summary without exposing direct API keys or
// rendered prompt content.
func (r GenerateRequest) String() string {
return r.redactedString()
}
// GoString returns a concise request summary without exposing direct API keys or
// rendered prompt content.
func (r GenerateRequest) GoString() string {
return r.redactedString()
}
func (r GenerateRequest) redactedString() string {
return fmt.Sprintf(
"scriptorium.GenerateRequest{Messages:%d Model:%q APIKeySet:%t StructuredOutputSet:%t ExtraParams:%d}",
len(r.Prompt.Messages),
r.Target.Model,
r.APIKey != "",
r.StructuredOutput != nil,
len(r.Target.ExtraParams),
)
}