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]>
248 lines
12 KiB
Swift
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)))
|
|
}
|
|
}
|