diff --git a/README.md b/README.md index e10be6a..ea6637e 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,8 @@ pinned local file, and `agentgate-verify` only reads the local SQLite file. Pass `--expected-head :` (from a checkpoint recorded separately, e.g. at handoff to an auditor) to also assert completeness, not just chain integrity. For scripts, add `--format json` to receive one machine-readable -result object while keeping the same exit codes. +result object while keeping the same exit codes. Add `--quiet` (or `-q`) to +text output to print only the `PASS:` summary on successful verification. ### Development option diff --git a/cmd/agentgate-verify/main.go b/cmd/agentgate-verify/main.go index aa68d17..c9593a5 100644 --- a/cmd/agentgate-verify/main.go +++ b/cmd/agentgate-verify/main.go @@ -54,12 +54,15 @@ func run(args []string, stdout, stderr io.Writer) int { trustRoot string expectedHead string outputFormat string + quiet bool ) fs.StringVar(&source, "source", "", "receipt source: sqlite | jsonl") fs.StringVar(&path, "path", "", "input path; '-' means stdin (jsonl source only)") fs.StringVar(&trustRoot, "trust-root", "", "path to a JSON trust file; optional if the jsonl source embeds its own keys") fs.StringVar(&expectedHead, "expected-head", "", "optional SEQ:HEXHASH; overrides a manifest-derived expected head") fs.StringVar(&outputFormat, "format", "text", "output format: text | json") + fs.BoolVar(&quiet, "quiet", false, "suppress successful verification details in text output") + fs.BoolVar(&quiet, "q", false, "suppress successful verification details in text output") if err := fs.Parse(args); err != nil { return 2 } @@ -161,6 +164,9 @@ func run(args []string, stdout, stderr io.Writer) int { } fmt.Fprintf(stdout, "PASS: %d receipts verified, head seq=%d hash=%x\n", result.VerifiedCount, result.HeadSeq, result.HeadEntryHash[:8]) + if quiet { + return 0 + } if result.Complete { fmt.Fprintln(stdout, "completeness: proven against the supplied expected head") } else { diff --git a/cmd/agentgate-verify/main_test.go b/cmd/agentgate-verify/main_test.go index 0dfbbb9..1cbe312 100644 --- a/cmd/agentgate-verify/main_test.go +++ b/cmd/agentgate-verify/main_test.go @@ -14,6 +14,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "strings" "testing" _ "modernc.org/sqlite" @@ -216,6 +217,59 @@ func TestRun_TextFormatMatchesDefault(t *testing.T) { } } +func TestRun_QuietFormatPrintsOnlyPassSummary(t *testing.T) { + dbPath, trustPath, _ := buildTestChain(t, 2) + args := []string{"--source", "sqlite", "--path", dbPath, "--trust-root", trustPath} + + for _, quietFlag := range []string{"--quiet", "-q"} { + var stdout, stderr bytes.Buffer + code := run(append(args, quietFlag), &stdout, &stderr) + if code != 0 { + t.Fatalf("%s exit code = %d, want 0; stderr=%s", quietFlag, code, stderr.String()) + } + if got := stdout.String(); !strings.HasPrefix(got, "PASS: ") || strings.Count(got, "\n") != 1 { + t.Fatalf("%s stdout = %q, want one PASS line", quietFlag, got) + } + if strings.Contains(stdout.String(), "completeness:") || strings.Contains(stdout.String(), "range:") { + t.Fatalf("%s stdout = %q, want no informational lines", quietFlag, stdout.String()) + } + } +} + +func TestRun_QuietFormatPreservesFailureOutput(t *testing.T) { + dbPath, trustPath, _ := buildTestChain(t, 3) + + db, err := sql.Open("sqlite", dbPath) + if err != nil { + t.Fatal(err) + } + defer db.Close() + dropAppendOnlyTriggers(t, db) + if _, err := db.Exec(`UPDATE receipts SET status_code = 404 WHERE seq = 2`); err != nil { + t.Fatal(err) + } + args := []string{"--source", "sqlite", "--path", dbPath, "--trust-root", trustPath} + + var defaultOut, defaultErr, quietOut, quietErr bytes.Buffer + defaultCode := run(args, &defaultOut, &defaultErr) + quietCode := run(append(args, "--quiet"), &quietOut, &quietErr) + if defaultCode != 1 || quietCode != defaultCode || !bytes.Equal(defaultOut.Bytes(), quietOut.Bytes()) || !bytes.Equal(defaultErr.Bytes(), quietErr.Bytes()) { + t.Fatalf("default (%d, %q, %q) != quiet (%d, %q, %q)", defaultCode, defaultOut.String(), defaultErr.String(), quietCode, quietOut.String(), quietErr.String()) + } +} + +func TestRun_QuietDoesNotChangeJSONOutput(t *testing.T) { + dbPath, trustPath, _ := buildTestChain(t, 2) + args := []string{"--source", "sqlite", "--path", dbPath, "--trust-root", trustPath, "--format", "json"} + + var jsonOut, jsonErr, quietJSONOut, quietJSONErr bytes.Buffer + jsonCode := run(args, &jsonOut, &jsonErr) + quietJSONCode := run(append(args, "--quiet"), &quietJSONOut, &quietJSONErr) + if jsonCode != 0 || quietJSONCode != jsonCode || !bytes.Equal(jsonOut.Bytes(), quietJSONOut.Bytes()) || !bytes.Equal(jsonErr.Bytes(), quietJSONErr.Bytes()) { + t.Fatalf("json (%d, %q, %q) != quiet json (%d, %q, %q)", jsonCode, jsonOut.String(), jsonErr.String(), quietJSONCode, quietJSONOut.String(), quietJSONErr.String()) + } +} + func TestRun_InvalidFormatExitsTwo(t *testing.T) { var stdout, stderr bytes.Buffer code := run([]string{"--source", "jsonl", "--path", "x", "--format", "yaml"}, &stdout, &stderr)