Skip to content

Commit 2c83090

Browse files
committed
fix(ocap-kernel): deserialize CapData rejections in Kernel.queueMessage
Vat rejections are serialized as CapData objects before leaving the vat runtime. Previously, callers of Kernel.queueMessage (the RPC handler and SubclusterManager) each had their own isCapData/kunser call-site patches to convert them back into plain Errors. Consolidate the deserialization into Kernel.queueMessage itself so all callers see plain Error objects without needing individual workarounds. ([#928](#928))
1 parent b6dc2be commit 2c83090

5 files changed

Lines changed: 47 additions & 20 deletions

File tree

packages/ocap-kernel/CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Deserialize CapData rejections in `Kernel.queueMessage` so vat errors surface as plain `Error` objects to all callers ([#928](https://github.com/MetaMask/ocap-kernel/pull/928))
13+
1014
## [0.7.0]
1115

1216
### Added

packages/ocap-kernel/src/Kernel.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Mocked, MockInstance } from 'vitest';
77
import { describe, it, expect, vi, beforeEach } from 'vitest';
88

99
import { Kernel } from './Kernel.ts';
10+
import { kser } from './liveslots/kernel-marshal.ts';
1011
import type {
1112
VatId,
1213
VatConfig,
@@ -214,6 +215,33 @@ describe('Kernel', () => {
214215
const result = await kernel.queueMessage('ko1', 'hello', []);
215216
expect(result).toStrictEqual({ body: '{"result":"ok"}', slots: [] });
216217
});
218+
219+
it('deserializes CapData rejections into Errors', async () => {
220+
const kernel = await Kernel.make(
221+
mockPlatformServices,
222+
mockKernelDatabase,
223+
);
224+
mocks.KernelQueue.lastInstance.enqueueMessage.mockRejectedValueOnce(
225+
kser(new Error('vat rejection message')),
226+
);
227+
await expect(kernel.queueMessage('ko1', 'hello', [])).rejects.toThrow(
228+
'vat rejection message',
229+
);
230+
});
231+
232+
it('propagates non-CapData rejections as-is', async () => {
233+
const kernel = await Kernel.make(
234+
mockPlatformServices,
235+
mockKernelDatabase,
236+
);
237+
const internalError = new Error('internal kernel error');
238+
mocks.KernelQueue.lastInstance.enqueueMessage.mockRejectedValueOnce(
239+
internalError,
240+
);
241+
await expect(kernel.queueMessage('ko1', 'hello', [])).rejects.toThrow(
242+
'internal kernel error',
243+
);
244+
});
217245
});
218246

219247
describe('launchSubcluster()', () => {

packages/ocap-kernel/src/Kernel.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CapData } from '@endo/marshal';
22
import type { KernelDatabase } from '@metamask/kernel-store';
3+
import { isCapData } from '@metamask/kernel-utils';
34
import { Logger } from '@metamask/logger';
45

56
import { IOManager } from './io/IOManager.ts';
@@ -10,8 +11,8 @@ import { KernelQueue } from './KernelQueue.ts';
1011
import { KernelRouter } from './KernelRouter.ts';
1112
import { KernelServiceManager } from './KernelServiceManager.ts';
1213
import type { KernelService } from './KernelServiceManager.ts';
14+
import { kslot, kunser } from './liveslots/kernel-marshal.ts';
1315
import type { SlotValue } from './liveslots/kernel-marshal.ts';
14-
import { kslot } from './liveslots/kernel-marshal.ts';
1516
import { OcapURLManager } from './remotes/kernel/OcapURLManager.ts';
1617
import { RemoteManager } from './remotes/kernel/RemoteManager.ts';
1718
import type { RemoteCommsOptions } from './remotes/types.ts';
@@ -386,7 +387,14 @@ export class Kernel {
386387
method: string,
387388
args: unknown[],
388389
): Promise<CapData<KRef>> {
389-
return this.#kernelQueue.enqueueMessage(target, method, args);
390+
try {
391+
return await this.#kernelQueue.enqueueMessage(target, method, args);
392+
} catch (rejection) {
393+
if (isCapData(rejection)) {
394+
throw kunser(rejection as CapData<KRef>);
395+
}
396+
throw rejection;
397+
}
390398
}
391399

392400
/**

packages/ocap-kernel/src/rpc/kernel-control/queue-message.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe('queueMessageHandler', () => {
6464
expect(result).toStrictEqual(expectedResult);
6565
});
6666

67-
it('should propagate errors from kernel.queueMessage', async () => {
67+
it('propagates rejections from kernel.queueMessage', async () => {
6868
const error = new Error('Queue message failed');
6969
vi.mocked(mockKernel.queueMessage).mockRejectedValueOnce(error);
7070

packages/ocap-kernel/src/vats/SubclusterManager.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { CapData } from '@endo/marshal';
22
import { SubclusterNotFoundError } from '@metamask/kernel-errors';
3-
import { isCapData } from '@metamask/kernel-utils';
43
import { Logger } from '@metamask/logger';
54

65
import type { IOManager } from '../io/IOManager.ts';
@@ -341,22 +340,10 @@ export class SubclusterManager {
341340
`Bootstrap vat "${config.bootstrap}" not found in rootIds`,
342341
);
343342
}
344-
let bootstrapResult: CapData<KRef>;
345-
try {
346-
bootstrapResult = await this.#queueMessage(rootKref, 'bootstrap', [
347-
roots,
348-
services,
349-
]);
350-
} catch (rejection) {
351-
// queueMessage rejects with CapData for rejected kernel promises.
352-
// Deserialize to surface the original Error to the caller.
353-
// If the rejection isn't CapData (e.g., an internal error before the
354-
// kernel promise was created), re-throw as-is.
355-
if (isCapData(rejection)) {
356-
throw kunser(rejection as CapData<KRef>);
357-
}
358-
throw rejection;
359-
}
343+
const bootstrapResult = await this.#queueMessage(rootKref, 'bootstrap', [
344+
roots,
345+
services,
346+
]);
360347
const unserialized = kunser(bootstrapResult);
361348
if (unserialized instanceof Error) {
362349
throw unserialized;

0 commit comments

Comments
 (0)