@lapxo/head 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/TARGET.bound +317 -53
- package/dist/src/access/obtain.d.ts +7 -1
- package/dist/src/access/obtain.js +4 -4
- package/dist/src/ahead/budget.d.ts +6 -0
- package/dist/src/ahead/budget.js +11 -0
- package/dist/src/ahead/fold.d.ts +2 -0
- package/dist/src/ahead/fold.js +19 -0
- package/dist/src/ahead/read.d.ts +10 -0
- package/dist/src/ahead/read.js +18 -0
- package/dist/src/claims/claim.d.ts +9 -0
- package/dist/src/claims/claim.js +40 -0
- package/dist/src/core/answer.d.ts +3 -0
- package/dist/src/core/package.js +4 -3
- package/dist/src/core/read.js +1 -1
- package/dist/src/flows/call.d.ts +1 -3
- package/dist/src/flows/call.js +4 -7
- package/dist/src/flows/read.js +29 -25
- package/dist/src/index.js +23 -14
- package/dist/src/kept/kept.d.ts +30 -0
- package/dist/src/kept/kept.js +20 -0
- package/dist/src/links/inspect.d.ts +8 -0
- package/dist/src/links/inspect.js +41 -0
- package/dist/src/links/link.d.ts +7 -0
- package/dist/src/links/link.js +25 -0
- package/dist/src/marks/compare.d.ts +9 -0
- package/dist/src/marks/compare.js +48 -0
- package/dist/src/onboard/first.d.ts +5 -3
- package/dist/src/onboard/first.js +25 -8
- package/dist/src/onboard/owed.d.ts +10 -0
- package/dist/src/onboard/owed.js +28 -0
- package/dist/src/onboard/site.d.ts +12 -0
- package/dist/src/onboard/site.js +22 -0
- package/dist/src/ports/bound.d.ts +1 -0
- package/dist/src/ports/bound.js +15 -3
- package/dist/src/ports/documents.js +1 -1
- package/dist/src/ports/reading.d.ts +8 -0
- package/dist/src/ports/reading.js +14 -0
- package/dist/src/ports/spare.js +1 -1
- package/dist/src/ports/surface.d.ts +5 -1
- package/dist/src/ports/surface.js +7 -5
- package/dist/src/ports/warm.js +1 -2
- package/dist/src/sources/connect.d.ts +8 -0
- package/dist/src/sources/connect.js +18 -47
- package/dist/src/sources/refused.d.ts +7 -0
- package/dist/src/sources/refused.js +16 -0
- package/dist/src/tools/ask.d.ts +1 -0
- package/dist/src/tools/ask.js +1 -0
- package/dist/src/tools/card.d.ts +1 -1
- package/dist/src/tools/card.js +6 -5
- package/dist/src/tools/claim.d.ts +1 -0
- package/dist/src/tools/claim.js +1 -0
- package/dist/src/tools/do.d.ts +1 -0
- package/dist/src/tools/do.js +1 -0
- package/dist/src/tools/look.d.ts +5 -4
- package/dist/src/tools/look.js +14 -13
- package/dist/src/verbs/ask.d.ts +11 -0
- package/dist/src/verbs/ask.js +36 -0
- package/dist/src/verbs/card.d.ts +11 -0
- package/dist/src/verbs/card.js +16 -0
- package/dist/src/verbs/do.d.ts +8 -0
- package/dist/src/verbs/do.js +28 -0
- package/package.json +3 -3
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type { Attachment } from './contract.ts';
|
|
1
|
+
import type { Attachment, Obtained } from './contract.ts';
|
|
2
2
|
import type { Context } from '../core/answer.ts';
|
|
3
3
|
import type { Said } from '../core/lock.ts';
|
|
4
4
|
import type { Call } from '../core/read.ts';
|
|
5
|
+
type Kept = Obtained & {
|
|
6
|
+
readonly at?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const sealed: (got: Kept) => string;
|
|
9
|
+
export declare const opened: (kept: string | undefined) => Kept | undefined;
|
|
5
10
|
/** Whom a call is made as, never its secret: anonymous, or the access account and when the secret it holds now was obtained — still holding, unless what was made as it is only being looked back on; none while no secret is kept, or one is kept without saying when. */
|
|
6
11
|
export declare const principalOf: (context: Context, plan: Call, current?: boolean) => string | undefined;
|
|
7
12
|
/**
|
|
@@ -17,3 +22,4 @@ export declare const attachmentFor: (context: Context, place: string, lines: rea
|
|
|
17
22
|
readonly said: string;
|
|
18
23
|
readonly input?: string;
|
|
19
24
|
}>;
|
|
25
|
+
export {};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { canonical } from '@lapxo/topos/wire';
|
|
2
2
|
import { jsonOf } from "./contract.js";
|
|
3
3
|
import { placeNameOf } from "../core/answer.js";
|
|
4
|
-
import { members, valueOf } from "../core/lock.js";
|
|
4
|
+
import { aboutOf, members, valueOf } from "../core/lock.js";
|
|
5
5
|
import { fieldsOf, stepOf, vocabOf } from "../core/read.js";
|
|
6
|
-
const sealed = (got) => Buffer.from(JSON.stringify(got)).toString('base64url');
|
|
7
|
-
const opened = (kept) => { try {
|
|
6
|
+
export const sealed = (got) => Buffer.from(JSON.stringify(got)).toString('base64url');
|
|
7
|
+
export const opened = (kept) => { try {
|
|
8
8
|
return kept === undefined ? undefined : JSON.parse(Buffer.from(kept, 'base64url').toString('utf8'));
|
|
9
9
|
}
|
|
10
10
|
catch {
|
|
@@ -55,7 +55,7 @@ export const attachmentFor = async (context, place, lines, plan) => {
|
|
|
55
55
|
values: members(valueOf(lines, 'wire/access-values') || valueOf(context.lock, 'wire/access-values')),
|
|
56
56
|
host: Object.fromEntries(context.lock.filter((l) => l.scope?.startsWith('host/')).map((l) => [l.scope.slice('host/'.length), l.value ?? ''])),
|
|
57
57
|
scopes: [...new Set(lines.filter((l) => reads.has(l.scope) && l.measure === 'scopes').flatMap((l) => (l.value ?? '').split('|')))].sort(),
|
|
58
|
-
enter: (title, fields) => context.person.enter(title, fields), consent: async (message) => (await context.person.consent(message,
|
|
58
|
+
enter: (title, fields) => context.person.enter(title, fields), consent: async (message) => (await context.person.consent(message, aboutOf(context.lock, 'said/elicit/yes'))) === 'yes', choose: (message, choices) => context.person.choose(message, choices), visit: (message, to) => context.person.visit(message, to),
|
|
59
59
|
form: (source, operation) => fieldsOf(lines, `${v.offers}/${source}/${operation}/${v.form}`), call: step,
|
|
60
60
|
post: async (url, body, as) => ((transport) => (transport === undefined ? undefined : transport.post(url, body, most, Number(valueOf(context.lock, 'adapter/request/wait').split('..')[1]), as)))(await context.adapter(url)),
|
|
61
61
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Asked } from '../core/answer.ts';
|
|
2
|
+
import type { Said } from '../core/lock.ts';
|
|
3
|
+
/** Whether a read would spend past what the place allows: a question, past the place's rate; a read ahead, past the share of what the questions leave of it that reads ahead may spend. A read ahead is told by the mark beside its receipt, and counted once. */
|
|
4
|
+
export declare const overOf: (lines: readonly Said[], [family, at, mark]: readonly [string, string, string], source: string, now: number, rate: number, share: number, asked: Asked) => boolean;
|
|
5
|
+
/** The mark a read made ahead lands beside its receipt, under the receipt's own at, so what reads ahead spent is counted apart from what questions spent. */
|
|
6
|
+
export declare const markOf: (asked: Asked, [at, mark]: readonly [string, string], scope: string, digest: string, when: string) => readonly string[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
2
|
+
import { spentOf } from "../core/receipt.js";
|
|
3
|
+
const early = (asked) => asked['ahead'] === 'true';
|
|
4
|
+
/** Whether a read would spend past what the place allows: a question, past the place's rate; a read ahead, past the share of what the questions leave of it that reads ahead may spend. A read ahead is told by the mark beside its receipt, and counted once. */
|
|
5
|
+
export const overOf = (lines, [family, at, mark], source, now, rate, share, asked) => {
|
|
6
|
+
const ahead = lines.filter((l) => l.scope?.startsWith(`${family}/${source}/`) && l.scope.endsWith(`/${mark}`) && l.at?.startsWith(`${at}:`) && now - Date.parse(l.at.slice(at.length + 1)) < 3_600_000).length;
|
|
7
|
+
const calls = spentOf(lines, family, at, source, now) - ahead;
|
|
8
|
+
return calls >= rate || (early(asked) && ahead + 1 > ((rate - (calls - ahead)) * share) / 100);
|
|
9
|
+
};
|
|
10
|
+
/** The mark a read made ahead lands beside its receipt, under the receipt's own at, so what reads ahead spent is counted apart from what questions spent. */
|
|
11
|
+
export const markOf = (asked, [at, mark], scope, digest, when) => early(asked) ? [canonical({ at: `${at}:${when}`, by: 'target', form: 'alphabet', measure: 'digest', role: 'writes', scope: `${scope}/${mark}`, value: digest })] : [];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Cached projections are indexed only by identities returned by the instrument. */
|
|
2
|
+
const kept = new Map();
|
|
3
|
+
/** A refused identity cannot validate an old answer. The reader binds new text to the expected identity. */
|
|
4
|
+
export const folded = async (kind, place, identity, read) => {
|
|
5
|
+
const key = `${kind}\0${place}`, current = await identity(), held = kept.get(key);
|
|
6
|
+
if (current === undefined) {
|
|
7
|
+
kept.delete(key);
|
|
8
|
+
return read();
|
|
9
|
+
}
|
|
10
|
+
if (held?.identity === current)
|
|
11
|
+
return held.folded;
|
|
12
|
+
const reading = read(current);
|
|
13
|
+
kept.set(key, { identity: current, folded: reading });
|
|
14
|
+
const forget = () => { if (kept.get(key)?.folded === reading)
|
|
15
|
+
kept.delete(key); };
|
|
16
|
+
reading.then((result) => { if (result && typeof result === 'object' && 'refused' in result)
|
|
17
|
+
forget(); }, forget);
|
|
18
|
+
return reading;
|
|
19
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from '../core/answer.ts';
|
|
2
|
+
import type { Cell } from '../ports/surface.ts';
|
|
3
|
+
/**
|
|
4
|
+
* What an answer leaves to do before the next call, done after it is answered; another call coming gives up what is not
|
|
5
|
+
* begun, never what is, so no land is cut halfway:
|
|
6
|
+
* the reads a known next asks for, read in a context that asks the person nothing — a read only, never an act, nothing it
|
|
7
|
+
* would need a yes or a secret for — and then every place the answer names folded again where it moved, so the next
|
|
8
|
+
* question finds it folded. A fold calls no source, so it spends nothing of a place's rate.
|
|
9
|
+
*/
|
|
10
|
+
export declare const readAhead: (cells: readonly Cell[], context: Context, signal: AbortSignal) => Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readSource } from "../flows/read.js";
|
|
2
|
+
const none = async () => 'none', nothing = async () => undefined;
|
|
3
|
+
/**
|
|
4
|
+
* What an answer leaves to do before the next call, done after it is answered; another call coming gives up what is not
|
|
5
|
+
* begun, never what is, so no land is cut halfway:
|
|
6
|
+
* the reads a known next asks for, read in a context that asks the person nothing — a read only, never an act, nothing it
|
|
7
|
+
* would need a yes or a secret for — and then every place the answer names folded again where it moved, so the next
|
|
8
|
+
* question finds it folded. A fold calls no source, so it spends nothing of a place's rate.
|
|
9
|
+
*/
|
|
10
|
+
export const readAhead = async (cells, context, signal) => {
|
|
11
|
+
const quiet = { ...context, elicit: none, person: { consent: none, enter: nothing, choose: nothing, visit: nothing } };
|
|
12
|
+
for (const { next } of cells)
|
|
13
|
+
if (!signal.aborted && 'tool' in next && next.tool === 'ask' && next.args['source'] && next.args['operation'] && !next.args['why'] && !next.args['history'])
|
|
14
|
+
await readSource({ ...next.args, ahead: 'true' }, quiet);
|
|
15
|
+
await quiet.settle();
|
|
16
|
+
for (const place of new Set(cells.flatMap((c) => [c.fields['place'] ?? '', 'tool' in c.next ? c.next.args['place'] ?? '' : '']).filter((p) => p && !signal.aborted)))
|
|
17
|
+
await Promise.all([context.standing(place), context.policy(place)]).catch(() => undefined);
|
|
18
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Asked, type Context } from '../core/answer.ts';
|
|
2
|
+
/**
|
|
3
|
+
* What the person states, kept as theirs: a line their own key attests, their words exactly as given, in the place named;
|
|
4
|
+
* asked once a conversation whether what they state there is kept, and each statement said back as it goes in, so they see
|
|
5
|
+
* it, with who carried it here as the client says it is. Taken back by the same words: what a person attests is evidence, and
|
|
6
|
+
* stands for good, so taking it back is attesting that, beside it. A place whose lines admit no carrier is handed the line that does.
|
|
7
|
+
* Only this action keeps one, never a cell, an answer or a read ahead, so nothing the model concluded becomes the person's.
|
|
8
|
+
*/
|
|
9
|
+
export declare const claimed: (asked: Asked, context: Context) => Promise<readonly string[]>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
3
|
+
import { inPlace, standingOr, unless } from "../core/answer.js";
|
|
4
|
+
import { uncoveredIn } from "../core/device.js";
|
|
5
|
+
import { aboutOf, members, valueOf } from "../core/lock.js";
|
|
6
|
+
import { fill, sayIn } from "../core/surface.js";
|
|
7
|
+
/** Whether the person said, in this conversation, that what they state in a place is kept as theirs: asked once a place, a no kept as a yes is. */
|
|
8
|
+
const answered = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* What the person states, kept as theirs: a line their own key attests, their words exactly as given, in the place named;
|
|
11
|
+
* asked once a conversation whether what they state there is kept, and each statement said back as it goes in, so they see
|
|
12
|
+
* it, with who carried it here as the client says it is. Taken back by the same words: what a person attests is evidence, and
|
|
13
|
+
* stands for good, so taking it back is attesting that, beside it. A place whose lines admit no carrier is handed the line that does.
|
|
14
|
+
* Only this action keeps one, never a cell, an answer or a read ahead, so nothing the model concluded becomes the person's.
|
|
15
|
+
*/
|
|
16
|
+
export const claimed = (asked, context) => inPlace(asked, context, async (place) => {
|
|
17
|
+
const [say, word, said, held] = [sayIn(context.lock), (k) => valueOf(context.lock, k), (asked['said'] ?? '').trim(), await standingOr(context, place)];
|
|
18
|
+
if ('said' in held)
|
|
19
|
+
return held.said;
|
|
20
|
+
if (!said)
|
|
21
|
+
return [say('missing-param', { operation: 'claim', param: 'said' })];
|
|
22
|
+
const [family, withdraw, back] = [`${word('claim/family')}/${createHash('sha256').update(said).digest('hex').slice(0, 16)}`, asked['withdraw'] === 'true', word('claim/withdrawn')];
|
|
23
|
+
const standing = held.lines.filter((l) => l.scope?.startsWith(`${family}/`) && !l.scope.endsWith(`/${back}`) && !held.lines.some((w) => w.scope === `${l.scope}/${back}`)).map((l) => l.scope);
|
|
24
|
+
if (withdraw && !standing.length)
|
|
25
|
+
return [say('claim-none', { place, said })];
|
|
26
|
+
const wire = held.lines.find((l) => l.scope === 'wire/fields'), via = word('claim/via');
|
|
27
|
+
const bare = wire && Object.fromEntries(Object.entries(wire).filter(([k]) => !['sig', 'epoch', 'about'].includes(k)));
|
|
28
|
+
if (bare && !members(wire.value ?? '').includes(via))
|
|
29
|
+
return [say('claims-unadmitted', { place, lines: [canonical({ ...bare, by: 'target', value: 'withdraw' }), canonical({ ...bare, by: 'target', at: `policy:head/wire/${new Date(context.now()).toISOString()}`, value: `${wire.value}|${via}` })].join('\n') })];
|
|
30
|
+
const line = (scope) => canonical({ about: said, at: word('claim/at'), by: 'target', form: 'alphabet', measure: 'text', role: 'writes', scope, value: 'lock', [via]: context.client() ?? word('claim/via-unknown') });
|
|
31
|
+
const lot = withdraw ? standing.map((scope) => line(`${scope}/${back}`)) : [line(`${family}/${new Date(context.now()).toISOString().replace(/\D/g, '')}`)], outside = uncoveredIn(context.lock, held.lines, context.device('person'), place, lot);
|
|
32
|
+
if (outside)
|
|
33
|
+
return outside;
|
|
34
|
+
if (!withdraw && !answered.has(place))
|
|
35
|
+
answered.set(place, (await unless(context, fill(aboutOf(context.lock, 'said/elicit-claims'), { place }))) === undefined);
|
|
36
|
+
if (!withdraw && !answered.get(place))
|
|
37
|
+
return [say('claims-declined', { place, said })];
|
|
38
|
+
const landed = await context.landAs(place, lot, 'person', held.lines);
|
|
39
|
+
return 'refused' in landed ? [say('not-landed', { why: landed.refused })] : [say(withdraw ? 'unclaimed' : 'claimed', { place, said }), ...landed.lines];
|
|
40
|
+
});
|
|
@@ -15,6 +15,7 @@ export interface Context {
|
|
|
15
15
|
readonly lock: readonly Said[];
|
|
16
16
|
readonly bound: (cwd: string, args: readonly string[]) => Promise<Printed | undefined>;
|
|
17
17
|
readonly roots: () => Promise<readonly string[]>;
|
|
18
|
+
readonly client: () => string | undefined;
|
|
18
19
|
readonly keys: KeyStore;
|
|
19
20
|
readonly device: (role: string) => string | undefined;
|
|
20
21
|
readonly standing: (place: string) => Promise<{
|
|
@@ -64,6 +65,7 @@ export interface Context {
|
|
|
64
65
|
readonly signRoot: (place: string, lot: readonly string[], account: string) => Promise<Outcome>;
|
|
65
66
|
readonly landRoot: (place: string, lot: readonly string[], account: string) => Promise<Outcome>;
|
|
66
67
|
readonly settle: () => Promise<readonly string[]>;
|
|
68
|
+
readonly moved: (place: string) => boolean;
|
|
67
69
|
readonly pinned: (place: string, file: string) => Promise<{
|
|
68
70
|
readonly pin: string;
|
|
69
71
|
readonly bytes: string;
|
|
@@ -82,6 +84,7 @@ export interface Context {
|
|
|
82
84
|
readonly elicit: (message: string) => Promise<'yes' | 'no' | 'none'>;
|
|
83
85
|
readonly now: () => number;
|
|
84
86
|
readonly machine: () => number;
|
|
87
|
+
readonly begun: number;
|
|
85
88
|
}
|
|
86
89
|
export type Answer = (asked: Asked, context: Context) => Promise<readonly string[]>;
|
|
87
90
|
/** What answers a line of the surface: the module named as the line, in the folder of its family. */
|
package/dist/src/core/package.js
CHANGED
|
@@ -5,6 +5,7 @@ import { members, under, valueOf } from "./lock.js";
|
|
|
5
5
|
import { fieldsOf, vocabOf } from "./read.js";
|
|
6
6
|
import { sayIn } from "./surface.js";
|
|
7
7
|
import { digestIn, linesOf } from "./worlds.js";
|
|
8
|
+
import { linkOf } from "../links/link.js";
|
|
8
9
|
/** A line no package carries: one that is not a world, or a source's document, overlay or pin, said once by digest — with, beside a digest that names bytes, the encrypted address they are read at. */
|
|
9
10
|
export const foreignIn = (lines) => lines.find((l) => !/^(sources\/[a-z0-9-]+(\/overlay)?|uses\/[a-z0-9-]+)$/.test(l.scope ?? '') || lines.some((o) => o !== l && o.scope === l.scope && o.measure === l.measure)
|
|
10
11
|
|| (l.measure !== 'digest' && !(l.measure === 'id' && URL.canParse(l.value ?? '') && lines.some((o) => o.scope === l.scope && o.measure === 'digest' && o.needs))));
|
|
@@ -27,13 +28,13 @@ export const shared = (asked, context) => inPlace(asked, context, async (place)
|
|
|
27
28
|
return [say('share-to', { place, to })];
|
|
28
29
|
if ('said' in held)
|
|
29
30
|
return held.said;
|
|
30
|
-
const
|
|
31
|
+
const urls = ((u) => (u && typeof u === 'object' ? u : {}))((() => { try {
|
|
31
32
|
return JSON.parse(asked['urls'] || '{}');
|
|
32
33
|
}
|
|
33
34
|
catch {
|
|
34
35
|
return {};
|
|
35
|
-
} })())), bytes = new TextEncoder().encode(`${pack.lines.join('\n')}\n`);
|
|
36
|
-
return !pack.names.length ? [say('share-none', { place })] : ([...pack.files, [LOCK, bytes]].forEach(([at, laid]) => context.files.lay(to, at, laid)), [say('shared', { place, to, sources: pack.names.join(', '), digest: digestIn(context.lock)(bytes) }), ...pack.lines]);
|
|
36
|
+
} })()), pack = packed(context, place, held.lines, urls), bytes = new TextEncoder().encode(`${pack.lines.join('\n')}\n`);
|
|
37
|
+
return !pack.names.length ? [say('share-none', { place })] : ([...pack.files, [LOCK, bytes]].forEach(([at, laid]) => context.files.lay(to, at, laid)), [say('shared', { place, to, sources: pack.names.join(', '), digest: digestIn(context.lock)(bytes) }), ...linkOf(context, to, pack.lines, urls), ...pack.lines]);
|
|
37
38
|
});
|
|
38
39
|
/** What a package names, checked whole before anything is asked: every line a world or a source's document, overlay or pin, by digest; each world the one this head pins; an order its pins rest in. A source whose bytes did not arrive, or did not hash to their line, is open, and so is every source resting on it: absence is not a refusal, and the rest arrives. */
|
|
39
40
|
export const unpacked = (context, document, bytes, read) => {
|
package/dist/src/core/read.js
CHANGED
|
@@ -61,4 +61,4 @@ const callOf = (lines, lock, source, operation, place) => {
|
|
|
61
61
|
};
|
|
62
62
|
const isCallable = (lock, classes) => classes.length === 1 && classes[0] === vocabOf(lock).callable;
|
|
63
63
|
export const planRead = (lines, lock, source, operation, place) => ((call) => (call.kind === 'call' && !isCallable(lock, call.classes) ? said('not-callable', { source, operation, class: call.classes.join(', ') }) : call))(callOf(lines, lock, source, operation, place));
|
|
64
|
-
export const planAct = (lines, lock, source, operation, place) => ((call) => (call.kind === 'call' && isCallable(lock, call.classes) ? said('is-read', { source, operation }) : call.kind === 'call' && call.access.probe ? said('access-unstated', { source, operation }) : call))(callOf(lines, lock, source, operation, place));
|
|
64
|
+
export const planAct = (lines, lock, source, operation, place) => ((call) => (call.kind === 'call' && isCallable(lock, call.classes) ? said('is-read', { source, operation }) : call.kind === 'call' && call.access.probe ? said('access-unstated', { source, operation, method: call.carry['method']?.[0], path: call.carry['path']?.[0] }) : call))(callOf(lines, lock, source, operation, place));
|
package/dist/src/flows/call.d.ts
CHANGED
|
@@ -12,8 +12,6 @@ export declare const pickedOrigin: (context: Context, place: string, lines: read
|
|
|
12
12
|
} | {
|
|
13
13
|
readonly said: readonly string[];
|
|
14
14
|
}>;
|
|
15
|
-
/** Calls of one place take turns, so one asked while another lands sees its receipt; nothing waits on a timer. */
|
|
16
|
-
export declare const inTurn: <T>(place: string, act: () => Promise<T>) => Promise<T>;
|
|
17
15
|
export interface Frame {
|
|
18
16
|
readonly say: (key: string, fields?: Readonly<Record<string, string | undefined>>) => readonly string[];
|
|
19
17
|
readonly lands: (...lot: readonly string[]) => Promise<Outcome>;
|
|
@@ -43,7 +41,7 @@ export interface Frame {
|
|
|
43
41
|
readonly uncovered: (role: string, scopes: readonly string[]) => readonly string[] | undefined;
|
|
44
42
|
}
|
|
45
43
|
/**
|
|
46
|
-
* The steps a read and an act share: the place,
|
|
44
|
+
* The steps a read and an act share: the place, the lines it stands on once bound settled them, the offer's
|
|
47
45
|
* plan, the transport its origin names and the request it prepares; then what is particular, given a frame that says
|
|
48
46
|
* by the lock, attaches access when asked, types what came back and lands a line under head's key.
|
|
49
47
|
*/
|
package/dist/src/flows/call.js
CHANGED
|
@@ -27,18 +27,15 @@ export const pickedOrigin = async (context, place, lines, source, plan) => {
|
|
|
27
27
|
const landed = await by.land([canonical(line(chosen))]);
|
|
28
28
|
return 'refused' in landed ? { said: [sayIn(context.lock)('not-landed', { why: landed.refused })] } : { lines: [...lines, line(chosen)] };
|
|
29
29
|
};
|
|
30
|
-
const turns = new Map();
|
|
31
|
-
/** Calls of one place take turns, so one asked while another lands sees its receipt; nothing waits on a timer. */
|
|
32
|
-
export const inTurn = (place, act) => { const next = (turns.get(place) ?? Promise.resolve()).then(act, act); turns.set(place, next.catch(() => undefined)); return next; };
|
|
33
30
|
/**
|
|
34
|
-
* The steps a read and an act share: the place,
|
|
31
|
+
* The steps a read and an act share: the place, the lines it stands on once bound settled them, the offer's
|
|
35
32
|
* plan, the transport its origin names and the request it prepares; then what is particular, given a frame that says
|
|
36
33
|
* by the lock, attaches access when asked, types what came back and lands a line under head's key.
|
|
37
34
|
*/
|
|
38
35
|
export const called = async (asked, context, planOf, then) => {
|
|
39
36
|
const say = (key, fields = {}) => [sayIn(context.lock)(key, fields)];
|
|
40
37
|
const [source, operation] = [asked['source'] ?? '', asked['operation'] ?? ''];
|
|
41
|
-
return inPlace(asked, context,
|
|
38
|
+
return inPlace(asked, context, async (place) => {
|
|
42
39
|
const held = await standingFor(context, place);
|
|
43
40
|
if ('unsettled' in held)
|
|
44
41
|
return say('unsettled', { source, why: held.unsettled });
|
|
@@ -67,10 +64,10 @@ export const called = async (asked, context, planOf, then) => {
|
|
|
67
64
|
if ('refused' in prepared)
|
|
68
65
|
return say(prepared.refused, { source, origin: plan.origin });
|
|
69
66
|
const most = (scope) => Number((lines.find((l) => l.scope === scope)?.value ?? '0..0').split('..')[1]);
|
|
70
|
-
const typed = async (status, bytes) => ((got) => ['lines' in got ? say('answered', { source, operation, status })[0] : say('untyped', { source, operation, status, why: got.untyped })[0], ...('lines' in got ? got.lines : [])])(await typedAnswer(context, place, lines, source, operation, prepared.key, bytes));
|
|
67
|
+
const typed = async (status, bytes) => ((got, at) => ['lines' in got ? say('answered', { source, operation, status, at })[0] : say('untyped', { source, operation, status, at, why: got.untyped })[0], ...('lines' in got ? got.lines : [])])(await typedAnswer(context, place, lines, source, operation, prepared.key, bytes), new Date(context.now()).toISOString());
|
|
71
68
|
const lands = (...lot) => context.landAs(place, lot, 'head', lines);
|
|
72
69
|
const attach = async () => ((got) => ('said' in got ? { said: say(got.said, { source, operation, kinds: plan.access.kind, input: got.input }) } : got))(await attachmentFor(context, place, lines, plan));
|
|
73
70
|
const uncovered = (role, scopes) => uncoveredIn(context.lock, lines, context.device(role), place, [], scopes);
|
|
74
71
|
return then({ widen, word: (scope) => valueOf(context.lock, scope), say, place, lines, source, operation, plan, prepared, most, typed, attach, send: (attachment) => prepared.send(attachment, most('read/response-bytes')), lands, uncovered });
|
|
75
|
-
})
|
|
72
|
+
});
|
|
76
73
|
};
|
package/dist/src/flows/read.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { under } from "../core/lock.js";
|
|
2
2
|
import { observedAt, planRead } from "../core/read.js";
|
|
3
|
-
import { basisOf, draftOf, receiptsOf, scopeOf
|
|
3
|
+
import { basisOf, draftOf, receiptsOf, scopeOf } from "../core/receipt.js";
|
|
4
4
|
import { redIn, sayIn } from "../core/surface.js";
|
|
5
5
|
import { grantedOf, isOpen, untilOf } from "../core/act.js";
|
|
6
6
|
import { canonical } from '@lapxo/topos/wire';
|
|
7
7
|
import { called } from "./call.js";
|
|
8
8
|
import { principalOf } from "../access/obtain.js";
|
|
9
9
|
import { jsonOf } from "../access/contract.js";
|
|
10
|
+
import { refusedAccess } from "../sources/refused.js";
|
|
11
|
+
import { hand, keptOf } from "../kept/kept.js";
|
|
12
|
+
import { markOf, overOf } from "../ahead/budget.js";
|
|
10
13
|
/** An intent an earlier act left open, closed by the read that looked at what it would have changed: a line naming that read-s digest, signed by head. */
|
|
11
14
|
const closeWith = async (intent, source, digest, { word, lines, lands }, context) => {
|
|
12
15
|
const say = sayIn(context.lock);
|
|
@@ -24,12 +27,12 @@ const closeWith = async (intent, source, digest, { word, lines, lands }, context
|
|
|
24
27
|
* the reuse the place allows, it answers from what was kept: nothing called. Asked for history, it shows what was kept
|
|
25
28
|
* of that request as of when it was read — evidence of that read, never a current answer — where the place allows it.
|
|
26
29
|
*/
|
|
27
|
-
export const readSource = (asked, context) =>
|
|
30
|
+
export const readSource = (asked, context) => called(asked, context, planRead, async ({ word, say, place, lines, source, operation, plan, prepared, most, typed, attach, send, lands, widen, uncovered }) => {
|
|
28
31
|
const [family, at, when] = [word('receipt/family'), word('receipt/at'), new Date(context.now()).toISOString()];
|
|
29
32
|
const closing = (digest) => closeWith(asked['closes'], source, digest, { word, lines, lands }, context);
|
|
30
33
|
const scopes = (principal) => [scopeOf(family, source, operation, prepared.key, principal), `${scopeOf(word('receipt/kept'), source, operation, prepared.key, principal)}/${basisOf(lines, word, source)}`];
|
|
31
34
|
const held = (digest) => ((bytes) => (bytes === undefined || digest === undefined ? undefined : { digest, bytes }))(digest === undefined ? undefined : context.cache.held(digest));
|
|
32
|
-
const keeping = (kept, answer) => (answer.length > 1 ? [draftOf(word('receipt/kept'), at, kept, context.cache.keep(Buffer.from(answer.join('\n'))), when)] : []);
|
|
35
|
+
const keeping = (kept, answer) => (answer.length > 1 ? [draftOf(word('receipt/kept'), at, kept, (asked['ahead'] === 'true' ? String : hand)(context.cache.keep(Buffer.from(answer.join('\n')))), when)] : []);
|
|
33
36
|
const allowed = (key, measure) => (lines.find((l) => l.scope === `${word(key)}/${source}` && l.measure === measure) ?? lines.find((l) => l.scope === word(key) && l.measure === measure))?.value;
|
|
34
37
|
if (asked['why'] === 'true') {
|
|
35
38
|
const asOf = principalOf(context, plan, false), [newest] = asOf === undefined ? [] : receiptsOf(lines, at, scopes(asOf)[0]), pin = lines.find((l) => l.scope === `uses/${source}`);
|
|
@@ -53,43 +56,44 @@ export const readSource = (asked, context) => ((started) => called(asked, contex
|
|
|
53
56
|
if (stopped)
|
|
54
57
|
return stopped;
|
|
55
58
|
if (!grantedOf(lines, word('grant/family'), source, context.now())) {
|
|
59
|
+
if (asked['ahead'] === 'true')
|
|
60
|
+
return [];
|
|
56
61
|
const sources = [...new Set(lines.filter((l) => l.scope?.startsWith(`${word('vocab/region')}/`) && l.measure === word('vocab/origins')).map((l) => l.scope.split('/')[1]))].sort();
|
|
57
62
|
const until = untilOf(word('access/period'), context.now());
|
|
58
|
-
const line = (measure, value) => canonical({ at:
|
|
59
|
-
const
|
|
63
|
+
const line = (measure, value) => canonical({ at: `policy:person/grants/${new Date(context.now()).toISOString()}`, by: 'target', form: 'alphabet', measure, role: 'writes', scope: word('grant/family'), value });
|
|
64
|
+
const before = [...new Set(lines.filter((l) => l.scope === word('grant/family')).map(({ about: _about, sig: _sig, epoch: _epoch, ...l }) => canonical({ ...l, by: 'target', value: 'withdraw' })))];
|
|
65
|
+
const granted = await widen(say('grant', { sources: sources.join(', '), place, until })[0], [...before, line('sources', sources.join('|')), line('until', until)]);
|
|
60
66
|
if ('said' in granted)
|
|
61
67
|
return granted.said;
|
|
62
68
|
}
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (body !== undefined) {
|
|
72
|
-
const cut = body.bytes.indexOf(10);
|
|
73
|
-
const answer = await typed(Buffer.from(body.bytes.subarray(0, cut)).toString('utf8'), body.bytes.subarray(cut + 1));
|
|
74
|
-
const keptLines = keeping(kept, answer);
|
|
75
|
-
const landed = keptLines.length ? await lands(...keptLines) : { lines: [] };
|
|
76
|
-
return [...answer, ...say('reread', { scope, digest: body.digest, at: newest.at }), ...('lines' in landed ? landed.lines : []), ...(await closing(body.digest))];
|
|
77
|
-
}
|
|
69
|
+
const was = principal === undefined ? undefined : keptOf(lines, at, scopes(principal), grantedOf(lines, word('grant/family'), source, context.now()), allowed('reuse/family', 'seconds'), context.now(), held);
|
|
70
|
+
if (was?.kind === 'kept')
|
|
71
|
+
return [...(was.bytes ? [...new TextDecoder().decode(was.bytes).split('\n'), ...say('reused', { scope: scopes(principal)[1], digest: hand(was.digest), at: was.at, receipt: was.receipt })] : say('kept', { source, operation, at: was.at, digest: was.digest, receipt: was.receipt })),
|
|
72
|
+
...(await closing(was.receipt)), ...redIn(context.lock)('answer/kept', `answer/${source}/${operation}`, context.begun, context.machine())];
|
|
73
|
+
if (was?.kind === 'reread') {
|
|
74
|
+
const answer = await typed(Buffer.from(was.bytes.subarray(0, was.bytes.indexOf(10))).toString('utf8'), was.bytes.subarray(was.bytes.indexOf(10) + 1)), keptLines = keeping(scopes(principal)[1], answer);
|
|
75
|
+
const landed = keptLines.length ? await lands(...keptLines) : { lines: [] };
|
|
76
|
+
return [...answer, ...say('reread', { scope: scopes(principal)[0], digest: was.digest, at: was.at, receipt: was.digest }), ...('lines' in landed ? landed.lines : []), ...(await closing(was.digest))];
|
|
78
77
|
}
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
const again = was?.kind === 'again' ? say(`kept-${was.why}`, { source, operation, at: was.at }) : [];
|
|
79
|
+
if (overOf(lines, [family, at, word('ahead/mark')], source, context.now(), most('read/rate'), Number(word('ahead/share').split('..')[1]), asked))
|
|
80
|
+
return asked['ahead'] === 'true' ? [] : say('rate', { source });
|
|
81
81
|
const attached = await attach();
|
|
82
82
|
if ('said' in attached)
|
|
83
83
|
return attached.said;
|
|
84
84
|
const [scope, kept] = scopes(attached.principal ?? '');
|
|
85
85
|
const got = await send(attached.attachment);
|
|
86
|
+
if (asked['ahead'] === 'true' && (got.kind !== 'got' || got.status >= 300))
|
|
87
|
+
return [];
|
|
86
88
|
if (got.kind !== 'got')
|
|
87
89
|
return say(got.kind === 'over' ? 'too-large' : 'unreachable-source', { source });
|
|
90
|
+
if (got.status === 401 && attached.principal !== 'anonymous')
|
|
91
|
+
return refusedAccess(context, place, lines, plan, operation);
|
|
88
92
|
const digest = context.cache.keep(Buffer.concat([Buffer.from(`${got.status}\n`), got.bytes]));
|
|
89
93
|
const answer = await typed(String(got.status), got.bytes);
|
|
90
94
|
const observed = plan.access.probe && got.status < 300 ? [canonical({ at: `${at}:${when}`, by: 'target', form: 'alphabet', measure: word('vocab/access'), role: 'writes', scope: observedAt(context.lock, source, operation), value: word('vocab/anonymous') })] : [];
|
|
91
95
|
const found = plan.access.probe && got.status === 401 && got.authenticate ? await (await context.access(word('access/adapter')))?.discover?.(plan.origin, got.authenticate, async (url) => jsonOf((await context.files.read.documentOf(context, place, url))?.bytes ?? new Uint8Array())) : undefined;
|
|
92
96
|
const by = `${word('access/family')}/${source}/${word('access/authorizer')}`, authorizer = Object.entries(found ?? {}).flatMap(([k, f]) => Object.entries(f).map(([measure, all]) => canonical({ at: `${at}:${when}`, by: 'target', form: 'alphabet', measure, role: 'writes', scope: k ? `${by}/${k}` : by, value: all.join('|') })));
|
|
93
|
-
const landed = await lands(draftOf(family, at, scope, digest, when), ...(attached.principal === undefined ? [] : keeping(kept, answer)), ...observed, ...authorizer);
|
|
94
|
-
return [...answer, ...('lines' in landed ? [...landed.lines, ...(await closing(digest))] : say('not-landed', { why: landed.refused })), ...(found?.['']?.['issuer'] && 'lines' in landed ? say('authorizer-observed', { source, issuer: found['']['issuer'][0] }) : [])];
|
|
95
|
-
})
|
|
97
|
+
const landed = await lands(draftOf(family, at, scope, digest, when), ...markOf(asked, [at, word('ahead/mark')], scope, digest, when), ...(attached.principal === undefined ? [] : keeping(kept, answer)), ...observed, ...authorizer);
|
|
98
|
+
return [...again, ...answer, ...('lines' in landed ? [...landed.lines, ...(await closing(digest))] : say('not-landed', { why: landed.refused })), ...(found?.['']?.['issuer'] && 'lines' in landed ? say('authorizer-observed', { source, issuer: found['']['issuer'][0] }) : [])];
|
|
99
|
+
});
|
package/dist/src/index.js
CHANGED
|
@@ -13,7 +13,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
13
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
14
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
15
15
|
import { answerOf } from "./core/answer.js";
|
|
16
|
-
import {
|
|
16
|
+
import { parse } from '@lapxo/topos/wire';
|
|
17
|
+
import { aboutOf, valueOf } from "./core/lock.js";
|
|
17
18
|
import { fill, sayIn } from "./core/surface.js";
|
|
18
19
|
import { matchTemplate, refusedOn, shapeOf, shapeSchema, surfaceOf } from "./ports/surface.js";
|
|
19
20
|
import { closing, cooled, founded, land, landAct, limits, pinnedAt, policyAt, run, settledView, sign, standingAt, told, warm, words } from "./ports/bound.js";
|
|
@@ -26,18 +27,21 @@ import { keychain } from "./ports/keychain.js";
|
|
|
26
27
|
import { algorithmOf, deviceOf, idIn } from "./core/device.js";
|
|
27
28
|
import { publicOf } from "./ports/keys.js";
|
|
28
29
|
import { personOf } from "./ports/access/person.js";
|
|
29
|
-
import {
|
|
30
|
+
import { readAhead } from "./ahead/read.js";
|
|
30
31
|
const [ext, root] = ((self) => [self.slice(self.lastIndexOf('.')), fileURLToPath(new URL(self.endsWith('.ts') ? '..' : '../..', self))])(import.meta.url);
|
|
31
32
|
const lock = lockAt(root);
|
|
32
33
|
const surface = surfaceOf(lock);
|
|
33
34
|
const runs = valueOf(lock, 'runs');
|
|
34
|
-
Object.assign(words, Object.fromEntries(['read', 'view', 'sign', 'land', 'key', 'signer', 'pin', 'topos', 'standing', 'evidence', 'act', 'profile', 'receipts', 'refusal'].map((w) => [w, valueOf(lock, `bound/${w}`)])));
|
|
35
|
+
Object.assign(words, Object.fromEntries(['read', 'view', 'sign', 'land', 'key', 'signer', 'pin', 'topos', 'standing', 'evidence', 'act', 'profile', 'receipts', 'refusal', 'identity'].map((w) => [w, valueOf(lock, `bound/${w}`)])));
|
|
35
36
|
Object.assign(limits, { wait: Number(valueOf(lock, 'bound/wait').split('..')[1]), most: Number(valueOf(lock, 'bound/printed').split('..')[1]) });
|
|
36
37
|
Object.assign(warmth, { spares: Number(valueOf(lock, 'bound/spares').split('..')[1]), boot: valueOf(lock, 'bound/spare-env').split('|') });
|
|
37
|
-
/** One call at a time,
|
|
38
|
-
|
|
38
|
+
/** One call at a time, each after the one before it and never on a timer, so one asked while another lands sees its receipt; bound started ahead for it, and nothing of it kept once it is answered. */
|
|
39
|
+
let [last, ahead] = [Promise.resolve(), new AbortController()];
|
|
40
|
+
const turn = (act) => ((next) => { last = next.catch(() => undefined); return next; })(last.then(() => { void warm(runs); return act().finally(cooled); }));
|
|
41
|
+
for (const end of ['SIGTERM', 'SIGINT'])
|
|
42
|
+
process.once(end, () => void last.finally(() => process.exit(0)));
|
|
39
43
|
const offered = (n) => (n ? { listChanged: false } : undefined);
|
|
40
|
-
const server = new Server({ name: surface.name, version: surface.version }, { capabilities: { tools: offered(surface.tools.length), resources: offered(surface.resources.length), prompts: offered(surface.prompts.length) } });
|
|
44
|
+
const server = new Server({ name: surface.name, version: surface.version }, { capabilities: { tools: offered(surface.tools.length), resources: offered(surface.resources.length), prompts: offered(surface.prompts.length) }, instructions: aboutOf(lock, 'prose/en/instructions') });
|
|
41
45
|
/** The person, asked as the client lets head ask; every wait on them is counted, so a time ceiling measures head's work and never the person's. */
|
|
42
46
|
let waited = 0;
|
|
43
47
|
const timed = (ask) => async (...args) => { const t = Date.now(); try {
|
|
@@ -58,19 +62,21 @@ const device = (role) => devices.get(role) ?? ((secret) => (secret === undefined
|
|
|
58
62
|
const asRoot = (account) => ((device, key) => [key, device.signer, { BOUND_SIGNERS: registryFor(device.signer, service, account, key, valueOf(lock, 'founding/algorithm'), store) }])(deviceOf(lock), valueOf(lock, 'founding/root').split('/')[1]);
|
|
59
63
|
/** What a call is answered with, one call at a time as when bound blocked head: every bound process it starts is stopped when the client cancels it, and a call cancelled while it waits its turn is never begun. */
|
|
60
64
|
const contextOf = (signal) => {
|
|
61
|
-
/** One call's folds of a place, kept until something lands in it or is laid there;
|
|
62
|
-
const [folds, touched] = [new Map(), new Set()], forget = (place) => { for (const k of [...folds.keys()])
|
|
65
|
+
/** One call's folds of a place, kept until something lands in it or is laid there; the places its root acted in, whose receipts close once, when the call ends; and those where anything but a read's receipts landed, as bound printed it. */
|
|
66
|
+
const [folds, touched, moved] = [new Map(), new Set(), new Set()], forget = (place) => { for (const k of [...folds.keys()])
|
|
63
67
|
if (k.endsWith(`\0${place}`))
|
|
64
68
|
folds.delete(k); };
|
|
69
|
+
const read = (at) => at?.startsWith(`${valueOf(lock, 'receipt/at')}:`) === true;
|
|
65
70
|
const once = (key, place, fold) => (folds.get(`${key}\0${place}`) ?? ((p) => (folds.set(`${key}\0${place}`, p), p))(fold()));
|
|
66
|
-
const acted = (place, root, done) => done.
|
|
71
|
+
const acted = (place, root, done) => done.then((got) => { if ('lines' in got && !got.lines.every((l) => ((f) => f.kind === 'fact' && read(f.value.fields['at']))(parse(l))))
|
|
72
|
+
moved.add(place); return got; }).finally(() => { forget(place); if (root)
|
|
67
73
|
touched.add(place); });
|
|
68
74
|
const signRoot = (place, lot, account) => sign(runs, place, lot, ...asRoot(account), signal);
|
|
69
75
|
const signAs = (place, lot, role, lines) => ((one, id) => sign(runs, place, lot, id, one.signer, { BOUND_SIGNERS: registryFor(one.signer, service, one.account, id, algorithmOf(lines), store) }, signal))(deviceOf(lock, role), idIn(lines, device(role)) ?? deviceOf(lock, role).account);
|
|
70
76
|
return {
|
|
71
77
|
lock,
|
|
72
78
|
bound: (cwd, args) => run(runs, cwd, args, {}, signal),
|
|
73
|
-
standing: (place) => once('standing', place, () => standingAt(runs, place, (lines) => ['head', 'person'].flatMap((role) => ((id) => (id === undefined ? [] : [id]))(idIn(lines, device(role)))), (line) => line.at
|
|
79
|
+
standing: (place) => once('standing', place, () => standingAt(runs, place, (lines) => ['head', 'person'].flatMap((role) => ((id) => (id === undefined ? [] : [id]))(idIn(lines, device(role)))), (line) => read(line.at), signal)),
|
|
74
80
|
policy: (place) => once('policy', place, () => policyAt(runs, place, signal)),
|
|
75
81
|
world: (place, view) => once(`world/${view}`, place, () => settledView(runs, place, view, signal)),
|
|
76
82
|
adapter: async (origin) => ((name) => (!/^[a-z-]+$/.test(name) ? undefined : import(__rewriteRelativeImportExtension(new URL(`./ports/adapters/${name}${ext}`, import.meta.url).href))))(URL.canParse(origin) ? valueOf(lock, `adapter/${new URL(origin).protocol.slice(0, -1)}`) : ''),
|
|
@@ -80,6 +86,7 @@ const contextOf = (signal) => {
|
|
|
80
86
|
found: (place, account) => acted(place, true, founded(runs, place, ...asRoot(account), signal)),
|
|
81
87
|
signRoot,
|
|
82
88
|
landRoot: (place, lot, account) => acted(place, true, landAct(runs, place, lot, ...asRoot(account), signal)),
|
|
89
|
+
moved: (place) => moved.has(place),
|
|
83
90
|
settle: async () => { const red = []; for (const place of touched) {
|
|
84
91
|
const why = await closing(runs, place, signal);
|
|
85
92
|
if (why !== undefined)
|
|
@@ -93,12 +100,13 @@ const contextOf = (signal) => {
|
|
|
93
100
|
landAs: (place, lot, role, lines) => acted(place, false, ((one, id) => landAct(runs, place, lot, id, one.signer, { BOUND_SIGNERS: registryFor(one.signer, service, one.account, id, algorithmOf(lines), store) }, signal))(deviceOf(lock, role), idIn(lines, device(role)) ?? deviceOf(lock, role).account)),
|
|
94
101
|
land: (place, signed) => acted(place, true, land(runs, place, signed, signal)),
|
|
95
102
|
told: (place, signed) => told(runs, place, signed, words.standing, signal),
|
|
96
|
-
elicit: (message) => person.consent(message,
|
|
103
|
+
elicit: (message) => person.consent(message, aboutOf(lock, 'said/elicit/yes')),
|
|
97
104
|
now: () => Date.now(),
|
|
98
105
|
machine: () => Date.now() - waited,
|
|
106
|
+
begun: Date.now() - waited,
|
|
99
107
|
keys,
|
|
100
108
|
device,
|
|
101
|
-
roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []),
|
|
109
|
+
roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []), client: () => ((c) => (c ? `${c.name}@${c.version}` : undefined))(server.getClientVersion()),
|
|
102
110
|
};
|
|
103
111
|
};
|
|
104
112
|
const askedOf = (raw) => Object.fromEntries(Object.entries(raw ?? {}).map(([k, v]) => [k, v === undefined ? undefined : typeof v === 'object' ? JSON.stringify(v) : String(v)]));
|
|
@@ -108,14 +116,15 @@ if (surface.tools.length)
|
|
|
108
116
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: surface.tools.map((tool) => ({ name: tool.name, description: tool.about,
|
|
109
117
|
inputSchema: schemaOf([...tool.fields]), outputSchema: shapeSchema(lock), annotations: Object.fromEntries(tool.hints.map((hint) => [`${hint}Hint`, true])) })) }));
|
|
110
118
|
if (surface.tools.length)
|
|
111
|
-
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => turn(async () => {
|
|
119
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => (ahead.abort(), turn(async () => {
|
|
112
120
|
const tool = surface.tools.find((one) => one.name === request.params.name);
|
|
113
121
|
if (tool === undefined)
|
|
114
122
|
return { isError: true, content: [] };
|
|
115
123
|
const refused = refusedOn(lock, process.platform);
|
|
116
124
|
const context = contextOf(extra.signal), answer = refused !== undefined ? [refused] : extra.signal.aborted ? [] : [...await (await answerOf('tools', tool.name))(askedOf(request.params.arguments), context), ...await context.settle()];
|
|
125
|
+
void ((early) => turn(() => readAhead(shapeOf(answer).cells, contextOf(), early.signal)))(ahead = new AbortController());
|
|
117
126
|
return { content: [{ type: 'text', text: answer.join('\n') }], structuredContent: shapeOf(answer) };
|
|
118
|
-
}));
|
|
127
|
+
})));
|
|
119
128
|
if (surface.resources.length)
|
|
120
129
|
server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
|
|
121
130
|
if (surface.resources.length)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Said } from '../core/lock.ts';
|
|
2
|
+
export declare const hand: (digest: string) => string;
|
|
3
|
+
type Held = (digest: string | undefined) => {
|
|
4
|
+
readonly digest: string;
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
} | undefined;
|
|
7
|
+
export type Was = {
|
|
8
|
+
readonly kind: 'kept';
|
|
9
|
+
readonly at: string;
|
|
10
|
+
readonly digest: string;
|
|
11
|
+
readonly receipt: string;
|
|
12
|
+
readonly bytes?: Uint8Array;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'reread';
|
|
15
|
+
readonly at: string;
|
|
16
|
+
readonly digest: string;
|
|
17
|
+
readonly bytes: Uint8Array;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'again';
|
|
20
|
+
readonly at: string;
|
|
21
|
+
readonly why: 'withdrawn' | 'changed' | 'expired';
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A question read before, as its receipts and the place stand now. Where the place declares a reuse and the newest read
|
|
25
|
+
* stood, it is kept — its bytes only where this conversation does not hold them yet — or typed again from its receipt if
|
|
26
|
+
* nothing typed was kept; but read again, and why, where the person's grant had to be given again, the source is pinned
|
|
27
|
+
* otherwise than when it was read, or the read is older than the reuse allows. Never read, or no reuse: nothing is said.
|
|
28
|
+
*/
|
|
29
|
+
export declare const keptOf: (lines: readonly Said[], at: string, [read, kept]: readonly [string, string], granted: boolean, reuse: string | undefined, now: number, held: Held) => Was | undefined;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { receiptsOf } from "../core/receipt.js";
|
|
2
|
+
/** The typed answers this conversation already holds, by digest: head is one process a conversation, so they last as long as it. */
|
|
3
|
+
const handed = new Set();
|
|
4
|
+
export const hand = (digest) => (handed.add(digest), digest);
|
|
5
|
+
/**
|
|
6
|
+
* A question read before, as its receipts and the place stand now. Where the place declares a reuse and the newest read
|
|
7
|
+
* stood, it is kept — its bytes only where this conversation does not hold them yet — or typed again from its receipt if
|
|
8
|
+
* nothing typed was kept; but read again, and why, where the person's grant had to be given again, the source is pinned
|
|
9
|
+
* otherwise than when it was read, or the read is older than the reuse allows. Never read, or no reuse: nothing is said.
|
|
10
|
+
*/
|
|
11
|
+
export const keptOf = (lines, at, [read, kept], granted, reuse, now, held) => {
|
|
12
|
+
const [newest] = receiptsOf(lines, at, read), body = held(newest?.digest), window = Number(/^\d+\.\.(\d+)$/.exec(reuse ?? '')?.[1] ?? Number.NaN);
|
|
13
|
+
if (newest === undefined || body === undefined || Number.isNaN(window) || Number(Buffer.from(body.bytes.subarray(0, body.bytes.indexOf(10))).toString('utf8')) >= 300)
|
|
14
|
+
return undefined;
|
|
15
|
+
const typed = receiptsOf(lines, at, kept.slice(0, kept.lastIndexOf('/')), true).filter((k) => k.at >= newest.at), answered = held(typed.find((k) => k.scope === kept)?.digest);
|
|
16
|
+
const why = !granted ? 'withdrawn' : typed.length && !typed.some((k) => k.scope === kept) ? 'changed' : now - Date.parse(newest.at) > window * 1000 ? 'expired' : undefined;
|
|
17
|
+
if (why !== undefined)
|
|
18
|
+
return { kind: 'again', at: newest.at, why };
|
|
19
|
+
return answered === undefined ? { kind: 'reread', at: newest.at, digest: body.digest, bytes: body.bytes } : { kind: 'kept', at: newest.at, digest: answered.digest, receipt: newest.digest, ...(handed.has(answered.digest) ? {} : { bytes: answered.bytes }) };
|
|
20
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Asked, type Context } from '../core/answer.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A #bound: link connected: inspected by the instrument in the person's place, never authenticated — its bytes are the
|
|
4
|
+
* link's, and nothing shows who shared it — and what it names is shown; nothing is connected or granted, since a package
|
|
5
|
+
* is received by the digest its sharer sent apart. A place that admits no content transport is handed the lines that admit
|
|
6
|
+
* one, to sign; bytes that do not arrive are owed by the link until they do; bytes that are not what it names are refused.
|
|
7
|
+
*/
|
|
8
|
+
export declare const linked: (asked: Asked, context: Context) => Promise<readonly string[]>;
|