@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.
Files changed (168) hide show
  1. package/dist/known-external-services.json +3 -10
  2. package/dist/src/actions-runner-cli.js +3 -3
  3. package/dist/src/actions-runner.js +2 -1
  4. package/dist/src/app-url.js +2 -1
  5. package/dist/src/backings.d.ts +10 -1
  6. package/dist/src/backings.js +66 -7
  7. package/dist/src/branch.js +85 -52
  8. package/dist/src/browser-routes.cjs +59 -0
  9. package/dist/src/ca-trust.js +4 -3
  10. package/dist/src/changeset.js +4 -3
  11. package/dist/src/cli-commands.js +25 -9
  12. package/dist/src/cli.js +2 -1
  13. package/dist/src/command-lifetime.d.mts +12 -0
  14. package/dist/src/command-lifetime.d.ts +1 -15
  15. package/dist/src/command-lifetime.js +1 -98
  16. package/dist/src/command-lifetime.mjs +90 -0
  17. package/dist/src/configs.js +1 -1
  18. package/dist/src/console-apart.js +3 -2
  19. package/dist/src/covers.d.ts +3 -2
  20. package/dist/src/covers.js +95 -18
  21. package/dist/src/error-message.d.mts +1 -0
  22. package/dist/src/error-message.d.ts +1 -0
  23. package/dist/src/error-message.js +2 -0
  24. package/dist/src/error-message.mjs +10 -0
  25. package/dist/src/guest-command.d.mts +4 -0
  26. package/dist/src/guest-command.mjs +26 -0
  27. package/dist/src/guest-control.d.mts +1 -0
  28. package/dist/src/guest-control.mjs +17 -0
  29. package/dist/src/guest-entry.mjs +146 -0
  30. package/dist/src/guest-processes.d.mts +9 -0
  31. package/dist/src/guest-processes.mjs +47 -0
  32. package/dist/src/guest-runner.d.ts +6 -0
  33. package/dist/src/guest-runner.js +567 -0
  34. package/dist/src/host.js +5 -4
  35. package/dist/src/index.d.ts +1 -0
  36. package/dist/src/index.js +1 -0
  37. package/dist/src/infra-cli.js +4 -0
  38. package/dist/src/infra.js +133 -23
  39. package/dist/src/init.js +60 -23
  40. package/dist/src/lifecycle-record.d.ts +3 -1
  41. package/dist/src/lifecycle-record.js +29 -6
  42. package/dist/src/livekit-backing.mjs +37 -0
  43. package/dist/src/local-branches.js +11 -3
  44. package/dist/src/mongodb-backing.d.ts +7 -0
  45. package/dist/src/mongodb-backing.js +58 -0
  46. package/dist/src/native-postgres-backing.d.ts +4 -0
  47. package/dist/src/native-postgres-backing.js +404 -0
  48. package/dist/src/origin.js +2 -1
  49. package/dist/src/owned-path.d.ts +4 -0
  50. package/dist/src/owned-path.js +37 -0
  51. package/dist/src/pack-facts.d.ts +1 -0
  52. package/dist/src/pglite-backing.d.ts +4 -13
  53. package/dist/src/pglite-backing.js +198 -106
  54. package/dist/src/pglite-host.mjs +47 -17
  55. package/dist/src/placeholder.d.ts +3 -2
  56. package/dist/src/placeholder.js +9 -4
  57. package/dist/src/process-groups.d.mts +2 -0
  58. package/dist/src/process-groups.d.ts +1 -4
  59. package/dist/src/process-groups.js +1 -49
  60. package/dist/src/process-groups.mjs +59 -0
  61. package/dist/src/process-receipt.d.mts +6 -0
  62. package/dist/src/process-receipt.d.ts +11 -0
  63. package/dist/src/process-receipt.js +3 -0
  64. package/dist/src/process-receipt.mjs +45 -0
  65. package/dist/src/process-signal.d.mts +9 -0
  66. package/dist/src/process-signal.mjs +92 -0
  67. package/dist/src/project-inspect.d.ts +1 -1
  68. package/dist/src/project-inspect.js +4 -2
  69. package/dist/src/proxy-daemon.js +2 -1
  70. package/dist/src/redirect-proxy.js +186 -19
  71. package/dist/src/redis-backing.js +99 -35
  72. package/dist/src/reflect.js +2 -1
  73. package/dist/src/root.js +7 -6
  74. package/dist/src/run-task-worker.js +2 -1
  75. package/dist/src/runtime.d.ts +24 -7
  76. package/dist/src/runtime.js +703 -286
  77. package/dist/src/schema.d.ts +12 -1
  78. package/dist/src/schema.js +20 -5
  79. package/dist/src/served-world.d.ts +1 -0
  80. package/dist/src/served-world.js +26 -14
  81. package/dist/src/service-recorder.js +8 -3
  82. package/dist/src/signals.d.mts +1 -0
  83. package/dist/src/signals.d.ts +1 -1
  84. package/dist/src/signals.js +1 -11
  85. package/dist/src/signals.mjs +10 -0
  86. package/dist/src/tail.d.ts +2 -0
  87. package/dist/src/tail.js +9 -2
  88. package/dist/src/tcp-port.js +27 -13
  89. package/dist/src/temporal-backing.mjs +28 -0
  90. package/dist/src/up-cli.js +8 -3
  91. package/dist/src/up-task-worker.js +2 -1
  92. package/dist/src/websocket-relay.d.ts +1 -1
  93. package/dist/src/websocket-relay.js +23 -10
  94. package/dist/src/world-view.js +2 -2
  95. package/known-external-services.json +3 -10
  96. package/package.json +12 -7
  97. package/src/actions-runner-cli.ts +3 -3
  98. package/src/actions-runner.ts +2 -1
  99. package/src/app-url.ts +2 -1
  100. package/src/backings.ts +65 -7
  101. package/src/branch.ts +73 -34
  102. package/src/browser-routes.cjs +59 -0
  103. package/src/ca-trust.ts +4 -3
  104. package/src/changeset.ts +4 -3
  105. package/src/cli-commands.ts +21 -9
  106. package/src/cli.ts +2 -1
  107. package/src/command-lifetime.d.mts +12 -0
  108. package/src/command-lifetime.mjs +90 -0
  109. package/src/command-lifetime.ts +1 -79
  110. package/src/configs.ts +1 -1
  111. package/src/console-apart.ts +2 -1
  112. package/src/covers.ts +97 -19
  113. package/src/error-message.d.mts +1 -0
  114. package/src/error-message.mjs +10 -0
  115. package/src/error-message.ts +2 -0
  116. package/src/guest-command.d.mts +4 -0
  117. package/src/guest-command.mjs +26 -0
  118. package/src/guest-control.d.mts +1 -0
  119. package/src/guest-control.mjs +17 -0
  120. package/src/guest-entry.mjs +146 -0
  121. package/src/guest-processes.d.mts +9 -0
  122. package/src/guest-processes.mjs +47 -0
  123. package/src/guest-runner.ts +414 -0
  124. package/src/host.ts +5 -4
  125. package/src/index.ts +1 -0
  126. package/src/infra-cli.ts +4 -0
  127. package/src/infra.ts +103 -22
  128. package/src/init.ts +55 -22
  129. package/src/lifecycle-record.ts +21 -6
  130. package/src/livekit-backing.mjs +37 -0
  131. package/src/local-branches.ts +6 -3
  132. package/src/mongodb-backing.ts +55 -0
  133. package/src/native-postgres-backing.ts +329 -0
  134. package/src/origin.ts +2 -1
  135. package/src/owned-path.ts +32 -0
  136. package/src/pack-facts.ts +1 -0
  137. package/src/pglite-backing.ts +155 -79
  138. package/src/pglite-host.mjs +47 -17
  139. package/src/placeholder.ts +10 -5
  140. package/src/process-groups.d.mts +2 -0
  141. package/src/process-groups.mjs +59 -0
  142. package/src/process-groups.ts +1 -33
  143. package/src/process-receipt.d.mts +6 -0
  144. package/src/process-receipt.mjs +45 -0
  145. package/src/process-receipt.ts +5 -0
  146. package/src/process-signal.d.mts +9 -0
  147. package/src/process-signal.mjs +92 -0
  148. package/src/project-inspect.ts +4 -2
  149. package/src/proxy-daemon.ts +2 -1
  150. package/src/redirect-proxy.ts +153 -18
  151. package/src/redis-backing.ts +74 -25
  152. package/src/reflect.ts +2 -1
  153. package/src/root.ts +7 -6
  154. package/src/run-task-worker.ts +2 -1
  155. package/src/runtime.ts +596 -265
  156. package/src/schema.ts +30 -6
  157. package/src/served-world.ts +21 -15
  158. package/src/service-recorder.ts +4 -2
  159. package/src/signals.d.mts +1 -0
  160. package/src/signals.mjs +10 -0
  161. package/src/signals.ts +1 -10
  162. package/src/tail.ts +10 -3
  163. package/src/tcp-port.ts +24 -13
  164. package/src/temporal-backing.mjs +28 -0
  165. package/src/up-cli.ts +6 -3
  166. package/src/up-task-worker.ts +2 -1
  167. package/src/websocket-relay.ts +18 -11
  168. package/src/world-view.ts +2 -2
@@ -61,10 +61,10 @@ function printHelp(): void {
61
61
  # emission. Exit 0 = ready to \`up\`; exit 1 = the honest worklist.
62
62
  # Deterministic: same repo + same pack catalog => identical bytes.
63
63
  # init DIAGNOSES; it never boots — it prints the exact next commands
64
- volter-world up <config> --env-out=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--keep-state] [--root <repo>]
64
+ volter-world up <config> --env-out=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--keep-state] [--json] [--root <repo>]
65
65
  volter-world outdated <world> [--json] — each mounted twin: pinned / mounted / catalog-current version and protocol standing (R17)
66
66
  volter-world audit <world> [--json] — each mounted twin's grade, as the catalog records it (R17)
67
- volter-world down <name> [--grace-ms <ms>] [--purge] [--root <repo>] # SIGTERM, then SIGKILL survivors after the grace (default 5000ms)
67
+ volter-world down <name> [--grace-ms <ms>] [--purge] [--if-instance <token>] [--root <repo>] # SIGTERM, then SIGKILL survivors after the grace (default 5000ms)
68
68
  # --purge: after teardown, delete the instance dir (and per-service twin data) too — see docs/concepts/data-and-keys.md
69
69
  volter-world status <name> [--json] [--root <repo>]
70
70
  volter-world consumers retire <world> [--consumer <id>] [--root <repo>] [--json] # resolve attachments whose runner, command and process group are gone
@@ -98,14 +98,15 @@ function printHelp(): void {
98
98
  # fails loudly with the registration recipe. A world that boots
99
99
  # the app itself declares an "app" service (type "process") —
100
100
  # its URL answers without any registration
101
- volter-world log <name> [service...] [--no-follow] [--json] [--requests] [--root <repo>] # the log of changes (alias: tail)
101
+ volter-world log <name> [service...] [--no-follow] [--json] [--requests] [--logs] [--root <repo>] # the log of changes (alias: tail)
102
102
  # merged, occurredAt-ordered feed of the twins' action ledgers — one
103
103
  # \`HH:MM:SS.mmm <service> <operation> <subjectId>\` line per action;
104
104
  # follows until Ctrl-C (--no-follow: dump and exit; --json: raw JSONL;
105
105
  # --requests: also merge the request journals — reads/404s,
106
106
  # plus WHICH credential arrived in WHICH header/query param, named +
107
107
  # fingerprinted, never the value — kept by every twin and link
108
- # unless VOLTER_TWIN_REQUEST_JOURNAL=0)
108
+ # unless VOLTER_TWIN_REQUEST_JOURNAL=0; --logs: also read the services'
109
+ # process output verbatim, labeled stdout — not shape-only journals)
109
110
  volter-world seed <world> [--entry <file>] [--cwd <dir>] [--json] [--root <repo>]
110
111
  # pull from the placeholder remote: the seed entry runs with every twin's
111
112
  # window open, so the defaults land as observed state (diff empty, nothing pending)
@@ -266,9 +267,16 @@ export async function main(): Promise<void> {
266
267
  const graceArg = optionValue(rest, '--grace-ms');
267
268
  if (graceArg && !(Number(graceArg) >= 0)) throw new Error(`volter-world down: invalid --grace-ms "${graceArg}" (want a non-negative number)`);
268
269
  const purge = rest.includes('--purge');
270
+ if (rest.some(arg => arg === '--if-created-at' || arg.startsWith('--if-created-at='))) throw new Error('volter-world down: use --if-instance with the token returned by your own boot');
271
+ // absent flag: no ownership expectation (optionValue's '' fallback would read as an empty, refused token)
272
+ const ifInstance = rest.some(arg => arg === '--if-instance' || arg.startsWith('--if-instance='));
273
+ const expectedInstance = ifInstance ? optionValue(rest, '--if-instance') : undefined;
274
+ if (ifInstance
275
+ && (!expectedInstance || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(expectedInstance))) throw new Error('volter-world down: invalid --if-instance token');
269
276
  const result = await downWorld(subject, root, {
270
277
  ...(graceArg ? { graceMs: Number(graceArg) } : {}),
271
278
  ...(purge ? { purge: true } : {}),
279
+ ...(expectedInstance === undefined ? {} : { expectedInstance }),
272
280
  });
273
281
  process.stdout.write(result.externalErrors.length ? `World ${result.name}: teardown incomplete; lifecycle evidence retained\n`
274
282
  : `Stopped world ${result.name}${result.stopped.length ? `: ${result.stopped.join(', ')}` : ''}\n`);
@@ -278,6 +286,7 @@ export async function main(): Promise<void> {
278
286
  for (const message of result.externalErrors) process.stderr.write(`External teardown failed: ${message}\n`);
279
287
  }
280
288
  if (result.purged) {
289
+ for (const message of result.externalAbandoned ?? []) process.stdout.write(`Surrendered unavailable backend record (external stop unverified): ${message}\n`);
281
290
  process.stdout.write(`Purged instance data: ${result.purged}\n`);
282
291
  } else if (purge && result.externalErrors.length) {
283
292
  process.stdout.write(`Refused to purge instance data: an external service's teardown failed (see above) — its down command/discovered env must survive.\n`);
@@ -587,6 +596,7 @@ export async function main(): Promise<void> {
587
596
  follow: !rest.includes('--no-follow'),
588
597
  json: rest.includes('--json'),
589
598
  requests: rest.includes('--requests'),
599
+ logs: rest.includes('--logs'),
590
600
  signal: controller.signal,
591
601
  });
592
602
  return;
@@ -594,10 +604,12 @@ export async function main(): Promise<void> {
594
604
 
595
605
  if (cmd === 'seed' || cmd === 'reset') {
596
606
  if (!subject) throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world ${cmd} <world> [--entry <file>] [--cwd <dir>])`);
607
+ if (rest.some(arg => arg === '--env-file' || arg.startsWith('--env-file='))) throw new Error('--env-file is not an input for this command. Reset accepts --env-out=<new-output-path>.');
608
+ const envFile = optionValue(rest, '--env-out') || undefined;
597
609
  const entry = optionValue(rest, '--entry') || undefined;
598
610
  const cwd = optionValue(rest, '--cwd') || undefined;
599
611
  const json = rest.includes('--json');
600
- const outcome = cmd === 'seed' ? await seedWorld(subject, { root, ...(entry ? { entry } : {}), ...(cwd ? { cwd } : {}) }) : await resetWorld(subject, { root, ...(entry ? { entry } : {}), ...(cwd ? { cwd } : {}) });
612
+ const outcome = cmd === 'seed' ? await seedWorld(subject, { root, ...(entry ? { entry } : {}), ...(cwd ? { cwd } : {}) }) : await resetWorld(subject, { root, ...(envFile ? { envFile } : {}), ...(entry ? { entry } : {}), ...(cwd ? { cwd } : {}) });
601
613
  if (json) process.stdout.write(`${JSON.stringify({ world: outcome.world, entry: outcome.entry, exitCode: outcome.exitCode, observed: outcome.observed }, null, 2)}\n`);
602
614
  else process.stdout.write(`${cmd === 'reset' ? 'Reset' : 'Seeded'} world ${subject} from the placeholder remote (${outcome.entry})\n${Object.entries(outcome.observed).map(([s, n]) => ` ${s}: ${n} observed`).join('\n')}\n${outcome.exitCode === 0 ? '' : ` seed entry exited ${outcome.exitCode}\n`}`);
603
615
  process.exitCode = outcome.exitCode === 0 ? 0 : 1;
@@ -610,14 +622,14 @@ export async function main(): Promise<void> {
610
622
  if (!subject || !name) throw new Error('volter-world branch: usage: volter-world branch <world> <name> [--env-out=<path>]');
611
623
  const envFile = optionValue(rest, '--env-out') || undefined;
612
624
  const { instance, forked } = await branchWorld(subject, name, { root, forceEnvOutput: rest.includes('--force'), ...(envFile ? { envFile } : {}) });
613
- if (rest.includes('--json')) process.stdout.write(`${JSON.stringify({ name: instance.name, base: subject, forked }, null, 2)}\n`);
625
+ if (rest.includes('--json')) process.stdout.write(`${JSON.stringify({ name: instance.name, instance: instance.instance, createdAt: instance.createdAt, base: subject, forked }, null, 2)}\n`);
614
626
  else { process.stdout.write(`Branched world ${name} from ${subject}\n${Object.entries(forked).map(([s, states]) => ` ${s}: mirror forked (${states.join(', ')})`).join('\n')}\n`); printReady(instance); }
615
627
  return;
616
628
  }
617
629
  if (cmd === 'checkout') {
618
630
  if (!subject) throw new Error('volter-world checkout: missing world name');
619
631
  const instance = await checkoutWorld(subject, { root });
620
- if (rest.includes('--json')) process.stdout.write(`${JSON.stringify({ name: instance.name }, null, 2)}\n`); else printReady(instance);
632
+ if (rest.includes('--json')) process.stdout.write(`${JSON.stringify({ name: instance.name, instance: instance.instance, createdAt: instance.createdAt }, null, 2)}\n`); else printReady(instance);
621
633
  return;
622
634
  }
623
635
  if (cmd === 'fetch') {
@@ -975,7 +987,7 @@ export async function main(): Promise<void> {
975
987
  if (!envFile) throw new Error('volter-world run: missing required --env-out=<path>');
976
988
  const name = optionValue(worldArgs, '--name') || undefined;
977
989
  const mode = optionValue(worldArgs, '--mode', 'local') as 'local' | 'share' | 'sealed';
978
- const result = await runWorld(subject, rest.slice(split + 1), { root, name, mode, envFile, forceEnvOutput: worldArgs.includes('--force'), owner: ownerOption(worldArgs), keep: worldArgs.includes('--keep'), verbose: worldArgs.includes('--verbose') });
990
+ const result = await runWorld(subject, rest.slice(split + 1), { root, cwd: process.cwd(), name, mode, envFile, forceEnvOutput: worldArgs.includes('--force'), owner: ownerOption(worldArgs), keep: worldArgs.includes('--keep'), verbose: worldArgs.includes('--verbose') });
979
991
  if (result.exitCode !== 0) {
980
992
  const detail = result.outcome.error ? `could not start (${result.outcome.error})`
981
993
  : result.outcome.signal ? `was terminated by ${result.outcome.signal}`
@@ -1027,7 +1039,7 @@ export async function main(): Promise<void> {
1027
1039
  if (cmd === 'unshare') {
1028
1040
  if (!subject) throw new Error('volter-world unshare: missing world name');
1029
1041
  const service = optionValue(rest, '--service') || undefined;
1030
- const result = unshareWorld(subject, { root, service });
1042
+ const result = await unshareWorld(subject, { root, service });
1031
1043
  process.stdout.write(`Unshared world ${subject}${service ? `/${service}` : ''}\n`);
1032
1044
  if (result.escalated.length) process.stdout.write(`Escalated to SIGKILL (tunnel ignored SIGTERM past the grace): ${result.escalated.join(', ')}\n`);
1033
1045
  return;
package/src/cli.ts CHANGED
@@ -2,12 +2,13 @@
2
2
  // volter-world: each command loads only what it needs. `up` (./up-cli.ts) starts the World's owner process
3
3
  // before any other command's modules load; every other command is ./cli-commands.ts.
4
4
  import { optionValue } from './cli-args.ts';
5
+ import { errorMessage } from './error-message.ts';
5
6
 
6
7
  const [cmd, subject, ...rest] = process.argv.slice(2);
7
8
  const run = cmd === 'up' && subject !== '--help' && subject !== '-h'
8
9
  ? import('./up-cli.ts').then(({ upCommand }) => upCommand(subject, rest, optionValue(rest, '--root', process.cwd())))
9
10
  : import('./cli-commands.ts').then(({ main }) => main());
10
11
  run.catch((error) => {
11
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
12
+ process.stderr.write(`${errorMessage(error)}\n`);
12
13
  process.exit(1);
13
14
  });
@@ -0,0 +1,12 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ import type { SpawnIdentity } from './process-receipt.js';
3
+ export const COMMAND_GRACE_MS: number;
4
+ export const COMMAND_CONFIRM_MS: number;
5
+ export const commandProcessGroup: boolean;
6
+ export class CommandRetirementError extends Error {}
7
+ export function commandLifetime(child: ChildProcess, abort?: AbortSignal, preserveOnSuccess?: boolean): {
8
+ identity: Promise<SpawnIdentity | undefined>;
9
+ cancel(signal: NodeJS.Signals): void;
10
+ readonly forwarded: NodeJS.Signals | undefined;
11
+ finish(): Promise<{ code: number | null; signal: NodeJS.Signals | null }>;
12
+ };
@@ -0,0 +1,90 @@
1
+ import { offSignal } from './signals.mjs';
2
+ import { survivingOwnedGroups } from './process-groups.mjs';
3
+ import { childIdentity, signalOwnedProcess, signalChild } from './process-signal.mjs';
4
+
5
+ export const COMMAND_GRACE_MS = 5_000;
6
+ export const COMMAND_CONFIRM_MS = 1_000;
7
+ export const commandProcessGroup = process.platform !== 'win32';
8
+ export class CommandRetirementError extends Error {}
9
+
10
+ /** The child owns a private POSIX group. A bounded retirement requires both closed output and
11
+ * a vanished group. Unknown retirement keeps the caller's ownership record and reservation. */
12
+ export function commandLifetime(child, abort, preserveOnSuccess = false) {
13
+ const identity = child.pid && commandProcessGroup ? childIdentity(child) : Promise.resolve(undefined);
14
+ let signalFailure;
15
+ let pendingSignals = 0;
16
+ let forwarded;
17
+ let code = null;
18
+ let exitSignal = null;
19
+ let exited = false;
20
+ let closed = false;
21
+ let deadline = 0;
22
+ let escalated = false;
23
+ let timer;
24
+ let resolveFinished;
25
+ let rejectFinished;
26
+ const finished = new Promise((resolve, reject) => { resolveFinished = resolve; rejectFinished = reject; });
27
+ finished.catch(() => {});
28
+ const signal = (value) => {
29
+ if (!child.pid) return;
30
+ try {
31
+ if (signalChild(child, value, commandProcessGroup)) return;
32
+ if (!commandProcessGroup || !alive()) return;
33
+ // The leader has exited: this is now retained numeric group ownership.
34
+ pendingSignals++;
35
+ void identity.then(birth => signalOwnedProcess(birth, value, true))
36
+ .catch(error => { signalFailure = error; })
37
+ .finally(() => { pendingSignals--; check(); });
38
+ } catch (error) { signalFailure = error; }
39
+ };
40
+ const alive = () => {
41
+ if (!child.pid) return false;
42
+ return survivingOwnedGroups([child.pid]).length > 0;
43
+ };
44
+ const detach = () => {
45
+ if (timer) clearInterval(timer);
46
+ offSignal('SIGINT', onInt);
47
+ offSignal('SIGTERM', onTerm);
48
+ abort?.removeEventListener('abort', onAbort);
49
+ };
50
+ const check = () => {
51
+ if (pendingSignals) return;
52
+ if (exited && closed && ((preserveOnSuccess && code === 0 && !forwarded) || !alive())) {
53
+ detach(); resolveFinished(); return;
54
+ }
55
+ if (deadline && Date.now() >= deadline) {
56
+ if (!escalated) { escalated = true; signal('SIGKILL'); deadline = Date.now() + COMMAND_CONFIRM_MS; }
57
+ else {
58
+ detach();
59
+ child.stdout?.destroy(); child.stderr?.destroy();
60
+ rejectFinished(new CommandRetirementError('Command retirement could not be confirmed; ownership and cleanup evidence retained', { cause: signalFailure }));
61
+ }
62
+ }
63
+ };
64
+ const watch = () => {
65
+ if (!deadline) deadline = Date.now() + COMMAND_GRACE_MS;
66
+ timer ??= setInterval(check, 20);
67
+ check();
68
+ };
69
+ const cancel = (value) => { forwarded ??= value; signal(value); watch(); };
70
+ const onInt = () => cancel('SIGINT');
71
+ const onTerm = () => cancel('SIGTERM');
72
+ const onAbort = () => cancel(abort?.reason === 'SIGINT' ? 'SIGINT' : 'SIGTERM');
73
+ process.on('SIGINT', onInt);
74
+ process.on('SIGTERM', onTerm);
75
+ abort?.addEventListener('abort', onAbort, { once: true });
76
+ if (abort?.aborted) onAbort();
77
+ child.once('exit', (value, signalValue) => {
78
+ code = value; exitSignal = signalValue; exited = true;
79
+ if (!(preserveOnSuccess && code === 0 && !forwarded)) signal('SIGTERM');
80
+ watch();
81
+ });
82
+ child.once('error', () => { if (!child.pid) { exited = true; watch(); } });
83
+ child.once('close', () => { closed = true; check(); });
84
+ return {
85
+ identity,
86
+ cancel,
87
+ get forwarded() { return forwarded; },
88
+ async finish() { await finished; await identity; return { code, signal: exitSignal }; },
89
+ };
90
+ }
@@ -1,79 +1 @@
1
- import { offSignal } from './signals.ts';
2
- import type { ChildProcess } from 'node:child_process';
3
- import { survivingOwnedGroups } from './process-groups.ts';
4
-
5
- export const COMMAND_GRACE_MS = 5_000;
6
- export const commandProcessGroup = process.platform !== 'win32';
7
- export class CommandRetirementError extends Error {}
8
-
9
- /** The child owns a private POSIX group. A bounded retirement requires both closed output and
10
- * a vanished group. Unknown retirement keeps the caller's ownership record and reservation. */
11
- export function commandLifetime(child: ChildProcess, abort?: AbortSignal, preserveOnSuccess = false) {
12
- let forwarded: NodeJS.Signals | undefined;
13
- let code: number | null = null;
14
- let exitSignal: NodeJS.Signals | null = null;
15
- let exited = false;
16
- let closed = false;
17
- let deadline = 0;
18
- let escalated = false;
19
- let timer: ReturnType<typeof setInterval> | undefined;
20
- let resolveFinished!: () => void;
21
- let rejectFinished!: (error: Error) => void;
22
- const finished = new Promise<void>((resolve, reject) => { resolveFinished = resolve; rejectFinished = reject; });
23
- finished.catch(() => {});
24
- const signal = (value: NodeJS.Signals): void => {
25
- if (!child.pid) return;
26
- try {
27
- if (commandProcessGroup) process.kill(-child.pid, value);
28
- else child.kill(value);
29
- } catch { /* retirement is proved below, never inferred from signal delivery */ }
30
- };
31
- const alive = (): boolean => {
32
- if (!child.pid) return false;
33
- return survivingOwnedGroups([child.pid]).length > 0;
34
- };
35
- const detach = () => {
36
- if (timer) clearInterval(timer);
37
- offSignal('SIGINT', onInt);
38
- offSignal('SIGTERM', onTerm);
39
- abort?.removeEventListener('abort', onAbort);
40
- };
41
- const check = () => {
42
- if (exited && closed && ((preserveOnSuccess && code === 0 && !forwarded) || !alive())) {
43
- detach(); resolveFinished(); return;
44
- }
45
- if (deadline && Date.now() >= deadline) {
46
- if (!escalated) { escalated = true; signal('SIGKILL'); deadline = Date.now() + 1_000; }
47
- else {
48
- detach();
49
- child.stdout?.destroy(); child.stderr?.destroy();
50
- rejectFinished(new CommandRetirementError('Command retirement could not be confirmed; ownership and cleanup evidence retained'));
51
- }
52
- }
53
- };
54
- const watch = () => {
55
- if (!deadline) deadline = Date.now() + COMMAND_GRACE_MS;
56
- timer ??= setInterval(check, 20);
57
- check();
58
- };
59
- const cancel = (value: NodeJS.Signals): void => { forwarded ??= value; signal(value); watch(); };
60
- const onInt = () => cancel('SIGINT');
61
- const onTerm = () => cancel('SIGTERM');
62
- const onAbort = () => cancel(abort?.reason === 'SIGINT' ? 'SIGINT' : 'SIGTERM');
63
- process.on('SIGINT', onInt);
64
- process.on('SIGTERM', onTerm);
65
- abort?.addEventListener('abort', onAbort, { once: true });
66
- if (abort?.aborted) onAbort();
67
- child.once('exit', (value, signalValue) => {
68
- code = value; exitSignal = signalValue; exited = true;
69
- if (!(preserveOnSuccess && code === 0 && !forwarded)) signal('SIGTERM');
70
- watch();
71
- });
72
- child.once('error', () => { if (!child.pid) { exited = true; watch(); } });
73
- child.once('close', () => { closed = true; check(); });
74
- return {
75
- cancel,
76
- get forwarded() { return forwarded; },
77
- async finish() { await finished; return { code, signal: exitSignal }; },
78
- };
79
- }
1
+ export { COMMAND_GRACE_MS, COMMAND_CONFIRM_MS, commandProcessGroup, CommandRetirementError, commandLifetime } from './command-lifetime.mjs';
package/src/configs.ts CHANGED
@@ -33,7 +33,7 @@ export function writeWorldConfig(path: string, config: WorldConfig): void {
33
33
  }
34
34
 
35
35
  const LEGACY_TOP_LEVEL = new Set(['id', 'description', 'bare', 'remotes', 'resources', 'isolation', 'env', 'stripEnv', 'services', 'share', 'actors', 'fixtures', 'catalog', 'network']);
36
- const LEGACY_SERVICE = new Set(['id', 'type', 'package', 'command', 'args', 'cwd', 'port', 'portReason', 'env', 'controlPlane', 'injectEnv', 'injectEnvTemplates', 'root', 'rootArg', 'portArg', 'cliRedirect', 'ready', 'preload', 'external', 'colocate', 'version']);
36
+ const LEGACY_SERVICE = new Set(['id', 'type', 'package', 'command', 'args', 'cwd', 'port', 'portReason', 'env', 'controlPlane', 'branchState', 'injectEnv', 'injectEnvTemplates', 'root', 'rootArg', 'portArg', 'cliRedirect', 'ready', 'preload', 'external', 'colocate', 'version']);
37
37
  const LEGACY_NESTED = {
38
38
  bare: new Set(['name']), resources: new Set(['memoryMiB', 'writableStorageMiB']), catalog: new Set(['sha', 'protocol']),
39
39
  root: new Set(['url', 'deploy', 'refresh', 'scope']), refresh: new Set(['every', 'webhook']),
@@ -5,6 +5,7 @@
5
5
  // forwarded to the host — never a twin's path, so no twin-served script ever runs on this origin.
6
6
  // The console opens a World's pages (a vendor's screens) on the Worlds' origin, which it is told.
7
7
 
8
+ import { errorMessage } from './error-message.ts';
8
9
  import { serveHttp, type HttpServer } from '@volter/world-core/runtime';
9
10
 
10
11
  /** What a console offers a host: its shell and assets under `base`, told where the Worlds are served. */
@@ -53,7 +54,7 @@ export async function serveConsoleFor(target: string, opts: { hostname?: string;
53
54
  let mount: ConsoleMount;
54
55
  try { mount = ((await import('@volter/world-console')) as { createConsole: () => ConsoleMount }).createConsole(); }
55
56
  catch (error) {
56
- if (error instanceof Error && /Cannot find (module|package)/.test(error.message)) throw new Error('the console is not installed: add @volter/world-console beside @volter/world');
57
+ if (error instanceof Error && /Cannot find (module|package)/.test(errorMessage(error))) throw new Error('the console is not installed: add @volter/world-console beside @volter/world', { cause: error });
57
58
  throw error;
58
59
  }
59
60
  const hostname = opts.hostname ?? '127.0.0.1';
package/src/covers.ts CHANGED
@@ -22,11 +22,13 @@
22
22
  // unknown-sdk → exit 1 by default (`--allow-unknown` downgrades to a warning:
23
23
  // unknowns still print, but stop failing the proof).
24
24
 
25
+ import { errorMessage } from './error-message.ts';
25
26
  import { existsSync, readFileSync } from 'node:fs';
26
- import { dirname, join, relative, resolve } from 'node:path';
27
+ import { basename, dirname, join, relative, resolve } from 'node:path';
27
28
  import { fileURLToPath } from 'node:url';
28
29
  import { vendorOfPackModule } from '@volter/world-core/runtime';
29
30
  import { loadWorldConfig } from './configs.ts';
31
+ import { VENDOR_SERVER_BACKINGS } from './backings.ts';
30
32
  import { injectableVendorKeys, loadInject, twinUrlVendorFor } from './inject-map.ts';
31
33
  import { connectionProtocol, projectConnections, projectDependencies, projectManifestDirs, projectEnvNames, projectFetchUrls, projectNpmRegistries, PYPI_TWINS, SDK_TWINS } from './project-inspect.ts';
32
34
  import { overlayCoversMaps, packFacts } from './pack-facts.ts';
@@ -340,6 +342,7 @@ export type WorldTwinId = {
340
342
  };
341
343
 
342
344
  type WorldTwinInventory = {
345
+ services: Array<WorldServiceConfig | WorldServiceInstance>;
343
346
  source: 'instance' | 'config';
344
347
  path: string;
345
348
  /** normalized identifier → id record (e.g. 'stripe' → { label: 'stripe (service)', … }) */
@@ -350,15 +353,56 @@ type WorldTwinInventory = {
350
353
  declared: Map<string, WorldTwinId>;
351
354
  };
352
355
 
353
- function declaredTwin(services: Array<WorldServiceConfig | WorldServiceInstance>): Map<string, WorldTwinId> {
356
+ /** Identity comes from the registered wrapper, not an arbitrary process with the vendor's name. */
357
+ function serverBacking(service: WorldServiceConfig | WorldServiceInstance) {
358
+ const backing = VENDOR_SERVER_BACKINGS[service.id];
359
+ if (!backing || service.type !== backing.service.type) return undefined;
360
+ // an instance records the whole argv in `command`; a config splits it into `command` and `args`
361
+ const command = Array.isArray(service.command) ? service.command : [service.command, ...(('args' in service ? service.args : undefined) ?? [])];
362
+ if (!command[0] || basename(command[0]) !== backing.service.command || command[1] !== backing.service.args?.[0]) return undefined;
363
+ return backing;
364
+ }
365
+
366
+ function addBackingIds(
367
+ ids: Map<string, WorldTwinId>,
368
+ services: Array<WorldServiceConfig | WorldServiceInstance>,
369
+ effectiveEnv?: Record<string, string>,
370
+ ): void {
371
+ for (const service of services) {
372
+ const backing = serverBacking(service);
373
+ if (backing) {
374
+ const template = backing.facts.endpointEnv.templates[backing.facts.endpointEnv.name];
375
+ const instance = 'pid' in service;
376
+ const values = instance ? service.env : service.injectEnvTemplates ?? {};
377
+ const endpoint = instance && service.url && service.port ? template?.replaceAll('${host}', new URL(service.url).hostname)
378
+ .replaceAll('${port}', String(service.port)).replaceAll('${url}', service.url) : undefined;
379
+ const routed = template !== undefined && (instance
380
+ ? endpoint !== undefined && Object.entries(values).some(([name, value]) => value === endpoint && effectiveEnv?.[name] === value)
381
+ : service.port !== undefined && Object.values(values).includes(template));
382
+ addId(ids, normalizeId(service.id), `${service.id} (server backing)`, routed);
383
+ }
384
+ // A protocol already proved owned by this service can satisfy its declared native driver.
385
+ // HTTP-only siblings of the same vendor retain their independent interface obligations.
386
+ for (const binding of serviceConnections([service], effectiveEnv).values()) {
387
+ for (const [vendor, facts] of Object.entries(packFacts())) {
388
+ if (facts.transport === 'raw-tcp' && facts.managedService?.scheme === binding.protocol) {
389
+ addId(ids, normalizeId(vendor), `${binding.label}; managed ${facts.managedService.kind} backing`, true);
390
+ }
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ function declaredTwin(services: Array<WorldServiceConfig | WorldServiceInstance>, effectiveEnv?: Record<string, string>): Map<string, WorldTwinId> {
354
397
  const declared = new Map<string, WorldTwinId>();
355
398
  for (const service of services) {
356
- if (service.type !== undefined && service.type !== 'twin') continue;
399
+ if (service.type !== undefined && service.type !== 'twin' && !serverBacking(service)) continue;
357
400
  const config = service as WorldServiceConfig;
358
401
  const identity = config.package?.match(/^@volter\/twin-(.+)$/)?.[1] ?? servicePackNames(config)[0] ?? service.id;
359
402
  const vendor = Object.keys(packFacts()).find((candidate) => worldIdsFor(candidate).includes(normalizeId(identity))) ?? identity;
360
403
  const ids = new Map<string, WorldTwinId>();
361
- addServiceIds(ids, service);
404
+ if (!serverBacking(service)) addServiceIds(ids, service);
405
+ addBackingIds(ids, [service], effectiveEnv);
362
406
  const match = [...worldIdsFor(vendor), vendor].map(normalizeId).map((id) => ids.get(id)).find(Boolean);
363
407
  declared.set(vendor, match ?? { label: `${service.id} (declared twin)`, interceptable: false });
364
408
  }
@@ -373,14 +417,22 @@ function addId(ids: Map<string, WorldTwinId>, key: string, label: string, interc
373
417
  else if (interceptable && !existing.interceptable) existing.interceptable = true;
374
418
  }
375
419
 
376
- function servicePackNames(service: { package?: string; command?: string | string[]; args?: string[]; colocate?: { module?: string } }): string[] {
420
+ function servicePackNames(service: { id?: string; package?: string; command?: string | string[]; args?: string[]; colocate?: { module?: string } }): string[] {
377
421
  const commandParts = [
378
422
  ...(service.package ? [service.package] : []),
379
423
  ...(Array.isArray(service.command) ? service.command : service.command ? [service.command] : []),
380
424
  ...(service.args ?? []),
381
425
  ...(service.colocate?.module ? [service.colocate.module] : []),
382
426
  ];
383
- return commandParts.flatMap((part) => {
427
+ return commandParts.flatMap((argument, index) => {
428
+ // A shared host command is recorded on each service. Only that service's
429
+ // module identifies its pack; its neighbours' modules must not cover it.
430
+ let part = argument;
431
+ if (commandParts[index - 1] === '--spec') {
432
+ const [id, module] = argument.split('|');
433
+ if (id !== service.id || !module) return [];
434
+ part = module;
435
+ }
384
436
  const fromPath = part.match(/(?:^|[/\\])twin-packs-p[23][/\\]([a-z0-9-]+)[/\\]/);
385
437
  const fromBin = part.match(/^world-([a-z0-9-]+)$/);
386
438
  const fromPkg = part.match(/(?:^|[/\\])@volter\/twin-([a-z0-9-]+)(?:[/\\]|$)/);
@@ -519,29 +571,31 @@ export function worldTwinInventory(world: string, root: string): WorldTwinInvent
519
571
  try {
520
572
  const status = statusWorld(world, root, { readOnly: true });
521
573
  const ids = new Map<string, WorldTwinId>();
522
- for (const service of Object.values(status.services)) addServiceIds(ids, service);
574
+ for (const service of Object.values(status.services)) if (!serverBacking(service)) addServiceIds(ids, service);
575
+ addBackingIds(ids, Object.values(status.services), status.env);
523
576
  for (const key of Object.keys(status.env)) {
524
577
  const stem = key.match(/^(.+)_TWIN_URL$/);
525
578
  // a bare env `*_TWIN_URL` var is only a REACHABLE twin when the injector reads it
526
579
  if (stem) addId(ids, normalizeId(stem[1]!), `env ${key}`, twinUrlVendorFor(key) !== null);
527
580
  }
528
- return { source: 'instance', path: `${status.dirs.instance}/instance.json`, ids, connections: serviceConnections(Object.values(status.services), status.env), selection: status.selection, declared: declaredTwin(Object.values(status.services)) };
581
+ return { services: Object.values(status.services), source: 'instance', path: `${status.dirs.instance}/instance.json`, ids, connections: serviceConnections(Object.values(status.services), status.env), selection: status.selection, declared: declaredTwin(Object.values(status.services), status.env) };
529
582
  } catch (error) {
530
- instanceError = error instanceof Error ? error.message : String(error);
583
+ instanceError = errorMessage(error);
531
584
  }
532
585
  try {
533
586
  const { path, config } = loadWorldConfig(world, root);
534
587
  const ids = new Map<string, WorldTwinId>();
535
- for (const service of config.services as WorldServiceConfig[]) addServiceIds(ids, service);
588
+ for (const service of config.services as WorldServiceConfig[]) if (!serverBacking(service)) addServiceIds(ids, service);
589
+ addBackingIds(ids, config.services);
536
590
  const connections = serviceConnections(config.services);
537
591
  // a name the World's env leaves to a twin's credential door (`$issue:<service>`) is that twin's, whatever scheme the
538
592
  // issued value has: the twin that issues it serves it
539
593
  for (const [name, value] of Object.entries(config.env ?? {})) {
540
594
  if (value.startsWith(ISSUE_ENV_PREFIX)) connections.set(name, { protocol: 'issued', label: `${value.slice(ISSUE_ENV_PREFIX.length)} (${name}; issued)` });
541
595
  }
542
- return { source: 'config', path, ids, connections, selection: config.selection, declared: declaredTwin(config.services) };
596
+ return { services: config.services, source: 'config', path, ids, connections, selection: config.selection, declared: declaredTwin(config.services) };
543
597
  } catch (error) {
544
- const configError = error instanceof Error ? error.message : String(error);
598
+ const configError = errorMessage(error);
545
599
  throw new Error(
546
600
  `volter-world covers: world "${world}" not found as an instance or a config.\n` +
547
601
  ` instance: ${instanceError}\n config: ${configError}`,
@@ -614,7 +668,14 @@ function usesOf(signals: Map<string, string[]>): Map<string, Array<{ usage: Worl
614
668
  * directly to fetch in production source. URL attribution reads the injector's VENDOR_HOSTS table
615
669
  * rather than re-encoding host rules here. Insertion order follows the sorted dependency, env-name,
616
670
  * and source lists, so callers that preserve it are deterministic. */
617
- export function detectRepoVendors(repoPath: string): RepoVendorSignals {
671
+ export function detectRepoVendors(repoPath: string, services: Array<WorldServiceConfig | WorldServiceInstance> = []): RepoVendorSignals {
672
+ // Application aliases are names the selected backing actually injects, never global vendor adoption.
673
+ const backingEnvVendors = new Map<string, string>();
674
+ for (const service of services) {
675
+ if (!serverBacking(service)) continue;
676
+ const bindings = 'pid' in service ? service.env : service.injectEnvTemplates;
677
+ for (const name of Object.keys(bindings ?? {})) backingEnvVendors.set(name, service.id);
678
+ }
618
679
  const repo = resolve(repoPath);
619
680
  // the platform's own packages — the twins an app installed, the command, the runtime — are
620
681
  // never a vendor signal: an app that installed @volter/twin-slack did not gain a Slack dependency.
@@ -660,7 +721,7 @@ export function detectRepoVendors(repoPath: string): RepoVendorSignals {
660
721
  detected.set('smtp', [...(detected.get('smtp') ?? []), `env: ${name}`]);
661
722
  continue;
662
723
  }
663
- const vendor = envStemVendor(name, ENV_SUFFIX_BROAD);
724
+ const vendor = backingEnvVendors.get(name) ?? envStemVendor(name, ENV_SUFFIX_BROAD);
664
725
  if (vendor) detected.set(vendor, [...(detected.get(vendor) ?? []), `env: ${name}`]);
665
726
  }
666
727
 
@@ -684,6 +745,9 @@ export function detectRepoVendors(repoPath: string): RepoVendorSignals {
684
745
  ...Object.values(SDK_TWINS).map((entry) => entry.vendor),
685
746
  ...Object.values(ENV_STEM_VENDORS),
686
747
  ...Object.keys(VENDOR_WORLD_IDS),
748
+ // a pack whose vendor is called only by its hosts (no SDK, no credential: Poly Haven's and ambientCG's public
749
+ // catalogs) is known by its descriptor's hosts, or a raw fetch matched to it would be dropped here
750
+ ...Object.entries(packFacts()).filter(([, facts]) => (facts.hosts ?? []).length).map(([vendor]) => vendor),
687
751
  ]) knownVendorById.set(normalizeId(vendor), vendor);
688
752
  const canonicalVendor = (injectorKey: string): string | null => {
689
753
  const key = normalizeId(injectorKey);
@@ -765,7 +829,7 @@ export function detectRepoVendors(repoPath: string): RepoVendorSignals {
765
829
  if (/_TWIN_URL$/.test(name) || /^VOLTER_/.test(name)) continue;
766
830
  if (isSmtpEnvSignal(name)) continue; // now a DETECTED vendor (above), no longer an unknown
767
831
  const stem = envVendorStem(name, ENV_SUFFIX_STRICT);
768
- if (stem === null || envStemVendor(name, ENV_SUFFIX_STRICT) || ENV_STEM_IGNORE.has(stem)) continue;
832
+ if (stem === null || backingEnvVendors.has(name) || envStemVendor(name, ENV_SUFFIX_STRICT) || ENV_STEM_IGNORE.has(stem)) continue;
769
833
  // The registry's notExternal suppressions apply to ENV STEMS too — the census adjudicated
770
834
  // 40 app-internal stems ("names no vendor") that this loop kept flagging because only the
771
835
  // dep-side checks consulted the registry (§9 round two H4, 2026-08-31: 17 of 18 formally
@@ -786,7 +850,7 @@ export function coverWorld(world: string, repoPath: string, options: CoverageOpt
786
850
  const repo = resolve(repoPath);
787
851
  const inventory = worldTwinInventory(world, root);
788
852
 
789
- const { detected, unknown: unknownSources, uses, unknownUses } = detectRepoVendors(repo);
853
+ const { detected, unknown: unknownSources, uses, unknownUses } = detectRepoVendors(repo, inventory.services);
790
854
 
791
855
  // Does ANY injector vendor key exist for this vendor? Decides whether an UNINTERCEPTABLE
792
856
  // finding is acknowledgeable: a key that exists means the twin COULD be intercepted and the
@@ -812,7 +876,14 @@ export function coverWorld(world: string, repoPath: string, options: CoverageOpt
812
876
  // twin (no injector vendor, no app-read endpoint env) is a distinct, failing status.
813
877
  const candidates = worldIdsFor(vendor).filter((id) => inventory.ids.has(id));
814
878
  const matchId = candidates.find((id) => inventory.ids.get(id)!.interceptable) ?? candidates[0];
815
- const match = matchId === undefined ? null : inventory.ids.get(matchId)!;
879
+ const managed = packFacts()[vendor]?.managedService;
880
+ // Only a booted, service-owned endpoint establishes a managed pack's reachable wire.
881
+ const managedBinding = inventory.source === 'instance' && managed
882
+ ? [...inventory.connections.values()].find(binding => binding.protocol === managed.scheme)
883
+ : undefined;
884
+ const match = matchId === undefined
885
+ ? managedBinding ? { label: managedBinding.label, interceptable: true } : null
886
+ : inventory.ids.get(matchId)!;
816
887
  let status: CoverageStatus = match === null ? 'missing' : match.interceptable ? 'covered' : 'uninterceptable';
817
888
  let ackRefused: string | undefined;
818
889
  if (acknowledge[vendor] !== undefined) {
@@ -884,9 +955,16 @@ export function coverWorld(world: string, repoPath: string, options: CoverageOpt
884
955
 
885
956
  for (const requirement of projectConnections(repo, new Set(
886
957
  [...detected.values(), ...unknownSources.values()].flat().filter((source) => source.startsWith('env: ')).map((source) => source.slice(5)),
887
- ))) {
958
+ ), new Map([...inventory.connections].flatMap(([name, binding]) =>
959
+ binding.protocol && !['http', 'issued', 'incompatible'].includes(binding.protocol) ? [[name, binding.protocol] as const] : [])))) {
888
960
  const vendor = `${requirement.protocol ?? 'unknown-connection'}:${requirement.env ?? requirement.sources[0]}`;
889
- if (inventory.selection !== undefined && !worldSelectionIncludes(inventory.selection, vendor, 'application')) {
961
+ const connectionNames = [vendor, ...(requirement.protocol ? [requirement.protocol] : [])];
962
+ const selectorMatches = (selector: WorldSelection['include'][number]) =>
963
+ (selector.usage === undefined || selector.usage === 'application') &&
964
+ (selector.vendor === undefined || connectionNames.includes(selector.vendor));
965
+ const connectionSelected = inventory.selection === undefined ||
966
+ (inventory.selection.include.some(selectorMatches) && !inventory.selection.exclude.some(selectorMatches));
967
+ if (!connectionSelected) {
890
968
  excluded.push(vendor);
891
969
  rows.push({
892
970
  vendor,
@@ -0,0 +1 @@
1
+ export function errorMessage(error: unknown): string;
@@ -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,2 @@
1
+ // The native Node hosts and typed runtime entrypoints use the same renderer.
2
+ export { errorMessage } from './error-message.mjs';
@@ -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>;