From 89d7bc75c3adcd4141854df1e232a1ec41d17e3a Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 4 May 2026 10:53:53 -0500 Subject: [PATCH] Audita API key credentials are now optional --- README.md | 3 +- architecture.md | 4 +-- internal/adapters/audita/subprocess.go | 18 +++++----- internal/adapters/audita/subprocess_test.go | 39 ++++++++++++--------- internal/app/runner.go | 2 +- internal/config/load_validate_test.go | 21 ----------- internal/config/validate.go | 3 -- 7 files changed, 35 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 26c909d..01a8971 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Seriatim config contract in `pipeline.yml`: Audita config contract in `pipeline.yml`: - required: `audita.binary` (name or path; existence is checked at execution time, not config validation time) -- required: `audita.llm_api_key_env` (environment variable name holding the API key secret) +- optional: `audita.llm_api_key_env` (environment variable name holding the API key secret; defaults to `AUDITA_LLM_API_KEY`) - defaulted when omitted: `audita.timeout` (`3h`), `audita.llm_api_key_env` (`AUDITA_LLM_API_KEY`), `audita.modules` (`glossary,homophones,glossary,spoken_word,grammar,homophones,glossary`), `audita.base_url` (`https://openrouter.ai/api/v1`), `audita.model` (`openrouter/google/gemma-4-31b-it`), `audita.llm_concurrency` (`1`), `audita.validation_model` (`""`), `audita.validation_llm_concurrency` (`1`), `audita.report` (`true`) - allowed `audita.modules` values: `glossary`, `homophones`, `spoken_word`, `grammar` (order and repeats are allowed) - `audita.base_url` must be a valid URL when provided @@ -36,6 +36,7 @@ Audita credentials note: - store only the environment variable **name** in config (`audita.llm_api_key_env`), never the API key value itself - API key values must not be written to pipeline config, generated configs, logs, or manifest metadata +- if the named env var is not set (or is empty), Narratio omits `AUDITA_LLM_API_KEY` from the Audita subprocess environment instead of failing Audita runtime note: diff --git a/architecture.md b/architecture.md index d442a57..5a694c7 100644 --- a/architecture.md +++ b/architecture.md @@ -132,7 +132,7 @@ Audita config keys: - `pipeline.audita.binary` (required) - `pipeline.audita.timeout` (default: `3h`) -- `pipeline.audita.llm_api_key_env` (required; default: `AUDITA_LLM_API_KEY`) +- `pipeline.audita.llm_api_key_env` (optional; default: `AUDITA_LLM_API_KEY`) - `pipeline.audita.modules` (default sequence: `glossary,homophones,glossary,spoken_word,grammar,homophones,glossary`) - `pipeline.audita.base_url` (default: `https://openrouter.ai/api/v1`) - `pipeline.audita.model` (default: `openrouter/google/gemma-4-31b-it`) @@ -145,6 +145,7 @@ Audita secret-handling policy: - `llm_api_key_env` stores only the environment variable **name**. - API key values are read from the process environment at runtime and are not stored in `pipeline.yml`, manifest metadata, generated configs, or logs. +- If the named env var is missing/empty, the Audita adapter omits `AUDITA_LLM_API_KEY` from subprocess env overrides and continues. Validation currently enforces: @@ -160,7 +161,6 @@ Validation currently enforces: - optional `pipeline.seriatim.env.*` values must be `> 0` when provided. - `pipeline.audita.binary` is required. - `pipeline.audita.timeout` must parse as Go duration. -- `pipeline.audita.llm_api_key_env` is required. - `pipeline.audita.modules` must be non-empty and each entry must be one of `glossary|homophones|spoken_word|grammar`. - `pipeline.audita.base_url` must be a valid URL when provided. - `pipeline.audita.model` is required. diff --git a/internal/adapters/audita/subprocess.go b/internal/adapters/audita/subprocess.go index ca615b0..85b87fc 100644 --- a/internal/adapters/audita/subprocess.go +++ b/internal/adapters/audita/subprocess.go @@ -83,9 +83,6 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error) if cfg.Timeout <= 0 { return nil, fmt.Errorf("audita timeout must be > 0") } - if strings.TrimSpace(cfg.LLMAPIKeyEnv) == "" { - return nil, fmt.Errorf("audita llm api key env var name is required") - } if len(cfg.Modules) == 0 { return nil, fmt.Errorf("audita modules must include at least one module") } @@ -160,13 +157,14 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe } args := r.buildArgs(req, reqModules) - credential, credentialPresent := os.LookupEnv(r.llmAPIKeyEnv) - if !credentialPresent || strings.TrimSpace(credential) == "" { - return PolishResult{}, fmt.Errorf("audita credential env var %q is required but not set", r.llmAPIKeyEnv) - } - - env := map[string]string{ - "AUDITA_LLM_API_KEY": credential, + credentialPresent := false + env := map[string]string{} + if strings.TrimSpace(r.llmAPIKeyEnv) != "" { + credential, ok := os.LookupEnv(r.llmAPIKeyEnv) + if ok && strings.TrimSpace(credential) != "" { + env["AUDITA_LLM_API_KEY"] = credential + credentialPresent = true + } } primaryConcurrencyViaEnv := false if r.llmConcurrency != nil { diff --git a/internal/adapters/audita/subprocess_test.go b/internal/adapters/audita/subprocess_test.go index b90b95c..8df827a 100644 --- a/internal/adapters/audita/subprocess_test.go +++ b/internal/adapters/audita/subprocess_test.go @@ -120,33 +120,38 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) { } } -func TestSubprocessRunnerMissingCredentialFails(t *testing.T) { +func TestSubprocessRunnerMissingCredentialEnvOmitsCredential(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("helper wrapper script uses /bin/sh") + } + t.Setenv("GO_WANT_AUDITA_HELPER", "1") + t.Setenv("AUDITA_HELPER_MODE", "success") + recordPath := filepath.Join(t.TempDir(), "record.json") + t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath) + llmConcurrency := 1 - runner, err := NewSubprocessRunner(SubprocessRunnerConfig{ - Binary: "audita", - Timeout: mustParseAuditaDuration(t, "1s"), + runner := mustAuditaRunner(t, SubprocessRunnerConfig{ + Binary: writeAuditaHelperWrapper(t), + Timeout: mustParseAuditaDuration(t, "2s"), LLMAPIKeyEnv: "MISSING_AUDITA_KEY", Modules: []string{"glossary"}, BaseURL: "https://openrouter.ai/api/v1", Model: "openrouter/google/gemma-4-31b-it", LLMConcurrency: &llmConcurrency, + Report: false, }) - if err != nil { - t.Fatalf("NewSubprocessRunner() error = %v", err) - } + req := auditaReqForTest(t, false) - req := PolishRequest{ - MergedTranscriptPath: "/tmp/merged.json", - GlossaryPath: "/tmp/glossary.yml", - OutputProcessedPath: "/tmp/processed.json", - WorkDir: "/tmp/audita-work", + res, err := runner.Run(context.Background(), req) + if err != nil { + t.Fatalf("Run() error = %v", err) } - _, err = runner.Run(context.Background(), req) - if err == nil { - t.Fatal("expected error, got nil") + if res.Metadata["credential_present"] != false { + t.Fatalf("credential_present = %#v, want false", res.Metadata["credential_present"]) } - if !strings.Contains(err.Error(), "MISSING_AUDITA_KEY") { - t.Fatalf("error = %q, want env var name context", err.Error()) + rec := readAuditaHelperRecord(t, recordPath) + if rec.Env["AUDITA_LLM_API_KEY"] != "" { + t.Fatalf("AUDITA_LLM_API_KEY = %q, want omitted/empty", rec.Env["AUDITA_LLM_API_KEY"]) } } diff --git a/internal/app/runner.go b/internal/app/runner.go index f87400d..7bc5f59 100644 --- a/internal/app/runner.go +++ b/internal/app/runner.go @@ -227,7 +227,7 @@ func buildDefaultAuditaRunner(cfg *config.Config) (audita.Runner, error) { } a := cfg.Pipeline.Audita - if strings.TrimSpace(a.Binary) == "" || strings.TrimSpace(a.Timeout) == "" || strings.TrimSpace(a.LLMAPIKeyEnv) == "" || len(a.Modules) == 0 || strings.TrimSpace(a.BaseURL) == "" || strings.TrimSpace(a.Model) == "" { + if strings.TrimSpace(a.Binary) == "" || strings.TrimSpace(a.Timeout) == "" || len(a.Modules) == 0 || strings.TrimSpace(a.BaseURL) == "" || strings.TrimSpace(a.Model) == "" { // Compatibility fallback for tests or internal call paths that bypass config validation/defaults. return &audita.NoopRunner{}, nil } diff --git a/internal/config/load_validate_test.go b/internal/config/load_validate_test.go index a15ffa7..5fe749f 100644 --- a/internal/config/load_validate_test.go +++ b/internal/config/load_validate_test.go @@ -412,27 +412,6 @@ inputs: `, wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.timeout must be a valid duration", }, - { - name: "empty audita llm_api_key_env fails", - pipelineYAML: `workspace: - root: /tmp/narratio -whisperx: - transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe -seriatim: - binary: seriatim -audita: - binary: audita - llm_api_key_env: " " -`, - sessionYAML: `session_id: 2026-05-03 -inputs: - audio_dir: ./audio - speakers_file: ./speakers.yml - autocorrect_file: ./autocorrect.yml - glossary_file: ./glossary.yml -`, - wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.llm_api_key_env is required", - }, { name: "empty audita modules fails", pipelineYAML: `workspace: diff --git a/internal/config/validate.go b/internal/config/validate.go index 371b600..b767c93 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -129,9 +129,6 @@ func validateAudita(cfg AuditaConfig) error { if err := validateDuration("pipeline.audita.timeout", cfg.Timeout); err != nil { return err } - if strings.TrimSpace(cfg.LLMAPIKeyEnv) == "" { - return fmt.Errorf("pipeline.audita.llm_api_key_env is required") - } if len(cfg.Modules) == 0 { return fmt.Errorf("pipeline.audita.modules must include at least one module") }