@volter/world-runtime 3.0.37 → 3.0.39
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/dist/known-external-services.json +3 -10
- package/dist/src/actions-runner-cli.js +3 -3
- package/dist/src/actions-runner.js +2 -1
- package/dist/src/app-url.js +2 -1
- package/dist/src/backings.d.ts +10 -1
- package/dist/src/backings.js +66 -7
- package/dist/src/branch.js +85 -52
- package/dist/src/browser-routes.cjs +59 -0
- package/dist/src/ca-trust.js +4 -3
- package/dist/src/changeset.js +4 -3
- package/dist/src/cli-commands.js +25 -9
- package/dist/src/cli.js +2 -1
- package/dist/src/command-lifetime.d.mts +12 -0
- package/dist/src/command-lifetime.d.ts +1 -15
- package/dist/src/command-lifetime.js +1 -98
- package/dist/src/command-lifetime.mjs +90 -0
- package/dist/src/configs.js +1 -1
- package/dist/src/console-apart.js +3 -2
- package/dist/src/covers.d.ts +3 -2
- package/dist/src/covers.js +95 -18
- package/dist/src/error-message.d.mts +1 -0
- package/dist/src/error-message.d.ts +1 -0
- package/dist/src/error-message.js +2 -0
- package/dist/src/error-message.mjs +10 -0
- package/dist/src/guest-command.d.mts +4 -0
- package/dist/src/guest-command.mjs +26 -0
- package/dist/src/guest-control.d.mts +1 -0
- package/dist/src/guest-control.mjs +17 -0
- package/dist/src/guest-entry.mjs +146 -0
- package/dist/src/guest-processes.d.mts +9 -0
- package/dist/src/guest-processes.mjs +47 -0
- package/dist/src/guest-runner.d.ts +6 -0
- package/dist/src/guest-runner.js +567 -0
- package/dist/src/host.js +5 -4
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/infra-cli.js +4 -0
- package/dist/src/infra.js +133 -23
- package/dist/src/init.js +60 -23
- package/dist/src/lifecycle-record.d.ts +3 -1
- package/dist/src/lifecycle-record.js +29 -6
- package/dist/src/livekit-backing.mjs +37 -0
- package/dist/src/local-branches.js +11 -3
- package/dist/src/mongodb-backing.d.ts +7 -0
- package/dist/src/mongodb-backing.js +58 -0
- package/dist/src/native-postgres-backing.d.ts +4 -0
- package/dist/src/native-postgres-backing.js +404 -0
- package/dist/src/origin.js +2 -1
- package/dist/src/owned-path.d.ts +4 -0
- package/dist/src/owned-path.js +37 -0
- package/dist/src/pack-facts.d.ts +1 -0
- package/dist/src/pglite-backing.d.ts +4 -13
- package/dist/src/pglite-backing.js +198 -106
- package/dist/src/pglite-host.mjs +47 -17
- package/dist/src/placeholder.d.ts +3 -2
- package/dist/src/placeholder.js +9 -4
- package/dist/src/process-groups.d.mts +2 -0
- package/dist/src/process-groups.d.ts +1 -4
- package/dist/src/process-groups.js +1 -49
- package/dist/src/process-groups.mjs +59 -0
- package/dist/src/process-receipt.d.mts +6 -0
- package/dist/src/process-receipt.d.ts +11 -0
- package/dist/src/process-receipt.js +3 -0
- package/dist/src/process-receipt.mjs +45 -0
- package/dist/src/process-signal.d.mts +9 -0
- package/dist/src/process-signal.mjs +92 -0
- package/dist/src/project-inspect.d.ts +1 -1
- package/dist/src/project-inspect.js +4 -2
- package/dist/src/proxy-daemon.js +2 -1
- package/dist/src/redirect-proxy.js +186 -19
- package/dist/src/redis-backing.js +99 -35
- package/dist/src/reflect.js +2 -1
- package/dist/src/root.js +7 -6
- package/dist/src/run-task-worker.js +2 -1
- package/dist/src/runtime.d.ts +24 -7
- package/dist/src/runtime.js +703 -286
- package/dist/src/schema.d.ts +12 -1
- package/dist/src/schema.js +20 -5
- package/dist/src/served-world.d.ts +1 -0
- package/dist/src/served-world.js +26 -14
- package/dist/src/service-recorder.js +8 -3
- package/dist/src/signals.d.mts +1 -0
- package/dist/src/signals.d.ts +1 -1
- package/dist/src/signals.js +1 -11
- package/dist/src/signals.mjs +10 -0
- package/dist/src/tail.d.ts +2 -0
- package/dist/src/tail.js +9 -2
- package/dist/src/tcp-port.js +27 -13
- package/dist/src/temporal-backing.mjs +28 -0
- package/dist/src/up-cli.js +8 -3
- package/dist/src/up-task-worker.js +2 -1
- package/dist/src/websocket-relay.d.ts +1 -1
- package/dist/src/websocket-relay.js +23 -10
- package/dist/src/world-view.js +2 -2
- package/known-external-services.json +3 -10
- package/package.json +12 -7
- package/src/actions-runner-cli.ts +3 -3
- package/src/actions-runner.ts +2 -1
- package/src/app-url.ts +2 -1
- package/src/backings.ts +65 -7
- package/src/branch.ts +73 -34
- package/src/browser-routes.cjs +59 -0
- package/src/ca-trust.ts +4 -3
- package/src/changeset.ts +4 -3
- package/src/cli-commands.ts +21 -9
- package/src/cli.ts +2 -1
- package/src/command-lifetime.d.mts +12 -0
- package/src/command-lifetime.mjs +90 -0
- package/src/command-lifetime.ts +1 -79
- package/src/configs.ts +1 -1
- package/src/console-apart.ts +2 -1
- package/src/covers.ts +97 -19
- package/src/error-message.d.mts +1 -0
- package/src/error-message.mjs +10 -0
- package/src/error-message.ts +2 -0
- package/src/guest-command.d.mts +4 -0
- package/src/guest-command.mjs +26 -0
- package/src/guest-control.d.mts +1 -0
- package/src/guest-control.mjs +17 -0
- package/src/guest-entry.mjs +146 -0
- package/src/guest-processes.d.mts +9 -0
- package/src/guest-processes.mjs +47 -0
- package/src/guest-runner.ts +414 -0
- package/src/host.ts +5 -4
- package/src/index.ts +1 -0
- package/src/infra-cli.ts +4 -0
- package/src/infra.ts +103 -22
- package/src/init.ts +55 -22
- package/src/lifecycle-record.ts +21 -6
- package/src/livekit-backing.mjs +37 -0
- package/src/local-branches.ts +6 -3
- package/src/mongodb-backing.ts +55 -0
- package/src/native-postgres-backing.ts +329 -0
- package/src/origin.ts +2 -1
- package/src/owned-path.ts +32 -0
- package/src/pack-facts.ts +1 -0
- package/src/pglite-backing.ts +155 -79
- package/src/pglite-host.mjs +47 -17
- package/src/placeholder.ts +10 -5
- package/src/process-groups.d.mts +2 -0
- package/src/process-groups.mjs +59 -0
- package/src/process-groups.ts +1 -33
- package/src/process-receipt.d.mts +6 -0
- package/src/process-receipt.mjs +45 -0
- package/src/process-receipt.ts +5 -0
- package/src/process-signal.d.mts +9 -0
- package/src/process-signal.mjs +92 -0
- package/src/project-inspect.ts +4 -2
- package/src/proxy-daemon.ts +2 -1
- package/src/redirect-proxy.ts +153 -18
- package/src/redis-backing.ts +74 -25
- package/src/reflect.ts +2 -1
- package/src/root.ts +7 -6
- package/src/run-task-worker.ts +2 -1
- package/src/runtime.ts +596 -265
- package/src/schema.ts +30 -6
- package/src/served-world.ts +21 -15
- package/src/service-recorder.ts +4 -2
- package/src/signals.d.mts +1 -0
- package/src/signals.mjs +10 -0
- package/src/signals.ts +1 -10
- package/src/tail.ts +10 -3
- package/src/tcp-port.ts +24 -13
- package/src/temporal-backing.mjs +28 -0
- package/src/up-cli.ts +6 -3
- package/src/up-task-worker.ts +2 -1
- package/src/websocket-relay.ts +18 -11
- package/src/world-view.ts +2 -2
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Operator-facing failures include aggregate members and retained causes. */
|
|
2
|
+
export function errorMessage(error, seen = new Set()) {
|
|
3
|
+
const message = error && typeof error === 'object' && 'message' in error ? String(error.message) : String(error);
|
|
4
|
+
if (seen.has(error)) return '[circular error]';
|
|
5
|
+
seen.add(error);
|
|
6
|
+
const details = error instanceof AggregateError ? Array.from(error.errors, member => errorMessage(member, seen)) : [];
|
|
7
|
+
if (error instanceof Error && error.cause !== undefined) details.push(`cause: ${errorMessage(error.cause, seen)}`);
|
|
8
|
+
seen.delete(error);
|
|
9
|
+
return [message, ...details.map(detail => detail.split('\n').map(line => ` ${line}`).join('\n'))].join('\n');
|
|
10
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export function admitGuestArgv(argv: string[], runtime: string, root: string, cwd: string): { argv: string[]; cwd: string };
|
|
2
|
+
export function guestCommandGap(message: string): Error & { code: string };
|
|
3
|
+
export function guestEnvironment(env: Record<string, string>): Record<string, string>;
|
|
4
|
+
export function guestEnvironment(env: Record<string, string | undefined>): Record<string, string | undefined>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Declared execution limits reach packs as capability gaps, never vendor launch failures.
|
|
2
|
+
export const guestCommandGap = message => Object.assign(new Error(message), { code: 'WORLD_MACHINE_CAPABILITY_GAP' });
|
|
3
|
+
// Only World routing may restore Node preload/CA configuration after caller controls are stripped.
|
|
4
|
+
export function guestEnvironment(env) {
|
|
5
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => !/^(NODE_|LD_|DYLD_|SUBSTRATE_|OPENSSL_CONF$|OPENSSL_MODULES$)/i.test(key)));
|
|
6
|
+
}
|
|
7
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
8
|
+
import { resolve, relative, isAbsolute } from 'node:path';
|
|
9
|
+
// Interim admission list: the image's Node runtime and an ordinary JS file inside its folder.
|
|
10
|
+
export function admitGuestArgv(argv, runtime, root, cwd) {
|
|
11
|
+
if (runtime !== 'node' || !Array.isArray(argv) || argv.length < 2 || !argv.every(value => typeof value === 'string' && !value.includes('\0')) || argv[0] !== 'node') {
|
|
12
|
+
throw guestCommandGap(`unsupported guest command ${Array.isArray(argv) ? argv[0] : '(invalid argv)'}; admitted program: node with an image entry file`);
|
|
13
|
+
}
|
|
14
|
+
if (argv[1].startsWith('-')) throw guestCommandGap('unsupported Node option/eval; guest command requires an image entry file');
|
|
15
|
+
const folder = realpathSync(root);
|
|
16
|
+
const within = path => {
|
|
17
|
+
const real = realpathSync(path), rest = relative(folder, real);
|
|
18
|
+
if (rest === '..' || rest.startsWith('../') || isAbsolute(rest)) throw guestCommandGap('guest command path leaves its folder');
|
|
19
|
+
return real;
|
|
20
|
+
};
|
|
21
|
+
const directory = within(cwd);
|
|
22
|
+
if (!statSync(directory).isDirectory()) throw guestCommandGap('guest command cwd is not a directory');
|
|
23
|
+
const entry = resolve(directory, argv[1]);
|
|
24
|
+
if (!existsSync(entry) || !/\.(?:mjs|cjs|js)$/.test(entry) || !statSync(within(entry)).isFile()) throw guestCommandGap('guest command needs an ordinary JS entry file in its folder');
|
|
25
|
+
return { argv: ['node', within(entry), ...argv.slice(2)], cwd: directory };
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function guestControl<T>(operation: (signal: AbortSignal) => Promise<T>, label: string, signal?: AbortSignal, budget?: number): Promise<T>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { COMMAND_GRACE_MS } from './command-lifetime.mjs';
|
|
2
|
+
// Runtime teardown contract: existing 5000 ms grace, not a new vendor default.
|
|
3
|
+
export function guestControl(operation, label, signal, budget = COMMAND_GRACE_MS) {
|
|
4
|
+
const controller = new AbortController();
|
|
5
|
+
const abort = () => controller.abort(signal.reason);
|
|
6
|
+
if (signal?.aborted) abort(); else signal?.addEventListener('abort', abort, { once: true });
|
|
7
|
+
const timer = setTimeout(() => controller.abort(new Error(`${label} exceeded ${budget} ms runtime control budget; ownership retained`)), budget);
|
|
8
|
+
let rejectAbort;
|
|
9
|
+
const failed = new Promise((_, reject) => { rejectAbort = reject; });
|
|
10
|
+
const cancelled = () => rejectAbort(controller.signal.reason);
|
|
11
|
+
controller.signal.addEventListener('abort', cancelled, { once: true });
|
|
12
|
+
if (controller.signal.aborted) cancelled();
|
|
13
|
+
return Promise.race([Promise.resolve().then(() => { controller.signal.throwIfAborted(); return operation(controller.signal); }), failed]).finally(() => {
|
|
14
|
+
clearTimeout(timer); signal?.removeEventListener('abort', abort);
|
|
15
|
+
controller.signal.removeEventListener('abort', cancelled);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// One pinned Substrate host; the interim adapter admits Node argv, never an OS shell.
|
|
2
|
+
import childProcess from 'node:child_process';
|
|
3
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { createNodeHost, serveAgent } from '@volter/browser-host-node';
|
|
6
|
+
import { parseSimpleBrowserCommand } from '@volter/browser-runtime';
|
|
7
|
+
import { signalOwnedProcess } from './process-signal.mjs';
|
|
8
|
+
import { commandLifetime, COMMAND_GRACE_MS } from './command-lifetime.mjs';
|
|
9
|
+
import { beginGuestProcess, retainGuestProcess, releaseGuestProcesses } from './guest-processes.mjs';
|
|
10
|
+
import { admitGuestArgv, guestEnvironment } from './guest-command.mjs';
|
|
11
|
+
import { guestControl } from './guest-control.mjs';
|
|
12
|
+
|
|
13
|
+
const host = createNodeHost({ root: process.env.SUBSTRATE_ROOT });
|
|
14
|
+
const routing = JSON.parse(process.env.SUBSTRATE_GUEST_ENV);
|
|
15
|
+
const retained = new Map(), admissions = new Map();
|
|
16
|
+
let closing = false, launching;
|
|
17
|
+
const rawSpawn = childProcess.spawn;
|
|
18
|
+
// The pinned lane creates/retains children synchronously in run(). Replace exactly its
|
|
19
|
+
// launcher before spawning, and keep its own pipes, process tracking and socket discovery.
|
|
20
|
+
// Scope is synchronous: inspectors and future unrelated spawns never acquire this admission.
|
|
21
|
+
childProcess.spawn = (file, args, options) => {
|
|
22
|
+
if (file !== '/bin/sh') return rawSpawn(file, args, options);
|
|
23
|
+
if (!launching || args.length !== 2 || args[0] !== '-c' || args[1] !== 'world-owned-node') throw new Error('guest shell execution refused');
|
|
24
|
+
const launch = launching;
|
|
25
|
+
const admitted = admitGuestArgv(launch.argv, process.env.SUBSTRATE_GUEST_RUNTIME, host.root, options.cwd);
|
|
26
|
+
const receipt = beginGuestProcess(process.env.SUBSTRATE_RECEIPTS, process.env.SUBSTRATE_WORLD_INSTANCE, process.env.SUBSTRATE_GUEST_NAME, 'command');
|
|
27
|
+
const child = rawSpawn(process.execPath, admitted.argv.slice(1), { ...options, env: { ...guestEnvironment(options.env ?? {}), ...routing }, cwd: admitted.cwd, shell: false });
|
|
28
|
+
const lifetime = commandLifetime(child, launch.signal);
|
|
29
|
+
const held = { child, lifetime, receipt };
|
|
30
|
+
if (!child.pid) {
|
|
31
|
+
void lifetime.finish().then(() => releaseGuestProcesses([receipt])).catch(error => console.error('Unspawned guest receipt retained:', error));
|
|
32
|
+
throw new Error('guest Node process did not spawn');
|
|
33
|
+
}
|
|
34
|
+
retained.set(child.pid, held); // ownership before publication
|
|
35
|
+
retainGuestProcess(receipt, { pid: child.pid });
|
|
36
|
+
held.birth = lifetime.identity.then(birth => {
|
|
37
|
+
if (!birth?.start || child.exitCode !== null || child.signalCode !== null || retained.get(child.pid) !== held) {
|
|
38
|
+
throw new Error('guest spawn birth invalid: original child no longer held/live or birth unavailable; admission refused');
|
|
39
|
+
}
|
|
40
|
+
retainGuestProcess(receipt, birth); // readiness cannot precede this successful atomic publication
|
|
41
|
+
return birth;
|
|
42
|
+
});
|
|
43
|
+
held.birth.then(launch.resolve, launch.reject);
|
|
44
|
+
void lifetime.finish().then(() => {
|
|
45
|
+
retained.delete(child.pid);
|
|
46
|
+
releaseGuestProcesses([receipt]); // commandLifetime confirms close and whole-group absence
|
|
47
|
+
}).catch(error => console.error('Guest command retirement unconfirmed; receipt retained:', error));
|
|
48
|
+
launch.held = held;
|
|
49
|
+
return child;
|
|
50
|
+
};
|
|
51
|
+
syncBuiltinESMExports();
|
|
52
|
+
const laneRun = host.lane.run.bind(host.lane);
|
|
53
|
+
host.lane.run = () => Promise.reject(new Error('guest shell fallback refused; only admitted Node argv is supported'));
|
|
54
|
+
host.lane.openPty = () => Promise.reject(new Error('guest PTY/shell execution is unsupported by Node argv admission'));
|
|
55
|
+
const program = '__world_node_argv';
|
|
56
|
+
host.runtime.registerProgram(program, async (argv, options) => {
|
|
57
|
+
const launch = argv.length === 2 ? admissions.get(argv[1]) : undefined;
|
|
58
|
+
if (closing || !launch || launch.consumed) throw new Error('guest command has no live argv admission');
|
|
59
|
+
launch.consumed = true;
|
|
60
|
+
if (options.terminal || options.stdinStream) throw new Error('guest PTY/streamed stdin is unsupported');
|
|
61
|
+
const cwd = host.filesystem.real(options.cwd ?? '/');
|
|
62
|
+
admitGuestArgv(launch.argv, process.env.SUBSTRATE_GUEST_RUNTIME, host.root, cwd);
|
|
63
|
+
launch.signal = options.signal;
|
|
64
|
+
let result;
|
|
65
|
+
try {
|
|
66
|
+
launching = launch;
|
|
67
|
+
// The pinned lane's raw abort callback signals a numeric PID; leave cancellation to
|
|
68
|
+
// our retained commandLifetime so every delivery uses the held child or verified birth.
|
|
69
|
+
result = laneRun('world-owned-node', { ...options, signal: undefined, env: { ...guestEnvironment(options.env ?? {}), ...routing } });
|
|
70
|
+
} catch (error) { launch.reject(error); throw error; }
|
|
71
|
+
finally { launching = undefined; }
|
|
72
|
+
if (!launch.held) { const error = new Error('Substrate did not retain the admitted Node child'); launch.reject(error); throw error; }
|
|
73
|
+
return (await Promise.all([result, launch.birth]))[0];
|
|
74
|
+
});
|
|
75
|
+
const runtimeStart = host.runtime.start.bind(host.runtime);
|
|
76
|
+
host.runtime.start = (command, options) => {
|
|
77
|
+
let argv = parseSimpleBrowserCommand(command);
|
|
78
|
+
// RemoteRuntime's wire envelope is decoded as data; neither envelope nor payload runs in a shell.
|
|
79
|
+
if (argv.length === 3 && argv[0] === 'sh' && argv[1] === '-c') argv = parseSimpleBrowserCommand(argv[2]);
|
|
80
|
+
if (closing || argv.length !== 2 || argv[0] !== program || !admissions.has(argv[1])) throw new Error('guest command is not an admitted Node argv operation');
|
|
81
|
+
return runtimeStart(`${program} ${argv[1]}`, options);
|
|
82
|
+
};
|
|
83
|
+
async function signalCommands(signal) {
|
|
84
|
+
await Promise.all([...retained.values()].map(async held => {
|
|
85
|
+
const birth = await held.birth; // rejected/unheld captures can never authorize a numeric signal
|
|
86
|
+
await signalOwnedProcess(birth, signal, true);
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
const agent = await guestControl(() => serveAgent({ host, token: process.env.SUBSTRATE_TOKEN, port: Number(process.env.SUBSTRATE_AGENT_PORT), bind: '127.0.0.1' }), 'guest agent startup');
|
|
90
|
+
const streams = new Set();
|
|
91
|
+
agent.server.on('request', (request, response) => {
|
|
92
|
+
if (!new URL(request.url, 'http://guest').pathname.endsWith('/events')) return;
|
|
93
|
+
streams.add(response); response.on('close', () => streams.delete(response));
|
|
94
|
+
});
|
|
95
|
+
async function close() {
|
|
96
|
+
if (closing) return;
|
|
97
|
+
closing = true;
|
|
98
|
+
const commands = [...retained.values()];
|
|
99
|
+
for (const held of commands) { held.lifetime.cancel('SIGCONT'); held.lifetime.cancel('SIGTERM'); }
|
|
100
|
+
// Every command retires through the same bounded grace/KILL/confirmation as World commands.
|
|
101
|
+
const results = await Promise.allSettled(commands.map(held => held.lifetime.finish()));
|
|
102
|
+
for (const result of results) if (result.status === 'rejected') throw result.reason;
|
|
103
|
+
await guestControl(async () => {
|
|
104
|
+
await host.close();
|
|
105
|
+
for (const response of streams) response.end();
|
|
106
|
+
await agent.close();
|
|
107
|
+
}, 'Substrate guest drain');
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
process.on('message', async ({ id, action, path }) => {
|
|
111
|
+
try {
|
|
112
|
+
let value;
|
|
113
|
+
if (action === 'prepare') {
|
|
114
|
+
if (closing) throw new Error('Substrate guest is retiring');
|
|
115
|
+
const argv = JSON.parse(path);
|
|
116
|
+
if (!Array.isArray(argv) || argv[0] !== 'node') throw new Error('admitted guest program is node');
|
|
117
|
+
const key = randomUUID();
|
|
118
|
+
let resolve, reject;
|
|
119
|
+
const birth = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
120
|
+
birth.catch(() => {});
|
|
121
|
+
admissions.set(key, { argv, birth, resolve, reject, consumed: false });
|
|
122
|
+
value = key;
|
|
123
|
+
} else if (action === 'admission') {
|
|
124
|
+
const launch = admissions.get(path);
|
|
125
|
+
if (!launch) throw new Error('guest launch admission missing');
|
|
126
|
+
try { value = await guestControl(() => launch.birth, 'guest birth admission'); }
|
|
127
|
+
finally { if (launch.consumed) admissions.delete(path); }
|
|
128
|
+
} else if (action === 'pause' || action === 'resume') {
|
|
129
|
+
await guestControl(() => signalCommands(action === 'pause' ? 'SIGSTOP' : 'SIGCONT'), `guest ${action}`);
|
|
130
|
+
} else if (action === 'stat') {
|
|
131
|
+
const s = host.filesystem.statSync(path);
|
|
132
|
+
value = { size: s.size, mode: s.mode, mtime: s.mtime.toISOString(), type: s.isDirectory() ? 'directory' : 'file' };
|
|
133
|
+
} else throw new Error(`unsupported guest control ${action}`);
|
|
134
|
+
if (process.connected) process.send?.({ id, value });
|
|
135
|
+
} catch (error) { if (process.connected) process.send?.({ id, error: error.message }); }
|
|
136
|
+
});
|
|
137
|
+
const retire = () => void close().catch(error => {
|
|
138
|
+
console.error('Substrate guest retirement refused; ownership retained:', error);
|
|
139
|
+
// No success or receipt deletion on uncertainty; exit closes control. World down owns retry.
|
|
140
|
+
process.exit(1);
|
|
141
|
+
});
|
|
142
|
+
process.on('disconnect', retire);
|
|
143
|
+
if (!process.connected) retire();
|
|
144
|
+
process.on('SIGTERM', retire);
|
|
145
|
+
process.on('SIGINT', retire);
|
|
146
|
+
process.send?.({ ready: true, port: agent.port, controlBudgetMs: COMMAND_GRACE_MS });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SpawnIdentity } from './process-receipt.js';
|
|
2
|
+
export interface GuestProcessRecord extends SpawnIdentity { instance: string; guest: string; kind: 'agent' | 'command' | 'pty'; launching: false }
|
|
3
|
+
export interface GuestProcessReceipt { path: string; record: GuestProcessRecord }
|
|
4
|
+
export interface GuestProcessLaunch { path: string; record: Omit<GuestProcessRecord, 'pid' | 'launching'> & { pid?: number; launching: boolean } }
|
|
5
|
+
export function guestProcessDirectory(instanceDir: string): string;
|
|
6
|
+
export function beginGuestProcess(directory: string, instance: string, guest: string, kind: GuestProcessRecord['kind']): GuestProcessLaunch;
|
|
7
|
+
export function retainGuestProcess(receipt: GuestProcessLaunch, identity: SpawnIdentity): void;
|
|
8
|
+
export function readGuestProcesses(instanceDir: string, expectedInstance?: string): GuestProcessReceipt[];
|
|
9
|
+
export function releaseGuestProcesses(receipts: Array<{ path: string }>): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Guest metadata over the existing atomic process receipt; no independent lifecycle.
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { publishProcessReceipt, readProcessReceipt } from './process-receipt.mjs';
|
|
6
|
+
|
|
7
|
+
export const guestProcessDirectory = instanceDir => join(instanceDir, 'guest-processes');
|
|
8
|
+
export function beginGuestProcess(directory, instance, guest, kind) {
|
|
9
|
+
mkdirSync(directory, { recursive: true });
|
|
10
|
+
const path = join(directory, `${randomUUID()}.json`);
|
|
11
|
+
const record = { instance, guest, kind, launching: true };
|
|
12
|
+
publishProcessReceipt(path, JSON.stringify(record));
|
|
13
|
+
return { path, record };
|
|
14
|
+
}
|
|
15
|
+
export function retainGuestProcess(receipt, identity) {
|
|
16
|
+
receipt.record = { ...receipt.record, launching: false, ...identity };
|
|
17
|
+
publishProcessReceipt(receipt.path, JSON.stringify(receipt.record));
|
|
18
|
+
}
|
|
19
|
+
export function readGuestProcesses(instanceDir, expectedInstance) {
|
|
20
|
+
const directory = guestProcessDirectory(instanceDir);
|
|
21
|
+
if (!existsSync(directory)) return [];
|
|
22
|
+
if (lstatSync(directory).isSymbolicLink()) throw new Error('Guest receipt directory is a symbolic link; ownership retained');
|
|
23
|
+
const names = readdirSync(directory);
|
|
24
|
+
if (!names.length) return [];
|
|
25
|
+
if (!expectedInstance) {
|
|
26
|
+
try { expectedInstance = JSON.parse(readFileSync(join(instanceDir, 'instance.json'), 'utf8')).instance; }
|
|
27
|
+
catch { throw new Error('Guest instance identity unreadable; ownership retained'); }
|
|
28
|
+
}
|
|
29
|
+
return names.map(name => {
|
|
30
|
+
const path = join(directory, name);
|
|
31
|
+
if (!/^[0-9a-f-]+\.json$/.test(name) || !lstatSync(path).isFile() || lstatSync(path).isSymbolicLink()) throw new Error('Unresolved guest receipt publication; ownership retained');
|
|
32
|
+
const receipt = readProcessReceipt(path, text => {
|
|
33
|
+
const r = JSON.parse(text);
|
|
34
|
+
if (r.instance !== expectedInstance || typeof r.guest !== 'string' || !['agent', 'command', 'pty'].includes(r.kind)
|
|
35
|
+
|| r.launching || !Number.isInteger(r.pid) || r.pid < 1 || (r.start !== undefined && (typeof r.start !== 'string' || !r.start))) {
|
|
36
|
+
throw new Error('Guest launch or spawn identity unresolved');
|
|
37
|
+
}
|
|
38
|
+
return r;
|
|
39
|
+
});
|
|
40
|
+
if (receipt.error || !receipt.value) throw receipt.error ?? new Error('Guest receipt disappeared during inspection; ownership retained');
|
|
41
|
+
return { path, record: receipt.value };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// The caller must have confirmed each recorded group absent before releasing these receipts.
|
|
45
|
+
export function releaseGuestProcesses(receipts) {
|
|
46
|
+
for (const { path } of receipts) rmSync(path);
|
|
47
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MachinePool, LightPoolOptions } from '@volter/world-core/runtime';
|
|
2
|
+
/** Local provider for Substrate's sandbox contract; all guest operations are vendor-neutral. */
|
|
3
|
+
export declare function createLightGuestPool(options: LightPoolOptions & {
|
|
4
|
+
world: string;
|
|
5
|
+
root?: string;
|
|
6
|
+
}): MachinePool;
|