Track Notarius profiles in extraction reuse

This commit is contained in:
2026-09-21 15:07:36 +00:00
parent 931c9591cd
commit 995354f1a5
5 changed files with 192 additions and 12 deletions

View File

@@ -1,6 +1,6 @@
# Implementation Plan: Notarius Configuration Profile Selection
Status: Implementation in progress; Stage 1 complete.
Status: Implementation in progress; Stages 1-2 complete.
## Scope and Working Instructions
@@ -107,7 +107,13 @@ selections as an intermediate implementation.
## Stage 2: Wire Extraction and Profile-Sensitive Reuse
Status: Pending.
Status: Complete.
Completion marker (2026-09-21): Wired the selected profile into extraction,
added a shared base-selector reuse identity to both existing fingerprints, and
covered request forwarding, force precedence, semantic sensitivity, prior
no-profile encoding, and focused resume validation. Verified with
`go test ./internal/stage ./internal/adapters/notarius ./internal/config`.
Preparation:

View File

@@ -146,7 +146,8 @@ func (extractStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (re
chunkCache = "refresh"
}
request := notarius.RunRequest{
Binary: resolvedBinary, LLMAPIKey: notariusConfig.LLMAPIKey, ConfigPath: configPath, PipelineID: notariusConfig.PipelineID,
Binary: resolvedBinary, LLMAPIKey: notariusConfig.LLMAPIKey, ConfigProfile: notariusConfig.ConfigProfile,
ConfigPath: configPath, PipelineID: notariusConfig.PipelineID,
InputPath: inputPath, OutputRoot: outputRoot, WorkingDirectory: workingDirectory,
ReceiptPath: receiptPath, LogPath: logPath, Timeout: timeout, Resume: resume, ChunkCache: chunkCache, References: referenceBindings,
}
@@ -449,10 +450,11 @@ type fingerprintOutput struct {
}
type fingerprintDocument struct {
PipelineID string `json:"pipeline_id"`
Input artifacts.ExtractionInputIdentity `json:"input"`
References []extractReferenceIdentity `json:"references"`
Outputs []fingerprintOutput `json:"outputs"`
PipelineID string `json:"pipeline_id"`
ConfigProfile string `json:"config_profile,omitempty"`
Input artifacts.ExtractionInputIdentity `json:"input"`
References []extractReferenceIdentity `json:"references"`
Outputs []fingerprintOutput `json:"outputs"`
}
func extractionFingerprint(
@@ -484,7 +486,8 @@ func extractionFingerprint(
return sortedReferences[i].Path < sortedReferences[j].Path
})
payload, err := json.Marshal(fingerprintDocument{
PipelineID: cfg.PipelineID, Input: input, References: sortedReferences, Outputs: outputs,
PipelineID: cfg.PipelineID, ConfigProfile: notariusConfigProfileReuseIdentity(cfg.ConfigProfile),
Input: input, References: sortedReferences, Outputs: outputs,
})
if err != nil {
return "", err

View File

@@ -2,6 +2,8 @@ package stage
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -70,6 +72,8 @@ notarius:
env, m, fake := setupExtractEnv(t)
env.Config.Pipeline.Notarius.LLMAPIKey = loadedPipeline.Notarius.LLMAPIKey
configProfile := "inexpensive"
env.Config.Pipeline.Notarius.ConfigProfile = &configProfile
result, err := (extractStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatalf("Run() error = %v", err)
@@ -102,6 +106,9 @@ notarius:
if req.LLMAPIKey != loadedPipeline.Notarius.LLMAPIKey || req.LLMAPIKey.EnvName() != credentialName || !req.LLMAPIKey.IsResolved() {
t.Fatal("request did not retain the loaded Notarius credential reference")
}
if req.ConfigProfile == nil || *req.ConfigProfile != configProfile {
t.Fatalf("request config profile = %#v, want %q", req.ConfigProfile, configProfile)
}
if result.Metadata["reference_count"] != 0 || len(result.Metadata["references"].([]map[string]any)) != 0 {
t.Fatalf("reference metadata = count %#v references %#v", result.Metadata["reference_count"], result.Metadata["references"])
}
@@ -134,6 +141,8 @@ func TestExtractStageAppliesNotariusCacheControlsAndForcePrecedence(t *testing.T
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
env, m, fake := setupExtractEnv(t)
configProfile := "inexpensive"
env.Config.Pipeline.Notarius.ConfigProfile = &configProfile
env.Config.Pipeline.Notarius.Resume = test.configuredResume
env.Config.Pipeline.Notarius.ChunkCache = test.configuredMode
env.Force = test.force
@@ -148,6 +157,9 @@ func TestExtractStageAppliesNotariusCacheControlsAndForcePrecedence(t *testing.T
if request.Resume != test.wantResume || request.ChunkCache != test.wantMode {
t.Fatalf("cache controls = resume %v, mode %q; want resume %v, mode %q", request.Resume, request.ChunkCache, test.wantResume, test.wantMode)
}
if request.ConfigProfile == nil || *request.ConfigProfile != configProfile {
t.Fatalf("config profile = %#v, want %q", request.ConfigProfile, configProfile)
}
})
}
}
@@ -971,6 +983,60 @@ func TestExtractionFingerprintIsIndependentOfOutputMapOrder(t *testing.T) {
}
}
func TestExtractionFingerprintBaseSelectorsPreservePriorPayloadShape(t *testing.T) {
input := artifacts.ExtractionInputIdentity{
Checksum: "checksum", SourceID: artifacts.ArtifactTranscriptFinalTrimmed,
ProducerStage: "trim", OutputKind: artifacts.TranscriptOutputKindFinalTrimmed,
ProducerRunID: "trim-run", Provenance: "manifest.trim.outputs",
}
references := []extractReferenceIdentity{{
Selector: "party", SourceID: artifactpolicy.SourceInputParty,
Path: "inputs/party.yml", Checksum: "party-checksum", SizeBytes: 34,
}}
cfg := &config.NotariusConfig{
PipelineID: "pipeline",
Outputs: map[string]config.NotariusOutputConfig{
"alpha": {LaneID: "a", MediaType: "application/json", SchemaID: "a", SchemaVersion: "v1"},
},
}
legacyPayload, err := json.Marshal(struct {
PipelineID string `json:"pipeline_id"`
Input artifacts.ExtractionInputIdentity `json:"input"`
References []extractReferenceIdentity `json:"references"`
Outputs []fingerprintOutput `json:"outputs"`
}{
PipelineID: cfg.PipelineID,
Input: input,
References: references,
Outputs: []fingerprintOutput{{
Key: "alpha", LaneID: "a", MediaType: "application/json", SchemaID: "a", SchemaVersion: "v1",
}},
})
if err != nil {
t.Fatal(err)
}
legacyDigest := sha256.Sum256(legacyPayload)
want := hex.EncodeToString(legacyDigest[:])
for _, profile := range []*string{nil, stringPointerForExtractTest(""), stringPointerForExtractTest("default")} {
cfg.ConfigProfile = profile
got, err := extractionFingerprint(cfg, input, references)
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("base selection fingerprint = %q, want prior no-profile payload digest %q", got, want)
}
}
cfg.ConfigProfile = stringPointerForExtractTest("inexpensive")
named, err := extractionFingerprint(cfg, input, references)
if err != nil {
t.Fatal(err)
}
if named == want {
t.Fatal("named selection retained prior no-profile fingerprint")
}
}
func TestExtractStageResumeValidationAcceptsCurrentImmutableResult(t *testing.T) {
env, m, _ := setupExtractEnv(t)
env.Config.Pipeline.Notarius.References = map[string]string{
@@ -989,6 +1055,31 @@ func TestExtractStageResumeValidationAcceptsCurrentImmutableResult(t *testing.T)
}
}
func TestExtractStageResumeValidationTracksConfigProfile(t *testing.T) {
env, m, _ := setupExtractEnv(t)
selected := "inexpensive"
env.Config.Pipeline.Notarius.ConfigProfile = &selected
seedSucceededExtractResult(t, env, m)
validation, err := (extractStage{}).ValidateResume(context.Background(), env, m)
if err != nil {
t.Fatalf("ValidateResume(same selection) error = %v", err)
}
if !validation.Resumable {
t.Fatalf("same selection validation = %#v, want reusable recorded bundle", validation)
}
changed := "experimental"
env.Config.Pipeline.Notarius.ConfigProfile = &changed
validation, err = (extractStage{}).ValidateResume(context.Background(), env, m)
if err != nil {
t.Fatalf("ValidateResume(changed selection) error = %v", err)
}
if validation.Resumable || !strings.Contains(validation.Reason, "invocation contract changed") {
t.Fatalf("changed selection validation = %#v, want non-resumable invocation evidence", validation)
}
}
func TestExtractStageResumeValidationRejectsChangedPreparedReferenceIdentity(t *testing.T) {
env, m, _ := setupExtractEnv(t)
env.Config.Pipeline.Notarius.References = map[string]string{
@@ -1277,6 +1368,8 @@ type extractFixture struct {
inputPath string
}
func stringPointerForExtractTest(value string) *string { return &value }
func extractFixtureFromEnv(t *testing.T, env *Env, m *manifest.Manifest) extractFixture {
t.Helper()
paths := sessionPathsForEnv(env, m.SessionID)

View File

@@ -20,10 +20,18 @@ const (
)
type extractSemanticConfig struct {
Enabled bool `json:"enabled"`
PipelineID string `json:"pipeline_id,omitempty"`
References []extractSemanticReference `json:"references"`
Outputs []extractSemanticOutput `json:"outputs"`
Enabled bool `json:"enabled"`
PipelineID string `json:"pipeline_id,omitempty"`
ConfigProfile string `json:"config_profile,omitempty"`
References []extractSemanticReference `json:"references"`
Outputs []extractSemanticOutput `json:"outputs"`
}
func notariusConfigProfileReuseIdentity(profile *string) string {
if profile == nil || *profile == "" || *profile == "default" {
return ""
}
return *profile
}
type extractSemanticReference struct {
@@ -77,6 +85,7 @@ func (extractStage) SemanticConfigFingerprint(env *Env) (manifest.SemanticConfig
}
payload.Enabled = true
payload.PipelineID = strings.TrimSpace(cfg.PipelineID)
payload.ConfigProfile = notariusConfigProfileReuseIdentity(cfg.ConfigProfile)
for rawSelector, rawSourceID := range cfg.References {
selector, err := notariusref.NormalizeSelector(rawSelector)
if err != nil {

View File

@@ -9,6 +9,7 @@ import (
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
func testSecretReference(name string) *configkit.Secret {
@@ -96,6 +97,74 @@ func TestExtractSemanticConfigSensitivityAndOperationalExclusions(t *testing.T)
}
})
}
profilePointer := func(value string) *string { return &value }
baseFingerprints := make([]manifest.SemanticConfigFingerprint, 0, 3)
for _, profile := range []*string{nil, profilePointer(""), profilePointer("default")} {
env := downstreamSemanticEnv()
env.Config.Pipeline.Notarius.ConfigProfile = profile
fingerprint, err := (extractStage{}).SemanticConfigFingerprint(env)
if err != nil {
t.Fatal(err)
}
baseFingerprints = append(baseFingerprints, fingerprint)
}
for index := 1; index < len(baseFingerprints); index++ {
if !baseFingerprints[0].Equal(baseFingerprints[index]) {
t.Fatal("nil, empty, and default Notarius selections must share base reuse evidence")
}
}
profiled := downstreamSemanticEnv()
profiled.Config.Pipeline.Notarius.ConfigProfile = profilePointer("inexpensive")
firstProfile, err := (extractStage{}).SemanticConfigFingerprint(profiled)
if err != nil {
t.Fatal(err)
}
if baseFingerprints[0].Equal(firstProfile) {
t.Fatal("base to named Notarius selection retained semantic fingerprint")
}
unchangedProfile, err := (extractStage{}).SemanticConfigFingerprint(profiled)
if err != nil {
t.Fatal(err)
}
if !firstProfile.Equal(unchangedProfile) {
t.Fatal("unchanged Notarius selection changed semantic fingerprint")
}
profiled.Config.Pipeline.Notarius.ConfigProfile = profilePointer("experimental")
secondProfile, err := (extractStage{}).SemanticConfigFingerprint(profiled)
if err != nil {
t.Fatal(err)
}
if firstProfile.Equal(secondProfile) {
t.Fatal("named to different named Notarius selection retained semantic fingerprint")
}
profiled.Config.Pipeline.Notarius.ConfigProfile = nil
returnedToBase, err := (extractStage{}).SemanticConfigFingerprint(profiled)
if err != nil {
t.Fatal(err)
}
if !baseFingerprints[0].Equal(returnedToBase) {
t.Fatal("named to base Notarius selection did not restore base semantic evidence")
}
if secondProfile.Equal(returnedToBase) {
t.Fatal("named to base Notarius selection retained named semantic evidence")
}
disabledBase := downstreamSemanticEnv()
disabledBase.Config.Pipeline.Notarius.Enabled = false
disabledNamed := downstreamSemanticEnv()
disabledNamed.Config.Pipeline.Notarius.Enabled = false
disabledNamed.Config.Pipeline.Notarius.ConfigProfile = profilePointer("ignored")
disabledBaseFingerprint, err := (extractStage{}).SemanticConfigFingerprint(disabledBase)
if err != nil {
t.Fatal(err)
}
disabledNamedFingerprint, err := (extractStage{}).SemanticConfigFingerprint(disabledNamed)
if err != nil {
t.Fatal(err)
}
if !disabledBaseFingerprint.Equal(disabledNamedFingerprint) {
t.Fatal("disabled extraction became sensitive to unused Notarius selection")
}
}
func TestExtractResumeValidationExcludesOperationalSettings(t *testing.T) {