@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,470 +0,0 @@
1
- // QuickJS-in-WebAssembly runtime used inside the disposable child process.
2
- //
3
- // Model-written code runs inside a QuickJS interpreter compiled to WASM:
4
- // memory-safe, no ambient authority — the guest has no fetch, filesystem,
5
- // env, timers, or imports. The only bridge to the host is `__call`, which
6
- // returns a QuickJS promise that the host resolves with a JSON payload after
7
- // running the provider function; `executePendingJobs` drives guest
8
- // continuations from a host-side loop. Values cross the boundary as JSON, so
9
- // provider args/results must be JSON-serializable.
10
-
11
- import {
12
- getQuickJS,
13
- type QuickJSContext,
14
- type QuickJSDeferredPromise,
15
- } from "quickjs-emscripten";
16
- import type { ExecuteResult, ExecutorProvider } from "../types.js";
17
- import {
18
- hostCallLabel,
19
- MAX_QUICKJS_LOG_TRANSPORT_BYTES,
20
- serializedBytes,
21
- } from "./quickjs-protocol.js";
22
-
23
- export interface QuickJsRuntimeOptions {
24
- timeoutMs: number;
25
- cpuTimeMs: number;
26
- memoryLimitBytes: number;
27
- maxStackSizeBytes: number;
28
- }
29
-
30
- /** Load and compile the shared QuickJS WASM module before a run budget starts. */
31
- export async function prepareQuickJs(): Promise<void> {
32
- await getQuickJS();
33
- }
34
-
35
- const MAX_LOG_ENTRIES = 200;
36
- // Cap each entry AND the cumulative buffer at capture time so untrusted guest
37
- // code can't retain unbounded host memory: a single `console.log("x".repeat(N))`
38
- // otherwise copies the whole N-char guest string into a host array we hold for
39
- // the entire execution. The separate transport-byte budget below accounts for
40
- // JSON escaping and is what keeps the result envelope below its IPC ceiling.
41
- const MAX_LOG_ENTRY_CHARS = 8_000;
42
- const MAX_LOG_TOTAL_CHARS = 256_000;
43
- const LOG_ENTRY_LIMIT_MARKER = `[log truncated after ${MAX_LOG_ENTRIES} entries]`;
44
- const LOG_SIZE_LIMIT_MARKER = "[log truncated: size budget exceeded]";
45
- const MAX_LOG_MARKER_TRANSPORT_BYTES = Math.max(
46
- logTransportBytes(LOG_ENTRY_LIMIT_MARKER),
47
- logTransportBytes(LOG_SIZE_LIMIT_MARKER),
48
- );
49
- // Keep one host result below the range where quickjs-emscripten@0.32.0 can
50
- // nondeterministically fail during runtime disposal under concurrent load.
51
- // This still lets guest code reduce data more than ten times larger than
52
- // connecta's final response budget.
53
- const MAX_HOST_RESULT_BYTES = 256 * 1024;
54
-
55
- function msg(err: unknown): string {
56
- return err instanceof Error ? err.message : String(err);
57
- }
58
-
59
- function logTransportBytes(entry: string): number {
60
- // The log is encoded into ExecutionPayload, then that payloadJson string is
61
- // encoded into ChildToParentMessage. Measure the units the IPC cap sees.
62
- return serializedBytes(JSON.stringify(JSON.stringify(entry)));
63
- }
64
-
65
- function exceedsUtf8ByteLimit(value: string, limit: number): boolean {
66
- let bytes = 0;
67
- for (let index = 0; index < value.length; index += 1) {
68
- const code = value.charCodeAt(index);
69
- if (code <= 0x7f) bytes += 1;
70
- else if (code <= 0x7ff) bytes += 2;
71
- else if (
72
- code >= 0xd800 &&
73
- code <= 0xdbff &&
74
- index + 1 < value.length &&
75
- value.charCodeAt(index + 1) >= 0xdc00 &&
76
- value.charCodeAt(index + 1) <= 0xdfff
77
- ) {
78
- bytes += 4;
79
- index += 1;
80
- } else {
81
- bytes += 3;
82
- }
83
- if (bytes > limit) return true;
84
- }
85
- return false;
86
- }
87
-
88
- /** Normalize model output into an async-arrow expression: strip markdown fences, wrap bare bodies. */
89
- export function normalizeCode(code: string): string {
90
- let c = code.trim();
91
- const fence = /^```[\w-]*\s*\n([\s\S]*?)\n?```$/.exec(c);
92
- const fencedCode = fence?.[1];
93
- if (fencedCode !== undefined) c = fencedCode.trim();
94
- // Sniff for an existing function expression past any leading comments/blank
95
- // lines — models routinely prefix their code with a `//` or `/* */` note,
96
- // which would otherwise defeat the detection and wrap the whole thing.
97
- const head = c.replace(/^(?:\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/)+/, "");
98
- if (/^async\b/.test(head) || head.startsWith("(")) return c;
99
- return `async () => {\n${c}\n}`;
100
- }
101
-
102
- /**
103
- * Constant-size guest prelude: one lazy namespace Proxy per provider, never one
104
- * generated closure per visible tool. Unknown properties cross the host bridge
105
- * and fail closed there.
106
- */
107
- function setupScript(providers: ExecutorProvider[]): string {
108
- const lines: string[] = [
109
- `globalThis.console = (() => {
110
- const fmt = (x) => { if (typeof x === "string") return x; try { return JSON.stringify(x); } catch { return String(x); } };
111
- const emit = (...a) => __log(a.map(fmt).join(" "));
112
- return { log: emit, info: emit, warn: emit, error: emit, debug: emit };
113
- })();`,
114
- `globalThis.__invoke = async (ns, fn, args) => {
115
- const r = JSON.parse(await __call(ns, fn, JSON.stringify(args)));
116
- if (!r.ok) throw new Error(r.error);
117
- return r.value;
118
- };`,
119
- `globalThis.__namespace = (ns) => Object.freeze(new Proxy(Object.create(null), {
120
- get: (_target, key) => typeof key === "string" ? (...args) => __invoke(ns, key, args) : undefined
121
- }));`,
122
- ];
123
- for (const p of providers) {
124
- const ns = JSON.stringify(p.name);
125
- lines.push(`globalThis[${ns}] = __namespace(${ns});`);
126
- }
127
- for (const p of providers) {
128
- if (p.prelude) lines.push(p.prelude);
129
- }
130
- lines.push(`delete globalThis.__namespace;`);
131
- return lines.join("\n");
132
- }
133
-
134
- /** True when a dumped error is QuickJS's deadline-interrupt signal. */
135
- function isInterrupt(dumped: unknown): boolean {
136
- if (!dumped || typeof dumped !== "object") return false;
137
- const e = dumped as { name?: unknown; message?: unknown };
138
- return e.name === "InternalError" && e.message === "interrupted";
139
- }
140
-
141
- function formatGuestError(dumped: unknown): string {
142
- if (dumped && typeof dumped === "object") {
143
- const e = dumped as { name?: unknown; message?: unknown };
144
- if (e.message != null) {
145
- return e.name != null && e.name !== "Error"
146
- ? `${String(e.name)}: ${String(e.message)}`
147
- : String(e.message);
148
- }
149
- }
150
- return typeof dumped === "string" ? dumped : JSON.stringify(dumped);
151
- }
152
-
153
- interface HostBridge {
154
- pending: number;
155
- aborted: boolean;
156
- wake: () => void;
157
- waitForSettle: Promise<void>;
158
- }
159
-
160
- function armWake(bridge: HostBridge): void {
161
- bridge.waitForSettle = new Promise((resolve) => {
162
- bridge.wake = resolve;
163
- });
164
- }
165
-
166
- function waitForHostOrDeadline(
167
- waitForSettle: Promise<void>,
168
- remainingMs: number,
169
- ): Promise<boolean> {
170
- return new Promise((resolve) => {
171
- let done = false;
172
- const timer = setTimeout(() => {
173
- done = true;
174
- resolve(false);
175
- }, remainingMs);
176
- void waitForSettle.then(() => {
177
- if (done) return;
178
- done = true;
179
- clearTimeout(timer);
180
- resolve(true);
181
- });
182
- });
183
- }
184
-
185
- function installBridge(
186
- ctx: QuickJSContext,
187
- providers: ExecutorProvider[],
188
- logs: string[],
189
- ): HostBridge {
190
- const bridge: HostBridge = {
191
- pending: 0,
192
- aborted: false,
193
- wake: () => {},
194
- waitForSettle: Promise.resolve(),
195
- };
196
- armWake(bridge);
197
-
198
- // Keep both the in-memory character budget and the twice-JSON-encoded
199
- // transport budget. Reserve enough byte budget for whichever truncation
200
- // marker ends the stream.
201
- let logTotalChars = 0;
202
- let logTotalTransportBytes = 0;
203
- let logBudgetSpent = false;
204
- const logFn = ctx.newFunction("__log", (h) => {
205
- if (logs.length >= MAX_LOG_ENTRIES) {
206
- if (logs.length === MAX_LOG_ENTRIES) {
207
- logs.push(LOG_ENTRY_LIMIT_MARKER);
208
- }
209
- return;
210
- }
211
- if (logBudgetSpent) return;
212
- // Cap the entry before retaining it: `getString` yields a transient copy,
213
- // but slicing here keeps only a bounded string alive in `logs`.
214
- let entry = ctx.getString(h);
215
- if (entry.length > MAX_LOG_ENTRY_CHARS) {
216
- entry = `${entry.slice(0, MAX_LOG_ENTRY_CHARS)}…[entry truncated]`;
217
- }
218
- const entryTransportBytes = logTransportBytes(entry);
219
- if (
220
- logTotalChars + entry.length > MAX_LOG_TOTAL_CHARS ||
221
- logTotalTransportBytes + entryTransportBytes >
222
- MAX_QUICKJS_LOG_TRANSPORT_BYTES - MAX_LOG_MARKER_TRANSPORT_BYTES
223
- ) {
224
- logs.push(LOG_SIZE_LIMIT_MARKER);
225
- logBudgetSpent = true;
226
- return;
227
- }
228
- logs.push(entry);
229
- logTotalChars += entry.length;
230
- logTotalTransportBytes += entryTransportBytes;
231
- });
232
- ctx.setProp(ctx.global, "__log", logFn);
233
- logFn.dispose();
234
-
235
- const byName = new Map(providers.map((p) => [p.name, p]));
236
- const invoke = async (
237
- ns: string,
238
- fn: string,
239
- argsJson: string,
240
- ): Promise<string> => {
241
- try {
242
- const provider = byName.get(ns);
243
- const f =
244
- provider && Object.hasOwn(provider.fns, fn)
245
- ? provider.fns[fn]
246
- : undefined;
247
- if (!f) throw new Error(`Unknown function ${ns}.${fn}`);
248
- const args = JSON.parse(argsJson) as unknown[];
249
- // Name the address the program called, never the internal dispatcher the
250
- // lazy connector namespaces share.
251
- const label = hostCallLabel({ namespace: ns, functionName: fn, args });
252
- const value = await f(...args);
253
- let json: string;
254
- try {
255
- json = JSON.stringify({ ok: true, value });
256
- } catch (err) {
257
- throw new Error(
258
- `Host result from ${label} could not be serialized: ${msg(err)}`,
259
- );
260
- }
261
- if (exceedsUtf8ByteLimit(json, MAX_HOST_RESULT_BYTES)) {
262
- throw new Error(
263
- `Host result from ${label} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`,
264
- );
265
- }
266
- return json;
267
- } catch (err) {
268
- return JSON.stringify({ ok: false, error: msg(err) });
269
- }
270
- };
271
-
272
- const callFn = ctx.newFunction("__call", (nsH, fnH, argsH) => {
273
- const ns = ctx.getString(nsH);
274
- const fn = ctx.getString(fnH);
275
- const argsJson = ctx.getString(argsH);
276
- const deferred: QuickJSDeferredPromise = ctx.newPromise();
277
- bridge.pending++;
278
- void invoke(ns, fn, argsJson).then((json) => {
279
- bridge.pending--;
280
- if (bridge.aborted) {
281
- // Context is (about to be) torn down — settle nothing, free our handles.
282
- deferred.dispose();
283
- } else {
284
- const h = ctx.newString(json);
285
- deferred.resolve(h);
286
- h.dispose();
287
- }
288
- bridge.wake();
289
- });
290
- return deferred.handle;
291
- });
292
- ctx.setProp(ctx.global, "__call", callFn);
293
- callFn.dispose();
294
-
295
- return bridge;
296
- }
297
-
298
- /**
299
- * ExecuteResult plus a structural wall-expiry marker. Guest errors flow through
300
- * formatGuestError verbatim, so the timeout message text is forgeable; this
301
- * flag is set only by the return sites below and never from guest values.
302
- */
303
- export interface QuickJsExecutionResult extends ExecuteResult {
304
- timedOut?: boolean;
305
- }
306
-
307
- /**
308
- * Execute one program inside the child. Wall time includes host waits; guest
309
- * CPU accumulates only while evalCode/executePendingJobs is synchronously
310
- * driving QuickJS, so a slow downstream does not consume the short CPU budget.
311
- */
312
- export async function executeQuickJs(
313
- code: string,
314
- providers: ExecutorProvider[],
315
- options: QuickJsRuntimeOptions,
316
- ): Promise<QuickJsExecutionResult> {
317
- const { timeoutMs, cpuTimeMs, memoryLimitBytes, maxStackSizeBytes } =
318
- options;
319
- const QuickJS = await getQuickJS();
320
- const ctx = QuickJS.newContext();
321
- const deadline = Date.now() + timeoutMs;
322
- const timeoutError = `Execution timed out after ${timeoutMs}ms.`;
323
- const cpuTimeoutError = `Execution exceeded the ${cpuTimeMs}ms guest CPU budget.`;
324
- let cpuUsedMs = 0;
325
- let segmentStarted = 0;
326
- let inGuest = false;
327
- let cpuInterrupted = false;
328
- let wallInterrupted = false;
329
- const runGuest = <T>(operation: () => T): T => {
330
- segmentStarted = Date.now();
331
- inGuest = true;
332
- try {
333
- return operation();
334
- } finally {
335
- cpuUsedMs += Date.now() - segmentStarted;
336
- inGuest = false;
337
- }
338
- };
339
- ctx.runtime.setMemoryLimit(memoryLimitBytes);
340
- ctx.runtime.setMaxStackSize(maxStackSizeBytes);
341
- ctx.runtime.setInterruptHandler(() => {
342
- if (!inGuest) return false;
343
- wallInterrupted = Date.now() >= deadline;
344
- cpuInterrupted =
345
- cpuUsedMs + (Date.now() - segmentStarted) >= cpuTimeMs;
346
- return wallInterrupted || cpuInterrupted;
347
- });
348
-
349
- const logs: string[] = [];
350
- const bridge = installBridge(ctx, providers, logs);
351
- const finish = <T extends ExecuteResult>(r: T): T => {
352
- bridge.aborted = true;
353
- // Outstanding host calls still hold deferred-promise handles; their
354
- // callbacks free them and wake the drain, which disposes the context
355
- // once the last straggler settles. Re-arm before every wait so the
356
- // deferred never resolves-and-stays-resolved between settles — that
357
- // would spin the microtask queue and starve the very timers the
358
- // stragglers are waiting on. Arming before the pending check (and
359
- // synchronously, before the first await) closes the settle-before-arm
360
- // window: any settle after this point resolves the promise we await.
361
- if (bridge.pending === 0) ctx.dispose();
362
- else {
363
- void (async () => {
364
- armWake(bridge);
365
- while (bridge.pending > 0) {
366
- await bridge.waitForSettle;
367
- armWake(bridge);
368
- }
369
- ctx.dispose();
370
- })();
371
- }
372
- return logs.length > 0 ? { ...r, logs } : r;
373
- };
374
- const fail = (error: string): ExecuteResult =>
375
- finish({ result: undefined, error });
376
- const timedOut = (): QuickJsExecutionResult => ({
377
- result: undefined,
378
- error: timeoutError,
379
- timedOut: true,
380
- });
381
-
382
- const setup = runGuest(() => ctx.evalCode(setupScript(providers)));
383
- if (setup.error) {
384
- const detail = formatGuestError(ctx.dump(setup.error));
385
- setup.error.dispose();
386
- if (wallInterrupted) return finish(timedOut());
387
- if (cpuInterrupted) return fail(cpuTimeoutError);
388
- return fail(`Sandbox setup failed: ${detail}`);
389
- }
390
- setup.value.dispose();
391
-
392
- const evaluated = runGuest(() =>
393
- ctx.evalCode(`Promise.resolve((\n${normalizeCode(code)}\n)())`),
394
- );
395
- if (evaluated.error) {
396
- const dumped = ctx.dump(evaluated.error);
397
- evaluated.error.dispose();
398
- if (isInterrupt(dumped) && wallInterrupted) return finish(timedOut());
399
- if (isInterrupt(dumped) && cpuInterrupted) return fail(cpuTimeoutError);
400
- return fail(formatGuestError(dumped));
401
- }
402
- const promiseHandle = evaluated.value;
403
-
404
- // Drive the guest: run microtask jobs, inspect the result promise,
405
- // sleep until a host call settles (or the budget runs out), repeat.
406
- // Returns without touching the context lifecycle; disposal happens below.
407
- const drive = async (): Promise<QuickJsExecutionResult> => {
408
- for (;;) {
409
- if (Date.now() >= deadline) {
410
- return timedOut();
411
- }
412
- const jobs = runGuest(() => ctx.runtime.executePendingJobs());
413
- if (jobs.error) {
414
- const dumped = ctx.dump(jobs.error);
415
- jobs.error.dispose();
416
- if (isInterrupt(dumped) && wallInterrupted) {
417
- return timedOut();
418
- }
419
- if (isInterrupt(dumped) && cpuInterrupted) {
420
- return { result: undefined, error: cpuTimeoutError };
421
- }
422
- return { result: undefined, error: formatGuestError(dumped) };
423
- }
424
- const state = ctx.getPromiseState(promiseHandle);
425
- if (state.type === "fulfilled") {
426
- const result: unknown = runGuest(() => ctx.dump(state.value));
427
- state.value.dispose();
428
- return { result };
429
- }
430
- if (state.type === "rejected") {
431
- const dumped = ctx.dump(state.error);
432
- state.error.dispose();
433
- if (isInterrupt(dumped) && wallInterrupted) {
434
- return timedOut();
435
- }
436
- if (isInterrupt(dumped) && cpuInterrupted) {
437
- return { result: undefined, error: cpuTimeoutError };
438
- }
439
- return { result: undefined, error: formatGuestError(dumped) };
440
- }
441
- if (bridge.pending === 0) {
442
- return {
443
- result: undefined,
444
- error:
445
- "Execution stalled: code awaits something that can never settle.",
446
- };
447
- }
448
- const remaining = deadline - Date.now();
449
- if (remaining <= 0) {
450
- return timedOut();
451
- }
452
- const settled = await waitForHostOrDeadline(
453
- bridge.waitForSettle,
454
- remaining,
455
- );
456
- if (settled) armWake(bridge);
457
- else {
458
- return timedOut();
459
- }
460
- }
461
- };
462
-
463
- let outcome: QuickJsExecutionResult;
464
- try {
465
- outcome = await drive();
466
- } finally {
467
- promiseHandle.dispose();
468
- }
469
- return finish(outcome);
470
- }