Redact direct API keys in request formatting
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
51
formatting.go
Normal 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),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user