@volter/twin-world 0.1.0 → 0.1.2

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.
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ // One managed-infrastructure postgres service, served without a container:
3
+ // PGlite (real Postgres compiled to WASM) behind a pg-gateway wire-protocol
4
+ // front. Spawned by `volter-world-managed-infra` when the pglite backing is
5
+ // selected; never operated directly (the same contract as the compose file).
6
+ //
7
+ // Plain .mjs under node, deliberately: PGlite's WASM init does not complete
8
+ // under bun, and under node its create() promise only progresses while
9
+ // something holds the event loop — an interval carries the init, and the TCP
10
+ // server starts listening only once the backend is ready, so a port that
11
+ // answers is a port that serves.
12
+ //
13
+ // PGlite is a SINGLE-SESSION backend, so this host serializes connections
14
+ // with a transaction-affinity lock: a connection acquires the global lock at
15
+ // its first protocol message and keeps it for as long as the backend reports
16
+ // an open transaction (ReadyForQuery status != 'I'). Without the hold, two
17
+ // pooled clients interleave into one session and a ROLLBACK on one swallows
18
+ // the other's writes. A connection that dies mid-transaction is rolled back
19
+ // before the lock is released.
20
+ //
21
+ // Identity is aliased, not faked: trust auth accepts the world's declared
22
+ // user/password/database, but the backend is PGlite's single `postgres`
23
+ // database — apps that introspect current_database() will see that. Worlds
24
+ // accept this; an app needing true multi-database Postgres needs the
25
+ // container backing.
26
+ import net from 'node:net';
27
+ import { mkdirSync } from 'node:fs';
28
+ import { PGlite } from '@electric-sql/pglite';
29
+ import { fromNodeSocket } from 'pg-gateway/node';
30
+
31
+ function arg(name) {
32
+ const index = process.argv.indexOf(name);
33
+ const value = index >= 0 ? process.argv[index + 1] : undefined;
34
+ if (!value) {
35
+ process.stderr.write(`pglite-host: ${name} is required\n`);
36
+ process.exit(2);
37
+ }
38
+ return value;
39
+ }
40
+
41
+ const port = Number(arg('--port'));
42
+ const dataDir = arg('--data');
43
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
44
+ process.stderr.write('pglite-host: --port must be 1-65535\n');
45
+ process.exit(2);
46
+ }
47
+
48
+ mkdirSync(dataDir, { recursive: true });
49
+ // stdio may be a log fd whose reader is gone; never die on a status line
50
+ process.stdout.on('error', () => {});
51
+ process.stderr.on('error', () => {});
52
+ // PGlite's create() only progresses while something holds the event loop;
53
+ // listening starts AFTER readiness so "port answers" means "serves queries"
54
+ const initKeepAlive = setInterval(() => {}, 500);
55
+ const dbReady = PGlite.create({ dataDir });
56
+ dbReady.catch((error) => {
57
+ process.stderr.write(`pglite-host: backend failed to start: ${error?.message ?? error}\n`);
58
+ process.exit(1);
59
+ });
60
+
61
+ // ---- transaction-affinity lock ---------------------------------------------
62
+ let holder = null;
63
+ const waiters = [];
64
+ async function acquire(token) {
65
+ while (holder !== null && holder !== token) {
66
+ await new Promise((resolve) => waiters.push(resolve));
67
+ }
68
+ holder = token;
69
+ }
70
+ function release(token) {
71
+ if (holder !== token) return;
72
+ holder = null;
73
+ const next = waiters.shift();
74
+ if (next) next();
75
+ }
76
+
77
+ /** The transaction status of the LAST ReadyForQuery ('Z') message in a raw
78
+ * protocol response, or null when the response carries none (mid-pipeline:
79
+ * the sender keeps the lock). 'I' = idle, 'T' = in transaction, 'E' = failed
80
+ * transaction. */
81
+ function lastReadyStatus(response) {
82
+ let status = null;
83
+ let offset = 0;
84
+ while (offset + 5 <= response.length) {
85
+ const type = response[offset];
86
+ const length = (response[offset + 1] << 24) | (response[offset + 2] << 16)
87
+ | (response[offset + 3] << 8) | response[offset + 4];
88
+ if (length < 4) break; // malformed; stop scanning rather than loop
89
+ if (type === 0x5a /* 'Z' */ && offset + 5 < response.length) {
90
+ status = String.fromCharCode(response[offset + 5]);
91
+ }
92
+ offset += 1 + length;
93
+ }
94
+ return status;
95
+ }
96
+
97
+ const server = net.createServer(async (socket) => {
98
+ const token = {};
99
+ let holding = false;
100
+ socket.on('close', () => {
101
+ if (!holding) return;
102
+ // the client vanished inside its transaction: roll it back, then let the
103
+ // next connection in — never leak a half-open transaction into a stranger
104
+ void dbReady
105
+ .then((db) => db.query('ROLLBACK'))
106
+ .catch(() => {})
107
+ .then(() => {
108
+ holding = false;
109
+ release(token);
110
+ });
111
+ });
112
+ socket.on('error', () => { /* close handler owns cleanup */ });
113
+ await fromNodeSocket(socket, {
114
+ serverVersion: '18.3 (PGlite)',
115
+ auth: { method: 'trust' },
116
+ async onMessage(data, { isAuthenticated }) {
117
+ if (!isAuthenticated) return undefined; // gateway owns startup/auth traffic
118
+ const db = await dbReady;
119
+ await acquire(token);
120
+ holding = true;
121
+ const response = await db.execProtocolRaw(data);
122
+ const status = lastReadyStatus(response);
123
+ if (status === 'I') {
124
+ holding = false;
125
+ release(token);
126
+ }
127
+ return response;
128
+ },
129
+ });
130
+ });
131
+
132
+ void dbReady.then(() => {
133
+ clearInterval(initKeepAlive);
134
+ server.listen(port, '127.0.0.1', () => {
135
+ process.stdout.write(`pglite-host serving postgres wire protocol on 127.0.0.1:${port}\n`);
136
+ });
137
+ });
138
+
139
+ for (const signal of ['SIGTERM', 'SIGINT']) {
140
+ process.on(signal, () => {
141
+ server.close(() => {
142
+ void dbReady.then((db) => db.close()).finally(() => process.exit(0));
143
+ });
144
+ // a hung close must not make `down` hang the world
145
+ setTimeout(() => process.exit(0), 3000).unref();
146
+ });
147
+ }
@@ -1,9 +1,6 @@
1
- import { existsSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
1
  import { spawnSync } from 'node:child_process';
5
2
 
6
- export type PrerequisiteId = 'docker';
3
+ export type PrerequisiteId = 'local-execution';
7
4
 
8
5
  export type PrerequisiteCheck = {
9
6
  id: PrerequisiteId;
@@ -26,75 +23,35 @@ const realRunner: Runner = (cmd, args) => {
26
23
  };
27
24
  };
28
25
 
29
- function trim(value: string): string {
30
- return value.trim();
31
- }
32
-
33
- function dockerContextHost(runner: Runner): { context?: string; host?: string } {
34
- const contextResult = runner('docker', ['context', 'show']);
35
- const context = contextResult.status === 0 ? trim(contextResult.stdout) : undefined;
36
- if (!context) return {};
37
-
38
- const inspect = runner('docker', ['context', 'inspect', context]);
39
- if (inspect.status !== 0 || !inspect.stdout.trim()) return { context };
40
- try {
41
- const parsed = JSON.parse(inspect.stdout) as Array<{ Endpoints?: { docker?: { Host?: string } } }>;
42
- return { context, host: parsed[0]?.Endpoints?.docker?.Host };
43
- } catch {
44
- return { context };
45
- }
46
- }
47
-
48
- function commonDockerGuidance(details: Record<string, string>): string[] {
49
- const guidance = [
50
- 'Start the computer-native Docker-compatible runtime before running this world.',
51
- 'If you are an AI agent running this command, it is your job to start that local runtime now; do not hand this back to the user unless the runtime cannot be started from the machine.',
52
- 'Agents should boot the runtime installed on this machine: `colima start` for Colima, open/start Docker Desktop for Docker Desktop, or start the native Docker service on Linux.',
53
- ];
54
-
55
- if (details.context || details.host) {
56
- guidance.push(`Active Docker context${details.context ? ` '${details.context}'` : ''}${details.host ? ` points at ${details.host}` : ''}.`);
57
- }
58
-
59
- const colimaSocket = join(homedir(), '.colima', 'default', 'docker.sock');
60
- if (existsSync(colimaSocket)) {
61
- guidance.push(`A Colima socket exists at ${colimaSocket}; if the Docker context points elsewhere, update the context or export DOCKER_HOST=unix://${colimaSocket}.`);
62
- }
63
-
64
- return guidance;
65
- }
66
-
67
- export function checkDockerRuntime(runner: Runner = realRunner): PrerequisiteCheck {
26
+ /** Private implementation probe behind the public World capability name. Its implementation and
27
+ * diagnostics never become instructions for application agents. */
28
+ export function checkLocalExecution(runner: Runner = realRunner): PrerequisiteCheck {
68
29
  const info = runner('docker', ['info']);
69
30
  if (info.status === 0) {
70
31
  return {
71
- id: 'docker',
32
+ id: 'local-execution',
72
33
  ok: true,
73
- label: 'Docker-compatible daemon',
74
- message: 'Docker-compatible daemon is reachable.',
34
+ label: 'Local execution',
35
+ message: 'Local execution capacity is available.',
75
36
  guidance: [],
76
37
  };
77
38
  }
78
39
 
79
- const context = dockerContextHost(runner);
80
- const details: Record<string, string> = {};
81
- if (context.context) details.context = context.context;
82
- if (context.host) details.host = context.host;
83
- if (info.stderr.trim()) details.error = info.stderr.trim();
84
-
85
40
  return {
86
- id: 'docker',
41
+ id: 'local-execution',
87
42
  ok: false,
88
- label: 'Docker-compatible daemon',
89
- message: 'Docker-compatible daemon is not reachable.',
90
- guidance: commonDockerGuidance(details),
91
- details,
43
+ label: 'Local execution',
44
+ message: 'Local execution capacity is unavailable.',
45
+ guidance: [
46
+ 'Run the World through `volter-world up`, then use `volter-world doctor <world>` and its declared service log if startup still fails.',
47
+ 'Do not start, inspect, or clean up a backing runtime separately; the declared World service owns it.',
48
+ ],
92
49
  };
93
50
  }
94
51
 
95
52
  export function checkPrerequisites(ids: PrerequisiteId[], runner: Runner = realRunner): PrerequisiteCheck[] {
96
53
  return ids.map((id) => {
97
- if (id === 'docker') return checkDockerRuntime(runner);
54
+ if (id === 'local-execution') return checkLocalExecution(runner);
98
55
  throw new Error(`Unknown prerequisite: ${id}`);
99
56
  });
100
57
  }