Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions cmd/socair-preview/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//go:build js && wasm

// Command socair-preview is Socair's engine built for a browser, for
// socair.ai's preview of a Hugging Face model. The page mounts the repo as a
// filesystem whose reads are HTTP range requests (tools/socair-preview) and
// calls socairPreview(dir, hashesJSON), which reads it with engine.PreviewDir
// and resolves to the report as JSON and as HTML.
package main

import (
"bytes"
"encoding/json"
"syscall/js"

"github.com/defilantech/socair/internal/engine"
"github.com/defilantech/socair/internal/render"
rpt "github.com/defilantech/socair/internal/report"
)

func main() {
js.Global().Set("socairPreview", js.FuncOf(preview))
js.Global().Set("socairPreviewFile", js.FuncOf(previewFile))
js.Global().Set("socairVersion", engine.Version)
select {}
}

// preview returns a Promise for the report of the directory args[0], with
// each file's SHA-256 as its source states it in the JSON object args[1].
func preview(_ js.Value, args []js.Value) any {
if len(args) != 2 {
return js.Global().Get("Promise").Call("reject", "socairPreview(dir, hashesJSON)")
}
dir, hashesJSON := args[0].String(), args[1].String()
var run js.Func
run = js.FuncOf(func(_ js.Value, p []js.Value) any {
resolve, reject := p[0], p[1]
go func() {
defer run.Release()
out, err := report(dir, hashesJSON)
if err != nil {
reject.Invoke(err.Error())
return
}
resolve.Invoke(js.ValueOf(out))
}()
return nil
})
return js.Global().Get("Promise").New(run)
}

// previewFile returns a Promise for the header-only report of the single file
// args[0], for a file whose own metadata carries checks a directory scan
// does not run on it (a GGUF's chat template, tokenizer, and quant).
func previewFile(_ js.Value, args []js.Value) any {
if len(args) != 1 {
return js.Global().Get("Promise").Call("reject", "socairPreviewFile(path)")
}
path := args[0].String()
var run js.Func
run = js.FuncOf(func(_ js.Value, p []js.Value) any {
resolve, reject := p[0], p[1]
go func() {
defer run.Release()
d, err := engine.ScanMode(path, engine.ModeHeaders)
if err == nil {
var out map[string]any
if out, err = documents(d); err == nil {
resolve.Invoke(js.ValueOf(out))
return
}
}
reject.Invoke(err.Error())
}()
return nil
})
return js.Global().Get("Promise").New(run)
}

func report(dir, hashesJSON string) (map[string]any, error) {
var hashes map[string]string
if err := json.Unmarshal([]byte(hashesJSON), &hashes); err != nil {
return nil, err
}
d, err := engine.PreviewDir(dir, hashes)
if err != nil {
return nil, err
}
return documents(d)
}

// documents is the report as JSON and as the HTML a scan renders.
func documents(d *rpt.Document) (map[string]any, error) {
doc, err := json.Marshal(d)
if err != nil {
return nil, err
}
var html bytes.Buffer
if err := render.Render(&html, d); err != nil {
return nil, err
}
return map[string]any{"json": string(doc), "html": html.String()}, nil
}
29 changes: 29 additions & 0 deletions internal/engine/preview.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package engine

import (
"time"

"github.com/defilantech/socair/internal/report"
)

// PreviewDir reads a model directory the way a scan does, in place and as
// far as each check reads, and never hashes a file: each file's SHA-256 is
// the one stated gives for its path, as its source (a hub's API) states it,
// and every file needs one. Nothing it reads is copied, so a reader that
// fetches byte ranges on demand (socair.ai's browser preview) fetches only
// what the checks read: headers, configs, templates, tokenizers.
//
// It is a preview, never an attestation: the subject is the manifest digest
// of hashes the scan did not compute, and the caller must say so. Tier 2
// does not run.
func PreviewDir(dir string, stated map[string]string) (*report.Document, error) {
start := time.Now().UTC()
refs, err := loadReferences(start)
if err != nil {
return nil, err
}
if stated == nil {
stated = map[string]string{}
}
return scanDir(dir, start, refs, Inputs{}, nil, stated)
}
89 changes: 89 additions & 0 deletions internal/engine/preview_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package engine

import (
"path/filepath"
"strings"
"testing"

"github.com/defilantech/socair/internal/modeldir"
"github.com/defilantech/socair/internal/report"
)

// stated returns each file's true SHA-256, as a hub would state it.
func stated(t *testing.T, dir string) map[string]string {
t.Helper()
_, files, err := modeldir.Hash(dir)
if err != nil {
t.Fatal(err)
}
m := map[string]string{}
for _, f := range files {
m[f.Path] = f.SHA256
}
return m
}

func TestPreviewReadsInPlaceWithStatedHashes(t *testing.T) {
dir := modelRepo(t, nil)
hashes := stated(t, dir)
// A stated hash is reported as stated: the preview never hashes a file.
hashes["model.safetensors"] = strings.Repeat("ab", 32)
// Falsification: a snapshot needs this volume, which does not exist.
t.Setenv("SOCAIR_SCAN_TMP", filepath.Join(t.TempDir(), "missing"))

d, err := PreviewDir(dir, hashes)
if err != nil {
t.Fatal(err)
}
if problems := report.Validate(d); len(problems) != 0 {
t.Fatal(problems)
}
for _, f := range d.Artifact.Files {
if f.SHA256 != hashes[f.Path] {
t.Errorf("%s: %s, want the stated %s", f.Path, f.SHA256, hashes[f.Path])
}
}
for name, want := range map[string]report.Status{
"Format and structure": report.StatusPass, "Chat template": report.StatusPass,
"Remote code": report.StatusPass, "Tokenizer config": report.StatusPass,
} {
if got := row(d, name).Status; got != want {
t.Errorf("%s = %s (%s), want %s", name, got, row(d, name).Notes, want)
}
}
if d.Artifact.Name != "org/tiny" {
t.Errorf("name %q", d.Artifact.Name)
}
}

func TestPreviewNeedsEveryFilesHash(t *testing.T) {
dir := modelRepo(t, nil)
hashes := stated(t, dir)
delete(hashes, "config.json")
if _, err := PreviewDir(dir, hashes); err == nil || !strings.Contains(err.Error(), "config.json") {
t.Fatalf("a file with no stated hash must stop the preview, naming it: %v", err)
}
}

func TestPreviewFindsWhatAScanFinds(t *testing.T) {
dir := modelRepo(t, map[string]string{"modeling_tiny.py": "import os\n", "config.json": `{"auto_map":{"AutoModel":"modeling_tiny.TinyModel"}}`})
full, err := Scan(dir)
if err != nil {
t.Fatal(err)
}
d, err := PreviewDir(dir, stated(t, dir))
if err != nil {
t.Fatal(err)
}
if d.Artifact.SHA256 != full.Artifact.SHA256 {
t.Errorf("subject %s, a full scan's %s", d.Artifact.SHA256, full.Artifact.SHA256)
}
for _, c := range full.Checks {
if got := row(d, c.Name); got.Status != c.Status {
t.Errorf("%s: preview %s, full scan %s", c.Name, got.Status, c.Status)
}
}
if row(d, "Remote code").Status != report.StatusLead {
t.Errorf("remote code = %s, want LEAD", row(d, "Remote code").Status)
}
}
2 changes: 1 addition & 1 deletion internal/engine/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func ScanWith(path string, mode Mode, in Inputs) (*report.Document, error) {
if mode != ModeFull {
return nil, fmt.Errorf("%s is a directory; a directory scan always reads every file in full", path)
}
return scanDir(path, start, refs, in, t2)
return scanDir(path, start, refs, in, t2, nil)
}

// A full scan is an attestation, so it checks a private snapshot whose
Expand Down
31 changes: 28 additions & 3 deletions internal/engine/scandir.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ var pickleExt = map[string]bool{".bin": true, ".pt": true, ".pth": true, ".ckpt"
// Every file is snapshotted and hashed; the subject is the manifest digest.
// Each per-file check runs on every file it applies to and reports one merged
// row, so one bad shard withholds the whole directory.
func scanDir(dir string, start time.Time, refs *references, in Inputs, t2 *tier2.Config) (*report.Document, error) {
root, files, excluded, cleanup, err := modeldir.Snapshot(dir, strings.TrimSpace(os.Getenv("SOCAIR_SCAN_TMP")))
//
// With stated hashes it is a preview instead (PreviewDir): the files are read
// in place, as far as each check reads them, and never hashed.
func scanDir(dir string, start time.Time, refs *references, in Inputs, t2 *tier2.Config, stated map[string]string) (*report.Document, error) {
root, files, excluded, cleanup, err := snapshotOrList(dir, stated)
var short *diskfree.ShortError
if errors.As(err, &short) {
return nil, fmt.Errorf("%w (set SOCAIR_SCAN_TMP to a volume with room)", err)
Expand All @@ -50,7 +53,7 @@ func scanDir(dir string, start time.Time, refs *references, in Inputs, t2 *tier2
return nil, err
}
defer cleanup()
if afterSnapshot != nil {
if afterSnapshot != nil && stated == nil {
afterSnapshot(dir)
}
at := func(f modeldir.File) string { return filepath.Join(root, filepath.FromSlash(f.Path)) }
Expand Down Expand Up @@ -280,6 +283,28 @@ func scanDir(dir string, start time.Time, refs *references, in Inputs, t2 *tier2
return d, nil
}

// snapshotOrList is a scan's private snapshot of dir, or, for a preview, the
// directory itself with the hashes its source states, every file needing one.
func snapshotOrList(dir string, stated map[string]string) (string, []modeldir.File, []modeldir.Excluded, func(), error) {
if stated == nil {
return modeldir.Snapshot(dir, strings.TrimSpace(os.Getenv("SOCAIR_SCAN_TMP")))
}
files, excluded, err := modeldir.List(dir, func(rel string) string { return stated[rel] })
if err != nil {
return "", nil, nil, nil, err
}
var missing []string
for _, f := range files {
if f.SHA256 == "" {
missing = append(missing, f.Path)
}
}
if len(missing) > 0 {
return "", nil, nil, nil, fmt.Errorf("no stated hash for %s", strings.Join(missing, ", "))
}
return dir, files, excluded, func() {}, nil
}

// kerasExt are the extensions of Keras models: HDF5 files and .keras
// archives. A file of one is read as Keras when its bytes are HDF5 or a
// .keras archive (isKeras), whichever its extension names; otherwise it is
Expand Down
42 changes: 42 additions & 0 deletions internal/modeldir/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package modeldir

import (
"os"
"path/filepath"
"testing"
)

func TestListStatesHashesWithoutReadingAFile(t *testing.T) {
dir := t.TempDir()
for rel, body := range map[string]string{"config.json": "{}", "model.safetensors": "weights"} {
if err := os.WriteFile(filepath.Join(dir, rel), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
// Falsification: a file nobody can read fails any walk that opens it.
locked := filepath.Join(dir, "model.safetensors")
if err := os.Chmod(locked, 0); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chmod(locked, 0o644) })
if f, err := os.Open(locked); err == nil {
f.Close()
t.Skip("running as a user who reads any file")
}

files, _, err := List(dir, func(rel string) string { return "stated:" + rel })
if err != nil {
t.Fatal(err)
}
if len(files) != 2 {
t.Fatalf("%d files", len(files))
}
for _, f := range files {
if f.SHA256 != "stated:"+f.Path {
t.Errorf("%s: hash %q", f.Path, f.SHA256)
}
}
if files[1].Path != "model.safetensors" || files[1].Size != int64(len("weights")) || files[1].Role != RoleWeights {
t.Errorf("%+v", files[1])
}
}
17 changes: 17 additions & 0 deletions internal/modeldir/modeldir.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,23 @@ func Hash(dir string) (string, []File, error) {
return Digest(files), files, nil
}

// List walks dir in the scan's rules and reads no file: each file's digest
// is the one stated gives for its path ("" when it gives none), and its size
// is its size on disk. It is for a header-only preview, which attests nothing
// and names where its hashes came from.
func List(dir string, stated func(rel string) string) ([]File, []Excluded, error) {
if err := isDir(dir); err != nil {
return nil, nil, err
}
return walk(dir, func(src, rel string) (string, int64, error) {
st, err := os.Stat(src)
if err != nil {
return "", 0, err
}
return stated(rel), st.Size(), nil
})
}

func isDir(dir string) error {
fi, err := os.Stat(dir)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions tools/socair-preview/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
Loading
Loading