@lapxo/head 0.2.0 → 0.2.1
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 +1 -1
- package/TARGET.bound +150 -34
- package/dist/src/adopt/adopt.d.ts +2 -2
- package/dist/src/adopt/adopt.js +20 -6
- package/dist/src/answers/answered.js +1 -1
- package/dist/src/compile/pin.d.ts +1 -1
- package/dist/src/compile/pin.js +5 -3
- package/dist/src/core/answer.d.ts +10 -0
- package/dist/src/core/answer.js +1 -1
- package/dist/src/core/association.d.ts +16 -0
- package/dist/src/core/association.js +34 -0
- package/dist/src/core/composition.d.ts +32 -0
- package/dist/src/core/composition.js +28 -0
- package/dist/src/core/package.d.ts +1 -0
- package/dist/src/core/package.js +14 -3
- package/dist/src/core/read.d.ts +2 -1
- package/dist/src/core/read.js +2 -1
- package/dist/src/core/receiver.d.ts +11 -0
- package/dist/src/core/receiver.js +122 -0
- package/dist/src/core/share-bundle.d.ts +9 -0
- package/dist/src/core/share-bundle.js +56 -0
- package/dist/src/doors/head.d.ts +1 -0
- package/dist/src/doors/head.js +3 -0
- package/dist/src/flows/answer.js +7 -3
- package/dist/src/flows/call.d.ts +1 -1
- package/dist/src/flows/call.js +28 -4
- package/dist/src/flows/device.d.ts +11 -5
- package/dist/src/flows/device.js +13 -5
- package/dist/src/flows/plan.js +1 -1
- package/dist/src/index.js +3 -31
- package/dist/src/links/inspect.js +1 -1
- package/dist/src/onboard/first.js +1 -1
- package/dist/src/onboard/owed.js +1 -1
- package/dist/src/places/admit.js +1 -1
- package/dist/src/places/found.js +3 -1
- package/dist/src/ports/adapters/bundle.d.ts +44 -0
- package/dist/src/ports/adapters/bundle.js +173 -0
- package/dist/src/ports/adapters/configured.d.ts +35 -0
- package/dist/src/ports/adapters/configured.js +67 -0
- package/dist/src/ports/adapters/mcp.d.ts +59 -0
- package/dist/src/ports/adapters/mcp.js +122 -0
- package/dist/src/ports/adapters/network.d.ts +12 -0
- package/dist/src/ports/adapters/network.js +67 -0
- package/dist/src/ports/adapters/protocol.d.ts +6 -0
- package/dist/src/ports/adapters/protocol.js +51 -0
- package/dist/src/ports/adapters/remote.d.ts +13 -0
- package/dist/src/ports/adapters/remote.js +33 -0
- package/dist/src/sources/connect.d.ts +1 -0
- package/dist/src/sources/connect.js +13 -10
- package/dist/src/verbs/ask.js +6 -0
- package/dist/src/verbs/do.js +2 -1
- package/package.json +5 -4
- package/runtime.bundle.json +24026 -0
package/dist/src/flows/call.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { configuredAt, configuredRefusal, configuredProtocol } from "../ports/adapters/configured.js";
|
|
2
|
+
import { receiverAllows } from "../core/receiver.js";
|
|
1
3
|
import { inPlace, placeNameOf, unless } from "../core/answer.js";
|
|
2
4
|
import { fieldsOf, vocabOf } from "../core/read.js";
|
|
3
5
|
import { canonical } from '@lapxo/topos/wire';
|
|
@@ -17,7 +19,7 @@ export const pickedOrigin = async (context, place, lines, source, plan) => {
|
|
|
17
19
|
const v = vocabOf(context.lock);
|
|
18
20
|
const origins = fieldsOf(lines, `${v.region}/${source}`)[v.origins] ?? [];
|
|
19
21
|
const line = (value) => ({ at: 'policy:person/picks', by: 'target', form: 'alphabet', measure: 'id', role: 'writes', scope: `${v.pick}/${source}`, value });
|
|
20
|
-
const by = widening(context, place, lines, origins.map((o) => canonical(line(o))));
|
|
22
|
+
const by = await widening(context, place, lines, origins.map((o) => canonical(line(o))));
|
|
21
23
|
if ('said' in by)
|
|
22
24
|
return { said: by.said };
|
|
23
25
|
const chosen = (await context.person.choose(sayIn(context.lock)('pick', { source }), [{ name: v.origin, options: origins.map((o) => ({ value: o, label: o })) }]))?.[v.origin];
|
|
@@ -38,8 +40,15 @@ export const called = async (asked, context, planOf, then) => {
|
|
|
38
40
|
if ('said' in named)
|
|
39
41
|
return named.said;
|
|
40
42
|
const operation = named.operation;
|
|
43
|
+
try {
|
|
44
|
+
if (!await receiverAllows(context, place, source, operation))
|
|
45
|
+
return say('bundle-not-selected', { place, source, operation });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return say('bundle-refused', { why: 'bundle/invalid' });
|
|
49
|
+
}
|
|
41
50
|
const widen = async (message, lot) => {
|
|
42
|
-
const by = widening(context, place, held.lines, lot);
|
|
51
|
+
const by = await widening(context, place, held.lines, lot);
|
|
43
52
|
if ('said' in by)
|
|
44
53
|
return { said: by.said };
|
|
45
54
|
const refused = await unless(context, message);
|
|
@@ -52,16 +61,31 @@ export const called = async (asked, context, planOf, then) => {
|
|
|
52
61
|
if ('said' in picked)
|
|
53
62
|
return picked.said;
|
|
54
63
|
const lines = picked.lines;
|
|
55
|
-
|
|
64
|
+
let remote;
|
|
65
|
+
try {
|
|
66
|
+
remote = configuredAt(context, place, source, lines);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return configuredRefusal(context, error);
|
|
70
|
+
}
|
|
71
|
+
const plan = planOf(lines, context.lock, source, operation, placeNameOf(place), remote !== undefined);
|
|
56
72
|
if (plan.kind === 'said')
|
|
57
73
|
return say(plan.key, plan.fields);
|
|
58
|
-
|
|
74
|
+
if (remote && asked['ahead'] === 'true')
|
|
75
|
+
return [];
|
|
76
|
+
if (remote && (!remote.offers.includes(operation) || new URL(remote.endpoint).origin !== new URL(plan.origin).origin))
|
|
77
|
+
return configuredRefusal(context, Error(configuredProtocol.toolFailure));
|
|
78
|
+
const args = JSON.parse(asked['args'] || '{}');
|
|
79
|
+
let request = '';
|
|
80
|
+
const prepared = remote ? (await context.remote?.())?.prepare(remote, operation, args, async () => await context.elicit(sayIn(context.lock)(configuredProtocol.invoke, { source, operation, endpoint: remote.endpoint, request })) === 'yes')
|
|
81
|
+
: ((transport) => transport?.prepare(plan, args, context.lock))(await context.adapter(plan.origin));
|
|
59
82
|
if (prepared === undefined)
|
|
60
83
|
return say('no-transport', { source, origin: plan.origin });
|
|
61
84
|
if ('missing' in prepared)
|
|
62
85
|
return say('missing-param', { operation, param: prepared.missing });
|
|
63
86
|
if ('refused' in prepared)
|
|
64
87
|
return say(prepared.refused, { source, origin: plan.origin });
|
|
88
|
+
request = prepared.key;
|
|
65
89
|
const most = (scope) => Number((lines.find((l) => l.scope === scope)?.value ?? '0..0').split('..')[1]);
|
|
66
90
|
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());
|
|
67
91
|
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));
|
|
@@ -2,12 +2,18 @@ import type { Context, Outcome } from '../core/answer.ts';
|
|
|
2
2
|
import type { Said } from '../core/lock.ts';
|
|
3
3
|
/** A key of this device by its role: read from the keychain, or made there once; only its public half is ever handed on. */
|
|
4
4
|
export declare const deviceKey: (context: Context, role?: string) => string | undefined;
|
|
5
|
-
/**
|
|
6
|
-
export declare const
|
|
5
|
+
/** Read the receiver index through its authenticated policy, or an empty index before its birth. */
|
|
6
|
+
export declare const homeIndex: (context: Context) => Promise<{
|
|
7
|
+
readonly lines: readonly Said[];
|
|
8
|
+
} | {
|
|
9
|
+
readonly refused: string;
|
|
10
|
+
}>;
|
|
11
|
+
/** Resolve the account through the authenticated index; a legacy folder name only locates a key whose public half must match the authenticated root. */
|
|
12
|
+
export declare const rootIn: (context: Context, place: string) => Promise<{
|
|
7
13
|
readonly account: string;
|
|
8
14
|
} | {
|
|
9
15
|
readonly said: readonly string[];
|
|
10
|
-
}
|
|
16
|
+
}>;
|
|
11
17
|
/**
|
|
12
18
|
* Who signs a widening of a place: its root, when this keychain holds it and the place admits this device's person to
|
|
13
19
|
* witness it — the line is then the place's own policy, in its lock, binding every device it admits, and names as its
|
|
@@ -23,7 +29,7 @@ type By = {
|
|
|
23
29
|
readonly sign: (lot: readonly string[]) => Promise<Outcome>;
|
|
24
30
|
readonly land: (lot: readonly string[]) => Promise<Outcome>;
|
|
25
31
|
};
|
|
26
|
-
export declare const widening: (context: Context, place: string, lines: readonly Said[], lot: readonly string[]) => By | {
|
|
32
|
+
export declare const widening: (context: Context, place: string, lines: readonly Said[], lot: readonly string[]) => Promise<By | {
|
|
27
33
|
readonly said: readonly string[];
|
|
28
|
-
}
|
|
34
|
+
}>;
|
|
29
35
|
export {};
|
package/dist/src/flows/device.js
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { placeNameOf } from "../core/answer.js";
|
|
2
|
-
import { canonical, parse } from '@lapxo/topos/wire';
|
|
2
|
+
import { canonical, parse, LOCK } from '@lapxo/topos/wire';
|
|
3
3
|
import { deviceOf, idIn, uncoveredIn } from "../core/device.js";
|
|
4
4
|
import { members, valueOf } from "../core/lock.js";
|
|
5
5
|
import { fill, sayIn } from "../core/surface.js";
|
|
6
6
|
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
|
-
/**
|
|
10
|
-
export const
|
|
11
|
-
|
|
9
|
+
/** Read the receiver index through its authenticated policy, or an empty index before its birth. */
|
|
10
|
+
export const homeIndex = (context) => context.files.has(context.home(), LOCK) ? context.policy(context.home()) : Promise.resolve({ lines: [] });
|
|
11
|
+
/** Resolve the account through the authenticated index; a legacy folder name only locates a key whose public half must match the authenticated root. */
|
|
12
|
+
export const rootIn = async (context, place) => {
|
|
13
|
+
const home = context.home(), index = await homeIndex(context);
|
|
14
|
+
const names = 'refused' in index ? [] : [...new Set(index.lines.filter((l) => l.scope?.startsWith(`${valueOf(context.lock, 'home/family')}/`) && l.value === place).map((l) => l.scope.slice(valueOf(context.lock, 'home/family').length + 1)))];
|
|
15
|
+
const name = place === home ? valueOf(context.lock, 'home/name') : names[0] ?? placeNameOf(place), account = fill(valueOf(context.lock, 'founding/root-account'), { place: name });
|
|
16
|
+
const held = await context.policy(place), key = 'refused' in held ? undefined : held.lines.find((l) => l.scope === valueOf(context.lock, 'founding/root') && l.measure === 'public-key')?.value;
|
|
17
|
+
return 'refused' in index || names.length > 1 || key === undefined || context.keys.held?.(account) !== key ? { said: [sayIn(context.lock)('no-root', { place })] } : { account };
|
|
18
|
+
};
|
|
19
|
+
export const widening = async (context, place, lines, lot) => {
|
|
12
20
|
const parent = adoptsIn(context.lock, lines);
|
|
13
21
|
if (parent !== undefined)
|
|
14
22
|
return { said: [sayIn(context.lock)('parent-lock', { place, parent })] };
|
|
15
|
-
const [root, witness, field] = [rootIn(context, place), idIn(lines, context.device('person')), valueOf(context.lock, 'widening/witness')];
|
|
23
|
+
const [root, witness, field] = [await rootIn(context, place), idIn(lines, context.device('person')), valueOf(context.lock, 'widening/witness')];
|
|
16
24
|
const wire = lines.filter((l) => l.scope === 'wire/fields'), admits = wire.some((l) => members(l.value ?? '').includes(field));
|
|
17
25
|
const witnessed = (signed) => signed.map((line) => ((got) => (got.kind === 'fact' ? canonical({ ...got.value.fields, [field]: witness }) : line))(parse(line)));
|
|
18
26
|
if ('account' in root && witness !== undefined) {
|
package/dist/src/flows/plan.js
CHANGED
|
@@ -23,7 +23,7 @@ export const planLot = (asked, context) => inPlace(asked, context, async (place)
|
|
|
23
23
|
if ('said' in held)
|
|
24
24
|
return held.said;
|
|
25
25
|
const base = baseOf(held.lines);
|
|
26
|
-
const by = widening(context, place, held.lines, lot);
|
|
26
|
+
const by = await widening(context, place, held.lines, lot);
|
|
27
27
|
if ('said' in by)
|
|
28
28
|
return by.said;
|
|
29
29
|
const initial = by.root ? await authorityFor(context, place, by.key, 'record-coverage', by.records(lot)) : undefined;
|
package/dist/src/index.js
CHANGED
|
@@ -8,14 +8,9 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
return path;
|
|
9
9
|
};
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
-
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
14
11
|
import { aboutOf, members, valueOf } from "./core/lock.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { folderOf, lockAt } from "./ports/place.js";
|
|
18
|
-
import { askedOf, headOf } from "./doors/head.js";
|
|
12
|
+
import { lockAt } from "./ports/place.js";
|
|
13
|
+
import { serve } from "./ports/adapters/protocol.js";
|
|
19
14
|
const [ext, root] = ((self) => [self.slice(self.lastIndexOf('.')), fileURLToPath(new URL(self.endsWith('.ts') ? '..' : '../..', self))])(import.meta.url);
|
|
20
15
|
const lock = lockAt(root);
|
|
21
16
|
const [door, doors] = [process.argv[3], members(valueOf(lock, 'cli/run/doors'))];
|
|
@@ -27,27 +22,4 @@ if (door !== undefined && door !== doors[0]) {
|
|
|
27
22
|
await (await import(__rewriteRelativeImportExtension(new URL(`./doors/${door}${ext}`, import.meta.url).href))).open(lock, root, ext, door);
|
|
28
23
|
process.exit();
|
|
29
24
|
}
|
|
30
|
-
|
|
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;
|
|
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') });
|
|
36
|
-
const schemaOf = (fields) => ({ type: 'object',
|
|
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) });
|
|
38
|
-
if (surface.tools.length)
|
|
39
|
-
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: surface.tools.map((tool) => ({ name: tool.name, description: tool.about,
|
|
40
|
-
inputSchema: schemaOf([...tool.fields]), outputSchema: shapeSchema(lock), annotations: Object.fromEntries(tool.hints.map((hint) => [`${hint}Hint`, true])) })) }));
|
|
41
|
-
if (surface.tools.length)
|
|
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)));
|
|
43
|
-
if (surface.resources.length)
|
|
44
|
-
server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
|
|
45
|
-
if (surface.resources.length)
|
|
46
|
-
server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({ resourceTemplates: surface.resources.map((r) => ({ name: r.name, uriTemplate: r.template, description: r.about, mimeType: r.mime })) }));
|
|
47
|
-
if (surface.resources.length)
|
|
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)));
|
|
49
|
-
if (surface.prompts.length)
|
|
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 })) })) }));
|
|
51
|
-
if (surface.prompts.length)
|
|
52
|
-
server.setRequestHandler(GetPromptRequestSchema, (request) => ((p) => ({ messages: p === undefined ? [] : [{ role: 'user', content: { type: 'text', text: [p.text, p.line].filter(Boolean).map((t) => fill(t, askedOf(request.params.arguments))).join('\n') } }] }))(surface.prompts.find((one) => one.name === request.params.name)));
|
|
53
|
-
await server.connect(new StdioServerTransport());
|
|
25
|
+
await serve(lock, root, ext, doors[0]);
|
|
@@ -38,7 +38,7 @@ export const linked = (asked, context) => inStanding(asked, context, async (plac
|
|
|
38
38
|
const settled = async (lot) => {
|
|
39
39
|
if (!lot.length)
|
|
40
40
|
return [];
|
|
41
|
-
const root = rootIn(context, place);
|
|
41
|
+
const root = await rootIn(context, place);
|
|
42
42
|
if ('said' in root)
|
|
43
43
|
return root.said;
|
|
44
44
|
const got = await context.landRoot(place, lot, root.account);
|
|
@@ -22,7 +22,7 @@ export const firstRun = async (asked, context, then) => {
|
|
|
22
22
|
const refused = await unless(context, say('elicit-first-run', { dir, source, document, until }));
|
|
23
23
|
if (refused)
|
|
24
24
|
return refused;
|
|
25
|
-
const born = await bornAt(context, dir, name), root = rootIn(context, dir);
|
|
25
|
+
const born = await bornAt(context, dir, name), root = await rootIn(context, dir);
|
|
26
26
|
if (born[0] !== say('born', { name, dir }) || 'said' in root)
|
|
27
27
|
return 'said' in root ? [...born, ...root.said] : born;
|
|
28
28
|
const connected = await then(dir, true), unread = connected.some((l) => l.startsWith('said/head/document-unread ')), held = unread ? undefined : await context.policy(dir);
|
package/dist/src/onboard/owed.js
CHANGED
|
@@ -15,7 +15,7 @@ export const paidIn = async (asked, context, place) => {
|
|
|
15
15
|
const owed = 'said' in held ? undefined : held.lines.find((l) => l.scope === scope && l.role === 'demands' && l.value !== 'withdraw');
|
|
16
16
|
if (owed === undefined)
|
|
17
17
|
return { said: [], answer: true };
|
|
18
|
-
const document = owed.value ?? '', read = await context.files.read.documentOf(context, place, document), root = rootIn(context, place);
|
|
18
|
+
const document = owed.value ?? '', read = await context.files.read.documentOf(context, place, document), root = await rootIn(context, place);
|
|
19
19
|
if ('said' in root)
|
|
20
20
|
return { said: root.said, answer: false };
|
|
21
21
|
if (read === undefined)
|
package/dist/src/places/admit.js
CHANGED
|
@@ -15,7 +15,7 @@ export const admitted = (asked, context) => inPlace(asked, context, async (place
|
|
|
15
15
|
const key = (asked['key'] ?? '').split('\n').map((line) => parse(line.trim())).flatMap((got) => (got.kind === 'fact' && got.value.fields['measure'] === 'public-key' ? [got.value.fields['value'] ?? ''] : []))[0];
|
|
16
16
|
if (!name || !key)
|
|
17
17
|
return [say('no-key')];
|
|
18
|
-
const [root, held] = [rootIn(context, place), await standingOr(context, place, context.policy)];
|
|
18
|
+
const [root, held] = [await rootIn(context, place), await standingOr(context, place, context.policy)];
|
|
19
19
|
if ('said' in root)
|
|
20
20
|
return root.said;
|
|
21
21
|
if ('said' in held)
|
package/dist/src/places/found.js
CHANGED
|
@@ -14,6 +14,8 @@ const birthOf = (lock, root, head, person, place, mine = `${valueOf(lock, 'found
|
|
|
14
14
|
];
|
|
15
15
|
/** A place founded in a folder: a root key made in the keychain under the place's name — never a file — the birth lines written, and the first land signed by that root. */
|
|
16
16
|
const founded = async (context, dir, name) => {
|
|
17
|
+
if (context.files.has(dir, LOCK) || context.files.has(dir, '.bound'))
|
|
18
|
+
return { refused: 'place-exists' };
|
|
17
19
|
const account = fill(valueOf(context.lock, 'founding/root-account'), { place: name });
|
|
18
20
|
const [head, person] = [deviceKey(context, 'head'), deviceKey(context, 'person')];
|
|
19
21
|
if (head === undefined || person === undefined)
|
|
@@ -37,7 +39,7 @@ export const bornPlace = async (asked, context) => {
|
|
|
37
39
|
if (!isAbsolute(dir))
|
|
38
40
|
return [say('place-absolute')];
|
|
39
41
|
const name = (asked['name'] || basename(dir)).toLowerCase().replace(/[^a-z0-9-]+/g, '-');
|
|
40
|
-
if (context.files.has(dir, LOCK))
|
|
42
|
+
if (context.files.has(dir, LOCK) || context.files.has(dir, '.bound'))
|
|
41
43
|
return [say('place-exists', { dir })];
|
|
42
44
|
const refused = await unless(context, say('elicit-place', { name, dir }));
|
|
43
45
|
return refused ?? bornAt(context, dir, name);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Recipe, type Source } from '../../core/composition.ts';
|
|
2
|
+
export interface Member {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly bytes: number;
|
|
5
|
+
readonly digest: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Runtime {
|
|
8
|
+
readonly protocol: 'head-runtime-closure@1';
|
|
9
|
+
readonly files: readonly Member[];
|
|
10
|
+
readonly bytes: number;
|
|
11
|
+
}
|
|
12
|
+
export interface BundlePort {
|
|
13
|
+
readonly runtime: Uint8Array;
|
|
14
|
+
readonly expected: string;
|
|
15
|
+
readonly take: (path: string) => Uint8Array | undefined;
|
|
16
|
+
readonly profile: Uint8Array;
|
|
17
|
+
}
|
|
18
|
+
export interface Payload {
|
|
19
|
+
readonly recipe: Recipe;
|
|
20
|
+
readonly sources: readonly Source[];
|
|
21
|
+
readonly files: ReadonlyMap<string, Uint8Array>;
|
|
22
|
+
}
|
|
23
|
+
/** Only the host's pinned public runtime closure is copied. A recipe never supplies executable paths, environment or grants. */
|
|
24
|
+
export declare function bundle(port: BundlePort, payload: Payload | undefined, budget: {
|
|
25
|
+
readonly bytes: number;
|
|
26
|
+
readonly selections: number;
|
|
27
|
+
readonly milliseconds: number;
|
|
28
|
+
readonly printed: number;
|
|
29
|
+
}, signal?: AbortSignal): Promise<{
|
|
30
|
+
bytes: NonSharedBuffer;
|
|
31
|
+
digest: string;
|
|
32
|
+
sources: string[];
|
|
33
|
+
}>;
|
|
34
|
+
/** A host provisioned closure, pinned by Head's own profile. Never discover an arbitrary node_modules tree or fetch code here. */
|
|
35
|
+
export declare function bundleAt(root: string, lock: readonly import('../../core/lock.ts').Said[], payload: Payload | undefined, signal?: AbortSignal): Promise<{
|
|
36
|
+
bytes: NonSharedBuffer;
|
|
37
|
+
digest: string;
|
|
38
|
+
sources: string[];
|
|
39
|
+
}>;
|
|
40
|
+
/** A bounded snapshot of installed public members, never arbitrary files or symlink targets. */
|
|
41
|
+
export declare function publicAt(root: string, budget: number): {
|
|
42
|
+
root: string;
|
|
43
|
+
read: (path: string) => Uint8Array | undefined;
|
|
44
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, lstatSync, realpathSync, openSync, closeSync, fstatSync, readSync, constants } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { VERSIONED_MANIFEST_SCHEMAS } from '@anthropic-ai/mcpb';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { spawned } from "../process.js";
|
|
8
|
+
import { compose } from "../../core/composition.js";
|
|
9
|
+
const digest = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
10
|
+
const safe = (path) => /^[A-Za-z0-9_@.+/-]+$/.test(path) && path.split('/').every(part => part !== '' && part !== '.' && part !== '..');
|
|
11
|
+
/** Only the host's pinned public runtime closure is copied. A recipe never supplies executable paths, environment or grants. */
|
|
12
|
+
export async function bundle(port, payload, budget, signal) {
|
|
13
|
+
signal?.throwIfAborted();
|
|
14
|
+
if (Object.values(budget).some(n => !Number.isSafeInteger(n) || n < 1))
|
|
15
|
+
throw Error('bundle/budget');
|
|
16
|
+
if (digest(port.runtime) !== port.expected)
|
|
17
|
+
throw Error('bundle/runtime-digest');
|
|
18
|
+
const runtime = JSON.parse(new TextDecoder().decode(port.runtime));
|
|
19
|
+
if (runtime.protocol !== 'head-runtime-closure@1' || !Array.isArray(runtime.files) || !runtime.files.length || !Number.isSafeInteger(runtime.bytes) || runtime.bytes < 1)
|
|
20
|
+
throw Error('bundle/runtime');
|
|
21
|
+
const files = new Map();
|
|
22
|
+
let size = 0;
|
|
23
|
+
for (const member of runtime.files) {
|
|
24
|
+
if (!safe(member.path) || !/^(dist\/src\/|node_modules\/|package\.json$|LICENSE$)/.test(member.path) || files.has(member.path))
|
|
25
|
+
throw Error('bundle/runtime-path');
|
|
26
|
+
const bytes = port.take(member.path);
|
|
27
|
+
if (!bytes || bytes.byteLength !== member.bytes || digest(bytes) !== member.digest)
|
|
28
|
+
throw Error('bundle/runtime-member');
|
|
29
|
+
files.set(member.path, bytes);
|
|
30
|
+
size += bytes.byteLength;
|
|
31
|
+
}
|
|
32
|
+
if (size !== runtime.bytes || !files.has('dist/src/index.js') || !files.has('package.json') || !files.has('LICENSE'))
|
|
33
|
+
throw Error('bundle/runtime-closure');
|
|
34
|
+
files.set('runtime.bundle.json', port.runtime).set('TARGET.bound', port.profile).set('.mcpbignore', Buffer.from('!**\n*.mts\n*changelog*\n.env*\n.git/\n.bound/\nbirth.bound\n'));
|
|
35
|
+
let manifest = { manifest_version: '0.3', name: 'head', version: JSON.parse(new TextDecoder().decode(files.get('package.json'))).version, description: 'Head without connected sources', author: { name: 'Package author' } };
|
|
36
|
+
if (payload) {
|
|
37
|
+
const made = compose(payload.recipe, payload.sources, budget.selections);
|
|
38
|
+
let publicBytes = made.bytes.byteLength;
|
|
39
|
+
for (const [path, bytes] of payload.files) {
|
|
40
|
+
if (!safe(path) || /(^|\/)(\.bound|\.git|birth\.bound|node_modules)(\/|$)/.test(path))
|
|
41
|
+
throw Error('bundle/public-path');
|
|
42
|
+
publicBytes += bytes.byteLength;
|
|
43
|
+
if (publicBytes > budget.bytes)
|
|
44
|
+
throw Error('bundle/public-budget');
|
|
45
|
+
files.set(`composition/${path}`, bytes);
|
|
46
|
+
}
|
|
47
|
+
if (!payload.files.has('TARGET.bound'))
|
|
48
|
+
throw Error('bundle/public-lock');
|
|
49
|
+
files.set('composition/selection.json', made.bytes);
|
|
50
|
+
manifest = { ...manifest, name: payload.recipe.id, version: payload.recipe.revision, display_name: payload.recipe.title, description: payload.recipe.description };
|
|
51
|
+
}
|
|
52
|
+
manifest = { ...manifest, server: { type: 'node', entry_point: 'dist/src/index.js', mcp_config: { command: 'node', args: ['${__dirname}/dist/src/index.js', 'run'] } }, compatibility: { platforms: ['darwin'], runtimes: { node: '>=22.18' } } };
|
|
53
|
+
VERSIONED_MANIFEST_SCHEMAS['0.3'].parse(manifest);
|
|
54
|
+
files.set('manifest.json', Buffer.from(JSON.stringify(manifest, null, 2) + '\n'));
|
|
55
|
+
const temporary = mkdtempSync(join(tmpdir(), 'head-export-'));
|
|
56
|
+
try {
|
|
57
|
+
const stage = join(temporary, 'package'), output = join(temporary, 'package.mcpb');
|
|
58
|
+
for (const [path, bytes] of files) {
|
|
59
|
+
signal?.throwIfAborted();
|
|
60
|
+
const at = join(stage, path);
|
|
61
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
62
|
+
writeFileSync(at, bytes);
|
|
63
|
+
}
|
|
64
|
+
const cli = join(dirname(createRequire(import.meta.url).resolve('@anthropic-ai/mcpb')), 'cli/cli.js');
|
|
65
|
+
const result = await spawned(process.execPath, [cli, 'pack', stage, output], { cwd: temporary, env: { PATH: dirname(process.execPath), HOME: temporary, TMPDIR: temporary } }, { wait: budget.milliseconds, most: budget.printed, ...(signal ? { signal } : {}) });
|
|
66
|
+
if (result.code !== 0)
|
|
67
|
+
throw Error('bundle/pack');
|
|
68
|
+
signal?.throwIfAborted();
|
|
69
|
+
const bytes = readFileSync(output);
|
|
70
|
+
return { bytes, digest: digest(bytes), sources: payload?.recipe.sources.map(source => source.source) ?? [] };
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** A host provisioned closure, pinned by Head's own profile. Never discover an arbitrary node_modules tree or fetch code here. */
|
|
77
|
+
export async function bundleAt(root, lock, payload, signal) {
|
|
78
|
+
const { valueOf } = await import("../../core/lock.js");
|
|
79
|
+
const expected = valueOf(lock, 'dep/runtime-bundle');
|
|
80
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(expected))
|
|
81
|
+
throw Error('bundle/runtime-unavailable');
|
|
82
|
+
const packages = new Map();
|
|
83
|
+
const take = (path) => {
|
|
84
|
+
try {
|
|
85
|
+
return readFileSync(join(root, path));
|
|
86
|
+
}
|
|
87
|
+
catch { /* Resolve only the package paths selected by the pinned inventory. */ }
|
|
88
|
+
try {
|
|
89
|
+
let rest = path, from = createRequire(join(root, 'package.json')), at = root;
|
|
90
|
+
while (rest.startsWith('node_modules/')) {
|
|
91
|
+
const parts = rest.slice(13).split('/'), count = parts[0].startsWith('@') ? 2 : 1, name = parts.slice(0, count).join('/');
|
|
92
|
+
const key = at + '\0' + name, cached = packages.get(key);
|
|
93
|
+
if (cached)
|
|
94
|
+
at = cached;
|
|
95
|
+
else {
|
|
96
|
+
try {
|
|
97
|
+
at = dirname(from.resolve(name + '/package.json'));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
at = dirname(from.resolve(name));
|
|
101
|
+
while (true) {
|
|
102
|
+
try {
|
|
103
|
+
if (JSON.parse(readFileSync(join(at, 'package.json'), 'utf8')).name === name)
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
catch { }
|
|
107
|
+
const parent = dirname(at);
|
|
108
|
+
if (parent === at)
|
|
109
|
+
throw Error('bundle/runtime-member');
|
|
110
|
+
at = parent;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
packages.set(key, at);
|
|
114
|
+
}
|
|
115
|
+
from = createRequire(join(at, 'package.json'));
|
|
116
|
+
rest = parts.slice(count).join('/');
|
|
117
|
+
}
|
|
118
|
+
return path.startsWith('node_modules/') ? readFileSync(join(at, rest)) : undefined;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
return bundle({ runtime: readFileSync(join(root, 'runtime.bundle.json')), expected, take, profile: readFileSync(join(root, 'TARGET.bound')) }, payload, { bytes: Number(valueOf(lock, 'share/bundle-bytes').split('..')[1]), selections: Number(valueOf(lock, 'share/bundle-selections').split('..')[1]), milliseconds: Number(valueOf(lock, 'bound/wait').split('..')[1]), printed: Number(valueOf(lock, 'bound/printed').split('..')[1]) }, signal);
|
|
125
|
+
}
|
|
126
|
+
/** A bounded snapshot of installed public members, never arbitrary files or symlink targets. */
|
|
127
|
+
export function publicAt(root, budget) {
|
|
128
|
+
if (!Number.isSafeInteger(budget) || budget < 1)
|
|
129
|
+
throw Error('bundle/budget');
|
|
130
|
+
const base = realpathSync(root), held = new Map();
|
|
131
|
+
let consumed = 0;
|
|
132
|
+
return { root: base, read: (path) => {
|
|
133
|
+
if (!safe(path) || !(path === 'manifest.json' || path.startsWith('composition/')))
|
|
134
|
+
throw Error('bundle/public-path');
|
|
135
|
+
if (held.has(path))
|
|
136
|
+
return held.get(path);
|
|
137
|
+
try {
|
|
138
|
+
let at = base;
|
|
139
|
+
for (const part of path.split('/')) {
|
|
140
|
+
at = join(at, part);
|
|
141
|
+
if (lstatSync(at).isSymbolicLink())
|
|
142
|
+
throw Error('bundle/public-path');
|
|
143
|
+
}
|
|
144
|
+
const fd = openSync(at, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
145
|
+
try {
|
|
146
|
+
const stat = fstatSync(fd);
|
|
147
|
+
if (!stat.isFile() || stat.size > budget - consumed)
|
|
148
|
+
throw Error('bundle/public-budget');
|
|
149
|
+
const bytes = Buffer.alloc(stat.size);
|
|
150
|
+
let offset = 0;
|
|
151
|
+
while (offset < bytes.length) {
|
|
152
|
+
const count = readSync(fd, bytes, offset, bytes.length - offset, null);
|
|
153
|
+
if (!count)
|
|
154
|
+
throw Error('bundle/public-member');
|
|
155
|
+
offset += count;
|
|
156
|
+
}
|
|
157
|
+
if (readSync(fd, Buffer.alloc(1), 0, 1, null))
|
|
158
|
+
throw Error('bundle/public-budget');
|
|
159
|
+
consumed += bytes.length;
|
|
160
|
+
held.set(path, bytes);
|
|
161
|
+
return bytes;
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
closeSync(fd);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error.code === 'ENOENT')
|
|
169
|
+
return undefined;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
} };
|
|
173
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ConfiguredSource, type Tool } from './mcp.ts';
|
|
2
|
+
import type { Asked, Context } from '../../core/answer.ts';
|
|
3
|
+
import type { Said } from '../../core/lock.ts';
|
|
4
|
+
import { vocabOf } from '../../core/read.ts';
|
|
5
|
+
export declare const configuredProtocol: Readonly<{
|
|
6
|
+
id: "mcp";
|
|
7
|
+
sourceNeeded: "bundle-mcp-needed";
|
|
8
|
+
invoke: "elicit-mcp-call";
|
|
9
|
+
failurePrefix: "mcp/";
|
|
10
|
+
toolFailure: "mcp/tool";
|
|
11
|
+
connection: (bytes: Uint8Array, source: string, overlay: boolean, v: Pick<ReturnType<typeof vocabOf>, "offers" | "class">) => {
|
|
12
|
+
worlds: string[];
|
|
13
|
+
verify: (lines: readonly Said[]) => void;
|
|
14
|
+
} | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export interface ConfiguredDocument {
|
|
17
|
+
readonly protocol: 'head-configured-mcp-document@1';
|
|
18
|
+
readonly configuration: ConfiguredSource;
|
|
19
|
+
readonly tools: readonly Tool[];
|
|
20
|
+
readonly resource: string;
|
|
21
|
+
}
|
|
22
|
+
/** The public document remains input to the published MCP interpreter. Host metadata supplies transport, never an offer class. */
|
|
23
|
+
export declare function configuredDocument(bytes: Uint8Array): ConfiguredDocument | undefined;
|
|
24
|
+
export declare const configuredBytes: (configuration: ConfiguredSource, tools: readonly Tool[]) => Uint8Array;
|
|
25
|
+
/** Fresh source bytes must be covered by the compiled reading before retained results or a network request can be used. */
|
|
26
|
+
export declare function configuredAt(context: Context, place: string, source: string, lines: readonly Said[]): ConfiguredSource | undefined;
|
|
27
|
+
/** Discovery is explicitly selected by the consumer; there is no automatic protocol guessing or network on package inspection. */
|
|
28
|
+
export declare function discoverConfigured(context: Context, asked: Asked, source: string): Promise<{
|
|
29
|
+
bytes: Uint8Array<ArrayBufferLike>;
|
|
30
|
+
kind: string;
|
|
31
|
+
}>;
|
|
32
|
+
/** The same safe, named refusal is used at connect and call, without printing transport exceptions or credentials. */
|
|
33
|
+
export declare const configuredRefusal: (context: Context, error: unknown) => string[];
|
|
34
|
+
/** Verify public configured inputs at inspection, and require an exact compiled operation mapping; never guess tool names. */
|
|
35
|
+
export declare const configuredPackage: (_context: Context, files: ReadonlyMap<string, Uint8Array | undefined>) => ConfiguredDocument[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { configuredSource, fingerprint } from "./mcp.js";
|
|
2
|
+
import { valueOf } from "../../core/lock.js";
|
|
3
|
+
import { fieldsOf, vocabOf } from "../../core/read.js";
|
|
4
|
+
import { digestIn } from "../../core/worlds.js";
|
|
5
|
+
import { sayIn } from "../../core/surface.js";
|
|
6
|
+
export const configuredProtocol = Object.freeze({ id: 'mcp', sourceNeeded: 'bundle-mcp-needed', invoke: 'elicit-mcp-call', failurePrefix: 'mcp/', toolFailure: 'mcp/tool',
|
|
7
|
+
connection: (bytes, source, overlay, v) => { const doc = configuredDocument(bytes); if (!doc)
|
|
8
|
+
return undefined; if (doc.configuration.name !== source || overlay)
|
|
9
|
+
throw Error('mcp/document'); return { worlds: ['topos-mcp'], verify: (lines) => { if (doc.configuration.offers.some(name => !lines.some(l => l.scope === `${v.offers}/${source}/${name}` && l.measure === v.class)))
|
|
10
|
+
throw Error('mcp/tool-mapping'); } }; }
|
|
11
|
+
});
|
|
12
|
+
/** The public document remains input to the published MCP interpreter. Host metadata supplies transport, never an offer class. */
|
|
13
|
+
export function configuredDocument(bytes) {
|
|
14
|
+
let doc;
|
|
15
|
+
try {
|
|
16
|
+
doc = JSON.parse(Buffer.from(bytes).toString());
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (doc?.protocol !== 'head-configured-mcp-document@1')
|
|
22
|
+
return undefined;
|
|
23
|
+
if (Object.keys(doc).sort().join('|') !== 'configuration|protocol|resource|tools' || !Array.isArray(doc.tools))
|
|
24
|
+
throw Error('mcp/document');
|
|
25
|
+
const configuration = configuredSource(doc.configuration);
|
|
26
|
+
if (doc.resource !== configuration.endpoint || doc.tools.some((t) => !t || typeof t.name !== 'string' || t.inputSchema?.type !== 'object')
|
|
27
|
+
|| new Set(doc.tools.map((t) => t.name)).size !== doc.tools.length || fingerprint(doc.tools).digest !== configuration.catalogue
|
|
28
|
+
|| configuration.offers.some(name => !doc.tools.some((t) => t.name === name)))
|
|
29
|
+
throw Error('mcp/document');
|
|
30
|
+
return { protocol: doc.protocol, configuration, tools: doc.tools, resource: doc.resource };
|
|
31
|
+
}
|
|
32
|
+
export const configuredBytes = (configuration, tools) => {
|
|
33
|
+
const bytes = Buffer.from(JSON.stringify({ protocol: 'head-configured-mcp-document@1', configuration, tools, resource: configuration.endpoint }) + '\n');
|
|
34
|
+
configuredDocument(bytes);
|
|
35
|
+
return bytes;
|
|
36
|
+
};
|
|
37
|
+
/** Fresh source bytes must be covered by the compiled reading before retained results or a network request can be used. */
|
|
38
|
+
export function configuredAt(context, place, source, lines) {
|
|
39
|
+
const laid = context.files.read.laidOf(context, place, source).filter(([path]) => !path.includes('.overlay.'));
|
|
40
|
+
const docs = laid.map(([path, bytes]) => ({ path, bytes, doc: configuredDocument(bytes) })).filter(one => one.doc);
|
|
41
|
+
if (!docs.length) {
|
|
42
|
+
if (lines.some(l => l.scope?.startsWith(`offers/${source}/`) && l.measure === 'declares' && ['read-only', 'unhinted', 'destructive'].includes(l.value ?? '')))
|
|
43
|
+
throw Error('mcp/document-pin');
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const v = vocabOf(context.lock);
|
|
47
|
+
if (docs.length !== 1 || docs[0].doc.configuration.name !== source || !(fieldsOf(lines, `${v.region}/${source}`)[valueOf(context.lock, 'vocab/documents')] ?? []).includes(digestIn(context.lock)(docs[0].bytes)))
|
|
48
|
+
throw Error('mcp/document-pin');
|
|
49
|
+
return docs[0].doc.configuration;
|
|
50
|
+
}
|
|
51
|
+
/** Discovery is explicitly selected by the consumer; there is no automatic protocol guessing or network on package inspection. */
|
|
52
|
+
export async function discoverConfigured(context, asked, source) {
|
|
53
|
+
if (!context.remote || !source || !/^[a-z][a-z0-9-]*$/.test(source))
|
|
54
|
+
throw Error('mcp/unavailable');
|
|
55
|
+
const endpoint = asked['document'] ?? '', url = new URL(endpoint);
|
|
56
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.href !== endpoint)
|
|
57
|
+
throw Error('mcp/endpoint');
|
|
58
|
+
if (asked['via'])
|
|
59
|
+
throw Error('mcp/discovery-access-unsupported');
|
|
60
|
+
const read = await (await context.remote()).discover(endpoint);
|
|
61
|
+
const configuration = configuredSource({ protocol: 'head-configured-mcp@1', name: source, endpoint, catalogue: read.digest, offers: read.tools.map(tool => tool.name) });
|
|
62
|
+
return { bytes: configuredBytes(configuration, read.tools), kind: 'json' };
|
|
63
|
+
}
|
|
64
|
+
/** The same safe, named refusal is used at connect and call, without printing transport exceptions or credentials. */
|
|
65
|
+
export const configuredRefusal = (context, error) => [sayIn(context.lock)('mcp-refused', { why: error instanceof Error && /^mcp\/[a-z-]+$/.test(error.message) ? error.message : 'mcp/transport' })];
|
|
66
|
+
/** Verify public configured inputs at inspection, and require an exact compiled operation mapping; never guess tool names. */
|
|
67
|
+
export const configuredPackage = (_context, files) => [...files].flatMap(([path, bytes]) => { const doc = bytes && path.endsWith('.json') ? configuredDocument(bytes) : undefined; return doc ? [doc] : []; });
|