@zackbart/connecta 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/CHANGELOG.md +788 -5
  2. package/README.md +62 -42
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1584 -1580
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +93 -71
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +59 -7
  52. package/dist/connectors/api-connector.js +106 -19
  53. package/dist/connectors/api.js +4 -1
  54. package/dist/connectors/guarded-fetch.d.ts +144 -0
  55. package/dist/connectors/guarded-fetch.js +84 -35
  56. package/dist/connectors/negotiation-cache.js +46 -0
  57. package/dist/connectors/option-shapes.js +27 -0
  58. package/dist/connectors/remote-mcp.d.ts +30 -4
  59. package/dist/connectors/remote-mcp.js +1774 -320
  60. package/dist/connectors/resource-uri.js +335 -0
  61. package/dist/credential-contract.d.ts +10 -0
  62. package/dist/credential-rules.js +1 -3
  63. package/dist/credential-url.js +108 -0
  64. package/dist/credentials.d.ts +6 -1
  65. package/dist/credentials.js +27 -18
  66. package/dist/d1.d.ts +130 -0
  67. package/dist/d1.js +290 -0
  68. package/dist/describe-config.d.ts +165 -0
  69. package/dist/describe-config.js +315 -0
  70. package/dist/described.js +106 -0
  71. package/dist/downstream-input-context.js +119 -0
  72. package/dist/downstream-input.js +323 -0
  73. package/dist/errors.d.ts +97 -20
  74. package/dist/errors.js +158 -80
  75. package/dist/execute.js +453 -305
  76. package/dist/executor-admission.d.ts +1 -0
  77. package/dist/executor-admission.js +11 -15
  78. package/dist/executor-contract.d.ts +20 -0
  79. package/dist/executor-contract.js +48 -0
  80. package/dist/executor-result.js +24 -9
  81. package/dist/executors/quickjs-child.js +3 -4
  82. package/dist/executors/quickjs-runtime.js +147 -59
  83. package/dist/executors/quickjs.js +29 -35
  84. package/dist/favicon.js +1 -2
  85. package/dist/fix-prompt.js +1 -3
  86. package/dist/guest-runtime.js +188 -0
  87. package/dist/guest-types.d.ts +176 -0
  88. package/dist/guest-types.js +1 -0
  89. package/dist/html-security.js +24 -0
  90. package/dist/identity.js +1 -3
  91. package/dist/inbound-credential.js +52 -0
  92. package/dist/index.d.ts +24 -256
  93. package/dist/index.js +65 -478
  94. package/dist/invocation-auth.js +69 -0
  95. package/dist/invocation.js +230 -73
  96. package/dist/json-schema.d.ts +1 -1
  97. package/dist/mcp-client-context.js +35 -0
  98. package/dist/mcp-result.js +11 -7
  99. package/dist/meta-output.js +76 -0
  100. package/dist/meta-tool-names.d.ts +2 -0
  101. package/dist/meta-tool-names.js +3 -0
  102. package/dist/meta-tools.js +285 -209
  103. package/dist/module-contracts.d.ts +8 -23
  104. package/dist/node.d.ts +0 -2
  105. package/dist/node.js +2 -2
  106. package/dist/oauth-callback-outcome.js +1 -3
  107. package/dist/oauth-handoff.js +117 -0
  108. package/dist/oauth-sealing.js +20 -0
  109. package/dist/operator-record.d.ts +137 -0
  110. package/dist/operator-record.js +420 -0
  111. package/dist/operator-ui/assets.js +31 -0
  112. package/dist/operator-ui/contract.d.ts +72 -0
  113. package/dist/operator-ui/contract.js +1 -0
  114. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  115. package/dist/operator-ui/generated.js +5 -4
  116. package/dist/operator-ui/model.d.ts +12 -87
  117. package/dist/operator-ui/model.js +2 -9
  118. package/dist/operator-ui/setup-commands.js +1 -3
  119. package/dist/operator-ui/view.d.ts +323 -0
  120. package/dist/operator-ui/view.js +62 -189
  121. package/dist/page-styles.js +2 -3
  122. package/dist/program-source.js +14 -2
  123. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  124. package/dist/provider.d.ts +142 -0
  125. package/dist/provider.js +187 -0
  126. package/dist/providers/_shared/google/result-size.js +57 -0
  127. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  128. package/dist/providers/_shared/google/service-account.js +500 -0
  129. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  130. package/dist/providers/_shared/google/workspace.js +518 -0
  131. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  132. package/dist/providers/basecamp/index.js +997 -0
  133. package/dist/providers/basecamp/skill.generated.js +9 -0
  134. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  135. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  136. package/dist/providers/breeze/skill.generated.js +9 -0
  137. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  138. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  139. package/dist/providers/ccb/skill.generated.js +9 -0
  140. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  141. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  142. package/dist/providers/cloudflare/skill.generated.js +10 -0
  143. package/dist/providers/docs/index.d.ts +54 -0
  144. package/dist/providers/docs/index.js +1237 -0
  145. package/dist/providers/docs/skill.generated.js +9 -0
  146. package/dist/providers/drive/index.d.ts +53 -0
  147. package/dist/providers/drive/index.js +1523 -0
  148. package/dist/providers/drive/skill.generated.js +9 -0
  149. package/dist/providers/forms/index.d.ts +54 -0
  150. package/dist/providers/forms/index.js +1217 -0
  151. package/dist/providers/forms/skill.generated.js +11 -0
  152. package/dist/providers/github/auth.d.ts +32 -0
  153. package/dist/providers/github/auth.js +361 -0
  154. package/dist/providers/github/hosted.js +218 -0
  155. package/dist/providers/github/index.d.ts +13 -0
  156. package/dist/providers/github/index.js +140 -0
  157. package/dist/providers/github/key.js +100 -0
  158. package/dist/providers/github/rest.js +321 -0
  159. package/dist/providers/github/scope.d.ts +29 -0
  160. package/dist/providers/github/scope.js +124 -0
  161. package/dist/providers/github/skill.generated.js +8 -0
  162. package/dist/providers/gmail/index.d.ts +51 -0
  163. package/dist/providers/gmail/index.js +1436 -0
  164. package/dist/providers/gmail/skill.generated.js +9 -0
  165. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  166. package/dist/providers/linear/index.js +352 -0
  167. package/dist/providers/linear/skill.generated.js +8 -0
  168. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  169. package/dist/providers/mixpanel/index.js +416 -0
  170. package/dist/providers/mixpanel/skill.generated.js +9 -0
  171. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  172. package/dist/providers/{notion.js → notion/index.js} +226 -239
  173. package/dist/providers/notion/skill.generated.js +10 -0
  174. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  175. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  176. package/dist/providers/overflow/skill.generated.js +8 -0
  177. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  178. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  179. package/dist/providers/planning-center/skill.generated.js +10 -0
  180. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  181. package/dist/providers/revenuecat/index.js +557 -0
  182. package/dist/providers/revenuecat/skill.generated.js +11 -0
  183. package/dist/providers/sheets/index.d.ts +54 -0
  184. package/dist/providers/sheets/index.js +1237 -0
  185. package/dist/providers/sheets/skill.generated.js +10 -0
  186. package/dist/providers/slides/index.d.ts +53 -0
  187. package/dist/providers/slides/index.js +2645 -0
  188. package/dist/providers/slides/skill.generated.js +9 -0
  189. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  190. package/dist/providers/stripe/index.js +266 -0
  191. package/dist/providers/stripe/skill.generated.js +11 -0
  192. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  193. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  194. package/dist/providers/tithely/skill.generated.js +8 -0
  195. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  196. package/dist/providers/vercel/index.js +1038 -0
  197. package/dist/providers/vercel/mcp-ownership.js +212 -0
  198. package/dist/providers/vercel/skill.generated.js +11 -0
  199. package/dist/registry.d.ts +108 -171
  200. package/dist/registry.js +513 -925
  201. package/dist/request-state.js +21 -0
  202. package/dist/resource-read.js +100 -0
  203. package/dist/result-shapes.js +17 -49
  204. package/dist/retired-toolkits.js +1 -1
  205. package/dist/routes/access-tokens.js +3 -6
  206. package/dist/routes/activity-context.js +21 -0
  207. package/dist/routes/activity-disclosure.js +40 -0
  208. package/dist/routes/activity.js +59 -21
  209. package/dist/routes/actor-labels.js +3 -50
  210. package/dist/routes/connect.js +221 -0
  211. package/dist/routes/credentials.js +15 -27
  212. package/dist/routes/mcp.js +356 -130
  213. package/dist/routes/oauth-client-metadata.js +15 -0
  214. package/dist/routes/oauth-management.js +45 -107
  215. package/dist/routes/oauth.js +110 -57
  216. package/dist/routes/operator.js +3 -3
  217. package/dist/routes/shared.d.ts +25 -64
  218. package/dist/routes/shared.js +160 -80
  219. package/dist/routes/skills.js +30 -0
  220. package/dist/routes/ui-config.js +225 -0
  221. package/dist/routes/ui-data.js +40 -35
  222. package/dist/routes/ui.js +66 -56
  223. package/dist/runtime/admission.js +4 -2
  224. package/dist/runtime/call-admission.js +3 -1
  225. package/dist/runtime/connector-scope.js +1 -4
  226. package/dist/runtime/run.js +10 -39
  227. package/dist/runtime/services.js +12 -117
  228. package/dist/runtime/shared-read.js +1 -3
  229. package/dist/runtime/storage.js +8 -1
  230. package/dist/sent-secrets.d.ts +52 -0
  231. package/dist/sent-secrets.js +474 -0
  232. package/dist/server.js +28 -71
  233. package/dist/skills.js +406 -174
  234. package/dist/sqlite.d.ts +48 -0
  235. package/dist/sqlite.js +166 -0
  236. package/dist/storage/keys.js +428 -0
  237. package/dist/storage/memory.js +7 -0
  238. package/dist/storage/sql.d.ts +66 -0
  239. package/dist/storage/sql.js +534 -0
  240. package/dist/timeout.js +4 -2
  241. package/dist/tool-name.js +17 -0
  242. package/dist/tool-safety.d.ts +9 -35
  243. package/dist/tool-safety.js +20 -40
  244. package/dist/types.d.ts +320 -54
  245. package/dist/types.js +0 -1
  246. package/dist/ui.d.ts +7 -19
  247. package/dist/ui.js +76 -90
  248. package/dist/url-safety.js +3 -6
  249. package/dist/usage-guide.js +137 -0
  250. package/dist/validate.d.ts +7 -0
  251. package/dist/validate.js +309 -47
  252. package/dist/version.d.ts +1 -7
  253. package/dist/version.js +3 -7
  254. package/dist/worker.d.ts +11 -0
  255. package/dist/worker.js +434 -0
  256. package/documentation/architecture.md +664 -231
  257. package/documentation/auth.md +936 -546
  258. package/documentation/code-mode.md +501 -312
  259. package/documentation/meta-tools.md +449 -200
  260. package/documentation/operator-tests.md +93 -0
  261. package/documentation/provider-migration-0.29.md +60 -0
  262. package/examples/worker/AGENTS.md +34 -12
  263. package/examples/worker/README.md +286 -255
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  266. package/examples/worker/src/connecta.config.ts +151 -0
  267. package/examples/worker/src/index.ts +14 -197
  268. package/examples/worker/wrangler.jsonc +15 -35
  269. package/package.json +120 -54
  270. package/templates/node/.dockerignore +1 -2
  271. package/templates/node/.env.example +20 -21
  272. package/templates/node/AGENTS.md +21 -17
  273. package/templates/node/Dockerfile +3 -5
  274. package/templates/node/README.md +133 -58
  275. package/templates/node/docker-compose.yml +11 -12
  276. package/templates/node/package.json +4 -2
  277. package/templates/node/src/connecta.config.ts +119 -0
  278. package/templates/node/src/index.ts +9 -154
  279. package/templates/node/src/provision-token.ts +17 -0
  280. package/AGENTS.md +0 -170
  281. package/dist/artifacts/connector.d.ts +0 -58
  282. package/dist/artifacts/connector.js +0 -730
  283. package/dist/artifacts/document.js +0 -86
  284. package/dist/artifacts/guide.js +0 -140
  285. package/dist/artifacts/html-scan.js +0 -329
  286. package/dist/artifacts/json.js +0 -78
  287. package/dist/artifacts/kv-store.d.ts +0 -20
  288. package/dist/artifacts/kv-store.js +0 -140
  289. package/dist/artifacts/markdown.js +0 -478
  290. package/dist/artifacts/module.d.ts +0 -37
  291. package/dist/artifacts/module.js +0 -66
  292. package/dist/artifacts/operations.d.ts +0 -197
  293. package/dist/artifacts/operations.js +0 -1002
  294. package/dist/artifacts/refresh.d.ts +0 -55
  295. package/dist/artifacts/refresh.js +0 -236
  296. package/dist/artifacts/routes.js +0 -206
  297. package/dist/artifacts/types.d.ts +0 -214
  298. package/dist/artifacts/types.js +0 -22
  299. package/dist/artifacts/validate.d.ts +0 -62
  300. package/dist/artifacts/validate.js +0 -651
  301. package/dist/artifacts.d.ts +0 -6
  302. package/dist/artifacts.js +0 -9
  303. package/dist/auth/bearer.d.ts +0 -11
  304. package/dist/auth/bearer.js +0 -50
  305. package/dist/providers/basecamp.js +0 -445
  306. package/dist/providers/linear.js +0 -217
  307. package/dist/providers/mixpanel.js +0 -247
  308. package/dist/providers/revenuecat.js +0 -317
  309. package/dist/providers/stripe.js +0 -253
  310. package/dist/providers/vercel.js +0 -1253
  311. package/dist/storage/file.d.ts +0 -15
  312. package/dist/storage/file.js +0 -455
  313. package/ethos.md +0 -93
  314. package/examples/worker/src/cloudflare-kv.ts +0 -43
  315. package/examples/worker/src/d1-activity-row.ts +0 -155
  316. package/examples/worker/src/d1-activity.ts +0 -147
  317. package/examples/worker/src/d1-storage.ts +0 -138
  318. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  319. package/templates/node/src/file-activity.ts +0 -182
package/dist/execute.js CHANGED
@@ -1,21 +1,28 @@
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, jsonResult } from "./meta-tools.js";
9
+ import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText, } from "./executor-result.js";
10
+ import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor } from "./executor-admission.js";
11
+ import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
12
+ import { DEFAULT_MAX_WRITES, ProgramWrites, writeStateOf } from "./program-writes.js";
13
+ import { InvocationFailure, InvocationService, timed } from "./invocation.js";
14
+ import { guestPrelude, programError, wrapGuestProgram } from "./guest-runtime.js";
15
+ import { EXECUTE_OUTPUT } from "./meta-output.js";
11
16
  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";
17
+ import { readResource } from "./resource-read.js";
18
+ import { agentOutputOperations, redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
19
+ import { normalizeTimeoutMs, underAnySignal } from "./timeout.js";
20
+ import { fromSignal, runEdge, withDeadlineEffect } from "./runtime/run.js";
21
+ import { closeScope } from "./runtime/connector-scope.js";
22
+ import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
16
23
  /** 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;
24
+ const EXECUTE_MAX_HOST_CALLS = CONFIG_DEFAULTS.execute.maxHostCalls;
25
+ const EXECUTE_HOST_CALL_TIMEOUT_MS = CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
19
26
  /** Above every program in the recorded evals, below the QuickJS IPC ceiling. */
20
27
  const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
21
28
  /**
@@ -24,7 +31,7 @@ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
24
31
  * always reports its own timeout first and this fires only for one that
25
32
  * never settles at all.
26
33
  */
27
- const EXECUTE_WATCHDOG_MS = 120_000;
34
+ const EXECUTE_WATCHDOG_MS = CONFIG_DEFAULTS.execute.watchdogMs;
28
35
  /** Complete entries plus an exact omission count, all inside this byte cap. */
29
36
  export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
30
37
  /**
@@ -34,8 +41,9 @@ export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
34
41
  * two or three real screenshots after base64's 4/3 inflation, well short of a
35
42
  * file-hosting ambition (design record M5).
36
43
  */
37
- export const EXECUTE_MAX_EMITTED_BYTES = 4_000_000;
38
- export const EXECUTE_MAX_EMITTED_BLOCKS = 32;
44
+ export const EXECUTE_MAX_EMITTED_BYTES = CONFIG_DEFAULTS.execute.maxEmittedBytes;
45
+ export const EXECUTE_MAX_EMITTED_BLOCKS = CONFIG_DEFAULTS.execute.maxEmittedBlocks;
46
+ const CALL_SIGNATURE = "Use connecta.call(address, args?, { timeoutMs? }) or connecta.call({ address, args?, timeoutMs? }). address must be a canonical connector.tool string.";
39
47
  const diagnosticsEncoder = new TextEncoder();
40
48
  class ExecuteDiagnostics {
41
49
  started = Date.now();
@@ -85,6 +93,15 @@ class ExecuteDiagnostics {
85
93
  stats.catalogMs += outcome.timing.catalogMs;
86
94
  stats.connectorMs += outcome.timing.connectorMs;
87
95
  }
96
+ recordRead(durationMs, connectorMs, ok, value) {
97
+ const stats = this.stats("read");
98
+ stats.count++;
99
+ stats.failures += ok ? 0 : 1;
100
+ stats.durationMs += durationMs;
101
+ stats.connectorMs += connectorMs;
102
+ if (ok)
103
+ stats.resultBytes += serializedDiagnosticBytes(value);
104
+ }
88
105
  finish() {
89
106
  const operations = [...this.operations.values()];
90
107
  return {
@@ -130,6 +147,19 @@ function requireEmittedBlock(raw) {
130
147
  if (extra.length > 0) {
131
148
  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
149
  }
150
+ if (block.type === "image") {
151
+ if (!["image/png", "image/jpeg", "image/gif", "image/webp"].includes(block.mimeType)) {
152
+ throw guestFailure("invalid_args", "connecta.emit image mimeType must be image/png, image/jpeg, image/gif, or image/webp.");
153
+ }
154
+ const data = block.data;
155
+ // RFC 4648 alphabet, complete quads and canonical padding, including zero
156
+ // pad bits. Decode only the last quad; validation does not copy the image.
157
+ if (!data.length ||
158
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(data) ||
159
+ btoa(atob(data.slice(-4))) !== data.slice(-4)) {
160
+ throw guestFailure("invalid_args", "connecta.emit image data must be nonempty, strict base64 with canonical padding.");
161
+ }
162
+ }
133
163
  return raw;
134
164
  }
135
165
  export class EmitCollector {
@@ -138,6 +168,7 @@ export class EmitCollector {
138
168
  diagnostics;
139
169
  blocks = [];
140
170
  bytes = 0;
171
+ textChars = 0;
141
172
  constructor(maxBytes, maxBlocks, diagnostics) {
142
173
  this.maxBytes = maxBytes;
143
174
  this.maxBlocks = maxBlocks;
@@ -153,6 +184,12 @@ export class EmitCollector {
153
184
  if (this.bytes + size > this.maxBytes) {
154
185
  throw guestFailure("budget_exceeded", `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`);
155
186
  }
187
+ const textChars = block.type === "text" ? JSON.stringify(block).length : 0;
188
+ // Reserve space for a reduced result envelope even if text fills the budget.
189
+ if (this.textChars + textChars > MAX_EXECUTE_RESULT_CHARS - 512) {
190
+ throw guestFailure("result_too_large", "Emitted text shares the 24,000-character program result limit. Reduce the text before emitting it.");
191
+ }
192
+ this.textChars += textChars;
156
193
  this.blocks.push(block);
157
194
  this.bytes += size;
158
195
  this.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
@@ -160,16 +197,12 @@ export class EmitCollector {
160
197
  }
161
198
  /** A positive whole-number budget, or the default when the value is unusable. */
162
199
  function resolveBudget(value, fallback) {
163
- return typeof value === "number" && Number.isFinite(value) && value >= 1
164
- ? Math.trunc(value)
165
- : fallback;
200
+ return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.trunc(value) : fallback;
166
201
  }
167
202
  function serializedDiagnosticBytes(value) {
168
203
  try {
169
204
  const text = JSON.stringify(value);
170
- return text === undefined
171
- ? 0
172
- : diagnosticsEncoder.encode(text).byteLength;
205
+ return text === undefined ? 0 : diagnosticsEncoder.encode(text).byteLength;
173
206
  }
174
207
  catch {
175
208
  // The executor's normal result guard owns the error. Diagnostics must
@@ -180,16 +213,8 @@ function serializedDiagnosticBytes(value) {
180
213
  function guestFailure(code, message, retryable = false) {
181
214
  return new InvocationFailure({ code, message, retryable });
182
215
  }
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
216
  /**
192
- * E1/X11: bound the serialized frame, including JSON escapes, before transport.
217
+ * E1/X11: bound typed host failure metadata, including JSON escapes, before transport.
193
218
  * Keep optional recovery whole: clipping an address or echoed args could turn
194
219
  * recovery into a different call. Oversized metadata is omitted instead.
195
220
  */
@@ -216,67 +241,17 @@ function boundedGuestFailure(failure) {
216
241
  code: boundText(failure.details.code, 128),
217
242
  message: boundText(failure.details.message, 2_000),
218
243
  };
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.
244
+ // Keep typed metadata below the executor bridge bound.
221
245
  if (JSON.stringify(details).length > 3_700) {
222
246
  details = {
223
247
  code: details.code,
224
248
  message: details.message,
225
249
  retryable: details.retryable,
226
- ...(details.retryAfterMs !== undefined
227
- ? { retryAfterMs: details.retryAfterMs }
228
- : {}),
250
+ ...(details.retryAfterMs !== undefined ? { retryAfterMs: details.retryAfterMs } : {}),
229
251
  };
230
252
  }
231
253
  return new InvocationFailure(details);
232
254
  }
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
255
  /**
281
256
  * Expose one host provider and typed guest errors. Catalogs load only when
282
257
  * the program calls a tool or asks search/describe.
@@ -284,58 +259,78 @@ function guestErrorPrelude(failureSecret) {
284
259
  export async function buildSandboxProviders(registry, baseUrl, _logger, activity, limits = {}) {
285
260
  return [sandboxProvider(registry, baseUrl, activity, limits)];
286
261
  }
262
+ /** Numeric accounting only; no addresses, arguments, or results. */
263
+ class HostCallBudgetExceeded extends InvocationFailure {
264
+ hostCalls;
265
+ constructor(hostCalls, maxHostCalls) {
266
+ super({
267
+ code: "budget_exceeded",
268
+ message: `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`,
269
+ retryable: false,
270
+ });
271
+ this.hostCalls = hostCalls;
272
+ }
273
+ }
287
274
  /**
288
275
  * The `connecta` provider for one execution.
289
276
  *
290
277
  * Building it is synchronous and loads nothing. Each function is a Promise
291
278
  * 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.
279
+ * the budget, do the operation, and retain a typed failure for the
280
+ * executor bridge to rebuild inside the guest. Nothing is shared between those fibers
281
+ * but the budget counters and this request's catalog. Budget exhaustion ends
282
+ * the whole run, including calls the program never awaits.
296
283
  */
297
- function sandboxProvider(registry, baseUrl, activity, limits) {
284
+ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {}) {
285
+ const sentSecrets = sentSecretsForRequest(requestScope, limits.sentSecrets);
298
286
  // All host calls made by one execute_code invocation share a downstream
299
287
  // connection, while a later invocation receives a fresh request scope.
300
- const requestScope = {};
288
+ const hostAccessSignal = limits.dispatchController
289
+ ? AbortSignal.any([limits.dispatchController.signal, ...(limits.signal ? [limits.signal] : [])])
290
+ : limits.signal;
301
291
  const catalog = new CatalogService(registry, baseUrl, {
302
292
  requestScope,
293
+ requestSignal: hostAccessSignal,
303
294
  // A program that just missed an address cannot call search_tools.
304
295
  searchRoute: "connecta.search",
305
296
  concurrency: limits.discoveryConcurrency,
306
297
  probeTimeoutMs: limits.probeTimeoutMs,
307
298
  defer: limits.defer,
308
- approval: limits.approval,
309
299
  });
310
300
  const invocation = new InvocationService(registry, catalog, activity);
311
301
  const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
312
302
  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;
303
+ const { signal, diagnostics, programWrites } = limits;
304
+ const hostCalls = limits.hostCalls ?? { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
305
+ let budgetFailure;
306
+ let emitAttempts = 0;
307
+ const maxEmitAttempts = Math.max(EXECUTE_MAX_EMITTED_BLOCKS, limits.emitCollector?.maxBlocks ?? EXECUTE_MAX_EMITTED_BLOCKS) * 4;
308
+ // A rejected bridge promise is catchable in both sandboxes. End the host
309
+ // run instead and leave that bridge pending: an awaiting guest cannot catch
310
+ // the refusal and loop. Later calls, including emit, lose host access too.
311
+ const stopped = new Promise((_resolve, reject) => {
312
+ const ended = () => reject(new Error("The host run ended."));
313
+ if (signal?.aborted)
314
+ ended();
315
+ else
316
+ signal?.addEventListener("abort", ended, { once: true });
317
+ });
318
+ // Stay pending until the lease has disposed the sandbox, then settle the
319
+ // abandoned host RPC waits without retaining request resources forever.
320
+ stopped.catch(() => { });
322
321
  const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
323
322
  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) => ({
323
+ /** Budget and account for writes admitted by the pool trust decision. */
324
+ const invocationContext = (sending, timeoutMs) => ({
332
325
  source: "execute_code",
333
- timeoutMs: hostCallTimeoutMs,
326
+ sentSecrets,
327
+ timeoutMs,
334
328
  ...(signal !== undefined ? { requestSignal: signal } : {}),
329
+ ...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
335
330
  unwrapResult: true,
336
- gates: (target) => isApprovalExempt(approval, target.connector, target.toolName, target.definition),
337
- writeGate: (target) => Effect.sync(() => {
338
- if (exemptWrites?.isClosed) {
331
+ trust: limits.trust,
332
+ beforeWrite: (target) => Effect.sync(() => {
333
+ if (programWrites?.isClosed) {
339
334
  return {
340
335
  kind: "refuse",
341
336
  error: {
@@ -357,23 +352,53 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
357
352
  };
358
353
  }
359
354
  writes++;
360
- if (exemptWrites)
361
- sending.settle = exemptWrites.begin();
355
+ if (programWrites)
356
+ sending.settle = programWrites.begin();
362
357
  return { kind: "dispatch" };
363
358
  }),
364
359
  });
365
360
  // A call brings its own cancellation: the invocation pipeline reads the
366
361
  // run's signal, refuses a call that starts after it, and records the
367
362
  // 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
363
+ const call = (address, args, options) => Effect.gen(function* () {
364
+ let timeout;
365
+ if (address !== null && typeof address === "object" && !Array.isArray(address)) {
366
+ const request = address;
367
+ if (args !== undefined ||
368
+ options !== undefined ||
369
+ Object.keys(request).some((key) => !["address", "args", "timeoutMs"].includes(key))) {
370
+ return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
371
+ }
372
+ address = request.address;
373
+ args = request.args;
374
+ timeout = request.timeoutMs;
375
+ }
376
+ else if (options !== undefined) {
377
+ if (!options ||
378
+ typeof options !== "object" ||
379
+ Array.isArray(options) ||
380
+ Object.keys(options).some((key) => key !== "timeoutMs")) {
381
+ return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
382
+ }
383
+ timeout = options.timeoutMs;
384
+ }
385
+ if (typeof address !== "string" ||
386
+ (timeout !== undefined && (typeof timeout !== "number" || !Number.isSafeInteger(timeout) || timeout < 1))) {
387
+ return yield* Effect.fail(guestFailure("invalid_args", CALL_SIGNATURE));
388
+ }
389
+ const requestedMs = normalizeTimeoutMs(timeout) ??
390
+ normalizeTimeoutMs(limits.defaultToolTimeoutMs) ??
391
+ hostCallTimeoutMs;
392
+ const timeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
393
+ // A trusted-pool write settles here — unknown if the call never returned an
371
394
  // outcome.
372
395
  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"))));
396
+ const outcome = yield* invocation
397
+ .pipeline(address, sentSecrets.redact(args ?? {}), invocationContext(sending, timeoutMs))
398
+ .pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
374
399
  diagnostics?.recordCall(outcome);
375
400
  return outcome.ok
376
- ? outcome.value
401
+ ? { data: outcome.value, format: outcome.format }
377
402
  : yield* Effect.fail(new InvocationFailure(outcome.error));
378
403
  });
379
404
  // Discovery gets the same treatment from here (L2): once the run has
@@ -386,21 +411,71 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
386
411
  const cancelled = () => guestFailure("cancelled", `connecta.${operation} was cancelled because the run ended.`);
387
412
  const reading = Effect.tryPromise({
388
413
  try: read,
389
- catch: (err) => err instanceof DiscoveryPolicyError
390
- ? guestFailure(err.code, err.message)
391
- : err,
414
+ catch: (err) => (err instanceof DiscoveryPolicyError ? guestFailure(err.code, err.message) : err),
392
415
  });
393
- return spendHostCall.pipe(Effect.andThen(!signal
416
+ return (!hostAccessSignal
394
417
  ? reading
395
- : signal.aborted
418
+ : hostAccessSignal.aborted
396
419
  ? 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))));
420
+ : 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))));
401
421
  });
422
+ const meta = createMetaTools(registry, baseUrl, { trust: limits.trust, requestScope });
402
423
  const operations = {
403
424
  call,
425
+ result: (id, options, signal) => Effect.tryPromise({
426
+ try: async () => {
427
+ if (typeof id !== "string" ||
428
+ (!options && options !== undefined) ||
429
+ (options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
430
+ throw guestFailure("invalid_args", "Use connecta.result(id, { offset?, maxBytes? }).");
431
+ }
432
+ const args = (options ?? {});
433
+ if (Object.keys(args).some((key) => !["offset", "maxBytes"].includes(key)) ||
434
+ (args.offset !== undefined &&
435
+ (typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
436
+ (args.maxBytes !== undefined &&
437
+ (typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
438
+ throw guestFailure("invalid_args", "Use connecta.result(id, { offset?, maxBytes? }) with whole byte offsets >= 0 and maxBytes >= 1.");
439
+ }
440
+ const page = await meta.readResult({ id, ...args }, { signal: signal });
441
+ if (page.isError)
442
+ throw new InvocationFailure((page.structuredContent?.error ?? {
443
+ code: "not_found",
444
+ message: page.content[0]?.text ?? "Result unavailable.",
445
+ retryable: false,
446
+ }));
447
+ return page.structuredContent;
448
+ },
449
+ catch: (err) => err,
450
+ }),
451
+ skill: (name, _options, utilitySignal) => Effect.tryPromise({
452
+ try: async () => {
453
+ if (typeof name !== "string")
454
+ throw guestFailure("invalid_args", "Use connecta.skill(name) with an exact skill name.");
455
+ const skills = new SkillsRegistry(registry, baseUrl, {
456
+ requestScope,
457
+ requestSignal: utilitySignal ?? hostAccessSignal,
458
+ probeTimeoutMs: limits.probeTimeoutMs,
459
+ defer: limits.defer,
460
+ });
461
+ return { name, format: "text", text: await skills.text(name) };
462
+ },
463
+ catch: (err) => err instanceof ConnectorCallError ? guestFailure(err.code, err.message, err.code === "unavailable") : err,
464
+ }),
465
+ read: (uri) => Effect.suspend(() => {
466
+ const started = Date.now();
467
+ let connectorMs = 0;
468
+ return readResource(registry, uri, {
469
+ baseUrl,
470
+ requestScope,
471
+ sentSecrets,
472
+ timeoutMs: hostCallTimeoutMs,
473
+ onConnectorTime: (elapsed) => {
474
+ connectorMs += elapsed;
475
+ },
476
+ ...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
477
+ }).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordRead(Date.now() - started, connectorMs, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
478
+ }),
404
479
  // Emission is a provider function, never an ExecuteResult field —
405
480
  // that is what keeps the Executor contract untouched and parity
406
481
  // structural (M8). It spends no host-call budget (M7); its own
@@ -410,7 +485,7 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
410
485
  if (!limits.emitCollector) {
411
486
  throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
412
487
  }
413
- limits.emitCollector.accept(block);
488
+ limits.emitCollector.accept(sentSecrets.redact(block));
414
489
  },
415
490
  catch: (err) => err,
416
491
  }),
@@ -418,6 +493,7 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
418
493
  const args = (raw ?? {});
419
494
  const result = flatSearchResult(await catalog.search({
420
495
  ...args,
496
+ includeSchemas: args.includeSchemas ?? "json",
421
497
  includeSchemaKeys: args.includeSchemaKeys !== false,
422
498
  }));
423
499
  boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
@@ -425,30 +501,90 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
425
501
  }),
426
502
  describe: (raw) => discovery("describe", async () => {
427
503
  const args = (raw ?? {});
428
- const result = { tools: await catalog.describe(args) };
504
+ const result = { tools: await catalog.describe({ ...args, format: args.format ?? "json" }) };
429
505
  boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
430
506
  return result;
431
507
  }),
432
508
  };
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(() => {
509
+ // Recoverable failures remain typed at the host-owned executor bridge. Terminal host-budget exhaustion bypasses it.
510
+ // Anything that is not an InvocationFailure crosses unchanged.
511
+ const typedFailure = (err) => Effect.suspend(() => {
437
512
  if (!(err instanceof InvocationFailure))
438
513
  return Effect.fail(err);
439
- const failure = boundedGuestFailure(err);
440
- const frame = framedGuestFailure(failureSecret, failure);
441
- limits.onInvocationFailure?.(failure);
442
- return Effect.fail(frame);
514
+ const failure = boundedGuestFailure(sentSecrets.redact(err));
515
+ return Effect.fail(failure);
443
516
  });
444
517
  return {
445
518
  name: "connecta",
446
519
  // Trusted host code the program runs after and cannot undo.
447
- prelude: guestErrorPrelude(failureSecret),
520
+ prelude: guestPrelude(),
448
521
  fns: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
449
522
  name,
450
523
  (...args) => {
451
- const settled = runEdge(Effect.suspend(() => operation(...args)).pipe(Effect.catch(framed)));
524
+ if (budgetFailure)
525
+ return stopped;
526
+ // Invalid emits still allocate guest promises and typed rejections.
527
+ // Bound those attempts without consuming the invocation budget or discarding identities.
528
+ if (name === "emit" && ++emitAttempts > maxEmitAttempts) {
529
+ budgetFailure = guestFailure("budget_exceeded", `execute_code emission-attempt budget exceeded (${maxEmitAttempts} attempts maximum).`);
530
+ programWrites?.close();
531
+ limits.dispatchController?.abort();
532
+ limits.onHostCallBudgetExceeded?.(budgetFailure);
533
+ return stopped;
534
+ }
535
+ // L4/M7: spend on entry, before even invalid arguments are checked;
536
+ // emit has a separate budget and never spends this one.
537
+ const counted = name !== "emit";
538
+ if (counted && ++hostCalls.attempted > maxHostCalls) {
539
+ hostCalls.failed++;
540
+ budgetFailure = new HostCallBudgetExceeded(hostCalls, maxHostCalls);
541
+ programWrites?.close();
542
+ limits.dispatchController?.abort();
543
+ limits.onHostCallBudgetExceeded?.(budgetFailure);
544
+ return stopped;
545
+ }
546
+ if (counted)
547
+ hostCalls.admitted++;
548
+ const started = Date.now();
549
+ const utility = name === "result" || name === "skill";
550
+ const invoke = Effect.suspend(() => operation(...args));
551
+ const guarded = utility
552
+ ? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal)), {
553
+ timeoutMs: hostCallTimeoutMs,
554
+ ...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
555
+ timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
556
+ })
557
+ : invoke;
558
+ const settled = runEdge(guarded.pipe(Effect.catch((error) => {
559
+ if (utility && hostAccessSignal?.aborted)
560
+ return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
561
+ if (utility && error instanceof InvocationFailure && error.code === "timeout") {
562
+ const elapsedMs = Date.now() - started;
563
+ const message = `Operation "connecta.${name}" timed out during ${name === "result" ? "result storage" : "skill lookup"} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
564
+ return typedFailure(new InvocationFailure({
565
+ code: "timeout",
566
+ message,
567
+ retryable: true,
568
+ details: {
569
+ operation: `connecta.${name}`,
570
+ stage: name === "result" ? "result storage" : "skill lookup",
571
+ elapsedMs,
572
+ deadlineMs: hostCallTimeoutMs,
573
+ },
574
+ }));
575
+ }
576
+ return typedFailure(error);
577
+ }))).then((value) => {
578
+ if (counted)
579
+ hostCalls.succeeded++;
580
+ return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
581
+ }, (err) => {
582
+ if (counted)
583
+ hostCalls.failed++;
584
+ if (budgetFailure)
585
+ return stopped;
586
+ throw redactAgentOutput(sentSecrets, err);
587
+ });
452
588
  // The executor owns this promise, and a program may abandon a call
453
589
  // that the run's end then cancels before anyone has awaited it.
454
590
  // The rejection is still there for whoever does; it is just not an
@@ -476,11 +612,12 @@ function sandboxProvider(registry, baseUrl, activity, limits) {
476
612
  * ceiling as an untyped executor failure naming the key that sets it. Either
477
613
  * way the handler returns and releases the lease, and the abandoned call
478
614
  * 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
615
+ * the Worker adapter disposes its handles without waiting for the guest. A
616
+ * lease granted after its request gave up goes to `late`. The result contract is untouched
481
617
  * — this only decides when to stop waiting for one.
482
618
  */
483
619
  function awaitExecutor(start, signal, options = {}) {
620
+ const started = Date.now();
484
621
  const contenders = [
485
622
  Effect.suspend(() => {
486
623
  let pending;
@@ -501,12 +638,21 @@ function awaitExecutor(start, signal, options = {}) {
501
638
  // which the executor's promise chain can still lose to.)
502
639
  fromSignal(signal).pipe(Effect.catch(() => Effect.sleep(Duration.millis(1))), Effect.andThen(Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled.")))),
503
640
  ];
641
+ if (options.terminal)
642
+ contenders.push(options.terminal);
504
643
  const { watchdog } = options;
505
644
  if (watchdog) {
506
645
  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"));
646
+ watchdog.logger.warn("[connecta] execute_code executor did not settle; run abandoned", {
647
+ watchdogMs: watchdog.ms,
648
+ });
649
+ const elapsedMs = Date.now() - started;
650
+ return Effect.fail(new InvocationFailure({
651
+ code: "timeout",
652
+ message: `Operation "execute_code" timed out during sandbox watchdog after ${elapsedMs}ms (effective deadline ${watchdog.ms}ms); sandbox unresponsive.`,
653
+ retryable: false,
654
+ details: { operation: "execute_code", stage: "sandbox watchdog", elapsedMs, deadlineMs: watchdog.ms },
655
+ }));
510
656
  }))));
511
657
  }
512
658
  return Effect.raceAllFirst(contenders);
@@ -540,44 +686,39 @@ function runSignal(caller) {
540
686
  * which workerd refuses for any I/O. Awaiting the promise resumes it here.
541
687
  */
542
688
  function leased(executor, signal) {
543
- return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal }), signal, {
689
+ return Effect.acquireRelease(awaitExecutor(() => executor.acquire({ signal, wait: true }), signal, {
544
690
  late: (lease) => lease.release(),
545
691
  }), (lease) => Effect.sync(() => lease.release()));
546
692
  }
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. */
693
+ /** The execute_code handler. Exported for direct testing. */
560
694
  export function createExecuteTool(registry, baseUrl, executor, logger, activity, config = {}) {
561
695
  const watchdog = {
562
696
  ms: resolveBudget(config.watchdogMs, EXECUTE_WATCHDOG_MS),
563
697
  logger,
564
698
  };
565
699
  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;
700
+ const play = (program, diagnosticsRequested, callerSignal, requestScope) => Effect.suspend(() => {
701
+ const sentSecrets = sentSecretsForRequest(requestScope);
702
+ const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
570
703
  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();
704
+ const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
705
+ const programWrites = new ProgramWrites();
706
+ const dispatchController = new AbortController();
707
+ const terminal = Deferred.makeUnsafe();
708
+ let budgetFailure;
709
+ let executorLogs;
573
710
  // The run's scope holds its signal and its lease. However the run
574
711
  // ends — a result, a thrown executor, the watchdog, cancellation —
575
712
  // closing it releases the lease and then aborts the signal, so
576
713
  // nothing the run started outlives the request.
577
714
  const run = Effect.gen(function* () {
715
+ // Per-call signals cancel individual fetches, but do not close the
716
+ // shared transport. Close its scope after the run's signal ends, on
717
+ // every exit, using the existing bounded cleanup and deferred tail.
718
+ yield* Effect.addFinalizer(() => Effect.forEach(registry.listConnectors(), (connector) => closeScope(connector, registry.contextFor(connector.id, baseUrl, requestScope), config.defer), { concurrency: "unbounded", discard: true }));
578
719
  const signal = yield* runSignal(callerSignal);
579
720
  // Admission comes before provider construction: queued calls retain
580
- // no catalogs, request scopes, or provider closures.
721
+ // no catalogs or provider closures.
581
722
  let lease;
582
723
  if (isAdmittingExecutor(executor)) {
583
724
  lease = yield* timed((elapsed) => {
@@ -594,11 +735,12 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
594
735
  if (diagnostics)
595
736
  diagnostics.setupMs = elapsed;
596
737
  }, Effect.sync(() => sandboxProvider(registry, baseUrl, activity, {
738
+ sentSecrets,
597
739
  signal,
598
- onInvocationFailure: (failure) => {
599
- invocationFailures.push(failure);
600
- if (invocationFailures.length > 64)
601
- invocationFailures.shift();
740
+ hostCalls,
741
+ onHostCallBudgetExceeded: (failure) => {
742
+ budgetFailure = failure;
743
+ Deferred.doneUnsafe(terminal, Effect.fail(failure));
602
744
  },
603
745
  emitCollector: emitted,
604
746
  ...(diagnostics ? { diagnostics } : {}),
@@ -606,62 +748,105 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
606
748
  probeTimeoutMs: config.probeTimeoutMs,
607
749
  maxHostCalls: config.maxHostCalls,
608
750
  hostCallTimeoutMs,
751
+ defaultToolTimeoutMs: config.defaultToolTimeoutMs,
752
+ programDeadlineAt: Date.now() + watchdog.ms,
609
753
  defer: config.defer,
610
- approval: config.approval,
754
+ trust: config.trust,
611
755
  maxWrites: config.maxWrites,
612
- exemptWrites,
613
- })));
756
+ programWrites,
757
+ dispatchController,
758
+ }, requestScope)));
614
759
  if (signal.aborted) {
615
760
  return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
616
761
  }
617
762
  const admitted = lease;
618
- // An exempt write may still be on the wire when the program settles:
763
+ // A trusted-pool write may still be on the wire when the program settles:
619
764
  // close, so a write gated after this is not sent, then let the ones
620
765
  // already dispatched finish before the scope aborts them — an
621
766
  // abandoned write would be one whose outcome nobody knows.
622
767
  return yield* timed((elapsed) => {
623
768
  if (diagnostics)
624
769
  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();
770
+ }, awaitExecutor(() => (admitted
771
+ ? admitted.execute(wrapGuestProgram(program), [provider])
772
+ : executor.execute(wrapGuestProgram(program), [provider])).then((outcome) => {
773
+ executorLogs = sentSecrets.redact(outcome?.logs);
774
+ return sentSecrets.redact(outcome);
775
+ }, (err) => {
776
+ if (err !== null && typeof err === "object" && "logs" in err) {
777
+ executorLogs = sentSecrets.redact(err.logs);
778
+ }
779
+ throw sentSecrets.redact(err);
780
+ }), signal, { watchdog, terminal: Deferred.await(terminal) }).pipe(Effect.ensuring(Effect.suspend(() => {
781
+ programWrites.close();
782
+ dispatchController.abort();
783
+ return programWrites.drain();
630
784
  }))));
631
785
  });
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,
786
+ const reported = { emitted, diagnostics, program };
787
+ const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
788
+ // Releasing a QuickJS lease ends its child and rejects execute() with
789
+ // the retained log prefix. Give that report one timer turn, just as
790
+ // cancellation does in awaitExecutor; never wait on a Worker isolate.
791
+ budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
792
+ return Effect.map(Effect.map(exited, (exit) => {
793
+ // A synchronous fire-and-forget burst can also settle its executor in
794
+ // this turn. The host's terminal refusal always wins over that value.
795
+ if (budgetFailure instanceof HostCallBudgetExceeded) {
796
+ const failed = failureResponse(budgetFailure.details.message, {
797
+ code: budgetFailure.details,
798
+ logs: executeLogs(executorLogs),
799
+ emitted,
800
+ diagnostics,
642
801
  });
802
+ return failed;
643
803
  }
644
- return exemptWrites.finish(finished);
645
- });
804
+ if (Exit.isFailure(exit)) {
805
+ return failedRun(sentSecrets.redact(Cause.squash(exit.cause)), logger, reported);
806
+ }
807
+ return finishedRun(sentSecrets.redact(exit.value), reported);
808
+ }), (unfinished) => {
809
+ // Every run response passes through write accounting.
810
+ const response = programWrites.finish(unfinished);
811
+ // Calls abandoned by the guest are failed when the run cancels them.
812
+ const counts = { ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded };
813
+ response.structuredContent = { ...response.structuredContent, hostCalls: counts };
814
+ if (response.content[0]?.type === "text")
815
+ response.content[0].text = JSON.stringify(response.structuredContent);
816
+ return response;
817
+ }).pipe(Effect.map((result) => redactAgentOutput(sentSecrets, result)));
646
818
  });
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
- };
819
+ return agentOutputOperations((requestScope) => ({
820
+ execute: ({ code, diagnostics }, options = {}) => {
821
+ // A code-unit count above the cap is already too large in UTF-8. Check
822
+ // that first so a huge direct-call string is never encoded in full.
823
+ if (code.length > EXECUTE_MAX_CODE_BYTES ||
824
+ new TextEncoder().encode(code).byteLength > EXECUTE_MAX_CODE_BYTES) {
825
+ const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
826
+ const response = failureResponse(message, { code: { code: "invalid_args", message, retryable: false } });
827
+ response.structuredContent = {
828
+ ...response.structuredContent,
829
+ hostCalls: { attempted: 0, admitted: 0, succeeded: 0, failed: 0 },
830
+ };
831
+ response.content[0].text = JSON.stringify(response.structuredContent);
832
+ return Promise.resolve(response);
833
+ }
834
+ return runEdge(Effect.suspend(() => play(normalizeProgramSource(code), diagnostics, options.signal, requestScope)));
835
+ },
836
+ }), config.requestScope).execute;
659
837
  }
660
838
  /** 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;
839
+ function failedRun(err, logger, { emitted, diagnostics, program }) {
840
+ const logs = err !== null && typeof err === "object" && "logs" in err ? executeLogs(err.logs) : undefined;
841
+ if (err instanceof InvocationFailure)
842
+ return failureResponse(err.details.message, { logs, emitted, diagnostics, code: err.details });
843
+ if (err instanceof SyntaxError)
844
+ return failureResponse(err.message, {
845
+ logs,
846
+ emitted,
847
+ diagnostics,
848
+ code: programError({ name: "SyntaxError", message: err.message }, program),
849
+ });
665
850
  if (err instanceof ExecutorAdmissionError) {
666
851
  if (err.code === "executor_overloaded") {
667
852
  logger.warn("[connecta] execute_code admission rejected", {
@@ -677,9 +862,7 @@ function failedRun(err, logger, { emitted, diagnostics }) {
677
862
  code: err.code,
678
863
  message: err.message,
679
864
  retryable: err.retryable,
680
- ...(err.retryAfterMs !== undefined
681
- ? { retryAfterMs: err.retryAfterMs }
682
- : {}),
865
+ ...(err.retryAfterMs !== undefined ? { retryAfterMs: err.retryAfterMs } : {}),
683
866
  },
684
867
  });
685
868
  }
@@ -691,7 +874,7 @@ function failedRun(err, logger, { emitted, diagnostics }) {
691
874
  });
692
875
  }
693
876
  /** The response for an ExecuteResult: the program's error or its value. */
694
- function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
877
+ function finishedRun(outcome, { emitted, diagnostics, program }) {
695
878
  if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
696
879
  return failureResponse("Executor failed: expected an ExecuteResult object.", {
697
880
  emitted,
@@ -709,52 +892,32 @@ function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
709
892
  });
710
893
  }
711
894
  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, {
895
+ const failure = outcome.failure?.call;
896
+ if (outcome.failure?.timeout) {
897
+ const { elapsedMs, deadlineMs } = outcome.failure.timeout;
898
+ const message = `Operation "execute_code" timed out during sandbox after ${elapsedMs}ms (effective deadline ${deadlineMs}ms).`;
899
+ return failureResponse(message, {
746
900
  logs,
747
901
  emitted,
748
902
  diagnostics,
749
- code: invocationFailure.details,
903
+ code: {
904
+ code: "timeout",
905
+ message,
906
+ retryable: false,
907
+ details: { operation: "execute_code", stage: "sandbox", elapsedMs, deadlineMs },
908
+ },
750
909
  });
751
910
  }
752
- const message = `Error: ${outcome.error || "Execution failed without an error message."}`;
753
- return failureResponse(message, {
911
+ return failureResponse(failure?.message ?? outcome.error, {
754
912
  logs,
755
913
  emitted,
756
914
  diagnostics,
757
- code: "executor_failed",
915
+ code: failure ??
916
+ programError({
917
+ name: outcome.failure?.name ?? /^(SyntaxError|ReferenceError|TypeError|RangeError):/.exec(outcome.error)?.[1],
918
+ message: outcome.error || "Execution failed without an error message.",
919
+ ...(outcome.failure?.line !== undefined ? { line: outcome.failure.line - 1 } : {}),
920
+ }, program),
758
921
  });
759
922
  }
760
923
  // A result crossing back as a host BigInt (or otherwise unserializable
@@ -762,7 +925,7 @@ function finishedRun(outcome, { emitted, diagnostics, invocationFailures }) {
762
925
  // error path so captured logs survive instead of a raw SDK 500.
763
926
  let result;
764
927
  try {
765
- result = guardExecuteResultValue(outcome.result);
928
+ result = guardExecuteResultValue(outcome.result, MAX_EXECUTE_RESULT_CHARS - emitted.textChars);
766
929
  }
767
930
  catch (err) {
768
931
  const message = `Error: result is not JSON-serializable: ${msg(err)}`;
@@ -794,33 +957,20 @@ function executeLogs(value) {
794
957
  }
795
958
  function failureResponse(message, options) {
796
959
  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) : ""}`);
960
+ const result = jsonResult({
961
+ error: typeof code === "string" ? { code, message, retryable: false } : code,
962
+ ...(logs ? { logs } : {}),
963
+ ...(emitted ? discardedEmits(emitted) : {}),
964
+ ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
965
+ });
966
+ result.isError = true;
967
+ return result;
810
968
  }
811
969
  /**
812
970
  * A failed program delivers no blocks and reports how many were discarded.
813
971
  */
814
972
  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
- : "";
973
+ return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
824
974
  }
825
975
  function connectorInventory(connectors) {
826
976
  const prefix = "Connectors: ";
@@ -829,14 +979,10 @@ function connectorInventory(connectors) {
829
979
  const entries = connectors.map((connector) => {
830
980
  const address = connector.id;
831
981
  const title = connector.title?.replace(/\s+/g, " ").trim();
832
- const label = title && title !== connector.id
833
- ? `${address}: ${boundedEchoText(title, 45)}`
834
- : address;
982
+ const label = title && title !== connector.id ? `${address}: ${boundedEchoText(title, 45)}` : address;
835
983
  if (!connectorGuide(connector))
836
984
  return label;
837
- const requirement = connectorGuideRequired(connector)
838
- ? "required guide"
839
- : "guide";
985
+ const requirement = connectorGuideRequired(connector) ? "required guide" : "guide";
840
986
  return `${label} (${requirement} ${connectorSkillName(connector.id)})`;
841
987
  });
842
988
  const shown = [];
@@ -858,29 +1004,26 @@ function connectorInventory(connectors) {
858
1004
  return `${prefix}${shown.join(", ")}.`;
859
1005
  return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
860
1006
  }
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
-
1007
+ 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
1008
  ${connectorInventory(connectors)}
864
1009
 
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.
1010
+ Use async () => { ... }, no arguments, with the connecta global. No portable ambient capabilities. API notation:
1011
+ \`\`\`ts
1012
+ declare const connecta: {
1013
+ ${GUEST_API_DECLARATION}
1014
+ };
1015
+ \`\`\`
1016
+ Discovery: { tools }; check search catalogErrors/absence. JSON schemas default; compact is text. GuestResult is { data, format: "json" | "text" }; inspect format before fields. Pages: text, hasMore, nextOffset in UTF-8 bytes. Promise.allSettled keeps typed failures. Host-call budget_exceeded is terminal, even through catch/allSettled. Runs report hostCalls.
871
1017
 
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
1018
+ Reduce JSON. Never repeat writes for output. emit: text or base64 image/audio, safe without await, ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares the result cap. skills({ name: "investigate" }): workflow; skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}.`;
1019
+ // Module scope, like the other five meta-tool inputs: its JSON Schema is
874
1020
  // derived once per process. Budgets and connectors vary by deployment and
875
1021
  // view, so they live in the description, never here.
876
1022
  const EXECUTE_INPUT = advertisedSchema(z.object({
877
1023
  code: z
878
1024
  .string()
879
1025
  .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."),
1026
+ diagnostics: z.boolean().optional().describe("Add request-local, payload-free timing and result-size summaries."),
884
1027
  }));
885
1028
  /** Register the execute_code meta-tool. Only called when an executor is configured. */
886
1029
  export function registerExecuteTool(server, registry, ctx) {
@@ -899,6 +1042,7 @@ export function registerExecuteTool(server, registry, ctx) {
899
1042
  const connectors = registry.listConnectors();
900
1043
  const maxWrites = resolveBudget(ctx.maxWrites, DEFAULT_MAX_WRITES);
901
1044
  const execute = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
1045
+ defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
902
1046
  discoveryConcurrency: ctx.discoveryConcurrency,
903
1047
  probeTimeoutMs: ctx.probeTimeoutMs,
904
1048
  maxEmittedBytes: emitBudgets.maxBytes,
@@ -907,25 +1051,29 @@ export function registerExecuteTool(server, registry, ctx) {
907
1051
  hostCallTimeoutMs: hostLimits.hostCallTimeoutMs,
908
1052
  watchdogMs: ctx.watchdogMs,
909
1053
  defer: ctx.defer,
910
- approval: ctx.approval,
1054
+ trust: ctx.trust,
911
1055
  maxWrites,
1056
+ requestScope: ctx.requestScope,
912
1057
  });
913
1058
  server.registerTool("execute_code", {
914
- description: executeDescription(emitBudgets, hostLimits, hasConnectorGuides(connectors), connectors, maxWrites),
1059
+ description: executeDescription(emitBudgets, {
1060
+ ...hostLimits,
1061
+ hostCallTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1062
+ }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.trust ?? "read-only"),
915
1063
  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.
1064
+ outputSchema: EXECUTE_OUTPUT,
1065
+ // The host sees execute_code as a write only on a trusted endpoint.
923
1066
  annotations: {
924
- readOnlyHint: true,
925
- destructiveHint: false,
1067
+ readOnlyHint: ctx.trust !== "trusted",
1068
+ destructiveHint: ctx.trust === "trusted",
926
1069
  openWorldHint: true,
927
1070
  },
928
- }, (args, extra) => underAnySignal([extra.mcpReq.signal, ctx.requestSignal], (signal) => execute(args, {
929
- signal,
930
- })));
1071
+ }, (args, extra) => {
1072
+ if (ctx.client)
1073
+ bindMcpClient(extra, ctx.client, ctx.activity);
1074
+ const run = () => underAnySignal([extra.mcpReq.signal, ctx.requestSignal], (signal) => execute(args, {
1075
+ signal,
1076
+ }));
1077
+ return ctx.authElicitation ? ctx.authElicitation.run("execute_code", args, extra, run) : run();
1078
+ });
931
1079
  }