go build ./...
go test ./...
CI (.github/workflows/ci.yml) runs on pushes to main and on pull requests:
gofmt, vet, build, and the tests with the race detector; staticcheck,
govulncheck, and a vendor-drift check; short fuzzing of the parsers; the
chat-template check over llama.cpp's real templates; the same build, test, and
supply-chain checks for tools/socair-sigstore; and the wizard's check, tests,
and build.
cmd/socair: the CLI. A thin caller of the engine, not the product surface.internal/gguf: the GGUF artifact reader. Produces agguf.Manifest.internal/airlock: the controlled junction between egress and the clean store. A pull lands in staging; only a promotion crosses.internal/modeldir: reads a model directory as one artifact (walk, snapshot, manifest digest, chat templates).internal/report: the report data model, the cross-stack contract.internal/checks/...: one package per check. Each returns PASS, FAIL, LEAD, or NOT_TESTED with evidence.docs/: the product artifacts (attestation template, brand, stack) and design docs.testdata/: golden files. GGUF fixtures are generated in tests, not committed, so they cannot rot silently.
A full scan copies the artifact into a private, read-only snapshot and hashes
it while copying; every check reads the snapshot, so the hash in the
attestation is the hash of the bytes that were checked, even if the original
changes mid-scan. The snapshot needs free space equal to the artifact's size in
the temp directory; set SOCAIR_SCAN_TMP to a volume with room. It is removed
when the scan ends.
socair scan <dir> scans a Hugging Face repo checkout or cache snapshot
(~/.cache/huggingface/hub/models--org--name/snapshots/<commit>) as one
artifact:
- Every file is snapshotted and hashed. Symlinks to files are followed (a
cache snapshot is built of them); symlinked directories and tool metadata
(
.git,.cache,__pycache__) are excluded and named in the report. - The subject is the digest of the canonical manifest:
socair.modeldir/v1\nthen<sha256> <size> <path>\nper file, sorted by path. The report lists every file with its hash and role.socair verify --artifact <dir>recomputes it and names any file added, removed, or changed. - Each weight file runs the structure, inventory, and pickle checks, merged to
one row per check: the worst result wins, and every file that did not PASS
is named. A
*.safetensors.index.jsonmust agree exactly with its shards. - The chat template comes from
tokenizer_config.json,chat_template.json, and any.jinjafile. - Remote code is a LEAD for the Python a loader would import: a module an
auto_map(any config, any depth),custom_pipelines, a sentence-transformersmodules.jsonor Router, or a diffusersmodel_index.jsonnames; a file a loader imports by its path (custom_generate/generate.py, andhandler.py,pipeline.py, orcode/inference.pyat the root); and every repository file those import, transitively. A reference to another repository or outside the directory is a LEAD too. Any other.pyfile is listed in the notes, not judged (internal/checks/remotecode, #254). - The tokenizer is inspected from
tokenizer.json,tokenizer_config.json,special_tokens_map.json, and the special-token ids inconfig.jsonandgeneration_config.json(tokenizer.InspectHF): inconsistent tables FAIL, injected text LEADs, and a repo with notokenizer.jsonis NOT_TESTED.artifact.tokenizer_hashis the SHA-256 oftokenizer.json. - An adapter (
adapter_config.json) names its base model as a separate artifact that needs its own attestation. airlock pull --repo(pinned to a commit or digest) stages a whole repo, verified file by file against the hub's hashes, andairlock promotecarries a directory intoclean/<digest>/<name>/(seedocs/airlock.md).
- Tests are hermetic. No network, no secrets, no model files required. A check is proven with generated fixtures. The airlock tests stand an
httptestserver in for the hub and a temp directory in for the store. - A test that needs a real model is gated by an environment variable and skipped by default. See
internal/gguf/integration_test.goand the egress test ininternal/airlock/integration_test.go. - The report data model in
internal/reportanddocs/report-schema/v1.jsonis the contract. No component may invent a divergent report shape. - Every check carries a falsification: a test that fails if the detector is neutered. See each package's
_test.go. - A change to the rules (the check packages, or the GGUF, safetensors, ONNX, OpenVINO IR, HDF5, and Keras parsers) needs a row in
docs/check-set.md, and a version bump if it can alter a verdict.TestCheckSetVersionTracksRulesenforces it.