@volter/world-runtime 3.0.166 → 3.0.168

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 (82) hide show
  1. package/dist/known-external-services.json +0 -1
  2. package/dist/src/actions-runner.js +53 -14
  3. package/dist/src/attach.d.ts +9 -0
  4. package/dist/src/attach.js +9 -2
  5. package/dist/src/cli-commands.js +19 -3
  6. package/dist/src/console-apart.js +3 -2
  7. package/dist/src/credential-door.d.ts +8 -0
  8. package/dist/src/credential-door.js +54 -0
  9. package/dist/src/fixture-env.js +2 -2
  10. package/dist/src/forwardable-headers.d.ts +1 -0
  11. package/dist/src/forwardable-headers.js +19 -0
  12. package/dist/src/go-mysql-binding.d.ts +2 -0
  13. package/dist/src/go-mysql-binding.js +7 -1
  14. package/dist/src/guest-runner.js +8 -4
  15. package/dist/src/host-ready.d.ts +7 -3
  16. package/dist/src/host-ready.js +16 -3
  17. package/dist/src/index.d.ts +6 -1
  18. package/dist/src/index.js +7 -1
  19. package/dist/src/infra.js +10 -5
  20. package/dist/src/init.d.ts +5 -0
  21. package/dist/src/init.js +157 -23
  22. package/dist/src/managed-api-needs.d.ts +101 -0
  23. package/dist/src/managed-api-needs.js +312 -0
  24. package/dist/src/managed-api-source.d.ts +24 -0
  25. package/dist/src/managed-api-source.js +59 -0
  26. package/dist/src/mongodb-backing.js +33 -58
  27. package/dist/src/mongodb-command.d.ts +21 -0
  28. package/dist/src/mongodb-command.js +317 -0
  29. package/dist/src/pglite-host.mjs +2 -2
  30. package/dist/src/placeholder.d.ts +24 -1
  31. package/dist/src/placeholder.js +105 -13
  32. package/dist/src/process-signal.d.mts +2 -0
  33. package/dist/src/process-signal.mjs +44 -2
  34. package/dist/src/project-inspect.d.ts +1 -1
  35. package/dist/src/project-inspect.js +8 -1
  36. package/dist/src/redirect-proxy.js +4 -19
  37. package/dist/src/reflect.js +2 -2
  38. package/dist/src/runtime.d.ts +5 -1
  39. package/dist/src/runtime.js +311 -59
  40. package/dist/src/schema.d.ts +10 -0
  41. package/dist/src/schema.js +76 -3
  42. package/dist/src/serve.js +24 -4
  43. package/dist/src/served-world.d.ts +2 -1
  44. package/dist/src/served-world.js +6 -4
  45. package/dist/src/service-command.d.ts +7 -3
  46. package/dist/src/service-command.js +53 -22
  47. package/dist/src/service-environment.d.ts +4 -0
  48. package/dist/src/service-environment.js +21 -0
  49. package/dist/src/service-recorder.js +9 -13
  50. package/known-external-services.json +0 -1
  51. package/package.json +5 -5
  52. package/src/actions-runner.ts +38 -10
  53. package/src/attach.ts +15 -2
  54. package/src/cli-commands.ts +18 -3
  55. package/src/console-apart.ts +3 -2
  56. package/src/credential-door.ts +54 -0
  57. package/src/fixture-env.ts +2 -2
  58. package/src/forwardable-headers.ts +15 -0
  59. package/src/go-mysql-binding.ts +8 -1
  60. package/src/guest-runner.ts +8 -4
  61. package/src/host-ready.ts +12 -3
  62. package/src/index.ts +8 -1
  63. package/src/infra.ts +8 -3
  64. package/src/init.ts +145 -23
  65. package/src/managed-api-needs.ts +285 -0
  66. package/src/managed-api-source.ts +67 -0
  67. package/src/mongodb-backing.ts +32 -48
  68. package/src/mongodb-command.ts +238 -0
  69. package/src/pglite-host.mjs +2 -2
  70. package/src/placeholder.ts +90 -13
  71. package/src/process-signal.d.mts +2 -0
  72. package/src/process-signal.mjs +44 -2
  73. package/src/project-inspect.ts +9 -2
  74. package/src/redirect-proxy.ts +4 -15
  75. package/src/reflect.ts +2 -2
  76. package/src/runtime.ts +272 -45
  77. package/src/schema.ts +73 -3
  78. package/src/serve.ts +22 -4
  79. package/src/served-world.ts +7 -5
  80. package/src/service-command.ts +41 -19
  81. package/src/service-environment.ts +21 -0
  82. package/src/service-recorder.ts +9 -9
@@ -1,9 +1,11 @@
1
+ import { isScalarCredentialField, withCurrentCredentialDoor } from "./credential-door.js";
1
2
  import { errorMessage } from "./error-message.js";
3
+ import { recorderEnvironment, recordedApplicationEnvironment } from "./service-environment.js";
2
4
  import { materializeWorldBrowserFiles } from "./browser-files.js";
3
5
  import { claimEnvOutput, publishEnvOutput } from "./env-output.js";
4
6
  import { offSignal } from "./signals.js";
5
7
  import { parallelStartup } from "./startup.js";
6
- import { waitForHostReady } from "./host-ready.js";
8
+ import { startedTwins, waitForHostReady } from "./host-ready.js";
7
9
  import { assertWorldStateRemovable, removeWorldState } from "./ancestry.js";
8
10
  import { packCli, packEntry } from "./catalog.js";
9
11
  import { vendorOfPackModule } from '@volter/world-core/runtime-state';
@@ -32,7 +34,7 @@ import { loadWorldConfig } from "./configs.js";
32
34
  import { createWorldNetworkPolicy, parseWorldNetworkPolicy, normalizeWorldNetwork, intersectWorldNetwork, WORLD_NETWORK_POLICY_ENV } from '@volter/world-core/network-policy';
33
35
  import { overlayPackFactsFile, resolveServicePackage } from "./catalog.js";
34
36
  import { fakeEnvValue } from "./fixture-env.js";
35
- import { APP_URL_ENV, ISSUE_ENV_PREFIX, MINT_ENV } from "./schema.js";
37
+ import { APP_URL_ENV, ISSUE_ENV_PREFIX, MINT_ENV, assertRequiredCredentialBindings } from "./schema.js";
36
38
  import { readAppUrl } from "./app-url.js";
37
39
  import { inertInjectEnvWarnings } from "./inject-map.js";
38
40
  import { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, proxyEnvFor, tearDownCa } from "./proxy-env.js";
@@ -64,16 +66,14 @@ export function worldEventsPath(logs) {
64
66
  * its output straight to `log` (capped, time-marked), its start and end are recorded there and in the World's events
65
67
  * file, and it is handed a directory of its own for the log files it writes itself (VOLTER_WORLD_SERVICE_LOG_DIR).
66
68
  * `ended` settles when the recorder exits, right after the service ends and its exit record is written. */
67
- function spawnRecorded(serviceId, command, args, cwd, env, log, boot) {
69
+ function spawnRecorded(serviceId, command, args, cwd, env, log, boot, required) {
68
70
  const logDir = ownedPath(dirname(log), `${basename(log).replace(/\.log$/, '')}.d`);
71
+ const recorderEnv = recorderEnvironment(env, serviceId, log, logDir, worldEventsPath(dirname(log)));
72
+ // The recorder adds and removes its own names. Validate the same final child environment before opening a log or spawning.
73
+ if (required !== undefined)
74
+ assertRequiredCredentials(required, recordedApplicationEnvironment(recorderEnv), `service ${serviceId}'s recorded application environment`);
69
75
  mkdirSync(logDir, { recursive: true, mode: 0o700 });
70
76
  const out = openPrivateLog(log, 'a'); // the service's output and the recorder's records, appended
71
- const recorderEnv = { ...env, VOLTER_WORLD_SERVICE_ID: serviceId, VOLTER_WORLD_SERVICE_LOG: log,
72
- VOLTER_WORLD_SERVICE_LOG_DIR: logDir, VOLTER_WORLD_EVENTS: worldEventsPath(dirname(log)) };
73
- if (recorderEnv.NODE_OPTIONS !== undefined) {
74
- recorderEnv.VOLTER_WORLD_SERVICE_NODE_OPTIONS = recorderEnv.NODE_OPTIONS;
75
- delete recorderEnv.NODE_OPTIONS;
76
- }
77
77
  const child = spawn(process.execPath, [siblingScript(import.meta.url, 'service-recorder'), command, ...args], { windowsHide: true,
78
78
  cwd,
79
79
  env: recorderEnv,
@@ -563,9 +563,12 @@ function networkOf(name, own, recorded, parentEnv = process.env) {
563
563
  return undefined;
564
564
  return { [WORLD_NETWORK_POLICY_ENV]: JSON.stringify(createWorldNetworkPolicy(name, network)) };
565
565
  }
566
- /** Strip the publication envelope before combining permissions (the config shape admits only these keys). */
566
+ /** Strip the publication envelope before combining permissions (the config shape admits only these keys). Every
567
+ * permission the policy carries stays: a dropped `writes` or `publicReads` would read as the default when combined,
568
+ * and a build World inside a closed one would read the public build origins its parent may not (sealing never widens). */
567
569
  function networkPermission(policy) {
568
- return { egress: policy.egress, phase: policy.phase, passthrough: policy.passthrough, ...(policy.ownLoopback === false ? { ownLoopback: false } : {}) };
570
+ return { egress: policy.egress, ...(policy.writes !== undefined ? { writes: policy.writes } : {}), phase: policy.phase, passthrough: policy.passthrough,
571
+ ...(policy.publicReads !== undefined ? { publicReads: policy.publicReads } : {}), ...(policy.ownLoopback === false ? { ownLoopback: false } : {}) };
569
572
  }
570
573
  /** The boot permission, never a fallback to a mutable config. */
571
574
  export function recordedWorldNetwork(instance) {
@@ -701,7 +704,13 @@ function readWorldBootRecordLocked(name, root) {
701
704
  throw new Error(`World boot record is incomplete or invalid: ${path}\nFrom the app directory, inspect: volter-world doctor ${name} --root .\nComplete teardown with the owning runtime: volter-world down ${name} --root .\nRetain the startup diagnostics and any history. Only a disposable instance with no state to keep may be removed with down --purge; resume never discards it automatically.`);
702
705
  }
703
706
  }
704
- export function admittedEnv(strip, env = process.env) {
707
+ /** What Windows itself needs in every process: without these, Winsock and DLL loading fail, the temp directory falls
708
+ * back to C:\Windows\Temp and tools lose their profile folders. They hold no caller credential, so a strip (even
709
+ * `*`) never removes them on win32. Windows names are case-insensitive. */
710
+ const WINDOWS_BASELINE_ENV = new Set(['systemroot', 'windir', 'systemdrive', 'comspec', 'pathext', 'temp', 'tmp', 'userprofile',
711
+ 'appdata', 'localappdata', 'programdata', 'programfiles', 'programfiles(x86)', 'programw6432', 'commonprogramfiles',
712
+ 'commonprogramfiles(x86)', 'commonprogramw6432', 'homedrive', 'homepath', 'os', 'processor_architecture', 'number_of_processors']);
713
+ export function admittedEnv(strip, env = process.env, platform = process.platform) {
705
714
  const out = {};
706
715
  const rules = (strip ?? []).map((s) => (s.endsWith('*') ? { prefix: s.slice(0, -1) } : { name: s }));
707
716
  for (const [k, v] of Object.entries(env)) {
@@ -711,6 +720,10 @@ export function admittedEnv(strip, env = process.env) {
711
720
  // whichever World happened to launch this operator process.
712
721
  if (k === WORLD_NETWORK_POLICY_ENV)
713
722
  continue;
723
+ if (platform === 'win32' && WINDOWS_BASELINE_ENV.has(k.toLowerCase())) {
724
+ out[k] = v;
725
+ continue;
726
+ }
714
727
  if (rules.some((r) => ('prefix' in r ? k.startsWith(r.prefix) : k === r.name)))
715
728
  continue;
716
729
  out[k] = v;
@@ -758,7 +771,7 @@ function savedStripEnvOf(instanceEnv) {
758
771
  return ['*'];
759
772
  }
760
773
  }
761
- function serviceProcessEnv(config, service, worldEnv, port) {
774
+ function serviceProcessEnv(config, service, worldEnv, port, twin = service.type === 'twin') {
762
775
  const env = {
763
776
  ...admittedEnv(config.stripEnv),
764
777
  ...(config.env ?? {}),
@@ -772,6 +785,13 @@ function serviceProcessEnv(config, service, worldEnv, port) {
772
785
  delete env[key];
773
786
  }
774
787
  dropUnissued(env);
788
+ // NODE_ENV is the application's mode: a twin is the World's own server, run as built (React's production server build
789
+ // crashes Bun 1.3.11 at load, so Twenty's NODE_ENV=production stopped every twin with screens); a service's own env
790
+ // may still set it
791
+ // a twin is a service that runs a pack: declared `type: 'twin'`, or untyped and spawning a pack's module (never an
792
+ // untyped command of the application's)
793
+ if (twin)
794
+ delete env.NODE_ENV;
775
795
  const out = { ...env, ...(service.env ?? {}) };
776
796
  // the application's own origin: the World's own application service is reached where it listens (the URL
777
797
  // `volter-world app-url` answers for it); any other service never reads it, and a command resolves it at attach
@@ -1288,13 +1308,15 @@ export function externalServiceEnv(service, paths, worldEnv, stripEnv) {
1288
1308
  delete env[key];
1289
1309
  return env;
1290
1310
  }
1291
- async function startExternalService(service, paths, worldEnv, stripEnv, boot) {
1311
+ async function startExternalService(service, paths, worldEnv, required, stripEnv, boot) {
1292
1312
  const external = service.external;
1293
1313
  if (!external)
1294
1314
  throw new Error(`External service "${service.id}" is missing its external config`);
1295
1315
  const log = ownedPath(paths.logs, `${service.id}.log`);
1296
1316
  const cwd = service.cwd ? resolve(paths.root, service.cwd) : paths.root;
1297
1317
  const env = externalServiceEnv(service, paths, worldEnv, stripEnv);
1318
+ if (!ownInfrastructureService(service))
1319
+ assertRequiredCredentials(required, env, `external service ${service.id}`);
1298
1320
  // 1. up — start the self-managed stack, STREAMING its output to the service log (no 1MB cap).
1299
1321
  if (!external.up[0] || !commandExists(external.up[0])) {
1300
1322
  throw new Error(`External service "${service.id}": \`${external.up[0] ?? ''}\` not found on PATH (the up command's tool is not installed)`);
@@ -1412,9 +1434,11 @@ async function trackBootingChild(child, boot, birth) {
1412
1434
  boot.identities[child.pid] = identity.start;
1413
1435
  boot.recordPids();
1414
1436
  }
1415
- async function startService(config, service, worldEnv, paths, allocatedPorts, boot) {
1437
+ async function startService(config, service, worldEnv, paths, allocatedPorts, boot, required) {
1438
+ if ((service.type ?? 'twin') !== 'twin' && !ownInfrastructureService(service))
1439
+ assertRequiredCredentials(required, worldEnv, `service ${service.id}`);
1416
1440
  if (service.type === 'external')
1417
- return startExternalService(service, paths, worldEnv, config.stripEnv, boot);
1441
+ return startExternalService(service, paths, worldEnv, required, config.stripEnv, boot);
1418
1442
  if (!service.command)
1419
1443
  throw new Error(`Service "${service.id}" must define command`);
1420
1444
  const port = service.port === undefined || service.port === 'auto' ? await allocatePort(allocatedPorts) : service.port;
@@ -1439,7 +1463,7 @@ async function startService(config, service, worldEnv, paths, allocatedPorts, bo
1439
1463
  if (service.rootArg !== false)
1440
1464
  args.push(service.rootArg ?? '--root', serviceDataDir);
1441
1465
  const log = ownedPath(paths.logs, `${service.id}.log`);
1442
- const env = serviceProcessEnv(config, service, worldEnv, port);
1466
+ const env = serviceProcessEnv(config, service, worldEnv, port, service.type === 'twin' || (service.type === undefined && spawnedModule !== undefined));
1443
1467
  const cwd = service.cwd ? resolve(paths.root, service.cwd) : paths.root;
1444
1468
  // Augment NODE_OPTIONS with declared extra preloads, so a service can add an app-local preload
1445
1469
  // (e.g. a sandbox) WITHOUT replacing the world's injector. Paths resolve to absolute against the
@@ -1452,7 +1476,9 @@ async function startService(config, service, worldEnv, paths, allocatedPorts, bo
1452
1476
  // The service runs under the service recorder, which outlives it to write its end into its own log: `up`
1453
1477
  // returns once the World is ready, so nothing else is left to see a service stop.
1454
1478
  env.VOLTER_WORLD_BOOT_ID = boot.bootId;
1455
- const { child, ended, identity } = spawnRecorded(service.id, service.command, args, cwd, env, log, boot);
1479
+ if ((service.type ?? 'twin') !== 'twin' && !ownInfrastructureService(service))
1480
+ assertRequiredCredentials(required, env, `service ${service.id}'s effective environment`);
1481
+ const { child, ended, identity } = spawnRecorded(service.id, service.command, args, cwd, env, log, boot, (service.type ?? 'twin') === 'twin' ? undefined : required);
1456
1482
  child.once('error', error => boot.fail(error));
1457
1483
  child.unref();
1458
1484
  await trackBootingChild(child, boot, identity);
@@ -1501,7 +1527,8 @@ async function startService(config, service, worldEnv, paths, allocatedPorts, bo
1501
1527
  boot.progress(`${service.id}: port ${port} is its own`);
1502
1528
  // Optional readiness probe beyond TCP (httpUrl/command): a process can bind its port
1503
1529
  // before it is actually serving, so this makes `up` wait until the service truly responds.
1504
- if (service.ready && service.ready !== 'tcp') {
1530
+ // `ready: { timeoutMs }` with neither is TCP readiness with its own budget: the bind wait above has used it.
1531
+ if (service.ready && service.ready !== 'tcp' && (service.ready.command !== undefined || service.ready.httpUrl !== undefined)) {
1505
1532
  const readiness = await untilReadyOrEnded((abort) => awaitReadiness(resolveReadyProbe(service.ready, { host, port, url }), cwd, service.id, env, abort), ended, boot, 'the service', log).catch(async (error) => {
1506
1533
  if (undeadPids([child.pid]).length)
1507
1534
  recordWorldStop([log], 'world.service.stop', 'up (startup failed)', 'SIGTERM');
@@ -1674,25 +1701,110 @@ function takeIssued(env) {
1674
1701
  /** How long a twin's credential door has to answer before its names are left unset (a door answers from the twin's
1675
1702
  * own store, in milliseconds; this bounds a hung twin, not a slow one). */
1676
1703
  const ISSUE_TIMEOUT_MS = 15_000;
1704
+ function requiredCredentialClaims(config) {
1705
+ const required = new Map();
1706
+ for (const service of config.services)
1707
+ for (const name of service.issues?.required ?? [])
1708
+ required.set(name, { service: service.id, value: undefined });
1709
+ return required;
1710
+ }
1711
+ /** Bind names and owners to the actual values retained in instance.env; the hash is not vendor authentication. */
1712
+ function requiredCredentialValuesDigest(required) {
1713
+ const rows = [...required.keys()].sort().map(name => {
1714
+ const claim = required.get(name);
1715
+ if (claim.value === undefined)
1716
+ throw new Error(`${name}: no issued value exists to retain`);
1717
+ return [name, claim.service, claim.value];
1718
+ });
1719
+ return createHash('sha256').update(JSON.stringify(rows)).digest('hex');
1720
+ }
1721
+ /** New boots retain their required owners even if source config later changes; legacy optional records gain no proof. */
1722
+ function recordedRequiredCredentialClaims(instance) {
1723
+ const required = new Map();
1724
+ if (!Object.hasOwn(instance, 'requiredCredentials'))
1725
+ throw new Error('World boot has no retained credential requirement declaration; finish teardown with its owning runtime and boot with this runtime before attachment');
1726
+ const raw = instance.requiredCredentials;
1727
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
1728
+ throw new Error('World required credential record is invalid');
1729
+ const record = raw;
1730
+ if (Object.keys(record).some(key => !['version', 'owners', 'valuesSha256'].includes(key))
1731
+ || !Object.hasOwn(record, 'version') || record.version !== 1 || !Object.hasOwn(record, 'owners') || record.owners === null
1732
+ || typeof record.owners !== 'object' || Array.isArray(record.owners))
1733
+ throw new Error('World required credential record has an unsupported version or shape');
1734
+ if (!instance.env || typeof instance.env !== 'object' || Array.isArray(instance.env)
1735
+ || !instance.services || typeof instance.services !== 'object' || Array.isArray(instance.services))
1736
+ throw new Error('World required credential record lacks its retained environment or services');
1737
+ for (const [name, owner] of Object.entries(record.owners)) {
1738
+ if (!isEnvironmentName(name) || typeof owner !== 'string' || !owner || !Object.hasOwn(instance.services, owner))
1739
+ throw new Error(`World required credential owner is invalid for ${name}`);
1740
+ const service = instance.services[owner];
1741
+ if (!service || service.id !== owner || service.type !== 'twin')
1742
+ throw new Error(`World required credential ${name} names no twin owner`);
1743
+ const value = instance.env[name];
1744
+ if (!Object.hasOwn(instance.env, name) || typeof value !== 'string'
1745
+ || value.startsWith(ISSUE_ENV_PREFIX) || value === APP_URL_ENV)
1746
+ throw new Error(`World required credential ${name} has no retained scalar value`);
1747
+ required.set(name, { service: owner, value });
1748
+ }
1749
+ if (!Object.hasOwn(record, 'valuesSha256')) {
1750
+ if (required.size)
1751
+ throw new Error('World required credential issuance has not completed; refusing attachment');
1752
+ return required;
1753
+ }
1754
+ if (typeof record.valuesSha256 !== 'string' || !/^[0-9a-f]{64}$/.test(record.valuesSha256)
1755
+ || record.valuesSha256 !== requiredCredentialValuesDigest(required))
1756
+ throw new Error('World required credential names, owners or retained values changed; refusing attachment');
1757
+ return required;
1758
+ }
1759
+ /** Capture the validated boot's values before an owned executor applies its own environment layers. */
1760
+ export function worldRequiredCredentialGate(instance) {
1761
+ const required = recordedRequiredCredentialClaims(instance);
1762
+ return (env, boundary) => assertRequiredCredentials(required, env, boundary);
1763
+ }
1764
+ /** These transports do not carry required ownership/value authority; refuse instead of exporting an optional-only rendering. */
1765
+ export function refuseRequiredCredentialTransport(instance, transport, code) {
1766
+ const required = recordedRequiredCredentialClaims(instance);
1767
+ if (required.size) {
1768
+ const error = new Error(`World ${instance.name}: ${transport} does not carry required credential bindings (${[...required.keys()].join(', ')}); use local env attachment`);
1769
+ throw code ? Object.assign(error, { code }) : error;
1770
+ }
1771
+ }
1772
+ /** Check the actual response value, then the environment this boundary will use; never print credential values. */
1773
+ function assertRequiredCredentials(required, env, boundary) {
1774
+ const failures = [];
1775
+ for (const [name, claim] of required) {
1776
+ if (claim.value === undefined)
1777
+ failures.push(new Error(`${name}: ${claim.service} has not issued its required scalar credential`));
1778
+ else if (!Object.hasOwn(env, name) || env[name] !== claim.value)
1779
+ failures.push(new Error(`${name}: its issued value is missing or overridden at ${boundary}`));
1780
+ }
1781
+ if (failures.length)
1782
+ throw new AggregateError(failures, `World required credential gate refused at ${boundary}: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
1783
+ }
1677
1784
  /** A twin's issued credential (its `issues`, copied from its pack's credentialDoor by init): once it is up, the door is
1678
1785
  * asked for one and each pending name of that service is set to it. A twin that answers no credential, or has no
1679
- * door, leaves the names unset and says so, rather than handing the application a fixture the twin would refuse. */
1680
- async function issueCredentials(service, started, env, pending, unissued, controlRoot, times) {
1786
+ * door, leaves optional names unset and says so. Required scalar names fail the boot with their original reasons. */
1787
+ async function issueCredentials(service, started, env, pending, unissued, controlRoot, claims, fresh, times) {
1681
1788
  const direct = new Set([...pending].filter(([, id]) => id === `${service.id}@direct`).map(([name]) => name));
1682
1789
  const wanted = [...pending].filter(([name, id]) => id === service.id || direct.has(name)).map(([name]) => name);
1683
1790
  if (times) {
1684
1791
  times.names = wanted.length;
1685
1792
  }
1793
+ const required = new Set(service.issues?.required ?? []);
1794
+ const failures = [];
1795
+ for (const name of required) {
1796
+ if (!wanted.includes(name) || direct.has(name) || claims.get(name)?.service !== service.id)
1797
+ failures.push(new Error(`${name}: its exact $issue:${service.id} scalar request is absent at issuance`));
1798
+ }
1799
+ if (failures.length)
1800
+ throw new AggregateError(failures, `World service ${service.id}: required credential requests refused: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
1686
1801
  if (wanted.length === 0)
1687
1802
  return;
1688
1803
  for (const name of wanted)
1689
1804
  pending.delete(name);
1690
- // the pack's door as it stands now (a door the pack fixed since the World was made reaches it), else the saved copy
1691
- let door = service.issues;
1692
- try {
1693
- door = packFacts()[service.id]?.credentialDoor ?? door;
1694
- }
1695
- catch { /* no artifact: the saved copy */ }
1805
+ // Revalidate aliases at the actual issuance boundary. A missing current pack door cannot revive a saved issuer.
1806
+ const door = (service.type ?? 'twin') === 'twin'
1807
+ ? withCurrentCredentialDoor(service, packFacts()[service.id]?.credentialDoor).issues : service.issues;
1696
1808
  if (times)
1697
1809
  times.door = performance.now();
1698
1810
  try {
@@ -1700,8 +1812,12 @@ async function issueCredentials(service, started, env, pending, unissued, contro
1700
1812
  throw new Error('its service names no credential door');
1701
1813
  if (!started.url)
1702
1814
  throw new Error('it has no URL to ask');
1703
- // A boot's identity is existing account state, not an application write to deploy.
1704
- const enclosingSeed = placeholderPullActive(service.id, controlRoot);
1815
+ // A boot's identity is existing account state, not an application write to deploy: asked inside the twin's
1816
+ // placeholder-pull window, as the seed is (architecture, the descriptor's credentialDoor), on a fresh World's first
1817
+ // issuance only: a resumed World's door answers what it issued before, and its branch keeps its own writes; a twin
1818
+ // with a root is not seeded, and its door's writes stay its own
1819
+ const seeds = fresh && !service.root;
1820
+ const enclosingSeed = !seeds || placeholderPullActive(service.id, controlRoot);
1705
1821
  if (!enclosingSeed)
1706
1822
  beginPlaceholderPull(service.id, controlRoot);
1707
1823
  let answer;
@@ -1828,7 +1944,12 @@ async function issueCredentials(service, started, env, pending, unissued, contro
1828
1944
  continue;
1829
1945
  }
1830
1946
  const field = spec;
1947
+ if (required.has(name) && (!isScalarCredentialField(field) || answer === null || typeof answer !== 'object'
1948
+ || Array.isArray(answer) || !Object.hasOwn(answer, field)))
1949
+ throw new Error(`answered no own scalar ${field}`);
1831
1950
  const value = answer[field];
1951
+ if (required.has(name) && typeof value === 'string' && (value.startsWith(ISSUE_ENV_PREFIX) || value === APP_URL_ENV))
1952
+ throw new Error(`answered an interpreted environment marker for ${field}`);
1832
1953
  if (typeof value !== 'string')
1833
1954
  throw new Error(`answered no ${field}`);
1834
1955
  if (direct.has(name)) {
@@ -1842,11 +1963,23 @@ async function issueCredentials(service, started, env, pending, unissued, contro
1842
1963
  }
1843
1964
  else
1844
1965
  env[name] = value;
1966
+ if (required.has(name)) {
1967
+ const claim = claims.get(name);
1968
+ if (!claim || claim.service !== service.id)
1969
+ throw new Error('its required credential owner changed');
1970
+ claim.value = value;
1971
+ }
1845
1972
  }
1846
1973
  catch (error) {
1847
- process.stderr.write(`volter-world: ${service.id} did not issue ${name} (${errorMessage(error)}${typeof answer.codex_unavailable === 'string' ? `; the twin says: ${answer.codex_unavailable.slice(0, 300)}` : ''}); left unset\n`);
1974
+ process.stderr.write(`volter-world: ${service.id} did not issue ${name} (${errorMessage(error)}${typeof answer?.codex_unavailable === 'string' ? `; the twin says: ${answer.codex_unavailable.slice(0, 300)}` : ''}); left unset\n`);
1848
1975
  delete env[name];
1849
1976
  unissued.add(name);
1977
+ if (required.has(name)) {
1978
+ const claim = claims.get(name);
1979
+ if (claim)
1980
+ claim.value = undefined;
1981
+ failures.push(new Error(`${name}: ${service.id} did not issue its required credential (${errorMessage(error)})`, { cause: error }));
1982
+ }
1850
1983
  }
1851
1984
  }
1852
1985
  if (started.url) {
@@ -1862,9 +1995,17 @@ async function issueCredentials(service, started, env, pending, unissued, contro
1862
1995
  delete env[name];
1863
1996
  unissued.add(name);
1864
1997
  }
1998
+ for (const name of required) {
1999
+ const claim = claims.get(name);
2000
+ if (claim)
2001
+ claim.value = undefined;
2002
+ failures.push(new Error(`${name}: ${service.id}'s required credential ask failed (${errorMessage(error)})`, { cause: error }));
2003
+ }
1865
2004
  }
1866
2005
  if (times)
1867
2006
  times.filled = performance.now();
2007
+ if (failures.length)
2008
+ throw new AggregateError(failures, `World service ${service.id}: required credential issuance refused: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
1868
2009
  }
1869
2010
  function materializeConfig(config, root) {
1870
2011
  const services = config.services.map((service) => {
@@ -1976,6 +2117,8 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
1976
2117
  if (value === APP_URL_ENV)
1977
2118
  delete env[key];
1978
2119
  dropUnissued(env);
2120
+ // the colocated twins' host runs the World's own servers: the application's NODE_ENV is not theirs (serviceProcessEnv)
2121
+ delete env.NODE_ENV;
1979
2122
  env.VOLTER_WORLD_BOOT_ID = boot.bootId;
1980
2123
  const spawnedAt = Date.now();
1981
2124
  // Under the same exit recorder as a process service, so the host's end (and every twin it holds) is in host.log.
@@ -2014,12 +2157,7 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
2014
2157
  });
2015
2158
  const ready = (async () => {
2016
2159
  const hostTimeoutMs = allocated.reduce((timeout, { service }) => Math.max(timeout, typeof service.ready === 'object' ? service.ready.timeoutMs ?? 15_000 : 15_000), 15_000);
2017
- const hostTimes = await untilReadyOrEnded((abort) => waitForHostReady(readyFile, boot.bootId, new Map(allocated.map(({ service, port }) => [service.id, port])), hostTimeoutMs, abort, () => { try {
2018
- return statSync(log).size;
2019
- }
2020
- catch {
2021
- return 0;
2022
- } }), ended, boot, 'the co-located host', log).catch(async (error) => {
2160
+ const hostTimes = await untilReadyOrEnded((abort) => waitForHostReady(readyFile, boot.bootId, new Map(allocated.map(({ service, port }) => [service.id, port])), hostTimeoutMs, abort, () => startedTwins(log)), ended, boot, 'the co-located host', log).catch(async (error) => {
2023
2161
  if (undeadPids([child.pid]).length)
2024
2162
  recordWorldStop([log], 'world.service.stop', 'up (host startup incomplete)', 'SIGTERM');
2025
2163
  try {
@@ -2144,6 +2282,11 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2144
2282
  if (packFactsPaths.length)
2145
2283
  createRequire(import.meta.url)('@volter/world-core/pack-facts').extendPackFacts(packFactsPaths);
2146
2284
  const loaded = { path: found.path, config: materializeConfig(found.config, root) };
2285
+ // Before any output claim or lifecycle write, every saved alias fits the actual current pack.
2286
+ loaded.config.services = loaded.config.services.map(service => (service.type ?? 'twin') === 'twin'
2287
+ ? withCurrentCredentialDoor(service, packFacts()[service.id]?.credentialDoor) : service);
2288
+ assertRequiredCredentialBindings(loaded.config, found.path);
2289
+ const required = requiredCredentialClaims(loaded.config);
2147
2290
  const name = options.name ?? loaded.config.id;
2148
2291
  const mode = options.mode ?? 'local';
2149
2292
  assertSafeWorldName(name);
@@ -2252,8 +2395,6 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2252
2395
  // Verified teardown is independent of record shape. A fresh boot discards incomplete metadata.
2253
2396
  if (existsSync(instanceFile)) {
2254
2397
  let prior;
2255
- if (options.keepState)
2256
- prior = readWorldBootRecordLocked(name, root);
2257
2398
  const priorRaw = readFileSync(instanceFile, 'utf8');
2258
2399
  const receiptPath = join(instance, 'stopped.json');
2259
2400
  const receipt = existsSync(receiptPath) ? JSON.parse(readFileSync(receiptPath, 'utf8')) : undefined;
@@ -2261,6 +2402,17 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2261
2402
  || !Number.isFinite(Date.parse(receipt.stoppedAt))) {
2262
2403
  throw new Error(`World "${name}" has no verified teardown receipt for its prior instance; use its owning runtime to complete cleanup before replacement`);
2263
2404
  }
2405
+ // a resume reads the record its last boot completed; a boot that failed and was rolled back (the receipt above)
2406
+ // completed none, and the resume writes a complete one over the kept state (architecture, the boot record)
2407
+ let priorIncomplete = false;
2408
+ if (options.keepState) {
2409
+ try {
2410
+ prior = readWorldBootRecordLocked(name, root);
2411
+ }
2412
+ catch {
2413
+ priorIncomplete = true;
2414
+ }
2415
+ }
2264
2416
  if (!prior) {
2265
2417
  try {
2266
2418
  prior = JSON.parse(priorRaw);
@@ -2284,7 +2436,7 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2284
2436
  carriedNetwork = carriedNetwork === undefined ? ceiling : intersectWorldNetwork(carriedNetwork, ceiling);
2285
2437
  }
2286
2438
  catch {
2287
- if (options.keepState)
2439
+ if (options.keepState && !priorIncomplete)
2288
2440
  throw new Error(`World boot record is incomplete or invalid: ${instanceFile}`);
2289
2441
  }
2290
2442
  }
@@ -2409,7 +2561,10 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2409
2561
  const issuing = takeIssued(env);
2410
2562
  const unissued = new Set();
2411
2563
  writeProxyEnv(root, name, env);
2564
+ const requiredCredentials = { version: 1,
2565
+ owners: Object.fromEntries([...required].map(([name, claim]) => [name, claim.service])) };
2412
2566
  const worldInstance = {
2567
+ requiredCredentials,
2413
2568
  name, instance: instanceToken, config: loaded.config.id, configPath: loaded.path, root, createdAt, mode,
2414
2569
  dirs: { instance, logs, data }, services, env, envFile, pidsFile,
2415
2570
  ...(envOutputClaim.contents === undefined ? {} : { previousEnvOutput: { path: envFile, sha256: createHash('sha256').update(envOutputClaim.contents).digest('hex') } }),
@@ -2492,7 +2647,8 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2492
2647
  // own sent and status says that twin's door.
2493
2648
  const origin = performance.now();
2494
2649
  const steps = [];
2495
- const asks = await Promise.all(host.services.map(async (started) => {
2650
+ // A descriptor refusal still settles every owned ask before rollback can retire its custody.
2651
+ const asks = await Promise.allSettled(host.services.map(async (started) => {
2496
2652
  const declared = colocated.find((s) => s.id === started.id);
2497
2653
  if (!declared)
2498
2654
  return undefined;
@@ -2500,7 +2656,7 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2500
2656
  const times = { ...(declared.package ? { twin: declared.package } : {}) };
2501
2657
  steps.push({ id: started.id, queued: began, times });
2502
2658
  const own = { ...before };
2503
- await issueCredentials(declared, started, own, issuing, unissued, ownedPath(data, started.id), times);
2659
+ await issueCredentials(declared, started, own, issuing, unissued, ownedPath(data, started.id), required, !options.keepState, times);
2504
2660
  return { id: started.id, ms: Math.round(performance.now() - began), own };
2505
2661
  }));
2506
2662
  const at = (time) => time === undefined ? '-' : `+${Math.round(time - origin)}`;
@@ -2510,7 +2666,13 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2510
2666
  .sort((left, right) => (right.times.filled ?? 0) - (left.times.filled ?? 0))
2511
2667
  .map(({ id, queued, times }) => `${id}${times.twin ? ` (${times.twin})` : ''} ${at(queued)}/${at(times.door)}/${at(times.sent)}/${at(times.headers)}/${at(times.answered)}/${at(times.filled)}`).join('; ')}`);
2512
2668
  const asked = [];
2513
- for (const ask of asks) {
2669
+ const failures = [];
2670
+ for (const result of asks) {
2671
+ if (result.status === 'rejected') {
2672
+ failures.push(result.reason);
2673
+ continue;
2674
+ }
2675
+ const ask = result.value;
2514
2676
  if (!ask)
2515
2677
  continue;
2516
2678
  for (const [key, value] of Object.entries(ask.own))
@@ -2523,6 +2685,8 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2523
2685
  }
2524
2686
  if (asked.length)
2525
2687
  boot.progress(`co-located twins: each asked for its credentials at once, slowest first: ${[...asked].sort((left, right) => right.ms - left.ms).slice(0, 6).map((each) => `${each.id} ${each.ms} ms`).join(', ')}; ${asked.length} asked, ${Math.max(...asked.map((each) => each.ms))} ms the longest`);
2688
+ if (failures.length)
2689
+ throw new AggregateError(failures, `World credential issuance refused: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
2526
2690
  writeProxyEnv(root, name, env);
2527
2691
  if (colocated.length)
2528
2692
  boot.progress('co-located twins: credentials issued, proxy environment written');
@@ -2537,7 +2701,7 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2537
2701
  if (services[service.id] || !ownInfrastructureService(service))
2538
2702
  continue;
2539
2703
  boot.progress(`${service.id}: starting beside the host (the runtime's own infrastructure)`);
2540
- const starting = startService(loaded.config, service, { ...env }, { root, instance, logs, data }, allocatedPorts, boot);
2704
+ const starting = startService(loaded.config, service, { ...env }, { root, instance, logs, data }, allocatedPorts, boot, required);
2541
2705
  starting.catch((error) => boot.fail(error instanceof Error ? error : new Error(errorMessage(error))));
2542
2706
  early.set(service.id, starting);
2543
2707
  }
@@ -2586,20 +2750,22 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2586
2750
  boot.signal.throwIfAborted();
2587
2751
  if (!starting)
2588
2752
  boot.progress(`${service.id}: starting (${service.type ?? 'twin'})`);
2589
- const started = await (starting ?? startService(loaded.config, service, env, { root, instance, logs, data }, allocatedPorts, boot));
2753
+ const started = await (starting ?? startService(loaded.config, service, env, { root, instance, logs, data }, allocatedPorts, boot, required));
2590
2754
  // An early start published its teardown when it began; its record moves to its declared position.
2591
2755
  if (starting)
2592
2756
  delete services[service.id];
2593
2757
  recordService(started);
2594
2758
  recordPidsSoFar();
2595
2759
  Object.assign(env, started.env);
2596
- await issueCredentials(service, started, env, issuing, unissued, ownedPath(data, started.id));
2760
+ await issueCredentials(service, started, env, issuing, unissued, ownedPath(data, started.id), required, !options.keepState);
2597
2761
  writeProxyEnv(root, name, env);
2598
2762
  boot.progress(`${service.id}: started, its environment merged`);
2599
2763
  }
2600
2764
  if (colocatedReady)
2601
2765
  await colocatedReady();
2602
- detachBootSignals();
2766
+ // An abort records its reason while the final owned credential asks settle. No later service may remain
2767
+ // to observe it, so completion itself must reject it before publishing a successful environment.
2768
+ boot.signal.throwIfAborted();
2603
2769
  // a name a service of no twin in this World issues (a config edited after init) is left unset, and said
2604
2770
  if (issuing.size > 0)
2605
2771
  process.stderr.write(`volter-world: nothing in this World issues ${[...issuing].map(([n, id]) => `${n} ($issue:${id})`).join(', ')}; left unset\n`);
@@ -2614,6 +2780,7 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2614
2780
  // next service" never fires when there is no next service (TWIN-64) — the single-twin sealed
2615
2781
  // config test below exercises exactly this.
2616
2782
  attemptProxy();
2783
+ assertRequiredCredentials(required, env, 'World environment publication');
2617
2784
  // Inert-injectEnv check (LibreChat blind-adoption finding): an injectEnv like AWS_TWIN_URL
2618
2785
  // lands in the world env, LOOKS wired, and does nothing — the injector reads no such vendor.
2619
2786
  // Warn loudly at `up`, when the operator is watching, instead of letting the disagreement
@@ -2621,6 +2788,12 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2621
2788
  for (const warning of inertInjectEnvWarnings(loaded.config.services)) {
2622
2789
  process.stderr.write(`${warning}\n`);
2623
2790
  }
2791
+ // This can await the browser materializer's import. Keep boot cancellation attached through that work,
2792
+ // and do not persist completed credential claims or publish the environment until it has finished.
2793
+ await materializeWorldBrowserFiles(data, root, name, loaded.config.id);
2794
+ boot.signal.throwIfAborted();
2795
+ assertRequiredCredentials(required, env, 'World environment publication');
2796
+ requiredCredentials.valuesSha256 = requiredCredentialValuesDigest(required);
2624
2797
  mkdirSync(dirname(envFile), { recursive: true });
2625
2798
  // Config env and discovered credentials: a file this writes is owner-only; a file the caller already made keeps
2626
2799
  // the mode the caller chose (a shared env file may be read under another uid).
@@ -2631,11 +2804,12 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2631
2804
  delete worldInstance.previousEnvOutput;
2632
2805
  saveWorldInstance(worldInstance);
2633
2806
  boot.progress(`environment written: ${envFile}`);
2634
- await materializeWorldBrowserFiles(data, root, name, loaded.config.id);
2635
2807
  writePidsFromInstance(worldInstance, Object.values(worldInstance.services).map((service) => service.pid).filter((pid) => pid > 0));
2636
2808
  saveWorldInstance(worldInstance);
2637
2809
  const servicePids = [...new Set(Object.values(services).map((service) => service.pid).filter((pid) => pid > 0))];
2810
+ boot.signal.throwIfAborted();
2638
2811
  handoffWorldLifecycle(root, name, servicePids);
2812
+ detachBootSignals();
2639
2813
  boot.progress(`returning: the World is up; ${lag.account()}`);
2640
2814
  if (mode === 'share') {
2641
2815
  try {
@@ -2895,6 +3069,32 @@ function killHeldSurvivorsSync(child, graceMs) {
2895
3069
  * purge leaves that lock alone; its owner's finally releases it, so waiting claimants never
2896
3070
  * enter through a replacement lock while the original is still held.
2897
3071
  */
3072
+ /** A purged World's env output outside its folder (architecture, "World environment exports are outputs"): removed with
3073
+ * the record that proved it the World's own, when it still holds exactly the World's exports as a single-link regular
3074
+ * file and no other World of the root records that path; anything else stays. */
3075
+ function purgeEnvOutput(root, name, purged, instance) {
3076
+ const path = typeof instance.envFile === 'string' ? resolve(instance.envFile) : undefined;
3077
+ if (!path || !relative(purged, path).startsWith('..'))
3078
+ return;
3079
+ const others = existsSync(worldBaseDir(root)) ? readdirSync(worldBaseDir(root)).filter((world) => world !== name && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(world)) : [];
3080
+ const shared = others.some((world) => {
3081
+ try {
3082
+ return resolve(JSON.parse(readFileSync(join(instanceDir(root, world), 'instance.json'), 'utf8')).envFile) === path;
3083
+ }
3084
+ catch {
3085
+ return false;
3086
+ }
3087
+ });
3088
+ if (shared)
3089
+ return;
3090
+ try {
3091
+ const named = lstatSync(path);
3092
+ if (!named.isFile() || named.nlink !== 1 || !readFileSync(path).equals(Buffer.from(envFileContents(instance.env))))
3093
+ return;
3094
+ rmSync(path);
3095
+ }
3096
+ catch { /* no output, or one the World cannot read: it stays */ }
3097
+ }
2898
3098
  function purgeInstanceDir(root, name) {
2899
3099
  assertSafeWorldName(name);
2900
3100
  const base = worldBaseDir(root);
@@ -3175,6 +3375,8 @@ async function downWorldClaimed(name, root, options, externalErrors = []) {
3175
3375
  const purged = purgeInstanceDir(resolvedRoot, name);
3176
3376
  if (purged && instance)
3177
3377
  clearBrowserFiles(instance.dirs.data, resolvedRoot);
3378
+ if (purged && instance)
3379
+ purgeEnvOutput(resolvedRoot, name, purged, instance);
3178
3380
  return { ...result, ...(externalAbandoned ? { externalAbandoned } : {}), purged };
3179
3381
  }));
3180
3382
  }
@@ -3857,6 +4059,18 @@ export async function doctorWorld(name, options = {}) {
3857
4059
  checks.push({ id: 'startup', ok: missingServices.length === 0, message: missingServices.length
3858
4060
  ? `startup incomplete: no ready service record for ${missingServices.join(', ')}; retained state requires normal teardown before resume`
3859
4061
  : 'declared boot services have instance records' });
4062
+ // a vendor-backed root whose vendor rotated its refresh token while the seal failed (world-core head.ts
4063
+ // unsealedMarkerOf): it renews only inside the process that held the new one, so a person must seal it again
4064
+ const credentialDir = join(root, stateDirName(), 'credentials');
4065
+ for (const file of existsSync(credentialDir) ? readdirSync(credentialDir).filter((f) => f.endsWith('.unsealed.json')).sort() : []) {
4066
+ let said = '';
4067
+ try {
4068
+ const r = JSON.parse(readFileSync(join(credentialDir, file), 'utf8'));
4069
+ said = `: ${r.field ?? 'a field'} rotated at ${r.at ?? '?'} was not sealed (${r.cause ?? 'no cause recorded'})`;
4070
+ }
4071
+ catch { /* the marker's presence is the finding */ }
4072
+ checks.push({ id: `root-${file.replace(/\.unsealed\.json$/, '')}`, ok: false, message: `the root's credential lost a rotation${said}; seal it again before its process ends` });
4073
+ }
3860
4074
  if (instance.lifecycle) {
3861
4075
  checks.push({ id: 'lifecycle', ok: hasWorldLifecycle(root, name) && existsSync(instance.lifecycle.log),
3862
4076
  message: `lifecycle ownership recorded; no capacity reserved; log=${instance.lifecycle.log}` });
@@ -4163,6 +4377,7 @@ export function listWorlds(root = process.cwd()) {
4163
4377
  }
4164
4378
  function worldAttachedCommandEnv(name, root, verbose = false, instance) {
4165
4379
  const status = instance ?? statusWorld(name, root);
4380
+ const required = recordedRequiredCredentialClaims(status);
4166
4381
  const sealed = status.env.VOLTER_WORLD_MODE === 'sealed' || status.env[WORLD_NETWORK_POLICY_ENV] !== undefined;
4167
4382
  const baseEnv = { ...admittedEnv(stripEnvOf(status.env)), ...status.env };
4168
4383
  let proxyEnv = {};
@@ -4182,8 +4397,10 @@ function worldAttachedCommandEnv(name, root, verbose = false, instance) {
4182
4397
  throw new Error(`world "${name}" requires its network policy proxy: attachment proxy failed; refusing to run the command (${errorMessage(error)})`);
4183
4398
  // Keep env-only redirect if the ambient proxy cannot start.
4184
4399
  }
4185
- return { instance: status, env: withAppUrl(name, root, { ...baseEnv, ...proxyEnv, VOLTER_WORLD: name,
4186
- VOLTER_TWIN_INJECT_QUIET: verbose ? '0' : baseEnv.VOLTER_TWIN_INJECT_QUIET ?? '1' }) };
4400
+ const env = withAppUrl(name, root, { ...baseEnv, ...proxyEnv, VOLTER_WORLD: name,
4401
+ VOLTER_TWIN_INJECT_QUIET: verbose ? '0' : baseEnv.VOLTER_TWIN_INJECT_QUIET ?? '1' });
4402
+ assertRequiredCredentials(required, env, 'command attachment');
4403
+ return { instance: status, env, required };
4187
4404
  }
4188
4405
  /** The env with each `$app-url` value made the application URL its booter recorded. Unrecorded, the name is left
4189
4406
  * unset (the application's own env files decide) and the command is told how to record it: never a guess. */
@@ -4210,10 +4427,12 @@ export function runWithWorldEnv(name, command, root = process.cwd(), options = {
4210
4427
  if (command.length === 0)
4211
4428
  throw new Error('Missing command after --');
4212
4429
  const attached = worldAttachedCommandEnv(name, root, options.verbose);
4430
+ const env = { ...attached.env, ...(options.env ?? {}) };
4431
+ assertRequiredCredentials(attached.required, env, 'SDK command environment');
4213
4432
  return new Promise((resolveExit) => {
4214
4433
  const child = spawn(command[0], command.slice(1), { windowsHide: true,
4215
4434
  cwd: resolve(options.cwd ?? root),
4216
- env: { ...attached.env, ...(options.env ?? {}) },
4435
+ env,
4217
4436
  stdio: 'inherit',
4218
4437
  });
4219
4438
  const forward = (signal) => {
@@ -4290,6 +4509,8 @@ export async function attachWorld(name, command, root = process.cwd(), options =
4290
4509
  throw new Error(`World "${name}" is not running while attaching: none of its recorded processes is live and its own (recorded pids ${readPids(current.pidsFile).join(', ') || 'none'}; live and its own: none); ${describeWorldLifecycle(resolvedRoot, name, current.createdAt)})`);
4291
4510
  if (current.createdAt !== attached.instance.createdAt)
4292
4511
  throw new Error(`World "${name}" changed while attaching: its instance was created ${attached.instance.createdAt} when the command's environment was read and ${current.createdAt} now`);
4512
+ // A supplied instance is a snapshot, not authority to erase the current boot's required bindings.
4513
+ assertRequiredCredentials(recordedRequiredCredentialClaims(current), attached.env, 'registered command attachment');
4293
4514
  // Attaching to a World whose services died is allowed (a command may not need them), but never silent.
4294
4515
  for (const [id, state] of Object.entries(current.serviceStates)) {
4295
4516
  if (state === 'running' || state === 'external')
@@ -4472,14 +4693,21 @@ async function runWithWorldEnvLogged(name, command, root, options = {}) {
4472
4693
  * TLS-proxy env (HTTPS_PROXY + the CA-trust vars) is layered in by `shellWorld` once the proxy is up,
4473
4694
  * since starting a server is async; this pure helper stays synchronous + side-effect-free for tests. */
4474
4695
  export function worldShellEnv(name, root = process.cwd()) {
4696
+ const { shell, cwd, env } = worldShellContext(name, root);
4697
+ return { shell, cwd, env };
4698
+ }
4699
+ /** Keep the boot's required proof with the shell environment until its proxy transformations have finished. */
4700
+ function worldShellContext(name, root) {
4475
4701
  const status = statusWorld(name, root);
4476
4702
  if (!status.running)
4477
4703
  throw new Error(`World "${name}" is not running — run: volter-world up <config> --env-out <path> --name ${name}`);
4478
- return {
4479
- shell: status.env.SHELL || admittedEnv(stripEnvOf(status.env)).SHELL || '/bin/bash',
4480
- cwd: resolve(root),
4481
- env: withAppUrl(name, root, { ...admittedEnv(stripEnvOf(status.env)), ...status.env, VOLTER_WORLD: name }),
4482
- };
4704
+ const required = recordedRequiredCredentialClaims(status);
4705
+ const env = withAppUrl(name, root, { ...admittedEnv(stripEnvOf(status.env)), ...status.env, VOLTER_WORLD: name });
4706
+ assertRequiredCredentials(required, env, 'shell environment');
4707
+ // Windows has no /bin/bash: its shell is the one ComSpec names (cmd.exe), kept by the Windows baseline (admittedEnv).
4708
+ const inherited = admittedEnv(stripEnvOf(status.env));
4709
+ const fallback = process.platform === 'win32' ? (inherited.ComSpec ?? inherited.COMSPEC ?? process.env.ComSpec ?? 'cmd.exe') : '/bin/bash';
4710
+ return { shell: status.env.SHELL || inherited.SHELL || fallback, cwd: resolve(root), env, required };
4483
4711
  }
4484
4712
  /** Drop into an interactive subshell with the world active — the contained sibling of `activate`
4485
4713
  * (which wires your CURRENT shell + sets the `(world:<name>)` prompt marker). The subshell gets the
@@ -4488,7 +4716,7 @@ export function worldShellEnv(name, root = process.cwd()) {
4488
4716
  * land in the twins; `exit` restores normal (the proxy + its session CA are torn down). The CA is
4489
4717
  * trusted ONLY via this subshell's env — never installed system-wide. See docs/guides/route-a-cli-through-the-world.md. */
4490
4718
  export async function shellWorld(name, root = process.cwd()) {
4491
- const { shell, cwd, env } = worldShellEnv(name, root);
4719
+ const { shell, cwd, env, required } = worldShellContext(name, root);
4492
4720
  const resolvedRoot = resolve(root);
4493
4721
  let proxy = null;
4494
4722
  const sealed = env.VOLTER_WORLD_MODE === 'sealed' || env[WORLD_NETWORK_POLICY_ENV] !== undefined;
@@ -4515,14 +4743,35 @@ export async function shellWorld(name, root = process.cwd()) {
4515
4743
  throw new Error(`world "${name}" requires its network policy proxy: openssl is unavailable; refusing to open a shell`);
4516
4744
  process.stderr.write(`world '${name}' active in a subshell — vendor calls hit the twins (env-only; install openssl for ambient https redirect). type 'exit' to leave.\n`);
4517
4745
  }
4746
+ let failed = false;
4747
+ let failure;
4518
4748
  try {
4749
+ assertRequiredCredentials(required, env, 'interactive shell');
4519
4750
  const result = spawnSync(shell, ['-i'], { windowsHide: true, cwd, env, stdio: 'inherit' });
4520
- return result.status ?? 0;
4751
+ if (result.error)
4752
+ throw result.error;
4753
+ if (result.signal)
4754
+ throw new Error(`World shell ended by ${result.signal}`);
4755
+ if (result.status === null)
4756
+ throw new Error('World shell returned no exit status');
4757
+ return result.status;
4758
+ }
4759
+ catch (error) {
4760
+ failed = true;
4761
+ failure = error;
4762
+ throw error;
4521
4763
  }
4522
4764
  finally {
4523
4765
  if (proxy) {
4524
- await proxy.close();
4525
- tearDownCa(tlsDir(resolvedRoot, name));
4766
+ try {
4767
+ await proxy.close();
4768
+ tearDownCa(tlsDir(resolvedRoot, name));
4769
+ }
4770
+ catch (cleanup) {
4771
+ if (failed)
4772
+ throw new AggregateError([failure, cleanup], `World shell failed (${errorMessage(failure)}); proxy cleanup also failed (${errorMessage(cleanup)})`, { cause: failure });
4773
+ throw cleanup;
4774
+ }
4526
4775
  }
4527
4776
  }
4528
4777
  }
@@ -4844,6 +5093,7 @@ function clearWorldProxyFiles(root, name) {
4844
5093
  */
4845
5094
  export function activateScript(name, root = process.cwd()) {
4846
5095
  const status = statusWorld(name, root);
5096
+ const required = recordedRequiredCredentialClaims(status);
4847
5097
  if (!status.running)
4848
5098
  throw new Error(`World "${name}" is not running — run: volter-world up <config> --env-out <path> --name ${name}`);
4849
5099
  const baseEnv = status.env ?? {};
@@ -4869,6 +5119,8 @@ export function activateScript(name, root = process.cwd()) {
4869
5119
  // openssl missing or daemon failed — keep env-only redirect; never fabricate proxy success.
4870
5120
  }
4871
5121
  env = withAppUrl(name, root, env);
5122
+ // Exports follow the bookkeeping assignments; the final prompt assignment is the one later value change.
5123
+ assertRequiredCredentials(required, { ...env, PS1: `(world:${name}) ${env.PS1 ?? ''}` }, 'shell activation including its prompt');
4872
5124
  const keys = Object.keys(env).sort();
4873
5125
  const lines = [
4874
5126
  `# volter-world activate ${name} — run: eval "$(volter-world activate ${name})"`,