Files
sientia-dataops-laborious_t…/docs/sientia-bundle-format.md
vitor-aignosi 68600e7b31
Some checks failed
Quality gate / quality-gate (push) Has been cancelled
Add docs
2026-09-01 13:28:13 -03:00

5.3 KiB

The .sientia bundle format

A .sientia file is one exported MLflow model: its whole artifact tree, its run parameters and its origin metadata, zipped and then encrypted under a password the exporter chose.

It is written by the Streamlit platform, sientia-projects-templates (app/src/operations/ model_export/). That application owns the export — its screen, its flow, its code — and documents it. This page is the short version: the parameters both sides have to agree on, and what this repository's reader accepts.

How it is generated

The producer downloads the run's artifact tree whole, copies run.data.params verbatim (with target_variable among them, or the import fails at gate 6), writes metadata.json first into a ZIP_DEFLATED archive with the artifacts following as artifacts/<relpath> and no directory entries, encrypts that zip, and offers it as {model_name}_v{model_version}.sientia.

metadata.json has exactly two keys: parameters — the run's parameters verbatim — and metadata, six fields: model_name, experiment_name, run_id, model_version, model_project, export_timestamp (UTC ISO 8601).

Encryption parameters

The reader derives the key with the values the header carries, but refuses anything above the producer's own cost — a header declaring more is a resource-exhaustion attempt, refused before any memory is allocated.

Value
KDF Argon2id (crypto_pwhash_ALG_ARGON2ID13)
memlimit 268435456 (256 MiB) — a ceiling on the reader's side
opslimit 3 — likewise a ceiling
parallelism 1 (reserved, not configurable)
salt 16 bytes, fresh per bundle
key 32 bytes
AEAD XChaCha20-Poly1305 secretstream, 64 KiB plaintext chunks, last tagged FINAL
digest SHA-256 of the pushed chunks only — on disk, sha256(file[164:]), past the 140-byte header and the 24-byte stream header. Hashing from 140 rejects every valid bundle
password normalised NFC, encoded UTF-8 — no trimming, no case folding

The export-side password rule (12+ characters, mixed case, digit, symbol) is enforced there only: by the time a bundle exists, the password is whatever it was, and the reader cannot re-check it.

Shape of the file

HEADER_STRUCT is >8sBBB16sQQB32s64s. Big-endian, no alignment padding, so the offsets are exact:

offset  width  field                 constraint the reader enforces
------  -----  --------------------  ------------------------------------------------------
     0      8  magic                 == b'SIENTIA1'
     8      1  format_version        == 1
     9      1  aead_id               == 1 (XChaCha20-Poly1305 secretstream)
    10      1  kdf_id                == 1 (Argon2id)
    11     16  salt                  passed to the KDF as-is
    27      8  kdf_memlimit_bytes    <= 268435456
    35      8  kdf_opslimit          <= 3
    43      1  kdf_parallelism       == 1
    44     32  ciphertext_digest     == sha256(file[164:])
    76     64  reserved_signature    all zero
------  -----
   140     24  secretstream header   libsodium HEADERBYTES
   164    ...  chunk stream          64 KiB plaintext chunks, each +17 bytes of AEAD tag

format_version is checked for equality, not for a range: an unknown version is refused at gate 2 rather than parsed optimistically. reserved_signature must be all zero today, so a signed bundle will be a new format version and not a silent change of meaning.

laborious/utils/bundle/format.py is the normative layout on this side, and the golden fixture is what keeps it and the producer from drifting apart.

How it is read

BundleReader.open() runs seven ordered gates, each mapped to an import step and to one message a person can read:

Gate Step Refuses
1 download object too large, wrong prefix or suffix, digest ≠ the one the uploader sent
2 download header that is not this format or not this version
3 decryption wrong password, or altered bytes — the AEAD cannot tell them apart, and the message says so
4 archive_inspection zip bomb, absolute or .. entries, symlinks — read from the central directory, before extracting
5 extraction anything that escapes the fresh work directory, or overwrites a file
6 structure_validation missing metadata.json fields, missing target_variable, missing prediction_model/data_model
7 content_policy a file that is not a known model file by name or suffix

Gates 1 and 2 share the download step on purpose: neither may borrow the decryption sentence, which implicates the password. A gate-6 rejection naming target_variable means the origin run never logged it — the fix is a re-export, on the producer's side.

Error codes, sentences and the ceilings the gates read from configuration: model-import.md.

Fixtures

  • tests/fixtures/bundle/ — a real bundle emitted by the producer, with its password and producer commit recorded in that directory's README.md. It pays the producer's real Argon2id cost, so it is used where the real bytes matter and not in every test.
  • tests/helpers/bundle_factory.py — the test-only writer: same header at the same offsets, cheap KDF parameters, and a knob for every hostile variant the gates above refuse.