diff --git a/README.md b/README.md index 48ae6b2..6ef2590 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Each domain is its own package, frozen at a per-file version. A capsule imports | `host/elicit@1.0.0.wit` | `astrid:elicit@1.0.0` | Interactive user input during install/upgrade lifecycle. | | `host/approval@1.0.0.wit` | `astrid:approval@1.0.0` | Human-in-the-loop approval gate for sensitive actions. | | `host/identity@1.0.0.wit` | `astrid:identity@1.0.0` | Multi-platform identity resolve and link. | +| `host/compute@1.0.0.wit` | `astrid:compute@1.0.0` | Draft principal-scoped core-Wasm compute groups, bounded shared memory, jobs, cancellation and accounting. Requires a live owner-UID/capsule/package-generation provision and finite deployment policy; see [compute RFC #42](https://github.com/astrid-runtime/rfcs/pull/42). | | `host/guest@1.0.0.wit` | `astrid:guest@1.0.0` | Guest export contract — `astrid-hook-trigger`, `run`, `astrid-install`, `astrid-upgrade`. Each entry point lives in its own world (`interceptor`, `background`, `installable`, `upgradable`) so capsules `include` only what they implement. | A capsule's world declares only the imports it uses plus the guest-export worlds it actually implements: diff --git a/host/compute@1.0.0.wit b/host/compute@1.0.0.wit new file mode 100644 index 0000000..9746a9d --- /dev/null +++ b/host/compute@1.0.0.wit @@ -0,0 +1,201 @@ +/// Principal-scoped parallel execution of capsule-authored core-Wasm workers. +/// +/// DRAFT. This contract is developed on an unmerged branch. If merged, this +/// package version follows the repository immutability policy. The host is +/// algorithm-blind: the capsule supplies signed `compute-worker` bytes; +/// Astrid validates, schedules, meters, cancels, and accounts for them. +/// +/// Workers import one bounded shared memory32 or memory64 and may use ABI-1 +/// read-only imports for their authenticated, ordered immutable assets. Bulk +/// data and synchronization stay in that memory; this is the audited control +/// plane. A signed declaration identifies candidate bytes, not execution +/// authority. The host independently provisions the current principal UID, +/// capsule and exact published generation, including worker/asset digests, and +/// rechecks the live registry at activation and open. + +package astrid:compute@1.0.0; + +interface host { + use astrid:io/poll@1.0.0.{pollable}; + + /// Stable error classification. Strings are diagnostics only. + variant error-code { + /// No current authenticated owner/capsule/generation provision, or its + /// publication was replaced or revoked in the live registry. + capability-denied, + /// A count, range, memory shape, or other request field is invalid. + invalid-input(string), + /// The id is unknown or does not name a `compute-worker` component. + no-such-worker, + /// Hash, imports, exports, memory type, or ABI version is invalid. + worker-invalid(string), + /// Effective principal/operator worker or memory policy denied admission. + quota, + /// A targeted worker already has one pending/running job, or an + /// untargeted dispatch meets a full bounded worker queue. + busy, + /// Operation raced group, principal, capsule, or kernel cancellation. + cancelled, + /// Worker compile, instantiate, trap, fuel, or forced-interrupt failure. + worker-failed(string), + /// The resource is already terminal or closed. + closed, + /// A control-plane read or write exceeded the per-call transfer limit. + too-large, + /// Non-classified host error. Detail is best-effort. + unknown(string), + } + + /// Whether the runtime may execute more than one worker concurrently. + enum execution-mode { + /// Resolve exactly one worker and dispatch jobs FIFO. + deterministic, + /// Use the resolved parallelism. + parallel, + } + + /// How a requested worker count is resolved. + variant parallelism { + /// Useful host parallelism, bounded by admitted operator policy/reservations. + auto, + /// Zero is invalid. In parallel mode, admit exactly this positive count + /// or fail with `quota`. + /// Deterministic mode selects one worker regardless of a positive count. + exact(u32), + /// Zero is invalid. Admit at least one and no more than this count. + at-most(u32), + } + + /// Request to open one compute group over one declared worker artifact. + record group-request { + /// `[[component]] id` whose type is `compute-worker`. + worker: string, + mode: execution-mode, + parallelism: parallelism, + /// Initial 64-KiB shared-memory pages. Must be non-zero. + initial-memory-pages: u32, + /// Maximum pages; zero requests automatic resolution against the worker + /// maximum and remaining policy capacity. A nonzero value must be + /// >= initial; the resolved shared memory always has a finite maximum. + maximum-memory-pages: u32, + } + + /// Opaque pointer to capsule-defined bytes in the group's shared memory. + record work-descriptor { + /// Byte offset. Must be >= 64 (the advisory ABI header occupies 0..64). + offset: u64, + /// Byte length. `offset + length` is checked for overflow and bounds. + length: u64, + /// Application-defined value passed through without interpretation. + tag: u64, + /// `none` schedules the next available worker; `some` pins by index. + worker-index: option, + /// Optional self-limit. The daemon resolves `none` to a finite share + /// derived from effective principal fuel-rate policy, the admitted + /// job maximum and worker count, further narrowed atomically to currently + /// unspent/unreserved capacity. Zero capacity and `some(0)` deny admission. + /// Explicit positive values are admitted exactly or denied; they cannot + /// widen policy. Worker startup also requires finite fuel and a timeout. + fuel: option, + } + + enum job-state { + queued, + running, + completed, + cancelled, + failed, + } + + /// Non-blocking snapshot of one job. + record job-status { + state: job-state, + worker-index: option, + } + + /// Terminal result of a worker invocation. + record job-result { + state: job-state, + worker-index: u32, + /// Capsule-defined `astrid_compute_run` return code. + worker-status: s32, + fuel-consumed: u64, + elapsed-ns: u64, + } + + /// Group and principal-attributed counters. Values are host-stamped, but + /// owner UID, generation, digests and assets require a separate host audit. + record accounting { + workers-reserved: u32, + memory-bytes-current: u64, + memory-bytes-peak: u64, + jobs-submitted: u64, + jobs-completed: u64, + jobs-cancelled: u64, + jobs-failed: u64, + fuel-consumed: u64, + } + + /// Non-blocking snapshot of one compute group. + record group-info { + worker: string, + mode: execution-mode, + parallelism: u32, + memory-pages: u32, + maximum-memory-pages: u32, + queued-jobs: u32, + running-jobs: u32, + usage: accounting, + } + + /// Observer handle for one submitted invocation. + /// + /// Dropping this handle does not cancel work. The owning group controls the + /// worker lifetime and cancels every queued/running job when dropped. + resource job { + /// Current state without blocking. + status: func() -> result; + + /// Pollable that becomes ready when the job reaches a terminal state. + subscribe: func() -> pollable; + + /// Wait for completion or a typed terminal failure. + join: func() -> result; + + /// Request cancellation. Idempotent. + cancel: func() -> result<_, error-code>; + } + + /// Worker instances plus the shared memory they import. + resource compute-group { + /// Current immutable configuration and mutable counters. + info: func() -> result; + + /// Copy bytes out of retained shared memory (maximum 1 MiB/call). + /// Cancelled groups may permit reads until the resource is retired. + read: func(offset: u64, length: u32) -> result, error-code>; + + /// Copy bytes into retained shared memory (maximum 1 MiB/call). + /// Cancellation alone does not retire the memory; retirement closes access. + write: func(offset: u64, data: list) -> result<_, error-code>; + + /// Grow within the admitted maximum, returning the previous page count + /// and updating accounting/header. Closed groups reject growth. + grow: func(delta-pages: u32) -> result; + + /// Queue one worker invocation and return immediately. + submit: func(descriptor: work-descriptor) -> result; + + /// Cancel all queued/running work and reject new submissions/growth. + /// This is not evidence that native workers exited or RAM was released. + cancel: func() -> result<_, error-code>; + } + + /// Open requires a live, independently authenticated package provision bound + /// to immutable principal UID, capsule and exact publication generation, + /// checked against the live registry. A declaration alone grants nothing. + /// A failed call returns no usable group. Interrupted startup may leave a + /// worker settling asynchronously; its memory and reservations remain + /// charged until the worker actually exits and cleanup releases them. + open: func(request: group-request) -> result; +}