From 697b61b8aa6a95e8a671bd828bf6f2518406a280 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 24 Aug 2026 18:09:20 +0200 Subject: [PATCH 1/2] test(kernel): add cross-kernel revocation tests (#995) Add a two-kernel revocation test to both the fast integration harness and the full e2e suite, covering the five-step scenario from issue #995: export an object via ocap URL, assert the remote peer can invoke it, revoke it through the public kernel.revoke API, assert the peer's next invocation fails with OBJECT_REVOKED, and assert an unrelated bystander object is unaffected. Co-Authored-By: Claude Sonnet 4.6 --- .../test/e2e/remote-comms.test.ts | 64 ++++++++++++++++++ packages/kernel-test/src/remote-comms.test.ts | 67 +++++++++++++++++++ .../src/vats/revocation-target-vat.ts | 58 ++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 packages/kernel-test/src/vats/revocation-target-vat.ts diff --git a/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts b/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts index 552339a2ac..8be69cff5c 100644 --- a/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts +++ b/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts @@ -1300,6 +1300,70 @@ describe.sequential('Remote Communications E2E', () => { ); }); + describe('Revocation', () => { + it( + 'blocks remote invocation after revocation and leaves other objects unaffected', + async () => { + await kernel1.initRemoteComms({ + relays: testRelays, + ...testBackoffOptions, + }); + await kernel2.initRemoteComms({ + relays: testRelays, + ...testBackoffOptions, + }); + + // step 1: kernel A exports a revocable target and an unrelated bystander + const targetURL = await launchVatAndGetURL( + kernel1, + makeRemoteVatConfig('Target'), + ); + const bystanderURL = await launchVatAndGetURL( + kernel1, + makeRemoteVatConfig('Bystander'), + ); + + // kernel B: a vat that can invoke remote objects via URL + await launchVatAndGetURL(kernel2, makeRemoteVatConfig('Sender')); + const senderRef = getVatRootRef(kernel2, kernelStore2, 'Sender'); + + // step 2: B can invoke the target before revocation + const beforeResult = await sendRemoteMessage( + kernel2, + senderRef, + targetURL, + 'ping', + [], + ); + expect(beforeResult).toBe('pong from Target'); + + // step 3: revoke the target on A through the public API + const targetKRef = getVatRootRef(kernel1, kernelStore1, 'Target'); + kernel1.revoke(targetKRef); + + // step 4: B's next invocation fails + await expect( + kernel2.queueMessage(senderRef, 'sendRemoteMessage', [ + targetURL, + 'ping', + [], + ]), + ).rejects.toThrow('OBJECT_REVOKED'); + + // step 5: the bystander and B's other references are unaffected + const bystanderResult = await sendRemoteMessage( + kernel2, + senderRef, + bystanderURL, + 'ping', + [], + ); + expect(bystanderResult).toBe('pong from Bystander'); + }, + NETWORK_TIMEOUT, + ); + }); + describe('Distributed Garbage Collection', () => { it( 'creates remote endpoint with clist entries after cross-kernel message', diff --git a/packages/kernel-test/src/remote-comms.test.ts b/packages/kernel-test/src/remote-comms.test.ts index 00b69bfaae..92343976a1 100644 --- a/packages/kernel-test/src/remote-comms.test.ts +++ b/packages/kernel-test/src/remote-comms.test.ts @@ -378,6 +378,73 @@ describe('Remote Communications (Integration Tests)', () => { expect(response).toContain('says hello back to'); }); + it('revokes a cross-kernel object held by a remote peer', async () => { + // step 1: kernel A exports a revocable target and an unrelated bystander + const { targetURL, bystanderURL } = (await runTestVats(kernel1, { + bootstrap: 'target', + forceReset: true, + services: ['ocapURLIssuerService'], + vats: { + target: { + bundleSpec: getBundleSpec('revocation-target-vat'), + parameters: { name: 'Target' }, + }, + }, + })) as { targetURL: string; bystanderURL: string }; + + // Obtain the kernel-level KRef so we can later revoke it via the public API + const targetRootRef = kernelStore1.getRootObject('v1') as KRef; + const targetCapdata = await kernel1.queueMessage( + targetRootRef, + 'getTarget', + [], + ); + const targetKRef = targetCapdata.slots[0] as KRef; + + // kernel B: set up a vat that can invoke objects via ocap URL redemption + const kernelStore2 = makeKernelStore(kernelDatabase2); + await runTestVats(kernel2, { + bootstrap: 'sender', + forceReset: true, + services: ['ocapURLRedemptionService'], + vats: { + sender: { + bundleSpec: getBundleSpec('remote-sender-vat'), + parameters: { name: 'Sender' }, + }, + }, + }); + const senderRootRef = kernelStore2.getRootObject('v1') as KRef; + + // step 2: B can invoke the target before revocation + const beforeResult = await kernel2.queueMessage( + senderRootRef, + 'sendMessage', + [targetURL, 'ping', []], + ); + expect(kunser(beforeResult)).toContain('pong from Target target'); + + // step 3: revoke the target on A through the public API + kernel1.revoke(targetKRef); + + // step 4: B's next invocation fails + await expect( + kernel2.queueMessage(senderRootRef, 'sendMessage', [ + targetURL, + 'ping', + [], + ]), + ).rejects.toThrow('OBJECT_REVOKED'); + + // step 5: the bystander object, and B's other references, are unaffected + const bystanderResult = await kernel2.queueMessage( + senderRootRef, + 'sendMessage', + [bystanderURL, 'ping', []], + ); + expect(kunser(bystanderResult)).toContain('pong from Target bystander'); + }); + it('remote relationships should survive kernel restart', async () => { // This test needs file-based databases for persistence across stop/restart. // Stop the beforeEach kernels and replace them with file-backed ones. diff --git a/packages/kernel-test/src/vats/revocation-target-vat.ts b/packages/kernel-test/src/vats/revocation-target-vat.ts new file mode 100644 index 0000000000..8462b4231f --- /dev/null +++ b/packages/kernel-test/src/vats/revocation-target-vat.ts @@ -0,0 +1,58 @@ +import { E } from '@endo/eventual-send'; +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; + +import type { TestPowers } from '../test-powers.ts'; + +/** + * Build function for a vat that exports two objects — a revocable target and an + * unrelated bystander — so cross-kernel revocation tests can check both sides. + * + * @param vatPowers - Special powers granted to this vat. + * @param vatPowers.logger - The logger for the vat. + * @param parameters - Initialization parameters from the vat's config object. + * @param parameters.name - The name of the vat. + * @param _baggage - Root of vat's persistent state (not used here). + * @returns The root object for the new vat. + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildRootObject( + { logger }: TestPowers, + parameters: { name?: string } = {}, + _baggage: unknown = null, +) { + const name = parameters?.name ?? 'RevocationTarget'; + logger.log(`buildRootObject "${name}"`); + + const target = makeDefaultExo('target', { + ping() { + return `pong from ${name} target`; + }, + }); + + const bystander = makeDefaultExo('bystander', { + ping() { + return `pong from ${name} bystander`; + }, + }); + + return makeDefaultExo('root', { + async bootstrap( + _vats: unknown, + services: { ocapURLIssuerService?: unknown }, + ) { + logger.log(`vat ${name} is bootstrap`); + const issuerService = services.ocapURLIssuerService; + const targetURL = await E(issuerService).issue(target); + const bystanderURL = await E(issuerService).issue(bystander); + return { targetURL, bystanderURL }; + }, + + getTarget() { + return target; + }, + + getBystander() { + return bystander; + }, + }); +} From 1ef30b6703f15c41cce713b7a7c1e69fb0132050 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 24 Aug 2026 18:15:33 +0200 Subject: [PATCH 2/2] fix(kernel-test): harden bootstrap return value in revocation-target-vat Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-test/src/vats/revocation-target-vat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kernel-test/src/vats/revocation-target-vat.ts b/packages/kernel-test/src/vats/revocation-target-vat.ts index 8462b4231f..ee9ed08226 100644 --- a/packages/kernel-test/src/vats/revocation-target-vat.ts +++ b/packages/kernel-test/src/vats/revocation-target-vat.ts @@ -44,7 +44,7 @@ export function buildRootObject( const issuerService = services.ocapURLIssuerService; const targetURL = await E(issuerService).issue(target); const bystanderURL = await E(issuerService).issue(bystander); - return { targetURL, bystanderURL }; + return harden({ targetURL, bystanderURL }); }, getTarget() {