Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions packages/kernel-errors/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,43 @@
# `errors`
# `@metamask/kernel-errors`

Ocap Kernel errors.
Error types, utilities, and serialization for the OCAP kernel.

This package contains three distinct categories of error tooling, each serving
a different domain. They exist in the same package because they share base
infrastructure, but they are not interchangeable.

## Error classes (`src/errors/`)

Typed `BaseError` subclasses used **kernel-side and host-side**. Each carries a
structured `.code` (`ErrorCode` enum) and `.data` (JSON) property. Kernel code
inspects these via `instanceof` checks and type guards like
`isResourceLimitError()`.

These classes **never reach vat code directly** — they are thrown and caught
within the kernel, platform services, and agent infrastructure.

## Stream error marshalling (`src/marshal/`)

Custom JSON serialization (`marshalError` / `unmarshalError`) that preserves
`.code`, `.data`, `.cause`, and `.stack` across stream and IPC boundaries. Used
by `@metamask/streams` to transport errors through message ports.

This is unrelated to the kernel's `@endo/marshal`-based `kser`/`kunser`
serialization. The two systems operate at different layers and never interact.

## Vat-observable error codes (`src/vat-observable-errors.ts`)

Machine-readable codes embedded in the error `.message` for errors that reach
**vat code as promise rejections**. These errors are serialized via `kser`
(`@endo/marshal` with `errorTagging: 'off'`), which strips all `Error`
properties except `.message` and `.name`. The message is therefore the only
reliable channel for structured information.

Format: `[KERNEL:<CODE>] Human-readable detail` for expected errors,
`[KERNEL:VAT_FATAL:<CODE>] detail` for fatal errors that terminate the vat.

Detection utilities (`isKernelError`, `getKernelErrorCode`,
`isFatalKernelError`) let vat code programmatically categorize these errors.

## Installation

Expand All @@ -12,4 +49,5 @@ or

## Contributing

This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/ocap-kernel#readme).
This package is part of a monorepo. Instructions for contributing can be found
in the [monorepo README](https://github.com/MetaMask/ocap-kernel#readme).
4 changes: 4 additions & 0 deletions packages/kernel-errors/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('index', () => {
'ErrorSentinel',
'ErrorStruct',
'EvaluatorError',
'KERNEL_ERROR_PATTERN',
'MarshaledErrorStruct',
'MarshaledOcapErrorStruct',
'ResourceLimitError',
Expand All @@ -20,7 +21,10 @@ describe('index', () => {
'VatAlreadyExistsError',
'VatDeletedError',
'VatNotFoundError',
'getKernelErrorCode',
'getNetworkErrorCode',
'isFatalKernelError',
'isKernelError',
'isMarshaledError',
'isMarshaledOcapError',
'isOcapError',
Expand Down
11 changes: 11 additions & 0 deletions packages/kernel-errors/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 './vat-observable-errors.ts';
export {
KERNEL_ERROR_PATTERN,
isKernelError,
getKernelErrorCode,
isFatalKernelError,
} from './vat-observable-errors.ts';
115 changes: 115 additions & 0 deletions packages/kernel-errors/src/vat-observable-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, it, expect } from 'vitest';

import {
KERNEL_ERROR_PATTERN,
isKernelError,
getKernelErrorCode,
isFatalKernelError,
} from './vat-observable-errors.ts';

describe('KERNEL_ERROR_PATTERN', () => {
it.each([
['[KERNEL:OBJECT_REVOKED] Target object has been revoked', 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],
['[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:VAT_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:VAT_FATAL:ILLEGAL_SYSCALL] bad')),
).toBe('ILLEGAL_SYSCALL');
expect(
getKernelErrorCode(Error('[KERNEL:VAT_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:VAT_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:VAT_FATAL:${code}] ${detail}`;
const error = Error(message);

expect(isKernelError(error)).toBe(true);
expect(getKernelErrorCode(error)).toBe(code);
expect(isFatalKernelError(error)).toBe(true);
});
});
66 changes: 66 additions & 0 deletions packages/kernel-errors/src/vat-observable-errors.ts
Original file line number Diff line number Diff line change
@@ -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 `VAT_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:<CODE>]` and `[KERNEL:VAT_FATAL:<CODE>]`.
*/
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
* `[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
* `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] === 'VAT_FATAL';
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion packages/kernel-test/src/orphaned-ephemeral-exo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]'),
});
});
});
8 changes: 4 additions & 4 deletions packages/kernel-test/src/syscall-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', []);
Expand Down Expand Up @@ -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', []);
Expand All @@ -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
Expand Down Expand Up @@ -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]'),
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/kernel-test/src/vat-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading