@lapxo/head 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/TARGET.bound +205 -112
- package/dist/src/access/contract.d.ts +0 -2
- package/dist/src/access/obtain.js +3 -3
- package/dist/src/ahead/budget.d.ts +4 -0
- package/dist/src/ahead/budget.js +8 -0
- package/dist/src/claims/claim.d.ts +1 -0
- package/dist/src/claims/claim.js +15 -15
- package/dist/src/claims/view.d.ts +3 -0
- package/dist/src/claims/view.js +11 -0
- package/dist/src/compile/pin.d.ts +7 -0
- package/dist/src/compile/pin.js +6 -7
- package/dist/src/core/answer.d.ts +1 -0
- package/dist/src/core/answer.js +1 -0
- package/dist/src/core/device.d.ts +2 -2
- package/dist/src/core/device.js +5 -5
- package/dist/src/core/lock.d.ts +1 -0
- package/dist/src/core/lock.js +2 -1
- package/dist/src/core/read.js +1 -2
- package/dist/src/core/worlds.js +2 -3
- package/dist/src/doors/guard.d.ts +18 -0
- package/dist/src/doors/guard.js +63 -0
- package/dist/src/doors/head.d.ts +60 -0
- package/dist/src/doors/head.js +136 -0
- package/dist/src/doors/http.d.ts +3 -0
- package/dist/src/doors/http.js +34 -0
- package/dist/src/doors/lines.d.ts +3 -0
- package/dist/src/doors/lines.js +21 -0
- package/dist/src/flows/device.js +4 -6
- package/dist/src/flows/plan.js +7 -10
- package/dist/src/flows/read.js +4 -1
- package/dist/src/index.js +17 -116
- package/dist/src/links/inspect.js +7 -9
- package/dist/src/places/found.js +1 -1
- package/dist/src/ports/bound.d.ts +9 -10
- package/dist/src/ports/bound.js +25 -20
- package/dist/src/ports/host.d.ts +35 -0
- package/dist/src/ports/host.js +108 -0
- package/dist/src/ports/keychain.d.ts +14 -2
- package/dist/src/ports/keychain.js +19 -7
- package/dist/src/ports/scratch.d.ts +4 -0
- package/dist/src/ports/scratch.js +10 -0
- package/dist/src/signer.js +8 -7
- package/dist/src/tools/card.js +3 -6
- package/dist/src/verbs/ask.js +2 -1
- package/dist/src/walk/context.js +3 -6
- package/package.json +3 -3
- package/dist/src/ports/signers.d.ts +0 -6
- package/dist/src/ports/signers.js +0 -14
- package/dist/src/ports/spare.d.ts +0 -1
- package/dist/src/ports/spare.js +0 -33
- package/dist/src/ports/warm.d.ts +0 -19
- package/dist/src/ports/warm.js +0 -45
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
6
|
+
import { installedHostInstrument, nativeHostEnvironment, startNativeHost } from '@lapxo/bound/host';
|
|
7
|
+
import { members, valueOf } from "../core/lock.js";
|
|
8
|
+
/** 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
|
+
/** 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
|
+
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
|
+
/** 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
|
+
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()];
|
|
18
|
+
const cleared = (dir) => { rmSync(dir, { recursive: true, force: true }); scratches.delete(dir); };
|
|
19
|
+
const shut = (at) => { const one = live.get(at); live.delete(at); if (one) {
|
|
20
|
+
clearTimeout(one.idle);
|
|
21
|
+
closing.set(at, one.host.close().catch(() => undefined).finally(() => { cleared(one.scratch); closing.delete(at); }));
|
|
22
|
+
} };
|
|
23
|
+
/** Every host closed, its scratch removed with it: whenever a caller asks, and when head ends, its scratch then removed at once, whatever its host is still doing. */
|
|
24
|
+
export const cooled = () => { for (const at of [...live.keys()])
|
|
25
|
+
shut(at); };
|
|
26
|
+
/** The process a place's fold host runs as, while one is held for it. */
|
|
27
|
+
export const hostOf = (place) => live.get(realpathSync(place))?.host.pid;
|
|
28
|
+
process.once('exit', () => { cooled(); for (const dir of [...scratches])
|
|
29
|
+
cleared(dir); });
|
|
30
|
+
/** 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
|
+
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'));
|
|
33
|
+
const scratch = mkdtempSync(join(tmpdir(), 'head-host-'));
|
|
34
|
+
scratches.add(scratch);
|
|
35
|
+
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 };
|
|
37
|
+
};
|
|
38
|
+
const started = async (at, from) => ((one) => (live.set(at, one), one))(await opened(at, from));
|
|
39
|
+
const lines = (text) => text.split('\n').filter(Boolean), refused = (why) => ({ code: -1, out: [], err: [why] });
|
|
40
|
+
/**
|
|
41
|
+
* 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
|
|
42
|
+
* read, kept while it is asked within its idle and its life, the installed bound checked file by file before every call.
|
|
43
|
+
* A refusal, an error or a changed instrument closes it, and the next read starts another.
|
|
44
|
+
*/
|
|
45
|
+
export const hosted = async (place, args, signal, from = root) => {
|
|
46
|
+
if (args[0] !== 'fold')
|
|
47
|
+
return refused(`REFUSE·host a place's fold host only folds, and never holds a signer: ${args[0]} goes to a process of its own`);
|
|
48
|
+
const at = realpathSync(place), was = live.get(at);
|
|
49
|
+
if (was !== undefined && Date.now() - was.born > hosting.life * 1000)
|
|
50
|
+
shut(at);
|
|
51
|
+
let one;
|
|
52
|
+
try {
|
|
53
|
+
await closing.get(at);
|
|
54
|
+
one = live.get(at) ?? await (starting.get(at) ?? ((p) => (starting.set(at, p), p))(started(at, from).finally(() => starting.delete(at))));
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
return refused(e.message);
|
|
58
|
+
}
|
|
59
|
+
if (JSON.stringify(inventory(from)) !== one.members) {
|
|
60
|
+
shut(at);
|
|
61
|
+
return refused(`REFUSE·host the installed bound changed after its host started, so it is no longer ${hosting.installed}: the host is closed, and nothing was read`);
|
|
62
|
+
}
|
|
63
|
+
clearTimeout(one.idle);
|
|
64
|
+
one.busy += 1;
|
|
65
|
+
try {
|
|
66
|
+
const got = await one.host.request({ verb: 'fold', args: args.slice(1) }, signal);
|
|
67
|
+
if (got.status !== 0)
|
|
68
|
+
shut(at);
|
|
69
|
+
return { code: got.status, out: lines(got.stdout), err: lines(got.stderr) };
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
shut(at);
|
|
73
|
+
return refused(e.message);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
one.busy -= 1;
|
|
77
|
+
if (one.busy === 0 && live.get(at) === one)
|
|
78
|
+
one.idle = setTimeout(() => shut(at), hosting.idle * 1000).unref();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* A sign or a land in a host of its own, started for this one call with the signers the lock names and closed once it
|
|
83
|
+
* answers: the signer never outlives the call it signed for. The place's fold host is closed first, a place having one
|
|
84
|
+
* host at a time; the lot goes in as the host's input, never a file of head's.
|
|
85
|
+
*/
|
|
86
|
+
export const signedIn = async (place, args, lot, signers, keyId, signal, from = root) => {
|
|
87
|
+
const at = realpathSync(place);
|
|
88
|
+
shut(at);
|
|
89
|
+
await closing.get(at);
|
|
90
|
+
let one;
|
|
91
|
+
try {
|
|
92
|
+
one = await opened(at, from, signers);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
return refused(e.message);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const got = await one.host.request({ verb: args[0], args: [...(lot ? ['@input/lot.bound'] : []), ...args.slice(1)], ...(lot ? { inputs: [{ name: 'lot.bound', text: `${lot.join('\n')}\n` }] } : {}), ...(keyId ? { signer: keyId } : {}) }, signal);
|
|
99
|
+
return { code: got.status, out: lines(got.stdout), err: lines(got.stderr) };
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
return refused(e.message);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await one.host.close().catch(() => undefined);
|
|
106
|
+
cleared(one.scratch);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
@@ -2,7 +2,19 @@ export interface KeyStore {
|
|
|
2
2
|
readonly read: (account: string) => string | undefined;
|
|
3
3
|
readonly write: (account: string, secret: string) => void;
|
|
4
4
|
readonly made?: (account: string) => string | undefined;
|
|
5
|
+
readonly held?: (account: string) => string | undefined;
|
|
5
6
|
}
|
|
6
7
|
export declare const plain: (word: string) => string;
|
|
7
|
-
/** The secret store this host keeps head's keys in, by the name its lock gives the platform's: one adapter each, every secret crossing a command's stdin and stdout only
|
|
8
|
-
export declare const
|
|
8
|
+
/** The secret store this host keeps head's keys in, by the name its lock gives the platform's: one adapter each, every secret crossing a command's stdin and stdout only. Only head's signer opens it for a signing key. */
|
|
9
|
+
export declare const secretsOf: (service: string, kind: string) => KeyStore;
|
|
10
|
+
/** The store head's long process is handed: a signing account is refused by name before any command starts, and its key is made, or its public half read, in head's signer, never here; the secrets of access stay this process's, since it calls their APIs. */
|
|
11
|
+
export declare const keychain: (service: string, kind: string, signing: (account: string) => string | undefined) => KeyStore;
|
|
12
|
+
/** The signing port a host calls for one lot: head's signer started with the key's names alone and the lot on its stdin, within the wait the lock gives bound; what it prints are the signatures, read as the signing-lot contract reads them. */
|
|
13
|
+
export declare const portOf: (service: string, account: string, kind: string, wait: number) => (lot: {
|
|
14
|
+
readonly bytes: readonly string[];
|
|
15
|
+
readonly keyId: string;
|
|
16
|
+
readonly algorithm: string;
|
|
17
|
+
}, signal: AbortSignal) => Promise<readonly {
|
|
18
|
+
readonly keyId: string;
|
|
19
|
+
readonly signature: string;
|
|
20
|
+
}[]>;
|
|
@@ -1,12 +1,24 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { signerResponseOf } from '@lapxo/topos/wire';
|
|
4
|
+
import { spawned } from "./process.js";
|
|
1
5
|
import { store as credentialManager } from "./stores/credential-manager.js";
|
|
2
6
|
import { store as keychainOfMac } from "./stores/keychain.js";
|
|
3
7
|
import { store as secretTool } from "./stores/secret-tool.js";
|
|
4
|
-
import { newKey, publicOf } from "./keys.js";
|
|
5
8
|
export const plain = (word) => { if (!/^[\w.@/+=-]+$/.test(word))
|
|
6
9
|
throw Error('REFUSE·keychain a name holds more than word characters'); return word; };
|
|
7
|
-
/** The secret store this host keeps head's keys in, by the name its lock gives the platform's: one adapter each, every secret crossing a command's stdin and stdout only
|
|
8
|
-
export const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
}
|
|
10
|
+
/** The secret store this host keeps head's keys in, by the name its lock gives the platform's: one adapter each, every secret crossing a command's stdin and stdout only. Only head's signer opens it for a signing key. */
|
|
11
|
+
export const secretsOf = (service, kind) => ((made) => { if (made === undefined)
|
|
12
|
+
throw Error(`REFUSE·keychain ${kind} is no store head carries`); return made(service); })({ keychain: keychainOfMac, 'secret-tool': secretTool, 'credential-manager': credentialManager }[kind]);
|
|
13
|
+
const signer = fileURLToPath(new URL(`../signer${import.meta.url.slice(import.meta.url.lastIndexOf('.'))}`, import.meta.url)), half = (mode, service, account, kind) => { try {
|
|
14
|
+
return execFileSync(process.execPath, [signer, mode, service, plain(account), kind], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) || undefined;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
} };
|
|
19
|
+
/** The store head's long process is handed: a signing account is refused by name before any command starts, and its key is made, or its public half read, in head's signer, never here; the secrets of access stay this process's, since it calls their APIs. */
|
|
20
|
+
export const keychain = (service, kind, signing) => ((keys, guard) => ({ read: (account) => keys.read(guard(account)), write: (account, secret) => keys.write(guard(account), secret), made: (account) => half('make', service, account, kind), held: (account) => half('exists', service, account, kind) }))(secretsOf(service, kind), (account) => ((key) => { if (key !== undefined)
|
|
21
|
+
throw Error(`REFUSE·keychain ${key} names a signing key: only head's signer reads it`); return account; })(signing(account)));
|
|
22
|
+
/** The signing port a host calls for one lot: head's signer started with the key's names alone and the lot on its stdin, within the wait the lock gives bound; what it prints are the signatures, read as the signing-lot contract reads them. */
|
|
23
|
+
export const portOf = (service, account, kind, wait) => async (lot, signal) => ((got) => { if (got.code !== 0)
|
|
24
|
+
throw Error(`REFUSE·signer head's signer did not sign for ${lot.keyId}`); return signerResponseOf(`${got.out.join('\n')}\n`, lot); })(await spawned(process.execPath, [signer, service, plain(account), lot.keyId, lot.algorithm, kind], { input: `${lot.bytes.join('\n')}\n` }, { wait, most: 1 << 20, signal }));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** A file one bound run reads, in a folder of its own under this device's temporary folder, kept only until that run ends. */
|
|
2
|
+
export declare const scratchOf: (prefix: string, name: string, text: string) => string;
|
|
3
|
+
/** The folders made for what a run read, removed once it ended, however it ended; any other path is left as it is. */
|
|
4
|
+
export declare const cleared: (paths: readonly string[]) => void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
const made = new Set();
|
|
5
|
+
/** A file one bound run reads, in a folder of its own under this device's temporary folder, kept only until that run ends. */
|
|
6
|
+
export const scratchOf = (prefix, name, text) => ((dir) => { made.add(dir); writeFileSync(join(dir, name), text); return join(dir, name); })(mkdtempSync(join(tmpdir(), prefix)));
|
|
7
|
+
/** The folders made for what a run read, removed once it ended, however it ended; any other path is left as it is. */
|
|
8
|
+
export const cleared = (paths) => { for (const at of paths)
|
|
9
|
+
if (made.delete(dirname(at)))
|
|
10
|
+
rmSync(dirname(at), { recursive: true, force: true }); };
|
package/dist/src/signer.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { signLot } from "./ports/keys.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { newKey, publicOf, signLot } from "./ports/keys.js";
|
|
4
|
+
import { secretsOf } from "./ports/keychain.js";
|
|
5
|
+
/** The one process that reads a signing secret, and it prints a public half at most: `make` keeps a key made once, `exists` says whether one is kept, else it signs the lot on stdin. */
|
|
6
|
+
const [first = '', ...rest] = process.argv.slice(2), half = first === 'make' || first === 'exists', [service = '', account = '', key = '', algorithm = '', store = 'keychain'] = half ? [rest[0], rest[1], '', '', rest[2]] : [first, ...rest], keys = secretsOf(service, store);
|
|
7
|
+
const kept = keys.read(account) ?? (first === 'make' ? ((made) => { keys.write(account, made.secret); return keys.read(account) === made.secret ? made.secret : undefined; })(newKey()) : undefined);
|
|
8
|
+
if (kept === undefined)
|
|
9
|
+
process.exit(half ? 3 : 1);
|
|
9
10
|
try {
|
|
10
|
-
process.stdout.write(signLot(readFileSync(0, 'utf8'),
|
|
11
|
+
process.stdout.write(half ? publicOf(kept) : signLot(readFileSync(0, 'utf8'), kept, key, algorithm));
|
|
11
12
|
}
|
|
12
13
|
catch {
|
|
13
14
|
process.exit(1);
|
package/dist/src/tools/card.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { inStanding, printedOr } from "../core/answer.js";
|
|
2
2
|
import { members, valueOf } from "../core/lock.js";
|
|
3
3
|
import { sayIn } from "../core/surface.js";
|
|
4
4
|
/** 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) =>
|
|
5
|
+
export const answer = (asked, context) => inStanding(asked, context, async (place, lines) => {
|
|
6
6
|
const word = (scope) => valueOf(context.lock, `bound/${scope}`);
|
|
7
|
-
const
|
|
8
|
-
if ('said' in held)
|
|
9
|
-
return held.said;
|
|
10
|
-
const view = held.lines.find((f) => f.scope?.startsWith('view/') && !f.shape && members(f.value ?? '').includes(word('cells')));
|
|
7
|
+
const view = lines.find((f) => f.scope?.startsWith('view/') && !f.shape && members(f.value ?? '').includes(word('cells')));
|
|
11
8
|
if (view === undefined)
|
|
12
9
|
return [sayIn(context.lock)('no-cells', { place })];
|
|
13
10
|
return printedOr(context, await context.bound(place, [word('read'), word('view'), `${view.scope.split('/')[1]}${asked['at'] ? `${word('at')}${asked['at']}` : ''}`, ...(asked['key'] ? [word('key'), asked['key']] : [])]));
|
package/dist/src/verbs/ask.js
CHANGED
|
@@ -6,6 +6,7 @@ import { sayIn } from "../core/surface.js";
|
|
|
6
6
|
import { paidIn } from "../onboard/owed.js";
|
|
7
7
|
import { pendingIn } from "../sources/refused.js";
|
|
8
8
|
import { compared } from "../marks/compare.js";
|
|
9
|
+
import { viewed } from "../claims/view.js";
|
|
9
10
|
import { cardAt } from "./card.js";
|
|
10
11
|
/** Route a question at the declared resolution, then attach the card of the place it read. */
|
|
11
12
|
export const asked = async (asked, context) => {
|
|
@@ -21,7 +22,7 @@ export const asked = async (asked, context) => {
|
|
|
21
22
|
const owed = asked['source'] && founded ? await paidIn(asked, context, place) : undefined;
|
|
22
23
|
if (owed !== undefined && !owed.answer)
|
|
23
24
|
return owed.said;
|
|
24
|
-
const answer = asked['sources'] && founded ? await compared(asked, context, place) : asked['lines'] ? await read('plan', 'plan') : asked['view'] ? await read('view', 'look') : asked['source'] ? await read('offers', 'look')
|
|
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')
|
|
25
26
|
: !carded ? await read('places', 'places') : founded ? [...await read('card', 'card', { place }), ...await pendingIn(context, place)] : [];
|
|
26
27
|
const after = held === undefined || 'said' in held || place === undefined || !context.moved(place) ? held : await cardAt(context, place);
|
|
27
28
|
return [...(owed?.said ?? []), ...answer, ...(after === undefined || 'said' in after ? [] : [sayIn(context.lock)('card', { place: place ?? '', card: after.card, from: 'ask' })])];
|
package/dist/src/walk/context.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { canonical, LOCK } from '@lapxo/topos/wire';
|
|
3
3
|
import { members, valueOf } from "../core/lock.js";
|
|
4
4
|
import { linesOf } from "../core/worlds.js";
|
|
5
|
+
import { pinnedAs } from "../compile/pin.js";
|
|
5
6
|
/**
|
|
6
7
|
* A place made walkable once it is founded: the context another place authenticates it by — its wire, its keys and its
|
|
7
8
|
* origin, read off the lines it was born with — folded by bound into a standing, laid beside its lock and pinned by its
|
|
@@ -11,13 +12,9 @@ export const walkable = async (context, dir, account) => {
|
|
|
11
12
|
const [word, at] = [(k) => valueOf(context.lock, k), valueOf(context.lock, 'walk/context/file')];
|
|
12
13
|
const reads = members(word('walk/context/reads'));
|
|
13
14
|
const born = linesOf(context.files.take(join(dir, LOCK)) ?? new Uint8Array()).filter((l) => reads.some((r) => (r.endsWith('/**') ? l.scope?.startsWith(r.slice(0, -2)) : l.scope === r)) && l.value !== 'withdraw');
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
canonical({ at: 'policy:head/walk', by: 'target', form: 'alphabet', measure: 'reads', role: 'writes', scope: word('walk/context/region'), value: reads.join('|') })].join('\n')}\n`));
|
|
17
|
-
const pinned = await context.pinned(dir, draft);
|
|
18
|
-
context.files.drop(dir, draft);
|
|
15
|
+
const pinned = await pinnedAs(context, dir, at, [...born.map(({ sig: _s, epoch: _e, ...l }) => canonical({ ...l, by: 'target' })),
|
|
16
|
+
canonical({ at: 'policy:head/walk', by: 'target', form: 'alphabet', measure: 'reads', role: 'writes', scope: word('walk/context/region'), value: reads.join('|') })]);
|
|
19
17
|
if (pinned === undefined || 'refused' in pinned)
|
|
20
18
|
return { refused: pinned === undefined ? 'walk context folded nothing' : pinned.refused };
|
|
21
|
-
context.files.lay(dir, at, new TextEncoder().encode(pinned.bytes));
|
|
22
19
|
return context.landRoot(dir, [canonical({ at: 'policy:head/walk', by: 'target', form: 'alphabet', measure: 'digest', needs: at, role: 'writes', scope: 'walk/context', value: pinned.pin })], account);
|
|
23
20
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lapxo/head",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"test": "node --test test/*.test.ts && node --test --test-concurrency=1 test/time/*.test.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@lapxo/bound": "0.2.
|
|
30
|
-
"@lapxo/topos": "0.2.
|
|
29
|
+
"@lapxo/bound": "0.2.7",
|
|
30
|
+
"@lapxo/topos": "0.2.4",
|
|
31
31
|
"@modelcontextprotocol/sdk": "1.32.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The host's registry bound reads to bind a declared signer name to a program: head's own signer, started by this node,
|
|
3
|
-
* told the keychain service, the account its secret is kept under, the key it signs as, the algorithm and the store this
|
|
4
|
-
* host keeps secrets in. It holds no secret; it lives beside nothing a repository ships.
|
|
5
|
-
*/
|
|
6
|
-
export declare const registryFor: (name: string, service: string, account: string, key: string, algorithm: string, store?: string) => string;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, mkdtempSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
/**
|
|
6
|
-
* The host's registry bound reads to bind a declared signer name to a program: head's own signer, started by this node,
|
|
7
|
-
* told the keychain service, the account its secret is kept under, the key it signs as, the algorithm and the store this
|
|
8
|
-
* host keeps secrets in. It holds no secret; it lives beside nothing a repository ships.
|
|
9
|
-
*/
|
|
10
|
-
export const registryFor = (name, service, account, key, algorithm, store = 'keychain') => {
|
|
11
|
-
const at = join(mkdtempSync(join(tmpdir(), 'head-signers-')), 'signers.json');
|
|
12
|
-
writeFileSync(at, JSON.stringify({ [name]: { kind: 'command', executable: process.execPath, args: [fileURLToPath(new URL(`../signer${import.meta.url.slice(import.meta.url.lastIndexOf('.'))}`, import.meta.url)), service, account, key, algorithm, store] } }));
|
|
13
|
-
return at;
|
|
14
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/src/ports/spare.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
import { readFileSync } from 'node:fs';
|
|
10
|
-
import { Socket } from 'node:net';
|
|
11
|
-
import { pathToFileURL } from 'node:url';
|
|
12
|
-
/**
|
|
13
|
-
* A process started before it is asked: it loads what the program it will run imports, says it is ready on its fourth
|
|
14
|
-
* pipe, and waits there for one ask, the folder, the arguments and the variables a process started then would have been
|
|
15
|
-
* given. It takes them as its own, closes that pipe and runs the program once, as that process would have. An ask that
|
|
16
|
-
* never comes ends it.
|
|
17
|
-
*/
|
|
18
|
-
const [program] = process.argv.slice(2);
|
|
19
|
-
const url = pathToFileURL(program), pipe = new Socket({ fd: 3, readable: true, writable: true });
|
|
20
|
-
const asked = new Promise((resolve) => { let text = ''; pipe.setEncoding('utf8').on('data', (chunk) => { text += chunk; }).on('error', () => undefined).on('close', () => resolve(text)); });
|
|
21
|
-
await Promise.all([...readFileSync(program, 'utf8').matchAll(/(?:^|[;\n])\s*import\s*(?:(?:[^'";]*?)\s*from\s*)?['"](\.{1,2}\/[^'"]+)['"]/g)].map(([, path]) => import(__rewriteRelativeImportExtension(new URL(path, url).href)).catch(() => undefined)));
|
|
22
|
-
pipe.write('ready\n');
|
|
23
|
-
const text = await asked;
|
|
24
|
-
if (!text)
|
|
25
|
-
process.exit(0);
|
|
26
|
-
const { cwd, args, env } = JSON.parse(text);
|
|
27
|
-
for (const name of Object.keys(process.env))
|
|
28
|
-
if (!(name in env))
|
|
29
|
-
delete process.env[name];
|
|
30
|
-
Object.assign(process.env, env);
|
|
31
|
-
process.chdir(cwd);
|
|
32
|
-
process.argv = [process.argv[0], program, ...args];
|
|
33
|
-
await import(__rewriteRelativeImportExtension(url.href));
|
package/dist/src/ports/warm.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { Bounds, Printed } from './process.ts';
|
|
2
|
-
/** How many processes head keeps started ahead of each program while it answers a call, and the variables node reads as it starts: a process started under other values of them is never asked. */
|
|
3
|
-
export declare const warmth: {
|
|
4
|
-
spares: number;
|
|
5
|
-
boot: readonly string[];
|
|
6
|
-
};
|
|
7
|
-
/** Keeps as many processes started ahead of a program as head's lock says; resolved when each has loaded what it runs. */
|
|
8
|
-
export declare const kept: (program: string) => Promise<unknown>;
|
|
9
|
-
/** Every process still waiting to be asked is stopped: head keeps none past the call it started them for. */
|
|
10
|
-
export declare const cooled: () => void;
|
|
11
|
-
/**
|
|
12
|
-
* A program run in a process started ahead under the same starting variables, handed the folder, arguments and
|
|
13
|
-
* variables a process started now would get, and watched within the same bounds; undefined when none is held ready,
|
|
14
|
-
* so the caller starts one as before and never waits on one still loading. Each process answers one ask and ends.
|
|
15
|
-
*/
|
|
16
|
-
export declare const warmed: (program: string, args: readonly string[], at: {
|
|
17
|
-
readonly cwd: string;
|
|
18
|
-
readonly env: NodeJS.ProcessEnv;
|
|
19
|
-
}, bounds: Bounds) => Promise<Printed> | undefined;
|
package/dist/src/ports/warm.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
-
import { grouped, watched } from "./process.js";
|
|
3
|
-
/** How many processes head keeps started ahead of each program while it answers a call, and the variables node reads as it starts: a process started under other values of them is never asked. */
|
|
4
|
-
export const warmth = { spares: 0, boot: [] };
|
|
5
|
-
const spares = new Map();
|
|
6
|
-
const alive = (one) => one.child.exitCode === null && one.child.signalCode === null;
|
|
7
|
-
const entry = fileURLToPath(new URL(`./spare${import.meta.url.slice(import.meta.url.lastIndexOf('.'))}`, import.meta.url));
|
|
8
|
-
const bootOf = (env) => JSON.stringify(Object.entries(env).filter(([name]) => warmth.boot.some((p) => name.startsWith(p))).sort(([a], [b]) => (a < b ? -1 : 1)));
|
|
9
|
-
const stop = (one) => { try {
|
|
10
|
-
process.kill(-one.child.pid, 'SIGKILL');
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
one.child.kill('SIGKILL');
|
|
14
|
-
} };
|
|
15
|
-
const started = (program) => {
|
|
16
|
-
const env = { ...process.env }, child = grouped(process.execPath, [entry, program], { env }, 4);
|
|
17
|
-
child.stdin.on('error', () => undefined).end('');
|
|
18
|
-
const spare = { child, boot: bootOf(env), ready: new Promise((resolve) => { child.stdio[3].on('error', () => undefined).once('data', () => { spare.loaded = true; resolve(); }); child.once('close', () => resolve()); }), loaded: false };
|
|
19
|
-
return spare;
|
|
20
|
-
};
|
|
21
|
-
/** Keeps as many processes started ahead of a program as head's lock says; resolved when each has loaded what it runs. */
|
|
22
|
-
export const kept = (program) => {
|
|
23
|
-
const live = (spares.get(program) ?? []).filter(alive);
|
|
24
|
-
while (live.length < warmth.spares)
|
|
25
|
-
live.push(started(program));
|
|
26
|
-
spares.set(program, live);
|
|
27
|
-
return Promise.all(live.map((one) => one.ready));
|
|
28
|
-
};
|
|
29
|
-
/** Every process still waiting to be asked is stopped: head keeps none past the call it started them for. */
|
|
30
|
-
export const cooled = () => { for (const one of [...spares.values()].flat())
|
|
31
|
-
stop(one); spares.clear(); };
|
|
32
|
-
process.once('exit', cooled);
|
|
33
|
-
/**
|
|
34
|
-
* A program run in a process started ahead under the same starting variables, handed the folder, arguments and
|
|
35
|
-
* variables a process started now would get, and watched within the same bounds; undefined when none is held ready,
|
|
36
|
-
* so the caller starts one as before and never waits on one still loading. Each process answers one ask and ends.
|
|
37
|
-
*/
|
|
38
|
-
export const warmed = (program, args, at, bounds) => {
|
|
39
|
-
const live = spares.get(program) ?? [], index = live.findIndex((one) => one.loaded && alive(one) && one.boot === bootOf(at.env));
|
|
40
|
-
const spare = index < 0 ? undefined : live.splice(index, 1)[0];
|
|
41
|
-
if (spare === undefined)
|
|
42
|
-
return undefined;
|
|
43
|
-
spare.child.stdio[3].end(`${JSON.stringify({ cwd: at.cwd, args, env: at.env })}\n`);
|
|
44
|
-
return watched(spare.child, bounds).finally(() => { void kept(program); });
|
|
45
|
-
};
|