@volter/world-runtime 3.0.166 → 3.0.167

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 +5 -1
  18. package/dist/src/index.js +4 -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 +309 -58
  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 +5 -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 +270 -44
  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);
@@ -1674,25 +1700,110 @@ function takeIssued(env) {
1674
1700
  /** How long a twin's credential door has to answer before its names are left unset (a door answers from the twin's
1675
1701
  * own store, in milliseconds; this bounds a hung twin, not a slow one). */
1676
1702
  const ISSUE_TIMEOUT_MS = 15_000;
1703
+ function requiredCredentialClaims(config) {
1704
+ const required = new Map();
1705
+ for (const service of config.services)
1706
+ for (const name of service.issues?.required ?? [])
1707
+ required.set(name, { service: service.id, value: undefined });
1708
+ return required;
1709
+ }
1710
+ /** Bind names and owners to the actual values retained in instance.env; the hash is not vendor authentication. */
1711
+ function requiredCredentialValuesDigest(required) {
1712
+ const rows = [...required.keys()].sort().map(name => {
1713
+ const claim = required.get(name);
1714
+ if (claim.value === undefined)
1715
+ throw new Error(`${name}: no issued value exists to retain`);
1716
+ return [name, claim.service, claim.value];
1717
+ });
1718
+ return createHash('sha256').update(JSON.stringify(rows)).digest('hex');
1719
+ }
1720
+ /** New boots retain their required owners even if source config later changes; legacy optional records gain no proof. */
1721
+ function recordedRequiredCredentialClaims(instance) {
1722
+ const required = new Map();
1723
+ if (!Object.hasOwn(instance, 'requiredCredentials'))
1724
+ throw new Error('World boot has no retained credential requirement declaration; finish teardown with its owning runtime and boot with this runtime before attachment');
1725
+ const raw = instance.requiredCredentials;
1726
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
1727
+ throw new Error('World required credential record is invalid');
1728
+ const record = raw;
1729
+ if (Object.keys(record).some(key => !['version', 'owners', 'valuesSha256'].includes(key))
1730
+ || !Object.hasOwn(record, 'version') || record.version !== 1 || !Object.hasOwn(record, 'owners') || record.owners === null
1731
+ || typeof record.owners !== 'object' || Array.isArray(record.owners))
1732
+ throw new Error('World required credential record has an unsupported version or shape');
1733
+ if (!instance.env || typeof instance.env !== 'object' || Array.isArray(instance.env)
1734
+ || !instance.services || typeof instance.services !== 'object' || Array.isArray(instance.services))
1735
+ throw new Error('World required credential record lacks its retained environment or services');
1736
+ for (const [name, owner] of Object.entries(record.owners)) {
1737
+ if (!isEnvironmentName(name) || typeof owner !== 'string' || !owner || !Object.hasOwn(instance.services, owner))
1738
+ throw new Error(`World required credential owner is invalid for ${name}`);
1739
+ const service = instance.services[owner];
1740
+ if (!service || service.id !== owner || service.type !== 'twin')
1741
+ throw new Error(`World required credential ${name} names no twin owner`);
1742
+ const value = instance.env[name];
1743
+ if (!Object.hasOwn(instance.env, name) || typeof value !== 'string'
1744
+ || value.startsWith(ISSUE_ENV_PREFIX) || value === APP_URL_ENV)
1745
+ throw new Error(`World required credential ${name} has no retained scalar value`);
1746
+ required.set(name, { service: owner, value });
1747
+ }
1748
+ if (!Object.hasOwn(record, 'valuesSha256')) {
1749
+ if (required.size)
1750
+ throw new Error('World required credential issuance has not completed; refusing attachment');
1751
+ return required;
1752
+ }
1753
+ if (typeof record.valuesSha256 !== 'string' || !/^[0-9a-f]{64}$/.test(record.valuesSha256)
1754
+ || record.valuesSha256 !== requiredCredentialValuesDigest(required))
1755
+ throw new Error('World required credential names, owners or retained values changed; refusing attachment');
1756
+ return required;
1757
+ }
1758
+ /** Capture the validated boot's values before an owned executor applies its own environment layers. */
1759
+ export function worldRequiredCredentialGate(instance) {
1760
+ const required = recordedRequiredCredentialClaims(instance);
1761
+ return (env, boundary) => assertRequiredCredentials(required, env, boundary);
1762
+ }
1763
+ /** These transports do not carry required ownership/value authority; refuse instead of exporting an optional-only rendering. */
1764
+ export function refuseRequiredCredentialTransport(instance, transport, code) {
1765
+ const required = recordedRequiredCredentialClaims(instance);
1766
+ if (required.size) {
1767
+ const error = new Error(`World ${instance.name}: ${transport} does not carry required credential bindings (${[...required.keys()].join(', ')}); use local env attachment`);
1768
+ throw code ? Object.assign(error, { code }) : error;
1769
+ }
1770
+ }
1771
+ /** Check the actual response value, then the environment this boundary will use; never print credential values. */
1772
+ function assertRequiredCredentials(required, env, boundary) {
1773
+ const failures = [];
1774
+ for (const [name, claim] of required) {
1775
+ if (claim.value === undefined)
1776
+ failures.push(new Error(`${name}: ${claim.service} has not issued its required scalar credential`));
1777
+ else if (!Object.hasOwn(env, name) || env[name] !== claim.value)
1778
+ failures.push(new Error(`${name}: its issued value is missing or overridden at ${boundary}`));
1779
+ }
1780
+ if (failures.length)
1781
+ throw new AggregateError(failures, `World required credential gate refused at ${boundary}: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
1782
+ }
1677
1783
  /** A twin's issued credential (its `issues`, copied from its pack's credentialDoor by init): once it is up, the door is
1678
1784
  * 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) {
1785
+ * door, leaves optional names unset and says so. Required scalar names fail the boot with their original reasons. */
1786
+ async function issueCredentials(service, started, env, pending, unissued, controlRoot, claims, fresh, times) {
1681
1787
  const direct = new Set([...pending].filter(([, id]) => id === `${service.id}@direct`).map(([name]) => name));
1682
1788
  const wanted = [...pending].filter(([name, id]) => id === service.id || direct.has(name)).map(([name]) => name);
1683
1789
  if (times) {
1684
1790
  times.names = wanted.length;
1685
1791
  }
1792
+ const required = new Set(service.issues?.required ?? []);
1793
+ const failures = [];
1794
+ for (const name of required) {
1795
+ if (!wanted.includes(name) || direct.has(name) || claims.get(name)?.service !== service.id)
1796
+ failures.push(new Error(`${name}: its exact $issue:${service.id} scalar request is absent at issuance`));
1797
+ }
1798
+ if (failures.length)
1799
+ throw new AggregateError(failures, `World service ${service.id}: required credential requests refused: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
1686
1800
  if (wanted.length === 0)
1687
1801
  return;
1688
1802
  for (const name of wanted)
1689
1803
  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 */ }
1804
+ // Revalidate aliases at the actual issuance boundary. A missing current pack door cannot revive a saved issuer.
1805
+ const door = (service.type ?? 'twin') === 'twin'
1806
+ ? withCurrentCredentialDoor(service, packFacts()[service.id]?.credentialDoor).issues : service.issues;
1696
1807
  if (times)
1697
1808
  times.door = performance.now();
1698
1809
  try {
@@ -1700,8 +1811,12 @@ async function issueCredentials(service, started, env, pending, unissued, contro
1700
1811
  throw new Error('its service names no credential door');
1701
1812
  if (!started.url)
1702
1813
  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);
1814
+ // A boot's identity is existing account state, not an application write to deploy: asked inside the twin's
1815
+ // placeholder-pull window, as the seed is (architecture, the descriptor's credentialDoor), on a fresh World's first
1816
+ // issuance only: a resumed World's door answers what it issued before, and its branch keeps its own writes; a twin
1817
+ // with a root is not seeded, and its door's writes stay its own
1818
+ const seeds = fresh && !service.root;
1819
+ const enclosingSeed = !seeds || placeholderPullActive(service.id, controlRoot);
1705
1820
  if (!enclosingSeed)
1706
1821
  beginPlaceholderPull(service.id, controlRoot);
1707
1822
  let answer;
@@ -1828,7 +1943,12 @@ async function issueCredentials(service, started, env, pending, unissued, contro
1828
1943
  continue;
1829
1944
  }
1830
1945
  const field = spec;
1946
+ if (required.has(name) && (!isScalarCredentialField(field) || answer === null || typeof answer !== 'object'
1947
+ || Array.isArray(answer) || !Object.hasOwn(answer, field)))
1948
+ throw new Error(`answered no own scalar ${field}`);
1831
1949
  const value = answer[field];
1950
+ if (required.has(name) && typeof value === 'string' && (value.startsWith(ISSUE_ENV_PREFIX) || value === APP_URL_ENV))
1951
+ throw new Error(`answered an interpreted environment marker for ${field}`);
1832
1952
  if (typeof value !== 'string')
1833
1953
  throw new Error(`answered no ${field}`);
1834
1954
  if (direct.has(name)) {
@@ -1842,11 +1962,23 @@ async function issueCredentials(service, started, env, pending, unissued, contro
1842
1962
  }
1843
1963
  else
1844
1964
  env[name] = value;
1965
+ if (required.has(name)) {
1966
+ const claim = claims.get(name);
1967
+ if (!claim || claim.service !== service.id)
1968
+ throw new Error('its required credential owner changed');
1969
+ claim.value = value;
1970
+ }
1845
1971
  }
1846
1972
  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`);
1973
+ 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
1974
  delete env[name];
1849
1975
  unissued.add(name);
1976
+ if (required.has(name)) {
1977
+ const claim = claims.get(name);
1978
+ if (claim)
1979
+ claim.value = undefined;
1980
+ failures.push(new Error(`${name}: ${service.id} did not issue its required credential (${errorMessage(error)})`, { cause: error }));
1981
+ }
1850
1982
  }
1851
1983
  }
1852
1984
  if (started.url) {
@@ -1862,9 +1994,17 @@ async function issueCredentials(service, started, env, pending, unissued, contro
1862
1994
  delete env[name];
1863
1995
  unissued.add(name);
1864
1996
  }
1997
+ for (const name of required) {
1998
+ const claim = claims.get(name);
1999
+ if (claim)
2000
+ claim.value = undefined;
2001
+ failures.push(new Error(`${name}: ${service.id}'s required credential ask failed (${errorMessage(error)})`, { cause: error }));
2002
+ }
1865
2003
  }
1866
2004
  if (times)
1867
2005
  times.filled = performance.now();
2006
+ if (failures.length)
2007
+ throw new AggregateError(failures, `World service ${service.id}: required credential issuance refused: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
1868
2008
  }
1869
2009
  function materializeConfig(config, root) {
1870
2010
  const services = config.services.map((service) => {
@@ -1976,6 +2116,8 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
1976
2116
  if (value === APP_URL_ENV)
1977
2117
  delete env[key];
1978
2118
  dropUnissued(env);
2119
+ // the colocated twins' host runs the World's own servers: the application's NODE_ENV is not theirs (serviceProcessEnv)
2120
+ delete env.NODE_ENV;
1979
2121
  env.VOLTER_WORLD_BOOT_ID = boot.bootId;
1980
2122
  const spawnedAt = Date.now();
1981
2123
  // 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 +2156,7 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
2014
2156
  });
2015
2157
  const ready = (async () => {
2016
2158
  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) => {
2159
+ 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
2160
  if (undeadPids([child.pid]).length)
2024
2161
  recordWorldStop([log], 'world.service.stop', 'up (host startup incomplete)', 'SIGTERM');
2025
2162
  try {
@@ -2144,6 +2281,11 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2144
2281
  if (packFactsPaths.length)
2145
2282
  createRequire(import.meta.url)('@volter/world-core/pack-facts').extendPackFacts(packFactsPaths);
2146
2283
  const loaded = { path: found.path, config: materializeConfig(found.config, root) };
2284
+ // Before any output claim or lifecycle write, every saved alias fits the actual current pack.
2285
+ loaded.config.services = loaded.config.services.map(service => (service.type ?? 'twin') === 'twin'
2286
+ ? withCurrentCredentialDoor(service, packFacts()[service.id]?.credentialDoor) : service);
2287
+ assertRequiredCredentialBindings(loaded.config, found.path);
2288
+ const required = requiredCredentialClaims(loaded.config);
2147
2289
  const name = options.name ?? loaded.config.id;
2148
2290
  const mode = options.mode ?? 'local';
2149
2291
  assertSafeWorldName(name);
@@ -2252,8 +2394,6 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2252
2394
  // Verified teardown is independent of record shape. A fresh boot discards incomplete metadata.
2253
2395
  if (existsSync(instanceFile)) {
2254
2396
  let prior;
2255
- if (options.keepState)
2256
- prior = readWorldBootRecordLocked(name, root);
2257
2397
  const priorRaw = readFileSync(instanceFile, 'utf8');
2258
2398
  const receiptPath = join(instance, 'stopped.json');
2259
2399
  const receipt = existsSync(receiptPath) ? JSON.parse(readFileSync(receiptPath, 'utf8')) : undefined;
@@ -2261,6 +2401,17 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2261
2401
  || !Number.isFinite(Date.parse(receipt.stoppedAt))) {
2262
2402
  throw new Error(`World "${name}" has no verified teardown receipt for its prior instance; use its owning runtime to complete cleanup before replacement`);
2263
2403
  }
2404
+ // a resume reads the record its last boot completed; a boot that failed and was rolled back (the receipt above)
2405
+ // completed none, and the resume writes a complete one over the kept state (architecture, the boot record)
2406
+ let priorIncomplete = false;
2407
+ if (options.keepState) {
2408
+ try {
2409
+ prior = readWorldBootRecordLocked(name, root);
2410
+ }
2411
+ catch {
2412
+ priorIncomplete = true;
2413
+ }
2414
+ }
2264
2415
  if (!prior) {
2265
2416
  try {
2266
2417
  prior = JSON.parse(priorRaw);
@@ -2284,7 +2435,7 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2284
2435
  carriedNetwork = carriedNetwork === undefined ? ceiling : intersectWorldNetwork(carriedNetwork, ceiling);
2285
2436
  }
2286
2437
  catch {
2287
- if (options.keepState)
2438
+ if (options.keepState && !priorIncomplete)
2288
2439
  throw new Error(`World boot record is incomplete or invalid: ${instanceFile}`);
2289
2440
  }
2290
2441
  }
@@ -2409,7 +2560,10 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2409
2560
  const issuing = takeIssued(env);
2410
2561
  const unissued = new Set();
2411
2562
  writeProxyEnv(root, name, env);
2563
+ const requiredCredentials = { version: 1,
2564
+ owners: Object.fromEntries([...required].map(([name, claim]) => [name, claim.service])) };
2412
2565
  const worldInstance = {
2566
+ requiredCredentials,
2413
2567
  name, instance: instanceToken, config: loaded.config.id, configPath: loaded.path, root, createdAt, mode,
2414
2568
  dirs: { instance, logs, data }, services, env, envFile, pidsFile,
2415
2569
  ...(envOutputClaim.contents === undefined ? {} : { previousEnvOutput: { path: envFile, sha256: createHash('sha256').update(envOutputClaim.contents).digest('hex') } }),
@@ -2492,7 +2646,8 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2492
2646
  // own sent and status says that twin's door.
2493
2647
  const origin = performance.now();
2494
2648
  const steps = [];
2495
- const asks = await Promise.all(host.services.map(async (started) => {
2649
+ // A descriptor refusal still settles every owned ask before rollback can retire its custody.
2650
+ const asks = await Promise.allSettled(host.services.map(async (started) => {
2496
2651
  const declared = colocated.find((s) => s.id === started.id);
2497
2652
  if (!declared)
2498
2653
  return undefined;
@@ -2500,7 +2655,7 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2500
2655
  const times = { ...(declared.package ? { twin: declared.package } : {}) };
2501
2656
  steps.push({ id: started.id, queued: began, times });
2502
2657
  const own = { ...before };
2503
- await issueCredentials(declared, started, own, issuing, unissued, ownedPath(data, started.id), times);
2658
+ await issueCredentials(declared, started, own, issuing, unissued, ownedPath(data, started.id), required, !options.keepState, times);
2504
2659
  return { id: started.id, ms: Math.round(performance.now() - began), own };
2505
2660
  }));
2506
2661
  const at = (time) => time === undefined ? '-' : `+${Math.round(time - origin)}`;
@@ -2510,7 +2665,13 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2510
2665
  .sort((left, right) => (right.times.filled ?? 0) - (left.times.filled ?? 0))
2511
2666
  .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
2667
  const asked = [];
2513
- for (const ask of asks) {
2668
+ const failures = [];
2669
+ for (const result of asks) {
2670
+ if (result.status === 'rejected') {
2671
+ failures.push(result.reason);
2672
+ continue;
2673
+ }
2674
+ const ask = result.value;
2514
2675
  if (!ask)
2515
2676
  continue;
2516
2677
  for (const [key, value] of Object.entries(ask.own))
@@ -2523,6 +2684,8 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2523
2684
  }
2524
2685
  if (asked.length)
2525
2686
  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`);
2687
+ if (failures.length)
2688
+ throw new AggregateError(failures, `World credential issuance refused: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
2526
2689
  writeProxyEnv(root, name, env);
2527
2690
  if (colocated.length)
2528
2691
  boot.progress('co-located twins: credentials issued, proxy environment written');
@@ -2537,7 +2700,7 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2537
2700
  if (services[service.id] || !ownInfrastructureService(service))
2538
2701
  continue;
2539
2702
  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);
2703
+ const starting = startService(loaded.config, service, { ...env }, { root, instance, logs, data }, allocatedPorts, boot, required);
2541
2704
  starting.catch((error) => boot.fail(error instanceof Error ? error : new Error(errorMessage(error))));
2542
2705
  early.set(service.id, starting);
2543
2706
  }
@@ -2586,20 +2749,22 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2586
2749
  boot.signal.throwIfAborted();
2587
2750
  if (!starting)
2588
2751
  boot.progress(`${service.id}: starting (${service.type ?? 'twin'})`);
2589
- const started = await (starting ?? startService(loaded.config, service, env, { root, instance, logs, data }, allocatedPorts, boot));
2752
+ const started = await (starting ?? startService(loaded.config, service, env, { root, instance, logs, data }, allocatedPorts, boot, required));
2590
2753
  // An early start published its teardown when it began; its record moves to its declared position.
2591
2754
  if (starting)
2592
2755
  delete services[service.id];
2593
2756
  recordService(started);
2594
2757
  recordPidsSoFar();
2595
2758
  Object.assign(env, started.env);
2596
- await issueCredentials(service, started, env, issuing, unissued, ownedPath(data, started.id));
2759
+ await issueCredentials(service, started, env, issuing, unissued, ownedPath(data, started.id), required, !options.keepState);
2597
2760
  writeProxyEnv(root, name, env);
2598
2761
  boot.progress(`${service.id}: started, its environment merged`);
2599
2762
  }
2600
2763
  if (colocatedReady)
2601
2764
  await colocatedReady();
2602
- detachBootSignals();
2765
+ // An abort records its reason while the final owned credential asks settle. No later service may remain
2766
+ // to observe it, so completion itself must reject it before publishing a successful environment.
2767
+ boot.signal.throwIfAborted();
2603
2768
  // a name a service of no twin in this World issues (a config edited after init) is left unset, and said
2604
2769
  if (issuing.size > 0)
2605
2770
  process.stderr.write(`volter-world: nothing in this World issues ${[...issuing].map(([n, id]) => `${n} ($issue:${id})`).join(', ')}; left unset\n`);
@@ -2614,6 +2779,7 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2614
2779
  // next service" never fires when there is no next service (TWIN-64) — the single-twin sealed
2615
2780
  // config test below exercises exactly this.
2616
2781
  attemptProxy();
2782
+ assertRequiredCredentials(required, env, 'World environment publication');
2617
2783
  // Inert-injectEnv check (LibreChat blind-adoption finding): an injectEnv like AWS_TWIN_URL
2618
2784
  // lands in the world env, LOOKS wired, and does nothing — the injector reads no such vendor.
2619
2785
  // Warn loudly at `up`, when the operator is watching, instead of letting the disagreement
@@ -2621,6 +2787,12 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2621
2787
  for (const warning of inertInjectEnvWarnings(loaded.config.services)) {
2622
2788
  process.stderr.write(`${warning}\n`);
2623
2789
  }
2790
+ // This can await the browser materializer's import. Keep boot cancellation attached through that work,
2791
+ // and do not persist completed credential claims or publish the environment until it has finished.
2792
+ await materializeWorldBrowserFiles(data, root, name, loaded.config.id);
2793
+ boot.signal.throwIfAborted();
2794
+ assertRequiredCredentials(required, env, 'World environment publication');
2795
+ requiredCredentials.valuesSha256 = requiredCredentialValuesDigest(required);
2624
2796
  mkdirSync(dirname(envFile), { recursive: true });
2625
2797
  // Config env and discovered credentials: a file this writes is owner-only; a file the caller already made keeps
2626
2798
  // the mode the caller chose (a shared env file may be read under another uid).
@@ -2631,11 +2803,12 @@ async function upWorldRecorded(configId, options, abort, upStarted, lag) {
2631
2803
  delete worldInstance.previousEnvOutput;
2632
2804
  saveWorldInstance(worldInstance);
2633
2805
  boot.progress(`environment written: ${envFile}`);
2634
- await materializeWorldBrowserFiles(data, root, name, loaded.config.id);
2635
2806
  writePidsFromInstance(worldInstance, Object.values(worldInstance.services).map((service) => service.pid).filter((pid) => pid > 0));
2636
2807
  saveWorldInstance(worldInstance);
2637
2808
  const servicePids = [...new Set(Object.values(services).map((service) => service.pid).filter((pid) => pid > 0))];
2809
+ boot.signal.throwIfAborted();
2638
2810
  handoffWorldLifecycle(root, name, servicePids);
2811
+ detachBootSignals();
2639
2812
  boot.progress(`returning: the World is up; ${lag.account()}`);
2640
2813
  if (mode === 'share') {
2641
2814
  try {
@@ -2895,6 +3068,32 @@ function killHeldSurvivorsSync(child, graceMs) {
2895
3068
  * purge leaves that lock alone; its owner's finally releases it, so waiting claimants never
2896
3069
  * enter through a replacement lock while the original is still held.
2897
3070
  */
3071
+ /** A purged World's env output outside its folder (architecture, "World environment exports are outputs"): removed with
3072
+ * the record that proved it the World's own, when it still holds exactly the World's exports as a single-link regular
3073
+ * file and no other World of the root records that path; anything else stays. */
3074
+ function purgeEnvOutput(root, name, purged, instance) {
3075
+ const path = typeof instance.envFile === 'string' ? resolve(instance.envFile) : undefined;
3076
+ if (!path || !relative(purged, path).startsWith('..'))
3077
+ return;
3078
+ const others = existsSync(worldBaseDir(root)) ? readdirSync(worldBaseDir(root)).filter((world) => world !== name && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(world)) : [];
3079
+ const shared = others.some((world) => {
3080
+ try {
3081
+ return resolve(JSON.parse(readFileSync(join(instanceDir(root, world), 'instance.json'), 'utf8')).envFile) === path;
3082
+ }
3083
+ catch {
3084
+ return false;
3085
+ }
3086
+ });
3087
+ if (shared)
3088
+ return;
3089
+ try {
3090
+ const named = lstatSync(path);
3091
+ if (!named.isFile() || named.nlink !== 1 || !readFileSync(path).equals(Buffer.from(envFileContents(instance.env))))
3092
+ return;
3093
+ rmSync(path);
3094
+ }
3095
+ catch { /* no output, or one the World cannot read: it stays */ }
3096
+ }
2898
3097
  function purgeInstanceDir(root, name) {
2899
3098
  assertSafeWorldName(name);
2900
3099
  const base = worldBaseDir(root);
@@ -3175,6 +3374,8 @@ async function downWorldClaimed(name, root, options, externalErrors = []) {
3175
3374
  const purged = purgeInstanceDir(resolvedRoot, name);
3176
3375
  if (purged && instance)
3177
3376
  clearBrowserFiles(instance.dirs.data, resolvedRoot);
3377
+ if (purged && instance)
3378
+ purgeEnvOutput(resolvedRoot, name, purged, instance);
3178
3379
  return { ...result, ...(externalAbandoned ? { externalAbandoned } : {}), purged };
3179
3380
  }));
3180
3381
  }
@@ -3857,6 +4058,18 @@ export async function doctorWorld(name, options = {}) {
3857
4058
  checks.push({ id: 'startup', ok: missingServices.length === 0, message: missingServices.length
3858
4059
  ? `startup incomplete: no ready service record for ${missingServices.join(', ')}; retained state requires normal teardown before resume`
3859
4060
  : 'declared boot services have instance records' });
4061
+ // a vendor-backed root whose vendor rotated its refresh token while the seal failed (world-core head.ts
4062
+ // unsealedMarkerOf): it renews only inside the process that held the new one, so a person must seal it again
4063
+ const credentialDir = join(root, stateDirName(), 'credentials');
4064
+ for (const file of existsSync(credentialDir) ? readdirSync(credentialDir).filter((f) => f.endsWith('.unsealed.json')).sort() : []) {
4065
+ let said = '';
4066
+ try {
4067
+ const r = JSON.parse(readFileSync(join(credentialDir, file), 'utf8'));
4068
+ said = `: ${r.field ?? 'a field'} rotated at ${r.at ?? '?'} was not sealed (${r.cause ?? 'no cause recorded'})`;
4069
+ }
4070
+ catch { /* the marker's presence is the finding */ }
4071
+ 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` });
4072
+ }
3860
4073
  if (instance.lifecycle) {
3861
4074
  checks.push({ id: 'lifecycle', ok: hasWorldLifecycle(root, name) && existsSync(instance.lifecycle.log),
3862
4075
  message: `lifecycle ownership recorded; no capacity reserved; log=${instance.lifecycle.log}` });
@@ -4163,6 +4376,7 @@ export function listWorlds(root = process.cwd()) {
4163
4376
  }
4164
4377
  function worldAttachedCommandEnv(name, root, verbose = false, instance) {
4165
4378
  const status = instance ?? statusWorld(name, root);
4379
+ const required = recordedRequiredCredentialClaims(status);
4166
4380
  const sealed = status.env.VOLTER_WORLD_MODE === 'sealed' || status.env[WORLD_NETWORK_POLICY_ENV] !== undefined;
4167
4381
  const baseEnv = { ...admittedEnv(stripEnvOf(status.env)), ...status.env };
4168
4382
  let proxyEnv = {};
@@ -4182,8 +4396,10 @@ function worldAttachedCommandEnv(name, root, verbose = false, instance) {
4182
4396
  throw new Error(`world "${name}" requires its network policy proxy: attachment proxy failed; refusing to run the command (${errorMessage(error)})`);
4183
4397
  // Keep env-only redirect if the ambient proxy cannot start.
4184
4398
  }
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' }) };
4399
+ const env = withAppUrl(name, root, { ...baseEnv, ...proxyEnv, VOLTER_WORLD: name,
4400
+ VOLTER_TWIN_INJECT_QUIET: verbose ? '0' : baseEnv.VOLTER_TWIN_INJECT_QUIET ?? '1' });
4401
+ assertRequiredCredentials(required, env, 'command attachment');
4402
+ return { instance: status, env, required };
4187
4403
  }
4188
4404
  /** The env with each `$app-url` value made the application URL its booter recorded. Unrecorded, the name is left
4189
4405
  * unset (the application's own env files decide) and the command is told how to record it: never a guess. */
@@ -4210,10 +4426,12 @@ export function runWithWorldEnv(name, command, root = process.cwd(), options = {
4210
4426
  if (command.length === 0)
4211
4427
  throw new Error('Missing command after --');
4212
4428
  const attached = worldAttachedCommandEnv(name, root, options.verbose);
4429
+ const env = { ...attached.env, ...(options.env ?? {}) };
4430
+ assertRequiredCredentials(attached.required, env, 'SDK command environment');
4213
4431
  return new Promise((resolveExit) => {
4214
4432
  const child = spawn(command[0], command.slice(1), { windowsHide: true,
4215
4433
  cwd: resolve(options.cwd ?? root),
4216
- env: { ...attached.env, ...(options.env ?? {}) },
4434
+ env,
4217
4435
  stdio: 'inherit',
4218
4436
  });
4219
4437
  const forward = (signal) => {
@@ -4290,6 +4508,8 @@ export async function attachWorld(name, command, root = process.cwd(), options =
4290
4508
  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
4509
  if (current.createdAt !== attached.instance.createdAt)
4292
4510
  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`);
4511
+ // A supplied instance is a snapshot, not authority to erase the current boot's required bindings.
4512
+ assertRequiredCredentials(recordedRequiredCredentialClaims(current), attached.env, 'registered command attachment');
4293
4513
  // Attaching to a World whose services died is allowed (a command may not need them), but never silent.
4294
4514
  for (const [id, state] of Object.entries(current.serviceStates)) {
4295
4515
  if (state === 'running' || state === 'external')
@@ -4472,14 +4692,21 @@ async function runWithWorldEnvLogged(name, command, root, options = {}) {
4472
4692
  * TLS-proxy env (HTTPS_PROXY + the CA-trust vars) is layered in by `shellWorld` once the proxy is up,
4473
4693
  * since starting a server is async; this pure helper stays synchronous + side-effect-free for tests. */
4474
4694
  export function worldShellEnv(name, root = process.cwd()) {
4695
+ const { shell, cwd, env } = worldShellContext(name, root);
4696
+ return { shell, cwd, env };
4697
+ }
4698
+ /** Keep the boot's required proof with the shell environment until its proxy transformations have finished. */
4699
+ function worldShellContext(name, root) {
4475
4700
  const status = statusWorld(name, root);
4476
4701
  if (!status.running)
4477
4702
  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
- };
4703
+ const required = recordedRequiredCredentialClaims(status);
4704
+ const env = withAppUrl(name, root, { ...admittedEnv(stripEnvOf(status.env)), ...status.env, VOLTER_WORLD: name });
4705
+ assertRequiredCredentials(required, env, 'shell environment');
4706
+ // Windows has no /bin/bash: its shell is the one ComSpec names (cmd.exe), kept by the Windows baseline (admittedEnv).
4707
+ const inherited = admittedEnv(stripEnvOf(status.env));
4708
+ const fallback = process.platform === 'win32' ? (inherited.ComSpec ?? inherited.COMSPEC ?? process.env.ComSpec ?? 'cmd.exe') : '/bin/bash';
4709
+ return { shell: status.env.SHELL || inherited.SHELL || fallback, cwd: resolve(root), env, required };
4483
4710
  }
4484
4711
  /** Drop into an interactive subshell with the world active — the contained sibling of `activate`
4485
4712
  * (which wires your CURRENT shell + sets the `(world:<name>)` prompt marker). The subshell gets the
@@ -4488,7 +4715,7 @@ export function worldShellEnv(name, root = process.cwd()) {
4488
4715
  * land in the twins; `exit` restores normal (the proxy + its session CA are torn down). The CA is
4489
4716
  * trusted ONLY via this subshell's env — never installed system-wide. See docs/guides/route-a-cli-through-the-world.md. */
4490
4717
  export async function shellWorld(name, root = process.cwd()) {
4491
- const { shell, cwd, env } = worldShellEnv(name, root);
4718
+ const { shell, cwd, env, required } = worldShellContext(name, root);
4492
4719
  const resolvedRoot = resolve(root);
4493
4720
  let proxy = null;
4494
4721
  const sealed = env.VOLTER_WORLD_MODE === 'sealed' || env[WORLD_NETWORK_POLICY_ENV] !== undefined;
@@ -4515,14 +4742,35 @@ export async function shellWorld(name, root = process.cwd()) {
4515
4742
  throw new Error(`world "${name}" requires its network policy proxy: openssl is unavailable; refusing to open a shell`);
4516
4743
  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
4744
  }
4745
+ let failed = false;
4746
+ let failure;
4518
4747
  try {
4748
+ assertRequiredCredentials(required, env, 'interactive shell');
4519
4749
  const result = spawnSync(shell, ['-i'], { windowsHide: true, cwd, env, stdio: 'inherit' });
4520
- return result.status ?? 0;
4750
+ if (result.error)
4751
+ throw result.error;
4752
+ if (result.signal)
4753
+ throw new Error(`World shell ended by ${result.signal}`);
4754
+ if (result.status === null)
4755
+ throw new Error('World shell returned no exit status');
4756
+ return result.status;
4757
+ }
4758
+ catch (error) {
4759
+ failed = true;
4760
+ failure = error;
4761
+ throw error;
4521
4762
  }
4522
4763
  finally {
4523
4764
  if (proxy) {
4524
- await proxy.close();
4525
- tearDownCa(tlsDir(resolvedRoot, name));
4765
+ try {
4766
+ await proxy.close();
4767
+ tearDownCa(tlsDir(resolvedRoot, name));
4768
+ }
4769
+ catch (cleanup) {
4770
+ if (failed)
4771
+ throw new AggregateError([failure, cleanup], `World shell failed (${errorMessage(failure)}); proxy cleanup also failed (${errorMessage(cleanup)})`, { cause: failure });
4772
+ throw cleanup;
4773
+ }
4526
4774
  }
4527
4775
  }
4528
4776
  }
@@ -4844,6 +5092,7 @@ function clearWorldProxyFiles(root, name) {
4844
5092
  */
4845
5093
  export function activateScript(name, root = process.cwd()) {
4846
5094
  const status = statusWorld(name, root);
5095
+ const required = recordedRequiredCredentialClaims(status);
4847
5096
  if (!status.running)
4848
5097
  throw new Error(`World "${name}" is not running — run: volter-world up <config> --env-out <path> --name ${name}`);
4849
5098
  const baseEnv = status.env ?? {};
@@ -4869,6 +5118,8 @@ export function activateScript(name, root = process.cwd()) {
4869
5118
  // openssl missing or daemon failed — keep env-only redirect; never fabricate proxy success.
4870
5119
  }
4871
5120
  env = withAppUrl(name, root, env);
5121
+ // Exports follow the bookkeeping assignments; the final prompt assignment is the one later value change.
5122
+ assertRequiredCredentials(required, { ...env, PS1: `(world:${name}) ${env.PS1 ?? ''}` }, 'shell activation including its prompt');
4872
5123
  const keys = Object.keys(env).sort();
4873
5124
  const lines = [
4874
5125
  `# volter-world activate ${name} — run: eval "$(volter-world activate ${name})"`,