Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ pinned local file, and `agentgate-verify` only reads the local SQLite file.
Pass `--expected-head <seq>:<hash>` (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

Expand Down
6 changes: 6 additions & 0 deletions cmd/agentgate-verify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down
54 changes: 54 additions & 0 deletions cmd/agentgate-verify/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"

_ "modernc.org/sqlite"
Expand Down Expand Up @@ -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)
Expand Down
Loading