Skip to content

Commit d979a06

Browse files
rekmarksclaude
andauthored
refactor(ocap-kernel): branded kernel identifiers with runtime validation (#917)
Fixes #809 ## Summary - Add branded string types (`VatId`, `RemoteId`, `KRef`, `VRef`, `RRef`, `SubclusterId`, `GCAction`, etc.) for kernel identifiers with runtime validators (`isX`/`insistX`) - Split `Message` into `KernelMessage` (kernel-space, KRef slots) and `EndpointMessage` (endpoint-space, ERef slots) - Remove public `kv` from `KernelStore` API, replace with typed accessor methods - Add `KernelOneResolution` type for kernel-space promise resolutions - Add `KernelSyscallObject` discriminated union type for kernel-space syscalls, removing casts from `VatSyscall` - Add `makeGCAction` factory with runtime validation - Remove redundant interior runtime validation where branded types enforce correctness at compile time - Tighten function signatures throughout OCAP URL redemption chain and `kslot`/`makeStandinPromise` to use `KRef` - Validate `coerceEndpointMessage` with `EndpointMessageStruct` at the liveslots boundary - Fix `getOwner` to handle `'kernel'` owner without throwing EndpointId validation failure - Document branded type trust model (types.ts header, store README) - Add comprehensive tests for all ref validators and assertion functions ## TODO - `kernel-ui` type safety is not fully addressed by this PR, and will be implemented in a follow-up - While the API of the kernel store is type safe, it is only type safe by an established convention of `writeX(value: X): void` / `readX(): X` pairs that use the same db keys internally. If we want to make the store actually type safe, we would have to establish some kind of compile-time mapping between db keys and value types. This may or may not be worth the trouble, and we defer this decision to the future. ## Test plan - [x] `yarn workspace @MetaMask/ocap-kernel lint:fix` passes - [x] `yarn workspace @MetaMask/ocap-kernel build` passes - [x] `yarn workspace @MetaMask/ocap-kernel test:dev:quiet` passes - [x] `yarn workspace @metamask/kernel-node-runtime test:e2e:ci` passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches core kernel routing/queueing, remote comms, and persistence access patterns; while largely type-safety refactors, signature changes and reduced runtime checks could surface as behavioral regressions at boundaries (RPC/liveslots/remotes) if any call sites or stored data violate the new invariants. > > **Overview** > **Branded kernel identifiers are enforced end-to-end.** This introduces branded string types (e.g., `KRef`, `VatId`, `SubclusterId`) with `is*`/`insist*` validators, and updates kernel/public RPC APIs (e.g., `queueMessage`, `issueOcapURL`, `terminateSubcluster`) and UI call sites to use these types. > > **Message and resolution types are split and tightened.** `Message` is separated into `KernelMessage` vs `EndpointMessage`, promise resolution flows switch to kernel-space types, and the queue/router/service-manager/remotes paths are updated accordingly (including narrowed `kslot`/`krefOf` behavior). > > **Raw KV access is removed from `KernelStore`.** `KernelStore.kv` is no longer exposed; consumers/tests now use typed accessors for initialization state, kernel-service krefs, remote identity values, and known relay storage (with added validation on relay parsing). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 812f5bf. 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.6 <noreply@anthropic.com>
1 parent a0c1560 commit d979a06

74 files changed

Lines changed: 1500 additions & 918 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/kernel-store/src/sqlite/nodejs.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,11 @@ function makeKVStore(db: Database): KVStore {
108108
}
109109

110110
return {
111-
get: (key) => kvGet(key, false),
111+
get: <Value extends string = string>(key: string) =>
112+
kvGet(key, false) as Value | undefined,
112113
getNextKey: kvGetNextKey,
113-
getRequired: (key) => kvGet(key, true) as string,
114+
getRequired: <Value extends string = string>(key: string) =>
115+
kvGet(key, true) as Value,
114116
set: kvSet,
115117
delete: kvDelete,
116118
};

packages/kernel-store/src/sqlite/wasm.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,11 @@ function makeKVStore(db: Database, logger?: Logger): KVStore {
137137
}
138138

139139
return {
140-
get: (key) => kvGet(key, false),
140+
get: <Value extends string = string>(key: string) =>
141+
kvGet(key, false) as Value | undefined,
141142
getNextKey: kvGetNextKey,
142-
getRequired: (key) => kvGet(key, true) as string,
143+
getRequired: <Value extends string = string>(key: string) =>
144+
kvGet(key, true) as Value,
143145
set: kvSet,
144146
delete: kvDelete,
145147
};

packages/kernel-store/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export type KVStore = {
2-
get(key: string): string | undefined;
3-
getRequired(key: string): string;
2+
get<Value extends string = string>(key: string): Value | undefined;
3+
getRequired<Value extends string = string>(key: string): Value;
44
getNextKey(previousKey: string): string | undefined;
55
set(key: string, value: string): void;
66
delete(key: string): void;

packages/kernel-test/src/persistence.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CapData } from '@endo/marshal';
22
import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs';
33
import { waitUntilQuiescent } from '@metamask/kernel-utils';
4-
import { kunser, makeKernelStore } from '@metamask/ocap-kernel';
4+
import { kunser } from '@metamask/ocap-kernel';
55
import { unlink } from 'node:fs/promises';
66
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
77

@@ -153,7 +153,7 @@ describe('persistent storage', { timeout: 20_000 }, () => {
153153

154154
it('handles messages in queue after kernel restart', async () => {
155155
const database1 = await makeSQLKernelDatabase({ dbFilename: databasePath });
156-
const kernelStore1 = makeKernelStore(database1);
156+
const kv1 = database1.kernelKVStore;
157157
const kernel1 = await makeKernel(
158158
database1,
159159
false,
@@ -168,31 +168,31 @@ describe('persistent storage', { timeout: 20_000 }, () => {
168168
const result1 = await kernel1.queueMessage(v1Root, 'resume', []);
169169
expect(kunser(result1)).toBe('Counter incremented to: 2');
170170
// Enqueue a send message into the database
171-
kernelStore1.kv.set('queue.run.head', '4');
172-
kernelStore1.kv.set('nextPromiseId', '4');
173-
kernelStore1.kv.set(`${v1Root}.refCount`, '3,3');
174-
kernelStore1.kv.set('queue.kp3.head', '1');
175-
kernelStore1.kv.set('queue.kp3.tail', '1');
176-
kernelStore1.kv.set('kp3.state', 'unresolved');
177-
kernelStore1.kv.set('kp3.subscribers', '[]');
178-
kernelStore1.kv.set('kp3.refCount', '2');
179-
kernelStore1.kv.set(
171+
kv1.set('queue.run.head', '4');
172+
kv1.set('nextPromiseId', '4');
173+
kv1.set(`${v1Root}.refCount`, '3,3');
174+
kv1.set('queue.kp3.head', '1');
175+
kv1.set('queue.kp3.tail', '1');
176+
kv1.set('kp3.state', 'unresolved');
177+
kv1.set('kp3.subscribers', '[]');
178+
kv1.set('kp3.refCount', '2');
179+
kv1.set(
180180
'queue.run.3',
181181
`{"type":"send","target":"${v1Root}","message":{"methargs":{"body":"#[\\"resume\\",[]]","slots":[]},"result":"kp3"}}`,
182182
);
183183
await kernel1.stop();
184184
// Open a fresh connection to verify the message is in the database
185185
const database2 = await makeSQLKernelDatabase({ dbFilename: databasePath });
186-
const kernelStore2 = makeKernelStore(database2);
187-
expect(kernelStore2.kv.get('queue.run.3')).toBeDefined();
186+
const kv2 = database2.kernelKVStore;
187+
expect(kv2.get('queue.run.3')).toBeDefined();
188188
// restart the kernel
189189
const kernel2 = await makeKernel(
190190
database2,
191191
false,
192192
logger.logger.subLogger({ tags: ['test'] }),
193193
);
194194
// verify that the run queue is empty
195-
expect(kernelStore2.kv.get('queue.run.3')).toBeUndefined();
195+
expect(kv2.get('queue.run.3')).toBeUndefined();
196196
// verify that the message is processed and the counter is incremented
197197
const result2 = await kernel2.queueMessage(v1Root, 'resume', []);
198198
expect(kunser(result2)).toBe('Counter incremented to: 4');

packages/kernel-ui/src/components/SendMessageForm.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
TextColor,
1010
} from '@metamask/design-system-react';
1111
import { stringify } from '@metamask/kernel-utils';
12+
import type { KRef } from '@metamask/ocap-kernel';
1213
import type { Json } from '@metamask/utils';
1314
import { useState, useMemo } from 'react';
1415

@@ -68,7 +69,7 @@ export const SendMessageForm: React.FC = () => {
6869
.then(async (args) =>
6970
callKernelMethod({
7071
method: 'queueMessage',
71-
params: [target, method, args],
72+
params: [target as KRef, method, args],
7273
}),
7374
)
7475
.then((response) => {

packages/kernel-ui/src/components/SubclusterAccordion.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
TextVariant,
88
} from '@metamask/design-system-react';
99
import { stringify } from '@metamask/kernel-utils';
10+
import type { SubclusterId, VatId } from '@metamask/ocap-kernel';
1011
import { useMemo, useState } from 'react';
1112

1213
import type { VatRecord } from '../types.ts';
@@ -15,13 +16,13 @@ import { Modal } from './shared/Modal.tsx';
1516
import { VatTable } from './VatTable.tsx';
1617

1718
export const SubclusterAccordion: React.FC<{
18-
id: string;
19+
id: SubclusterId;
1920
vats: VatRecord[];
2021
config: unknown;
21-
onPingVat: (id: string) => void;
22-
onRestartVat: (id: string) => void;
23-
onTerminateVat: (id: string) => void;
24-
onTerminateSubcluster: (id: string) => void;
22+
onPingVat: (id: VatId) => void;
23+
onRestartVat: (id: VatId) => void;
24+
onTerminateVat: (id: VatId) => void;
25+
onTerminateSubcluster: (id: SubclusterId) => void;
2526
}> = ({
2627
id,
2728
vats,

packages/kernel-ui/src/components/VatTable.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
IconName,
66
ButtonIcon,
77
} from '@metamask/design-system-react';
8+
import type { VatId } from '@metamask/ocap-kernel';
89

910
import type { VatRecord } from '../types.ts';
1011
import {
@@ -17,9 +18,9 @@ import {
1718

1819
export const VatTable: React.FC<{
1920
vats: VatRecord[];
20-
onPingVat: (id: string) => void;
21-
onRestartVat: (id: string) => void;
22-
onTerminateVat: (id: string) => void;
21+
onPingVat: (id: VatId) => void;
22+
onRestartVat: (id: VatId) => void;
23+
onTerminateVat: (id: VatId) => void;
2324
}> = ({ vats, onPingVat, onRestartVat, onTerminateVat }) => {
2425
if (vats.length === 0) {
2526
return null;

packages/kernel-ui/src/hooks/useVats.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { stringify } from '@metamask/kernel-utils';
22
import type {
33
VatConfig,
44
VatId,
5+
SubclusterId,
56
Subcluster,
67
KernelStatus,
78
} from '@metamask/ocap-kernel';
@@ -45,7 +46,7 @@ export const useVats = (): {
4546
pingVat: (id: VatId) => void;
4647
restartVat: (id: VatId) => void;
4748
terminateVat: (id: VatId) => void;
48-
terminateSubcluster: (id: string) => void;
49+
terminateSubcluster: (id: SubclusterId) => void;
4950
hasVats: boolean;
5051
} => {
5152
const { callKernelMethod, status, logMessage } = usePanelContext();
@@ -120,7 +121,7 @@ export const useVats = (): {
120121
);
121122

122123
const terminateSubcluster = useCallback(
123-
(id: string) => {
124+
(id: SubclusterId) => {
124125
callKernelMethod({
125126
method: 'terminateSubcluster',
126127
params: { id },

packages/kernel-ui/src/services/db-parser.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { KRef } from '@metamask/ocap-kernel';
2+
13
import type {
24
ObjectRegistry,
35
VatSnapshot,
@@ -22,8 +24,8 @@ export function parseObjectRegistry(
2224
const kpValueRaw: Record<string, { body: string; slots: string[] }> = {};
2325
const vatConfigs: Record<string, { name: string; bundleSpec: string }> = {};
2426
// C-lists
25-
const objCList: { vat: string; kref: string; eref: string }[] = [];
26-
const prmCList: { vat: string; kref: string; eref: string }[] = [];
27+
const objCList: { vat: string; kref: KRef; eref: string }[] = [];
28+
const prmCList: { vat: string; kref: KRef; eref: string }[] = [];
2729

2830
let gcActions = '';
2931
let reapQueue = '';
@@ -78,7 +80,7 @@ export function parseObjectRegistry(
7880
matches[1] &&
7981
objCList.push({
8082
vat: matches[1] ?? '',
81-
kref: matches[2] ?? '',
83+
kref: (matches[2] ?? '') as KRef,
8284
eref: value.replace(/^R\s*/u, ''),
8385
});
8486
continue;
@@ -87,7 +89,7 @@ export function parseObjectRegistry(
8789
matches[1] &&
8890
prmCList.push({
8991
vat: matches[1] ?? '',
90-
kref: matches[2] ?? '',
92+
kref: (matches[2] ?? '') as KRef,
9193
eref: value.replace(/^R\s*/u, ''),
9294
});
9395
continue;
@@ -112,7 +114,11 @@ export function parseObjectRegistry(
112114
// Helper to resolve slots
113115
const resolveSlot = (kref: string): SlotInfo => {
114116
const entry = objCList.find((item) => item.kref === kref);
115-
return { kref, eref: entry?.eref ?? null, vat: entry?.vat ?? null };
117+
return {
118+
kref: kref as KRef,
119+
eref: entry?.eref ?? null,
120+
vat: entry?.vat ?? null,
121+
};
116122
};
117123

118124
// 3) Populate objects

packages/kernel-ui/src/types.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { VatId } from '@metamask/ocap-kernel';
1+
import type { KRef, SubclusterId, VatId } from '@metamask/ocap-kernel';
22

33
export type VatRecord = {
44
id: VatId;
55
source: string;
66
parameters: string;
77
creationOptions: string;
8-
subclusterId: string;
8+
subclusterId: SubclusterId;
99
};
1010

1111
/**
@@ -37,7 +37,7 @@ export type VatSnapshot = {
3737
};
3838

3939
export type ObjectBinding = {
40-
kref: string;
40+
kref: KRef;
4141
eref: string;
4242
refCount: string;
4343
};
@@ -52,7 +52,7 @@ export type ObjectBindingWithTargets = {
5252
} & ObjectBinding;
5353

5454
export type PromiseBinding = {
55-
kref: string;
55+
kref: KRef;
5656
eref: string;
5757
state: string;
5858
value: { body: string; slots: SlotInfo[] };
@@ -67,7 +67,7 @@ export type PromiseBindingWithTargets = {
6767
} & PromiseBinding;
6868

6969
export type SlotInfo = {
70-
kref: string;
70+
kref: KRef;
7171
eref: string | null;
7272
vat: string | null;
7373
};

0 commit comments

Comments
 (0)