Migrate Notarius to credential lists
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user