@volter/world-runtime 3.0.165 → 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
package/src/runtime.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import { isScalarCredentialField, withCurrentCredentialDoor } from './credential-door.ts';
1
2
  import { errorMessage } from './error-message.ts';
3
+ import { recorderEnvironment, recordedApplicationEnvironment } from './service-environment.ts';
2
4
  import { materializeWorldBrowserFiles } from './browser-files.ts';
3
5
  import { claimEnvOutput, publishEnvOutput } from './env-output.ts';
4
6
  import { offSignal } from './signals.ts';
5
7
  import { parallelStartup } from './startup.ts';
6
- import { type HostReadyTimes, waitForHostReady } from './host-ready.ts';
8
+ import { startedTwins, type HostReadyTimes, waitForHostReady } from './host-ready.ts';
7
9
  import { assertWorldStateRemovable, removeWorldState } from './ancestry.ts';
8
10
  import { packCli, packEntry } from './catalog.ts';
9
11
  import { vendorOfPackModule } from '@volter/world-core/runtime-state';
@@ -34,7 +36,7 @@ import { loadWorldConfig } from './configs.ts';
34
36
  import { createWorldNetworkPolicy, parseWorldNetworkPolicy, normalizeWorldNetwork, intersectWorldNetwork, type WorldNetwork, WORLD_NETWORK_POLICY_ENV } from '@volter/world-core/network-policy';
35
37
  import { overlayPackFactsFile, resolveServicePackage } from './catalog.ts';
36
38
  import { fakeEnvValue } from './fixture-env.ts';
37
- import { APP_URL_ENV, ISSUE_ENV_PREFIX, MINT_ENV } from './schema.ts';
39
+ import { APP_URL_ENV, ISSUE_ENV_PREFIX, MINT_ENV, assertRequiredCredentialBindings } from './schema.ts';
38
40
  import { readAppUrl } from './app-url.ts';
39
41
  import { inertInjectEnvWarnings } from './inject-map.ts';
40
42
  import { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, proxyEnvFor, tearDownCa } from './proxy-env.ts';
@@ -69,13 +71,13 @@ export function worldEventsPath(logs: string): string {
69
71
  * its output straight to `log` (capped, time-marked), its start and end are recorded there and in the World's events
70
72
  * file, and it is handed a directory of its own for the log files it writes itself (VOLTER_WORLD_SERVICE_LOG_DIR).
71
73
  * `ended` settles when the recorder exits, right after the service ends and its exit record is written. */
72
- function spawnRecorded(serviceId: string, command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv, log: string, boot: BootContext): { child: ReturnType<typeof spawn>; ended: Promise<void>; identity: Promise<SpawnIdentity | undefined> } {
74
+ function spawnRecorded(serviceId: string, command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv, log: string, boot: BootContext, required?: RequiredCredentialClaims): { child: ReturnType<typeof spawn>; ended: Promise<void>; identity: Promise<SpawnIdentity | undefined> } {
73
75
  const logDir = ownedPath(dirname(log), `${basename(log).replace(/\.log$/, '')}.d`);
76
+ const recorderEnv = recorderEnvironment(env, serviceId, log, logDir, worldEventsPath(dirname(log)));
77
+ // The recorder adds and removes its own names. Validate the same final child environment before opening a log or spawning.
78
+ if (required !== undefined) assertRequiredCredentials(required, recordedApplicationEnvironment(recorderEnv), `service ${serviceId}'s recorded application environment`);
74
79
  mkdirSync(logDir, { recursive: true, mode: 0o700 });
75
80
  const out = openPrivateLog(log, 'a'); // the service's output and the recorder's records, appended
76
- const recorderEnv: NodeJS.ProcessEnv = { ...env, VOLTER_WORLD_SERVICE_ID: serviceId, VOLTER_WORLD_SERVICE_LOG: log,
77
- VOLTER_WORLD_SERVICE_LOG_DIR: logDir, VOLTER_WORLD_EVENTS: worldEventsPath(dirname(log)) };
78
- if (recorderEnv.NODE_OPTIONS !== undefined) { recorderEnv.VOLTER_WORLD_SERVICE_NODE_OPTIONS = recorderEnv.NODE_OPTIONS; delete recorderEnv.NODE_OPTIONS; }
79
81
  const child = spawn(process.execPath, [siblingScript(import.meta.url, 'service-recorder'), command, ...args], { windowsHide: true,
80
82
  cwd,
81
83
  env: recorderEnv,
@@ -614,9 +616,12 @@ function networkOf(name: string, own: WorldNetwork | undefined, recorded: WorldN
614
616
  return { [WORLD_NETWORK_POLICY_ENV]: JSON.stringify(createWorldNetworkPolicy(name, network)) };
615
617
  }
616
618
 
617
- /** Strip the publication envelope before combining permissions (the config shape admits only these keys). */
619
+ /** Strip the publication envelope before combining permissions (the config shape admits only these keys). Every
620
+ * permission the policy carries stays: a dropped `writes` or `publicReads` would read as the default when combined,
621
+ * and a build World inside a closed one would read the public build origins its parent may not (sealing never widens). */
618
622
  function networkPermission(policy: WorldNetwork): WorldNetwork {
619
- return { egress: policy.egress, phase: policy.phase, passthrough: policy.passthrough, ...(policy.ownLoopback === false ? { ownLoopback: false } : {}) };
623
+ return { egress: policy.egress, ...(policy.writes !== undefined ? { writes: policy.writes } : {}), phase: policy.phase, passthrough: policy.passthrough,
624
+ ...(policy.publicReads !== undefined ? { publicReads: policy.publicReads } : {}), ...(policy.ownLoopback === false ? { ownLoopback: false } : {}) };
620
625
  }
621
626
 
622
627
  /** The boot permission, never a fallback to a mutable config. */
@@ -724,7 +729,13 @@ function readWorldBootRecordLocked(name: string, root: string): WorldInstance {
724
729
  } catch { 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.`); }
725
730
  }
726
731
 
727
- export function admittedEnv(strip: string[] | undefined, env: NodeJS.ProcessEnv = process.env): Record<string, string> {
732
+ /** What Windows itself needs in every process: without these, Winsock and DLL loading fail, the temp directory falls
733
+ * back to C:\Windows\Temp and tools lose their profile folders. They hold no caller credential, so a strip (even
734
+ * `*`) never removes them on win32. Windows names are case-insensitive. */
735
+ const WINDOWS_BASELINE_ENV = new Set(['systemroot', 'windir', 'systemdrive', 'comspec', 'pathext', 'temp', 'tmp', 'userprofile',
736
+ 'appdata', 'localappdata', 'programdata', 'programfiles', 'programfiles(x86)', 'programw6432', 'commonprogramfiles',
737
+ 'commonprogramfiles(x86)', 'commonprogramw6432', 'homedrive', 'homepath', 'os', 'processor_architecture', 'number_of_processors']);
738
+ export function admittedEnv(strip: string[] | undefined, env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): Record<string, string> {
728
739
  const out: Record<string, string> = {};
729
740
  const rules = (strip ?? []).map((s): { prefix: string } | { name: string } => (s.endsWith('*') ? { prefix: s.slice(0, -1) } : { name: s }));
730
741
  for (const [k, v] of Object.entries(env)) {
@@ -732,6 +743,7 @@ export function admittedEnv(strip: string[] | undefined, env: NodeJS.ProcessEnv
732
743
  // Permission is published by the attached instance, never inherited from
733
744
  // whichever World happened to launch this operator process.
734
745
  if (k === WORLD_NETWORK_POLICY_ENV) continue;
746
+ if (platform === 'win32' && WINDOWS_BASELINE_ENV.has(k.toLowerCase())) { out[k] = v; continue; }
735
747
  if (rules.some((r) => ('prefix' in r ? k.startsWith(r.prefix) : k === r.name))) continue;
736
748
  out[k] = v;
737
749
  }
@@ -769,6 +781,7 @@ function serviceProcessEnv(
769
781
  service: WorldServiceConfig,
770
782
  worldEnv: Record<string, string>,
771
783
  port: number,
784
+ twin = service.type === 'twin',
772
785
  ): Record<string, string> {
773
786
  const env: Record<string, string> = {
774
787
  ...admittedEnv(config.stripEnv),
@@ -782,6 +795,12 @@ function serviceProcessEnv(
782
795
  for (const key of CONTROL_PLANE_EGRESS_ENV) delete env[key];
783
796
  }
784
797
  dropUnissued(env);
798
+ // NODE_ENV is the application's mode: a twin is the World's own server, run as built (React's production server build
799
+ // crashes Bun 1.3.11 at load, so Twenty's NODE_ENV=production stopped every twin with screens); a service's own env
800
+ // may still set it
801
+ // a twin is a service that runs a pack: declared `type: 'twin'`, or untyped and spawning a pack's module (never an
802
+ // untyped command of the application's)
803
+ if (twin) delete env.NODE_ENV;
785
804
  const out: Record<string, string> = { ...env, ...(service.env ?? {}) };
786
805
  // the application's own origin: the World's own application service is reached where it listens (the URL
787
806
  // `volter-world app-url` answers for it); any other service never reads it, and a command resolves it at attach
@@ -1273,6 +1292,7 @@ async function startExternalService(
1273
1292
  service: WorldServiceConfig,
1274
1293
  paths: { root: string; logs: string },
1275
1294
  worldEnv: Record<string, string>,
1295
+ required: RequiredCredentialClaims,
1276
1296
  stripEnv?: string[],
1277
1297
  boot?: BootContext,
1278
1298
  ): Promise<WorldServiceInstance> {
@@ -1281,6 +1301,7 @@ async function startExternalService(
1281
1301
  const log = ownedPath(paths.logs, `${service.id}.log`);
1282
1302
  const cwd = service.cwd ? resolve(paths.root, service.cwd) : paths.root;
1283
1303
  const env = externalServiceEnv(service, paths, worldEnv, stripEnv);
1304
+ if (!ownInfrastructureService(service)) assertRequiredCredentials(required, env, `external service ${service.id}`);
1284
1305
 
1285
1306
  // 1. up — start the self-managed stack, STREAMING its output to the service log (no 1MB cap).
1286
1307
  if (!external.up[0] || !commandExists(external.up[0])) {
@@ -1411,8 +1432,11 @@ async function startService(
1411
1432
  paths: { root: string; instance: string; logs: string; data: string },
1412
1433
  allocatedPorts: Set<number>,
1413
1434
  boot: BootContext,
1435
+ required: RequiredCredentialClaims,
1414
1436
  ): Promise<WorldServiceInstance> {
1415
- if (service.type === 'external') return startExternalService(service, paths, worldEnv, config.stripEnv, boot);
1437
+ if ((service.type ?? 'twin') !== 'twin' && !ownInfrastructureService(service))
1438
+ assertRequiredCredentials(required, worldEnv, `service ${service.id}`);
1439
+ if (service.type === 'external') return startExternalService(service, paths, worldEnv, required, config.stripEnv, boot);
1416
1440
  if (!service.command) throw new Error(`Service "${service.id}" must define command`);
1417
1441
  const port = service.port === undefined || service.port === 'auto' ? await allocatePort(allocatedPorts) : service.port;
1418
1442
  const serviceDataDir = ownedPath(paths.data, service.id);
@@ -1433,7 +1457,7 @@ async function startService(
1433
1457
  if (service.rootArg !== false) args.push(service.rootArg ?? '--root', serviceDataDir);
1434
1458
 
1435
1459
  const log = ownedPath(paths.logs, `${service.id}.log`);
1436
- const env = serviceProcessEnv(config, service, worldEnv, port);
1460
+ const env = serviceProcessEnv(config, service, worldEnv, port, service.type === 'twin' || (service.type === undefined && spawnedModule !== undefined));
1437
1461
  const cwd = service.cwd ? resolve(paths.root, service.cwd) : paths.root;
1438
1462
 
1439
1463
  // Augment NODE_OPTIONS with declared extra preloads, so a service can add an app-local preload
@@ -1447,7 +1471,9 @@ async function startService(
1447
1471
  // The service runs under the service recorder, which outlives it to write its end into its own log: `up`
1448
1472
  // returns once the World is ready, so nothing else is left to see a service stop.
1449
1473
  env.VOLTER_WORLD_BOOT_ID = boot.bootId;
1450
- const { child, ended, identity } = spawnRecorded(service.id, service.command, args, cwd, env, log, boot);
1474
+ if ((service.type ?? 'twin') !== 'twin' && !ownInfrastructureService(service))
1475
+ assertRequiredCredentials(required, env, `service ${service.id}'s effective environment`);
1476
+ const { child, ended, identity } = spawnRecorded(service.id, service.command, args, cwd, env, log, boot, (service.type ?? 'twin') === 'twin' ? undefined : required);
1451
1477
  child.once('error', error => boot.fail(error));
1452
1478
  child.unref();
1453
1479
  await trackBootingChild(child, boot, identity);
@@ -1645,6 +1671,87 @@ function takeIssued(env: Record<string, string>): Map<string, string> {
1645
1671
  * own store, in milliseconds; this bounds a hung twin, not a slow one). */
1646
1672
  const ISSUE_TIMEOUT_MS = 15_000;
1647
1673
 
1674
+ /** Each required name belongs to this boot and gains a value only from its owner's scalar door response. */
1675
+ type RequiredCredentialClaims = Map<string, { service: string; value: string | undefined }>;
1676
+
1677
+ function requiredCredentialClaims(config: WorldConfig): RequiredCredentialClaims {
1678
+ const required: RequiredCredentialClaims = new Map();
1679
+ for (const service of config.services) for (const name of service.issues?.required ?? [])
1680
+ required.set(name, { service: service.id, value: undefined });
1681
+ return required;
1682
+ }
1683
+
1684
+ /** Bind names and owners to the actual values retained in instance.env; the hash is not vendor authentication. */
1685
+ function requiredCredentialValuesDigest(required: RequiredCredentialClaims): string {
1686
+ const rows = [...required.keys()].sort().map(name => {
1687
+ const claim = required.get(name)!;
1688
+ if (claim.value === undefined) throw new Error(`${name}: no issued value exists to retain`);
1689
+ return [name, claim.service, claim.value];
1690
+ });
1691
+ return createHash('sha256').update(JSON.stringify(rows)).digest('hex');
1692
+ }
1693
+
1694
+ /** New boots retain their required owners even if source config later changes; legacy optional records gain no proof. */
1695
+ function recordedRequiredCredentialClaims(instance: WorldInstance): RequiredCredentialClaims {
1696
+ const required: RequiredCredentialClaims = new Map();
1697
+ if (!Object.hasOwn(instance, 'requiredCredentials')) throw new Error('World boot has no retained credential requirement declaration; finish teardown with its owning runtime and boot with this runtime before attachment');
1698
+ const raw: unknown = instance.requiredCredentials;
1699
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('World required credential record is invalid');
1700
+ const record = raw as Record<string, unknown>;
1701
+ if (Object.keys(record).some(key => !['version', 'owners', 'valuesSha256'].includes(key))
1702
+ || !Object.hasOwn(record, 'version') || record.version !== 1 || !Object.hasOwn(record, 'owners') || record.owners === null
1703
+ || typeof record.owners !== 'object' || Array.isArray(record.owners))
1704
+ throw new Error('World required credential record has an unsupported version or shape');
1705
+ if (!instance.env || typeof instance.env !== 'object' || Array.isArray(instance.env)
1706
+ || !instance.services || typeof instance.services !== 'object' || Array.isArray(instance.services))
1707
+ throw new Error('World required credential record lacks its retained environment or services');
1708
+ for (const [name, owner] of Object.entries(record.owners as Record<string, unknown>)) {
1709
+ if (!isEnvironmentName(name) || typeof owner !== 'string' || !owner || !Object.hasOwn(instance.services, owner))
1710
+ throw new Error(`World required credential owner is invalid for ${name}`);
1711
+ const service = instance.services[owner];
1712
+ if (!service || service.id !== owner || service.type !== 'twin') throw new Error(`World required credential ${name} names no twin owner`);
1713
+ const value = instance.env[name];
1714
+ if (!Object.hasOwn(instance.env, name) || typeof value !== 'string'
1715
+ || value.startsWith(ISSUE_ENV_PREFIX) || value === APP_URL_ENV)
1716
+ throw new Error(`World required credential ${name} has no retained scalar value`);
1717
+ required.set(name, { service: owner, value });
1718
+ }
1719
+ if (!Object.hasOwn(record, 'valuesSha256')) {
1720
+ if (required.size) throw new Error('World required credential issuance has not completed; refusing attachment');
1721
+ return required;
1722
+ }
1723
+ if (typeof record.valuesSha256 !== 'string' || !/^[0-9a-f]{64}$/.test(record.valuesSha256)
1724
+ || record.valuesSha256 !== requiredCredentialValuesDigest(required))
1725
+ throw new Error('World required credential names, owners or retained values changed; refusing attachment');
1726
+ return required;
1727
+ }
1728
+
1729
+ /** Capture the validated boot's values before an owned executor applies its own environment layers. */
1730
+ export function worldRequiredCredentialGate(instance: WorldInstance): (env: NodeJS.ProcessEnv, boundary: string) => void {
1731
+ const required = recordedRequiredCredentialClaims(instance);
1732
+ return (env, boundary) => assertRequiredCredentials(required, env, boundary);
1733
+ }
1734
+
1735
+ /** These transports do not carry required ownership/value authority; refuse instead of exporting an optional-only rendering. */
1736
+ export function refuseRequiredCredentialTransport(instance: WorldInstance, transport: string, code?: 'WORLD_MACHINE_CAPABILITY_GAP'): void {
1737
+ const required = recordedRequiredCredentialClaims(instance);
1738
+ if (required.size) {
1739
+ const error = new Error(`World ${instance.name}: ${transport} does not carry required credential bindings (${[...required.keys()].join(', ')}); use local env attachment`);
1740
+ throw code ? Object.assign(error, { code }) : error;
1741
+ }
1742
+ }
1743
+
1744
+ /** Check the actual response value, then the environment this boundary will use; never print credential values. */
1745
+ function assertRequiredCredentials(required: RequiredCredentialClaims, env: NodeJS.ProcessEnv, boundary: string): void {
1746
+ const failures: Error[] = [];
1747
+ for (const [name, claim] of required) {
1748
+ if (claim.value === undefined) failures.push(new Error(`${name}: ${claim.service} has not issued its required scalar credential`));
1749
+ else if (!Object.hasOwn(env, name) || env[name] !== claim.value)
1750
+ failures.push(new Error(`${name}: its issued value is missing or overridden at ${boundary}`));
1751
+ }
1752
+ if (failures.length) throw new AggregateError(failures, `World required credential gate refused at ${boundary}: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
1753
+ }
1754
+
1648
1755
  /** Where one credential ask's time went (S2, the twins' credentials): performance.now() at each step, read by the caller
1649
1756
  * against its own origin. `door` is the pack's door read, `sent` the request handed to fetch, `headers` its answer's
1650
1757
  * status, `answered` its body parsed, `filled` every name set or said unset. Door→sent and answered→filled include synchronous placeholder-pull
@@ -1653,22 +1760,33 @@ type CredentialAskTimes = { twin?: string; names?: number; door?: number; sent?:
1653
1760
 
1654
1761
  /** A twin's issued credential (its `issues`, copied from its pack's credentialDoor by init): once it is up, the door is
1655
1762
  * asked for one and each pending name of that service is set to it. A twin that answers no credential, or has no
1656
- * door, leaves the names unset and says so, rather than handing the application a fixture the twin would refuse. */
1657
- async function issueCredentials(service: WorldServiceConfig, started: WorldServiceInstance, env: Record<string, string>, pending: Map<string, string>, unissued: Set<string>, controlRoot: string, times?: CredentialAskTimes): Promise<void> {
1763
+ * door, leaves optional names unset and says so. Required scalar names fail the boot with their original reasons. */
1764
+ async function issueCredentials(service: WorldServiceConfig, started: WorldServiceInstance, env: Record<string, string>, pending: Map<string, string>, unissued: Set<string>, controlRoot: string, claims: RequiredCredentialClaims, fresh: boolean, times?: CredentialAskTimes): Promise<void> {
1658
1765
  const direct = new Set([...pending].filter(([, id]) => id === `${service.id}@direct`).map(([name]) => name));
1659
1766
  const wanted = [...pending].filter(([name, id]) => id === service.id || direct.has(name)).map(([name]) => name);
1660
1767
  if (times) { times.names = wanted.length; }
1768
+ const required = new Set(service.issues?.required ?? []);
1769
+ const failures: Error[] = [];
1770
+ for (const name of required) {
1771
+ if (!wanted.includes(name) || direct.has(name) || claims.get(name)?.service !== service.id)
1772
+ failures.push(new Error(`${name}: its exact $issue:${service.id} scalar request is absent at issuance`));
1773
+ }
1774
+ if (failures.length) throw new AggregateError(failures, `World service ${service.id}: required credential requests refused: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
1661
1775
  if (wanted.length === 0) return;
1662
1776
  for (const name of wanted) pending.delete(name);
1663
- // the pack's door as it stands now (a door the pack fixed since the World was made reaches it), else the saved copy
1664
- let door = service.issues;
1665
- try { door = packFacts()[service.id]?.credentialDoor ?? door; } catch { /* no artifact: the saved copy */ }
1777
+ // Revalidate aliases at the actual issuance boundary. A missing current pack door cannot revive a saved issuer.
1778
+ const door = (service.type ?? 'twin') === 'twin'
1779
+ ? withCurrentCredentialDoor(service, packFacts()[service.id]?.credentialDoor).issues : service.issues;
1666
1780
  if (times) times.door = performance.now();
1667
1781
  try {
1668
1782
  if (!door) throw new Error('its service names no credential door');
1669
1783
  if (!started.url) throw new Error('it has no URL to ask');
1670
- // A boot's identity is existing account state, not an application write to deploy.
1671
- const enclosingSeed = placeholderPullActive(service.id, controlRoot);
1784
+ // A boot's identity is existing account state, not an application write to deploy: asked inside the twin's
1785
+ // placeholder-pull window, as the seed is (architecture, the descriptor's credentialDoor), on a fresh World's first
1786
+ // issuance only: a resumed World's door answers what it issued before, and its branch keeps its own writes; a twin
1787
+ // with a root is not seeded, and its door's writes stay its own
1788
+ const seeds = fresh && !(service as { root?: unknown }).root;
1789
+ const enclosingSeed = !seeds || placeholderPullActive(service.id, controlRoot);
1672
1790
  if (!enclosingSeed) beginPlaceholderPull(service.id, controlRoot);
1673
1791
  let answer: Record<string, unknown>;
1674
1792
  try {
@@ -1769,7 +1887,11 @@ async function issueCredentials(service: WorldServiceConfig, started: WorldServi
1769
1887
  continue;
1770
1888
  }
1771
1889
  const field = spec;
1890
+ if (required.has(name) && (!isScalarCredentialField(field) || answer === null || typeof answer !== 'object'
1891
+ || Array.isArray(answer) || !Object.hasOwn(answer, field))) throw new Error(`answered no own scalar ${field}`);
1772
1892
  const value = answer[field];
1893
+ if (required.has(name) && typeof value === 'string' && (value.startsWith(ISSUE_ENV_PREFIX) || value === APP_URL_ENV))
1894
+ throw new Error(`answered an interpreted environment marker for ${field}`);
1773
1895
  if (typeof value !== 'string') throw new Error(`answered no ${field}`);
1774
1896
  if (direct.has(name)) {
1775
1897
  const issued = new URL(value);
@@ -1779,9 +1901,19 @@ async function issueCredentials(service: WorldServiceConfig, started: WorldServi
1779
1901
  issued.host = endpoint.host;
1780
1902
  env[name] = issued.toString();
1781
1903
  } else env[name] = value;
1904
+ if (required.has(name)) {
1905
+ const claim = claims.get(name);
1906
+ if (!claim || claim.service !== service.id) throw new Error('its required credential owner changed');
1907
+ claim.value = value;
1908
+ }
1782
1909
  } catch (error) {
1783
- 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`);
1910
+ 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`);
1784
1911
  delete env[name]; unissued.add(name);
1912
+ if (required.has(name)) {
1913
+ const claim = claims.get(name);
1914
+ if (claim) claim.value = undefined;
1915
+ failures.push(new Error(`${name}: ${service.id} did not issue its required credential (${errorMessage(error)})`, { cause: error }));
1916
+ }
1785
1917
  }
1786
1918
  }
1787
1919
  if (started.url) {
@@ -1793,8 +1925,14 @@ async function issueCredentials(service: WorldServiceConfig, started: WorldServi
1793
1925
  } catch (error) {
1794
1926
  process.stderr.write(`volter-world: ${service.id} did not issue ${wanted.join(', ')} (${errorMessage(error)}); left unset\n`);
1795
1927
  for (const name of wanted) { delete env[name]; unissued.add(name); }
1928
+ for (const name of required) {
1929
+ const claim = claims.get(name);
1930
+ if (claim) claim.value = undefined;
1931
+ failures.push(new Error(`${name}: ${service.id}'s required credential ask failed (${errorMessage(error)})`, { cause: error }));
1932
+ }
1796
1933
  }
1797
1934
  if (times) times.filled = performance.now();
1935
+ if (failures.length) throw new AggregateError(failures, `World service ${service.id}: required credential issuance refused: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
1798
1936
  }
1799
1937
 
1800
1938
  function materializeConfig(config: WorldConfig, root: string): WorldConfig {
@@ -1906,6 +2044,8 @@ async function startColocatedServices(
1906
2044
  // the twins never read the application's own origin, nor a credential not yet issued
1907
2045
  for (const [key, value] of Object.entries(env)) if (value === APP_URL_ENV) delete env[key];
1908
2046
  dropUnissued(env);
2047
+ // the colocated twins' host runs the World's own servers: the application's NODE_ENV is not theirs (serviceProcessEnv)
2048
+ delete env.NODE_ENV;
1909
2049
  env.VOLTER_WORLD_BOOT_ID = boot.bootId;
1910
2050
  const spawnedAt = Date.now();
1911
2051
  // Under the same exit recorder as a process service, so the host's end (and every twin it holds) is in host.log.
@@ -1946,7 +2086,7 @@ async function startColocatedServices(
1946
2086
  });
1947
2087
  const ready = (async (): Promise<void> => {
1948
2088
  const hostTimeoutMs = allocated.reduce((timeout, { service }) => Math.max(timeout, typeof service.ready === 'object' ? service.ready.timeoutMs ?? 15_000 : 15_000), 15_000);
1949
- const hostTimes = await untilReadyOrEnded((abort) => waitForHostReady(readyFile, boot.bootId, new Map(allocated.map(({ service, port }) => [service.id, port])), hostTimeoutMs, abort, () => { try { return statSync(log).size; } catch { return 0; } }), ended, boot, 'the co-located host', log).catch(async (error) => {
2089
+ 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) => {
1950
2090
  if (undeadPids([child.pid!]).length) recordWorldStop([log], 'world.service.stop', 'up (host startup incomplete)', 'SIGTERM');
1951
2091
  try { await signalHeldChild(child, 'SIGTERM', true, identity); } catch { /* best effort cleanup */ }
1952
2092
  throw new Error(`Co-located host failed to complete startup (isolation=${isolation}): ${errorMessage(error)}\nLog: ${log}`);
@@ -2051,6 +2191,11 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2051
2191
  // instead of loading and rebuilding a second facts/host table on every empty up.
2052
2192
  if (packFactsPaths.length) createRequire(import.meta.url)('@volter/world-core/pack-facts').extendPackFacts(packFactsPaths);
2053
2193
  const loaded = { path: found.path, config: materializeConfig(found.config, root) };
2194
+ // Before any output claim or lifecycle write, every saved alias fits the actual current pack.
2195
+ loaded.config.services = loaded.config.services.map(service => (service.type ?? 'twin') === 'twin'
2196
+ ? withCurrentCredentialDoor(service, packFacts()[service.id]?.credentialDoor) : service);
2197
+ assertRequiredCredentialBindings(loaded.config, found.path);
2198
+ const required = requiredCredentialClaims(loaded.config);
2054
2199
  const name = options.name ?? loaded.config.id;
2055
2200
  const mode = options.mode ?? 'local';
2056
2201
  assertSafeWorldName(name);
@@ -2155,7 +2300,6 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2155
2300
  // Verified teardown is independent of record shape. A fresh boot discards incomplete metadata.
2156
2301
  if (existsSync(instanceFile)) {
2157
2302
  let prior: WorldInstance | undefined;
2158
- if (options.keepState) prior = readWorldBootRecordLocked(name, root);
2159
2303
  const priorRaw = readFileSync(instanceFile, 'utf8');
2160
2304
  const receiptPath = join(instance, 'stopped.json');
2161
2305
  const receipt = existsSync(receiptPath) ? JSON.parse(readFileSync(receiptPath, 'utf8')) : undefined;
@@ -2163,6 +2307,13 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2163
2307
  || !Number.isFinite(Date.parse(receipt.stoppedAt))) {
2164
2308
  throw new Error(`World "${name}" has no verified teardown receipt for its prior instance; use its owning runtime to complete cleanup before replacement`);
2165
2309
  }
2310
+ // a resume reads the record its last boot completed; a boot that failed and was rolled back (the receipt above)
2311
+ // completed none, and the resume writes a complete one over the kept state (architecture, the boot record)
2312
+ let priorIncomplete = false;
2313
+ if (options.keepState) {
2314
+ try { prior = readWorldBootRecordLocked(name, root); }
2315
+ catch { priorIncomplete = true; }
2316
+ }
2166
2317
  if (!prior) { try { prior = JSON.parse(priorRaw) as WorldInstance; } catch { /* fresh boot needs no retained shape */ } }
2167
2318
  if (prior) {
2168
2319
  // A readable ceiling remains useful even when unrelated metadata is incomplete.
@@ -2175,7 +2326,7 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2175
2326
  }
2176
2327
  const ceiling = recordedWorldNetwork(permissionRecord);
2177
2328
  if (ceiling !== undefined) carriedNetwork = carriedNetwork === undefined ? ceiling : intersectWorldNetwork(carriedNetwork, ceiling);
2178
- } catch { if (options.keepState) throw new Error(`World boot record is incomplete or invalid: ${instanceFile}`); }
2329
+ } catch { if (options.keepState && !priorIncomplete) throw new Error(`World boot record is incomplete or invalid: ${instanceFile}`); }
2179
2330
  }
2180
2331
  // the origin belongs to the World, not to its running state (git: a remote outlives every checkout): an up of the
2181
2332
  // same config carries the one the last left while world.json's `origin` remote still names it (`remote remove`
@@ -2277,7 +2428,10 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2277
2428
  const unissued = new Set<string>();
2278
2429
  writeProxyEnv(root, name, env);
2279
2430
 
2431
+ const requiredCredentials: NonNullable<WorldInstance['requiredCredentials']> = { version: 1,
2432
+ owners: Object.fromEntries([...required].map(([name, claim]) => [name, claim.service])) };
2280
2433
  const worldInstance: WorldInstance = {
2434
+ requiredCredentials,
2281
2435
  name, instance: instanceToken, config: loaded.config.id, configPath: loaded.path, root, createdAt, mode,
2282
2436
  dirs: { instance, logs, data }, services, env, envFile, pidsFile,
2283
2437
  ...(envOutputClaim.contents === undefined ? {} : { previousEnvOutput: { path: envFile, sha256: createHash('sha256').update(envOutputClaim.contents).digest('hex') } }),
@@ -2352,14 +2506,15 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2352
2506
  // own sent and status says that twin's door.
2353
2507
  const origin = performance.now();
2354
2508
  const steps: Array<{ id: string; queued: number; times: CredentialAskTimes }> = [];
2355
- const asks = await Promise.all(host.services.map(async (started) => {
2509
+ // A descriptor refusal still settles every owned ask before rollback can retire its custody.
2510
+ const asks = await Promise.allSettled(host.services.map(async (started) => {
2356
2511
  const declared = colocated.find((s) => s.id === started.id);
2357
2512
  if (!declared) return undefined;
2358
2513
  const began = performance.now();
2359
2514
  const times: CredentialAskTimes = { ...(declared.package ? { twin: declared.package } : {}) };
2360
2515
  steps.push({ id: started.id, queued: began, times });
2361
2516
  const own: Record<string, string> = { ...before };
2362
- await issueCredentials(declared, started, own, issuing, unissued, ownedPath(data, started.id), times);
2517
+ await issueCredentials(declared, started, own, issuing, unissued, ownedPath(data, started.id), required, !options.keepState, times);
2363
2518
  return { id: started.id, ms: Math.round(performance.now() - began), own };
2364
2519
  }));
2365
2520
  const at = (time: number | undefined): string => time === undefined ? '-' : `+${Math.round(time - origin)}`;
@@ -2368,13 +2523,17 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2368
2523
  .sort((left, right) => (right.times.filled ?? 0) - (left.times.filled ?? 0))
2369
2524
  .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('; ')}`);
2370
2525
  const asked: Array<{ id: string; ms: number }> = [];
2371
- for (const ask of asks) {
2526
+ const failures: unknown[] = [];
2527
+ for (const result of asks) {
2528
+ if (result.status === 'rejected') { failures.push(result.reason); continue; }
2529
+ const ask = result.value;
2372
2530
  if (!ask) continue;
2373
2531
  for (const [key, value] of Object.entries(ask.own)) if (before[key] !== value) env[key] = value;
2374
2532
  for (const key of Object.keys(before)) if (!Object.hasOwn(ask.own, key)) delete env[key];
2375
2533
  asked.push({ id: ask.id, ms: ask.ms });
2376
2534
  }
2377
2535
  if (asked.length) 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`);
2536
+ if (failures.length) throw new AggregateError(failures, `World credential issuance refused: ${failures.map(reason => errorMessage(reason)).join('; ')}`);
2378
2537
  writeProxyEnv(root, name, env);
2379
2538
  if (colocated.length) boot.progress('co-located twins: credentials issued, proxy environment written');
2380
2539
  };
@@ -2387,7 +2546,7 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2387
2546
  for (const service of loaded.config.services) {
2388
2547
  if (services[service.id] || !ownInfrastructureService(service)) continue;
2389
2548
  boot.progress(`${service.id}: starting beside the host (the runtime's own infrastructure)`);
2390
- const starting = startService(loaded.config, service, { ...env }, { root, instance, logs, data }, allocatedPorts, boot);
2549
+ const starting = startService(loaded.config, service, { ...env }, { root, instance, logs, data }, allocatedPorts, boot, required);
2391
2550
  starting.catch((error: unknown) => boot.fail(error instanceof Error ? error : new Error(errorMessage(error))));
2392
2551
  early.set(service.id, starting);
2393
2552
  }
@@ -2429,18 +2588,20 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2429
2588
  attemptProxy();
2430
2589
  boot.signal.throwIfAborted();
2431
2590
  if (!starting) boot.progress(`${service.id}: starting (${service.type ?? 'twin'})`);
2432
- const started = await (starting ?? startService(loaded.config, service, env, { root, instance, logs, data }, allocatedPorts, boot));
2591
+ const started = await (starting ?? startService(loaded.config, service, env, { root, instance, logs, data }, allocatedPorts, boot, required));
2433
2592
  // An early start published its teardown when it began; its record moves to its declared position.
2434
2593
  if (starting) delete services[service.id];
2435
2594
  recordService(started);
2436
2595
  recordPidsSoFar();
2437
2596
  Object.assign(env, started.env);
2438
- await issueCredentials(service, started, env, issuing, unissued, ownedPath(data, started.id));
2597
+ await issueCredentials(service, started, env, issuing, unissued, ownedPath(data, started.id), required, !options.keepState);
2439
2598
  writeProxyEnv(root, name, env);
2440
2599
  boot.progress(`${service.id}: started, its environment merged`);
2441
2600
  }
2442
2601
  if (colocatedReady) await colocatedReady();
2443
- detachBootSignals();
2602
+ // An abort records its reason while the final owned credential asks settle. No later service may remain
2603
+ // to observe it, so completion itself must reject it before publishing a successful environment.
2604
+ boot.signal.throwIfAborted();
2444
2605
  // a name a service of no twin in this World issues (a config edited after init) is left unset, and said
2445
2606
  if (issuing.size > 0) process.stderr.write(`volter-world: nothing in this World issues ${[...issuing].map(([n, id]) => `${n} ($issue:${id})`).join(', ')}; left unset\n`);
2446
2607
  for (const [name] of issuing) unissued.add(name);
@@ -2450,6 +2611,7 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2450
2611
  // next service" never fires when there is no next service (TWIN-64) — the single-twin sealed
2451
2612
  // config test below exercises exactly this.
2452
2613
  attemptProxy();
2614
+ assertRequiredCredentials(required, env, 'World environment publication');
2453
2615
  // Inert-injectEnv check (LibreChat blind-adoption finding): an injectEnv like AWS_TWIN_URL
2454
2616
  // lands in the world env, LOOKS wired, and does nothing — the injector reads no such vendor.
2455
2617
  // Warn loudly at `up`, when the operator is watching, instead of letting the disagreement
@@ -2458,6 +2620,12 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2458
2620
  process.stderr.write(`${warning}\n`);
2459
2621
  }
2460
2622
 
2623
+ // This can await the browser materializer's import. Keep boot cancellation attached through that work,
2624
+ // and do not persist completed credential claims or publish the environment until it has finished.
2625
+ await materializeWorldBrowserFiles(data, root, name, loaded.config.id);
2626
+ boot.signal.throwIfAborted();
2627
+ assertRequiredCredentials(required, env, 'World environment publication');
2628
+ requiredCredentials.valuesSha256 = requiredCredentialValuesDigest(required);
2461
2629
  mkdirSync(dirname(envFile), { recursive: true });
2462
2630
  // Config env and discovered credentials: a file this writes is owner-only; a file the caller already made keeps
2463
2631
  // the mode the caller chose (a shared env file may be read under another uid).
@@ -2468,11 +2636,12 @@ async function upWorldRecorded(configId: string, options: UpWorldOptions, abort:
2468
2636
  delete worldInstance.previousEnvOutput;
2469
2637
  saveWorldInstance(worldInstance);
2470
2638
  boot.progress(`environment written: ${envFile}`);
2471
- await materializeWorldBrowserFiles(data, root, name, loaded.config.id);
2472
2639
  writePidsFromInstance(worldInstance, Object.values(worldInstance.services).map((service) => service.pid).filter((pid) => pid > 0));
2473
2640
  saveWorldInstance(worldInstance);
2474
2641
  const servicePids = [...new Set(Object.values(services).map((service) => service.pid).filter((pid) => pid > 0))];
2642
+ boot.signal.throwIfAborted();
2475
2643
  handoffWorldLifecycle(root, name, servicePids);
2644
+ detachBootSignals();
2476
2645
  boot.progress(`returning: the World is up; ${lag.account()}`);
2477
2646
  if (mode === 'share') {
2478
2647
  try {
@@ -2692,6 +2861,24 @@ function killHeldSurvivorsSync(child: ReturnType<typeof spawn>, graceMs: number)
2692
2861
  * purge leaves that lock alone; its owner's finally releases it, so waiting claimants never
2693
2862
  * enter through a replacement lock while the original is still held.
2694
2863
  */
2864
+ /** A purged World's env output outside its folder (architecture, "World environment exports are outputs"): removed with
2865
+ * the record that proved it the World's own, when it still holds exactly the World's exports as a single-link regular
2866
+ * file and no other World of the root records that path; anything else stays. */
2867
+ function purgeEnvOutput(root: string, name: string, purged: string, instance: WorldInstance): void {
2868
+ const path = typeof instance.envFile === 'string' ? resolve(instance.envFile) : undefined;
2869
+ if (!path || !relative(purged, path).startsWith('..')) return;
2870
+ const others = existsSync(worldBaseDir(root)) ? readdirSync(worldBaseDir(root)).filter((world) => world !== name && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(world)) : [];
2871
+ const shared = others.some((world) => {
2872
+ try { return resolve((JSON.parse(readFileSync(join(instanceDir(root, world), 'instance.json'), 'utf8')) as WorldInstance).envFile) === path; } catch { return false; }
2873
+ });
2874
+ if (shared) return;
2875
+ try {
2876
+ const named = lstatSync(path);
2877
+ if (!named.isFile() || named.nlink !== 1 || !readFileSync(path).equals(Buffer.from(envFileContents(instance.env)))) return;
2878
+ rmSync(path);
2879
+ } catch { /* no output, or one the World cannot read: it stays */ }
2880
+ }
2881
+
2695
2882
  function purgeInstanceDir(root: string, name: string): string {
2696
2883
  assertSafeWorldName(name);
2697
2884
  const base = worldBaseDir(root);
@@ -2922,6 +3109,7 @@ async function downWorldClaimed(
2922
3109
  if (!options.purge) return result;
2923
3110
  const purged = purgeInstanceDir(resolvedRoot, name);
2924
3111
  if (purged && instance) clearBrowserFiles!(instance.dirs.data, resolvedRoot);
3112
+ if (purged && instance) purgeEnvOutput(resolvedRoot, name, purged, instance);
2925
3113
  return { ...result, ...(externalAbandoned ? { externalAbandoned } : {}), purged };
2926
3114
  }));
2927
3115
  }
@@ -3597,6 +3785,14 @@ export async function doctorWorld(name: string, options: { root?: string; verify
3597
3785
  checks.push({ id: 'startup', ok: missingServices.length === 0, message: missingServices.length
3598
3786
  ? `startup incomplete: no ready service record for ${missingServices.join(', ')}; retained state requires normal teardown before resume`
3599
3787
  : 'declared boot services have instance records' });
3788
+ // a vendor-backed root whose vendor rotated its refresh token while the seal failed (world-core head.ts
3789
+ // unsealedMarkerOf): it renews only inside the process that held the new one, so a person must seal it again
3790
+ const credentialDir = join(root, stateDirName(), 'credentials');
3791
+ for (const file of existsSync(credentialDir) ? readdirSync(credentialDir).filter((f) => f.endsWith('.unsealed.json')).sort() : []) {
3792
+ let said = '';
3793
+ try { const r = JSON.parse(readFileSync(join(credentialDir, file), 'utf8')) as { field?: string; at?: string; cause?: string }; said = `: ${r.field ?? 'a field'} rotated at ${r.at ?? '?'} was not sealed (${r.cause ?? 'no cause recorded'})`; } catch { /* the marker's presence is the finding */ }
3794
+ 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` });
3795
+ }
3600
3796
 
3601
3797
  if (instance.lifecycle) {
3602
3798
  checks.push({ id: 'lifecycle', ok: hasWorldLifecycle(root, name) && existsSync(instance.lifecycle.log),
@@ -3888,8 +4084,9 @@ export function listWorlds(root = process.cwd()): Array<{ name: string; running:
3888
4084
  .sort((a, b) => a.name.localeCompare(b.name));
3889
4085
  }
3890
4086
 
3891
- function worldAttachedCommandEnv(name: string, root: string, verbose = false, instance?: WorldInstance): { instance: WorldInstance; env: NodeJS.ProcessEnv } {
4087
+ function worldAttachedCommandEnv(name: string, root: string, verbose = false, instance?: WorldInstance): { instance: WorldInstance; env: NodeJS.ProcessEnv; required: RequiredCredentialClaims } {
3892
4088
  const status = instance ?? statusWorld(name, root);
4089
+ const required = recordedRequiredCredentialClaims(status);
3893
4090
  const sealed = status.env.VOLTER_WORLD_MODE === 'sealed' || status.env[WORLD_NETWORK_POLICY_ENV] !== undefined;
3894
4091
  const baseEnv = { ...admittedEnv(stripEnvOf(status.env)), ...status.env };
3895
4092
  let proxyEnv: Record<string, string> = {};
@@ -3904,8 +4101,10 @@ function worldAttachedCommandEnv(name: string, root: string, verbose = false, in
3904
4101
  if (sealed && hostHasRawEgress()) throw new Error(`world "${name}" requires its network policy proxy: attachment proxy failed; refusing to run the command (${errorMessage(error)})`);
3905
4102
  // Keep env-only redirect if the ambient proxy cannot start.
3906
4103
  }
3907
- return { instance: status, env: withAppUrl(name, root, { ...baseEnv, ...proxyEnv, VOLTER_WORLD: name,
3908
- VOLTER_TWIN_INJECT_QUIET: verbose ? '0' : baseEnv.VOLTER_TWIN_INJECT_QUIET ?? '1' }) };
4104
+ const env = withAppUrl(name, root, { ...baseEnv, ...proxyEnv, VOLTER_WORLD: name,
4105
+ VOLTER_TWIN_INJECT_QUIET: verbose ? '0' : baseEnv.VOLTER_TWIN_INJECT_QUIET ?? '1' });
4106
+ assertRequiredCredentials(required, env, 'command attachment');
4107
+ return { instance: status, env, required };
3909
4108
  }
3910
4109
 
3911
4110
  /** The env with each `$app-url` value made the application URL its booter recorded. Unrecorded, the name is left
@@ -3929,10 +4128,12 @@ export function withAppUrl<E extends Record<string, string | undefined>>(name: s
3929
4128
  export function runWithWorldEnv(name: string, command: string[], root = process.cwd(), options: { cwd?: string; verbose?: boolean; env?: Record<string, string> } = {}): Promise<number> {
3930
4129
  if (command.length === 0) throw new Error('Missing command after --');
3931
4130
  const attached = worldAttachedCommandEnv(name, root, options.verbose);
4131
+ const env = { ...attached.env, ...(options.env ?? {}) };
4132
+ assertRequiredCredentials(attached.required, env, 'SDK command environment');
3932
4133
  return new Promise((resolveExit) => {
3933
4134
  const child = spawn(command[0]!, command.slice(1), { windowsHide: true,
3934
4135
  cwd: resolve(options.cwd ?? root),
3935
- env: { ...attached.env, ...(options.env ?? {}) },
4136
+ env,
3936
4137
  stdio: 'inherit',
3937
4138
  });
3938
4139
  const forward = (signal: NodeJS.Signals) => {
@@ -3998,6 +4199,8 @@ export async function attachWorld(name: string, command: string[], root = proces
3998
4199
  // Which of the two it was, with what was read: "stopped" and "another instance" are different repairs.
3999
4200
  if (!current.running) 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)})`);
4000
4201
  if (current.createdAt !== attached.instance.createdAt) 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`);
4202
+ // A supplied instance is a snapshot, not authority to erase the current boot's required bindings.
4203
+ assertRequiredCredentials(recordedRequiredCredentialClaims(current), attached.env, 'registered command attachment');
4001
4204
  // Attaching to a World whose services died is allowed (a command may not need them), but never silent.
4002
4205
  for (const [id, state] of Object.entries(current.serviceStates)) {
4003
4206
  if (state === 'running' || state === 'external') continue;
@@ -4139,13 +4342,21 @@ async function runWithWorldEnvLogged(name: string, command: string[], root: stri
4139
4342
  * TLS-proxy env (HTTPS_PROXY + the CA-trust vars) is layered in by `shellWorld` once the proxy is up,
4140
4343
  * since starting a server is async; this pure helper stays synchronous + side-effect-free for tests. */
4141
4344
  export function worldShellEnv(name: string, root = process.cwd()): { shell: string; cwd: string; env: Record<string, string> } {
4345
+ const { shell, cwd, env } = worldShellContext(name, root);
4346
+ return { shell, cwd, env };
4347
+ }
4348
+
4349
+ /** Keep the boot's required proof with the shell environment until its proxy transformations have finished. */
4350
+ function worldShellContext(name: string, root: string): { shell: string; cwd: string; env: Record<string, string>; required: RequiredCredentialClaims } {
4142
4351
  const status = statusWorld(name, root);
4143
4352
  if (!status.running) throw new Error(`World "${name}" is not running — run: volter-world up <config> --env-out <path> --name ${name}`);
4144
- return {
4145
- shell: status.env.SHELL || admittedEnv(stripEnvOf(status.env)).SHELL || '/bin/bash',
4146
- cwd: resolve(root),
4147
- env: withAppUrl(name, root, { ...admittedEnv(stripEnvOf(status.env)), ...status.env, VOLTER_WORLD: name } as Record<string, string>),
4148
- };
4353
+ const required = recordedRequiredCredentialClaims(status);
4354
+ const env = withAppUrl(name, root, { ...admittedEnv(stripEnvOf(status.env)), ...status.env, VOLTER_WORLD: name } as Record<string, string>);
4355
+ assertRequiredCredentials(required, env, 'shell environment');
4356
+ // Windows has no /bin/bash: its shell is the one ComSpec names (cmd.exe), kept by the Windows baseline (admittedEnv).
4357
+ const inherited = admittedEnv(stripEnvOf(status.env));
4358
+ const fallback = process.platform === 'win32' ? (inherited.ComSpec ?? inherited.COMSPEC ?? process.env.ComSpec ?? 'cmd.exe') : '/bin/bash';
4359
+ return { shell: status.env.SHELL || inherited.SHELL || fallback, cwd: resolve(root), env, required };
4149
4360
  }
4150
4361
 
4151
4362
  /** Drop into an interactive subshell with the world active — the contained sibling of `activate`
@@ -4155,7 +4366,7 @@ export function worldShellEnv(name: string, root = process.cwd()): { shell: stri
4155
4366
  * land in the twins; `exit` restores normal (the proxy + its session CA are torn down). The CA is
4156
4367
  * trusted ONLY via this subshell's env — never installed system-wide. See docs/guides/route-a-cli-through-the-world.md. */
4157
4368
  export async function shellWorld(name: string, root = process.cwd()): Promise<number> {
4158
- const { shell, cwd, env } = worldShellEnv(name, root);
4369
+ const { shell, cwd, env, required } = worldShellContext(name, root);
4159
4370
  const resolvedRoot = resolve(root);
4160
4371
  let proxy: Awaited<ReturnType<typeof startRedirectProxy>> | null = null;
4161
4372
  const sealed = env.VOLTER_WORLD_MODE === 'sealed' || env[WORLD_NETWORK_POLICY_ENV] !== undefined;
@@ -4176,13 +4387,25 @@ export async function shellWorld(name: string, root = process.cwd()): Promise<nu
4176
4387
  if (sealed) throw new Error(`world "${name}" requires its network policy proxy: openssl is unavailable; refusing to open a shell`);
4177
4388
  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`);
4178
4389
  }
4390
+ let failed = false;
4391
+ let failure: unknown;
4179
4392
  try {
4393
+ assertRequiredCredentials(required, env, 'interactive shell');
4180
4394
  const result = spawnSync(shell, ['-i'], { windowsHide: true, cwd, env, stdio: 'inherit' });
4181
- return result.status ?? 0;
4395
+ if (result.error) throw result.error;
4396
+ if (result.signal) throw new Error(`World shell ended by ${result.signal}`);
4397
+ if (result.status === null) throw new Error('World shell returned no exit status');
4398
+ return result.status;
4399
+ } catch (error) {
4400
+ failed = true; failure = error;
4401
+ throw error;
4182
4402
  } finally {
4183
4403
  if (proxy) {
4184
- await proxy.close();
4185
- tearDownCa(tlsDir(resolvedRoot, name));
4404
+ try { await proxy.close(); tearDownCa(tlsDir(resolvedRoot, name)); }
4405
+ catch (cleanup) {
4406
+ if (failed) throw new AggregateError([failure, cleanup], `World shell failed (${errorMessage(failure)}); proxy cleanup also failed (${errorMessage(cleanup)})`, { cause: failure });
4407
+ throw cleanup;
4408
+ }
4186
4409
  }
4187
4410
  }
4188
4411
  }
@@ -4446,6 +4669,7 @@ function clearWorldProxyFiles(root: string, name: string): void {
4446
4669
  */
4447
4670
  export function activateScript(name: string, root = process.cwd()): string {
4448
4671
  const status = statusWorld(name, root);
4672
+ const required = recordedRequiredCredentialClaims(status);
4449
4673
  if (!status.running) throw new Error(`World "${name}" is not running — run: volter-world up <config> --env-out <path> --name ${name}`);
4450
4674
  const baseEnv = status.env ?? {};
4451
4675
  // Bring up the ambient redirect proxy (best-effort) and fold its env in so the activated shell
@@ -4465,6 +4689,8 @@ export function activateScript(name: string, root = process.cwd()): string {
4465
4689
  // openssl missing or daemon failed — keep env-only redirect; never fabricate proxy success.
4466
4690
  }
4467
4691
  env = withAppUrl(name, root, env);
4692
+ // Exports follow the bookkeeping assignments; the final prompt assignment is the one later value change.
4693
+ assertRequiredCredentials(required, { ...env, PS1: `(world:${name}) ${env.PS1 ?? ''}` }, 'shell activation including its prompt');
4468
4694
  const keys = Object.keys(env).sort();
4469
4695
  const lines: string[] = [
4470
4696
  `# volter-world activate ${name} — run: eval "$(volter-world activate ${name})"`,