Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions packages/kernel-utils/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ describe('index', () => {
'GET_DESCRIPTION',
'abortableDelay',
'calculateReconnectionBackoff',
'callable',
'collectSheafGuard',
'constant',
'delay',
'fetchValidatedJson',
'fromHex',
'getStalk',
'guardCoversPoint',
'ifDefined',
'installWakeDetector',
'isJsonRpcCall',
Expand All @@ -30,6 +35,8 @@ describe('index', () => {
'mergeDisjointRecords',
'retry',
'retryWithBackoff',
'sheafify',
'source',
'stringify',
'toHex',
'waitUntilQuiescent',
Expand Down
14 changes: 14 additions & 0 deletions packages/kernel-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,17 @@ export {
DEFAULT_MAX_DELAY_MS,
} from './retry.ts';
export type { RetryBackoffOptions, RetryOnRetryInfo } from './retry.ts';
export type {
Section,
PresheafSection,
EvaluatedSection,
MetaDataSpec,
Lift,
LiftContext,
Presheaf,
Sheaf,
} from './sheaf/types.ts';
export { constant, source, callable } from './sheaf/metadata.ts';
export { sheafify } from './sheaf/sheafify.ts';
export { collectSheafGuard } from './sheaf/guard.ts';
export { getStalk, guardCoversPoint } from './sheaf/stalk.ts';
103 changes: 103 additions & 0 deletions packages/kernel-utils/src/sheaf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Sheaf

Runtime capability routing adapted from sheaf theory in algebraic topology.

`sheafify({ name, sections })` produces a **sheaf** — an authority manager
over a presheaf of capabilities. The sheaf grants revocable dispatch sections
via `getSection`, tracks all delegated authority, and supports point-wise
revocation.

## Concepts

**Presheaf section** (`PresheafSection`) — The input data: a capability (exo)
paired with operational metadata, assigned over the open set defined by the
exo's guard. This is an element of the presheaf F = F_sem x F_op.

> A `getBalance(string)` provider with `{ cost: 100 }` is one presheaf
> section. A `getBalance("alice")` provider with `{ cost: 1 }` is another,
> covering a narrower open set.

**Germ** — An equivalence class of presheaf sections at an invocation point,
identified by metadata. At dispatch time, sections in the stalk with identical
metadata are collapsed into a single germ; the system picks an arbitrary
representative for dispatch. If two capabilities are indistinguishable by
metadata, the sheaf has no data to prefer one over the other.

> Two `getBalance(string)` providers both with `{ cost: 1 }` collapse into
> one germ. The lift never sees both — it receives one representative.

**Stalk** — The set of germs matching a specific `(method, args)` invocation,
computed at dispatch time by guard filtering and then collapsing equivalent
entries.

> Stalk at `("getBalance", "alice")` might contain two germs (cost 1 vs 100);
> stalk at `("transfer", ...)` might contain one.

**Lift** — An async function that selects one germ from a multi-germ stalk.
At dispatch time, metadata is decomposed into **constraints** (keys with the
same value across every germ — topologically determined, not a choice) and
**options** (the remaining keys — the lift's actual decision space). The lift
receives only options on each germ; constraints arrive separately in the
context.

> `argmin` by cost, `argmin` by latency, or any custom selection logic. The
> lift is never invoked for single-germ stalks.

**Sheaf** — The authority manager returned by `sheafify`. Holds the presheaf
data (captured at construction time) and a registry of all granted sections.

```
const sheaf = sheafify({ name: 'Wallet', sections });
```

- `sheaf.getSection({ guard?, lift })` — produce a revocable dispatch exo
- `sheaf.revokePoint(method, ...args)` — revoke every granted section whose
guard covers the point
- `sheaf.getExported()` — union guard of all active (non-revoked) sections
- `sheaf.revokeAll()` — revoke every granted section

## Dispatch pipeline

At each invocation point `(method, args)` within a granted section:

```
getStalk(sections, method, args) presheaf → stalk (filter by guard)
collapseEquivalent(stalk) locality condition (quotient by metadata)
decomposeMetadata(collapsed) restriction map (constraints / options)
lift(stripped, { method, args, operational selection (extra-theoretic)
constraints })
dispatch to collapsed[index].exo evaluation
```

## Design choices

**Germ identity is metadata identity.** The collapse step quotients by
metadata: if two sections should be distinguishable, the caller must give them
distinguishable metadata. Sections with identical metadata are treated as
interchangeable. Under the sheaf condition (effect-equivalence), this recovers
the classical equivalence relation on germs.

**Pseudosheafification.** The sheafification functor would precompute the full
etale space. This system defers to invocation time: compute the stalk,
collapse, decompose, lift. The trade-off is that global coherence (a lift
choosing consistently across points) is not guaranteed.

**Restriction and gluing are implicit.** Guard restriction induces a
restriction map on metadata: restricting to a point filters the presheaf to
covering sections (`getStalk`), then `decomposeMetadata` strips the metadata
to distinguishing keys — the restricted metadata over that point. The join
works dually: the union of two sections has the join of their metadata, and
restriction at any point recovers the local distinguishing keys in O(n).
Gluing follows: compatible sections (equal metadata on their overlap) produce a
well-defined join. The dispatch pipeline computes all of this implicitly. The
remaining gap is `revokeSite` (revoking over an open set rather than a point),
which requires an `intersects` operator on guards not yet available.

## Relationship to stacks

This construction is more properly a **stack** in algebraic geometry. We call
it a sheaf because engineers already know "stack" as a LIFO data structure, and
the algebraic geometry term is unrelated. Within a germ, any representative
will do — authority-equivalence is asserted by constructor contract, not
verified at runtime. Between germs, metadata distinguishes them and the lift
resolves the choice.
218 changes: 218 additions & 0 deletions packages/kernel-utils/src/sheaf/guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { makeExo } from '@endo/exo';
import {
M,
matches,
getInterfaceGuardPayload,
getMethodGuardPayload,
} from '@endo/patterns';
import type { MethodGuard, Pattern } from '@endo/patterns';
import { describe, it, expect } from 'vitest';

import { collectSheafGuard } from './guard.ts';
import { guardCoversPoint } from './stalk.ts';
import type { Section } from './types.ts';

const makeSection = (
tag: string,
guards: Record<string, MethodGuard>,
methods: Record<string, (...args: unknown[]) => unknown>,
): Section => {
const interfaceGuard = M.interface(tag, guards);
return makeExo(tag, interfaceGuard, methods) as unknown as Section;
};

describe('collectSheafGuard', () => {
it('variable arity: add with 1, 2, and 3 args', () => {
const sections = [
makeSection(
'Calc:0',
{ add: M.call(M.number()).returns(M.number()) },
{ add: (a: number) => a },
),
makeSection(
'Calc:1',
{ add: M.call(M.number(), M.number()).returns(M.number()) },
{ add: (a: number, b: number) => a + b },
),
makeSection(
'Calc:2',
{
add: M.call(M.number(), M.number(), M.number()).returns(M.number()),
},
{ add: (a: number, b: number, cc: number) => a + b + cc },
),
];

const guard = collectSheafGuard('Calc', sections);
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
methodGuards: Record<string, MethodGuard>;
};
const payload = getMethodGuardPayload(methodGuards.add) as unknown as {
argGuards: Pattern[];
optionalArgGuards?: Pattern[];
};

// 1 required arg (present in all), 2 optional (variable arity)
expect(payload.argGuards).toHaveLength(1);
expect(payload.optionalArgGuards).toHaveLength(2);
});

it('return guard union', () => {
const sections = [
makeSection(
'S:0',
{ f: M.call(M.eq(0)).returns(M.eq(0)) },
{ f: (_: number) => 0 },
),
makeSection(
'S:1',
{ f: M.call(M.eq(1)).returns(M.eq(1)) },
{ f: (_: number) => 1 },
),
];

const guard = collectSheafGuard('S', sections);
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
methodGuards: Record<string, MethodGuard>;
};
const { returnGuard } = getMethodGuardPayload(
methodGuards.f,
) as unknown as { returnGuard: Pattern };

// Return guard is union of eq(0) and eq(1)
expect(matches(0, returnGuard)).toBe(true);
expect(matches(1, returnGuard)).toBe(true);
});

it('section with its own optional args: optional preserved in union', () => {
const sections = [
makeSection(
'Greeter',
{
greet: M.callWhen(M.string())
.optional(M.string())
.returns(M.string()),
},
{ greet: (name: string, _greeting?: string) => `hello ${name}` },
),
];

const guard = collectSheafGuard('Greeter', sections);
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
methodGuards: Record<string, MethodGuard>;
};
const payload = getMethodGuardPayload(methodGuards.greet) as unknown as {
argGuards: Pattern[];
optionalArgGuards?: Pattern[];
};

expect(payload.argGuards).toHaveLength(1);
expect(payload.optionalArgGuards).toHaveLength(1);
});

it('rest arg guard preserved in collected union', () => {
const sections = [
makeSection(
'Logger',
{ log: M.call(M.string()).rest(M.string()).returns(M.any()) },
{ log: (..._args: string[]) => undefined },
),
];

const guard = collectSheafGuard('Logger', sections);
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
methodGuards: Record<string, MethodGuard>;
};
const payload = getMethodGuardPayload(methodGuards.log) as unknown as {
argGuards: Pattern[];
optionalArgGuards?: Pattern[];
restArgGuard?: Pattern;
};

expect(payload.argGuards).toHaveLength(1);
expect(payload.optionalArgGuards ?? []).toHaveLength(0);
expect(payload.restArgGuard).toBeDefined();
});

it('rest arg guards unioned across sections', () => {
const sections = [
makeSection(
'A',
{ log: M.call(M.string()).rest(M.string()).returns(M.any()) },
{ log: (..._args: string[]) => undefined },
),
makeSection(
'B',
{ log: M.call(M.string()).rest(M.number()).returns(M.any()) },
{ log: (..._args: unknown[]) => undefined },
),
];

const guard = collectSheafGuard('AB', sections);
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
methodGuards: Record<string, MethodGuard>;
};
const { restArgGuard } = getMethodGuardPayload(
methodGuards.log,
) as unknown as { restArgGuard?: Pattern };

expect(matches('hello', restArgGuard)).toBe(true);
expect(matches(42, restArgGuard)).toBe(true);
});

it('rest-arg section covers optional positions (no false negative)', () => {
// Section A requires 1 number; Section B requires 0 args but accepts any
// number of strings via rest. A call ['hello'] is covered by B — the
// collected guard must pass it too.
const sections = [
makeSection(
'AB:0',
{ f: M.call(M.number()).returns(M.any()) },
{ f: (_: number) => undefined },
),
makeSection(
'AB:1',
{ f: M.call().rest(M.string()).returns(M.any()) },
{ f: (..._args: string[]) => undefined },
),
];

const guard = collectSheafGuard('AB', sections);

expect(guardCoversPoint(guard, 'f', ['hello'])).toBe(true); // covered by B
expect(guardCoversPoint(guard, 'f', [42])).toBe(true); // covered by A
expect(guardCoversPoint(guard, 'f', [])).toBe(true); // covered by B (0 required)
});

it('multi-method guard collection', () => {
const sections = [
makeSection(
'Multi:0',
{
translate: M.call(M.string(), M.string()).returns(M.string()),
},
{
translate: (from: string, to: string) => `${from}->${to}`,
},
),
makeSection(
'Multi:1',
{
translate: M.call(M.string(), M.string()).returns(M.string()),
summarize: M.call(M.string()).returns(M.string()),
},
{
translate: (from: string, to: string) => `${from}->${to}`,
summarize: (text: string) => `summary: ${text}`,
},
),
];

const guard = collectSheafGuard('Multi', sections);
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
methodGuards: Record<string, MethodGuard>;
};
expect('translate' in methodGuards).toBe(true);
expect('summarize' in methodGuards).toBe(true);
});
});
Loading
Loading