Uh oh!
There was an error while loading. Please reload this page.
feat(kernel-utils): add described*() combinators for guard+schema authoring - #958
Merged
Conversation
4 tasks
Contributor
Coverage Report
File Coverage
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
grypezforce-pushed
the
feat/described-exo-combinators
branch
2 times, most recently
from
June 17, 2026 16:41
d7220eb to
b8ac175Compare…horing
Add a `./described` export whose combinators author an `@endo/patterns`
interface guard and a matching `MethodSchema` from a single source. The
combinator namespace is exported as `S` (mirroring `@endo/patterns`'s `M`):
each leaf (`S.string`/`number`/`boolean`/`arrayOf`/`record`/`object`/`nothing`)
yields a `{ pattern, schema }` pair; `S.arg` names a positional parameter; and
`S.method` / `S.interface` assemble them into a `{ guard, schema }` / `{
interfaceGuard, schemas }` ready to splat into `makeDiscoverableExo`.
Because the enforced pattern and the descriptive schema are projected from the
same authored leaves, their conformance is a construction invariant rather than
an after-the-fact check. Method guards use `M.callWhen(...).returns(...)` (exo
methods are invoked across an eventual-send boundary) and the interface guard
sets `defaultGuards: 'passable'` so the injected `__getDescription__` is allowed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>grypezforce-pushed
the
feat/described-exo-combinators
branch
from
June 18, 2026 15:50
b8ac175 to
6bc1feeComparegrypez
marked this pull request as ready for review
June 18, 2026 16:07
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6bc1fee. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…ir guards
Resolve two review findings on the described() combinators:
- S.object is documented as a closed/shaped object but M.splitRecord defaults
its rest pattern to M.any(), so neither the pattern nor the schema rejected
extra keys. Close both: pass an empty-record rest pattern and emit
additionalProperties: false (which routes jsonSchemaToStruct through its
strict branch).
- S.method emits optional trailing args via M.callWhen(...).optional(...), but
MethodSchema could not express optionality, so methodArgsToStruct and
method-schema-convert treated every arg as required. Add an optional
`required` field to MethodSchema (mirroring object JsonSchema's `required`),
populate it from S.method, honor it in methodArgsToStruct via a `{ required }`
option, and map it to ValueSpec.optional in method-schema-convert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>The converter now emits optional parameters from `MethodSchema.required`; record that consumer-observable change under the unreleased section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Jun 23, 2026
SherfeyInv pushed a commit
to SherfeyInv/ocap-kernel
that referenced
this pull request
Jul 29, 2026
…exos (MetaMask#959) ## Explanation Rewrites the `math`, `end`, and `examples` capabilities as discoverable exos built with the `described*()` combinators (added in MetaMask#958, now on `main`), so each capability's argument shape is enforced by the exo's interface guard at invocation rather than only advertised in the prompt. A mistyped argument now fails with a guard rejection at the membrane instead of surfacing deep inside the capability. Each module derives its `{ func, schema }` capability specs via a new synchronous `makeInternalCapabilities` constructor, which builds the pattern-guarded exo (kept private as the in-realm enforcement membrane) and projects a capability record from the just-authored schemas — without round-tripping through `GET_DESCRIPTION`. All existing consumers (example transcripts, the REPL evaluator, `prepare-attempt`) keep the same spec shape and `makeEnd` stays synchronous. `end`'s closed-over result object is intentionally left un-hardened so the exo method can mutate it. `makeInternalCapabilities` asserts at construction that the implementation and schema method sets match exactly. A missing implementation already throws inside `makeDiscoverableExo`, but an extra implementation absent from the schema would otherwise be silently accepted by the guard's `defaultGuards: 'passable'` and never be reachable as a capability — so an authoring typo (e.g. `serch` vs `search`) now fails loudly at construction instead of surfacing as a capability that resolves to `undefined`. A colocated `discover.test.ts` covers the positional-arg mapping, guard rejection at the membrane, and this construction check. Installs the endoify mock as a package-wide vitest setup, since capability modules now build exos at import and need a `harden` global before they load. ### Notable behavior changes - `getMoonPhase` loses its (already unsupported, `@ts-expect-error`'d) `enum` return hint. - `end`'s off-spec per-argument `required` flags are gone; `final` is required and `attachments` optional, expressed by the guard. ## Test plan - [x] `yarn workspace @ocap/kernel-agents test` (58 pass) and `test:dev:quiet` - [x] `yarn workspace @ocap/kernel-agents-repl test` (178 pass) - [x] `build` + `lint` for both packages; changelog validates - [ ] Local-only `kernel-test-local` agent e2e: see MetaMask#961 (out of scope here) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches core agent capability invocation and changes runtime validation behavior (guard rejections vs deep errors), though external capability shapes and `makeEnd` sync API stay the same. > > **Overview** > Built-in **`math`**, **`end`**, and **`examples`** capabilities are no longer hand-authored with `capability()`; they are built via new **`makeInternalCapabilities`**, which wraps implementations in a private pattern-guarded discoverable exo and projects the same `{ func, schema }` shape agents already use. Invalid or missing arguments are rejected at the exo interface guard before implementation code runs. > > **`discover`** is refactored to share **`capabilitiesFrom`** with the local path so remote and in-realm invocation both map named-arg objects to positional exo calls the same way. **`makeInternalCapabilities`** also fails at construction if schema and implementation method names do not match exactly. > > Tests cover mapping, membrane rejection, and the construction check; package **vitest** loads the endoify mock globally because capability modules build exos at import. **`getMoonPhase`** no longer advertises an unsupported `enum` return hint; **`end`** optional/required args are expressed only via the guard schema. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 06d6853. 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.8 <noreply@anthropic.com>
SherfeyInv pushed a commit
to SherfeyInv/ocap-kernel
that referenced
this pull request
Jul 29, 2026
…g enforcer (MetaMask#960) ## Explanation The base PRs ([MetaMask#958](MetaMask#958), [MetaMask#959](MetaMask#959)) made every built-in capability a pattern-guarded discoverable exo. Now that the exo's interface guard already enforces each capability's argument shape, this PR retires the parallel membraneless authoring and validation paths so the guard is the single argument enforcer: - Removes the `capability()` authoring helper and the internal `validateCapabilityArgs` validator (and its now-dead module). The chat strategy no longer re-validates arguments before invoking — it relies on the guard rejection it catches and reports as an `Error calling …` tool message. That catch is hardened to handle a non-`Error` rejection, so an invalid-argument tool call surfaces as a tool error instead of crashing the task (covered by a new regression test). - Collapses the redundant `CapabilitySchema` type into kernel-utils' `MethodSchema` (a capability's `schema` is exactly the `MethodSchema` its exo describes), removing the parallel type and its `ExtractRecordKeys` helper. - Adds a `test/make-method-capability.ts` helper that builds a guarded, discovered single-method capability from an `S.method`, and migrates the chat and JSON evaluator tests (and the capability test, repurposed to cover the surviving `extract*` helpers) onto it. - Drops the now-unused `@metamask/superstruct` dependency. ### Breaking changes - The `capability()` authoring helper is no longer exported from `@ocap/kernel-agents/capabilities/capability`. Author capabilities as pattern-guarded discoverable exos (via the `described*()` combinators in `@metamask/kernel-utils`) and convert them with `discover`. (`validateCapabilityArgs` was internal and never exported.) ## Test plan - [x] \`yarn workspace @ocap/kernel-agents test:dev:quiet\` (56 pass), incl. a chat-strategy regression test that an invalid-argument tool call comes back as an \`Error calling …\` tool message instead of crashing the task - [x] \`yarn workspace @ocap/kernel-agents-repl test:dev:quiet\` (178 pass) - [x] \`build\` + \`lint\` for both packages; changelog validates <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Breaking public API (`capability()` removal) affects downstream authors, but runtime behavior stays aligned with prior exo-backed builtins; main risk is consumers still using the old helper or assuming pre-invoke Superstruct errors. > > **Overview** > **Breaking:** Removes the exported `capability()` helper and the internal Superstruct-based `validateCapabilityArgs` path. Capabilities are expected to be authored as pattern-guarded discoverable exos (`described*()` + `discover` / `makeInternalCapabilities`); `CapabilitySpec.schema` is now kernel-utils `MethodSchema` instead of a parallel `CapabilitySchema` type. > > Invocation errors from the exo interface guard are normalized in `capabilitiesFrom` to `Error calling <name>(<params>): …` so chat and other callers can surface actionable tool messages without a second validation layer. The chat agent parses tool JSON locally when needed, invokes capabilities directly, and pushes guard/implementation failures as tool errors (including a regression test for bad args) instead of crashing the loop. > > Tests migrate to `test/make-method-capability.ts`; `@metamask/superstruct` is dropped from dependencies. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 6dc44a8. 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.8 <noreply@anthropic.com>
SherfeyInv pushed a commit
to SherfeyInv/ocap-kernel
that referenced
this pull request
Aug 11, 2026
…un-queue cache fix (MetaMask#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 MetaMask#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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Explanation
Adds a
./describedexport whose combinators author an@endo/patternsinterface guard and a matching
MethodSchemafrom a single source, so adiscoverable exo's enforced shape and its
__getDescription__hint cannotdrift.
The combinator namespace is exported as
S(mirroring@endo/patterns'sMand
@endo/eventual-send'sE).Dwas avoided because it reads as liveslots'D()device operator to readers familiar with that codebase.Sis the singleauthoring surface — there are no bare combinator function exports:
S.string/S.number/S.boolean/S.arrayOf/S.record/S.object/S.nothingeach yield a{ pattern, schema }pair.S.argnames a positional parameter.S.methodandS.interfaceassemble them into{ guard, schema }/{ interfaceGuard, schemas }, ready to splat intomakeDiscoverableExo.Because the enforced pattern and the descriptive schema are projected from the
same authored leaves, their conformance is a construction invariant. Method
guards use
M.callWhen(...).returns(...)(exo methods are invoked across aneventual-send boundary) and the interface guard sets
defaultGuards: 'passable'so the
__getDescription__method injected bymakeDiscoverableExois allowed.Optional arguments must be trailing (enforced by
S.method).Before this PR
After this PR
Test plan
yarn workspace @metamask/kernel-utils test:dev:quiet(304 pass, incl. 13 newdescribedtests)yarn workspace @metamask/kernel-utils buildyarn workspace @metamask/kernel-utils lintNote
Low Risk
Additive API and schema metadata; behavior change is limited to optional-arg validation/conversion where
requiredis set.Overview
Adds
@metamask/kernel-utils./described(and rootS) so discoverable exos can author@endo/patternsguards andMethodSchemafrom one definition, avoiding drift between membrane enforcement and__getDescription__.Sexposes leaves (string,number,boolean,arrayOf,record,object,nothing),S.arg,S.method(asyncM.callWhen, trailing optional args), andS.interface(defaultGuards: 'passable'for injected description).MethodSchemagains optionalrequired;methodArgsToStructhonors it for Superstruct validation.service-discovery-typesmethodSchemaToMethodSpecnow marks parameters not inrequiredasoptional.Reviewed by Cursor Bugbot for commit af20bba. Bugbot is set up for automated code reviews on this repo. Configure here.