@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
package/src/execute.ts DELETED
@@ -1,1406 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/server";
2
- import { z } from "zod";
3
- import type { ActivityRequestContext } from "./activity.js";
4
- import {
5
- PROGRAM_UI_META_KEY,
6
- PROGRAM_UI_RESOURCE_URI,
7
- } from "./apps-shell.js";
8
- import {
9
- boundedDiscoveryText,
10
- CatalogService,
11
- DiscoveryPolicyError,
12
- flatSearchResult,
13
- } from "./catalog-service.js";
14
- import { errorResult, jsonResult, type ToolResult } from "./meta-tools.js";
15
- import {
16
- guardExecuteResultValue,
17
- MAX_EXECUTE_LOG_CHARS,
18
- truncateExecuteText,
19
- } from "./executor-result.js";
20
- import {
21
- ExecutorAdmissionError,
22
- ExecutorExecutionError,
23
- isAdmittingExecutor,
24
- } from "./executor-admission.js";
25
- import { classifyCallError } from "./errors.js";
26
- import {
27
- InvocationFailure,
28
- InvocationService,
29
- } from "./invocation.js";
30
- import type { RegistryView } from "./registry.js";
31
- import { hasConnectorGuides } from "./skills.js";
32
- import { isExplicitlyReadOnly } from "./tool-safety.js";
33
- import type {
34
- Executor,
35
- ExecutorProvider,
36
- Logger,
37
- } from "./types.js";
38
-
39
- /** Keep one model-written program from amplifying into an unbounded fan-out. */
40
- const EXECUTE_MAX_HOST_CALLS = 20;
41
- export const EXECUTE_MAX_BATCH_CALLS = 10;
42
- const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
43
- /**
44
- * Default budgets for `connecta.emit`. The byte budget is a transport bound,
45
- * not a context bound — emitted image/audio blocks reach the model as media,
46
- * not base64 text — so it sits far above the 24k return-value guard: room for
47
- * two or three real screenshots after base64's 4/3 inflation, well short of a
48
- * file-hosting ambition (design record M5).
49
- */
50
- export const EXECUTE_MAX_EMITTED_BYTES = 4_000_000;
51
- export const EXECUTE_MAX_EMITTED_BLOCKS = 32;
52
- const diagnosticsEncoder = new TextEncoder();
53
-
54
- type ExecuteDiagnosticOperation = "search" | "describe" | "call" | "batch";
55
-
56
- interface ExecuteOperationDiagnostics {
57
- operation: ExecuteDiagnosticOperation;
58
- count: number;
59
- failures: number;
60
- durationMs: number;
61
- resultBytes: number;
62
- catalogMs: number;
63
- connectorMs: number;
64
- calls?: number;
65
- }
66
-
67
- type MutableOperationDiagnostics = ExecuteOperationDiagnostics;
68
-
69
- class ExecuteDiagnostics {
70
- private readonly started = Date.now();
71
- private readonly operations = new Map<
72
- ExecuteDiagnosticOperation,
73
- MutableOperationDiagnostics
74
- >();
75
- admissionMs = 0;
76
- setupMs = 0;
77
- executorWallMs = 0;
78
- private emitted?: { count: number; bytes: number };
79
- private ui?: number;
80
-
81
- /** Numbers only, per R8 — and only once something was emitted, so a
82
- * non-emitting run's diagnostics stay byte-for-byte what they were. */
83
- recordEmitted(count: number, bytes: number): void {
84
- if (count > 0) this.emitted = { count, bytes };
85
- }
86
-
87
- /**
88
- * U9: the UI payload gets its own aggregate — one number, the payload's
89
- * serialized size. Folding it into `emitted` would desync that aggregate's
90
- * pair, which reports the bytes a specific block count cost.
91
- */
92
- recordUi(bytes: number): void {
93
- this.ui = bytes;
94
- }
95
-
96
- private stats(operation: ExecuteDiagnosticOperation) {
97
- let stats = this.operations.get(operation);
98
- if (!stats) {
99
- stats = {
100
- operation,
101
- count: 0,
102
- failures: 0,
103
- durationMs: 0,
104
- resultBytes: 0,
105
- catalogMs: 0,
106
- connectorMs: 0,
107
- };
108
- this.operations.set(operation, stats);
109
- }
110
- return stats;
111
- }
112
-
113
- recordCatalog(
114
- operation: "search" | "describe",
115
- durationMs: number,
116
- ok: boolean,
117
- result?: unknown,
118
- ): void {
119
- const stats = this.stats(operation);
120
- stats.count++;
121
- stats.failures += ok ? 0 : 1;
122
- stats.durationMs += durationMs;
123
- stats.catalogMs += durationMs;
124
- if (ok) stats.resultBytes += serializedDiagnosticBytes(result);
125
- }
126
-
127
- recordCall(
128
- operation: "call" | "batch",
129
- outcome: {
130
- ok: boolean;
131
- durationMs: number;
132
- timing: { catalogMs: number; connectorMs: number };
133
- value?: unknown;
134
- },
135
- ): void {
136
- const stats = this.stats(operation);
137
- if (operation === "call") {
138
- stats.count++;
139
- if (outcome.ok) {
140
- stats.resultBytes += serializedDiagnosticBytes(outcome.value);
141
- }
142
- } else {
143
- stats.calls = (stats.calls ?? 0) + 1;
144
- }
145
- stats.failures += outcome.ok ? 0 : 1;
146
- if (operation === "call") stats.durationMs += outcome.durationMs;
147
- stats.catalogMs += outcome.timing.catalogMs;
148
- stats.connectorMs += outcome.timing.connectorMs;
149
- }
150
-
151
- recordBatch(
152
- durationMs: number,
153
- ok: boolean,
154
- calls: number,
155
- result?: unknown,
156
- ): void {
157
- const stats = this.stats("batch");
158
- stats.count++;
159
- // Calls normally accrue while each child runs. Invalid batch input never
160
- // starts children, so retain the attempted cardinality here.
161
- if (!ok) stats.calls = Math.max(stats.calls ?? 0, calls);
162
- stats.durationMs += durationMs;
163
- if (!ok) stats.failures++;
164
- else stats.resultBytes += serializedDiagnosticBytes(result);
165
- }
166
-
167
- finish(): {
168
- timing: {
169
- totalMs: number;
170
- admissionMs: number;
171
- setupMs: number;
172
- executorWallMs: number;
173
- catalogMs: number;
174
- connectorMs: number;
175
- };
176
- operations: ExecuteOperationDiagnostics[];
177
- emitted?: { count: number; bytes: number };
178
- ui?: number;
179
- } {
180
- const operations = [...this.operations.values()];
181
- return {
182
- timing: {
183
- totalMs: Date.now() - this.started,
184
- admissionMs: this.admissionMs,
185
- setupMs: this.setupMs,
186
- executorWallMs: this.executorWallMs,
187
- catalogMs: operations.reduce((sum, item) => sum + item.catalogMs, 0),
188
- connectorMs: operations.reduce(
189
- (sum, item) => sum + item.connectorMs,
190
- 0,
191
- ),
192
- },
193
- operations,
194
- ...(this.emitted ? { emitted: this.emitted } : {}),
195
- ...(this.ui !== undefined ? { ui: this.ui } : {}),
196
- };
197
- }
198
- }
199
-
200
- /**
201
- * One MCP content block a program may emit. The complete set, by design:
202
- * `resource` and `resource_link` are refused in ethos.md — pointers get
203
- * followed, and connecta serves no resources for them to point at.
204
- */
205
- export type EmittedBlock =
206
- | { type: "text"; text: string }
207
- | { type: "image"; data: string; mimeType: string }
208
- | { type: "audio"; data: string; mimeType: string };
209
-
210
- const EMIT_SHAPE_HINT =
211
- '{ type: "text", text } or { type: "image" | "audio", data (base64), mimeType }';
212
-
213
- /**
214
- * Strict M1 validation: required fields present and string-valued, nothing
215
- * else — no `annotations`, no `_meta`, no sugar forms. Rejected rather than
216
- * stripped, because silently deleting fields would deliver something the
217
- * program did not ask to emit.
218
- */
219
- function requireEmittedBlock(raw: unknown): EmittedBlock {
220
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
221
- throw new Error(
222
- `connecta.emit accepts exactly one content block: ${EMIT_SHAPE_HINT}`,
223
- );
224
- }
225
- const block = raw as Record<string, unknown>;
226
- const fields =
227
- block.type === "text"
228
- ? ["type", "text"]
229
- : block.type === "image" || block.type === "audio"
230
- ? ["type", "data", "mimeType"]
231
- : undefined;
232
- if (!fields) {
233
- throw new Error(
234
- `connecta.emit supports content types "text", "image", and "audio"; got ${JSON.stringify(block.type)}`,
235
- );
236
- }
237
- for (const field of fields) {
238
- if (typeof block[field] !== "string") {
239
- throw new Error(
240
- `connecta.emit block field "${field}" must be a string: ${EMIT_SHAPE_HINT}`,
241
- );
242
- }
243
- }
244
- const extra = Object.keys(block).filter((key) => !fields.includes(key));
245
- if (extra.length > 0) {
246
- throw new Error(
247
- `connecta.emit block carries unsupported field(s) ${extra.map((key) => JSON.stringify(key)).join(", ")}; a "${String(block.type)}" block is exactly { ${fields.join(", ")} }`,
248
- );
249
- }
250
- return raw as EmittedBlock;
251
- }
252
-
253
- const UI_SHAPE_HINT =
254
- "connecta.ui accepts exactly one HTML argument and, optionally, one read-binding options object";
255
-
256
- const MAX_UI_READ_BINDINGS = 32;
257
- const MAX_UI_VIEW_ARGS = 32;
258
- const UI_READ_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
259
- const FORBIDDEN_UI_KEY = new Set(["__proto__", "constructor", "prototype"]);
260
-
261
- interface UiReadBinding {
262
- address: string;
263
- fixedArgs: Record<string, unknown>;
264
- viewArgs: string[];
265
- }
266
-
267
- interface UiPayload {
268
- html: string;
269
- reads?: Record<string, UiReadBinding>;
270
- }
271
-
272
- /** What the argument was, named the way the emit validator names a bad field. */
273
- function describeUiArgument(raw: unknown): string {
274
- if (raw === null) return "null";
275
- if (raw === undefined) return "undefined";
276
- if (Array.isArray(raw)) return "an array";
277
- if (typeof raw === "string") return "an empty string";
278
- const kind = typeof raw;
279
- return `${/^[aeiou]/.test(kind) ? "an" : "a"} ${kind}`;
280
- }
281
-
282
- /**
283
- * Strict U1/V1 validation. The first argument remains HTML; the only second
284
- * argument is one read-binding manifest. There are no alternate object or MCP
285
- * block forms.
286
- */
287
- function requireUiHtml(raw: unknown): string {
288
- if (typeof raw !== "string" || raw.length === 0) {
289
- throw new Error(`${UI_SHAPE_HINT}; got ${describeUiArgument(raw)}`);
290
- }
291
- return raw;
292
- }
293
-
294
- function requireRecord(raw: unknown, label: string): Record<string, unknown> {
295
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
296
- throw new Error(`${label} must be an object`);
297
- }
298
- return raw as Record<string, unknown>;
299
- }
300
-
301
- function requireExactKeys(
302
- value: Record<string, unknown>,
303
- allowed: readonly string[],
304
- label: string,
305
- ): void {
306
- const extras = Object.keys(value).filter((key) => !allowed.includes(key));
307
- if (extras.length > 0) {
308
- throw new Error(
309
- `${label} carries unsupported field(s) ${extras.map((key) => JSON.stringify(key)).join(", ")}`,
310
- );
311
- }
312
- }
313
-
314
- function requireUiReadKey(raw: unknown, label: string): string {
315
- if (
316
- typeof raw !== "string" ||
317
- raw.length === 0 ||
318
- raw.length > 128 ||
319
- FORBIDDEN_UI_KEY.has(raw)
320
- ) {
321
- throw new Error(
322
- `${label} must be a non-empty string of at most 128 characters and cannot be __proto__, constructor, or prototype`,
323
- );
324
- }
325
- return raw;
326
- }
327
-
328
- function requireUiReads(raw: unknown): Record<string, UiReadBinding> {
329
- const record = requireRecord(raw, "connecta.ui options.reads");
330
- const names = Object.keys(record);
331
- if (names.length === 0 || names.length > MAX_UI_READ_BINDINGS) {
332
- throw new Error(
333
- `connecta.ui options.reads must contain from 1 through ${MAX_UI_READ_BINDINGS} named bindings`,
334
- );
335
- }
336
- const reads = Object.create(null) as Record<string, UiReadBinding>;
337
- for (const name of names) {
338
- if (!UI_READ_NAME.test(name) || FORBIDDEN_UI_KEY.has(name)) {
339
- throw new Error(
340
- `connecta.ui read binding name ${JSON.stringify(name)} must match ${UI_READ_NAME}`,
341
- );
342
- }
343
- const value = requireRecord(
344
- record[name],
345
- `connecta.ui read binding ${JSON.stringify(name)}`,
346
- );
347
- requireExactKeys(
348
- value,
349
- ["address", "fixedArgs", "viewArgs"],
350
- `connecta.ui read binding ${JSON.stringify(name)}`,
351
- );
352
- if (typeof value.address !== "string" || value.address.length === 0) {
353
- throw new Error(
354
- `connecta.ui read binding ${JSON.stringify(name)} address must be a non-empty string`,
355
- );
356
- }
357
- const fixedArgs =
358
- value.fixedArgs === undefined
359
- ? {}
360
- : requireRecord(
361
- value.fixedArgs,
362
- `connecta.ui read binding ${JSON.stringify(name)} fixedArgs`,
363
- );
364
- const rawViewArgs = value.viewArgs ?? [];
365
- if (!Array.isArray(rawViewArgs) || rawViewArgs.length > MAX_UI_VIEW_ARGS) {
366
- throw new Error(
367
- `connecta.ui read binding ${JSON.stringify(name)} viewArgs must be an array of at most ${MAX_UI_VIEW_ARGS} strings`,
368
- );
369
- }
370
- const viewArgs = rawViewArgs.map((key) =>
371
- requireUiReadKey(
372
- key,
373
- `connecta.ui read binding ${JSON.stringify(name)} viewArgs entry`,
374
- )
375
- );
376
- if (new Set(viewArgs).size !== viewArgs.length) {
377
- throw new Error(
378
- `connecta.ui read binding ${JSON.stringify(name)} viewArgs must not repeat a key`,
379
- );
380
- }
381
- for (const key of viewArgs) {
382
- if (Object.prototype.hasOwnProperty.call(fixedArgs, key)) {
383
- throw new Error(
384
- `connecta.ui read binding ${JSON.stringify(name)} view argument ${JSON.stringify(key)} cannot override a fixed argument`,
385
- );
386
- }
387
- }
388
- reads[name] = {
389
- address: value.address,
390
- fixedArgs,
391
- viewArgs,
392
- };
393
- }
394
- return reads;
395
- }
396
-
397
- function requireUiPayload(values: unknown[]): UiPayload {
398
- if (values.length !== 1 && values.length !== 2) {
399
- throw new Error(
400
- `${UI_SHAPE_HINT}; got ${values.length} arguments`,
401
- );
402
- }
403
- const html = requireUiHtml(values[0]);
404
- if (values.length === 1) return { html };
405
- const options = requireRecord(values[1], "connecta.ui options");
406
- requireExactKeys(options, ["reads"], "connecta.ui options");
407
- if (!Object.prototype.hasOwnProperty.call(options, "reads")) {
408
- throw new Error("connecta.ui options must contain reads");
409
- }
410
- return { html, reads: requireUiReads(options.reads) };
411
- }
412
-
413
- /**
414
- * Request-local collection for `connecta.emit` and `connecta.ui`. Budgets fail
415
- * loudly at the crossing call — nothing is partially accepted and prior blocks
416
- * are unaffected — so a program learns it is over budget while it can still
417
- * choose differently (M5, U4). Accepted output never rides `ExecuteResult`; the
418
- * handler that owns this collector delivers it on the final tool result.
419
- *
420
- * The two channels share the byte aggregate and nothing else: a UI payload is
421
- * not a block, so it spends no block count, and at most one is ever accepted.
422
- */
423
- export class EmitCollector {
424
- readonly blocks: EmittedBlock[] = [];
425
- /** The shared transport aggregate: emitted blocks plus the UI payload. */
426
- bytes = 0;
427
- /** The one accepted UI payload (U2), delivered in result `_meta` on success. */
428
- ui?: UiPayload;
429
- /** What the blocks alone cost, so the `emitted` aggregate stays a true pair. */
430
- private blockBytes = 0;
431
- constructor(
432
- private readonly maxBytes: number,
433
- private readonly maxBlocks: number,
434
- private readonly diagnostics?: ExecuteDiagnostics,
435
- ) {}
436
-
437
- accept(raw: unknown): void {
438
- const block = requireEmittedBlock(raw);
439
- if (this.blocks.length >= this.maxBlocks) {
440
- throw new Error(
441
- `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 remaining`,
442
- );
443
- }
444
- const size = diagnosticsEncoder.encode(JSON.stringify(block)).byteLength;
445
- if (this.bytes + size > this.maxBytes) {
446
- throw new Error(
447
- `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`,
448
- );
449
- }
450
- this.blocks.push(block);
451
- this.bytes += size;
452
- this.blockBytes += size;
453
- this.diagnostics?.recordEmitted(this.blocks.length, this.blockBytes);
454
- }
455
-
456
- /**
457
- * U2 and U4: one payload per run, measured as the serialized bytes of
458
- * `{ html }` against the same aggregate emit spends. A second call throws
459
- * naming the constraint rather than replacing the first — one tool result
460
- * renders one view, and last-wins would silently discard a payload the
461
- * program deliberately supplied.
462
- *
463
- * Multiplicity is checked before shape, so the second call is told what it
464
- * actually broke. A program whose second payload is also malformed has one
465
- * problem worth naming — that there is a second payload at all — and a
466
- * complaint about its type would send the author to fix the wrong thing.
467
- */
468
- acceptUi(...values: unknown[]): void {
469
- if (this.ui) {
470
- throw new Error(
471
- "connecta.ui accepts at most one payload per run: a view was already accepted and stands",
472
- );
473
- }
474
- this.acceptUiPayload(requireUiPayload(values));
475
- }
476
-
477
- acceptUiPayload(payload: UiPayload): void {
478
- if (this.ui) {
479
- throw new Error(
480
- "connecta.ui accepts at most one payload per run: a view was already accepted and stands",
481
- );
482
- }
483
- let serialized: string;
484
- try {
485
- serialized = JSON.stringify(payload);
486
- } catch {
487
- throw new Error("connecta.ui payload must be JSON-serializable");
488
- }
489
- const size = diagnosticsEncoder.encode(serialized).byteLength;
490
- if (this.bytes + size > this.maxBytes) {
491
- throw new Error(
492
- `connecta.ui byte budget exceeded: payload is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`,
493
- );
494
- }
495
- this.ui = payload;
496
- this.bytes += size;
497
- this.diagnostics?.recordUi(size);
498
- }
499
- }
500
-
501
- /** A configured emit budget must be a finite number >= 1; anything else falls back. */
502
- function resolveEmitBudget(
503
- value: number | undefined,
504
- fallback: number,
505
- ): number {
506
- return typeof value === "number" && Number.isFinite(value) && value >= 1
507
- ? Math.trunc(value)
508
- : fallback;
509
- }
510
-
511
- function serializedDiagnosticBytes(value: unknown): number {
512
- try {
513
- const text = JSON.stringify(value);
514
- return text === undefined
515
- ? 0
516
- : diagnosticsEncoder.encode(text).byteLength;
517
- } catch {
518
- // The executor's normal result guard owns the error. Diagnostics must
519
- // never turn measurement into a second failure path.
520
- return 0;
521
- }
522
- }
523
-
524
- // deno-fmt-ignore
525
- const RESERVED = new Set([
526
- "break",
527
- "case",
528
- "catch",
529
- "class",
530
- "const",
531
- "continue",
532
- "debugger",
533
- "default",
534
- "delete",
535
- "do",
536
- "else",
537
- "enum",
538
- "export",
539
- "extends",
540
- "false",
541
- "finally",
542
- "for",
543
- "function",
544
- "if",
545
- "import",
546
- "in",
547
- "instanceof",
548
- "let",
549
- "new",
550
- "null",
551
- "return",
552
- "static",
553
- "super",
554
- "switch",
555
- "this",
556
- "throw",
557
- "true",
558
- "try",
559
- "typeof",
560
- "var",
561
- "void",
562
- "while",
563
- "with",
564
- "yield",
565
- "await",
566
- "async",
567
- ]);
568
-
569
- /** Convert a connector/tool name into a valid JS identifier. */
570
- export function sanitizeIdentifier(name: string): string {
571
- let id = name.replace(/[^A-Za-z0-9_$]/g, "_");
572
- if (/^[0-9]/.test(id)) id = `_${id}`;
573
- if (RESERVED.has(id)) id = `${id}_`;
574
- return id;
575
- }
576
-
577
- const SANDBOX_RESERVED_NAMES = new Set([
578
- "connecta",
579
- "console",
580
- "arguments",
581
- "result",
582
- "undefined",
583
- "setTimeout",
584
- "Promise",
585
- "Error",
586
- "WorkerEntrypoint",
587
- "CodeExecutor",
588
- "__invoke",
589
- "__namespace",
590
- "__call",
591
- "__log",
592
- "__dispatchers",
593
- "__connectors",
594
- "__logs",
595
- "__CODEMODE_BINARY_TAG",
596
- "__bytesToBase64",
597
- "__base64ToBytes",
598
- "__encodeCodemodeValue",
599
- "__decodeCodemodeValue",
600
- "__stringifyForCodemode",
601
- "__parseForCodemode",
602
- ]);
603
-
604
- function msg(err: unknown): string {
605
- return err instanceof Error ? err.message : String(err);
606
- }
607
-
608
- function lazyNamespacePrelude(
609
- connectors: Array<{ id: string; namespace: string }>,
610
- ): string {
611
- const declarations = connectors
612
- .map(
613
- ({ id, namespace }) =>
614
- `globalThis[${JSON.stringify(namespace)}] = __makeConnectaNamespace(${JSON.stringify(id)});`,
615
- )
616
- .join("\n");
617
- return `(() => {
618
- const __makeConnectaNamespace = (connectorId) => Object.freeze(new Proxy(Object.create(null), {
619
- get: (_target, toolName) => typeof toolName === "string"
620
- ? (args) => connecta.__callNamespace(connectorId, toolName, args)
621
- : undefined
622
- }));
623
- ${declarations}
624
- })();`;
625
- }
626
-
627
- /**
628
- * Expose one fixed host provider plus trusted sandbox setup that creates a
629
- * lazy proxy global per connector. No connector catalog is touched until code
630
- * calls that namespace or explicitly asks search/describe.
631
- */
632
- export async function buildSandboxProviders(
633
- registry: RegistryView,
634
- baseUrl: string,
635
- logger: Logger,
636
- activity?: ActivityRequestContext,
637
- limits: {
638
- signal?: AbortSignal;
639
- maxHostCalls?: number;
640
- hostCallTimeoutMs?: number;
641
- discoveryConcurrency?: number;
642
- /** Per-connector deadline for in-program catalog probes. Default 30_000. */
643
- probeTimeoutMs?: number;
644
- onInvocationFailure?: (failure: InvocationFailure) => void;
645
- diagnostics?: ExecuteDiagnostics;
646
- /**
647
- * Where `connecta.emit` collects. The handler that will deliver the
648
- * blocks owns it; without one, emit fails loudly rather than accept
649
- * blocks nobody will ever return.
650
- */
651
- emitCollector?: EmitCollector;
652
- } = {},
653
- ): Promise<ExecutorProvider[]> {
654
- // All host calls made by one execute_code invocation share a downstream
655
- // connection, while a later invocation receives a fresh request scope.
656
- const requestScope = {};
657
- const catalog = new CatalogService(registry, baseUrl, {
658
- requestScope,
659
- // A program that just missed an address cannot call search_tools.
660
- searchRoute: "connecta.search",
661
- ...(limits.discoveryConcurrency !== undefined
662
- ? { concurrency: limits.discoveryConcurrency }
663
- : {}),
664
- ...(limits.probeTimeoutMs !== undefined
665
- ? { probeTimeoutMs: limits.probeTimeoutMs }
666
- : {}),
667
- });
668
- const invocation = new InvocationService(registry, catalog, activity);
669
- const maxHostCalls = Math.max(
670
- 1,
671
- Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS),
672
- );
673
- const hostCallTimeoutMs = Math.max(
674
- 1,
675
- Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS),
676
- );
677
- let hostCalls = 0;
678
- const connectors = registry.listConnectors();
679
- const namespaces: Array<{ id: string; namespace: string }> = [];
680
- const namespaceOwners = new Map<string, string>();
681
- for (const connector of connectors) {
682
- const namespace = sanitizeIdentifier(connector.id);
683
- const owner = namespaceOwners.get(namespace);
684
- const problem = SANDBOX_RESERVED_NAMES.has(namespace)
685
- ? `Connector "${connector.id}" sanitizes to reserved execute_code namespace "${namespace}". Rename or exclude the connector before using execute_code.`
686
- : owner
687
- ? `Connector ids "${owner}" and "${connector.id}" both sanitize to execute_code namespace "${namespace}". Rename or exclude one connector before using execute_code.`
688
- : undefined;
689
- if (problem) {
690
- logger.warn(`[connecta] execute_code: ${problem}`);
691
- throw new Error(problem);
692
- }
693
- namespaceOwners.set(namespace, connector.id);
694
- namespaces.push({ id: connector.id, namespace });
695
- }
696
-
697
- const invocationContext = () => ({
698
- source: "execute_code" as const,
699
- timeoutMs: hostCallTimeoutMs,
700
- ...(limits.signal !== undefined ? { requestSignal: limits.signal } : {}),
701
- unwrapResult: true,
702
- beforeDispatch: () => {
703
- hostCalls++;
704
- if (hostCalls > maxHostCalls) {
705
- throw new Error(
706
- `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`,
707
- );
708
- }
709
- },
710
- });
711
- /**
712
- * A discovery bound is as typed a failure as a tool call is, and a program
713
- * that lets one escape deserves the same envelope: register it on the same
714
- * request-local channel so an unhandled `invalid_args`/`result_too_large`
715
- * reaches the model with its code instead of as prose. The guest still sees
716
- * only the message — that is the bridge's limit, not a policy.
717
- */
718
- const typedDiscovery = async <T>(operation: () => Promise<T>): Promise<T> => {
719
- try {
720
- return await operation();
721
- } catch (err) {
722
- if (err instanceof DiscoveryPolicyError) {
723
- limits.onInvocationFailure?.(
724
- new InvocationFailure({
725
- code: err.code,
726
- message: err.message,
727
- retryable: false,
728
- }),
729
- );
730
- }
731
- throw err;
732
- }
733
- };
734
- const callAddress = async (
735
- address: unknown,
736
- args: unknown,
737
- diagnosticOperation: "call" | "batch" = "call",
738
- ) => {
739
- const outcome = await invocation.invoke(
740
- String(address),
741
- args ?? {},
742
- invocationContext(),
743
- );
744
- limits.diagnostics?.recordCall(diagnosticOperation, outcome);
745
- if (!outcome.ok) {
746
- const failure = new InvocationFailure(outcome.error);
747
- limits.onInvocationFailure?.(failure);
748
- throw failure;
749
- }
750
- return outcome.value;
751
- };
752
- const callNamespace = async (
753
- connectorId: unknown,
754
- toolAlias: unknown,
755
- args: unknown,
756
- ) => {
757
- const outcome = await invocation.invokeToolAlias(
758
- String(connectorId),
759
- String(toolAlias),
760
- sanitizeIdentifier,
761
- args ?? {},
762
- invocationContext(),
763
- );
764
- limits.diagnostics?.recordCall("call", outcome);
765
- if (!outcome.ok) {
766
- const failure = new InvocationFailure(outcome.error);
767
- limits.onInvocationFailure?.(failure);
768
- throw failure;
769
- }
770
- return outcome.value;
771
- };
772
-
773
- /**
774
- * A read binding is admitted while the program still owns the request. The
775
- * shell later calls the ordinary `call_tool`, which repeats this same
776
- * fail-closed check against the then-current catalog; validating here keeps
777
- * a typo or destructive address from producing a view whose controls can
778
- * never work, while validation at use keeps a stale view from retaining old
779
- * authority.
780
- */
781
- const validateUiReads = async (payload: UiPayload): Promise<UiPayload> => {
782
- if (!payload.reads) return payload;
783
- const reads = Object.create(null) as Record<string, UiReadBinding>;
784
- for (const [name, binding] of Object.entries(payload.reads)) {
785
- const resolution = await catalog.resolveTool(
786
- binding.address,
787
- limits.signal !== undefined ? { signal: limits.signal } : {},
788
- );
789
- if (!resolution.ok) {
790
- throw new Error(
791
- `connecta.ui read binding ${JSON.stringify(name)} could not resolve ${JSON.stringify(binding.address)}: ${resolution.error.message}`,
792
- );
793
- }
794
- if (!isExplicitlyReadOnly(resolution.resolved.definition)) {
795
- throw new Error(
796
- `connecta.ui read binding ${JSON.stringify(name)} refuses ${JSON.stringify(binding.address)}: the tool is not explicitly read-only`,
797
- );
798
- }
799
- reads[name] = {
800
- ...binding,
801
- address:
802
- `${resolution.resolved.connector.id}.${resolution.resolved.toolName}`,
803
- };
804
- }
805
- return { html: payload.html, reads };
806
- };
807
-
808
- return [
809
- {
810
- name: "connecta",
811
- prelude: lazyNamespacePrelude(namespaces),
812
- fns: {
813
- __callNamespace: callNamespace,
814
- call: (address: unknown, args: unknown) =>
815
- callAddress(address, args),
816
- // Emission is a provider function, never an ExecuteResult field —
817
- // that is what keeps the Executor contract untouched and parity
818
- // structural (M8). It spends no host-call budget (M7); its own
819
- // budgets live in the collector.
820
- emit: async (block: unknown) => {
821
- if (!limits.emitCollector) {
822
- throw new Error(
823
- "connecta.emit is unavailable: no emission collector was configured for this execution",
824
- );
825
- }
826
- limits.emitCollector.accept(block);
827
- },
828
- // The rendered-output channel rides the same bridge for the same
829
- // reason (U7): one more provider fn, no change to ExecuteResult or
830
- // the Executor contract. Delivery is the handler's job, not the
831
- // guest's — nothing here becomes addressable.
832
- ui: async (...values: unknown[]) => {
833
- if (!limits.emitCollector) {
834
- throw new Error(
835
- "connecta.ui is unavailable: no emission collector was configured for this execution",
836
- );
837
- }
838
- const payload = await validateUiReads(requireUiPayload(values));
839
- limits.emitCollector.acceptUiPayload(payload);
840
- },
841
- batch: async (calls: unknown) => {
842
- const started = Date.now();
843
- const callCount = Array.isArray(calls) ? calls.length : 0;
844
- try {
845
- if (!Array.isArray(calls)) throw new Error("calls must be an array");
846
- if (calls.length > EXECUTE_MAX_BATCH_CALLS) {
847
- throw new Error(
848
- `connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS} calls`,
849
- );
850
- }
851
- const result = await Promise.all(
852
- calls.map(async (call) => {
853
- const item = call as { address?: unknown; args?: unknown };
854
- try {
855
- return {
856
- address: String(item.address),
857
- ok: true,
858
- data: await callAddress(
859
- item.address,
860
- item.args,
861
- "batch",
862
- ),
863
- };
864
- } catch (err) {
865
- // The failure shape connecta.batch reports: the message a
866
- // program can log, plus the typed details it must classify by. A
867
- // thrown host error crosses the sandbox bridge as a bare
868
- // message string in every executor, so this is the one place a
869
- // program can tell a policy refusal from a transient failure.
870
- const details =
871
- err instanceof InvocationFailure
872
- ? err.details
873
- : classifyCallError(err, "batch_call_failed");
874
- return {
875
- address: String(item.address),
876
- ok: false,
877
- error: details.message,
878
- errorDetails: details,
879
- };
880
- }
881
- }),
882
- );
883
- limits.diagnostics?.recordBatch(
884
- Date.now() - started,
885
- true,
886
- callCount,
887
- result,
888
- );
889
- return result;
890
- } catch (err) {
891
- limits.diagnostics?.recordBatch(
892
- Date.now() - started,
893
- false,
894
- callCount,
895
- );
896
- throw err;
897
- }
898
- },
899
- search: async (raw: unknown) => {
900
- const started = Date.now();
901
- try {
902
- const result = await typedDiscovery(async () => {
903
- const args = (raw ?? {}) as {
904
- query?: string;
905
- connector?: string;
906
- safety?: "readOnly" | "approvalRequired" | "all";
907
- limit?: number;
908
- offset?: number;
909
- fullDescriptions?: boolean;
910
- includeSchemas?: "compact" | "json";
911
- includeSchemaKeys?: boolean;
912
- };
913
- const result = flatSearchResult(
914
- await catalog.search({
915
- ...args,
916
- // Key metadata rides along with schemas by default, since that
917
- // is the whole point of it in code mode. It stays opt-out
918
- // because it counts against the same discovery-byte ceiling.
919
- includeSchemaKeys: args.includeSchemaKeys !== false,
920
- }),
921
- );
922
- boundedDiscoveryText(
923
- result,
924
- "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.",
925
- );
926
- return result;
927
- });
928
- limits.diagnostics?.recordCatalog(
929
- "search",
930
- Date.now() - started,
931
- true,
932
- result,
933
- );
934
- return result;
935
- } catch (err) {
936
- limits.diagnostics?.recordCatalog(
937
- "search",
938
- Date.now() - started,
939
- false,
940
- );
941
- throw err;
942
- }
943
- },
944
- describe: async (raw: unknown) => {
945
- const started = Date.now();
946
- try {
947
- const result = await typedDiscovery(async () => {
948
- const args = (raw ?? {}) as {
949
- address?: unknown;
950
- addresses?: unknown;
951
- format?: "compact" | "json";
952
- fullDescriptions?: boolean;
953
- };
954
- const result = { tools: await catalog.describe(args) };
955
- boundedDiscoveryText(
956
- result,
957
- 'Split the address list or use format: "compact".',
958
- );
959
- return result;
960
- });
961
- limits.diagnostics?.recordCatalog(
962
- "describe",
963
- Date.now() - started,
964
- true,
965
- result,
966
- );
967
- return result;
968
- } catch (err) {
969
- limits.diagnostics?.recordCatalog(
970
- "describe",
971
- Date.now() - started,
972
- false,
973
- );
974
- throw err;
975
- }
976
- },
977
- },
978
- },
979
- ];
980
- }
981
-
982
- /** The execute_code handler. Exported for direct testing. */
983
- export function createExecuteTool(
984
- registry: RegistryView,
985
- baseUrl: string,
986
- executor: Executor,
987
- logger: Logger,
988
- activity?: ActivityRequestContext,
989
- config: {
990
- discoveryConcurrency?: number;
991
- probeTimeoutMs?: number;
992
- maxEmittedBytes?: number;
993
- maxEmittedBlocks?: number;
994
- } = {},
995
- ) {
996
- return async (
997
- { code, diagnostics: diagnosticsRequested }: {
998
- code: string;
999
- diagnostics?: boolean;
1000
- },
1001
- options: { signal?: AbortSignal } = {},
1002
- ): Promise<ToolResult> => {
1003
- const controller = new AbortController();
1004
- const forwardAbort = () => controller.abort(options.signal?.reason);
1005
- if (options.signal?.aborted) forwardAbort();
1006
- else {
1007
- options.signal?.addEventListener("abort", forwardAbort, { once: true });
1008
- }
1009
- let lease;
1010
- let outcome;
1011
- const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
1012
- const emitted = new EmitCollector(
1013
- resolveEmitBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES),
1014
- resolveEmitBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS),
1015
- diagnostics,
1016
- );
1017
- const invocationFailures: InvocationFailure[] = [];
1018
- try {
1019
- // Admission comes before provider construction: queued calls retain no
1020
- // catalogs, request scopes, or one-closure-per-tool provider arrays.
1021
- if (isAdmittingExecutor(executor)) {
1022
- const admissionStarted = Date.now();
1023
- try {
1024
- lease = await executor.acquire({ signal: controller.signal });
1025
- } finally {
1026
- if (diagnostics) {
1027
- diagnostics.admissionMs = Date.now() - admissionStarted;
1028
- }
1029
- }
1030
- if ((lease.waitMs ?? 0) > 0) {
1031
- logger.debug("[connecta] execute_code admitted after queue wait", {
1032
- waitMs: lease.waitMs,
1033
- });
1034
- }
1035
- }
1036
- const setupStarted = Date.now();
1037
- let providers: ExecutorProvider[];
1038
- try {
1039
- providers = await buildSandboxProviders(
1040
- registry,
1041
- baseUrl,
1042
- logger,
1043
- activity,
1044
- {
1045
- signal: controller.signal,
1046
- onInvocationFailure: (failure) => {
1047
- invocationFailures.push(failure);
1048
- },
1049
- emitCollector: emitted,
1050
- ...(diagnostics ? { diagnostics } : {}),
1051
- ...(config.discoveryConcurrency !== undefined
1052
- ? { discoveryConcurrency: config.discoveryConcurrency }
1053
- : {}),
1054
- ...(config.probeTimeoutMs !== undefined
1055
- ? { probeTimeoutMs: config.probeTimeoutMs }
1056
- : {}),
1057
- },
1058
- );
1059
- } finally {
1060
- if (diagnostics) diagnostics.setupMs = Date.now() - setupStarted;
1061
- }
1062
- if (controller.signal.aborted) {
1063
- throw new ExecutorAdmissionError(
1064
- "executor_cancelled",
1065
- "Execution was cancelled during sandbox setup.",
1066
- );
1067
- }
1068
- const executorStarted = Date.now();
1069
- try {
1070
- outcome = lease
1071
- ? await lease.execute(code, providers)
1072
- : await executor.execute(code, providers);
1073
- } finally {
1074
- if (diagnostics) {
1075
- diagnostics.executorWallMs = Date.now() - executorStarted;
1076
- }
1077
- }
1078
- } catch (err) {
1079
- if (err instanceof ExecutorAdmissionError) {
1080
- if (err.code === "executor_overloaded") {
1081
- logger.warn("[connecta] execute_code admission rejected", {
1082
- code: err.code,
1083
- retryAfterMs: err.retryAfterMs,
1084
- });
1085
- }
1086
- const result = jsonResult({
1087
- error: {
1088
- code: err.code,
1089
- message: err.message,
1090
- retryable: err.retryable,
1091
- ...(err.retryAfterMs !== undefined
1092
- ? { retryAfterMs: err.retryAfterMs }
1093
- : {}),
1094
- },
1095
- ...(err instanceof ExecutorExecutionError
1096
- ? discardedEmits(emitted)
1097
- : {}),
1098
- ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
1099
- });
1100
- result.isError = true;
1101
- return result;
1102
- }
1103
- if (diagnostics) {
1104
- const result = jsonResult({
1105
- error: {
1106
- code: "executor_failed",
1107
- message: `Executor failed: ${msg(err)}`,
1108
- retryable: false,
1109
- },
1110
- ...discardedEmits(emitted),
1111
- diagnostics: diagnostics.finish(),
1112
- });
1113
- result.isError = true;
1114
- return result;
1115
- }
1116
- return errorResult(
1117
- `Executor failed: ${msg(err)}${discardedEmitsText(emitted)}`,
1118
- );
1119
- } finally {
1120
- // A sandbox timeout or early return must also release any outstanding
1121
- // host waits and signal cooperative connectors to stop their work.
1122
- controller.abort();
1123
- lease?.release();
1124
- options.signal?.removeEventListener("abort", forwardAbort);
1125
- }
1126
- const logs =
1127
- outcome.logs && outcome.logs.length > 0
1128
- ? truncateExecuteText(
1129
- outcome.logs.join("\n"),
1130
- MAX_EXECUTE_LOG_CHARS,
1131
- )
1132
- : undefined;
1133
- if (outcome.error) {
1134
- // Executor bridges necessarily reduce thrown host errors to strings.
1135
- // Match that terminal string back to the request-local typed failure so
1136
- // an unhandled tool failure keeps the same structured contract as
1137
- // call_tool. Failures caught by model code never reach
1138
- // outcome.error and therefore remain under that code's control.
1139
- //
1140
- // An error the program let through unchanged matches exactly, and an
1141
- // exact match always wins: a program that wrapped one failure's message
1142
- // around another's must not have the wrong type attached. Containment is
1143
- // the fallback, so a wrapped message still reports its underlying type
1144
- // rather than losing it to prose.
1145
- let invocationFailure: InvocationFailure | undefined;
1146
- for (const match of [
1147
- (candidate: InvocationFailure) => outcome.error === candidate.message,
1148
- (candidate: InvocationFailure) =>
1149
- outcome.error?.includes(candidate.message) === true,
1150
- ]) {
1151
- for (let i = invocationFailures.length - 1; i >= 0; i--) {
1152
- const candidate = invocationFailures[i];
1153
- if (candidate && match(candidate)) {
1154
- invocationFailure = candidate;
1155
- break;
1156
- }
1157
- }
1158
- if (invocationFailure) break;
1159
- }
1160
- if (invocationFailure) {
1161
- // Handed back whole, with no size guard of its own — the failure was
1162
- // framed with bounded caller text (`boundedEchoText`) precisely so
1163
- // this path never needs one. Adding a cap here instead would leave the
1164
- // top-level surfaces, which have the same amplification, uncovered.
1165
- const result = jsonResult({
1166
- error: invocationFailure.details,
1167
- ...(logs ? { logs } : {}),
1168
- ...discardedEmits(emitted),
1169
- ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
1170
- });
1171
- result.isError = true;
1172
- return result;
1173
- }
1174
- const message = `Error: ${outcome.error}`;
1175
- if (diagnostics) {
1176
- const result = jsonResult({
1177
- error: {
1178
- code: "executor_failed",
1179
- message,
1180
- retryable: false,
1181
- },
1182
- ...(logs ? { logs } : {}),
1183
- ...discardedEmits(emitted),
1184
- diagnostics: diagnostics.finish(),
1185
- });
1186
- result.isError = true;
1187
- return result;
1188
- }
1189
- return errorResult(
1190
- `${message}${logs ? `\n\nLogs:\n${logs}` : ""}${discardedEmitsText(emitted)}`,
1191
- );
1192
- }
1193
- // A result crossing back as a host BigInt (or otherwise unserializable
1194
- // value) makes JSON.stringify throw — keep that inside the structured
1195
- // error path so captured logs survive instead of a raw SDK 500.
1196
- let result: unknown;
1197
- try {
1198
- result = guardExecuteResultValue(outcome.result);
1199
- } catch (err) {
1200
- const message = `Error: result is not JSON-serializable: ${msg(err)}`;
1201
- if (diagnostics) {
1202
- const response = jsonResult({
1203
- error: {
1204
- code: "executor_failed",
1205
- message,
1206
- retryable: false,
1207
- },
1208
- ...(logs ? { logs } : {}),
1209
- ...discardedEmits(emitted),
1210
- diagnostics: diagnostics.finish(),
1211
- });
1212
- response.isError = true;
1213
- return response;
1214
- }
1215
- return errorResult(
1216
- `${message}${logs ? `\n\nLogs:\n${logs}` : ""}${discardedEmitsText(emitted)}`,
1217
- );
1218
- }
1219
- const response = jsonResult({
1220
- result,
1221
- ...(emitted.blocks.length > 0 ? { emitted: emitted.blocks.length } : {}),
1222
- // U3: the model learns a view rendered without seeing its bytes. Spread
1223
- // conditionally so a program that never called connecta.ui produces
1224
- // today's byte-for-byte response.
1225
- ...(emitted.ui ? { ui: true } : {}),
1226
- ...(logs ? { logs } : {}),
1227
- ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
1228
- });
1229
- if (emitted.ui) {
1230
- // _meta is where the Apps spec's best practices put data "not intended
1231
- // for model context", and how shipped hosts behave. The shell reads
1232
- // exactly this key out of the tool result the host delivers to it.
1233
- response._meta = { [PROGRAM_UI_META_KEY]: emitted.ui };
1234
- }
1235
- if (emitted.blocks.length > 0) {
1236
- // Emitted image/audio blocks are valid MCP content that ToolResult's
1237
- // text-only typing does not model — the same acknowledged gap
1238
- // guardContent lives with for downstream block passthrough.
1239
- response.content.push(
1240
- ...(emitted.blocks as unknown as typeof response.content),
1241
- );
1242
- }
1243
- return response;
1244
- };
1245
- }
1246
-
1247
- /**
1248
- * M4 and U3: a failed program delivers no blocks and no view, but each
1249
- * discard is visible — and one failure can discard both.
1250
- */
1251
- function discardedEmits(emitted: EmitCollector): {
1252
- emittedDiscarded?: number;
1253
- uiDiscarded?: true;
1254
- } {
1255
- return {
1256
- ...(emitted.blocks.length > 0
1257
- ? { emittedDiscarded: emitted.blocks.length }
1258
- : {}),
1259
- ...(emitted.ui ? { uiDiscarded: true as const } : {}),
1260
- };
1261
- }
1262
-
1263
- /** The same visibility for the plain-text error paths. */
1264
- function discardedEmitsText(emitted: EmitCollector): string {
1265
- const lines = [
1266
- ...(emitted.blocks.length > 0
1267
- ? [`emittedDiscarded: ${emitted.blocks.length}`]
1268
- : []),
1269
- ...(emitted.ui ? ["uiDiscarded: true"] : []),
1270
- ];
1271
- return lines.length > 0 ? `\n\n${lines.join("\n")}` : "";
1272
- }
1273
-
1274
- const executeDescription = (
1275
- emitBudgets: { maxBytes: number; maxBlocks: number },
1276
- connectorGuides: boolean,
1277
- ) => `Choose the route before discovery. Exactly one unknown-address read uses top-level search_tools then call_tool; a known address uses call_tool directly. This is the primary surface for everything wider. For any reduction, dependency, multiple calls, loop, join, or branch, make exactly one execute_code call that searches, selects, calls, and reduces. A discovery-only program wastes its round trip: finish here, don't return catalog matches for a later call. Only readOnlyHint: true tools are available. Limits: ${EXECUTE_MAX_HOST_CALLS} host calls per run, ${EXECUTE_MAX_BATCH_CALLS} per batch, ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}-second host deadline.
1278
-
1279
- Write an async arrow function. NO network, filesystem, timers, or imports; only:
1280
- - Connector globals call <connectorId>.<toolName>(args). Sanitization: non-[A-Za-z0-9_$] → "_" (my-service.get.thing → my_service.get_thing); prefix a leading digit; suffix a reserved word.
1281
- - connecta.call(address, args) and connecta.batch(calls) use canonical addresses. Every batch entry is { address, ok: true, data } or { address, ok: false, error, errorDetails: { code, retryable } }; destructure it.
1282
- - top-level search_tools returns { connectors: [{ id, tools }], total, offset, limit, hasMore }; connecta.search returns { tools, total, offset, limit, hasMore }; connecta.describe returns { tools }.
1283
- - connecta.search(args) loads catalogs and must be followed by selection and calls in this program; set connector to the obvious id to load one, otherwise it loads all. For distinct operations, make separate short searches here. Require address/description to match the operation, then check requiredInputKeys, truncation, safety, and outputs; never take the first lexical or merely input-compatible match. Select by fit; do not require it to be the only match. Missing outputKeys means inspect outputSchema, not discard the candidate. Every required key needs task/prior-result data; do not prefer zero required keys. Put every requiredInputKey in call args. For dependencies, match an earlier outputKey to the later requiredInputKey. [] means no required keys, not permission to invent args. Describe only a truncated/insufficient compact shape. Reducers use declared outputKeys, never guessed items/results roots. connecta.describe takes { address: "<connectorId>.<toolName>" } or { addresses: [...] }. Use safety: "readOnly" to avoid advertising calls this sandbox cannot execute; it changes results, not authority. Missing key list = non-object, not no fields; read the schema.${connectorGuides ? " guideRequired: true = stop. Describe clears only schema_truncated; otherwise return its exact guide, fetch with top-level skills, then write the informed call." : ""}
1284
- - connecta.emit(block) — emit exactly { type: "text", text } or { type: "image" | "audio", data (base64), mimeType }. Success-only, no host call, ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes; invalid or over-budget throws before accepting.
1285
- - connecta.ui(html, options?) — one success-only view; one arg is display-only. Reads declare { reads: { name: { address, fixedArgs?, viewArgs? } } }; markup calls connecta.read(name, args). Admission applies. It shares the ${emitBudgets.maxBytes}-byte emit budget: one budget, not two; a second, over-budget, or invalid call throws catchably. Bytes stay out; the model reads the return value, not the view; return the initial summary from its variables.
1286
- - console.log(...) — captured and returned with the result.
1287
-
1288
- Dependent example (only when the second call requires a value returned by the first): async () => { const { tools } = await connecta.search({ query: "pipeline run job logs", safety: "readOnly", includeSchemas: "compact" }); const pick = (suffix) => { const match = tools.find((t) => t.address.endsWith(suffix)); if (!match) throw new Error("no tool for " + suffix); return match.address; }; const run = await connecta.call(pick(".get_run"), { runId: 42 }); const logs = await connecta.call(pick(".get_job_logs"), { jobId: run.failedJobId }); return [run, logs]; }
1289
-
1290
- Calls return plain values (JSON-parsing MCP text when possible) and throw; catch errors. A thrown error is only a message; connecta.batch tells a policy refusal from a transient failure. Never retry retryable: false, or rate_limited immediately; no timers. Return JSON; reduce large results before they truncate.
1291
-
1292
- Plain JS, no TypeScript. Compact schemas are TypeScript-like, not JSON Schema: write the property names they display; never guess positions or aliases.`;
1293
-
1294
- /** Register the execute_code meta-tool. Only called when an executor is configured. */
1295
- export function registerExecuteTool(
1296
- server: McpServer,
1297
- registry: RegistryView,
1298
- ctx: {
1299
- baseUrl: string;
1300
- executor: Executor;
1301
- logger: Logger;
1302
- activity?: ActivityRequestContext;
1303
- requestSignal?: AbortSignal;
1304
- discoveryConcurrency?: number;
1305
- /**
1306
- * The deployment's configured per-connector probe deadline. Programs probe
1307
- * the same downstream catalogs the top-level tools do, so an operator who
1308
- * tightened `discovery.probeTimeoutMs` gets it honored inside the sandbox
1309
- * too rather than silently falling back to the 30s default.
1310
- */
1311
- probeTimeoutMs?: number;
1312
- /** Aggregate serialized-byte budget for connecta.emit. Default 4_000_000. */
1313
- maxEmittedBytes?: number;
1314
- /** Block-count budget for connecta.emit. Default 32. */
1315
- maxEmittedBlocks?: number;
1316
- },
1317
- ): void {
1318
- // Resolved once so the description and the collector cannot disagree about
1319
- // the budgets this deployment actually enforces.
1320
- const emitBudgets = {
1321
- maxBytes: resolveEmitBudget(ctx.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES),
1322
- maxBlocks: resolveEmitBudget(
1323
- ctx.maxEmittedBlocks,
1324
- EXECUTE_MAX_EMITTED_BLOCKS,
1325
- ),
1326
- };
1327
- const handler = createExecuteTool(
1328
- registry,
1329
- ctx.baseUrl,
1330
- ctx.executor,
1331
- ctx.logger,
1332
- ctx.activity,
1333
- {
1334
- ...(ctx.discoveryConcurrency !== undefined
1335
- ? { discoveryConcurrency: ctx.discoveryConcurrency }
1336
- : {}),
1337
- ...(ctx.probeTimeoutMs !== undefined
1338
- ? { probeTimeoutMs: ctx.probeTimeoutMs }
1339
- : {}),
1340
- maxEmittedBytes: emitBudgets.maxBytes,
1341
- maxEmittedBlocks: emitBudgets.maxBlocks,
1342
- },
1343
- );
1344
- server.registerTool(
1345
- "execute_code",
1346
- {
1347
- description: executeDescription(
1348
- emitBudgets,
1349
- hasConnectorGuides(registry.listConnectors()),
1350
- ),
1351
- inputSchema: z.object({
1352
- code: z
1353
- .string()
1354
- .describe(
1355
- "One complete JavaScript async arrow function. Consume search/describe results and finish the task inside it; returning catalog data for a later call spends a round trip and buys nothing. So does aborting on a missing tool match or result key — re-search, describe, or read the result's actual keys here instead.",
1356
- ),
1357
- diagnostics: z
1358
- .boolean()
1359
- .optional()
1360
- .describe(
1361
- "Add request-local, payload-free timing and result-size summaries.",
1362
- ),
1363
- }),
1364
- // The sandbox exposes only tools that are explicitly read-only, and the
1365
- // executor grants no network, filesystem, env, or timer capabilities.
1366
- annotations: {
1367
- readOnlyHint: true,
1368
- destructiveHint: false,
1369
- openWorldHint: true,
1370
- },
1371
- // U5 and U10: declared unconditionally. A host without the Apps
1372
- // extension ignores unknown _meta and sees the ordinary envelope, which
1373
- // is the text fallback the spec mandates — and a stateless aggregator
1374
- // has nowhere dependable to hold a negotiation check anyway. The
1375
- // explicit visibility keeps hosts from being told the view may call
1376
- // execute_code; the default ["model","app"] would say exactly that.
1377
- _meta: {
1378
- ui: {
1379
- resourceUri: PROGRAM_UI_RESOURCE_URI,
1380
- visibility: ["model"],
1381
- },
1382
- },
1383
- },
1384
- async (args, extra) => {
1385
- const controller = new AbortController();
1386
- const signals = [extra.mcpReq.signal, ctx.requestSignal].filter(
1387
- (signal): signal is AbortSignal => signal !== undefined,
1388
- );
1389
- const forwarders = signals.map((signal) => {
1390
- const forward = () => controller.abort(signal.reason);
1391
- if (signal.aborted) forward();
1392
- else signal.addEventListener("abort", forward, { once: true });
1393
- return { signal, forward };
1394
- });
1395
- try {
1396
- return await handler(args as { code: string; diagnostics?: boolean }, {
1397
- signal: controller.signal,
1398
- });
1399
- } finally {
1400
- for (const { signal, forward } of forwarders) {
1401
- signal.removeEventListener("abort", forward);
1402
- }
1403
- }
1404
- },
1405
- );
1406
- }