diff --git a/packages/ocap-kernel/CHANGELOG.md b/packages/ocap-kernel/CHANGELOG.md index c1e67d864..5803ed883 100644 --- a/packages/ocap-kernel/CHANGELOG.md +++ b/packages/ocap-kernel/CHANGELOG.md @@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Deserialize CapData rejections in `Kernel.queueMessage` so vat errors surface as plain `Error` objects to all callers ([#928](https://github.com/MetaMask/ocap-kernel/pull/928)) - Detect peer restart across receiver state loss so the receiving kernel no longer silently drops a restarted peer's `seq=1` messages ([#948](https://github.com/MetaMask/ocap-kernel/pull/948)) - Persist the peer's last-observed incarnation and compare it on every successful handshake; on a detected restart, clear the peer's c-list contributions and reject the promises it was deciding before the new incarnation reuses any erefs +- Accept liveslots-allocated durable, virtual, and faceted vrefs (e.g. `o+d10/1`, `o+v3/4:0`) in `isVRef` / `insistERef` / `EndpointMessageStruct` validation ([#949](https://github.com/MetaMask/ocap-kernel/pull/949)) + - Previously the regex only matched plain `[op][+-]N`, so any vat using `defineDurableKind` failed outgoing-send validation and persisted-slot reads ## [0.7.0] diff --git a/packages/ocap-kernel/src/types.test.ts b/packages/ocap-kernel/src/types.test.ts index 98869f937..9590ea580 100644 --- a/packages/ocap-kernel/src/types.test.ts +++ b/packages/ocap-kernel/src/types.test.ts @@ -602,12 +602,21 @@ describe('insistKRef', () => { }); describe('isVRef', () => { - it.each(['o+0', 'o-1', 'p+42', 'p-0', 'o+123456789'])( - 'returns true for valid VRef %s', - (value) => { - expect(isVRef(value)).toBe(true); - }, - ); + it.each([ + 'o+0', + 'o-1', + 'p+42', + 'p-0', + 'o+123456789', + // Vat-allocated durable / virtual objects (liveslots `defineDurableKind` + // and friends). See parseVatSlot in @agoric/swingset-liveslots. + 'o+d10/1', + 'o+v3/4', + 'o+d10/1:0', + 'o+v3/4:7', + ])('returns true for valid VRef %s', (value) => { + expect(isVRef(value)).toBe(true); + }); it.each([ { name: 'missing sign', value: 'o1' }, @@ -616,6 +625,16 @@ describe('isVRef', () => { { name: 'non-digit suffix', value: 'o+1abc' }, { name: 'kernel ref', value: 'ko1' }, { name: 'remote ref', value: 'ro+1' }, + // Durability marker only valid on `o+`. `o-` is kernel-allocated; + // promises and devices never carry a durability/subid suffix. + { name: 'durable on import', value: 'o-d10/1' }, + { name: 'durable on promise', value: 'p+d10/1' }, + // Subid grammar requires a durability marker. + { name: 'subid without marker', value: 'o+10/1' }, + // Facet requires a subid. + { name: 'facet without subid', value: 'o+d10:0' }, + // Device refs are not supported by this kernel. + { name: 'device ref', value: 'd+0' }, { name: 'number', value: 123 }, { name: 'null', value: null }, ])('returns false for $name', ({ value }) => { @@ -624,7 +643,7 @@ describe('isVRef', () => { }); describe('insistVRef', () => { - it.each(['o+0', 'p-1', 'o+42'])( + it.each(['o+0', 'p-1', 'o+42', 'o+d10/1', 'o+v3/4:7'])( 'does not throw for valid VRef %s', (value) => { expect(() => insistVRef(value)).not.toThrow(); @@ -680,7 +699,7 @@ describe('insistRRef', () => { }); describe('isERef', () => { - it.each(['o+0', 'p-1', 'ro+1', 'rp-2'])( + it.each(['o+0', 'p-1', 'ro+1', 'rp-2', 'o+d10/1', 'o+v3/4:7'])( 'returns true for valid ERef %s', (value) => { expect(isERef(value)).toBe(true); @@ -698,7 +717,7 @@ describe('isERef', () => { }); describe('insistERef', () => { - it.each(['o+0', 'p-1', 'ro+1', 'rp-2'])( + it.each(['o+0', 'p-1', 'ro+1', 'rp-2', 'o+d10/1', 'o+v3/4:7'])( 'does not throw for valid ERef %s', (value) => { expect(() => insistERef(value)).not.toThrow(); diff --git a/packages/ocap-kernel/src/types.ts b/packages/ocap-kernel/src/types.ts index 599a85fb1..e5ea314b0 100644 --- a/packages/ocap-kernel/src/types.ts +++ b/packages/ocap-kernel/src/types.ts @@ -101,8 +101,22 @@ export type KRef = string & { readonly [KRefBrand]: never }; declare const VRefBrand: unique symbol; /** - * Vat-space reference. Format: `${'o'|'p'}${'+' | '-'}${number}`. - * E.g. `"o+0"`, `"p-7"`. + * Vat-space reference. Mirrors the vref grammar produced by + * `@agoric/swingset-liveslots`'s `parseVatSlot`: + * + * - `${'o'|'p'}${'+' | '-'}${number}` for plain refs + * (kernel imports, ephemeral exports, promises). + * E.g. `"o+0"`, `"o-3"`, `"p-7"`. + * - `o+${'d'|'v'}${kindId}/${instanceId}` for vat-allocated + * virtual (`v`) or durable (`d`) objects. + * E.g. `"o+d10/1"`, `"o+v3/4"`. + * - `o+${'d'|'v'}${kindId}/${instanceId}:${facetId}` for the + * same with a facet selector. + * E.g. `"o+d10/1:0"`. + * + * The durability/subid/facet syntax is only valid for `o+` + * (vat-allocated objects); kernel imports, promises, and remotes + * never carry it. */ export type VRef = string & { readonly [VRefBrand]: never }; @@ -139,7 +153,8 @@ export const isKRef = (value: unknown): value is KRef => typeof value === 'string' && /^k[op]\d+$/u.test(value); export const isVRef = (value: unknown): value is VRef => - typeof value === 'string' && /^[op][+-]\d+$/u.test(value); + typeof value === 'string' && + /^(?:p[+-]\d+|o-\d+|o\+(?:\d+|[dv]\d+\/\d+(?::\d+)?))$/u.test(value); export const isRRef = (value: unknown): value is RRef => typeof value === 'string' && /^r[op][+-]\d+$/u.test(value);