Skip to content

Read a witness's chain of statements in audit verify (gateway ADR-0013, PR 4 of 6) - #228

Merged
kikashy merged 22 commits into
mainfrom
feat/audit-verify-witness-adr-0013-pr4
Oct 5, 2026
Merged

kikashy merged 22 commits into
mainfrom
feat/audit-verify-witness-adr-0013-pr4

Conversation

@kikashy

@kikashy kikashy commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

PR 4 of 6 of gateway ADR-0013, "checkpoint witness" (Judgment-Pack/judgment-pack-gateway#199). PR 1 (Judgment-Pack/judgment-pack-gateway#211, merged as c916ee9) stated the witness statement and how a chain of them is read in the gateway's SPEC.md §8, with 54 vectors in corpus/witness/. This pull request is the runtime's side, ADR-0013 §7 row 4: jpack audit verify reads a witness's chain of statements, holds the trail to it, credits it, and reads a long chain in steps. It needs only that format and those vectors, not the witness service, which no gateway release has yet.

It follows the ADR's determination 6 and the maintainer's answers 8 (reporting), 9 (complete and current) and 11 (limits), as recorded in the ADR.

Review round 2, and what changed

Round 2 (head 127899f) found the round-1 HIGH class protected in every probe it ran, and both MEDIUMs and the LOW resolved. It left three things, each answered in its own commit:

  1. A refused destination hid a finding (MEDIUM). The destination was checked before the trail was read, so a verification with a finding answered JPS-INVOCATION-AUDIT-WITNESS-SAVE (exit 3) with no finding shown. Whether the destination may be written is still decided before anything is written; the verification now runs regardless, and the order of what is reported follows it (ce47031):

    Verification Destination Exit What is printed Destination
    a finding acceptable 1 the report as without --witness-save (the finding, e.g. witness-trail-mismatch); continuationSaved false unchanged
    a finding refused 1 the same report, and witness.saveRefused: "--witness-save holds something other than a continuation, and is left as it is; name a new file, or the continuation a reading saved; nothing was saved", and a note: line in the human report unchanged
    no finding acceptable 0 the report, continuationSaved true ("witness continuation saved") the new continuation
    no finding refused 3 JPS-INVOCATION-AUDIT-WITNESS-SAVE: "--witness-save holds something other than a continuation, …. The verification itself found nothing, and nothing was saved." unchanged

    A destination refused before the inputs are read (its directory cannot be opened, or it names no file) waits for the verification the same way. TestARefusedSaveNeverHidesAFinding holds the four combinations and the human note.

  2. The configuration's identity was looked up again by name (LOW). project.ConfigFile() now answers the identity taken from the descriptor the configuration was read through (fssecure.Root.ReadIdentified), not whatever is at its name when asked. The App's other recording places already took it from the opened descriptor (noteInput, openInput through it, standard input). TestAnInputIsRecordedAsTheFileRead, the reviewer's test turned around, moves the configuration and an opened statements file away after they are read and puts other files at their names: the files read are still the ones recorded (72683b6; b53e5c2 makes the test's own reference compare at once, since on Windows a FileInfo from os.Stat finds its identity by path when first compared, while File.Stat on a descriptor, which the App records, fills it from the handle).

  3. The two residues, stated (d556217), in ReplaceByRename's comment and the guide's "What a save may replace", and claimed no more strongly in the README, the CHANGELOG or the help, which no longer say "never replaces": (a) the destination's last check and the rename are two steps, so another process changing that one name in the instant between them is not detected, a file put there then being replaced and a symbolic link put there replaced itself, the file it names untouched; (b) a build with Go 1.25 or later renames through the directory opened, a build with Go 1.24 renames by the directory's path after checking it still names the directory opened.

Mutation check for round 2: the 8 new mutants (M107 to M114) and the 29 rows in the files it touched were run in a scratch copy as before: 36 of 37 caught. M114, which records an opened input by looking its name up again at the moment it is opened, survives: it differs from the code only if the name is changed in the instant between the open and that lookup, which no test can schedule; a lookup made later, as ConfigFile made it, is M113, caught. The round-1 rows in files round 2 did not touch stand as reported below.

Review round 1, and what changed

The first cross-vendor review (head e6f8cd8) found the save path destructive: --witness-save renamed the continuation onto whatever it named, so the trail, a witness key, a statements file or the head given as its destination was replaced and the command exited 0, and a linked parent directed the write through the link. The class is closed by recording every file the invocation reads where the App opens it, refusing a destination that is any of them by the file's own identity (the resume file alone excepted), refusing anything already there that is not a regular file holding a continuation, and writing through the destination's directory handle (7f3c85d, tests abc698f, 127899f). It also found, and this round fixes: a statement's and a continuation's members decoded before their closed shape was checked (about 150 MB for a 4.69 MB line of 400,000 unknown members; now refused at the first, a few KB) and continuation tests whose two statements were one, so a reader that skipped checking a distinct latest checkpoint statement passed them (1eab87a); and README wording that credited "a statement" where only a checkpoint statement is credited (4f0e706).

What audit verify now does, flag by flag

  • --witness-key <file>, repeatable, at most 16: a witness's public key, 64 hexadecimal characters, obtained out of band. Each is held to CheckPublicKey (the guide's "Record signatures, exactly") before anything is read; one it refuses is JPS-AUDIT-WITNESS-KEY-INVALID (exit 3) with key-not-canonical, key-small-order or key-not-on-curve. Seventeen are refused before any key file is read (keys-over-bound). Every other witness flag needs it (JPS-INVOCATION-AUDIT-WITNESS, exit 3).
  • --witness <file>, repeatable: statements, one per line, as the witness serves them.
  • --witness-head <file>: the head the reader fetched from the witness, one statement. With it the reading is current, as of the fetch; without it, historical.
  • --witness-resume <file>: a continuation the reader's own earlier successful reading saved. Its two statements join the set and are checked again, the chain continues at the index after its last, and its checkpoint is held against the trail again.
  • --witness-save <file>: where to save a continuation, written only when the verification has no finding at all. Before anything is verified it is held to every file the invocation reads and to what is already there (below); the continuation is written through a temporary file in its directory, renamed into place.
  • --require-countersigned-through <sequence>: fails (countersigned-coverage-missing, exit 1) while the records up to that sequence are not all countersigned.

The statements are read against the identity the trail's chained records carry. The checkpoint of every checkpoint statement that verifies is held to the trail as --expect's are, with the same four findings (checkpoint-beyond-trail, checkpoint-not-chained, checkpoint-trail-mismatch, checkpoint-record-mismatch), credited or not. A chain with any witness-* finding is credited nothing (answer 8). Otherwise its checkpoints join the held ones for checkpointed, witnessed and --require-checkpoint-through, and the coverage gains countersigned: not-checked without --witness-key, failed on a witness finding, otherwise through the highest credited checkpoint that matched with no failed check at or before it, or none. The payload gains witness (statement lines read, statements checked, keys supplied, began index-0 or continued with continuedAfter, status read or failed, reading current or historical, headIndex, highestIndex, latestCheckpoint {index, sequence, witnessedAt}, conflicts (the first 100) and conflictsTotal, retired, countersignedAt, continuationSaved) and requiredCountersigned. Additive output: outputVersion stays "2", and a report without --witness-key keeps every sentence it had.

Bounds (answer 11), refused before any statement is checked, never truncated (JPS-AUDIT-WITNESS-REFUSED, exit 3, the reason in the message): more than 16 keys (keys-over-bound); the --witness, --witness-head and --witness-resume files over 64 MiB together, measured by their sizes before any is read (bytes-over-bound); more than 110,000 statements, the continuation's two counted first and every non-blank line counted as the files are split, stopping at the line past the bound (statements-over-bound); and a statements file holding a statement at or below the continuation's last index (statement-before-continuation). One Ed25519 verification per distinct statement, under the one key its keyId names.

What --witness-save may replace. The App records every file it reads, by the file's identity (os.FileInfo from the opened handle), where it opens it: readInput (which readPack now is), openInput, loadProject for the configuration, and noteInput for the trail and the sidecar and stamps file a project opens through its own handle; standard input is recorded when it is a file. audit verify reads every input through these, so an input added later through them is covered without naming it in the check. Once every input is open, and before anything is verified, the destination is looked up by its path (following every link) and through its directory's handle, and compared with each recorded input by os.SameFile: another spelling, a symbolic link or a hard link to an input is the same file. The continuation --witness-resume read is the one input it may be, so --witness-resume X --witness-save X advances it; if that file is also given as another input, it is refused. When something is already there it must be a regular file, not a symbolic link, holding a continuation by IsContinuation (the continuation's own form); anything else, a directory, a FIFO or a device included, is refused and left untouched.

New refusals:

  • JPS-INVOCATION-AUDIT-WITNESS-SAVE, exit 3, when the verification itself found nothing (the message says so, and that nothing was saved): the destination is a file this verification reads (the message names which, e.g. "is the trail, which this verification reads, and saving would replace it"); is a symbolic link; is not a regular file; holds something other than a continuation; or names no file (a path ending in a separator, . or ..).
  • JPS-AUDIT-WITNESS-SAVE, exit 4: the destination's directory cannot be opened, when the verification found nothing; or the write failed, or the file there changed after it was checked, in which case the verification had no finding and the destination is as it was.
  • When the verification has a finding, a refused destination never replaces it: the report is what it would be without --witness-save, exit 1, and the refusal is noted in the witness section's saveRefused ("--witness-save ; nothing was saved") and on a note: line of the human report (review round 2).

Findings

witness-malformed, witness-signature-invalid, witness-trail-mismatch, witness-equivocation, witness-chain-broken, witness-head-unreached, witness-head-behind, and countersigned-coverage-missing. A witness finding is about no line of the trail: its line is 0, and it moves no line's coverage.

Fixed sentences

The record's eight, verbatim (N, T and K filled in):

  • Establishes: "Lines 1 to N are the lines that existed when a witness under a key supplied signed its statement for checkpoint N, which it states it did at T, if that witness is independent of the trail's operator."
  • "Lines after N are covered by no statement of a witness under a key supplied."
  • Current: "That the witness's head for this trail is still index K: the head supplied is as current as the reader's fetch of it, and a signature does not say when it was fetched."
  • Historical: "That the witness held no statement for this trail after index K: no head fetched from the witness was supplied, so the chain was read only as far as it was supplied."
  • Continued: "Anything about statements up to index K, which this reading did not read: it continued from a continuation supplied as the reader's own earlier successful reading, which the runtime cannot tell from one someone else wrote, and is as complete as that reading was."
  • "Anything against a witness that is not independent of the operator: one that colludes can sign what it is asked, at any time it states, and a second history for another audience; a key supplied is trusted because the verifier chose it."
  • "Who submitted any checkpoint: a statement does not name its submitter, and the witness cannot tell the trail's operator from a holder of the operator's credential."
  • "When any record was made: the time a witness states is its own clock's, for when it held the checkpoint."

Two of this runtime's, for what the record decides without giving words (see "Decisions the ADR left to the bytes"):

  • When nothing is credited: "That any line is covered by a statement of a witness under a key supplied: no statement that was read is credited with one."
  • For conflict statements, which fail nothing and are "reported with a fixed sentence": "Which of two records is the trail's at the sequence of each of the N conflict statement(s) read, the first at sequence S: a submitter the witness allowed for the trail offered there another record than the one the witness held, and a conflict statement says neither which of the two is the trail's nor who that submitter was, beyond the witness's own registration."

They follow the stamp sentences and precede the sentence on attempts, which stays last.

The vectors

internal/audit/testdata/witness/ holds the gateway's 54 vectors copied verbatim from corpus/witness/ at c916ee933dff0b72ebf7741c1ce8022487bfb807. internal/audit/testdata/witness.lock.json records the repository, commit and path, and each file's size and SHA-256, in the form of internal/artifacts' lock; TestTheWitnessVectorsAreTheGatewaysCopy holds the directory to it, so a file added, removed or edited fails. .gitattributes marks them -text, as the release-pinned artifacts are: the first Windows run of this branch failed that test, its checkout having ended every line with a carriage return. TestTheGatewaysWitnessVectorsReadAsTheyState reads every vector through PrepareWitness and the reading, writing the parts files out in full first, and compares each answer as a short string: a refusal by its reason, findings as a set of names, every member of a reading with none. All 54 read as stated, and the family counts match the gateway's README.

Tests

  • internal/audit/witness_test.go, over chains a test witness signs under a seed of its own:
    • Determination 6, whole and in steps: a conflict at 100 after checkpoints at 100 and 200, continued from index 1; a conflict above the latest checkpoint, failing either way; a long run of conflicts after the last checkpoint, which no one step holds. Each is read whole and in steps with the statement bound lowered in the test, giving the same findings and coverage; the long run read whole under the lowered bound is refused.
    • The continuation's own rules, each broken once: a head at its last index that matches, and one that differs (witness-equivocation); a head below it (witness-head-behind); statements at or below it supplied (refused); its checkpoint held again after the trail copy changed (checkpoint-record-mismatch, checkpoint-beyond-trail); a conflict last with its latest before it (passes); its latest of another kind, above its last, not its last when that is a checkpoint, below a conflict last, or not the one a retirement last repeats (witness-chain-broken, each by the detail naming its rule); either saved statement failing its signature or shape, and the envelope out of shape; a failing step saving nothing, including on a finding of the trail alone; its two statements counted in the bound; a successful save at a retirement, read again with nothing new, and a statement after it.
    • The trail comparison: a rewrite from line 3 caught by the statement at 5 although the rewrite's checkpoint at 12 was signed later, and the same chain read late (witness-chain-broken); a trail cut short below the latest; another record at a witnessed sequence; a chain with a hole whose checkpoints are still held; a copy of another trail.
    • The report: credited checkpoints joining a holder's, with held staying the holder's own; countersigned; the sentences word for word, current, historical, continued and conflict; a current reading whose chain runs past its head; nothing credited on a finding; the requirement met and unmet; --require-checkpoint-through met by a witness; no witness key, no witness member or sentence.
    • Bounds: 4,194,304 one-byte lines stop at line 110,001 keeping 110,000 (compared as "stopped at line 110001, kept 110000 lines"); 109,998 lines and a continuation read, one more refused; the refusals in their order.
    • Edges: each statement form just inside and just outside it, and each chain rule at its edge.
  • TestEveryReportSaysTheTrailIsSilentAboutAttempts gains three shapes: a witness credited, an unmet countersigned requirement, and a witness finding.
  • internal/cli/audit_witness_test.go, through the command, over a project's own trail, with statement bytes spelled out from SPEC.md §8.3 rather than built by the audit package: a reading to a head with --witness-save, the continuation compared byte for byte; the next reading from it with what the witness signed since, a conflict among them; the human report's lines; every sentence; a stale head, an unmet requirement and a refusal each leaving the continuation as it was; and the flags without a key, seventeen keys refused before any is read, each key the rule refuses, the byte bound by the files' sizes (a sparse file), the statement bound, a statement at the continuation's last, standard input and remote paths, an absent file, and a statement of another trail.

Added in review round 1:

  • TestAMemberTheFormDoesNotAllowEndsTheReading: eight lines of 4 to 10 MB, unknown members before and after the eight, one member given 400,000 times, unknown members in the checkpoint, an array of 2,100,000 elements where a string stands, and the same for continuations (unknown members, unknown members in its last statement, a member given 400,000 times). Each is refused, and the bytes allocated while reading it, compared as a number, must be at most 1 MiB; they are 1 to 11 KB.
  • TestBothOfAContinuationsStatementsAreChecked: a continuation whose last (a conflict) and latest checkpoint statement are distinct, each corrupted in turn by a zeroed signature, a member altered after signing, another key and another trail: each is its finding, nothing is credited and nothing saved. The reviewer's mutation (skip the check of a distinct latest checkpoint statement) is M83 below, caught here.
  • TestAuditVerifyNeverSavesOverAnInput: every flag of audit verify is classified as an input or as no file, so a flag added later fails until it is; the App's record is checked to hold every input each flag names, the trail and the files beside the project's trail and a named trail, and standard input that is a file; each input as the destination, by the same spelling, another spelling, a symbolic link and a hard link, is refused, its bytes unchanged and no temporary file left; inputs that hold a continuation (trail, sidecar, stamps file, statements, head, the resume file given as --witness too, the project's sidecar and stamps file), which only their being read can refuse, are refused for that; and standard input that is the destination.
  • TestAuditVerifySavesOnlyOverAContinuation: resume-equals-save advances in place; a continuation that is no input is replaced; a file that is not one, a pack, a directory, a FIFO, a link to a continuation, a dangling link and a path naming no file are refused and unchanged; a directory that is not there is JPS-AUDIT-WITNESS-SAVE; a save after a finding (an unmet requirement, a chain begun late, a held checkpoint that does not match) writes nothing.
  • TestTheDestinationIsLookedUpInTheDirectoryHeld: an input in the directory held is refused when the path names nothing by the time of the check.
  • internal/fssecure/replace_test.go: whole creation and replacement leaving nothing beside; a name that changed after its check (something appeared, another file, the file gone, a link put in its place), a name that is not one file and one outside the directory refused with nothing left; an unwritable directory leaves nothing; and a path re-pointed after the open: the write goes to the directory held (Go 1.25 and later), or is refused (Go 1.24). Run under Go 1.26.5 and Go 1.24.0.
  • Statements and continuations followed by another value or a byte are malformed; whitespace after one is not.

No test compares or prints a structure that can hold millions of entries: answers are short strings and counts.

Mutation check

Each mutant was applied to a scratch copy of the branch, never to this branch, compiled with go vet, and run against the witness and save tests of its package under a 12 GB memory cap and a timeout; the copy was restored after each and deleted at the end. A mutant that did not compile, or that a panic or a timeout stopped, does not count as caught: three that did not compile were rewritten until they did.

  • Round 0 (before review): 76 of 76 caught, after five gaps found and fixed (bb4d81e, 39dadd5, 1fe9a51, e0619c8).
  • Review round 1: 31 new mutants for the new and changed checks (M77 to M106 and M14w: the streaming parser, the reviewer's continuation mutation M83, the destination's comparison with the inputs and its exception, the leaf, kind and continuation checks, the App's record at each place it is made, and ReplaceByRename's checks, cleanup and handle-bound rename), and M13, M72 and M73 rewritten for the changed code; one first-round mutant, M78, was an equivalent change and was rewritten to read the value whole. The 73 others were rerun against the new code; M14 (ParseCheckpoint's closed set) now runs against the checkpoint tests, since the reader holds a statement's checkpoint to its own set (M14w).

Final run at 127899f: 107 of 107 caught, every one compiling, none by a panic or a timeout. Not mutation-tested, because no test can observe them: the directory sync after the rename, and the exclusivity of the temporary file's creation under a random name.

# File Mutation Verdict Caught by
M01 audit/witness.go keys bound one past (package) caught TestTheGatewaysWitnessVectorsReadAsTheyState/bound-keys-one-past, TestAReadingIsRefusedInItsOrder
M02 cli/audit.go keys counted before any key file is read (CLI) caught TestAuditVerifyWitnessFlagsAreChecked
M03 audit/witness.go key rule not applied (package) caught TestTheGatewaysWitnessVectorsReadAsTheyState/key-not-canonical-identity, TestAReadingIsRefusedInItsOrder
M04 audit/witness.go small-order key refused with another reason caught TestTheGatewaysWitnessVectorsReadAsTheyState/key-small-order-all-zero, TestAReadingIsRefusedInItsOrder
M05 audit/witness.go bytes bound one past (package) caught TestTheGatewaysWitnessVectorsReadAsTheyState/bound-bytes-one-past, TestAReadingIsRefusedInItsOrder
M06 cli/audit.go bytes bounded by sizes before reading (CLI) caught TestAuditVerifyWitnessFlagsAreChecked
M07 audit/witness.go statements bound one past caught TestAContinuationIsHeldToItsOwnRules/the_continuation's_two_statements_are_counted, TestStatementLinesAreCountedAsTheyAreSplit
M08 audit/witness.go split does not stop at the line past the bound caught TestStatementLinesAreCountedAsTheyAreSplit
M09 audit/witness.go continuation's two statements not counted caught TestAContinuationIsHeldToItsOwnRules/the_continuation's_two_statements_are_counted, TestStatementLinesAreCountedAsTheyAreSplit
M10 audit/witness.go statement at the continuation's last index not refused caught TestAContinuationIsHeldToItsOwnRules/statements_at_or_below_its_last_index, TestAReadingIsRefusedInItsOrder
M11 audit/witness.go statements at or below the continuation not refused caught TestAContinuationIsHeldToItsOwnRules/statements_at_or_below_its_last_index, TestAReadingIsRefusedInItsOrder
M12 audit/witness.go witnessVersion not held caught TestAContinuationIsHeldToItsOwnRules/a_saved_statement_whose_signature_fails,_or_out_of_shape, TestTheGatewaysWitnessVectorsReadAsTheyState/malformed-witness-version
M13 audit/witness.go closed member set not held while reading (unknown names read, then refused by count) caught TestAMemberTheFormDoesNotAllowEndsTheReading
M14 audit/checkpoint.go checkpoint's closed member set not held (ParseCheckpoint's rule) caught TestACheckpointIsCanonicalAndReadStrictly/an_unknown_member
M14w audit/witness.go a statement's checkpoint may hold a member it does not define caught TestEveryReportSaysTheTrailIsSilentAboutAttempts/a_witness's_statements
M15 audit/witness.go index -0 read as 0 caught TestTheGatewaysWitnessVectorsReadAsTheyState/malformed-index-minus-zero
M16 audit/witness.go index bound one past caught TestEachFormAndRuleHoldsAtItsEdge
M17 audit/witness.go signature in upper case accepted caught TestTheGatewaysWitnessVectorsReadAsTheyState/malformed-signature-upper-case
M18 audit/witness.go prevSignature form not held caught TestEachFormAndRuleHoldsAtItsEdge
M19 audit/witness.go witnessedAt form not held caught TestEachFormAndRuleHoldsAtItsEdge
M20 audit/witness.go keyId form not held caught TestEachFormAndRuleHoldsAtItsEdge
M21 audit/witness.go kind not held caught TestEachFormAndRuleHoldsAtItsEdge
M22 audit/witness.go another signing prefix caught TestTheGatewaysWitnessVectorsReadAsTheyState/begins-late-index-one
M23 audit/witness.go members out of code-point order in the signed bytes caught TestTheGatewaysWitnessVectorsReadAsTheyState/begins-late-index-one
M24 audit/witness.go signature not verified caught TestAContinuationIsHeldToItsOwnRules/a_saved_statement_whose_signature_fails,_or_out_of_shape, TestBothOfAContinuationsStatementsAreChecked/its_latest_checkpoint_statement,_a_signature_zeroed
M25 audit/witness.go any key supplied accepted, whatever keyId names caught TestTheGatewaysWitnessVectorsReadAsTheyState/signature-key-id-names-another
M26 audit/witness.go trail not held caught TestAWitnessedCheckpointIsHeldAgainstTheTrailCopy/a_copy_of_another_trail, TestBothOfAContinuationsStatementsAreChecked/its_latest_checkpoint_statement,_of_another_trail
M27 audit/witness.go trail checked before signature caught TestTheGatewaysWitnessVectorsReadAsTheyState/signature-before-trail
M28 audit/witness.go two copies of one statement not one caught TestAChainReadInStepsAnswersAsAWholeReadingDoes/a_conflict_at_100_after_checkpoints_at_100_and_200, TestAContinuationIsHeldToItsOwnRules
M29 audit/witness.go a head file of two statements read caught TestTheGatewaysWitnessVectorsReadAsTheyState/malformed-head-two-statements
M30 audit/witness.go equivocation not found caught TestAContinuationIsHeldToItsOwnRules/a_head_at_its_last_index_that_differs, TestTheGatewaysWitnessVectorsReadAsTheyState/equivocation-head-differs
M31 audit/witness.go a chain from index 0 not required (first statement taken as it is) caught TestEveryReportSaysTheTrailIsSilentAboutAttempts/a_witness's_statements_with_a_finding, TestAWitnessedCheckpointIsHeldAgainstTheTrailCopy/a_rewrite_caught_by_an_earlier_statement
M32 audit/witness.go an empty set read as a chain caught TestTheGatewaysWitnessVectorsReadAsTheyState/begins-late-nothing-supplied
M33 audit/witness.go a gap in the indexes allowed caught TestTheGatewaysWitnessVectorsReadAsTheyState/chain-skips-an-index
M34 audit/witness.go links not held caught TestTheGatewaysWitnessVectorsReadAsTheyState/chain-first-names-a-previous
M35 audit/witness.go equal checkpoint sequences allowed caught TestTheGatewaysWitnessVectorsReadAsTheyState/chain-sequence-not-increasing
M36 audit/witness.go decreasing checkpoint sequences allowed caught TestEachFormAndRuleHoldsAtItsEdge
M37 audit/witness.go a retirement with no checkpoint before it allowed (first run labelled for conflicts; the empty case was the retirement's) caught TestEachFormAndRuleHoldsAtItsEdge
M37c audit/witness.go a conflict with no checkpoint before it allowed caught TestTheGatewaysWitnessVectorsReadAsTheyState/chain-conflict-first
M38 audit/witness.go a conflict above the latest allowed caught TestAChainReadInStepsAnswersAsAWholeReadingDoes/a_conflict_above_the_latest_checkpoint, TestTheGatewaysWitnessVectorsReadAsTheyState/chain-conflict-above-latest
M39 audit/witness.go a conflict at the latest's own sequence refused caught TestEachFormAndRuleHoldsAtItsEdge
M40 audit/witness.go a retirement need not repeat the latest caught TestTheGatewaysWitnessVectorsReadAsTheyState/chain-retirement-not-repeating
M41 audit/witness.go a retirement need not be last caught TestTheGatewaysWitnessVectorsReadAsTheyState/chain-retirement-not-last
M42 audit/witness.go a head two past reached caught TestEachFormAndRuleHoldsAtItsEdge
M43 audit/witness.go a head one past unreached caught TestAWitnessReportSaysHowFarTheWitnessReaches/a_current_reading, TestEachFormAndRuleHoldsAtItsEdge
M44 audit/witness.go an unreached head kept in the chain caught TestTheGatewaysWitnessVectorsReadAsTheyState/head-unreached, TestEachFormAndRuleHoldsAtItsEdge
M45 audit/witness.go a head at the continuation's last is behind caught TestAContinuationIsHeldToItsOwnRules/a_head_at_its_last_index_that_is_its_last
M46 audit/witness.go a head below the continuation not found behind caught TestAContinuationIsHeldToItsOwnRules/a_head_below_its_last_index
M47 audit/witness.go continuation's latest of another kind allowed caught TestAContinuationIsHeldToItsOwnRules/its_latest_checkpoint_statement_of_another_kind
M48 audit/witness.go continuation's latest above its last allowed caught TestAContinuationIsHeldToItsOwnRules/its_latest_checkpoint_statement_above_its_last
M49 audit/witness.go continuation's last a checkpoint, latest another, allowed caught TestAContinuationIsHeldToItsOwnRules/its_last_a_checkpoint_statement,_and_its_latest_another
M50 audit/witness.go continuation's conflict above its latest allowed caught TestAContinuationIsHeldToItsOwnRules/its_last_a_conflict_above_its_latest
M51 audit/witness.go continuation's retirement not repeating its latest allowed caught TestAContinuationSavedAtARetirementEndsTheChain
M52 audit/witness.go a statement after a retirement continuation allowed caught TestAContinuationSavedAtARetirementEndsTheChain
M53 audit/witness.go a continued chain not linked to the continuation's last caught TestAChainReadInStepsAnswersAsAWholeReadingDoes/a_conflict_at_100_after_checkpoints_at_100_and_200, TestAContinuationIsHeldToItsOwnRules
M54 audit/witness.go continuation's latest checkpoint not carried into the walk caught TestAChainReadInStepsAnswersAsAWholeReadingDoes/a_conflict_at_100_after_checkpoints_at_100_and_200, TestAContinuationSavedAtARetirementEndsTheChain
M55 audit/witness.go a continuation out of shape passed over caught TestAContinuationIsHeldToItsOwnRules/a_saved_statement_whose_signature_fails,_or_out_of_shape
M56 audit/verify.go continuation saved despite a finding caught TestAContinuationIsHeldToItsOwnRules/a_step_that_fails_saves_nothing, TestAWitnessReportSaysHowFarTheWitnessReaches/an_unmet_requirement
M57 audit/verify.go a chain with a finding credited caught TestAChainReadInStepsAnswersAsAWholeReadingDoes/a_conflict_above_the_latest_checkpoint, TestAContinuationIsHeldToItsOwnRules/a_head_at_its_last_index_that_differs
M58 audit/verify.go checkpoints of a chain with a finding not held caught TestAWitnessedCheckpointIsHeldAgainstTheTrailCopy/a_record_other_than_the_one_witnessed,_and_a_chain_with_a_finding
M59 audit/verify.go countersigned past a failed check caught TestAWitnessedCheckpointIsHeldAgainstTheTrailCopy/a_rewrite_caught_by_an_earlier_statement
M60 audit/verify.go a witnessed checkpoint's mismatch voids no coverage caught TestAWitnessedCheckpointIsHeldAgainstTheTrailCopy/a_rewrite_caught_by_an_earlier_statement
M61 audit/verify.go requirement unmet at its own sequence caught TestEveryReportSaysTheTrailIsSilentAboutAttempts/a_witness's_statements, TestAWitnessReportSaysHowFarTheWitnessReaches/credited_checkpoints_join_the_held_ones
M62 audit/verify.go requirement never unmet caught TestEveryReportSaysTheTrailIsSilentAboutAttempts/an_unmet_countersigned_requirement, TestAWitnessReportSaysHowFarTheWitnessReaches/nothing_credited_on_a_finding
M63 audit/verify.go witnessed sequences' lines not kept caught TestEveryReportSaysTheTrailIsSilentAboutAttempts/a_witness's_statements
M64 audit/verify.go credited checkpoints do not join the held ones caught TestAChainReadInStepsAnswersAsAWholeReadingDoes/a_conflict_at_100_after_checkpoints_at_100_and_200, TestAContinuationIsHeldToItsOwnRules
M65 audit/verify.go the continued sentence dropped caught TestAWitnessReportSaysHowFarTheWitnessReaches/a_continued_reading
M66 audit/verify.go the conflict sentence dropped caught TestAWitnessReportSaysHowFarTheWitnessReaches/credited_checkpoints_join_the_held_ones
M67 audit/verify.go the none sentence dropped caught TestAWitnessReportSaysHowFarTheWitnessReaches/nothing_credited_on_a_finding
M68 audit/verify.go the current sentence names the highest index, not the head's caught TestAWitnessReportSaysHowFarTheWitnessReaches/a_current_reading_whose_chain_runs_past_its_head
M69 audit/verify.go the time sentence dropped caught TestAWitnessReportSaysHowFarTheWitnessReaches/credited_checkpoints_join_the_held_ones
M70 audit/verify.go the establishes sentence dropped caught TestAWitnessReportSaysHowFarTheWitnessReaches/credited_checkpoints_join_the_held_ones
M71 cli/audit.go witness flags without a key not refused caught TestAuditVerifyWitnessFlagsAreChecked
M72 cli/audit.go continuation not saved caught TestAuditVerifySavesOnlyOverAContinuation, TestAuditVerifyReadsAWitnessesChainAndContinuesIt
M73 cli/audit.go a save after a finding writes caught TestAuditVerifySavesOnlyOverAContinuation, TestAuditVerifyReadsAWitnessesChainAndContinuesIt
M74 cli/audit.go human countersigned line changed caught TestAuditVerifyReadsAWitnessesChainAndContinuesIt
M76 audit/witness.go the continuation's latest checkpoint's line not kept caught TestAChainReadInStepsAnswersAsAWholeReadingDoes/a_long_run_of_conflicts_after_the_last_checkpoint, TestAContinuationIsHeldToItsOwnRules/a_conflict_last,_and_its_latest_checkpoint_statement_before_it
M77 audit/witness.go a member given twice read on caught TestEachFormAndRuleHoldsAtItsEdge, TestAMemberTheFormDoesNotAllowEndsTheReading
M78 audit/witness.go every value read whole before its form is held (an array where a string stands) caught TestAMemberTheFormDoesNotAllowEndsTheReading
M79 audit/witness.go content after a statement accepted caught TestEachFormAndRuleHoldsAtItsEdge
M80 audit/witness.go content after a continuation accepted caught TestAContinuationIsHeldToItsOwnRules/a_saved_statement_whose_signature_fails,_or_out_of_shape
M81 audit/witness.go a continuation's members decoded before its shape is checked caught TestAMemberTheFormDoesNotAllowEndsTheReading
M82 audit/witness.go a statement's members decoded before its shape is checked (the code under review) caught TestAMemberTheFormDoesNotAllowEndsTheReading
M83 audit/witness.go the reviewer's: a latest checkpoint statement distinct from the last not checked caught TestBothOfAContinuationsStatementsAreChecked/its_latest_checkpoint_statement,_a_signature_zeroed
M84 cli/audit.go destination not compared with the inputs caught TestAuditVerifyNeverSavesOverAnInput, TestTheDestinationIsLookedUpInTheDirectoryHeld
M85 cli/audit.go the resume file may not be advanced caught TestAuditVerifySavesOnlyOverAContinuation, TestAuditVerifyReadsAWitnessesChainAndContinuesIt
M86 cli/audit.go one kind of input exempted caught TestAuditVerifyNeverSavesOverAnInput, TestTheDestinationIsLookedUpInTheDirectoryHeld
M87 cli/audit.go destination not looked up by its path caught TestAuditVerifyNeverSavesOverAnInput
M88 cli/audit.go destination not looked up through the directory held caught TestTheDestinationIsLookedUpInTheDirectoryHeld
M89 cli/audit.go a leaf symbolic link not refused as one caught TestAuditVerifySavesOnlyOverAContinuation
M90 cli/audit.go a destination that is no regular file not refused as one caught TestAuditVerifySavesOnlyOverAContinuation
M91 cli/audit.go a file that is not a continuation replaced caught TestAuditVerifySavesOnlyOverAContinuation
M92 cli/audit.go the destination never checked caught TestAuditVerifyNeverSavesOverAnInput, TestAuditVerifySavesOnlyOverAContinuation
M93 cli/audit.go a path naming no file not refused as one caught TestAuditVerifySavesOnlyOverAContinuation
M94 cli/app.go opened inputs not recorded caught TestAuditVerifyNeverSavesOverAnInput, TestTheDestinationIsLookedUpInTheDirectoryHeld
M95 cli/app.go standard input not recorded caught TestAuditVerifyNeverSavesOverAnInput
M96 cli/packs.go the configuration not recorded caught TestAuditVerifyNeverSavesOverAnInput
M97 cli/audit.go the project's trail not recorded caught TestAuditVerifyNeverSavesOverAnInput
M98 cli/audit.go the project's companion files not recorded caught TestAuditVerifyNeverSavesOverAnInput
M99 cli/audit.go a named trail's companion files not recorded caught TestAuditVerifyNeverSavesOverAnInput
M100 cli/audit.go witness files not recorded caught TestAuditVerifyNeverSavesOverAnInput
M101 fssecure/replace.go the name not checked again before the rename caught TestReplaceByRenameReplacesOnlyWhatWasChecked
M102 fssecure/replace.go the temporary file left on failure caught TestReplaceByRenameReplacesOnlyWhatWasChecked
M103 fssecure/replace.go a name that is not one file accepted caught TestReplaceByRenameReplacesOnlyWhatWasChecked
M104 fssecure/rename_root.go renamed by the path, not the handle caught TestReplaceByRenameWritesToTheDirectoryHeld
M105 fssecure/replace.go something that appeared after a check finding nothing replaced caught TestReplaceByRenameReplacesOnlyWhatWasChecked
M106 fssecure/replace.go another file than the one checked replaced caught TestReplaceByRenameReplacesOnlyWhatWasChecked
M107 cli/audit.go a refused destination answered instead of the finding (the order under review) caught TestARefusedSaveNeverHidesAFinding/a_finding,_the_destination_refused
M108 cli/audit.go a refused destination never answered when nothing was found caught TestARefusedSaveNeverHidesAFinding/no_finding,_the_destination_refused, TestAuditVerifyNeverSavesOverAnInput
M109 cli/audit.go the refusal not noted beside a finding caught TestARefusedSaveNeverHidesAFinding/a_finding,_the_destination_refused, TestAuditVerifyNeverSavesOverAnInput
M110 cli/audit.go the refusal not noted in the human report caught TestARefusedSaveNeverHidesAFinding
M111 cli/audit.go the refusal not saying the verification found nothing caught TestARefusedSaveNeverHidesAFinding/no_finding,_the_destination_refused, TestAuditVerifyNeverSavesOverAnInput
M112 cli/audit.go the destination checked only after a clean verification (a finding hides no refusal note) caught TestARefusedSaveNeverHidesAFinding/a_finding,_the_destination_refused, TestAuditVerifyNeverSavesOverAnInput
M113 project/project.go the configuration looked up again by its name (the record under review) caught TestAnInputIsRecordedAsTheFileRead
M114 cli/app.go an opened input recorded by looking its name up again survived equivalent outside a race no test schedules

Decisions the ADR left to the bytes

  1. The save's directory (review round 1, 1d). The destination's directory is opened once, with os.Root, before any input is read, and held until the rename. A symbolic link among the directories of the path is followed when it is opened: a reader may name any directory, so a linked parent is ordinary. Once it is open, the write goes to that directory and no other, even if the path is re-pointed meanwhile: fssecure.Root.ReplaceByRename creates the temporary file in it exclusively (O_CREATE|O_EXCL, a random hidden name), writes, syncs and closes it, checks that the name still holds what was checked (nothing, or the same regular file, not a link), renames through the handle (os.Root.Rename) and syncs the directory, removing the temporary file on every failure path. A symbolic link at the leaf is refused. os.Root.Rename arrived in Go 1.25 and this module's floor is Go 1.24: a build with Go 1.24 renames by the held directory's path after checking it still names the directory held, and refuses otherwise (a narrower window, not none); release binaries are built with Go 1.26.5, which renames through the handle. The last check of the name and the rename are two steps: another process changing that one name in the instant between them is not detected (a file put there then is replaced; a symbolic link put there is replaced itself, the file it names untouched). A process killed between the create and the rename can leave the hidden temporary file; no error path does.

  2. The order of a statement's checks. ADR §6 lists the trail check first; SPEC.md §8.6, written after it in PR 1, fixes form, then signature, then trail, a statement taking the first it fails and no other (vector signature-before-trail). This follows SPEC.md.

  3. The trail being verified is the identity of the trail copy's last chained record, the trail the report states. A copy with no chained record makes every statement witness-trail-mismatch.

  4. "Refused before anything is read." ADR §6 says statements at or below a continuation's index "are refused before anything is read, never passed over", with no finding named. It is a refusal, statement-before-continuation, decided from each --witness line's form alone, its index, before any signature is checked. The head is not held to it: ADR §6 judges a head at the continuation's index (its last, or witness-equivocation) and below it (witness-head-behind).

  5. The continuation's bytes: {"continuationVersion":"1","last":<statement>,"latestCheckpoint":<statement>} and a newline, its canonical form, each statement as a JSON object in its canonical form. A continuation out of shape, not JSON, of another version, with other members, or with a statement out of form, is witness-malformed, a finding, not a refusal. A continuation with nothing new supplied is a reading that ends at its last, current when a head at that index is supplied; from index 0 an empty set fails, as SPEC.md says.

  6. The continuation's latest is the latest. Beyond ADR §6's rule (a checkpoint statement at or before the last), the parts of "the latest checkpoint statement at or before it" a reader can check are held: the last itself when it is a checkpoint statement, at or above the sequence of a conflict last, and the checkpoint a retirement last repeats. Each is witness-chain-broken, naming its rule.

  7. Counting. The continuation's two statements are counted first, then the statements files in order, the head file last. statementsRead reports that count; statementsChecked the distinct statements, two copies with the same canonical bytes being one. The CLI bounds the bytes by the files' sizes before reading any, and a file that grows while read is refused with its own detail.

  8. Findings' granularity. One witness-malformed per distinct malformed line, one witness-signature-invalid or witness-trail-mismatch per failing statement, one witness-equivocation per index, one witness-chain-broken naming the first break, and one head finding. Their line is 0, and they are recorded as the signature sidecar's are, moving no line's coverage.

  9. What is held, and what is credited. Every checkpoint statement that passes form, signature and trail is held against the trail, the continuation's latest included, even when the chain has a finding; conflict and retirement statements never are, a conflict naming another record by design. A failed hold is the holder's finding with the detail prefixed "the checkpoint of the witness statement at index N: ", and voids coverage at and after it as a holder's does.

  10. Coverage beside held. checkpointed and witnessed take the highest of a holder's and the credited witness's. held (latest, status) stays the holder's own, and is absent without --expect. With a witness read and nothing credited, checkpointed is failed; the scope is checkpoint whenever a witness key is given. The human status line names a witness's checkpoint when it reaches further than a held one.

  11. Sentences. The two of this runtime's above: the record gives countersigned a none and failed but no sentence for them, and says a conflict "is reported with a fixed sentence" without giving one; the conflict sentence is written from ADR §3's table. The independence, submitter and time sentences are in every report with --witness-key; the reading, continued and conflict sentences only when the statements had no finding.

  12. Lists and codes. conflicts lists the first 100, as the report lists discontinuities and segments, with conflictsTotal. New codes: JPS-INVOCATION-AUDIT-WITNESS, JPS-AUDIT-WITNESS-REFUSED, JPS-AUDIT-WITNESS-KEY-INVALID (exit 3); JPS-AUDIT-WITNESS-KEY-READ, JPS-AUDIT-WITNESS-READ, JPS-AUDIT-WITNESS-SAVE (exit 4). A key file reads as --public-key's does.

  13. JSON. A statement, its checkpoint and a continuation are read a member at a time from encoding/json's token stream (streamedObject): the first member whose name the form does not allow, or that was given before, ends the reading before anything after it is read; a member whose value must be a string, a number or null is refused at its first token when it is an object or an array; content after the object is refused. Integers are digits alone, and a string that is not UTF-8 or a lone surrogate decodes to U+FFFD and so fails every form a statement's strings are held to, the witness-malformed SPEC.md asks for. A statement inside a line has no bound of its own: the checkpoint member is held to ParseCheckpoint's rule (checkpointOfMembers, factored out, unchanged).

Not in this PR

  • The witness service, its log, marks, registrations and endpoints (gateway PRs 2 and 3), and Desk's hand-over to a witness and its panel (PRs 5 and 6). Nothing here fetches.
  • The process contract's witness command for gateway conform --impl: audit verify needs a trail copy, which the vectors do not carry, so the vectors are read in-process by the same reader.
  • Reading two witnesses as one, a salted checkpoint, and anything the ADR lists to revisit.

Checks

  • go fmt ./... (no change), go vet ./... on linux, darwin and windows, and with Go 1.24.0: clean.
  • go test ./internal/fssecure ./internal/audit with Go 1.24.0: pass (the path-renaming fallback).
  • go test ./...: every package passes, under a 12 GB address-space cap.
  • go run ./cmd/jpack spec test-conformance --quiet, and --spec-version 0.2.0-draft: exit 0.
  • CGO_ENABLED=0 go build -trimpath ./cmd/jpack: OK.
  • The claim-surface tests pass; the CHANGELOG entry carries the CONFORMANCE.md line. Changed prose lines are at most 100 columns, but for the two pinned links.
  • CI: see the checks on this PR.

Material-decision impact: public-surface, documented-claim, conformance, security; review: #228 (comment) (round 1 at e6f8cd8: not mergeable, one HIGH, two MEDIUM, one LOW; round 2 completed at b53e5c2: mergeable)

🤖 Generated with Claude Code

kikashy and others added 13 commits October 5, 2026 12:49
…heir digests

The vectors of gateway ADR-0013's PR 1 (corpus/witness/ at c916ee9) are copied verbatim into internal/audit/testdata/witness/, for the reader of a witness's statements that audit verify gains next. testdata/witness.lock.json records the source repository, commit and path, and each file's size and SHA-256; a test holds the directory to it, so a file added, removed or edited without the lock fails.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…teway's 54 vectors

A new reader in internal/audit (witness.go) reads what a checkpoint witness serves (gateway SPEC.md §8, ADR-0013): each statement held to its form by its JSON value, its signature checked once under the key its keyId names among those supplied, by the runtime's own key rule (CheckPublicKey) and equation, over "judgment-pack-gateway/witness/1:" and the statement's canonical form without its signature; then equivocation, the chain rule from index 0, and the head. The bounds of one reading are applied first and refuse rather than truncate: 16 keys, 64 MiB, and 110,000 statement lines counted as the files are split, stopping at the line past the bound.

A continuation, the last statement and the latest checkpoint statement of an earlier successful reading, is read too: its two statements join the set and are checked like the others, the chain goes on from the index after its last, and a head below that index is witness-head-behind. A statements file holding a statement at or below it is refused before any signature is checked.

The checkpoint member rule ParseCheckpoint applies is factored out, unchanged, so a statement's checkpoint is held to it without the bound of a checkpoint document.

Every one of the gateway's vectors reads with the answer it states, compared as short strings; nothing in audit verify uses the reader yet.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…l with them

jpack audit verify gains --witness-key, --witness, --witness-head, --witness-resume, --witness-save and --require-countersigned-through (gateway ADR-0013 §6). The statements are read against the identity the trail's chained records carry. The checkpoint of every checkpoint statement that verifies is held to the trail as a held checkpoint is, with the same four findings; a chain with no witness finding is credited, and a credited checkpoint joins the held ones for checkpointed, witnessed and --require-checkpoint-through. A chain with any witness finding is credited nothing.

The coverage gains countersigned (not-checked, failed, through or none), the payload gains witness (the statements read and checked, where the reading began and ended, the latest checkpoint statement, the conflicts, whether the chain is retired) and requiredCountersigned, and an unmet requirement is countersigned-coverage-missing. A verification with no finding at all saves a continuation when asked, through a file renamed into place.

The report states the record's fixed sentences, and two of this runtime's for what the record decides without giving words: a reading that credits no line, and a conflict statement. A report without --witness-key keeps every sentence it had; its coverage gains countersigned, not-checked.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…ves, and the continuation to its own rules

Tests for gateway ADR-0013 determination 6, over chains signed in the test under a key of its own: a conflict at 100 after checkpoints at 100 and 200, continued from index 1; a conflict above the latest checkpoint, which fails either way; and a long run of conflicts after the last checkpoint, which no one step holds, each read whole and in steps with the step's bound lowered, giving the same findings and coverage. Each of the continuation's rules is broken once: a head at its last index that matches and one that differs, a head below it, statements at or below it supplied, its checkpoint held again after the trail copy changed, a statement after a retirement, a latest checkpoint statement of another kind, above its last or not the latest, either saved statement failing its signature or shape, a failing step saving nothing, and its two statements counted in the bound.

The trail comparison: a rewrite caught by an earlier statement although the rewrite's later checkpoint was signed, and the same chain read late; a trail cut short; another record at a witnessed sequence; a chain with a finding whose checkpoints are still held; a copy of another trail. The report: credited checkpoints joining the held ones, countersigned coverage and its sentences word for word, current, historical and continued readings, nothing credited on a finding, and the requirement met and unmet. The split stops at line 110001 of 4194304 one-byte lines, keeping 110000.

The conflict sentence is reworded so its count reads in one form.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…d their exit codes

Through the command, over a project's own trail and statements whose bytes the test spells out from the gateway's SPEC.md §8.3: a reading to the head the reader fetched, credited and countersigned, its continuation saved byte for byte; the next reading continued from it with only what the witness signed since, a conflict among them; the human report's witness lines; every fixed sentence word for word; and a step that fails, by a stale head, an unmet requirement or a refusal, leaving the continuation as it was. The flags without --witness-key, seventeen keys refused before any is read, each key the key rule refuses with its reason, the files' bytes bounded by their sizes before any is read, the statements over their bound, a statement at the continuation's last, standard input and remote paths, and a statement of another trail.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…ss's statements establishes

The guide gains "Reading a witness's statements", after the stamping section: what a witness is and that no gateway release serves its statements yet, one worked command, the key, the statement, the chain, the head, what is credited and the countersigned coverage, reading in steps by continuation, the bounds and their refusals, the report's witness section, and what a credited statement establishes and does not, linking the gateway's SPEC.md §8 and ADR-0013 at the commit the vectors came from. The sentence on attempts now names --witness-key among what may be supplied. The README gains a paragraph, the audit group's help a clause, and CHANGELOG.md an Unreleased entry with the flags, findings, refusals, codes and the two sentences of this runtime's own.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…refusal made by the files' sizes

Each form of a statement just inside and just outside it: the largest index and one past it, a fraction, a previous signature of no form or a number, a time in its form that no calendar holds and one of another form, a keyId in upper case, another kind, a checkpoint at sequence 0, a member given twice, an escaped name and surrounding spaces. Each chain rule at its edge: a conflict at the latest checkpoint's own sequence, a checkpoint below the latest, and a head one and two indexes past the statements supplied.

audit verify's refusal over the byte bound now says, when it is made by the files' sizes before any is read, how many bytes they hold together, and, when a file grew while it was read, that it grew; the test holds the first.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
… rule

A mutation that let a retirement at the head of a chain pass survived every test: no vector or test held a retirement with no checkpoint statement before it. One now does, and fails the chain.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
… tests to it

A continuation whose latest checkpoint statement stands above its last is broken by the walk after it too, by the rule that checkpoint sequences increase, so a mutation that dropped the continuation's own rule survived on the finding's name alone. The continuation's rules are now held by the detail that names each, as the report gives it.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…panic

A test that reads the first finding's detail of a report with none now fails as a test, not by an index out of range, so a mutation it catches is caught by its assertion.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…n runs past the head

A mutation that named the highest index read in place of the head's survived: every current reading tested ended at its head. A chain that runs past the head it was read to now holds the sentence to the head's index.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
… endings

On Windows the checkout ended each line of the copied vectors with a carriage return, so the copy no longer matched the lock of its digests, and a vector's files were not their bytes. The vectors are marked -text, as the release-pinned artifacts are.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
@kikashy
kikashy marked this pull request as ready for review October 5, 2026 17:33
kikashy and others added 9 commits October 5, 2026 14:05
…d, and check both of a continuation's statements under every corruption

Review round 1 found that every member of a statement was decoded into a map before its closed shape was checked: a 4.69 MB statement of 400,000 unknown members allocated about 150 MB before it was refused. A statement, its checkpoint and a continuation are now read a member at a time from a token stream: the first member whose name the form does not allow, or that was given before, ends the reading, and a member whose value must be a string, a number or null is refused at its first token when it is an object or an array, without reading into it. The checks of each value are unchanged, and the checkpoint is still held to ParseCheckpoint's rule. A test holds eight such lines of 4 to 10 MB, statements and continuations, to at most 1 MiB allocated each; they allocate between 1 and 11 KB.

The review also found that the continuation tests corrupted a continuation whose two statements were one, so a reader that skipped the check of a latest checkpoint statement distinct from the last would pass them; with that change its probe credited a checkpoint with an all-zero signature. A test now corrupts each of two distinct statements in turn, by a zeroed signature, a member altered after signing, another key and another trail: each is a finding, and nothing is credited.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…ything but a continuation

Review round 1 found the save path destructive: --witness-save renamed the continuation onto whatever it named, so with the trail, a witness key, a statements file or the head as its destination the command exited 0 and replaced that input, and a linked parent directed the write through the link. The class is closed in three parts.

Every file an invocation reads is now recorded by the identity of the file itself where the App opens it: readInput (which readPack now is), openInput, loadProject for the configuration, and noteInput for the trail and companion files a project opens through its own handle, standard input included when it is a file. audit verify reads every input through these. Once every input is open, and before anything is verified, the file --witness-save names is compared with each of them by os.SameFile, looked up both by its path, following every link, and through its directory's handle; the continuation --witness-resume read is the one it may be. When something is there it must be a regular file, not a symbolic link, holding a continuation by IsContinuation; anything else is refused and left as it is (JPS-INVOCATION-AUDIT-WITNESS-SAVE, exit 3).

The directory is opened once, before the inputs are read, and held to the rename: fssecure.Root gains Stat, Lstat and ReplaceByRename, which creates a temporary file beside the name exclusively, writes, syncs and closes it, checks the name still holds what was checked, renames through the handle and syncs the directory, removing the temporary file on every failure. A link among the directories of the path is followed, since a reader may name any directory, and the write goes to the directory opened. os.Root renames from Go 1.25; a build with this module's Go 1.24 floor renames by the held directory's path after checking it still names that directory. Release binaries are built with Go 1.26.5.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…destination

Every flag of audit verify is classified as an input or as no file, so a flag added later fails until it is. For each input flag the App's record of what it read is checked to hold that file, and the file named as --witness-save by the same spelling, another spelling, a symbolic link and a hard link is refused, its bytes unchanged and no temporary file left; so are the sidecar and stamps file read beside the project's trail and beside a named trail, and an input read from standard input that is a file. Inputs that hold a continuation, so that only their being read can refuse them, are refused for that: a trail, a sidecar, a stamps file, a statements file, a head, and the resume file given as --witness too. The resume file saved over advances. A continuation that is no input is replaced; a file that is not one, a pack, a directory, a FIFO, a link to a continuation, a dangling link and a path naming no file are refused and left as they are; a directory that is not there is JPS-AUDIT-WITNESS-SAVE; and a save after a finding writes nothing.

fssecure's own tests hold ReplaceByRename to creating and replacing whole, to refusing a name that changed after its check, a name that is not one file and one outside the directory, to leaving nothing after a failed write, and to writing to the directory held when its path is re-pointed, or, in a build before Go 1.25, refusing then.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…nd credited, and what a save may replace

Review round 1 found the README saying every verified statement's checkpoint is held to the trail and crediting "a statement" with showing the lines existed, where a conflict statement carries a checkpoint offered and refused and is credited nothing. The README, the guide, the CHANGELOG and the command's help now say checkpoint statement where they mean one, and the README says a conflict statement is credited nothing.

The guide gains "What a save may replace": the destination held to every input by the file's identity, the resume file the one exception, a file already there replaced only when it holds a continuation, the refusal's code and exit code, and the directory opened once with a symbolic link among its directories followed. The CHANGELOG entry and the help say the same.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…atement or a continuation, to tests

The App's record of what it read is now checked for the files no flag names, the project's trail and the files beside the project's trail and a named trail, and for an input read from standard input that is a file; the project's trail is also refused as --witness-save under each spelling. A direct test shows the file a save would replace is looked up through the directory held, so an input there is refused when the path names nothing by the time of the check. A statement followed by another value or a byte is malformed, and whitespace after it is not; so is a continuation followed by another value.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…ever instead of it

Review round 2 found that a refused save destination, checked before the trail was read, turned a verification with a finding (exit 1, the finding reported) into JPS-INVOCATION-AUDIT-WITNESS-SAVE (exit 3) with no finding shown. Whether the destination may be written is still decided before anything is written, as before: its directory is opened before the inputs are read and it is held to them once they are open. The verification now runs regardless, and what is reported follows it. With a finding, the report is what it is without --witness-save, exit 1, and the refusal is noted beside it: the witness section's new saveRefused, and a "note:" line in the human report. With none, the refusal is the answer as before, its code and exit code unchanged, and its message says the verification itself found nothing and nothing was saved. A destination whose directory cannot be opened, or that names no file, waits for the verification the same way.

A test holds the four combinations, a finding or none and the destination acceptable or refused, to their exit codes, the finding reported as without --witness-save, the refusal noted or answered, and the destination's bytes; the save helper accepts either shape of refusal.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
… read through

Review round 2 found that project.ConfigFile looked the configuration up again by its name, so the identity recorded was of whatever was at that name when asked, not of the file opened and read; a file put in its place after loading was recorded instead. The configuration is now read with fssecure.Root.ReadIdentified, which answers the identity of the opened file from its descriptor with the bytes, and the project keeps it. The App's other recording places already take it from the opened descriptor: noteInput, openInput, which records through it, and standard input.

A test, the reviewer's turned around, loads a project and opens a statements file, moves each away and puts another file at its name: the file read is still the one recorded, under the name it was moved to, and the file put in its place is not.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…e guide, and claim no more elsewhere

Review round 2 asked for the two residues of the save path to be stated where a reader finds them, without machinery for either. ReplaceByRename's comment and the guide's "What a save may replace" now say both: the destination's last check and the rename are two steps, so another process changing that one name in the instant between them is not detected, a file put there then being replaced and a symbolic link put there replaced itself, the file it names untouched; and a build with Go 1.25 or later renames through the directory opened, while a build with Go 1.24 renames by the directory's path after checking it still names the directory opened. The guide also says how a refused destination is reported beside a finding.

The README, the CHANGELOG and the command's help said --witness-save "never replaces" or "never names" an input; they now say it refuses such a destination, and the CHANGELOG states both residues.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
…ved, for Windows

On Windows a FileInfo from os.Stat finds its file's identity by its path the first time it is compared, so the test's reference to the configuration read, compared only after the move, named the file put in its place, and the test failed there while the code under it held the file read. The reference is now compared at once, while its path still names that file. The App's own records come from File.Stat on the opened descriptor, whose identity Windows fills in from the handle.

Part of gateway ADR-0013 (Judgment-Pack/judgment-pack-gateway#199), PR 4 of 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Brian Jin <35789537+kikashy@users.noreply.github.com>
@kikashy

kikashy commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Cross-vendor adversarial review

Drafting model: Anthropic Claude Opus 5.5 (the builder agent), 2026-10-05. Reviewing model, every run: OpenAI gpt-6-astra (codex-cli 0.157.1, reasoning effort high), 2026-10-05 UTC. Material-decision categories: public-surface, documented-claim, conformance, security.

Runs, all of them. Five were started and three produced results.

  • Round 1, first start: discarded. The coordinating session had put an address-space limit around the reviewer, and the reviewer's own tool host could not start under it; it read nothing and said so. Its whole output is below.
  • Round 1, second start, without that limit: complete. This is round 1.
  • Round 2, first run: the reviewing provider ended the session before the reviewer wrote its verdict. It had been asked to append each result to a file as it went, and that file is below as it stood.
  • Round 2, completion run: complete, with its verdict. Its results file is below.

Round 1, reviewed commit e6f8cd8f0d4c6b2ffe5ac48096d8c1be7b236acf

Verdict: NOT MERGEABLE. 1 HIGH, 2 MEDIUM, 1 LOW, all accepted.

# Severity Finding Disposition
1 HIGH audit verify --witness-save <path> renamed the saved continuation onto whatever path it was given. With the trail, a witness key, the statements file or the head file as the destination, the command exited 0 and that input was replaced; a linked parent directory redirected the write. Accept, as a class (7f3c85d, abc698f, 127899f). Every file the command reads is recorded by the identity of the opened file, at the places files are opened. Before anything is written, the destination is compared with each of them and refused if it is one, under any spelling or link; the one allowed coincidence is the file --witness-resume read. An existing destination is replaced only if it is a regular file, not a link, that parses as a continuation. The write goes through a directory handle held from before the inputs are read: a temporary file created exclusively there, synced, the name checked again, renamed, the directory synced, and the temporary file removed on every failure. A test requires every flag of the command to be classified as an input or not a file.
2 MEDIUM Every member of a statement or continuation object was read into memory before the closed shape was checked: a 4.69 MB line with 400,000 unknown members allocated 149.5 MB before it was refused. Accept (1eab87a). Statements, their checkpoint and continuations are read member by member and refused at the first member the form does not allow, the first repeated member, or a nested value where a scalar belongs. Lines of 4 to 10 MB are refused within 1 to 11 KB, asserted as a number.
3 MEDIUM The continuation test used one statement for both members, so a change that skipped the check of a distinct latestCheckpoint statement passed the tests. Accept (1eab87a). The test's two statements are distinct and each is altered in four ways; the reviewer's change now fails it.
4 LOW README text credited "a statement" with what only a checkpoint statement shows. Accept (4f0e706), in README, the guide, the CHANGELOG and the help text.

Round 1 also established: all 54 gateway vectors read as they state and 516 further cases agree with the gateway's reader; the eight sentences taken from the ADR match it word for word; a report made without --witness-key is byte for byte what it was; the added payload member is allowed under this repository's rule for output version "2"; a silent edit of a copied vector fails the lock test.

Round 2 (narrow), first run at 127899f51c95c99901683cda1111a3940e4c0d61, ended before its verdict

Its results file records the round-1 LOW and both MEDIUMs as resolved, and two new items. A third came from the probe tests it left and was not yet written to the file.

# Severity Finding Disposition
5 MEDIUM A refused save destination was answered before the trail was read, so a verification with a finding (exit 1) became an invocation refusal (exit 3) with no finding shown. Not a false success. Accept (ce47031). The verification runs whatever the destination's state. A finding is reported exactly as without --witness-save, with a note when the destination was also refused; with no finding and a refused destination the refusal says that the verification found nothing and nothing was saved.
6 LOW The configuration's recorded identity was looked up again by its path, so it named whatever was at that path later and not the file that was read. The continuation-shape rule still protected the original. Accept (72683b6). The identity is taken from the descriptor the configuration was read through; the other recording places already did so.
7 Observation The last check of the destination and the rename are two steps: a file put at that name by another process in between is replaced, and a link put there is replaced without its referent being touched. Accept as a stated limit (d556217), together with the Go 1.24 limit the author had stated (a Go 1.24 build renames by the directory's path after a check; Go 1.25 and later rename through the held directory; release binaries are built with Go 1.26). Both are now in the comment on ReplaceByRename, the guide and the CHANGELOG, and the documents no longer say "never replaces". No machinery was added. The completion run was told these two are accepted limits and reviewed their wording only.

Round 2, completion run at b53e5c2da578c21b417149331a4bee12c1d1eb00

Verdict: MERGEABLE. The round-1 HIGH is RESOLVED; findings 5 and 6 are RESOLVED; no new finding in the four commits. Five CLI tests that listen on a socket could not run in the reviewer's sandbox; CI runs them on every platform.

One mutation survives and is stated in the PR body: recording an opened input by looking its name up again at the moment of opening differs from the code only if the name changes in that instant, which no test can schedule.

Local paths in the prompts and outputs below are replaced by labels (<scratch>, <tmp>, <home>), and links to the reviewer's local files are reduced to their text; otherwise they are verbatim.

Round 1, first start (discarded): complete output

FINDINGS: None established. I could not read the diff or run checks because the execution tool crashed on every call:

code-mode host exited with status signal: 5 (SIGTRAP) (core dumped)

Even text("probe"), which invokes no shell command, failed. This blocks all repository inspection and testing. No tracked files were changed.

VERDICT: Undetermined. Neither MERGEABLE nor NOT MERGEABLE is supportable without inspecting the code; no HIGH finding has been established.

Please rerun with a working execution tool. The 12 GB address-space limit may affect the tool’s V8 host, but I could not verify that cause.

Round 1 prompt
You are the cross-vendor reviewer for Judgment-Pack/judgment-pack-runtime pull request #228, "PR 4 of 6 of gateway ADR-0013": `jpack audit verify` learns to read a checkpoint witness's chain of statements. You are in a clone at the PR head e6f8cd8; the base is main at 7a2fcfe. Read the change with: git diff 7a2fcfe...HEAD (27 of the 69 files are vectors copied from the gateway under internal/audit/testdata/witness/).

Context. The design is the gateway's ADR-0013, accepted, and the gateway's SPEC.md §8, merged with 54 vectors; both are readable in a gateway clone at <scratch>/rt13-gw (docs/adr/0013-checkpoint-witness.md, above all §6 "How the runtime's verifier reads it", determination 6 and "The maintainer's answers" 8, 9 and 11; SPEC.md §8; corpus/witness/; the gateway's own readers go/witness.go and verify-ts/src/witness.ts). A witness run by another party signs a statement over the runtime's checkpoint line and chains its statements per trail; this PR makes the runtime read such a chain under keys the reader supplies and report how far a witness's signature reaches. The change adds the flags --witness-key (at most 16, required by the others), --witness, --witness-head, --witness-resume, --witness-save and --require-countersigned-through; the findings witness-malformed, witness-signature-invalid, witness-trail-mismatch, witness-equivocation, witness-chain-broken, witness-head-unreached, witness-head-behind and countersigned-coverage-missing; refusals JPS-AUDIT-WITNESS-REFUSED (keys-over-bound, bytes-over-bound, statements-over-bound, statement-before-continuation), JPS-AUDIT-WITNESS-KEY-INVALID, JPS-INVOCATION-AUDIT-WITNESS, JPS-AUDIT-WITNESS-KEY-READ, JPS-AUDIT-WITNESS-READ and JPS-AUDIT-WITNESS-SAVE; a coverage member `countersigned`; payload members `witness` and `requiredCountersigned` with outputVersion unchanged at "2"; a continuation file {"continuationVersion":"1","last":<statement>,"latestCheckpoint":<statement>}; eight fixed sentences taken from the ADR and two the author worded where the ADR decides a case without words (nothing credited; conflict statements); a guide section and a CHANGELOG entry. The PR body lists "Decisions the ADR left to the bytes" and four places where the author found the ADR conflicting or silent: the order of checks follows SPEC §8.6 (form, signature, trail) where ADR §6 lists the trail first; a statement at or below a continuation's last index is refused as statement-before-continuation, judged from the line's form before any signature is checked; the two sentences; and the vectors are read in process because they carry no trail copy. Material-decision categories: public-surface, documented-claim, conformance, security.

Report FINDINGS FIRST, each with a severity (HIGH, MEDIUM, LOW), the file and line, the concrete failure mode, and how you showed it; then VERDICT: MERGEABLE or NOT MERGEABLE, naming the HIGH findings that decide it. Scrutinise: (1) agreement with the gateway: every one of the 54 vectors reads as it states; the reader reuses the runtime's own canonical form, key rule and signature equation and the witness prefix of SPEC §8.3; look for an input outside the vectors on which this reader and the gateway's Go reader would answer differently (statement form, integers, duplicate members, case, blank lines, CR, a head file of two statements, the head rule, retirement, conflicts), and say how you compared; (2) the comparison with the trail copy, which is the runtime's own part: show whether any presentation makes `countersigned` (or `checkpointed`, `witnessed`) cover a line the witness did not sign for this trail: a statement of another trail, a checkpoint whose record digest is not the trail's at that sequence, a rewritten or shortened trail, a conflict, a retirement, a chain with any witness finding (which must be credited nothing), two keys; and whether reading a witness can lower or raise the holder's own `held` coverage in a way ADR §6 and answer 8 do not decide; (3) continuations: what a continuation someone else wrote can and cannot do; that its two statements are verified again under a supplied key and trail; every rule of the continuation broken once; that --witness-save writes only when the verification has no finding at all, writes atomically, does not follow a link or write through one, cannot overwrite one of its own inputs or the trail, and leaves nothing behind on failure; (4) bounds and resources: 16 keys, 64 MiB and 110000 statements at and one past, counted with a continuation's two statements; bytes judged from file sizes before reading and what happens when a file grows, is a FIFO, a device, a directory or a link; that statements are counted as the files are split and the reader stops at the line past the bound without keeping the rest; any input small on disk that makes the verifier allocate or run out of proportion; any panic; (5) the report and the exit codes: the eight ADR sentences word for word against the ADR; whether the two authored sentences say more than the code establishes; that a report made without --witness-key is byte for byte what it was before this change (run the existing audit tests and golden outputs); whether adding members to the payload under outputVersion "2" agrees with this repository's own rule for output versions; --require-countersigned-through met and unmet; (6) the four decisions listed above: for each, whether the choice loses anything ADR-0013 relies on, and in particular whether refusing on an unauthenticated line (statement-before-continuation) or the order of checks lets a party who can add a line to a statements file turn a good reading into a refusal or hide a finding; (7) the command surface and the documents: flag names and help against ADR §7 row 4, the guide section and CHANGELOG sentence by sentence against what the code does, the claim-surface tests; (8) the copied vectors: the lock file and its test, the .gitattributes change, and that a silent edit of a vector fails; (9) variants: for each finding and refusal, the single change that makes the wrong outcome pass; any test that would still pass if a chain with a finding were credited, if a continuation were not verified again, or if a continuation were saved after a finding.

You may run, with GOCACHE set to a directory of your own: gofmt -l . ; go vet ./... ; go test ./... (tests that listen on a socket may be refused by the sandbox; say so) ; go run ./cmd/jpack audit verify ... on trails and statement files you craft in a temporary directory ; the gateway's readers from the gateway clone for comparison ; python3 for arithmetic ; read-only git commands. Wrap every go, python3 and built-program command you run in a capped subshell, ( ulimit -v 12000000; <command> ), because an uncapped run exhausted this machine earlier today; a watchdog stops this whole session if its processes together hold more than 14 GB. Keep every probe bounded, create no file over 100 MB except what the tests themselves create, and never print or diff a structure with millions of entries. Do not modify tracked files in place; try variants only in copies and restore nothing by git checkout.
Round 1 complete output
  1. HIGH — continuation saving can destroy the trail or verification inputs. internal/cli/audit.go:325 unconditionally renames the continuation onto the requested destination. Live CLI probes using the trail, witness key, statements file, or head file as --witness-save each returned exit 0 and replaced that input with continuation JSON. A symlinked parent also directed the write through that link. Protect input destinations while retaining intentional advancement of the resume continuation itself.

  2. MEDIUM — malformed objects cause unnecessary memory amplification. internal/audit/witness.go:198 materializes every object member before checking the closed shape. A 4.69 MB statement containing 400,000 unknown members allocated 149.5 MB before rejection; the gateway reader allocated approximately 15 KB. Continuation parsing uses the same pattern. Reject excess/unknown members during parsing. The probes demonstrated amplification, not memory exhaustion.

  3. MEDIUM — tests miss verification of a distinct saved checkpoint. internal/audit/witness_test.go:359 uses the same statement for both continuation members and corrupts only the first occurrence. A mutation skipping verification solely for a distinct latestCheckpoint survived the existing witness and audit-verify tests. An additional probe then falsely credited a checkpoint with an all-zero signature through sequence 8. The unmodified implementation correctly rejects it; this is a regression-test gap.

  4. LOW — README overstates conflict statements’ guarantees. README.md:613 says every verified statement’s checkpoint is held against the trail, then attributes prefix-existence evidence to “A statement.” Conflict statements carry offered-and-refused checkpoints and receive no such credit. The passing conflict vectors demonstrate this. Both claims need the qualification “checkpoint statement.”

VERDICT: NOT MERGEABLE. The HIGH finding—destructive continuation-save destinations—decides the verdict.

The full review and mutation matrix covers all nine requested areas. Principal results:

  • Both readers passed all 54 vectors and agreed on 516 additional cases. No additional semantic divergence was found.
  • Coverage, continuation, two-key, boundary, growing-file, and nonregular-file probes behaved as detailed in the report.
  • All eight ADR sentences matched exactly. No-key human reports were byte-identical; JSON added only coverage.countersigned, permitted under the repository’s version "2" rule.
  • An unauthenticated old-index line can turn a resumed reading into a refusal and suppress detailed findings, as the ADR’s pre-reading refusal explicitly requires; it cannot obtain successful credit.
  • Formatting, vet, audit tests, targeted CLI tests, claim tests and golden checks passed. The full suite encountered sandbox socket-listening refusals in CLI/timestamp tests.
  • A silent vector edit failed the lock test. Tracked workspace files remain unchanged.

@kikashy

kikashy commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Cross-vendor adversarial review, continued

The reviewer's own longer report for round 1, and the prompts and results of round 2. Local paths are replaced by labels as in the record above.

Round 1: the reviewer's full report (the file its output refers to)

FINDINGS FIRST

  1. HIGH — unsafe continuation destination can destroy the trail or verification inputs. internal/cli/audit.go:156–160, 311–325, particularly the unconditional os.Rename(temporary, target) at line 325. Nothing compares the destination with the opened trail, witness keys, statements, head, held checkpoints, or other inputs. In live CLI probes against a valid signed chain, setting --witness-save to the trail, key, statements, or head path returned exit 0, reported a saved continuation, and replaced that input with continuation JSON. The trail overwrite destroys the history immediately after reporting its verification successful. A symlinked parent also directed the write into its target directory; a final symlink was replaced, rather than followed, leaving its referent untouched. Validate protected input destinations and use safe directory-relative creation/replacement. Preserve intentional advancement of the resume continuation itself: the existing CLI test explicitly resumes and saves to the same continuation path. Existing save tests do not cover destructive destination collisions.

  2. MEDIUM — a malformed statement allocates its entire arbitrary member map before rejecting its shape. internal/audit/witness.go:198–204 and 241–243, calling internal/audit/audit.go:536 (exactObject). The exact eight-member/three-member checks happen after every supplied object member has been decoded and stored. A single 4,688,891-byte object containing 400,000 distinct unknown scalar members was rejected only after approximately 149,509,880 bytes of allocation, with approximately 62 MB additional live heap and 0.80 seconds elapsed. The gateway Go reader rejected the same object after approximately 14,832 bytes of allocation and 0.14 milliseconds, using its 64-value parsing budget. A 1,088,891-byte case allocated 37,329,064 bytes in the runtime. These probes were bounded and did not exhaust memory; the measured growth is linear, not an assertion of unbounded allocation. Nevertheless one malformed line bypasses the practical protection of the statement-count limit and needlessly materializes a large map. Reject unknown/excess members as they are encountered, or apply a structural budget, including continuation/checkpoint objects.

  3. MEDIUM — the distinct continuation checkpoint's signature is not protected by regression tests. internal/audit/witness_test.go:358–363. The test builds last and latestCheckpoint from the same statement and alters only the first occurrence. In a disposable source copy, I bypassed verification only for statement == latest && latest != last. All existing witness-vector, witness/continuation, and TestAuditVerify* tests still passed. A separate probe with a conflict as last and a checkpoint with an all-zero signature as latestCheckpoint then incorrectly reported valid/countersigned through 8; the unmodified implementation correctly reports witness-signature-invalid and credits nothing. This is a test gap, not a claim that the current verifier skips that signature. Add separate malformed, bad-signature, and wrong-trail cases for each distinct saved statement.

  4. LOW — the README assigns checkpoint guarantees to conflict statements. README.md:613–618. It says the checkpoint of “every statement that verifies” is held against the trail and that “A statement shows” the trail prefix existed. Conflicts sign an offered-and-refused checkpoint; their digests are intentionally not matched or credited. The passing valid-conflict-keeps-coverage vector and the whole/in-steps conflict tests demonstrate that distinction. Say “checkpoint statement” in both claims. The guide and CHANGELOG use the necessary qualification more accurately.

VERDICT: NOT MERGEABLE. Finding 1, the HIGH destructive continuation-save destination bug, decides this verdict. Findings 2–4 should also be addressed; they do not assert an observed signature or trail-coverage bypass in the unmodified implementation.

Scope and validation. Reviewed runtime e6f8cd8f0d4c6b2ffe5ac48096d8c1be7b236acf against 7a2fcfe, gateway ADR-0013 and SPEC §8 at c916ee933dff0b72ebf7741c1ce8022487bfb807, both gateway reader implementations, and the copied vectors. No tracked workspace files were modified. Variants ran in <tmp>/rt13-review-work and <tmp>/rt13-review-gateway. Every Go, Python, and built-program process ran inside or inherited a subshell capped with ulimit -v 12000000; no probe file exceeded 100 MB.

  • gofmt -l ., go vet ./..., and git diff --check were clean.
  • go test ./... passed internal/audit and the other nonblocked packages. CLI TestAuditStampStampsTheCurrentCheckpointOnce and timestamp TestAskingAnAuthorityHoldsItsReplyToTheRequest stopped their packages with httptest socket-listening denial. These are sandbox restrictions, not witness panics. Consequently the full suite is not claimed green.
  • Separately ran CLI audit verify/checkpoint/repair/handover tests, help and claim-surface tests, and corpus-envelope golden tests successfully. Ran corresponding selected baseline tests successfully as well.
  • Runtime and gateway each passed all 54 witness vectors. The copied directory matched the gateway directory byte for byte. Appending one space to one vector in the disposable copy failed TestTheWitnessVectorsAreTheGatewaysCopy with its size/digest mismatch. The lock identifies the exact gateway commit, the directory membership is checked, and .gitattributes disables text conversion for these bytes.

Agreement outside the vectors. Generated 516 shared cases and ran them through runtime PrepareWitness/read and gateway Go readWitness, comparing refusals, finding sets, and all clean-reading answer members. Cases included digit boundaries, minus zero, fractions, exponents, oversized integers, duplicate and escape-equivalent member names, member/value case, escaped strings, malformed Unicode, blank lines, CRLF, bare CR between objects, trailing JSON, two-statement heads, adjacent/distant heads, conflicts, retirement, reordered files, and single-byte corruptions. All semantic answers matched. For the raw-invalid-UTF-8 case, the gateway harness restored the original invalid byte after decoding the JSON case manifest, so JSON transport normalization did not substitute a different probe. I did not find an additional accepted/rejected-input divergence. The resource behavior differs as finding 2 records. TypeScript was inspected, not executed.

The runtime uses CheckPublicKey, ParsePublicKey, KeyID, EncodeCheckpoint and its existing strict integer/string readers. Signing bytes have exactly judgment-pack-gateway/witness/1: followed by canonical unsigned statement bytes. All admitted strings are fixed/ASCII forms and integers are below 2^53−1, so the manual outer canonical construction agrees with SPEC §8.3. Both Go readers use the same non-cofactored crypto/ed25519.Verify equation after their key checks; the mixed-order, small-order-R, identity-R, noncanonical-key and S+L vectors exercise the relevant edges.

Trail comparison and coverage. Every checkpoint statement that passes form, signature and trail checks is held against its sequence and exact record digest, even when its witness chain has another finding. Wrong-trail statements are excluded; malformed/bad-signature statements never supply checkpoints. Existing tests exercise earlier signed checkpoints detecting rewrites even when a later checkpoint matches the rewritten history, shortened trails, wrong digests, conflicts and retirements. A shortened trail below checkpoint 12 retained only countersigned coverage through the earlier matching checkpoint 5; an earlier mismatch blocked later credit. Chains with a witness finding credited no statement.

A separately supplied holder checkpoint through 10 retained its own matched summary when a witness file gained a malformed line. A verified but mismatching witness checkpoint at 3 lowered holder coverage through 10, even if that witness chain also had a malformed line. This follows ADR §6's express requirement to compare every verified checkpoint and allow no failed trail/checkpoint check at or before credited coverage. Witness credit can raise aggregate checkpointed/witnessed, but does not increase the holder's held.latest or counts. I found no unexplained change to the holder's own coverage.

A chain whose successive statements were signed under two supplied keys passed; independent statements from two keys at index 0 produced witness-equivocation. That follows the specified one-set/index rule; this is not independent aggregation of two witnesses' separate chains. A valid conflict retained preceding checkpoint coverage; neither conflict nor retirement added credit of its own.

Continuations. The unmodified reader rechecks both saved signatures and trail identities, their shapes, and the continuation version/closed member set. The tests break checkpoint-kind/index ordering, last-checkpoint identity, conflict sequence, retirement checkpoint repetition, and post-retirement continuation. They exercise matching/different/behind heads, refusing supplied statements at/below the saved index, changed/shortened trails, successful retirement saves, failed steps, and long runs of conflicts read in steps. Additional probes separately corrupted each distinct saved signature and shape and signed a latest checkpoint for another trail; all failed with the appropriate witness finding and no credit.

A continuation written by someone else can omit earlier contradictory signed evidence. Demonstration: a whole reading found checkpoint-record-mismatch@3; a hand-made continuation consisting solely of the later valid checkpoint for the rewritten trail passed through 8. It cannot manufacture a trusted signature or avoid comparing that saved checkpoint with the supplied trail. This is the trust boundary expressly accepted by ADR §6, not an implementation bypass: the fixed continued-reading sentence states that earlier completeness is the supplier's assertion. Do not describe continuations as independently authenticated summaries of the omitted chain.

Continuation bytes are produced only for a clean witness reading with no verification finding at all. The existing tests catch saving after a trail finding and after unmet required coverage, and CLI tests preserve the previous continuation on failed steps. Writes use a temporary sibling, file sync/close, then rename; a tested rename failure returned exit 4 and left no temporary file. This provides atomic replacement but lacks the destination protections in finding 1. Swallowing save errors was another surviving CLI-test mutation: error propagation needs an explicit regression test.

Bounds and resource probes. Both readers' corpora tested 16 keys, 64 MiB, and 110,000 statements at and one beyond their bounds. Additional live CLI probes established:

Input At boundary One past
Actual supplied keys 16 valid keys: exit 0 17: exit 3, keys-over-bound
Aggregate bytes including continuation Exactly 64 MiB: exit 0 64 MiB + 1: exit 3, bytes-over-bound
Nonblank lines including continuation's two 109,998 malformed lines + two: exit 1 for malformed content One additional line: exit 3, statements-over-bound

The line-splitting test explicitly stops at count 110,001 with only 110,000 line descriptors retained. File data itself is read before splitting, within the byte limit. Size checks precede reading the statements/head/resume files. In a live growth probe, an inotify access event triggered expansion from 32 MiB to 64 MiB + 1 after reading began; the verifier returned the “grew past” bytes-over-bound refusal. FIFO, /dev/zero, directory and final-symlink statement inputs promptly returned JPS-AUDIT-WITNESS-READ, exit 4. The aggregate byte cap works; it does not solve the object-member allocation amplification in finding 2. No adversarial-probe panic occurred.

Reports, command surface and decisions. All six flag names match ADR §7 row 4. The CLI tests cover requiring a supplied witness key, invalid keys, missing key/statement files, remote/stdin restrictions, unmet/matched required countersigned coverage, and the documented exit classes. Successful matched coverage exits 0; unmet coverage exits 1 with countersigned-coverage-missing; bounds/invocation/key-form refusals exit 3; file I/O/save failures exit 4.

All eight fixed sentences matched the ADR word for word after joining prose line wraps and substituting N/K/T placeholders. The two authored sentences are appropriately limited: “nothing credited” describes credit, not the nonexistence of signatures; the conflict sentence describes the witness's assertion about an offered/refused record and does not choose either history. The guide/CHANGELOG correctly describe the principal flags, coverage, continuation trust boundary and output additions. They do not cure or promise protection from the save-path bug. The README overstatement is finding 4.

Compared actual baseline/head CLI output on valid, holder-checkpointed, shortened and damaged trails without a witness key. Human output was byte-identical in all four cases. JSON was not byte-identical: it gained only coverage.countersigned={"status":"not-checked","detail":"no witness key was supplied"}; removing that addition made the parsed payloads equal. Sentences and exits were unchanged. VERSIONING.md expressly permits additive output members without changing outputVersion; staying at "2" is therefore correct. The existing golden/claim tests passed but do not establish whole-audit-JSON byte identity.

The four disclosed choices resolve as follows:

Choice Assessment
Form → signature → trail, following SPEC §8.6 Correct precedence. An unauthenticated wrong-trail claim yields signature-invalid; it is not trusted as evidence about another trail. Any such failure still denies witness credit. Equivocation is tested among independently passing statements.
Refuse statements at/below a continuation before authentication Matches the ADR's explicit pre-reading refusal. A fabricated but well-formed old-index line changed a good resumed reading into statement-before-continuation; therefore an appender can also suppress the detailed findings that a full reading would have produced, replacing exit 1/report with exit 3/refusal. It cannot turn failure into successful credit. This availability/diagnostic limitation is inherent in the adopted rule and must not be mistaken for an authenticated finding.
Two authored sentences No additional coverage or submitter/time guarantee beyond the accepted design was identified.
Vectors read in process, without trail copies Appropriate for shared chain semantics. They cannot test the CLI, trail comparison, holder coverage or save lifecycle; separate tests/probes are necessary and were run.

Mutation results. Each following single code change was tried in a disposable copy. “Killed” means existing relevant tests failed, not that the mutant was shipped.

Rule disabled or relaxed Result
Accept another witness version Killed: continuation and vector tests
Skip signature verification Killed: continuation and vector tests
Skip trail comparison Killed: trail-copy and vector tests
Ignore equivocation Killed: continuation-head and vector tests
Suppress chain-broken Killed: whole/in-steps and continuation tests
Suppress head-unreached Killed: edge and vector tests
Suppress head-behind Killed: continuation tests
Skip required countersigned failure Killed: report requirement tests
Admit key 17 Killed: refusal-order and boundary vectors
Bypass public-key rule (canonical/small-order/on-curve cases) Killed: refusal-order and key vectors
Admit byte 64 MiB + 1 Killed: refusal-order and boundary vectors
Admit statement 110,001 Killed: line-split and continuation-bound tests
Change old-index refusal from <= to < Killed: continuation and refusal-order tests
Credit verified checkpoints despite witness findings Killed: whole/in-steps and continuation/report tests
Skip both continuation signatures Killed: continuation bad-signature test
Allow saving after a non-witness finding Killed: trail-finding and unmet-requirement tests
Skip only a distinct latestCheckpoint's verification Survived existing witness/vector/audit-verify tests; additional probe demonstrated false credit (finding 3)
Skip a distinct last statement's verification Killed by the existing continuation test
Ignore saveContinuation errors Survived existing audit-verify tests; add a failing-save regression

The shared 54-vector harness alone would still pass mutations of runtime-only credit, continuation verification and save-after-finding behavior; it calls neither trail verification nor continuation saving. The runtime-specific tests catch the broad versions of those mutations, with the narrower gap above. The live save-error and destination probes expose paths not guarded by the existing suite.

Evidence is retained under <tmp>/rt13-review-*.log, <tmp>/rt13-review-fixtures, <tmp>/rt13-review-work/internal/audit/review_test.go, and <tmp>/rt13-review-gateway/go/review_test.go.

Round 2, first run: prompt
You are the cross-vendor reviewer for Judgment-Pack/judgment-pack-runtime pull request #228, second round, narrow. Round 1 reviewed commit e6f8cd8 and found one HIGH, two MEDIUM and one LOW. You are in a clone at the new head 127899f. Read the answer to round 1 with: git diff e6f8cd8...HEAD (five commits). This round decides whether the HIGH is resolved and whether the fix introduced a new HIGH; it is not a second full review.

As soon as you establish a finding or a resolution, append it to the file <tmp>/rt228-r2-findings.md (create it if absent): severity, file and line, the failure in one or two sentences, and how you showed it. Keep doing so as you go, so that the file is complete even if this session ends early. Describe each finding as a defect with a reproduction written as a test case or a command sequence on files you create.

Round 1, for reference. HIGH: `jpack audit verify --witness-save <path>` renamed the saved continuation onto whatever path it was given; with the trail, a witness key, the statements file or the head file as the destination the command exited 0 and that input was replaced, and a linked parent directory redirected the write. MEDIUM: every member of a statement or continuation object was materialised before the closed shape was checked (a 4.69 MB line with 400,000 unknown members allocated 149.5 MB). MEDIUM: the continuation test used one statement for both members, so skipping the check of a distinct latestCheckpoint statement passed the tests. LOW: README text credited "a statement" where only a checkpoint statement is credited.

What the author says the fix does. The application records every file it reads, by the identity of the opened file, at the places it opens files (readInput and openInput, loadProject for the configuration, noteInput for the trail and companion files, and standard input when it is a regular file). Before anything is verified, the save destination is looked up by its path and through a held handle on its directory and compared with each recorded input by os.SameFile; the one allowed coincidence is the file --witness-resume read. An existing destination must be a regular file, not a symbolic link, that parses as a continuation; anything else is refused and left as it was. The destination's directory is opened once with os.Root before any input is read and held until the rename; the temporary file is created exclusively in it under a random hidden name, written, synced and closed; the name is checked again, the file is renamed through the handle and the directory is synced; the temporary file is removed on every failure path. New refusals: JPS-INVOCATION-AUDIT-WITNESS-SAVE (exit 3) and JPS-AUDIT-WITNESS-SAVE (exit 4). The package internal/fssecure gains Root.Stat, Root.Lstat and ReplaceByRename. The author states one limitation: os.Root.Rename exists from Go 1.25 and this module's floor is Go 1.24, so a Go 1.24 build renames by the directory's path after checking that the path still names the directory held, which narrows the window without closing it; release binaries are built with Go 1.26.

Report FINDINGS FIRST, each with a severity (HIGH, MEDIUM, LOW), the file and line, the concrete failure, and how you showed it; then for each round-1 item RESOLVED or NOT RESOLVED with your evidence; then VERDICT: MERGEABLE or NOT MERGEABLE, naming any HIGH that decides it. Check: (1) the class, not the four cases: find any file `audit verify` reads, under any combination of its flags or through a project configuration, an environment variable or a default path, that is not in the recorded set, and show whether it can be named as the save destination and replaced; check each spelling (the same path, a relative path, a path through a symbolic link, a hard link, a path through a linked directory, different letter case where the filesystem folds it); check that the test which requires every flag to be classified would fail for a new unclassified flag; (2) the allowed coincidence: resume equal to save advances the continuation, and the resume file given a second time as another input is refused; (3) what may be replaced: only a regular, unlinked-at-the-leaf file that parses as a continuation; a pack, a key, a trail, a directory, a FIFO, a device, a symbolic link to a continuation, a dangling link, a name in a directory that does not exist; that after every refusal and every failed write the destination's bytes and the directory's entries are what they were, with no temporary file left; (4) the write itself: the order create exclusive, write, sync, close, check again, rename, sync the directory; what happens when the destination is replaced, removed or turned into a link between the check and the rename; when the directory path is re-pointed after it was opened; when the temporary name exists; when the disk write fails midway; judge the Go 1.24 path and say whether the stated limitation is an acceptable residue or a HIGH, given that it applies only to builds with Go 1.24; (5) that nothing is saved when the verification has any finding, and that a refused save never changes the verification's own exit code or report in a way that hides a finding; (6) the changes outside the save path in these five commits: internal/cli/app.go, internal/cli/packs.go and internal/project/project.go now record inputs for every command, so run the existing tests of the other commands and confirm their behaviour and outputs are unchanged; internal/fssecure's new functions against the package's existing rules; (7) the two MEDIUM answers: parse a line with many unknown members, a repeated member, a nested value where a scalar belongs and content after the object, for statements, their checkpoint and continuations, and measure the allocation; corrupt each of a continuation's two distinct statements in turn; (8) the LOW: the README, the guide, the CHANGELOG and the help text say "checkpoint statement" where they mean one, and the new sentences about what a save may replace match the code.

You may run, with GOCACHE set to a directory of your own: gofmt -l . ; go vet ./... ; go test ./... (tests that listen on a socket may be refused by the sandbox; say so) ; go run ./cmd/jpack audit verify ... on files you create in a temporary directory ; read-only git commands. Wrap every go and python3 command in a capped subshell, ( ulimit -v 12000000; <command> ); a watchdog stops this whole session if its processes together hold more than 14 GB. The disk has little free space: keep your Go cache and temporary files under 2 GB in total and delete them when you finish, except the findings file. Keep every probe bounded and never print or compare a structure with millions of entries. Do not modify tracked files in place; try variants only in copies and restore nothing by git checkout.
Round 2, first run: the results file as it stood when the session ended

ROUND-1 LOW — RESOLVED: README.md:613, docs/building-with-packs.md:1221, CHANGELOG.md:29, internal/cli/audit.go:94. The credit-bearing statement is now explicitly a checkpoint statement; conflict statements are expressly excluded in README/CHANGELOG. Evidence: git diff e6f8cd8...HEAD -- README.md docs/building-with-packs.md CHANGELOG.md internal/cli/audit.go. Save-guarantee documentation is assessed separately against the write probes below.
MEDIUM — internal/cli/audit.go:160-165: A refused save suppresses the verification's findings and changes exit 1 into exit 3. The save destination is checked before readTrailWith, so an unrelated invalid save path prevents an otherwise reportable witness-trail-mismatch from being reported. Reproduced in a disposable git-archive copy with TestR2RefusedSaveMasksFinding: create newSaveFixture(t), create bad.jsonl containing "not JSON\n" and keep.txt containing "preserve me"; run audit verify --format json --trail bad.jsonl --witness-key fixture.key --witness fixture.statements (exit 1, findingsTotal=1, witness-trail-mismatch), then add --witness-save keep.txt (exit 3, only JPS-INVOCATION-AUDIT-WITNESS-SAVE, no findings). keep.txt was unchanged. This is not a false success, hence MEDIUM rather than HIGH.

ROUND-1 MEDIUM (distinct continuation statements) — RESOLVED: internal/audit/witness_test.go:863-907. TestBothOfAContinuationsStatementsAreChecked uses last=index 2 conflict and latestCheckpoint=index 1 checkpoint; independently corrupts signatures, signed content, signer and trail of each, requiring failed coverage and no continuation. Evidence: ( ulimit -v 12000000; GOCACHE=/rt228-r2-cache go test ./internal/audit -run TestBothOfAContinuationsStatementsAreChecked -v ) passed all eight corruptions.

ROUND-1 MEDIUM (unknown-member allocations) — RESOLVED for the reported defect: internal/audit/witness.go:302-347. TestAMemberTheFormDoesNotAllowEndsTheReading passed: 4,689,496-byte unknown-member statement allocated 1,064 bytes (10,608 with unknown members last), checkpoint case 1,248; 4,690,158-byte continuation allocated 1,080 bytes (4,400 inside last); 400,000 repeated members allocated 4,192 / 4,240 bytes, nested-array scalar 6,608 bytes. Command: ( ulimit -v 12000000; GOCACHE=/rt228-r2-cache go test ./internal/audit -run TestAMemberTheFormDoesNotAllowEndsTheReading -v ). Additional shape/trailing-content probes follow.
SAVE RACE OBSERVATION — internal/fssecure/replace.go:87-92: The final identity check is not atomic with rename. In a disposable copy, a test-only callback placed immediately before renameWithin removed the checked destination and (a) renamed an input onto it, (b) left it absent, or (c) made it a symlink. ReplaceByRename returned nil in all three cases: (a)'s input bytes were lost, (b) was recreated, and (c)'s link was replaced but its referent remained unchanged. TestR2LeafChangedAfterFinalCheck reproduces these schedules using two small files. Severity and relation to the original HIGH are assessed in the final verdict; this is not evidence of symlink-following on Go 1.26.

PASS — internal/fssecure/replace.go:64-109: TestR2PartialWriteCleanup set RLIMIT_FSIZE=1024, ignored SIGXFSZ, and attempted a 4096-byte replacement. Write returned EFBIG; old destination bytes and directory entries were unchanged, with no temporary left. TestR2TemporaryCollisions used a test-only random-name override in a disposable copy: an existing name survived all bounded attempts; one collision followed by a fresh name succeeded with no leftover. TestR2NewStatContainment refused lexical and linked-directory escapes; Lstat returned the leaf symlink itself while Stat refused following it outside the root.

Round 2, completion run: prompt
You are the cross-vendor reviewer for Judgment-Pack/judgment-pack-runtime pull request #228. This is the completion of the second, narrow review round. You are in a clone at the head b53e5c2. An earlier run of this round read commit 127899f and ended before it wrote its verdict; what it had established is listed below. Four commits followed it: read them with git diff 127899f...HEAD.

Write everything to the file <tmp>/rt228-r2b-findings.md: append each result as soon as you establish it (item, RESOLVED or NOT RESOLVED or a new finding with severity, file and line, one or two sentences, and the test or command that shows it), and write the final line "VERDICT: MERGEABLE" or "VERDICT: NOT MERGEABLE" with the deciding reason INTO THAT FILE before you finish. Your final message should be one line saying the results are in that file. Describe every result as a defect or a confirmation with a test case; keep the wording plain.

Background. Round 1 (commit e6f8cd8) found: HIGH, `jpack audit verify --witness-save <path>` wrote the saved continuation over whatever file was named, including the command's own inputs; MEDIUM, a statement's members were all read into memory before its shape was checked; MEDIUM, a test used one statement for both members of a continuation; LOW, README wording. The answer: every file the command reads is recorded by the identity of the opened file; a save destination that is one of them is refused; an existing destination is replaced only if it is a regular file, not a link, that parses as a continuation; the write goes through a held directory handle with an exclusively created temporary file.

Established by the earlier run of this round, at 127899f: the round-1 LOW is resolved; both round-1 MEDIUMs are resolved (measured allocations of 1 to 11 KB for lines of several megabytes; eight alterations of two distinct continuation statements each reported); in its probes the flag inputs, the project defaults, the configuration selected by the environment, companion files and regular-file standard input were all refused as save destinations, saving to the resume file advanced it, and the resume file given again as another input was refused; a partial write left the old file and no temporary file. It reported one new MEDIUM (a refused destination was answered before the trail was read, so a finding was not shown) and one LOW (the configuration's recorded identity was looked up again by path). The four commits since answer those two and add wording: the verification now runs whatever the destination's state, with four outcomes (a finding and an acceptable destination: exit 1, the finding reported; a finding and a refused destination: exit 1, the finding reported with a note that the destination was refused; no finding and an acceptable destination: exit 0, saved; no finding and a refused destination: exit 3, the message says the verification found nothing and nothing was saved); the configuration's identity is taken from the descriptor it was read through.

Two limits are stated in the code comment on ReplaceByRename, the guide and the CHANGELOG, and are accepted by the maintainers as documented limits of a local command writing to a path its user names: the last check of the destination and the rename are two steps; and a build with Go 1.24 renames by the directory's path after a check, while Go 1.25 and later rename through the held directory. Do not test these two further. Do check that the documents describe them accurately and claim nothing stronger.

What to do: (1) read the four commits and confirm or refute each of the four outcomes by running the command on small files you create (a trail that is not valid, a valid fixture from the existing tests, a destination that is a plain text file, a destination that does not exist); confirm that in every case the destination's bytes are unchanged unless a continuation was saved, and that no temporary file remains; (2) confirm the configuration's identity now comes from the opened file, using the existing test TestAnInputIsRecordedAsTheFileRead and your own check; (3) run the existing tests of the packages these commits touch (internal/cli, internal/project, internal/fssecure, internal/audit) and say what passed; (4) read README, the guide, the CHANGELOG and the command's help for the save's wording and say whether any sentence claims more than the code does; (5) state, for the round-1 HIGH, RESOLVED or NOT RESOLVED, on the evidence above and your own; (6) report any new finding in these four commits with its severity.

You may run, with GOCACHE set to a directory of your own: gofmt -l . ; go vet ./... ; go test on the packages named (tests that listen on a socket may be refused by the sandbox; say so) ; go run ./cmd/jpack audit verify ... on files you create in a temporary directory ; read-only git commands. Wrap every go command in ( ulimit -v 12000000; <command> ). Keep your Go cache and temporary files under 2 GB and delete them when you finish, except the results file. Do not modify tracked files in place.
Round 2, completion run: results file, with the verdict

PR #228 narrow second-round completion; head b53e5c2.
Scope: git diff 127899f...HEAD (ce47031, 72683b6, d556217, b53e5c2). The two accepted rename limits are reviewed in text only.

  1. Confirmation — PASS: internal/project/project.go:437 and internal/fssecure/replace.go:46. The complete internal/project and internal/fssecure package suites passed (26.576s and 1.150s). Command: ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp go test ./internal/cli ./internal/project ./internal/fssecure ./internal/audit ).

  2. Confirmation — sandbox limitation: internal/cli/audit_stamp_test.go:27. The full CLI suite stopped at TestAuditStampStampsTheCurrentCheckpointOnce because httptest could not listen on tcp6 [::1]:0 (socket: operation not permitted); this is not a product failure. Command: the four-package go test command in item 1; a CLI run excluding the socket tests follows.

  3. Round-2 LOW — RESOLVED: internal/fssecure/replace.go:57, internal/fssecure/root.go:261, internal/project/project.go:426. ReadIdentified returns File.Stat information from the descriptor it reads, and ConfigFile returns that stored identity; TestAnInputIsRecordedAsTheFileRead passed. My separate TestReviewerR2BConfigIdentityAfterUnlinkAndClose also passed: after hard-linking the original, unlinking its configured name, installing a continuation at that name, and closing the project, the original was still recorded/refused and the replacement was not mistaken for an input; command in an untracked temporary source copy: ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp go test ./internal/cli -run '^Test(ReviewerR2B|AnInputIsRecordedAsTheFileRead)' -v ).

  4. Confirmation — PASS: internal/audit/witness.go:1. The complete internal/audit package suite passed in 37.305s; command: the four-package go test command in item 1.

Confirmation — PASS, finding-acceptable: internal/cli/audit.go:173. CLI exit 1; the destination remained absent and no temporary file remained. Findings matched the command without --witness-save. Command: ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp go run ./cmd/jpack audit verify --format json --witness-key /rt228-r2b-work/fixtures/key.pub --witness /rt228-r2b-work/fixtures/statements.jsonl --trail /rt228-r2b-work/fixtures/invalid.jsonl --witness-save /rt228-r2b-work/finding-acceptable/save.json ); fixture exported by newSaveFixture from internal/cli/audit_witness_save_test.go:40 in a temporary source copy.

Confirmation — PASS, finding-refused: internal/cli/audit.go:173. CLI exit 1; the destination stayed byte-for-byte unchanged and no temporary file remained. Findings matched the command without --witness-save and saveRefused explained the refusal. Command: ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp go run ./cmd/jpack audit verify --format json --witness-key /rt228-r2b-work/fixtures/key.pub --witness /rt228-r2b-work/fixtures/statements.jsonl --trail /rt228-r2b-work/fixtures/invalid.jsonl --witness-save /rt228-r2b-work/finding-refused/save.json ); fixture exported by newSaveFixture from internal/cli/audit_witness_save_test.go:40 in a temporary source copy.

Confirmation — PASS, clean-acceptable: internal/cli/audit.go:173. CLI exit 0; a continuation was saved to the previously absent destination and no temporary file remained. The saved JSON parsed as continuationVersion 1. Command: ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp go run ./cmd/jpack audit verify --format json --witness-key /rt228-r2b-work/fixtures/key.pub --witness /rt228-r2b-work/fixtures/statements.jsonl --trail /rt228-r2b-work/fixtures/valid.jsonl --witness-save /rt228-r2b-work/clean-acceptable/save.json ); fixture exported by newSaveFixture from internal/cli/audit_witness_save_test.go:40 in a temporary source copy.

Confirmation — PASS, clean-refused: internal/cli/audit.go:173. CLI exit 3; the destination stayed byte-for-byte unchanged and no temporary file remained. The refusal explicitly said verification found nothing and nothing was saved. Command: ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp go run ./cmd/jpack audit verify --format json --witness-key /rt228-r2b-work/fixtures/key.pub --witness /rt228-r2b-work/fixtures/statements.jsonl --trail /rt228-r2b-work/fixtures/valid.jsonl --witness-save /rt228-r2b-work/clean-refused/save.json ); fixture exported by newSaveFixture from internal/cli/audit_witness_save_test.go:40 in a temporary source copy.

  1. Confirmation — PASS: repository Go sources (including internal/cli/audit.go:173). Formatting and static checks produced no output and exited 0: ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp gofmt -l . ) and ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp go vet ./... ).

  2. Round-1 LOW / save wording — RESOLVED: README.md:618, docs/building-with-packs.md:1199, CHANGELOG.md:46, internal/cli/audit.go:92. The revised wording describes refusal checks rather than an unconditional promise that an input can never be replaced; the guide and CHANGELOG state the final-check/rename gap and the Go 1.24 path-based fallback, consistent with ReplaceByRename and the two build-tagged rename implementations. Check: read git diff 127899f...HEAD, those documentation sections, internal/fssecure/replace.go:69, internal/fssecure/rename_path.go and rename_root.go (build-tagged implementations), and run ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp go run ./cmd/jpack audit verify --help ); .github/workflows/release.yml:51 pins Go 1.26.5, supporting the release-toolchain statement. Within the two stated limits, the revised save guarantees claim no stronger protection than the implementation; neither limit was probed further.

  3. Round-2 MEDIUM — RESOLVED: internal/cli/audit.go:168–190, internal/cli/audit_witness_save_test.go:466. All four independent command-line cases above passed, including unchanged destinations and no temporary files; finding cases matched the report without --witness-save, and the refused case added its reason. TestARefusedSaveNeverHidesAFinding also passed in the CLI suite, covering existing continuations, the human report, an absent destination directory, and a destination with no filename.

  4. Confirmation — PASS with five socket tests excluded: internal/cli/audit_witness_save_test.go:154, :339, :437, :466, :567. The remaining CLI suite passed in 16.312s, including all save protection, four-outcome, and opened-input identity tests; command: ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp go test ./internal/cli -skip '^(TestAuditStamp|TestAStampThatFailsLeavesTheTrailAndTheDecisionsUntouched$)' ). The excluded tests were TestAuditStampStampsTheCurrentCheckpointOnce, TestAuditStampReadsTheAuthorityFromTheConfiguration, TestAStampThatFailsLeavesTheTrailAndTheDecisionsUntouched, TestAuditStampAndItsFlagsRefuseWhatTheyCannotDo, and TestAuditStampRefusesAnUnchainedTrail; the first retry excluding only TestAuditStamp also confirmed the same sandbox socket refusal in TestAStampThatFailsLeavesTheTrailAndTheDecisionsUntouched.

  5. Round-1 MEDIUM (statement allocation) — RESOLVED: internal/audit/witness_test.go:804. The supplied earlier review measured only 1–11 KB allocated for multi-megabyte malformed statement lines; the four commits do not alter that parser, and TestAMemberTheFormDoesNotAllowEndsTheReading passed in the complete internal/audit suite (item 4).

  6. Round-1 MEDIUM (two continuation statements) — RESOLVED: internal/audit/witness_test.go:862. The supplied earlier review confirmed eight alterations of two distinct statements were reported; these commits leave that test unchanged, and TestBothOfAContinuationsStatementsAreChecked passed in the complete internal/audit suite (item 4).

  7. Round-1 HIGH (save overwrites inputs or arbitrary files) — RESOLVED: internal/cli/audit.go:1120, internal/fssecure/replace.go:100. The earlier evidence covers flag inputs, defaults, environment-selected configuration, companion files, regular-file stdin, allowed resume advancement, resume also used as another input, and partial-write cleanup; my plain-text destination probes and the current TestAuditVerifyNeverSavesOverAnInput / TestAuditVerifySavesOnlyOverAContinuation runs confirm preservation, while the independent identity check confirms the configuration remains protected after its name is replaced. Evidence: items 3, 7, and 8 and the four CLI cases above; this resolution accepts the two documented rename limits as instructed.

Confirmation — PASS, independent input-alias probe: internal/cli/audit.go:1134. Saving a clean verification onto a hard link to its trail exited 3 with the input-refusal explanation, preserved both names' bytes, and left no temporary file. Command: ( ulimit -v 12000000; GOCACHE=/rt228-r2b-work/cache GOTMPDIR=/rt228-r2b-work/tmp go run ./cmd/jpack audit verify --format json --trail /rt228-r2b-work/fixtures/valid.jsonl --witness-key /rt228-r2b-work/fixtures/key.pub --witness /rt228-r2b-work/fixtures/statements.jsonl --witness-save /rt228-r2b-work/input-alias/trail-hardlink ).

  1. Confirmation — no new finding in the four commits: internal/cli/audit.go:173, internal/project/project.go:437, internal/fssecure/replace.go:46, internal/result/audit.go:212. Review of git diff 127899f...HEAD and the tests/probes above found no new defect; the added saveRefused field preserves the finding report, and the configuration identity comes from the file actually read. Command: git diff --check 127899f...HEAD also passed.

  2. Confirmation — cleanup PASS: workspace and /rt228-r2b-work. The review cache and temporary source, fixtures, and logs stayed below 2 GB (observed high-water directory size 365 MB; 213854593 bytes before removal) and were deleted; git status --porcelain is empty. Check: directory-size checks during execution, final size assertion, shutil.rmtree, and absence check; only /rt228-r2b-findings.md is retained.

VERDICT: MERGEABLE — the round-1 HIGH and both round-2 findings are resolved, all four requested outcomes passed without unintended writes, and no new defect was found; only the five socket-dependent CLI tests remain unverified because the sandbox refuses listening sockets.

Round 2, completion run: final message

The results are in /rt228-r2b-findings.md.

@kikashy
kikashy merged commit 6484745 into main Oct 5, 2026
6 checks passed
@kikashy
kikashy deleted the feat/audit-verify-witness-adr-0013-pr4 branch October 5, 2026 19:11
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🎉 Thank you for your contribution to Judgment Pack!

If you’d like another task, choose a path that interests you:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant