Skip to content
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