Skip to content

Commit 175b7c0

Browse files
sirtimidclaude
andauthored
feat(ocap-kernel): integrate Snaps network endowment factory (#942)
Closes #936. Integrates the Snaps `@metamask/snaps-execution-environments/endowments` network factory into vat globals. Replaces the platform-level `fetch` capability with a per-vat endowment that ships `fetch`/`Request`/`Headers`/`Response`, wires teardown for in-flight requests and open body streams, and enforces a per-vat host allowlist (`VatConfig.network.allowedHosts`) applied at `#initVat` via `makeHostCaveat`. `fetch` without an allowlist fails init — no implicit allow-all. `file://` URLs are rejected by the caveat with a hint to use the `fs` platform capability. ## Summary - Add `fetch`, `Request`, `Headers`, `Response` to the default vat endowments via the Snaps factory; teardown aggregates into `VatSupervisor.terminate()`. - New `VatConfig.network: { allowedHosts: string[] }`; host matching is `URL.hostname`-only (ports and schemes ignored). - Wire the Snaps factory's `notify` callback to the vat logger at `debug`; transport failures surface via `console.error`. - Remove the entire `packages/kernel-platforms/src/capabilities/fetch/` directory (BREAKING for direct consumers of `fetchConfigStruct`, `FetchCapability`, `makeHostCaveat`, `makeCaveatedFetch`). - Migrate consumers: `kernel-node-runtime` drops `platformOptions.fetch.fromFetch`; `nodejs-test-workers/mock-fetch` stubs `globalThis.fetch` directly; `evm-wallet-experiment` cluster config and docker e2e helper move to `globals: ['fetch', ...]` + `network.allowedHosts`. - Update `docs/kernel-guide.md` endowment table, `docs/usage.md`, and `packages/evm-wallet-experiment/docs/setup-guide.md`. ## Test plan - Unit: `network-caveat.test.ts` covers host/hostname matching, port-insensitivity, `file://` rejection (string + Request input), malformed URL propagation, and a positive+negative end-to-end chain through `makeCaveatedFetch`. - `endowments.test.ts` verifies the new globals list, the `notify` wiring, logger-transport error containment, and that teardown aborts an in-flight `fetch` (stubs `globalThis.fetch` with an abort-respecting promise, asserts the abort signal propagates and teardown returns cleanly). - `VatSupervisor.test.ts` exercises both the fetch-without-allowlist throw path and the positive-path where `network.allowedHosts` is supplied (asserts absence of the guard error in dispatch). - `types.test.ts` validates the new `network` field, rejecting non-array and non-string entries. - Integration: `kernel-test/src/endowments.test.ts` exercises allowed/disallowed hosts end-to-end through a real `VatSupervisor` and asserts `Request`/`Headers`/`Response` constructors are available in the vat compartment. - Lint + changelog validate clean across all three touched packages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Moderate risk due to **breaking configuration/API changes** (removing platform-level `fetch`) and new network enforcement in `VatSupervisor` that can cause vats to fail `initVat` if misconfigured; changes touch security-sensitive outbound network gating but are covered by new unit/integration tests. > > **Overview** > Adds a Snaps-provided **network endowment** (`fetch` + `Request`/`Headers`/`Response`) to vat globals and enforces a new per-vat allowlist via `VatConfig.network.allowedHosts`; requesting `fetch` without this allowlist now fails `initVat`, and the caveat rejects `file://` targets. > > Removes the `fetch` **platform capability** from `kernel-platforms` (and updates downstream configs/tests) and updates supervisors/endowment factories to accept a `{ logger }` options bag, wiring Snaps network `notify` events into debug logging and ensuring teardown cancels in-flight requests. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 21ba678. 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 (1M context) <noreply@anthropic.com>
1 parent 190af28 commit 175b7c0

36 files changed

Lines changed: 772 additions & 702 deletions

docs/kernel-guide.md

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -155,27 +155,56 @@ Only names in the vat's `globals` array are installed in the vat's compartment.
155155

156156
The kernel ships with the following set, sourced from `@metamask/snaps-execution-environments`:
157157

158-
| Name | Category | Notes |
159-
| ----------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
160-
| `setTimeout` | Timer (attenuated) | Isolated per vat. Cancelled automatically on vat termination. |
161-
| `clearTimeout` | Timer (attenuated) | Only clears timers created by the same vat. |
162-
| `setInterval` | Timer (attenuated) | Isolated per vat. Cancelled automatically on vat termination. |
163-
| `clearInterval` | Timer (attenuated) | Only clears intervals created by the same vat. |
164-
| `Date` | Attenuated | Each `Date.now()` read adds up to 1 ms of random jitter, clamped monotonic non-decreasing; precise sub-millisecond timing cannot leak through. |
165-
| `Math` | Attenuated | `Math.random()` is sourced from `crypto.getRandomValues`. **Not a CSPRNG** per the upstream NOTE — defends against stock-RNG timing side channels only. |
166-
| `crypto` | Web Crypto | Hardened Web Crypto API. |
167-
| `SubtleCrypto` | Web Crypto | Hardened Web Crypto API. |
168-
| `TextEncoder` | Text codec | Plain hardened. |
169-
| `TextDecoder` | Text codec | Plain hardened. |
170-
| `URL` | URL | Plain hardened. |
171-
| `URLSearchParams` | URL | Plain hardened. |
172-
| `atob` | Base64 | Plain hardened. |
173-
| `btoa` | Base64 | Plain hardened. |
174-
| `AbortController` | Abort | Plain hardened. |
175-
| `AbortSignal` | Abort | Plain hardened. |
158+
| Name | Category | Notes |
159+
| ----------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160+
| `setTimeout` | Timer (attenuated) | Isolated per vat. Cancelled automatically on vat termination. |
161+
| `clearTimeout` | Timer (attenuated) | Only clears timers created by the same vat. |
162+
| `setInterval` | Timer (attenuated) | Isolated per vat. Cancelled automatically on vat termination. |
163+
| `clearInterval` | Timer (attenuated) | Only clears intervals created by the same vat. |
164+
| `Date` | Attenuated | Each `Date.now()` read adds up to 1 ms of random jitter, clamped monotonic non-decreasing; precise sub-millisecond timing cannot leak through. |
165+
| `Math` | Attenuated | `Math.random()` is sourced from `crypto.getRandomValues`. **Not a CSPRNG** per the upstream NOTE — defends against stock-RNG timing side channels only. |
166+
| `crypto` | Web Crypto | Hardened Web Crypto API. |
167+
| `SubtleCrypto` | Web Crypto | Hardened Web Crypto API. |
168+
| `fetch` | Network (attenuated) | Wrapped by the Snaps network factory; teardown aborts in-flight requests and cancels open body streams on vat termination. **Requires `network.allowedHosts`** — see [Network endowment](#network-endowment). |
169+
| `Request` | Network | Hardened constructor surfaced alongside `fetch` so vat code can build requests before calling it. |
170+
| `Headers` | Network | Hardened constructor. |
171+
| `Response` | Network | Hardened constructor; overrides `[Symbol.hasInstance]` so wrapped fetch results still pass `instanceof Response`. |
172+
| `TextEncoder` | Text codec | Plain hardened. |
173+
| `TextDecoder` | Text codec | Plain hardened. |
174+
| `URL` | URL | Plain hardened. |
175+
| `URLSearchParams` | URL | Plain hardened. |
176+
| `atob` | Base64 | Plain hardened. |
177+
| `btoa` | Base64 | Plain hardened. |
178+
| `AbortController` | Abort | Plain hardened. |
179+
| `AbortSignal` | Abort | Plain hardened. |
176180

177181
"Plain hardened" means the value is the host's implementation wrapped with `harden()` — it behaves identically to the browser/Node version. "Attenuated" means the value is a deliberate reimplementation with different semantics; the Notes column flags the relevant differences. The canonical list lives in [`endowments.ts`](../packages/ocap-kernel/src/vats/endowments.ts).
178182

183+
### Network endowment
184+
185+
`fetch`, `Request`, `Headers`, and `Response` are only available when the vat also declares a per-vat host allowlist in `VatConfig.network.allowedHosts`:
186+
187+
```ts
188+
await kernel.launchSubcluster({
189+
bootstrap: 'worker',
190+
vats: {
191+
worker: {
192+
bundleSpec: '...',
193+
globals: ['fetch', 'Request', 'Headers', 'Response'],
194+
network: { allowedHosts: ['api.example.com', 'api.github.com'] },
195+
},
196+
},
197+
});
198+
```
199+
200+
Requesting `'fetch'` without an `allowedHosts` entry (or with an absent `network` block) fails `initVat` with `Vat "<id>" requested "fetch" but no network.allowedHosts was specified`. There is no implicit allow-all; an empty `allowedHosts: []` is legal but rejects every outbound host. Host matching is a case-sensitive exact comparison against `URL.hostname` — ports and schemes are not considered, so `allowedHosts: ['api.example.com']` accepts both `http://api.example.com` and `https://api.example.com:8443`. `file://` URLs are **rejected** by fetch — use the `fs` platform capability for filesystem access.
201+
202+
Lifecycle notes:
203+
204+
- The network factory reads `globalThis.fetch` at call time — host applications that need to stub it (e.g., tests) should override the global before constructing the `VatSupervisor`.
205+
- Teardown cancels in-flight requests and open body streams. It runs as part of `VatSupervisor.terminate()` alongside timer teardown.
206+
- `fetch` returns a `ResponseWrapper` rather than the raw `Response`; the endowed `Response` constructor is patched so `instanceof Response` still returns `true` for wrapper instances.
207+
179208
### Restricting or replacing the allowed set
180209

181210
Two levers, applied at different layers:
@@ -536,8 +565,9 @@ type VatConfig = {
536565
bundleName?: string; // Name of a pre-registered bundle
537566
creationOptions?: Record<string, Json>; // Options for vat creation
538567
parameters?: Record<string, Json>; // Static parameters passed to buildRootObject
539-
platformConfig?: Partial<PlatformConfig>; // Platform-specific configuration
540-
globals?: string[]; // Host/Web API globals the vat requests — see [Vat Endowments](#vat-endowments)
568+
platformConfig?: Partial<PlatformConfig>; // Platform-specific configuration (currently `fs` only)
569+
globals?: AllowedGlobalName[]; // Host/Web API globals the vat requests — see [Vat Endowments](#vat-endowments)
570+
network?: { allowedHosts: string[] }; // Host allowlist required when requesting `fetch`
541571
};
542572

543573
// Configuration for a system subcluster

docs/usage.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,21 @@ A vat can also request host/Web API globals (timers, `Date`, `crypto`, `URL`,
207207
}
208208
```
209209

210+
Network access is a special case: requesting `fetch` (and optionally `Request`/`Headers`/`Response`) also requires a per-vat host allowlist under `network.allowedHosts`. Without it, `initVat` rejects the vat.
211+
212+
```json
213+
{
214+
"bootstrap": "alice",
215+
"vats": {
216+
"alice": {
217+
"bundleSpec": "http://localhost:3000/sample-vat.bundle",
218+
"globals": ["fetch", "Request", "Headers", "Response"],
219+
"network": { "allowedHosts": ["api.example.com"] }
220+
}
221+
}
222+
}
223+
```
224+
210225
See [Vat Endowments](./kernel-guide.md#vat-endowments) in the kernel guide for the full list and for how to narrow the set with `Kernel.make({ allowedGlobalNames })`.
211226

212227
## Kernel API

packages/evm-wallet-experiment/docs/setup-guide.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,8 @@ yarn ocap daemon exec launchSubcluster '{
472472
},
473473
"provider": {
474474
"bundleSpec": "packages/evm-wallet-experiment/src/vats/provider-vat.bundle",
475-
"globals": ["TextEncoder", "TextDecoder"],
476-
"platformConfig": { "fetch": { "allowedHosts": ["<chain>.infura.io", "api.pimlico.io", "swap.api.cx.metamask.io"] } }
475+
"globals": ["TextEncoder", "TextDecoder", "fetch", "Request", "Headers", "Response"],
476+
"network": { "allowedHosts": ["<chain>.infura.io", "api.pimlico.io", "swap.api.cx.metamask.io"] }
477477
},
478478
"delegation": {
479479
"bundleSpec": "packages/evm-wallet-experiment/src/vats/delegation-vat.bundle",

packages/evm-wallet-experiment/src/cluster-config.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,17 @@ export function makeWalletClusterConfig(
6262
},
6363
provider: {
6464
bundleSpec: `${bundleBaseUrl}/provider-vat.bundle`,
65-
globals: ['TextEncoder', 'TextDecoder'],
66-
platformConfig: {
67-
fetch: allowedHosts ? { allowedHosts } : {},
68-
},
65+
globals: allowedHosts
66+
? [
67+
'TextEncoder',
68+
'TextDecoder',
69+
'fetch',
70+
'Request',
71+
'Headers',
72+
'Response',
73+
]
74+
: ['TextEncoder', 'TextDecoder'],
75+
...(allowedHosts ? { network: { allowedHosts } } : {}),
6976
},
7077
...auxiliaryVat,
7178
},

packages/evm-wallet-experiment/test/e2e/docker/helpers/wallet-setup.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,15 @@ export function launchWalletSubcluster(
7171
},
7272
provider: {
7373
bundleSpec: `${BUNDLE_BASE}/provider-vat.bundle`,
74-
globals: ['TextEncoder', 'TextDecoder'],
75-
platformConfig: { fetch: { allowedHosts } },
74+
globals: [
75+
'TextEncoder',
76+
'TextDecoder',
77+
'fetch',
78+
'Request',
79+
'Headers',
80+
'Response',
81+
],
82+
network: { allowedHosts },
7683
},
7784
...auxiliaryVat,
7885
},

packages/kernel-node-runtime/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **BREAKING:** Drop `platformOptions.fetch` from `makeNodeJsVatSupervisor` ([#942](https://github.com/MetaMask/ocap-kernel/pull/942))
13+
- `fetch` is now a vat endowment; stub `globalThis.fetch` directly if needed
14+
1015
## [0.1.0]
1116

1217
### Added

packages/kernel-node-runtime/src/vat/vat-worker.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ async function main(): Promise<void> {
2222
const { logger: streamLogger } = await makeNodeJsVatSupervisor(
2323
vatId,
2424
LOG_TAG,
25-
{ fetch: { fromFetch: fetch } },
2625
);
2726
logger = streamLogger;
2827
logger.debug('vat-worker main');

packages/kernel-platforms/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Removed
11+
12+
- **BREAKING:** Remove the `fetch` platform capability and its exports (`fetchConfigStruct`, `FetchCapability`, `FetchConfig`, `makeHostCaveat`, `makeCaveatedFetch`) ([#942](https://github.com/MetaMask/ocap-kernel/pull/942))
13+
- `fetch` is now a vat endowment in `@metamask/ocap-kernel`; see its changelog for the migration
14+
1015
## [0.1.0]
1116

1217
### Added
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { capabilityFactory as fetchCapabilityFactory } from './capabilities/fetch/browser.ts';
21
import { capabilityFactory as fsCapabilityFactory } from './capabilities/fs/browser.ts';
32
import { makePlatformFactory } from './factory.ts';
43

54
export const makePlatform = makePlatformFactory({
6-
fetch: fetchCapabilityFactory,
75
fs: fsCapabilityFactory,
86
});

packages/kernel-platforms/src/capabilities/fetch/browser.test.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)