@zackbart/connecta 0.28.0 → 0.29.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 (320) hide show
  1. package/CHANGELOG.md +961 -5
  2. package/README.md +62 -39
  3. package/bin/connecta.mjs +110 -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/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1632 -1509
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +526 -0
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +220 -0
  52. package/dist/connectors/api-connector.js +214 -0
  53. package/dist/connectors/api.d.ts +8 -71
  54. package/dist/connectors/api.js +13 -99
  55. package/dist/connectors/guarded-fetch.d.ts +144 -0
  56. package/dist/connectors/guarded-fetch.js +108 -25
  57. package/dist/connectors/negotiation-cache.js +46 -0
  58. package/dist/connectors/option-shapes.js +27 -0
  59. package/dist/connectors/remote-mcp.d.ts +30 -4
  60. package/dist/connectors/remote-mcp.js +1780 -351
  61. package/dist/connectors/resource-uri.js +335 -0
  62. package/dist/credential-contract.d.ts +10 -0
  63. package/dist/credential-rules.js +1 -3
  64. package/dist/credential-url.js +108 -0
  65. package/dist/credentials.d.ts +6 -1
  66. package/dist/credentials.js +27 -18
  67. package/dist/d1.d.ts +130 -0
  68. package/dist/d1.js +290 -0
  69. package/dist/describe-config.d.ts +165 -0
  70. package/dist/describe-config.js +315 -0
  71. package/dist/described.js +106 -0
  72. package/dist/downstream-input-context.js +119 -0
  73. package/dist/downstream-input.js +323 -0
  74. package/dist/errors.d.ts +97 -20
  75. package/dist/errors.js +158 -80
  76. package/dist/execute.js +453 -305
  77. package/dist/executor-admission.d.ts +1 -0
  78. package/dist/executor-admission.js +11 -15
  79. package/dist/executor-contract.d.ts +20 -0
  80. package/dist/executor-contract.js +48 -0
  81. package/dist/executor-result.js +24 -9
  82. package/dist/executors/quickjs-child.js +3 -4
  83. package/dist/executors/quickjs-runtime.js +147 -59
  84. package/dist/executors/quickjs.js +29 -35
  85. package/dist/favicon.js +1 -2
  86. package/dist/fix-prompt.js +1 -3
  87. package/dist/guest-runtime.js +188 -0
  88. package/dist/guest-types.d.ts +176 -0
  89. package/dist/guest-types.js +1 -0
  90. package/dist/html-security.js +24 -0
  91. package/dist/identity.js +1 -3
  92. package/dist/inbound-credential.js +52 -0
  93. package/dist/index.d.ts +25 -257
  94. package/dist/index.js +65 -478
  95. package/dist/invocation-auth.js +69 -0
  96. package/dist/invocation.js +230 -73
  97. package/dist/json-schema.d.ts +1 -1
  98. package/dist/mcp-client-context.js +35 -0
  99. package/dist/mcp-result.js +11 -7
  100. package/dist/meta-output.js +76 -0
  101. package/dist/meta-tool-names.d.ts +2 -0
  102. package/dist/meta-tool-names.js +3 -0
  103. package/dist/meta-tools.js +285 -209
  104. package/dist/module-contracts.d.ts +8 -23
  105. package/dist/node.d.ts +0 -2
  106. package/dist/node.js +2 -2
  107. package/dist/oauth-callback-outcome.js +1 -3
  108. package/dist/oauth-handoff.js +117 -0
  109. package/dist/oauth-partition.js +17 -0
  110. package/dist/oauth-sealing.js +20 -0
  111. package/dist/operator-record.d.ts +137 -0
  112. package/dist/operator-record.js +420 -0
  113. package/dist/operator-ui/assets.js +31 -0
  114. package/dist/operator-ui/contract.d.ts +72 -0
  115. package/dist/operator-ui/contract.js +1 -0
  116. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  117. package/dist/operator-ui/generated.js +5 -4
  118. package/dist/operator-ui/model.d.ts +12 -87
  119. package/dist/operator-ui/model.js +2 -9
  120. package/dist/operator-ui/setup-commands.js +1 -3
  121. package/dist/operator-ui/view.d.ts +323 -0
  122. package/dist/operator-ui/view.js +62 -189
  123. package/dist/page-styles.js +2 -3
  124. package/dist/program-source.js +14 -2
  125. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  126. package/dist/provider.d.ts +142 -0
  127. package/dist/provider.js +187 -0
  128. package/dist/providers/_shared/google/result-size.js +57 -0
  129. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  130. package/dist/providers/_shared/google/service-account.js +500 -0
  131. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  132. package/dist/providers/_shared/google/workspace.js +518 -0
  133. package/dist/providers/basecamp/index.d.ts +43 -0
  134. package/dist/providers/basecamp/index.js +997 -0
  135. package/dist/providers/basecamp/skill.generated.js +9 -0
  136. package/dist/providers/breeze/index.d.ts +34 -0
  137. package/dist/providers/breeze/index.js +1446 -0
  138. package/dist/providers/breeze/skill.generated.js +9 -0
  139. package/dist/providers/ccb/index.d.ts +103 -0
  140. package/dist/providers/ccb/index.js +1725 -0
  141. package/dist/providers/ccb/skill.generated.js +9 -0
  142. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  143. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  144. package/dist/providers/cloudflare/skill.generated.js +10 -0
  145. package/dist/providers/docs/index.d.ts +54 -0
  146. package/dist/providers/docs/index.js +1237 -0
  147. package/dist/providers/docs/skill.generated.js +9 -0
  148. package/dist/providers/drive/index.d.ts +53 -0
  149. package/dist/providers/drive/index.js +1523 -0
  150. package/dist/providers/drive/skill.generated.js +9 -0
  151. package/dist/providers/forms/index.d.ts +54 -0
  152. package/dist/providers/forms/index.js +1217 -0
  153. package/dist/providers/forms/skill.generated.js +11 -0
  154. package/dist/providers/github/auth.d.ts +32 -0
  155. package/dist/providers/github/auth.js +361 -0
  156. package/dist/providers/github/hosted.js +218 -0
  157. package/dist/providers/github/index.d.ts +13 -0
  158. package/dist/providers/github/index.js +140 -0
  159. package/dist/providers/github/key.js +100 -0
  160. package/dist/providers/github/rest.js +321 -0
  161. package/dist/providers/github/scope.d.ts +29 -0
  162. package/dist/providers/github/scope.js +124 -0
  163. package/dist/providers/github/skill.generated.js +8 -0
  164. package/dist/providers/gmail/index.d.ts +51 -0
  165. package/dist/providers/gmail/index.js +1436 -0
  166. package/dist/providers/gmail/skill.generated.js +9 -0
  167. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  168. package/dist/providers/linear/index.js +352 -0
  169. package/dist/providers/linear/skill.generated.js +8 -0
  170. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  171. package/dist/providers/mixpanel/index.js +416 -0
  172. package/dist/providers/mixpanel/skill.generated.js +9 -0
  173. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  174. package/dist/providers/{notion.js → notion/index.js} +226 -239
  175. package/dist/providers/notion/skill.generated.js +10 -0
  176. package/dist/providers/overflow/index.d.ts +60 -0
  177. package/dist/providers/overflow/index.js +1535 -0
  178. package/dist/providers/overflow/skill.generated.js +8 -0
  179. package/dist/providers/planning-center/index.d.ts +59 -0
  180. package/dist/providers/planning-center/index.js +2199 -0
  181. package/dist/providers/planning-center/skill.generated.js +10 -0
  182. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  183. package/dist/providers/revenuecat/index.js +557 -0
  184. package/dist/providers/revenuecat/skill.generated.js +11 -0
  185. package/dist/providers/sheets/index.d.ts +54 -0
  186. package/dist/providers/sheets/index.js +1237 -0
  187. package/dist/providers/sheets/skill.generated.js +10 -0
  188. package/dist/providers/slides/index.d.ts +53 -0
  189. package/dist/providers/slides/index.js +2645 -0
  190. package/dist/providers/slides/skill.generated.js +9 -0
  191. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  192. package/dist/providers/stripe/index.js +266 -0
  193. package/dist/providers/stripe/skill.generated.js +11 -0
  194. package/dist/providers/tithely/index.d.ts +49 -0
  195. package/dist/providers/tithely/index.js +1003 -0
  196. package/dist/providers/tithely/skill.generated.js +8 -0
  197. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  198. package/dist/providers/vercel/index.js +1038 -0
  199. package/dist/providers/vercel/mcp-ownership.js +212 -0
  200. package/dist/providers/vercel/skill.generated.js +11 -0
  201. package/dist/registry.d.ts +108 -171
  202. package/dist/registry.js +513 -925
  203. package/dist/request-state.js +21 -0
  204. package/dist/resource-read.js +100 -0
  205. package/dist/result-shapes.js +17 -49
  206. package/dist/retired-toolkits.js +1 -1
  207. package/dist/routes/access-tokens.js +3 -6
  208. package/dist/routes/activity-context.js +21 -0
  209. package/dist/routes/activity-disclosure.js +40 -0
  210. package/dist/routes/activity.js +59 -21
  211. package/dist/routes/actor-labels.js +3 -50
  212. package/dist/routes/connect.js +221 -0
  213. package/dist/routes/credentials.js +15 -27
  214. package/dist/routes/mcp.js +356 -130
  215. package/dist/routes/oauth-client-metadata.js +15 -0
  216. package/dist/routes/oauth-management.js +45 -107
  217. package/dist/routes/oauth.js +136 -57
  218. package/dist/routes/operator.js +3 -3
  219. package/dist/routes/shared.d.ts +25 -64
  220. package/dist/routes/shared.js +160 -80
  221. package/dist/routes/skills.js +30 -0
  222. package/dist/routes/ui-config.js +225 -0
  223. package/dist/routes/ui-data.js +40 -35
  224. package/dist/routes/ui.js +66 -56
  225. package/dist/runtime/admission.js +4 -2
  226. package/dist/runtime/call-admission.js +3 -1
  227. package/dist/runtime/connector-scope.js +1 -4
  228. package/dist/runtime/run.js +10 -39
  229. package/dist/runtime/services.js +12 -117
  230. package/dist/runtime/shared-read.js +1 -3
  231. package/dist/runtime/storage.js +8 -1
  232. package/dist/sent-secrets.d.ts +52 -0
  233. package/dist/sent-secrets.js +474 -0
  234. package/dist/server.js +28 -71
  235. package/dist/skills.js +406 -174
  236. package/dist/sqlite.d.ts +48 -0
  237. package/dist/sqlite.js +166 -0
  238. package/dist/storage/keys.js +428 -0
  239. package/dist/storage/memory.js +7 -0
  240. package/dist/storage/sql.d.ts +66 -0
  241. package/dist/storage/sql.js +534 -0
  242. package/dist/timeout.js +4 -2
  243. package/dist/tool-name.js +17 -0
  244. package/dist/tool-safety.d.ts +9 -35
  245. package/dist/tool-safety.js +20 -40
  246. package/dist/types.d.ts +320 -54
  247. package/dist/types.js +0 -1
  248. package/dist/ui.d.ts +7 -19
  249. package/dist/ui.js +76 -90
  250. package/dist/url-safety.js +3 -6
  251. package/dist/usage-guide.js +137 -0
  252. package/dist/validate.d.ts +7 -0
  253. package/dist/validate.js +309 -47
  254. package/dist/version.d.ts +1 -7
  255. package/dist/version.js +3 -7
  256. package/dist/worker.d.ts +11 -0
  257. package/dist/worker.js +434 -0
  258. package/documentation/architecture.md +665 -230
  259. package/documentation/auth.md +962 -458
  260. package/documentation/code-mode.md +501 -312
  261. package/documentation/meta-tools.md +449 -200
  262. package/documentation/operator-tests.md +93 -0
  263. package/documentation/provider-migration-0.29.md +60 -0
  264. package/examples/worker/AGENTS.md +34 -12
  265. package/examples/worker/README.md +286 -255
  266. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  267. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  268. package/examples/worker/src/connecta.config.ts +151 -0
  269. package/examples/worker/src/index.ts +14 -197
  270. package/examples/worker/wrangler.jsonc +15 -35
  271. package/package.json +125 -35
  272. package/templates/node/.dockerignore +1 -2
  273. package/templates/node/.env.example +20 -21
  274. package/templates/node/AGENTS.md +21 -17
  275. package/templates/node/Dockerfile +3 -5
  276. package/templates/node/README.md +133 -58
  277. package/templates/node/docker-compose.yml +11 -12
  278. package/templates/node/package.json +4 -2
  279. package/templates/node/src/connecta.config.ts +119 -0
  280. package/templates/node/src/index.ts +9 -154
  281. package/templates/node/src/provision-token.ts +17 -0
  282. package/AGENTS.md +0 -170
  283. package/dist/artifacts/connector.d.ts +0 -58
  284. package/dist/artifacts/connector.js +0 -730
  285. package/dist/artifacts/document.js +0 -86
  286. package/dist/artifacts/guide.js +0 -140
  287. package/dist/artifacts/html-scan.js +0 -329
  288. package/dist/artifacts/json.js +0 -78
  289. package/dist/artifacts/kv-store.d.ts +0 -20
  290. package/dist/artifacts/kv-store.js +0 -140
  291. package/dist/artifacts/markdown.js +0 -478
  292. package/dist/artifacts/module.d.ts +0 -37
  293. package/dist/artifacts/module.js +0 -66
  294. package/dist/artifacts/operations.d.ts +0 -197
  295. package/dist/artifacts/operations.js +0 -1002
  296. package/dist/artifacts/refresh.d.ts +0 -55
  297. package/dist/artifacts/refresh.js +0 -236
  298. package/dist/artifacts/routes.js +0 -206
  299. package/dist/artifacts/types.d.ts +0 -214
  300. package/dist/artifacts/types.js +0 -22
  301. package/dist/artifacts/validate.d.ts +0 -62
  302. package/dist/artifacts/validate.js +0 -651
  303. package/dist/artifacts.d.ts +0 -6
  304. package/dist/artifacts.js +0 -9
  305. package/dist/auth/bearer.d.ts +0 -11
  306. package/dist/auth/bearer.js +0 -50
  307. package/dist/providers/linear.js +0 -217
  308. package/dist/providers/mixpanel.js +0 -247
  309. package/dist/providers/revenuecat.js +0 -317
  310. package/dist/providers/stripe.js +0 -253
  311. package/dist/providers/vercel.js +0 -1253
  312. package/dist/storage/file.d.ts +0 -15
  313. package/dist/storage/file.js +0 -455
  314. package/ethos.md +0 -93
  315. package/examples/worker/src/cloudflare-kv.ts +0 -43
  316. package/examples/worker/src/d1-activity-row.ts +0 -155
  317. package/examples/worker/src/d1-activity.ts +0 -147
  318. package/examples/worker/src/d1-storage.ts +0 -138
  319. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  320. 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,44 @@ 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) {
22
22
  const base = {
23
23
  truncated: true,
24
24
  preview: "",
25
- totalChars: text.length,
25
+ totalChars,
26
26
  hint: TRUNCATION_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) {
41
41
  const text = serializeResultText(value);
42
- if (text.length <= MAX_EXECUTE_RESULT_CHARS)
42
+ if (text.length <= maxChars)
43
43
  return value;
44
- return truncationEnvelope(text);
44
+ // A child may already have guarded this value at a larger transport cap.
45
+ // Shrink its preview once more without nesting notices or losing original size.
46
+ if (value !== null && typeof value === "object") {
47
+ const prior = value;
48
+ if (prior.truncated === true &&
49
+ prior.hint === TRUNCATION_HINT &&
50
+ typeof prior.preview === "string" &&
51
+ typeof prior.totalChars === "number" &&
52
+ Number.isSafeInteger(prior.totalChars) &&
53
+ prior.totalChars >= prior.preview.length &&
54
+ Object.keys(value).length === 4) {
55
+ return truncationEnvelope(prior.preview, maxChars, prior.totalChars);
56
+ }
57
+ }
58
+ return truncationEnvelope(text, maxChars);
45
59
  }
46
60
  export function truncateExecuteText(text, max) {
47
61
  if (text.length <= max)
@@ -63,6 +77,7 @@ export function prepareExecuteResultForTransport(outcome) {
63
77
  return {
64
78
  result: undefined,
65
79
  error: outcome.error,
80
+ ...(outcome.failure ? { failure: outcome.failure } : {}),
66
81
  ...(logs ? { logs } : {}),
67
82
  };
68
83
  }
@@ -1,4 +1,5 @@
1
1
  import { prepareExecuteResultForTransport } from "../executor-result.js";
2
+ import { InvocationFailure } from "../invocation.js";
2
3
  import { msg } from "../errors.js";
3
4
  import { MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
4
5
  import { executeQuickJs, prepareQuickJs } from "./quickjs-runtime.js";
@@ -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
  }
@@ -104,7 +103,7 @@ process.on("message", (message) => {
104
103
  if (result.ok)
105
104
  request.resolve(result.value);
106
105
  else
107
- request.reject(new Error(result.error));
106
+ request.reject(result.call ? new InvocationFailure(result.call) : new Error(result.error));
108
107
  }
109
108
  catch (err) {
110
109
  request.reject(new Error(`Invalid host result: ${msg(err)}`));
@@ -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,37 @@ 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({ code: call.code, retryable: call.retryable, details: call })) {
257
+ const property = ctx.unwrapResult(ctx.evalCode(`(${JSON.stringify(value)})`));
258
+ ctx.setProp(error, key, property);
259
+ property.dispose();
260
+ }
261
+ }
262
+ deferred.reject(error);
263
+ error.dispose();
264
+ }
256
265
  }
257
266
  bridge.wake();
258
267
  });
@@ -296,14 +305,18 @@ export async function executeQuickJs(code, providers, options, onLog) {
296
305
  if (!inGuest)
297
306
  return false;
298
307
  wallInterrupted = Date.now() >= deadline;
299
- cpuInterrupted =
300
- cpuUsedMs + (Date.now() - segmentStarted) >= cpuTimeMs;
308
+ cpuInterrupted = cpuUsedMs + (Date.now() - segmentStarted) >= cpuTimeMs;
301
309
  return wallInterrupted || cpuInterrupted;
302
310
  });
311
+ let describeError;
303
312
  const logs = [];
304
313
  const bridge = installBridge(ctx, providers, logs, onLog);
305
314
  const finish = (r) => {
306
315
  bridge.aborted = true;
316
+ describeError?.dispose();
317
+ for (const failure of bridge.failures)
318
+ failure.error.dispose();
319
+ bridge.failures.length = 0;
307
320
  // Outstanding host calls still hold deferred-promise handles; their
308
321
  // callbacks free them and wake the drain, which disposes the context
309
322
  // once the last straggler settles. Re-arm before every wait so the
@@ -326,32 +339,119 @@ export async function executeQuickJs(code, providers, options, onLog) {
326
339
  }
327
340
  return logs.length > 0 ? { ...r, logs } : r;
328
341
  };
329
- const fail = (error) => finish({ result: undefined, error });
342
+ const rejected = (error) => {
343
+ // Interrupt/deadline facts are host state, never inferred by inspecting
344
+ // the thrown value. Do not reenter a guest whose budget has ended.
345
+ if (wallInterrupted || Date.now() >= deadline)
346
+ return timedOut();
347
+ if (cpuInterrupted || cpuUsedMs >= cpuTimeMs)
348
+ return { result: undefined, error: cpuTimeoutError };
349
+ const call = bridge.failures.find((failure) => ctx.eq(failure.error, error))?.call;
350
+ let description = { name: "Error", message: "Program threw a value." };
351
+ const type = ctx.typeof(error);
352
+ if (type === "string")
353
+ description = ctx.getString(error).slice(0, 4000);
354
+ else if (type === "number")
355
+ description = ctx.getNumber(error);
356
+ else if (type === "undefined")
357
+ description = "undefined";
358
+ else if (type === "boolean")
359
+ description = ctx.eq(error, ctx.true);
360
+ else if (ctx.eq(error, ctx.null))
361
+ description = null;
362
+ else if (describeError) {
363
+ const describe = describeError;
364
+ const described = runGuest(() => ctx.callFunction(describe, ctx.undefined, error));
365
+ if (described.error)
366
+ described.error.dispose();
367
+ else {
368
+ description = JSON.parse(ctx.getString(described.value)) ?? description;
369
+ described.value.dispose();
370
+ }
371
+ if (wallInterrupted || Date.now() >= deadline)
372
+ return timedOut();
373
+ if (cpuInterrupted || cpuUsedMs >= cpuTimeMs)
374
+ return { result: undefined, error: cpuTimeoutError };
375
+ }
376
+ const name = description && typeof description === "object" && "name" in description && typeof description.name === "string"
377
+ ? description.name
378
+ : "Error";
379
+ const stack = description && typeof description === "object" && "stack" in description && typeof description.stack === "string"
380
+ ? description.stack
381
+ : "";
382
+ const location = /:(\d+)(?::\d+)?\)?(?:\n|$)/.exec(stack);
383
+ return {
384
+ result: undefined,
385
+ error: formatGuestError(description).slice(0, 4000),
386
+ failure: {
387
+ name,
388
+ ...(call ? { call } : {}),
389
+ ...(location ? { line: Math.max(1, Number(location[1]) - 1) } : {}),
390
+ },
391
+ };
392
+ };
330
393
  const timedOut = () => ({
331
394
  result: undefined,
332
395
  error: timeoutError,
396
+ failure: {
397
+ name: "TimeoutError",
398
+ timeout: { elapsedMs: Date.now() - (deadline - timeoutMs), deadlineMs: timeoutMs },
399
+ },
333
400
  timedOut: true,
334
401
  });
402
+ // Keep this diagnostic function in a host-owned handle. Error.isError
403
+ // checks the native brand without Proxy traps. Own data descriptors of
404
+ // native Errors are safe to read; accessors and arbitrary thrown objects
405
+ // get a fixed description instead of dump's guest serialization/getters.
406
+ const described = runGuest(() => ctx.evalCode(`(() => {
407
+ const isError = Error.isError;
408
+ const descriptor = Object.getOwnPropertyDescriptor;
409
+ const prototypeOf = Object.getPrototypeOf;
410
+ const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
411
+ const slice = Function.prototype.call.bind(String.prototype.slice);
412
+ const stringify = JSON.stringify;
413
+ const prototypes = [
414
+ [Error.prototype, "Error"], [TypeError.prototype, "TypeError"],
415
+ [SyntaxError.prototype, "SyntaxError"], [ReferenceError.prototype, "ReferenceError"],
416
+ [RangeError.prototype, "RangeError"], [EvalError.prototype, "EvalError"],
417
+ [URIError.prototype, "URIError"], [AggregateError.prototype, "AggregateError"]
418
+ ];
419
+ return (error) => {
420
+ if (!isError(error)) return "null";
421
+ const text = (key, fallback, limit) => {
422
+ const own = descriptor(error, key);
423
+ return own && hasOwn(own, "value") && typeof own.value === "string"
424
+ ? slice(own.value, 0, limit) : fallback;
425
+ };
426
+ const prototype = prototypeOf(error);
427
+ let name = "Error";
428
+ for (let i = 0; i < prototypes.length; i++) {
429
+ if (prototype === prototypes[i][0]) { name = prototypes[i][1]; break; }
430
+ }
431
+ return stringify({ __proto__: null, name: text("name", name, 64),
432
+ message: text("message", "Program threw a value.", 4000), stack: text("stack", "", 1000) });
433
+ };
434
+ })()`));
435
+ if (described.error) {
436
+ const rejection = rejected(described.error);
437
+ described.error.dispose();
438
+ return finish(rejection);
439
+ }
440
+ describeError = described.value;
335
441
  const setup = runGuest(() => ctx.evalCode(setupScript(providers)));
336
442
  if (setup.error) {
337
- const detail = formatGuestError(ctx.dump(setup.error));
443
+ const rejection = rejected(setup.error);
338
444
  setup.error.dispose();
339
- if (wallInterrupted)
340
- return finish(timedOut());
341
- if (cpuInterrupted)
342
- return fail(cpuTimeoutError);
343
- return fail(`Sandbox setup failed: ${detail}`);
445
+ return finish(rejection.timedOut || cpuInterrupted
446
+ ? rejection
447
+ : { ...rejection, error: `Sandbox setup failed: ${rejection.error}` });
344
448
  }
345
449
  setup.value.dispose();
346
450
  const evaluated = runGuest(() => ctx.evalCode(`Promise.resolve((\n${normalizeCode(code)}\n)())`));
347
451
  if (evaluated.error) {
348
- const dumped = ctx.dump(evaluated.error);
452
+ const rejection = rejected(evaluated.error);
349
453
  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));
454
+ return finish(rejection);
355
455
  }
356
456
  const promiseHandle = evaluated.value;
357
457
  // Drive the guest: run microtask jobs, inspect the result promise,
@@ -364,15 +464,9 @@ export async function executeQuickJs(code, providers, options, onLog) {
364
464
  }
365
465
  const jobs = runGuest(() => ctx.runtime.executePendingJobs());
366
466
  if (jobs.error) {
367
- const dumped = ctx.dump(jobs.error);
467
+ const rejection = rejected(jobs.error);
368
468
  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) };
469
+ return rejection;
376
470
  }
377
471
  const state = ctx.getPromiseState(promiseHandle);
378
472
  if (state.type === "fulfilled") {
@@ -381,15 +475,9 @@ export async function executeQuickJs(code, providers, options, onLog) {
381
475
  return { result };
382
476
  }
383
477
  if (state.type === "rejected") {
384
- const dumped = ctx.dump(state.error);
478
+ const rejection = rejected(state.error);
385
479
  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) };
480
+ return rejection;
393
481
  }
394
482
  if (bridge.pending === 0) {
395
483
  return {