@zackbart/connecta 0.28.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
@@ -65,6 +65,7 @@ export declare class AdmissionController {
65
65
  snapshot(): AdmissionSnapshot;
66
66
  acquire(options?: {
67
67
  signal?: AbortSignal;
68
+ wait?: boolean;
68
69
  }): Promise<AdmissionLease>;
69
70
  close(): void;
70
71
  private overloaded;
@@ -14,9 +14,7 @@ export class ExecutorAdmissionError extends Error {
14
14
  this.code = code;
15
15
  this.name = "ExecutorAdmissionError";
16
16
  this.retryable = code === "executor_overloaded";
17
- if (opts.retryAfterMs !== undefined &&
18
- Number.isFinite(opts.retryAfterMs) &&
19
- opts.retryAfterMs >= 0) {
17
+ if (opts.retryAfterMs !== undefined && Number.isFinite(opts.retryAfterMs) && opts.retryAfterMs >= 0) {
20
18
  this.retryAfterMs = Math.trunc(opts.retryAfterMs);
21
19
  }
22
20
  }
@@ -72,9 +70,8 @@ export class AdmissionController {
72
70
  this.maxQueueSize = nonNegativeWhole(options.maxQueueSize, "maxQueueSize");
73
71
  this.queueTimeoutMs = positiveWhole(options.queueTimeoutMs, "queueTimeoutMs");
74
72
  this.retryAfterMs = nonNegativeWhole(options.retryAfterMs ?? this.queueTimeoutMs, "retryAfterMs");
75
- this.maxDurationMs = options.maxDurationMs === undefined
76
- ? undefined
77
- : positiveWhole(options.maxDurationMs, "maxDurationMs");
73
+ this.maxDurationMs =
74
+ options.maxDurationMs === undefined ? undefined : positiveWhole(options.maxDurationMs, "maxDurationMs");
78
75
  if (this.maxDurationMs !== undefined && this.maxDurationMs > 2_147_483_647) {
79
76
  throw new TypeError("maxDurationMs must be at most 2,147,483,647 milliseconds.");
80
77
  }
@@ -129,9 +126,7 @@ export class AdmissionController {
129
126
  }
130
127
  makeLease(waitMs) {
131
128
  const id = Symbol("admission lease");
132
- const record = this.maxDurationMs !== undefined
133
- ? { expiresAt: Date.now() + this.maxDurationMs }
134
- : {};
129
+ const record = this.maxDurationMs !== undefined ? { expiresAt: Date.now() + this.maxDurationMs } : {};
135
130
  this.leases.set(id, record);
136
131
  return {
137
132
  waitMs,
@@ -202,9 +197,7 @@ export class AdmissionController {
202
197
  }
203
198
  // A queued request's own timer reaps the earliest admitted lease. A
204
199
  // fallback check covers a briefly empty lease map after a handoff.
205
- return Number.isFinite(next)
206
- ? Math.max(1, next - Date.now())
207
- : 250;
200
+ return Number.isFinite(next) ? Math.max(1, next - Date.now()) : 250;
208
201
  }
209
202
  remove(waiter) {
210
203
  const index = this.waiters.indexOf(waiter);
@@ -233,7 +226,7 @@ export class AdmissionController {
233
226
  // static block is the one place outside an instance method that may read
234
227
  // this bookkeeping while emitting nothing there.
235
228
  static {
236
- provideAdmissionProgram((controller, signal) => Effect.suspend(() => {
229
+ provideAdmissionProgram((controller, signal, wait) => Effect.suspend(() => {
237
230
  controller.reapExpired();
238
231
  if (controller.closed) {
239
232
  controller.closedTotal++;
@@ -248,6 +241,10 @@ export class AdmissionController {
248
241
  controller.admittedTotal++;
249
242
  return Effect.succeed(controller.makeLease(0));
250
243
  }
244
+ if (!wait) {
245
+ controller.rejectedTotal++;
246
+ return Effect.fail(controller.overloaded("No executor slot is free for nested execution. Invoke the operation directly, after this program ends."));
247
+ }
251
248
  if (controller.waiters.length >= controller.maxQueueSize) {
252
249
  controller.rejectedTotal++;
253
250
  return Effect.fail(controller.overloaded("Executor queue is full."));
@@ -284,8 +281,7 @@ export class AdmissionController {
284
281
  }
285
282
  /** Preserve the one-method Workers seam while recognizing richer Node executors. */
286
283
  export function isAdmittingExecutor(executor) {
287
- return ("acquire" in executor &&
288
- typeof executor.acquire === "function");
284
+ return "acquire" in executor && typeof executor.acquire === "function";
289
285
  }
290
286
  /** Terminal- and JSON-safe upper bound for an executor's self-reported name. */
291
287
  const MAX_EXECUTOR_NAME_LENGTH = 40;
@@ -0,0 +1,20 @@
1
+ import type { AdmittingExecutor, Executor } from "./types.js";
2
+ type ExecutorLifecycle = "leased" | "self-managed";
3
+ export declare function brandExecutor<T extends Executor>(executor: T, lifecycle: ExecutorLifecycle): T;
4
+ export declare function assertExecutor(executor: Executor): void;
5
+ /** Explicitly accept responsibility for a custom sandbox's run resources. */
6
+ export interface CustomExecutorOptions {
7
+ /**
8
+ * The custom executor owns guest termination and resource cleanup, including
9
+ * budget exhaustion, cancellation, and deadlines. Connecta cannot add these
10
+ * guarantees to an executor that exposes only execute().
11
+ */
12
+ lifecycle: "self-managed";
13
+ }
14
+ /**
15
+ * Opt a custom sandbox into createConnecta without changing the original object.
16
+ * Delegated methods retain their original receiver, including private fields.
17
+ */
18
+ export declare function customExecutor(executor: AdmittingExecutor, options: CustomExecutorOptions): AdmittingExecutor;
19
+ export declare function customExecutor(executor: Executor, options: CustomExecutorOptions): Executor;
20
+ export {};
@@ -0,0 +1,48 @@
1
+ import { executorName, isAdmittingExecutor } from "./executor-admission.js";
2
+ // Symbol.for and a data descriptor survive duplicate packages and minification.
3
+ // This is a construction contract, not a security boundary against host config.
4
+ const EXECUTOR_BRAND = Symbol.for("connecta.executor");
5
+ export function brandExecutor(executor, lifecycle) {
6
+ Object.defineProperty(executor, EXECUTOR_BRAND, {
7
+ value: Object.freeze({ version: 1, lifecycle }),
8
+ enumerable: false,
9
+ writable: false,
10
+ configurable: false,
11
+ });
12
+ return executor;
13
+ }
14
+ export function assertExecutor(executor) {
15
+ const descriptor = Object.getOwnPropertyDescriptor(executor, EXECUTOR_BRAND);
16
+ const brand = descriptor?.value;
17
+ if (typeof executor.execute === "function" &&
18
+ descriptor?.enumerable === false &&
19
+ brand?.version === 1 &&
20
+ (brand.lifecycle === "leased" || brand.lifecycle === "self-managed"))
21
+ return;
22
+ throw new Error("ConnectaConfig.executor must declare its lifecycle. Unbranded executors, including " +
23
+ "upstream DynamicWorkerExecutor, are unsupported because request-owned resources " +
24
+ "must be released when a run ends. Use one of these options: " +
25
+ 'import { workerExecutor } from "@zackbart/connecta/worker"; ' +
26
+ "executor: workerExecutor({ loader: env.LOADER }); " +
27
+ 'import { quickJsExecutor } from "@zackbart/connecta/quickjs"; executor: quickJsExecutor(); ' +
28
+ 'or import { customExecutor } from "@zackbart/connecta"; ' +
29
+ 'executor: customExecutor(myExecutor, { lifecycle: "self-managed" }).');
30
+ }
31
+ export function customExecutor(executor, options) {
32
+ if (options?.lifecycle !== "self-managed" || typeof executor?.execute !== "function") {
33
+ throw new Error('customExecutor requires an executor and { lifecycle: "self-managed" }.');
34
+ }
35
+ const name = executorName(executor);
36
+ const wrapper = {
37
+ ...(name ? { name } : {}),
38
+ execute: executor.execute.bind(executor),
39
+ ...(executor.close ? { close: executor.close.bind(executor) } : {}),
40
+ ...(isAdmittingExecutor(executor)
41
+ ? {
42
+ acquire: executor.acquire.bind(executor),
43
+ ...(executor.admissionSnapshot ? { admissionSnapshot: executor.admissionSnapshot.bind(executor) } : {}),
44
+ }
45
+ : {}),
46
+ };
47
+ return brandExecutor(wrapper, "self-managed");
48
+ }
@@ -1,6 +1,6 @@
1
1
  import { msg } from "./errors.js";
2
2
  /** ~6k tokens. Sandbox code should filter data down before returning. */
3
- const MAX_EXECUTE_RESULT_CHARS = 24_000;
3
+ export const MAX_EXECUTE_RESULT_CHARS = 24_000;
4
4
  export const MAX_EXECUTE_LOG_CHARS = 4_000;
5
5
  export function serializeResultText(value) {
6
6
  const serialized = JSON.stringify(value);
@@ -18,30 +18,45 @@ const TRUNCATION_HINT = "filter/map/slice data inside execute_code and return on
18
18
  * of what the program returned, and truncation happens exactly once no matter
19
19
  * how many hops the value takes.
20
20
  */
21
- function truncationEnvelope(text) {
21
+ function truncationEnvelope(text, maxChars, totalChars = text.length, hint = TRUNCATION_HINT) {
22
22
  const base = {
23
23
  truncated: true,
24
24
  preview: "",
25
- totalChars: text.length,
26
- hint: TRUNCATION_HINT,
25
+ totalChars,
26
+ hint,
27
27
  };
28
- let budget = Math.max(0, MAX_EXECUTE_RESULT_CHARS - JSON.stringify(base).length);
28
+ let budget = Math.max(0, maxChars - JSON.stringify(base).length);
29
29
  for (let attempt = 0; attempt < 8 && budget > 0; attempt += 1) {
30
30
  const candidate = { ...base, preview: text.slice(0, budget) };
31
31
  const size = JSON.stringify(candidate).length;
32
- if (size <= MAX_EXECUTE_RESULT_CHARS)
32
+ if (size <= maxChars)
33
33
  return candidate;
34
34
  // Every character costs at least one serialized character, so scaling by
35
35
  // the overshoot ratio (minus a step) strictly shrinks the budget.
36
- budget = Math.max(0, Math.floor(budget * (MAX_EXECUTE_RESULT_CHARS / size)) - 8);
36
+ budget = Math.max(0, Math.floor(budget * (maxChars / size)) - 8);
37
37
  }
38
38
  return { ...base, preview: text.slice(0, budget) };
39
39
  }
40
- export function guardExecuteResultValue(value) {
40
+ export function guardExecuteResultValue(value, maxChars = MAX_EXECUTE_RESULT_CHARS, hintPrefix) {
41
+ const hint = hintPrefix ? `${hintPrefix} ${TRUNCATION_HINT}` : TRUNCATION_HINT;
41
42
  const text = serializeResultText(value);
42
- if (text.length <= MAX_EXECUTE_RESULT_CHARS)
43
+ if (text.length <= maxChars)
43
44
  return value;
44
- return truncationEnvelope(text);
45
+ // A custom executor may already have guarded this value at a larger cap.
46
+ // Shrink its preview once more without nesting notices or losing original size.
47
+ if (value !== null && typeof value === "object") {
48
+ const prior = value;
49
+ if (prior.truncated === true &&
50
+ prior.hint === TRUNCATION_HINT &&
51
+ typeof prior.preview === "string" &&
52
+ typeof prior.totalChars === "number" &&
53
+ Number.isSafeInteger(prior.totalChars) &&
54
+ prior.totalChars >= prior.preview.length &&
55
+ Object.keys(value).length === 4) {
56
+ return truncationEnvelope(prior.preview, maxChars, prior.totalChars, hint);
57
+ }
58
+ }
59
+ return truncationEnvelope(text, maxChars, text.length, hint);
45
60
  }
46
61
  export function truncateExecuteText(text, max) {
47
62
  if (text.length <= max)
@@ -49,8 +64,8 @@ export function truncateExecuteText(text, max) {
49
64
  return `${text.slice(0, max)}\n--- TRUNCATED (${text.length} chars total) — filter/map/slice data inside your code and return only what you need ---`;
50
65
  }
51
66
  /**
52
- * Apply the public execute_code result/log policy before a child result enters
53
- * IPC. The parent repeats the guard for third-party Executor implementations.
67
+ * Validate serialization before IPC without discarding the program return.
68
+ * The host applies the presentation cap after it can stash the full value.
54
69
  */
55
70
  export function prepareExecuteResultForTransport(outcome) {
56
71
  // QuickJS already bounds captured logs at source (entries + cumulative
@@ -63,12 +78,14 @@ export function prepareExecuteResultForTransport(outcome) {
63
78
  return {
64
79
  result: undefined,
65
80
  error: outcome.error,
81
+ ...(outcome.failure ? { failure: outcome.failure } : {}),
66
82
  ...(logs ? { logs } : {}),
67
83
  };
68
84
  }
69
85
  try {
86
+ serializeResultText(outcome.result);
70
87
  return {
71
- result: guardExecuteResultValue(outcome.result),
88
+ result: outcome.result,
72
89
  ...(logs ? { logs } : {}),
73
90
  };
74
91
  }
@@ -1,6 +1,7 @@
1
1
  import { prepareExecuteResultForTransport } from "../executor-result.js";
2
+ import { InvocationFailure } from "../invocation.js";
2
3
  import { msg } from "../errors.js";
3
- import { MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
4
+ import { MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_RESULT_BYTES, QUICKJS_RESULT_CHUNK_CHARS, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
4
5
  import { executeQuickJs, prepareQuickJs } from "./quickjs-runtime.js";
5
6
  let activeJobId;
6
7
  let nextCallId = 1;
@@ -38,9 +39,7 @@ function provider(name, jobId, prelude) {
38
39
  return result;
39
40
  };
40
41
  },
41
- getOwnPropertyDescriptor: (_target, key) => typeof key === "string"
42
- ? { configurable: true, enumerable: false }
43
- : undefined,
42
+ getOwnPropertyDescriptor: (_target, key) => typeof key === "string" ? { configurable: true, enumerable: false } : undefined,
44
43
  });
45
44
  return { name, fns: functions, ...(prelude ? { prelude } : {}) };
46
45
  }
@@ -52,6 +51,13 @@ function fixedTransportFailure(message) {
52
51
  },
53
52
  };
54
53
  }
54
+ function sendResult(jobId, payloadJson) {
55
+ for (let offset = 0; offset + QUICKJS_RESULT_CHUNK_CHARS < payloadJson.length; offset += QUICKJS_RESULT_CHUNK_CHARS) {
56
+ send({ type: "result-chunk", jobId, payloadJson: payloadJson.slice(offset, offset + QUICKJS_RESULT_CHUNK_CHARS) });
57
+ }
58
+ const tail = Math.floor((payloadJson.length - 1) / QUICKJS_RESULT_CHUNK_CHARS) * QUICKJS_RESULT_CHUNK_CHARS;
59
+ send({ type: "result", jobId, payloadJson: payloadJson.slice(tail) });
60
+ }
55
61
  async function run(payload) {
56
62
  if (activeJobId !== undefined) {
57
63
  throw new Error("QuickJS child received overlapping executions.");
@@ -70,12 +76,12 @@ async function run(payload) {
70
76
  };
71
77
  let payloadJson;
72
78
  try {
73
- payloadJson = stringifyBounded(prepared, "QuickJS execution result");
79
+ payloadJson = stringifyBounded(prepared, "QuickJS execution result", MAX_QUICKJS_RESULT_BYTES);
74
80
  }
75
81
  catch (err) {
76
- payloadJson = JSON.stringify(fixedTransportFailure(`QuickJS execution result exceeded the ${MAX_QUICKJS_IPC_BYTES}-byte IPC limit: ${msg(err)}`));
82
+ payloadJson = JSON.stringify(fixedTransportFailure(`QuickJS execution result exceeded the ${MAX_QUICKJS_RESULT_BYTES}-byte aggregate transport limit: ${msg(err)}`));
77
83
  }
78
- send({ type: "result", jobId: payload.id, payloadJson });
84
+ sendResult(payload.id, payloadJson);
79
85
  }
80
86
  finally {
81
87
  activeJobId = undefined;
@@ -104,7 +110,7 @@ process.on("message", (message) => {
104
110
  if (result.ok)
105
111
  request.resolve(result.value);
106
112
  else
107
- request.reject(new Error(result.error));
113
+ request.reject(result.call ? new InvocationFailure(result.call) : new Error(result.error));
108
114
  }
109
115
  catch (err) {
110
116
  request.reject(new Error(`Invalid host result: ${msg(err)}`));
@@ -119,7 +125,7 @@ process.on("message", (message) => {
119
125
  const payload = JSON.parse(message.payloadJson);
120
126
  void run(payload).catch((err) => {
121
127
  const payloadJson = JSON.stringify(fixedTransportFailure(`QuickJS child failed: ${msg(err)}`));
122
- send({ type: "result", jobId: payload.id, payloadJson });
128
+ sendResult(payload.id, payloadJson);
123
129
  activeJobId = undefined;
124
130
  });
125
131
  });
@@ -1,10 +1,9 @@
1
- // An execution result is serialized once into payloadJson and again as that
2
- // string is embedded in ChildToParentMessage. Captured logs therefore get a
3
- // separate 512 KiB budget measured after both JSON encodings. The final result
4
- // is shaped to 24k characters; even if every character takes its worst-case
5
- // seven transport bytes, that leaves ample structural overhead below this hard
6
- // process.send ceiling.
1
+ // Result messages remain bounded even when a full return travels in chunks.
2
+ // The host's presentation guard stashes the completed value after assembly.
7
3
  export const MAX_QUICKJS_IPC_BYTES = 1024 * 1024;
4
+ /** Aggregate result transport bound, independent of per-message IPC and log caps. */
5
+ export const MAX_QUICKJS_RESULT_BYTES = 64 * 1024 * 1024;
6
+ export const QUICKJS_RESULT_CHUNK_CHARS = 65_536;
8
7
  export const MAX_QUICKJS_LOG_TRANSPORT_BYTES = 512 * 1024;
9
8
  /** Preserve #84's stopgap before a host value enters the child/WASM process. */
10
9
  export const MAX_QUICKJS_HOST_RPC_BYTES = 256 * 1024;
@@ -7,10 +7,11 @@
7
7
  // running the provider function; `executePendingJobs` drives guest
8
8
  // continuations from a host-side loop. Values cross the boundary as JSON, so
9
9
  // provider args/results must be JSON-serializable.
10
- import { getQuickJS, } from "quickjs-emscripten";
10
+ import { getQuickJS } from "quickjs-emscripten";
11
+ import { InvocationFailure } from "../invocation.js";
11
12
  import { msg } from "../errors.js";
12
13
  import { withoutProgramTerminator } from "../program-source.js";
13
- import { hostCallLabel, MAX_QUICKJS_LOG_TRANSPORT_BYTES, serializedBytes, } from "./quickjs-protocol.js";
14
+ import { hostCallLabel, MAX_QUICKJS_LOG_TRANSPORT_BYTES, serializedBytes } from "./quickjs-protocol.js";
14
15
  /** Load and compile the shared QuickJS WASM module before a run budget starts. */
15
16
  export async function prepareQuickJs() {
16
17
  await getQuickJS();
@@ -82,8 +83,8 @@ export function normalizeCode(code) {
82
83
  * and fail closed there.
83
84
  */
84
85
  function setupScript(providers) {
85
- // X11: raw replies carry an authenticated frame. Capture the bridge and
86
- // codec before guest code runs, and expose only the decoded provider calls.
86
+ // Capture the host bridge and codec before guest code runs.
87
+ // Rejections are native guest Errors created and retained by the host.
87
88
  const lines = [
88
89
  `(() => {
89
90
  const call = globalThis.__call;
@@ -102,7 +103,6 @@ function setupScript(providers) {
102
103
  })();
103
104
  const invoke = async (ns, fn, args) => {
104
105
  const r = parse(await call(ns, fn, stringify(args)));
105
- if (!r.ok) throw new GuestError(r.error);
106
106
  return r.value;
107
107
  };
108
108
  const namespace = (ns) => freeze(new ProxyConstructor(create(null), {
@@ -117,24 +117,14 @@ function setupScript(providers) {
117
117
  if (p.prelude)
118
118
  lines.push(p.prelude);
119
119
  }
120
- lines.push(`const GuestError = globalThis.Error;
121
- })();`);
120
+ lines.push(`})();`);
122
121
  return lines.join("\n");
123
122
  }
124
- /** True when a dumped error is QuickJS's deadline-interrupt signal. */
125
- function isInterrupt(dumped) {
126
- if (!dumped || typeof dumped !== "object")
127
- return false;
128
- const e = dumped;
129
- return e.name === "InternalError" && e.message === "interrupted";
130
- }
131
123
  function formatGuestError(dumped) {
132
124
  if (dumped && typeof dumped === "object") {
133
125
  const e = dumped;
134
126
  if (e.message != null) {
135
- return e.name != null && e.name !== "Error"
136
- ? `${String(e.name)}: ${String(e.message)}`
137
- : String(e.message);
127
+ return e.name != null && e.name !== "Error" ? `${String(e.name)}: ${String(e.message)}` : String(e.message);
138
128
  }
139
129
  }
140
130
  return typeof dumped === "string" ? dumped : JSON.stringify(dumped);
@@ -166,6 +156,7 @@ function installBridge(ctx, providers, logs, onLog) {
166
156
  aborted: false,
167
157
  wake: () => { },
168
158
  waitForSettle: Promise.resolve(),
159
+ failures: [],
169
160
  };
170
161
  armWake(bridge);
171
162
  // Keep both the in-memory character budget and the twice-JSON-encoded
@@ -195,8 +186,7 @@ function installBridge(ctx, providers, logs, onLog) {
195
186
  }
196
187
  const entryTransportBytes = logTransportBytes(entry);
197
188
  if (logTotalChars + entry.length > MAX_LOG_TOTAL_CHARS ||
198
- logTotalTransportBytes + entryTransportBytes >
199
- MAX_QUICKJS_LOG_TRANSPORT_BYTES - MAX_LOG_MARKER_TRANSPORT_BYTES) {
189
+ logTotalTransportBytes + entryTransportBytes > MAX_QUICKJS_LOG_TRANSPORT_BYTES - MAX_LOG_MARKER_TRANSPORT_BYTES) {
200
190
  captureLog(LOG_SIZE_LIMIT_MARKER);
201
191
  logBudgetSpent = true;
202
192
  return;
@@ -211,9 +201,7 @@ function installBridge(ctx, providers, logs, onLog) {
211
201
  const invoke = async (ns, fn, argsJson) => {
212
202
  try {
213
203
  const provider = byName.get(ns);
214
- const f = provider && Object.hasOwn(provider.fns, fn)
215
- ? provider.fns[fn]
216
- : undefined;
204
+ const f = provider && Object.hasOwn(provider.fns, fn) ? provider.fns[fn] : undefined;
217
205
  if (!f)
218
206
  throw new Error(`Unknown function ${ns}.${fn}`);
219
207
  const args = JSON.parse(argsJson);
@@ -231,10 +219,10 @@ function installBridge(ctx, providers, logs, onLog) {
231
219
  if (exceedsUtf8ByteLimit(json, MAX_HOST_RESULT_BYTES)) {
232
220
  throw new Error(`Host result from ${label} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`);
233
221
  }
234
- return json;
222
+ return { json };
235
223
  }
236
224
  catch (err) {
237
- return JSON.stringify({ ok: false, error: msg(err) });
225
+ return { error: err };
238
226
  }
239
227
  };
240
228
  const callFn = ctx.newFunction("__call", (nsH, fnH, argsH) => {
@@ -243,16 +231,42 @@ function installBridge(ctx, providers, logs, onLog) {
243
231
  const argsJson = ctx.getString(argsH);
244
232
  const deferred = ctx.newPromise();
245
233
  bridge.pending++;
246
- void invoke(ns, fn, argsJson).then((json) => {
234
+ void invoke(ns, fn, argsJson).then((reply) => {
247
235
  bridge.pending--;
248
236
  if (bridge.aborted) {
249
237
  // Context is (about to be) torn down — settle nothing, free our handles.
250
238
  deferred.dispose();
251
239
  }
252
240
  else {
253
- const h = ctx.newString(json);
254
- deferred.resolve(h);
255
- h.dispose();
241
+ if ("json" in reply) {
242
+ const h = ctx.newString(reply.json);
243
+ deferred.resolve(h);
244
+ h.dispose();
245
+ }
246
+ else {
247
+ const error = ctx.newError();
248
+ const message = ctx.unwrapResult(ctx.evalCode(JSON.stringify(msg(reply.error).slice(0, 4_000))));
249
+ ctx.setProp(error, "message", message);
250
+ message.dispose();
251
+ if (reply.error instanceof InvocationFailure) {
252
+ const call = reply.error.details;
253
+ // The error identity and typed facts stay in host state. A guest
254
+ // may edit public fields without changing the retained failure.
255
+ bridge.failures.push({ error: error.dup(), call });
256
+ for (const [key, value] of Object.entries({
257
+ code: call.code,
258
+ retryable: call.retryable,
259
+ details: call,
260
+ data: call,
261
+ })) {
262
+ const property = ctx.unwrapResult(ctx.evalCode(`(${JSON.stringify(value)})`));
263
+ ctx.setProp(error, key, property);
264
+ property.dispose();
265
+ }
266
+ }
267
+ deferred.reject(error);
268
+ error.dispose();
269
+ }
256
270
  }
257
271
  bridge.wake();
258
272
  });
@@ -296,14 +310,18 @@ export async function executeQuickJs(code, providers, options, onLog) {
296
310
  if (!inGuest)
297
311
  return false;
298
312
  wallInterrupted = Date.now() >= deadline;
299
- cpuInterrupted =
300
- cpuUsedMs + (Date.now() - segmentStarted) >= cpuTimeMs;
313
+ cpuInterrupted = cpuUsedMs + (Date.now() - segmentStarted) >= cpuTimeMs;
301
314
  return wallInterrupted || cpuInterrupted;
302
315
  });
316
+ let describeError;
303
317
  const logs = [];
304
318
  const bridge = installBridge(ctx, providers, logs, onLog);
305
319
  const finish = (r) => {
306
320
  bridge.aborted = true;
321
+ describeError?.dispose();
322
+ for (const failure of bridge.failures)
323
+ failure.error.dispose();
324
+ bridge.failures.length = 0;
307
325
  // Outstanding host calls still hold deferred-promise handles; their
308
326
  // callbacks free them and wake the drain, which disposes the context
309
327
  // once the last straggler settles. Re-arm before every wait so the
@@ -326,32 +344,119 @@ export async function executeQuickJs(code, providers, options, onLog) {
326
344
  }
327
345
  return logs.length > 0 ? { ...r, logs } : r;
328
346
  };
329
- const fail = (error) => finish({ result: undefined, error });
347
+ const rejected = (error) => {
348
+ // Interrupt/deadline facts are host state, never inferred by inspecting
349
+ // the thrown value. Do not reenter a guest whose budget has ended.
350
+ if (wallInterrupted || Date.now() >= deadline)
351
+ return timedOut();
352
+ if (cpuInterrupted || cpuUsedMs >= cpuTimeMs)
353
+ return { result: undefined, error: cpuTimeoutError };
354
+ const call = bridge.failures.find((failure) => ctx.eq(failure.error, error))?.call;
355
+ let description = { name: "Error", message: "Program threw a value." };
356
+ const type = ctx.typeof(error);
357
+ if (type === "string")
358
+ description = ctx.getString(error).slice(0, 4000);
359
+ else if (type === "number")
360
+ description = ctx.getNumber(error);
361
+ else if (type === "undefined")
362
+ description = "undefined";
363
+ else if (type === "boolean")
364
+ description = ctx.eq(error, ctx.true);
365
+ else if (ctx.eq(error, ctx.null))
366
+ description = null;
367
+ else if (describeError) {
368
+ const describe = describeError;
369
+ const described = runGuest(() => ctx.callFunction(describe, ctx.undefined, error));
370
+ if (described.error)
371
+ described.error.dispose();
372
+ else {
373
+ description = JSON.parse(ctx.getString(described.value)) ?? description;
374
+ described.value.dispose();
375
+ }
376
+ if (wallInterrupted || Date.now() >= deadline)
377
+ return timedOut();
378
+ if (cpuInterrupted || cpuUsedMs >= cpuTimeMs)
379
+ return { result: undefined, error: cpuTimeoutError };
380
+ }
381
+ const name = description && typeof description === "object" && "name" in description && typeof description.name === "string"
382
+ ? description.name
383
+ : "Error";
384
+ const stack = description && typeof description === "object" && "stack" in description && typeof description.stack === "string"
385
+ ? description.stack
386
+ : "";
387
+ const location = /:(\d+)(?::\d+)?\)?(?:\n|$)/.exec(stack);
388
+ return {
389
+ result: undefined,
390
+ error: formatGuestError(description).slice(0, 4000),
391
+ failure: {
392
+ name,
393
+ ...(call ? { call } : {}),
394
+ ...(location ? { line: Math.max(1, Number(location[1]) - 1) } : {}),
395
+ },
396
+ };
397
+ };
330
398
  const timedOut = () => ({
331
399
  result: undefined,
332
400
  error: timeoutError,
401
+ failure: {
402
+ name: "TimeoutError",
403
+ timeout: { elapsedMs: Date.now() - (deadline - timeoutMs), deadlineMs: timeoutMs },
404
+ },
333
405
  timedOut: true,
334
406
  });
407
+ // Keep this diagnostic function in a host-owned handle. Error.isError
408
+ // checks the native brand without Proxy traps. Own data descriptors of
409
+ // native Errors are safe to read; accessors and arbitrary thrown objects
410
+ // get a fixed description instead of dump's guest serialization/getters.
411
+ const described = runGuest(() => ctx.evalCode(`(() => {
412
+ const isError = Error.isError;
413
+ const descriptor = Object.getOwnPropertyDescriptor;
414
+ const prototypeOf = Object.getPrototypeOf;
415
+ const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
416
+ const slice = Function.prototype.call.bind(String.prototype.slice);
417
+ const stringify = JSON.stringify;
418
+ const prototypes = [
419
+ [Error.prototype, "Error"], [TypeError.prototype, "TypeError"],
420
+ [SyntaxError.prototype, "SyntaxError"], [ReferenceError.prototype, "ReferenceError"],
421
+ [RangeError.prototype, "RangeError"], [EvalError.prototype, "EvalError"],
422
+ [URIError.prototype, "URIError"], [AggregateError.prototype, "AggregateError"]
423
+ ];
424
+ return (error) => {
425
+ if (!isError(error)) return "null";
426
+ const text = (key, fallback, limit) => {
427
+ const own = descriptor(error, key);
428
+ return own && hasOwn(own, "value") && typeof own.value === "string"
429
+ ? slice(own.value, 0, limit) : fallback;
430
+ };
431
+ const prototype = prototypeOf(error);
432
+ let name = "Error";
433
+ for (let i = 0; i < prototypes.length; i++) {
434
+ if (prototype === prototypes[i][0]) { name = prototypes[i][1]; break; }
435
+ }
436
+ return stringify({ __proto__: null, name: text("name", name, 64),
437
+ message: text("message", "Program threw a value.", 4000), stack: text("stack", "", 1000) });
438
+ };
439
+ })()`));
440
+ if (described.error) {
441
+ const rejection = rejected(described.error);
442
+ described.error.dispose();
443
+ return finish(rejection);
444
+ }
445
+ describeError = described.value;
335
446
  const setup = runGuest(() => ctx.evalCode(setupScript(providers)));
336
447
  if (setup.error) {
337
- const detail = formatGuestError(ctx.dump(setup.error));
448
+ const rejection = rejected(setup.error);
338
449
  setup.error.dispose();
339
- if (wallInterrupted)
340
- return finish(timedOut());
341
- if (cpuInterrupted)
342
- return fail(cpuTimeoutError);
343
- return fail(`Sandbox setup failed: ${detail}`);
450
+ return finish(rejection.timedOut || cpuInterrupted
451
+ ? rejection
452
+ : { ...rejection, error: `Sandbox setup failed: ${rejection.error}` });
344
453
  }
345
454
  setup.value.dispose();
346
455
  const evaluated = runGuest(() => ctx.evalCode(`Promise.resolve((\n${normalizeCode(code)}\n)())`));
347
456
  if (evaluated.error) {
348
- const dumped = ctx.dump(evaluated.error);
457
+ const rejection = rejected(evaluated.error);
349
458
  evaluated.error.dispose();
350
- if (isInterrupt(dumped) && wallInterrupted)
351
- return finish(timedOut());
352
- if (isInterrupt(dumped) && cpuInterrupted)
353
- return fail(cpuTimeoutError);
354
- return fail(formatGuestError(dumped));
459
+ return finish(rejection);
355
460
  }
356
461
  const promiseHandle = evaluated.value;
357
462
  // Drive the guest: run microtask jobs, inspect the result promise,
@@ -364,15 +469,9 @@ export async function executeQuickJs(code, providers, options, onLog) {
364
469
  }
365
470
  const jobs = runGuest(() => ctx.runtime.executePendingJobs());
366
471
  if (jobs.error) {
367
- const dumped = ctx.dump(jobs.error);
472
+ const rejection = rejected(jobs.error);
368
473
  jobs.error.dispose();
369
- if (isInterrupt(dumped) && wallInterrupted) {
370
- return timedOut();
371
- }
372
- if (isInterrupt(dumped) && cpuInterrupted) {
373
- return { result: undefined, error: cpuTimeoutError };
374
- }
375
- return { result: undefined, error: formatGuestError(dumped) };
474
+ return rejection;
376
475
  }
377
476
  const state = ctx.getPromiseState(promiseHandle);
378
477
  if (state.type === "fulfilled") {
@@ -381,15 +480,9 @@ export async function executeQuickJs(code, providers, options, onLog) {
381
480
  return { result };
382
481
  }
383
482
  if (state.type === "rejected") {
384
- const dumped = ctx.dump(state.error);
483
+ const rejection = rejected(state.error);
385
484
  state.error.dispose();
386
- if (isInterrupt(dumped) && wallInterrupted) {
387
- return timedOut();
388
- }
389
- if (isInterrupt(dumped) && cpuInterrupted) {
390
- return { result: undefined, error: cpuTimeoutError };
391
- }
392
- return { result: undefined, error: formatGuestError(dumped) };
485
+ return rejection;
393
486
  }
394
487
  if (bridge.pending === 0) {
395
488
  return {