cli.sgit.ai / Dev packs / Pack indexes, 04: compatibility, security and tests

04. Compatibility, security and tests

1. Compatibility

ClientVault without index packsVault with index packs
0.23.x and olderrefused by the pack gate, as todayrefused, as today; nothing changes
0.24.0 to 0.25.0as todayas today. It never lists bare/packs/indexes/, ignores the advisory feature, extracts path-ordered packs (sorted by name before writing) and verifies them by hash. It pushes commit packs without indexes: gaps, which new readers fill locally
new, Phase 1+as today: no index listing without the featureuses the indexes; walks across gaps

Acceptance test C1 must hold on every phase: sgit-ai 0.25.0 from PyPI clones, pulls, pushes, compacts and runs pack verify on an indexed vault with output identical to an unindexed one. That includes not one extra request to indexes/.

Loose vaults are untouched. Indexes are a pack-vault feature; the cache layer (08/12) remains the loose vault's index.

2. Threat model

New rows for the threat model, beside TM-R41 (the 0.25.0 caches).

ThreatWhat stops itResidual
The host forges or alters an index memberAES-GCM under the read key; the header's member hashes; the signaturenone beyond today: the host can delete, which is a gap
The host learns paths, names, messages or which objects are trees from index packseverything is encrypted; member names are sequence numberssizes of index packs and members, and their count per commit, which already shows as the data pack's size
The host learns structure from path-ordered data packs (Phase 2)it sees which members are adjacent, which shows blobs grouped by folder and roughly each folder's size, not namesa new, mild leak. Mitigations: order by top-level folder groups and by id inside each group; a per-vault option pack-order: id that keeps today's order and gives up ranged scoped reads. Record it as TM-R42
The host learns what a reader reads, from its ranged readsnone, as today: per-object reads on loose vaults show the sameunchanged
A write-key holder pushes an index that disagrees with the treessignature binds it to a key; under signatures-required the same policy as commits; the anchor check (root equals the commit's tree); every object still verified by id before usea signing teammate can mislead a reader's plan (fetch the wrong set, show a wrong log), never what is written. pack index verify detects it in full
A write-key holder pushes an index whose at points at other bytesthe bytes must hash to the blob id the entry namesnone: a mismatch is refused, the index dropped
A local reader of the disk learns more than todayindex packs stay encrypted on disk; assembly is in memorynone beyond TM-R41
A crafted header exhausts memory or timeheader reserved size R at most 64 MiB; member length at most 256 MiB; JSON depth bounded; members checked against their hash before parsinga large but honest index costs what it costs

3. Rollout

StageWhatWho
1Phase 1 behind SGIT_PACK_INDEXES=1 for writers; readers use indexes when presentthe CLI team, on test vaults and the benchmark vaults
2writers on by default; sgit pack index build run once on the newsroom's vaults to backfillthe newsroom's agents, which upgrade on a brief
3Phase 2: path-ordered packs from new pushes and compactions; clone --path uses rangesafter C1 and T7 pass on dev
4Phase 3: pack store, behind SGIT_PACK_STORE=1, then default after the cloud-disk benchmarkthe CLI team
5Phase 4: reads without a clonewith the SG/Vault team

Rollback at any stage: stop writing index packs. Readers fall back per pack; nothing on the host needs removing, and old clients never noticed.

4. Acceptance tests

Each phase ships with its tests green on the in-memory host and once live on dev.

#TestPhase
C10.25.0 from PyPI on an indexed vault: clone, pull, push, compact, pack verify give identical results and make no request under indexes/every phase
T1Index equals walk. The fuzzer (16 seeds, about 450 commits, merges, deletes, renames): after every push, the flat map assembled from indexes equals a full tree flatten; every changes equals diff_trees of the commit against its first parent; every folder member equals its tree1
T2History. base..head and merge base from generations equal a full walk, on the fuzzer's histories and on a history with a clock 1 day behind1
T3Gaps. A 0.25.0 client pushes three commits into an indexed vault; a new reader's flat map, log and commit list equal a full walk, and pack index build fills the gaps1
T4A lying index. For each: a changed blob id in flat, a wrong tree_id, a bad signature on a signatures-required vault, a member whose hash differs from the header, a truncated header. The index is dropped, the command succeeds by walking, and nothing wrong is written1
T5Orphans. An index pack without its data pack is ignored; a data pack without an index is a gap1
T6Compaction. After compaction, index packs of archived commit packs are in indexes/archive/; a fresh clone uses the snapshot index alone1
T7Scoped clone by ranges. On the counted host, clone --path raw/c09/s4 of the benchmark vault downloads at most the index packs plus 1.1 × the scope's bytes, and its working copy equals a full clone's folder2
T8Determinism. The same commit gives the same data pack bytes, path-ordered; 0.25.0 extracts it identically2
T9Pack store. A clone with the pack store and one without produce identical working copies and identical answers to status, log and fsck; one flipped byte in a kept pack is caught on first read3
T10Nothing new in clear. After clone, pull and log, no file under .sg_vault/ holds a path or name that 0.25.0 would not have held1

5. Benchmarks

Run before and after each phase, on the 76,000-file benchmark vault and the CLI team's 228,000-file synthetic vault, on the in-memory host and once against dev. Record them in the CHANGELOG.

BenchmarkTodayPhase target
pull of a 300-file pass31.7 s (branch; 228,000 files)Phase 1: the 6 s presence check and any tree reads gone; what remains is the stat pass and the file writes
history log, 10 commits with counts7.4 s (228,000 files)Phase 1: under 100 ms
base..head on a 5,000-commit history1.9 s at 0.25.0Phase 1: under 1 ms after the index is loaded
first flatten of an unseen headabout 45 s (228,000 files)Phase 1: under 2 s
clone --path of 1,520 filesthe whole snapshotPhase 2: index packs plus about 0.22 MB
full clone216 s (228,000 files)Phase 3: the object writes gone; measure on a cloud session's disk
push of one five-file commitas 0.25.0every phase: no more than 1% slower

6. Open questions

  1. Path-order grouping. Group by top-level folder (milder leak, coarser ranges) or full path order (one range per folder)? The recommendation is top-level groups by default, with full path order as a per-vault choice.
  2. flat and folder:* both in the snapshot. flat serves full clones in one member; folders serve scoped readers. Both cost about 14 MB for the benchmark vault. Keep both until a measurement says otherwise.
  3. file-commits across a shallow boundary. Stop at the boundary, as Vault__Scope does, and say so in the member.
  4. Index packs for loose vaults. Not needed now: loose vaults are the small ones. Revisit if a large loose vault appears.
  5. Under sealed vault keys, the local index is already encrypted at rest. The in-memory assembly lives in the process that holds the read key, so this needs nothing new.

This page as markdown · its source, admin/content/dev/pack-indexes/04-compatibility-security-and-tests.md