Commit b1dc339
feat: IOListener with accept(), plus anonymous kernel objects and a run-queue cache fix (#1007)
Extracts the kernel-side work developed on `chip/orchestration-demo`
into `main`. No demo code is included — every change here is
general-purpose kernel machinery. Each commit is independently
meaningful and reviewable in order.
## Why
A vat needed to serve a line-delimited JSON-RPC socket to more than one
local client at a time. It couldn't: `IOChannel` models exactly one
bidirectional stream, so the socket server destroyed every connection
after the first. Chasing that surfaced two further bugs, one of them a
latent kernel defect with a genuinely nasty failure mode.
## Commits
**`fix(ocap-kernel): honor the run-queue length cache's invalid
sentinel`**
`runQueueLengthCache` uses a negative value to mean "unknown, re-read
from the database", but `enqueueRun`/`dequeueRun` adjusted it
arithmetically without materializing it first. An enqueue while the
cache held its startup value of `-1` produced `0` for a queue that
actually held an item — and because `0` isn't negative, it was never
re-read. The run loop then saw an empty queue, went to sleep, and
stranded everything queued behind it. **No error, no log, no crash: the
kernel just silently stops delivering.** The run loop is also now woken
by any non-empty queue rather than only the empty→1 transition, so a
drifted count can't lose the wakeup either.
Latent for a long time — reachable only when something enqueues before
the run loop's first length read. Worth reviewing on its own merits
regardless of the rest.
**`feat(ocap-kernel): anonymous kernel-hosted objects`**
`registerAnonymousKernelObject()` / `releaseAnonymousKernelObject()`:
allocate a kref and enter the object in the by-kref routing table, but
deliberately *not* in the service-name index. The object therefore has
no name in the global service namespace and cannot be requested via a
cluster config's `services` list — authority comes from holding the
reference.
Needed by `accept()`, and the naming half is the part we specifically
didn't want: a per-session connection should be reachable by reference
only.
**`feat(ocap-kernel): IOListener with accept(), replacing single-client
channels`**
The BSD listen/accept split. A cluster config's `io` entry now creates a
listener; `accept()` yields one `IOChannel` per peer, each wrapped in
its own exo and hosted as an anonymous kernel object, so the vat
receives a Presence per connection.
Isolation is structural rather than by discipline: sessions are separate
objects, so holding one connection conveys no way to reach another. That
matters because the names a vat hands across a non-ocap boundary are
plain forgeable strings; scoping them per connection is what stops one
client naming another's references. `direction` moves to the connection,
where the data actually flows. `accept()` resolves `null` once the
listener closes, so an accept loop terminates instead of hanging.
**`feat(kernel-node-runtime): socket listener with per-connection
channels`**
`makeSocketIOChannel` → `makeSocketIOListener`. Each connection's
buffer, decoder, line queue, and reader queue are local to it, which is
precisely why many peers can now be served at once. Connections arriving
before `accept()` are queued rather than dropped.
Deleted with the single-client design: `currentSocket`,
`pendingSessionEnd`, the merged line queue, and the `socket.destroy()`
that rejected second connections. The session-boundary latch didn't need
replacing — one channel serves one peer, so the end of the socket simply
*is* the end of the channel.
**`test(kernel-test): io-vat accepts connections; cover two concurrent
peers`**
The integration test drops its hand-rolled duplicate channel in favour
of the real `makeIOListenerFactory`, and adds a case driving two
concurrent peers end to end through a real kernel, asserting neither
reads the other's data nor receives the other's writes. That case was
unrepresentable before — the second connection was destroyed on arrival.
**`feat(kernel-utils,service-discovery-types): interface variant for
JsonSchema`**
`{ type: 'interface', description?, methods }` describes an object whose
methods can be invoked, so a method returning an object reference can
declare that object's API inline instead of forcing a second round-trip.
`methods` is recursive. The variant describes an *interface*; whether
the reference is unforgeable is a property of the reference plumbing,
not the description, so one schema serves both cases.
`service-discovery-types` converts it to a `RemotableSpec`, which means
`remotable` is no longer among the kinds `JsonSchema` can't express.
## Renamed API surface
Nothing in this repository is left broken — every in-tree consumer is
updated in this PR, and the full suite passes. These renames are flagged
`**BREAKING:**` in the changelogs because the packages are published and
the exported surface changed, so release tooling and any external
consumer need the signal:
| Was | Now |
|---|---|
| `Kernel.make({ ioChannelFactory })` | `Kernel.make({ ioListenerFactory
})` |
| `IOChannelFactory` | `IOListener` / `IOListenerFactory` |
| `makeIOChannelFactory()` | `makeIOListenerFactory()` |
| `makeSocketIOChannel()` | `makeSocketIOListener()` |
The behavioural change behind the renames: a vat that previously read
and wrote an `io` endowment directly now calls `accept()` to obtain a
connection first. `IOChannel` itself is unchanged and still represents
exactly one connection.
## Validation
Full monorepo on this exact tree: **30/30 builds, 52/52 test tasks, lint
clean.** Files that `main` also changed since the branch point were
three-way merged and individually diffed against `main` to confirm
nothing of `main`'s was reverted — in particular `main`'s #958
optional-parameter handling in `methodSchemaToMethodSpec` is preserved.
The whole stack has also been exercised live: two independent clients
holding concurrent connections to one vat, each with its own isolated
name table, driving a multi-service workflow end to end.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **High Risk**
> Touches core kernel IO, service routing, and run-queue delivery with
breaking public API renames; incorrect behavior could strand the run
loop or mishandle concurrent RPC sessions.
>
> **Overview**
> Replaces single-client Unix-socket IO with a **listen/accept** model
so vats can serve many concurrent line-delimited peers. Cluster `io`
entries now create **`IOListener`** services; vats call **`accept()`**
to get a per-peer **`IOChannel`** (Presence), with **`direction`**
enforced on each connection.
>
> **Breaking renames:** `ioChannelFactory` → `ioListenerFactory`,
`makeIOChannelFactory` / `makeSocketIOChannel` → `makeIOListenerFactory`
/ `makeSocketIOListener`. Node runtime gives each connection its own
buffer/decoder/queues; early connects are queued instead of dropped.
>
> Kernel adds **anonymous kernel objects**
(`registerAnonymousKernelObject` / release + init sweep) so accepted
connections are routable by kref but not by global service name.
**`invokeKernelService`** rejects missing services with
**`ENDPOINT_UNREACHABLE`** instead of throwing (avoids killing the run
loop on stale IO refs after restart).
>
> Fixes a **run-queue length cache** bug: enqueue/dequeue now
materialize the `-1` sentinel before arithmetic, and the run loop wakes
on any non-empty queue. **`JsonSchema`** gains an **`interface`**
variant (recursive methods) for inline return-type APIs;
service-discovery converts it to **`RemotableSpec`**.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f7570df. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>1 parent 6cc875c commit b1dc339
35 files changed
Lines changed: 2332 additions & 863 deletions
File tree
- packages
- kernel-node-runtime
- src
- io
- kernel
- test/helpers
- kernel-test/src
- vats
- kernel-utils
- src
- ocap-kernel
- src
- io
- store
- methods
- service-discovery-types
- src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
9 | | - | |
| 8 | + | |
| 9 | + | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
| 17 | + | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| |||
Lines changed: 0 additions & 301 deletions
This file was deleted.
0 commit comments