@volter/world-runtime 2.0.18 → 2.0.19

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.
@@ -28,11 +28,13 @@ for (let i = 0; i < argv.length; i++) {
28
28
  const [id, module, exportName, port, root, ...flags] = raw.split('|');
29
29
  const ro = flags.find((f) => f === 'ro');
30
30
  const scenarioPath = flags.find((f) => f.startsWith('scenario='))?.slice('scenario='.length);
31
+ // the World's managed database a pack declaring `managedDatabase` is bound to (runtime.ts, managedDatabaseFor)
32
+ const database = flags.find((f) => f.startsWith('database='))?.slice('database='.length);
31
33
  if (!id || !module || !exportName || !port || !root) {
32
- process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>])\n`);
34
+ process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>][|database=<url>])\n`);
33
35
  process.exit(2);
34
36
  }
35
- specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}) });
37
+ specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}), ...(database ? { database } : {}) });
36
38
  }
37
39
  if (specs.length === 0) {
38
40
  process.stderr.write('no --spec given\n');
@@ -11,6 +11,6 @@ if (typeof factory !== 'function') {
11
11
  throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
12
12
  }
13
13
  // Awaited: an async factory (fly) must post `ready` only once its listener is bound.
14
- const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly });
14
+ const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.database !== undefined ? { database: spec.database } : {}) });
15
15
  parentPort?.postMessage({ type: 'ready', port: server.port });
16
16
  // The Bun.serve listener keeps this worker's event loop alive; nothing else to do.
@@ -11,6 +11,9 @@ export type ColocatedTwinSpec = {
11
11
  /** The world's handlers file for a scenario-carrying (generative) twin — the colocate contract's
12
12
  * scenarioPath (R2a): the same file the spawn path passes as --scenario. */
13
13
  scenarioPath?: string;
14
+ /** The World's managed database a pack declaring `managedDatabase` serves over (runtime.ts, managedDatabaseFor):
15
+ * the colocated form of the spawn path's `serve <arg> <url>`. */
16
+ database?: string;
14
17
  };
15
18
  export type HostIsolation = 'shared' | 'worker';
16
19
  export type ColocatedHost = {
package/dist/src/host.js CHANGED
@@ -62,7 +62,7 @@ export async function startColocatedHost(specs, options = {}) {
62
62
  const servers = [];
63
63
  for (const spec of specs) {
64
64
  const factory = await loadFactory(spec);
65
- servers.push(await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}) }));
65
+ servers.push(await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}), ...(spec.database !== undefined ? { database: spec.database } : {}) }));
66
66
  }
67
67
  return {
68
68
  isolation,
@@ -184,9 +184,6 @@ export declare function packCatalog(root: string): string[];
184
184
  * inngest `INNGEST_BASE_URL` (+ `INNGEST_EVENT_API_BASE_URL` for the event API)
185
185
  * — inngest@4's own apiBaseUrl/eventBaseUrl getters.
186
186
  * ai-gateway `AI_GATEWAY_BASE_URL` — `@ai-sdk/gateway`'s baseURL env.
187
- * supabase `SUPABASE_MGMT_TWIN_URL` — the MANAGEMENT (control-plane) twin only,
188
- * which is what this pack is; the DATA plane is the real local Supabase
189
- * stack and is emitted as an infra stub, not faked.
190
187
  * livekit `LIVEKIT_URL` as a ws:// template — the media plane is a WebSocket
191
188
  * URL the SDK takes as its first constructor argument.
192
189
  *
package/dist/src/init.js CHANGED
@@ -63,9 +63,6 @@ export function packCatalog(root) {
63
63
  * inngest `INNGEST_BASE_URL` (+ `INNGEST_EVENT_API_BASE_URL` for the event API)
64
64
  * — inngest@4's own apiBaseUrl/eventBaseUrl getters.
65
65
  * ai-gateway `AI_GATEWAY_BASE_URL` — `@ai-sdk/gateway`'s baseURL env.
66
- * supabase `SUPABASE_MGMT_TWIN_URL` — the MANAGEMENT (control-plane) twin only,
67
- * which is what this pack is; the DATA plane is the real local Supabase
68
- * stack and is emitted as an infra stub, not faked.
69
66
  * livekit `LIVEKIT_URL` as a ws:// template — the media plane is a WebSocket
70
67
  * URL the SDK takes as its first constructor argument.
71
68
  *
@@ -86,12 +83,6 @@ export const APP_READ_ENDPOINT_ENV = {
86
83
  injectEnv: 'AI_GATEWAY_BASE_URL',
87
84
  note: 'no injector entry: @ai-sdk/gateway is base-URL-configured through AI_GATEWAY_BASE_URL.',
88
85
  },
89
- supabase: {
90
- injectEnv: 'SUPABASE_MGMT_TWIN_URL',
91
- note: 'CONTROL PLANE ONLY — this twin is the Supabase MANAGEMENT API. The DATA plane (Postgres/PostgREST/Storage) '
92
- + 'is the REAL local Supabase stack: see the `//infra` supabase stub, fill in its `supabase start` lifecycle, and '
93
- + 'move it into "services". Until then the proof reports supabase UNINTERCEPTABLE, which is the truth.',
94
- },
95
86
  livekit: {
96
87
  injectEnvTemplates: { LIVEKIT_URL: 'ws://${host}:${port}' },
97
88
  note: 'no injector entry: LIVEKIT_URL is a ws:// endpoint the SDK takes directly — WebSocket media is outside the HTTP injector\'s reach.',
@@ -38,6 +38,12 @@ export type PackFacts = {
38
38
  * declared under ambiguity) by scripts/pack-facts.ts. Consumed by `init` to emit
39
39
  * `colocate:` service entries so a world's twins boot inside ONE host process (R2a). */
40
40
  serveExport?: string;
41
+ /** The World's managed database the runtime binds the twin to at boot (TwinPack.managedDatabase). */
42
+ managedDatabase?: {
43
+ kind: 'postgres';
44
+ arg: string;
45
+ note: string;
46
+ };
41
47
  /** R16: the protocol major the package targets and its standing against the kernel that compiled the artifact. */
42
48
  protocol?: {
43
49
  declared?: string;
@@ -75,6 +75,18 @@ export declare function sharingClaimAlive(claim: SharingClaim): boolean;
75
75
  /** The process's environment as the world admits it: what `stripEnv` names stays outside. */
76
76
  export declare function admittedEnv(strip: string[] | undefined, env?: NodeJS.ProcessEnv): Record<string, string>;
77
77
  export declare function saveWorldInstance(instance: WorldInstance): void;
78
+ /**
79
+ * THE MANAGED-DATABASE BINDING (architecture.md, managed infrastructure: the data plane's declared exception): for a
80
+ * twin whose pack declares `managedDatabase`, the URL of the World's own managed postgres — the service its
81
+ * `world.infrastructure.yml` declares, reached at the loopback URL the World's environment gives it (the connection
82
+ * `volter-world-infra` publishes, infraConnections). Read from the config beside the definition, never from the
83
+ * caller's environment; a World that declares no managed postgres, or whose database URL points outside it, binds
84
+ * nothing (undefined), and the twin refuses its data plane.
85
+ */
86
+ export declare function managedDatabaseFor(vendor: string, configPath: string | undefined, env: Record<string, string | undefined>): {
87
+ arg: string;
88
+ url: string;
89
+ } | undefined;
78
90
  export declare function upWorld(configId: string, options?: UpWorldOptions, abort?: AbortSignal): Promise<WorldInstance>;
79
91
  export declare function downWorld(name: string, root?: string, options?: {
80
92
  graceMs?: number;
@@ -15,6 +15,7 @@ import { constants as osConstants, hostname as osHostname } from 'node:os';
15
15
  import { accessSync, appendFileSync, chmodSync, closeSync, constants as fsConstants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync, existsSync } from 'node:fs';
16
16
  import { basename, dirname, join, relative, resolve, sep } from 'node:path';
17
17
  import { packFacts } from "./pack-facts.js";
18
+ import { infraConnections, parseInfraDefinition } from "./pglite-backing.js";
18
19
  import { describeServiceEnd, readServiceEnd, serviceOutput } from "./service-exit.js";
19
20
  import { Socket } from 'node:net';
20
21
  import { getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core';
@@ -1035,6 +1036,11 @@ async function startService(config, service, worldEnv, paths, allocatedPorts, bo
1035
1036
  const spawnedModule = (service.type ?? 'twin') === 'twin' ? spawnedTwinModule(service, paths.root) : undefined;
1036
1037
  const mounted = spawnedModule !== undefined ? lockfileCheck(service, spawnedModule) : {};
1037
1038
  const args = [...(service.args ?? [])];
1039
+ if ((service.type ?? 'twin') === 'twin') {
1040
+ const binding = managedDatabaseFor(vendorOfModule(spawnedModule, service.id), worldEnv.VOLTER_WORLD_CONFIG, config.env ?? {});
1041
+ if (binding !== undefined)
1042
+ args.push(binding.arg, binding.url);
1043
+ }
1038
1044
  if (service.portArg !== false)
1039
1045
  args.push(service.portArg ?? '--port', String(port));
1040
1046
  if (service.rootArg !== false)
@@ -1185,6 +1191,33 @@ function spawnedTwinModule(service, root) {
1185
1191
  const first = (service.args ?? []).find((a) => /packages\/twin\/[^/]+\//.test(a) || /@volter\/twin-[^/]+\//.test(a));
1186
1192
  return first === undefined ? undefined : (first.startsWith('/') ? first : resolve(root, first));
1187
1193
  }
1194
+ /** The vendor a twin module belongs to (`packages/twin/<vendor>/…` or `@volter/twin-<vendor>/…`), else the service id. */
1195
+ function vendorOfModule(modulePath, serviceId) {
1196
+ const m = modulePath === undefined ? null : (/\/packages\/twin\/([^/]+)\//.exec(modulePath) ?? /@volter\/twin-([^/]+)(\/|$)/.exec(modulePath));
1197
+ return m?.[1] ?? serviceId;
1198
+ }
1199
+ /**
1200
+ * THE MANAGED-DATABASE BINDING (architecture.md, managed infrastructure: the data plane's declared exception): for a
1201
+ * twin whose pack declares `managedDatabase`, the URL of the World's own managed postgres — the service its
1202
+ * `world.infrastructure.yml` declares, reached at the loopback URL the World's environment gives it (the connection
1203
+ * `volter-world-infra` publishes, infraConnections). Read from the config beside the definition, never from the
1204
+ * caller's environment; a World that declares no managed postgres, or whose database URL points outside it, binds
1205
+ * nothing (undefined), and the twin refuses its data plane.
1206
+ */
1207
+ export function managedDatabaseFor(vendor, configPath, env) {
1208
+ const declared = packFacts()[vendor]?.managedDatabase;
1209
+ if (declared === undefined || configPath === undefined)
1210
+ return undefined;
1211
+ const definition = join(dirname(configPath), 'world.infrastructure.yml');
1212
+ if (!existsSync(definition))
1213
+ return undefined;
1214
+ const services = parseInfraDefinition(readFileSync(definition, 'utf8')).filter((service) => service.kind === declared.kind);
1215
+ if (services.length === 0)
1216
+ return undefined;
1217
+ const connections = infraConnections(services, env);
1218
+ const url = Object.keys(connections).sort().map((name) => connections[name])[0];
1219
+ return url === undefined ? undefined : { arg: declared.arg, url };
1220
+ }
1188
1221
  /**
1189
1222
  * The config as the runtime boots it (docs/concepts/worlds.md#the-config-and-the-running-world): a service
1190
1223
  * naming a `package` gets its entry and colocate module resolved from the world root NOW, by
@@ -1247,7 +1280,8 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
1247
1280
  // catalog recorded for the package (pack-facts), refused two majors back, warned one back.
1248
1281
  const mounted = lockfileCheck(service, module);
1249
1282
  const scenario = service.colocate.scenarioPath;
1250
- args.push('--spec', [service.id, module, service.colocate.export, String(port), serviceDataDir, ...(scenario ? [`scenario=${scenario.startsWith('/') ? scenario : resolve(paths.root, scenario)}`] : [])].join('|'));
1283
+ const binding = managedDatabaseFor(vendorOfModule(module, service.id), worldEnv.VOLTER_WORLD_CONFIG, config.env ?? {});
1284
+ args.push('--spec', [service.id, module, service.colocate.export, String(port), serviceDataDir, ...(scenario ? [`scenario=${scenario.startsWith('/') ? scenario : resolve(paths.root, scenario)}`] : []), ...(binding !== undefined ? [`database=${binding.url}`] : [])].join('|'));
1251
1285
  allocated.push({ service, port, mounted });
1252
1286
  }
1253
1287
  const log = join(paths.logs, 'host.log');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.18",
3
+ "version": "2.0.19",
4
4
  "description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
5
5
  "keywords": [
6
6
  "twin",
@@ -67,14 +67,14 @@
67
67
  "node": ">=22.3"
68
68
  },
69
69
  "peerDependencies": {
70
- "@volter/world-core": "2.0.11",
70
+ "@volter/world-core": "2.0.12",
71
71
  "@volter/world-console": "2.0.9"
72
72
  },
73
73
  "dependencies": {
74
74
  "@electric-sql/pglite": "0.5.8",
75
75
  "@electric-sql/pglite-pgvector": "0.0.9",
76
76
  "@volter/world-access": "2.0.1",
77
- "@volter/world-core": "2.0.11",
77
+ "@volter/world-core": "2.0.12",
78
78
  "pg-gateway": "0.3.0-beta.4",
79
79
  "smol-toml": "^1.8.0"
80
80
  },
@@ -84,6 +84,6 @@
84
84
  }
85
85
  },
86
86
  "optionalDependencies": {
87
- "@volter/twin-mongodb": "0.1.11"
87
+ "@volter/twin-mongodb": "0.1.12"
88
88
  }
89
89
  }
package/src/host-cli.ts CHANGED
@@ -30,11 +30,13 @@ for (let i = 0; i < argv.length; i++) {
30
30
  const [id, module, exportName, port, root, ...flags] = raw.split('|');
31
31
  const ro = flags.find((f) => f === 'ro');
32
32
  const scenarioPath = flags.find((f) => f.startsWith('scenario='))?.slice('scenario='.length);
33
+ // the World's managed database a pack declaring `managedDatabase` is bound to (runtime.ts, managedDatabaseFor)
34
+ const database = flags.find((f) => f.startsWith('database='))?.slice('database='.length);
33
35
  if (!id || !module || !exportName || !port || !root) {
34
- process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>])\n`);
36
+ process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>][|database=<url>])\n`);
35
37
  process.exit(2);
36
38
  }
37
- specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}) });
39
+ specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}), ...(database ? { database } : {}) });
38
40
  }
39
41
  if (specs.length === 0) { process.stderr.write('no --spec given\n'); process.exit(2); }
40
42
 
@@ -9,7 +9,7 @@ import { importModule } from './import-module.ts';
9
9
  const spec = workerData as ColocatedTwinSpec;
10
10
 
11
11
  type TwinServer = { port: number; stop: () => void | Promise<void> };
12
- type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean }) => TwinServer | Promise<TwinServer>;
12
+ type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean; database?: string }) => TwinServer | Promise<TwinServer>;
13
13
 
14
14
  const mod = await importModule(spec.module);
15
15
  const factory = mod[spec.export] as TwinServerFactory | undefined;
@@ -18,7 +18,7 @@ if (typeof factory !== 'function') {
18
18
  }
19
19
 
20
20
  // Awaited: an async factory (fly) must post `ready` only once its listener is bound.
21
- const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly });
21
+ const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.database !== undefined ? { database: spec.database } : {}) });
22
22
  parentPort?.postMessage({ type: 'ready', port: server.port });
23
23
 
24
24
  // The Bun.serve listener keeps this worker's event loop alive; nothing else to do.
package/src/host.ts CHANGED
@@ -37,6 +37,9 @@ export type ColocatedTwinSpec = {
37
37
  /** The world's handlers file for a scenario-carrying (generative) twin — the colocate contract's
38
38
  * scenarioPath (R2a): the same file the spawn path passes as --scenario. */
39
39
  scenarioPath?: string;
40
+ /** The World's managed database a pack declaring `managedDatabase` serves over (runtime.ts, managedDatabaseFor):
41
+ * the colocated form of the spawn path's `serve <arg> <url>`. */
42
+ database?: string;
40
43
  };
41
44
 
42
45
  export type HostIsolation = 'shared' | 'worker';
@@ -55,7 +58,7 @@ export type ColocatedHost = {
55
58
  // made host.stop() throw `s.stop is not a function`, turning every SIGTERM into downWorld's
56
59
  // SIGKILL escalation for the whole world).
57
60
  type TwinServer = { port: number; stop: () => void | Promise<void> };
58
- type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean; scenarioPath?: string }) => TwinServer | Promise<TwinServer>;
61
+ type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean; scenarioPath?: string; database?: string }) => TwinServer | Promise<TwinServer>;
59
62
 
60
63
  async function loadFactory(spec: ColocatedTwinSpec): Promise<TwinServerFactory> {
61
64
  const mod = await importModule(spec.module);
@@ -103,7 +106,7 @@ export async function startColocatedHost(
103
106
  const servers: TwinServer[] = [];
104
107
  for (const spec of specs) {
105
108
  const factory = await loadFactory(spec);
106
- servers.push(await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}) }));
109
+ servers.push(await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}), ...(spec.database !== undefined ? { database: spec.database } : {}) }));
107
110
  }
108
111
  return {
109
112
  isolation,
package/src/init.ts CHANGED
@@ -244,9 +244,6 @@ export function packCatalog(root: string): string[] {
244
244
  * inngest `INNGEST_BASE_URL` (+ `INNGEST_EVENT_API_BASE_URL` for the event API)
245
245
  * — inngest@4's own apiBaseUrl/eventBaseUrl getters.
246
246
  * ai-gateway `AI_GATEWAY_BASE_URL` — `@ai-sdk/gateway`'s baseURL env.
247
- * supabase `SUPABASE_MGMT_TWIN_URL` — the MANAGEMENT (control-plane) twin only,
248
- * which is what this pack is; the DATA plane is the real local Supabase
249
- * stack and is emitted as an infra stub, not faked.
250
247
  * livekit `LIVEKIT_URL` as a ws:// template — the media plane is a WebSocket
251
248
  * URL the SDK takes as its first constructor argument.
252
249
  *
@@ -267,12 +264,6 @@ export const APP_READ_ENDPOINT_ENV: Record<string, { injectEnv?: string; injectE
267
264
  injectEnv: 'AI_GATEWAY_BASE_URL',
268
265
  note: 'no injector entry: @ai-sdk/gateway is base-URL-configured through AI_GATEWAY_BASE_URL.',
269
266
  },
270
- supabase: {
271
- injectEnv: 'SUPABASE_MGMT_TWIN_URL',
272
- note: 'CONTROL PLANE ONLY — this twin is the Supabase MANAGEMENT API. The DATA plane (Postgres/PostgREST/Storage) '
273
- + 'is the REAL local Supabase stack: see the `//infra` supabase stub, fill in its `supabase start` lifecycle, and '
274
- + 'move it into "services". Until then the proof reports supabase UNINTERCEPTABLE, which is the truth.',
275
- },
276
267
  livekit: {
277
268
  injectEnvTemplates: { LIVEKIT_URL: 'ws://${host}:${port}' },
278
269
  note: 'no injector entry: LIVEKIT_URL is a ws:// endpoint the SDK takes directly — WebSocket media is outside the HTTP injector\'s reach.',
package/src/pack-facts.ts CHANGED
@@ -25,6 +25,8 @@ export type PackFacts = {
25
25
  * declared under ambiguity) by scripts/pack-facts.ts. Consumed by `init` to emit
26
26
  * `colocate:` service entries so a world's twins boot inside ONE host process (R2a). */
27
27
  serveExport?: string;
28
+ /** The World's managed database the runtime binds the twin to at boot (TwinPack.managedDatabase). */
29
+ managedDatabase?: { kind: 'postgres'; arg: string; note: string };
28
30
  /** R16: the protocol major the package targets and its standing against the kernel that compiled the artifact. */
29
31
  protocol?: { declared?: string; major: number; standing: 'current' | 'deprecated' | 'refused' | 'assumed' };
30
32
  };
package/src/runtime.ts CHANGED
@@ -15,6 +15,7 @@ import { constants as osConstants, hostname as osHostname } from 'node:os';
15
15
  import { accessSync, appendFileSync, chmodSync, closeSync, constants as fsConstants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync, existsSync } from 'node:fs';
16
16
  import { basename, dirname, join, relative, resolve, sep } from 'node:path';
17
17
  import { packFacts } from './pack-facts.ts';
18
+ import { infraConnections, parseInfraDefinition } from './pglite-backing.ts';
18
19
  import { describeServiceEnd, readServiceEnd, serviceOutput } from './service-exit.ts';
19
20
  import { Socket } from 'node:net';
20
21
  import { getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core';
@@ -1073,6 +1074,10 @@ async function startService(
1073
1074
  const mounted: MountedPackage = spawnedModule !== undefined ? lockfileCheck(service, spawnedModule) : {};
1074
1075
 
1075
1076
  const args = [...(service.args ?? [])];
1077
+ if ((service.type ?? 'twin') === 'twin') {
1078
+ const binding = managedDatabaseFor(vendorOfModule(spawnedModule, service.id), worldEnv.VOLTER_WORLD_CONFIG, config.env ?? {});
1079
+ if (binding !== undefined) args.push(binding.arg, binding.url);
1080
+ }
1076
1081
  if (service.portArg !== false) args.push(service.portArg ?? '--port', String(port));
1077
1082
  if (service.rootArg !== false) args.push(service.rootArg ?? '--root', serviceDataDir);
1078
1083
 
@@ -1205,6 +1210,32 @@ function spawnedTwinModule(service: WorldServiceConfig, root: string): string |
1205
1210
  return first === undefined ? undefined : (first.startsWith('/') ? first : resolve(root, first));
1206
1211
  }
1207
1212
 
1213
+ /** The vendor a twin module belongs to (`packages/twin/<vendor>/…` or `@volter/twin-<vendor>/…`), else the service id. */
1214
+ function vendorOfModule(modulePath: string | undefined, serviceId: string): string {
1215
+ const m = modulePath === undefined ? null : (/\/packages\/twin\/([^/]+)\//.exec(modulePath) ?? /@volter\/twin-([^/]+)(\/|$)/.exec(modulePath));
1216
+ return m?.[1] ?? serviceId;
1217
+ }
1218
+
1219
+ /**
1220
+ * THE MANAGED-DATABASE BINDING (architecture.md, managed infrastructure: the data plane's declared exception): for a
1221
+ * twin whose pack declares `managedDatabase`, the URL of the World's own managed postgres — the service its
1222
+ * `world.infrastructure.yml` declares, reached at the loopback URL the World's environment gives it (the connection
1223
+ * `volter-world-infra` publishes, infraConnections). Read from the config beside the definition, never from the
1224
+ * caller's environment; a World that declares no managed postgres, or whose database URL points outside it, binds
1225
+ * nothing (undefined), and the twin refuses its data plane.
1226
+ */
1227
+ export function managedDatabaseFor(vendor: string, configPath: string | undefined, env: Record<string, string | undefined>): { arg: string; url: string } | undefined {
1228
+ const declared = packFacts()[vendor]?.managedDatabase;
1229
+ if (declared === undefined || configPath === undefined) return undefined;
1230
+ const definition = join(dirname(configPath), 'world.infrastructure.yml');
1231
+ if (!existsSync(definition)) return undefined;
1232
+ const services = parseInfraDefinition(readFileSync(definition, 'utf8')).filter((service) => service.kind === declared.kind);
1233
+ if (services.length === 0) return undefined;
1234
+ const connections = infraConnections(services, env);
1235
+ const url = Object.keys(connections).sort().map((name) => connections[name]!)[0];
1236
+ return url === undefined ? undefined : { arg: declared.arg, url };
1237
+ }
1238
+
1208
1239
  /**
1209
1240
  * The config as the runtime boots it (docs/concepts/worlds.md#the-config-and-the-running-world): a service
1210
1241
  * naming a `package` gets its entry and colocate module resolved from the world root NOW, by
@@ -1272,7 +1303,8 @@ async function startColocatedServices(
1272
1303
  // catalog recorded for the package (pack-facts), refused two majors back, warned one back.
1273
1304
  const mounted = lockfileCheck(service, module);
1274
1305
  const scenario = service.colocate!.scenarioPath;
1275
- args.push('--spec', [service.id, module, service.colocate!.export, String(port), serviceDataDir, ...(scenario ? [`scenario=${scenario.startsWith('/') ? scenario : resolve(paths.root, scenario)}`] : [])].join('|'));
1306
+ const binding = managedDatabaseFor(vendorOfModule(module, service.id), worldEnv.VOLTER_WORLD_CONFIG, config.env ?? {});
1307
+ args.push('--spec', [service.id, module, service.colocate!.export, String(port), serviceDataDir, ...(scenario ? [`scenario=${scenario.startsWith('/') ? scenario : resolve(paths.root, scenario)}`] : []), ...(binding !== undefined ? [`database=${binding.url}`] : [])].join('|'));
1276
1308
  allocated.push({ service, port, mounted });
1277
1309
  }
1278
1310