Adopt Audita v2 configuration and subprocess contract
This commit is contained in:
@@ -3,12 +3,14 @@ package audita
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||
)
|
||||
@@ -16,93 +18,27 @@ import (
|
||||
// MaxProcessedOutputBytes bounds Audita's processed-transcript JSON result.
|
||||
const MaxProcessedOutputBytes int64 = 64 * 1024 * 1024
|
||||
|
||||
// MaxReportOutputBytes bounds Audita's optional report JSON result.
|
||||
// MaxReportOutputBytes bounds each optional Audita report.
|
||||
const MaxReportOutputBytes int64 = 16 * 1024 * 1024
|
||||
|
||||
// SubprocessRunnerConfig defines deterministic settings for Audita CLI execution.
|
||||
type SubprocessRunnerConfig struct {
|
||||
Binary string
|
||||
Timeout time.Duration
|
||||
LLMAPIKey string
|
||||
LLMAPIKeyReference string
|
||||
Modules []string
|
||||
BaseURL string
|
||||
Model string
|
||||
TranscriptDescription string
|
||||
ConfigPath string
|
||||
OutputSchema string
|
||||
WorkDirRetention string
|
||||
TotalLLMConcurrency *int
|
||||
ProposalLLMConcurrency *int
|
||||
ValidationModel string
|
||||
ValidationLLMConcurrency *int
|
||||
Report bool
|
||||
Binary string
|
||||
Timeout time.Duration
|
||||
Credentials []configkit.Secret
|
||||
Modules []string
|
||||
Overlay string
|
||||
ProposalProfile string
|
||||
ValidationProfile string
|
||||
TranscriptDescription string
|
||||
ConfigPath string
|
||||
OutputSchema string
|
||||
}
|
||||
|
||||
// SubprocessRunner invokes Audita via subprocess.
|
||||
type SubprocessRunner struct {
|
||||
binary string
|
||||
timeout time.Duration
|
||||
llmAPIKey string
|
||||
llmAPIKeyReference string
|
||||
modules []string
|
||||
baseURL string
|
||||
model string
|
||||
transcriptDescription string
|
||||
configPath string
|
||||
outputSchema string
|
||||
workDirRetention string
|
||||
totalLLMConcurrency *int
|
||||
proposalLLMConcurrency *int
|
||||
validationModel string
|
||||
validationLLMConcurrency *int
|
||||
report bool
|
||||
}
|
||||
|
||||
// NewSubprocessRunnerFromConfigValues parses config-derived values once.
|
||||
func NewSubprocessRunnerFromConfigValues(
|
||||
binary string,
|
||||
timeout string,
|
||||
llmAPIKey string,
|
||||
llmAPIKeyReference string,
|
||||
modules []string,
|
||||
baseURL string,
|
||||
model string,
|
||||
transcriptDescription string,
|
||||
configPath string,
|
||||
outputSchema string,
|
||||
workDirRetention string,
|
||||
totalLLMConcurrency *int,
|
||||
proposalLLMConcurrency *int,
|
||||
validationModel string,
|
||||
validationLLMConcurrency *int,
|
||||
report bool,
|
||||
) (*SubprocessRunner, error) {
|
||||
if strings.TrimSpace(timeout) == "" {
|
||||
return nil, fmt.Errorf("audita timeout is required")
|
||||
}
|
||||
parsedTimeout, err := time.ParseDuration(timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse audita timeout %q: %w", timeout, err)
|
||||
}
|
||||
return NewSubprocessRunner(SubprocessRunnerConfig{
|
||||
Binary: binary,
|
||||
Timeout: parsedTimeout,
|
||||
LLMAPIKey: llmAPIKey,
|
||||
LLMAPIKeyReference: llmAPIKeyReference,
|
||||
Modules: modules,
|
||||
BaseURL: baseURL,
|
||||
Model: model,
|
||||
TranscriptDescription: transcriptDescription,
|
||||
ConfigPath: configPath,
|
||||
OutputSchema: outputSchema,
|
||||
WorkDirRetention: workDirRetention,
|
||||
TotalLLMConcurrency: totalLLMConcurrency,
|
||||
ProposalLLMConcurrency: proposalLLMConcurrency,
|
||||
ValidationModel: validationModel,
|
||||
ValidationLLMConcurrency: validationLLMConcurrency,
|
||||
Report: report,
|
||||
})
|
||||
cfg SubprocessRunnerConfig
|
||||
run func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error)
|
||||
}
|
||||
|
||||
// NewSubprocessRunner constructs a validated Audita subprocess runner.
|
||||
@@ -113,281 +49,177 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error)
|
||||
if cfg.Timeout <= 0 {
|
||||
return nil, fmt.Errorf("audita timeout must be > 0")
|
||||
}
|
||||
if (cfg.LLMAPIKey == "") != (strings.TrimSpace(cfg.LLMAPIKeyReference) == "") {
|
||||
return nil, fmt.Errorf("audita credential value and reference must be provided together")
|
||||
}
|
||||
for i, module := range cfg.Modules {
|
||||
if strings.TrimSpace(module) == "" {
|
||||
return nil, fmt.Errorf("audita module at index %d is empty", i)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(cfg.BaseURL) != "" {
|
||||
u, err := url.Parse(cfg.BaseURL)
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("audita base url %q is invalid: %w", cfg.BaseURL, err)
|
||||
}
|
||||
return nil, fmt.Errorf("audita base url %q is invalid", cfg.BaseURL)
|
||||
}
|
||||
}
|
||||
if cfg.TotalLLMConcurrency != nil && *cfg.TotalLLMConcurrency <= 0 {
|
||||
return nil, fmt.Errorf("audita total llm concurrency must be > 0 when provided")
|
||||
}
|
||||
if cfg.ProposalLLMConcurrency != nil && *cfg.ProposalLLMConcurrency <= 0 {
|
||||
return nil, fmt.Errorf("audita proposal llm concurrency must be > 0 when provided")
|
||||
}
|
||||
if cfg.ValidationLLMConcurrency != nil && *cfg.ValidationLLMConcurrency <= 0 {
|
||||
return nil, fmt.Errorf("audita validation llm concurrency must be > 0 when provided")
|
||||
}
|
||||
switch strings.TrimSpace(cfg.OutputSchema) {
|
||||
case "", "bare-segments", "audita-v1":
|
||||
default:
|
||||
return nil, fmt.Errorf("audita output schema must be one of: bare-segments, audita-v1")
|
||||
}
|
||||
switch strings.TrimSpace(cfg.WorkDirRetention) {
|
||||
case "", "always", "auto", "never":
|
||||
default:
|
||||
return nil, fmt.Errorf("audita work dir retention must be one of: always, auto, never")
|
||||
cfg.Binary = strings.TrimSpace(cfg.Binary)
|
||||
cfg.Credentials = append([]configkit.Secret(nil), cfg.Credentials...)
|
||||
cfg.Modules = cloneModules(cfg.Modules)
|
||||
for i := range cfg.Modules {
|
||||
cfg.Modules[i] = strings.TrimSpace(cfg.Modules[i])
|
||||
}
|
||||
|
||||
modules := make([]string, len(cfg.Modules))
|
||||
for i, m := range cfg.Modules {
|
||||
modules[i] = strings.TrimSpace(m)
|
||||
}
|
||||
|
||||
return &SubprocessRunner{
|
||||
binary: strings.TrimSpace(cfg.Binary),
|
||||
timeout: cfg.Timeout,
|
||||
llmAPIKey: cfg.LLMAPIKey,
|
||||
llmAPIKeyReference: strings.TrimSpace(cfg.LLMAPIKeyReference),
|
||||
modules: modules,
|
||||
baseURL: strings.TrimSpace(cfg.BaseURL),
|
||||
model: strings.TrimSpace(cfg.Model),
|
||||
transcriptDescription: strings.TrimSpace(cfg.TranscriptDescription),
|
||||
configPath: strings.TrimSpace(cfg.ConfigPath),
|
||||
outputSchema: strings.TrimSpace(cfg.OutputSchema),
|
||||
workDirRetention: strings.TrimSpace(cfg.WorkDirRetention),
|
||||
totalLLMConcurrency: cfg.TotalLLMConcurrency,
|
||||
proposalLLMConcurrency: cfg.ProposalLLMConcurrency,
|
||||
validationModel: strings.TrimSpace(cfg.ValidationModel),
|
||||
validationLLMConcurrency: cfg.ValidationLLMConcurrency,
|
||||
report: cfg.Report,
|
||||
}, nil
|
||||
return &SubprocessRunner{cfg: cfg, run: subprocess.Run}, nil
|
||||
}
|
||||
|
||||
// Run executes Audita process with deterministic flags and validates output artifacts.
|
||||
func cloneModules(modules []string) []string {
|
||||
if modules == nil {
|
||||
return nil
|
||||
}
|
||||
return append([]string{}, modules...)
|
||||
}
|
||||
|
||||
// Run executes Audita process and validates its corrected transcript.
|
||||
func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishResult, error) {
|
||||
if r == nil {
|
||||
return PolishResult{}, fmt.Errorf("audita subprocess runner is nil")
|
||||
}
|
||||
if strings.TrimSpace(req.MergedTranscriptPath) == "" {
|
||||
return PolishResult{}, fmt.Errorf("audita merged transcript path is required")
|
||||
for _, field := range []struct{ name, value string }{
|
||||
{"merged transcript path", req.MergedTranscriptPath},
|
||||
{"glossary path", req.GlossaryPath},
|
||||
{"output processed path", req.OutputProcessedPath},
|
||||
} {
|
||||
if strings.TrimSpace(field.value) == "" {
|
||||
return PolishResult{}, fmt.Errorf("audita %s is required", field.name)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(req.GlossaryPath) == "" {
|
||||
return PolishResult{}, fmt.Errorf("audita glossary path is required")
|
||||
modules := req.Modules
|
||||
if modules == nil {
|
||||
modules = r.cfg.Modules
|
||||
}
|
||||
if strings.TrimSpace(req.OutputProcessedPath) == "" {
|
||||
return PolishResult{}, fmt.Errorf("audita output processed path is required")
|
||||
for i, module := range modules {
|
||||
if strings.TrimSpace(module) == "" {
|
||||
return PolishResult{}, fmt.Errorf("audita module at index %d is empty", i)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(req.WorkDir) == "" {
|
||||
return PolishResult{}, fmt.Errorf("audita work dir is required")
|
||||
modules = cloneModules(modules)
|
||||
for i := range modules {
|
||||
modules[i] = strings.TrimSpace(modules[i])
|
||||
}
|
||||
if r.report && strings.TrimSpace(req.ReportPath) == "" {
|
||||
return PolishResult{}, fmt.Errorf("audita report is enabled but report path is missing")
|
||||
args := r.buildArgs(req, modules)
|
||||
env, names, err := subprocess.CredentialEnvironment(r.cfg.Credentials)
|
||||
if err != nil {
|
||||
return PolishResult{}, fmt.Errorf("audita credentials: %w", err)
|
||||
}
|
||||
|
||||
reqModules := req.Modules
|
||||
if reqModules == nil {
|
||||
reqModules = append([]string(nil), r.modules...)
|
||||
}
|
||||
args := r.buildArgs(req, reqModules)
|
||||
|
||||
credentialPresent := r.llmAPIKeyReference != ""
|
||||
env := map[string]string{}
|
||||
if credentialPresent {
|
||||
env["AUDITA_LLM_API_KEY"] = r.llmAPIKey
|
||||
}
|
||||
|
||||
if req.GeneratedConfigPath != "" {
|
||||
if err := r.writeInvocationConfig(req, args, reqModules, credentialPresent); err != nil {
|
||||
if err := r.writeInvocationConfig(req, args, modules, names); err != nil {
|
||||
return PolishResult{}, fmt.Errorf("write audita invocation config %q: %w", req.GeneratedConfigPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
runRes, err := subprocess.Run(ctx, subprocess.RunRequest{
|
||||
Executable: r.binary,
|
||||
Args: args,
|
||||
Timeout: r.timeout,
|
||||
EnvOverrides: env,
|
||||
DiagnosticOwner: "audita",
|
||||
StdoutLogPath: req.StdoutLogPath,
|
||||
StderrLogPath: req.StderrLogPath,
|
||||
run := r.run
|
||||
if run == nil {
|
||||
run = subprocess.Run
|
||||
}
|
||||
runRes, runErr := run(ctx, subprocess.RunRequest{
|
||||
Executable: r.cfg.Binary, Args: args, Timeout: r.cfg.Timeout,
|
||||
EnvOverrides: env, SensitiveEnvNames: names, DiagnosticOwner: "audita",
|
||||
StdoutLogPath: req.StdoutLogPath, StderrLogPath: req.StderrLogPath,
|
||||
})
|
||||
if err != nil {
|
||||
wrappedMessage := fmt.Sprintf(
|
||||
"run audita process (binary=%q, stdout_log=%q, stderr_log=%q)",
|
||||
r.binary,
|
||||
req.StdoutLogPath,
|
||||
req.StderrLogPath,
|
||||
)
|
||||
wrappedMessage = addSubprocessStreamHint(wrappedMessage, err)
|
||||
return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf(
|
||||
"%s: %w",
|
||||
wrappedMessage,
|
||||
err,
|
||||
)
|
||||
result := r.result(req, modules, names, runRes)
|
||||
if runErr != nil {
|
||||
result.Reports, _ = discoverReports(req.OutputProcessedPath)
|
||||
return result, fmt.Errorf("run audita process (binary=%q, output_dir=%q, stdout_log=%q, stderr_log=%q): %w",
|
||||
r.cfg.Binary, filepath.Dir(req.OutputProcessedPath), req.StdoutLogPath, req.StderrLogPath, runErr)
|
||||
}
|
||||
|
||||
if err := validateProcessedOutput(req.OutputProcessedPath); err != nil {
|
||||
return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf("validate audita processed output %q: %w", req.OutputProcessedPath, err)
|
||||
result.Reports, _ = discoverReports(req.OutputProcessedPath)
|
||||
return result, fmt.Errorf("validate audita processed output %q: %w", req.OutputProcessedPath, err)
|
||||
}
|
||||
if r.report {
|
||||
if err := validateJSONFile(req.ReportPath); err != nil {
|
||||
return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf("validate audita report output %q: %w", req.ReportPath, err)
|
||||
}
|
||||
reports, err := discoverReports(req.OutputProcessedPath)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return PolishResult{
|
||||
ProcessedTranscriptPath: req.OutputProcessedPath,
|
||||
ReportPath: req.ReportPath,
|
||||
WorkDir: req.WorkDir,
|
||||
StdoutLogPath: req.StdoutLogPath,
|
||||
StderrLogPath: req.StderrLogPath,
|
||||
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||
ExitCode: runRes.ExitCode,
|
||||
Duration: runRes.Duration,
|
||||
InvokedBinary: r.binary,
|
||||
Metadata: map[string]any{
|
||||
"adapter": "audita_subprocess",
|
||||
"modules": reqModules,
|
||||
"base_url": r.baseURL,
|
||||
"model": r.model,
|
||||
"transcript_description": r.transcriptDescription,
|
||||
"config_path": r.configPath,
|
||||
"output_schema": r.outputSchema,
|
||||
"work_dir_retention": r.workDirRetention,
|
||||
"validation_model": r.validationModel,
|
||||
"total_llm_concurrency": r.totalLLMConcurrency,
|
||||
"proposal_llm_concurrency": r.proposalLLMConcurrency,
|
||||
"validation_llm_concurrency": r.validationLLMConcurrency,
|
||||
"credential_env_var": r.llmAPIKeyReference,
|
||||
"credential_present": credentialPresent,
|
||||
},
|
||||
}, nil
|
||||
result.Reports = reports
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *SubprocessRunner) failureResult(req PolishRequest, modules []string, runRes subprocess.RunResult, credentialPresent bool) PolishResult {
|
||||
func (r *SubprocessRunner) result(req PolishRequest, modules, names []string, runRes subprocess.RunResult) PolishResult {
|
||||
return PolishResult{
|
||||
ProcessedTranscriptPath: req.OutputProcessedPath,
|
||||
ReportPath: req.ReportPath,
|
||||
WorkDir: req.WorkDir,
|
||||
StdoutLogPath: req.StdoutLogPath,
|
||||
StderrLogPath: req.StderrLogPath,
|
||||
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||
ExitCode: runRes.ExitCode,
|
||||
Duration: runRes.Duration,
|
||||
InvokedBinary: r.binary,
|
||||
StdoutLogPath: req.StdoutLogPath, StderrLogPath: req.StderrLogPath,
|
||||
GeneratedConfigPath: req.GeneratedConfigPath, ExitCode: runRes.ExitCode,
|
||||
Duration: runRes.Duration, InvokedBinary: r.cfg.Binary,
|
||||
Metadata: map[string]any{
|
||||
"adapter": "audita_subprocess",
|
||||
"modules": modules,
|
||||
"base_url": r.baseURL,
|
||||
"model": r.model,
|
||||
"transcript_description": r.transcriptDescription,
|
||||
"config_path": r.configPath,
|
||||
"output_schema": r.outputSchema,
|
||||
"work_dir_retention": r.workDirRetention,
|
||||
"validation_model": r.validationModel,
|
||||
"total_llm_concurrency": r.totalLLMConcurrency,
|
||||
"proposal_llm_concurrency": r.proposalLLMConcurrency,
|
||||
"validation_llm_concurrency": r.validationLLMConcurrency,
|
||||
"credential_env_var": r.llmAPIKeyReference,
|
||||
"credential_present": credentialPresent,
|
||||
"adapter": "audita_subprocess", "modules": modules,
|
||||
"overlay": r.cfg.Overlay, "proposal_profile": r.cfg.ProposalProfile,
|
||||
"validation_profile": r.cfg.ValidationProfile,
|
||||
"transcript_description": r.cfg.TranscriptDescription,
|
||||
"config_path": r.cfg.ConfigPath, "output_schema": r.cfg.OutputSchema,
|
||||
"credential_names": names,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SubprocessRunner) buildArgs(req PolishRequest, modules []string) []string {
|
||||
args := []string{
|
||||
"process",
|
||||
req.MergedTranscriptPath,
|
||||
"--glossary", req.GlossaryPath,
|
||||
"--output", req.OutputProcessedPath,
|
||||
"--work-dir", req.WorkDir,
|
||||
}
|
||||
if r.baseURL != "" {
|
||||
args = append(args, "--base-url", r.baseURL)
|
||||
}
|
||||
if r.model != "" {
|
||||
args = append(args, "--model", r.model)
|
||||
}
|
||||
if len(modules) > 0 {
|
||||
args := []string{"process", req.MergedTranscriptPath, "--glossary", req.GlossaryPath, "--output", req.OutputProcessedPath}
|
||||
if modules != nil {
|
||||
args = append(args, "--modules", strings.Join(modules, ","))
|
||||
}
|
||||
if r.report {
|
||||
args = append(args, "--report-json", req.ReportPath)
|
||||
}
|
||||
if r.transcriptDescription != "" {
|
||||
args = append(args, "--transcript-description", r.transcriptDescription)
|
||||
}
|
||||
if r.configPath != "" {
|
||||
args = append(args, "--config", r.configPath)
|
||||
}
|
||||
if r.outputSchema != "" {
|
||||
args = append(args, "--output-schema", r.outputSchema)
|
||||
}
|
||||
if r.workDirRetention != "" {
|
||||
args = append(args, "--work-dir-retention", r.workDirRetention)
|
||||
}
|
||||
if r.totalLLMConcurrency != nil {
|
||||
args = append(args, "--total-llm-concurrency", strconv.Itoa(*r.totalLLMConcurrency))
|
||||
}
|
||||
if r.proposalLLMConcurrency != nil {
|
||||
args = append(args, "--proposal-llm-concurrency", strconv.Itoa(*r.proposalLLMConcurrency))
|
||||
}
|
||||
if r.validationModel != "" {
|
||||
args = append(args, "--validation-model", r.validationModel)
|
||||
}
|
||||
if r.validationLLMConcurrency != nil {
|
||||
args = append(args, "--validation-llm-concurrency", strconv.Itoa(*r.validationLLMConcurrency))
|
||||
for _, setting := range []struct{ flag, value string }{
|
||||
{"--config", r.cfg.ConfigPath}, {"--overlay", r.cfg.Overlay},
|
||||
{"--proposal-profile", r.cfg.ProposalProfile}, {"--validation-profile", r.cfg.ValidationProfile},
|
||||
{"--transcript-description", r.cfg.TranscriptDescription}, {"--output-schema", r.cfg.OutputSchema},
|
||||
} {
|
||||
if setting.value != "" {
|
||||
args = append(args, setting.flag, setting.value)
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func (r *SubprocessRunner) writeInvocationConfig(req PolishRequest, args []string, modules []string, credentialPresent bool) error {
|
||||
payload := map[string]any{
|
||||
"schema": "audita.generated.v1",
|
||||
"binary": r.binary,
|
||||
"args": args,
|
||||
"timeout": r.timeout.String(),
|
||||
"modules": modules,
|
||||
"base_url": r.baseURL,
|
||||
"model": r.model,
|
||||
"transcript_description": r.transcriptDescription,
|
||||
"config_path": r.configPath,
|
||||
"output_schema": r.outputSchema,
|
||||
"work_dir_retention": r.workDirRetention,
|
||||
"validation_model": r.validationModel,
|
||||
"total_llm_concurrency": r.totalLLMConcurrency,
|
||||
"proposal_llm_concurrency": r.proposalLLMConcurrency,
|
||||
"validation_llm_concurrency": r.validationLLMConcurrency,
|
||||
"report_enabled": r.report,
|
||||
"merged_transcript_path": req.MergedTranscriptPath,
|
||||
"glossary_path": req.GlossaryPath,
|
||||
"output_path": req.OutputProcessedPath,
|
||||
"report_path": req.ReportPath,
|
||||
"work_dir": req.WorkDir,
|
||||
"credential_env_var": r.llmAPIKeyReference,
|
||||
"credential_present": credentialPresent,
|
||||
func (r *SubprocessRunner) writeInvocationConfig(req PolishRequest, args, modules, names []string) error {
|
||||
return writeGeneratedYAML(req.GeneratedConfigPath, map[string]any{
|
||||
"schema": "audita.generated.v2", "binary": r.cfg.Binary, "args": args,
|
||||
"timeout": r.cfg.Timeout.String(), "modules": modules,
|
||||
"overlay": r.cfg.Overlay, "proposal_profile": r.cfg.ProposalProfile,
|
||||
"validation_profile": r.cfg.ValidationProfile,
|
||||
"transcript_description": r.cfg.TranscriptDescription,
|
||||
"config_path": r.cfg.ConfigPath, "output_schema": r.cfg.OutputSchema,
|
||||
"merged_transcript_path": req.MergedTranscriptPath, "glossary_path": req.GlossaryPath,
|
||||
"output_path": req.OutputProcessedPath, "credential_names": names,
|
||||
})
|
||||
}
|
||||
|
||||
func discoverReports(outputPath string) (ReportBundle, error) {
|
||||
var reports ReportBundle
|
||||
for _, report := range []struct {
|
||||
name string
|
||||
dest *string
|
||||
}{
|
||||
{"errors.yml", &reports.ErrorsPath},
|
||||
{"recoveries.yml", &reports.RecoveriesPath},
|
||||
{"report.md", &reports.OverviewPath},
|
||||
} {
|
||||
path := filepath.Join(filepath.Dir(outputPath), report.name)
|
||||
info, err := os.Lstat(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return reports, fmt.Errorf("inspect audita report %q: %w", path, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return reports, fmt.Errorf("audita report %q is not a regular file", path)
|
||||
}
|
||||
if _, err := fileops.ReadFileBounded(path, MaxReportOutputBytes); err != nil {
|
||||
return reports, fmt.Errorf("audita report %q exceeds or cannot be read within %d-byte limit: %w", path, MaxReportOutputBytes, err)
|
||||
}
|
||||
*report.dest = path
|
||||
}
|
||||
return writeGeneratedYAML(req.GeneratedConfigPath, payload)
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
func validateProcessedOutput(path string) error {
|
||||
data, err := readAuditaResult(path, MaxProcessedOutputBytes, "processed transcript")
|
||||
data, err := fileops.ReadFileBounded(path, MaxProcessedOutputBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("audita processed transcript result exceeds or cannot be read within %d-byte limit: %w", MaxProcessedOutputBytes, err)
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return fmt.Errorf("parse json: %w", err)
|
||||
@@ -401,34 +233,3 @@ func validateProcessedOutput(path string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addSubprocessStreamHint(message string, runErr error) string {
|
||||
if runErr == nil {
|
||||
return message
|
||||
}
|
||||
lower := strings.ToLower(runErr.Error())
|
||||
if strings.Contains(lower, "bad file descriptor") || strings.Contains(lower, "exit code 120") {
|
||||
return message + "; hint=audita child process may have started with invalid stderr/stdout descriptors"
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func validateJSONFile(path string) error {
|
||||
data, err := readAuditaResult(path, MaxReportOutputBytes, "report")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v any
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return fmt.Errorf("parse json: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAuditaResult(path string, limit int64, category string) ([]byte, error) {
|
||||
data, err := fileops.ReadFileBounded(path, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("audita %s result exceeds or cannot be read within %d-byte limit: %w", category, limit, err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user