open-standards/wai/wai-sink-swift/Sources/WaiSink/IntDecoder.swift
Transaction Science 8584456f5c Sinks hold their operator's model policy: each neural decode checked before it runs, its runs kept, the run report signed
Model governance was defined, verified and rolled up, and no sink enforced it at decode time.

wai-rs `model_policy::sink` is the one implementation every sink calls. A host gives the sink a
configuration (the operator's policies, each the successor of the one before; its key log; the
device's class and key; its attestation), read as model-governance §8.2's steps 1 to 3 read a
bundle, and refused where no policy places the device in its class. Before a neural decode, at the
sink's instant, `admit_decode` checks the policy in force (`policy_not_yet_valid`,
`policy_expired`, a successor in force from its own start), the device in its class
(`device_not_in_class`, an attestation's span included), the entry for the set the decode reads,
its capability and the role `decode` (`model_not_in_policy`), a path the sink offers at the entry's
tier or above, a path below it not offered (`model_tier_below_policy`), and no identity of the set
revoked at that instant: its digest, a sealed set's `inner`, and every other digest of the bytes
read, as SPEC §3.1 check 5 covers them (`model_revoked`). A refused decode decodes nothing and its
answer names the code; a decoder with no policy decodes as before.

`Runs` keeps each decode that gave its output as model-governance §6.2 states a decode entry (a
refused decode, a payload refused, a decode no path ran, and a float path claiming
decode-equivalence are never kept), and `sign_report` signs `wai-model-run` for a period with the
device key: one policy in force over the period, no overlap with a reported period, no held run
left out, the device in its class throughout. `model_runs_full` refuses a decode while 65 536 runs
are unreported. `model_policy::rollup` is the operator's roll-up.

- sink_decode: `IntDecoder::{hold_policy, set_clock, now, model_state, run_report}`; whole
  payloads, envelopes (the pin's digest) and frame-stream objects (the stream's set named once) go
  through `governed`, and dispatch runs over the paths the check lets through. The answer adds
  `model` (`policy`, `set`, `at`, `kept`). The feature `sink_decode` includes `model_policy`.
- C interface: `wai_int_decoder_policy`, `wai_int_decoder_clock`, `wai_int_decoder_model`,
  `wai_int_run_report`, `wai_model_verify` (what `wai model verify` runs); include/wai.h
  regenerated.
- Swift and Kotlin (and the JNI layer): `holdPolicy`, `setClock`, `model`, `runReport`,
  `verifyModelBundle`, and the decode's `model`.
- wai-web: `model_gate` (bundles `neural_npu`, `neural_webgpu`): `ModelPolicy`, `ModelSet`, the
  check (`admit`) a WebNN page asks before it creates a context and `keep` after, and
  `IntNeural.holdPolicy`, which checks and keeps each decode itself.

Tests: the shared check's unit tests (the policy in force and its successor, a path below the tier
not offered, runs kept in order and reported once, a float path claiming decode-equivalence never
kept), and wai-web's native test of a page's WebNN path (refused before any context, kept, a
changed ticket keeping nothing, nothing kept for a decode that needs decode-equivalence, the report
accepted in a bundle). The corpus and the wrappers' tests follow in the next commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 21:11:15 -05:00

248 lines
12 KiB
Swift

// The integer decode a native sink runs in its own process (wai/extensions/native-sink.md §13),
// wrapped for Swift: one `WaiIntDecoder` per process finds the device's GPU and checks it once;
// each decode returns the output's bytes and the answer, whose `dispatch` report names the unit
// that ran it (`gpu` or `cpu`) and why any other path was refused.
import Foundation
import WaiFFI
/// One decode's outcome: the bytes, what they are, and the dispatch report.
public struct WaiIntDecoded {
/// The output: RGB8 rows (a picture, or each frame of a clip in order), int16 PCM
/// little-endian, or a splat attribute tensor. Empty for a refusal.
public let bytes: Data
/// The answer of native-sink §13.3: `capability`, `media`, `refused`, `unit`, `dispatch`,
/// `micros`.
public let answer: [String: Any]
/// The unit that ran the decode: `"gpu"` or `"cpu"`, or nil when nothing ran.
public var unit: String? { answer["unit"] as? String }
/// The report `wai-neural-dispatch/1` (neural-dispatch §5); empty for a capability with no
/// network (`wai.video.int_motion`), which decodes on the CPU.
public var dispatch: [String: Any] { answer["dispatch"] as? [String: Any] ?? [:] }
/// The refusal's code, or nil for a decode that gave its output.
public var refused: String? { (answer["refused"] as? [String: Any])?["code"] as? String }
/// What the bytes are: `form` (`rgb8`, `rgb8-frames`, `pcm-s16le`, `splat-attributes`) and
/// its dimensions.
public var media: [String: Any]? { answer["media"] as? [String: Any] }
/// For an object of a frame stream: the frame admitted, `kind` (`intra` or `inter`), `index`
/// and `content_hash`; nil for a refused object and for a whole payload.
public var frame: [String: Any]? { answer["frame"] as? [String: Any] }
/// Under a model policy (native-sink §14): `policy`, `set` (`sha256`, `inner`), `at` and
/// `kept`, the policy the decode was checked against, the set it reads, the decoder's instant
/// and whether its run was kept; nil without a policy or for a capability that runs no set.
public var model: [String: Any]? { answer["model"] as? [String: Any] }
}
/// A stream of one integer frame form (`wai.video.int_motion.frame`, `wai.video.int_hyper.frame`,
/// `wai.video.int_cond.frame`; frame-stream §3), one frame an object: it holds the reference,
/// the last frame decoded and the state it left (frame-stream §5), and a copy of its parameter
/// set. Give its objects to `WaiIntDecoder.decode(frame:stream:)` in the order received.
public final class WaiIntFrameStream: @unchecked Sendable {
fileprivate let handle: OpaquePointer
/// A stream of `capability` decoded against `set` (empty for `wai.video.int_motion.frame`).
public init(capability: String, set: [String: Data] = [:], listing: Data? = nil) throws {
handle = try WaiIntDecoder.withSet(set, listing: listing) { s in
var out: OpaquePointer?
let st = capability.withCString { wai_int_frames_open($0, s, &out) }
guard st == 0, let h = out else { throw WaiSink.error(st) }
return h
}
}
deinit { wai_int_frames_free(handle) }
/// Drop the reference: the next frame decoded is an intra frame.
public func reset() throws {
let st = wai_int_frames_reset(handle)
guard st == 0 else { throw WaiSink.error(st) }
}
}
/// The integer decoder (native-sink §13). Safe to call from any thread; decodes on one device
/// run one at a time.
public final class WaiIntDecoder: @unchecked Sendable {
/// What the host asks of the GPU.
public enum GPU: String, Sendable {
/// Find the device's GPU, run the start-up exactness check, and offer it.
case auto
/// Decode on the CPU alone.
case off
}
/// Which unit runs each graph while the GPU is ready (native-sink §13.2).
public enum UnitRule: String, Sendable {
/// The unit measured cheaper for the graph's shape on this device: energy where the
/// platform gives a reading, time otherwise. The default.
case measured
/// The GPU for every graph it runs.
case gpuFirst = "gpu-first"
}
private let handle: OpaquePointer
/// Open the decoder: with `.auto`, the device's GPU is found and checked here, once; `unit`
/// says which unit runs each graph while it is ready.
public init(gpu: GPU = .auto, unit: UnitRule = .measured) throws {
var out: OpaquePointer?
let st = "{\"gpu\": \"\(gpu.rawValue)\", \"unit\": \"\(unit.rawValue)\"}".withCString { wai_int_decoder_open($0, &out) }
guard st == 0, let h = out else { throw WaiSink.error(st) }
handle = h
}
deinit { wai_int_decoder_free(handle) }
/// The GPU: `state` (`ready`, `inexact`, `absent`, `failed`, `off`), the `adapter` as the
/// backend reports it, the `translator` its kernels went through, and `why` it is not used.
public func gpu() throws -> [String: Any] {
var out = wai_buffer_t(data: nil, len: 0, cap: 0)
let st = wai_int_decoder_info(handle, &out)
defer { wai_buffer_free(out) }
guard st == 0 else { throw WaiSink.error(st) }
return try WaiSink.object(WaiSink.bytes(out))["gpu"] as? [String: Any] ?? [:]
}
/// Decode `payload` through `capability` against the parameter set `set` (member name to
/// bytes; empty for a capability that reads none), unpinned. `intent` and `pinTier` are
/// the envelope's and a pin's where the host has them (neural-dispatch §2).
public func decode(capability: String, payload: Data, set: [String: Data] = [:], intent: String? = nil, pinTier: String? = nil) throws -> WaiIntDecoded {
var opts: [String: Any] = [:]
if let intent { opts["intent"] = intent }
if let pinTier { opts["pin_tier"] = pinTier }
let options = try WaiSink.text(opts)
return try Self.withSet(set, listing: nil) { s in
try run { bytes, answer in
capability.withCString { c in
options.withCString { o in
payload.withUnsafeBytes { p in
wai_int_decode(handle, c, p.bindMemory(to: UInt8.self).baseAddress, UInt(payload.count), s, o, &bytes, &answer)
}
}
}
}
}
}
/// Decode a `WAI1` envelope: its capability, intent and pin from its manifest, the pin
/// verified against `set` (and `listing`, for a pin in the form `wai.prior.set`) before
/// anything is decoded.
public func decode(envelope: Data, set: [String: Data] = [:], listing: Data? = nil) throws -> WaiIntDecoded {
try Self.withSet(set, listing: listing) { s in
try run { bytes, answer in
envelope.withUnsafeBytes { e in
wai_int_decode_envelope(handle, e.bindMemory(to: UInt8.self).baseAddress, UInt(envelope.count), s, &bytes, &answer)
}
}
}
}
/// Decode the next object of `stream`, a `WAI1` envelope, in the order received
/// (frame-stream §5): the frame's RGB8 rows, and the answer with `frame`. A refused object
/// leaves the stream with no reference until an intra frame.
public func decode(frame envelope: Data, stream: WaiIntFrameStream) throws -> WaiIntDecoded {
try run { bytes, answer in
envelope.withUnsafeBytes { e in
wai_int_decode_frame(handle, stream.handle, e.bindMemory(to: UInt8.self).baseAddress, UInt(envelope.count), &bytes, &answer)
}
}
}
// MARK: the model policy (native-sink §14)
/// Hold the operator's model policy: `config` the UTF-8 JSON text `{"policies", "key_log",
/// "class", "device", "attestation"}`. From then on every neural decode is checked against it
/// before it runs: one it refuses answers the policy's code (`model_not_in_policy`,
/// `model_revoked`, `policy_expired`, `policy_not_yet_valid`, `model_tier_below_policy`, …)
/// with no bytes. A later call replaces it, keeping the runs held under a policy the new one
/// still holds. Throws for one that does not load, the message starting with
/// model-governance's code.
public func holdPolicy(_ config: Data) throws {
let st = String(decoding: config, as: UTF8.self).withCString { wai_int_decoder_policy(handle, $0) }
guard st == 0 else { throw WaiSink.error(st) }
}
/// The instant the decoder checks and keeps decodes at: a log date-time
/// (`YYYY-MM-DDTHH:MM:SSZ`), the host's trusted clock, or nil for the system's clock.
public func setClock(_ at: String?) throws {
let st: Int32
if let at {
st = at.withCString { wai_int_decoder_clock(handle, $0) }
} else {
st = wai_int_decoder_clock(handle, nil)
}
guard st == 0 else { throw WaiSink.error(st) }
}
/// What the decoder holds under its policy: `policy` (`class`, `device`, `operator`,
/// `policies`) and `runs` (`held`, `reported_to`, `decodes`), both null without a policy.
public func model() throws -> [String: Any] {
var out = wai_buffer_t(data: nil, len: 0, cap: 0)
let st = wai_int_decoder_model(handle, &out)
defer { wai_buffer_free(out) }
guard st == 0 else { throw WaiSink.error(st) }
return try WaiSink.object(WaiSink.bytes(out))
}
/// Sign the run report (model-governance §6.1) for the period `from` to `to`, two log
/// date-times, with the device key whose 32-byte seed is `deviceSeed`, at `issuedAt` (the
/// decoder's instant where nil): the signed `wai-model-run` record's JSON text, whose runs then
/// leave the decoder. Throws for a period it cannot sign, the message starting with the
/// policy's code (`record_overlap`, `outside_period`, `policy_expired`, `bad_key`, …).
public func runReport(from: String, to: String, issuedAt: String? = nil, deviceSeed: Data) throws -> Data {
var req: [String: Any] = ["from": from, "to": to]
if let issuedAt { req["issued_at"] = issuedAt }
let request = try WaiSink.text(req)
var out = wai_buffer_t(data: nil, len: 0, cap: 0)
let st = request.withCString { r in
deviceSeed.withUnsafeBytes { s in
wai_int_run_report(handle, r, s.bindMemory(to: UInt8.self).baseAddress, UInt(deviceSeed.count), &out)
}
}
defer { wai_buffer_free(out) }
guard st == 0 else { throw WaiSink.error(st) }
return WaiSink.bytes(out)
}
/// A model-governance bundle (`*.wai-models.json`, model-governance §8) verified as `wai model
/// verify` verifies it: `outcome` `accept` with the statement, or `refuse` with `reason` and
/// `at`.
public static func verifyModelBundle(_ bundle: Data) throws -> [String: Any] {
var out = wai_buffer_t(data: nil, len: 0, cap: 0)
let st = bundle.withUnsafeBytes { b in wai_model_verify(b.bindMemory(to: UInt8.self).baseAddress, UInt(bundle.count), &out) }
defer { wai_buffer_free(out) }
guard st == 0 else { throw WaiSink.error(st) }
return try WaiSink.object(WaiSink.bytes(out))
}
// MARK: plumbing
fileprivate static func withSet<T>(_ files: [String: Data], listing: Data?, _ f: (OpaquePointer?) throws -> T) throws -> T {
var s: OpaquePointer?
let st = wai_int_set_new(&s)
guard st == 0, let set = s else { throw WaiSink.error(st) }
defer { wai_int_set_free(set) }
for (name, bytes) in files {
let st = name.withCString { n in
bytes.withUnsafeBytes { b in wai_int_set_add(set, n, b.bindMemory(to: UInt8.self).baseAddress, UInt(bytes.count)) }
}
guard st == 0 else { throw WaiSink.error(st) }
}
if let listing {
let st = listing.withUnsafeBytes { b in wai_int_set_listing(set, b.bindMemory(to: UInt8.self).baseAddress, UInt(listing.count)) }
guard st == 0 else { throw WaiSink.error(st) }
}
return try f(set)
}
private func run(_ f: (inout wai_buffer_t, inout wai_buffer_t) -> Int32) throws -> WaiIntDecoded {
var bytes = wai_buffer_t(data: nil, len: 0, cap: 0)
var answer = wai_buffer_t(data: nil, len: 0, cap: 0)
let st = f(&bytes, &answer)
defer {
wai_buffer_free(bytes)
wai_buffer_free(answer)
}
guard st == 0 else { throw WaiSink.error(st) }
return WaiIntDecoded(bytes: WaiSink.bytes(bytes), answer: try WaiSink.object(WaiSink.bytes(answer)))
}
}