@addai/node 0.30.12 → 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 +31 -6
  4. package/dist/capabilities.js +48 -26
  5. package/dist/claude-models.d.ts +71 -0
  6. package/dist/claude-models.js +330 -0
  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,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EntityboxVaultProvider = void 0;
4
+ exports.entityboxAnthropicKey = entityboxAnthropicKey;
5
+ exports.entityboxNativeProvider = entityboxNativeProvider;
6
+ const crypto_1 = require("crypto");
7
+ const config_1 = require("./config");
8
+ const entitybox_execution_lease_1 = require("./entitybox-execution-lease");
9
+ const entitybox_operation_1 = require("./entitybox-operation");
10
+ const unavailable = () => new entitybox_execution_lease_1.EntityboxExecutionError('The selected provider credential is unavailable. Check its Vault grant and provider fields.');
11
+ // Same field contract as EntityBox/services/worker/provider-key.mjs. Parse in
12
+ // host memory only. Do not include Vault values or diagnostics in errors.
13
+ function entityboxAnthropicKey(values) {
14
+ if (!values || typeof values !== 'object' || Array.isArray(values))
15
+ throw unavailable();
16
+ const fields = values;
17
+ const explicit = ['api_key', 'ANTHROPIC_API_KEY', 'anthropic_api_key', 'API_KEY'].map(k => fields[k]).filter((v) => typeof v === 'string' && !!v.trim());
18
+ const found = explicit.length ? explicit.map(v => v.trim()) : typeof fields.content === 'string' ? (fields.content.match(/sk-ant-[A-Za-z0-9_-]{20,}/g) ?? []) : [];
19
+ const unique = [...new Set(found)];
20
+ if (unique.length !== 1 || unique[0].length > 4096 || !/^sk-ant-[A-Za-z0-9_-]{20,}$/.test(unique[0]))
21
+ throw unavailable();
22
+ return unique[0];
23
+ }
24
+ /** Reuse the existing entity-authenticated Vault MCP get_login operation.
25
+ * Each lookup checks the current Vault grant; no secret cache, host login
26
+ * directory, environment fallback or credential file is used. */
27
+ class EntityboxVaultProvider {
28
+ lease;
29
+ config;
30
+ constructor(lease, config) {
31
+ this.lease = lease;
32
+ if (!/^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(config.itemId)
33
+ || typeof config.entityKey !== 'string' || !config.entityKey.trim() || config.entityKey.length > 8192 || /[\r\n]/.test(config.entityKey))
34
+ throw unavailable();
35
+ this.config = Object.freeze({ itemId: config.itemId.toLowerCase(), entityKey: config.entityKey, fetch: config.fetch });
36
+ }
37
+ async apiKey(signal) {
38
+ try {
39
+ this.lease.assertActive();
40
+ return await (0, entitybox_operation_1.entityboxOperation)([signal, this.lease.signal], 10000, async (operationSignal) => {
41
+ const id = (0, crypto_1.randomUUID)();
42
+ const response = await (this.config.fetch ?? fetch)(config_1.SUPABASE_URL + '/functions/v1/vault-mcp', {
43
+ method: 'POST', redirect: 'error', signal: operationSignal,
44
+ headers: { 'Content-Type': 'application/json', 'ENTITY-API-KEY': this.config.entityKey },
45
+ body: JSON.stringify({ jsonrpc: '2.0', id, method: 'tools/call', params: { name: 'get_login', arguments: { item_id: this.config.itemId, reason: 'EntityBox run ' + this.lease.identity.scope.runId + ': authenticate the selected AI provider' } } }),
46
+ });
47
+ if (!response.ok || !response.body) {
48
+ void response.body?.cancel().catch(() => { });
49
+ throw unavailable();
50
+ }
51
+ const length = response.headers.get('content-length');
52
+ if (length !== null && (!/^\d+$/.test(length) || Number(length) > 128 * 1024)) {
53
+ void response.body.cancel().catch(() => { });
54
+ throw unavailable();
55
+ }
56
+ const reader = response.body.getReader(), parts = [];
57
+ let size = 0;
58
+ const cancel = () => { void reader.cancel().catch(() => { }); };
59
+ operationSignal.addEventListener('abort', cancel, { once: true });
60
+ try {
61
+ if (operationSignal.aborted)
62
+ throw unavailable();
63
+ while (true) {
64
+ const chunk = await reader.read();
65
+ if (operationSignal.aborted)
66
+ throw unavailable();
67
+ if (chunk.done)
68
+ break;
69
+ size += chunk.value.byteLength;
70
+ if (size > 128 * 1024)
71
+ throw unavailable();
72
+ parts.push(chunk.value);
73
+ }
74
+ const envelope = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(parts)));
75
+ if (envelope.jsonrpc !== '2.0' || envelope.id !== id || envelope.error || !envelope.result || envelope.result.isError)
76
+ throw unavailable();
77
+ let payload = envelope.result.structuredContent;
78
+ if (payload === undefined) {
79
+ const values = [];
80
+ for (const part of envelope.result.content ?? [])
81
+ if (part?.type === 'text' && typeof part.text === 'string') {
82
+ try {
83
+ values.push(JSON.parse(part.text));
84
+ }
85
+ catch { }
86
+ }
87
+ if (values.length !== 1)
88
+ throw unavailable();
89
+ payload = values[0];
90
+ }
91
+ this.lease.assertActive();
92
+ if (operationSignal.aborted)
93
+ throw unavailable();
94
+ if (typeof payload?.item_id !== 'string' || payload.item_id.toLowerCase() !== this.config.itemId)
95
+ throw unavailable();
96
+ return entityboxAnthropicKey(payload?.values);
97
+ }
98
+ finally {
99
+ operationSignal.removeEventListener('abort', cancel);
100
+ cancel();
101
+ reader.releaseLock();
102
+ }
103
+ });
104
+ }
105
+ catch {
106
+ throw unavailable();
107
+ }
108
+ }
109
+ }
110
+ exports.EntityboxVaultProvider = EntityboxVaultProvider;
111
+ /** Construct the contained broker's API provider from server-verified native
112
+ * selection. The entity key comes from the read-only native desktop resolver.
113
+ * Every inference lookup rechecks that selection before asking Vault, so a
114
+ * revoked or changed binding cannot quietly switch credentials mid-task. */
115
+ async function entityboxNativeProvider(lease, entityKey, signal, transport) {
116
+ const selection = await (0, entitybox_operation_1.entityboxOperation)([signal, lease.signal], 4500, () => lease.providerSelection());
117
+ const vault = new EntityboxVaultProvider(lease, { itemId: selection.vaultItemId, entityKey, fetch: transport });
118
+ return {
119
+ models: Object.freeze([selection.model]),
120
+ apiKey: caller => (0, entitybox_operation_1.entityboxOperation)([caller, lease.signal], 10000, async (operationSignal) => {
121
+ await lease.providerSelection();
122
+ if (operationSignal.aborted)
123
+ throw unavailable();
124
+ return vault.apiKey(operationSignal);
125
+ }),
126
+ };
127
+ }
@@ -48,7 +48,9 @@ exports.HARNESSES = {
48
48
  label: 'Codex',
49
49
  npmPackage: '@openai/codex',
50
50
  findBinary: codex_binary_1.findCodexBinary,
51
- efforts: ['minimal', 'low', 'medium', 'high'],
51
+ // Until codex lists its models (codex-models.ts), whose efforts then win.
52
+ // xhigh and max reach older codex as high; see codexEffort().
53
+ efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
52
54
  login: {
53
55
  strategy: 'api-key',
54
56
  instructions: 'Paste an OpenAI API key (applied via `codex login --with-api-key`).',
package/dist/index.js CHANGED
@@ -1,6 +1,4 @@
1
1
  "use strict";
2
- // ainode — the +Ai Node background daemon that pairs with +Ai (Supabase)
3
- // and runs Claude / Codex sessions on behalf of authenticated requests.
4
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
3
  if (k2 === undefined) k2 = k;
6
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -37,6 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
37
35
  Object.defineProperty(exports, "__esModule", { value: true });
38
36
  exports.VERSION = void 0;
39
37
  exports.start = start;
38
+ const entitybox_capacity_health_host_1 = require("./entitybox-capacity-health-host");
39
+ const entitybox_recovery_host_1 = require("./entitybox-recovery-host");
40
+ const entitybox_execution_host_1 = require("./entitybox-execution-host");
41
+ const entitybox_capacity_host_1 = require("./entitybox-capacity-host");
42
+ // ainode — the +Ai Node background daemon that pairs with +Ai (Supabase)
43
+ // and runs Claude / Codex sessions on behalf of authenticated requests.
40
44
  const path = __importStar(require("path"));
41
45
  const control_server_1 = require("./control-server");
42
46
  const paths_1 = require("./paths");
@@ -63,6 +67,8 @@ const relay_client_1 = require("./desktop/relay-client");
63
67
  const session_runner_1 = require("./session-runner");
64
68
  const diskguard_1 = require("./diskguard");
65
69
  const pty_helper_1 = require("./pty-helper");
70
+ const supervisor_1 = require("./direct/supervisor");
71
+ const config_1 = require("./direct/config");
66
72
  // Read version from package.json at module-load. Previously hardcoded to
67
73
  // '0.1.0' and never updated when package.json bumped — the CLI banner lied
68
74
  // about which version was actually running, making upgrade verification
@@ -144,6 +150,13 @@ async function start(argv = []) {
144
150
  throw err;
145
151
  }
146
152
  }
153
+ // Explicit host opt-in only. Install shared admission before any desktop
154
+ // starts; ordinary runtime behavior stays unchanged when the flag is absent.
155
+ const entityboxCapacity = (0, entitybox_capacity_host_1.startEntityboxCapacityHost)();
156
+ const entityboxExecution = (0, entitybox_execution_host_1.startEntityboxExecutionHost)({ rpc: supabase_client_1.rpc, pairing: store_1.readPairing });
157
+ (0, request_pump_1.configureEntityboxQueueServices)(entityboxExecution.services);
158
+ const entityboxHealth = (0, entitybox_capacity_health_host_1.startEntityboxCapacityHealthHost)({ rpc: supabase_client_1.rpc, pairing: store_1.readPairing });
159
+ const entityboxRecovery = (0, entitybox_recovery_host_1.startEntityboxRecoveryHost)({ rpc: supabase_client_1.rpc, pairing: store_1.readPairing, activeRequests: request_pump_1.activeRequestIdList });
147
160
  // Begin the heartbeat — this also pushes the first capabilities probe
148
161
  // up to Supabase so the runtime appears "online" immediately. When the
149
162
  // server reports queued install/login commands, the command runner
@@ -230,6 +243,9 @@ async function start(argv = []) {
230
243
  (0, relay_client_1.onWake)('command', command_runner_1.wake);
231
244
  (0, relay_client_1.onWake)('request', request_pump_1.pumpNow);
232
245
  (0, relay_client_1.startRelayClient)();
246
+ // Optional direct coding sessions use their own child process and relay.
247
+ // Disabled by default; the entity/flow/desktop execution paths are unchanged.
248
+ const directService = (0, supervisor_1.startDirectService)((0, config_1.directEnvironment)());
233
249
  let stopped = false;
234
250
  // Outcome of the last drain, so a remote roll can report whether it left
235
251
  // work unfinished instead of claiming a clean restart either way.
@@ -238,12 +254,17 @@ async function start(argv = []) {
238
254
  if (stopped)
239
255
  return;
240
256
  stopped = true;
257
+ await directService.stop();
241
258
  // Stop accepting new work first — otherwise the pump would queue
242
259
  // another request between drain start and process exit.
243
260
  (0, projects_1.stop)();
244
261
  (0, manager_1.stopDesktopManager)();
262
+ entityboxCapacity.stop();
245
263
  (0, relay_client_1.stopRelayClient)();
246
264
  (0, request_pump_1.stop)();
265
+ entityboxExecution.stop();
266
+ entityboxRecovery.stop();
267
+ entityboxHealth.stop();
247
268
  (0, pump_1.stop)();
248
269
  (0, heartbeat_1.stop)();
249
270
  (0, auto_update_1.stop)();
@@ -260,8 +281,12 @@ async function start(argv = []) {
260
281
  // timeout to shut down, and the timeout is there because something
261
282
  // upstream is waiting for this process to be gone.
262
283
  const [requests, flows] = await Promise.all([
284
+ // Result is intentionally not used to release native resources.
263
285
  (0, request_pump_1.drain)(drainTimeoutMs),
264
286
  (0, pump_1.drain)(drainTimeoutMs),
287
+ entityboxCapacity.drain(drainTimeoutMs),
288
+ entityboxRecovery.drain(drainTimeoutMs),
289
+ entityboxHealth.drain(drainTimeoutMs),
265
290
  ]);
266
291
  lastDrain = { drained: requests.drained, timedOut: requests.timedOut };
267
292
  if (requests.drained || requests.timedOut) {
@@ -301,44 +326,51 @@ async function start(argv = []) {
301
326
  // and npm is never called — nothing to fetch, and nothing that can fail
302
327
  // offline.
303
328
  const performRestart = async ({ commandId, version, restartOnly }) => {
304
- const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
305
- const plan = (0, self_update_1.planRespawn)(mode, process.argv[1] ?? '', version, process.execPath, process.argv.slice(2));
306
- const supervised = (0, autostart_1.isSupervised)();
307
- if (plan.installFirst && !restartOnly) {
308
- const err = await (0, self_update_1.installGlobal)(version);
309
- if (err)
310
- throw new Error(err);
311
- }
312
- // Drain generously — a restart is elective, so it should never guillotine
313
- // a live entity turn the way a 15s SIGTERM drain would.
314
- await stop(RESTART_DRAIN_MS);
315
- // Point of no return. Past this line a replacement daemon either exists or
316
- // is guaranteed by the supervisor, so we MUST exit — two live daemons on
317
- // one node is worse than a command left unreported. Hence the handoff
318
- // write (which only affects reporting) can never abort the exit.
319
- if (!supervised) {
320
- (0, self_update_1.spawnReplacement)(plan, path.join(paths_1.RUNTIME_HOME, 'daemon.log'), process.cwd());
321
- }
329
+ if (!await directService.quiesce())
330
+ throw new Error('Close active direct coding sessions before updating or restarting this node.');
322
331
  try {
323
- (0, self_update_1.writeHandoff)(paths_1.RUNTIME_HOME, {
324
- command_id: commandId,
325
- from_version: VERSION,
326
- target_version: version,
327
- mode,
328
- at: new Date().toISOString(),
329
- drained: lastDrain.drained,
330
- timed_out: lastDrain.timedOut,
331
- restart_only: restartOnly === true,
332
- });
332
+ const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
333
+ const plan = (0, self_update_1.planRespawn)(mode, process.argv[1] ?? '', version, process.execPath, process.argv.slice(2));
334
+ const supervised = (0, autostart_1.isSupervised)();
335
+ if (plan.installFirst && !restartOnly) {
336
+ const err = await (0, self_update_1.installGlobal)(version);
337
+ if (err)
338
+ throw new Error(err);
339
+ }
340
+ // Drain generously — a restart is elective, so it should never guillotine
341
+ // a live entity turn the way a 15s SIGTERM drain would.
342
+ await stop(RESTART_DRAIN_MS);
343
+ // Point of no return. Past this line a replacement daemon either exists or
344
+ // is guaranteed by the supervisor, so we MUST exit — two live daemons on
345
+ // one node is worse than a command left unreported. Hence the handoff
346
+ // write (which only affects reporting) can never abort the exit.
347
+ if (!supervised) {
348
+ (0, self_update_1.spawnReplacement)(plan, path.join(paths_1.RUNTIME_HOME, 'daemon.log'), process.cwd());
349
+ }
350
+ try {
351
+ (0, self_update_1.writeHandoff)(paths_1.RUNTIME_HOME, {
352
+ command_id: commandId,
353
+ from_version: VERSION,
354
+ target_version: version,
355
+ mode,
356
+ at: new Date().toISOString(),
357
+ drained: lastDrain.drained,
358
+ timed_out: lastDrain.timedOut,
359
+ restart_only: restartOnly === true,
360
+ });
361
+ }
362
+ catch (err) {
363
+ console.error('[restart] handoff write failed — the roll still happened, it just cannot self-report:', err.message);
364
+ }
365
+ const what = restartOnly ? `${mode} → restart on ${version}` : `${mode} → ${version}`;
366
+ console.log(supervised
367
+ ? `[restart] standing down for launchd to restart us (${what}); exiting`
368
+ : `[restart] handed over to ${plan.file} (${what}); exiting`);
369
+ setTimeout(() => process.exit(0), 250).unref?.();
333
370
  }
334
- catch (err) {
335
- console.error('[restart] handoff write failed — the roll still happened, it just cannot self-report:', err.message);
371
+ finally {
372
+ directService.release();
336
373
  }
337
- const what = restartOnly ? `${mode} → restart on ${version}` : `${mode} → ${version}`;
338
- console.log(supervised
339
- ? `[restart] standing down for launchd to restart us (${what}); exiting`
340
- : `[restart] handed over to ${plan.file} (${what}); exiting`);
341
- setTimeout(() => process.exit(0), 250).unref?.();
342
374
  };
343
375
  (0, command_runner_1.setRestartHook)(performRestart);
344
376
  // Hourly auto-update — the node keeps itself and its harnesses current
@@ -347,7 +379,9 @@ async function start(argv = []) {
347
379
  // roll cannot drift apart.
348
380
  (0, auto_update_1.start)({
349
381
  token: () => (0, store_1.readPairing)()?.daemonToken ?? null,
350
- isBusy: () => (0, request_pump_1.activeRequestIdList)().length > 0,
382
+ isBusy: () => (0, request_pump_1.activeRequestIdList)().length > 0 || directService.busy(),
383
+ quiesceDirect: () => directService.quiesce(),
384
+ releaseDirect: () => directService.release(),
351
385
  breakerOpen: () => (0, supabase_client_1.rpcShouldSkip)(),
352
386
  enabled: heartbeat_1.autoUpdateEnabled,
353
387
  probe: () => (0, capabilities_1.probeCapabilities)(),
package/dist/install.d.ts CHANGED
@@ -42,7 +42,16 @@ export interface InstallResult {
42
42
  /** Cleanup callback — removes everything this install wrote. */
43
43
  dispose: () => Promise<void>;
44
44
  }
45
- export declare function writeMcpConfig(cwd: string, mcps: ResolvedMcp[]): string | null;
45
+ /** Resolve the existing request's desktop identity for the contained broker.
46
+ * This path is read-only: it never writes MCP/skill files, uses the installer
47
+ * cache, falls back to a different request, or accepts a caller's entity key.
48
+ * The caller must already hold a verified native execution lease. */
49
+ export declare function resolveEntityboxDesktopConnection(set: InstallSet, overrides: Record<string, Record<string, string>> | undefined, requestId: string, signal: AbortSignal): Promise<{
50
+ command: string;
51
+ args: string[];
52
+ env: Record<string, string>;
53
+ }>;
54
+ export declare function writeMcpConfig(cwd: string, mcps: ResolvedMcp[], entityboxRequestId?: string): string | null;
46
55
  /**
47
56
  * Run install side-effects against `cwd` and return paths to clean up
48
57
  * later. Safe to call with an empty set — returns a no-op result.
@@ -60,5 +69,8 @@ skillVarValues?: Record<string, Record<string, string>>,
60
69
  /** The entity_runtime_requests row being installed. Passed to the server
61
70
  * so credential resolution is scoped to THIS request instead of the
62
71
  * latest in-flight request on the runtime. */
63
- requestId?: string): Promise<InstallResult>;
72
+ requestId?: string,
73
+ /** Report a required EntityBox setup failure before the caller can spawn a
74
+ * harness. Ordinary native installs never enter this failure callback. */
75
+ onRequiredSetupFailure?: (message: string) => Promise<void>): Promise<InstallResult>;
64
76
  export {};
package/dist/install.js CHANGED
@@ -47,9 +47,13 @@ var __importStar = (this && this.__importStar) || (function () {
47
47
  })();
48
48
  Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.LEGACY_MCP_CONFIG_FILENAME = exports.MCP_CONFIG_FILENAME = void 0;
50
+ exports.resolveEntityboxDesktopConnection = resolveEntityboxDesktopConnection;
50
51
  exports.writeMcpConfig = writeMcpConfig;
51
52
  exports.install = install;
52
53
  const fs = __importStar(require("fs"));
54
+ const crypto_1 = require("crypto");
55
+ const entitybox_install_1 = require("./entitybox-install");
56
+ const entitybox_operation_1 = require("./entitybox-operation");
53
57
  const path = __importStar(require("path"));
54
58
  const supabase_client_1 = require("./supabase-client");
55
59
  const store_1 = require("./store");
@@ -68,10 +72,13 @@ const NOOP_RESULT = {
68
72
  function token() {
69
73
  return (0, store_1.readPairing)()?.daemonToken ?? null;
70
74
  }
71
- async function fetchResolved(set, requestId) {
75
+ async function fetchResolved(set, requestId, requireRequest = false) {
72
76
  const t = token();
73
- if (!t)
77
+ if (!t) {
78
+ if (requireRequest)
79
+ throw new entitybox_install_1.EntityboxInstallError('Pair the runtime before starting a computer task.');
74
80
  return { mcps: [], skills: [] };
81
+ }
75
82
  if (set.mcpIds.length === 0 && set.skillIds.length === 0 && set.packageIds.length === 0) {
76
83
  return { mcps: [], skills: [] };
77
84
  }
@@ -93,7 +100,7 @@ async function fetchResolved(set, requestId) {
93
100
  // A DB that predates the p_request_id migration rejects the unknown
94
101
  // parameter (PostgREST 404 "function not found"). Degrade to the legacy
95
102
  // runtime-wide heuristic rather than failing the whole install.
96
- if (requestId && err instanceof supabase_client_1.RpcError && err.status === 404) {
103
+ if (!requireRequest && requestId && err instanceof supabase_client_1.RpcError && err.status === 404) {
97
104
  out = await (0, supabase_client_1.rpc)('runtime_resolve_install_set', baseArgs);
98
105
  }
99
106
  else {
@@ -105,7 +112,37 @@ async function fetchResolved(set, requestId) {
105
112
  skills: Array.isArray(out?.skills) ? out.skills : [],
106
113
  };
107
114
  }
108
- function writeMcpConfig(cwd, mcps) {
115
+ /** Resolve the existing request's desktop identity for the contained broker.
116
+ * This path is read-only: it never writes MCP/skill files, uses the installer
117
+ * cache, falls back to a different request, or accepts a caller's entity key.
118
+ * The caller must already hold a verified native execution lease. */
119
+ async function resolveEntityboxDesktopConnection(set, overrides, requestId, signal) {
120
+ try {
121
+ const scope = (0, entitybox_install_1.entityboxInstallScope)(overrides, requestId);
122
+ if (!scope || Object.keys(overrides?.['entity-desktop-mcp'] ?? {}).some(key => key !== 'ENTITYBOX_DESKTOP_SCOPE'))
123
+ throw new Error();
124
+ // Snapshot before awaiting: the selected set and scope cannot be changed
125
+ // underneath an outstanding credential lookup by a mutable request row.
126
+ const selection = structuredClone(set);
127
+ return await (0, entitybox_operation_1.entityboxOperation)([signal], 4500, async (operationSignal) => {
128
+ const resolved = await fetchResolved(selection, requestId, true);
129
+ if (operationSignal.aborted)
130
+ throw new Error();
131
+ const rows = resolved.mcps.filter(m => m.slug === 'entity-desktop-mcp')
132
+ .map(m => ({ ...m, env: { ...(m.env ?? {}), ENTITYBOX_DESKTOP_SCOPE: scope } }));
133
+ (0, entitybox_install_1.assertEntityboxDesktopMcp)(rows, scope);
134
+ const m = rows[0], guard = path.join(__dirname, 'entitybox-mcp-guard.js');
135
+ if (!fs.existsSync(guard))
136
+ throw new Error();
137
+ const wrapped = (0, win_1.wrapMcpCommandForPlatform)(m.command, Array.isArray(m.args) ? m.args : []);
138
+ return { command: process.execPath, args: [guard, '--', wrapped.command, ...wrapped.args], env: m.env };
139
+ });
140
+ }
141
+ catch {
142
+ throw new entitybox_install_1.EntityboxInstallError('The selected computer identity could not be verified. Check the runtime connection and task access.');
143
+ }
144
+ }
145
+ function writeMcpConfig(cwd, mcps, entityboxRequestId) {
109
146
  if (mcps.length === 0)
110
147
  return null;
111
148
  const servers = {};
@@ -150,14 +187,23 @@ function writeMcpConfig(cwd, mcps) {
150
187
  // The daemon scopes these files per request id; the caller passes a
151
188
  // unique cwd directory or we'd race ourselves on concurrent sessions
152
189
  // in the same project.
153
- const target = path.join(cwd, exports.MCP_CONFIG_FILENAME);
190
+ if (entityboxRequestId !== undefined && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(entityboxRequestId)) {
191
+ throw new entitybox_install_1.EntityboxInstallError('Invalid computer task request identifier.');
192
+ }
193
+ // Each scoped attempt owns a unique file; a late dispose cannot remove a
194
+ // replacement attempt's credentials, even when a working directory is reused.
195
+ const target = path.join(cwd, entityboxRequestId
196
+ ? '.entitybox-' + entityboxRequestId + '-' + (0, crypto_1.randomUUID)() + '.mcp.json'
197
+ : exports.MCP_CONFIG_FILENAME);
154
198
  // A session dir reused from before the rename can still hold the old file;
155
199
  // leaving it would let an agent read a stale server list.
156
- try {
157
- fs.rmSync(path.join(cwd, exports.LEGACY_MCP_CONFIG_FILENAME), { force: true });
200
+ if (!entityboxRequestId) {
201
+ try {
202
+ fs.rmSync(path.join(cwd, exports.LEGACY_MCP_CONFIG_FILENAME), { force: true });
203
+ }
204
+ catch { /* best effort */ }
158
205
  }
159
- catch { /* best effort */ }
160
- fs.writeFileSync(target, JSON.stringify({ mcpServers: servers }, null, 2));
206
+ fs.writeFileSync(target, JSON.stringify({ mcpServers: servers }, null, 2), entityboxRequestId ? { flag: 'wx', mode: 0o600 } : undefined);
161
207
  return target;
162
208
  }
163
209
  function writeSkills(cwd, skills, skillVarValues) {
@@ -299,7 +345,66 @@ skillVarValues,
299
345
  /** The entity_runtime_requests row being installed. Passed to the server
300
346
  * so credential resolution is scoped to THIS request instead of the
301
347
  * latest in-flight request on the runtime. */
302
- requestId) {
348
+ requestId,
349
+ /** Report a required EntityBox setup failure before the caller can spawn a
350
+ * harness. Ordinary native installs never enter this failure callback. */
351
+ onRequiredSetupFailure) {
352
+ try {
353
+ const scope = (0, entitybox_install_1.entityboxInstallScope)(envOverrides, requestId);
354
+ if (scope !== null) {
355
+ let scoped;
356
+ try {
357
+ scoped = await fetchResolved(set, requestId, true);
358
+ }
359
+ catch {
360
+ throw new entitybox_install_1.EntityboxInstallError('The computer connection could not be verified. Check the runtime connection and try again.');
361
+ }
362
+ for (const m of scoped.mcps) {
363
+ const override = envOverrides?.[m.slug];
364
+ if (override)
365
+ m.env = { ...(m.env || {}), ...override };
366
+ }
367
+ (0, entitybox_install_1.assertEntityboxDesktopMcp)(scoped.mcps, scope);
368
+ const guard = path.join(__dirname, 'entitybox-mcp-guard.js');
369
+ if (!fs.existsSync(guard))
370
+ throw new entitybox_install_1.EntityboxInstallError('Update the runtime before starting a computer task.');
371
+ scoped.mcps = scoped.mcps.map(m => {
372
+ if (m.slug !== 'entity-desktop-mcp')
373
+ return m;
374
+ const wrapped = (0, win_1.wrapMcpCommandForPlatform)(m.command, Array.isArray(m.args) ? m.args : []);
375
+ // All harnesses consume the guarded descriptor, whether they read the
376
+ // generated config or build their own config from installed.mcps.
377
+ return { ...m, command: process.execPath, args: [guard, '--', wrapped.command, ...wrapped.args] };
378
+ });
379
+ // Required computer installs never use the ordinary best-effort cache.
380
+ const config = writeMcpConfig(cwd, scoped.mcps, requestId);
381
+ if (!config)
382
+ throw new entitybox_install_1.EntityboxInstallError('The required computer configuration could not be written.');
383
+ try {
384
+ const skills = writeSkills(cwd, scoped.skills, skillVarValues);
385
+ return { mcpConfigPath: config, skillSlugs: skills.slugs, mcps: scoped.mcps,
386
+ dispose: async () => { try {
387
+ fs.unlinkSync(config);
388
+ }
389
+ catch { /* already removed */ } } };
390
+ }
391
+ catch (error) {
392
+ try {
393
+ fs.unlinkSync(config);
394
+ }
395
+ catch { }
396
+ throw error;
397
+ }
398
+ }
399
+ }
400
+ catch (error) {
401
+ // A present restriction must never degrade to the ordinary install path.
402
+ const failure = error instanceof entitybox_install_1.EntityboxInstallError ? error
403
+ : new entitybox_install_1.EntityboxInstallError('The computer connection could not be installed.');
404
+ if (onRequiredSetupFailure)
405
+ await onRequiredSetupFailure(failure.message);
406
+ throw failure;
407
+ }
303
408
  if (set.mcpIds.length === 0 && set.skillIds.length === 0 && set.packageIds.length === 0) {
304
409
  return NOOP_RESULT;
305
410
  }
@@ -0,0 +1,38 @@
1
+ export type ModelProbeResult<R> = {
2
+ ok: true;
3
+ report: R;
4
+ } | {
5
+ ok: false;
6
+ reason: string;
7
+ };
8
+ export interface ModelCacheOptions<R extends {
9
+ models: string[];
10
+ }> {
11
+ probe: (bin: string) => Promise<ModelProbeResult<R>>;
12
+ now: () => number;
13
+ log: (line: string) => void;
14
+ /** Which CLI, for the log. */
15
+ agent: string;
16
+ /** Detail for the log line when a new list arrives. */
17
+ describe?: (report: R) => string;
18
+ /** True when the answer already held for this binary is worth more than a
19
+ * new one — codex's account list over the bundled one a probe that had to
20
+ * leave the login alone gets. The held answer then counts as refreshed. */
21
+ keep?: (held: R, next: R) => boolean;
22
+ }
23
+ export declare function createModelCache<R extends {
24
+ models: string[];
25
+ }>(opts: ModelCacheOptions<R>): {
26
+ /**
27
+ * The models for this binary state, or null to mean "use the built-in
28
+ * list". Never waits: a missing or stale answer is fetched in the
29
+ * background and arrives on a later call.
30
+ */
31
+ get(bin: string, key: string): R | null;
32
+ /** The last real answer, whichever binary gave it. */
33
+ last(): R | null;
34
+ };
35
+ /** A throwaway folder for one probe, under RUNTIME_HOME/model-probe. Folders
36
+ * older than a day are swept — a probe's own cleanup can lose a race with
37
+ * Windows releasing its files. */
38
+ export declare function makeProbeDir(prefix?: string): string | null;