diff --git a/packages/kernel-test/src/ocap-url.test.ts b/packages/kernel-test/src/ocap-url.test.ts new file mode 100644 index 000000000..b31540a2e --- /dev/null +++ b/packages/kernel-test/src/ocap-url.test.ts @@ -0,0 +1,38 @@ +import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; +import { waitUntilQuiescent } from '@metamask/kernel-utils'; +import { describe, expect, it } from 'vitest'; + +import { + extractTestLogs, + getBundleSpec, + makeKernel, + makeTestLogger, +} from './utils.ts'; + +describe('ocap-url', () => { + it('user-code can make an ocap url', async () => { + const { logger, entries } = makeTestLogger(); + const database = await makeSQLKernelDatabase({}); + const kernel = await makeKernel(database, true, logger); + const vatIds = ['v1']; + const vat = await kernel.launchSubcluster({ + bootstrap: 'alice', + vats: { + alice: { + bundleSpec: getBundleSpec('ocap-url'), + parameters: {}, + }, + }, + }); + expect(vat).toBeDefined(); + const vats = kernel.getVatIds(); + expect(vats).toStrictEqual(vatIds); + + await waitUntilQuiescent(); + const vatLogs = vatIds.map((vatId) => extractTestLogs(entries, vatId)); + expect(vatLogs).toStrictEqual([ + // This is a placeholder for the actual ocap url. + [expect.stringContaining(`Alice's ocap url: ocap://o+`)], + ]); + }); +}); diff --git a/packages/kernel-test/src/revocation.test.ts b/packages/kernel-test/src/revocation.test.ts new file mode 100644 index 000000000..876c3863a --- /dev/null +++ b/packages/kernel-test/src/revocation.test.ts @@ -0,0 +1,45 @@ +import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; +import { waitUntilQuiescent } from '@metamask/kernel-utils'; +import { describe, expect, it } from 'vitest'; + +import { + extractTestLogs, + getBundleSpec, + makeKernel, + makeTestLogger, +} from './utils.ts'; + +describe('revocation', () => { + it('user-code revoker can call kernel syscall', async () => { + const { logger, entries } = makeTestLogger(); + const database = await makeSQLKernelDatabase({}); + const kernel = await makeKernel(database, true, logger); + const vatIds = ['v1', 'v2']; + const vat = await kernel.launchSubcluster({ + bootstrap: 'main', + vats: { + main: { + bundleSpec: getBundleSpec('revocation-bootstrap'), + parameters: {}, + }, + provider: { + bundleSpec: getBundleSpec('revocation-provider'), + parameters: {}, + }, + }, + }); + expect(vat).toBeDefined(); + const vats = kernel.getVatIds(); + expect(vats).toStrictEqual(vatIds); + + await waitUntilQuiescent(); + expect(kernel.isRevoked('ko1')).toBe(false); + expect(kernel.isRevoked('ko2')).toBe(false); + expect(kernel.isRevoked('ko3')).toBe(true); + const vatLogs = vatIds.map((vatId) => extractTestLogs(entries, vatId)); + expect(vatLogs).toStrictEqual([ + ['foo', 'bar', 'revoked object', 'done'], + ['slam:0'], + ]); + }); +}); diff --git a/packages/kernel-test/src/vats/ocap-url.js b/packages/kernel-test/src/vats/ocap-url.js new file mode 100644 index 000000000..90f17c631 --- /dev/null +++ b/packages/kernel-test/src/vats/ocap-url.js @@ -0,0 +1,31 @@ +import { E } from '@endo/eventual-send'; +import { Far } from '@endo/marshal'; + +/** + * Build function for vats that will run various tests. + * + * @param {object} vatPowers - Special powers granted to this vat. + * @param {object} vatPowers.logger - The logger for this vat. + * @returns {*} The root object for the new vat. + */ +export function buildRootObject({ logger }) { + const { log } = logger.subLogger({ tags: ['test'] }); + let contact; + return Far('root', { + async bootstrap({ alice }) { + contact = Far('contact', { + // An external actor can send a message to Alice by following an + // ocap url like "ocap://.../contact?whoAmI=Bob&message=Hello". + contact: (whoAmI, message) => E(alice).contact(whoAmI, message), + }); + const ocapUrl = E(alice).makeContactUrl(); + log(`Alice's ocap url: ${await ocapUrl}`); + }, + // `makeOcapUrl` is an endowment available in global scope. + // eslint-disable-next-line no-undef + makeContactUrl: () => makeOcapUrl(contact), + async contact(sender = 'unknown', message = 'hello') { + log(`contact from ${sender}: ${message}`); + }, + }); +} diff --git a/packages/kernel-test/src/vats/revocation-bootstrap.js b/packages/kernel-test/src/vats/revocation-bootstrap.js new file mode 100644 index 000000000..7f7c38d04 --- /dev/null +++ b/packages/kernel-test/src/vats/revocation-bootstrap.js @@ -0,0 +1,24 @@ +import { E } from '@endo/eventual-send'; +import { Far } from '@endo/marshal'; + +/** + * Build function for vats that will run various tests. + * + * @param {object} vatPowers - Special powers granted to this vat. + * @returns {*} The root object for the new vat. + */ +export function buildRootObject(vatPowers) { + const { log } = vatPowers.logger.subLogger({ tags: ['test'] }); + return Far('root', { + async bootstrap({ provider }) { + const [gate, revoker] = await E(provider).requestPlatform(); + await E(gate).foo().then(log); + await E(gate).bar().then(log); + await E(revoker).slam(); + // XXX Methods called on a revoked object should reject, but currently + // resolve with a 'revoked object' string. + await E(gate).foo().catch(log); + log('done'); + }, + }); +} diff --git a/packages/kernel-test/src/vats/revocation-provider.js b/packages/kernel-test/src/vats/revocation-provider.js new file mode 100644 index 000000000..05e93164a --- /dev/null +++ b/packages/kernel-test/src/vats/revocation-provider.js @@ -0,0 +1,32 @@ +import { Far } from '@endo/marshal'; + +/** + * Build function for vats that will run various tests. + * + * @param {object} vatPowers - The vat powers. + * @param {object} vatPowers.logger - The logger for this vat. + * @returns {*} The root object for the new vat. + */ +export function buildRootObject({ logger }) { + const { log } = logger.subLogger({ tags: ['test'] }); + const platform = { foo: () => `foo`, bar: () => `bar` }; + let revokerCount = 0; + const revocable = (obj) => { + const gate = Far('gate', { ...obj }); + // XXX makeRevoker is defined as an endowment (in VatSupervisor.ts), but + // the linter has no way to know that it is defined. + // eslint-disable-next-line no-undef + const revoker = makeRevoker(gate); + const id = revokerCount; + revokerCount += 1; + const slam = () => { + revoker(); + log(`slam:${id}`); + }; + return [gate, Far(`slam:${id}`, { slam })]; + }; + return Far('root', { + requestPlatform: () => revocable(platform), + revokerCount: () => revokerCount, + }); +} diff --git a/packages/ocap-kernel/src/KernelQueue.ts b/packages/ocap-kernel/src/KernelQueue.ts index 280c2a4d6..82554edba 100644 --- a/packages/ocap-kernel/src/KernelQueue.ts +++ b/packages/ocap-kernel/src/KernelQueue.ts @@ -225,7 +225,7 @@ export class KernelQueue { if (state !== 'unresolved') { Fail`${kpid} was already resolved`; } - if (decider !== vatId) { + if (vatId && decider !== vatId) { const why = decider ? `its decider is ${decider}` : `it has no decider`; Fail`${vatId} not permitted to resolve ${kpid} because ${why}`; } diff --git a/packages/ocap-kernel/src/VatSupervisor.ts b/packages/ocap-kernel/src/VatSupervisor.ts index 5d4cbdff1..53e66cc2f 100644 --- a/packages/ocap-kernel/src/VatSupervisor.ts +++ b/packages/ocap-kernel/src/VatSupervisor.ts @@ -1,7 +1,6 @@ import { makeLiveSlots as localMakeLiveSlots } from '@agoric/swingset-liveslots'; import type { VatDeliveryObject, - VatSyscallObject, VatSyscallResult, } from '@agoric/swingset-liveslots'; import { importBundle } from '@endo/import-bundle'; @@ -17,13 +16,19 @@ import { serializeError } from '@metamask/rpc-errors'; import type { DuplexStream } from '@metamask/streams'; import { isJsonRpcRequest, isJsonRpcResponse } from '@metamask/utils'; +import { makeEndowments } from './endowments/index.ts'; import { vatSyscallMethodSpecs, vatHandlers } from './rpc/index.ts'; import { makeGCAndFinalize } from './services/gc-finalize.ts'; import { makeDummyMeterControl } from './services/meter-control.ts'; import { makeSupervisorSyscall } from './services/syscall.ts'; import type { DispatchFn, MakeLiveSlotsFn, GCTools } from './services/types.ts'; import { makeVatKVStore } from './store/vat-kv-store.ts'; -import type { VatConfig, VatDeliveryResult, VatId } from './types.ts'; +import type { + VatConfig, + VatDeliveryResult, + VatId, + VatSyscallObject, +} from './types.ts'; import { isVatConfig, coerceVatSyscallObject } from './types.ts'; const makeLiveSlots: MakeLiveSlotsFn = localMakeLiveSlots; @@ -254,6 +259,7 @@ export class VatSupervisor { const workerEndowments = { console: this.#logger.subLogger({ tags: ['console'] }), assert: globalThis.assert, + ...makeEndowments(syscall, gcTools, this.id), }; const { bundleSpec, parameters } = vatConfig; diff --git a/packages/ocap-kernel/src/VatSyscall.ts b/packages/ocap-kernel/src/VatSyscall.ts index 2d1565c45..5c9c6bd86 100644 --- a/packages/ocap-kernel/src/VatSyscall.ts +++ b/packages/ocap-kernel/src/VatSyscall.ts @@ -1,7 +1,6 @@ import type { SwingSetCapData, VatOneResolution, - VatSyscallObject, VatSyscallResult, } from '@agoric/swingset-liveslots'; import { Logger } from '@metamask/logger'; @@ -11,7 +10,7 @@ import { makeError } from './services/kernel-marshal.ts'; import type { KernelStore } from './store/index.ts'; import { parseRef } from './store/utils/parse-ref.ts'; import { coerceMessage } from './types.ts'; -import type { Message, VatId, KRef } from './types.ts'; +import type { VatSyscallObject, Message, VatId, KRef } from './types.ts'; type VatSyscallProps = { vatId: VatId; @@ -100,6 +99,23 @@ export class VatSyscall { } } + /** + * Handle a 'revoke' syscall from the vat. + * + * @param krefs - The KRefs of the objects to be revoked. + */ + #handleSyscallRevoke(krefs: KRef[]): void { + for (const kref of krefs) { + const owner = this.#kernelStore.getOwner(kref); + if (owner !== this.vatId) { + throw Error( + `vat ${this.vatId} cannot revoke ${kref} (owned by ${owner})`, + ); + } + this.#kernelStore.revoke(kref); + } + } + /** * Handle a 'dropImports' syscall from the vat. * @@ -186,10 +202,7 @@ export class VatSyscall { return harden(['error', 'vat not found']); } - const kso: VatSyscallObject = this.#kernelStore.translateSyscallVtoK( - this.vatId, - vso, - ); + const kso = this.#kernelStore.translateSyscallVtoK(this.vatId, vso); const [op] = kso; const { vatId } = this; const { log } = console; @@ -226,6 +239,13 @@ export class VatSyscall { this.vatRequestedTermination = { reject: isFailure, info }; break; } + case 'revoke': { + // [KRef[]]; + const [, krefs] = kso; + log(`@@@@ ${vatId} syscall revoke ${JSON.stringify(krefs)}`); + this.#handleSyscallRevoke(krefs); + break; + } case 'dropImports': { // [KRef[]]; const [, refs] = kso; diff --git a/packages/ocap-kernel/src/endowments/context.ts b/packages/ocap-kernel/src/endowments/context.ts new file mode 100644 index 000000000..0a9d70c5d --- /dev/null +++ b/packages/ocap-kernel/src/endowments/context.ts @@ -0,0 +1,50 @@ +import { makeMarshaller } from '@agoric/swingset-liveslots'; +import { Fail } from '@endo/errors'; + +// Used in the docs for a safe use of `toRef`. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { factory as makeRevoker } from './factories/make-revoker.ts'; +import type { EndowmentContext, ToRef, Marshaller } from './types.ts'; +import type { GCTools, Syscall } from '../services/types.ts'; +import type { VatId } from '../types.ts'; + +/** + * Make a function that converts an object to a vref. + * + * **ATTN**: Do not expose the return value of this function to user code. + * + * This is a hack that disrespects liveslots's encapsulation of the marshaller. + * If the user code gets a handle on `toRef` and a capability to send messages + * to the kernel, it can break vat containment by impersonating its supervisor. + * + * It is fine to expose a hardened function which passes an object to `toRef`, + * as long as the vref cannot escape the scope of that function. + * + * @see {@link makeRevoker} for an example of safe use of `toRef`. + * + * @param marshaller - The liveslots marshaller. + * @returns A function that converts an object to a vref. + */ +function makeToRef(marshaller: Marshaller): ToRef { + const toRef: ToRef = (object) => + marshaller.toCapData(object).slots[0] ?? + Fail`cannot make ocap url for object ${object}`; + return harden(toRef); +} + +/** + * Make a context for an endowment. + * + * @param syscall - The syscall object. + * @param gcTools - The gc tools. + * @param vatId - The vat id. + * @returns A context for an endowment. + */ +export function makeEndowmentContext( + syscall: Syscall, + gcTools: GCTools, + vatId: VatId, +): EndowmentContext { + const toRef = makeToRef(makeMarshaller(syscall, gcTools, vatId).m); + return { syscall, gcTools, vatId, toRef }; +} diff --git a/packages/ocap-kernel/src/endowments/factories/index.ts b/packages/ocap-kernel/src/endowments/factories/index.ts new file mode 100644 index 000000000..72f2c6da3 --- /dev/null +++ b/packages/ocap-kernel/src/endowments/factories/index.ts @@ -0,0 +1,18 @@ +import * as makeOcapUrl from './make-ocap-url.ts'; +import * as makeRevoker from './make-revoker.ts'; +import * as scry from './scry.ts'; +import type { EndowmentDefinition } from '../types.ts'; + +const endowmentDefinitions = { + makeOcapUrl, + makeRevoker, + scry, +} as const satisfies Record; + +export type EndowmentName = keyof typeof endowmentDefinitions; + +export type Endowments = { + [K in EndowmentName]: ReturnType<(typeof endowmentDefinitions)[K]['factory']>; +}; + +export default endowmentDefinitions; diff --git a/packages/ocap-kernel/src/endowments/factories/make-ocap-url.ts b/packages/ocap-kernel/src/endowments/factories/make-ocap-url.ts new file mode 100644 index 000000000..0eb4d15f8 --- /dev/null +++ b/packages/ocap-kernel/src/endowments/factories/make-ocap-url.ts @@ -0,0 +1,49 @@ +import { makePromiseKit } from '@endo/promise-kit'; +import { waitUntilQuiescent } from '@metamask/kernel-utils'; + +import type { EndowmentContext } from '../types.ts'; + +/** + * An ocap url. + */ +export type OcapUrl = `ocap://${string}`; + +/** + * A function that makes an ocap url for a given object. + */ +export type MakeOcapUrl = (object: unknown) => Promise; + +/** + * Make a function that makes an ocap url for a given object. Intended to be + * used as a user code endowment. + * + * @param context - The context in which the endowment is created. + * @returns A function that makes an ocap url for a given object. + */ +export function factory(context: EndowmentContext): MakeOcapUrl { + const { toRef } = context; + + /** + * Make an ocap url for a given object. + * + * @param object - The object to make an ocap url for. + * @returns A promise that resolves with the ocap url. + */ + const makeOcapUrl = async (object: unknown): Promise => { + const { promise, resolve } = makePromiseKit(); + // This vpid can be sent to the kernel as the resolution target. + const vpid = toRef(promise); + const vref = toRef(object); + console.log('makeOcapUrl', vpid, vref); + // XXX this is a placeholder for the actual implementation which sends the + // kernel a request to resolve the promise with an ocap url for the given + // vref. + waitUntilQuiescent() + .catch((error) => console.error('wait until quiescent failed:', error)) + .then(() => resolve(`ocap://${vref}`)) + .catch((error) => console.error('resolve failed:', error)); + return promise; + }; + + return harden(makeOcapUrl); +} diff --git a/packages/ocap-kernel/src/endowments/factories/make-revoker.ts b/packages/ocap-kernel/src/endowments/factories/make-revoker.ts new file mode 100644 index 000000000..77a29687a --- /dev/null +++ b/packages/ocap-kernel/src/endowments/factories/make-revoker.ts @@ -0,0 +1,40 @@ +import type { VRef } from '../../types.ts'; +import type { EndowmentContext } from '../types.ts'; + +/** + * A function that revokes a distributed object, making it impossible to call + * any methods on it. + */ +export type Revoker = () => void; + +/** + * A function that makes a revoker for a given object. + */ +export type MakeRevoker = (object: unknown) => Revoker; + +/** + * Make a function that makes a revoker for a given object. Intended to be used + * as a user code endowment. + * + * @param context - The context in which the endowment is created. + * @returns A function that makes a revoker for a given object. + */ +export function factory(context: EndowmentContext): MakeRevoker { + const { syscall, toRef } = context; + const revoke = (vref: VRef): void => { + syscall.revoke([vref]); + }; + /** + * Make a revoker for a given object. A vat can only revoke its own objects, + * so revokable delegation is not possible. After the revoker is called, all + * eventual method calls from importers of the distributed object will fail. + * + * @param object - The object to revoke. + * @returns A function that revokes the object. + */ + function makeRevoker(object: unknown): Revoker { + const ref = toRef(object); + return harden(() => revoke(ref)); + } + return harden(makeRevoker); +} diff --git a/packages/ocap-kernel/src/endowments/factories/scry.ts b/packages/ocap-kernel/src/endowments/factories/scry.ts new file mode 100644 index 000000000..82a7f0771 --- /dev/null +++ b/packages/ocap-kernel/src/endowments/factories/scry.ts @@ -0,0 +1,21 @@ +import type { VRef } from '../../types.ts'; +import type { EndowmentContext } from '../types.ts'; + +/** + * A function that scries a vref. + */ +export type Scry = (vref: VRef) => unknown; + +/** + * Make a function that scries a vref. It logs to the console, which in + * production is a no-op. In development, it can be used to inspect the + * contents of a vref. + * + * @param context - The context in which the endowment is created. + * @returns A function that scries a vref. + */ +export function factory(context: EndowmentContext): Scry { + const { toRef } = context; + const scry: Scry = (object: unknown) => console.log(`scry ${toRef(object)}`); + return harden(scry); +} diff --git a/packages/ocap-kernel/src/endowments/index.ts b/packages/ocap-kernel/src/endowments/index.ts new file mode 100644 index 000000000..d0ba419b4 --- /dev/null +++ b/packages/ocap-kernel/src/endowments/index.ts @@ -0,0 +1,31 @@ +import type { GCTools, Syscall } from '../services/types.ts'; +import type { VatId } from '../types.ts'; +import { makeEndowmentContext } from './context.ts'; +import type { EndowmentName, Endowments } from './factories/index.ts'; +import endowmentDefinitions from './factories/index.ts'; + +const allEndowmentNames = Object.keys(endowmentDefinitions) as EndowmentName[]; + +/** + * Make a set of endowments for a vat. + * + * @param syscall - The syscall object. + * @param gcTools - The gc tools. + * @param vatId - The vat id. + * @param names - The names of the endowments to make. If not provided, all + * endowments are made. XXX The default should be to make *no* endowments. + * @returns A set of endowments for a vat. + */ +export function makeEndowments( + syscall: Syscall, + gcTools: GCTools, + vatId: VatId, + names: EndowmentName[] = allEndowmentNames, +): Endowments { + const context = makeEndowmentContext(syscall, gcTools, vatId); + return Object.fromEntries( + Object.entries(endowmentDefinitions) + .filter(([name]) => names.includes(name as EndowmentName)) + .map(([name, definition]) => [name, definition.factory(context)]), + ) as Endowments; +} diff --git a/packages/ocap-kernel/src/endowments/types.ts b/packages/ocap-kernel/src/endowments/types.ts new file mode 100644 index 000000000..45bc71765 --- /dev/null +++ b/packages/ocap-kernel/src/endowments/types.ts @@ -0,0 +1,25 @@ +import type { makeMarshaller } from '@agoric/swingset-liveslots'; + +import type { GCTools, Syscall } from '../services/types.ts'; +import type { VRef, VatId } from '../types.ts'; + +export type Marshaller = ReturnType['m']; + +/** + * A function that converts an object to a vref. + */ +export type ToRef = (object: unknown) => VRef; + +/** + * The context in which an endowment is created. + */ +export type EndowmentContext = { + syscall: Syscall; + gcTools: GCTools; + vatId: VatId; + toRef: ToRef; +}; + +export type EndowmentDefinition = { + factory: (context: EndowmentContext) => unknown; +}; diff --git a/packages/ocap-kernel/src/rpc/vat-syscall/vat-syscall.ts b/packages/ocap-kernel/src/rpc/vat-syscall/vat-syscall.ts index 873b7d738..049f08713 100644 --- a/packages/ocap-kernel/src/rpc/vat-syscall/vat-syscall.ts +++ b/packages/ocap-kernel/src/rpc/vat-syscall/vat-syscall.ts @@ -22,6 +22,7 @@ const ResolveStruct = tuple([ array(VatOneResolutionStruct), ]); const ExitStruct = tuple([literal('exit'), boolean(), CapDataStruct]); +const RevokeStruct = tuple([literal('revoke'), array(string())]); const DropImportsStruct = tuple([literal('dropImports'), array(string())]); const RetireImportsStruct = tuple([literal('retireImports'), array(string())]); const RetireExportsStruct = tuple([literal('retireExports'), array(string())]); @@ -50,6 +51,7 @@ const VatSyscallParamsStruct = union([ ResolveStruct, ExitStruct, DropImportsStruct, + RevokeStruct, RetireImportsStruct, RetireExportsStruct, AbandonExportsStruct, diff --git a/packages/ocap-kernel/src/services/syscall.test.ts b/packages/ocap-kernel/src/services/syscall.test.ts index 851952c45..f86de682a 100644 --- a/packages/ocap-kernel/src/services/syscall.test.ts +++ b/packages/ocap-kernel/src/services/syscall.test.ts @@ -54,6 +54,7 @@ describe('syscall', () => { expect(syscall).toHaveProperty('resolve'); expect(syscall).toHaveProperty('exit'); expect(syscall).toHaveProperty('dropImports'); + expect(syscall).toHaveProperty('revoke'); expect(syscall).toHaveProperty('retireImports'); expect(syscall).toHaveProperty('retireExports'); expect(syscall).toHaveProperty('abandonExports'); @@ -129,6 +130,20 @@ describe('syscall', () => { ]); }); + it('handles revoke syscall', () => { + const supervisor = createMockSupervisor(); + const kv = createMockKVStore(); + const syscall = makeSupervisorSyscall(supervisor, kv); + + const vrefs = ['ko1', 'ko2']; + syscall.revoke(vrefs); + + expect(supervisor.executeSyscall).toHaveBeenCalledWith([ + 'revoke', + vrefs, + ]); + }); + it('handles dropImports syscall', () => { const supervisor = createMockSupervisor(); const kv = createMockKVStore(); diff --git a/packages/ocap-kernel/src/services/syscall.ts b/packages/ocap-kernel/src/services/syscall.ts index 7b7ae8141..a493f4a3a 100644 --- a/packages/ocap-kernel/src/services/syscall.ts +++ b/packages/ocap-kernel/src/services/syscall.ts @@ -2,14 +2,11 @@ import { insistVatSyscallObject, insistVatSyscallResult, } from '@agoric/swingset-liveslots'; -import type { - VatSyscallObject, - VatOneResolution, -} from '@agoric/swingset-liveslots'; +import type { VatOneResolution } from '@agoric/swingset-liveslots'; import type { CapData } from '@endo/marshal'; import type { KVStore } from '@metamask/kernel-store'; -import type { Syscall, SyscallResult } from './types.ts'; +import type { Syscall, SyscallResult, VatSyscallObject } from './types.ts'; import type { VatSupervisor } from '../VatSupervisor.ts'; /** @@ -37,7 +34,9 @@ function makeSupervisorSyscall( * @returns the result from performing the syscall. */ function doSyscall(vso: VatSyscallObject): SyscallResult { - insistVatSyscallObject(vso); + vso[0] === 'revoke' + ? assert(Array.isArray(vso[1])) + : insistVatSyscallObject(vso); let syscallResult; try { syscallResult = supervisor.executeSyscall(vso); @@ -72,6 +71,7 @@ function makeSupervisorSyscall( doSyscall(['resolve', resolutions]), exit: (isFailure: boolean, info: CapData) => doSyscall(['exit', isFailure, info]), + revoke: (vrefs: string[]) => doSyscall(['revoke', vrefs]), dropImports: (vrefs: string[]) => doSyscall(['dropImports', vrefs]), retireImports: (vrefs: string[]) => doSyscall(['retireImports', vrefs]), retireExports: (vrefs: string[]) => doSyscall(['retireExports', vrefs]), diff --git a/packages/ocap-kernel/src/services/types.ts b/packages/ocap-kernel/src/services/types.ts index cf5c89ded..37f1ede7c 100644 --- a/packages/ocap-kernel/src/services/types.ts +++ b/packages/ocap-kernel/src/services/types.ts @@ -4,6 +4,7 @@ import type { SwingSetCapData, LiveSlotsOptions, MeterControl, + VatSyscallObject as LiveslotsVatSyscallObject, } from '@agoric/swingset-liveslots'; import type { CapData } from '@endo/marshal'; @@ -13,6 +14,10 @@ export type LiveSlots = { dispatch: DispatchFn; }; +type VatSyscallRevoke = ['revoke', string[]]; + +export type VatSyscallObject = LiveslotsVatSyscallObject | VatSyscallRevoke; + export type Syscall = { send: ( target: string, @@ -22,6 +27,7 @@ export type Syscall = { subscribe: (vpid: string) => SyscallResult; resolve: (resolutions: VatOneResolution[]) => SyscallResult; exit: (isFailure: boolean, info: CapData) => SyscallResult; + revoke: (vrefs: string[]) => SyscallResult; dropImports: (vrefs: string[]) => SyscallResult; retireImports: (vrefs: string[]) => SyscallResult; retireExports: (vrefs: string[]) => SyscallResult; diff --git a/packages/ocap-kernel/src/store/methods/translators.ts b/packages/ocap-kernel/src/store/methods/translators.ts index f8f764b1c..584a5a39d 100644 --- a/packages/ocap-kernel/src/store/methods/translators.ts +++ b/packages/ocap-kernel/src/store/methods/translators.ts @@ -1,11 +1,14 @@ -import type { - VatOneResolution, - VatSyscallObject, -} from '@agoric/swingset-liveslots'; +import type { VatOneResolution } from '@agoric/swingset-liveslots'; import type { CapData } from '@endo/marshal'; import { coerceMessage } from '../../types.ts'; -import type { Message, VatId, KRef, VRef } from '../../types.ts'; +import type { + Message, + VatId, + KRef, + VRef, + VatSyscallObject, +} from '../../types.ts'; import type { StoreContext } from '../types.ts'; import { getCListMethods } from './clist.ts'; import { getVatMethods } from './vat.ts'; @@ -200,6 +203,7 @@ export function getTranslators(ctx: StoreContext) { ]; break; } + case 'revoke': case 'dropImports': case 'retireImports': case 'retireExports': diff --git a/packages/ocap-kernel/src/types.ts b/packages/ocap-kernel/src/types.ts index 55f28dbec..c00f7261f 100644 --- a/packages/ocap-kernel/src/types.ts +++ b/packages/ocap-kernel/src/types.ts @@ -1,7 +1,6 @@ import type { SwingSetCapData, Message as SwingsetMessage, - VatSyscallObject, VatSyscallSend, } from '@agoric/swingset-liveslots'; import type { CapData } from '@endo/marshal'; @@ -26,8 +25,11 @@ import type { Infer } from '@metamask/superstruct'; import type { Json } from '@metamask/utils'; import { UnsafeJsonStruct } from '@metamask/utils'; +import type { VatSyscallObject } from './services/types.ts'; import { Fail } from './utils/assert.ts'; +export type { VatSyscallObject }; + export type VatId = string; export type RemoteId = string; export type EndpointId = VatId | RemoteId; diff --git a/vitest.config.ts b/vitest.config.ts index 487dc15bf..72b31940f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -140,10 +140,10 @@ export default defineConfig({ lines: 73.58, }, 'packages/ocap-kernel/**': { - statements: 92.52, - functions: 95.27, - branches: 82.82, - lines: 92.49, + statements: 91.77, + functions: 92.67, + branches: 82.65, + lines: 91.85, }, 'packages/streams/**': { statements: 100,