@lapxo/head 0.1.5 → 0.1.7
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 +6 -4
- package/TARGET.bound +245 -121
- package/dist/src/access/contract.d.ts +0 -2
- package/dist/src/access/obtain.js +3 -3
- package/dist/src/ahead/budget.d.ts +4 -0
- package/dist/src/ahead/budget.js +8 -0
- package/dist/src/answers/answered.d.ts +4 -0
- package/dist/src/answers/answered.js +22 -0
- package/dist/src/claims/claim.d.ts +1 -0
- package/dist/src/claims/claim.js +15 -15
- package/dist/src/claims/view.d.ts +3 -0
- package/dist/src/claims/view.js +11 -0
- package/dist/src/compile/pin.d.ts +7 -0
- package/dist/src/compile/pin.js +8 -13
- package/dist/src/core/answer.d.ts +1 -0
- package/dist/src/core/answer.js +1 -0
- package/dist/src/core/device.d.ts +3 -6
- package/dist/src/core/device.js +7 -10
- package/dist/src/core/lock.d.ts +1 -0
- package/dist/src/core/lock.js +2 -1
- package/dist/src/core/read.js +1 -2
- package/dist/src/core/worlds.js +2 -3
- package/dist/src/doors/guard.d.ts +18 -0
- package/dist/src/doors/guard.js +63 -0
- package/dist/src/doors/head.d.ts +60 -0
- package/dist/src/doors/head.js +136 -0
- package/dist/src/doors/http.d.ts +3 -0
- package/dist/src/doors/http.js +34 -0
- package/dist/src/doors/lines.d.ts +3 -0
- package/dist/src/doors/lines.js +21 -0
- package/dist/src/flows/act.d.ts +1 -5
- package/dist/src/flows/act.js +1 -5
- package/dist/src/flows/call.d.ts +2 -9
- package/dist/src/flows/call.js +12 -12
- package/dist/src/flows/device.js +4 -6
- package/dist/src/flows/plan.js +7 -10
- package/dist/src/flows/read.d.ts +1 -7
- package/dist/src/flows/read.js +6 -9
- package/dist/src/index.js +20 -114
- package/dist/src/links/inspect.d.ts +1 -0
- package/dist/src/links/inspect.js +9 -8
- package/dist/src/onboard/first.js +1 -1
- package/dist/src/places/admit.js +1 -1
- package/dist/src/places/found.js +6 -10
- package/dist/src/ports/adapters/request.d.ts +1 -0
- package/dist/src/ports/adapters/request.js +2 -2
- package/dist/src/ports/bound.d.ts +15 -18
- package/dist/src/ports/bound.js +33 -30
- package/dist/src/ports/host.d.ts +35 -0
- package/dist/src/ports/host.js +108 -0
- package/dist/src/ports/keychain.d.ts +14 -2
- package/dist/src/ports/keychain.js +19 -7
- package/dist/src/ports/scratch.d.ts +4 -0
- package/dist/src/ports/scratch.js +10 -0
- package/dist/src/signer.js +8 -7
- package/dist/src/tools/card.js +3 -6
- package/dist/src/tools/look.js +5 -5
- package/dist/src/use/named.d.ts +12 -0
- package/dist/src/use/named.js +25 -0
- package/dist/src/verbs/ask.d.ts +1 -9
- package/dist/src/verbs/ask.js +4 -11
- package/dist/src/verbs/card.d.ts +2 -2
- package/dist/src/verbs/card.js +3 -3
- package/dist/src/walk/context.js +3 -6
- package/package.json +3 -3
- package/dist/src/ports/signers.d.ts +0 -6
- package/dist/src/ports/signers.js +0 -14
- package/dist/src/ports/spare.d.ts +0 -1
- package/dist/src/ports/spare.js +0 -33
- package/dist/src/ports/warm.d.ts +0 -19
- package/dist/src/ports/warm.js +0 -45
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { basename, join } from 'node:path';
|
|
10
|
+
import { answerOf, placeOf } from "../core/answer.js";
|
|
11
|
+
import { LOCK, parse } from '@lapxo/topos/wire';
|
|
12
|
+
import { aboutOf, members, valueOf } from "../core/lock.js";
|
|
13
|
+
import { sayIn } from "../core/surface.js";
|
|
14
|
+
import { matchTemplate, refusedOn, shapeOf, surfaceOf } from "../ports/surface.js";
|
|
15
|
+
import { birth, closing, founded, land, landAct, limits, pinnedAt, policyAt, run, settledView, sign, standingAt, told, words } from "../ports/bound.js";
|
|
16
|
+
import { configureHost } from "../ports/host.js";
|
|
17
|
+
import { held, keep } from "../ports/cache.js";
|
|
18
|
+
import { dropAt, hasAt, layAt, listAt, takeAt } from "../ports/place.js";
|
|
19
|
+
import * as documents from "../ports/documents.js";
|
|
20
|
+
import { keychain, portOf } from "../ports/keychain.js";
|
|
21
|
+
import { deviceOf, idIn, signingOf } from "../core/device.js";
|
|
22
|
+
import { personOf } from "../ports/access/person.js";
|
|
23
|
+
import { readAhead } from "../ahead/read.js";
|
|
24
|
+
import { cardAt } from "../verbs/card.js";
|
|
25
|
+
import { digestOf, guardOf, refusalsOf } from "./guard.js";
|
|
26
|
+
export const askedOf = (raw) => Object.fromEntries(Object.entries(raw ?? {}).map(([k, v]) => [k, v === undefined ? undefined : typeof v === 'object' ? JSON.stringify(v) : String(v)]));
|
|
27
|
+
/** head behind a door: its lock read, every call answered one at a time and each answer's next read ahead, the same for every door. */
|
|
28
|
+
export const headOf = (lock, root, ext, door) => {
|
|
29
|
+
const surface = surfaceOf(lock), runs = valueOf(lock, 'runs'), refused = refusedOn(lock, process.platform);
|
|
30
|
+
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}`)])));
|
|
31
|
+
Object.assign(limits, { wait: Number(valueOf(lock, 'bound/wait').split('..')[1]), most: Number(valueOf(lock, 'bound/printed').split('..')[1]) });
|
|
32
|
+
Object.assign(birth, { at: valueOf(lock, 'founding/person-at'), file: valueOf(lock, 'founding/birth-file') });
|
|
33
|
+
configureHost(lock, limits);
|
|
34
|
+
/** 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; a place's reads go to its fold host, which outlives the call and never holds a signer. */
|
|
35
|
+
let [last, ahead] = [Promise.resolve(), new AbortController()];
|
|
36
|
+
const turn = (act) => ((next) => { last = next.catch(() => undefined); return next; })(last.then(act));
|
|
37
|
+
/** What head refuses through this door, written as every door writes it; a door that reaches for a signing key is one such refusal, named by the lock's line for that key and never by what the door asked. */
|
|
38
|
+
const [refusals, keyRefused] = [refusalsOf(lock, { write: (place, lot) => write(place, lot), now: () => Date.now() }, door.name), shapeOf([sayIn(lock)('signing-key-refused', {})]).cells[0].code];
|
|
39
|
+
for (const end of ['SIGTERM', 'SIGINT'])
|
|
40
|
+
process.once(end, () => void last.then(refusals.settle).finally(() => process.exit(0)));
|
|
41
|
+
/** 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
|
+
let waited = 0;
|
|
43
|
+
const timed = (ask) => async (...args) => { const t = Date.now(); try {
|
|
44
|
+
return await ask(...args);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
waited += Date.now() - t;
|
|
48
|
+
} };
|
|
49
|
+
const asked = personOf(lock, door);
|
|
50
|
+
const person = { consent: timed(asked.consent), enter: timed(asked.enter), choose: timed(asked.choose), visit: timed(asked.visit) };
|
|
51
|
+
/** head is its home: the folder of its index of places, and the keychain service its keys live under, one per home, so two heads on one keychain never share a key. A place's root is told to bound by the key id its lock names, its secret kept there under the place's account. */
|
|
52
|
+
const home = process.env[valueOf(lock, 'home/env')] || join(process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '', valueOf(lock, 'home/folder'));
|
|
53
|
+
const service = `${deviceOf(lock).service}/${basename(home).replace(/[^\w.-]+/g, '-')}`;
|
|
54
|
+
const store = valueOf(lock, `platform/${process.platform}/store`);
|
|
55
|
+
const keys = keychain(service, store, signingOf(lock));
|
|
56
|
+
const devices = new Map();
|
|
57
|
+
const device = (role) => devices.get(role) ?? ((key) => (key === undefined ? undefined : (devices.set(role, key), key)))(keys.held?.(deviceOf(lock, role).account));
|
|
58
|
+
const asRoot = (account) => ((device, key) => [key, device.signer, portOf(service, account, store, limits.wait)])(deviceOf(lock), valueOf(lock, 'founding/root').split('/')[1]);
|
|
59
|
+
const asRole = (role, lines) => ((one, id) => [id, one.signer, portOf(service, one.account, store, limits.wait)])(deviceOf(lock, role), idIn(lines, device(role)) ?? deviceOf(lock, role).account);
|
|
60
|
+
/** 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. */
|
|
61
|
+
const contextOf = (signal) => {
|
|
62
|
+
/** 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. */
|
|
63
|
+
const [folds, touched, moved] = [new Map(), new Set(), new Set()], forget = (place) => { for (const k of [...folds.keys()])
|
|
64
|
+
if (k.endsWith(`\0${place}`))
|
|
65
|
+
folds.delete(k); };
|
|
66
|
+
const read = (at) => at?.startsWith(`${valueOf(lock, 'receipt/at')}:`) === true;
|
|
67
|
+
const once = (key, place, fold) => (folds.get(`${key}\0${place}`) ?? ((p) => (folds.set(`${key}\0${place}`, p), p))(fold()));
|
|
68
|
+
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))))
|
|
69
|
+
moved.add(place); return got; }).finally(() => { forget(place); if (root)
|
|
70
|
+
touched.add(place); });
|
|
71
|
+
return {
|
|
72
|
+
lock,
|
|
73
|
+
bound: (cwd, args) => run(runs, cwd, args, {}, signal),
|
|
74
|
+
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)),
|
|
75
|
+
policy: (place) => once('policy', place, () => policyAt(runs, place, signal)),
|
|
76
|
+
world: (place, view) => once(`world/${view}`, place, () => settledView(runs, place, view, signal)),
|
|
77
|
+
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)}`) : ''),
|
|
78
|
+
files: { lay: (place, rel, bytes) => { forget(place); layAt(place, rel, bytes); }, drop: (place, rel) => { forget(place); dropAt(place, rel); }, has: hasAt, list: listAt, read: documents, take: takeAt, own: (rel) => takeAt(join(root, rel)) },
|
|
79
|
+
home: () => home,
|
|
80
|
+
personal: () => process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '',
|
|
81
|
+
found: (place, account) => acted(place, true, founded(runs, place, ...asRoot(account), signal)),
|
|
82
|
+
signRoot: (place, lot, account) => sign(runs, place, lot, ...asRoot(account), signal),
|
|
83
|
+
landRoot: (place, lot, account) => acted(place, true, landAct(runs, place, lot, ...asRoot(account), signal)),
|
|
84
|
+
moved: (place) => moved.has(place),
|
|
85
|
+
settle: async () => { const red = []; for (const place of touched) {
|
|
86
|
+
const why = await closing(runs, place, signal);
|
|
87
|
+
if (why !== undefined)
|
|
88
|
+
red.push(sayIn(lock)('receipts-open', { place, why }));
|
|
89
|
+
} touched.clear(); return red; },
|
|
90
|
+
pinned: (place, file) => pinnedAt(runs, place, file, signal),
|
|
91
|
+
access: async (kind) => (kind !== valueOf(lock, 'access/adapter') || !/^[a-z0-9-]+$/.test(kind) ? undefined : import(__rewriteRelativeImportExtension(new URL(`../ports/access/${kind}${ext}`, import.meta.url).href)).then((m) => m.adapter)),
|
|
92
|
+
person,
|
|
93
|
+
cache: { keep, held },
|
|
94
|
+
signAs: (place, lot, role, lines) => sign(runs, place, lot, ...asRole(role, lines), signal),
|
|
95
|
+
landAs: (place, lot, role, lines) => acted(place, false, landAct(runs, place, lot, ...asRole(role, lines), signal)),
|
|
96
|
+
land: (place, signed) => acted(place, true, land(runs, place, signed, signal)),
|
|
97
|
+
told: (place, signed) => told(runs, place, signed, words.standing, signal),
|
|
98
|
+
elicit: (message) => person.consent(message, aboutOf(lock, 'said/elicit/yes')),
|
|
99
|
+
now: () => Date.now(),
|
|
100
|
+
machine: () => Date.now() - waited,
|
|
101
|
+
begun: Date.now() - waited,
|
|
102
|
+
keys,
|
|
103
|
+
device,
|
|
104
|
+
roots: door.roots, client: door.client,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
const call = (name, args, signal) => (ahead.abort(), turn(async () => {
|
|
108
|
+
const tool = surface.tools.find((one) => one.name === name), place = typeof args?.['place'] === 'string' ? args['place'] : '', keyed = (e) => ((key) => { if (key === undefined || !lock.some((l) => l.scope === key))
|
|
109
|
+
throw e; return [sayIn(lock)('signing-key-refused', { key, place })]; })(/^REFUSE·keychain (\S+) names a signing key/.exec(e.message)?.[1]);
|
|
110
|
+
if (tool === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
const context = contextOf(signal), answer = refused !== undefined ? [refused] : signal.aborted ? [] : [...await (await answerOf('tools', tool.name))(askedOf(args), context).catch(keyed), ...await context.settle()];
|
|
113
|
+
void ((early) => turn(() => readAhead(shapeOf(answer).cells, contextOf(), early.signal)))(ahead = new AbortController());
|
|
114
|
+
return answer;
|
|
115
|
+
}).then(async (answer) => { if (answer !== undefined && shapeOf(answer).cells.some((c) => c.code === keyRefused))
|
|
116
|
+
await refusals.write(typeof args?.['place'] === 'string' ? args['place'] : await placed(), name, digestOf(name, args ?? {}), keyRefused); return answer; }));
|
|
117
|
+
const resource = (uri, signal) => turn(async () => {
|
|
118
|
+
const found = surface.resources.flatMap((r) => ((vars) => (vars === undefined ? [] : [{ r, vars }]))(matchTemplate(r.template, uri)))[0];
|
|
119
|
+
if (found === undefined)
|
|
120
|
+
return undefined;
|
|
121
|
+
if (refused !== undefined)
|
|
122
|
+
return { mime: found.r.mime, lines: [refused] };
|
|
123
|
+
return { mime: found.r.mime, lines: await (await answerOf('resources', found.r.name))(askedOf(found.vars), contextOf(signal)) };
|
|
124
|
+
});
|
|
125
|
+
/** A place's card now, as do reads it; whether head's index names a place yet, and the one it names; lines head's own key lands in a place, on its standing. */
|
|
126
|
+
const [cardOf, placed] = [(place) => turn(() => cardAt(contextOf(), place)), () => turn(() => placeOf({}, contextOf()))];
|
|
127
|
+
const hasPlace = () => turn(async () => ((c) => (!c.files.has(home, LOCK) ? false : c.policy(home).then((held) => !('refused' in held) && held.lines.some((l) => l.scope?.startsWith(`${valueOf(lock, 'home/family')}/`) && l.value !== 'withdraw'))))(contextOf()));
|
|
128
|
+
const write = (place, lot) => turn(async () => ((c) => c.standing(place).then((held) => ('refused' in held ? held : c.landAs(place, lot, 'head', held.lines))))(contextOf()));
|
|
129
|
+
return { surface, call, resource, idle: () => last, cardOf, placed, hasPlace, write, refusals, now: () => contextOf().now() };
|
|
130
|
+
};
|
|
131
|
+
/** A door that shows no form: its verbs, each answered by its tool through the guard every such door asks, a verb that is no tool carried as its carrier's action; undefined for a verb it lacks. */
|
|
132
|
+
export const plainOf = (lock, root, ext, name) => {
|
|
133
|
+
const [head, verbs, carrier] = [headOf(lock, root, ext, { can: () => ({ url: false, form: false }), elicit: async () => ({ action: 'decline' }), roots: async () => [], client: () => undefined, name }), members(valueOf(lock, 'doors/verbs')), valueOf(lock, 'doors/carrier')];
|
|
134
|
+
const guard = guardOf(lock, head, (verb, args) => ((tool) => head.call(tool ? verb : carrier, tool ? args : { ...args, action: verb }, new AbortController().signal))(head.surface.tools.some((t) => t.name === verb)));
|
|
135
|
+
return { head, verbs, settle: guard.settle, asked: async (verb, args) => (verbs.includes(verb) ? guard.call(verb, args) : undefined) };
|
|
136
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type Said } from '../core/lock.ts';
|
|
2
|
+
/** The http door, on this device's loopback alone: one POST a verb, its body the args, its answer the cells every door gives; a Host not its own, or any Origin, is refused by name. It ends when its input does. */
|
|
3
|
+
export declare const open: (lock: readonly Said[], root: string, ext: string, name: string) => Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { valueOf } from "../core/lock.js";
|
|
3
|
+
import { sayIn } from "../core/surface.js";
|
|
4
|
+
import { shapeOf } from "../ports/surface.js";
|
|
5
|
+
import { plainOf } from "./head.js";
|
|
6
|
+
/** The http door, on this device's loopback alone: one POST a verb, its body the args, its answer the cells every door gives; a Host not its own, or any Origin, is refused by name. It ends when its input does. */
|
|
7
|
+
export const open = (lock, root, ext, name) => new Promise((done) => {
|
|
8
|
+
const [loopback, asked, say] = [valueOf(lock, 'doors/loopback'), process.argv[4] ?? '0', sayIn(lock)];
|
|
9
|
+
if (!/^\d+$/.test(asked)) {
|
|
10
|
+
process.stdout.write(`${JSON.stringify(shapeOf([say('door-bind', { asked, loopback })]))}\n`);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
done();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const door = plainOf(lock, root, ext, name);
|
|
16
|
+
const server = createServer((request, out) => {
|
|
17
|
+
const [own, answer] = [`${loopback}:${server.address().port}`, (status, lines) => out.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify(shapeOf(lines)))];
|
|
18
|
+
if (request.headers.origin !== undefined || request.headers.host !== own)
|
|
19
|
+
return void answer(403, [say(request.headers.origin !== undefined ? 'door-origin' : 'door-host', { origin: request.headers.origin ?? '', host: request.headers.host ?? '', own })]);
|
|
20
|
+
let body = '';
|
|
21
|
+
request.on('data', (chunk) => { body += chunk; }).on('end', async () => {
|
|
22
|
+
const args = ((t) => { try {
|
|
23
|
+
return JSON.parse(t || '{}');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
} })(body);
|
|
28
|
+
const lines = request.method === 'POST' && args !== null && typeof args === 'object' && !Array.isArray(args) ? await door.asked((request.url ?? '').slice(1), args) : undefined;
|
|
29
|
+
answer(lines === undefined ? 400 : 200, lines ?? [say('door-unasked', { verbs: door.verbs.join(', ') })]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
server.listen(Number(asked), loopback, () => process.stdout.write(`${JSON.stringify({ port: server.address().port })}\n`));
|
|
33
|
+
process.stdin.on('end', () => server.close(() => void door.settle().then(door.head.idle).then(() => done()))).resume();
|
|
34
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { sayIn } from "../core/surface.js";
|
|
3
|
+
import { shapeOf } from "../ports/surface.js";
|
|
4
|
+
import { plainOf } from "./head.js";
|
|
5
|
+
/** The lines door: one JSON request a line in, its answer one JSON line out, the cells as every door gives them. */
|
|
6
|
+
export const open = (lock, root, ext, name) => new Promise((done) => {
|
|
7
|
+
const [door, say] = [plainOf(lock, root, ext, name), sayIn(lock)];
|
|
8
|
+
const answered = (id, answer) => process.stdout.write(`${JSON.stringify({ id, ...shapeOf(answer ?? [say('door-unread', { verbs: door.verbs.join(', ') })]) })}\n`);
|
|
9
|
+
let pending = Promise.resolve();
|
|
10
|
+
createInterface({ input: process.stdin }).on('line', (text) => {
|
|
11
|
+
pending = pending.then(async () => {
|
|
12
|
+
const asked = ((t) => { try {
|
|
13
|
+
return JSON.parse(t);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
} })(text);
|
|
18
|
+
answered(asked.id, await door.asked(asked.verb ?? '', asked.args));
|
|
19
|
+
});
|
|
20
|
+
}).on('close', () => void pending.then(door.settle).then(door.head.idle).then(() => done()));
|
|
21
|
+
});
|
package/dist/src/flows/act.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
1
|
import type { Asked, Context } from '../core/answer.ts';
|
|
2
|
-
/**
|
|
3
|
-
* One offer that is not of the class head reads: within a cap the person signed it is called as far as the cap goes;
|
|
4
|
-
* without one, each call asks the person once, and their yes is signed for exactly that request. The intent lands before
|
|
5
|
-
* the call, the receipt after; a call that never answered stays open until a look of what it would have changed closes it.
|
|
6
|
-
*/
|
|
2
|
+
/** Run one action within its declared authority, with the person's approval when required. */
|
|
7
3
|
export declare const act: (asked: Asked, context: Context) => Promise<readonly string[]>;
|
package/dist/src/flows/act.js
CHANGED
|
@@ -4,11 +4,7 @@ import { draftOf } from "../core/receipt.js";
|
|
|
4
4
|
import { sayIn } from "../core/surface.js";
|
|
5
5
|
import { canonical } from '@lapxo/topos/wire';
|
|
6
6
|
import { called } from "./call.js";
|
|
7
|
-
/**
|
|
8
|
-
* One offer that is not of the class head reads: within a cap the person signed it is called as far as the cap goes;
|
|
9
|
-
* without one, each call asks the person once, and their yes is signed for exactly that request. The intent lands before
|
|
10
|
-
* the call, the receipt after; a call that never answered stays open until a look of what it would have changed closes it.
|
|
11
|
-
*/
|
|
7
|
+
/** Run one action within its declared authority, with the person's approval when required. */
|
|
12
8
|
export const act = (asked, context) => called(asked, context, planAct, async ({ word, say, lines, source, operation, prepared, typed, attach, send, lands, widen, uncovered }) => {
|
|
13
9
|
const [family, done] = [word('act/family'), word('act/done')];
|
|
14
10
|
const allowed = allowedOf(lines, word('allow/family'), source, operation);
|
package/dist/src/flows/call.d.ts
CHANGED
|
@@ -3,10 +3,7 @@ import type { Said } from '../core/lock.ts';
|
|
|
3
3
|
import type { Call, Plan } from '../core/read.ts';
|
|
4
4
|
import type { Attachment } from '../access/contract.ts';
|
|
5
5
|
import type { Fetched, Prepared } from '../ports/adapters/request.ts';
|
|
6
|
-
/**
|
|
7
|
-
* The origin of a source a person picks once for a place, when its document declares several: offered as a choice among
|
|
8
|
-
* them, none a default, and kept as the person's line; the lines a call stands on, with it.
|
|
9
|
-
*/
|
|
6
|
+
/** Ask for one declared origin when the source offers several. */
|
|
10
7
|
export declare const pickedOrigin: (context: Context, place: string, lines: readonly Said[], source: string, plan: Call | Plan) => Promise<{
|
|
11
8
|
readonly lines: readonly Said[];
|
|
12
9
|
} | {
|
|
@@ -40,9 +37,5 @@ export interface Frame {
|
|
|
40
37
|
readonly send: (attachment: Attachment | undefined) => Promise<Fetched>;
|
|
41
38
|
readonly uncovered: (role: string, scopes: readonly string[]) => readonly string[] | undefined;
|
|
42
39
|
}
|
|
43
|
-
/**
|
|
44
|
-
* The steps a read and an act share: the place, the lines it stands on once bound settled them, the offer's
|
|
45
|
-
* plan, the transport its origin names and the request it prepares; then what is particular, given a frame that says
|
|
46
|
-
* by the lock, attaches access when asked, types what came back and lands a line under head's key.
|
|
47
|
-
*/
|
|
40
|
+
/** Prepare one call, authorize its access and pass its frame to the read or action flow. */
|
|
48
41
|
export declare const called: (asked: Asked, context: Context, planOf: (lines: readonly Said[], lock: readonly Said[], source: string, operation: string, place: string) => Call | Plan, then: (frame: Frame) => Promise<readonly string[]>) => Promise<readonly string[]>;
|
package/dist/src/flows/call.js
CHANGED
|
@@ -5,13 +5,12 @@ import { sayIn } from "../core/surface.js";
|
|
|
5
5
|
import { valueOf } from "../core/lock.js";
|
|
6
6
|
import { uncoveredIn } from "../core/device.js";
|
|
7
7
|
import { widening } from "./device.js";
|
|
8
|
+
import { namedIn } from "../use/named.js";
|
|
9
|
+
import { answering } from "../answers/answered.js";
|
|
8
10
|
import { attachmentFor } from "../access/obtain.js";
|
|
9
11
|
import { typedAnswer } from "./answer.js";
|
|
10
12
|
import { standingFor } from "./world.js";
|
|
11
|
-
/**
|
|
12
|
-
* The origin of a source a person picks once for a place, when its document declares several: offered as a choice among
|
|
13
|
-
* them, none a default, and kept as the person's line; the lines a call stands on, with it.
|
|
14
|
-
*/
|
|
13
|
+
/** Ask for one declared origin when the source offers several. */
|
|
15
14
|
export const pickedOrigin = async (context, place, lines, source, plan) => {
|
|
16
15
|
if (plan.kind !== 'said' || plan.key !== 'pick-origin')
|
|
17
16
|
return { lines };
|
|
@@ -27,18 +26,18 @@ export const pickedOrigin = async (context, place, lines, source, plan) => {
|
|
|
27
26
|
const landed = await by.land([canonical(line(chosen))]);
|
|
28
27
|
return 'refused' in landed ? { said: [sayIn(context.lock)('not-landed', { why: landed.refused })] } : { lines: [...lines, line(chosen)] };
|
|
29
28
|
};
|
|
30
|
-
/**
|
|
31
|
-
* The steps a read and an act share: the place, the lines it stands on once bound settled them, the offer's
|
|
32
|
-
* plan, the transport its origin names and the request it prepares; then what is particular, given a frame that says
|
|
33
|
-
* by the lock, attaches access when asked, types what came back and lands a line under head's key.
|
|
34
|
-
*/
|
|
29
|
+
/** Prepare one call, authorize its access and pass its frame to the read or action flow. */
|
|
35
30
|
export const called = async (asked, context, planOf, then) => {
|
|
36
31
|
const say = (key, fields = {}) => [sayIn(context.lock)(key, fields)];
|
|
37
|
-
const
|
|
32
|
+
const source = asked['source'] ?? '';
|
|
38
33
|
return inPlace(asked, context, async (place) => {
|
|
39
34
|
const held = await standingFor(context, place);
|
|
40
35
|
if ('unsettled' in held)
|
|
41
36
|
return say('unsettled', { source, why: held.unsettled });
|
|
37
|
+
const named = await namedIn(context, held.lines, source, asked['operation'] ?? '', asked['ahead'] === 'true');
|
|
38
|
+
if ('said' in named)
|
|
39
|
+
return named.said;
|
|
40
|
+
const operation = named.operation;
|
|
42
41
|
const widen = async (message, lot) => {
|
|
43
42
|
const by = widening(context, place, held.lines, lot);
|
|
44
43
|
if ('said' in by)
|
|
@@ -65,9 +64,10 @@ export const called = async (asked, context, planOf, then) => {
|
|
|
65
64
|
return say(prepared.refused, { source, origin: plan.origin });
|
|
66
65
|
const most = (scope) => Number((lines.find((l) => l.scope === scope)?.value ?? '0..0').split('..')[1]);
|
|
67
66
|
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());
|
|
68
|
-
const lands = (...lot) => context.landAs(place, lot, 'head', lines);
|
|
69
67
|
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));
|
|
70
68
|
const uncovered = (role, scopes) => uncoveredIn(context.lock, lines, context.device(role), place, [], scopes);
|
|
71
|
-
|
|
69
|
+
let use = named.use !== undefined && uncovered('head', [named.scope]) === undefined ? [named.use] : [];
|
|
70
|
+
const lands = (...lot) => { const once = use; use = []; return context.landAs(place, [...once, ...lot], 'head', lines).then((out) => ('lines' in out ? { lines: out.lines.filter((l) => !l.includes(` scope=${valueOf(context.lock, 'use/family')}/`)) } : out)); };
|
|
71
|
+
return then(answering(context, place, asked, { 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 }));
|
|
72
72
|
});
|
|
73
73
|
};
|
package/dist/src/flows/device.js
CHANGED
|
@@ -7,19 +7,17 @@ import { adoptsIn } from "../adopt/adopt.js";
|
|
|
7
7
|
/** A key of this device by its role: read from the keychain, or made there once; only its public half is ever handed on. */
|
|
8
8
|
export const deviceKey = (context, role) => context.keys.made?.(deviceOf(context.lock, role).account);
|
|
9
9
|
/** The keychain account of a place's root, when this keychain holds it — who configures the place is here; else what head says of that. */
|
|
10
|
-
export const rootIn = (context, place) => ((account) => (context.keys.
|
|
10
|
+
export const rootIn = (context, place) => ((account) => (context.keys.held?.(account) === undefined ? { said: [sayIn(context.lock)('no-root', { place })] } : { account }))(fill(valueOf(context.lock, 'founding/root-account'), { place: placeNameOf(place) }));
|
|
11
11
|
export const widening = (context, place, lines, lot) => {
|
|
12
12
|
const parent = adoptsIn(context.lock, lines);
|
|
13
13
|
if (parent !== undefined)
|
|
14
14
|
return { said: [sayIn(context.lock)('parent-lock', { place, parent })] };
|
|
15
15
|
const [root, witness, field] = [rootIn(context, place), idIn(lines, context.device('person')), valueOf(context.lock, 'widening/witness')];
|
|
16
|
-
const wire = lines.filter((l) => l.scope === 'wire/fields');
|
|
16
|
+
const wire = lines.filter((l) => l.scope === 'wire/fields'), admits = wire.some((l) => members(l.value ?? '').includes(field));
|
|
17
17
|
const witnessed = (signed) => signed.map((line) => ((got) => (got.kind === 'fact' ? canonical({ ...got.value.fields, [field]: witness }) : line))(parse(line)));
|
|
18
18
|
if ('account' in root && witness !== undefined) {
|
|
19
|
-
if (
|
|
20
|
-
return { sign: (signed) => context.signRoot(place, witnessed(signed), root.account), land: (signed) => context.landRoot(place, witnessed(signed), root.account) };
|
|
21
|
-
if (lot.every((line) => / scope=wire\/fields /.test(line)))
|
|
22
|
-
return { sign: (signed) => context.signRoot(place, signed, root.account), land: (signed) => context.landRoot(place, signed, root.account) };
|
|
19
|
+
if (admits || lot.every((line) => / scope=wire\/fields /.test(line)))
|
|
20
|
+
return { sign: (signed) => context.signRoot(place, admits ? witnessed(signed) : signed, root.account), land: (signed) => context.landRoot(place, admits ? witnessed(signed) : signed, root.account) };
|
|
23
21
|
const [named, bare] = [[...new Set([...wire.flatMap((l) => members(l.value ?? '')), field])].join('|'), (l) => Object.fromEntries(Object.entries(l).filter(([k]) => !['sig', 'epoch'].includes(k)))];
|
|
24
22
|
return { said: [sayIn(context.lock)('unwitnessed', { place }), ...wire.map((l) => canonical({ ...bare(l), by: 'target', value: 'withdraw' })),
|
|
25
23
|
canonical({ at: `policy:head/wire/${new Date(context.now()).toISOString()}`, by: 'target', form: 'alphabet', measure: 'id', role: 'reads', scope: 'wire/fields', value: named })] };
|
package/dist/src/flows/plan.js
CHANGED
|
@@ -2,6 +2,7 @@ import { baseOf, inPlace, standingOr } from "../core/answer.js";
|
|
|
2
2
|
import { sayIn } from "../core/surface.js";
|
|
3
3
|
import { widening } from "./device.js";
|
|
4
4
|
import { parse } from '@lapxo/topos/wire';
|
|
5
|
+
const moved = async (context, place, base) => ((now) => ('said' in now ? now.said : baseOf(now.lines) !== base ? [sayIn(context.lock)('base-moved', { base })] : undefined))(await standingOr(context, place));
|
|
5
6
|
/**
|
|
6
7
|
* A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
|
|
7
8
|
* their yes to exactly those, signed by their key; what bound prints folding it as if landed comes back as it printed it. Nothing lands; the lot is kept by its digest.
|
|
@@ -26,11 +27,9 @@ export const planLot = (asked, context) => inPlace(asked, context, async (place)
|
|
|
26
27
|
const signed = await by.sign(lot);
|
|
27
28
|
if ('refused' in signed)
|
|
28
29
|
return [signed.refused];
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
32
|
-
if (baseOf(now.lines) !== base)
|
|
33
|
-
return [say('base-moved', { base })];
|
|
30
|
+
const stale = await moved(context, place, base);
|
|
31
|
+
if (stale)
|
|
32
|
+
return stale;
|
|
34
33
|
const kept = context.cache.keep(Buffer.from(JSON.stringify({ place, base, signed: signed.lines })));
|
|
35
34
|
return [say('planned', { lot: kept, base }), ...(await context.told(place, signed.lines))];
|
|
36
35
|
});
|
|
@@ -41,11 +40,9 @@ export const landLot = async (asked, context) => {
|
|
|
41
40
|
if (bytes === undefined)
|
|
42
41
|
return [say('no-lot', { lot: asked['lot'] ?? '' })];
|
|
43
42
|
const kept = JSON.parse(Buffer.from(bytes).toString('utf8'));
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
47
|
-
if (baseOf(now.lines) !== kept.base)
|
|
48
|
-
return [say('base-moved', { base: kept.base })];
|
|
43
|
+
const stale = await moved(context, kept.place, kept.base);
|
|
44
|
+
if (stale)
|
|
45
|
+
return stale;
|
|
49
46
|
const landed = await context.land(kept.place, kept.signed);
|
|
50
47
|
return 'refused' in landed ? [landed.refused] : [say('landed', { count: String(landed.lines.length) }), ...landed.lines];
|
|
51
48
|
};
|
package/dist/src/flows/read.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
import type { Asked, Context } from '../core/answer.ts';
|
|
2
|
-
/**
|
|
3
|
-
* A read of one offer the place declares of the class head reads: the response comes back once, typed by the world,
|
|
4
|
-
* and both are kept — the body's digest and the typed answer's, receipts the device key signs. Asked again as the same
|
|
5
|
-
* principal, while the place stands on what typed it, the person's grant holds now and the newest read is younger than
|
|
6
|
-
* the reuse the place allows, it answers from what was kept: nothing called. Asked for history, it shows what was kept
|
|
7
|
-
* of that request as of when it was read — evidence of that read, never a current answer — where the place allows it.
|
|
8
|
-
*/
|
|
2
|
+
/** Read one permitted offer, reuse only under its policy and land the signed receipt. */
|
|
9
3
|
export declare const readSource: (asked: Asked, context: Context) => Promise<readonly string[]>;
|
package/dist/src/flows/read.js
CHANGED
|
@@ -9,7 +9,7 @@ import { principalOf } from "../access/obtain.js";
|
|
|
9
9
|
import { jsonOf } from "../access/contract.js";
|
|
10
10
|
import { refusedAccess } from "../sources/refused.js";
|
|
11
11
|
import { hand, keptOf } from "../kept/kept.js";
|
|
12
|
-
import { markOf, overOf } from "../ahead/budget.js";
|
|
12
|
+
import { calledIn, markOf, overOf } from "../ahead/budget.js";
|
|
13
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. */
|
|
14
14
|
const closeWith = async (intent, source, digest, { word, lines, lands }, context) => {
|
|
15
15
|
const say = sayIn(context.lock);
|
|
@@ -20,13 +20,7 @@ const closeWith = async (intent, source, digest, { word, lines, lands }, context
|
|
|
20
20
|
const landed = await lands(draftOf('', word('receipt/at'), `${intent}/${word('act/closed')}`, digest, new Date(context.now()).toISOString()));
|
|
21
21
|
return 'refused' in landed ? [say('not-landed', { why: landed.refused })] : [...landed.lines, say('closed', { intent, digest })];
|
|
22
22
|
};
|
|
23
|
-
/**
|
|
24
|
-
* A read of one offer the place declares of the class head reads: the response comes back once, typed by the world,
|
|
25
|
-
* and both are kept — the body's digest and the typed answer's, receipts the device key signs. Asked again as the same
|
|
26
|
-
* principal, while the place stands on what typed it, the person's grant holds now and the newest read is younger than
|
|
27
|
-
* the reuse the place allows, it answers from what was kept: nothing called. Asked for history, it shows what was kept
|
|
28
|
-
* of that request as of when it was read — evidence of that read, never a current answer — where the place allows it.
|
|
29
|
-
*/
|
|
23
|
+
/** Read one permitted offer, reuse only under its policy and land the signed receipt. */
|
|
30
24
|
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 }) => {
|
|
31
25
|
const [family, at, when] = [word('receipt/family'), word('receipt/at'), new Date(context.now()).toISOString()];
|
|
32
26
|
const closing = (digest) => closeWith(asked['closes'], source, digest, { word, lines, lands }, context);
|
|
@@ -78,6 +72,9 @@ export const readSource = (asked, context) => called(asked, context, planRead, a
|
|
|
78
72
|
const again = was?.kind === 'again' ? say(`kept-${was.why}`, { source, operation, at: was.at }) : [];
|
|
79
73
|
if (overOf(lines, [family, at, word('ahead/mark')], source, context.now(), most('read/rate'), Number(word('ahead/share').split('..')[1]), asked))
|
|
80
74
|
return asked['ahead'] === 'true' ? [] : say('rate', { source });
|
|
75
|
+
const calls = lines.find((l) => l.scope === `${word('calls/family')}/${source}`), made = calls && calledIn(lines, [family, at, word('ahead/mark')], source, calls.measure ?? '', context.now());
|
|
76
|
+
if (calls && (made === undefined || made >= most(calls.scope)))
|
|
77
|
+
return asked['ahead'] === 'true' ? [] : say(made === undefined ? 'calls-unwindowed' : 'calls-owed', { source, calls: String(most(calls.scope)), window: calls.measure });
|
|
81
78
|
const attached = await attach();
|
|
82
79
|
if ('said' in attached)
|
|
83
80
|
return attached.said;
|
|
@@ -86,7 +83,7 @@ export const readSource = (asked, context) => called(asked, context, planRead, a
|
|
|
86
83
|
if (asked['ahead'] === 'true' && (got.kind !== 'got' || got.status >= 300))
|
|
87
84
|
return [];
|
|
88
85
|
if (got.kind !== 'got')
|
|
89
|
-
return say(got.kind === 'over' ? 'too-large' : 'unreachable-source', { source });
|
|
86
|
+
return say(got.kind === 'over' ? 'too-large' : 'unreachable-source', { source, why: got.kind === 'failed' ? got.why : '' });
|
|
90
87
|
if (got.status === 401 && attached.principal !== 'anonymous')
|
|
91
88
|
return refusedAccess(context, place, lines, plan, operation);
|
|
92
89
|
const digest = context.cache.keep(Buffer.concat([Buffer.from(`${got.status}\n`), got.bytes]));
|
package/dist/src/index.js
CHANGED
|
@@ -7,139 +7,45 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
}
|
|
8
8
|
return path;
|
|
9
9
|
};
|
|
10
|
-
import { basename, join } from 'node:path';
|
|
11
10
|
import { fileURLToPath } from 'node:url';
|
|
12
11
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
13
12
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
13
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { closing, cooled, founded, land, landAct, limits, pinnedAt, policyAt, run, settledView, sign, standingAt, told, warm, words } from "./ports/bound.js";
|
|
21
|
-
import { warmth } from "./ports/warm.js";
|
|
22
|
-
import { held, keep } from "./ports/cache.js";
|
|
23
|
-
import { registryFor } from "./ports/signers.js";
|
|
24
|
-
import { dropAt, folderOf, hasAt, layAt, listAt, lockAt, takeAt } from "./ports/place.js";
|
|
25
|
-
import * as documents from "./ports/documents.js";
|
|
26
|
-
import { keychain } from "./ports/keychain.js";
|
|
27
|
-
import { algorithmOf, deviceOf, idIn } from "./core/device.js";
|
|
28
|
-
import { publicOf } from "./ports/keys.js";
|
|
29
|
-
import { personOf } from "./ports/access/person.js";
|
|
30
|
-
import { readAhead } from "./ahead/read.js";
|
|
14
|
+
import { aboutOf, members, valueOf } from "./core/lock.js";
|
|
15
|
+
import { fill } from "./core/surface.js";
|
|
16
|
+
import { shapeOf, shapeSchema } from "./ports/surface.js";
|
|
17
|
+
import { folderOf, lockAt } from "./ports/place.js";
|
|
18
|
+
import { askedOf, headOf } from "./doors/head.js";
|
|
31
19
|
const [ext, root] = ((self) => [self.slice(self.lastIndexOf('.')), fileURLToPath(new URL(self.endsWith('.ts') ? '..' : '../..', self))])(import.meta.url);
|
|
32
20
|
const lock = lockAt(root);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
process.once(end, () => void last.finally(() => process.exit(0)));
|
|
21
|
+
const [door, doors] = [process.argv[3], members(valueOf(lock, 'cli/run/doors'))];
|
|
22
|
+
if (process.argv[2] !== valueOf(lock, 'cli/run') || (door !== undefined && !doors.includes(door))) {
|
|
23
|
+
process.stdout.write(`${aboutOf(lock, 'cli/help')}\n`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
if (door !== undefined && door !== doors[0]) {
|
|
27
|
+
await (await import(__rewriteRelativeImportExtension(new URL(`./doors/${door}${ext}`, import.meta.url).href))).open(lock, root, ext, door);
|
|
28
|
+
process.exit();
|
|
29
|
+
}
|
|
43
30
|
const offered = (n) => (n ? { listChanged: false } : undefined);
|
|
31
|
+
const head = headOf(lock, root, ext, {
|
|
32
|
+
can: () => ((e) => ({ url: Boolean(e?.url), form: e !== undefined && (e.form !== undefined || Object.keys(e).length === 0) }))(server.getClientCapabilities()?.elicitation), elicit: (params) => server.elicitInput(params, { timeout: Number(valueOf(lock, 'access/wait').split('..')[1]) }),
|
|
33
|
+
roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []), client: () => ((c) => (c ? `${c.name}@${c.version}` : undefined))(server.getClientVersion()), name: doors[0],
|
|
34
|
+
}), surface = head.surface;
|
|
44
35
|
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') });
|
|
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. */
|
|
46
|
-
let waited = 0;
|
|
47
|
-
const timed = (ask) => async (...args) => { const t = Date.now(); try {
|
|
48
|
-
return await ask(...args);
|
|
49
|
-
}
|
|
50
|
-
finally {
|
|
51
|
-
waited += Date.now() - t;
|
|
52
|
-
} };
|
|
53
|
-
const asked = personOf(lock, { can: () => ((e) => ({ url: Boolean(e?.url), form: e !== undefined && (e.form !== undefined || Object.keys(e).length === 0) }))(server.getClientCapabilities()?.elicitation), elicit: (params) => server.elicitInput(params, { timeout: Number(valueOf(lock, 'access/wait').split('..')[1]) }) });
|
|
54
|
-
const person = { consent: timed(asked.consent), enter: timed(asked.enter), choose: timed(asked.choose), visit: timed(asked.visit) };
|
|
55
|
-
/** head is its home: the folder of its index of places, and the keychain service its keys live under, one per home, so two heads on one keychain never share a key. A place's root is told to bound by the key id its lock names, its secret kept there under the place's account. */
|
|
56
|
-
const home = process.env[valueOf(lock, 'home/env')] || join(process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '', valueOf(lock, 'home/folder'));
|
|
57
|
-
const service = `${deviceOf(lock).service}/${basename(home).replace(/[^\w.-]+/g, '-')}`;
|
|
58
|
-
const store = valueOf(lock, `platform/${process.platform}/store`);
|
|
59
|
-
const keys = keychain(service, store);
|
|
60
|
-
const devices = new Map();
|
|
61
|
-
const device = (role) => devices.get(role) ?? ((secret) => (secret === undefined ? undefined : ((key) => { devices.set(role, key); return key; })(publicOf(secret))))(keys.read(deviceOf(lock, role).account));
|
|
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]);
|
|
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. */
|
|
64
|
-
const contextOf = (signal) => {
|
|
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()])
|
|
67
|
-
if (k.endsWith(`\0${place}`))
|
|
68
|
-
folds.delete(k); };
|
|
69
|
-
const read = (at) => at?.startsWith(`${valueOf(lock, 'receipt/at')}:`) === true;
|
|
70
|
-
const once = (key, place, fold) => (folds.get(`${key}\0${place}`) ?? ((p) => (folds.set(`${key}\0${place}`, p), p))(fold()));
|
|
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)
|
|
73
|
-
touched.add(place); });
|
|
74
|
-
const signRoot = (place, lot, account) => sign(runs, place, lot, ...asRoot(account), signal);
|
|
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);
|
|
76
|
-
return {
|
|
77
|
-
lock,
|
|
78
|
-
bound: (cwd, args) => run(runs, cwd, args, {}, signal),
|
|
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)),
|
|
80
|
-
policy: (place) => once('policy', place, () => policyAt(runs, place, signal)),
|
|
81
|
-
world: (place, view) => once(`world/${view}`, place, () => settledView(runs, place, view, signal)),
|
|
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)}`) : ''),
|
|
83
|
-
files: { lay: (place, rel, bytes) => { forget(place); layAt(place, rel, bytes); }, drop: (place, rel) => { forget(place); dropAt(place, rel); }, has: hasAt, list: listAt, read: documents, take: takeAt, own: (rel) => takeAt(join(root, rel)) },
|
|
84
|
-
home: () => home,
|
|
85
|
-
personal: () => process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '',
|
|
86
|
-
found: (place, account) => acted(place, true, founded(runs, place, ...asRoot(account), signal)),
|
|
87
|
-
signRoot,
|
|
88
|
-
landRoot: (place, lot, account) => acted(place, true, landAct(runs, place, lot, ...asRoot(account), signal)),
|
|
89
|
-
moved: (place) => moved.has(place),
|
|
90
|
-
settle: async () => { const red = []; for (const place of touched) {
|
|
91
|
-
const why = await closing(runs, place, signal);
|
|
92
|
-
if (why !== undefined)
|
|
93
|
-
red.push(sayIn(lock)('receipts-open', { place, why }));
|
|
94
|
-
} touched.clear(); return red; },
|
|
95
|
-
pinned: (place, file) => pinnedAt(runs, place, file, signal),
|
|
96
|
-
access: async (kind) => (kind !== valueOf(lock, 'access/adapter') || !/^[a-z0-9-]+$/.test(kind) ? undefined : import(__rewriteRelativeImportExtension(new URL(`./ports/access/${kind}${ext}`, import.meta.url).href)).then((m) => m.adapter)),
|
|
97
|
-
person,
|
|
98
|
-
cache: { keep, held },
|
|
99
|
-
signAs,
|
|
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)),
|
|
101
|
-
land: (place, signed) => acted(place, true, land(runs, place, signed, signal)),
|
|
102
|
-
told: (place, signed) => told(runs, place, signed, words.standing, signal),
|
|
103
|
-
elicit: (message) => person.consent(message, aboutOf(lock, 'said/elicit/yes')),
|
|
104
|
-
now: () => Date.now(),
|
|
105
|
-
machine: () => Date.now() - waited,
|
|
106
|
-
begun: Date.now() - waited,
|
|
107
|
-
keys,
|
|
108
|
-
device,
|
|
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()),
|
|
110
|
-
};
|
|
111
|
-
};
|
|
112
|
-
const askedOf = (raw) => Object.fromEntries(Object.entries(raw ?? {}).map(([k, v]) => [k, v === undefined ? undefined : typeof v === 'object' ? JSON.stringify(v) : String(v)]));
|
|
113
36
|
const schemaOf = (fields) => ({ type: 'object',
|
|
114
37
|
properties: Object.fromEntries(fields.map((f) => [f.name, { type: f.type, description: f.about }])), required: fields.filter((f) => f.required).map((f) => f.name) });
|
|
115
38
|
if (surface.tools.length)
|
|
116
39
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: surface.tools.map((tool) => ({ name: tool.name, description: tool.about,
|
|
117
40
|
inputSchema: schemaOf([...tool.fields]), outputSchema: shapeSchema(lock), annotations: Object.fromEntries(tool.hints.map((hint) => [`${hint}Hint`, true])) })) }));
|
|
118
41
|
if (surface.tools.length)
|
|
119
|
-
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => (
|
|
120
|
-
const tool = surface.tools.find((one) => one.name === request.params.name);
|
|
121
|
-
if (tool === undefined)
|
|
122
|
-
return { isError: true, content: [] };
|
|
123
|
-
const refused = refusedOn(lock, process.platform);
|
|
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());
|
|
126
|
-
return { content: [{ type: 'text', text: answer.join('\n') }], structuredContent: shapeOf(answer) };
|
|
127
|
-
})));
|
|
42
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => ((answer) => (answer === undefined ? { isError: true, content: [] } : { content: [{ type: 'text', text: answer.join('\n') }], structuredContent: shapeOf(answer) }))(await head.call(request.params.name, request.params.arguments, extra.signal)));
|
|
128
43
|
if (surface.resources.length)
|
|
129
44
|
server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
|
|
130
45
|
if (surface.resources.length)
|
|
131
46
|
server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({ resourceTemplates: surface.resources.map((r) => ({ name: r.name, uriTemplate: r.template, description: r.about, mimeType: r.mime })) }));
|
|
132
47
|
if (surface.resources.length)
|
|
133
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) =>
|
|
134
|
-
const found = surface.resources.flatMap((r) => ((vars) => (vars === undefined ? [] : [{ r, vars }]))(matchTemplate(r.template, request.params.uri)))[0];
|
|
135
|
-
if (found === undefined)
|
|
136
|
-
return { contents: [] };
|
|
137
|
-
const refused = refusedOn(lock, process.platform);
|
|
138
|
-
if (refused !== undefined)
|
|
139
|
-
return { contents: [{ uri: request.params.uri, mimeType: found.r.mime, text: refused }] };
|
|
140
|
-
const lines = await (await answerOf('resources', found.r.name))(askedOf(found.vars), contextOf(extra.signal));
|
|
141
|
-
return { contents: [{ uri: request.params.uri, mimeType: found.r.mime, text: lines.join('\n') }] };
|
|
142
|
-
}));
|
|
48
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => ((got) => ({ contents: got === undefined ? [] : [{ uri: request.params.uri, mimeType: got.mime, text: got.lines.join('\n') }] }))(await head.resource(request.params.uri, extra.signal)));
|
|
143
49
|
if (surface.prompts.length)
|
|
144
50
|
server.setRequestHandler(ListPromptsRequestSchema, () => ({ prompts: surface.prompts.map((p) => ({ name: p.name, description: p.about, arguments: p.fields.map((f) => ({ name: f.name, description: f.about, required: f.required })) })) }));
|
|
145
51
|
if (surface.prompts.length)
|