@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.
Files changed (70) hide show
  1. package/README.md +39 -14
  2. package/TARGET.bound +237 -31
  3. package/dist/src/adopt/adopt.d.ts +2 -2
  4. package/dist/src/adopt/adopt.js +20 -6
  5. package/dist/src/ahead/read.d.ts +2 -2
  6. package/dist/src/ahead/read.js +2 -4
  7. package/dist/src/answers/answered.js +1 -1
  8. package/dist/src/compile/pin.d.ts +1 -1
  9. package/dist/src/compile/pin.js +5 -3
  10. package/dist/src/core/answer.d.ts +12 -0
  11. package/dist/src/core/answer.js +1 -1
  12. package/dist/src/core/association.d.ts +16 -0
  13. package/dist/src/core/association.js +34 -0
  14. package/dist/src/core/composition.d.ts +32 -0
  15. package/dist/src/core/composition.js +28 -0
  16. package/dist/src/core/package.d.ts +1 -0
  17. package/dist/src/core/package.js +17 -4
  18. package/dist/src/core/read.d.ts +2 -1
  19. package/dist/src/core/read.js +2 -1
  20. package/dist/src/core/receiver.d.ts +11 -0
  21. package/dist/src/core/receiver.js +122 -0
  22. package/dist/src/core/share-bundle.d.ts +9 -0
  23. package/dist/src/core/share-bundle.js +56 -0
  24. package/dist/src/doors/head.d.ts +1 -0
  25. package/dist/src/doors/head.js +11 -3
  26. package/dist/src/flows/answer.js +7 -3
  27. package/dist/src/flows/call.d.ts +1 -1
  28. package/dist/src/flows/call.js +28 -4
  29. package/dist/src/flows/device.d.ts +14 -5
  30. package/dist/src/flows/device.js +15 -7
  31. package/dist/src/flows/plan.d.ts +2 -0
  32. package/dist/src/flows/plan.js +16 -2
  33. package/dist/src/index.js +3 -31
  34. package/dist/src/links/birth.d.ts +6 -0
  35. package/dist/src/links/birth.js +48 -0
  36. package/dist/src/links/inspect.d.ts +1 -1
  37. package/dist/src/links/inspect.js +23 -12
  38. package/dist/src/links/link.d.ts +2 -2
  39. package/dist/src/links/link.js +7 -10
  40. package/dist/src/onboard/first.js +1 -1
  41. package/dist/src/onboard/owed.js +1 -1
  42. package/dist/src/places/admit.js +1 -1
  43. package/dist/src/places/found.js +3 -1
  44. package/dist/src/ports/adapters/bundle.d.ts +44 -0
  45. package/dist/src/ports/adapters/bundle.js +173 -0
  46. package/dist/src/ports/adapters/configured.d.ts +35 -0
  47. package/dist/src/ports/adapters/configured.js +67 -0
  48. package/dist/src/ports/adapters/mcp.d.ts +59 -0
  49. package/dist/src/ports/adapters/mcp.js +122 -0
  50. package/dist/src/ports/adapters/network.d.ts +12 -0
  51. package/dist/src/ports/adapters/network.js +67 -0
  52. package/dist/src/ports/adapters/protocol.d.ts +6 -0
  53. package/dist/src/ports/adapters/protocol.js +51 -0
  54. package/dist/src/ports/adapters/remote.d.ts +13 -0
  55. package/dist/src/ports/adapters/remote.js +33 -0
  56. package/dist/src/ports/bound.d.ts +0 -1
  57. package/dist/src/ports/bound.js +14 -15
  58. package/dist/src/ports/host.d.ts +18 -0
  59. package/dist/src/ports/host.js +58 -9
  60. package/dist/src/sources/connect.d.ts +1 -0
  61. package/dist/src/sources/connect.js +15 -11
  62. package/dist/src/tools/card.js +3 -2
  63. package/dist/src/use/named.js +3 -3
  64. package/dist/src/verbs/ask.js +7 -1
  65. package/dist/src/verbs/card.js +2 -2
  66. package/dist/src/verbs/do.js +2 -1
  67. package/package.json +6 -5
  68. package/runtime.bundle.json +24026 -0
  69. package/dist/src/ports/reading.d.ts +0 -8
  70. package/dist/src/ports/reading.js +0 -14
@@ -0,0 +1,51 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { Client as RemoteClient } from '@modelcontextprotocol/sdk/client/index.js';
5
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
6
+ import { aboutOf, valueOf } from "../../core/lock.js";
7
+ import { fill } from "../../core/surface.js";
8
+ import { shapeOf, shapeSchema } from "../surface.js";
9
+ import { folderOf } from "../place.js";
10
+ import { askedOf, headOf } from "../../doors/head.js";
11
+ /** Incoming and outgoing SDK bindings; application decisions stay behind the existing ports. */
12
+ export async function serve(lock, root, ext, doorName) {
13
+ const offered = (n) => (n ? { listChanged: false } : undefined);
14
+ const head = headOf(lock, root, ext, {
15
+ 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]) }),
16
+ 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: doorName,
17
+ }), surface = head.surface;
18
+ 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') });
19
+ const schemaOf = (fields) => ({ type: 'object',
20
+ properties: Object.fromEntries(fields.map((f) => [f.name, { type: f.type, description: f.about }])), required: fields.filter((f) => f.required).map((f) => f.name) });
21
+ if (surface.tools.length)
22
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: surface.tools.map((tool) => ({ name: tool.name, description: tool.about,
23
+ inputSchema: schemaOf([...tool.fields]), outputSchema: shapeSchema(lock), annotations: Object.fromEntries(tool.hints.map((hint) => [`${hint}Hint`, true])) })) }));
24
+ if (surface.tools.length)
25
+ 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)));
26
+ if (surface.resources.length)
27
+ server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
28
+ if (surface.resources.length)
29
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({ resourceTemplates: surface.resources.map((r) => ({ name: r.name, uriTemplate: r.template, description: r.about, mimeType: r.mime })) }));
30
+ if (surface.resources.length)
31
+ 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)));
32
+ if (surface.prompts.length)
33
+ 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 })) })) }));
34
+ if (surface.prompts.length)
35
+ 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)));
36
+ await server.connect(new StdioServerTransport());
37
+ }
38
+ /** Concrete outgoing SDK transport. Every network operation still goes through the separately permitted host capability. */
39
+ export const remoteFactory = async (endpoint, send, options) => {
40
+ const client = new RemoteClient({ name: 'head', version: '0' });
41
+ const transport = new StreamableHTTPClientTransport(new URL(endpoint), { fetch: (input, init) => send(String(input), init ?? {}), reconnectionOptions: { maxRetries: 0, maxReconnectionDelay: 0, initialReconnectionDelay: 0, reconnectionDelayGrowFactor: 1 } });
42
+ try {
43
+ await client.connect(transport, options);
44
+ }
45
+ catch (error) {
46
+ await client.close();
47
+ throw error;
48
+ }
49
+ return { list: async (after, opts) => { const got = await client.listTools(after ? { cursor: after } : {}, opts); return { tools: got.tools, ...(got.nextCursor ? { next: got.nextCursor } : {}) }; },
50
+ callTool: (params, opts) => client.callTool(params, undefined, opts), close: () => client.close() };
51
+ };
@@ -0,0 +1,13 @@
1
+ import { type ConfiguredSource, type Limits, type PermittedFetch } from './mcp.ts';
2
+ import type { Prepared } from './request.ts';
3
+ export interface RemotePort {
4
+ readonly discover: (endpoint: string) => Promise<{
5
+ readonly tools: readonly import('./mcp.ts').Tool[];
6
+ readonly digest: string;
7
+ }>;
8
+ readonly prepare: (source: ConfiguredSource, tool: string, args: Record<string, unknown>, authorize: () => Promise<boolean>) => Prepared;
9
+ }
10
+ /** This port owns no permission. Callers bind the receiver's consent and existing access flow; SDK hints grant nothing. */
11
+ export declare function remoteAt(limits: Limits & {
12
+ readonly wireBytes: number;
13
+ }, signal?: AbortSignal, network?: (endpoint: string) => PermittedFetch): RemotePort;
@@ -0,0 +1,33 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { transportFor, withCatalogue, withConfiguredSource, remoteFactory } from "./mcp.js";
3
+ import { networkFor } from "./network.js";
4
+ /** This port owns no permission. Callers bind the receiver's consent and existing access flow; SDK hints grant nothing. */
5
+ export function remoteAt(limits, signal, network = networkFor) {
6
+ return {
7
+ discover: endpoint => withCatalogue(options => remoteFactory(endpoint, transportFor(endpoint, network(endpoint), limits.wireBytes, options), options), limits, signal, session => session.read()),
8
+ prepare: (source, tool, args, authorize) => {
9
+ if (!source.offers.includes(tool))
10
+ return { refused: 'mcp-tool' };
11
+ const sentArgs = JSON.parse(JSON.stringify(args));
12
+ const key = createHash('sha256').update(JSON.stringify([source.endpoint, source.catalogue, tool, sentArgs])).digest('hex');
13
+ return { key, describe: `${source.endpoint} · ${tool} · sha256:${key}`, send: async (attachment, most) => {
14
+ if (!Number.isSafeInteger(most) || most < 1 || Object.keys(attachment?.query ?? {}).length)
15
+ return { kind: 'failed', why: 'mcp/access-location' };
16
+ try {
17
+ const permitted = network(source.endpoint);
18
+ const result = await withConfiguredSource(source, remoteFactory, (url, init) => {
19
+ const headers = new Headers(init.headers);
20
+ for (const [key, value] of Object.entries(attachment?.headers ?? {}))
21
+ headers.set(key, value);
22
+ return permitted(url, { ...init, headers });
23
+ }, limits, signal, call => call(tool, sentArgs, authorize));
24
+ const bytes = Buffer.from(JSON.stringify(result));
25
+ return bytes.length > most ? { kind: 'over' } : { kind: 'got', status: 200, bytes, type: 'application/json' };
26
+ }
27
+ catch (error) {
28
+ return { kind: 'failed', why: error instanceof Error && /^mcp\/[a-z-]+$/.test(error.message) ? error.message : 'mcp/transport' };
29
+ }
30
+ } };
31
+ },
32
+ };
33
+ }
@@ -14,7 +14,6 @@ export type Folded = {
14
14
  } | {
15
15
  readonly refused: string;
16
16
  };
17
- /** Read normative standing plus authenticated device evidence; ambiguous device history refuses. */
18
17
  export declare const policyAt: (name: string, place: string, signal?: AbortSignal) => Promise<Folded>;
19
18
  export declare const standingAt: (name: string, place: string, devicesOf: (lines: readonly Said[]) => readonly string[], isHistory: (line: Said) => boolean, signal?: AbortSignal) => Promise<Folded>;
20
19
  /** A sign or a land: in a host of its own for this one call, signed by head's signer through the port of the key it names, the lot handed in as the host's input. */
@@ -4,10 +4,9 @@ import { dirname, join } from 'node:path';
4
4
  import { canonical, LOCK, parse } from '@lapxo/topos/wire';
5
5
  import { factsOf } from "../core/lock.js";
6
6
  import { spawned } from "./process.js";
7
- import { hosted, hosting, signedIn } from "./host.js";
7
+ import { capturedViews, hosted, hosting, signedIn } from "./host.js";
8
8
  import { cleared, scratchOf } from "./scratch.js";
9
9
  import { folded } from "../ahead/fold.js";
10
- import { readingOf } from "./reading.js";
11
10
  const binOf = (name) => {
12
11
  try {
13
12
  const manifest = createRequire(import.meta.url).resolve(join(name, 'package.json'));
@@ -40,21 +39,21 @@ const isRecord = (line) => line.startsWith('{') && recordOf(line) !== undefined;
40
39
  const apart = (printed) => ((record) => (record === undefined ? printed : { ...printed, refusal: recordOf(record) }))([...printed.err].reverse().find(isRecord));
41
40
  const why = (printed) => printed?.err.filter((line) => !isRecord(line)).at(-1);
42
41
  const readOf = (printed) => (printed === undefined || printed.code !== 0 ? { refused: why(printed) ?? '' } : { lines: factsOf(printed.out) });
43
- /** Native identities and expected-identity results come from the same public CLI contract. Without it, read fresh. */
44
- const nativeView = (name, place, view, signal) => {
45
- const read = async (expected) => {
46
- if (expected === undefined)
47
- return readOf(await run(name, place, [words.read, words.view, view], {}, signal));
48
- const result = readingOf(await run(name, place, [words.read, words.view, `${words.identity}@${view}@${expected}`], {}, signal), view, expected);
49
- return 'refused' in result ? result : { lines: factsOf(result.text.split('\n')) };
50
- };
51
- return !words.identity ? read() : folded(`${name}/${view}`, place, async () => { const result = readingOf(await run(name, place, [words.read, words.view, `${words.identity}@${view}`], {}, signal), view); return 'refused' in result ? undefined : result.identity; }, read);
42
+ /** Only the requested views are captured by Bound; every result binds its text to the actual current native identity. */
43
+ const nativeViews = async (name, place, views, signal) => {
44
+ const captured = capturedViews(place, views, signal);
45
+ if (captured?.kind === 'refuse' && captured.code === 'host/instrument-pin')
46
+ return views.map(() => ({ refused: captured.diagnostic }));
47
+ return Promise.all(views.map(async (view) => {
48
+ const result = captured?.kind === 'fact' ? captured.views.find((one) => one.view === view) : undefined;
49
+ const read = result ? await folded(`${name}/${view}`, place, async () => result.identity, async () => ({ lines: factsOf(result.text.split('\n')) })) : readOf(await run(name, place, [words.read, words.view, view], {}, signal));
50
+ return 'refused' in read || view !== words.standing || read.lines.some((line) => line.scope === words.profile) ? read
51
+ : { refused: `REFUSE·act the place declares no ${words.profile}: land would print nothing of what it admits, so head lands nothing there; its root can land the local-act@1 profile and view/act` };
52
+ }));
52
53
  };
53
- /** Read normative standing plus authenticated device evidence; ambiguous device history refuses. */
54
- export const policyAt = async (name, place, signal) => ((folded) => ('refused' in folded || folded.lines.some((line) => line.scope === words.profile) ? folded
55
- : { refused: `REFUSE·act the place declares no ${words.profile}: land would print nothing of what it admits, so head lands nothing there; its root can land the local-act@1 profile and view/act` }))(await nativeView(name, place, words.standing, signal));
54
+ export const policyAt = async (name, place, signal) => (await nativeViews(name, place, [words.standing], signal))[0];
56
55
  export const standingAt = async (name, place, devicesOf, isHistory, signal) => {
57
- const [folded, evidence] = await Promise.all([policyAt(name, place, signal), nativeView(name, place, words.evidence, signal)]);
56
+ const [folded, evidence] = await nativeViews(name, place, [words.standing, words.evidence], signal);
58
57
  if ('refused' in folded)
59
58
  return folded;
60
59
  if ('refused' in evidence)
@@ -10,6 +10,11 @@ export declare const hosting: {
10
10
  bytes: number;
11
11
  heap: number;
12
12
  queued: number;
13
+ authority: {
14
+ clock: string;
15
+ unit: string;
16
+ items: number;
17
+ };
13
18
  http: NativeHostOptions["http"];
14
19
  };
15
20
  /** Receiver permissions and work budgets are declared by head's own lock, never inferred from a redirect. */
@@ -21,6 +26,8 @@ export declare const configureHost: (lock: readonly Said[], bounds: {
21
26
  export declare const cooled: () => void;
22
27
  /** The process a place's fold host runs as, while one is held for it. */
23
28
  export declare const hostOf: (place: string) => number | undefined;
29
+ /** Native read-only snapshots use Bound's actual caller environment; their identities are not borrowed from a different CLI context. */
30
+ export declare const capturedViews: (place: string, views: readonly string[], signal?: AbortSignal, from?: string) => import("@lapxo/bound/host").NativeViewIdentitiesResult | undefined;
24
31
  /**
25
32
  * A read of a place's view through the place's own fold host, which only folds and never holds a signer: started on the first
26
33
  * read, kept while it is asked within its idle and its life, the installed bound checked file by file before every call.
@@ -33,3 +40,14 @@ export declare const hosted: (place: string, args: readonly string[], signal?: A
33
40
  * host at a time; the lot goes in as the host's input, never a file of head's.
34
41
  */
35
42
  export declare const signedIn: (place: string, args: readonly string[], lot: readonly string[] | undefined, signers: NonNullable<NativeHostOptions["signers"]>, keyId?: string, signal?: AbortSignal, from?: string) => Promise<Printed>;
43
+ export type AuthorityObservation = {
44
+ readonly refused: string;
45
+ readonly code: string;
46
+ } | {
47
+ readonly authority: string;
48
+ readonly request: string;
49
+ readonly decision: 'allow' | 'ask' | 'deny' | 'open';
50
+ readonly needs: readonly string[];
51
+ };
52
+ /** Configuration-only observations at an explicit receiver clock; no signing, admission or API execution occurs here. */
53
+ export declare const authorityAt: (place: string, key: string, kind: string, records: readonly string[], now: number, signal?: AbortSignal, presented?: string) => Promise<AuthorityObservation>;
@@ -1,20 +1,21 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
2
+ import { mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { dirname, join, relative, sep } from 'node:path';
6
- import { installedHostInstrument, nativeHostEnvironment, startNativeHost } from '@lapxo/bound/host';
6
+ import { installedHostInstrument, nativeHostEnvironment, prepareNativeViewIdentities, prepareNativeAuthorityCard, answerNativeAuthorityCard, nativeAuthorityRequestIdentity, startNativeHost } from '@lapxo/bound/host';
7
+ import { decodeAuthorityCard } from '@lapxo/topos/contract';
7
8
  import { members, valueOf } from "../core/lock.js";
8
9
  /** What a place's fold host is held to, from head's lock: the installed instrument it must be, how long it stays idle and lives, and its limits. */
9
- export const hosting = { installed: '', idle: 0, life: 0, wait: 0, bytes: 0, heap: 0, queued: 0, http: undefined };
10
+ export const hosting = { installed: '', idle: 0, life: 0, wait: 0, bytes: 0, heap: 0, queued: 0, authority: { clock: '', unit: '', items: 0 }, http: undefined };
10
11
  /** Receiver permissions and work budgets are declared by head's own lock, never inferred from a redirect. */
11
- export const configureHost = (lock, bounds) => { Object.assign(hosting, { installed: valueOf(lock, 'dep/bound/installed'), wait: bounds.wait, bytes: bounds.most, ...Object.fromEntries(['idle', 'life', 'heap', 'queued'].map((k) => [k, Number(valueOf(lock, `bound/host/${k}`).split('..')[1])])), http: { origins: members(valueOf(lock, 'bound/receiver/origins')), redirects: { maxRedirects: Number(valueOf(lock, 'bound/receiver/redirects').split('..')[1]), headerBytes: Number(valueOf(lock, 'bound/receiver/header-bytes').split('..')[1]) } } }); };
12
+ export const configureHost = (lock, bounds) => { Object.assign(hosting, { installed: valueOf(lock, 'dep/bound/installed'), wait: bounds.wait, bytes: bounds.most, ...Object.fromEntries(['idle', 'life', 'heap', 'queued'].map((k) => [k, Number(valueOf(lock, `bound/host/${k}`).split('..')[1])])), authority: { clock: valueOf(lock, 'bound/authority/clock'), unit: valueOf(lock, 'bound/authority/unit'), items: Number(valueOf(lock, 'bound/authority/items').split('..')[1]) }, http: { origins: members(valueOf(lock, 'bound/receiver/origins')), redirects: { maxRedirects: Number(valueOf(lock, 'bound/receiver/redirects').split('..')[1]), headerBytes: Number(valueOf(lock, 'bound/receiver/header-bytes').split('..')[1]) } } }); };
12
13
  const root = dirname(createRequire(import.meta.url).resolve('@lapxo/bound/package.json'));
13
- const walk = (at) => (statSync(at).isDirectory() ? readdirSync(at).flatMap((e) => walk(join(at, e))) : [at]);
14
+ const walk = (at) => readdirSync(at, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walk(join(at, e.name)) : [join(at, e.name)]);
14
15
  /** Every file of the installed bound by its coordinate and digest: what its identity is computed from, and what is checked again before each call. */
15
16
  const inventory = (at) => walk(at).map((f) => ({ coordinate: relative(at, f).split(sep).join('/'), digest: `sha256:${createHash('sha256').update(readFileSync(f)).digest('hex')}` })).sort((a, b) => (a.coordinate < b.coordinate ? -1 : 1));
16
- /** The hosts held, those starting, those closing — a place gets its next host once its last one is gone — and the folders head made for their scratch, each removed once its host is. */
17
- const [live, starting, closing, scratches] = [new Map(), new Map(), new Map(), new Set()];
17
+ /** Held, starting and closing hosts, their owned scratch, and one instrument descriptor reused only after its complete current byte inventory matches. */
18
+ const [live, starting, closing, scratches, selected] = [new Map(), new Map(), new Map(), new Set(), { root: '', members: '', instrument: undefined }];
18
19
  const cleared = (dir) => { rmSync(dir, { recursive: true, force: true }); scratches.delete(dir); };
19
20
  const shut = (at) => { const one = live.get(at); live.delete(at); if (one) {
20
21
  clearTimeout(one.idle);
@@ -27,13 +28,34 @@ export const cooled = () => { for (const at of [...live.keys()])
27
28
  export const hostOf = (place) => live.get(realpathSync(place))?.host.pid;
28
29
  process.once('exit', () => { cooled(); for (const dir of [...scratches])
29
30
  cleared(dir); });
31
+ /** One verified installed descriptor, reused only after hashing every current installed member. */
32
+ const instrumentAt = (from) => {
33
+ const members = inventory(from), current = JSON.stringify(members);
34
+ const instrument = selected.root === from && selected.members === current ? selected.instrument : Object.assign(selected, { root: from, members: current, instrument: installedHostInstrument({ directory: from, shape: 'package', members, exports: [['exports', './engine', 'default']], pins: [], limits: { members: 4096, bytes: 1 << 27, responseBytes: 1 << 22 } }, join(from, 'dist/cli/verb.js')) }).instrument;
35
+ return { members: current, instrument };
36
+ };
37
+ const pinnedInstrument = (from) => { const { instrument } = instrumentAt(from); if (instrument.installed?.digest !== hosting.installed)
38
+ throw Error('REFUSE·host/instrument-pin'); return instrument; };
39
+ /** Native read-only snapshots use Bound's actual caller environment; their identities are not borrowed from a different CLI context. */
40
+ export const capturedViews = (place, views, signal, from = root) => {
41
+ try {
42
+ if (signal?.aborted)
43
+ return undefined;
44
+ const instrument = pinnedInstrument(from);
45
+ return prepareNativeViewIdentities({ place, views, instrument });
46
+ }
47
+ catch (error) {
48
+ cooled();
49
+ return { kind: 'refuse', code: 'host/instrument-pin', diagnostic: error.message };
50
+ }
51
+ };
30
52
  /** A host started on a place, on the installed instrument the lock pins and in a scratch folder of head's own; with signers only for one call's sign or land. */
31
53
  const opened = async (at, from, signers) => {
32
- const members = inventory(from), instrument = installedHostInstrument({ directory: from, shape: 'package', members, exports: [['exports', './engine', 'default']], pins: [], limits: { members: 4096, bytes: 1 << 27, responseBytes: 1 << 22 } }, join(from, 'dist/cli/verb.js'));
54
+ const { members, instrument } = instrumentAt(from);
33
55
  const scratch = mkdtempSync(join(tmpdir(), 'head-host-'));
34
56
  scratches.add(scratch);
35
57
  const host = await startNativeHost({ place: at, instrument, expectedInstrument: hosting.installed, environment: nativeHostEnvironment({ ...process.env }), scratchParent: scratch, ...(hosting.http ? { http: hosting.http } : {}), ...(signers ? { signers } : {}), limits: { requestBytes: hosting.bytes, responseBytes: hosting.bytes, timeoutMs: hosting.wait, heapMiB: hosting.heap, queuedRequests: hosting.queued } }).catch((e) => { cleared(scratch); throw e; });
36
- return { host, members: JSON.stringify(members), born: Date.now(), scratch, busy: 0 };
58
+ return { host, members, born: Date.now(), scratch, busy: 0 };
37
59
  };
38
60
  const started = async (at, from) => ((one) => (live.set(at, one), one))(await opened(at, from));
39
61
  const lines = (text) => text.split('\n').filter(Boolean), refused = (why) => ({ code: -1, out: [], err: [why] });
@@ -106,3 +128,30 @@ export const signedIn = async (place, args, lot, signers, keyId, signal, from =
106
128
  cleared(one.scratch);
107
129
  }
108
130
  };
131
+ /** Configuration-only observations at an explicit receiver clock; no signing, admission or API execution occurs here. */
132
+ export const authorityAt = async (place, key, kind, records, now, signal, presented) => {
133
+ const current = () => { if (signal?.aborted)
134
+ throw Error('REFUSE·authority-preflight/cancelled'); };
135
+ try {
136
+ current();
137
+ const instrument = pinnedInstrument(root);
138
+ const action = { kind, records }, clock = { name: hosting.authority.clock, unit: hosting.authority.unit, now };
139
+ const context = { root: realpathSync(place), place: '.', key, instrument, clock, horizon: { clock: clock.name, unit: clock.unit, from: now, through: now }, limits: { bytes: hosting.bytes, items: hosting.authority.items }, actions: ['record-coverage', 'admission'].includes(kind) ? [action] : [] };
140
+ if (!clock.name || !clock.unit || !Number.isSafeInteger(now) || hosting.authority.items < 1)
141
+ throw Error('REFUSE·authority-preflight/context');
142
+ const prepared = await prepareNativeAuthorityCard(context);
143
+ if (prepared.kind !== 'fact')
144
+ return { refused: prepared.diagnostic, code: prepared.code };
145
+ const request = nativeAuthorityRequestIdentity(action), card = presented ? decodeAuthorityCard(presented, context.limits) : prepared.value.card;
146
+ const answer = await answerNativeAuthorityCard(card, request, context);
147
+ current();
148
+ if ('kind' in answer)
149
+ return { refused: answer.diagnostic, code: answer.code };
150
+ if (answer.status === 'refused')
151
+ return { refused: `REFUSE·${answer.code}`, code: answer.code };
152
+ return { authority: prepared.value.line, request, decision: answer.status === 'observed' ? answer.decision : 'open', needs: answer.status === 'open' ? answer.needs : prepared.value.needs };
153
+ }
154
+ catch (error) {
155
+ return { refused: error instanceof Error ? error.message : String(error), code: 'authority-preflight/context' };
156
+ }
157
+ };
@@ -17,5 +17,6 @@ type Connection = {
17
17
  readonly connected: boolean;
18
18
  };
19
19
  type Laid = (source: string, document: string, doc: Read, overlay: Read | undefined, started: number) => Promise<Connection>;
20
+ export declare const received: (context: Context, place: string, document: string, bytes: Uint8Array, named: string, laid: Laid, account: string, publicFiles?: ReadonlyMap<string, Uint8Array | undefined>) => Promise<readonly string[]>;
20
21
  export declare const laidIn: (context: Context, place: string, account: string) => Laid;
21
22
  export {};
@@ -1,6 +1,8 @@
1
+ import { discoverConfigured, configuredRefusal, configuredProtocol } from "../ports/adapters/configured.js";
2
+ import { receiverConnect } from "../core/receiver.js";
1
3
  import { canonical, EXTENSION } from '@lapxo/topos/wire';
2
4
  import { inPlace, placeOf, unless } from "../core/answer.js";
3
- import { valueOf } from "../core/lock.js";
5
+ import { members, valueOf } from "../core/lock.js";
4
6
  import { fieldsOf, vocabOf } from "../core/read.js";
5
7
  import { redIn, sayIn } from "../core/surface.js";
6
8
  import { digestIn, linesOf } from "../core/worlds.js";
@@ -9,6 +11,7 @@ import { paidBy, pinIn } from "../compile/pin.js";
9
11
  import { adopted } from "../adopt/adopt.js";
10
12
  import { firstRun } from "../onboard/first.js";
11
13
  import { resolved } from "../onboard/site.js";
14
+ import { receivedBirth } from "../links/birth.js";
12
15
  import { linked } from "../links/inspect.js";
13
16
  import { rootIn } from "../flows/device.js";
14
17
  import { attachedFor } from "./access.js";
@@ -23,8 +26,8 @@ import { attachedFor } from "./access.js";
23
26
  * its pins rest in, its pin held to the package's; one that differs is refused by name, and the place holds what it
24
27
  * compiled. Nothing the sharing place was granted comes with it: access is asked here, issuer by issuer.
25
28
  */
26
- export const connected = async (asked, context, covered = false) => ((asked['document'] ?? '').startsWith(valueOf(context.lock, 'link/prefix')) ? linked(asked, context)
27
- : ((got) => ('said' in got ? got.said : connectedAs(got.asked, context, covered)))(await resolved(asked, context)));
29
+ export const connected = async (asked, context, covered = false) => (await receiverConnect(asked, context)) ?? (await receivedBirth(asked, context)) ?? (members(valueOf(context.lock, 'link/prefixes')).some(prefix => (asked['document'] ?? '').startsWith(prefix)) ? linked(asked, context)
30
+ : ((got) => ('said' in got ? got.said : connectedAs(got.asked, context, covered)))(asked['protocol'] === configuredProtocol.id ? { asked } : await resolved(asked, context)));
28
31
  const connectedAs = async (asked, context, covered = false) => (asked['source'] && !(await placeOf(asked, context)) ? firstRun(asked, context, (dir, covered) => connected({ ...asked, place: dir }, context, covered))
29
32
  : ((started) => inPlace(asked, context, async (place) => {
30
33
  const say = sayIn(context.lock), digest = digestIn(context.lock), { documentOf, fetchedAs, kindOf } = context.files.read;
@@ -34,7 +37,7 @@ const connectedAs = async (asked, context, covered = false) => (asked['source']
34
37
  return [say('parent-lock', { place, parent: source || document })];
35
38
  if (!source && !lock)
36
39
  return [say('missing-param', { operation: 'connect', param: 'source' })];
37
- const root = rootIn(context, place);
40
+ const root = await rootIn(context, place);
38
41
  if ('said' in root)
39
42
  return root.said;
40
43
  if (named && !/^sha256:[0-9a-f]{64}$/.test(named))
@@ -48,7 +51,7 @@ const connectedAs = async (asked, context, covered = false) => (asked['source']
48
51
  const got = via ? await attachedFor(context, place, via) : undefined;
49
52
  if (got && 'said' in got)
50
53
  return got.said;
51
- const [read, overlay] = [got ? await fetchedAs(context, document, via, got, named, accept) : await documentOf(context, place, document, named), asked['overlay'] ? await documentOf(context, place, asked['overlay']) : undefined];
54
+ const [read, overlay] = [asked['protocol'] === configuredProtocol.id ? await discoverConfigured(context, asked, source) : got ? await fetchedAs(context, document, via, got, named, accept) : await documentOf(context, place, document, named), asked['overlay'] ? await documentOf(context, place, asked['overlay']) : undefined];
52
55
  if (read !== undefined && 'said' in read)
53
56
  return read.said;
54
57
  if (read === undefined || (asked['overlay'] && overlay === undefined))
@@ -56,13 +59,14 @@ const connectedAs = async (asked, context, covered = false) => (asked['source']
56
59
  if (named && digest(read.bytes) !== named)
57
60
  return [say('document-digest', { document, digest: named, got: digest(read.bytes) })];
58
61
  return `.${read.kind}` === EXTENSION ? [say('parent-lock', { place, parent: source })] : (await laidIn(context, place, root.account)(source, document, read, overlay, started)).said;
59
- }))(context.machine()));
60
- const received = async (context, place, document, bytes, named, laid, account) => {
61
- const [say, got] = [sayIn(context.lock), unpacked(context, document, bytes, await context.files.read.besideOf(context, document, linesOf(bytes)))];
62
+ }))(context.machine())).catch(error => { if (asked['protocol'] === configuredProtocol.id || error instanceof Error && error.message.startsWith(configuredProtocol.failurePrefix))
63
+ return configuredRefusal(context, error); throw error; });
64
+ export const received = async (context, place, document, bytes, named, laid, account, publicFiles) => {
65
+ const [say, got] = [sayIn(context.lock), unpacked(context, document, bytes, publicFiles ?? await context.files.read.besideOf(context, document, linesOf(bytes)))];
62
66
  if (!got.sources)
63
67
  return got.said ?? [];
64
68
  const [names, said, before] = [got.sources.map((s) => s.source).join(', '), [], await context.policy(place)];
65
- const refused = coveredIn(context, 'refused' in before ? [] : before.lines, got.sources, named) ? undefined : await unless(context, say('elicit-receive', { place, package: named, sources: names || '—', open: (got.open ?? []).map((o) => o.source).join(', ') || '—' }));
69
+ const refused = publicFiles || coveredIn(context, 'refused' in before ? [] : before.lines, got.sources, named) ? undefined : await unless(context, say('elicit-receive', { place, package: named, sources: names || '—', open: (got.open ?? []).map((o) => o.source).join(', ') || '—' }));
66
70
  if (refused)
67
71
  return refused;
68
72
  for (const s of got.sources)
@@ -76,8 +80,8 @@ export const laidIn = (context, place, account) => (source, document, doc, overl
76
80
  const say = sayIn(context.lock), v = vocabOf(context.lock), word = (k) => valueOf(context.lock, k), digest = digestIn(context.lock);
77
81
  const lands = (lot) => context.landRoot(place, lot, account);
78
82
  const line = (scope, measure, value, more = {}) => canonical({ at: 'policy:head/connect', by: 'target', form: 'alphabet', measure, role: 'writes', scope, value, ...more });
79
- const pin = pinIn(context, place, lands, line);
80
- const worlds = await adopted(context, place, lands, line);
83
+ const configured = configuredProtocol.connection(doc.bytes, source, overlay !== undefined, v);
84
+ const [pin, worlds] = [pinIn(context, place, lands, line, configured?.verify), await adopted(context, place, lands, line, configured?.worlds ?? [])];
81
85
  if ('refused' in worlds)
82
86
  return { said: [say('not-landed', { why: worlds.refused })], connected: false };
83
87
  const laid = [[`${word('place/connected')}/${source}.${doc.kind}`, doc.bytes], ...(overlay ? [[`${word('place/connected')}/${source}.overlay.${overlay.kind}`, overlay.bytes]] : [])];
@@ -1,8 +1,9 @@
1
- import { inStanding, printedOr } from "../core/answer.js";
1
+ import { authorityFor } from "../flows/plan.js";
2
+ import { inStanding, inPlace, printedOr } from "../core/answer.js";
2
3
  import { members, valueOf } from "../core/lock.js";
3
4
  import { sayIn } from "../core/surface.js";
4
5
  /** What is open for a key: the cells view the place's standing declares, as bound folded it at the resolution asked, narrowed to the key; a refused fold is said, never read as no view. The standing is the one the card was read off, folded once a call. */
5
- export const answer = (asked, context) => inStanding(asked, context, async (place, lines) => {
6
+ export const answer = (asked, context) => asked['lines'] !== undefined || asked['preflight'] !== undefined || asked['authority'] !== undefined ? inPlace(asked, context, async (place) => (await authorityFor(context, place, asked['key'] ?? '', asked['preflight'] ?? 'record-coverage', (asked['lines'] ?? '').split('\n').filter(Boolean), true, asked['authority']))) : inStanding(asked, context, async (place, lines) => {
6
7
  const word = (scope) => valueOf(context.lock, `bound/${scope}`);
7
8
  const view = lines.find((f) => f.scope?.startsWith('view/') && !f.shape && members(f.value ?? '').includes(word('cells')));
8
9
  if (view === undefined)
@@ -5,16 +5,16 @@ import { sayIn } from "../core/surface.js";
5
5
  /** The words of a name as a person or a model says it: its runs of letters and digits, lowercase. */
6
6
  const words = (text) => text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
7
7
  /** The offers of a source a place holds: head's own registry of it, local, never sent to the model whole. */
8
- export const offersOf = (lock, lines, source) => ((at) => lines.filter((l) => l.measure === vocabOf(lock).class && l.scope?.startsWith(at)).map((l) => l.scope.slice(at.length)))(`${vocabOf(lock).offers}/${source}/`);
8
+ export const offersOf = (lock, lines, source) => ((v) => ((at) => lines.filter((l) => l.measure === v.class && l.scope?.startsWith(at)).map((l) => l.scope.slice(at.length)))(`${v.offers}/${source}/`))(vocabOf(lock));
9
9
  /** Resolve the offer against the local registry. Ambiguity asks the person; read-ahead creates no use line. */
10
10
  export const namedIn = async (context, lines, source, asked, ahead) => {
11
11
  const [v, word, say] = [vocabOf(context.lock), (k) => valueOf(context.lock, k), sayIn(context.lock)];
12
- const [family, name] = [word('use/family'), words(asked).join('-')], uses = lines.filter((l) => l.scope?.startsWith(`${family}/${source}/`)), offers = offersOf(context.lock, lines, source);
12
+ const [family, name] = [word('use/family'), words(asked).join('-')], uses = lines.filter((l) => l.scope?.startsWith(`${family}/${source}/`));
13
13
  const used = uses.find((l) => l.scope === `${family}/${source}/${name}`)?.value;
14
14
  if (used !== undefined || ahead || !asked)
15
15
  return { operation: used ?? asked };
16
16
  const named = (op) => words([op, ...lines.filter((l) => l.scope === `${v.offers}/${source}/${op}/${v.carry}`).map((l) => l.value ?? '')].join(' '));
17
- const found = offers.includes(asked) ? [asked] : offers.filter((op) => words(asked).length > 0 && words(asked).every((w) => named(op).includes(w)));
17
+ const found = ((offers) => offers.includes(asked) ? [asked] : offers.filter((op) => words(asked).length > 0 && words(asked).every((w) => named(op).includes(w))))(offersOf(context.lock, lines, source));
18
18
  const operation = found.length > 1 ? (await context.person.choose(say('pick-offer', { source, asked }), [{ name: family, options: found.map((o) => ({ value: o, label: o })) }]))?.[family] : found[0];
19
19
  if (!found.length)
20
20
  return { operation: asked };
@@ -1,3 +1,4 @@
1
+ import { receiverQuestion } from "../core/receiver.js";
1
2
  import { join } from 'node:path';
2
3
  import { LOCK } from '@lapxo/topos/wire';
3
4
  import { answerOf, placeOf } from "../core/answer.js";
@@ -10,6 +11,11 @@ import { viewed } from "../claims/view.js";
10
11
  import { cardAt } from "./card.js";
11
12
  /** Route a question at the declared resolution, then attach the card of the place it read. */
12
13
  export const asked = async (asked, context) => {
14
+ const packaged = await receiverQuestion(asked, context);
15
+ if (packaged && 'said' in packaged)
16
+ return packaged.said;
17
+ if (packaged)
18
+ asked = { ...asked, place: packaged.place };
13
19
  const why = asked['why'] === 'true', at = (form) => ((v) => (v && v !== valueOf(context.lock, 'resolution/own') ? v : undefined))(valueOf(context.lock, `resolution/${form}/${why ? 'why' : 'person'}`));
14
20
  const read = async (form, tool, more = {}) => [...(why && at(form) === undefined ? [sayIn(context.lock)('why-undeclared', { form })] : []),
15
21
  ...await (await answerOf('tools', tool))({ ...asked, ...more, at: at(form) }, context)];
@@ -22,7 +28,7 @@ export const asked = async (asked, context) => {
22
28
  const owed = asked['source'] && founded ? await paidIn(asked, context, place) : undefined;
23
29
  if (owed !== undefined && !owed.answer)
24
30
  return owed.said;
25
- const answer = asked['sources'] && founded ? await compared(asked, context, place) : asked['lines'] ? await read('plan', 'plan') : founded && asked['view'] === valueOf(context.lock, 'claim/view') ? await viewed(asked, context) : asked['view'] ? await read('view', 'look') : asked['source'] ? await read('offers', 'look')
31
+ const answer = carded && (asked['lines'] !== undefined || asked['preflight'] !== undefined || asked['authority'] !== undefined) ? await read('card', 'card', { place }) : asked['sources'] && founded ? await compared(asked, context, place) : asked['lines'] ? await read('plan', 'plan') : founded && asked['view'] === valueOf(context.lock, 'claim/view') ? await viewed(asked, context) : asked['view'] ? await read('view', 'look') : asked['source'] ? await read('offers', 'look')
26
32
  : !carded ? await read('places', 'places') : founded ? [...await read('card', 'card', { place }), ...await pendingIn(context, place)] : [];
27
33
  const after = held === undefined || 'said' in held || place === undefined || !context.moved(place) ? held : await cardAt(context, place);
28
34
  return [...(owed?.said ?? []), ...answer, ...(after === undefined || 'said' in after ? [] : [sayIn(context.lock)('card', { place: place ?? '', card: after.card, from: 'ask' })])];
@@ -11,6 +11,6 @@ export const cardAt = async (context, place) => {
11
11
  const held = place === undefined || !context.files.has(place, LOCK) ? { lines: [] } : await standingOr(context, place);
12
12
  if ('said' in held)
13
13
  return held;
14
- const read = `${valueOf(context.lock, 'receipt/at')}:`, keys = ['head', 'person'].map((role) => context.device(role) ?? '');
15
- return { card: createHash('sha256').update([baseOf(held.lines.filter((l) => !l.at?.startsWith(read) && !l.scope?.startsWith(`${valueOf(context.lock, 'use/family')}/`))), ...keys].join('\n')).digest('hex').slice(0, 16) };
14
+ const read = `${valueOf(context.lock, 'receipt/at')}:`, use = `${valueOf(context.lock, 'use/family')}/`, keys = ['head', 'person'].map((role) => context.device(role) ?? '');
15
+ return { card: createHash('sha256').update([baseOf(held.lines.filter((l) => !l.at?.startsWith(read) && !l.scope?.startsWith(use))), ...keys].join('\n')).digest('hex').slice(0, 16) };
16
16
  };
@@ -2,6 +2,7 @@ import { answerOf, placeOf } from "../core/answer.js";
2
2
  import { members, valueOf } from "../core/lock.js";
3
3
  import { sayIn } from "../core/surface.js";
4
4
  import { cardAt } from "./card.js";
5
+ import { LOCK } from '@lapxo/topos/wire';
5
6
  /**
6
7
  * What a person does, carried out by the action it names and only on the card it was asked on: an act on a place that
7
8
  * moved since is refused before anything is done. An action the place does not allow is said not to be in the card, by
@@ -16,7 +17,7 @@ export const done = async (asked, context) => {
16
17
  const place = asked['place'] || asked['dir'] || await placeOf(asked, context), held = await cardAt(context, place);
17
18
  if ('said' in held)
18
19
  return held.said;
19
- if (held.card !== asked['card'])
20
+ if (held.card !== asked['card'] && !(action === 'place' && place !== undefined && !context.files.has(place, LOCK) && !context.files.has(place, '.bound')))
20
21
  return [say('card-stale', { action, place: place ?? '', card: asked['card'] ?? '' })];
21
22
  const answer = await (await answerOf('tools', action))(asked, context);
22
23
  const [bound, own] = [members(word('do/not-in-card/bound')), members(word('do/not-in-card/head'))];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lapxo/head",
3
- "version": "0.1.7",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "An MCP server that folds: it runs bound as a process, hands a model what bound printed, and lands what it read as receipts.",
6
6
  "license": "MIT",
@@ -18,7 +18,8 @@
18
18
  "files": [
19
19
  "dist/src",
20
20
  "TARGET.bound",
21
- "LICENSE"
21
+ "LICENSE",
22
+ "runtime.bundle.json"
22
23
  ],
23
24
  "scripts": {
24
25
  "build": "tsc -p . --incremental false --tsBuildInfoFile null",
@@ -26,12 +27,12 @@
26
27
  "test": "node --test test/*.test.ts && node --test --test-concurrency=1 test/time/*.test.ts"
27
28
  },
28
29
  "dependencies": {
29
- "@lapxo/bound": "0.2.7",
30
+ "@lapxo/bound": "0.2.8",
30
31
  "@lapxo/topos": "0.2.4",
31
- "@modelcontextprotocol/sdk": "1.32.1"
32
+ "@modelcontextprotocol/sdk": "1.32.1",
33
+ "@anthropic-ai/mcpb": "2.1.2"
32
34
  },
33
35
  "devDependencies": {
34
- "@anthropic-ai/mcpb": "2.1.2",
35
36
  "@types/node": "22.20.5",
36
37
  "typescript": "5.9.3"
37
38
  }