@addai/node 0.30.13 → 0.30.14

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 (143) hide show
  1. package/dist/auto-update.d.ts +3 -0
  2. package/dist/auto-update.js +15 -0
  3. package/dist/capabilities.d.ts +17 -3
  4. package/dist/capabilities.js +18 -8
  5. package/dist/claude-models.d.ts +4 -12
  6. package/dist/claude-models.js +12 -88
  7. package/dist/claude-print.d.ts +15 -0
  8. package/dist/claude-print.js +16 -0
  9. package/dist/codex-models.d.ts +81 -0
  10. package/dist/codex-models.js +385 -0
  11. package/dist/codex-spawn.d.ts +14 -8
  12. package/dist/codex-spawn.js +30 -10
  13. package/dist/command-runner.js +31 -9
  14. package/dist/desktop/docker.js +2 -0
  15. package/dist/desktop/manager.d.ts +2 -1
  16. package/dist/desktop/manager.js +51 -1
  17. package/dist/desktop/provider.d.ts +1 -1
  18. package/dist/desktop/provider.js +2 -0
  19. package/dist/desktop/relay-client.js +7 -0
  20. package/dist/desktop/spec.d.ts +2 -0
  21. package/dist/direct/aios-bridge.d.ts +13 -0
  22. package/dist/direct/aios-bridge.js +177 -0
  23. package/dist/direct/aios-mcp.d.ts +2 -0
  24. package/dist/direct/aios-mcp.js +132 -0
  25. package/dist/direct/aios-tools.d.ts +351 -0
  26. package/dist/direct/aios-tools.js +120 -0
  27. package/dist/direct/board-chat-provider.d.ts +64 -0
  28. package/dist/direct/board-chat-provider.js +151 -0
  29. package/dist/direct/board-chat.d.ts +40 -0
  30. package/dist/direct/board-chat.js +414 -0
  31. package/dist/direct/config.d.ts +2 -0
  32. package/dist/direct/config.js +60 -0
  33. package/dist/direct/launch.d.ts +14 -0
  34. package/dist/direct/launch.js +121 -0
  35. package/dist/direct/protocol.d.ts +46 -0
  36. package/dist/direct/protocol.js +84 -0
  37. package/dist/direct/relay-main.d.ts +1 -0
  38. package/dist/direct/relay-main.js +21 -0
  39. package/dist/direct/relay.d.ts +26 -0
  40. package/dist/direct/relay.js +334 -0
  41. package/dist/direct/sessions.d.ts +61 -0
  42. package/dist/direct/sessions.js +368 -0
  43. package/dist/direct/supervisor.d.ts +9 -0
  44. package/dist/direct/supervisor.js +137 -0
  45. package/dist/direct/terminal.d.ts +1 -0
  46. package/dist/direct/terminal.js +106 -0
  47. package/dist/direct/worker.d.ts +1 -0
  48. package/dist/direct/worker.js +278 -0
  49. package/dist/entitybox/container-action.cjs +85 -0
  50. package/dist/entitybox/container-watchdog.cjs +70 -0
  51. package/dist/entitybox/model-entry.d.ts +3 -0
  52. package/dist/entitybox/model-entry.js +233 -0
  53. package/dist/entitybox/model-mcp-proxy.d.ts +3 -0
  54. package/dist/entitybox/model-mcp-proxy.js +85 -0
  55. package/dist/entitybox-application-boundary.d.ts +59 -0
  56. package/dist/entitybox-application-boundary.js +161 -0
  57. package/dist/entitybox-application-guard-core.d.ts +44 -0
  58. package/dist/entitybox-application-guard-core.js +164 -0
  59. package/dist/entitybox-application-guard-process.d.ts +1 -0
  60. package/dist/entitybox-application-guard-process.js +83 -0
  61. package/dist/entitybox-application-guard.d.ts +46 -0
  62. package/dist/entitybox-application-guard.js +239 -0
  63. package/dist/entitybox-capacity-health-host.d.ts +12 -0
  64. package/dist/entitybox-capacity-health-host.js +80 -0
  65. package/dist/entitybox-capacity-health-worker.d.ts +22 -0
  66. package/dist/entitybox-capacity-health-worker.js +168 -0
  67. package/dist/entitybox-capacity-host.d.ts +17 -0
  68. package/dist/entitybox-capacity-host.js +155 -0
  69. package/dist/entitybox-capacity-owner-process.d.ts +1 -0
  70. package/dist/entitybox-capacity-owner-process.js +32 -0
  71. package/dist/entitybox-capacity-owner.d.ts +16 -0
  72. package/dist/entitybox-capacity-owner.js +90 -0
  73. package/dist/entitybox-capacity-programs.d.ts +7 -0
  74. package/dist/entitybox-capacity-programs.js +49 -0
  75. package/dist/entitybox-capacity-provisioner.d.ts +82 -0
  76. package/dist/entitybox-capacity-provisioner.js +404 -0
  77. package/dist/entitybox-capacity-publication.d.ts +29 -0
  78. package/dist/entitybox-capacity-publication.js +199 -0
  79. package/dist/entitybox-capacity-registration.d.ts +36 -0
  80. package/dist/entitybox-capacity-registration.js +75 -0
  81. package/dist/entitybox-capacity-worker.d.ts +29 -0
  82. package/dist/entitybox-capacity-worker.js +113 -0
  83. package/dist/entitybox-computer-task.d.ts +69 -0
  84. package/dist/entitybox-computer-task.js +176 -0
  85. package/dist/entitybox-controller-recovery.d.ts +40 -0
  86. package/dist/entitybox-controller-recovery.js +156 -0
  87. package/dist/entitybox-desktop-action-server.d.ts +34 -0
  88. package/dist/entitybox-desktop-action-server.js +176 -0
  89. package/dist/entitybox-desktop-admission.d.ts +32 -0
  90. package/dist/entitybox-desktop-admission.js +79 -0
  91. package/dist/entitybox-display-binding.d.ts +13 -0
  92. package/dist/entitybox-display-binding.js +31 -0
  93. package/dist/entitybox-execution-container.d.ts +82 -0
  94. package/dist/entitybox-execution-container.js +450 -0
  95. package/dist/entitybox-execution-host.d.ts +40 -0
  96. package/dist/entitybox-execution-host.js +168 -0
  97. package/dist/entitybox-execution-lease.d.ts +81 -0
  98. package/dist/entitybox-execution-lease.js +332 -0
  99. package/dist/entitybox-file-inputs.d.ts +51 -0
  100. package/dist/entitybox-file-inputs.js +182 -0
  101. package/dist/entitybox-file-session.d.ts +38 -0
  102. package/dist/entitybox-file-session.js +82 -0
  103. package/dist/entitybox-host-process.d.ts +12 -0
  104. package/dist/entitybox-host-process.js +120 -0
  105. package/dist/entitybox-install.d.ts +10 -0
  106. package/dist/entitybox-install.js +40 -0
  107. package/dist/entitybox-mcp-broker.d.ts +77 -0
  108. package/dist/entitybox-mcp-broker.js +420 -0
  109. package/dist/entitybox-mcp-contract.d.ts +10 -0
  110. package/dist/entitybox-mcp-contract.js +24 -0
  111. package/dist/entitybox-mcp-guard.d.ts +3 -0
  112. package/dist/entitybox-mcp-guard.js +167 -0
  113. package/dist/entitybox-model-artifact.d.ts +9 -0
  114. package/dist/entitybox-model-artifact.js +83 -0
  115. package/dist/entitybox-native-services.d.ts +33 -0
  116. package/dist/entitybox-native-services.js +172 -0
  117. package/dist/entitybox-operation.d.ts +4 -0
  118. package/dist/entitybox-operation.js +34 -0
  119. package/dist/entitybox-provider-broker.d.ts +41 -0
  120. package/dist/entitybox-provider-broker.js +149 -0
  121. package/dist/entitybox-queue-request.d.ts +32 -0
  122. package/dist/entitybox-queue-request.js +141 -0
  123. package/dist/entitybox-recovery-host.d.ts +12 -0
  124. package/dist/entitybox-recovery-host.js +80 -0
  125. package/dist/entitybox-recovery-store.d.ts +33 -0
  126. package/dist/entitybox-recovery-store.js +150 -0
  127. package/dist/entitybox-recovery-worker.d.ts +30 -0
  128. package/dist/entitybox-recovery-worker.js +134 -0
  129. package/dist/entitybox-vault-provider.d.ts +27 -0
  130. package/dist/entitybox-vault-provider.js +127 -0
  131. package/dist/harness-registry.js +3 -1
  132. package/dist/index.js +72 -38
  133. package/dist/install.d.ts +14 -2
  134. package/dist/install.js +115 -10
  135. package/dist/model-cache.d.ts +38 -0
  136. package/dist/model-cache.js +137 -0
  137. package/dist/request-pump.d.ts +3 -0
  138. package/dist/request-pump.js +21 -7
  139. package/dist/session-runner.d.ts +8 -1
  140. package/dist/session-runner.js +48 -3
  141. package/dist/supabase-client.js +4 -1
  142. package/package.json +5 -1
  143. package/scripts/copy-assets.js +4 -2
@@ -0,0 +1,278 @@
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.runDirectWorker = runDirectWorker;
40
+ const path = __importStar(require("node:path"));
41
+ const ws_1 = __importDefault(require("ws"));
42
+ const store_1 = require("../store");
43
+ const paths_1 = require("../paths");
44
+ const sessions_1 = require("./sessions");
45
+ const board_chat_1 = require("./board-chat");
46
+ const launch_1 = require("./launch");
47
+ const protocol_1 = require("./protocol");
48
+ function runDirectWorker() {
49
+ const roots = JSON.parse(process.env.AINODE_DIRECT_ROOTS ?? '[]');
50
+ if (!Array.isArray(roots) || roots.some(r => typeof r !== 'string'))
51
+ throw new Error('AINODE_DIRECT_ROOTS must be a JSON array of project folders.');
52
+ const endpoint = (0, protocol_1.relayUrl)(process.env.AINODE_DIRECT_RELAY_URL ?? '', '/node');
53
+ const healthUrl = new URL(endpoint);
54
+ healthUrl.protocol = healthUrl.protocol === 'wss:' ? 'https:' : 'http:';
55
+ healthUrl.pathname = '/health';
56
+ const maxActive = Number(process.env.AINODE_DIRECT_MAX_SESSIONS ?? 2);
57
+ if (!Number.isInteger(maxActive) || maxActive < 1 || maxActive > 8)
58
+ throw new Error('Direct-session limit must be between 1 and 8.');
59
+ let ws = null;
60
+ let stopped = false;
61
+ let ready = false;
62
+ let retry;
63
+ let retryMs = 1000;
64
+ let boundToken;
65
+ let relayId;
66
+ let lastPing = Date.now();
67
+ let queued = 0;
68
+ let accepting = true;
69
+ let chain = Promise.resolve();
70
+ const connectedViewers = new Set();
71
+ let chats;
72
+ const notifyParent = (count) => { if (process.connected)
73
+ process.send?.({ type: 'direct-count', count }); };
74
+ function send(value) {
75
+ if (!ready || ws?.readyState !== ws_1.default.OPEN)
76
+ return;
77
+ if (ws.bufferedAmount > 4 * 1024 * 1024) {
78
+ ws.close(1013, 'Reconnecting a slow connection.');
79
+ return;
80
+ }
81
+ ws.send(JSON.stringify(value));
82
+ }
83
+ const sessions = new sessions_1.DirectSessions({
84
+ directory: path.join(paths_1.RUNTIME_HOME, 'direct-sessions'), roots, maxActive,
85
+ providers: launch_1.directProviders, spawn: launch_1.spawnDirect,
86
+ emit: (viewerId, event) => send({ type: 'event', viewerId, event }),
87
+ onCount: count => notifyParent(count + (chats?.count ?? 0)),
88
+ canStart: () => sessions.count + (chats?.count ?? 0) < maxActive,
89
+ });
90
+ chats = new board_chat_1.BoardChats({
91
+ directory: path.join(paths_1.RUNTIME_HOME, 'board-chats'),
92
+ emit: (viewerId, event) => send({ type: 'event', viewerId, event }),
93
+ canStart: () => accepting && sessions.count + (chats?.count ?? 0) < maxActive,
94
+ onCount: () => notifyParent(sessions.count + (chats?.count ?? 0)),
95
+ });
96
+ notifyParent(0);
97
+ function schedule() {
98
+ if (stopped)
99
+ return;
100
+ retry = setTimeout(connect, retryMs);
101
+ retryMs = Math.min(retryMs * 2, 30_000);
102
+ }
103
+ function connect() {
104
+ if (stopped)
105
+ return;
106
+ const pairing = (0, store_1.readPairing)();
107
+ if (!pairing) {
108
+ schedule();
109
+ return;
110
+ }
111
+ if (boundToken && boundToken !== pairing.daemonToken) {
112
+ // Pairing ownership changed: never carry interactive sessions into a different pairing.
113
+ stop();
114
+ return;
115
+ }
116
+ boundToken = pairing.daemonToken;
117
+ const socket = new ws_1.default(endpoint, { maxPayload: protocol_1.MAX_FRAME, perMessageDeflate: false });
118
+ ws = socket;
119
+ ready = false;
120
+ lastPing = Date.now();
121
+ const handshake = setTimeout(() => socket.terminate(), 12_000);
122
+ socket.on('open', () => socket.send(JSON.stringify({ type: 'authenticate', token: pairing.daemonToken })));
123
+ socket.on('ping', () => { lastPing = Date.now(); });
124
+ socket.on('error', () => { });
125
+ socket.on('close', () => {
126
+ clearTimeout(handshake);
127
+ if (ws !== socket)
128
+ return;
129
+ ready = false;
130
+ ws = null;
131
+ connectedViewers.clear();
132
+ sessions.detachAll();
133
+ chats?.detachAll();
134
+ schedule();
135
+ });
136
+ socket.on('message', raw => {
137
+ if (ws !== socket)
138
+ return;
139
+ let message;
140
+ try {
141
+ message = JSON.parse(raw.toString());
142
+ }
143
+ catch {
144
+ socket.close(1008, 'Invalid protocol.');
145
+ return;
146
+ }
147
+ if (message.type === 'ready') {
148
+ if (message.protocol !== protocol_1.DIRECT_PROTOCOL || message.runtimeId !== pairing.runtimeId) {
149
+ socket.close(1008, 'Incompatible relay.');
150
+ return;
151
+ }
152
+ clearTimeout(handshake);
153
+ ready = true;
154
+ retryMs = 1000;
155
+ relayId = message.relayId;
156
+ return;
157
+ }
158
+ if (!ready || !protocol_1.UUID.test(message.viewerId ?? ''))
159
+ return;
160
+ if (message.type === 'detach_viewer') {
161
+ connectedViewers.delete(message.viewerId);
162
+ sessions.detach(message.viewerId);
163
+ chats?.disconnect(message.viewerId);
164
+ return;
165
+ }
166
+ if (message.type !== 'request' || !protocol_1.UUID.test(message.ownerId ?? ''))
167
+ return;
168
+ connectedViewers.add(message.viewerId);
169
+ if (++queued > 128) {
170
+ queued--;
171
+ socket.close(1013, 'Request queue full.');
172
+ return;
173
+ }
174
+ const viewerId = message.viewerId;
175
+ chain = chain.then(async () => {
176
+ // A queued create/input from a disconnected viewer must never execute later.
177
+ if (ws !== socket || !ready || !connectedViewers.has(viewerId))
178
+ return;
179
+ const id = message.request?.id;
180
+ try {
181
+ if (!accepting && ['create', 'resume'].includes(message.request?.op))
182
+ throw new Error('The node is updating. Try again after it reconnects.');
183
+ const result = message.request?.op === 'chat'
184
+ ? chats.request(viewerId, message.ownerId, message.request)
185
+ : await sessions.request(viewerId, message.ownerId, message.request);
186
+ if (message.request?.op === 'describe')
187
+ result.chat = { version: 1, providers: ['codex'] };
188
+ if (!connectedViewers.has(viewerId)) {
189
+ sessions.detach(viewerId);
190
+ chats?.disconnect(viewerId);
191
+ return;
192
+ }
193
+ if (typeof result?.screen === 'string') {
194
+ const screen = result.screen;
195
+ // Snapshot chunks keep every relay frame bounded, even for colourful wide terminals.
196
+ for (let i = 0; i < screen.length; i += 16_384)
197
+ send({ type: 'snapshot', viewerId, id, data: screen.slice(i, i + 16_384) });
198
+ delete result.screen;
199
+ }
200
+ send({ type: 'response', viewerId, id, ok: true, result });
201
+ }
202
+ catch (err) {
203
+ send({ type: 'response', viewerId, id, ok: false, error: err.message });
204
+ }
205
+ }).catch(() => { }).finally(() => { queued--; });
206
+ });
207
+ }
208
+ let checking = false;
209
+ const health = setInterval(async () => {
210
+ notifyParent(sessions.count + (chats?.count ?? 0));
211
+ if (boundToken && (0, store_1.readPairing)()?.daemonToken !== boundToken) {
212
+ stop();
213
+ return;
214
+ }
215
+ if (ready && Date.now() - lastPing > 75_000)
216
+ ws?.terminate();
217
+ if (!ready || checking)
218
+ return;
219
+ checking = true;
220
+ try {
221
+ const response = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
222
+ const data = await response.json();
223
+ // Reconnect to the current deployment rather than an old draining relay instance.
224
+ if (response.ok && relayId && data.relayId && relayId !== data.relayId)
225
+ ws?.close(1012, 'Relay updated.');
226
+ }
227
+ catch { /* Ping timeout handles a failed link; a failed health check does not end sessions. */ }
228
+ finally {
229
+ checking = false;
230
+ }
231
+ }, 15_000);
232
+ function stop() {
233
+ if (stopped)
234
+ return;
235
+ stopped = true;
236
+ ready = false;
237
+ clearInterval(health);
238
+ if (retry)
239
+ clearTimeout(retry);
240
+ ws?.terminate();
241
+ connectedViewers.clear();
242
+ sessions.stop();
243
+ chats?.stop();
244
+ if (process.connected)
245
+ process.disconnect();
246
+ }
247
+ process.once('disconnect', stop);
248
+ process.once('SIGTERM', stop);
249
+ process.once('SIGINT', stop);
250
+ process.on('message', (value) => {
251
+ if (value?.type === 'direct-stop')
252
+ stop();
253
+ if (value?.type === 'direct-accept')
254
+ accepting = true;
255
+ if (value?.type === 'direct-quiesce') {
256
+ accepting = false;
257
+ void chain.then(() => {
258
+ const count = sessions.count + (chats?.count ?? 0);
259
+ if (count > 0)
260
+ accepting = true;
261
+ if (process.connected)
262
+ process.send?.({ type: 'direct-quiesced', id: value.id, count });
263
+ });
264
+ }
265
+ });
266
+ connect();
267
+ return stop;
268
+ }
269
+ if (require.main === module) {
270
+ try {
271
+ runDirectWorker();
272
+ }
273
+ catch (err) {
274
+ if (process.connected)
275
+ process.send?.({ type: 'direct-error', message: err.message });
276
+ process.exitCode = 1;
277
+ }
278
+ }
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+ // Trusted root entrypoint for one command. Input and command output are separate
3
+ // from the watchdog's renewal channel; neither can grant more execution time.
4
+ const fs = require('node:fs');
5
+ const {spawn} = require('node:child_process');
6
+ const authorityPath = '/run/entitybox/authority.json';
7
+ const uuid = x => typeof x === 'string' && /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(x);
8
+ let packet, child, watchdogPid, input = '', launched = false, finished = false, timer;
9
+ const now = () => process.hrtime.bigint();
10
+ if (process.getuid?.() !== 0 || !fs.existsSync('/.dockerenv')) process.exit(78);
11
+
12
+ function sameIdentity(a, b) {
13
+ return a && b && a.requestId === b.requestId && a.attemptId === b.attemptId
14
+ && a.scope?.version === 1 && b.scope?.version === 1
15
+ && ['runId','desktopId','runtimeId'].every(k => a.scope[k] === b.scope[k]);
16
+ }
17
+ function authority() {
18
+ const dir = fs.lstatSync('/run/entitybox'), stat = fs.lstatSync(authorityPath);
19
+ if (!dir.isDirectory() || dir.uid !== 0 || (dir.mode & 0o777) !== 0o700
20
+ || !stat.isFile() || stat.uid !== 0 || (stat.mode & 0o777) !== 0o600 || stat.size > 4096) throw Error();
21
+ const a = JSON.parse(fs.readFileSync(authorityPath, 'utf8'));
22
+ if (a.version !== 1 || !sameIdentity(a.identity, packet.identity) || a.phase !== 'running'
23
+ || (packet.display !== undefined && (packet.display !== ':1' || a.display !== packet.display))
24
+ || typeof a.expiresNs !== 'string' || !/^\d{1,30}$/.test(a.expiresNs)
25
+ || !Number.isSafeInteger(a.watchdogPid) || a.watchdogPid <= 1 || a.watchdogPid === process.pid) throw Error();
26
+ // Remember only a root-owned, correctly scoped watchdog identity. If a running
27
+ // action loses authority, end the namespace, including detached descendants.
28
+ watchdogPid = a.watchdogPid;
29
+ if (now() >= BigInt(a.expiresNs)) throw Error();
30
+ return a;
31
+ }
32
+ function fail() {
33
+ if (finished) return;
34
+ finished = true; clearInterval(timer);
35
+ if (launched && watchdogPid) {try {process.kill(watchdogPid, 'SIGTERM');} catch {}}
36
+ process.stderr.write('EntityBox action authority ended\n');
37
+ process.exit(78);
38
+ }
39
+ function launch() {
40
+ clearTimeout(startup);
41
+ try {
42
+ packet = JSON.parse(input);
43
+ if (packet?.version !== 1 || !uuid(packet.actionId) || !uuid(packet.identity?.requestId)
44
+ || !uuid(packet.identity?.attemptId) || packet.identity?.scope?.version !== 1
45
+ || !['runId','desktopId','runtimeId'].every(k => uuid(packet.identity.scope[k]))
46
+ || !Array.isArray(packet.argv) || !packet.argv.length || packet.argv.length > 64
47
+ || !packet.argv.every(v => typeof v === 'string' && v.length <= 32768 && !v.includes('\0'))
48
+ || !packet.argv[0] || !Number.isInteger(packet.timeoutMs) || packet.timeoutMs < 1 || packet.timeoutMs > 600000
49
+ || typeof packet.input !== 'string' || packet.input.length%4!==0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(packet.input)) throw Error();
50
+ authority();
51
+ // A task action ID is single-use, even if the first Docker exec reply is lost.
52
+ // The root-only ledger is bounded by this container's tiny private tmpfs.
53
+ fs.closeSync(fs.openSync('/run/entitybox/action-' + packet.actionId, 'wx', 0o600));
54
+ const until = now() + BigInt(packet.timeoutMs) * 1000000n;
55
+ authority();
56
+ launched = true;
57
+ child = spawn(packet.argv[0], packet.argv.slice(1), {
58
+ uid:10001, gid:10001, cwd:'/work',
59
+ env:{PATH:'/usr/local/bin:/usr/bin:/bin',HOME:'/home/entitybox',TERM:'dumb',NODE_ENV:'production',LANG:'C.UTF-8',...(packet.display?{DISPLAY:packet.display}:{})},
60
+ stdio:['pipe','pipe','pipe'], detached:false,
61
+ });
62
+ child.on('error', fail);
63
+ child.stdin.on('error', () => {});
64
+ child.stdin.end(Buffer.from(packet.input, 'base64'));
65
+ const emit = (type, data) => process.stdout.write(JSON.stringify({type,data:data.toString('base64')})+'\n');
66
+ for (const [stream, type] of [[child.stdout,'stdout'],[child.stderr,'stderr']]) {
67
+ stream.on('data', data => {if(!emit(type,data)) stream.pause();});
68
+ }
69
+ process.stdout.on('drain', () => {child.stdout.resume();child.stderr.resume();});
70
+ child.on('close', (code, signal) => {
71
+ if (finished) return;
72
+ try {authority(); if (now() >= until || signal || !Number.isInteger(code)) throw Error();} catch {fail();return;}
73
+ finished = true; clearInterval(timer);
74
+ process.stdout.write(JSON.stringify({type:'result',code})+'\n');
75
+ });
76
+ timer = setInterval(() => {try {authority(); if(now() >= until) throw Error();} catch {fail();}}, 25);
77
+ } catch {fail();}
78
+ }
79
+ process.stdin.setEncoding('utf8');
80
+ process.stdin.on('data', chunk => {input += chunk; if(Buffer.byteLength(input) > 512*1024) fail();});
81
+ process.stdin.once('end', launch);
82
+ process.stdin.on('error', fail); process.stdout.on('error', fail); process.stderr.on('error', fail);
83
+ process.on('SIGTERM', fail); process.on('SIGINT', fail);
84
+ // A delayed/broken transport cannot leave a root dispatcher waiting forever.
85
+ const startup = setTimeout(fail, 10000); startup.unref();
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+ // Trusted container supervisor; model code runs as a different, unprivileged
3
+ // UID. No RPC, provider or entity credentials enter this process or its child.
4
+ const {spawn}=require('node:child_process');
5
+ const {randomUUID}=require('node:crypto');
6
+ const fs=require('node:fs');
7
+ if(process.getuid?.()!==0||!fs.existsSync('/.dockerenv'))process.exit(78);
8
+ // hrtime is shared across processes in this PID namespace. A separate trusted
9
+ // action dispatcher can check this deadline without trusting wall-clock time.
10
+ const now=()=>process.hrtime.bigint(),authorityPath='/run/entitybox/authority.json';
11
+ let input='',config,child,pending,deadline=now()+10000000000n,stopped=false,challengeTimer;
12
+ const requests=new Set();let brokerBuffer='';
13
+ const write=value=>process.stdout.write(JSON.stringify(value)+'\n');
14
+ function publish(phase){
15
+ const value={version:1,identity:config.identity,display:config.display??null,phase,expiresNs:deadline.toString(),watchdogPid:process.pid};
16
+ fs.writeFileSync(authorityPath+'.next',JSON.stringify(value),{mode:0o600});
17
+ fs.renameSync(authorityPath+'.next',authorityPath);
18
+ }
19
+ function stop(code){if(stopped)return;stopped=true;clearTimeout(challengeTimer);try{if(config)publish('stopped');}catch{}process.exit(code);}
20
+ function challenge(){if(stopped||pending)return;pending={id:randomUUID(),at:now()};write({type:'challenge',id:pending.id});}
21
+ function validIdentity(v){const uuid=x=>typeof x==='string'&&/^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(x);return v&&uuid(v.requestId)&&uuid(v.attemptId)&&v.scope?.version===1&&['runId','desktopId','runtimeId'].every(k=>uuid(v.scope[k]));}
22
+ function launch(){
23
+ child=spawn(config.command[0],config.command.slice(1),{uid:10001,gid:10001,cwd:'/work',env:{PATH:'/usr/local/bin:/usr/bin:/bin',HOME:'/home/entitybox',TERM:'dumb',NODE_ENV:'production'},stdio:['pipe','pipe','pipe','pipe'],detached:false});
24
+ child.on('error',()=>stop(70));
25
+ child.stdio[3].setEncoding('utf8');child.stdio[3].on('error',()=>stop(70));
26
+ child.stdio[3].on('data',chunk=>{try{brokerBuffer+=chunk;if(Buffer.byteLength(brokerBuffer)>1024*1024)throw Error();let end;while((end=brokerBuffer.indexOf('\n'))>=0){const p=JSON.parse(brokerBuffer.slice(0,end));brokerBuffer=brokerBuffer.slice(end+1);if(typeof p.id!=='string'||!/^[-0-9a-f]{36}$/.test(p.id))throw Error();if(p.type==='broker_cancel'){if(requests.delete(p.id))write({type:'broker_cancel',id:p.id});continue;}if(p.type!=='broker_request'||requests.size>=8||requests.has(p.id))throw Error();requests.add(p.id);write({type:'broker_request',id:p.id,request:p.request});}}catch{stop(78);}});
27
+ // The child never writes raw control messages. Framing is exclusively owned
28
+ // by this root supervisor; output is encoded and backpressure pauses only
29
+ // the model stream, leaving lease challenges and timers independent.
30
+ for(const [stream,type] of [[child.stdout,'output'],[child.stderr,'diagnostic']])stream.on('data',data=>{
31
+ if(type==='diagnostic')return; // raw model diagnostics can contain task data
32
+ if(!write({type,data:data.toString('base64')}))stream.pause();
33
+ });
34
+ process.stdout.on('drain',()=>child.stdout.resume());
35
+ child.stdin.on('error',()=>{});child.stdin.end(JSON.stringify(config.task)+'\n');
36
+ child.on('close',code=>{write({type:'model_exit',code:Number.isInteger(code)?code:70});stop(Number.isInteger(code)?code:70);});
37
+ write({type:'started'});
38
+ }
39
+ function packet(value){
40
+ if(!config){
41
+ if(value?.type!=='configure'||!Array.isArray(value.command)||!value.command.length||value.command.length>32||!value.command.every(x=>typeof x==='string'&&x.length<16000)||value.task===undefined)throw Error();
42
+ if(!validIdentity(value.identity))throw Error();
43
+ if(value.display!==undefined&&value.display!==':1')throw Error();
44
+ const dir=fs.lstatSync('/run/entitybox');if(!dir.isDirectory()||dir.uid!==0||(dir.mode&0o777)!==0o700)throw Error();
45
+ config=value;publish('waiting');challenge();return;
46
+ }
47
+ if(value?.type==='broker_event'){
48
+ if(!requests.has(value.id))return;
49
+ if(now()>=deadline)throw Error();
50
+ child.stdio[3].write(JSON.stringify({id:value.id,event:value.event})+'\n');
51
+ if(['end','error'].includes(value.event?.type))requests.delete(value.id);
52
+ return;
53
+ }
54
+ if(value?.type==='stop'){stop(78);return;}
55
+ if(value?.type!=='grant'||!pending||value.id!==pending.id)throw Error();
56
+ const next=pending.at+12000000000n;
57
+ // Compare both the current authority and the original challenge deadline.
58
+ // Arrival time is never a new grant origin; replay cannot reset either clock.
59
+ if(now()>=deadline||now()>=next)throw Error();
60
+ deadline=next;pending=null;
61
+ publish('running');
62
+ if(!child)launch();
63
+ challengeTimer=setTimeout(challenge,3000);
64
+ }
65
+ process.stdin.setEncoding('utf8');
66
+ process.stdin.on('data',chunk=>{try{input+=chunk;if(Buffer.byteLength(input)>1024*1024)throw Error();let end;while((end=input.indexOf('\n'))>=0){const line=input.slice(0,end);input=input.slice(end+1);packet(JSON.parse(line));}}catch{stop(78);}});
67
+ process.stdin.on('end',()=>stop(78));process.stdin.on('error',()=>stop(78));
68
+ process.stdout.on('error',()=>stop(78));
69
+ process.on('SIGTERM',()=>stop(78));process.on('SIGINT',()=>stop(78));
70
+ setInterval(()=>{if(now()>=deadline)stop(78);},50);
@@ -0,0 +1,3 @@
1
+ /** Runs only as the isolated model user. Its extra inherited pipe carries
2
+ * bounded broker requests; ordinary model stdout never becomes control data. */
3
+ export declare function runEntityboxModel(task: unknown): Promise<number>;
@@ -0,0 +1,233 @@
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.runEntityboxModel = runEntityboxModel;
37
+ const fs = __importStar(require("fs"));
38
+ const http = __importStar(require("http"));
39
+ const net = __importStar(require("net"));
40
+ const crypto_1 = require("crypto");
41
+ const claude_print_1 = require("../claude-print");
42
+ const model_mcp_proxy_1 = require("./model-mcp-proxy");
43
+ /** Runs only as the isolated model user. Its extra inherited pipe carries
44
+ * bounded broker requests; ordinary model stdout never becomes control data. */
45
+ async function runEntityboxModel(task) {
46
+ if (process.getuid?.() !== 10001 || process.env.HOME !== '/home/entitybox' || !fs.existsSync('/.dockerenv'))
47
+ throw Error('Isolated model container required');
48
+ const t = task;
49
+ if (!t || typeof t.prompt !== 'string' || t.prompt.length > 12200 || typeof t.model !== 'string' || !/^[a-zA-Z0-9_.:-]{1,120}$/.test(t.model)
50
+ || (t.systemPrompt !== undefined && (typeof t.systemPrompt !== 'string' || t.systemPrompt.length > 64000))
51
+ || (t.effort !== undefined && !['low', 'medium', 'high', 'xhigh', 'max'].includes(t.effort))
52
+ || (t.desktopMcp !== undefined && typeof t.desktopMcp !== 'boolean')
53
+ || (t.allowedTools !== undefined && (!Array.isArray(t.allowedTools) || !t.allowedTools.every(x => typeof x === 'string' && x.length < 200))))
54
+ throw Error('Invalid isolated model task');
55
+ const bus = new net.Socket({ fd: 3, readable: true, writable: true }), pending = new Map();
56
+ let buffer = '';
57
+ let handle, failed = false, finishing = false, catalogReady = !t.desktopMcp, cliReady = !t.desktopMcp;
58
+ let readyResolve, readyReject;
59
+ const desktopReady = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
60
+ desktopReady.catch(() => { });
61
+ let startupTimer;
62
+ const maybeReady = () => { if (catalogReady && cliReady) {
63
+ clearTimeout(startupTimer);
64
+ readyResolve();
65
+ } };
66
+ const send = (packet) => { if (bus.destroyed)
67
+ throw Error('Broker disconnected'); bus.write(JSON.stringify(packet) + '\n'); };
68
+ const clearPending = () => { for (const value of pending.values()) {
69
+ clearTimeout(value.timer);
70
+ value.res.destroy();
71
+ } pending.clear(); };
72
+ const failAll = () => { if (finishing)
73
+ return; failed = true; readyReject(Error('Required desktop connection unavailable')); clearPending(); handle?.kill(); bus.destroy(); };
74
+ bus.setEncoding('utf8');
75
+ bus.on('error', failAll);
76
+ bus.on('close', failAll);
77
+ bus.on('data', chunk => {
78
+ try {
79
+ buffer += chunk;
80
+ if (Buffer.byteLength(buffer) > 1024 * 1024)
81
+ throw Error();
82
+ let end;
83
+ while ((end = buffer.indexOf('\n')) >= 0) {
84
+ const packet = JSON.parse(buffer.slice(0, end));
85
+ buffer = buffer.slice(end + 1);
86
+ const item = pending.get(packet.id);
87
+ if (!item)
88
+ continue;
89
+ const event = packet.event;
90
+ if (event?.type === 'head' && [200, 400, 429, 502].includes(event.status) && ['application/json', 'text/event-stream'].includes(event.contentType))
91
+ item.res.writeHead(event.status, { 'content-type': event.contentType });
92
+ else if (event?.type === 'data' && typeof event.data === 'string') {
93
+ const bytes = Buffer.from(event.data, 'base64');
94
+ item.res.write(bytes);
95
+ if (item.catalog) {
96
+ item.catalogBytes = (item.catalogBytes ?? 0) + bytes.length;
97
+ if (item.catalogBytes > 512 * 1024)
98
+ throw Error();
99
+ item.catalog.push(bytes);
100
+ }
101
+ }
102
+ else if (event?.type === 'end') {
103
+ if (item.catalog) {
104
+ const response = JSON.parse(Buffer.concat(item.catalog).toString('utf8'));
105
+ if (!Array.isArray(response?.result?.tools) || !response.result.tools.length)
106
+ throw Error();
107
+ catalogReady = true;
108
+ }
109
+ clearTimeout(item.timer);
110
+ pending.delete(packet.id);
111
+ item.res.end();
112
+ maybeReady();
113
+ }
114
+ else if (event?.type === 'error') {
115
+ clearTimeout(item.timer);
116
+ pending.delete(packet.id);
117
+ if (item.res.headersSent)
118
+ item.res.destroy();
119
+ else {
120
+ item.res.writeHead(502, { 'content-type': 'application/json' });
121
+ item.res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message: 'The task connection ended.' } }));
122
+ }
123
+ if (t.desktopMcp)
124
+ failAll();
125
+ }
126
+ else
127
+ throw Error();
128
+ }
129
+ }
130
+ catch {
131
+ failAll();
132
+ }
133
+ });
134
+ const proxy = http.createServer(async (req, res) => {
135
+ const id = (0, crypto_1.randomUUID)();
136
+ let tracked = false;
137
+ try {
138
+ const desktop = req.url === '/entitybox/mcp' && t.desktopMcp === true;
139
+ if (req.method !== 'POST' || (!desktop && !['/v1/messages', '/v1/messages?beta=true', '/v1/messages/count_tokens', '/v1/messages/count_tokens?beta=true'].includes(req.url || '')) || pending.size >= 8)
140
+ throw Error();
141
+ let body = '';
142
+ req.setEncoding('utf8');
143
+ for await (const chunk of req) {
144
+ body += chunk;
145
+ if (Buffer.byteLength(body) > 512 * 1024)
146
+ throw Error();
147
+ }
148
+ const message = desktop ? JSON.parse(body) : undefined;
149
+ // Do not let the CLI silently continue with built-in tools after ignoring
150
+ // a failed required MCP startup. Both its connection status and the
151
+ // successfully delivered broker catalog must precede provider traffic.
152
+ if (!desktop)
153
+ await desktopReady;
154
+ if (failed || finishing || bus.destroyed || res.destroyed || pending.size >= 8)
155
+ throw Error();
156
+ const timer = setTimeout(() => { pending.delete(id); res.destroy(); try {
157
+ send({ type: 'broker_cancel', id });
158
+ }
159
+ catch { } if (desktop)
160
+ failAll(); }, 125000);
161
+ pending.set(id, { res, timer, ...(message?.method === 'tools/list' ? { catalog: [], catalogBytes: 0 } : {}) });
162
+ tracked = true;
163
+ res.on('close', () => { if (pending.delete(id)) {
164
+ clearTimeout(timer);
165
+ try {
166
+ send({ type: 'broker_cancel', id });
167
+ }
168
+ catch { }
169
+ } });
170
+ send({ type: 'broker_request', id, request: desktop ? { kind: 'desktop_mcp', message } : { kind: 'provider', method: 'POST', path: req.url, body, ...(typeof req.headers['anthropic-beta'] === 'string' ? { beta: req.headers['anthropic-beta'] } : {}) } });
171
+ }
172
+ catch {
173
+ if (tracked) {
174
+ const value = pending.get(id);
175
+ if (value)
176
+ clearTimeout(value.timer);
177
+ pending.delete(id);
178
+ }
179
+ if (!res.headersSent)
180
+ res.writeHead(502, { 'content-type': 'application/json' });
181
+ res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message: 'The task connection is unavailable.' } }));
182
+ // Optional CLI discovery (for example HEAD /api/hello) is refused
183
+ // locally without treating it as a lost required MCP connection.
184
+ if (t.desktopMcp && req.url === '/entitybox/mcp')
185
+ failAll();
186
+ }
187
+ });
188
+ proxy.requestTimeout = 130000;
189
+ await new Promise(resolve => proxy.listen(0, '127.0.0.1', resolve));
190
+ const port = proxy.address().port;
191
+ Object.assign(process.env, { ANTHROPIC_BASE_URL: 'http://127.0.0.1:' + port, ANTHROPIC_API_KEY: 'entitybox-broker', CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', DISABLE_AUTOUPDATER: '1', SHELL: '/bin/sh' });
192
+ const mcpServers = {};
193
+ if (t.desktopMcp) {
194
+ fs.writeFileSync('/work/entitybox-mcp-proxy.cjs', (0, model_mcp_proxy_1.entityboxModelMcpProxySource)('http://127.0.0.1:' + port + '/entitybox/mcp'), { mode: 0o600 });
195
+ mcpServers['entity-desktop-mcp'] = { command: process.execPath, args: ['/work/entitybox-mcp-proxy.cjs'] };
196
+ startupTimer = setTimeout(failAll, 25000);
197
+ }
198
+ fs.writeFileSync('/work/entitybox-mcp.json', JSON.stringify({ mcpServers }), { mode: 0o600 });
199
+ maybeReady();
200
+ try {
201
+ handle = (0, claude_print_1.spawnClaudePrint)({ workingDirectory: '/work', prompt: t.prompt, model: t.model, permissionMode: t.permissionMode ?? 'default', allowedTools: t.allowedTools, mcpConfigPath: '/work/entitybox-mcp.json', appendSystemPrompt: t.systemPrompt, effortLevel: t.effort });
202
+ handle.onEvent(event => {
203
+ if (event.type === 'error')
204
+ failed = true;
205
+ if (t.desktopMcp && event.type === 'claude:system') {
206
+ const raw = event.raw;
207
+ if (raw?.subtype === 'init') {
208
+ cliReady = Array.isArray(raw.mcp_servers) && raw.mcp_servers.some(server => server.name === 'entity-desktop-mcp' && server.status === 'connected');
209
+ if (!cliReady)
210
+ failAll();
211
+ else
212
+ maybeReady();
213
+ }
214
+ }
215
+ if (event.type === 'turn_complete' && (failed || !catalogReady || !cliReady))
216
+ return;
217
+ process.stdout.write(JSON.stringify(event) + '\n');
218
+ });
219
+ if (failed)
220
+ handle.kill();
221
+ const code = await handle.done;
222
+ return code === 0 && !failed && catalogReady && cliReady ? 0 : 70;
223
+ }
224
+ finally {
225
+ finishing = true;
226
+ clearTimeout(startupTimer);
227
+ readyReject(Error('Task ended'));
228
+ clearPending();
229
+ proxy.closeAllConnections();
230
+ await new Promise(resolve => proxy.close(() => resolve()));
231
+ bus.destroy();
232
+ }
233
+ }