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
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 './kernel-error.ts';
export {
KERNEL_ERROR_PATTERN,
isKernelError,
getKernelErrorCode,
isFatalKernelError,
} from './kernel-error.ts';
115 changes: 115 additions & 0 deletions packages/kernel-errors/src/kernel-error.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 './kernel-error.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/kernel-error.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
2 changes: 1 addition & 1 deletion packages/ocap-kernel/src/KernelRouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
}),
]),
Expand Down
34 changes: 22 additions & 12 deletions packages/ocap-kernel/src/KernelRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 };
Expand All @@ -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':
Expand Down Expand Up @@ -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)',
),
],
]);
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/ocap-kernel/src/KernelServiceManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -355,7 +355,7 @@ describe('KernelServiceManager', () => {
await delay();

expect(mockKernelQueue.resolvePromises).toHaveBeenCalledWith('kernel', [
['kp123', true, kser(testError)],
['kp123', true, makeKernelError('DELIVERY_FAILED', testError.message)],
]);
});

Expand Down
Loading
Loading