Add redacted run metadata artifacts
This commit is contained in:
@@ -36,6 +36,20 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
|
|||||||
return nil, nil, runDir, fmt.Errorf("%s: %w", phase, err)
|
return nil, nil, runDir, fmt.Errorf("%s: %w", phase, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := runDir.WriteInvocationMetadata(diagnostics.InvocationMetadata{
|
||||||
|
Operation: "process",
|
||||||
|
TranscriptPath: inv.TranscriptPath,
|
||||||
|
GlossaryPath: inv.GlossaryPath,
|
||||||
|
OutputPath: inv.OutputPath,
|
||||||
|
ReportJSONPath: inv.ReportJSONPath,
|
||||||
|
Modules: append([]string(nil), inv.Config.Modules...),
|
||||||
|
}); err != nil {
|
||||||
|
_ = runDir.WriteErrorLog(fmt.Sprintf("invocation_metadata: %v", err))
|
||||||
|
}
|
||||||
|
if err := runDir.WriteEffectiveConfig(inv.Config); err != nil {
|
||||||
|
_ = runDir.WriteErrorLog(fmt.Sprintf("effective_config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
transcriptBytes, err := coreio.ReadRequiredFile(inv.TranscriptPath, "transcript")
|
transcriptBytes, err := coreio.ReadRequiredFile(inv.TranscriptPath, "transcript")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fail("transcript_read", err)
|
return fail("transcript_read", err)
|
||||||
|
|||||||
@@ -515,6 +515,137 @@ func TestRunProcessChunkingSummaryArtifactWritten(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunProcessWritesRedactedRunMetadataArtifacts(t *testing.T) {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
|
workDir := t.TempDir()
|
||||||
|
outputPath := filepath.Join(t.TempDir(), "normalized.json")
|
||||||
|
reportPath := filepath.Join(t.TempDir(), "report.json")
|
||||||
|
primaryKey := "super-secret-primary-key"
|
||||||
|
validationKey := "super-secret-validation-key"
|
||||||
|
t.Setenv("AUDITA_LLM_API_KEY", primaryKey)
|
||||||
|
t.Setenv("AUDITA_VALIDATION_LLM_API_KEY", validationKey)
|
||||||
|
|
||||||
|
exitCode := Run([]string{
|
||||||
|
"process",
|
||||||
|
fixturePath("tiny_transcript.json"),
|
||||||
|
"--glossary",
|
||||||
|
fixturePath("tiny_glossary.yaml"),
|
||||||
|
"--output",
|
||||||
|
outputPath,
|
||||||
|
"--report-json",
|
||||||
|
reportPath,
|
||||||
|
"--work-dir",
|
||||||
|
workDir,
|
||||||
|
"--work-dir-retention",
|
||||||
|
"always",
|
||||||
|
}, &stdout, &stderr)
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
||||||
|
}
|
||||||
|
if stdout.Len() != 0 {
|
||||||
|
t.Fatalf("expected empty stdout when --output is provided, got %q", stdout.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Existing report-json behavior should still work.
|
||||||
|
report := readProcessReport(t, reportPath)
|
||||||
|
if report.Status != "success" {
|
||||||
|
t.Fatalf("expected success report status, got %q", report.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
runPath := onlyRunDir(t, workDir)
|
||||||
|
invocationPath := filepath.Join(runPath, "invocation.json")
|
||||||
|
configPath := filepath.Join(runPath, "effective-config.json")
|
||||||
|
|
||||||
|
if _, err := os.Stat(invocationPath); err != nil {
|
||||||
|
t.Fatalf("expected invocation metadata artifact: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
t.Fatalf("expected effective config metadata artifact: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
invocationBytes := readFile(t, invocationPath)
|
||||||
|
configBytes := readFile(t, configPath)
|
||||||
|
if strings.Contains(string(invocationBytes), primaryKey) || strings.Contains(string(invocationBytes), validationKey) {
|
||||||
|
t.Fatalf("invocation artifact leaked API key material")
|
||||||
|
}
|
||||||
|
if strings.Contains(string(configBytes), primaryKey) || strings.Contains(string(configBytes), validationKey) {
|
||||||
|
t.Fatalf("effective config artifact leaked API key material")
|
||||||
|
}
|
||||||
|
|
||||||
|
var invocation struct {
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
TranscriptPath string `json:"transcript_path"`
|
||||||
|
GlossaryPath string `json:"glossary_path"`
|
||||||
|
OutputPath string `json:"output_path"`
|
||||||
|
ReportJSONPath string `json:"report_json_path"`
|
||||||
|
Modules []string `json:"modules"`
|
||||||
|
RunID string `json:"run_id"`
|
||||||
|
StartedAt string `json:"started_at"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(invocationBytes, &invocation); err != nil {
|
||||||
|
t.Fatalf("failed to parse invocation metadata: %v", err)
|
||||||
|
}
|
||||||
|
if invocation.Operation != "process" {
|
||||||
|
t.Fatalf("expected operation=process, got %q", invocation.Operation)
|
||||||
|
}
|
||||||
|
if invocation.TranscriptPath != fixturePath("tiny_transcript.json") {
|
||||||
|
t.Fatalf("unexpected transcript_path: %q", invocation.TranscriptPath)
|
||||||
|
}
|
||||||
|
if invocation.GlossaryPath != fixturePath("tiny_glossary.yaml") {
|
||||||
|
t.Fatalf("unexpected glossary_path: %q", invocation.GlossaryPath)
|
||||||
|
}
|
||||||
|
if invocation.OutputPath != outputPath {
|
||||||
|
t.Fatalf("unexpected output_path: %q", invocation.OutputPath)
|
||||||
|
}
|
||||||
|
if invocation.ReportJSONPath != reportPath {
|
||||||
|
t.Fatalf("unexpected report_json_path: %q", invocation.ReportJSONPath)
|
||||||
|
}
|
||||||
|
if len(invocation.Modules) == 0 {
|
||||||
|
t.Fatalf("expected non-empty modules list in invocation metadata")
|
||||||
|
}
|
||||||
|
if invocation.RunID == "" {
|
||||||
|
t.Fatalf("expected non-empty run_id in invocation metadata")
|
||||||
|
}
|
||||||
|
if invocation.StartedAt == "" {
|
||||||
|
t.Fatalf("expected non-empty started_at in invocation metadata")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunProcessStdoutTranscriptWhenNoOutputWithRunMetadataArtifacts(t *testing.T) {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
|
workDir := t.TempDir()
|
||||||
|
|
||||||
|
exitCode := Run([]string{
|
||||||
|
"process",
|
||||||
|
fixturePath("tiny_transcript.json"),
|
||||||
|
"--glossary",
|
||||||
|
fixturePath("tiny_glossary.yaml"),
|
||||||
|
"--work-dir",
|
||||||
|
workDir,
|
||||||
|
"--work-dir-retention",
|
||||||
|
"always",
|
||||||
|
}, &stdout, &stderr)
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := schema.ParseTranscriptJSON(stdout.Bytes()); err != nil {
|
||||||
|
t.Fatalf("expected stdout to contain only transcript JSON, got parse error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
runPath := onlyRunDir(t, workDir)
|
||||||
|
if _, err := os.Stat(filepath.Join(runPath, "invocation.json")); err != nil {
|
||||||
|
t.Fatalf("expected invocation metadata artifact: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(runPath, "effective-config.json")); err != nil {
|
||||||
|
t.Fatalf("expected effective config metadata artifact: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunProcessTargetSectionsThroughCLI(t *testing.T) {
|
func TestRunProcessTargetSectionsThroughCLI(t *testing.T) {
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/core/chunking"
|
"gitea.maximumdirect.net/eric/audita/internal/core/chunking"
|
||||||
|
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/core/normalization"
|
"gitea.maximumdirect.net/eric/audita/internal/core/normalization"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
|
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
|
||||||
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
||||||
@@ -20,6 +21,18 @@ type RunDirectory struct {
|
|||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InvocationMetadata captures non-secret invocation details for diagnostics.
|
||||||
|
type InvocationMetadata struct {
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
TranscriptPath string `json:"transcript_path"`
|
||||||
|
GlossaryPath string `json:"glossary_path"`
|
||||||
|
OutputPath string `json:"output_path,omitempty"`
|
||||||
|
ReportJSONPath string `json:"report_json_path,omitempty"`
|
||||||
|
Modules []string `json:"modules"`
|
||||||
|
RunID string `json:"run_id"`
|
||||||
|
StartedAt time.Time `json:"started_at"`
|
||||||
|
}
|
||||||
|
|
||||||
// NewRunDirectory creates a new run directory under the configured work dir
|
// NewRunDirectory creates a new run directory under the configured work dir
|
||||||
func NewRunDirectory(workDir, retention string) (*RunDirectory, error) {
|
func NewRunDirectory(workDir, retention string) (*RunDirectory, error) {
|
||||||
if workDir == "" {
|
if workDir == "" {
|
||||||
@@ -51,6 +64,46 @@ func (r *RunDirectory) Path() string {
|
|||||||
return r.path
|
return r.path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RunDirectory) runID() string {
|
||||||
|
return filepath.Base(r.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteInvocationMetadata writes invocation metadata for this run.
|
||||||
|
func (r *RunDirectory) WriteInvocationMetadata(metadata InvocationMetadata) error {
|
||||||
|
if metadata.RunID == "" {
|
||||||
|
metadata.RunID = r.runID()
|
||||||
|
}
|
||||||
|
if metadata.StartedAt.IsZero() {
|
||||||
|
metadata.StartedAt = r.createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(r.path, "invocation.json")
|
||||||
|
bytes, err := json.MarshalIndent(metadata, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal invocation metadata: %w", err)
|
||||||
|
}
|
||||||
|
bytes = append(bytes, '\n')
|
||||||
|
if err := os.WriteFile(path, bytes, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write invocation metadata: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteEffectiveConfig writes redacted effective config metadata for this run.
|
||||||
|
func (r *RunDirectory) WriteEffectiveConfig(cfg config.Config) error {
|
||||||
|
path := filepath.Join(r.path, "effective-config.json")
|
||||||
|
redacted := cfg.Redacted()
|
||||||
|
bytes, err := json.MarshalIndent(redacted, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal effective config: %w", err)
|
||||||
|
}
|
||||||
|
bytes = append(bytes, '\n')
|
||||||
|
if err := os.WriteFile(path, bytes, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write effective config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// WriteSourceTranscript writes the source transcript artifact
|
// WriteSourceTranscript writes the source transcript artifact
|
||||||
func (r *RunDirectory) WriteSourceTranscript(transcript *schema.SourceTranscript, raw []byte) error {
|
func (r *RunDirectory) WriteSourceTranscript(transcript *schema.SourceTranscript, raw []byte) error {
|
||||||
// Write raw source for reference
|
// Write raw source for reference
|
||||||
|
|||||||
Reference in New Issue
Block a user