Share configured LLM secret extraction across diagnostics paths
This commit is contained in:
17
internal/framework/llm/secrets.go
Normal file
17
internal/framework/llm/secrets.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package llm
|
||||||
|
|
||||||
|
import "gitea.maximumdirect.net/eric/audita/internal/core/config"
|
||||||
|
|
||||||
|
// ConfiguredSecrets returns all configured LLM API-key values that should be
|
||||||
|
// redacted from diagnostics and surfaced error payloads.
|
||||||
|
func ConfiguredSecrets(cfg *config.Config) []string {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
effectiveValidation := cfg.EffectiveValidationLLMConfig()
|
||||||
|
return []string{
|
||||||
|
cfg.PrimaryLLM.APIKey,
|
||||||
|
cfg.ValidationLLM.APIKey,
|
||||||
|
effectiveValidation.APIKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
38
internal/framework/llm/secrets_test.go
Normal file
38
internal/framework/llm/secrets_test.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package llm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfiguredSecretsNilConfig(t *testing.T) {
|
||||||
|
if got := ConfiguredSecrets(nil); got != nil {
|
||||||
|
t.Fatalf("expected nil secrets for nil config, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfiguredSecretsWithValidationOverride(t *testing.T) {
|
||||||
|
cfg := config.Default()
|
||||||
|
cfg.PrimaryLLM.APIKey = "primary-secret"
|
||||||
|
cfg.ValidationLLM.APIKey = "validation-secret"
|
||||||
|
|
||||||
|
got := ConfiguredSecrets(&cfg)
|
||||||
|
want := []string{"primary-secret", "validation-secret", "validation-secret"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("unexpected secrets: got=%v want=%v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfiguredSecretsWithInheritedValidationKey(t *testing.T) {
|
||||||
|
cfg := config.Default()
|
||||||
|
cfg.PrimaryLLM.APIKey = "primary-secret"
|
||||||
|
cfg.ValidationLLM.APIKey = ""
|
||||||
|
|
||||||
|
got := ConfiguredSecrets(&cfg)
|
||||||
|
want := []string{"primary-secret", "", "primary-secret"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("unexpected inherited secrets: got=%v want=%v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,7 +108,7 @@ func GenerateCandidates(ctx context.Context, req Request) (Result, error) {
|
|||||||
writer = diagnosticsWriterAdapter{
|
writer = diagnosticsWriterAdapter{
|
||||||
writer: llm.NewDiagnosticsWriter(
|
writer: llm.NewDiagnosticsWriter(
|
||||||
filepath.Join(req.DiagnosticsDir, req.ModuleInstance),
|
filepath.Join(req.DiagnosticsDir, req.ModuleInstance),
|
||||||
proposalGenerationSecrets(req.Config),
|
llm.ConfiguredSecrets(req.Config),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,17 +213,6 @@ func resolveModel(cfg *config.Config, override string) string {
|
|||||||
return llm.ResolvePrimaryConfig(*cfg).Model
|
return llm.ResolvePrimaryConfig(*cfg).Model
|
||||||
}
|
}
|
||||||
|
|
||||||
func proposalGenerationSecrets(cfg *config.Config) []string {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []string{
|
|
||||||
cfg.PrimaryLLM.APIKey,
|
|
||||||
cfg.ValidationLLM.APIKey,
|
|
||||||
cfg.EffectiveValidationLLMConfig().APIKey,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func errPayload(err error) any {
|
func errPayload(err error) any {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -361,20 +361,22 @@ func TestGenerateCandidatesMultipleSectionsStableMetadata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateCandidatesDiagnosticsWrittenAndRedacted(t *testing.T) {
|
func TestGenerateCandidatesDiagnosticsWrittenAndRedacted(t *testing.T) {
|
||||||
secret := "proposal-secret"
|
primarySecret := "proposal-primary-secret"
|
||||||
|
validationSecret := "proposal-validation-secret"
|
||||||
client := &fakeStructuredClient{
|
client := &fakeStructuredClient{
|
||||||
responses: []StructuredCorrectionSet{
|
responses: []StructuredCorrectionSet{
|
||||||
{Corrections: []StructuredCorrectionProposal{{TargetSegmentID: 1, OriginalText: secret, CorrectedText: "safe", Confidence: 0.9}}},
|
{Corrections: []StructuredCorrectionProposal{{TargetSegmentID: 1, OriginalText: validationSecret, CorrectedText: "safe", Confidence: 0.9}}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cfg := config.Default()
|
cfg := config.Default()
|
||||||
cfg.PrimaryLLM.APIKey = secret
|
cfg.PrimaryLLM.APIKey = primarySecret
|
||||||
|
cfg.ValidationLLM.APIKey = validationSecret
|
||||||
req := defaultRequest(t)
|
req := defaultRequest(t)
|
||||||
req.Config = &cfg
|
req.Config = &cfg
|
||||||
req.LLMClient = client
|
req.LLMClient = client
|
||||||
req.DiagnosticsDir = t.TempDir()
|
req.DiagnosticsDir = t.TempDir()
|
||||||
req.Messages = []contracts.LLMMessage{
|
req.Messages = []contracts.LLMMessage{
|
||||||
{Role: "system", Content: "include secret " + secret},
|
{Role: "system", Content: "include secret " + primarySecret},
|
||||||
{Role: "user", Content: "fix it"},
|
{Role: "user", Content: "fix it"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,8 +393,8 @@ func TestGenerateCandidatesDiagnosticsWrittenAndRedacted(t *testing.T) {
|
|||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
t.Fatalf("read artifact %q: %v", path, readErr)
|
t.Fatalf("read artifact %q: %v", path, readErr)
|
||||||
}
|
}
|
||||||
if strings.Contains(string(raw), secret) {
|
if strings.Contains(string(raw), primarySecret) || strings.Contains(string(raw), validationSecret) {
|
||||||
t.Fatalf("artifact leaked secret %q: %s", path, string(raw))
|
t.Fatalf("artifact leaked configured secret in %q: %s", path, string(raw))
|
||||||
}
|
}
|
||||||
if !strings.Contains(string(raw), "[REDACTED]") {
|
if !strings.Contains(string(raw), "[REDACTED]") {
|
||||||
t.Fatalf("expected redaction marker in artifact %q: %s", path, string(raw))
|
t.Fatalf("expected redaction marker in artifact %q: %s", path, string(raw))
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ func validateSectionCandidates(ctx context.Context, input validateSectionCandida
|
|||||||
diagnosticsWriter = &llmDiagnosticsWriterAdapter{
|
diagnosticsWriter = &llmDiagnosticsWriterAdapter{
|
||||||
writer: llm.NewDiagnosticsWriter(
|
writer: llm.NewDiagnosticsWriter(
|
||||||
filepath.Join(input.DiagnosticsDir, input.Spec.InstanceName),
|
filepath.Join(input.DiagnosticsDir, input.Spec.InstanceName),
|
||||||
validatorSecrets(input.Config),
|
llm.ConfiguredSecrets(input.Config),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -744,15 +744,3 @@ func (a *llmDiagnosticsWriterAdapter) WriteInteraction(stage string, requestMeta
|
|||||||
ErrorPayloadPath: art.ErrorPayloadPath,
|
ErrorPayloadPath: art.ErrorPayloadPath,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatorSecrets(cfg *config.Config) []string {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
effective := cfg.EffectiveValidationLLMConfig()
|
|
||||||
return []string{
|
|
||||||
cfg.PrimaryLLM.APIKey,
|
|
||||||
effective.APIKey,
|
|
||||||
cfg.ValidationLLM.APIKey,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1307,12 +1307,13 @@ func TestRunnerLLMValidatorBatchingAndSchedulerUsage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunnerLLMValidatorDiagnosticsWrittenAndRedacted(t *testing.T) {
|
func TestRunnerLLMValidatorDiagnosticsWrittenAndRedacted(t *testing.T) {
|
||||||
secret := "super-secret-key"
|
primarySecret := "runner-primary-secret"
|
||||||
client := &fakeStructuredClient{responses: []validators.LLMValidationResponse{{Validations: []validators.LLMValidationDecision{{CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: secret}}}}}
|
validationSecret := "runner-validation-secret"
|
||||||
|
client := &fakeStructuredClient{responses: []validators.LLMValidationResponse{{Validations: []validators.LLMValidationDecision{{CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: validationSecret}}}}}
|
||||||
llmValidator, _ := validators.NewLLMBackedValidator("spoken_form_plausibility_review", validators.LLMValidatorTypeSpokenFormPlausibility, "")
|
llmValidator, _ := validators.NewLLMBackedValidator("spoken_form_plausibility_review", validators.LLMValidatorTypeSpokenFormPlausibility, "")
|
||||||
cfg := config.Default()
|
cfg := config.Default()
|
||||||
cfg.PrimaryLLM.APIKey = secret
|
cfg.PrimaryLLM.APIKey = primarySecret
|
||||||
cfg.ValidationLLM.APIKey = secret
|
cfg.ValidationLLM.APIKey = validationSecret
|
||||||
diagDir := t.TempDir()
|
diagDir := t.TempDir()
|
||||||
r := New(fakeFactory{modules: map[string]contracts.TranscriptModule{
|
r := New(fakeFactory{modules: map[string]contracts.TranscriptModule{
|
||||||
"m": fakeModule{key: "m", policy: proposals.ReplacementPolicyRequireUnique, validators: []contracts.Validator{llmValidator}, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
"m": fakeModule{key: "m", policy: proposals.ReplacementPolicyRequireUnique, validators: []contracts.Validator{llmValidator}, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||||
@@ -1321,7 +1322,7 @@ func TestRunnerLLMValidatorDiagnosticsWrittenAndRedacted(t *testing.T) {
|
|||||||
}})
|
}})
|
||||||
out, err := r.Run(context.Background(), RunInput{
|
out, err := r.Run(context.Background(), RunInput{
|
||||||
Config: &cfg,
|
Config: &cfg,
|
||||||
Transcript: &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "There were gestures at the temple."}}},
|
Transcript: &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "There were gestures at the temple. " + primarySecret}}},
|
||||||
ModuleSpecs: []contracts.ModuleRunSpec{{ModuleKey: "m", InstanceName: "m"}},
|
ModuleSpecs: []contracts.ModuleRunSpec{{ModuleKey: "m", InstanceName: "m"}},
|
||||||
ValidationLLMClient: client,
|
ValidationLLMClient: client,
|
||||||
ValidationDiagnosticsDir: diagDir,
|
ValidationDiagnosticsDir: diagDir,
|
||||||
@@ -1332,12 +1333,26 @@ func TestRunnerLLMValidatorDiagnosticsWrittenAndRedacted(t *testing.T) {
|
|||||||
if len(out.ModuleResults[0].ValidatorDecisions) == 0 || out.ModuleResults[0].ValidatorDecisions[0].DiagnosticArtifactPath == "" {
|
if len(out.ModuleResults[0].ValidatorDecisions) == 0 || out.ModuleResults[0].ValidatorDecisions[0].DiagnosticArtifactPath == "" {
|
||||||
t.Fatalf("expected diagnostic artifact path on decision")
|
t.Fatalf("expected diagnostic artifact path on decision")
|
||||||
}
|
}
|
||||||
|
matches, globErr := filepath.Glob(filepath.Join(diagDir, "m", "*.json"))
|
||||||
|
if globErr != nil {
|
||||||
|
t.Fatalf("glob diagnostics: %v", globErr)
|
||||||
|
}
|
||||||
|
if len(matches) == 0 {
|
||||||
|
t.Fatalf("expected diagnostics JSON artifacts under %s", filepath.Join(diagDir, "m"))
|
||||||
|
}
|
||||||
|
for _, path := range matches {
|
||||||
|
raw, readErr := os.ReadFile(path)
|
||||||
|
if readErr != nil {
|
||||||
|
t.Fatalf("read diagnostic %q: %v", path, readErr)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(raw), primarySecret) || strings.Contains(string(raw), validationSecret) {
|
||||||
|
t.Fatalf("configured secret leaked in diagnostics %q: %s", path, string(raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
raw, readErr := os.ReadFile(out.ModuleResults[0].ValidatorDecisions[0].DiagnosticArtifactPath)
|
raw, readErr := os.ReadFile(out.ModuleResults[0].ValidatorDecisions[0].DiagnosticArtifactPath)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
t.Fatalf("read diagnostic: %v", readErr)
|
t.Fatalf("read decision diagnostic: %v", readErr)
|
||||||
}
|
|
||||||
if strings.Contains(string(raw), secret) {
|
|
||||||
t.Fatalf("secret leaked in diagnostics: %s", string(raw))
|
|
||||||
}
|
}
|
||||||
if !strings.Contains(string(raw), "[REDACTED]") {
|
if !strings.Contains(string(raw), "[REDACTED]") {
|
||||||
t.Fatalf("expected redaction marker in diagnostics")
|
t.Fatalf("expected redaction marker in diagnostics")
|
||||||
|
|||||||
Reference in New Issue
Block a user