@lapxo/head 0.1.6 → 0.2.0
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 +39 -14
- package/TARGET.bound +301 -118
- 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/ahead/read.d.ts +2 -2
- package/dist/src/ahead/read.js +2 -4
- 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 +6 -7
- package/dist/src/core/answer.d.ts +3 -0
- package/dist/src/core/answer.js +1 -0
- package/dist/src/core/device.d.ts +2 -2
- package/dist/src/core/device.js +5 -5
- package/dist/src/core/lock.d.ts +1 -0
- package/dist/src/core/lock.js +2 -1
- package/dist/src/core/package.js +3 -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 +141 -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/device.d.ts +3 -0
- package/dist/src/flows/device.js +5 -7
- package/dist/src/flows/plan.d.ts +2 -0
- package/dist/src/flows/plan.js +22 -11
- package/dist/src/flows/read.js +4 -1
- package/dist/src/index.js +17 -116
- package/dist/src/links/birth.d.ts +6 -0
- package/dist/src/links/birth.js +48 -0
- package/dist/src/links/inspect.d.ts +1 -1
- package/dist/src/links/inspect.js +28 -19
- package/dist/src/links/link.d.ts +2 -2
- package/dist/src/links/link.js +7 -10
- package/dist/src/places/found.js +1 -1
- package/dist/src/ports/bound.d.ts +9 -11
- package/dist/src/ports/bound.js +37 -33
- package/dist/src/ports/host.d.ts +53 -0
- package/dist/src/ports/host.js +157 -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/sources/connect.js +3 -2
- package/dist/src/tools/card.js +4 -6
- package/dist/src/use/named.js +3 -3
- package/dist/src/verbs/ask.js +2 -1
- package/dist/src/verbs/card.js +2 -2
- package/dist/src/walk/context.js +3 -6
- package/package.json +3 -3
- package/dist/src/ports/reading.d.ts +0 -8
- package/dist/src/ports/reading.js +0 -14
- 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,141 @@
|
|
|
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, authorityAt } 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
|
+
const covered = async (place, lot, then) => {
|
|
72
|
+
const checked = await authorityAt(place, valueOf(lock, 'founding/root').split('/')[1], 'record-coverage', lot, Date.now(), signal);
|
|
73
|
+
return 'refused' in checked ? { refused: checked.refused } : checked.decision !== 'allow' ? { refused: `REFUSE·authority-preflight/${checked.decision} ${checked.needs.join('|')}` } : then();
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
lock,
|
|
77
|
+
bound: (cwd, args) => run(runs, cwd, args, {}, signal),
|
|
78
|
+
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)),
|
|
79
|
+
authority: (place, key, kind, records, presented) => authorityAt(place, key, kind, records, Date.now(), signal, presented),
|
|
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: (place, lot, account) => covered(place, lot, () => sign(runs, place, lot, ...asRoot(account), signal)),
|
|
88
|
+
landRoot: (place, lot, account) => acted(place, true, covered(place, lot, () => 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: (place, lot, role, lines) => sign(runs, place, lot, ...asRole(role, lines), signal),
|
|
100
|
+
landAs: (place, lot, role, lines) => acted(place, false, landAct(runs, place, lot, ...asRole(role, lines), signal)),
|
|
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: door.roots, client: door.client,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const call = (name, args, signal) => (ahead.abort(), turn(async () => {
|
|
113
|
+
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))
|
|
114
|
+
throw e; return [sayIn(lock)('signing-key-refused', { key, place })]; })(/^REFUSE·keychain (\S+) names a signing key/.exec(e.message)?.[1]);
|
|
115
|
+
if (tool === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
const context = contextOf(signal), answer = refused !== undefined ? [refused] : signal.aborted ? [] : [...await (await answerOf('tools', tool.name))(askedOf(args), context).catch(keyed), ...await context.settle()];
|
|
118
|
+
void ((early) => turn(() => readAhead(shapeOf(answer).cells, contextOf(), early.signal)))(ahead = new AbortController());
|
|
119
|
+
return answer;
|
|
120
|
+
}).then(async (answer) => { if (answer !== undefined && shapeOf(answer).cells.some((c) => c.code === keyRefused))
|
|
121
|
+
await refusals.write(typeof args?.['place'] === 'string' ? args['place'] : await placed(), name, digestOf(name, args ?? {}), keyRefused); return answer; }));
|
|
122
|
+
const resource = (uri, signal) => turn(async () => {
|
|
123
|
+
const found = surface.resources.flatMap((r) => ((vars) => (vars === undefined ? [] : [{ r, vars }]))(matchTemplate(r.template, uri)))[0];
|
|
124
|
+
if (found === undefined)
|
|
125
|
+
return undefined;
|
|
126
|
+
if (refused !== undefined)
|
|
127
|
+
return { mime: found.r.mime, lines: [refused] };
|
|
128
|
+
return { mime: found.r.mime, lines: await (await answerOf('resources', found.r.name))(askedOf(found.vars), contextOf(signal)) };
|
|
129
|
+
});
|
|
130
|
+
/** 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. */
|
|
131
|
+
const [cardOf, placed] = [(place) => turn(() => cardAt(contextOf(), place)), () => turn(() => placeOf({}, contextOf()))];
|
|
132
|
+
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()));
|
|
133
|
+
const write = (place, lot) => turn(async () => ((c) => c.standing(place).then((held) => ('refused' in held ? held : c.landAs(place, lot, 'head', held.lines))))(contextOf()));
|
|
134
|
+
return { surface, call, resource, idle: () => last, cardOf, placed, hasPlace, write, refusals, now: () => contextOf().now() };
|
|
135
|
+
};
|
|
136
|
+
/** 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. */
|
|
137
|
+
export const plainOf = (lock, root, ext, name) => {
|
|
138
|
+
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')];
|
|
139
|
+
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)));
|
|
140
|
+
return { head, verbs, settle: guard.settle, asked: async (verb, args) => (verbs.includes(verb) ? guard.call(verb, args) : undefined) };
|
|
141
|
+
};
|
|
@@ -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
|
+
});
|
|
@@ -17,6 +17,9 @@ export declare const rootIn: (context: Context, place: string) => {
|
|
|
17
17
|
* lock as its parent has nothing signed here: what the parent would cut is bound's to judge.
|
|
18
18
|
*/
|
|
19
19
|
type By = {
|
|
20
|
+
readonly key: string;
|
|
21
|
+
readonly root: boolean;
|
|
22
|
+
readonly records: (lot: readonly string[]) => readonly string[];
|
|
20
23
|
readonly sign: (lot: readonly string[]) => Promise<Outcome>;
|
|
21
24
|
readonly land: (lot: readonly string[]) => Promise<Outcome>;
|
|
22
25
|
};
|
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 { key: valueOf(context.lock, 'founding/root').split('/')[1], root: true, records: (signed) => admits ? witnessed(signed) : signed, 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 })] };
|
|
@@ -28,5 +26,5 @@ export const widening = (context, place, lines, lot) => {
|
|
|
28
26
|
if (stopped)
|
|
29
27
|
return { said: stopped };
|
|
30
28
|
const again = lot.flatMap((line) => ((got) => (got.kind === 'fact' && lines.some((l) => l.scope === got.value.fields.scope && l.measure === got.value.fields.measure) ? [got.value.fields.scope ?? ''] : []))(parse(line)))[0];
|
|
31
|
-
return again !== undefined ? { said: [sayIn(context.lock)('attested-again', { place, scope: again })] } : { sign: (signed) => context.signAs(place, signed, 'person', lines), land: (signed) => context.landAs(place, signed, 'person', lines) };
|
|
29
|
+
return again !== undefined ? { said: [sayIn(context.lock)('attested-again', { place, scope: again })] } : { key: witness, root: false, records: (signed) => signed, sign: (signed) => context.signAs(place, signed, 'person', lines), land: (signed) => context.landAs(place, signed, 'person', lines) };
|
|
32
30
|
};
|
package/dist/src/flows/plan.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Asked, Context } from '../core/answer.ts';
|
|
2
|
+
/** Bound observes exact configuration records; Q4 domain checks remain in Head's existing card. */
|
|
3
|
+
export declare const authorityFor: (context: Context, place: string, key: string, kind: string, records: readonly string[], observe?: boolean, presented?: string) => Promise<readonly string[] | undefined>;
|
|
2
4
|
/**
|
|
3
5
|
* A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
|
|
4
6
|
* 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.
|
package/dist/src/flows/plan.js
CHANGED
|
@@ -2,6 +2,14 @@ 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));
|
|
6
|
+
/** Bound observes exact configuration records; Q4 domain checks remain in Head's existing card. */
|
|
7
|
+
export const authorityFor = async (context, place, key, kind, records, observe = false, presented) => {
|
|
8
|
+
const result = await context.authority(place, key, kind, records, presented), say = sayIn(context.lock);
|
|
9
|
+
if ('refused' in result)
|
|
10
|
+
return [say('authority-refused', { place, key, kind, why: result.refused, code: result.code })];
|
|
11
|
+
return observe || result.decision !== 'allow' ? [say(observe ? 'authority-card' : result.decision === 'deny' ? 'authority-refused' : 'authority-needed', { place, key, kind, decision: result.decision, authority: result.authority, request: result.request, needs: result.needs.join('|'), why: result.decision })] : undefined;
|
|
12
|
+
};
|
|
5
13
|
/**
|
|
6
14
|
* A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
|
|
7
15
|
* 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.
|
|
@@ -18,6 +26,9 @@ export const planLot = (asked, context) => inPlace(asked, context, async (place)
|
|
|
18
26
|
const by = widening(context, place, held.lines, lot);
|
|
19
27
|
if ('said' in by)
|
|
20
28
|
return by.said;
|
|
29
|
+
const initial = by.root ? await authorityFor(context, place, by.key, 'record-coverage', by.records(lot)) : undefined;
|
|
30
|
+
if (initial)
|
|
31
|
+
return initial;
|
|
21
32
|
const consent = await context.elicit(say('elicit', { count: String(lot.length), place, base, lines: lot.join('\n') }));
|
|
22
33
|
if (consent === 'none')
|
|
23
34
|
return [say('sign-yourself', { place }), ...lot];
|
|
@@ -26,12 +37,10 @@ export const planLot = (asked, context) => inPlace(asked, context, async (place)
|
|
|
26
37
|
const signed = await by.sign(lot);
|
|
27
38
|
if ('refused' in signed)
|
|
28
39
|
return [signed.refused];
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
return [say('base-moved', { base })];
|
|
34
|
-
const kept = context.cache.keep(Buffer.from(JSON.stringify({ place, base, signed: signed.lines })));
|
|
40
|
+
const stale = await moved(context, place, base);
|
|
41
|
+
if (stale)
|
|
42
|
+
return stale;
|
|
43
|
+
const kept = context.cache.keep(Buffer.from(JSON.stringify({ place, base, signed: signed.lines, root: by.root })));
|
|
35
44
|
return [say('planned', { lot: kept, base }), ...(await context.told(place, signed.lines))];
|
|
36
45
|
});
|
|
37
46
|
/** A planned lot landed by its digest, only while the place stands on the base it was planned against. */
|
|
@@ -41,11 +50,13 @@ export const landLot = async (asked, context) => {
|
|
|
41
50
|
if (bytes === undefined)
|
|
42
51
|
return [say('no-lot', { lot: asked['lot'] ?? '' })];
|
|
43
52
|
const kept = JSON.parse(Buffer.from(bytes).toString('utf8'));
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
const stale = await moved(context, kept.place, kept.base);
|
|
54
|
+
if (stale)
|
|
55
|
+
return stale;
|
|
56
|
+
const author = parse(kept.signed[0] ?? '');
|
|
57
|
+
const checked = kept.root ? await authorityFor(context, kept.place, author.kind === 'fact' ? author.value.fields.by ?? '' : '', 'admission', kept.signed) : undefined;
|
|
58
|
+
if (checked)
|
|
59
|
+
return checked;
|
|
49
60
|
const landed = await context.land(kept.place, kept.signed);
|
|
50
61
|
return 'refused' in landed ? [landed.refused] : [say('landed', { count: String(landed.lines.length) }), ...landed.lines];
|
|
51
62
|
};
|
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);
|
|
@@ -72,6 +72,9 @@ export const readSource = (asked, context) => called(asked, context, planRead, a
|
|
|
72
72
|
const again = was?.kind === 'again' ? say(`kept-${was.why}`, { source, operation, at: was.at }) : [];
|
|
73
73
|
if (overOf(lines, [family, at, word('ahead/mark')], source, context.now(), most('read/rate'), Number(word('ahead/share').split('..')[1]), asked))
|
|
74
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 });
|
|
75
78
|
const attached = await attach();
|
|
76
79
|
if ('said' in attached)
|
|
77
80
|
return attached.said;
|
package/dist/src/index.js
CHANGED
|
@@ -7,144 +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 { birth, 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
|
-
|
|
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))) {
|
|
34
23
|
process.stdout.write(`${aboutOf(lock, 'cli/help')}\n`);
|
|
35
24
|
process.exit(0);
|
|
36
25
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Object.assign(birth, { at: valueOf(lock, 'founding/person-at'), file: valueOf(lock, 'founding/birth-file') });
|
|
42
|
-
Object.assign(warmth, { spares: Number(valueOf(lock, 'bound/spares').split('..')[1]), boot: valueOf(lock, 'bound/spare-env').split('|') });
|
|
43
|
-
/** 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. */
|
|
44
|
-
let [last, ahead] = [Promise.resolve(), new AbortController()];
|
|
45
|
-
const turn = (act) => ((next) => { last = next.catch(() => undefined); return next; })(last.then(() => { void warm(runs); return act().finally(cooled); }));
|
|
46
|
-
for (const end of ['SIGTERM', 'SIGINT'])
|
|
47
|
-
process.once(end, () => void last.finally(() => process.exit(0)));
|
|
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
|
+
}
|
|
48
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;
|
|
49
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') });
|
|
50
|
-
/** 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. */
|
|
51
|
-
let waited = 0;
|
|
52
|
-
const timed = (ask) => async (...args) => { const t = Date.now(); try {
|
|
53
|
-
return await ask(...args);
|
|
54
|
-
}
|
|
55
|
-
finally {
|
|
56
|
-
waited += Date.now() - t;
|
|
57
|
-
} };
|
|
58
|
-
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]) }) });
|
|
59
|
-
const person = { consent: timed(asked.consent), enter: timed(asked.enter), choose: timed(asked.choose), visit: timed(asked.visit) };
|
|
60
|
-
/** 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. */
|
|
61
|
-
const home = process.env[valueOf(lock, 'home/env')] || join(process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '', valueOf(lock, 'home/folder'));
|
|
62
|
-
const service = `${deviceOf(lock).service}/${basename(home).replace(/[^\w.-]+/g, '-')}`;
|
|
63
|
-
const store = valueOf(lock, `platform/${process.platform}/store`);
|
|
64
|
-
const keys = keychain(service, store);
|
|
65
|
-
const devices = new Map();
|
|
66
|
-
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));
|
|
67
|
-
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]);
|
|
68
|
-
/** 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. */
|
|
69
|
-
const contextOf = (signal) => {
|
|
70
|
-
/** 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. */
|
|
71
|
-
const [folds, touched, moved] = [new Map(), new Set(), new Set()], forget = (place) => { for (const k of [...folds.keys()])
|
|
72
|
-
if (k.endsWith(`\0${place}`))
|
|
73
|
-
folds.delete(k); };
|
|
74
|
-
const read = (at) => at?.startsWith(`${valueOf(lock, 'receipt/at')}:`) === true;
|
|
75
|
-
const once = (key, place, fold) => (folds.get(`${key}\0${place}`) ?? ((p) => (folds.set(`${key}\0${place}`, p), p))(fold()));
|
|
76
|
-
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))))
|
|
77
|
-
moved.add(place); return got; }).finally(() => { forget(place); if (root)
|
|
78
|
-
touched.add(place); });
|
|
79
|
-
const signRoot = (place, lot, account) => sign(runs, place, lot, ...asRoot(account), signal);
|
|
80
|
-
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);
|
|
81
|
-
return {
|
|
82
|
-
lock,
|
|
83
|
-
bound: (cwd, args) => run(runs, cwd, args, {}, signal),
|
|
84
|
-
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)),
|
|
85
|
-
policy: (place) => once('policy', place, () => policyAt(runs, place, signal)),
|
|
86
|
-
world: (place, view) => once(`world/${view}`, place, () => settledView(runs, place, view, signal)),
|
|
87
|
-
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)}`) : ''),
|
|
88
|
-
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)) },
|
|
89
|
-
home: () => home,
|
|
90
|
-
personal: () => process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '',
|
|
91
|
-
found: (place, account) => acted(place, true, founded(runs, place, ...asRoot(account), signal)),
|
|
92
|
-
signRoot,
|
|
93
|
-
landRoot: (place, lot, account) => acted(place, true, landAct(runs, place, lot, ...asRoot(account), signal)),
|
|
94
|
-
moved: (place) => moved.has(place),
|
|
95
|
-
settle: async () => { const red = []; for (const place of touched) {
|
|
96
|
-
const why = await closing(runs, place, signal);
|
|
97
|
-
if (why !== undefined)
|
|
98
|
-
red.push(sayIn(lock)('receipts-open', { place, why }));
|
|
99
|
-
} touched.clear(); return red; },
|
|
100
|
-
pinned: (place, file) => pinnedAt(runs, place, file, signal),
|
|
101
|
-
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)),
|
|
102
|
-
person,
|
|
103
|
-
cache: { keep, held },
|
|
104
|
-
signAs,
|
|
105
|
-
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)),
|
|
106
|
-
land: (place, signed) => acted(place, true, land(runs, place, signed, signal)),
|
|
107
|
-
told: (place, signed) => told(runs, place, signed, words.standing, signal),
|
|
108
|
-
elicit: (message) => person.consent(message, aboutOf(lock, 'said/elicit/yes')),
|
|
109
|
-
now: () => Date.now(),
|
|
110
|
-
machine: () => Date.now() - waited,
|
|
111
|
-
begun: Date.now() - waited,
|
|
112
|
-
keys,
|
|
113
|
-
device,
|
|
114
|
-
roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []), client: () => ((c) => (c ? `${c.name}@${c.version}` : undefined))(server.getClientVersion()),
|
|
115
|
-
};
|
|
116
|
-
};
|
|
117
|
-
const askedOf = (raw) => Object.fromEntries(Object.entries(raw ?? {}).map(([k, v]) => [k, v === undefined ? undefined : typeof v === 'object' ? JSON.stringify(v) : String(v)]));
|
|
118
36
|
const schemaOf = (fields) => ({ type: 'object',
|
|
119
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) });
|
|
120
38
|
if (surface.tools.length)
|
|
121
39
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: surface.tools.map((tool) => ({ name: tool.name, description: tool.about,
|
|
122
40
|
inputSchema: schemaOf([...tool.fields]), outputSchema: shapeSchema(lock), annotations: Object.fromEntries(tool.hints.map((hint) => [`${hint}Hint`, true])) })) }));
|
|
123
41
|
if (surface.tools.length)
|
|
124
|
-
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => (
|
|
125
|
-
const tool = surface.tools.find((one) => one.name === request.params.name);
|
|
126
|
-
if (tool === undefined)
|
|
127
|
-
return { isError: true, content: [] };
|
|
128
|
-
const refused = refusedOn(lock, process.platform);
|
|
129
|
-
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()];
|
|
130
|
-
void ((early) => turn(() => readAhead(shapeOf(answer).cells, contextOf(), early.signal)))(ahead = new AbortController());
|
|
131
|
-
return { content: [{ type: 'text', text: answer.join('\n') }], structuredContent: shapeOf(answer) };
|
|
132
|
-
})));
|
|
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)));
|
|
133
43
|
if (surface.resources.length)
|
|
134
44
|
server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
|
|
135
45
|
if (surface.resources.length)
|
|
136
46
|
server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({ resourceTemplates: surface.resources.map((r) => ({ name: r.name, uriTemplate: r.template, description: r.about, mimeType: r.mime })) }));
|
|
137
47
|
if (surface.resources.length)
|
|
138
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) =>
|
|
139
|
-
const found = surface.resources.flatMap((r) => ((vars) => (vars === undefined ? [] : [{ r, vars }]))(matchTemplate(r.template, request.params.uri)))[0];
|
|
140
|
-
if (found === undefined)
|
|
141
|
-
return { contents: [] };
|
|
142
|
-
const refused = refusedOn(lock, process.platform);
|
|
143
|
-
if (refused !== undefined)
|
|
144
|
-
return { contents: [{ uri: request.params.uri, mimeType: found.r.mime, text: refused }] };
|
|
145
|
-
const lines = await (await answerOf('resources', found.r.name))(askedOf(found.vars), contextOf(extra.signal));
|
|
146
|
-
return { contents: [{ uri: request.params.uri, mimeType: found.r.mime, text: lines.join('\n') }] };
|
|
147
|
-
}));
|
|
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)));
|
|
148
49
|
if (surface.prompts.length)
|
|
149
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 })) })) }));
|
|
150
51
|
if (surface.prompts.length)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Asked, type Context } from '../core/answer.ts';
|
|
2
|
+
import { type Said } from '../core/lock.ts';
|
|
3
|
+
/** A separate public birth artifact. The source package, authority and personal birth journal are not its payload. */
|
|
4
|
+
export declare const sharedBirth: (context: Context, to: string, standing: readonly Said[]) => readonly string[];
|
|
5
|
+
/** Inline or client-shared local birth bytes are inspected only; no transport, signer, file write or admission is acquired. */
|
|
6
|
+
export declare const receivedBirth: (asked: Asked, context: Context) => Promise<readonly string[] | undefined>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { basename, join } from 'node:path';
|
|
2
|
+
import { canonical, parse } from '@lapxo/topos/wire';
|
|
3
|
+
import { PORTABLE_BIRTH_PROFILE, decodePortableBirth, encodePortableBirth, inspectPortableBirth } from '@lapxo/topos/contract';
|
|
4
|
+
import { semanticStandingBytes } from '@lapxo/topos/standing';
|
|
5
|
+
import { inStanding } from "../core/answer.js";
|
|
6
|
+
import { members, valueOf } from "../core/lock.js";
|
|
7
|
+
import { sayIn } from "../core/surface.js";
|
|
8
|
+
import { digestIn } from "../core/worlds.js";
|
|
9
|
+
const budget = (context) => ({ bytes: Number(valueOf(context.lock, 'bound/printed').split('..')[1]), items: Number(valueOf(context.lock, 'share/birth-items').split('..')[1]) });
|
|
10
|
+
const refused = (context, error) => [sayIn(context.lock)('birth-refused', { why: error instanceof Error ? error.message : String(error) })];
|
|
11
|
+
/** A separate public birth artifact. The source package, authority and personal birth journal are not its payload. */
|
|
12
|
+
export const sharedBirth = (context, to, standing) => {
|
|
13
|
+
const say = sayIn(context.lock), scopes = members(valueOf(context.lock, 'share/birth-scopes')), file = valueOf(context.lock, 'share/birth-file');
|
|
14
|
+
const declarations = standing.filter(line => scopes.includes(line.scope ?? '')).map(({ by: _by, sig: _sig, epoch: _epoch, ...line }) => canonical(line));
|
|
15
|
+
if (!declarations.length)
|
|
16
|
+
return [say('birth-empty')];
|
|
17
|
+
try {
|
|
18
|
+
const record = encodePortableBirth({ origin: digestIn(context.lock)(semanticStandingBytes(standing.map(line => canonical(line)))), declarations }, budget(context)), bytes = new TextEncoder().encode(record);
|
|
19
|
+
context.files.lay(to, file, bytes);
|
|
20
|
+
return [say('birth-shared', { document: join(to, file), digest: digestIn(context.lock)(bytes), record })];
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return refused(context, error);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** Inline or client-shared local birth bytes are inspected only; no transport, signer, file write or admission is acquired. */
|
|
27
|
+
export const receivedBirth = async (asked, context) => {
|
|
28
|
+
const document = asked['document'] ?? '', inline = document.startsWith(PORTABLE_BIRTH_PROFILE.split('/')[0] + '/');
|
|
29
|
+
if (!inline && !(document.startsWith('/') && basename(document) === valueOf(context.lock, 'share/birth-file')))
|
|
30
|
+
return undefined;
|
|
31
|
+
return inStanding(asked, context, async (place, standing) => {
|
|
32
|
+
const say = sayIn(context.lock), bytes = inline ? new TextEncoder().encode(document) : (await context.files.read.documentOf(context, place, document))?.bytes;
|
|
33
|
+
if (!bytes)
|
|
34
|
+
return [say('birth-needs', { document })];
|
|
35
|
+
if (asked['digest'] && digestIn(context.lock)(bytes) !== asked['digest'])
|
|
36
|
+
return [say('document-digest', { document: inline ? PORTABLE_BIRTH_PROFILE : document, digest: asked['digest'], got: digestIn(context.lock)(bytes) })];
|
|
37
|
+
try {
|
|
38
|
+
const pack = decodePortableBirth(new TextDecoder().decode(bytes), budget(context)), allowed = members(valueOf(context.lock, 'share/birth-scopes'));
|
|
39
|
+
if (pack.declarations.some(line => ((p) => p.kind !== 'fact' || !allowed.includes(p.value.fields.scope ?? ''))(parse(line))))
|
|
40
|
+
return [say('birth-refused', { why: 'declaration outside the public birth selection' })];
|
|
41
|
+
const inspected = inspectPortableBirth(pack, [...new Set(standing.flatMap(line => line.scope ?? []))], budget(context));
|
|
42
|
+
return [say('birth-inspected', { inspection: inspected.status, collisions: inspected.collisions.join(', ') || 'none', origin: pack.origin, declarations: pack.declarations.join('\n') })];
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return refused(context, error);
|
|
46
|
+
}
|
|
47
|
+
}, context.policy);
|
|
48
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Asked, type Context } from '../core/answer.ts';
|
|
2
2
|
/**
|
|
3
|
-
* A
|
|
3
|
+
* A legacy or located reference connected: inspected by the instrument in the person's place, never authenticated — its bytes are the
|
|
4
4
|
* link's, and nothing shows who shared it — and what it names is shown; nothing is connected or granted, since a package
|
|
5
5
|
* is received by the digest its sharer sent apart. A place that admits no content transport is handed the lines that admit
|
|
6
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.
|