Mask padded credentials as sent to providers

This commit is contained in:
2026-11-10 20:29:59 +00:00
parent bbac312b32
commit 8a2a19ccd3
4 changed files with 48 additions and 1 deletions

View File

@@ -122,6 +122,11 @@ func (p *fixtureProvider) serve(w http.ResponseWriter, r *http.Request) {
return
}
content := `{"corrections":[{"id":1,"original_text":"hello world","corrected_text":"Hello world","confidence":0.99}]}`
if p.mode == "invalid-credential-echo" {
sent := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
raw, _ := json.Marshal(map[string]string{"unexpected": sent + "|" + p.secret})
content = string(raw)
}
if p.mode == "repair-filter-later-failure" && !validation {
if proposalCall == 1 {
content = `{"wrong":true}`
@@ -231,6 +236,9 @@ func newProcessFixture(t *testing.T, mode, modules string) processFixture {
t.Helper()
dir := t.TempDir()
p := &fixtureProvider{mode: mode, secret: `selected-secret-"\-123`, entered: make(chan struct{}, 1)}
if mode == "invalid-credential-echo" {
p.secret = " synthetic-echo-credential\t "
}
server := httptest.NewServer(http.HandlerFunc(p.serve))
t.Cleanup(server.Close)
resources := fmt.Sprintf("backends:\n - id: local\n endpoint: %s/v1\n api_key_env: AUDITA_TEST_PROVIDER_KEY\nprofiles:\n - id: proposal\n backend: local\n model: local\n - id: validation\n backend: local\n model: local\n", server.URL)
@@ -944,6 +952,38 @@ func TestSubprocessAllModelFailureStillCompletes(t *testing.T) {
}
}
func TestSubprocessMasksPaddedCredentialInProviderResponse(t *testing.T) {
f := newProcessFixture(t, "invalid-credential-echo", "[grammar]")
effective := strings.TrimSpace(f.provider.secret)
code, stdout, stderr := f.run(t, true, "--debug")
if code != 0 || f.provider.callCount() != 1 || !strings.Contains(stderr, "1 work units with errors") {
t.Fatalf("code=%d calls=%d stdout=%q stderr=%q", code, f.provider.callCount(), stdout, stderr)
}
if strings.Contains(stderr, effective) || strings.Contains(stdout, effective) {
t.Fatal("credential escaped redaction in command output")
}
responseCaptured := false
err := filepath.WalkDir(f.dir, func(path string, entry os.DirEntry, err error) error {
if err != nil || entry.IsDir() {
return err
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
if bytes.Contains(data, []byte(effective)) {
t.Errorf("credential escaped redaction in %s", entry.Name())
}
if strings.HasSuffix(entry.Name(), "-response-payload.json") && bytes.Contains(data, []byte("[REDACTED]|[REDACTED]")) {
responseCaptured = true
}
return nil
})
if err != nil || !responseCaptured {
t.Fatalf("missing redacted provider echo: %v", err)
}
}
func TestSubprocessAllValidationFailureStillCompletes(t *testing.T) {
f := newProcessFixture(t, "all-validation-failure", "[grammar, grammar]")
code, stdout, stderr := f.run(t, true, "--verbose")

View File

@@ -352,6 +352,8 @@ still produces normalized output without provider calls.
## R10 — P2: Mask the Credential Value PromptKit Actually Sends
**Status:** Completed. Redaction now includes the raw and trimmed forms of referenced credential values. A local provider regression echoes both forms and checks retained diagnostics, reports, and command output.
**Category**
Credential normalization and diagnostic redaction disagree in Stage 4.

View File

@@ -27,7 +27,7 @@ type transportFunc func(*http.Request) (*http.Response, error)
func (f transportFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
func TestRoleClientUsesNativeTargetAndRedactsCredential(t *testing.T) {
t.Setenv("AUDITA_TEST_KEY", "synthetic-secret-value")
t.Setenv("AUDITA_TEST_KEY", " synthetic-secret-value\t ")
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "config.yml"), []byte("version: 3\n"), 0644); err != nil {
t.Fatal(err)

View File

@@ -3,6 +3,7 @@ package llm
import (
"context"
"os"
"strings"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/redaction"
)
@@ -41,6 +42,10 @@ func (r *Runtime) ConfiguredSecrets(ctx context.Context, additionalEnvNames ...s
for name := range names {
if value := os.Getenv(name); value != "" {
secrets = append(secrets, value)
// PromptKit trims the value before sending it to the provider.
if trimmed := strings.TrimSpace(value); trimmed != "" && trimmed != value {
secrets = append(secrets, trimmed)
}
}
}
return secrets