@addai/node 0.11.3 → 0.13.0

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,214 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.reconcileAction = reconcileAction;
37
+ exports.listDesktops = listDesktops;
38
+ exports.setStatus = setStatus;
39
+ exports.allocateVncPort = allocateVncPort;
40
+ exports.ensureDirs = ensureDirs;
41
+ exports.startDesktopManager = startDesktopManager;
42
+ exports.stopDesktopManager = stopDesktopManager;
43
+ exports.ensureRunning = ensureRunning;
44
+ // Keeps the desktops the server believes in and the containers that actually
45
+ // exist in agreement. Modelled on projects.ts: a slow poll, no realtime, and
46
+ // every transition reported back so a Studio row reflects the machine.
47
+ const fs = __importStar(require("fs"));
48
+ const net = __importStar(require("net"));
49
+ const supabase_client_1 = require("../supabase-client");
50
+ const store_1 = require("../store");
51
+ const spec_1 = require("./spec");
52
+ const docker_1 = require("./docker");
53
+ // Desktops are not latency-sensitive: a container that died is a rare event
54
+ // and 15s to notice it is fine. Slower interval = less pooler pressure.
55
+ const POLL_INTERVAL_MS = 15_000;
56
+ /** Pure: given what the row claims and what the engine reports, what now?
57
+ * `actual` is null when no such container exists. */
58
+ function reconcileAction(row, actual) {
59
+ // In-flight and terminal states belong to the command runner, not here -
60
+ // reconciling mid-create would fight the thing doing the creating.
61
+ if (row.status === 'creating' || row.status === 'starting'
62
+ || row.status === 'deleting' || row.status === 'pending'
63
+ || row.status === 'failed')
64
+ return 'none';
65
+ if (actual === null) {
66
+ // The row says this exists and it does not. Someone pruned it by hand.
67
+ return row.status === 'running' || row.status === 'stopped' ? 'mark_failed' : 'none';
68
+ }
69
+ if (row.status === 'running' && !actual.running) {
70
+ return row.autostart ? 'start' : 'mark_stopped';
71
+ }
72
+ if (row.status === 'stopped' && actual.running)
73
+ return 'mark_running';
74
+ return 'none';
75
+ }
76
+ function token() { return (0, store_1.readPairing)()?.daemonToken ?? null; }
77
+ async function listDesktops() {
78
+ const t = token();
79
+ if (!t)
80
+ return [];
81
+ return (await (0, supabase_client_1.rpc)('runtime_desktops_list', { p_token: t })) ?? [];
82
+ }
83
+ async function setStatus(id, fields) {
84
+ const t = token();
85
+ if (!t)
86
+ return;
87
+ try {
88
+ await (0, supabase_client_1.rpc)('runtime_desktop_status_update', {
89
+ p_token: t, p_desktop_id: id,
90
+ p_status: fields.status ?? null,
91
+ p_status_message: fields.status_message ?? null,
92
+ p_container_id: fields.container_id ?? null,
93
+ p_engine: fields.engine ?? null,
94
+ p_vnc_port: fields.vnc_port ?? null,
95
+ });
96
+ }
97
+ catch (err) {
98
+ console.error('[desktops] status update failed:', err.message);
99
+ }
100
+ }
101
+ /** A free loopback port for this desktop's VNC. Asking the OS for port 0 and
102
+ * reading back what it bound is the only race-free way to pick one. */
103
+ function allocateVncPort() {
104
+ return new Promise((resolve, reject) => {
105
+ const srv = net.createServer();
106
+ srv.once('error', reject);
107
+ srv.listen(0, '127.0.0.1', () => {
108
+ const port = srv.address().port;
109
+ srv.close(() => resolve(port));
110
+ });
111
+ });
112
+ }
113
+ function ensureDirs(desktopId) {
114
+ const confDir = (0, spec_1.confDirFor)(desktopId);
115
+ const workDir = (0, spec_1.workDirFor)(desktopId);
116
+ fs.mkdirSync(confDir, { recursive: true, mode: 0o700 });
117
+ fs.mkdirSync(workDir, { recursive: true, mode: 0o700 });
118
+ return { confDir, workDir };
119
+ }
120
+ let timer = null;
121
+ let inflight = false;
122
+ async function tick() {
123
+ if (inflight)
124
+ return;
125
+ inflight = true;
126
+ try {
127
+ const provider = await (0, docker_1.getProvider)();
128
+ if (!provider)
129
+ return; // no engine: nothing to reconcile
130
+ const rows = await listDesktops();
131
+ for (const row of rows) {
132
+ const actual = await provider.inspect(row);
133
+ const action = reconcileAction(row, actual);
134
+ if (action === 'none')
135
+ continue;
136
+ try {
137
+ if (action === 'start') {
138
+ await provider.start(row);
139
+ await setStatus(row.id, { status: 'running', status_message: null });
140
+ }
141
+ else if (action === 'mark_failed') {
142
+ await setStatus(row.id, {
143
+ status: 'failed',
144
+ status_message: 'Container no longer exists on this machine. Rebuild to recreate it.',
145
+ });
146
+ }
147
+ else if (action === 'mark_stopped') {
148
+ await setStatus(row.id, { status: 'stopped' });
149
+ }
150
+ else if (action === 'mark_running') {
151
+ await setStatus(row.id, {
152
+ status: 'running', container_id: actual?.containerId ?? null, status_message: null,
153
+ });
154
+ }
155
+ }
156
+ catch (err) {
157
+ await setStatus(row.id, {
158
+ status: 'failed', status_message: err.message.slice(0, 400),
159
+ });
160
+ }
161
+ }
162
+ }
163
+ catch (err) {
164
+ console.error('[desktops] reconcile failed:', err.message);
165
+ }
166
+ finally {
167
+ inflight = false;
168
+ }
169
+ }
170
+ function startDesktopManager() {
171
+ if (timer)
172
+ return;
173
+ void tick();
174
+ timer = setInterval(() => { void tick(); }, POLL_INTERVAL_MS);
175
+ }
176
+ function stopDesktopManager() {
177
+ if (timer) {
178
+ clearInterval(timer);
179
+ timer = null;
180
+ }
181
+ }
182
+ /** Bring a desktop up and wait for it, for the run path. Returns the row when
183
+ * running, null when it could not be started - the caller then falls back to
184
+ * the host rather than failing the run. */
185
+ async function ensureRunning(desktopId, timeoutMs = 60_000) {
186
+ const provider = await (0, docker_1.getProvider)();
187
+ if (!provider)
188
+ return null;
189
+ const rows = await listDesktops();
190
+ const row = rows.find(r => r.id === desktopId);
191
+ if (!row)
192
+ return null;
193
+ const actual = await provider.inspect(row);
194
+ if (actual?.running)
195
+ return row;
196
+ if (!actual)
197
+ return null; // never created / pruned: cannot start
198
+ const deadline = Date.now() + timeoutMs;
199
+ try {
200
+ await provider.start(row);
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ while (Date.now() < deadline) {
206
+ const now = await provider.inspect(row);
207
+ if (now?.running) {
208
+ await setStatus(row.id, { status: 'running', status_message: null });
209
+ return row;
210
+ }
211
+ await new Promise(r => setTimeout(r, 1_000));
212
+ }
213
+ return null;
214
+ }
@@ -0,0 +1,35 @@
1
+ import { DesktopRow } from './spec';
2
+ export interface EngineInfo {
3
+ id: 'docker' | 'podman';
4
+ version: string;
5
+ }
6
+ export interface ExecOpts {
7
+ cwd: string;
8
+ env: Record<string, string>;
9
+ /** PTY-driven harnesses need a tty allocated inside the container. */
10
+ tty: boolean;
11
+ }
12
+ export interface CreateOpts {
13
+ confDir: string;
14
+ workDir?: string;
15
+ vncPort: number;
16
+ }
17
+ export interface DesktopProvider {
18
+ readonly id: 'docker' | 'podman';
19
+ probe(): Promise<EngineInfo | null>;
20
+ create(row: DesktopRow, opts: CreateOpts, onLog: (chunk: string) => void): Promise<string>;
21
+ start(row: DesktopRow): Promise<void>;
22
+ stop(row: DesktopRow): Promise<void>;
23
+ remove(row: DesktopRow): Promise<void>;
24
+ /** null when the container does not exist. */
25
+ inspect(row: DesktopRow): Promise<{
26
+ running: boolean;
27
+ containerId: string;
28
+ } | null>;
29
+ }
30
+ /** Docker/Podman object names allow [a-zA-Z0-9][a-zA-Z0-9_.-]*. The id suffix
31
+ * keeps a renamed desktop from colliding with a leftover container of the
32
+ * same name. */
33
+ export declare function containerName(row: Pick<DesktopRow, 'id' | 'name'>): string;
34
+ export declare function buildCreateArgs(row: DesktopRow, opts: CreateOpts): string[];
35
+ export declare function buildExecArgs(row: DesktopRow, cmd: string[], opts: ExecOpts): string[];
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.containerName = containerName;
4
+ exports.buildCreateArgs = buildCreateArgs;
5
+ exports.buildExecArgs = buildExecArgs;
6
+ // The interface every desktop backend implements, plus the pure command
7
+ // builders. Kept free of I/O so the argv construction - the part most likely
8
+ // to be subtly wrong, and the part that behaves differently on Windows - is
9
+ // unit-testable with no container engine installed.
10
+ const spec_1 = require("./spec");
11
+ /** Docker/Podman object names allow [a-zA-Z0-9][a-zA-Z0-9_.-]*. The id suffix
12
+ * keeps a renamed desktop from colliding with a leftover container of the
13
+ * same name. */
14
+ function containerName(row) {
15
+ const slug = row.name.toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ .replace(/^-+|-+$/g, '')
18
+ .slice(0, 32) || 'desktop';
19
+ return `ainode-desktop-${slug}-${row.id.slice(0, 8)}`;
20
+ }
21
+ function buildCreateArgs(row, opts) {
22
+ const args = [
23
+ 'run', '-d',
24
+ '--name', containerName(row),
25
+ '--cpus', String(row.cpus),
26
+ '--memory', `${row.memory_mb}m`,
27
+ '--shm-size', '512m', // Chromium crashes on the 64m default
28
+ '--restart', 'unless-stopped',
29
+ // Loopback ONLY. Binding 0.0.0.0 would expose a logged-in desktop to the
30
+ // whole LAN; the relay reaches it from the daemon on the same host.
31
+ '-p', `127.0.0.1:${opts.vncPort}:${spec_1.VNC_PORT_IN_CONTAINER}`,
32
+ '-v', `${opts.confDir}:${spec_1.CONF_ROOT}`,
33
+ ];
34
+ if (opts.workDir)
35
+ args.push('-v', `${opts.workDir}:${spec_1.WORK_ROOT}`);
36
+ if (row.vnc_password)
37
+ args.push('-e', `VNC_PASSWORD=${row.vnc_password}`);
38
+ args.push(row.image);
39
+ return args;
40
+ }
41
+ function buildExecArgs(row, cmd, opts) {
42
+ const args = ['exec', '-i'];
43
+ if (opts.tty)
44
+ args.push('-t');
45
+ args.push('-w', opts.cwd);
46
+ for (const [k, v] of Object.entries(opts.env))
47
+ args.push('-e', `${k}=${v}`);
48
+ args.push(containerName(row));
49
+ // cmd goes in as separate argv entries - never joined into a shell string.
50
+ // A prompt containing quotes, newlines or ESC bytes must survive verbatim.
51
+ args.push(...cmd);
52
+ return args;
53
+ }
@@ -0,0 +1,2 @@
1
+ export declare function startRelayClient(): void;
2
+ export declare function stopRelayClient(): void;
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.startRelayClient = startRelayClient;
40
+ exports.stopRelayClient = stopRelayClient;
41
+ // Dial-out socket to the desktop relay.
42
+ //
43
+ // The daemon connects OUT whenever it has a running desktop, so there is no
44
+ // inbound port and no NAT problem - and because the socket is already open,
45
+ // attaching a viewer is instant rather than waiting on the 30s heartbeat.
46
+ //
47
+ // It carries RAW RFB bytes. The container serves RFB directly (no websockify),
48
+ // so noVNC in the browser speaks RFB across this pipe unchanged.
49
+ const ws_1 = __importDefault(require("ws"));
50
+ const net = __importStar(require("net"));
51
+ const store_1 = require("../store");
52
+ const manager_1 = require("./manager");
53
+ const RELAY_URL = process.env.AINODE_RELAY_URL ?? 'wss://desktop-relay.add.ai';
54
+ const RECONNECT_MIN_MS = 2_000;
55
+ const RECONNECT_MAX_MS = 60_000;
56
+ /** Re-check whether any desktop is running this often while idle. */
57
+ const IDLE_CHECK_MS = 60_000;
58
+ let ws = null;
59
+ let stopped = true;
60
+ let backoff = RECONNECT_MIN_MS;
61
+ let idleTimer = null;
62
+ /** desktopId -> the local TCP socket to that desktop's published RFB port. */
63
+ const bridges = new Map();
64
+ function dropBridges() {
65
+ for (const s of bridges.values()) {
66
+ try {
67
+ s.destroy();
68
+ }
69
+ catch { /* gone */ }
70
+ }
71
+ bridges.clear();
72
+ }
73
+ async function anyDesktopRunning() {
74
+ try {
75
+ return (await (0, manager_1.listDesktops)()).some(d => d.status === 'running');
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ function scheduleReconnect() {
82
+ if (stopped)
83
+ return;
84
+ setTimeout(() => { void connect(); }, backoff);
85
+ backoff = Math.min(backoff * 2, RECONNECT_MAX_MS);
86
+ }
87
+ async function connect() {
88
+ if (stopped || ws)
89
+ return;
90
+ const token = (0, store_1.readPairing)()?.daemonToken;
91
+ if (!token) {
92
+ scheduleReconnect();
93
+ return;
94
+ }
95
+ // No point holding a socket open for a machine with nothing to show.
96
+ if (!(await anyDesktopRunning())) {
97
+ idleTimer = setTimeout(() => { void connect(); }, IDLE_CHECK_MS);
98
+ return;
99
+ }
100
+ const sock = new ws_1.default(`${RELAY_URL}/node?token=${encodeURIComponent(token)}`);
101
+ ws = sock;
102
+ sock.on('open', () => { backoff = RECONNECT_MIN_MS; });
103
+ sock.on('message', async (raw) => {
104
+ let msg;
105
+ try {
106
+ msg = JSON.parse(raw.toString());
107
+ }
108
+ catch {
109
+ return;
110
+ }
111
+ if (msg.type === 'attach') {
112
+ const row = (await (0, manager_1.listDesktops)()).find(d => d.id === msg.desktopId);
113
+ if (!row?.vnc_port)
114
+ return;
115
+ // Loopback only - the container published its RFB on 127.0.0.1 and this
116
+ // process is the only thing on the machine that reaches it.
117
+ const tcp = net.connect(row.vnc_port, '127.0.0.1');
118
+ tcp.on('data', chunk => {
119
+ if (sock.readyState === ws_1.default.OPEN) {
120
+ sock.send(JSON.stringify({
121
+ type: 'data', desktopId: msg.desktopId, b64: chunk.toString('base64'),
122
+ }));
123
+ }
124
+ });
125
+ const forget = () => { bridges.delete(msg.desktopId); };
126
+ tcp.on('close', forget);
127
+ tcp.on('error', forget);
128
+ bridges.get(msg.desktopId)?.destroy();
129
+ bridges.set(msg.desktopId, tcp);
130
+ return;
131
+ }
132
+ if (msg.type === 'data' && msg.b64) {
133
+ bridges.get(msg.desktopId)?.write(Buffer.from(msg.b64, 'base64'));
134
+ return;
135
+ }
136
+ if (msg.type === 'detach') {
137
+ bridges.get(msg.desktopId)?.destroy();
138
+ bridges.delete(msg.desktopId);
139
+ }
140
+ });
141
+ const retry = () => {
142
+ if (ws !== sock)
143
+ return; // superseded by a newer socket
144
+ dropBridges();
145
+ ws = null;
146
+ scheduleReconnect();
147
+ };
148
+ sock.on('close', retry);
149
+ sock.on('error', retry);
150
+ }
151
+ function startRelayClient() {
152
+ if (!stopped)
153
+ return;
154
+ stopped = false;
155
+ backoff = RECONNECT_MIN_MS;
156
+ void connect();
157
+ }
158
+ function stopRelayClient() {
159
+ stopped = true;
160
+ if (idleTimer) {
161
+ clearTimeout(idleTimer);
162
+ idleTimer = null;
163
+ }
164
+ dropBridges();
165
+ try {
166
+ ws?.close();
167
+ }
168
+ catch { /* already closing */ }
169
+ ws = null;
170
+ }
@@ -0,0 +1,37 @@
1
+ export interface DesktopRow {
2
+ id: string;
3
+ runtime_id: string;
4
+ name: string;
5
+ image: string;
6
+ engine: 'docker' | 'podman' | null;
7
+ container_id: string | null;
8
+ cpus: number;
9
+ memory_mb: number;
10
+ disk_gb: number;
11
+ setup_script: string | null;
12
+ enabled_mcps: string[];
13
+ enabled_skills: string[];
14
+ enabled_packages: string[];
15
+ autostart: boolean;
16
+ status: 'pending' | 'creating' | 'starting' | 'running' | 'stopped' | 'failed' | 'deleting';
17
+ status_message: string | null;
18
+ vnc_port: number | null;
19
+ vnc_password: string | null;
20
+ }
21
+ /** Workspace root inside the container. Projects clone to /work/<name>,
22
+ * session cwds live at /work/.sessions/<requestId>. */
23
+ export declare const WORK_ROOT = "/work";
24
+ /** Where the daemon's generated files (MCP config, memory pack, attachments)
25
+ * appear inside the container. Bind-mounted from the host. */
26
+ export declare const CONF_ROOT = "/conf";
27
+ /** The container's RAW RFB port, fixed by the image.
28
+ *
29
+ * Raw, not websockified: the relay carries these bytes over its own
30
+ * WebSocket and noVNC in the browser speaks RFB across it. Putting
31
+ * websockify in the container too would mean a WebSocket handshake tunnelled
32
+ * inside a WebSocket, which no client can read. */
33
+ export declare const VNC_PORT_IN_CONTAINER = 5900;
34
+ /** Host directory bind-mounted at CONF_ROOT for one desktop. */
35
+ export declare function confDirFor(desktopId: string): string;
36
+ /** Host directory holding the desktop's persistent /work volume. */
37
+ export declare function workDirFor(desktopId: string): string;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.VNC_PORT_IN_CONTAINER = exports.CONF_ROOT = exports.WORK_ROOT = void 0;
37
+ exports.confDirFor = confDirFor;
38
+ exports.workDirFor = workDirFor;
39
+ // Shapes and paths shared by every desktop module. No I/O here.
40
+ const path = __importStar(require("path"));
41
+ const paths_1 = require("../paths");
42
+ /** Workspace root inside the container. Projects clone to /work/<name>,
43
+ * session cwds live at /work/.sessions/<requestId>. */
44
+ exports.WORK_ROOT = '/work';
45
+ /** Where the daemon's generated files (MCP config, memory pack, attachments)
46
+ * appear inside the container. Bind-mounted from the host. */
47
+ exports.CONF_ROOT = '/conf';
48
+ /** The container's RAW RFB port, fixed by the image.
49
+ *
50
+ * Raw, not websockified: the relay carries these bytes over its own
51
+ * WebSocket and noVNC in the browser speaks RFB across it. Putting
52
+ * websockify in the container too would mean a WebSocket handshake tunnelled
53
+ * inside a WebSocket, which no client can read. */
54
+ exports.VNC_PORT_IN_CONTAINER = 5900;
55
+ /** Host directory bind-mounted at CONF_ROOT for one desktop. */
56
+ function confDirFor(desktopId) {
57
+ return path.join(paths_1.RUNTIME_HOME, 'desktops', desktopId, 'conf');
58
+ }
59
+ /** Host directory holding the desktop's persistent /work volume. */
60
+ function workDirFor(desktopId) {
61
+ return path.join(paths_1.RUNTIME_HOME, 'desktops', desktopId, 'work');
62
+ }
@@ -38,4 +38,8 @@ export interface GrokHandle {
38
38
  onActivity(cb: () => void): void;
39
39
  done: Promise<number>;
40
40
  }
41
+ /** Write the entity's MCP servers as a project-scoped `<cwd>/.grok/config.toml`.
42
+ * Project scope only supports `[mcp_servers]`, which is exactly what we need.
43
+ * Returns the config path (for logging) or null when there are no servers. */
44
+ export declare function writeProjectMcpConfig(workingDirectory: string, servers: GrokMcpServer[]): string | null;
41
45
  export declare function spawnGrok(input: GrokInput): GrokHandle;
@@ -64,6 +64,7 @@ var __importStar = (this && this.__importStar) || (function () {
64
64
  };
65
65
  })();
66
66
  Object.defineProperty(exports, "__esModule", { value: true });
67
+ exports.writeProjectMcpConfig = writeProjectMcpConfig;
67
68
  exports.spawnGrok = spawnGrok;
68
69
  const child_process_1 = require("child_process");
69
70
  const fs = __importStar(require("fs"));
@@ -158,6 +159,18 @@ function writeProjectMcpConfig(workingDirectory, servers) {
158
159
  }
159
160
  const lines = [];
160
161
  for (const s of usable) {
162
+ // Remote MCP servers (registry command='http'|'sse', args=[url]) have no
163
+ // verified grok config spelling, and writing the row verbatim is worse
164
+ // than skipping: grok would try to exec a binary called `http`, the
165
+ // server would never start, and its tools would vanish with no error —
166
+ // the exact silent failure that cost an entity its GitHub access after a
167
+ // provider-ladder hop. Skip it, and say so loudly enough to be findable.
168
+ if (s.command === 'http' || s.command === 'sse') {
169
+ console.error(`[grok] MCP ${s.slug}: remote ${s.command} servers are not supported by the grok ` +
170
+ `adapter — skipping. Its tools will be ABSENT from this run. Use claude (or codex, ` +
171
+ `which supports url + bearer_token_env_var) for entities that depend on it.`);
172
+ continue;
173
+ }
161
174
  // `cmd /c` wrapper for npm-shim commands on native Windows (no-op on POSIX).
162
175
  const wrapped = (0, win_1.wrapMcpCommandForPlatform)(s.command, Array.isArray(s.args) ? s.args.map(String) : []);
163
176
  lines.push(`[mcp_servers.${tomlStr(s.slug)}]`);
@@ -1,4 +1,5 @@
1
1
  export declare function setPendingCommandsHook(cb: () => void): void;
2
+ export declare function autoUpdateEnabled(): boolean;
2
3
  /** Announce a clean shutdown so the server flips us offline immediately
3
4
  * (status='offline') instead of waiting ~90s for last_seen_at to age out —
4
5
  * lets a planned restart fail over instantly. Best-effort; the stale-window
package/dist/heartbeat.js CHANGED
@@ -4,6 +4,7 @@
4
4
  // is paired.
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.setPendingCommandsHook = setPendingCommandsHook;
7
+ exports.autoUpdateEnabled = autoUpdateEnabled;
7
8
  exports.goOffline = goOffline;
8
9
  exports.beat = beat;
9
10
  exports.start = start;
@@ -21,6 +22,15 @@ let onPendingCommands = null;
21
22
  function setPendingCommandsHook(cb) {
22
23
  onPendingCommands = cb;
23
24
  }
25
+ // Is hourly auto-update armed for this node? It rides the heartbeat rather
26
+ // than getting an RPC of its own — the daemon is already talking to the server
27
+ // every 30s, and the answer is one boolean on the node's own row.
28
+ //
29
+ // Default ON, and specifically ON when the field is ABSENT: a server that has
30
+ // not had the migration yet must not read as "everybody opted out". Only an
31
+ // explicit `false` turns it off.
32
+ let autoUpdateArmed = true;
33
+ function autoUpdateEnabled() { return autoUpdateArmed; }
24
34
  async function tick() {
25
35
  // Skip if the daemon-wide circuit breaker is open. Heartbeats are
26
36
  // safe to miss — vault marks the runtime offline after ~90s; the
@@ -41,6 +51,8 @@ async function tick() {
41
51
  if (res && typeof res.pending_commands === 'number' && res.pending_commands > 0) {
42
52
  onPendingCommands?.();
43
53
  }
54
+ if (res && typeof res.auto_update === 'boolean')
55
+ autoUpdateArmed = res.auto_update;
44
56
  }
45
57
  catch (err) {
46
58
  if (err instanceof supabase_client_1.RpcError && err.status === 401) {