Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions docs/attenuation-by-narrowing.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ another — rather than by anticipation.

## The library

> **In progress:** the library described in this section has not fully landed.

Exported from `@metamask/kernel-utils`:

```ts
Expand Down
78 changes: 78 additions & 0 deletions packages/kernel-test/src/narrowing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs';
import { waitUntilQuiescent } from '@metamask/kernel-utils';
import { kunser } from '@metamask/ocap-kernel';
import type { Kernel, KRef, VatConfig } from '@metamask/ocap-kernel';
import { describe, expect, it } from 'vitest';

import { getBundleSpec, makeKernel, makeTestLogger } from './utils.ts';

const V1_ROOT: KRef = 'ko4';

/**
* Launch the narrowing vat.
*
* @returns The running kernel.
*/
const launchNarrowingVat = async (): Promise<Kernel> => {
const { logger } = makeTestLogger();
const database = await makeSQLKernelDatabase({});
const kernel = await makeKernel(database, true, logger);
const vat: VatConfig = {
bundleSpec: getBundleSpec('narrowing-vat'),
parameters: {},
};
await kernel.launchSubcluster({ bootstrap: 'main', vats: { main: vat } });
await waitUntilQuiescent();
return kernel;
};

/**
* Invoke one of the vat's probes.
*
* @param kernel - The kernel to send through.
* @param method - The probe to invoke.
* @param args - The probe's arguments.
* @returns `ok:<result>` or `rejected:<message>`, per the vat.
*/
const probe = async (
kernel: Kernel,
method: string,
args: unknown[],
): Promise<unknown> => kunser(await kernel.queueMessage(V1_ROOT, method, args));

describe('narrowing', () => {
it('narrows a vat-local exo', async () => {
const kernel = await launchNarrowingVat();
expect(
await probe(kernel, 'probeNarrowed', ['read', ['srv', 'data', 'x']]),
).toBe('ok:read:srv/data/x');
});

it('rejects a call outside the narrowing', async () => {
const kernel = await launchNarrowingVat();
expect(
await probe(kernel, 'probeNarrowed', ['read', ['etc', 'passwd']]),
).toMatch(/^rejected:.*\bread\b/u);
});

it('drops methods absent from the delta', async () => {
const kernel = await launchNarrowingVat();
expect(
await probe(kernel, 'probeNarrowed', ['stat', ['srv', 'data', 'x']]),
).toMatch(/^rejected:.*\bstat\b/u);
});

it('joins two narrowings of a common base', async () => {
const kernel = await launchNarrowingVat();
expect(await probe(kernel, 'probeJoined', [['srv', 'logs', 'y']])).toBe(
'ok:read:srv/logs/y',
);
});

it('narrows a default-guarded exo', async () => {
const kernel = await launchNarrowingVat();
expect(
await probe(kernel, 'probeDefaultGuarded', [['srv', 'data', 'x']]),
).toBe('ok:loose:srv/data/x');
});
});
95 changes: 95 additions & 0 deletions packages/kernel-test/src/vats/narrowing-vat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { E } from '@endo/eventual-send';
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
import { join, narrow, pathUnder } from '@metamask/kernel-utils';
import { makeDefaultExo } from '@metamask/kernel-utils/exo';

/**
* `stat` is named here although the delta drops it, so that a probe can try to
* call it.
*/
type Store = {
read: (segments: string[]) => Promise<string>;
stat: (segments: string[]) => Promise<string>;
};

/**
* Report an invocation's outcome, so a caller can tell a refusal from a result
* without matching on a rejection.
*
* @param call - The invocation to attempt.
* @returns `ok:<result>`, or `rejected:<message>`.
*/
const probe = async (call: () => Promise<unknown>): Promise<string> => {
try {
return `ok:${String(await call())}`;
} catch (error) {
return `rejected:${(error as Error).message}`;
}
};

/**
* Build function for a vat that narrows capabilities it builds itself.
*
* @returns The root object for the new vat.
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function buildRootObject() {
const base = makeExo(
'Store',
M.interface('Store', {
read: M.call(M.arrayOf(M.string())).returns(M.string()),
stat: M.call(M.arrayOf(M.string())).returns(M.string()),
}),
{
read: (segments: string[]) => `read:${segments.join('/')}`,
stat: (segments: string[]) => `stat:${segments.join('/')}`,
},
);

const looseBase = makeDefaultExo('LooseStore', {
read: (segments: string[]) => `loose:${segments.join('/')}`,
});

const underData = { read: [pathUnder(['srv', 'data'])] };

return makeDefaultExo('root', {
bootstrap: () => 'narrowing-vat',

probeNarrowed: async (method: 'read' | 'stat', segments: string[]) => {
const scoped = await narrow<Store>({
name: 'DataStore',
base,
delta: underData,
});
return probe(async () => E(scoped)[method](segments));
},

probeJoined: async (segments: string[]) => {
const data = await narrow<Store>({
name: 'DataStore',
base,
delta: underData,
});
const logs = await narrow<Store>({
name: 'LogStore',
base,
delta: { read: [pathUnder(['srv', 'logs'])] },
});
const both = await join<Store>({
name: 'DataAndLogStore',
refs: [data, logs],
});
return probe(async () => E(both).read(segments));
},

probeDefaultGuarded: async (segments: string[]) => {
const scoped = await narrow<Store>({
name: 'LooseDataStore',
base: looseBase,
delta: underData,
});
return probe(async () => E(scoped).read(segments));
},
});
}
4 changes: 4 additions & 0 deletions packages/kernel-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add `pathUnder(segments)`, which builds an `@endo/patterns` pattern matching segment arrays under a prefix, with `..` excluded past the prefix so that traversal out of it is unrepresentable. Empty `segments` matches every `..`-free segment array ([#1134](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1134))
- Add `narrow({ name, base, delta })`, which reads the base's interface guard over `E()` and returns an exo under the derived guard whose methods forward to the base. Narrowing a narrowing flattens: the result forwards straight to the original base under both deltas conjoined, so a chain of any depth stays one hop deep and costs one guard read. Every method of the result returns a promise. A base built with `makeDefaultExo` names no method guards, so each method the delta names gets its patterns as its whole guard, and a method the base does not implement rejects when called rather than when narrowing ([#1134](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1134))
- Add `join({ name, refs })`, which returns a narrowing of its refs' common base admitting whatever any of them admits: the result's methods are the union of the operands', and where two operands name the same method each argument position is disjoined. Every ref must be one `narrow` minted from that base, or the base itself, which admits everything and so absorbs. A ref minted from some other base, or by nothing at all, throws, as does a base built with `makeDefaultExo`, whose methods cannot be enumerated for it to absorb ([#1134](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1134))
- Add the `NarrowingDelta`, `NarrowOptions`, and `JoinOptions` types ([#1134](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1134))
- Add `getInterfaceMethodGuards`, `getMethodPayload`, `getGuardAt`, `buildMethodGuard`, and `asyncifyMethodGuards`, plus the `MethodGuardPayload` and `BuildMethodGuardOptions` types, for reading an `@endo/patterns` interface guard by argument position — required arguments, then optionals, then the rest guard — and reassembling it ([#1048](https://github.com/MetaMask/ocap-kernel/pull/1048))
- Add `makeGuardedFetch` and the `FetchGuard` type, which wrap a `fetch` so that a guard runs before every request it makes, redirect hops included ([#1026](https://github.com/MetaMask/ocap-kernel/pull/1026))
- `redirect: 'follow'`, in the caller's `init` or on a `Request`, is overridden so that each hop can be checked; `manual` and `error` are honoured. `baseFetch` is therefore always called with `redirect: 'manual'` and must honour it
Expand Down
1 change: 1 addition & 0 deletions packages/kernel-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"@chainsafe/libp2p-noise": "^17.0.0",
"@chainsafe/libp2p-yamux": "8.0.1",
"@endo/errors": "^1.3.1",
"@endo/eventual-send": "^1.5.0",
"@endo/exo": "^1.7.0",
"@endo/pass-style": "^1.8.2",
"@endo/patterns": "^1.9.1",
Expand Down
3 changes: 3 additions & 0 deletions packages/kernel-utils/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe('index', () => {
'isTypedArray',
'isTypedObject',
'isVatBundle',
'join',
'jsonSchemaToStruct',
'makeCounter',
'makeDefaultExo',
Expand All @@ -39,6 +40,8 @@ describe('index', () => {
'makeGuardedFetch',
'mergeDisjointRecords',
'methodArgsToStruct',
'narrow',
'pathUnder',
'prettifySmallcaps',
'resolveFetchInput',
'retry',
Expand Down
3 changes: 3 additions & 0 deletions packages/kernel-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export type {
BuildMethodGuardOptions,
MethodGuardPayload,
} from './guard-algebra.ts';
export type { NarrowingDelta } from './narrow-interface-guard.ts';
export { join, narrow, pathUnder } from './narrowing.ts';
export type { JoinOptions, NarrowOptions } from './narrowing.ts';
export { GET_DESCRIPTION, makeDiscoverableExo } from './discoverable.ts';
export type { DiscoverableExo } from './discoverable.ts';
export { S } from './described.ts';
Expand Down
Loading
Loading