@lapxo/head 0.1.7 → 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 +39 -14
- package/TARGET.bound +237 -31
- package/dist/src/adopt/adopt.d.ts +2 -2
- package/dist/src/adopt/adopt.js +20 -6
- package/dist/src/ahead/read.d.ts +2 -2
- package/dist/src/ahead/read.js +2 -4
- 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 +12 -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 +17 -4
- 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 +11 -3
- 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 +14 -5
- package/dist/src/flows/device.js +15 -7
- package/dist/src/flows/plan.d.ts +2 -0
- package/dist/src/flows/plan.js +16 -2
- package/dist/src/index.js +3 -31
- 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 +23 -12
- package/dist/src/links/link.d.ts +2 -2
- package/dist/src/links/link.js +7 -10
- 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/ports/bound.d.ts +0 -1
- package/dist/src/ports/bound.js +14 -15
- package/dist/src/ports/host.d.ts +18 -0
- package/dist/src/ports/host.js +58 -9
- package/dist/src/sources/connect.d.ts +1 -0
- package/dist/src/sources/connect.js +15 -11
- package/dist/src/tools/card.js +3 -2
- package/dist/src/use/named.js +3 -3
- package/dist/src/verbs/ask.js +7 -1
- package/dist/src/verbs/card.js +2 -2
- package/dist/src/verbs/do.js +2 -1
- package/package.json +6 -5
- package/runtime.bundle.json +24026 -0
- package/dist/src/ports/reading.d.ts +0 -8
- package/dist/src/ports/reading.js +0 -14
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] : []; });
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface Tool {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly inputSchema: {
|
|
4
|
+
readonly type: 'object';
|
|
5
|
+
};
|
|
6
|
+
readonly annotations?: {
|
|
7
|
+
readonly readOnlyHint?: boolean;
|
|
8
|
+
};
|
|
9
|
+
readonly [key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface Options {
|
|
12
|
+
readonly signal: AbortSignal;
|
|
13
|
+
readonly timeout: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ClientPort {
|
|
16
|
+
readonly list: (after: string | undefined, options: Options) => Promise<{
|
|
17
|
+
tools: Tool[];
|
|
18
|
+
next?: string;
|
|
19
|
+
}>;
|
|
20
|
+
readonly callTool: (params: {
|
|
21
|
+
name: string;
|
|
22
|
+
arguments: Record<string, unknown>;
|
|
23
|
+
}, options: Options) => Promise<unknown>;
|
|
24
|
+
readonly close: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export interface Limits {
|
|
27
|
+
readonly pages: number;
|
|
28
|
+
readonly tools: number;
|
|
29
|
+
readonly bytes: number;
|
|
30
|
+
readonly milliseconds: number;
|
|
31
|
+
}
|
|
32
|
+
export declare const fingerprint: (tools: readonly Tool[]) => {
|
|
33
|
+
tools: readonly Tool[];
|
|
34
|
+
bytes: Buffer<ArrayBuffer>;
|
|
35
|
+
digest: string;
|
|
36
|
+
};
|
|
37
|
+
/** The host supplies an already permitted transport. Discovery observes a catalogue; hints never authorize a call. */
|
|
38
|
+
export declare function withCatalogue<T>(connect: (options: Options) => Promise<ClientPort>, limits: Limits, signal: AbortSignal | undefined, use: (session: {
|
|
39
|
+
readonly read: () => Promise<ReturnType<typeof fingerprint>>;
|
|
40
|
+
readonly call: (pin: string, tool: string, args: Record<string, unknown>, authorize: () => Promise<boolean>) => Promise<unknown>;
|
|
41
|
+
}) => Promise<T>): Promise<T>;
|
|
42
|
+
export interface ConfiguredSource {
|
|
43
|
+
readonly protocol: 'head-configured-mcp@1';
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly endpoint: string;
|
|
46
|
+
readonly catalogue: string;
|
|
47
|
+
readonly offers: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
export type PermittedFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
50
|
+
export type RemoteFactory = (endpoint: string, send: PermittedFetch, options: Options) => Promise<ClientPort>;
|
|
51
|
+
/** Public configuration contains no execution, environment, headers or credentials. A URL never supplies a network capability. */
|
|
52
|
+
export declare function configuredSource(value: unknown): ConfiguredSource;
|
|
53
|
+
/** The host grants every connection, including DNS/address policy and receiver-only credentials, through its fetch capability. */
|
|
54
|
+
export declare function withConfiguredSource<T>(configuration: ConfiguredSource, factory: RemoteFactory, permitted: PermittedFetch | undefined, limits: Limits & {
|
|
55
|
+
readonly wireBytes: number;
|
|
56
|
+
}, signal: AbortSignal | undefined, use: (call: (tool: string, args: Record<string, unknown>, authorize: () => Promise<boolean>) => Promise<unknown>) => Promise<T>): Promise<T>;
|
|
57
|
+
/** One bounded SDK session; redirects and ambient credentials are never inherited. */
|
|
58
|
+
export declare function transportFor(endpoint: string, permitted: PermittedFetch, wireBytes: number, options: Options): PermittedFetch;
|
|
59
|
+
export { remoteFactory } from './protocol.ts';
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export const fingerprint = (tools) => {
|
|
3
|
+
const stable = (value) => Array.isArray(value) ? value.map(stable) : value !== null && typeof value === 'object'
|
|
4
|
+
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, child]) => [key, stable(child)])) : value;
|
|
5
|
+
const bytes = Buffer.from(JSON.stringify(stable([...tools].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0))));
|
|
6
|
+
return { tools, bytes, digest: `sha256:${createHash('sha256').update(bytes).digest('hex')}` };
|
|
7
|
+
};
|
|
8
|
+
/** The host supplies an already permitted transport. Discovery observes a catalogue; hints never authorize a call. */
|
|
9
|
+
export async function withCatalogue(connect, limits, signal, use) {
|
|
10
|
+
if (Object.values(limits).some(value => !Number.isSafeInteger(value) || value < 1))
|
|
11
|
+
throw Error('mcp/budget');
|
|
12
|
+
let client;
|
|
13
|
+
const stop = AbortSignal.any([AbortSignal.timeout(limits.milliseconds), ...(signal ? [signal] : [])]);
|
|
14
|
+
const close = () => { void client?.close().catch(() => { }); };
|
|
15
|
+
stop.addEventListener('abort', close, { once: true });
|
|
16
|
+
const options = { signal: stop, timeout: limits.milliseconds };
|
|
17
|
+
try {
|
|
18
|
+
stop.throwIfAborted();
|
|
19
|
+
client = await connect(options);
|
|
20
|
+
const connected = client;
|
|
21
|
+
const read = async () => {
|
|
22
|
+
const tools = [], names = new Set(), pages = new Set();
|
|
23
|
+
let after, size = 0;
|
|
24
|
+
for (let page = 0; page < limits.pages; page++) {
|
|
25
|
+
stop.throwIfAborted();
|
|
26
|
+
const result = await connected.list(after, options);
|
|
27
|
+
size += Buffer.byteLength(JSON.stringify(result));
|
|
28
|
+
if (size > limits.bytes || tools.length + result.tools.length > limits.tools)
|
|
29
|
+
throw Error('mcp/budget');
|
|
30
|
+
for (const tool of result.tools) {
|
|
31
|
+
if (names.has(tool.name))
|
|
32
|
+
throw Error('mcp/collision');
|
|
33
|
+
names.add(tool.name);
|
|
34
|
+
tools.push(tool);
|
|
35
|
+
}
|
|
36
|
+
after = result.next;
|
|
37
|
+
if (after === undefined)
|
|
38
|
+
return fingerprint(tools);
|
|
39
|
+
if (!after || pages.has(after))
|
|
40
|
+
throw Error('mcp/page-loop');
|
|
41
|
+
pages.add(after);
|
|
42
|
+
}
|
|
43
|
+
throw Error('mcp/budget');
|
|
44
|
+
};
|
|
45
|
+
return await use({ read, call: async (pin, tool, args, authorize) => {
|
|
46
|
+
const current = await read();
|
|
47
|
+
if (current.digest !== pin)
|
|
48
|
+
throw Error('mcp/catalogue-changed');
|
|
49
|
+
if (!current.tools.some(entry => entry.name === tool))
|
|
50
|
+
throw Error('mcp/tool');
|
|
51
|
+
if (!await authorize())
|
|
52
|
+
throw Error('mcp/not-authorized');
|
|
53
|
+
if ((await read()).digest !== pin)
|
|
54
|
+
throw Error('mcp/catalogue-changed');
|
|
55
|
+
stop.throwIfAborted();
|
|
56
|
+
return connected.callTool({ name: tool, arguments: args }, options);
|
|
57
|
+
} });
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
stop.removeEventListener('abort', close);
|
|
61
|
+
await client?.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Public configuration contains no execution, environment, headers or credentials. A URL never supplies a network capability. */
|
|
65
|
+
export function configuredSource(value) {
|
|
66
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
67
|
+
throw Error('mcp/configuration');
|
|
68
|
+
const one = value;
|
|
69
|
+
if (Object.keys(one).sort().join('|') !== 'catalogue|endpoint|name|offers|protocol' || one['protocol'] !== 'head-configured-mcp@1'
|
|
70
|
+
|| typeof one['name'] !== 'string' || !/^[a-z][a-z0-9-]*$/.test(one['name']) || typeof one['catalogue'] !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(one['catalogue'])
|
|
71
|
+
|| typeof one['endpoint'] !== 'string' || !URL.canParse(one['endpoint']) || !Array.isArray(one['offers']) || !one['offers'].length || one['offers'].some(n => typeof n !== 'string' || !n.trim()) || new Set(one['offers']).size !== one['offers'].length)
|
|
72
|
+
throw Error('mcp/configuration');
|
|
73
|
+
const url = new URL(one['endpoint']);
|
|
74
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.href !== one['endpoint'])
|
|
75
|
+
throw Error('mcp/endpoint');
|
|
76
|
+
return { protocol: 'head-configured-mcp@1', name: one['name'], endpoint: url.href, catalogue: one['catalogue'], offers: [...one['offers']] };
|
|
77
|
+
}
|
|
78
|
+
/** The host grants every connection, including DNS/address policy and receiver-only credentials, through its fetch capability. */
|
|
79
|
+
export async function withConfiguredSource(configuration, factory, permitted, limits, signal, use) {
|
|
80
|
+
const source = configuredSource(configuration);
|
|
81
|
+
if (!permitted)
|
|
82
|
+
throw Error('mcp/network-unavailable');
|
|
83
|
+
if (!Number.isSafeInteger(limits.wireBytes) || limits.wireBytes < 1)
|
|
84
|
+
throw Error('mcp/budget');
|
|
85
|
+
if (source.offers.length > limits.tools || Buffer.byteLength(JSON.stringify(source)) > limits.bytes)
|
|
86
|
+
throw Error('mcp/budget');
|
|
87
|
+
return withCatalogue(options => factory(source.endpoint, transportFor(source.endpoint, permitted, limits.wireBytes, options), options), limits, signal, async (session) => {
|
|
88
|
+
const current = await session.read();
|
|
89
|
+
if (current.digest !== source.catalogue || source.offers.some(name => !current.tools.some(tool => tool.name === name)))
|
|
90
|
+
throw Error('mcp/catalogue-changed');
|
|
91
|
+
return use((tool, args, authorize) => {
|
|
92
|
+
if (!source.offers.includes(tool))
|
|
93
|
+
return Promise.reject(Error('mcp/tool'));
|
|
94
|
+
return session.call(source.catalogue, tool, args, authorize);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** One bounded SDK session; redirects and ambient credentials are never inherited. */
|
|
99
|
+
export function transportFor(endpoint, permitted, wireBytes, options) {
|
|
100
|
+
if (!Number.isSafeInteger(wireBytes) || wireBytes < 1)
|
|
101
|
+
throw Error('mcp/budget');
|
|
102
|
+
let bytes = 0;
|
|
103
|
+
return async (url, init) => {
|
|
104
|
+
if (url !== endpoint || !['GET', 'POST', 'DELETE'].includes(init.method ?? 'GET'))
|
|
105
|
+
throw Error('mcp/endpoint');
|
|
106
|
+
options.signal.throwIfAborted();
|
|
107
|
+
const got = await permitted(url, { ...init, redirect: 'manual', credentials: 'omit', signal: options.signal });
|
|
108
|
+
if (got.status >= 300 && got.status < 400) {
|
|
109
|
+
await got.body?.cancel();
|
|
110
|
+
throw Error('mcp/redirect');
|
|
111
|
+
}
|
|
112
|
+
const body = got.body?.pipeThrough(new TransformStream({ transform(chunk, controller) {
|
|
113
|
+
options.signal.throwIfAborted();
|
|
114
|
+
bytes += chunk.byteLength;
|
|
115
|
+
if (bytes > wireBytes)
|
|
116
|
+
throw Error('mcp/budget');
|
|
117
|
+
controller.enqueue(chunk);
|
|
118
|
+
} }));
|
|
119
|
+
return new Response(body ?? null, { status: got.status, statusText: got.statusText, headers: got.headers });
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export { remoteFactory } from "./protocol.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import type { PermittedFetch } from './mcp.ts';
|
|
3
|
+
export interface NetworkPolicy {
|
|
4
|
+
readonly internal?: readonly {
|
|
5
|
+
readonly hostname: string;
|
|
6
|
+
readonly address: string;
|
|
7
|
+
}[];
|
|
8
|
+
readonly ca?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const publicAddress: (address: string) => boolean;
|
|
11
|
+
/** Receiver-owned transport: payloads cannot supply DNS policy, private bindings, trust roots or credentials. */
|
|
12
|
+
export declare function networkFor(endpoint: string, policy?: NetworkPolicy, resolve?: typeof lookup): PermittedFetch;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import { BlockList, isIP } from 'node:net';
|
|
3
|
+
import { request } from 'node:https';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
const excluded = new BlockList();
|
|
6
|
+
for (const [net, bits] of [['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8], ['169.254.0.0', 16], ['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24], ['192.168.0.0', 16], ['198.18.0.0', 15], ['198.51.100.0', 24], ['203.0.113.0', 24], ['224.0.0.0', 4], ['240.0.0.0', 4]])
|
|
7
|
+
excluded.addSubnet(net, bits);
|
|
8
|
+
for (const [net, bits] of [['2001::', 23], ['2001:db8::', 32], ['2002::', 16], ['3fff::', 20]])
|
|
9
|
+
excluded.addSubnet(net, bits, 'ipv6');
|
|
10
|
+
const global = new BlockList();
|
|
11
|
+
global.addSubnet('2000::', 3, 'ipv6');
|
|
12
|
+
export const publicAddress = (address) => isIP(address) === 4 ? !excluded.check(address) : isIP(address) === 6 && global.check(address, 'ipv6') && !excluded.check(address, 'ipv6');
|
|
13
|
+
/** Receiver-owned transport: payloads cannot supply DNS policy, private bindings, trust roots or credentials. */
|
|
14
|
+
export function networkFor(endpoint, policy = {}, resolve = lookup) {
|
|
15
|
+
const target = new URL(endpoint);
|
|
16
|
+
if (target.protocol !== 'https:' || target.username || target.password || target.search || target.hash || target.href !== endpoint)
|
|
17
|
+
throw Error('mcp/endpoint');
|
|
18
|
+
return async (url, init) => {
|
|
19
|
+
if (url !== endpoint || !['GET', 'POST', 'DELETE'].includes(init.method ?? 'GET') || init.redirect !== 'manual' || init.credentials !== 'omit' || !init.signal)
|
|
20
|
+
throw Error('mcp/network-policy');
|
|
21
|
+
const signal = init.signal;
|
|
22
|
+
signal.throwIfAborted();
|
|
23
|
+
const host = target.hostname.replace(/^\[|\]$/g, '');
|
|
24
|
+
let abort;
|
|
25
|
+
const stopped = new Promise((_, reject) => { abort = () => reject(Error('mcp/cancelled')); signal.addEventListener('abort', abort, { once: true }); });
|
|
26
|
+
let addresses;
|
|
27
|
+
try {
|
|
28
|
+
addresses = await Promise.race([resolve(host, { all: true, verbatim: true }), stopped]);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
signal.removeEventListener('abort', abort);
|
|
32
|
+
}
|
|
33
|
+
signal.throwIfAborted();
|
|
34
|
+
if (!addresses.length || addresses.some(({ address }) => !publicAddress(address) && !policy.internal?.some(one => one.hostname === host && one.address === address)))
|
|
35
|
+
throw Error('mcp/destination');
|
|
36
|
+
const chosen = addresses[0];
|
|
37
|
+
const headers = Object.fromEntries(new Headers(init.headers));
|
|
38
|
+
if (Object.keys(headers).some(k => ['host', 'connection', 'transfer-encoding', 'content-length', 'proxy-authorization', 'cookie'].includes(k)) || Buffer.byteLength(JSON.stringify(headers)) > 16384)
|
|
39
|
+
throw Error('mcp/headers');
|
|
40
|
+
const body = init.body;
|
|
41
|
+
if (body !== undefined && body !== null && typeof body !== 'string' && !(body instanceof Uint8Array))
|
|
42
|
+
throw Error('mcp/request-body');
|
|
43
|
+
if (body && Buffer.byteLength(body) > 16777216)
|
|
44
|
+
throw Error('mcp/request-budget');
|
|
45
|
+
return new Promise((resolveResponse, reject) => {
|
|
46
|
+
const req = request(target, { method: init.method, headers, agent: false, signal, rejectUnauthorized: true, maxHeaderSize: 16384, ...(policy.ca ? { ca: policy.ca } : {}), family: chosen.family,
|
|
47
|
+
lookup: (_hostname, options, done) => options.all ? done(null, [chosen]) : done(null, chosen.address, chosen.family) }, response => {
|
|
48
|
+
const responseHeaders = new Headers();
|
|
49
|
+
for (const [name, value] of Object.entries(response.headers))
|
|
50
|
+
if (value !== undefined)
|
|
51
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
52
|
+
responseHeaders.append(name, item);
|
|
53
|
+
const status = response.statusCode ?? 500;
|
|
54
|
+
if ([204, 205, 304].includes(status)) {
|
|
55
|
+
response.resume();
|
|
56
|
+
resolveResponse(new Response(null, { status, headers: responseHeaders }));
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
resolveResponse(new Response(Readable.toWeb(response), { status, headers: responseHeaders }));
|
|
60
|
+
});
|
|
61
|
+
req.on('error', () => reject(Error(signal.aborted ? 'mcp/cancelled' : 'mcp/transport')));
|
|
62
|
+
if (body)
|
|
63
|
+
req.write(body);
|
|
64
|
+
req.end();
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Said } from '../../core/lock.ts';
|
|
2
|
+
import type { RemoteFactory } from './mcp.ts';
|
|
3
|
+
/** Incoming and outgoing SDK bindings; application decisions stay behind the existing ports. */
|
|
4
|
+
export declare function serve(lock: readonly Said[], root: string, ext: string, doorName: string): Promise<void>;
|
|
5
|
+
/** Concrete outgoing SDK transport. Every network operation still goes through the separately permitted host capability. */
|
|
6
|
+
export declare const remoteFactory: RemoteFactory;
|