crates/heimdall-wasm/wit/heimdall.wit
// SPDX-License-Identifier: AGPL-3.0-only
//
// Heimdall is a defensive sandbox. This is the whole interface between the
// Heimdall wasm host and a guest (spec §6): bytes in, a verdict and files out.
// There is no WASI: no filesystem, no network, no clock, no environment.
package heimdall:guest@0.1.0;
world job {
/// One job parameter, passed through from the caller's Job params.
record param {
key: string,
value: string,
}
/// The guest accepted the input. `note` is a short human-readable summary.
record output {
note: string,
}
/// The guest refused the input. `reason` says why, in plain words.
record refusal {
reason: string,
}
/// Hand one output file to the host. `name` is a single plain file name
/// (ASCII letters, digits, '.', '_', '-', '+'; no '/', no leading '.').
/// A refused name or a limit hit stops the guest; the job is then refused.
import emit-file: func(name: string, bytes: list<u8>);
/// Run the job on `input`.
export run: func(input: list<u8>, params: list<param>) -> result<output, refusal>;
}
/// The DECODER half of a split guest (docs/GATLING-WASM.md). The host runs
/// many short-lived instances of it, each with its own memory, fuel and
/// deadline, each handed only the compressed frames of its own chunks. It
/// imports nothing at all, not even `emit-file`: bytes in, bytes out.
world decoder {
/// One compressed chunk: the whole frame, and the decoded size the
/// archive index declares (the decoder must produce exactly that many).
record chunk-ref {
frame: list<u8>,
size: u64,
/// false: only the digest is wanted and `bytes` comes back empty.
want-bytes: bool,
}
/// What one chunk decoded to: the BLAKE3-256 of the decoded bytes (32
/// bytes) and, when they were asked for, the bytes.
record decoded {
digest: list<u8>,
bytes: list<u8>,
}
/// One answer per chunk, in order: the decoded chunk, or why its frame
/// does not decode.
type chunk-result = result<decoded, string>;
export decode-chunks: func(chunks: list<chunk-ref>) -> list<chunk-result>;
}
/// The VERIFIER half of a split guest: a `job` that never decodes itself. It
/// hands compressed frames to the host, which decodes them in isolated
/// `decoder` instances (in parallel), checks the length and BLAKE3 of every
/// answer natively, and returns the answers in order. The verifier never
/// shares memory with a decoder.
world verifier {
include job;
record chunk-ref {
frame: list<u8>,
size: u64,
want-bytes: bool,
}
record decoded {
digest: list<u8>,
bytes: list<u8>,
}
type chunk-result = result<decoded, string>;
/// Decode `chunks` in decoder instances. A decoder that traps, runs out of
/// its own limits or answers falsely ends the job with the host's code.
import decode-chunks: func(chunks: list<chunk-ref>) -> list<chunk-result>;
/// SHA-256 and BLAKE3-256 (32 bytes each) of a blob.
record member-digest {
sha256: list<u8>,
blake3: list<u8>,
}
/// The digests of every blob, in order, computed by the host natively and
/// in parallel (the host is the code that signs the verdict; it hashes,
/// it never parses). The blobs are lent, not copied.
import digest-members: func(blobs: list<list<u8>>) -> list<member-digest>;
}