Migrate Notarius to credential lists

This commit is contained in:
2026-09-25 15:33:46 +00:00
parent 49948852d6
commit 7e0151ef93
21 changed files with 161 additions and 112 deletions

View File

@@ -98,7 +98,7 @@ a session nor create a workspace, manifest, run, lock, adapter, or remote
connection. Like every pipeline-loading command, they bootstrap the selected or connection. Like every pipeline-loading command, they bootstrap the selected or
conventional secrets directory before resolving configuration; `config show` conventional secrets directory before resolving configuration; `config show`
never includes bootstrap reports or secret values in its YAML output. never includes bootstrap reports or secret values in its YAML output.
Typed credential references, including `notarius.llm_api_key`, must therefore Typed credential references, including `notarius.credentials[]`, must therefore
resolve even though these inspection commands never construct an adapter. resolve even though these inspection commands never construct an adapter.
Campaign selection is optional for a pipeline without party-driven artifact Campaign selection is optional for a pipeline without party-driven artifact
@@ -178,7 +178,7 @@ Behavior:
- runs `render` before `extract`; an omitted or disabled Notarius - runs `render` before `extract`; an omitted or disabled Notarius
configuration records an explicit `notarius_disabled` self-skip; configuration records an explicit `notarius_disabled` self-skip;
- resolves every selected typed credential during the initial configuration - resolves every selected typed credential during the initial configuration
load, including a Notarius credential when extract is disabled, outside the load, including Notarius credentials when extract is disabled, outside the
selected range, or later skipped as resumable; selected range, or later skipped as resumable;
- skips already-succeeded stages unless `--force` is set or a stage-specific - skips already-succeeded stages unless `--force` is set or a stage-specific
resume check finds its durable result obsolete; resume check finds its durable result obsolete;

View File

@@ -230,7 +230,7 @@ inputs:
- Do not place raw secrets in YAML. - Do not place raw secrets in YAML.
- Credential fields contain environment-variable names, represented internally - Credential fields contain environment-variable names, represented internally
as ConfigKit typed secret references (for example as ConfigKit typed secret references (for example
`pipeline.notarius.llm_api_key: NOTARIUS_LLM_API_KEY`). A configured reference `pipeline.notarius.credentials: [NOTARIUS_LLM_API_KEY]`). A configured reference
must resolve to a nonempty environment value during load. must resolve to a nonempty environment value during load.
- Every surviving reference is resolved during ordinary pipeline loading, - Every surviving reference is resolved during ordinary pipeline loading,
regardless of whether its integration is disabled, outside the selected regardless of whether its integration is disabled, outside the selected
@@ -362,7 +362,7 @@ Rules:
| `pipeline.trim.seriatim.report` | bool | No | `false` | | `pipeline.trim.seriatim.report` | bool | No | `false` |
| `pipeline.notarius.enabled` | bool | No | `false` | | `pipeline.notarius.enabled` | bool | No | `false` |
| `pipeline.notarius.binary` | string | No | `notarius` | | `pipeline.notarius.binary` | string | No | `notarius` |
| `pipeline.notarius.llm_api_key` | secret reference | No | unset; exact environment-variable name forwarded to Notarius | | `pipeline.notarius.credentials[]` | list[secret reference] | No | empty; exact environment-variable names forwarded to Notarius |
| `pipeline.notarius.config_profile` | string | No | unset; exact Notarius ConfigKit profile selector | | `pipeline.notarius.config_profile` | string | No | unset; exact Notarius ConfigKit profile selector |
| `pipeline.notarius.config_path` | string | Conditional | required when enabled; relative paths resolve from the pipeline file directory | | `pipeline.notarius.config_path` | string | Conditional | required when enabled; relative paths resolve from the pipeline file directory |
| `pipeline.notarius.pipeline_id` | string | Conditional | required when enabled | | `pipeline.notarius.pipeline_id` | string | Conditional | required when enabled |
@@ -406,35 +406,36 @@ does not restrict profile names, resolve profile files, or rewrite the value.
This selection is independent of Narratio's own `--overlay` selection. A This selection is independent of Narratio's own `--overlay` selection. A
Narratio overlay may explicitly select a differently named Notarius overlay, Narratio overlay may explicitly select a differently named Notarius overlay,
but Narratio never forwards its own overlay name automatically. It is also but Narratio never forwards its own overlay name automatically. It is also
independent of `pipeline.notarius.llm_api_key`: an overlay may change either or independent of `pipeline.notarius.credentials`: an overlay may change either or
both fields according to the deployed Notarius configuration. See the both fields according to the deployed Notarius configuration. See the
[Notarius integration contract](./integrations/notarius.md) for invocation and [Notarius integration contract](./integrations/notarius.md) for invocation and
failure behavior. failure behavior.
### Notarius Credential ### Notarius Credentials
`pipeline.notarius.llm_api_key` optionally names the one environment variable `pipeline.notarius.credentials` optionally lists environment-variable names
to forward to the Notarius child process: to forward to the Notarius child process:
```yaml ```yaml
notarius: notarius:
llm_api_key: OPENROUTER_API_KEY credentials: [OPENROUTER_API_KEY, ANTHROPIC_API_KEY]
``` ```
The field contains the variable name, not a credential value. That name must The list contains variable names, not credential values. Those names must
match `api_key_env` in the Notarius/PromptKit profile selected by the deployed match the credential references in the selected Notarius/PromptKit configuration;
Notarius pipeline; Narratio does not infer or remap provider names. Omission or Narratio does not infer or remap provider names. Omission, `null`, or `[]`
`null` forwards nothing. A non-null selected reference must resolve at pipeline forwards nothing. Each selected reference must resolve at pipeline
load time through the process environment or existing secrets-directory load time through the process environment or existing secrets-directory
bootstrap, even when Notarius is disabled, extract is outside the requested bootstrap, even when Notarius is disabled, extract is outside the requested
range, or a completed extraction can be reused. range, or a completed extraction can be reused.
The Notarius child receives only Narratio's minimal execution environment plus The Notarius child receives only Narratio's minimal execution environment plus
this exact selected name and loaded value. Other credentials are not forwarded. these exact selected names and loaded values. Other credentials are not forwarded.
Changing the value, name, presence, or absence does not invalidate extract's Changing a value, name, presence, or absence does not invalidate extract's
semantic resume evidence. The public name remains part of safe effective semantic resume evidence. The public names remain part of safe effective
inspection, so renaming it may appear in `config diff` and change the effective inspection, so renaming them may appear in `config diff` and change the effective
configuration digest; rotating only its value does neither. configuration digest; rotating only their values does neither. An overlay
replaces the whole list, including with `[]` to clear it.
See the [Notarius integration contract](./integrations/notarius.md) for the See the [Notarius integration contract](./integrations/notarius.md) for the
delivery and redaction boundary. delivery and redaction boundary.

View File

@@ -105,15 +105,16 @@ causes a cold miss. Chunk refresh bypasses lookup and publishes the accepted
replacement for the active source; it does not delete unrelated plans or replacement for the active source; it does not delete unrelated plans or
checkpoint state. checkpoint state.
When `pipeline.notarius.llm_api_key` is absent or null, Narratio adds no When `pipeline.notarius.credentials` is absent, null, or empty, Narratio adds no
credential to that environment. When configured, the field names an environment credentials to that environment. When configured, each list entry names an
variable whose already-loaded nonempty value is forwarded under that exact environment variable whose already-loaded nonempty value is forwarded under
name. Operators must use the same name as the `api_key_env` in the selected that exact name. Operators must use the names referenced by the selected
Notarius/PromptKit profile. Narratio does not inspect the Notarius configuration Notarius/PromptKit providers. Narratio does not inspect the Notarius configuration
to infer a provider or destination name, and it does not forward unrelated to infer providers or destination names, and it does not forward unrelated
credentials from its parent environment or secrets directory. credentials from its parent environment or secrets directory. A Narratio
overlay replaces the whole credential list; `[]` clears it.
The selected name is registered with Narratio's shared streaming redactor. Every selected name is registered with Narratio's shared streaming redactor.
Credential bytes written by the child to captured stdout, captured stderr, or Credential bytes written by the child to captured stdout, captured stderr, or
stderr-derived failure diagnostics are replaced with `<redacted>`, including stderr-derived failure diagnostics are replaced with `<redacted>`, including
custom names not known to the built-in sensitive-name list. Narratio does not custom names not known to the built-in sensitive-name list. Narratio does not

View File

@@ -241,7 +241,10 @@ a resolver, provider policy, or ambient-secret inheritance.
## Stage 3: Migrate Notarius to credential lists ## Stage 3: Migrate Notarius to credential lists
Status: Not started. Status: Complete (2026-09-25).
Verification: `go test ./internal/config ./internal/adapters/notarius ./internal/stage ./internal/app`,
`go test -race ./internal/adapters/notarius`, and `git diff --check` passed.
Preparation: `internal/config/config.go`, `credential_projection.go`, loading/ Preparation: `internal/config/config.go`, `credential_projection.go`, loading/
inspection tests; `internal/adapters/notarius/runner.go`, `subprocess.go`; inspection tests; `internal/adapters/notarius/runner.go`, `subprocess.go`;

View File

@@ -312,7 +312,7 @@ and [Transcribe internals](./internal/stage-transcribe.md).
Symptom: Symptom:
- configuration loading reports `secret.unset`, `secret.empty`, or an invalid - configuration loading reports `secret.unset`, `secret.empty`, or an invalid
name for `pipeline.notarius.llm_api_key`, including during inspection or a name in `pipeline.notarius.credentials`, including during inspection or a
run that would not execute extraction. run that would not execute extraction.
Likely causes: Likely causes:

View File

@@ -131,9 +131,9 @@ notarius:
# Optional structured extraction after render and before analyze. # Optional structured extraction after render and before analyze.
enabled: true enabled: true
binary: notarius binary: notarius
# Optional environment-variable reference. Match this name to api_key_env in # Optional environment-variable references. Match these names to the
# the selected Notarius/PromptKit profile; never put the credential here. # selected Notarius/PromptKit providers; never put credential values here.
llm_api_key: NOTARIUS_LLM_API_KEY credentials: [NOTARIUS_LLM_API_KEY]
# Optionally select a Notarius ConfigKit overlay when extraction runs. This # Optionally select a Notarius ConfigKit overlay when extraction runs. This
# is independent of Narratio's selected overlay and may use a different name. # is independent of Narratio's selected overlay and may use a different name.
# See docs/config.md and docs/integrations/notarius.md for the contracts. # See docs/config.md and docs/integrations/notarius.md for the contracts.

View File

@@ -1,6 +1,10 @@
package notarius package notarius
import "context" import (
"context"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
// FakeRunner is a configurable in-memory runner for stage tests. // FakeRunner is a configurable in-memory runner for stage tests.
type FakeRunner struct { type FakeRunner struct {
@@ -15,6 +19,7 @@ func (f *FakeRunner) Run(ctx context.Context, req RunRequest) (RunResult, error)
return RunResult{}, err return RunResult{}, err
} }
copyRequest := req copyRequest := req
copyRequest.Credentials = append([]configkit.Secret(nil), req.Credentials...)
if req.ConfigProfile != nil { if req.ConfigProfile != nil {
profile := *req.ConfigProfile profile := *req.ConfigProfile
copyRequest.ConfigProfile = &profile copyRequest.ConfigProfile = &profile

View File

@@ -25,8 +25,8 @@ type ReferenceBinding struct {
// RunRequest contains the resolved inputs and diagnostic destinations for one invocation. // RunRequest contains the resolved inputs and diagnostic destinations for one invocation.
type RunRequest struct { type RunRequest struct {
Binary string Binary string
// LLMAPIKey is an optional already-resolved credential reference. // Credentials are already-resolved environment references selected for Notarius.
LLMAPIKey *configkit.Secret Credentials []configkit.Secret
ConfigProfile *string ConfigProfile *string
ConfigPath string ConfigPath string
PipelineID string PipelineID string

View File

@@ -52,19 +52,9 @@ func (r *SubprocessRunner) Run(ctx context.Context, req RunRequest) (RunResult,
if err != nil { if err != nil {
return RunResult{}, err return RunResult{}, err
} }
var envOverrides map[string]string envOverrides, sensitiveEnvNames, err := subprocess.CredentialEnvironment(req.Credentials)
var sensitiveEnvNames []string if err != nil {
if req.LLMAPIKey != nil { return RunResult{}, fmt.Errorf("notarius credentials: %w", err)
if !req.LLMAPIKey.IsResolved() {
return RunResult{}, fmt.Errorf("notarius LLM API credential must be resolved and nonempty")
}
name := req.LLMAPIKey.EnvName()
value := req.LLMAPIKey.Value()
if value == "" {
return RunResult{}, fmt.Errorf("notarius LLM API credential must be resolved and nonempty")
}
envOverrides = map[string]string{name: value}
sensitiveEnvNames = []string{name}
} }
args := []string{ args := []string{

View File

@@ -188,12 +188,16 @@ func TestSubprocessRunnerRejectsInvalidChunkCacheBeforeLaunch(t *testing.T) {
func TestSubprocessRunnerForwardsLoadedCredentialWithoutRereadingEnvironment(t *testing.T) { func TestSubprocessRunnerForwardsLoadedCredentialWithoutRereadingEnvironment(t *testing.T) {
const envName = "NARRATIO_NOTARIUS_CUSTOM_LLM_KEY" const envName = "NARRATIO_NOTARIUS_CUSTOM_LLM_KEY"
const otherName = "NARRATIO_NOTARIUS_CUSTOM_OTHER_KEY"
const loadedValue = "loaded-notarius-credential" const loadedValue = "loaded-notarius-credential"
const otherValue = "loaded-notarius-other-credential"
credential := loadTestCredential(t, envName, loadedValue) credential := loadTestCredential(t, envName, loadedValue)
otherCredential := loadTestCredential(t, otherName, otherValue)
t.Setenv(envName, "changed-parent-credential") t.Setenv(envName, "changed-parent-credential")
t.Setenv(otherName, "changed-parent-other-credential")
req := validRunRequest(t) req := validRunRequest(t)
req.LLMAPIKey = credential req.Credentials = []configkit.Secret{*credential, *otherCredential}
var captured sharedsubprocess.RunRequest var captured sharedsubprocess.RunRequest
runner := &SubprocessRunner{run: func(_ context.Context, processReq sharedsubprocess.RunRequest) (sharedsubprocess.RunResult, error) { runner := &SubprocessRunner{run: func(_ context.Context, processReq sharedsubprocess.RunRequest) (sharedsubprocess.RunResult, error) {
captured = processReq captured = processReq
@@ -203,10 +207,10 @@ func TestSubprocessRunnerForwardsLoadedCredentialWithoutRereadingEnvironment(t *
if _, err := runner.Run(context.Background(), req); err != nil { if _, err := runner.Run(context.Background(), req); err != nil {
t.Fatalf("Run() error = %v", err) t.Fatalf("Run() error = %v", err)
} }
if len(captured.EnvOverrides) != 1 || captured.EnvOverrides[envName] != loadedValue { if len(captured.EnvOverrides) != 2 || captured.EnvOverrides[envName] != loadedValue || captured.EnvOverrides[otherName] != otherValue {
t.Fatal("subprocess did not receive exactly the loaded credential override") t.Fatal("subprocess did not receive exactly the loaded credential overrides")
} }
if !reflect.DeepEqual(captured.SensitiveEnvNames, []string{envName}) { if !reflect.DeepEqual(captured.SensitiveEnvNames, []string{envName, otherName}) {
t.Fatalf("sensitive environment names = %#v, want configured reference name", captured.SensitiveEnvNames) t.Fatalf("sensitive environment names = %#v, want configured reference name", captured.SensitiveEnvNames)
} }
wantArgs := []string{ wantArgs := []string{
@@ -228,7 +232,7 @@ func TestSubprocessRunnerRejectsInvalidProgrammaticCredentialBeforeLaunch(t *tes
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
req := validRunRequest(t) req := validRunRequest(t)
req.LLMAPIKey = &test.reference req.Credentials = []configkit.Secret{test.reference}
started := false started := false
runner := &SubprocessRunner{run: func(context.Context, sharedsubprocess.RunRequest) (sharedsubprocess.RunResult, error) { runner := &SubprocessRunner{run: func(context.Context, sharedsubprocess.RunRequest) (sharedsubprocess.RunResult, error) {
started = true started = true
@@ -342,10 +346,12 @@ func TestSubprocessRunnerRejectsInvalidReferencesBeforeLaunch(t *testing.T) {
func TestSubprocessRunnerUsesMinimalEnvironmentAndSeparatesStreams(t *testing.T) { func TestSubprocessRunnerUsesMinimalEnvironmentAndSeparatesStreams(t *testing.T) {
const envName = "NARRATIO_NOTARIUS_CUSTOM_CAPTURE_KEY" const envName = "NARRATIO_NOTARIUS_CUSTOM_CAPTURE_KEY"
const otherName = "NARRATIO_NOTARIUS_CUSTOM_CAPTURE_OTHER_KEY"
const canary = "notarius-captured-stream-canary" const canary = "notarius-captured-stream-canary"
const otherCanary = "notarius-captured-stream-other-canary"
const unrelated = "NARRATIO_NOTARIUS_UNRELATED_PARENT_KEY" const unrelated = "NARRATIO_NOTARIUS_UNRELATED_PARENT_KEY"
req := validRunRequest(t) req := validRunRequest(t)
req.LLMAPIKey = loadTestCredential(t, envName, canary) req.Credentials = []configkit.Secret{*loadTestCredential(t, envName, canary), *loadTestCredential(t, otherName, otherCanary)}
writeValidBundleAndReceipt(t, req, false) writeValidBundleAndReceipt(t, req, false)
receiptFixture := req.ReceiptPath + ".fixture" receiptFixture := req.ReceiptPath + ".fixture"
data, err := os.ReadFile(req.ReceiptPath) data, err := os.ReadFile(req.ReceiptPath)
@@ -356,7 +362,7 @@ func TestSubprocessRunnerUsesMinimalEnvironmentAndSeparatesStreams(t *testing.T)
if err := json.Unmarshal(data, &receipt); err != nil { if err := json.Unmarshal(data, &receipt); err != nil {
t.Fatalf("decode receipt fixture: %v", err) t.Fatalf("decode receipt fixture: %v", err)
} }
receipt["credential_echo"] = canary receipt["credential_echo"] = canary + " " + otherCanary
writeJSONFile(t, receiptFixture, receipt) writeJSONFile(t, receiptFixture, receipt)
if err := os.Remove(req.ReceiptPath); err != nil { if err := os.Remove(req.ReceiptPath); err != nil {
t.Fatalf("Remove(receipt) error = %v", err) t.Fatalf("Remove(receipt) error = %v", err)
@@ -373,11 +379,16 @@ if [ "$`+envName+`" != '`+canary+`' ]; then
printf 'selected credential mismatch\n' >&2 printf 'selected credential mismatch\n' >&2
exit 8 exit 8
fi fi
if [ "$`+otherName+`" != '`+otherCanary+`' ]; then
printf 'other selected credential mismatch\n' >&2
exit 10
fi
if [ -n "$`+unrelated+`" ]; then if [ -n "$`+unrelated+`" ]; then
printf 'unrelated credential inherited\n' >&2 printf 'unrelated credential inherited\n' >&2
exit 9 exit 9
fi fi
printf '%s\n' "$`+envName+`" >&2 printf '%s\n' "$`+envName+`" >&2
printf '%s\n' "$`+otherName+`" >&2
cat "$NOTARIUS_RECEIPT_FIXTURE" cat "$NOTARIUS_RECEIPT_FIXTURE"
`) `)
req.Binary = script req.Binary = script
@@ -392,32 +403,39 @@ cat "$NOTARIUS_RECEIPT_FIXTURE"
assertTextFile(t, filepath.Join(captureDir, "working-directory"), req.WorkingDirectory+"\n") assertTextFile(t, filepath.Join(captureDir, "working-directory"), req.WorkingDirectory+"\n")
assertTextFile(t, filepath.Join(captureDir, "environment"), "") assertTextFile(t, filepath.Join(captureDir, "environment"), "")
assertRedactedCredentialFile(t, req.LogPath, canary) assertRedactedCredentialFile(t, req.LogPath, canary)
assertRedactedCredentialFile(t, req.LogPath, otherCanary)
receiptBytes, err := os.ReadFile(req.ReceiptPath) receiptBytes, err := os.ReadFile(req.ReceiptPath)
if err != nil { if err != nil {
t.Fatalf("ReadFile(receipt) error = %v", err) t.Fatalf("ReadFile(receipt) error = %v", err)
} }
assertRedactedCredentialBytes(t, receiptBytes, canary) assertRedactedCredentialBytes(t, receiptBytes, canary)
assertRedactedCredentialBytes(t, receiptBytes, otherCanary)
} }
func TestSubprocessRunnerRedactsCredentialOnProcessFailure(t *testing.T) { func TestSubprocessRunnerRedactsCredentialOnProcessFailure(t *testing.T) {
const envName = "NARRATIO_NOTARIUS_CUSTOM_FAILURE_KEY" const envName = "NARRATIO_NOTARIUS_CUSTOM_FAILURE_KEY"
const otherName = "NARRATIO_NOTARIUS_CUSTOM_FAILURE_OTHER_KEY"
const canary = "notarius-failure-stream-canary" const canary = "notarius-failure-stream-canary"
const otherCanary = "notarius-failure-stream-other-canary"
req := validRunRequest(t) req := validRunRequest(t)
req.LLMAPIKey = loadTestCredential(t, envName, canary) req.Credentials = []configkit.Secret{*loadTestCredential(t, envName, canary), *loadTestCredential(t, otherName, otherCanary)}
req.Binary = writeShellScript(t, `#!/bin/sh req.Binary = writeShellScript(t, `#!/bin/sh
printf '%s\n' "$`+envName+`" printf '%s\n' "$`+envName+`"
printf '%s\n' "$`+envName+`" >&2 printf '%s\n' "$`+envName+`" >&2
printf '%s\n' "$`+otherName+`" >&2
exit 7 exit 7
`) `)
_, err := NewSubprocessRunner().Run(context.Background(), req) _, err := NewSubprocessRunner().Run(context.Background(), req)
if err == nil { if err == nil {
t.Fatal("Run() error = nil, want process failure") t.Fatal("Run() error = nil, want process failure")
} }
if strings.Contains(err.Error(), canary) || !strings.Contains(err.Error(), "<redacted>") { if strings.Contains(err.Error(), canary) || strings.Contains(err.Error(), otherCanary) || !strings.Contains(err.Error(), "<redacted>") {
t.Fatal("process failure did not return redacted diagnostics") t.Fatal("process failure did not return redacted diagnostics")
} }
assertRedactedCredentialFile(t, req.ReceiptPath, canary) assertRedactedCredentialFile(t, req.ReceiptPath, canary)
assertRedactedCredentialFile(t, req.ReceiptPath, otherCanary)
assertRedactedCredentialFile(t, req.LogPath, canary) assertRedactedCredentialFile(t, req.LogPath, canary)
assertRedactedCredentialFile(t, req.LogPath, otherCanary)
} }
func TestSubprocessRunnerReturnsProcessFailuresWithoutParsingStdout(t *testing.T) { func TestSubprocessRunnerReturnsProcessFailuresWithoutParsingStdout(t *testing.T) {

View File

@@ -389,16 +389,16 @@ func TestConfigInspectionOutputsNeverExposeResolvedCredentialValues(t *testing.T
`, 1) `, 1)
pipelineYAML += `notarius: pipelineYAML += `notarius:
enabled: false enabled: false
llm_api_key: NARRATIO_INSPECT_NOTARIUS_ONE credentials: [NARRATIO_INSPECT_NOTARIUS_ONE]
` `
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil { if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
writeInspectionOverlays(t, pipelinePath, writeInspectionOverlays(t, pipelinePath,
"audita:\n model: production\n llm_api_key: NARRATIO_INSPECT_LLM_ONE\n", "audita:\n model: production\n llm_api_key: NARRATIO_INSPECT_LLM_ONE\n",
"audita:\n model: testing\n llm_api_key: NARRATIO_INSPECT_LLM_TWO\nnotarius:\n llm_api_key: NARRATIO_INSPECT_NOTARIUS_TWO\n", "audita:\n model: testing\n llm_api_key: NARRATIO_INSPECT_LLM_TWO\nnotarius:\n credentials: [NARRATIO_INSPECT_NOTARIUS_TWO]\n",
) )
mustWriteTestFile(t, filepath.Join(filepath.Dir(pipelinePath), "overlays", "without-notarius-key.yml"), "notarius:\n llm_api_key: null\n") mustWriteTestFile(t, filepath.Join(filepath.Dir(pipelinePath), "overlays", "without-notarius-key.yml"), "notarius:\n credentials: []\n")
var validate bytes.Buffer var validate bytes.Buffer
if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath, "--overlay", "production"}, &validate); err != nil { if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath, "--overlay", "production"}, &validate); err != nil {
@@ -439,11 +439,11 @@ func TestConfigInspectionOutputsNeverExposeResolvedCredentialValues(t *testing.T
!strings.Contains(diff.String(), "NARRATIO_INSPECT_NOTARIUS_ONE") || !strings.Contains(diff.String(), "NARRATIO_INSPECT_NOTARIUS_TWO") { !strings.Contains(diff.String(), "NARRATIO_INSPECT_NOTARIUS_ONE") || !strings.Contains(diff.String(), "NARRATIO_INSPECT_NOTARIUS_TWO") {
t.Fatalf("diff omitted safe credential references: %q", diff.String()) t.Fatalf("diff omitted safe credential references: %q", diff.String())
} }
if !strings.Contains(sources.String(), "notarius.llm_api_key") { if !strings.Contains(sources.String(), "notarius.credentials") {
t.Fatalf("sources omitted Notarius credential ownership: %q", sources.String()) t.Fatalf("sources omitted Notarius credential ownership: %q", sources.String())
} }
if strings.Contains(removed.String(), "llm_api_key") || strings.Contains(removed.String(), "NARRATIO_INSPECT_NOTARIUS_ONE") { if strings.Contains(removed.String(), "NARRATIO_INSPECT_NOTARIUS_ONE") {
t.Fatalf("overlay null did not remove Notarius credential: %q", removed.String()) t.Fatalf("empty overlay did not remove Notarius credential: %q", removed.String())
} }
} }
@@ -451,7 +451,7 @@ func TestConfigValidateRejectsUnavailableNotariusCredential(t *testing.T) {
const envName = "NARRATIO_INSPECT_NOTARIUS_MISSING" const envName = "NARRATIO_INSPECT_NOTARIUS_MISSING"
restoreEnvAfterTest(t, envName) restoreEnvAfterTest(t, envName)
path := filepath.Join(t.TempDir(), "pipeline.yml") path := filepath.Join(t.TempDir(), "pipeline.yml")
mustWriteTestFile(t, path, "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n llm_api_key: "+envName+"\n") mustWriteTestFile(t, path, "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n credentials: ["+envName+"]\n")
err := ConfigValidate(context.Background(), []string{"--config", path}, io.Discard) err := ConfigValidate(context.Background(), []string{"--config", path}, io.Discard)
if err == nil || !strings.Contains(err.Error(), "secret.unset") { if err == nil || !strings.Contains(err.Error(), "secret.unset") {
t.Fatalf("ConfigValidate() error = %v, want unavailable credential failure", err) t.Fatalf("ConfigValidate() error = %v, want unavailable credential failure", err)

View File

@@ -665,16 +665,16 @@ func TestExtractLifecycleCredentialChangesRemainResumable(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
cfg, env, runner := extractionLifecycleFixture(t, true) cfg, env, runner := extractionLifecycleFixture(t, true)
if test.before != nil { if test.before != nil {
cfg.Pipeline.Notarius.LLMAPIKey = test.before(t) cfg.Pipeline.Notarius.Credentials = []configkit.Secret{*test.before(t)}
} }
analyzeRuns := 0 analyzeRuns := 0
plan := extractionLifecyclePlan(t, &analyzeRuns) plan := extractionLifecyclePlan(t, &analyzeRuns)
if _, err := executeStages(context.Background(), cfg, plan, RunOptions{Env: env}); err != nil { if _, err := executeStages(context.Background(), cfg, plan, RunOptions{Env: env}); err != nil {
t.Fatalf("initial executeStages() error = %v", err) t.Fatalf("initial executeStages() error = %v", err)
} }
cfg.Pipeline.Notarius.LLMAPIKey = nil cfg.Pipeline.Notarius.Credentials = nil
if test.after != nil { if test.after != nil {
cfg.Pipeline.Notarius.LLMAPIKey = test.after(t) cfg.Pipeline.Notarius.Credentials = []configkit.Secret{*test.after(t)}
} }
resumed, err := executeStages(context.Background(), cfg, plan, RunOptions{Env: env}) resumed, err := executeStages(context.Background(), cfg, plan, RunOptions{Env: env})
if err != nil { if err != nil {
@@ -876,7 +876,7 @@ func TestPreparedPipelineAndManifestExcludeNotariusCredentialValue(t *testing.T)
const name = "NARRATIO_LIFECYCLE_PERSISTED_KEY" const name = "NARRATIO_LIFECYCLE_PERSISTED_KEY"
const canary = "notarius-persisted-credential-canary" const canary = "notarius-persisted-credential-canary"
cfg, env, _ := extractionLifecycleFixture(t, true) cfg, env, _ := extractionLifecycleFixture(t, true)
cfg.Pipeline.Notarius.LLMAPIKey = loadLifecycleNotariusCredential(t, name, canary) cfg.Pipeline.Notarius.Credentials = []configkit.Secret{*loadLifecycleNotariusCredential(t, name, canary)}
if _, err := executeStages(context.Background(), cfg, prepareExtractLifecyclePlan(t), RunOptions{Env: env}); err != nil { if _, err := executeStages(context.Background(), cfg, prepareExtractLifecyclePlan(t), RunOptions{Env: env}); err != nil {
t.Fatalf("executeStages() error = %v", err) t.Fatalf("executeStages() error = %v", err)
} }
@@ -1181,12 +1181,12 @@ func loadLifecycleNotariusCredential(t *testing.T, name, value string) *configki
t.Helper() t.Helper()
t.Setenv(name, value) t.Setenv(name, value)
path := filepath.Join(t.TempDir(), "pipeline.yml") path := filepath.Join(t.TempDir(), "pipeline.yml")
if err := os.WriteFile(path, []byte("whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n llm_api_key: "+name+"\n"), 0o644); err != nil { if err := os.WriteFile(path, []byte("whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n credentials: ["+name+"]\n"), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
pipeline, err := config.LoadPipeline(path) pipeline, err := config.LoadPipeline(path)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return pipeline.Notarius.LLMAPIKey return &pipeline.Notarius.Credentials[0]
} }

View File

@@ -138,7 +138,7 @@ func TestNotariusCredentialLoadsFromCommandSecretBootstrap(t *testing.T) {
restoreEnvAfterTest(t, envName) restoreEnvAfterTest(t, envName)
root := t.TempDir() root := t.TempDir()
pipelinePath := filepath.Join(root, "pipeline.yml") pipelinePath := filepath.Join(root, "pipeline.yml")
if err := os.WriteFile(pipelinePath, []byte("whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n llm_api_key: "+envName+"\n"), 0o644); err != nil { if err := os.WriteFile(pipelinePath, []byte("whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n credentials: ["+envName+"]\n"), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
secretsDir := filepath.Join(root, "secrets") secretsDir := filepath.Join(root, "secrets")
@@ -158,8 +158,8 @@ func TestNotariusCredentialLoadsFromCommandSecretBootstrap(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("load pipeline: %v", err) t.Fatalf("load pipeline: %v", err)
} }
if pipeline.Notarius == nil || pipeline.Notarius.LLMAPIKey == nil || !pipeline.Notarius.LLMAPIKey.IsResolved() || if pipeline.Notarius == nil || len(pipeline.Notarius.Credentials) != 1 || !pipeline.Notarius.Credentials[0].IsResolved() ||
pipeline.Notarius.LLMAPIKey.EnvName() != envName || pipeline.Notarius.LLMAPIKey.Value() != test.wantValue { pipeline.Notarius.Credentials[0].EnvName() != envName || pipeline.Notarius.Credentials[0].Value() != test.wantValue {
t.Fatal("Notarius credential did not resolve from the selected command bootstrap source") t.Fatal("Notarius credential did not resolve from the selected command bootstrap source")
} }
}) })
@@ -220,7 +220,7 @@ func TestExecuteConfigValidateReportsSkippedEmptyBeforeTypedLoadFailure(t *testi
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
pipelineYAML = append(pipelineYAML, []byte("notarius:\n enabled: false\n llm_api_key: "+name+"\n")...) pipelineYAML = append(pipelineYAML, []byte("notarius:\n enabled: false\n credentials: ["+name+"]\n")...)
if err := os.WriteFile(pipelinePath, pipelineYAML, 0o644); err != nil { if err := os.WriteFile(pipelinePath, pipelineYAML, 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -309,7 +309,7 @@ type ScriptoriumInputConfig struct {
type NotariusConfig struct { type NotariusConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"` Enabled bool `yaml:"enabled" json:"enabled"`
Binary string `yaml:"binary" json:"binary"` Binary string `yaml:"binary" json:"binary"`
LLMAPIKey *configkit.Secret `yaml:"llm_api_key,omitempty" json:"llm_api_key,omitempty"` Credentials []configkit.Secret `yaml:"credentials,omitempty" json:"credentials,omitempty"`
ConfigProfile *string `yaml:"config_profile,omitempty" json:"config_profile,omitempty"` ConfigProfile *string `yaml:"config_profile,omitempty" json:"config_profile,omitempty"`
Resume bool `yaml:"resume,omitempty" json:"resume,omitempty"` Resume bool `yaml:"resume,omitempty" json:"resume,omitempty"`
ChunkCache string `yaml:"chunk_cache,omitempty" json:"chunk_cache,omitempty"` ChunkCache string `yaml:"chunk_cache,omitempty" json:"chunk_cache,omitempty"`

View File

@@ -50,12 +50,12 @@ func (cfg AuditaConfig) MarshalYAML() (any, error) {
}, nil }, nil
} }
// MarshalYAML exposes only the public Notarius credential reference name. // MarshalYAML exposes only public Notarius credential reference names.
func (cfg NotariusConfig) MarshalYAML() (any, error) { func (cfg NotariusConfig) MarshalYAML() (any, error) {
type safeNotarius struct { type safeNotarius struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
Binary string `yaml:"binary"` Binary string `yaml:"binary"`
LLMAPIKey *string `yaml:"llm_api_key,omitempty"` Credentials []string `yaml:"credentials,omitempty"`
ConfigProfile *string `yaml:"config_profile,omitempty"` ConfigProfile *string `yaml:"config_profile,omitempty"`
Resume bool `yaml:"resume,omitempty"` Resume bool `yaml:"resume,omitempty"`
ChunkCache string `yaml:"chunk_cache,omitempty"` ChunkCache string `yaml:"chunk_cache,omitempty"`
@@ -67,7 +67,7 @@ func (cfg NotariusConfig) MarshalYAML() (any, error) {
Outputs map[string]NotariusOutputConfig `yaml:"outputs"` Outputs map[string]NotariusOutputConfig `yaml:"outputs"`
} }
return safeNotarius{ return safeNotarius{
Enabled: cfg.Enabled, Binary: cfg.Binary, LLMAPIKey: secretReferenceName(cfg.LLMAPIKey), Enabled: cfg.Enabled, Binary: cfg.Binary, Credentials: secretReferenceNames(cfg.Credentials),
ConfigProfile: cfg.ConfigProfile, ConfigProfile: cfg.ConfigProfile,
Resume: cfg.Resume, ChunkCache: cfg.ChunkCache, Resume: cfg.Resume, ChunkCache: cfg.ChunkCache,
ConfigPath: cfg.ConfigPath, PipelineID: cfg.PipelineID, Timeout: cfg.Timeout, ConfigPath: cfg.ConfigPath, PipelineID: cfg.PipelineID, Timeout: cfg.Timeout,
@@ -75,6 +75,17 @@ func (cfg NotariusConfig) MarshalYAML() (any, error) {
}, nil }, nil
} }
func secretReferenceNames(secrets []configkit.Secret) []string {
if secrets == nil {
return nil
}
names := make([]string, len(secrets))
for index, secret := range secrets {
names[index] = secret.EnvName()
}
return names
}
func secretReferenceName(secret *configkit.Secret) *string { func secretReferenceName(secret *configkit.Secret) *string {
if secret == nil { if secret == nil {
return nil return nil

View File

@@ -102,7 +102,7 @@ storage:
secret_access_key: NARRATIO_TEST_SECRET_KEY secret_access_key: NARRATIO_TEST_SECRET_KEY
notarius: notarius:
enabled: false enabled: false
llm_api_key: NARRATIO_TEST_NOTARIUS_KEY credentials: [NARRATIO_TEST_NOTARIUS_KEY]
config_profile: "" config_profile: ""
` `
pipelinePath, _ := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) pipelinePath, _ := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
@@ -146,8 +146,8 @@ notarius:
} }
var roundTrip struct { var roundTrip struct {
Notarius *struct { Notarius *struct {
LLMAPIKey *string `yaml:"llm_api_key"` Credentials []string `yaml:"credentials"`
ConfigProfile *string `yaml:"config_profile"` ConfigProfile *string `yaml:"config_profile"`
} `yaml:"notarius"` } `yaml:"notarius"`
} }
if err := yaml.Unmarshal(firstYAML, &roundTrip); err != nil { if err := yaml.Unmarshal(firstYAML, &roundTrip); err != nil {
@@ -156,8 +156,8 @@ notarius:
if roundTrip.Notarius == nil || roundTrip.Notarius.ConfigProfile == nil || *roundTrip.Notarius.ConfigProfile != "" { if roundTrip.Notarius == nil || roundTrip.Notarius.ConfigProfile == nil || *roundTrip.Notarius.ConfigProfile != "" {
t.Fatalf("round-trip config profile = %#v, want explicit empty", roundTrip.Notarius) t.Fatalf("round-trip config profile = %#v, want explicit empty", roundTrip.Notarius)
} }
if roundTrip.Notarius.LLMAPIKey == nil || *roundTrip.Notarius.LLMAPIKey != "NARRATIO_TEST_NOTARIUS_KEY" { if len(roundTrip.Notarius.Credentials) != 1 || roundTrip.Notarius.Credentials[0] != "NARRATIO_TEST_NOTARIUS_KEY" {
t.Fatalf("round-trip credential reference = %#v, want safe public name", roundTrip.Notarius.LLMAPIKey) t.Fatalf("round-trip credential reference = %#v, want safe public name", roundTrip.Notarius.Credentials)
} }
t.Setenv("NARRATIO_TEST_ACCESS_KEY", secondCanary) t.Setenv("NARRATIO_TEST_ACCESS_KEY", secondCanary)

View File

@@ -58,8 +58,10 @@ notarius:
func TestNotariusCredentialReferenceLoading(t *testing.T) { func TestNotariusCredentialReferenceLoading(t *testing.T) {
const envName = "NARRATIO_TEST_NOTARIUS_KEY" const envName = "NARRATIO_TEST_NOTARIUS_KEY"
const secondName = "NARRATIO_TEST_NOTARIUS_SECOND_KEY"
const canary = "notarius-credential-canary" const canary = "notarius-credential-canary"
t.Setenv(envName, canary) t.Setenv(envName, canary)
t.Setenv(secondName, "second-canary")
tests := []struct { tests := []struct {
name string name string
@@ -67,8 +69,9 @@ func TestNotariusCredentialReferenceLoading(t *testing.T) {
wantNil bool wantNil bool
}{ }{
{name: "omitted", wantNil: true}, {name: "omitted", wantNil: true},
{name: "null", credential: " llm_api_key: null\n", wantNil: true}, {name: "null", credential: " credentials: null\n", wantNil: true},
{name: "custom reference", credential: " llm_api_key: " + envName + "\n"}, {name: "empty", credential: " credentials: []\n"},
{name: "custom references", credential: " credentials: [" + envName + ", " + secondName + "]\n"},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
@@ -81,15 +84,19 @@ func TestNotariusCredentialReferenceLoading(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if test.wantNil { if test.wantNil && cfg.Notarius.Credentials != nil {
if cfg.Notarius.LLMAPIKey != nil { t.Fatal("credential list should be nil")
t.Fatal("credential reference should be nil") }
} if test.name == "empty" && len(cfg.Notarius.Credentials) == 0 {
return return
} }
if cfg.Notarius.LLMAPIKey == nil || cfg.Notarius.LLMAPIKey.EnvName() != envName || if test.wantNil {
!cfg.Notarius.LLMAPIKey.IsResolved() || cfg.Notarius.LLMAPIKey.Value() != canary { return
t.Fatal("credential reference was not loaded and resolved") }
if len(cfg.Notarius.Credentials) != 2 ||
cfg.Notarius.Credentials[0].EnvName() != envName || cfg.Notarius.Credentials[0].Value() != canary ||
cfg.Notarius.Credentials[1].EnvName() != secondName || cfg.Notarius.Credentials[1].Value() != "second-canary" {
t.Fatal("credential references were not loaded and resolved")
} }
}) })
} }
@@ -110,7 +117,7 @@ func TestNotariusCredentialReferenceFailures(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "pipeline.yml") path := filepath.Join(t.TempDir(), "pipeline.yml")
pipelineYAML := testPipelineBaseYAML + "\nnotarius:\n enabled: false\n llm_api_key: " + test.reference + "\n" pipelineYAML := testPipelineBaseYAML + "\nnotarius:\n enabled: false\n credentials: [" + test.reference + "]\n"
if err := os.WriteFile(path, []byte(pipelineYAML), 0o644); err != nil { if err := os.WriteFile(path, []byte(pipelineYAML), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -122,6 +129,17 @@ func TestNotariusCredentialReferenceFailures(t *testing.T) {
} }
} }
func TestNotariusRejectsRetiredCredentialField(t *testing.T) {
path := filepath.Join(t.TempDir(), "pipeline.yml")
pipelineYAML := testPipelineBaseYAML + "\nnotarius:\n llm_api_key: NARRATIO_TEST_NOTARIUS_KEY\n"
if err := os.WriteFile(path, []byte(pipelineYAML), 0o644); err != nil {
t.Fatal(err)
}
if _, err := LoadPipeline(path); err == nil || !strings.Contains(err.Error(), "decode.unknown_field") {
t.Fatalf("LoadPipeline() error = %v, want unknown field", err)
}
}
func TestNotariusEnabledDefaultsAndPathResolution(t *testing.T) { func TestNotariusEnabledDefaultsAndPathResolution(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
pipelinePath := filepath.Join(dir, "deployment", "pipeline.yml") pipelinePath := filepath.Join(dir, "deployment", "pipeline.yml")

View File

@@ -147,7 +147,7 @@ func TestPipelineOverlayDoesNotRequireUnusedBaseCredentials(t *testing.T) {
} }
} }
func TestNotariusCredentialProfileReplacementAndProvenance(t *testing.T) { func TestNotariusCredentialOverlayReplacementAndProvenance(t *testing.T) {
const baseKey = "NARRATIO_NOTARIUS_UNUSED_BASE_KEY" const baseKey = "NARRATIO_NOTARIUS_UNUSED_BASE_KEY"
const profileKey = "NARRATIO_NOTARIUS_PROFILE_KEY" const profileKey = "NARRATIO_NOTARIUS_PROFILE_KEY"
if err := os.Unsetenv(baseKey); err != nil { if err := os.Unsetenv(baseKey); err != nil {
@@ -155,24 +155,24 @@ func TestNotariusCredentialProfileReplacementAndProvenance(t *testing.T) {
} }
t.Setenv(profileKey, "synthetic-notarius-profile-key") t.Setenv(profileKey, "synthetic-notarius-profile-key")
for _, credential := range []string{profileKey, "null"} { for _, credential := range []string{"[" + profileKey + "]", "null", "[]"} {
t.Run(credential, func(t *testing.T) { t.Run(credential, func(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n llm_api_key: "+baseKey+"\n") root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n credentials: ["+baseKey+"]\n")
overlay := writePipelineFixture(t, dir, "overlays/testing.yml", "notarius:\n llm_api_key: "+credential+"\n") overlay := writePipelineFixture(t, dir, "overlays/testing.yml", "notarius:\n credentials: "+credential+"\n")
profile := "testing" profile := "testing"
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: &profile}) cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: &profile})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if credential == "null" { if credential == "null" || credential == "[]" {
if cfg.Notarius.LLMAPIKey != nil { if len(cfg.Notarius.Credentials) != 0 {
t.Fatal("profile did not remove the base credential") t.Fatal("overlay did not clear base credentials")
} }
return return
} }
if cfg.Notarius.LLMAPIKey == nil || cfg.Notarius.LLMAPIKey.EnvName() != profileKey || !cfg.Notarius.LLMAPIKey.IsResolved() { if len(cfg.Notarius.Credentials) != 1 || cfg.Notarius.Credentials[0].EnvName() != profileKey || !cfg.Notarius.Credentials[0].IsResolved() {
t.Fatal("profile credential was not resolved") t.Fatal("overlay credential was not resolved")
} }
sources, err := EffectivePipelineSources(cfg, ResolvedParty{}) sources, err := EffectivePipelineSources(cfg, ResolvedParty{})
if err != nil { if err != nil {
@@ -180,12 +180,12 @@ func TestNotariusCredentialProfileReplacementAndProvenance(t *testing.T) {
} }
found := false found := false
for _, source := range sources { for _, source := range sources {
if source.Path == "notarius.llm_api_key" { if source.Path == "notarius.credentials" {
found = source.Source == overlay found = source.Source == overlay
} }
} }
if !found { if !found {
t.Fatal("Notarius credential provenance did not identify the selected profile") t.Fatal("Notarius credential provenance did not identify the selected overlay")
} }
}) })
} }
@@ -234,7 +234,7 @@ func TestNotariusCredentialRootProvenance(t *testing.T) {
const envName = "NARRATIO_NOTARIUS_ROOT_KEY" const envName = "NARRATIO_NOTARIUS_ROOT_KEY"
t.Setenv(envName, "synthetic-notarius-root-key") t.Setenv(envName, "synthetic-notarius-root-key")
dir := t.TempDir() dir := t.TempDir()
root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n llm_api_key: "+envName+"\n") root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n credentials: ["+envName+"]\n")
cfg, err := LoadPipeline(root) cfg, err := LoadPipeline(root)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -244,7 +244,7 @@ func TestNotariusCredentialRootProvenance(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for _, source := range sources { for _, source := range sources {
if source.Path == "notarius.llm_api_key" && source.Source == root { if source.Path == "notarius.credentials" && source.Source == root {
return return
} }
} }

View File

@@ -146,7 +146,7 @@ func (extractStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (re
chunkCache = "refresh" chunkCache = "refresh"
} }
request := notarius.RunRequest{ request := notarius.RunRequest{
Binary: resolvedBinary, LLMAPIKey: notariusConfig.LLMAPIKey, ConfigProfile: notariusConfig.ConfigProfile, Binary: resolvedBinary, Credentials: notariusConfig.Credentials, ConfigProfile: notariusConfig.ConfigProfile,
ConfigPath: configPath, PipelineID: notariusConfig.PipelineID, ConfigPath: configPath, PipelineID: notariusConfig.PipelineID,
InputPath: inputPath, OutputRoot: outputRoot, WorkingDirectory: workingDirectory, InputPath: inputPath, OutputRoot: outputRoot, WorkingDirectory: workingDirectory,
ReceiptPath: receiptPath, LogPath: logPath, Timeout: timeout, Resume: resume, ChunkCache: chunkCache, References: referenceBindings, ReceiptPath: receiptPath, LogPath: logPath, Timeout: timeout, Resume: resume, ChunkCache: chunkCache, References: referenceBindings,

View File

@@ -61,7 +61,7 @@ func TestExtractStageResolvesManifestInputAndBuildsExactRequest(t *testing.T) {
transcribe_url: https://transcription.example.test/transcribe transcribe_url: https://transcription.example.test/transcribe
notarius: notarius:
enabled: false enabled: false
llm_api_key: NARRATIO_EXTRACT_NOTARIUS_KEY credentials: [NARRATIO_EXTRACT_NOTARIUS_KEY]
`), 0o644); err != nil { `), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -71,7 +71,7 @@ notarius:
} }
env, m, fake := setupExtractEnv(t) env, m, fake := setupExtractEnv(t)
env.Config.Pipeline.Notarius.LLMAPIKey = loadedPipeline.Notarius.LLMAPIKey env.Config.Pipeline.Notarius.Credentials = loadedPipeline.Notarius.Credentials
configProfile := "inexpensive" configProfile := "inexpensive"
env.Config.Pipeline.Notarius.ConfigProfile = &configProfile env.Config.Pipeline.Notarius.ConfigProfile = &configProfile
result, err := (extractStage{}).Run(context.Background(), env, m) result, err := (extractStage{}).Run(context.Background(), env, m)
@@ -103,7 +103,7 @@ notarius:
if req.Resume || req.ChunkCache != "" { if req.Resume || req.ChunkCache != "" {
t.Fatalf("default cache controls = resume %v, mode %q", req.Resume, req.ChunkCache) t.Fatalf("default cache controls = resume %v, mode %q", req.Resume, req.ChunkCache)
} }
if req.LLMAPIKey != loadedPipeline.Notarius.LLMAPIKey || req.LLMAPIKey.EnvName() != credentialName || !req.LLMAPIKey.IsResolved() { if len(req.Credentials) != 1 || req.Credentials[0].EnvName() != credentialName || !req.Credentials[0].IsResolved() {
t.Fatal("request did not retain the loaded Notarius credential reference") t.Fatal("request did not retain the loaded Notarius credential reference")
} }
if req.ConfigProfile == nil || *req.ConfigProfile != configProfile { if req.ConfigProfile == nil || *req.ConfigProfile != configProfile {

View File

@@ -194,7 +194,7 @@ func TestExtractSemanticConfigExcludesLoadedCredentialChanges(t *testing.T) {
renamed := loadNotariusCredentialForSemanticTest(t, secondName, "second-value") renamed := loadNotariusCredentialForSemanticTest(t, secondName, "second-value")
baseline := downstreamSemanticEnv() baseline := downstreamSemanticEnv()
baseline.Config.Pipeline.Notarius.LLMAPIKey = first baseline.Config.Pipeline.Notarius.Credentials = []configkit.Secret{*first}
want, err := (extractStage{}).SemanticConfigFingerprint(baseline) want, err := (extractStage{}).SemanticConfigFingerprint(baseline)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -209,7 +209,9 @@ func TestExtractSemanticConfigExcludesLoadedCredentialChanges(t *testing.T) {
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
env := downstreamSemanticEnv() env := downstreamSemanticEnv()
env.Config.Pipeline.Notarius.LLMAPIKey = test.credential if test.credential != nil {
env.Config.Pipeline.Notarius.Credentials = []configkit.Secret{*test.credential}
}
got, err := (extractStage{}).SemanticConfigFingerprint(env) got, err := (extractStage{}).SemanticConfigFingerprint(env)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -225,14 +227,14 @@ func loadNotariusCredentialForSemanticTest(t *testing.T, name, value string) *co
t.Helper() t.Helper()
t.Setenv(name, value) t.Setenv(name, value)
path := filepath.Join(t.TempDir(), "pipeline.yml") path := filepath.Join(t.TempDir(), "pipeline.yml")
if err := os.WriteFile(path, []byte("whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n llm_api_key: "+name+"\n"), 0o644); err != nil { if err := os.WriteFile(path, []byte("whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n credentials: ["+name+"]\n"), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
pipeline, err := config.LoadPipeline(path) pipeline, err := config.LoadPipeline(path)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return pipeline.Notarius.LLMAPIKey return &pipeline.Notarius.Credentials[0]
} }
func TestPublishSemanticConfigSensitivityAndOperationalExclusions(t *testing.T) { func TestPublishSemanticConfigSensitivityAndOperationalExclusions(t *testing.T) {