Skip to content

Commit f7b4257

Browse files
sirtimidclaude
andauthored
feat(ocap-kernel): add initRemoteComms and registerLocationHints RPC methods (#849)
## Summary - Adds `initRemoteComms` kernel-control RPC handler — calls `kernel.initRemoteComms(options)` with optional `relays`, `directListenAddresses`, `maxRetryAttempts`, and `maxQueue` params - Adds `registerLocationHints` kernel-control RPC handler — calls `kernel.registerLocationHints(peerId, hints)` for registering peer multiaddr location hints - Registers both in the kernel-control index and updates the index test These methods allow the daemon CLI (`ocap daemon exec initRemoteComms`, `ocap daemon exec registerLocationHints`) to initialize the libp2p networking stack and register peer location hints, which were previously only accessible via the Node.js API. ## Test plan - [x] Unit tests for `initRemoteComms` handler (empty options, individual options, all options, error propagation) - [x] Unit tests for `registerLocationHints` handler (single hint, multiple hints, error propagation) - [x] Index test updated to include both new handlers/specs - [x] `yarn workspace @MetaMask/ocap-kernel test:dev:quiet` passes (new tests green, no regressions) - [x] `yarn workspace @MetaMask/ocap-kernel build` compiles cleanly - [x] Manual: `ocap daemon exec initRemoteComms '{"directListenAddresses":["/ip4/0.0.0.0/udp/0/quic-v1"]}'` then `ocap daemon exec getStatus` shows `remoteComms.state === "connected"` 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces new RPC entry points that can start/affect remote comms behavior; while changes are additive and well-tested, they expand the daemon’s externally-invokable surface area. > > **Overview** > Adds two new kernel-control JSON-RPC methods to expose remote networking controls: `initRemoteComms` (initializes remote comms with optional relay/listen/retry/queue options) and `registerLocationHints` (registers multiaddr dial hints for a peer). > > Registers both methods in `kernel-control/index.ts`/`rpcHandlers`+`rpcMethodSpecs`, updates the index export test accordingly, and adds unit tests covering parameter validation, option forwarding, and error propagation. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 53aa8e2. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ceebfe5 commit f7b4257

6 files changed

Lines changed: 317 additions & 0 deletions

File tree

packages/ocap-kernel/src/rpc/kernel-control/index.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import {
1111
} from './execute-db-query.ts';
1212
import { getStatusHandler, getStatusSpec } from './get-status.ts';
1313
import { rpcHandlers, rpcMethodSpecs } from './index.ts';
14+
import {
15+
initRemoteCommsHandler,
16+
initRemoteCommsSpec,
17+
} from './init-remote-comms.ts';
1418
import { isRevokedHandler, isRevokedSpec } from './is-revoked.ts';
1519
import { issueOcapURLHandler, issueOcapURLSpec } from './issue-ocap-url.ts';
1620
import {
@@ -20,6 +24,10 @@ import {
2024
import { pingVatHandler, pingVatSpec } from './ping-vat.ts';
2125
import { queueMessageHandler, queueMessageSpec } from './queue-message.ts';
2226
import { redeemOcapURLHandler, redeemOcapURLSpec } from './redeem-ocap-url.ts';
27+
import {
28+
registerLocationHintsHandler,
29+
registerLocationHintsSpec,
30+
} from './register-location-hints.ts';
2331
import { restartVatHandler, restartVatSpec } from './restart-vat.ts';
2432
import { revokeHandler, revokeSpec } from './revoke.ts';
2533
import {
@@ -38,9 +46,11 @@ describe('handlers/index', () => {
3846
clearState: clearStateHandler,
3947
executeDBQuery: executeDBQueryHandler,
4048
getStatus: getStatusHandler,
49+
initRemoteComms: initRemoteCommsHandler,
4150
issueOcapURL: issueOcapURLHandler,
4251
pingVat: pingVatHandler,
4352
redeemOcapURL: redeemOcapURLHandler,
53+
registerLocationHints: registerLocationHintsHandler,
4454
restartVat: restartVatHandler,
4555
revoke: revokeHandler,
4656
isRevoked: isRevokedHandler,
@@ -67,9 +77,11 @@ describe('handlers/index', () => {
6777
clearState: clearStateSpec,
6878
executeDBQuery: executeDBQuerySpec,
6979
getStatus: getStatusSpec,
80+
initRemoteComms: initRemoteCommsSpec,
7081
issueOcapURL: issueOcapURLSpec,
7182
pingVat: pingVatSpec,
7283
redeemOcapURL: redeemOcapURLSpec,
84+
registerLocationHints: registerLocationHintsSpec,
7385
restartVat: restartVatSpec,
7486
revoke: revokeSpec,
7587
isRevoked: isRevokedSpec,

packages/ocap-kernel/src/rpc/kernel-control/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import {
88
executeDBQuerySpec,
99
} from './execute-db-query.ts';
1010
import { getStatusHandler, getStatusSpec } from './get-status.ts';
11+
import {
12+
initRemoteCommsHandler,
13+
initRemoteCommsSpec,
14+
} from './init-remote-comms.ts';
1115
import { isRevokedHandler, isRevokedSpec } from './is-revoked.ts';
1216
import { issueOcapURLHandler, issueOcapURLSpec } from './issue-ocap-url.ts';
1317
import {
@@ -17,6 +21,10 @@ import {
1721
import { pingVatHandler, pingVatSpec } from './ping-vat.ts';
1822
import { queueMessageHandler, queueMessageSpec } from './queue-message.ts';
1923
import { redeemOcapURLHandler, redeemOcapURLSpec } from './redeem-ocap-url.ts';
24+
import {
25+
registerLocationHintsHandler,
26+
registerLocationHintsSpec,
27+
} from './register-location-hints.ts';
2028
import { restartVatHandler, restartVatSpec } from './restart-vat.ts';
2129
import { revokeHandler, revokeSpec } from './revoke.ts';
2230
import {
@@ -36,9 +44,11 @@ export const rpcHandlers = {
3644
clearState: clearStateHandler,
3745
executeDBQuery: executeDBQueryHandler,
3846
getStatus: getStatusHandler,
47+
initRemoteComms: initRemoteCommsHandler,
3948
issueOcapURL: issueOcapURLHandler,
4049
pingVat: pingVatHandler,
4150
redeemOcapURL: redeemOcapURLHandler,
51+
registerLocationHints: registerLocationHintsHandler,
4252
revoke: revokeHandler,
4353
isRevoked: isRevokedHandler,
4454
restartVat: restartVatHandler,
@@ -52,9 +62,11 @@ export const rpcHandlers = {
5262
clearState: typeof clearStateHandler;
5363
executeDBQuery: typeof executeDBQueryHandler;
5464
getStatus: typeof getStatusHandler;
65+
initRemoteComms: typeof initRemoteCommsHandler;
5566
issueOcapURL: typeof issueOcapURLHandler;
5667
pingVat: typeof pingVatHandler;
5768
redeemOcapURL: typeof redeemOcapURLHandler;
69+
registerLocationHints: typeof registerLocationHintsHandler;
5870
revoke: typeof revokeHandler;
5971
isRevoked: typeof isRevokedHandler;
6072
restartVat: typeof restartVatHandler;
@@ -73,9 +85,11 @@ export const rpcMethodSpecs = {
7385
clearState: clearStateSpec,
7486
executeDBQuery: executeDBQuerySpec,
7587
getStatus: getStatusSpec,
88+
initRemoteComms: initRemoteCommsSpec,
7689
issueOcapURL: issueOcapURLSpec,
7790
pingVat: pingVatSpec,
7891
redeemOcapURL: redeemOcapURLSpec,
92+
registerLocationHints: registerLocationHintsSpec,
7993
revoke: revokeSpec,
8094
isRevoked: isRevokedSpec,
8195
restartVat: restartVatSpec,
@@ -89,9 +103,11 @@ export const rpcMethodSpecs = {
89103
clearState: typeof clearStateSpec;
90104
executeDBQuery: typeof executeDBQuerySpec;
91105
getStatus: typeof getStatusSpec;
106+
initRemoteComms: typeof initRemoteCommsSpec;
92107
issueOcapURL: typeof issueOcapURLSpec;
93108
pingVat: typeof pingVatSpec;
94109
redeemOcapURL: typeof redeemOcapURLSpec;
110+
registerLocationHints: typeof registerLocationHintsSpec;
95111
revoke: typeof revokeSpec;
96112
isRevoked: typeof isRevokedSpec;
97113
restartVat: typeof restartVatSpec;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { is } from '@metamask/superstruct';
2+
import { describe, it, expect, vi, beforeEach } from 'vitest';
3+
4+
import {
5+
initRemoteCommsHandler,
6+
initRemoteCommsSpec,
7+
} from './init-remote-comms.ts';
8+
import type { Kernel } from '../../Kernel.ts';
9+
10+
describe('initRemoteCommsHandler', () => {
11+
let mockKernel: Kernel;
12+
13+
beforeEach(() => {
14+
mockKernel = {
15+
initRemoteComms: vi.fn().mockResolvedValue(undefined),
16+
} as unknown as Kernel;
17+
});
18+
19+
it('calls initRemoteComms with empty options and returns null', async () => {
20+
const result = await initRemoteCommsHandler.implementation(
21+
{ kernel: mockKernel },
22+
{},
23+
);
24+
25+
expect(mockKernel.initRemoteComms).toHaveBeenCalledWith({});
26+
expect(result).toBeNull();
27+
});
28+
29+
it('passes relays option', async () => {
30+
const result = await initRemoteCommsHandler.implementation(
31+
{ kernel: mockKernel },
32+
{ relays: ['/dns4/relay.example.com/tcp/443/wss/p2p/QmRelay'] },
33+
);
34+
35+
expect(mockKernel.initRemoteComms).toHaveBeenCalledWith({
36+
relays: ['/dns4/relay.example.com/tcp/443/wss/p2p/QmRelay'],
37+
});
38+
expect(result).toBeNull();
39+
});
40+
41+
it('passes directListenAddresses option', async () => {
42+
const result = await initRemoteCommsHandler.implementation(
43+
{ kernel: mockKernel },
44+
{ directListenAddresses: ['/ip4/0.0.0.0/udp/0/quic-v1'] },
45+
);
46+
47+
expect(mockKernel.initRemoteComms).toHaveBeenCalledWith({
48+
directListenAddresses: ['/ip4/0.0.0.0/udp/0/quic-v1'],
49+
});
50+
expect(result).toBeNull();
51+
});
52+
53+
it('passes all options', async () => {
54+
const params = {
55+
relays: ['relay1'],
56+
directListenAddresses: ['/ip4/0.0.0.0/udp/0/quic-v1'],
57+
maxRetryAttempts: 5,
58+
maxQueue: 100,
59+
};
60+
61+
const result = await initRemoteCommsHandler.implementation(
62+
{ kernel: mockKernel },
63+
params,
64+
);
65+
66+
expect(mockKernel.initRemoteComms).toHaveBeenCalledWith({
67+
relays: ['relay1'],
68+
directListenAddresses: ['/ip4/0.0.0.0/udp/0/quic-v1'],
69+
maxRetryAttempts: 5,
70+
maxQueue: 100,
71+
});
72+
expect(result).toBeNull();
73+
});
74+
75+
it('propagates errors from kernel.initRemoteComms', async () => {
76+
const error = new Error('Remote comms initialization failed');
77+
vi.mocked(mockKernel.initRemoteComms).mockRejectedValueOnce(error);
78+
79+
await expect(
80+
initRemoteCommsHandler.implementation({ kernel: mockKernel }, {}),
81+
).rejects.toThrow(error);
82+
});
83+
84+
describe('params validation', () => {
85+
it('accepts valid params', () => {
86+
expect(is({}, initRemoteCommsSpec.params)).toBe(true);
87+
expect(
88+
is({ maxRetryAttempts: 0, maxQueue: 0 }, initRemoteCommsSpec.params),
89+
).toBe(true);
90+
expect(
91+
is({ maxRetryAttempts: 5, maxQueue: 100 }, initRemoteCommsSpec.params),
92+
).toBe(true);
93+
});
94+
95+
it.each([-1, -100, 1.5, 3.14, -0.5])(
96+
'rejects invalid maxRetryAttempts value: %s',
97+
(value) => {
98+
expect(
99+
is({ maxRetryAttempts: value }, initRemoteCommsSpec.params),
100+
).toBe(false);
101+
},
102+
);
103+
104+
it.each([-1, -100, 1.5, 3.14, -0.5])(
105+
'rejects invalid maxQueue value: %s',
106+
(value) => {
107+
expect(is({ maxQueue: value }, initRemoteCommsSpec.params)).toBe(false);
108+
},
109+
);
110+
});
111+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { MethodSpec, Handler } from '@metamask/kernel-rpc-methods';
2+
import {
3+
object,
4+
literal,
5+
optional,
6+
array,
7+
string,
8+
integer,
9+
min,
10+
} from '@metamask/superstruct';
11+
import type { Struct } from '@metamask/superstruct';
12+
import type { Infer } from '@metamask/superstruct';
13+
14+
import type { Kernel } from '../../Kernel.ts';
15+
import type { RemoteCommsOptions } from '../../remotes/types.ts';
16+
17+
const initRemoteCommsParamsStruct = object({
18+
relays: optional(array(string())),
19+
directListenAddresses: optional(array(string())),
20+
maxRetryAttempts: optional(min(integer(), 0)),
21+
maxQueue: optional(min(integer(), 0)),
22+
});
23+
24+
// Superstruct's `optional()` infers `T | undefined` for each field, but
25+
// `JsonRpcParams` (from `@metamask/utils`) does not include `undefined` in its
26+
// `Json` union. This mapped type strips `| undefined` while preserving
27+
// optionality, keeping the type derived from the struct (single source of truth).
28+
type InitRemoteCommsParams = {
29+
[K in keyof Infer<typeof initRemoteCommsParamsStruct>]: Exclude<
30+
Infer<typeof initRemoteCommsParamsStruct>[K],
31+
undefined
32+
>;
33+
};
34+
35+
export const initRemoteCommsSpec: MethodSpec<
36+
'initRemoteComms',
37+
InitRemoteCommsParams,
38+
null
39+
> = {
40+
method: 'initRemoteComms',
41+
// Safe: the struct validates JSON-RPC params which never contain `undefined`.
42+
params: initRemoteCommsParamsStruct as Struct<InitRemoteCommsParams>,
43+
result: literal(null),
44+
};
45+
46+
export type InitRemoteCommsHooks = {
47+
kernel: Pick<Kernel, 'initRemoteComms'>;
48+
};
49+
50+
export const initRemoteCommsHandler: Handler<
51+
'initRemoteComms',
52+
InitRemoteCommsParams,
53+
Promise<null>,
54+
InitRemoteCommsHooks
55+
> = {
56+
...initRemoteCommsSpec,
57+
hooks: { kernel: true },
58+
implementation: async ({ kernel }, params): Promise<null> => {
59+
const options: RemoteCommsOptions = {};
60+
if (params.relays !== undefined) {
61+
options.relays = params.relays;
62+
}
63+
if (params.directListenAddresses !== undefined) {
64+
options.directListenAddresses = params.directListenAddresses;
65+
}
66+
if (params.maxRetryAttempts !== undefined) {
67+
options.maxRetryAttempts = params.maxRetryAttempts;
68+
}
69+
if (params.maxQueue !== undefined) {
70+
options.maxQueue = params.maxQueue;
71+
}
72+
await kernel.initRemoteComms(options);
73+
return null;
74+
},
75+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
3+
import { registerLocationHintsHandler } from './register-location-hints.ts';
4+
import type { Kernel } from '../../Kernel.ts';
5+
6+
describe('registerLocationHintsHandler', () => {
7+
let mockKernel: Kernel;
8+
9+
beforeEach(() => {
10+
mockKernel = {
11+
registerLocationHints: vi.fn().mockResolvedValue(undefined),
12+
} as unknown as Kernel;
13+
});
14+
15+
it('calls registerLocationHints and returns null', async () => {
16+
const params = {
17+
peerId: 'peer-123',
18+
hints: ['/ip4/192.168.1.1/udp/4001/quic-v1'],
19+
};
20+
21+
const result = await registerLocationHintsHandler.implementation(
22+
{ kernel: mockKernel },
23+
params,
24+
);
25+
26+
expect(mockKernel.registerLocationHints).toHaveBeenCalledWith('peer-123', [
27+
'/ip4/192.168.1.1/udp/4001/quic-v1',
28+
]);
29+
expect(result).toBeNull();
30+
});
31+
32+
it('passes multiple hints', async () => {
33+
const params = {
34+
peerId: 'peer-456',
35+
hints: ['/ip4/10.0.0.1/udp/4001/quic-v1', '/ip4/10.0.0.1/tcp/4001'],
36+
};
37+
38+
const result = await registerLocationHintsHandler.implementation(
39+
{ kernel: mockKernel },
40+
params,
41+
);
42+
43+
expect(mockKernel.registerLocationHints).toHaveBeenCalledWith('peer-456', [
44+
'/ip4/10.0.0.1/udp/4001/quic-v1',
45+
'/ip4/10.0.0.1/tcp/4001',
46+
]);
47+
expect(result).toBeNull();
48+
});
49+
50+
it('propagates errors from kernel.registerLocationHints', async () => {
51+
const error = new Error('Failed to register hints');
52+
vi.mocked(mockKernel.registerLocationHints).mockRejectedValueOnce(error);
53+
54+
await expect(
55+
registerLocationHintsHandler.implementation(
56+
{ kernel: mockKernel },
57+
{ peerId: 'peer-123', hints: ['hint1'] },
58+
),
59+
).rejects.toThrow(error);
60+
});
61+
});

0 commit comments

Comments
 (0)