From e57b2b07ab11a27ca0d73a99c0a5fd8e178fdb54 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:43:29 -0700 Subject: [PATCH 1/3] feat(kernel-errors): standardize kernel errors observable in vat-land Introduce a machine-readable error format for kernel errors surfaced to vats as promise rejections: `[KERNEL:] detail` for expected errors and `[KERNEL:FATAL:] detail` for fatal ones. - Add `kernel-error.ts` to `@metamask/kernel-errors` with `ExpectedKernelErrorCode`, `FatalKernelErrorCode`, `KernelErrorCode` types, `KERNEL_ERROR_PATTERN` regex, and `isKernelError`, `getKernelErrorCode`, `isFatalKernelError` detection utilities - Add `makeKernelError` and `makeFatalKernelError` to `kernel-marshal.ts`, importing the shared types from `@metamask/kernel-errors` - Migrate all 17+ error sites in KernelRouter, RemoteManager, VatHandle, VatSyscall, and KernelServiceManager to the new helpers - Update all affected test assertions Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-errors/src/index.test.ts | 4 + packages/kernel-errors/src/index.ts | 11 ++ .../kernel-errors/src/kernel-error.test.ts | 113 ++++++++++++++++++ packages/kernel-errors/src/kernel-error.ts | 66 ++++++++++ .../test/e2e/orphaned-ephemeral-exo.test.ts | 4 +- .../src/orphaned-ephemeral-exo.test.ts | 2 +- .../src/syscall-validation.test.ts | 8 +- .../kernel-test/src/vat-lifecycle.test.ts | 2 +- packages/ocap-kernel/src/KernelRouter.test.ts | 2 +- packages/ocap-kernel/src/KernelRouter.ts | 34 ++++-- .../src/KernelServiceManager.test.ts | 4 +- .../ocap-kernel/src/KernelServiceManager.ts | 10 +- .../src/liveslots/kernel-marshal.ts | 36 +++++- .../src/remotes/kernel/RemoteManager.test.ts | 2 +- .../src/remotes/kernel/RemoteManager.ts | 16 +-- packages/ocap-kernel/src/vats/VatHandle.ts | 7 +- packages/ocap-kernel/src/vats/VatSyscall.ts | 25 +++- 17 files changed, 302 insertions(+), 44 deletions(-) create mode 100644 packages/kernel-errors/src/kernel-error.test.ts create mode 100644 packages/kernel-errors/src/kernel-error.ts diff --git a/packages/kernel-errors/src/index.test.ts b/packages/kernel-errors/src/index.test.ts index ff651779e4..6ffe117a78 100644 --- a/packages/kernel-errors/src/index.test.ts +++ b/packages/kernel-errors/src/index.test.ts @@ -11,6 +11,7 @@ describe('index', () => { 'ErrorSentinel', 'ErrorStruct', 'EvaluatorError', + 'KERNEL_ERROR_PATTERN', 'MarshaledErrorStruct', 'MarshaledOcapErrorStruct', 'ResourceLimitError', @@ -20,7 +21,10 @@ describe('index', () => { 'VatAlreadyExistsError', 'VatDeletedError', 'VatNotFoundError', + 'getKernelErrorCode', 'getNetworkErrorCode', + 'isFatalKernelError', + 'isKernelError', 'isMarshaledError', 'isMarshaledOcapError', 'isOcapError', diff --git a/packages/kernel-errors/src/index.ts b/packages/kernel-errors/src/index.ts index 73e8d33747..44205e83a2 100644 --- a/packages/kernel-errors/src/index.ts +++ b/packages/kernel-errors/src/index.ts @@ -29,3 +29,14 @@ export { isMarshaledOcapError } from './marshal/isMarshaledOcapError.ts'; export { isRetryableNetworkError } from './utils/isRetryableNetworkError.ts'; export { getNetworkErrorCode } from './utils/getNetworkErrorCode.ts'; export { isResourceLimitError } from './utils/isResourceLimitError.ts'; +export type { + ExpectedKernelErrorCode, + FatalKernelErrorCode, + KernelErrorCode, +} from './kernel-error.ts'; +export { + KERNEL_ERROR_PATTERN, + isKernelError, + getKernelErrorCode, + isFatalKernelError, +} from './kernel-error.ts'; diff --git a/packages/kernel-errors/src/kernel-error.test.ts b/packages/kernel-errors/src/kernel-error.test.ts new file mode 100644 index 0000000000..25b3f1d239 --- /dev/null +++ b/packages/kernel-errors/src/kernel-error.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from 'vitest'; + +import { + KERNEL_ERROR_PATTERN, + isKernelError, + getKernelErrorCode, + isFatalKernelError, +} from './kernel-error.ts'; + +describe('KERNEL_ERROR_PATTERN', () => { + it.each([ + ['[KERNEL:OBJECT_REVOKED] Target object has been revoked', true], + ['[KERNEL:FATAL:ILLEGAL_SYSCALL] Fatal syscall violation', true], + ['[KERNEL:CONNECTION_LOST] Remote connection lost', true], + ['Some other error', false], + ['KERNEL:OBJECT_REVOKED', false], + ['[KERNEL:lowercase] bad code', false], + ])('matches %j -> %j', (message, expected) => { + expect(KERNEL_ERROR_PATTERN.test(message)).toBe(expected); + }); +}); + +describe('isKernelError', () => { + it('returns true for an Error with a kernel error message', () => { + expect(isKernelError(Error('[KERNEL:OBJECT_DELETED] Target deleted'))).toBe( + true, + ); + }); + + it('returns true for a fatal kernel error', () => { + expect( + isKernelError(Error('[KERNEL:FATAL:INTERNAL_ERROR] Something broke')), + ).toBe(true); + }); + + it('returns false for a plain Error', () => { + expect(isKernelError(Error('just a normal error'))).toBe(false); + }); + + it('returns false for non-Error values', () => { + expect(isKernelError('string')).toBe(false); + expect(isKernelError(null)).toBe(false); + expect(isKernelError(undefined)).toBe(false); + expect(isKernelError(42)).toBe(false); + }); +}); + +describe('getKernelErrorCode', () => { + it('extracts expected error codes', () => { + expect(getKernelErrorCode(Error('[KERNEL:OBJECT_REVOKED] revoked'))).toBe( + 'OBJECT_REVOKED', + ); + expect(getKernelErrorCode(Error('[KERNEL:CONNECTION_LOST] lost'))).toBe( + 'CONNECTION_LOST', + ); + }); + + it('extracts fatal error codes', () => { + expect( + getKernelErrorCode(Error('[KERNEL:FATAL:ILLEGAL_SYSCALL] bad')), + ).toBe('ILLEGAL_SYSCALL'); + expect( + getKernelErrorCode(Error('[KERNEL:FATAL:INTERNAL_ERROR] broken')), + ).toBe('INTERNAL_ERROR'); + }); + + it('returns undefined for non-kernel errors', () => { + expect(getKernelErrorCode(Error('normal error'))).toBeUndefined(); + }); +}); + +describe('isFatalKernelError', () => { + it('returns true for fatal kernel errors', () => { + expect( + isFatalKernelError(Error('[KERNEL:FATAL:ILLEGAL_SYSCALL] bad syscall')), + ).toBe(true); + }); + + it('returns false for expected kernel errors', () => { + expect(isFatalKernelError(Error('[KERNEL:OBJECT_REVOKED] revoked'))).toBe( + false, + ); + }); + + it('returns false for non-kernel errors', () => { + expect(isFatalKernelError(Error('normal error'))).toBe(false); + }); +}); + +describe('round-trip', () => { + it('constructs and detects a kernel error message', () => { + const code = 'OBJECT_DELETED'; + const detail = 'Target object has no owner; it may have been deleted'; + const message = `[KERNEL:${code}] ${detail}`; + const error = Error(message); + + expect(isKernelError(error)).toBe(true); + expect(getKernelErrorCode(error)).toBe(code); + expect(isFatalKernelError(error)).toBe(false); + expect(error.message).toBe(`[KERNEL:OBJECT_DELETED] ${detail}`); + }); + + it('constructs and detects a fatal kernel error message', () => { + const code = 'INTERNAL_ERROR'; + const detail = 'Internal kernel error'; + const message = `[KERNEL:FATAL:${code}] ${detail}`; + const error = Error(message); + + expect(isKernelError(error)).toBe(true); + expect(getKernelErrorCode(error)).toBe(code); + expect(isFatalKernelError(error)).toBe(true); + }); +}); diff --git a/packages/kernel-errors/src/kernel-error.ts b/packages/kernel-errors/src/kernel-error.ts new file mode 100644 index 0000000000..6049efdbb5 --- /dev/null +++ b/packages/kernel-errors/src/kernel-error.ts @@ -0,0 +1,66 @@ +/** + * Error codes for expected kernel errors that vat code may handle gracefully. + */ +export type ExpectedKernelErrorCode = + | 'OBJECT_REVOKED' + | 'OBJECT_DELETED' + | 'BAD_PROMISE_RESOLUTION' + | 'ENDPOINT_UNREACHABLE' + | 'CONNECTION_LOST' + | 'PEER_RESTARTED' + | 'VAT_TERMINATED' + | 'DELIVERY_FAILED'; + +/** + * Error codes for fatal kernel errors (kernel bugs or illegal operations). + * These are prefixed with `FATAL:` in the error message. + */ +export type FatalKernelErrorCode = 'ILLEGAL_SYSCALL' | 'INTERNAL_ERROR'; + +/** + * All kernel error codes. + */ +export type KernelErrorCode = ExpectedKernelErrorCode | FatalKernelErrorCode; + +/** + * Pattern matching kernel error messages. + * Matches both `[KERNEL:]` and `[KERNEL:FATAL:]`. + */ +export const KERNEL_ERROR_PATTERN = /^\[KERNEL:(?:(FATAL):)?([A-Z_]+)\]/u; + +/** + * Check whether a value is a kernel error (an Error whose message starts with + * `[KERNEL:...]`). + * + * @param value - The value to check. + * @returns `true` if `value` is an Error with a kernel error message. + */ +export function isKernelError(value: unknown): value is Error { + return value instanceof Error && KERNEL_ERROR_PATTERN.test(value.message); +} + +/** + * Extract the kernel error code from an Error, if present. + * + * @param error - The error to inspect. + * @returns The kernel error code, or `undefined` if the error is not a kernel error. + */ +export function getKernelErrorCode(error: Error): KernelErrorCode | undefined { + const match = KERNEL_ERROR_PATTERN.exec(error.message); + if (!match) { + return undefined; + } + return match[2] as KernelErrorCode; +} + +/** + * Check whether an Error is a fatal kernel error (its message contains the + * `FATAL:` infix). + * + * @param error - The error to inspect. + * @returns `true` if the error is a fatal kernel error. + */ +export function isFatalKernelError(error: Error): boolean { + const match = KERNEL_ERROR_PATTERN.exec(error.message); + return match !== null && match[1] === 'FATAL'; +} diff --git a/packages/kernel-node-runtime/test/e2e/orphaned-ephemeral-exo.test.ts b/packages/kernel-node-runtime/test/e2e/orphaned-ephemeral-exo.test.ts index 54f286c8e7..afa54e0799 100644 --- a/packages/kernel-node-runtime/test/e2e/orphaned-ephemeral-exo.test.ts +++ b/packages/kernel-node-runtime/test/e2e/orphaned-ephemeral-exo.test.ts @@ -55,11 +55,11 @@ describe('Orphaned ephemeral exo', { timeout: 30_000 }, () => { // The consumer's E(ephemeral).increment() targets an orphaned vref. // Liveslots in the provider throws "I don't remember allocating", // which terminates the provider and rejects the caller's promise. - // This is surfaced to the caller as "target object has no owner". + // This is surfaced to the caller as an OBJECT_DELETED kernel error. await expect( kernel.queueMessage(rootKref, 'useEphemeral', []), ).rejects.toMatchObject({ - body: expect.stringContaining('target object has no owner'), + body: expect.stringContaining('[KERNEL:OBJECT_DELETED]'), }); } finally { await kernel.stop(); diff --git a/packages/kernel-test/src/orphaned-ephemeral-exo.test.ts b/packages/kernel-test/src/orphaned-ephemeral-exo.test.ts index 389d88494c..cb73d906fc 100644 --- a/packages/kernel-test/src/orphaned-ephemeral-exo.test.ts +++ b/packages/kernel-test/src/orphaned-ephemeral-exo.test.ts @@ -44,7 +44,7 @@ describe('orphaned ephemeral exo', () => { await expect( kernel.queueMessage(rootKref, 'useEphemeral', []), ).rejects.toMatchObject({ - body: expect.stringContaining('has no owner'), + body: expect.stringContaining('[KERNEL:OBJECT_DELETED]'), }); }); }); diff --git a/packages/kernel-test/src/syscall-validation.test.ts b/packages/kernel-test/src/syscall-validation.test.ts index 5ba50d2fd1..ed18941aef 100644 --- a/packages/kernel-test/src/syscall-validation.test.ts +++ b/packages/kernel-test/src/syscall-validation.test.ts @@ -98,7 +98,7 @@ describe('Syscall Validation & Revoked Objects', { timeout: 30_000 }, () => { await expect( kernel.queueMessage(objectKRef, 'getValue', []), ).rejects.toMatchObject({ - body: expect.stringContaining('target object has been revoked'), + body: expect.stringContaining('[KERNEL:OBJECT_REVOKED]'), }); // Verify kernel doesn't crash and exporter vat remains operational const exporterStatus = await kernel.queueMessage(exporterKRef, 'noop', []); @@ -146,7 +146,7 @@ describe('Syscall Validation & Revoked Objects', { timeout: 30_000 }, () => { await expect( kernel.queueMessage(objectKRef, 'getValue', []), ).rejects.toMatchObject({ - body: expect.stringContaining('target object has been revoked'), + body: expect.stringContaining('[KERNEL:OBJECT_REVOKED]'), }); // Verify exporter vat is still operational const exporterStatus = await kernel.queueMessage(exporterKRef, 'noop', []); @@ -156,7 +156,7 @@ describe('Syscall Validation & Revoked Objects', { timeout: 30_000 }, () => { await expect( kernel.queueMessage(objectKRef, 'getValue', []), ).rejects.toMatchObject({ - body: expect.stringContaining('target object has been revoked'), + body: expect.stringContaining('[KERNEL:OBJECT_REVOKED]'), }); } // Verify kernel remains stable @@ -218,7 +218,7 @@ describe('Syscall Validation & Revoked Objects', { timeout: 30_000 }, () => { await expect( kernel.queueMessage(objectKRef, 'getValue', []), ).rejects.toMatchObject({ - body: expect.stringContaining('target object has been revoked'), + body: expect.stringContaining('[KERNEL:OBJECT_REVOKED]'), }); } diff --git a/packages/kernel-test/src/vat-lifecycle.test.ts b/packages/kernel-test/src/vat-lifecycle.test.ts index b45077daa5..b4c652d3af 100644 --- a/packages/kernel-test/src/vat-lifecycle.test.ts +++ b/packages/kernel-test/src/vat-lifecycle.test.ts @@ -135,7 +135,7 @@ describe('Vat Lifecycle', { timeout: 30_000 }, () => { await expect( kernel.queueMessage(deadRootObject, 'resume', []), ).rejects.toMatchObject({ - body: expect.stringContaining('has no owner'), + body: expect.stringContaining('[KERNEL:OBJECT_DELETED]'), }); // Verify that messaging works as expected diff --git a/packages/ocap-kernel/src/KernelRouter.test.ts b/packages/ocap-kernel/src/KernelRouter.test.ts index 5a092730fe..75ad8a098e 100644 --- a/packages/ocap-kernel/src/KernelRouter.test.ts +++ b/packages/ocap-kernel/src/KernelRouter.test.ts @@ -194,7 +194,7 @@ describe('KernelRouter', () => { 'kp1', true, expect.objectContaining({ - body: expect.stringContaining('target object has been revoked'), + body: expect.stringContaining('[KERNEL:OBJECT_REVOKED]'), slots: [], }), ]), diff --git a/packages/ocap-kernel/src/KernelRouter.ts b/packages/ocap-kernel/src/KernelRouter.ts index f39deafb6c..e72794d7fd 100644 --- a/packages/ocap-kernel/src/KernelRouter.ts +++ b/packages/ocap-kernel/src/KernelRouter.ts @@ -3,7 +3,7 @@ import type { CapData } from '@endo/marshal'; import { Logger } from '@metamask/logger'; import { KernelQueue } from './KernelQueue.ts'; -import { kser } from './liveslots/kernel-marshal.ts'; +import { makeKernelError } from './liveslots/kernel-marshal.ts'; import type { KernelStore } from './store/index.ts'; import { extractSingleRef } from './store/utils/extract-ref.ts'; import { parseRef } from './store/utils/parse-ref.ts'; @@ -144,12 +144,17 @@ export class KernelRouter { }; const routeAsSend = (targetObject: KRef): MessageRoute => { if (this.#kernelStore.isRevoked(targetObject)) { - return routeAsSplat(kser('target object has been revoked')); + return routeAsSplat( + makeKernelError('OBJECT_REVOKED', 'Target object has been revoked'), + ); } const endpointId = this.#kernelStore.getOwner(targetObject); if (!endpointId) { return routeAsSplat( - kser('target object has no owner; it may have been deleted'), + makeKernelError( + 'OBJECT_DELETED', + 'Target object has no owner; it may have been deleted', + ), ); } return { endpointId, target: targetObject }; @@ -172,7 +177,10 @@ export class KernelRouter { } } return routeAsSplat( - kser('promise fulfilled but did not contain an object reference'), + makeKernelError( + 'BAD_PROMISE_RESOLUTION', + 'Promise fulfilled but did not contain an object reference', + ), ); } case 'rejected': @@ -238,8 +246,9 @@ export class KernelRouter { [ message.result, true, - kser( - 'target endpoint is unreachable (terminated or disconnected)', + makeKernelError( + 'ENDPOINT_UNREACHABLE', + 'Target endpoint is unreachable (terminated or disconnected)', ), ], ]); @@ -290,13 +299,14 @@ export class KernelRouter { // so the caller knows the message wasn't delivered. this.#logger?.error(`Delivery to ${endpointId} failed:`, error); if (message.result) { - const failure = kser( - error instanceof Error - ? error - : Error(`Delivery failed: ${String(error)}`), - ); + const detail = + error instanceof Error ? error.message : String(error); this.#kernelQueue.resolvePromises(endpointId, [ - [message.result, true, failure], + [ + message.result, + true, + makeKernelError('DELIVERY_FAILED', detail), + ], ]); } // Continue processing other messages - don't let one failure crash the queue diff --git a/packages/ocap-kernel/src/KernelServiceManager.test.ts b/packages/ocap-kernel/src/KernelServiceManager.test.ts index 6ccf6a5d37..5fff9d3c52 100644 --- a/packages/ocap-kernel/src/KernelServiceManager.test.ts +++ b/packages/ocap-kernel/src/KernelServiceManager.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import type { KernelQueue } from './KernelQueue.ts'; import { KernelServiceManager } from './KernelServiceManager.ts'; -import { kser } from './liveslots/kernel-marshal.ts'; +import { kser, makeKernelError } from './liveslots/kernel-marshal.ts'; import { makeKernelStore } from './store/index.ts'; import type { Message } from './types.ts'; import { makeMapKernelDatabase } from '../test/storage.ts'; @@ -355,7 +355,7 @@ describe('KernelServiceManager', () => { await delay(); expect(mockKernelQueue.resolvePromises).toHaveBeenCalledWith('kernel', [ - ['kp123', true, kser(testError)], + ['kp123', true, makeKernelError('DELIVERY_FAILED', testError.message)], ]); }); diff --git a/packages/ocap-kernel/src/KernelServiceManager.ts b/packages/ocap-kernel/src/KernelServiceManager.ts index 0af3164ff1..501175a2bb 100644 --- a/packages/ocap-kernel/src/KernelServiceManager.ts +++ b/packages/ocap-kernel/src/KernelServiceManager.ts @@ -2,7 +2,7 @@ import { E } from '@endo/eventual-send'; import type { Logger } from '@metamask/logger'; import type { KernelQueue } from './KernelQueue.ts'; -import { kser, kunser } from './liveslots/kernel-marshal.ts'; +import { kser, kunser, makeKernelError } from './liveslots/kernel-marshal.ts'; import type { KernelStore } from './store/index.ts'; import type { KRef, Message } from './types.ts'; import { assert } from './utils/assert.ts'; @@ -181,8 +181,10 @@ export class KernelServiceManager { }) .catch((problem: unknown) => { if (result) { + const detail = + problem instanceof Error ? problem.message : String(problem); this.#kernelQueue.resolvePromises('kernel', [ - [result, true, kser(problem)], + [result, true, makeKernelError('DELIVERY_FAILED', detail)], ]); } else { this.#logger?.error('Error in kernel service method:', problem); @@ -191,8 +193,10 @@ export class KernelServiceManager { } catch (syncError) { // Handle synchronous errors thrown before returning a Promise if (result) { + const detail = + syncError instanceof Error ? syncError.message : String(syncError); this.#kernelQueue.resolvePromises('kernel', [ - [result, true, kser(syncError)], + [result, true, makeKernelError('DELIVERY_FAILED', detail)], ]); } else { this.#logger?.error('Error in kernel service method:', syncError); diff --git a/packages/ocap-kernel/src/liveslots/kernel-marshal.ts b/packages/ocap-kernel/src/liveslots/kernel-marshal.ts index a023a9445b..f649dade17 100644 --- a/packages/ocap-kernel/src/liveslots/kernel-marshal.ts +++ b/packages/ocap-kernel/src/liveslots/kernel-marshal.ts @@ -2,6 +2,10 @@ import { assert, Fail } from '@endo/errors'; import { passStyleOf, makeMarshal } from '@endo/marshal'; import type { CapData } from '@endo/marshal'; import type { Passable } from '@endo/pass-style'; +import type { + ExpectedKernelErrorCode, + FatalKernelErrorCode, +} from '@metamask/kernel-errors'; import { makeDefaultExo } from '@metamask/kernel-utils'; import type { KRef } from '../types.ts'; @@ -139,10 +143,40 @@ export function kunser(serializedValue: CapData): unknown { * Produce a serialized form of an Error. * * @param message - The error message to construct the Error with. - * * @returns The resulting error after serialization. + * @deprecated Use {@link makeKernelError} or {@link makeFatalKernelError} instead. */ export function makeError(message: string): CapData { assert.typeof(message, 'string'); return kser(Error(message)); } + +/** + * Produce a serialized expected kernel error with a machine-readable code. + * The resulting error message has the format `[KERNEL:] `. + * + * @param code - The expected kernel error code. + * @param detail - A human-readable description. + * @returns The serialized error. + */ +export function makeKernelError( + code: ExpectedKernelErrorCode, + detail: string, +): CapData { + return kser(Error(`[KERNEL:${code}] ${detail}`)); +} + +/** + * Produce a serialized fatal kernel error with a machine-readable code. + * The resulting error message has the format `[KERNEL:FATAL:] `. + * + * @param code - The fatal kernel error code. + * @param detail - A human-readable description. + * @returns The serialized error. + */ +export function makeFatalKernelError( + code: FatalKernelErrorCode, + detail: string, +): CapData { + return kser(Error(`[KERNEL:FATAL:${code}] ${detail}`)); +} diff --git a/packages/ocap-kernel/src/remotes/kernel/RemoteManager.test.ts b/packages/ocap-kernel/src/remotes/kernel/RemoteManager.test.ts index 734804af0f..9b1eb90589 100644 --- a/packages/ocap-kernel/src/remotes/kernel/RemoteManager.test.ts +++ b/packages/ocap-kernel/src/remotes/kernel/RemoteManager.test.ts @@ -760,7 +760,7 @@ describe('RemoteManager', () => { onRemoteGiveUp(peerId); // Verify pending redemptions were rejected expect(rejectPendingRedemptionsSpy).toHaveBeenCalledWith( - `Remote connection lost: ${peerId} (max retries reached or non-retryable error)`, + 'Remote connection lost (max retries reached or non-retryable error)', ); }); diff --git a/packages/ocap-kernel/src/remotes/kernel/RemoteManager.ts b/packages/ocap-kernel/src/remotes/kernel/RemoteManager.ts index b23fd56939..e3a289771f 100644 --- a/packages/ocap-kernel/src/remotes/kernel/RemoteManager.ts +++ b/packages/ocap-kernel/src/remotes/kernel/RemoteManager.ts @@ -3,7 +3,7 @@ import type { Logger } from '@metamask/logger'; import { initRemoteComms, initRemoteIdentity } from './remote-comms.ts'; import { RemoteHandle } from './RemoteHandle.ts'; import type { KernelQueue } from '../../KernelQueue.ts'; -import { kser } from '../../liveslots/kernel-marshal.ts'; +import { makeKernelError } from '../../liveslots/kernel-marshal.ts'; import type { KernelStore } from '../../store/index.ts'; import type { PlatformServices, RemoteId } from '../../types.ts'; import type { @@ -174,16 +174,15 @@ export class RemoteManager { } const { remoteId } = remote; - const failure = kser( - Error( - `Remote connection lost: ${peerId} (max retries reached or non-retryable error)`, - ), + const failure = makeKernelError( + 'CONNECTION_LOST', + 'Remote connection lost (max retries reached or non-retryable error)', ); // Reject pending URL redemptions in the RemoteHandle // These are JavaScript promises that will propagate rejection to kernel promises remote.rejectPendingRedemptions( - `Remote connection lost: ${peerId} (max retries reached or non-retryable error)`, + 'Remote connection lost (max retries reached or non-retryable error)', ); // Reject all promises for which this remote is the decider @@ -218,8 +217,9 @@ export class RemoteManager { // Reject all kernel promises where this remote is the decider // The restarted peer has lost its state and won't resolve these promises const { remoteId } = remote; - const failure = kser( - Error(`Remote peer restarted: ${peerId} (incarnation changed)`), + const failure = makeKernelError( + 'PEER_RESTARTED', + 'Remote peer restarted (incarnation changed)', ); for (const kpid of this.#kernelStore.getPromisesByDecider(remoteId)) { this.#kernelQueue.resolvePromises(remoteId, [[kpid, true, failure]]); diff --git a/packages/ocap-kernel/src/vats/VatHandle.ts b/packages/ocap-kernel/src/vats/VatHandle.ts index b529afe246..950b12e9ba 100644 --- a/packages/ocap-kernel/src/vats/VatHandle.ts +++ b/packages/ocap-kernel/src/vats/VatHandle.ts @@ -16,7 +16,7 @@ import { isJsonRpcNotification, isJsonRpcResponse } from '@metamask/utils'; import type { JsonRpcNotification, JsonRpcResponse } from '@metamask/utils'; import type { KernelQueue } from '../KernelQueue.ts'; -import { kser, makeError } from '../liveslots/kernel-marshal.ts'; +import { makeError, makeKernelError } from '../liveslots/kernel-marshal.ts'; import { vatMethodSpecs, vatSyscallHandlers } from '../rpc/index.ts'; import type { PingVatResult, VatMethod } from '../rpc/index.ts'; import type { KernelStore } from '../store/index.ts'; @@ -300,7 +300,10 @@ export class VatHandle implements EndpointHandle { const terminationError = error ?? new VatDeletedError(this.vatId); if (terminating) { // Reject promises exported to other vats for which this vat is the decider - const failure = kser(terminationError); + const failure = makeKernelError( + 'VAT_TERMINATED', + terminationError.message, + ); for (const kpid of this.#kernelStore.getPromisesByDecider(this.vatId)) { this.#kernelQueue.resolvePromises(this.vatId, [[kpid, true, failure]]); } diff --git a/packages/ocap-kernel/src/vats/VatSyscall.ts b/packages/ocap-kernel/src/vats/VatSyscall.ts index 9453429c63..41d3c9be0f 100644 --- a/packages/ocap-kernel/src/vats/VatSyscall.ts +++ b/packages/ocap-kernel/src/vats/VatSyscall.ts @@ -12,7 +12,7 @@ import { performExportCleanup, } from '../garbage-collection/gc-handlers.ts'; import type { KernelQueue } from '../KernelQueue.ts'; -import { makeError } from '../liveslots/kernel-marshal.ts'; +import { makeFatalKernelError } from '../liveslots/kernel-marshal.ts'; import type { KernelStore } from '../store/index.ts'; import { coerceMessage } from '../types.ts'; import type { Message, VatId, KRef } from '../types.ts'; @@ -161,7 +161,10 @@ export class VatSyscall { // This is a safety check - this case should never happen if (!this.#kernelStore.isVatActive(this.vatId)) { - this.#recordVatFatalSyscall('vat not found'); + this.#recordVatFatalSyscall( + 'ILLEGAL_SYSCALL', + 'Syscall from inactive vat', + ); return harden(['error', 'vat not found']); } @@ -259,7 +262,10 @@ export class VatSyscall { return harden(['ok', null]); } catch (error) { this.#logger?.error(`Fatal syscall error in vat ${this.vatId}`, error); - this.#recordVatFatalSyscall('syscall translation error: prepare to die'); + this.#recordVatFatalSyscall( + 'ILLEGAL_SYSCALL', + 'Syscall translation error: prepare to die', + ); return harden([ 'error', error instanceof Error ? error.message : String(error), @@ -270,9 +276,16 @@ export class VatSyscall { /** * Log a fatal syscall error and set the illegalSyscall property. * - * @param error - The error message to log. + * @param code - The fatal kernel error code. + * @param detail - A human-readable description of the error. */ - #recordVatFatalSyscall(error: string): void { - this.illegalSyscall = { vatId: this.vatId, info: makeError(error) }; + #recordVatFatalSyscall( + code: 'ILLEGAL_SYSCALL' | 'INTERNAL_ERROR', + detail: string, + ): void { + this.illegalSyscall = { + vatId: this.vatId, + info: makeFatalKernelError(code, detail), + }; } } From 8d4e7e0cebfe9d5107fd66245c10a7f73c0c1754 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:49:13 -0700 Subject: [PATCH 2/3] refactor(kernel-errors): rename FATAL to VAT_FATAL Fatal kernel errors terminate the offending vat, not the kernel itself. Rename the infix from FATAL to VAT_FATAL to reflect this: `[KERNEL:VAT_FATAL:] detail`. Co-Authored-By: Claude Opus 4.6 --- packages/kernel-errors/src/kernel-error.test.ts | 14 ++++++++------ packages/kernel-errors/src/kernel-error.ts | 10 +++++----- .../ocap-kernel/src/liveslots/kernel-marshal.ts | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/kernel-errors/src/kernel-error.test.ts b/packages/kernel-errors/src/kernel-error.test.ts index 25b3f1d239..65955f00db 100644 --- a/packages/kernel-errors/src/kernel-error.test.ts +++ b/packages/kernel-errors/src/kernel-error.test.ts @@ -10,7 +10,7 @@ import { describe('KERNEL_ERROR_PATTERN', () => { it.each([ ['[KERNEL:OBJECT_REVOKED] Target object has been revoked', true], - ['[KERNEL:FATAL:ILLEGAL_SYSCALL] Fatal syscall violation', true], + ['[KERNEL:VAT_FATAL:ILLEGAL_SYSCALL] Fatal syscall violation', true], ['[KERNEL:CONNECTION_LOST] Remote connection lost', true], ['Some other error', false], ['KERNEL:OBJECT_REVOKED', false], @@ -29,7 +29,7 @@ describe('isKernelError', () => { it('returns true for a fatal kernel error', () => { expect( - isKernelError(Error('[KERNEL:FATAL:INTERNAL_ERROR] Something broke')), + isKernelError(Error('[KERNEL:VAT_FATAL:INTERNAL_ERROR] Something broke')), ).toBe(true); }); @@ -57,10 +57,10 @@ describe('getKernelErrorCode', () => { it('extracts fatal error codes', () => { expect( - getKernelErrorCode(Error('[KERNEL:FATAL:ILLEGAL_SYSCALL] bad')), + getKernelErrorCode(Error('[KERNEL:VAT_FATAL:ILLEGAL_SYSCALL] bad')), ).toBe('ILLEGAL_SYSCALL'); expect( - getKernelErrorCode(Error('[KERNEL:FATAL:INTERNAL_ERROR] broken')), + getKernelErrorCode(Error('[KERNEL:VAT_FATAL:INTERNAL_ERROR] broken')), ).toBe('INTERNAL_ERROR'); }); @@ -72,7 +72,9 @@ describe('getKernelErrorCode', () => { describe('isFatalKernelError', () => { it('returns true for fatal kernel errors', () => { expect( - isFatalKernelError(Error('[KERNEL:FATAL:ILLEGAL_SYSCALL] bad syscall')), + isFatalKernelError( + Error('[KERNEL:VAT_FATAL:ILLEGAL_SYSCALL] bad syscall'), + ), ).toBe(true); }); @@ -103,7 +105,7 @@ describe('round-trip', () => { it('constructs and detects a fatal kernel error message', () => { const code = 'INTERNAL_ERROR'; const detail = 'Internal kernel error'; - const message = `[KERNEL:FATAL:${code}] ${detail}`; + const message = `[KERNEL:VAT_FATAL:${code}] ${detail}`; const error = Error(message); expect(isKernelError(error)).toBe(true); diff --git a/packages/kernel-errors/src/kernel-error.ts b/packages/kernel-errors/src/kernel-error.ts index 6049efdbb5..1d425db9d0 100644 --- a/packages/kernel-errors/src/kernel-error.ts +++ b/packages/kernel-errors/src/kernel-error.ts @@ -13,7 +13,7 @@ export type ExpectedKernelErrorCode = /** * Error codes for fatal kernel errors (kernel bugs or illegal operations). - * These are prefixed with `FATAL:` in the error message. + * These are prefixed with `VAT_FATAL:` in the error message. */ export type FatalKernelErrorCode = 'ILLEGAL_SYSCALL' | 'INTERNAL_ERROR'; @@ -24,9 +24,9 @@ export type KernelErrorCode = ExpectedKernelErrorCode | FatalKernelErrorCode; /** * Pattern matching kernel error messages. - * Matches both `[KERNEL:]` and `[KERNEL:FATAL:]`. + * Matches both `[KERNEL:]` and `[KERNEL:VAT_FATAL:]`. */ -export const KERNEL_ERROR_PATTERN = /^\[KERNEL:(?:(FATAL):)?([A-Z_]+)\]/u; +export const KERNEL_ERROR_PATTERN = /^\[KERNEL:(?:(VAT_FATAL):)?([A-Z_]+)\]/u; /** * Check whether a value is a kernel error (an Error whose message starts with @@ -55,12 +55,12 @@ export function getKernelErrorCode(error: Error): KernelErrorCode | undefined { /** * Check whether an Error is a fatal kernel error (its message contains the - * `FATAL:` infix). + * `VAT_FATAL:` infix). * * @param error - The error to inspect. * @returns `true` if the error is a fatal kernel error. */ export function isFatalKernelError(error: Error): boolean { const match = KERNEL_ERROR_PATTERN.exec(error.message); - return match !== null && match[1] === 'FATAL'; + return match !== null && match[1] === 'VAT_FATAL'; } diff --git a/packages/ocap-kernel/src/liveslots/kernel-marshal.ts b/packages/ocap-kernel/src/liveslots/kernel-marshal.ts index f649dade17..21cf583714 100644 --- a/packages/ocap-kernel/src/liveslots/kernel-marshal.ts +++ b/packages/ocap-kernel/src/liveslots/kernel-marshal.ts @@ -168,7 +168,7 @@ export function makeKernelError( /** * Produce a serialized fatal kernel error with a machine-readable code. - * The resulting error message has the format `[KERNEL:FATAL:] `. + * The resulting error message has the format `[KERNEL:VAT_FATAL:] `. * * @param code - The fatal kernel error code. * @param detail - A human-readable description. @@ -178,5 +178,5 @@ export function makeFatalKernelError( code: FatalKernelErrorCode, detail: string, ): CapData { - return kser(Error(`[KERNEL:FATAL:${code}] ${detail}`)); + return kser(Error(`[KERNEL:VAT_FATAL:${code}] ${detail}`)); } From 3b6db98a8aaf9f05235e50d107f796c0585cb0fe Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:55:01 -0700 Subject: [PATCH 3/3] fix(kernel-errors): address review findings - Import FatalKernelErrorCode in VatSyscall.ts instead of inline union - Add unit tests for makeKernelError and makeFatalKernelError with round-trip verification through kernel-errors detection utilities Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/liveslots/kernel-marshal.test.ts | 63 ++++++++++++++++++- packages/ocap-kernel/src/vats/VatSyscall.ts | 6 +- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/packages/ocap-kernel/src/liveslots/kernel-marshal.test.ts b/packages/ocap-kernel/src/liveslots/kernel-marshal.test.ts index 22ed759318..8fe59a56b6 100644 --- a/packages/ocap-kernel/src/liveslots/kernel-marshal.test.ts +++ b/packages/ocap-kernel/src/liveslots/kernel-marshal.test.ts @@ -1,7 +1,20 @@ import { passStyleOf } from '@endo/marshal'; +import { + isKernelError, + getKernelErrorCode, + isFatalKernelError, +} from '@metamask/kernel-errors'; import { describe, it, expect } from 'vitest'; -import { kslot, krefOf, kser, kunser, makeError } from './kernel-marshal.ts'; +import { + kslot, + krefOf, + kser, + kunser, + makeError, + makeKernelError, + makeFatalKernelError, +} from './kernel-marshal.ts'; import type { SlotValue } from './kernel-marshal.ts'; describe('kernel-marshal', () => { @@ -121,6 +134,54 @@ describe('kernel-marshal', () => { }); }); + describe('makeKernelError', () => { + it('serializes an expected kernel error with the correct format', () => { + const serialized = makeKernelError('OBJECT_DELETED', 'Target deleted'); + const deserialized = kunser(serialized); + + expect(deserialized).toBeInstanceOf(Error); + expect((deserialized as Error).message).toBe( + '[KERNEL:OBJECT_DELETED] Target deleted', + ); + }); + + it('round-trips through kernel-errors detection utilities', () => { + const serialized = makeKernelError( + 'CONNECTION_LOST', + 'Remote connection lost', + ); + const deserialized = kunser(serialized) as Error; + + expect(isKernelError(deserialized)).toBe(true); + expect(getKernelErrorCode(deserialized)).toBe('CONNECTION_LOST'); + expect(isFatalKernelError(deserialized)).toBe(false); + }); + }); + + describe('makeFatalKernelError', () => { + it('serializes a fatal kernel error with the VAT_FATAL infix', () => { + const serialized = makeFatalKernelError('ILLEGAL_SYSCALL', 'Bad syscall'); + const deserialized = kunser(serialized); + + expect(deserialized).toBeInstanceOf(Error); + expect((deserialized as Error).message).toBe( + '[KERNEL:VAT_FATAL:ILLEGAL_SYSCALL] Bad syscall', + ); + }); + + it('round-trips through kernel-errors detection utilities', () => { + const serialized = makeFatalKernelError( + 'INTERNAL_ERROR', + 'Something broke', + ); + const deserialized = kunser(serialized) as Error; + + expect(isKernelError(deserialized)).toBe(true); + expect(getKernelErrorCode(deserialized)).toBe('INTERNAL_ERROR'); + expect(isFatalKernelError(deserialized)).toBe(true); + }); + }); + describe('makeError', () => { it('creates serialized error with message', () => { const message = 'Test error message'; diff --git a/packages/ocap-kernel/src/vats/VatSyscall.ts b/packages/ocap-kernel/src/vats/VatSyscall.ts index 41d3c9be0f..7c67e1769e 100644 --- a/packages/ocap-kernel/src/vats/VatSyscall.ts +++ b/packages/ocap-kernel/src/vats/VatSyscall.ts @@ -4,6 +4,7 @@ import type { VatSyscallObject, VatSyscallResult, } from '@agoric/swingset-liveslots'; +import type { FatalKernelErrorCode } from '@metamask/kernel-errors'; import { Logger } from '@metamask/logger'; import { @@ -279,10 +280,7 @@ export class VatSyscall { * @param code - The fatal kernel error code. * @param detail - A human-readable description of the error. */ - #recordVatFatalSyscall( - code: 'ILLEGAL_SYSCALL' | 'INTERNAL_ERROR', - detail: string, - ): void { + #recordVatFatalSyscall(code: FatalKernelErrorCode, detail: string): void { this.illegalSyscall = { vatId: this.vatId, info: makeFatalKernelError(code, detail),