@zackbart/connecta 0.15.1 → 0.16.1

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 (335) hide show
  1. package/AGENTS.md +40 -20
  2. package/CHANGELOG.md +653 -0
  3. package/README.md +25 -5
  4. package/bin/connecta.mjs +44 -3
  5. package/dist/access-tokens.d.ts +0 -1
  6. package/dist/access-tokens.js +0 -1
  7. package/dist/activity.d.ts +50 -2
  8. package/dist/activity.js +34 -1
  9. package/dist/apps-shell.d.ts +0 -1
  10. package/dist/apps-shell.js +0 -1
  11. package/dist/auth/bearer.d.ts +0 -1
  12. package/dist/auth/bearer.js +0 -1
  13. package/dist/auth/clerk.d.ts +0 -1
  14. package/dist/auth/clerk.js +45 -9
  15. package/dist/auth/downstream-oauth.d.ts +0 -1
  16. package/dist/auth/downstream-oauth.js +0 -1
  17. package/dist/call-admission.d.ts +0 -1
  18. package/dist/call-admission.js +0 -1
  19. package/dist/catalog-drift.d.ts +99 -0
  20. package/dist/catalog-drift.js +251 -0
  21. package/dist/catalog-fingerprint.d.ts +0 -1
  22. package/dist/catalog-fingerprint.js +0 -1
  23. package/dist/catalog-limits.d.ts +0 -1
  24. package/dist/catalog-limits.js +0 -1
  25. package/dist/catalog-service.d.ts +0 -1
  26. package/dist/catalog-service.js +41 -5
  27. package/dist/catalog.d.ts +11 -1
  28. package/dist/catalog.js +20 -1
  29. package/dist/concurrency.d.ts +0 -1
  30. package/dist/concurrency.js +0 -1
  31. package/dist/connector-scope.d.ts +0 -1
  32. package/dist/connector-scope.js +0 -1
  33. package/dist/connectors/api.d.ts +29 -21
  34. package/dist/connectors/api.js +45 -19
  35. package/dist/connectors/guarded-fetch.d.ts +106 -0
  36. package/dist/connectors/guarded-fetch.js +289 -0
  37. package/dist/connectors/remote-mcp.d.ts +0 -1
  38. package/dist/connectors/remote-mcp.js +0 -1
  39. package/dist/credentials.d.ts +0 -1
  40. package/dist/credentials.js +0 -1
  41. package/dist/errors.d.ts +30 -3
  42. package/dist/errors.js +1 -1
  43. package/dist/execute.d.ts +0 -1
  44. package/dist/execute.js +0 -1
  45. package/dist/executor-admission.d.ts +11 -1
  46. package/dist/executor-admission.js +28 -1
  47. package/dist/executor-result.d.ts +0 -1
  48. package/dist/executor-result.js +0 -1
  49. package/dist/executors/quickjs-child.d.ts +0 -1
  50. package/dist/executors/quickjs-child.js +0 -1
  51. package/dist/executors/quickjs-protocol.d.ts +0 -1
  52. package/dist/executors/quickjs-protocol.js +0 -1
  53. package/dist/executors/quickjs-runtime.d.ts +0 -1
  54. package/dist/executors/quickjs-runtime.js +0 -1
  55. package/dist/executors/quickjs.d.ts +0 -1
  56. package/dist/executors/quickjs.js +2 -1
  57. package/dist/favicon.d.ts +0 -1
  58. package/dist/favicon.js +0 -1
  59. package/dist/index.d.ts +2 -3
  60. package/dist/index.js +24 -7
  61. package/dist/invocation.d.ts +0 -1
  62. package/dist/invocation.js +0 -1
  63. package/dist/json-schema.d.ts +0 -1
  64. package/dist/json-schema.js +0 -1
  65. package/dist/mcp-result.d.ts +0 -1
  66. package/dist/mcp-result.js +0 -1
  67. package/dist/meta-tools.d.ts +0 -1
  68. package/dist/meta-tools.js +0 -1
  69. package/dist/node.d.ts +0 -1
  70. package/dist/node.js +0 -1
  71. package/dist/operator-ui/generated.d.ts +2 -3
  72. package/dist/operator-ui/generated.js +3 -4
  73. package/dist/operator-ui/model.d.ts +10 -1
  74. package/dist/operator-ui/model.js +0 -1
  75. package/dist/operator-ui/view.d.ts +167 -0
  76. package/dist/operator-ui/view.js +270 -0
  77. package/dist/providers/cloudflare.d.ts +0 -1
  78. package/dist/providers/cloudflare.js +261 -299
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -61
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -79
  83. package/dist/providers/notion.d.ts +0 -1
  84. package/dist/providers/notion.js +87 -42
  85. package/dist/providers/stripe.d.ts +13 -1
  86. package/dist/providers/stripe.js +31 -60
  87. package/dist/registry.d.ts +34 -2
  88. package/dist/registry.js +85 -5
  89. package/dist/retired-toolkits.d.ts +0 -1
  90. package/dist/retired-toolkits.js +0 -1
  91. package/dist/routes/access-tokens.d.ts +0 -1
  92. package/dist/routes/access-tokens.js +0 -1
  93. package/dist/routes/activity.d.ts +0 -1
  94. package/dist/routes/activity.js +0 -1
  95. package/dist/routes/credentials.d.ts +0 -1
  96. package/dist/routes/credentials.js +0 -1
  97. package/dist/routes/mcp.d.ts +0 -1
  98. package/dist/routes/mcp.js +0 -1
  99. package/dist/routes/oauth.d.ts +0 -1
  100. package/dist/routes/oauth.js +0 -1
  101. package/dist/routes/shared.d.ts +2 -1
  102. package/dist/routes/shared.js +0 -1
  103. package/dist/routes/ui.d.ts +0 -1
  104. package/dist/routes/ui.js +0 -1
  105. package/dist/server.d.ts +0 -1
  106. package/dist/server.js +11 -1
  107. package/dist/skills.d.ts +0 -1
  108. package/dist/skills.js +0 -1
  109. package/dist/storage/file.d.ts +0 -1
  110. package/dist/storage/file.js +0 -1
  111. package/dist/storage/memory.d.ts +0 -1
  112. package/dist/storage/memory.js +0 -1
  113. package/dist/timeout.d.ts +0 -1
  114. package/dist/timeout.js +0 -1
  115. package/dist/tool-safety.d.ts +0 -1
  116. package/dist/tool-safety.js +0 -1
  117. package/dist/types.d.ts +45 -1
  118. package/dist/types.js +0 -1
  119. package/dist/ui.d.ts +0 -1
  120. package/dist/ui.js +13 -137
  121. package/dist/validate.d.ts +13 -15
  122. package/dist/validate.js +24 -11
  123. package/dist/version.d.ts +1 -2
  124. package/dist/version.js +1 -2
  125. package/documentation/architecture.md +186 -5
  126. package/documentation/auth.md +11 -0
  127. package/documentation/call-admission.md +142 -6
  128. package/documentation/cloudflare.md +92 -10
  129. package/documentation/code-first-exploration.md +2 -2
  130. package/documentation/code-mode.md +14 -14
  131. package/documentation/connector-guides.md +172 -5
  132. package/documentation/connectors.md +89 -0
  133. package/documentation/linear.md +20 -2
  134. package/documentation/mcp-ui-design.md +2 -2
  135. package/documentation/meta-tools.md +7 -1
  136. package/documentation/mixpanel.md +42 -9
  137. package/documentation/notion.md +23 -2
  138. package/documentation/operations.md +325 -5
  139. package/documentation/operator-ui.md +120 -5
  140. package/documentation/provider-audit.md +160 -0
  141. package/documentation/provider-conventions.md +667 -0
  142. package/documentation/request-admission.md +167 -5
  143. package/documentation/rich-output-design.md +1 -2
  144. package/documentation/stripe.md +15 -0
  145. package/documentation/upgrading.md +454 -0
  146. package/ethos.md +14 -9
  147. package/examples/worker/README.md +102 -12
  148. package/examples/worker/src/index.ts +46 -1
  149. package/examples/worker/wrangler.jsonc +14 -1
  150. package/package.json +10 -6
  151. package/templates/node/.dockerignore +10 -0
  152. package/templates/node/.env.example +40 -3
  153. package/templates/node/AGENTS.md +16 -1
  154. package/templates/node/Dockerfile +49 -0
  155. package/templates/node/README.md +117 -2
  156. package/templates/node/docker-compose.yml +39 -0
  157. package/templates/node/package.json +1 -1
  158. package/templates/node/src/file-activity.ts +182 -0
  159. package/templates/node/src/index.ts +80 -4
  160. package/assets/connecta-clay-hero.png +0 -0
  161. package/dist/access-tokens.d.ts.map +0 -1
  162. package/dist/access-tokens.js.map +0 -1
  163. package/dist/activity.d.ts.map +0 -1
  164. package/dist/activity.js.map +0 -1
  165. package/dist/apps-shell.d.ts.map +0 -1
  166. package/dist/apps-shell.js.map +0 -1
  167. package/dist/auth/bearer.d.ts.map +0 -1
  168. package/dist/auth/bearer.js.map +0 -1
  169. package/dist/auth/clerk.d.ts.map +0 -1
  170. package/dist/auth/clerk.js.map +0 -1
  171. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  172. package/dist/auth/downstream-oauth.js.map +0 -1
  173. package/dist/call-admission.d.ts.map +0 -1
  174. package/dist/call-admission.js.map +0 -1
  175. package/dist/catalog-fingerprint.d.ts.map +0 -1
  176. package/dist/catalog-fingerprint.js.map +0 -1
  177. package/dist/catalog-limits.d.ts.map +0 -1
  178. package/dist/catalog-limits.js.map +0 -1
  179. package/dist/catalog-service.d.ts.map +0 -1
  180. package/dist/catalog-service.js.map +0 -1
  181. package/dist/catalog.d.ts.map +0 -1
  182. package/dist/catalog.js.map +0 -1
  183. package/dist/concurrency.d.ts.map +0 -1
  184. package/dist/concurrency.js.map +0 -1
  185. package/dist/connector-scope.d.ts.map +0 -1
  186. package/dist/connector-scope.js.map +0 -1
  187. package/dist/connectors/api.d.ts.map +0 -1
  188. package/dist/connectors/api.js.map +0 -1
  189. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  190. package/dist/connectors/remote-mcp.js.map +0 -1
  191. package/dist/credentials.d.ts.map +0 -1
  192. package/dist/credentials.js.map +0 -1
  193. package/dist/errors.d.ts.map +0 -1
  194. package/dist/errors.js.map +0 -1
  195. package/dist/execute.d.ts.map +0 -1
  196. package/dist/execute.js.map +0 -1
  197. package/dist/executor-admission.d.ts.map +0 -1
  198. package/dist/executor-admission.js.map +0 -1
  199. package/dist/executor-result.d.ts.map +0 -1
  200. package/dist/executor-result.js.map +0 -1
  201. package/dist/executors/quickjs-child.d.ts.map +0 -1
  202. package/dist/executors/quickjs-child.js.map +0 -1
  203. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  204. package/dist/executors/quickjs-protocol.js.map +0 -1
  205. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  206. package/dist/executors/quickjs-runtime.js.map +0 -1
  207. package/dist/executors/quickjs.d.ts.map +0 -1
  208. package/dist/executors/quickjs.js.map +0 -1
  209. package/dist/favicon.d.ts.map +0 -1
  210. package/dist/favicon.js.map +0 -1
  211. package/dist/index.d.ts.map +0 -1
  212. package/dist/index.js.map +0 -1
  213. package/dist/invocation.d.ts.map +0 -1
  214. package/dist/invocation.js.map +0 -1
  215. package/dist/json-schema.d.ts.map +0 -1
  216. package/dist/json-schema.js.map +0 -1
  217. package/dist/mcp-result.d.ts.map +0 -1
  218. package/dist/mcp-result.js.map +0 -1
  219. package/dist/meta-tools.d.ts.map +0 -1
  220. package/dist/meta-tools.js.map +0 -1
  221. package/dist/node.d.ts.map +0 -1
  222. package/dist/node.js.map +0 -1
  223. package/dist/operator-ui/generated.d.ts.map +0 -1
  224. package/dist/operator-ui/generated.js.map +0 -1
  225. package/dist/operator-ui/model.d.ts.map +0 -1
  226. package/dist/operator-ui/model.js.map +0 -1
  227. package/dist/providers/cloudflare.d.ts.map +0 -1
  228. package/dist/providers/cloudflare.js.map +0 -1
  229. package/dist/providers/linear.d.ts.map +0 -1
  230. package/dist/providers/linear.js.map +0 -1
  231. package/dist/providers/mixpanel.d.ts.map +0 -1
  232. package/dist/providers/mixpanel.js.map +0 -1
  233. package/dist/providers/notion.d.ts.map +0 -1
  234. package/dist/providers/notion.js.map +0 -1
  235. package/dist/providers/stripe.d.ts.map +0 -1
  236. package/dist/providers/stripe.js.map +0 -1
  237. package/dist/registry.d.ts.map +0 -1
  238. package/dist/registry.js.map +0 -1
  239. package/dist/retired-toolkits.d.ts.map +0 -1
  240. package/dist/retired-toolkits.js.map +0 -1
  241. package/dist/routes/access-tokens.d.ts.map +0 -1
  242. package/dist/routes/access-tokens.js.map +0 -1
  243. package/dist/routes/activity.d.ts.map +0 -1
  244. package/dist/routes/activity.js.map +0 -1
  245. package/dist/routes/credentials.d.ts.map +0 -1
  246. package/dist/routes/credentials.js.map +0 -1
  247. package/dist/routes/mcp.d.ts.map +0 -1
  248. package/dist/routes/mcp.js.map +0 -1
  249. package/dist/routes/oauth.d.ts.map +0 -1
  250. package/dist/routes/oauth.js.map +0 -1
  251. package/dist/routes/shared.d.ts.map +0 -1
  252. package/dist/routes/shared.js.map +0 -1
  253. package/dist/routes/ui.d.ts.map +0 -1
  254. package/dist/routes/ui.js.map +0 -1
  255. package/dist/server.d.ts.map +0 -1
  256. package/dist/server.js.map +0 -1
  257. package/dist/skills.d.ts.map +0 -1
  258. package/dist/skills.js.map +0 -1
  259. package/dist/storage/file.d.ts.map +0 -1
  260. package/dist/storage/file.js.map +0 -1
  261. package/dist/storage/memory.d.ts.map +0 -1
  262. package/dist/storage/memory.js.map +0 -1
  263. package/dist/timeout.d.ts.map +0 -1
  264. package/dist/timeout.js.map +0 -1
  265. package/dist/tool-safety.d.ts.map +0 -1
  266. package/dist/tool-safety.js.map +0 -1
  267. package/dist/types.d.ts.map +0 -1
  268. package/dist/types.js.map +0 -1
  269. package/dist/ui.d.ts.map +0 -1
  270. package/dist/ui.js.map +0 -1
  271. package/dist/validate.d.ts.map +0 -1
  272. package/dist/validate.js.map +0 -1
  273. package/dist/version.d.ts.map +0 -1
  274. package/dist/version.js.map +0 -1
  275. package/examples/node/README.md +0 -52
  276. package/examples/node/src/index.ts +0 -71
  277. package/src/access-tokens.ts +0 -289
  278. package/src/activity.ts +0 -234
  279. package/src/apps-shell.ts +0 -370
  280. package/src/auth/bearer.ts +0 -61
  281. package/src/auth/clerk.ts +0 -553
  282. package/src/auth/downstream-oauth.ts +0 -575
  283. package/src/call-admission.ts +0 -521
  284. package/src/catalog-fingerprint.ts +0 -52
  285. package/src/catalog-limits.ts +0 -11
  286. package/src/catalog-service.ts +0 -1153
  287. package/src/catalog.ts +0 -790
  288. package/src/concurrency.ts +0 -39
  289. package/src/connector-scope.ts +0 -66
  290. package/src/connectors/api.ts +0 -159
  291. package/src/connectors/remote-mcp.ts +0 -1090
  292. package/src/credentials.ts +0 -428
  293. package/src/errors.ts +0 -364
  294. package/src/execute.ts +0 -1406
  295. package/src/executor-admission.ts +0 -328
  296. package/src/executor-result.ts +0 -107
  297. package/src/executors/quickjs-child.ts +0 -172
  298. package/src/executors/quickjs-protocol.ts +0 -99
  299. package/src/executors/quickjs-runtime.ts +0 -470
  300. package/src/executors/quickjs.ts +0 -776
  301. package/src/favicon.ts +0 -53
  302. package/src/index.ts +0 -666
  303. package/src/invocation.ts +0 -655
  304. package/src/json-schema.ts +0 -11
  305. package/src/mcp-result.ts +0 -43
  306. package/src/meta-tools.ts +0 -1607
  307. package/src/node.ts +0 -206
  308. package/src/operator-ui/browser.css +0 -423
  309. package/src/operator-ui/browser.ts +0 -1281
  310. package/src/operator-ui/generated.ts +0 -4
  311. package/src/operator-ui/model.ts +0 -111
  312. package/src/providers/cloudflare.ts +0 -3999
  313. package/src/providers/linear.ts +0 -302
  314. package/src/providers/mixpanel.ts +0 -229
  315. package/src/providers/notion.ts +0 -1879
  316. package/src/providers/stripe.ts +0 -307
  317. package/src/registry.ts +0 -1085
  318. package/src/retired-toolkits.ts +0 -14
  319. package/src/routes/access-tokens.ts +0 -115
  320. package/src/routes/activity.ts +0 -215
  321. package/src/routes/credentials.ts +0 -254
  322. package/src/routes/mcp.ts +0 -429
  323. package/src/routes/oauth.ts +0 -322
  324. package/src/routes/shared.ts +0 -247
  325. package/src/routes/ui.ts +0 -152
  326. package/src/server.ts +0 -195
  327. package/src/skills.ts +0 -271
  328. package/src/storage/file.ts +0 -123
  329. package/src/storage/memory.ts +0 -44
  330. package/src/timeout.ts +0 -84
  331. package/src/tool-safety.ts +0 -15
  332. package/src/types.ts +0 -523
  333. package/src/ui.ts +0 -744
  334. package/src/validate.ts +0 -387
  335. package/src/version.ts +0 -7
@@ -1,776 +0,0 @@
1
- // Node's built-in code-mode executor. QuickJS itself lives in a disposable
2
- // child process: guest CPU, WASM aborts, and interpreter OOMs cannot block or
3
- // terminate the HTTP-serving process.
4
-
5
- import { Buffer } from "node:buffer";
6
- import { fork, type ChildProcess } from "node:child_process";
7
- import { existsSync } from "node:fs";
8
- import { fileURLToPath } from "node:url";
9
- import {
10
- AdmissionController,
11
- ExecutorAdmissionError,
12
- ExecutorExecutionError,
13
- } from "../executor-admission.js";
14
- import type {
15
- AdmittingExecutor,
16
- AdmissionSnapshot,
17
- ExecuteResult,
18
- ExecutorLease,
19
- ExecutorProvider,
20
- } from "../types.js";
21
- import {
22
- hostCallLabel,
23
- MAX_QUICKJS_IPC_BYTES,
24
- MAX_QUICKJS_HOST_RPC_BYTES,
25
- type ChildToParentMessage,
26
- type ExecutionPayload,
27
- type HostCallPayload,
28
- type HostResultPayload,
29
- type ParentToChildMessage,
30
- type RunPayload,
31
- serializedBytes,
32
- stringifyBounded,
33
- } from "./quickjs-protocol.js";
34
- import type { QuickJsRuntimeOptions } from "./quickjs-runtime.js";
35
-
36
- export { normalizeCode } from "./quickjs-runtime.js";
37
-
38
- export interface QuickJsExecutorOptions {
39
- /**
40
- * Wall-clock budget for the whole execution, host tool calls included.
41
- * Default 30s.
42
- */
43
- timeoutMs?: number;
44
- /**
45
- * Cumulative time spent synchronously driving guest JavaScript. Host waits do
46
- * not consume it. Default 250ms.
47
- */
48
- cpuTimeMs?: number;
49
- /** Guest heap limit per child execution. Default 64 MiB. */
50
- memoryLimitBytes?: number;
51
- /** Guest stack limit. Default 1 MiB. */
52
- maxStackSizeBytes?: number;
53
- /** Maximum simultaneous executions/child processes. Default 1. */
54
- concurrency?: number;
55
- /** Maximum callers waiting before provider construction. Default 32. */
56
- maxQueueSize?: number;
57
- /** Maximum admission wait. Default 5s. */
58
- queueTimeoutMs?: number;
59
- }
60
-
61
- interface ActiveRun {
62
- id: number;
63
- providers: Map<string, ExecutorProvider>;
64
- resolve: (outcome: ExecuteResult) => void;
65
- reject: (error: Error) => void;
66
- signal?: AbortSignal;
67
- onAbort?: () => void;
68
- wallTimer: ReturnType<typeof setTimeout>;
69
- }
70
-
71
- interface ChildSlot {
72
- child?: ChildProcess | undefined;
73
- ready?: Promise<void> | undefined;
74
- resolveReady?: (() => void) | undefined;
75
- rejectReady?: ((error: Error) => void) | undefined;
76
- readyTimer?: ReturnType<typeof setTimeout> | undefined;
77
- active?: ActiveRun | undefined;
78
- expectedExit?: ChildProcess | undefined;
79
- consecutiveCrashes: number;
80
- notBefore: number;
81
- }
82
-
83
- const DEFAULT_TIMEOUT_MS = 30_000;
84
- const DEFAULT_CPU_TIME_MS = 250;
85
- const DEFAULT_MEMORY_LIMIT_BYTES = 64 * 1024 * 1024;
86
- const DEFAULT_STACK_LIMIT_BYTES = 1024 * 1024;
87
- const DEFAULT_CONCURRENCY = 1;
88
- const DEFAULT_MAX_QUEUE_SIZE = 32;
89
- const DEFAULT_QUEUE_TIMEOUT_MS = 5_000;
90
- const CHILD_EXIT_GRACE_MS = 250;
91
- const CHILD_STARTUP_TIMEOUT_MS = 10_000;
92
- const MAX_CHILD_STDERR_BYTES = 8 * 1024;
93
- const MAX_ERROR_CHARS = 4_000;
94
-
95
- function msg(err: unknown): string {
96
- return err instanceof Error ? err.message : String(err);
97
- }
98
-
99
- function retainStderrTail(current: Buffer, chunk: Buffer | string): Buffer {
100
- const incoming = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
101
- if (incoming.length >= MAX_CHILD_STDERR_BYTES) {
102
- return Buffer.from(
103
- incoming.subarray(incoming.length - MAX_CHILD_STDERR_BYTES),
104
- );
105
- }
106
- const combined = Buffer.concat([current, incoming]);
107
- if (combined.length <= MAX_CHILD_STDERR_BYTES) return combined;
108
- return Buffer.from(combined.subarray(combined.length - MAX_CHILD_STDERR_BYTES));
109
- }
110
-
111
- function childExitError(message: string, stderrTail: Buffer): Error {
112
- if (stderrTail.length === 0) return new Error(message);
113
- return new Error(
114
- `${message}\nRecent child stderr (last ${stderrTail.length} bytes):\n` +
115
- stderrTail.toString("utf8"),
116
- );
117
- }
118
-
119
- function positiveWhole(
120
- value: number | undefined,
121
- fallback: number,
122
- name: string,
123
- ): number {
124
- const resolved = value ?? fallback;
125
- if (
126
- !Number.isFinite(resolved) ||
127
- !Number.isInteger(resolved) ||
128
- resolved < 1
129
- ) {
130
- throw new TypeError(`${name} must be a positive whole number.`);
131
- }
132
- return resolved;
133
- }
134
-
135
- function nonNegativeWhole(
136
- value: number | undefined,
137
- fallback: number,
138
- name: string,
139
- ): number {
140
- const resolved = value ?? fallback;
141
- if (
142
- !Number.isFinite(resolved) ||
143
- !Number.isInteger(resolved) ||
144
- resolved < 0
145
- ) {
146
- throw new TypeError(`${name} must be a non-negative whole number.`);
147
- }
148
- return resolved;
149
- }
150
-
151
- function errorPayload(error: string): string {
152
- return JSON.stringify({
153
- ok: false,
154
- error: error.slice(0, MAX_ERROR_CHARS),
155
- } satisfies HostResultPayload);
156
- }
157
-
158
- function delay(ms: number, signal?: AbortSignal): Promise<void> {
159
- if (ms <= 0) return Promise.resolve();
160
- return new Promise((resolve, reject) => {
161
- const timer = setTimeout(() => {
162
- signal?.removeEventListener("abort", onAbort);
163
- resolve();
164
- }, ms);
165
- const onAbort = () => {
166
- clearTimeout(timer);
167
- signal?.removeEventListener("abort", onAbort);
168
- reject(
169
- new ExecutorAdmissionError(
170
- "executor_cancelled",
171
- "Execution was cancelled while the sandbox was restarting.",
172
- ),
173
- );
174
- };
175
- signal?.addEventListener("abort", onAbort, { once: true });
176
- if (signal?.aborted) onAbort();
177
- });
178
- }
179
-
180
- function waitForReady(
181
- ready: Promise<void>,
182
- signal?: AbortSignal,
183
- ): Promise<void> {
184
- if (!signal) return ready;
185
- return new Promise((resolve, reject) => {
186
- let settled = false;
187
- const finish = (operation: () => void) => {
188
- if (settled) return;
189
- settled = true;
190
- signal.removeEventListener("abort", onAbort);
191
- operation();
192
- };
193
- const onAbort = () =>
194
- finish(() =>
195
- reject(
196
- new ExecutorAdmissionError(
197
- "executor_cancelled",
198
- "Execution was cancelled while the sandbox was starting.",
199
- ),
200
- ),
201
- );
202
- signal.addEventListener("abort", onAbort, { once: true });
203
- if (signal.aborted) {
204
- onAbort();
205
- return;
206
- }
207
- void ready.then(
208
- () => finish(resolve),
209
- (error: unknown) =>
210
- finish(() =>
211
- reject(error instanceof Error ? error : new Error(String(error))),
212
- ),
213
- );
214
- });
215
- }
216
-
217
- class QuickJsChildPool implements AdmittingExecutor {
218
- private readonly admission: AdmissionController;
219
- private readonly slots: ChildSlot[];
220
- private readonly available: ChildSlot[];
221
- private readonly runtimeOptions: QuickJsRuntimeOptions;
222
- private closed = false;
223
- private nextJobId = 1;
224
-
225
- constructor(options: QuickJsExecutorOptions) {
226
- const concurrency = positiveWhole(
227
- options.concurrency,
228
- DEFAULT_CONCURRENCY,
229
- "concurrency",
230
- );
231
- const queueTimeoutMs = positiveWhole(
232
- options.queueTimeoutMs,
233
- DEFAULT_QUEUE_TIMEOUT_MS,
234
- "queueTimeoutMs",
235
- );
236
- this.admission = new AdmissionController({
237
- concurrency,
238
- maxQueueSize: nonNegativeWhole(
239
- options.maxQueueSize,
240
- DEFAULT_MAX_QUEUE_SIZE,
241
- "maxQueueSize",
242
- ),
243
- queueTimeoutMs,
244
- retryAfterMs: queueTimeoutMs,
245
- });
246
- this.runtimeOptions = {
247
- timeoutMs: positiveWhole(
248
- options.timeoutMs,
249
- DEFAULT_TIMEOUT_MS,
250
- "timeoutMs",
251
- ),
252
- cpuTimeMs: positiveWhole(
253
- options.cpuTimeMs,
254
- DEFAULT_CPU_TIME_MS,
255
- "cpuTimeMs",
256
- ),
257
- memoryLimitBytes: positiveWhole(
258
- options.memoryLimitBytes,
259
- DEFAULT_MEMORY_LIMIT_BYTES,
260
- "memoryLimitBytes",
261
- ),
262
- maxStackSizeBytes: positiveWhole(
263
- options.maxStackSizeBytes,
264
- DEFAULT_STACK_LIMIT_BYTES,
265
- "maxStackSizeBytes",
266
- ),
267
- };
268
- this.slots = Array.from({ length: concurrency }, () => ({
269
- consecutiveCrashes: 0,
270
- notBefore: 0,
271
- }));
272
- this.available = [...this.slots];
273
- }
274
-
275
- async acquire(options: { signal?: AbortSignal } = {}): Promise<ExecutorLease> {
276
- const admission = await this.admission.acquire(options);
277
- const slot = this.available.shift();
278
- if (!slot) {
279
- admission.release();
280
- throw new Error("Executor admission invariant failed: no child slot.");
281
- }
282
- let released = false;
283
- let executed = false;
284
- return {
285
- waitMs: admission.waitMs,
286
- execute: async (code, providers) => {
287
- if (released) throw new Error("Executor lease was already released.");
288
- if (executed) throw new Error("Executor lease may execute only once.");
289
- executed = true;
290
- return this.run(slot, code, providers, options.signal);
291
- },
292
- release: () => {
293
- if (released) return;
294
- released = true;
295
- if (slot.active) {
296
- this.rejectActive(
297
- slot,
298
- new Error("Executor lease was released during execution."),
299
- );
300
- this.recycle(slot);
301
- }
302
- this.available.push(slot);
303
- admission.release();
304
- },
305
- };
306
- }
307
-
308
- admissionSnapshot(): AdmissionSnapshot {
309
- return this.admission.snapshot();
310
- }
311
-
312
- async execute(
313
- code: string,
314
- providers: ExecutorProvider[],
315
- ): Promise<ExecuteResult> {
316
- const lease = await this.acquire();
317
- try {
318
- return await lease.execute(code, providers);
319
- } finally {
320
- lease.release();
321
- }
322
- }
323
-
324
- async close(): Promise<void> {
325
- if (this.closed) return;
326
- this.closed = true;
327
- this.admission.close();
328
- const exits = this.slots.map((slot) => this.stopSlot(slot));
329
- await Promise.allSettled(exits);
330
- }
331
-
332
- private async run(
333
- slot: ChildSlot,
334
- code: string,
335
- providers: ExecutorProvider[],
336
- signal?: AbortSignal,
337
- ): Promise<ExecuteResult> {
338
- if (signal?.aborted) {
339
- throw new ExecutorAdmissionError(
340
- "executor_cancelled",
341
- "Execution was cancelled before it started.",
342
- );
343
- }
344
- await this.ensureChild(slot, signal);
345
- if (signal?.aborted) {
346
- throw new ExecutorAdmissionError(
347
- "executor_cancelled",
348
- "Execution was cancelled before it started.",
349
- );
350
- }
351
- const child = slot.child;
352
- if (!child?.connected) {
353
- throw new Error("QuickJS child IPC channel is unavailable.");
354
- }
355
-
356
- const id = this.nextJobId++;
357
- const providerMap = new Map(providers.map((item) => [item.name, item]));
358
- let payloadJson: string;
359
- let runMessage: ParentToChildMessage;
360
- try {
361
- payloadJson = stringifyBounded(
362
- {
363
- id,
364
- code,
365
- providers: providers.map((item) => ({
366
- name: item.name,
367
- ...(item.prelude ? { prelude: item.prelude } : {}),
368
- })),
369
- options: this.runtimeOptions,
370
- } satisfies RunPayload,
371
- "QuickJS run payload",
372
- );
373
- runMessage = { type: "run", payloadJson };
374
- stringifyBounded(runMessage, "QuickJS run IPC envelope");
375
- } catch (err) {
376
- return { result: undefined, error: msg(err) };
377
- }
378
-
379
- child.ref();
380
- child.channel?.ref();
381
- return new Promise<ExecuteResult>((resolve, reject) => {
382
- const wallTimer = setTimeout(() => {
383
- this.resolveActive(
384
- slot,
385
- {
386
- result: undefined,
387
- error: `QuickJS child exceeded the ${this.runtimeOptions.timeoutMs}ms wall budget and was terminated.`,
388
- },
389
- );
390
- this.recycle(slot);
391
- }, this.runtimeOptions.timeoutMs + CHILD_EXIT_GRACE_MS);
392
- const active: ActiveRun = {
393
- id,
394
- providers: providerMap,
395
- resolve,
396
- reject,
397
- ...(signal ? { signal } : {}),
398
- wallTimer,
399
- };
400
- active.onAbort = () => {
401
- this.rejectActive(
402
- slot,
403
- new ExecutorAdmissionError(
404
- "executor_cancelled",
405
- "Execution was cancelled.",
406
- ),
407
- );
408
- this.recycle(slot);
409
- };
410
- signal?.addEventListener("abort", active.onAbort, { once: true });
411
- slot.active = active;
412
- if (signal?.aborted) {
413
- active.onAbort();
414
- return;
415
- }
416
- try {
417
- child.send(
418
- runMessage,
419
- (error) => {
420
- if (!error || slot.active?.id !== id) return;
421
- this.rejectActive(slot, error);
422
- this.recycle(slot);
423
- },
424
- );
425
- } catch (err) {
426
- this.rejectActive(
427
- slot,
428
- err instanceof Error ? err : new Error(String(err)),
429
- );
430
- this.recycle(slot);
431
- }
432
- });
433
- }
434
-
435
- private async ensureChild(
436
- slot: ChildSlot,
437
- signal?: AbortSignal,
438
- ): Promise<void> {
439
- if (this.closed) {
440
- throw new ExecutorAdmissionError(
441
- "executor_closed",
442
- "Executor is shutting down.",
443
- );
444
- }
445
- if (slot.child?.connected) {
446
- if (slot.ready) await waitForReady(slot.ready, signal);
447
- return;
448
- }
449
- await delay(Math.max(0, slot.notBefore - Date.now()), signal);
450
- if (this.closed) {
451
- throw new ExecutorAdmissionError(
452
- "executor_closed",
453
- "Executor is shutting down.",
454
- );
455
- }
456
-
457
- const sourceMode = import.meta.url.endsWith(".ts");
458
- const childUrl = new URL(
459
- sourceMode ? "./quickjs-child.ts" : "./quickjs-child.js",
460
- import.meta.url,
461
- );
462
- const childPath = fileURLToPath(childUrl);
463
- if (!existsSync(childPath)) {
464
- throw new Error(
465
- `QuickJS child entry is missing at ${childPath}. ` +
466
- "The @zackbart/connecta/quickjs subpath requires the package file " +
467
- "layout on disk; externalize @zackbart/connecta (or at least " +
468
- "@zackbart/connecta/quickjs) when bundling the server.",
469
- );
470
- }
471
- const child = fork(childPath, [], {
472
- execArgv: sourceMode ? ["--import", "tsx"] : [],
473
- stdio: ["ignore", "ignore", "pipe", "ipc"],
474
- });
475
- let stderrTail: Buffer = Buffer.alloc(0);
476
- child.stderr?.on("data", (chunk: Buffer | string) => {
477
- stderrTail = retainStderrTail(stderrTail, chunk);
478
- });
479
- (
480
- child.stderr as (NodeJS.ReadableStream & { unref?: () => void }) | null
481
- )?.unref?.();
482
- slot.child = child;
483
- slot.ready = new Promise<void>((resolve, reject) => {
484
- slot.resolveReady = resolve;
485
- slot.rejectReady = reject;
486
- });
487
- slot.readyTimer = setTimeout(() => {
488
- if (slot.child !== child || !slot.ready) return;
489
- const error = new Error(
490
- `QuickJS child did not become ready within ${CHILD_STARTUP_TIMEOUT_MS}ms.`,
491
- );
492
- this.failChildStartup(slot, child, error);
493
- }, CHILD_STARTUP_TIMEOUT_MS);
494
- child.on("message", (message: ChildToParentMessage) => {
495
- void this.onMessage(slot, child, message);
496
- });
497
- child.on("error", (error) => {
498
- if (slot.child !== child) return;
499
- if (slot.ready) {
500
- this.failChildStartup(slot, child, error);
501
- return;
502
- }
503
- this.rejectActive(slot, error);
504
- this.recycle(slot);
505
- });
506
- // `close`, unlike `exit`, runs after the stdio streams have closed, so the
507
- // diagnostic includes stderr bytes flushed immediately before a crash.
508
- child.on("close", (code, exitSignal) => {
509
- const expected = slot.expectedExit === child;
510
- if (slot.expectedExit === child) slot.expectedExit = undefined;
511
- const exitDescription = `${
512
- exitSignal ? `signal ${exitSignal}` : `code ${String(code)}`
513
- }`;
514
- this.rejectChildReady(
515
- slot,
516
- child,
517
- childExitError(
518
- `QuickJS child exited before becoming ready (${exitDescription}).`,
519
- stderrTail,
520
- ),
521
- );
522
- if (slot.child === child) slot.child = undefined;
523
- if (expected) return;
524
- this.recordCrash(slot);
525
- const error = childExitError(
526
- `QuickJS child exited unexpectedly (${exitDescription}).`,
527
- stderrTail,
528
- );
529
- this.resolveActive(slot, { result: undefined, error: error.message });
530
- });
531
- child.unref();
532
- child.channel?.unref();
533
- if (slot.ready) await waitForReady(slot.ready, signal);
534
- }
535
-
536
- private async onMessage(
537
- slot: ChildSlot,
538
- child: ChildProcess,
539
- message: ChildToParentMessage,
540
- ): Promise<void> {
541
- // A compromised child is exactly the adversary this process boundary
542
- // contains, and this handler's rejection would crash the serving process.
543
- // Refuse malformed intake before touching any field.
544
- if (!message || typeof message !== "object") return;
545
- if (message.type === "ready") {
546
- this.resolveChildReady(slot, child);
547
- return;
548
- }
549
- const active = slot.active;
550
- if (!active || slot.child !== child) return;
551
- if (typeof message.payloadJson !== "string") return;
552
- if (message.type === "host-call") {
553
- if (message.jobId !== active.id) return;
554
- await this.handleHostCall(child, active, message);
555
- return;
556
- }
557
- if (message.type !== "result" || message.jobId !== active.id) return;
558
- if (serializedBytes(message.payloadJson) > MAX_QUICKJS_IPC_BYTES) {
559
- this.resolveActive(slot, {
560
- result: undefined,
561
- error: "QuickJS execution result exceeded the IPC limit.",
562
- });
563
- this.recycle(slot);
564
- return;
565
- }
566
- try {
567
- const payload = JSON.parse(message.payloadJson) as ExecutionPayload;
568
- slot.consecutiveCrashes = 0;
569
- slot.notBefore = 0;
570
- this.resolveActive(slot, payload.outcome);
571
- if (payload.timedOut) {
572
- // A timed-out host call may still retain QuickJS handles. A fresh child
573
- // is cheaper and safer than reusing a context with unknown stragglers.
574
- // The runtime sets this flag itself: matching error text here would let
575
- // a guest throw "Execution timed out…" and force cold-start churn.
576
- this.recycle(slot);
577
- }
578
- } catch (err) {
579
- this.resolveActive(slot, {
580
- result: undefined,
581
- error: `QuickJS child returned an invalid result: ${msg(err)}`,
582
- });
583
- this.recycle(slot);
584
- }
585
- }
586
-
587
- private async handleHostCall(
588
- child: ChildProcess,
589
- active: ActiveRun,
590
- message: Extract<ChildToParentMessage, { type: "host-call" }>,
591
- ): Promise<void> {
592
- let payloadJson: string;
593
- try {
594
- if (
595
- serializedBytes(message.payloadJson) >
596
- MAX_QUICKJS_HOST_RPC_BYTES
597
- ) {
598
- // Refused before parsing, so there is no address to name here: parsing
599
- // an over-limit payload to improve its error message would spend the
600
- // work the limit exists to refuse.
601
- throw new RangeError("Host call arguments exceeded the IPC limit.");
602
- }
603
- const payload = JSON.parse(message.payloadJson) as HostCallPayload;
604
- const provider = active.providers.get(payload.namespace);
605
- const fn =
606
- provider && Object.hasOwn(provider.fns, payload.functionName)
607
- ? provider.fns[payload.functionName]
608
- : undefined;
609
- if (!fn) {
610
- throw new Error(
611
- `Unknown function ${payload.namespace}.${payload.functionName}`,
612
- );
613
- }
614
- const value = await fn(...payload.args);
615
- try {
616
- payloadJson = stringifyBounded(
617
- { ok: true, value } satisfies HostResultPayload,
618
- `Host result from ${hostCallLabel(payload)}`,
619
- MAX_QUICKJS_HOST_RPC_BYTES,
620
- );
621
- } catch (err) {
622
- // The guest reads this text, so it names the address the program called
623
- // rather than the internal dispatcher every shortcut namespace shares.
624
- const label = hostCallLabel(payload);
625
- const detail =
626
- err instanceof RangeError
627
- ? `Host result from ${label} exceeds the ${MAX_QUICKJS_HOST_RPC_BYTES}-byte serialized bridge limit.`
628
- : `Host result from ${label} could not be serialized: ${msg(err)}`;
629
- payloadJson = errorPayload(detail);
630
- }
631
- } catch (err) {
632
- payloadJson = errorPayload(msg(err));
633
- }
634
- if (!child.connected) return;
635
- try {
636
- const response = {
637
- type: "host-result",
638
- jobId: message.jobId,
639
- callId: message.callId,
640
- payloadJson,
641
- } satisfies ParentToChildMessage;
642
- stringifyBounded(response, "QuickJS host-result IPC envelope");
643
- child.send(response);
644
- } catch {
645
- // The execution has already ended or the child is exiting. Its exit
646
- // handler owns the structured failure for any still-active job.
647
- }
648
- }
649
-
650
- private resolveActive(slot: ChildSlot, outcome: ExecuteResult): void {
651
- const active = slot.active;
652
- if (!active) return;
653
- this.clearActive(slot, active);
654
- active.resolve(outcome);
655
- }
656
-
657
- private rejectActive(slot: ChildSlot, error: Error): void {
658
- const active = slot.active;
659
- if (!active) return;
660
- this.clearActive(slot, active);
661
- active.reject(error);
662
- }
663
-
664
- private clearActive(slot: ChildSlot, active: ActiveRun): void {
665
- clearTimeout(active.wallTimer);
666
- if (active.onAbort) {
667
- active.signal?.removeEventListener("abort", active.onAbort);
668
- }
669
- slot.active = undefined;
670
- slot.child?.unref();
671
- slot.child?.channel?.unref();
672
- }
673
-
674
- private resolveChildReady(slot: ChildSlot, child: ChildProcess): void {
675
- if (slot.child !== child || !slot.ready) return;
676
- if (slot.readyTimer) clearTimeout(slot.readyTimer);
677
- const resolve = slot.resolveReady;
678
- slot.ready = undefined;
679
- slot.resolveReady = undefined;
680
- slot.rejectReady = undefined;
681
- slot.readyTimer = undefined;
682
- resolve?.();
683
- }
684
-
685
- private rejectChildReady(
686
- slot: ChildSlot,
687
- child: ChildProcess,
688
- error: Error,
689
- ): void {
690
- if (slot.child !== child || !slot.ready) return;
691
- if (slot.readyTimer) clearTimeout(slot.readyTimer);
692
- const reject = slot.rejectReady;
693
- slot.ready = undefined;
694
- slot.resolveReady = undefined;
695
- slot.rejectReady = undefined;
696
- slot.readyTimer = undefined;
697
- reject?.(error);
698
- }
699
-
700
- private recordCrash(slot: ChildSlot): void {
701
- slot.consecutiveCrashes++;
702
- slot.notBefore =
703
- Date.now() +
704
- Math.min(5_000, 100 * 2 ** (slot.consecutiveCrashes - 1));
705
- }
706
-
707
- private failChildStartup(
708
- slot: ChildSlot,
709
- child: ChildProcess,
710
- error: Error,
711
- ): void {
712
- if (slot.child !== child || !slot.ready) return;
713
- // Detach before the rejected readiness promise can release the lease. A
714
- // queued successor must never observe a connected child that is unready
715
- // and already on its way out.
716
- slot.expectedExit = child;
717
- this.rejectChildReady(slot, child, error);
718
- slot.child = undefined;
719
- this.recordCrash(slot);
720
- child.kill();
721
- }
722
-
723
- private recycle(slot: ChildSlot): void {
724
- const child = slot.child;
725
- if (!child) return;
726
- this.rejectChildReady(
727
- slot,
728
- child,
729
- new Error("QuickJS child was recycled before becoming ready."),
730
- );
731
- slot.expectedExit = child;
732
- slot.child = undefined;
733
- child.kill();
734
- }
735
-
736
- private stopSlot(slot: ChildSlot): Promise<void> {
737
- const child = slot.child;
738
- if (!child) return Promise.resolve();
739
- this.rejectActive(
740
- slot,
741
- new ExecutorExecutionError(
742
- "executor_closed",
743
- "Executor is shutting down.",
744
- ),
745
- );
746
- this.rejectChildReady(
747
- slot,
748
- child,
749
- new ExecutorAdmissionError(
750
- "executor_closed",
751
- "Executor is shutting down.",
752
- ),
753
- );
754
- slot.expectedExit = child;
755
- slot.child = undefined;
756
- return new Promise((resolve) => {
757
- const timer = setTimeout(resolve, 1_000);
758
- child.once("exit", () => {
759
- clearTimeout(timer);
760
- resolve();
761
- });
762
- child.kill();
763
- });
764
- }
765
- }
766
-
767
- /**
768
- * Bounded Node QuickJS executor. Admission is acquired before connecta builds
769
- * providers; one lease maps to one child and carries execution, avoiding a
770
- * double-acquire deadlock at concurrency 1.
771
- */
772
- export function quickJsExecutor(
773
- options: QuickJsExecutorOptions = {},
774
- ): AdmittingExecutor {
775
- return new QuickJsChildPool(options);
776
- }