Skip to content

feat(ocap-kernel): introduce IO streams for vats #831

@rekmarks

Description

@rekmarks

We need a way to pipe data from the outside world, for example via the command line, to vats. This requires the introduction of some kind of IO affordance that's the moral equivalent of stdin / stdout. This issue originally considered it infeasible for IO to be crank-bound, but for the use cases we have in mind (e.g. pipe some text, implement a REPL) this is actually fine. We need to create a design and then implement it.

Some points of consideration:

  • If IO is implemented as a kernel service, we can selectively expose it to different vats. How does the user of e.g. a kernel CLI know which vats can accept IO and which ones cannot?
  • If we implement reads as an AsyncIterator and input only arrives sporadically or even once during a vat's lifecycle, how does the vat know when to listen for data?
  • How are writes routed to a consumer?
Original issue description

Summary

Introduce a notion of IO to vats -- the moral equivalent of stdin/stdout that can, in practice, be wired to the actual stdin/stdout of a shell command (or any other stream source/sink). A vat configured with IO powers receives separate read and write stream capabilities. IO is text-based (strings) to start.

Motivation

Vats currently have no general-purpose mechanism for streaming text IO with the outside world. Adding IO streams would enable interactive CLI-driven vats, piping data through vats, and other stream-oriented use cases.

Design Decisions

The following decisions have been made:

  • Delivery mechanism: Direct endowments for the data path (simplest, avoids per-chunk crank overhead). See the lifecycle section below for the fundamental tension between waking vats (which currently requires the run queue) and streaming data (which can't afford per-chunk cranks).
  • Read model: Async iterator / stream semantics (for await...of). The write side is a simple async method (e.g. writer.write(text): Promise<void>).
  • Granularity: Separate read and write capabilities. A vat could receive read-only, write-only, or both.
  • Data format: Text (strings) to start. Can be expanded to bytes (Uint8Array) later.
  • Persistence: Ephemeral. IO streams do not survive vat restarts. External consumers must re-attach after restart.
  • Which vats: Any vat in a subcluster can be configured with IO powers (not limited to the bootstrap vat).
  • Architecture: IO plumbing should be implemented as a new platform service, following the existing pattern in packages/ocap-kernel/src/rpc/platform-services/.
  • External API: After launching a vat/subcluster, the kernel consumer retrieves the IO stream handles via a separate API call (e.g. kernel.getVatIO(vatId)).

Vat Lifecycle and the Run Queue

This is the hardest design problem. The kernel delivers messages to vats in discrete "cranks" via the run queue. Each crank is atomic -- state commits per-crank, GC (bringOutYourDead) runs between cranks, and ordering is deterministic. IO is inherently continuous and non-deterministic. These models must be reconciled.

Two competing requirements:

  1. IO must wake the vat. If a vat is idle (no pending messages) and IO data arrives, the vat must be activated to process it.
  2. Individual IO chunks cannot pass through the run queue. Making each chunk a crank would be untenable -- everything would grind to a halt. IO throughput demands a lighter-weight data path than the full crank lifecycle (state commit, GC, etc.).

Why this is hard: These requirements are in tension. The run queue is the only current mechanism for waking a vat and delivering data to it. But it's far too heavy for streaming IO. This rules out the naive "make IO a remotable exo" approach, since each E(io).write('chunk') would be a full crank.

Possible directions (all need investigation):

  • Hybrid approach: Use the run queue for lifecycle signals only (e.g. a single "IO available" wake-up message), but shuttle the actual data through a separate side-channel that bypasses the crank model. The vat would be woken by a normal delivery, then read data from a direct endowment or separate stream that doesn't go through cranks.
  • IO-aware scheduling: Extend the kernel scheduler to recognize IO-active vats as a special case. An IO-active vat stays "awake" and can pull data from its IO stream without each read being a crank. The vat only returns to the normal crank-based model when IO is idle.
  • Dedicated IO crank type: A lightweight crank variant for IO that skips some of the heavy per-crank machinery (e.g. no state commit, no GC). This preserves some ordering guarantees while being lighter-weight.

Open lifecycle questions:

  • How does IO interact with vat state persistence? If IO bypasses the crank model, vat state changes during IO processing may not be committed atomically. Is IO processing expected to be stateless with respect to durable vat state?
  • What are the ordering guarantees between IO data and normal message deliveries? If a vat is processing IO and a normal message arrives (or vice versa), what happens?
  • With a pull-based read model (async iterator), the vat's read() is essentially a syscall returning a promise. If IO bypasses the run queue, how does this promise resolve? A direct callback? A vat-local event loop?
  • What happens to pending reads when a vat is terminated? What happens if someone writes to a vat's IO after termination?

Other Open Questions

  • Named vs unnamed streams: Should a vat support multiple named IO channels (like stdin/stdout/stderr or numbered file descriptors), or just a single read + write pair? Named streams are more flexible but add complexity. Stderr could alternatively be handled through the existing logging/console mechanism.
  • Async iterator across the vat boundary: Vats communicate with the kernel over JSON-RPC on a DuplexStream. Implementing async iterator semantics inside the vat requires translating pull-based iteration into RPC calls (e.g. a readIO syscall-like RPC method that returns a promise resolving when data arrives). If IO bypasses the run queue, this translation may need to happen outside the normal syscall/delivery path. How exactly this should work needs investigation.
  • Backpressure: If the vat writes faster than the external consumer reads (or vice versa), what happens? Options include: promise-based backpressure (write resolves only when consumed), bounded buffering with errors on overflow, or unbounded buffering.
  • Write semantics: Should write() resolve when the data is buffered, when it reaches the external consumer, or when the consumer acknowledges it?
  • VatConfig shape: What exactly does the IO config look like in VatConfig? Likely a field in creationOptions or a new top-level field. E.g. { io: { read: true, write: true } } or { io: ['stdin', 'stdout'] }.
  • Platform service design: What platform service methods are needed? At minimum, something to create/destroy IO channels and to shuttle data. The data path is performance-sensitive -- every line of IO would be an RPC call across the kernel-platform boundary.

Implementation Pointers

Existing patterns to follow

  • Platform services pattern: packages/ocap-kernel/src/rpc/platform-services/ -- each service has a spec (superstruct validation), handler (with hooks for DI), and is registered in the index. New IO-related platform service methods would follow this pattern.
  • Vat endowments delivery: packages/ocap-kernel/src/vats/VatSupervisor.ts lines ~296-341 -- three layers of endowments (worker, platform, liveslots) are merged and passed to the vat compartment. IO capabilities would be a new endowment, likely created in the platform endowments layer.
  • Platform config: VatConfig.platformConfig (packages/ocap-kernel/src/types.ts) already exists for passing platform-specific configuration. IO config could extend this.
  • Kernel public API: packages/ocap-kernel/src/Kernel.ts -- the getVatIO(vatId) method would be added here.
  • Run queue / crank model: packages/ocap-kernel/src/KernelRouter.ts and packages/ocap-kernel/src/vats/VatHandle.ts -- message delivery and crank lifecycle.

Key files likely to be modified

  • packages/ocap-kernel/src/types.ts -- VatConfig, PlatformServices type
  • packages/ocap-kernel/src/rpc/platform-services/ -- new IO service handlers
  • packages/ocap-kernel/src/vats/VatSupervisor.ts -- deliver IO endowments to vat
  • packages/ocap-kernel/src/vats/VatManager.ts -- manage IO stream lifecycle
  • packages/ocap-kernel/src/Kernel.ts -- external API for accessing IO streams
  • packages/kernel-platforms/ -- platform-side implementation of IO streams

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions