@zackbart/connecta 0.28.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
package/dist/execute.js CHANGED
@@ -1,21 +1,29 @@
1
- import { Cause, Duration, Effect, Exit } from "effect";
1
+ import { GUEST_API_DECLARATION } from "./usage-guide.js";
2
+ import { bindMcpClient } from "./mcp-client-context.js";
3
+ import { CONFIG_DEFAULTS } from "./config-defaults.js";
4
+ import { Cause, Deferred, Duration, Effect, Exit } from "effect";
2
5
  import { z } from "zod";
3
6
  import { advertisedSchema } from "./advertised-schema.js";
4
7
  import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
5
- import { errorResult, jsonResult } from "./meta-tools.js";
6
- import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, truncateExecuteText, } from "./executor-result.js";
7
- import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor, } from "./executor-admission.js";
8
- import { boundedEchoText, msg } from "./errors.js";
9
- import { DEFAULT_MAX_WRITES, ExemptWrites, writeStateOf, } from "./exempt-writes.js";
10
- import { InvocationFailure, InvocationService, timed, } from "./invocation.js";
8
+ import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, } from "./meta-tools.js";
9
+ import { PROGRAM_RESULT_INLINE_BYTES, PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES, } from "./program-result.js";
10
+ import { MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText } from "./executor-result.js";
11
+ import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor } from "./executor-admission.js";
12
+ import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
13
+ import { DEFAULT_MAX_WRITES, ProgramWrites, writeStateOf } from "./program-writes.js";
14
+ import { InvocationFailure, InvocationService, timed } from "./invocation.js";
15
+ import { guestPrelude, programError, wrapGuestProgram } from "./guest-runtime.js";
16
+ import { EXECUTE_OUTPUT } from "./meta-output.js";
11
17
  import { normalizeProgramSource } from "./program-source.js";
12
- import { underAnySignal } from "./timeout.js";
13
- import { isApprovalExempt, NO_EXEMPTIONS, } from "./tool-safety.js";
14
- import { fromSignal, runEdge } from "./runtime/run.js";
15
- import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, } from "./skills.js";
18
+ import { readResource } from "./resource-read.js";
19
+ import { agentOutputOperations, redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
20
+ import { normalizeTimeoutMs, underAnySignal } from "./timeout.js";
21
+ import { fromSignal, runEdge, withDeadlineEffect } from "./runtime/run.js";
22
+ import { closeScope } from "./runtime/connector-scope.js";
23
+ import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
16
24
  /** Keep one model-written program from amplifying into an unbounded fan-out. */
17
- const EXECUTE_MAX_HOST_CALLS = 20;
18
- const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
25
+ const EXECUTE_MAX_HOST_CALLS = CONFIG_DEFAULTS.execute.maxHostCalls;
26
+ const EXECUTE_HOST_CALL_TIMEOUT_MS = CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
19
27
  /** Above every program in the recorded evals, below the QuickJS IPC ceiling. */
20
28
  const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
21
29
  /**
@@ -24,7 +32,7 @@ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
24
32
  * always reports its own timeout first and this fires only for one that
25
33
  * never settles at all.
26
34
  */
27
- const EXECUTE_WATCHDOG_MS = 120_000;
35
+ const EXECUTE_WATCHDOG_MS = CONFIG_DEFAULTS.execute.watchdogMs;
28
36
  /** Complete entries plus an exact omission count, all inside this byte cap. */
29
37
  export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
30
38
  /**
@@ -34,8 +42,9 @@ export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
34
42
  * two or three real screenshots after base64's 4/3 inflation, well short of a
35
43
  * file-hosting ambition (design record M5).
36
44
  */
37
- export const EXECUTE_MAX_EMITTED_BYTES = 4_000_000;
38
- export const EXECUTE_MAX_EMITTED_BLOCKS = 32;
45
+ export const EXECUTE_MAX_EMITTED_BYTES = CONFIG_DEFAULTS.execute.maxEmittedBytes;
46
+ export const EXECUTE_MAX_EMITTED_BLOCKS = CONFIG_DEFAULTS.execute.maxEmittedBlocks;
47
+ const CALL_SIGNATURE = "Use connecta.call(address, args?, { timeoutMs? }) or connecta.call({ address, args?, timeoutMs? }). address must be a canonical connector.tool string.";
39
48
  const diagnosticsEncoder = new TextEncoder();
40
49
  class ExecuteDiagnostics {
41
50
  started = Date.now();
@@ -85,6 +94,15 @@ class ExecuteDiagnostics {
85
94
  stats.catalogMs += outcome.timing.catalogMs;
86
95
  stats.connectorMs += outcome.timing.connectorMs;
87
96
  }
97
+ recordRead(durationMs, connectorMs, ok, value) {
98
+ const stats = this.stats("read");
99
+ stats.count++;
100
+ stats.failures += ok ? 0 : 1;
101
+ stats.durationMs += durationMs;
102
+ stats.connectorMs += connectorMs;
103
+ if (ok)
104
+ stats.resultBytes += serializedDiagnosticBytes(value);
105
+ }
88
106
  finish() {
89
107
  const operations = [...this.operations.values()];
90
108
  return {
@@ -101,6 +119,10 @@ class ExecuteDiagnostics {
101
119
  };
102
120
  }
103
121
  }
122
+ /**
123
+ * Guest-authored blocks exclude pointers (INV-3). Native call blocks are
124
+ * retained on the host and forwarded only through request-local references.
125
+ */
104
126
  const EMIT_SHAPE_HINT = '{ type: "text", text } or { type: "image" | "audio", data (base64), mimeType }';
105
127
  /**
106
128
  * Strict M1 validation: required fields present and string-valued, nothing
@@ -130,6 +152,19 @@ function requireEmittedBlock(raw) {
130
152
  if (extra.length > 0) {
131
153
  throw guestFailure("invalid_args", `connecta.emit block carries unsupported field(s) ${extra.map((key) => JSON.stringify(key)).join(", ")}; a "${String(block.type)}" block is exactly { ${fields.join(", ")} }`);
132
154
  }
155
+ if (block.type === "image") {
156
+ if (!["image/png", "image/jpeg", "image/gif", "image/webp"].includes(block.mimeType)) {
157
+ throw guestFailure("invalid_args", "connecta.emit image mimeType must be image/png, image/jpeg, image/gif, or image/webp.");
158
+ }
159
+ const data = block.data;
160
+ // RFC 4648 alphabet, complete quads and canonical padding, including zero
161
+ // pad bits. Decode only the last quad; validation does not copy the image.
162
+ if (!data.length ||
163
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(data) ||
164
+ btoa(atob(data.slice(-4))) !== data.slice(-4)) {
165
+ throw guestFailure("invalid_args", "connecta.emit image data must be nonempty, strict base64 with canonical padding.");
166
+ }
167
+ }
133
168
  return raw;
134
169
  }
135
170
  export class EmitCollector {
@@ -138,38 +173,157 @@ export class EmitCollector {
138
173
  diagnostics;
139
174
  blocks = [];
140
175
  bytes = 0;
176
+ textChars = 0;
141
177
  constructor(maxBytes, maxBlocks, diagnostics) {
142
178
  this.maxBytes = maxBytes;
143
179
  this.maxBlocks = maxBlocks;
144
180
  this.diagnostics = diagnostics;
145
181
  }
146
182
  accept(raw) {
147
- const block = requireEmittedBlock(raw);
148
- if (this.blocks.length >= this.maxBlocks) {
149
- // M5: distinguish exhausted block slots from the remaining byte budget.
150
- throw guestFailure("budget_exceeded", `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 blocks remaining; ${this.maxBytes - this.bytes} of ${this.maxBytes} serialized bytes remaining`);
151
- }
152
- const size = diagnosticsEncoder.encode(JSON.stringify(block)).byteLength;
153
- if (this.bytes + size > this.maxBytes) {
154
- throw guestFailure("budget_exceeded", `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`);
183
+ this.acceptNative([requireEmittedBlock(raw)]);
184
+ }
185
+ /** Only the host's retained downstream result may enter this path. */
186
+ acceptNative(blocks) {
187
+ let bytes = this.bytes;
188
+ let textChars = this.textChars;
189
+ let count = this.blocks.length;
190
+ for (const block of blocks) {
191
+ if (count >= this.maxBlocks) {
192
+ throw guestFailure("budget_exceeded", `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 blocks remaining; ${this.maxBytes - bytes} of ${this.maxBytes} serialized bytes remaining`);
193
+ }
194
+ const serialized = JSON.stringify(block);
195
+ const size = diagnosticsEncoder.encode(serialized).byteLength;
196
+ if (bytes + size > this.maxBytes) {
197
+ throw guestFailure("budget_exceeded", `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - bytes} of ${this.maxBytes} remaining`);
198
+ }
199
+ textChars += block.type === "text" ? serialized.length : 0;
200
+ // Embedded resource text shares the result cap too; binary blobs use
201
+ // the transport budget, just like images and audio.
202
+ if (block.type === "resource" && "text" in block.resource)
203
+ textChars += serialized.length;
204
+ if (textChars > MAX_EXECUTE_RESULT_CHARS - 512) {
205
+ throw guestFailure("result_too_large", "Emitted text shares the 24,000-character program result limit. Reduce the text before emitting it.");
206
+ }
207
+ count++;
208
+ bytes += size;
155
209
  }
156
- this.blocks.push(block);
157
- this.bytes += size;
210
+ this.textChars = textChars;
211
+ this.bytes = bytes;
212
+ this.blocks.push(...blocks);
158
213
  this.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
159
214
  }
160
215
  }
216
+ /** Bound both JSON size and object amplification before retaining native blocks. */
217
+ class NativePresentationLimit extends Error {
218
+ }
219
+ function boundedNativeValue(value, maxBytes, copy) {
220
+ let bytes = 0;
221
+ let nodes = 0;
222
+ // Empty objects cost only two JSON bytes but much more heap. Derive the
223
+ // allocation ceiling from the transport budget, with a fixed stack ceiling.
224
+ const maxNodes = Math.floor(Math.min(maxBytes, PROGRAM_RESULT_INLINE_BYTES) / 32);
225
+ const active = new Set();
226
+ const charge = (size) => {
227
+ bytes += size;
228
+ if (bytes > maxBytes)
229
+ throw new NativePresentationLimit();
230
+ };
231
+ const string = (text) => {
232
+ // Count JSON escapes and UTF-8 directly, without allocating a potentially
233
+ // six-times-larger escaped string or byte buffer just to reject it.
234
+ if (text.length > maxBytes - bytes)
235
+ throw new NativePresentationLimit();
236
+ charge(2);
237
+ for (let i = 0; i < text.length; i++) {
238
+ const unit = text.charCodeAt(i);
239
+ if (unit === 34 || unit === 92 || unit === 8 || unit === 9 || unit === 10 || unit === 12 || unit === 13)
240
+ charge(2);
241
+ else if (unit < 32)
242
+ charge(6);
243
+ else if (unit < 128)
244
+ charge(1);
245
+ else if (unit < 2048)
246
+ charge(2);
247
+ else if (unit >= 0xd800 &&
248
+ unit <= 0xdbff &&
249
+ text.charCodeAt(i + 1) >= 0xdc00 &&
250
+ text.charCodeAt(i + 1) <= 0xdfff) {
251
+ charge(4);
252
+ i++;
253
+ }
254
+ else
255
+ charge(unit >= 0xd800 && unit <= 0xdfff ? 6 : 3);
256
+ }
257
+ };
258
+ const visit = (item, depth) => {
259
+ if (++nodes > maxNodes || depth > 64)
260
+ throw new NativePresentationLimit();
261
+ if (typeof item === "string") {
262
+ string(item);
263
+ return item;
264
+ }
265
+ if (item === null || typeof item === "boolean" || typeof item === "number") {
266
+ charge(JSON.stringify(item).length);
267
+ return item;
268
+ }
269
+ if (typeof item !== "object" || active.has(item))
270
+ throw new NativePresentationLimit();
271
+ const array = Array.isArray(item);
272
+ if (![array ? Array.prototype : Object.prototype, null].includes(Object.getPrototypeOf(item)))
273
+ throw new NativePresentationLimit();
274
+ const jsonHook = Object.getOwnPropertyDescriptor(item, "toJSON");
275
+ if (jsonHook && (!("value" in jsonHook) || typeof jsonHook.value === "function"))
276
+ throw new NativePresentationLimit();
277
+ active.add(item);
278
+ charge(2);
279
+ if (array) {
280
+ if (item.length > maxNodes - nodes)
281
+ throw new NativePresentationLimit();
282
+ const result = copy ? [] : undefined;
283
+ for (let i = 0; i < item.length; i++) {
284
+ if (i)
285
+ charge(1);
286
+ const descriptor = Object.getOwnPropertyDescriptor(item, String(i));
287
+ if (descriptor && !("value" in descriptor))
288
+ throw new NativePresentationLimit();
289
+ const child = visit(descriptor?.value ?? null, depth + 1);
290
+ result?.push(child);
291
+ }
292
+ active.delete(item);
293
+ return result;
294
+ }
295
+ const result = copy ? {} : undefined;
296
+ let first = true;
297
+ for (const key in item) {
298
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
299
+ if (!descriptor?.enumerable)
300
+ continue;
301
+ if (!("value" in descriptor))
302
+ throw new NativePresentationLimit();
303
+ if (descriptor.value === undefined)
304
+ continue;
305
+ if (!first)
306
+ charge(1);
307
+ first = false;
308
+ string(key);
309
+ charge(1);
310
+ const child = visit(descriptor.value, depth + 1);
311
+ if (result)
312
+ Object.defineProperty(result, key, { value: child, enumerable: true, writable: true, configurable: true });
313
+ }
314
+ active.delete(item);
315
+ return result;
316
+ };
317
+ return { value: visit(value, 0), bytes };
318
+ }
161
319
  /** A positive whole-number budget, or the default when the value is unusable. */
162
320
  function resolveBudget(value, fallback) {
163
- return typeof value === "number" && Number.isFinite(value) && value >= 1
164
- ? Math.trunc(value)
165
- : fallback;
321
+ return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.trunc(value) : fallback;
166
322
  }
167
323
  function serializedDiagnosticBytes(value) {
168
324
  try {
169
325
  const text = JSON.stringify(value);
170
- return text === undefined
171
- ? 0
172
- : diagnosticsEncoder.encode(text).byteLength;
326
+ return text === undefined ? 0 : diagnosticsEncoder.encode(text).byteLength;
173
327
  }
174
328
  catch {
175
329
  // The executor's normal result guard owns the error. Diagnostics must
@@ -180,16 +334,8 @@ function serializedDiagnosticBytes(value) {
180
334
  function guestFailure(code, message, retryable = false) {
181
335
  return new InvocationFailure({ code, message, retryable });
182
336
  }
183
- const GUEST_FAILURE_FRAME = "\u001econnecta-error:";
184
- const guestFailureFrames = new WeakMap();
185
- /** Per-execution secret: guest prose cannot collide with this host frame. */
186
- function guestFailureSecret() {
187
- const words = new Uint32Array(4);
188
- crypto.getRandomValues(words);
189
- return Array.from(words, (word) => word.toString(16).padStart(8, "0")).join("");
190
- }
191
337
  /**
192
- * E1/X11: bound the serialized frame, including JSON escapes, before transport.
338
+ * E1/X11: bound typed host failure metadata, including JSON escapes, before transport.
193
339
  * Keep optional recovery whole: clipping an address or echoed args could turn
194
340
  * recovery into a different call. Oversized metadata is omitted instead.
195
341
  */
@@ -216,67 +362,17 @@ function boundedGuestFailure(failure) {
216
362
  code: boundText(failure.details.code, 128),
217
363
  message: boundText(failure.details.message, 2_000),
218
364
  };
219
- // 3,700 plus the prefix and 32-character secret stays below QuickJS's
220
- // 4,000-character error bound, with room for bridge-added context.
365
+ // Keep typed metadata below the executor bridge bound.
221
366
  if (JSON.stringify(details).length > 3_700) {
222
367
  details = {
223
368
  code: details.code,
224
369
  message: details.message,
225
370
  retryable: details.retryable,
226
- ...(details.retryAfterMs !== undefined
227
- ? { retryAfterMs: details.retryAfterMs }
228
- : {}),
371
+ ...(details.retryAfterMs !== undefined ? { retryAfterMs: details.retryAfterMs } : {}),
229
372
  };
230
373
  }
231
374
  return new InvocationFailure(details);
232
375
  }
233
- function framedGuestFailure(secret, failure) {
234
- const framed = new InvocationFailure(failure.details);
235
- framed.message =
236
- `${GUEST_FAILURE_FRAME}${secret}:${JSON.stringify(failure.details)}`;
237
- guestFailureFrames.set(failure, framed.message);
238
- return framed;
239
- }
240
- /** Rebuild host failures as guest errors without exposing the private frame. */
241
- function guestErrorPrelude(failureSecret) {
242
- return `((failurePrefix) => {
243
- const NativeError = globalThis.Error;
244
- const startsWith = Function.prototype.call.bind(String.prototype.startsWith);
245
- const slice = Function.prototype.call.bind(String.prototype.slice);
246
- const parse = JSON.parse;
247
- const freeze = Object.freeze;
248
- const defineProperties = Object.defineProperties;
249
- const construct = Reflect.construct;
250
- function ConnectaError(message, options) {
251
- let details;
252
- if (typeof message === "string" && startsWith(message, failurePrefix)) {
253
- try { details = parse(slice(message, failurePrefix.length)); }
254
- catch { message = "Invalid host failure frame."; }
255
- }
256
- const error = construct(
257
- NativeError,
258
- options === undefined ? [details ? details.message : message] : [details ? details.message : message, options],
259
- new.target || NativeError
260
- );
261
- if (details) {
262
- freeze(details);
263
- defineProperties(error, {
264
- code: { value: details.code, enumerable: true },
265
- retryable: { value: details.retryable, enumerable: true },
266
- details: { value: details, enumerable: true }
267
- });
268
- }
269
- return error;
270
- }
271
- ConnectaError.prototype = NativeError.prototype;
272
- Object.setPrototypeOf(ConnectaError, NativeError);
273
- Object.defineProperty(globalThis, "Error", {
274
- value: ConnectaError,
275
- writable: false,
276
- configurable: false
277
- });
278
- })(${JSON.stringify(`${GUEST_FAILURE_FRAME}${failureSecret}:`)});`;
279
- }
280
376
  /**
281
377
  * Expose one host provider and typed guest errors. Catalogs load only when
282
378
  * the program calls a tool or asks search/describe.
@@ -284,58 +380,172 @@ function guestErrorPrelude(failureSecret) {
284
380
  export async function buildSandboxProviders(registry, baseUrl, _logger, activity, limits = {}) {
285
381
  return [sandboxProvider(registry, baseUrl, activity, limits)];
286
382
  }
383
+ /** Numeric accounting only; no addresses, arguments, or results. */
384
+ class HostCallBudgetExceeded extends InvocationFailure {
385
+ hostCalls;
386
+ constructor(hostCalls, maxHostCalls) {
387
+ super({
388
+ code: "budget_exceeded",
389
+ message: `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`,
390
+ retryable: false,
391
+ });
392
+ this.hostCalls = hostCalls;
393
+ }
394
+ }
287
395
  /**
288
396
  * The `connecta` provider for one execution.
289
397
  *
290
398
  * Building it is synchronous and loads nothing. Each function is a Promise
291
399
  * edge the executor awaits, and behind it one host call is one fiber: spend
292
- * the budget, do the operation, and turn a typed failure into the frame the
293
- * prelude rebuilds inside the guest. Nothing is shared between those fibers
294
- * but the budget counters and this request's catalog, so a call the program
295
- * never awaits cannot disturb the others.
400
+ * the budget, do the operation, and retain a typed failure for the
401
+ * executor bridge to rebuild inside the guest. Nothing is shared between those fibers
402
+ * but the budget counters and this request's catalog. Budget exhaustion ends
403
+ * the whole run, including calls the program never awaits.
296
404
  */
297
- function sandboxProvider(registry, baseUrl, activity, limits) {
405
+ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {}) {
406
+ const sentSecrets = sentSecretsForRequest(requestScope, limits.sentSecrets);
298
407
  // All host calls made by one execute_code invocation share a downstream
299
408
  // connection, while a later invocation receives a fresh request scope.
300
- const requestScope = {};
409
+ const hostAccessSignal = limits.dispatchController
410
+ ? AbortSignal.any([limits.dispatchController.signal, ...(limits.signal ? [limits.signal] : [])])
411
+ : limits.signal;
301
412
  const catalog = new CatalogService(registry, baseUrl, {
302
413
  requestScope,
414
+ requestSignal: hostAccessSignal,
303
415
  // A program that just missed an address cannot call search_tools.
304
416
  searchRoute: "connecta.search",
305
417
  concurrency: limits.discoveryConcurrency,
306
418
  probeTimeoutMs: limits.probeTimeoutMs,
307
419
  defer: limits.defer,
308
- approval: limits.approval,
309
420
  });
310
421
  const invocation = new InvocationService(registry, catalog, activity);
311
422
  const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
312
423
  const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
313
- const failureSecret = guestFailureSecret();
314
- const { signal, diagnostics, exemptWrites } = limits;
315
- let hostCalls = 0;
316
- // L4/M7: discovery and invocation spend the same budget, on entry; emit
317
- // does not.
318
- const spendHostCall = Effect.suspend(() => ++hostCalls > maxHostCalls
319
- ? Effect.fail(guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`))
320
- : Effect.void);
321
- const approval = limits.approval ?? NO_EXEMPTIONS;
424
+ const { signal, diagnostics, programWrites } = limits;
425
+ const hostCalls = limits.hostCalls ?? { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
426
+ let budgetFailure;
427
+ let emitAttempts = 0;
428
+ const maxEmitAttempts = Math.max(EXECUTE_MAX_EMITTED_BLOCKS, limits.emitCollector?.maxBlocks ?? EXECUTE_MAX_EMITTED_BLOCKS) * 4;
429
+ // A rejected bridge promise is catchable in both sandboxes. End the host
430
+ // run instead and leave that bridge pending: an awaiting guest cannot catch
431
+ // the refusal and loop. Later calls, including emit, lose host access too.
432
+ const stopped = new Promise((_resolve, reject) => {
433
+ const ended = () => reject(new Error("The host run ended."));
434
+ if (signal?.aborted)
435
+ ended();
436
+ else
437
+ signal?.addEventListener("abort", ended, { once: true });
438
+ });
439
+ // Stay pending until the lease has disposed the sandbox, then settle the
440
+ // abandoned host RPC waits without retaining request resources forever.
441
+ stopped.catch(() => { });
322
442
  const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
323
443
  let writes = 0;
324
- /**
325
- * The write gate covers exactly the config-exempt calls (#566): each spends
326
- * the write budget and is tracked until it settles (`ExemptWrites`), so the
327
- * play can wait for it rather than abort it. Every other write keeps E4's
328
- * refusal, ahead of validation, so the host's prompt on
329
- * call_destructive_tool stays the only approval there is.
330
- */
331
- const invocationContext = (sending) => ({
444
+ // These are transfer cursors only. All result bytes live in the existing stash.
445
+ const transfers = new Map();
446
+ const nativeResults = new Map();
447
+ const transportResult = (value) => {
448
+ if (value.format === "paged") {
449
+ const handle = value;
450
+ if (handle.resultId && handle.totalBytes <= PROGRAM_RESULT_INLINE_BYTES) {
451
+ const transfer = `transfer:${crypto.randomUUID()}`;
452
+ transfers.set(transfer, { id: handle.resultId, offset: 0 });
453
+ return { transfer, handle };
454
+ }
455
+ }
456
+ return value;
457
+ };
458
+ /** Budget and account for writes admitted by the pool trust decision. */
459
+ const invocationContext = (sending, timeoutMs) => ({
332
460
  source: "execute_code",
333
- timeoutMs: hostCallTimeoutMs,
461
+ sentSecrets,
462
+ timeoutMs,
334
463
  ...(signal !== undefined ? { requestSignal: signal } : {}),
464
+ ...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
335
465
  unwrapResult: true,
336
- gates: (target) => isApprovalExempt(approval, target.connector, target.toolName, target.definition),
337
- writeGate: (target) => Effect.sync(() => {
338
- if (exemptWrites?.isClosed) {
466
+ trust: limits.trust,
467
+ processResult: async (value, resolved, secrets, format, raw) => {
468
+ const page = (data, valueFormat, wireBytes) => pageProgramResult(registry, baseUrl, resolved, data, valueFormat, secrets, limits.trust, requestScope, wireBytes);
469
+ const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
470
+ ? raw
471
+ : undefined;
472
+ const dataKey = envelope
473
+ ? "toolResult" in envelope
474
+ ? "toolResult"
475
+ : envelope.structuredContent !== undefined
476
+ ? "structuredContent"
477
+ : undefined
478
+ : undefined;
479
+ // Preserve #788 before even inspecting presentation. Paging serializes
480
+ // the structured value synchronously; no native metadata is visited.
481
+ if (dataKey !== undefined &&
482
+ diagnosticsEncoder.encode(JSON.stringify(value)).byteLength > PROGRAM_RESULT_INLINE_BYTES) {
483
+ return page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
484
+ }
485
+ const content = envelope?.content;
486
+ if (!Array.isArray(content) ||
487
+ !content.some((block) => block && ["image", "audio", "resource", "resource_link"].includes(block.type))) {
488
+ return transportResult(await page(value, format));
489
+ }
490
+ const unavailablePresentation = () => ({
491
+ format: "paged",
492
+ valueFormat: "json",
493
+ totalBytes: 0,
494
+ truncated: true,
495
+ hint: "Paging is unavailable. Native presentation exceeds byte, node or depth limits, or is not acyclic JSON. Reduce the downstream presentation before forwarding it.",
496
+ });
497
+ let snapshot;
498
+ let retained;
499
+ try {
500
+ // Oversized but bounded envelopes may still use the existing stash.
501
+ // Measure without copying them; only inline native blocks are retained.
502
+ const measured = boundedNativeValue(envelope, Math.max(EXECUTE_MAX_EMITTED_BYTES, limits.emitCollector?.maxBytes ?? 0), false);
503
+ if (measured.bytes <= PROGRAM_RESULT_INLINE_BYTES) {
504
+ retained = boundedNativeValue(content, PROGRAM_RESULT_INLINE_BYTES, true).value;
505
+ snapshot = { ...envelope, content: retained };
506
+ }
507
+ else {
508
+ snapshot = envelope;
509
+ }
510
+ }
511
+ catch (error) {
512
+ if (!(error instanceof NativePresentationLimit))
513
+ throw error;
514
+ const result = dataKey === undefined ? undefined : transportResult(await page(value, format));
515
+ const notice = unavailablePresentation();
516
+ return result ? { value: result, native: { value: notice } } : notice;
517
+ }
518
+ // Snapshot blocks before either stash operation yields. Start both
519
+ // serializations now so an oversized presentation's pages also own the
520
+ // original bytes even if the structured-data stash pauses.
521
+ const resultPending = dataKey === undefined ? undefined : page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
522
+ const nativePending = page(snapshot, "json");
523
+ const result = await resultPending;
524
+ const native = await nativePending;
525
+ let valueResult;
526
+ if (native.format === "paged") {
527
+ valueResult = result ? transportResult(result) : undefined;
528
+ if (!native.resultId || native.totalBytes > PROGRAM_RESULT_INLINE_BYTES) {
529
+ return valueResult ? { value: valueResult, native: { value: native } } : native;
530
+ }
531
+ }
532
+ // Redaction can shrink an oversized original below the inline cap. It
533
+ // still has no retained snapshot: keep its handle, never mint a ref to
534
+ // absent or connector-owned blocks.
535
+ if (!retained) {
536
+ const notice = native.format === "paged" ? native : unavailablePresentation();
537
+ const valueResult = result ? transportResult(result) : undefined;
538
+ return valueResult ? { value: valueResult, native: { value: notice } } : notice;
539
+ }
540
+ const ref = crypto.randomUUID();
541
+ nativeResults.set(ref, retained);
542
+ return {
543
+ ...(valueResult ? { value: valueResult } : {}),
544
+ native: { ref, value: transportResult(native), dataKey },
545
+ };
546
+ },
547
+ beforeWrite: (target) => Effect.sync(() => {
548
+ if (programWrites?.isClosed) {
339
549
  return {
340
550
  kind: "refuse",
341
551
  error: {
@@ -357,24 +567,75 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
357
567
  };
358
568
  }
359
569
  writes++;
360
- if (exemptWrites)
361
- sending.settle = exemptWrites.begin();
570
+ if (programWrites)
571
+ sending.settle = programWrites.begin();
362
572
  return { kind: "dispatch" };
363
573
  }),
364
574
  });
365
575
  // A call brings its own cancellation: the invocation pipeline reads the
366
576
  // run's signal, refuses a call that starts after it, and records the
367
577
  // cancelled attempt in activity like any other outcome.
368
- const call = (address, args) => Effect.gen(function* () {
369
- yield* spendHostCall;
370
- // An exempt write settles here — unknown if the call never returned an
371
- // outcome.
578
+ const call = (address, args, options) => Effect.gen(function* () {
579
+ let timeout;
580
+ if (address !== null && typeof address === "object" && !Array.isArray(address)) {
581
+ const request = address;
582
+ if (args !== undefined ||
583
+ options !== undefined ||
584
+ Object.keys(request).some((key) => !["address", "args", "timeoutMs"].includes(key))) {
585
+ return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
586
+ }
587
+ address = request.address;
588
+ args = request.args;
589
+ timeout = request.timeoutMs;
590
+ }
591
+ else if (options !== undefined) {
592
+ if (!options ||
593
+ typeof options !== "object" ||
594
+ Array.isArray(options) ||
595
+ Object.keys(options).some((key) => key !== "timeoutMs")) {
596
+ return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
597
+ }
598
+ timeout = options.timeoutMs;
599
+ }
600
+ if (typeof address !== "string" ||
601
+ (timeout !== undefined && (typeof timeout !== "number" || !Number.isSafeInteger(timeout) || timeout < 1))) {
602
+ return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
603
+ }
604
+ const requestedMs = normalizeTimeoutMs(timeout) ??
605
+ normalizeTimeoutMs(limits.defaultToolTimeoutMs) ??
606
+ hostCallTimeoutMs;
607
+ const timeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
608
+ // A trusted-pool write settles here — unknown if the call never returned an
609
+ // outcome. Credentials are filtered from arguments; submitted private values are not.
372
610
  const sending = {};
373
- const outcome = yield* invocation.pipeline(String(address), args ?? {}, invocationContext(sending)).pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
611
+ const outcome = yield* invocation
612
+ .pipeline(address, sentSecrets.redactCredentials(args ?? {}), invocationContext(sending, timeoutMs))
613
+ .pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
614
+ if (outcome.resolved) {
615
+ const resolved = outcome.resolved;
616
+ limits.resultDependencies?.push({
617
+ connector: resolved.connector.id,
618
+ tool: resolved.definition.name,
619
+ classification: outcome.dispatched && resolved.definition.classification !== "read" ? "write" : "read",
620
+ // A refusal exposes catalog metadata, not a completed write's result.
621
+ ...(!outcome.dispatched ? { access: "catalog" } : {}),
622
+ });
623
+ }
624
+ else {
625
+ // Resolution failures can expose a catalog error or connector metadata
626
+ // without ever finding a tool. Such bytes depend on connector visibility,
627
+ // not on the existence of the requested (possibly unknown) tool.
628
+ const target = registry.resolveAddress(address);
629
+ if (target)
630
+ catalogDependency(target.connector.id);
631
+ if (!outcome.ok)
632
+ for (const connector of outcome.error.configuredConnectors ?? [])
633
+ catalogDependency(connector);
634
+ }
374
635
  diagnostics?.recordCall(outcome);
375
- return outcome.ok
376
- ? outcome.value
377
- : yield* Effect.fail(new InvocationFailure(outcome.error));
636
+ if (!outcome.ok)
637
+ return yield* Effect.fail(new InvocationFailure(outcome.error));
638
+ return outcome.value;
378
639
  });
379
640
  // Discovery gets the same treatment from here (L2): once the run has
380
641
  // ended no search or describe starts, and one still in flight fails
@@ -386,21 +647,117 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
386
647
  const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
387
648
  const reading = Effect.tryPromise({
388
649
  try: read,
389
- catch: (err) => err instanceof DiscoveryPolicyError
390
- ? guestFailure(err.code, err.message)
391
- : err,
650
+ catch: (err) => (err instanceof DiscoveryPolicyError ? guestFailure(err.code, err.message) : err),
392
651
  });
393
- return spendHostCall.pipe(Effect.andThen(!signal
652
+ return (!hostAccessSignal
394
653
  ? reading
395
- : signal.aborted
654
+ : hostAccessSignal.aborted
396
655
  ? Effect.fail(cancelled())
397
- : Effect.raceAllFirst([
398
- reading,
399
- fromSignal(signal).pipe(Effect.mapError(cancelled)),
400
- ])), Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
656
+ : Effect.raceAllFirst([reading, fromSignal(hostAccessSignal).pipe(Effect.mapError(cancelled))])).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
657
+ });
658
+ const meta = createMetaTools(registry, baseUrl, {
659
+ trust: limits.trust,
660
+ requestScope,
661
+ onResultRead: (dependencies) => limits.resultDependencies?.push(...dependencies),
401
662
  });
663
+ const catalogDependency = (connector, tool) => {
664
+ if (registry.getConnector(connector))
665
+ limits.resultDependencies?.push({
666
+ connector,
667
+ ...(tool ? { tool } : {}),
668
+ classification: "read",
669
+ access: "catalog",
670
+ });
671
+ };
672
+ const catalogAddressDependency = (address) => {
673
+ const resolved = registry.resolveAddress(address);
674
+ if (resolved)
675
+ catalogDependency(resolved.connector.id, resolved.toolName);
676
+ };
402
677
  const operations = {
403
678
  call,
679
+ result: (id, options, signal) => Effect.tryPromise({
680
+ try: async () => {
681
+ const transfer = typeof id === "string" ? transfers.get(id) : undefined;
682
+ const transferId = id;
683
+ if (transfer) {
684
+ id = transfer.id;
685
+ options = { offset: transfer.offset, maxBytes: PROGRAM_RESULT_PAGE_BYTES };
686
+ }
687
+ else if (id !== null && typeof id === "object" && !Array.isArray(id)) {
688
+ id = id.resultId;
689
+ }
690
+ if (typeof id !== "string" ||
691
+ (!options && options !== undefined) ||
692
+ (options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
693
+ throw guestFailure("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }).");
694
+ }
695
+ const args = { ...(options ?? {}) };
696
+ if (Object.keys(args).some((key) => !["page", "offset", "maxBytes"].includes(key)) ||
697
+ (args.page !== undefined &&
698
+ (typeof args.page !== "number" ||
699
+ !Number.isSafeInteger(args.page) ||
700
+ args.page < 0 ||
701
+ args.offset !== undefined)) ||
702
+ (args.offset !== undefined &&
703
+ (typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
704
+ (args.maxBytes !== undefined &&
705
+ (typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
706
+ throw guestFailure("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }) with whole page/offset >= 0 and maxBytes >= 1; page and offset are mutually exclusive.");
707
+ }
708
+ // A page must fit every shipped bridge even when the connector cap is raised.
709
+ args.maxBytes = Math.min(args.maxBytes ?? PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_PAGE_BYTES);
710
+ const page = await meta.readResult({ id, ...args }, { signal: signal });
711
+ if (page.isError)
712
+ throw new InvocationFailure((page.structuredContent?.error ?? {
713
+ code: "not_found",
714
+ message: page.content[0]?.text ?? "Result unavailable.",
715
+ retryable: false,
716
+ }));
717
+ if (transfer) {
718
+ if (page.structuredContent?.hasMore)
719
+ transfer.offset = page.structuredContent.nextOffset;
720
+ else
721
+ transfers.delete(transferId);
722
+ }
723
+ return page.structuredContent;
724
+ },
725
+ catch: (err) => err,
726
+ }),
727
+ skill: (name, _options, utilitySignal) => Effect.tryPromise({
728
+ try: async () => {
729
+ if (typeof name !== "string")
730
+ throw guestFailure("invalid_args", "Use connecta.skill(name) with an exact skill name.");
731
+ const skills = new SkillsRegistry(registry, baseUrl, {
732
+ requestScope,
733
+ requestSignal: utilitySignal ?? hostAccessSignal,
734
+ onRead: (connector, source) => limits.resultDependencies?.push({
735
+ connector: connector.id,
736
+ classification: "read",
737
+ ...(source === "local" ? { access: "catalog" } : {}),
738
+ }),
739
+ probeTimeoutMs: limits.probeTimeoutMs,
740
+ defer: limits.defer,
741
+ });
742
+ return { name, format: "text", text: await skills.text(name) };
743
+ },
744
+ catch: (err) => err instanceof ConnectorCallError ? guestFailure(err.code, err.message, err.code === "unavailable") : err,
745
+ }),
746
+ read: (uri) => Effect.suspend(() => {
747
+ const started = Date.now();
748
+ let connectorMs = 0;
749
+ return readResource(registry, uri, {
750
+ baseUrl,
751
+ requestScope,
752
+ sentSecrets,
753
+ timeoutMs: hostCallTimeoutMs,
754
+ onResolved: (connector) => limits.resultDependencies?.push({ connector: connector.id, classification: "read" }),
755
+ onConnectorTime: (elapsed) => {
756
+ connectorMs += elapsed;
757
+ },
758
+ ...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
759
+ }).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordRead(Date.now() - started, connectorMs, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
760
+ }),
404
761
  // Emission is a provider function, never an ExecuteResult field —
405
762
  // that is what keeps the Executor contract untouched and parity
406
763
  // structural (M8). It spends no host-call budget (M7); its own
@@ -410,7 +767,26 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
410
767
  if (!limits.emitCollector) {
411
768
  throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
412
769
  }
413
- limits.emitCollector.accept(block);
770
+ if (block !== null && typeof block === "object" && "contentResult" in block) {
771
+ throw guestFailure("result_too_large", "Native content is not inline; page result.contentResult with connecta.result and reduce it before emitting.");
772
+ }
773
+ if (block !== null && typeof block === "object" && !Array.isArray(block) && "ref" in block) {
774
+ const reference = block;
775
+ const native = typeof reference.ref === "string" ? nativeResults.get(reference.ref) : undefined;
776
+ if (!native ||
777
+ Object.keys(reference).some((key) => !["ref", "index"].includes(key)) ||
778
+ (reference.index !== undefined &&
779
+ (typeof reference.index !== "number" ||
780
+ !Number.isSafeInteger(reference.index) ||
781
+ reference.index < 0 ||
782
+ reference.index >= native.length))) {
783
+ throw guestFailure("invalid_args", "connecta.emit requires a native result from this program's connecta.call.");
784
+ }
785
+ const blocks = reference.index === undefined ? native : [native[reference.index]];
786
+ limits.emitCollector.acceptNative(sentSecrets.redact(blocks));
787
+ }
788
+ else
789
+ limits.emitCollector.accept(sentSecrets.redact(block));
414
790
  },
415
791
  catch: (err) => err,
416
792
  }),
@@ -418,37 +794,122 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
418
794
  const args = (raw ?? {});
419
795
  const result = flatSearchResult(await catalog.search({
420
796
  ...args,
797
+ includeSchemas: args.includeSchemas ?? "json",
421
798
  includeSchemaKeys: args.includeSchemaKeys !== false,
422
799
  }));
423
800
  boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
801
+ for (const tool of result.tools)
802
+ catalogAddressDependency(tool.address);
803
+ for (const error of result.catalogErrors)
804
+ catalogDependency(error.connector);
805
+ for (const connector of result.absence?.configuredConnectors ?? [])
806
+ catalogDependency(connector);
807
+ for (const connector of result.queryAnalysis?.configuredConnectors ?? [])
808
+ catalogDependency(connector);
809
+ if (result.queryAnalysis?.connectorScope)
810
+ catalogDependency(result.queryAnalysis.connectorScope);
424
811
  return result;
425
812
  }),
426
813
  describe: (raw) => discovery("describe", async () => {
427
814
  const args = (raw ?? {});
428
- const result = { tools: await catalog.describe(args) };
815
+ const result = { tools: await catalog.describe({ ...args, format: args.format ?? "json" }) };
429
816
  boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
817
+ for (const tool of result.tools) {
818
+ if (!tool.error)
819
+ catalogAddressDependency(tool.address);
820
+ else {
821
+ const resolved = registry.resolveAddress(tool.address);
822
+ if (resolved)
823
+ catalogDependency(resolved.connector.id);
824
+ for (const connector of tool.errorDetails?.configuredConnectors ?? [])
825
+ catalogDependency(connector);
826
+ for (const suggestion of tool.errorDetails?.suggestions ?? [])
827
+ catalogAddressDependency(suggestion);
828
+ }
829
+ }
430
830
  return result;
431
831
  }),
432
832
  };
433
- // Every failure leaves through the same frame, so the prelude can rebuild
434
- // a typed error however the host call failed — refused, over budget, or
435
- // cancelled. Anything that is not an InvocationFailure crosses unchanged.
436
- const framed = (err) => Effect.suspend(() => {
833
+ // Recoverable failures remain typed at the host-owned executor bridge. Terminal host-budget exhaustion bypasses it.
834
+ // Anything that is not an InvocationFailure crosses unchanged.
835
+ const typedFailure = (err) => Effect.suspend(() => {
437
836
  if (!(err instanceof InvocationFailure))
438
837
  return Effect.fail(err);
439
- const failure = boundedGuestFailure(err);
440
- const frame = framedGuestFailure(failureSecret, failure);
441
- limits.onInvocationFailure?.(failure);
442
- return Effect.fail(frame);
838
+ const failure = boundedGuestFailure(redactAgentOutput(sentSecrets, err));
839
+ return Effect.fail(failure);
443
840
  });
444
841
  return {
445
842
  name: "connecta",
446
843
  // Trusted host code the program runs after and cannot undo.
447
- prelude: guestErrorPrelude(failureSecret),
844
+ prelude: guestPrelude(),
448
845
  fns: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
449
846
  name,
450
847
  (...args) => {
451
- const settled = runEdge(Effect.suspend(() => operation(...args)).pipe(Effect.catch(framed)));
848
+ if (budgetFailure)
849
+ return stopped;
850
+ // Invalid emits still allocate guest promises and typed rejections.
851
+ // Bound those attempts without consuming the invocation budget or discarding identities.
852
+ if (name === "emit" && ++emitAttempts > maxEmitAttempts) {
853
+ budgetFailure = guestFailure("budget_exceeded", `execute_code emission-attempt budget exceeded (${maxEmitAttempts} attempts maximum).`);
854
+ programWrites?.close();
855
+ limits.dispatchController?.abort();
856
+ limits.onHostCallBudgetExceeded?.(budgetFailure);
857
+ return stopped;
858
+ }
859
+ // L4/M7: spend on entry, before even invalid arguments are checked;
860
+ // emit has a separate budget and never spends this one.
861
+ // Transparent transfer pages consume the already-admitted call, not guest host calls.
862
+ const counted = name !== "emit" && !(name === "result" && typeof args[0] === "string" && transfers.has(args[0]));
863
+ if (counted && ++hostCalls.attempted > maxHostCalls) {
864
+ hostCalls.failed++;
865
+ budgetFailure = new HostCallBudgetExceeded(hostCalls, maxHostCalls);
866
+ programWrites?.close();
867
+ limits.dispatchController?.abort();
868
+ limits.onHostCallBudgetExceeded?.(budgetFailure);
869
+ return stopped;
870
+ }
871
+ if (counted)
872
+ hostCalls.admitted++;
873
+ const started = Date.now();
874
+ const utility = name === "result" || name === "skill";
875
+ const invoke = Effect.suspend(() => operation(...args));
876
+ const guarded = utility
877
+ ? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal)), {
878
+ timeoutMs: hostCallTimeoutMs,
879
+ ...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
880
+ timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
881
+ })
882
+ : invoke;
883
+ const settled = runEdge(guarded.pipe(Effect.catch((error) => {
884
+ if (utility && hostAccessSignal?.aborted)
885
+ return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
886
+ if (utility && error instanceof InvocationFailure && error.code === "timeout") {
887
+ const elapsedMs = Date.now() - started;
888
+ const message = `Operation "connecta.${name}" timed out during ${name === "result" ? "result storage" : "skill lookup"} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
889
+ return typedFailure(new InvocationFailure({
890
+ code: "timeout",
891
+ message,
892
+ retryable: true,
893
+ details: {
894
+ operation: `connecta.${name}`,
895
+ stage: name === "result" ? "result storage" : "skill lookup",
896
+ elapsedMs,
897
+ deadlineMs: hostCallTimeoutMs,
898
+ },
899
+ }));
900
+ }
901
+ return typedFailure(error);
902
+ }))).then((value) => {
903
+ if (counted)
904
+ hostCalls.succeeded++;
905
+ return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
906
+ }, (err) => {
907
+ if (counted)
908
+ hostCalls.failed++;
909
+ if (budgetFailure)
910
+ return stopped;
911
+ throw redactAgentOutput(sentSecrets, err);
912
+ });
452
913
  // The executor owns this promise, and a program may abandon a call
453
914
  // that the run's end then cancels before anyone has awaited it.
454
915
  // The rejection is still there for whoever does; it is just not an
@@ -476,11 +937,12 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
476
937
  * ceiling as an untyped executor failure naming the key that sets it. Either
477
938
  * way the handler returns and releases the lease, and the abandoned call
478
939
  * keeps whatever it was doing: the QuickJS lease release recycles its child,
479
- * a Dynamic Worker isolate runs on to its own deadline, and a lease granted
480
- * after its request gave up goes to `late`. The result contract is untouched
940
+ * the Worker adapter disposes its handles without waiting for the guest. A
941
+ * lease granted after its request gave up goes to `late`. The result contract is untouched
481
942
  * — this only decides when to stop waiting for one.
482
943
  */
483
944
  function awaitExecutor(start, signal, options = {}) {
945
+ const started = Date.now();
484
946
  const contenders = [
485
947
  Effect.suspend(() => {
486
948
  let pending;
@@ -501,12 +963,21 @@ function awaitExecutor(start, signal, options = {}) {
501
963
  // which the executor's promise chain can still lose to.)
502
964
  fromSignal(signal).pipe(Effect.catch(() => Effect.sleep(Duration.millis(1))), Effect.andThen(Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled.")))),
503
965
  ];
966
+ if (options.terminal)
967
+ contenders.push(options.terminal);
504
968
  const { watchdog } = options;
505
969
  if (watchdog) {
506
970
  contenders.push(Effect.sleep(Duration.millis(watchdog.ms)).pipe(Effect.andThen(Effect.suspend(() => {
507
- watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", { watchdogMs: watchdog.ms });
508
- return Effect.fail(new Error(`sandbox unresponsive: no outcome within the ${watchdog.ms}ms ` +
509
- "execute.watchdogMs ceiling, so the run was abandoned"));
971
+ watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", {
972
+ watchdogMs: watchdog.ms,
973
+ });
974
+ const elapsedMs = Date.now() - started;
975
+ return Effect.fail(new InvocationFailure({
976
+ code: "timeout",
977
+ message: `Operation "execute_code" timed out during sandbox watchdog after ${elapsedMs}ms (effective deadline ${watchdog.ms}ms); sandbox unresponsive.`,
978
+ retryable: false,
979
+ details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs: watchdog.ms },
980
+ }));
510
981
  }))));
511
982
  }
512
983
  return Effect.raceAllFirst(contenders);
@@ -540,44 +1011,40 @@ function runSignal(caller) {
540
1011
  * which workerd refuses for any I/O. Awaiting the promise resumes it here.
541
1012
  */
542
1013
  function leased(executor, signal) {
543
- return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal }), signal, {
1014
+ return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal, wait: true }), signal, {
544
1015
  late: (lease) => lease.release(),
545
1016
  }), (lease) => Effect.sync(() => lease.release()));
546
1017
  }
547
- /** Room for the executor queue a run waits in before its watchdog starts. */
548
- const RUN_CLAIM_SLACK_MS = 10_000;
549
- /**
550
- * How long one run may take before whoever claimed it may give up on it: the
551
- * watchdog, one more host-call deadline for an exempt write the play drains,
552
- * and queue slack. Artifact refresh leases its claim for this long.
553
- */
554
- export function runClaimMs(config) {
555
- return resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS) +
556
- resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS) +
557
- RUN_CLAIM_SLACK_MS;
558
- }
559
- /** The execute_code handler. Exported for direct testing and artifact refresh. */
1018
+ /** The execute_code handler. Exported for direct testing. */
560
1019
  export function createExecuteTool(registry, baseUrl, executor, logger, activity, config = {}) {
561
1020
  const watchdog = {
562
1021
  ms: resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS),
563
1022
  logger,
564
1023
  };
565
1024
  const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
566
- const play = (program, diagnosticsRequested, callerSignal) => Effect.suspend(() => {
567
- const diagnostics = diagnosticsRequested
568
- ? new ExecuteDiagnostics()
569
- : undefined;
1025
+ const play = (program, diagnosticsRequested, callerSignal, requestScope) => Effect.suspend(() => {
1026
+ const sentSecrets = sentSecretsForRequest(requestScope);
1027
+ const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
570
1028
  const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
571
- const invocationFailures = [];
572
- const exemptWrites = new ExemptWrites();
1029
+ const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
1030
+ const programWrites = new ProgramWrites();
1031
+ const resultDependencies = [];
1032
+ const dispatchController = new AbortController();
1033
+ const terminal = Deferred.makeUnsafe();
1034
+ let budgetFailure;
1035
+ let executorLogs;
573
1036
  // The run's scope holds its signal and its lease. However the run
574
1037
  // ends — a result, a thrown executor, the watchdog, cancellation —
575
1038
  // closing it releases the lease and then aborts the signal, so
576
1039
  // nothing the run started outlives the request.
577
1040
  const run = Effect.gen(function* () {
1041
+ // Per-call signals cancel individual fetches, but do not close the
1042
+ // shared transport. Close its scope after the run's signal ends, on
1043
+ // every exit, using the existing bounded cleanup and deferred tail.
1044
+ yield* Effect.addFinalizer(() => Effect.forEach(registry.listConnectors(), (connector) => closeScope(connector, registry.contextFor(connector.id, baseUrl, requestScope), config.defer), { concurrency: "unbounded", discard: true }));
578
1045
  const signal = yield* runSignal(callerSignal);
579
1046
  // Admission comes before provider construction: queued calls retain
580
- // no catalogs, request scopes, or provider closures.
1047
+ // no catalogs or provider closures.
581
1048
  let lease;
582
1049
  if (isAdmittingExecutor(executor)) {
583
1050
  lease = yield* timed((elapsed) => {
@@ -594,11 +1061,12 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
594
1061
  if (diagnostics)
595
1062
  diagnostics.setupMs = elapsed;
596
1063
  }, Effect.sync(() => sandboxProvider(registry, baseUrl, activity, {
1064
+ sentSecrets,
597
1065
  signal,
598
- onInvocationFailure: (failure) => {
599
- invocationFailures.push(failure);
600
- if (invocationFailures.length > 64)
601
- invocationFailures.shift();
1066
+ hostCalls,
1067
+ onHostCallBudgetExceeded: (failure) => {
1068
+ budgetFailure = failure;
1069
+ Deferred.doneUnsafe(terminal, Effect.fail(failure));
602
1070
  },
603
1071
  emitCollector: emitted,
604
1072
  ...(diagnostics ? { diagnostics } : {}),
@@ -606,62 +1074,111 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
606
1074
  probeTimeoutMs: config.probeTimeoutMs,
607
1075
  maxHostCalls: config.maxHostCalls,
608
1076
  hostCallTimeoutMs,
1077
+ defaultToolTimeoutMs: config.defaultToolTimeoutMs,
1078
+ programDeadlineAt: Date.now() + watchdog.ms,
609
1079
  defer: config.defer,
610
- approval: config.approval,
1080
+ trust: config.trust,
611
1081
  maxWrites: config.maxWrites,
612
- exemptWrites,
613
- })));
1082
+ programWrites,
1083
+ resultDependencies,
1084
+ dispatchController,
1085
+ }, requestScope)));
614
1086
  if (signal.aborted) {
615
1087
  return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
616
1088
  }
617
1089
  const admitted = lease;
618
- // An exempt write may still be on the wire when the program settles:
1090
+ // A trusted-pool write may still be on the wire when the program settles:
619
1091
  // close, so a write gated after this is not sent, then let the ones
620
1092
  // already dispatched finish before the scope aborts them — an
621
1093
  // abandoned write would be one whose outcome nobody knows.
622
1094
  return yield* timed((elapsed) => {
623
1095
  if (diagnostics)
624
1096
  diagnostics.executorWallMs = elapsed;
625
- }, awaitExecutor(() => admitted
626
- ? admitted.execute(program, [provider])
627
- : executor.execute(program, [provider]), signal, { watchdog }).pipe(Effect.ensuring(Effect.suspend(() => {
628
- exemptWrites.close();
629
- return exemptWrites.drain();
1097
+ }, awaitExecutor(() => (admitted
1098
+ ? admitted.execute(wrapGuestProgram(program), [provider])
1099
+ : executor.execute(wrapGuestProgram(program), [provider])).then((outcome) => {
1100
+ executorLogs = sentSecrets.redact(outcome?.logs, "prose");
1101
+ return sentSecrets.redact(outcome);
1102
+ }, (err) => {
1103
+ if (err !== null && typeof err === "object" && "logs" in err) {
1104
+ executorLogs = sentSecrets.redact(err.logs, "prose");
1105
+ }
1106
+ throw sentSecrets.redact(err);
1107
+ }), signal, { watchdog, terminal: Deferred.await(terminal) }).pipe(Effect.ensuring(Effect.suspend(() => {
1108
+ programWrites.close();
1109
+ dispatchController.abort();
1110
+ return programWrites.drain();
630
1111
  }))));
631
1112
  });
632
- const reported = { emitted, diagnostics, invocationFailures };
633
- return Effect.map(Effect.exit(Effect.scoped(run)), (exit) => {
634
- if (Exit.isFailure(exit)) {
635
- return exemptWrites.finish(failedRun(Cause.squash(exit.cause), logger, reported));
636
- }
637
- const finished = finishedRun(exit.value, reported);
638
- if (config.failOnInvocationFailure && invocationFailures.length > 0) {
639
- const refusal = invocationFailures[0];
640
- return failureResponse(refusal.details.message, {
641
- code: refusal.details,
1113
+ const reported = {
1114
+ emitted,
1115
+ diagnostics,
1116
+ program,
1117
+ guardResult: (value, maxChars) => guardProgramReturn(value, registry, baseUrl, [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()], sentSecrets, config.trust, logger, maxChars, programWrites.hasSucceeded),
1118
+ };
1119
+ const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
1120
+ // Releasing a QuickJS lease ends its child and rejects execute() with
1121
+ // the retained log prefix. Give that report one timer turn, just as
1122
+ // cancellation does in awaitExecutor; never wait on a Worker isolate.
1123
+ budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
1124
+ return Effect.map(Effect.flatMap(exited, (exit) => {
1125
+ // A synchronous fire-and-forget burst can also settle its executor in
1126
+ // this turn. The host's terminal refusal always wins over that value.
1127
+ if (budgetFailure instanceof HostCallBudgetExceeded) {
1128
+ const failed = failureResponse(budgetFailure.details.message, {
1129
+ code: budgetFailure.details,
1130
+ logs: executeLogs(executorLogs),
1131
+ emitted,
1132
+ diagnostics,
642
1133
  });
1134
+ return Effect.succeed(failed);
643
1135
  }
644
- return exemptWrites.finish(finished);
645
- });
1136
+ if (Exit.isFailure(exit)) {
1137
+ return Effect.succeed(failedRun(redactAgentOutput(sentSecrets, Cause.squash(exit.cause)), logger, reported));
1138
+ }
1139
+ return Effect.promise(() => finishedRun(redactAgentOutput(sentSecrets, exit.value), reported));
1140
+ }), (unfinished) => {
1141
+ // Every run response passes through write accounting.
1142
+ const response = programWrites.finish(unfinished);
1143
+ // Calls abandoned by the guest are failed when the run cancels them.
1144
+ const counts = { ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded };
1145
+ response.structuredContent = { ...response.structuredContent, hostCalls: counts };
1146
+ if (response.content[0]?.type === "text")
1147
+ response.content[0].text = JSON.stringify(response.structuredContent);
1148
+ return response;
1149
+ }).pipe(Effect.map((result) => redactAgentOutput(sentSecrets, result)));
646
1150
  });
647
- return ({ code, diagnostics }, options = {}) => {
648
- // A code-unit count above the cap is already too large in UTF-8. Check
649
- // that first so a huge direct-call string is never encoded in full.
650
- if (code.length > EXECUTE_MAX_CODE_BYTES ||
651
- new TextEncoder().encode(code).byteLength > EXECUTE_MAX_CODE_BYTES) {
652
- const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
653
- return Promise.resolve(failureResponse(message, {
654
- code: { code: "invalid_args", message, retryable: false },
655
- }));
656
- }
657
- return runEdge(Effect.suspend(() => play(normalizeProgramSource(code), diagnostics, options.signal)));
658
- };
1151
+ return agentOutputOperations((requestScope) => ({
1152
+ execute: ({ code, diagnostics }, options = {}) => {
1153
+ // A code-unit count above the cap is already too large in UTF-8. Check
1154
+ // that first so a huge direct-call string is never encoded in full.
1155
+ if (code.length > EXECUTE_MAX_CODE_BYTES ||
1156
+ new TextEncoder().encode(code).byteLength > EXECUTE_MAX_CODE_BYTES) {
1157
+ const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
1158
+ const response = failureResponse(message, { code: { code: "invalid_args", message, retryable: false } });
1159
+ response.structuredContent = {
1160
+ ...response.structuredContent,
1161
+ hostCalls: { attempted: 0, admitted: 0, succeeded: 0, failed: 0 },
1162
+ };
1163
+ response.content[0].text = JSON.stringify(response.structuredContent);
1164
+ return Promise.resolve(response);
1165
+ }
1166
+ return runEdge(Effect.suspend(() => play(normalizeProgramSource(code), diagnostics, options.signal, requestScope)));
1167
+ },
1168
+ }), config.requestScope).execute;
659
1169
  }
660
1170
  /** An execution that never produced an ExecuteResult, as the model sees it. */
661
- function failedRun(err, logger, { emitted, diagnostics }) {
662
- const logs = err !== null && typeof err === "object" && "logs" in err
663
- ? executeLogs(err.logs)
664
- : undefined;
1171
+ function failedRun(err, logger, { emitted, diagnostics, program }) {
1172
+ const logs = err !== null && typeof err === "object" && "logs" in err ? executeLogs(err.logs) : undefined;
1173
+ if (err instanceof InvocationFailure)
1174
+ return failureResponse(err.details.message, { logs, emitted, diagnostics, code: err.details });
1175
+ if (err instanceof SyntaxError)
1176
+ return failureResponse(err.message, {
1177
+ logs,
1178
+ emitted,
1179
+ diagnostics,
1180
+ code: programError({ name: "SyntaxError", message: err.message }, program),
1181
+ });
665
1182
  if (err instanceof ExecutorAdmissionError) {
666
1183
  if (err.code === "executor_overloaded") {
667
1184
  logger.warn("[connecta] execute_code admission rejected", {
@@ -677,9 +1194,7 @@ function failedRun(err, logger, { emitted, diagnostics }) {
677
1194
  code: err.code,
678
1195
  message: err.message,
679
1196
  retryable: err.retryable,
680
- ...(err.retryAfterMs !== undefined
681
- ? { retryAfterMs: err.retryAfterMs }
682
- : {}),
1197
+ ...(err.retryAfterMs !== undefined ? { retryAfterMs: err.retryAfterMs } : {}),
683
1198
  },
684
1199
  });
685
1200
  }
@@ -691,7 +1206,7 @@ function failedRun(err, logger, { emitted, diagnostics }) {
691
1206
  });
692
1207
  }
693
1208
  /** The response for an ExecuteResult: the program's error or its value. */
694
- function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
1209
+ async function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
695
1210
  if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
696
1211
  return failureResponse("Executor failed: expected an ExecuteResult object.", {
697
1212
  emitted,
@@ -709,52 +1224,32 @@ function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
709
1224
  });
710
1225
  }
711
1226
  if (outcome.error !== undefined) {
712
- // Executor bridges necessarily reduce thrown host errors to strings.
713
- // Match that terminal string back to the request-local typed failure so
714
- // an unhandled tool failure keeps the same structured contract as
715
- // call_tool. Failures caught by model code never reach
716
- // outcome.error and therefore remain under that code's control.
717
- //
718
- // An error the program let through unchanged matches exactly, and an
719
- // exact match always wins: a program that wrapped one failure's message
720
- // around another's must not have the wrong type attached. Containment is
721
- // the fallback, so a wrapped message still reports its underlying type
722
- // rather than losing it to prose.
723
- let invocationFailure;
724
- for (const match of [
725
- (candidate) => outcome.error !== "" &&
726
- [candidate.message, guestFailureFrames.get(candidate)].includes(outcome.error),
727
- (candidate) => [candidate.message, guestFailureFrames.get(candidate)].some((message) =>
728
- // E6: empty or tiny prose cannot identify a wrapped failure.
729
- message !== undefined &&
730
- message.length >= 8 &&
731
- outcome.error?.includes(message) === true),
732
- ]) {
733
- for (let i = invocationFailures.length - 1; i >= 0; i--) {
734
- const candidate = invocationFailures[i];
735
- if (candidate && match(candidate)) {
736
- invocationFailure = candidate;
737
- break;
738
- }
739
- }
740
- if (invocationFailure)
741
- break;
742
- }
743
- if (invocationFailure) {
744
- // E1/X11: return the same bounded details the guest received.
745
- return failureResponse(invocationFailure.details.message, {
1227
+ const failure = outcome.failure?.call;
1228
+ if (outcome.failure?.timeout) {
1229
+ const { elapsedMs, deadlineMs } = outcome.failure.timeout;
1230
+ const message = `Operation "execute_code" timed out during sandbox after ${elapsedMs}ms (effective deadline ${deadlineMs}ms).`;
1231
+ return failureResponse(message, {
746
1232
  logs,
747
1233
  emitted,
748
1234
  diagnostics,
749
- code: invocationFailure.details,
1235
+ code: {
1236
+ code: "timeout",
1237
+ message,
1238
+ retryable: false,
1239
+ details: { operation: "execute_code", stage: "sandbox", elapsedMs, deadlineMs },
1240
+ },
750
1241
  });
751
1242
  }
752
- const message = `Error: ${outcome.error || "Execution failed without an error message."}`;
753
- return failureResponse(message, {
1243
+ return failureResponse(failure?.message ?? outcome.error, {
754
1244
  logs,
755
1245
  emitted,
756
1246
  diagnostics,
757
- code: "executor_failed",
1247
+ code: failure ??
1248
+ programError({
1249
+ name: outcome.failure?.name ?? /^(SyntaxError|ReferenceError|TypeError|RangeError):/.exec(outcome.error)?.[1],
1250
+ message: outcome.error || "Execution failed without an error message.",
1251
+ ...(outcome.failure?.line !== undefined ? { line: outcome.failure.line - 1 } : {}),
1252
+ }, program),
758
1253
  });
759
1254
  }
760
1255
  // A result crossing back as a host BigInt (or otherwise unserializable
@@ -762,7 +1257,7 @@ function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
762
1257
  // error path so captured logs survive instead of a raw SDK 500.
763
1258
  let result;
764
1259
  try {
765
- result = guardExecuteResultValue(outcome.result);
1260
+ result = await guardResult(outcome.result, MAX_EXECUTE_RESULT_CHARS - emitted.textChars);
766
1261
  }
767
1262
  catch (err) {
768
1263
  const message = `Error: result is not JSON-serializable: ${msg(err)}`;
@@ -794,33 +1289,20 @@ function executeLogs(value) {
794
1289
  }
795
1290
  function failureResponse(message, options) {
796
1291
  const { logs, emitted, diagnostics, code } = options;
797
- if (diagnostics || typeof code !== "string") {
798
- const result = jsonResult({
799
- error: typeof code === "string"
800
- ? { code, message, retryable: false }
801
- : code,
802
- ...(logs ? { logs } : {}),
803
- ...(emitted ? discardedEmits(emitted) : {}),
804
- ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
805
- });
806
- result.isError = true;
807
- return result;
808
- }
809
- return errorResult(`${message}${logs ? `\n\nLogs:\n${logs}` : ""}${emitted ? discardedEmitsText(emitted) : ""}`);
1292
+ const result = jsonResult({
1293
+ error: typeof code === "string" ? { code, message, retryable: false } : code,
1294
+ ...(logs ? { logs } : {}),
1295
+ ...(emitted ? discardedEmits(emitted) : {}),
1296
+ ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
1297
+ });
1298
+ result.isError = true;
1299
+ return result;
810
1300
  }
811
1301
  /**
812
1302
  * A failed program delivers no blocks and reports how many were discarded.
813
1303
  */
814
1304
  function discardedEmits(emitted) {
815
- return emitted.blocks.length > 0
816
- ? { emittedDiscarded: emitted.blocks.length }
817
- : {};
818
- }
819
- /** The same visibility for the plain-text error paths. */
820
- function discardedEmitsText(emitted) {
821
- return emitted.blocks.length > 0
822
- ? `\n\nemittedDiscarded: ${emitted.blocks.length}`
823
- : "";
1305
+ return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
824
1306
  }
825
1307
  function connectorInventory(connectors) {
826
1308
  const prefix = "Connectors: ";
@@ -829,14 +1311,10 @@ function connectorInventory(connectors) {
829
1311
  const entries = connectors.map((connector) => {
830
1312
  const address = connector.id;
831
1313
  const title = connector.title?.replace(/\s+/g, " ").trim();
832
- const label = title && title !== connector.id
833
- ? `${address}: ${boundedEchoText(title, 45)}`
834
- : address;
1314
+ const label = title && title !== connector.id ? `${address}: ${boundedEchoText(title, 45)}` : address;
835
1315
  if (!connectorGuide(connector))
836
1316
  return label;
837
- const requirement = connectorGuideRequired(connector)
838
- ? "required guide"
839
- : "guide";
1317
+ const requirement = connectorGuideRequired(connector) ? "required guide" : "guide";
840
1318
  return `${label} (${requirement} ${connectorSkillName(connector.id)})`;
841
1319
  });
842
1320
  const shown = [];
@@ -858,29 +1336,26 @@ function connectorInventory(connectors) {
858
1336
  return `${prefix}${shown.join(", ")}.`;
859
1337
  return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
860
1338
  }
861
- const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites) => `Use the configured services below to answer the task. A known address uses call_tool. Unknown-address and wider read-only work uses one execute_code program for discovery, calls, and reduction. Do not return catalog matches alone. Only readOnlyHint: true tools and config-exempt writes are available; any other write goes through call_destructive_tool. Limits: ${hostLimits.maxHostCalls} host calls, ${maxWrites} exempt writes, ${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
862
-
1339
+ const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, trust) => `One known read: call_tool. One known write: call_destructive_tool. Everything else: execute_code. ${trust === "trusted" ? "Trusted pool: programs may read and write; the host approves the program as a write." : "Read-only pool: programs read; writes use call_destructive_tool."} Limits: ${hostLimits.maxHostCalls} host calls, ${maxWrites} writes, ${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
863
1340
  ${connectorInventory(connectors)}
864
1341
 
865
- Read guides with top-level skills. Write async () => { ... } using the global connecta:
866
- - connecta.search({ connector, query, safety: "readOnly", includeSchemas: "json" }) returns { tools }. Search operations separately; choose by connectorTitle and schema.required/.properties, never guessed fields. Compact schemas are text.
867
- - connecta.describe({ address }) clarifies schemas.
868
- - connecta.call(address, args) returns the provider value directly.
869
- - Use Promise.all for independent calls, or Promise.allSettled to retain failures. Check status; missing values are unknown, never false or zero.
870
- - connecta.emit(block): { type: "text", text } or { type: "image" | "audio", data (base64), mimeType }; success-only, ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. console.log(...) is captured.
1342
+ Use async () => { ... }, no arguments. No portable ambient capabilities. API:
1343
+ \`\`\`ts
1344
+ declare const connecta: {
1345
+ ${GUEST_API_DECLARATION}
1346
+ };
1347
+ \`\`\`
1348
+ Search: {tools}, catalogErrors. JSON schemas default; compact text. Calls: {data, format:"json"|"text"}; >1 MiB: paged handle. Oversized returns: resultId/nextAction. connecta.result pages; UTF-8 byte offsets. allSettled keeps typed errors; host budget_exceeded ends the run.
871
1349
 
872
- No portable ambient capabilities. Return reduced JSON. Sample unfamiliar reads. Reduce a one-time write's full result here, or use a direct call and get_result paging; never repeat it to recover output. Never guess fields or use the whole text as an id. Top-level skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}; skills({ name: "investigate" }): task planning.`;
873
- // Module scope, like the other six meta-tool inputs: its JSON Schema is
1350
+ Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks. Authored: text/base64 image/audio; no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap. skills({ name: "investigate" }): workflow; skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}.`;
1351
+ // Module scope, like the other five meta-tool inputs: its JSON Schema is
874
1352
  // derived once per process. Budgets and connectors vary by deployment and
875
1353
  // view, so they live in the description, never here.
876
1354
  const EXECUTE_INPUT = advertisedSchema(z.object({
877
1355
  code: z
878
1356
  .string()
879
1357
  .describe("One complete zero-argument JavaScript async arrow: async () => { ... }. Use the provided connecta global to discover, call, and return the reduced answer. At most 65,536 UTF-8 bytes."),
880
- diagnostics: z
881
- .boolean()
882
- .optional()
883
- .describe("Add request-local, payload-free timing and result-size summaries."),
1358
+ diagnostics: z.boolean().optional().describe("Add request-local, payload-free timing and result-size summaries."),
884
1359
  }));
885
1360
  /** Register the execute_code meta-tool. Only called when an executor is configured. */
886
1361
  export function registerExecuteTool(server, registry, ctx) {
@@ -899,6 +1374,7 @@ export function registerExecuteTool(server, registry, ctx) {
899
1374
  const connectors = registry.listConnectors();
900
1375
  const maxWrites = resolveBudget(ctx.maxWrites, DEFAULT_MAX_WRITES);
901
1376
  const execute = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
1377
+ defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
902
1378
  discoveryConcurrency: ctx.discoveryConcurrency,
903
1379
  probeTimeoutMs: ctx.probeTimeoutMs,
904
1380
  maxEmittedBytes: emitBudgets.maxBytes,
@@ -907,25 +1383,29 @@ export function registerExecuteTool(server, registry, ctx) {
907
1383
  hostCallTimeoutMs: hostLimits.hostCallTimeoutMs,
908
1384
  watchdogMs: ctx.watchdogMs,
909
1385
  defer: ctx.defer,
910
- approval: ctx.approval,
1386
+ trust: ctx.trust,
911
1387
  maxWrites,
1388
+ requestScope: ctx.requestScope,
912
1389
  });
913
1390
  server.registerTool("execute_code", {
914
- description: executeDescription(emitBudgets, hostLimits, hasConnectorGuides(connectors), connectors, maxWrites),
1391
+ description: executeDescription(emitBudgets, {
1392
+ ...hostLimits,
1393
+ hostCallTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1394
+ }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.trust ?? "read-only"),
915
1395
  inputSchema: EXECUTE_INPUT,
916
- // This hint describes connector calls: only explicitly read-only ones
917
- // run here, plus the writes config exempts from asking (W12) — the
918
- // deployment's decision, which no annotation can make for it. Any
919
- // other write is refused (E4) and crosses call_destructive_tool, which
920
- // is annotated destructive. The supported executor constructions deny
921
- // outbound access, filesystem, and deployment config; X5 documents
922
- // Dynamic runtime modules separately.
1396
+ outputSchema: EXECUTE_OUTPUT,
1397
+ // The host sees execute_code as a write only on a trusted endpoint.
923
1398
  annotations: {
924
- readOnlyHint: true,
925
- destructiveHint: false,
1399
+ readOnlyHint: ctx.trust !== "trusted",
1400
+ destructiveHint: ctx.trust === "trusted",
926
1401
  openWorldHint: true,
927
1402
  },
928
- }, (args, extra) => underAnySignal([extra.mcpReq.signal, ctx.requestSignal], (signal) => execute(args, {
929
- signal,
930
- })));
1403
+ }, (args, extra) => {
1404
+ if (ctx.client)
1405
+ bindMcpClient(extra, ctx.client, ctx.activity);
1406
+ const run = () => underAnySignal([extra.mcpReq.signal, ctx.requestSignal], (signal) => execute(args, {
1407
+ signal,
1408
+ }));
1409
+ return ctx.authElicitation ? ctx.authElicitation.run("execute_code", args, extra, run) : run();
1410
+ });
931
1411
  }