@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
@@ -1,23 +1,41 @@
1
1
  # Code mode — the guest API contract
2
2
 
3
- The normative description of what a program written for `execute_code` is
4
- promised: what it can reach, what it gets back, how failures look, what it may
5
- retry, what bounds it runs under, and what it leaves behind in activity. It is
6
- specified in prose first and implemented second.
7
-
8
- Two executors implement it: QuickJS in a child process on Node, and
9
- `DynamicWorkerExecutor` from `@cloudflare/codemode` on Workers. Divergence is a
10
- bug unless it appears in [Executor exceptions](#executor-exceptions), which
11
- names the reason; a third executor is implementable from this document alone.
12
- [`ethos.md`](../ethos.md) carries the verdicts behind the shape,
3
+ The `execute_code` contract: capabilities, results, failures, retries, limits,
4
+ and activity. Specified in prose first, implemented second.
5
+
6
+ Two executors implement it: QuickJS in a Node child process and `workerExecutor()`
7
+ from `@zackbart/connecta/worker` around upstream `DynamicWorkerExecutor` on Workers.
8
+ Divergence is a bug unless explained in [Executor exceptions](#executor-exceptions);
9
+ a third executor is implementable from this document alone.
10
+ [`PRINCIPLES.md`](https://github.com/zackbart/connecta/blob/main/PRINCIPLES.md) states the invariants,
13
11
  [`meta-tools.md`](./meta-tools.md) owns the top-level tool contract, and clause
14
12
  identifiers (`A1`, `E3`, …) are stable and cited by [Verification](#verification).
15
13
 
14
+ ## Task routes
15
+
16
+ For normal endpoint work, start with [Operating an endpoint](./operating.md).
17
+ Use this reference for exact guest behavior or executor implementation.
18
+ [Agent index](./README.md) routes other work.
19
+
20
+ | Task | Sections |
21
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
22
+ | Wire a shipped or custom executor | [Deploy-time capability](#deploy-time-capability), [executor requirements](#what-an-executor-must-implement), [exceptions](#executor-exceptions) |
23
+ | Write a portable program | [Program form](#the-program), [addresses](#addressing), [guest functions](#the-surface) |
24
+ | Discover and call tools | [Search](#connectasearch), [describe](#connectadescribe), [call](#connectacall), [parallel calls](#parallel-calls) |
25
+ | Page results, load skills or read resources | [Result and skill](#connectaresult-and-connectaskill), [read](#connectaread), [projection](#results-and-projection) |
26
+ | Emit incremental output | [emit](#connectaemit), [emitted output](#emitted-output) |
27
+ | Handle errors, writes, or cancellation | [Errors](#errors), [retries](#retry-semantics), [limits](#cancellation-and-limits), [writes](#writes), [activity](#activity) |
28
+ | Change the contract or check executor parity | [Verification](#verification), then the tests indexed by clause |
29
+
30
+ Clause IDs remain stable, including retired IDs. Check current behavior against
31
+ source and the verification table; do not reuse a retired clause to introduce
32
+ new behavior.
33
+
16
34
  ## Deploy-time capability
17
35
 
18
36
  The `executor` passed to `createConnecta()` is required. `tools/list` is exactly
19
- seven — `execute_code`, `search_tools`, `call_tool`, `call_destructive_tool`,
20
- `authorize_connector`, `get_result`, and `skills`. Construction fails when the
37
+ six — `execute_code`, `search_tools`, `call_tool`, `call_destructive_tool`,
38
+ `authorize_connector` and `skills`. Construction fails when the
21
39
  executor is missing, and the removed `surface` option is rejected rather than
22
40
  ignored ([#273](https://github.com/zackbart/connecta/issues/273)), as are the
23
41
  pause-only `execute.resumableWrites` and `execute.pausedRunTtlSeconds`
@@ -33,26 +51,37 @@ import { quickJsExecutor } from "@zackbart/connecta/quickjs";
33
51
  createConnecta({ executor: quickJsExecutor() /* connectors, auth, storage… */ });
34
52
  ```
35
53
 
36
- `quickJsExecutor()` runs each program in a disposable child-process sandbox; its
37
- CPU, wall-time, memory, stack, queue, result, log, and IPC bounds are configured
38
- on the executor. Server bundlers must keep the `@zackbart/connecta/quickjs`
39
- package files external so the child entry stays on disk. The
40
- [Node template](../templates/node/README.md) carries the complete setup.
54
+ `quickJsExecutor()` runs each program in a disposable child process, with CPU,
55
+ wall-time, memory, stack, queue, result, log, and IPC bounds on the executor.
56
+ Keep `@zackbart/connecta/quickjs` external in server bundles so the child entry
57
+ stays on disk. The [Node template](../templates/node/README.md) has the full setup.
41
58
 
42
- On Cloudflare Workers the Worker Loader binding is the sandbox, and Dynamic
43
- Workers require the Workers Paid plan:
59
+ On Workers the Worker Loader is the sandbox; Dynamic Workers require the Paid plan:
44
60
 
45
61
  ```ts
46
- createConnecta({ executor: new DynamicWorkerExecutor({ loader: env.LOADER }) });
62
+ import { workerExecutor } from "@zackbart/connecta/worker";
63
+
64
+ createConnecta({ executor: workerExecutor({ loader: env.LOADER }) });
47
65
  ```
48
66
 
49
- The supported constructor passes only `loader`; `bindings`, `modules`, or
50
- `globalOutbound` grant ambient guest authority and violate `P2`. The
51
- [Worker example](../examples/worker/README.md#code-mode) has the full setup.
67
+ The adapter uses only `loader` and an optional `timeout`. Its admission defaults
68
+ are two active runs, eight queued runs, and a five-second queue deadline, settable
69
+ through `admission`. Lease release disposes RPC and loader handles independently
70
+ of guest settlement. `bindings`, `modules`, or `globalOutbound` violate `P2`.
71
+ The [Worker example](../examples/worker/README.md#code-mode) has the full setup.
72
+
73
+ Replace `import { DynamicWorkerExecutor } from "@cloudflare/codemode";` with
74
+ `import { workerExecutor } from "@zackbart/connecta/worker";` and use the wiring above.
75
+ Keep the peer. Unbranded executors throw, including bundled upstream copies.
76
+ Shipped executors carry a non-enumerable `Symbol.for("connecta.executor")` version/lifecycle brand.
77
+ Custom sandboxes import `customExecutor` from `@zackbart/connecta`, then configure
78
+ `executor: customExecutor(myExecutor, { lifecycle: "self-managed" })`. They own guest termination
79
+ and cleanup at budget exhaustion, cancellation, and deadlines; the wrapper only delegates methods.
52
80
 
53
81
  ## What an executor must implement
54
82
 
55
- The host side of the seam is two types in `src/types.ts` and nothing else.
83
+ The Promise provider/result seam lives in `src/types.ts`. Custom implementations
84
+ also need the lifecycle wrapper above; structural compatibility alone is insufficient.
56
85
 
57
86
  ```ts
58
87
  interface Executor {
@@ -70,6 +99,8 @@ interface ExecutorProvider {
70
99
  interface ExecuteResult {
71
100
  result: unknown; // the program's resolved value
72
101
  error?: string; // set instead of result when the run failed
102
+ failure?: { name: string; line?: number; call?: CallErrorDetails;
103
+ timeout?: { elapsedMs: number; deadlineMs: number } };
73
104
  logs?: string[]; // captured console output, in call order
74
105
  }
75
106
  ```
@@ -78,34 +109,38 @@ Connecta passes exactly one provider, named `connecta`. An executor must:
78
109
 
79
110
  1. **Expose each provider as a guest global** whose properties are its `fns`,
80
111
  called with the program's arguments and awaited. Connecta supplies `search`,
81
- `describe`, `call`, and `emit`.
112
+ `describe`, `call`, `read`, `result`, `skill`, and `emit`.
82
113
  2. **Evaluate `prelude` after the provider globals exist and before the
83
- program.** It is trusted host code; connecta uses it to restore typed host
84
- errors without exposing the private error frame.
114
+ program.** It is trusted host code that installs the immutable namespace
115
+ and waits for emission acknowledgements. Capture runner intrinsics first;
116
+ privileged initialization must be single-use.
85
117
  3. **Marshal values as JSON** both directions (`P3`), and reject a host call
86
118
  whose function is not an own property of `fns` — the guest can ask for
87
119
  inherited members too.
88
- 4. **Return, never throw, for a failed program**: set `error` to the guest's
89
- message, leave `result` undefined. `createExecuteTool` reads `error` first and
90
- matches it back to the failures recorded during the run, which is how an
91
- uncaught tool failure keeps its type (`E1`).
120
+ 4. **Return, never throw, for a failed program.** Set `error` and leave
121
+ `result` undefined. Rebuild a rejected host `InvocationFailure` as a guest
122
+ Error with `code`, `retryable`, and `details`, and retain its identity in
123
+ executor-owned state. If that same Error escapes the program, return its
124
+ typed details in `failure.call`. A wrapped or copied Error is `program_error`.
125
+ Construct outcomes through the host-owned RPC return or QuickJS host bridge,
126
+ never by parsing guest stdout, prints, returned values, or thrown look-alikes.
127
+ No secret held in the guest realm authenticates anything. Guest module
128
+ specifiers must not resolve runner internals, including dynamic imports.
129
+ For compilation failures, return `failure: { name, line? }`. For a sandbox
130
+ timeout, add `failure.timeout: { elapsedMs, deadlineMs }`.
92
131
  5. **Capture `console.log`, `console.warn`, and `console.error`** into `logs` in
93
132
  call order (`R5`), bounding what it retains.
94
- 6. **Bound the guest**: wall clock, memory, stack, and CPU (`L3`, `L5`), staying
95
- inside the tested `P2`/`X5` boundary. Settle `execute()` on your own
96
- deadline: connecta stops waiting at `execute.watchdogMs` and reports the
97
- sandbox unresponsive (`L3`).
98
- 7. **Grant no ambient authority of its own.** Never back this with `eval` or
99
- `node:vm`: the sandbox is a containment layer on top of connecta's boundary,
100
- not a replacement for it, and every capability arrives through `fns`.
101
-
102
- Optionally implement `AdmittingExecutor` (`acquire()` returning a lease whose
103
- `execute` runs once) for bounded admission (`L7`) and `close()` for shutdown;
104
- connecta wraps a plain `Executor` with `withExecutorAdmission` otherwise. The
105
- optional `name` — else a class's constructor name, which a minifier may rewrite
106
- — is the executor name `/health` and `connecta doctor` report.
107
- [Emitted output](#emitted-output) asks nothing of an executor: `connecta.emit`
108
- is just another provider function (`M8`).
133
+ 6. **Bound the guest**: wall clock, memory, stack, and CPU (`L3`, `L5`), within
134
+ `P2`/`X5`. Settle `execute()` on your own deadline; connecta stops waiting
135
+ at `execute.watchdogMs` and reports the sandbox unresponsive (`L3`).
136
+ 7. **Grant no ambient authority.** Never use `eval` or `node:vm`: sandboxing
137
+ supplements connecta's boundary; every capability arrives through `fns`.
138
+
139
+ Optional `AdmittingExecutor.acquire()` returns a once-executed lease for bounded
140
+ admission (`L7`); otherwise connecta uses `withExecutorAdmission`. `close()` handles
141
+ shutdown. `/health` and `connecta doctor` report `name`, falling back to the
142
+ constructor name, which minifiers may rewrite. [Emission](#emitted-output) is
143
+ another provider function (`M8`).
109
144
 
110
145
  ## The program
111
146
 
@@ -120,7 +155,7 @@ statements, and bare statement bodies are outside host recovery. QuickJS may
120
155
  wrap a bare body when driven directly; that is outside the portable contract.
121
156
 
122
157
  **P2.** The only capabilities in the contract are `connecta.search`,
123
- `connecta.describe`, `connecta.call`, `connecta.emit`, and `console.log` /
158
+ `connecta.describe`, `connecta.call`, `connecta.read`, `connecta.result`, `connecta.skill`, `connecta.emit`, and `console.log` /
124
159
  `console.warn` / `console.error`, captured and returned. Anything else a runtime
125
160
  exposes is outside the portable contract and must not be used. QuickJS grants
126
161
  none of it. A loader-only Dynamic Worker denies external egress and filesystem
@@ -163,10 +198,10 @@ belonged to shortcut dispatch and stay retired; nothing reuses those ids.
163
198
 
164
199
  ## The surface
165
200
 
166
- Four functions, all `async`: `search`, `describe`, `call`, and `emit`. Nothing
167
- else works. Reading any other property yields a function — the guest namespace
168
- is a Proxy, so `typeof connecta.toString` is `"function"` — but *calling* it
169
- fails, because the host resolves only own members of the provider's `fns`.
201
+ Seven functions: `search`, `describe`, `call`, `read`, `result`, `skill`, and `emit`.
202
+ All return awaitable values; emission also works without `await`. The namespace
203
+ has no inherited members. Unknown properties are undefined, and calling them
204
+ fails as a guest `TypeError`. The host also dispatches only own members of `fns`.
170
205
 
171
206
  ### connecta.search
172
207
 
@@ -178,15 +213,17 @@ const page = await connecta.search({
178
213
  limit: 8, // 1–100, default 8
179
214
  offset: 0,
180
215
  fullDescriptions: false,
181
- includeSchemas: "compact", // or "json" / "typescript"
216
+ includeSchemas: "json", // default; or "compact" / "typescript"
182
217
  includeSchemaKeys: true, // default true in code mode
183
218
  });
184
219
  ```
185
220
 
186
- **S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus
187
- `nextOffset` when more remains and `matchMode: "partial"` when no tool matched
188
- every term. Top-level `search_tools` is different — it returns
189
- `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Ranking is
221
+ **S1.** Returns the same flat page as top-level `search_tools`:
222
+ `{ catalogErrors, tools, total, offset, limit, hasMore }`, plus `nextOffset`
223
+ when more remains and `matchMode: "partial"` when no tool matched every term.
224
+ Read `catalogErrors` and any `absence` before selecting a tool. Catalog failures
225
+ carry recovery actions at the top of the page; an absent service returns no
226
+ lookalikes. Both paths use only the caller's endpoint/pool and grants. Ranking is
190
227
  [lexical discovery](./meta-tools.md#lexical-discovery)'s, and rows expose
191
228
  neither lexical scores nor per-result coverage. An empty or whitespace-only
192
229
  query browses; non-empty input with no ASCII lexical terms returns no tools plus
@@ -201,16 +238,20 @@ bounded `guideSummary`, requested or not. An output shape learned under `S9`
201
238
  also carries `outputSchemaSource: "observed"`; provider declarations carry no
202
239
  source marker.
203
240
 
241
+ Programs default to `includeSchemas: "json"`: `inputSchema` and available
242
+ `outputSchema` are JSON Schema values, labeled `schemaFormat: "json"`.
243
+ Explicit `includeSchemas: "compact"` returns strings labeled
244
+ `schemaFormat: "text"`; TypeScript signatures carry the same text label.
204
245
  Compact shapes omit property prose and put required fields first, under these
205
246
  bounds:
206
247
 
207
- | Bound | Value |
208
- | --- | --- |
209
- | `execute_code` source | ≤ 65,536 UTF-8 bytes, checked before executor admission; over-limit source returns `invalid_args` |
210
- | Rendered shape | 1,024 UTF-8 bytes each |
211
- | Work per shape | 2,000 visits across schema nodes, property and required names, literal values, any constraint-free retry, and its key-only fallback |
212
- | Each enum node | 256 of those bytes |
213
- | Resolved `$ref` text | reused within one walk |
248
+ | Bound | Value |
249
+ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
250
+ | `execute_code` source | ≤ 65,536 UTF-8 bytes, checked before executor admission; over-limit source returns `invalid_args` |
251
+ | Rendered shape | 1,024 UTF-8 bytes each |
252
+ | Work per shape | 2,000 visits across schema nodes, property and required names, literal values, any constraint-free retry, and its key-only fallback |
253
+ | Each enum node | 256 of those bytes |
254
+ | Resolved `$ref` text | reused within one walk |
214
255
 
215
256
  About three near-cap enum nodes therefore coexist while the final quarter stays
216
257
  for surrounding syntax, and above 1,024 bytes the global fallback applies.
@@ -229,7 +270,7 @@ use `format: "json"` or JSON search.
229
270
  `includeSchemas: "typescript"` spends those same bounds on a different
230
271
  rendering: one `signature` string in place of `inputSchema` and
231
272
  `outputSchema`, reading `(args: { runId: number }) => Promise<Run>` for the
232
- value `connecta.call` returns. It is documentation for the program's author —
273
+ `data` value `connecta.call` returns. It is documentation for the program's author —
233
274
  the program itself is still JavaScript, and a type annotation copied into it
234
275
  is a syntax error (`P5`). Truncation keeps compact's flags and
235
276
  turns what compact would print as a name or raw JSON into a marked `unknown`;
@@ -239,14 +280,10 @@ its degradation rules are specified once, under
239
280
  signature is byte-identical to the one `search_tools` returns.
240
281
 
241
282
  **S1a.** `connector` loads only the named catalog; omit it only when the
242
- integration is ambiguous, because an unscoped search fans out across every
243
- configured connector. `safety: "readOnly"` returns exactly the tools
244
- `connecta.call` runs unasked; `"approvalRequired"` returns the complementary
245
- fail-closed class, false, missing, and contradictory annotations included,
246
- whose calls a program has refused (`E4`) unless config exempts them (`W12`),
247
- which a row marks with `approval: "exempt"` without moving it out of that
248
- class; omitted or `"all"` preserves the complete catalog. These filters grant
249
- no authority and change no admission decision.
283
+ integration is ambiguous. `safety: "readOnly"` selects stored read verdicts;
284
+ `"approvalRequired"` selects stored write verdicts; omitted or `"all"` selects
285
+ both. Search and describe rows carry `classification`. These filters grant no
286
+ authority and change no pool trust or call admission decision.
250
287
 
251
288
  **S2.** A requested object schema carries `inputKeys`, `requiredInputKeys`
252
289
  (declared properties only), and `outputKeys`: the names the rendered schema
@@ -278,12 +315,14 @@ hint and the stable `code`, `retryable`, and `details` fields (`E1`).
278
315
  const one = await connecta.describe({ address: "ci.get_run" });
279
316
  const many = await connecta.describe({
280
317
  addresses: ["ci.get_run", "ci.get_job_logs"], // ≤ 100
281
- format: "compact", // or "json" / "typescript"
318
+ format: "json", // default; or "compact" / "typescript"
282
319
  fullDescriptions: false,
283
320
  });
284
321
  ```
285
322
 
286
- **S4.** Returns `{ tools }` in order, one entry per address; one bad address
323
+ **S4.** Defaults to JSON Schema values with `schemaFormat: "json"`.
324
+ Explicit compact schemas and TypeScript signatures have `schemaFormat: "text"`.
325
+ Returns `{ tools }` in order, one entry per address; one bad address
287
326
  never fails the whole call. An unknown address or failed catalog returns `error`
288
327
  plus typed `errorDetails` (`code`, `message`, `retryable`). Misses carry a
289
328
  route-aware `nextAction`, a close miss may add three canonical `suggestions`,
@@ -302,15 +341,38 @@ says so inside its `Promise<…>`.
302
341
 
303
342
  ### connecta.call
304
343
 
344
+ The published `GuestApi` type describes the supplied global for editors.
345
+ The executable usage guide checks its API notation against those declarations;
346
+ programs still use plain JavaScript.
347
+
305
348
  ```js
306
- const run = await connecta.call("ci.get_run", { runId: 42 });
349
+ const { data: run, format } = await connecta.call("ci.get_run", { runId: 42 });
350
+ // Equivalent: connecta.call({ address: "ci.get_run", args: { runId: 42 }, timeoutMs: 30_000 })
307
351
  ```
308
352
 
309
- **S5.** Takes a canonical address and one arguments object; returns the tool's
310
- value already unwrapped. For an MCP connector that means `structuredContent`
311
- when present, otherwise text content JSON-parsed when it parses and the raw text
312
- when it does not; a downstream result flagged `isError` throws. Omitted `args`
313
- is treated as `{}`.
353
+ **S5.** Takes a canonical address, optional arguments, and optional
354
+ `{ timeoutMs }`; the object form is `{ address, args?, timeoutMs? }`. A malformed
355
+ signature fails with `invalid_args` and both exact signatures, before discovery.
356
+ Normally returns `{ data, format: "json" | "text" }`. Oversized values may return
357
+ a `format: "paged"` handle for `connecta.result`, as described in `L6` below.
358
+ MCP `toolResult` or
359
+ `structuredContent` is JSON; all-text content is JSON when it parses, otherwise
360
+ text. API string values are text, other API values are JSON. Check `format`
361
+ before treating `data` as an object. A downstream `isError` throws.
362
+
363
+ An inline MCP result containing image, audio, resource, or resource_link blocks
364
+ also exposes `content?: GuestNativeBlock[]`, even when `data` comes from
365
+ `structuredContent` or `toolResult`. `data` keeps its existing shape.
366
+ `connecta.emit(result)` forwards all native blocks; iterating `result.content`
367
+ and calling `connecta.emit(block)` forwards selected blocks. Both forward the
368
+ host-retained originals, so guest edits do not change native output. Results
369
+ over the 1 MiB inline cap return paging handles without native content; page
370
+ and reduce them as described in `L6`. If only the native presentation is too
371
+ large or its transfer fails, `data` stays intact and `contentResult` carries
372
+ its paging handle or an unavailable-paging notice for presentation that exceeds
373
+ bounded traversal limits or is cyclic. When a result ID exists,
374
+ `connecta.result(result.contentResult)` pages the original MCP envelope;
375
+ `emit(result)` refuses with `result_too_large`.
314
376
 
315
377
  **S6.** Every call goes through the same catalog, fail-closed read-only
316
378
  predicate, admission, credential containment, timeout classification, health
@@ -320,6 +382,32 @@ program does decides it. The sandbox is an additional containment layer, not a
320
382
  second implementation of the boundary, and nothing a program does widens what
321
383
  it can reach.
322
384
 
385
+ ### connecta.result and connecta.skill
386
+
387
+ `await connecta.result(handleOrId, { page?, offset?, maxBytes? })` reads a stashed direct-call
388
+ or program-call result bound to the admitted subject, principal, endpoint/pool, request origin,
389
+ connector, and tool. It returns
390
+ `{ resultId, offset, bytes, totalBytes, hasMore, nextOffset?, format: "text", text }`.
391
+ Offsets and sizes are UTF-8 bytes. Pages obey the original call's inline cap
392
+ and a 32 KiB bridge ceiling. `page` is zero-based and mutually exclusive with
393
+ `offset`; keep `maxBytes` fixed while incrementing pages. Nominal page windows
394
+ are at least four bytes wide and align both edges to UTF-8 boundaries.
395
+ Follow `nextOffset`, reassemble inside the program, and return a reduced value.
396
+ Before returning a page, the host rechecks current authentication, connector/tool
397
+ grants, pool membership, and endpoint trust against both the original and current
398
+ tool classification. A read-only endpoint cannot page a write result, so a
399
+ direct write there returns an inline truncation notice without stashing the
400
+ result or advertising a paging action. Binding
401
+ mismatches, revoked access, and old entries without bindings are `not_found`, as
402
+ are unknown or expired ids; a storage failure is `unavailable`. IDs are random UUIDs.
403
+ The `get_result` meta-tool is removed.
404
+
405
+ `await connecta.skill(name)` resolves the same URI or compatibility name as
406
+ top-level `skills`, within the admitted connector view. It reads the registry
407
+ behind native `skills/get` and `resources/read`, including opted-in downstream
408
+ skills. It returns
409
+ `{ name, format: "text", text }`; an unknown name is `not_found`.
410
+
323
411
  ### Parallel calls
324
412
 
325
413
  **S7.** Use `Promise.all` for independent calls when any failure should fail the
@@ -331,7 +419,7 @@ as sequential calls. There is no separate batch size or result contract.
331
419
  `details`. Project those fields before returning — an `Error` object is not a
332
420
  JSON result contract.
333
421
 
334
- **S9.** A successful explicitly read-only call whose provider declared no
422
+ **S9.** A successful call classified as a read whose provider declared no
335
423
  `outputSchema` passively learns one from the unwrapped result. The observation
336
424
  keeps field names and broad JSON types only — no arguments, scalar values, raw
337
425
  results, code, credentials, or errors — and property names may be user-authored.
@@ -350,10 +438,75 @@ successful call. No discovery read, timer, refresh, background job, or storage
350
438
  adapter executes or persists work for this cache: the result-sampling refusal in
351
439
  [#282](https://github.com/zackbart/connecta/issues/282) stands.
352
440
 
441
+ ### connecta.read
442
+
443
+ ```js
444
+ const resource = await connecta.read(
445
+ "resource://docs/" + encodeURIComponent("docs://manual/start"),
446
+ );
447
+ return resource.contents.map(content => content.text);
448
+ ```
449
+
450
+ **S10.** Reads a downstream resource through a configured connector that
451
+ implements `readResource`. The routing URI is exactly
452
+ `resource://<connectorId>/<encodeURIComponent(downstreamUri)>`, at most 8,192
453
+ UTF-8 bytes. The downstream URI must have a scheme and no control characters.
454
+ Encode it once, including any path, query and fragment. Bare URLs, malformed
455
+ encoding, routing userinfo, ports and extra routing query or fragment fields
456
+ fail with `invalid_args`. Remote MCP connectors first load the complete,
457
+ paginated resources and URI-template inventories through the same host-auth
458
+ partitioned catalog cache. The decoded URI must exactly match an advertised
459
+ resource or match an advertised URI template. An unmatched URI fails with a
460
+ fixed `not_found` message before any downstream read. Template matching keeps
461
+ the scheme and authority fixed and supports bounded RFC 6570 scalar and list
462
+ expansions, including prefix modifiers and safe multi-segment `{+path}` and
463
+ `{/p*}` paths. Variables cannot supply authority, backslashes, dot segments,
464
+ a scheme change, query delimiters, controls or Unicode format characters,
465
+ including through repeated percent encoding. Initial percent encoding and
466
+ UTF-8 decoding stay strict. Later decoding layers preserve literal percent
467
+ signs produced by `%25` while checking every remaining `%HH` escape. The
468
+ matcher scans forward to the
469
+ leftmost following literal without backtracking. Adjacent expressions and
470
+ literals that the preceding expression can contain are refused with
471
+ `resource_template_ambiguous`. Matching all templates is capped at 262144
472
+ combined template and URI characters per read, with
473
+ `resource_match_budget_exceeded` beyond that bound. Operator status retains
474
+ each typed refusal once, without URI or template text. Mixed exploded
475
+ composites remain unsupported.
476
+ Connecta passes an admitted decoded URI as an opaque MCP `resources/read`
477
+ argument to the configured endpoint. It never fetches that
478
+ URI or creates a connector from it.
479
+
480
+ Resources are protocol reads in both `read-only` and `trusted` pools. A read
481
+ requires a grant to the whole connector through the endpoint/pool and identity
482
+ intersection. Exact-tool grants, including guarded grants, authorize no
483
+ resources. An absent, hidden, tool-restricted or resource-incapable connector
484
+ fails with `unknown_address`. The URI and extra arguments cannot select a
485
+ different credential owner or downstream subject. Personal credentials come
486
+ from the admitted principal, just as they do for tool calls.
487
+
488
+ The return is the downstream JSON `{ contents }` result, with text or base64
489
+ blob entries and available metadata. It is not unwrapped or JSON-parsed.
490
+ Every read spends one shared host call and uses the connector's call admission
491
+ with `toolName: "resources/read"` and `args: { uri: downstreamUri }`. One deadline
492
+ covers admission and dispatch; cancellation ends the wait and releases its
493
+ permit. Reads share the execution's downstream request scope. They make one
494
+ attempt, bypass response caching, and never start authorization. A downstream
495
+ resource miss is `not_found`; a mid-call input request is
496
+ `input_required_unsupported`. Other failures use the existing typed error
497
+ contract, including credential recovery. Values and failures cross the same
498
+ credential redaction boundary as `call`. Diagnostics use the fixed operation
499
+ name `read`; resource URIs and bodies enter no activity or diagnostic record.
500
+ This function exposes no resource listing, template discovery, subscriptions
501
+ or upward proxy to programs. Native upward skill resources are separate
502
+ [Phase 3](https://github.com/zackbart/connecta/issues/707) work.
503
+
353
504
  ### connecta.emit
354
505
 
355
506
  ```js
356
- await connecta.emit({ type: "image", data: shot.data, mimeType: "image/png" });
507
+ const badge = await connecta.call("assets.get_badge_image", {});
508
+ connecta.emit(badge);
509
+ // Or: for (const block of badge.content ?? []) connecta.emit(block);
357
510
  ```
358
511
 
359
512
  The rich-output channel, delivered after the JSON envelope on success. Its
@@ -362,53 +515,65 @@ clauses are [Emitted output](#emitted-output) (`M1`–`M10`).
362
515
  ## Errors
363
516
 
364
517
  **E1.** There are three error channels. Connecta failures are typed whether
365
- caught or uncaught.
518
+ caught or uncaught. Host-call budget exhaustion ends the host run and blocks host
519
+ access (`L4`); Workers guest computation may continue briefly until teardown (`X3`).
366
520
 
367
- | Channel | Shape | Typed? |
368
- | --- | --- | --- |
369
- | A caught Connecta host failure | `Error` with `message`, `code`, `retryable`, and `details` | yes |
370
- | An uncaught **tool or discovery** failure, as the model sees it | `{ error: { code, message, retryable, … } }` with `isError` | yes |
371
- | Program or execution failure (`E5`, `E6`, a bridge bound in `L6`) | error text | no |
521
+ | Channel | Shape | Typed? |
522
+ | ------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------ |
523
+ | A caught Connecta host failure | `Error` with `message`, `code`, `retryable`, `data`, and `details` | yes |
524
+ | An uncaught **tool, resource or discovery** failure, as the model sees it | `{ error: { code, message, retryable, … } }` with `isError` | yes |
525
+ | Program or execution failure (`E5`, `E6`, a bridge bound in `L6`) | `{ error: { code, message, retryable, details? } }` | yes |
372
526
 
373
- Both executor bridges reduce a rejected host call to `new Error(message)`, and
374
- connecta restores the typed failure in a trusted prelude with a per-execution
375
- authenticated frame (`X11`). `message` stays human text, capped at 2,000
527
+ Both executor bridges rebuild typed host rejections as guest Errors and retain
528
+ their identity in executor-owned state (`X11`). `message` stays human text, capped at 2,000
376
529
  JSON-serialized characters including quotes and an `…` marker when clipped.
377
- `code` and `retryable` are the stable branch fields; `details` carries the host
378
- classification and fits 3,700 serialized characters. Optional recovery metadata
530
+ `code` and `retryable` are the stable branch fields; `data` and `details` carry the same host
531
+ classification, which fits 3,700 serialized characters. Optional recovery metadata
379
532
  that would exceed that bound is omitted whole, preserving `code`, `message`,
380
533
  `retryable`, and `retryAfterMs`, because a clipped recovery address or argument
381
- describes a different call. This covers `call`, `search`, `describe`, `emit`, and
382
- the host-call budget. Program-authored errors stay untyped, and code must never
534
+ describes a different call. This covers `call`, `read`, `search`, `describe`, `result`, `skill`, `emit`, and
535
+ write and emitted-output budgets. Program-authored errors use `program_error`; code must never
383
536
  parse error prose. An `unavailable` classification may add `details.host`, an
384
537
  HTTP(S) origin of at most 253 UTF-8 bytes, and `details.code`, a validated
385
538
  network errno, undici transport code, or `timeout` of at most 32 bytes; neither
386
539
  enters activity.
387
540
 
541
+ On hosts without URL elicitation, `connecta.call` auth failures carry `recovery`,
542
+ `nextAction`, and the handoff in `error.data`, also exposed as `error.details`.
543
+ An OAuth handoff includes `authorizationUrl` and `instructions`; operator
544
+ credential recovery includes `operatorUrl`, declared credential fields, and
545
+ instructions. `connecta.search` catalog errors and `connecta.describe`
546
+ `errorDetails` carry the same fields. When an auth error includes a handoff, give
547
+ it to the user or operator. One program run reuses one handoff per connector,
548
+ including concurrent call and catalog errors. Errors never request a forced
549
+ restart. Use `authorize_connector` for an explicit connection or a fresh link.
550
+ URL-elicitation hosts retain their existing flow.
551
+
388
552
  **E2.** The taxonomy: `retryable` is what connecta reports, `Y3` what a program
389
553
  may do. A provider maps each downstream failure to the code that tells the
390
554
  caller what to do next, and never invents a cause it was not told.
391
555
 
392
- | Code | Raised when | `retryable` |
393
- | --- | --- | --- |
394
- | `unknown_address` | no connector owns the address | false |
395
- | `unknown_tool` | the connector has no such tool | false |
396
- | `destructive_tool_requires_approval` | the tool is neither explicitly read-only nor config-exempt (`E4`) | false |
397
- | `auth_required` | the credential is missing, expired, or rejected | false |
398
- | `invalid_args` | arguments or discovery bounds were rejected | false |
399
- | `not_found` | the downstream answered and the resource is not there — the one code that says skip this id rather than stop, raised only where the provider tells absence from a permission gap | false |
400
- | `conflict` | the write named a base version someone else already moved past; nothing changed. `details.current` says where things stand (at most 20 whole-number entries) — re-read, reapply, retry with that base | false |
401
- | `input_required_unsupported` | a downstream asked for mid-call input | false |
402
- | `rate_limited` | the downstream reported a rate limit | true |
403
- | `unavailable` | the downstream is down or unreachable; optional sanitized `details.host` and `details.code` describe the transport failure without paths, queries, credentials, or provider prose | true |
404
- | `timeout` | the per-call deadline (`execute.hostCallTimeoutMs`, default 15 s) expired | true |
405
- | `cancelled` | the run ended while this call was in flight (`E5`) | false |
406
- | `connector_call_failed` | anything else the connector threw | per message |
407
- | `catalog_lookup_failed` | the connector's catalog could not be loaded | per cause |
408
- | `result_processing_failed` | the result could not be prepared | per message |
409
- | `result_too_large` | a discovery response exceeded its byte bound | false |
410
- | `budget_exceeded` | the run exhausted a host-call, write, or emitted-output budget | false |
411
- | `write_outcome_unknown` | an exempt write was sent and no answer came back; it is never sent again (`W9`) | false |
556
+ | Code | Raised when | `retryable` |
557
+ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
558
+ | `unknown_address` | no visible connector owns the tool address or grants the resource reader | false |
559
+ | `unknown_tool` | the connector has no such tool | false |
560
+ | `destructive_tool_requires_approval` | a program in a read-only pool attempted a write (`E4`) | false |
561
+ | `auth_required` | the credential is missing, expired, or rejected | false |
562
+ | `invalid_args` | arguments or discovery bounds were rejected | false |
563
+ | `not_found` | the downstream answered and the resource is not there — the one code that says skip this id rather than stop, raised only where the provider tells absence from a permission gap | false |
564
+ | `conflict` | the write named a base version someone else already moved past; nothing changed. `details.current` says where things stand (at most 20 whole-number entries) — re-read, reapply, retry with that base | false |
565
+ | `input_required_unsupported` | a downstream asked for input; use `nextAction` for the equivalent direct call | false |
566
+ | `rate_limited` | the downstream reported a rate limit | true |
567
+ | `unavailable` | the downstream is down or unreachable; optional sanitized `details.host` and `details.code` describe the transport failure without paths, queries, credentials, or provider prose | true |
568
+ | `timeout` | a call or sandbox deadline expired; details name operation, stage, elapsedMs, and deadlineMs | per operation |
569
+ | `program_error` | guest JavaScript failed; `details.name`, `line`, and a fixed repair hint describe it | false |
570
+ | `cancelled` | the run ended while this call was in flight (`E5`) | false |
571
+ | `connector_call_failed` | anything else the connector threw | per message |
572
+ | `catalog_lookup_failed` | the connector's catalog could not be loaded | per cause |
573
+ | `result_processing_failed` | the result could not be prepared | per message |
574
+ | `result_too_large` | a discovery response exceeded its byte bound | false |
575
+ | `budget_exceeded` | the run exhausted a host-call, write, or emitted-output budget | false |
576
+ | `write_outcome_unknown` | a trusted-pool write was sent and no answer came back; it is never sent again (`W9`) | false |
412
577
 
413
578
  **E3.** `auth_required` carries the same recovery envelope as `call_tool`:
414
579
  `connector`, `operation`, `recovery` (`oauth`, `operator_config`, or
@@ -416,17 +581,18 @@ caller what to do next, and never invents a cause it was not told.
416
581
  sentence. A program cannot recover credentials — only an operator can — so stop
417
582
  and let the failure reach the model.
418
583
 
419
- **E4.** An unannotated, write-capable, or destructive tool never runs in a
420
- program unless config exempts it (`W12`). It is refused with
584
+ **E4.** A tool classified as a write never runs in a
585
+ program unless the pool is trusted (`W12`). It is refused with
421
586
  `destructive_tool_requires_approval` before validation and before anything is
422
587
  sent, `nextAction` carrying its canonical address to `call_destructive_tool`
423
- plus the original arguments when they fit the 512-byte echo budget — whole or
424
- not at all, since a clipped copy is a different call. The host's prompt on that
588
+ and a schema-filtered snapshot within the 512-byte echo budget. `argsRedacted`
589
+ marks partial or withheld arguments; use the original arguments if this flag is
590
+ set or `args` is absent. Snapshots are never clipped. The host's prompt on that
425
591
  call is the approval, and the only one: generated code can neither mint the
426
592
  capability nor approve its own write, and the model's short `reason` grants no
427
593
  authority and never goes downstream.
428
594
 
429
- **E5.** Failures of the *execution*, not of a call, never appear inside the
595
+ **E5.** Failures of the _execution_, not of a call, never appear inside the
430
596
  guest: admission rejection (`executor_overloaded`, retryable, with
431
597
  `retryAfterMs`), cancellation (`executor_cancelled`), shutdown
432
598
  (`executor_closed`), deadline expiry, an executor that never settles (`L3`),
@@ -436,19 +602,33 @@ is cancelled fails with `cancelled`, catchable on the way out but never worth
436
602
  acting on (`Y3`). When shutdown tears down a program that had already started,
437
603
  accepted blocks are reported as discarded under `M4`; a failure before execution
438
604
  started carries no discard fields. A returned `error` field is a failure even
439
- when empty — an empty string reports `executor_failed` with
440
- `Error: Execution failed without an error message.`
441
-
442
- **E6.** An error the program raises itself — a `TypeError`, a call to a
443
- `connecta` member that is not a provider function (an inherited one like
444
- `toString` included), a `throw` of its own — ends the run with an untyped error
445
- result carrying that message. One precedence rule: connecta recognizes an
446
- escaped tool failure by its message, exactly first and by containment second for
447
- messages of at least eight characters, so a program that *wraps* a failure's
448
- message still reports the underlying typed failure — keeping the type beats
449
- keeping the prose. Matching retains only the most recent 64 failures per
450
- execution, caught refusals included; an older escaped message stays an untyped
451
- execution failure. An empty terminal error uses the fixed message in `E5`.
605
+ when empty. An empty string reports `program_error` with
606
+ `Program Error: Execution failed without an error message.`
607
+
608
+ **E6.** A guest `TypeError`, `ReferenceError`, syntax error, invented API, or
609
+ program-authored throw is `program_error`. `details.name` is a bounded engine
610
+ error name; `details.line` is the source line when the runtime supplies one,
611
+ otherwise null. Fixed hints repair imports/require/filesystem access, shadowed
612
+ `connecta`, and invented globals such as `callTool` and `mixpanel`.
613
+
614
+ An unchanged uncaught host error retains its classification through Error
615
+ identity. A new error containing its message is a new program error. Copied
616
+ public `code`/`details`, printed text, and returned outcome-shaped objects carry
617
+ no authority. Host-call counts stay in host state. The guest JSON codec, Object
618
+ prototype, and Promise prototype are immutable. Promise race and species are
619
+ protected too. Host completion uses captured intrinsics so guest edits cannot
620
+ change bridge decoding or async return adoption. Worker guest modules contain
621
+ only the user callback and an import helper limited to runtime builtins.
622
+ Relative and other guest module imports cannot reach runner code. Trusted
623
+ wrappers and preludes remain outside the guest's lexical scope, and privileged
624
+ initialization is single-use. A separate private runner module captures and
625
+ freezes its references before the guest module evaluates, including the guest
626
+ namespace initializer. The entrypoint imports that runner before its guest
627
+ dependency. Before loading, the adapter parses the assembled guest export and
628
+ requires exactly one expression with no additional statements. Normalized
629
+ multi-statement bodies remain one async arrow expression.
630
+ Program diagnostic fields are bounded before transport, including escaped text;
631
+ a large custom error name cannot turn a failure into a truncated success.
452
632
 
453
633
  **E7.** `retryable` for `unknown_address`, `unknown_tool`, and
454
634
  `destructive_tool_requires_approval` is pinned false, never inferred from an
@@ -479,32 +659,45 @@ fields. Program-authored projection saved bytes; host heuristics could drop
479
659
  deliberately returned fields. Helpers need [#222](https://github.com/zackbart/connecta/issues/222)
480
660
  evidence that programs fail to project.
481
661
 
482
- **R2.** The boundary is 24,000 serialized characters (~6k tokens). A value over
483
- it is replaced by exactly one envelope:
484
-
485
- ```json
486
- {
487
- "truncated": true,
488
- "preview": "…",
489
- "totalChars": 5242880,
490
- "hint": "filter/map/slice data inside execute_code and return only what you need"
491
- }
492
- ```
493
-
494
- The envelope is itself bounded as serialized, so `totalChars` is always the true
495
- size of what the program returned and truncation happens exactly once no matter
496
- how many hops the value takes.
497
-
498
- **R3.** Truncation is a *successful* result: the program ran but returned too
499
- much. Run a program that returns less; that is why the envelope says so.
500
-
501
- **R4 (verdict: no result paging for programs).** Program results have no
502
- `get_result` handle. A program can shrink its return; paging would reward
503
- unprojected data.
504
-
505
- For non-repeatable writes, inspect and reduce the full result before return,
506
- or page a direct `call_destructive_tool` result with `get_result`. Sampling a
507
- program result may discard the only copy.
662
+ **R2.** The boundary is 24,000 serialized characters (~6k tokens), shared with
663
+ emitted text. An oversized return is stashed as compact JSON through the same
664
+ result store as direct calls. The returned notice carries `truncated`, `resultId`,
665
+ `totalBytes`, `hint`, `nextOffset: 0`, and an executable `nextAction` that pages
666
+ with `connecta.result`. It also retains `totalChars` and a bounded `preview`.
667
+ The recovery action and hint precede the preview, so clients that spill results
668
+ after the first 2,000 characters retain the handle and the instruction.
669
+ Small returns pass through unchanged; Connecta does not select fields.
670
+
671
+ **R3.** Truncation is a _successful_ result. The program completed and its full
672
+ return can be read without running it again. Prefer filtering or reducing reads
673
+ before returning them. If the run completed a write, the notice opens with
674
+ "This write already ran: do not call it again to see its result."
675
+ This warning uses host accounting for writes completed in this run. Paging an
676
+ earlier write result does not count as a new write.
677
+
678
+ **R4.** Program returns share direct-call stash envelopes, 15-minute expiry,
679
+ aggregate capacity charges, and credential redaction before persistence.
680
+ Their pages bind the admitted subject, principal, endpoint/pool, origin, and
681
+ all contributing tool calls, resource and skill reads, and catalog metadata.
682
+ Downstream skill and resource dependencies require a whole-connector grant.
683
+ Local guides and catalog dependencies require visibility of the contributing connector or tool, even
684
+ when the metadata describes a write on a read-only endpoint.
685
+ Caught call failures retain that connector visibility when catalog loading or
686
+ tool resolution fails. Recovery messages listing configured connectors or
687
+ local guides retain visibility of those contributors too.
688
+ Each page rechecks current auth,
689
+ grants, pool membership, and trust. Reading a stash inside a program carries
690
+ its dependencies forward to any new return stash. The page cap is the smallest
691
+ of the deployment cap and contributing connectors' effective `maxResultBytes`.
692
+ Read-only endpoints cannot page write results. A UUID is a handle, never a grant.
693
+
694
+ If paging is unavailable because the stash is full, storage fails, or text
695
+ cannot round-trip as UTF-8, the successful return keeps the existing bounded
696
+ `{ truncated, preview, totalChars, hint }` fallback. Its hint explains that
697
+ paging is unavailable and retains the instruction to filter/map/slice. A write's
698
+ hint still says it already ran. Never rerun a write to recover its output.
699
+ JSON serialization preserves lone surrogates as escapes, so ordinary
700
+ JSON-returning programs can page them losslessly.
508
701
 
509
702
  **R5.** `console.log`, `console.warn`, and `console.error` are captured in call
510
703
  order and returned as one `logs` string, capped at 4,000 characters with a
@@ -512,18 +705,22 @@ truncation marker. Logs survive program failure through either a returned error
512
705
  result or a thrown error carrying `logs: string[]`. QuickJS streams captured
513
706
  entries to its parent and preserves the received prefix on cancellation,
514
707
  shutdown, deadline termination, child crashes, and IPC failures (`X4`). How a
515
- non-string argument renders is not contract (`X4`).
516
-
517
- **R6.** Nothing else is added to a normal program result. `diagnostics: true`
518
- adds one request-local, payload-free `diagnostics` block; a program that emitted
519
- adds `emitted: N` and its blocks (`M2`). Omitted, `false`, and emit-free are
520
- byte-for-byte the ordinary response path
521
- ([#247](https://github.com/zackbart/connecta/issues/247)).
708
+ non-string argument renders is not contract (`X4`). At terminal host-call
709
+ budget exhaustion, QuickJS returns its streamed prefix; a Dynamic Worker
710
+ has no host log stream and cannot supply logs from its suspended guest (`L4`).
711
+
712
+ **R6.** Every run returns `hostCalls: { attempted, admitted, succeeded, failed }`,
713
+ including admission failure, cancellation, budget refusal, and code-size refusal.
714
+ Calls include search, describe, call, result, and skill. `emit` has separate budgets.
715
+ An abandoned host call counts as failed when the run ends; guest catches do not
716
+ change the counts. These numbers are outside the program's returned `result`.
717
+ `diagnostics: true` adds the existing payload-free timing block. Emitting adds
718
+ `emitted: N` and its content blocks.
522
719
 
523
720
  **R7.** Diagnostic timing separates admission, provider setup, total executor
524
721
  wall time, catalog work, and connector work. Catalog and connector values are
525
722
  cumulative, so parallel work can exceed executor wall time. Each used operation
526
- kind (`search`, `describe`, `call`) gets one aggregate with count, failures,
723
+ kind (`search`, `describe`, `call`, `read`) gets one aggregate with count, failures,
527
724
  duration, returned serialized bytes, and catalog/connector time.
528
725
 
529
726
  **R8.** Diagnostics contain measurements and fixed operation names only: no
@@ -533,13 +730,37 @@ request; it is not activity, a session, or a stream.
533
730
 
534
731
  ## Emitted output
535
732
 
733
+ Guest-authored images accept only `image/png`, `image/jpeg`, `image/gif`, or `image/webp`
734
+ and nonempty canonical base64. Whitespace, URL-safe alphabets, bad padding,
735
+ and nonzero pad bits are rejected before collection. Size budgets still apply.
736
+
536
737
  MCP-native output a return value cannot carry: base64 is not projectable, so a
537
- block that survives intake uncapped (`S5`) must not die at the `R2` exit guard.
538
- The refused alternatives are in `ethos.md`
738
+ block that fits the inline call limit (`S5`, `L6`) can use the separate
739
+ emission transport budget instead of the `R2` return guard.
740
+ The earlier alternatives are in [decision history](https://github.com/zackbart/connecta/blob/main/decisions/0001-ethos-verdict-table.md)
539
741
  ([#267](https://github.com/zackbart/connecta/issues/267),
540
742
  [#270](https://github.com/zackbart/connecta/issues/270)).
541
743
 
542
- **M1.** `connecta.emit(block)` accepts exactly one block: `{ type: "text",
744
+ **M1.** `connecta.emit(result)` accepts a rich result from this program's
745
+ `connecta.call` and forwards its original native content blocks, including
746
+ resources, resource links, annotations and `_meta`. `connecta.emit(block)` also
747
+ accepts individual entries of that result's `content`. The trusted guest
748
+ prelude maps these objects to opaque request-local references; the host checks
749
+ those references and emits owned deep snapshots of retained native blocks taken
750
+ before result paging can yield. Oversized structured data returns its paging
751
+ handle before native presentation is inspected. Presentation traversal stops at
752
+ 64 levels, 32,768 values, or the larger of the default and configured emission
753
+ byte budgets. Only envelopes within the 1 MiB inline cap retain block snapshots.
754
+ Cycles, non-JSON values, and traversal limits refuse presentation with an
755
+ unavailable-paging notice; structured data remains usable. That notice has no
756
+ result ID and reports `totalBytes: 0` because traversal could not measure a
757
+ complete JSON value.
758
+ Connector mutations after retention cannot change guest output, accepted blocks,
759
+ or their byte and text charges, including nested resource fields and metadata.
760
+ Copies and guest-authored blocks keep the strict validation below. No program
761
+ can mint a native URI.
762
+
763
+ Guest-authored `connecta.emit(block)` accepts exactly one block: `{ type: "text",
543
764
  text }` or `{ type: "image" | "audio", data /* base64 */, mimeType }`, every
544
765
  field a string, no extra fields, no `annotations`, no `_meta`, no sugar forms.
545
766
  An invalid block throws catchably and nothing is accepted — rejected, not
@@ -552,34 +773,44 @@ a successful result, appended to `content` after the JSON envelope, which gains
552
773
  response (`R6`). `structuredContent` stays the envelope alone — emission is
553
774
  presentation, not a second data channel.
554
775
 
555
- **M3.** Return value and emission are independent: `R2` never measures emitted
556
- bytes, a truncated return does not suppress delivered blocks, and blocks do not
557
- shrink the return budget.
776
+ **M3.** Text emission shares `R2`'s 24,000 serialized-character cap. Its
777
+ serialized block size reduces the return budget, and oversized text fails with
778
+ `result_too_large`. Image/audio blocks use the separate transport budget and
779
+ reach the model as real MCP media content, including when `emit` is not awaited.
780
+ Native embedded resource text also shares the result cap; resource blobs count
781
+ against the serialized-byte emission budget.
782
+ The trusted runner waits for emission acknowledgments before returning success.
783
+ An unobserved emission failure fails the run; an awaited failure remains catchable.
784
+ Rejections propagated through `then` or `finally` can be caught later in that
785
+ chain. A separate unhandled branch still fails the run.
558
786
 
559
787
  **M4.** A failed program delivers no blocks. The error result reports
560
- `emittedDiscarded: N` when N > 0 — a field on the structured envelope, a
561
- trailing line on the plain-text paths — never silently.
788
+ `emittedDiscarded: N` when N > 0 as a field on the structured envelope.
562
789
 
563
790
  **M5.** Two budgets (`ConnectaConfig.execute.maxEmittedBytes` /
564
791
  `.maxEmittedBlocks`, defaults 4,000,000 serialized bytes and 32 blocks) fail
565
792
  loudly at the `emit` call, naming the budget and the room remaining; nothing is
566
- partially accepted and prior blocks stand. No `get_result` stash: the program
793
+ partially accepted and prior blocks stand. Forwarding a whole native result
794
+ checks every block before accepting any of them. Accepted and rejected emission attempts
795
+ also have a terminal limit of four times the configured block cap (at least 128),
796
+ which bounds retained host failures and guest acknowledgement state without
797
+ discarding an older error identity. No result stash for emitted blocks: the program
567
798
  learns while it can still choose differently. The byte default is a transport
568
799
  bound, not a context bound — emitted media reaches the model as media, not
569
800
  base64 text.
570
801
 
571
802
  **M6.** No provenance is claimed: every emitted block is program output, trusted
572
- exactly as much as the return value. Preservation is re-emission of the raw
573
- downstream block, so `S5`'s uncapped fallthrough is contract.
803
+ exactly as much as the return value. Native forwarding preserves the raw
804
+ downstream block through a host-issued reference, within `S5`, `L6` and `M5`.
574
805
 
575
806
  **M7.** `emit` alone spends no host-call budget (`L4`); `search`, `describe`,
576
- and `call` share it. `M5`'s bounds are emission's only bounds.
807
+ `call`, `read`, `result`, and `skill` share it. Text also observes `R2`.
577
808
 
578
- **M8.** Emission asks nothing of an executor: `emit` is a provider function,
579
- blocks cross the guest boundary once as an argument, and `ExecuteResult` is
580
- unchanged — `Executor` stays assignable from `@cloudflare/codemode`'s
581
- `DynamicWorkerExecutor`, so any executor that bridges provider calls gets
582
- emission for free.
809
+ **M8.** `emit` is a provider function. Guest-authored blocks cross as arguments;
810
+ native blocks cross once as call output and emissions send only their references.
811
+ Native call output uses the same bounded stash transfers as `L6`. `ExecuteResult` remains compatible with upstream codemode's types;
812
+ construction requires the Worker adapter or explicit custom opt-in. Any executor
813
+ that bridges provider calls gets emission for free.
583
814
 
584
815
  **M9.** Request-local and unstreamed: blocks exist only in the finished
585
816
  response, and `emit` resolving means "accepted," never "delivered."
@@ -607,7 +838,7 @@ host-call budget, so an unchecked loop converts a transient failure into
607
838
  Dynamic-Worker-only wait would spend the run's wall clock on code that fails
608
839
  on QuickJS. Return the failure and let the model, which can wait, re-issue
609
840
  with `retryAfterMs` in hand.
610
- - a cancelled or timed-out *execution*: it is already over (`L1`).
841
+ - a cancelled or timed-out _execution_: it is already over (`L1`).
611
842
 
612
843
  **Y4.** A provider's `retryAfterMs` is returned unchanged; the caller decides
613
844
  whether and when to reissue, and a later call gets its own deadline and
@@ -620,14 +851,21 @@ poll, no cancellation exception to catch, and no guarantee that a `finally`
620
851
  block runs — a cancelled QuickJS child is terminated outright. Write programs
621
852
  that need no cleanup.
622
853
 
623
- **L2.** What cancellation guarantees: in-flight host calls abort, no further
624
- host call is admitted — `search` and `describe` included, which fail
625
- `cancelled` rather than go on loading catalogs — the response returns
626
- `executor_cancelled` without waiting for the executor to settle, or for an
627
- `acquire()` that ignores the signal to grant, the admission lease is released
628
- (a lease granted after the run gave up is released on arrival), and nothing
629
- request-bound survives the request. Whether the program itself stops is the
630
- executor's (`X3`).
854
+ **L2.** Cancellation aborts in-flight host calls and admits no further calls,
855
+ including discovery. The response returns `executor_cancelled` without awaiting
856
+ a wedged executor or `acquire()` that ignores its signal. It releases the lease;
857
+ a late-granted lease releases on arrival. Nothing request-bound survives the
858
+ request. Whether guest computation stops is the executor's (`X3`).
859
+
860
+ Programs and direct calls use the same invocation deadline and stages. A call's
861
+ `timeoutMs` wins, then `calls.defaultTimeoutMs`, then `execute.hostCallTimeoutMs`
862
+ (default 15,000). A guest may request a longer individual call, but the sandbox
863
+ and request ceilings still bound the whole run. Timeout details are agent-facing;
864
+ INV-6 operator records retain only checked classifications and numeric facts.
865
+ A dispatched write timeout becomes `write_outcome_unknown` and retains its
866
+ deadline details even when the program catches the failure. The final program
867
+ error includes the first timeout in `details`; multiple write timeouts also
868
+ appear in `timeouts`, bounded to ten entries.
631
869
 
632
870
  **L3.** Every execution runs under a wall-clock deadline that includes time
633
871
  spent waiting on host calls. Expiry ends the run with an execution error and no
@@ -636,7 +874,7 @@ it sits a ceiling connecta enforces outside the sandbox, `execute.watchdogMs`
636
874
  (120 s by default, above both executors' own deadlines), because an executor's
637
875
  deadline may live inside the sandbox it is meant to stop: a wedged Dynamic
638
876
  Worker never fires its in-isolate timer. A run whose executor has not settled
639
- by the ceiling ends as a non-retryable `executor_failed` whose message calls
877
+ by the ceiling ends as a non-retryable `timeout` whose message calls
640
878
  the sandbox unresponsive, with no partial result, and its admission lease is
641
879
  released, so wedged runs cannot fill the code pool. Raising an executor's
642
880
  deadline past the ceiling means raising the ceiling too.
@@ -644,25 +882,38 @@ deadline past the ceiling means raising the ceiling too.
644
882
  **L4.** Per-execution bounds that are contract, identical in both executors
645
883
  because connecta enforces them above the sandbox:
646
884
 
647
- | Bound | Value |
648
- | --- | --- |
649
- | Host calls per execution, shared by `search`, `describe`, and `call` | 20 by default, `execute.maxHostCalls` |
650
- | Exempt writes per run, on top of the host calls they also spend (`W10`) | 10 by default, `execute.maxWrites` |
651
- | Deadline per host call | 15 s, `execute.hostCallTimeoutMs`; one deadline covers catalog resolution, admission, and the connector call |
652
- | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
653
- | `describe` addresses | ≤ 100 |
654
- | `describe` nearby suggestions | ≤ 3 canonical addresses per failed entry |
655
- | Caller text echoed by `describe` recovery | ≤ 512 UTF-8 bytes per field, plus `…` |
656
- | Result | 24,000 serialized characters |
657
- | Logs presented to the model | 4,000 characters |
658
-
659
- Every `call` attempt spends one host call on entry, before address resolution,
660
- catalog lookup, safety checks, validation, or dispatch, so unknown addresses,
661
- unknown tools, catalog failures, and other pre-dispatch refusals cost what a
662
- successful call costs; catching a refusal does not refund it. `search` and
663
- `describe` likewise spend on entry. Exhausting the budget fails that call with
664
- non-retryable `budget_exceeded` (`E2`) and a message naming the budget. No
665
- connector is reached, and the budget does not refill inside one execution.
885
+ | Bound | Value |
886
+ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
887
+ | Host calls per execution, shared by `search`, `describe`, `call`, and `read` | 20 by default, `execute.maxHostCalls` |
888
+ | Trusted-pool writes per run, on top of the host calls they also spend (`W10`) | 10 by default, `execute.maxWrites` |
889
+ | Deadline per host call | 15 s, `execute.hostCallTimeoutMs`; one deadline covers catalog resolution, admission, and the connector call |
890
+ | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
891
+ | `describe` addresses | ≤ 100 |
892
+ | `describe` nearby suggestions | ≤ 3 canonical addresses per failed entry |
893
+ | Caller text echoed by `describe` recovery | ≤ 512 UTF-8 bytes per field, plus `…` |
894
+ | Result | 24,000 serialized characters |
895
+ | Logs presented to the model | 4,000 characters |
896
+
897
+ Every `call`, `read`, `search`, and `describe` spends one host call on entry, before
898
+ resolution, validation, or dispatch. Catching a local refusal refunds nothing.
899
+ The first call beyond the budget ends the host run with one non-retryable
900
+ `budget_exceeded` (`E2`). Its bridge stays pending until lease disposal so guest
901
+ `try/catch` cannot swallow it. Later host access, including `emit`, stops; pending
902
+ replies are withheld. The refusal reaches no connector, returns no partial result,
903
+ and discards accepted emits (`M4`). Calls still resolving or awaiting admission
904
+ are cancelled, including trusted-pool writes past their gate. Dispatched trusted-pool writes
905
+ drain under `W9`; unknown outcomes retain precedence. The lease then releases:
906
+ QuickJS terminates its child, and the Worker adapter disposes RPC and loader
907
+ handles independently of the guest deadline. Workers guest computation may
908
+ continue briefly, including a timer escaping the bridge (`X3`), with no host
909
+ access. The Promise executor contract carries typed failures separately from guest values.
910
+
911
+ The failure adds payload-free `hostCalls: { attempted, admitted, succeeded, failed }`
912
+ without diagnostics. `attempted` includes the first refusal, normally 21; `admitted`
913
+ counts the 20 calls past the gate, local refusals included. `succeeded` and `failed`
914
+ count outcomes at response time, the budget refusal and abandoned pending calls
915
+ as failed, so `succeeded + failed === attempted`. `emit` and later attempts are
916
+ excluded; no budget refills.
666
917
 
667
918
  **L5.** The guest is memory-, stack-, and CPU-bounded, and a program that
668
919
  exhausts a bound ends the run with an error instead of degrading the host. The
@@ -672,22 +923,44 @@ the Dynamic Worker inherits the platform isolate's limits (`X2`). A third
672
923
  executor must bound all three somehow — this is the clause that makes untrusted
673
924
  code safe to run at all.
674
925
 
675
- **L6.** A host call's serialized arguments and its serialized result are each
676
- bounded — QuickJS caps both at 256 KiB (`X10`) — and exceeding either fails that
677
- call, not the execution, so a program can catch it and ask for less. The failure
678
- is executor-owned untyped text, not a Connecta host failure (`E1`). An
679
- over-bound *result* names the address the program called rather than only the
680
- generic bridge function; an over-bound *argument* payload is refused before it
681
- is parsed, so it names no address at all.
926
+ **L6.** QuickJS bounds each serialized host-call message at 256 KiB. Full guest
927
+ returns cross bounded 65,536-character IPC chunks, with a 64 MiB aggregate
928
+ transport ceiling. The parent retains the completed value until it can apply
929
+ the result stash policy; the 1 MiB per-message IPC ceiling stays in force. Oversized
930
+ `connecta.call` results use the existing direct-call stash before crossing that
931
+ bridge. The trusted guest initializer reconstructs values up to 1 MiB from
932
+ bounded pages, retaining JSON versus text format and every payload code point,
933
+ including U+FEFF at page starts. Larger values return a
934
+ `{ format: "paged", valueFormat, resultId, totalBytes, truncated: true, hint }`
935
+ handle. Pass the handle or its `resultId` to `connecta.result` and reduce the
936
+ pages before returning them. Automatic transfers spend no additional host-call
937
+ budget; explicit paging does. This policy is shared by QuickJS, Worker and custom
938
+ executors that run provider preludes.
939
+ Oversized raw text containing unpaired UTF-16 surrogates returns a handle without
940
+ `resultId`, with a notice that it cannot be paged as text. This preserves the
941
+ UTF-8 byte-offset contract without silently replacing code units. The completed
942
+ call stays successful, including write accounting; JSON-wrapped strings still
943
+ round-trip losslessly.
944
+
945
+ Stashes retain their existing bindings, 15-minute expiry, per-connector page
946
+ caps and aggregate capacity charges on memory, SQLite and D1. Capacity exhaustion
947
+ or a storage failure returns a successful notice without a `resultId`. A failed
948
+ automatic reconstruction retains the handle. Neither result path reissues the
949
+ call or changes a completed write into a failed or unknown write. Guest returns use the same store when they exceed the presentation cap (`R2`–`R4`).
950
+
951
+ Over-bound arguments and other unpaged bridge results still fail catchably with
952
+ executor-owned text. An over-bound result names the called address; an argument
953
+ payload is refused before parsing and names no address.
682
954
 
683
955
  **L7.** Executions are admitted, not queued indefinitely: bounded concurrency
684
956
  plus a bounded queue with a wait timeout. Overload is a retryable
685
957
  `executor_overloaded` carrying `retryAfterMs`; cancellation and shutdown are
686
- terminal. Admission happens *before* any catalog or provider is built, so a
958
+ terminal. Admission happens _before_ any catalog or provider is built, so a
687
959
  queued request holds no state.
688
960
 
689
- **L8.** Bounds are deployment configuration, not program inputs: a program
690
- cannot raise one by asking. `execute_code`'s description states the host-call
961
+ **L8.** Result, log, host-call, and emission budgets are deployment configuration.
962
+ A call may request `timeoutMs` just as a direct call does; the guest deadline is
963
+ capped by the time remaining before the program watchdog. `execute_code`'s description states the host-call
691
964
  budget and the per-call deadline — the ones a program must plan around before it
692
965
  runs. The result and log caps live here and in the truncation notice itself
693
966
  (`R2`, `R5`).
@@ -705,7 +978,7 @@ event.
705
978
 
706
979
  **V2.** Each event carries `connectorId`, `toolName`, `address`, `source`,
707
980
  `outcome` (`success`, `error`, `timeout`, `cancelled`), `durationMs`,
708
- `attempts`, and `errorCode` when the call *failed* — plus request, actor, and
981
+ `attempts`, and `errorCode` when the call _failed_ — plus request, actor, and
709
982
  server identity. Rows written before
710
983
  [#672](https://github.com/zackbart/connecta/issues/672) may also carry the
711
984
  `paused` and `approved` outcomes, `source: "resume_execution"`, and an
@@ -719,65 +992,89 @@ is still recorded. `address` is canonical (`A1`) where a tool resolved,
719
992
  otherwise the name the program used — the honest record of what was attempted.
720
993
 
721
994
  **V3.** A call whose connector does not exist is recorded at the address as
722
- written, *provided* it split into the two fields activity keeps — one with no
995
+ written, _provided_ it split into the two fields activity keeps — one with no
723
996
  interior dot records nothing. An invented id is the address mistake an operator
724
997
  most needs to see, but recording it as written puts caller-authored text in
725
998
  fields that are otherwise operator- and connector-authored, so `connectorId`
726
999
  and `toolName` clamp at 128 UTF-8 bytes (`address` at 257) with a `…` marker:
727
- payload-free *by construction* means the event has nowhere to put a payload.
1000
+ payload-free _by construction_ means the event has nowhere to put a payload.
728
1001
 
729
1002
  **V4.** The execution itself emits no event. It has no address, and its one
730
- distinctive artifact is the program source — exactly what a payload-free history
1003
+ distinctive output is the program source — exactly what a payload-free history
731
1004
  must never keep.
732
1005
 
733
- **V5.** An exempt write a program issues after it has already returned is not
1006
+ **V5.** A trusted-pool write a program issues after it has already returned is not
734
1007
  sent and records nothing: it was never an attempt (`W9`).
735
1008
 
736
1009
  ## Writes
737
1010
 
738
- A program does not ask anyone anything. It runs explicitly read-only tools and
739
- the writes config exempts from asking, and refuses every other write (`E4`), so
740
- each consequential call is its own `call_destructive_tool` and the host's
741
- permission prompt is the one approval there is. Programs once paused at a write
742
- and resumed by replay
743
- ([#565](https://github.com/zackbart/connecta/issues/565)); hosts approve one
744
- call at a time, so under always-allow that prompt approved nothing and cost a
745
- round trip and a replay, and it went
746
- ([#672](https://github.com/zackbart/connecta/issues/672)). `W1`–`W8` and `W11`
747
- belonged to pausing and stay retired; nothing reuses those ids.
748
-
749
- **W9.** An exempt write the program does not await outlives it: the play stops
750
- gating once the program settles — a write that reaches the gate after is not
751
- sent (`V5`) — and waits for writes already dispatched, each within its host-call
752
- deadline, before the run's scope aborts them. A write sent and never answered
753
- makes the result `write_outcome_unknown` with
754
- `writes: { succeeded, failed, unknown }`, whatever the program returned, and it
755
- is never sent again. Only a refusal (`auth_required`, `invalid_args`,
756
- `not_found`, `rate_limited`, `conflict`, `input_required_unsupported`) or the
757
- tool's own `isError` answers; an `isError` reading as a timeout does not, and
758
- `connector_call_failed` — an oversized body, a refused redirect — may come after
759
- the write landed. A program that fails after writing carries the same `writes`
760
- on its error, because it is not a program that is safe to run again.
761
-
762
- **W10.** `execute.maxWrites` (default 10) bounds a run's exempt writes beyond
763
- the host calls they spend, checked at the gate after validation and before
764
- admission, so an over-budget write fails `budget_exceeded` without a permit.
765
- The host-call budget bounds fan-out; this one bounds how much a program may
766
- change unasked, which is a different question even for writes a deployment
767
- called cheap.
768
-
769
- **W12.** Config may exempt a write from asking, and nothing else may
770
- ([#566](https://github.com/zackbart/connecta/issues/566)). `execute.approval`
771
- maps connector ids and `connector.tool` addresses to `"never"` or `"ask"`; the
772
- address wins over the connector entry, which wins over a connector's own
773
- built-in default (reserved for connectors connecta ships, such as artifacts),
774
- which wins over `"ask"`. An exempt call is decided at the write gate, after
775
- validation and before admission, and dispatches with the write budget (`W10`),
776
- `W9`'s tracking, and a `V1` event. Exempt is never read-only: no read-only tool
777
- is reported exempt, discovery keeps an exempt tool approval-required,
778
- `call_tool` refuses it, and no annotation grants it, or a downstream could
779
- exempt itself. Unknown connectors and `api()` addresses its tools lack refuse
780
- to construct; an unserved remote address never matches.
1011
+ Pool trust belongs to deployment code. The default `/mcp` endpoint uses
1012
+ `trust: "read-only"`; each named pool has its own `trust`, also defaulting to
1013
+ `"read-only"`. A trusted default endpoint does not make named pools trusted.
1014
+ The default preserves refusal of ordinary program writes. A deployment must opt into program writes explicitly.
1015
+
1016
+ ```ts
1017
+ createConnecta({
1018
+ connectors, executor,
1019
+ trust: "read-only",
1020
+ pools: {
1021
+ automation: { tools: ["posthog"], trust: "trusted", grant },
1022
+ },
1023
+ classification: {
1024
+ posthog: { exec: "read" }, // only for a deployment with a read-only exec contract
1025
+ vendor: { mislabeled_update: "write" },
1026
+ },
1027
+ });
1028
+ ```
1029
+
1030
+ The registry computes one `classifyTool` verdict on each request-scoped
1031
+ published catalog entry. It caches and persists only raw downstream facts.
1032
+ Every caller consumes that stored verdict. Config cannot classify an individual
1033
+ invocation's arguments: marking a mixed entry point read is an assertion about
1034
+ all calls this deployment can make through it.
1035
+
1036
+ | Precedence | Evidence | Result |
1037
+ | ---------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
1038
+ | 1 | Exact deployment `classification[connectorId][toolName]` | `read` or `write`, even over a stale provider review |
1039
+ | 2 | `Connector.classification` provider review | Reviewed writes remain writes; a stale schema digest makes a read a write; an explicit downstream contradiction also invalidates a reviewed read |
1040
+ | 3 | Downstream annotations | Read only when `readOnlyHint === true && destructiveHint !== true`; otherwise write |
1041
+
1042
+ Unknown connector override keys fail construction. Unknown static tool keys
1043
+ fail construction; unknown remote tool keys fail publication of the whole
1044
+ catalog. Overrides are exact tool names, including names containing dots.
1045
+ They are never inferred from descriptions or arguments.
1046
+
1047
+ **W9.** Programs in trusted pools may write. A dispatched write is awaited even
1048
+ if the program does not await it, bounded by its host-call deadline. A write
1049
+ that reaches accounting after the program settles is not sent. An unanswered
1050
+ write makes the run fail `write_outcome_unknown` with
1051
+ `writes: { succeeded, failed, unknown }`, regardless of its return value.
1052
+ A failed program that already wrote carries those counts on its error. No
1053
+ write is automatically retried, including an ambiguous timeout. A downstream
1054
+ refusal or a tool's `isError` can establish failure; a timeout or an unanswered
1055
+ transport failure leaves the outcome unknown.
1056
+
1057
+ **W10.** `execute.maxWrites`, default 10, bounds writes in trusted programs in
1058
+ addition to the host-call budget. It is checked after argument validation and
1059
+ before admission, so an over-budget write spends no permit.
1060
+
1061
+ Program `uncertainCall` and `uncertainCalls` use the same schema-filtered argument
1062
+ snapshots as direct calls, even when guest code catches the failure. `argsRedacted: true`
1063
+ means sensitive fields were omitted or the entire echo was withheld because
1064
+ sensitivity could not be resolved. `argsOmitted: true` means no `args` is present,
1065
+ including echoes above the byte budget. These fields identify targets for
1066
+ reconciliation. Any new call must use the original arguments when its echo is
1067
+ partial or absent. A returned `writes` count cannot make a run safe to repeat.
1068
+ See [argument echo rules](./meta-tools.md#echo-budgets) for supported schema shapes.
1069
+
1070
+ **W12.** `trusted` endpoints annotate `execute_code` as a write and permit
1071
+ program writes. `read-only` endpoints annotate it as read-only and refuse
1072
+ program writes before validation, with `call_destructive_tool` as the recovery.
1073
+ `call_tool` accepts only reads in either tier; `call_destructive_tool` may send
1074
+ writes in either tier. Approval belongs to the host. Trust grants no tool or
1075
+ connector access and cannot be set by guest code, request arguments, or a
1076
+ downstream catalog. `execute.approval`, `Connector.approval`, and per-tool
1077
+ approval exemptions are removed. `W1`–`W8` and `W11` remain retired.
781
1078
 
782
1079
  ## Executor exceptions
783
1080
 
@@ -796,12 +1093,13 @@ isolate limits apply untuned. A specific heap ceiling is a Node-only option.
796
1093
  `AbortSignal` and kills the child — SIGTERM, then SIGKILL if it has not exited
797
1094
  within a second — and releasing a lease whose child is still running recycles
798
1095
  that child, so a run the watchdog abandons (`L3`) ends too.
799
- The Dynamic Worker executor's `execute()` takes no signal, so a cancelled or
800
- abandoned program runs on in its isolate until its host calls fail or its own
801
- deadline expires. `L2` and `L3` hold either way — the calls abort, connecta
802
- stops awaiting the executor and frees the admission slot — but "the run ends"
803
- is best-effort on Workers, and for that tail the code pool bounds what
804
- connecta is waiting on, not what the platform is still running.
1096
+ Upstream `execute()` takes no signal; its guest timeout may never settle after
1097
+ the parent response ends. `workerExecutor()` owns each lease's RPC and loader
1098
+ handles: release disposes them, detaches host functions, and settles the adapter's
1099
+ promise independently of upstream evaluation. Guest computation may continue
1100
+ briefly until platform teardown, but no late return, callback, `finally`, call,
1101
+ or emit changes the finished host result. Disposal releases request resources;
1102
+ it does not guarantee a synchronous guest-CPU kill.
805
1103
 
806
1104
  **X4. Log rendering and capture.** QuickJS JSON-stringifies non-string arguments
807
1105
  and captures `log`, `info`, `warn`, `error`, and `debug`; the Dynamic Worker
@@ -820,14 +1118,16 @@ starts has no guest logs to recover.
820
1118
  `process`, timers, `crypto`, or `WebSocket`, and its Node child starts with an
821
1119
  explicit environment holding only `TZ=UTC`, so local time matches a Dynamic
822
1120
  Worker's, rather than inheriting
823
- deployment variables or `NODE_OPTIONS`. A Dynamic Worker has those globals plus a non-contract set of
824
- runtime builtins through `import()` and `process.getBuiltinModule()`, including
825
- `node:path`, `node:crypto`, `node:net`, `node:tls`, `node:dns`, `node:module`,
826
- and `cloudflare:workers`; the upstream set drifts, so that is not an allowlist.
827
- The supported construction is exactly `new DynamicWorkerExecutor({ loader })` —
1121
+ deployment variables or `NODE_OPTIONS`. A Dynamic Worker exposes runtime-only
1122
+ builtins through `import()` and `process.getBuiltinModule()`, including `node:path`,
1123
+ `node:crypto`, `node:net`, `node:tls`, `node:dns`, and
1124
+ `cloudflare:workers`; the upstream set drifts, so that is not an allowlist.
1125
+ The adapter refuses `node:module` and `node:process` imports and removes their
1126
+ builtin lookup routes, so `createRequire` cannot resolve runner modules.
1127
+ The supported adapter uses `new DynamicWorkerExecutor({ loader, timeout })`.
828
1128
  `bindings`, `modules`, and `globalOutbound` each grant ambient configuration,
829
- code, or egress. Under it, `process.env`, lexical `this.env`, and
830
- `cloudflare:workers.env` are empty; `node:fs`, `node:http`, and `node:https` are
1129
+ code, or egress. Under it, `process.env` and `cloudflare:workers.env` are empty,
1130
+ and lexical `this.env` is undefined; `node:fs`, `node:http`, and `node:https` are
831
1131
  unavailable through either access route; external `fetch`, `WebSocket`,
832
1132
  `node:net`, and `node:tls` fail with workerd's outbound-denial error; DNS lookup
833
1133
  ends unresolved; and `fetch("data:...")` resolves locally. `P2` is the portable
@@ -844,9 +1144,9 @@ reasonable demand on a platform sandbox.
844
1144
  values through a tagged envelope, so a `Uint8Array` may survive there. `P3` is
845
1145
  the contract: JSON-serializable values, or the program is Workers-only.
846
1146
 
847
- **X8. Unknown-property message.** An unknown `connecta` property throws
848
- `Unknown function connecta.x` on QuickJS and `Tool "x" not found` on the Dynamic
849
- Worker. Both satisfy `E6`; the text is not contract.
1147
+ **X8. Unknown properties.** The guest namespace exposes only the seven documented
1148
+ functions. Unknown and inherited members are absent on both executors; calling
1149
+ one is a guest `TypeError` under `E6`. Engine message text is not contract.
850
1150
 
851
1151
  **X9. Refusing a value outside JSON.** The Dynamic Worker ends the run with an
852
1152
  error when a program returns something its codec cannot carry. QuickJS converts
@@ -856,81 +1156,87 @@ Normalizing that would spend real CPU on every program to improve the error
856
1156
  message of a program that is already wrong. `P3` is the contract: neither
857
1157
  behavior returns the value.
858
1158
 
859
- **X10. Per-host-call payload bound.** `L6`'s 256 KiB ceiling on a host call's
860
- arguments and result is QuickJS's, enforced at its IPC boundary. The Dynamic
861
- Worker has no documented equivalent; Workers RPC limits apply and connecta adds
862
- none, because that boundary is an isolate-to-isolate call rather than a
863
- `process.send` with a hard ceiling. A program returning a quarter-megabyte from
864
- one tool call therefore fails on Node and may succeed on Workers — reduce inside
865
- the program either way (`R1`).
1159
+ **X10. Per-host-call payload bound.** QuickJS enforces `L6`'s 256 KiB message
1160
+ ceiling at its IPC boundary. The Dynamic Worker uses Workers RPC limits.
1161
+ Connecta's shared program result policy keeps call results and result pages
1162
+ within the QuickJS message ceiling on every executor. It does not raise the
1163
+ argument limit or the guest's heap, CPU, execution, or returned-result limits.
866
1164
 
867
1165
  **X12** described a paused sandbox and left with pausing
868
1166
  ([#672](https://github.com/zackbart/connecta/issues/672)); the id stays retired.
869
1167
 
870
- **X11. Typed host rejection.** Both executors rebuild Connecta's authenticated
871
- host-failure frame as a thrown guest `Error` (`E1`). The per-run secret stays in
872
- the trusted prelude closure and the prelude locks `globalThis.Error`, so guest
873
- code and connector prose cannot forge the frame. The host bounds details before
874
- framing (`E1`), JSON escapes included; QuickJS refuses an oversized frame whole
875
- rather than slicing through its JSON, hides one whose JSON is malformed, and
876
- keeps the raw bridge and its JSON decoder in a private closure so guest code
877
- cannot intercept a frame first. A mismatched frame is ordinary untyped prose.
1168
+ **X11. Typed host rejection.** QuickJS creates and retains guest Error handles
1169
+ in its host bridge and compares handles when the program rejects. The Worker
1170
+ adapter stores each typed failure in a per-run host map under a random UUID.
1171
+ Its private entrypoint associates that ID with the guest Error and returns only
1172
+ the ID through Worker Loader RPC. The host resolves it to its own record before
1173
+ upstream result shaping; unknown or forged IDs attach no typed details, and IDs
1174
+ from previous runs cannot resolve. Rethrowing an earlier call's Error retains
1175
+ that call's record. The guest receives a separate copy for catching errors.
1176
+ Both construct `failure.call` from host-retained values, never from a guest
1177
+ result, parsed guest-realm object, or printed text. The Worker codec checks own
1178
+ binary-envelope properties, and completion uses captured array iteration even
1179
+ when the executor runs without the Connecta guest prelude. Host failure details are
1180
+ bounded before bridging (`E1`), including JSON escapes. No frame parser or
1181
+ prose matching participates in classification. The guest cannot import runner
1182
+ modules or repeat privileged initialization. QuickJS checks host interrupt and
1183
+ deadline facts before describing a rejection. It retains a private native Error
1184
+ brand check and reads only own string data descriptors for diagnostics; guest
1185
+ accessors, serialization hooks, and Proxy traps do not run. Other thrown objects
1186
+ receive a fixed description.
878
1187
 
879
1188
  ## Verification
880
1189
 
881
- Every clause has a test. `test/guest-contract-cases.ts` holds the case table,
882
- written once and run twice: `test/guest-api-contract-quickjs.test.ts` runs it on
883
- the Node QuickJS executor, and `test/guest-api-contract.test.ts` runs it on a
884
- real `DynamicWorkerExecutor` in workerd — a Miniflare Worker Loader binding
885
- makes that arm real rather than simulated — alongside the clauses connecta
886
- enforces above any executor. Rows naming `test/guest-api-contract.test.ts` are
887
- covered by both arms, and each case's title carries its clauses. Two arms
888
- passing one table is also the check on the executor duties above, with
889
- `test/codemode-compat.test.ts` holding the upstream `Executor` shape assignable.
890
-
891
- | Clauses | Test |
892
- | --- | --- |
893
- | `P1`, `P5` | both guest-contract executors (TypeScript syntax, trailing terminators, wrapper recovery), `test/program-source.test.ts` (which semicolons and wrappers are recovered), `test/quickjs-executor.test.ts` (`normalizeCode`) |
894
- | `P2`, `X5` | `test/guest-api-contract.test.ts` (Dynamic globals plus loader-only filesystem, HTTP, environment, egress, DNS, and local `data:` boundaries), `test/guest-api-contract-quickjs.test.ts` (exact absent globals and blocked imports), `test/quickjs-child-stderr.test.ts` (a child-process environment holding only `TZ=UTC`), `test/deployment-shapes.test.ts` (loader-only Worker construction) |
895
- | `P3`, `X9` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` |
896
- | `P4` | `test/guest-api-contract.test.ts` (no cross-run leakage), `test/execute.test.ts` (one catalog load per connector per execution) |
897
- | `A1`, `A2` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (canonical addressing), `test/server.test.ts` (bounded live connector inventory) |
898
- | `S1`, `S1a`, `S2` | `test/guest-api-contract.test.ts` (flat page, connector guides, schema keys, unfiltered browse), `test/execute.test.ts` (guide pagination/partial/no-match behavior and `$ref`/`allOf`), `test/meta-tools-search.test.ts` (mixed complete/partial ranking, stable pagination, the two safety classes), `test/typescript-signatures.test.ts` (the TypeScript format over provider and pathological schemas, search/describe parity, observed labeling), `test/typescript-signatures-parse.test.ts` (every rendering parses) |
899
- | `S3` | `test/guest-api-contract.test.ts` (typed uncaught bound), `test/execute.test.ts` (count limits, fan-out bound) |
900
- | `S4` | both guest-contract executors (ordered mixed describe results with unknown-address, unknown-tool suggestion, and catalog-failure details), `test/meta-tools-search.test.ts` (top-level routing, no-suggestion, catalog-failure, and hostile-input bounds) |
901
- | `S5`, `S6` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`unwrapMcpResult`, fail-closed annotations, activity parity) |
902
- | `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (parallel calls and shared admission) |
903
- | `S8`, `E1`, `X11` | both guest-contract executors (caught call, discovery, utility, budget, removed-function, and forgery cases; typed promise rejections), `test/quickjs-executor.test.ts` (oversized messages, private transport, forged frames) |
904
- | `S9` | `test/result-shapes.test.ts` (value exclusion, bounds, merging, LRU and time expiry, runtime isolation, read-only admission, declared precedence, definition invalidation, unwrapped MCP results, discovery provenance, copy isolation, failure isolation) |
905
- | `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, parallel, and uncaught validation recovery, a conflict's bounded `current`), `test/meta-tools-call.test.ts` (direct, destructive, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
906
- | `E3`, `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`, destructive reroute), `test/program-writes.test.ts` (the refusal's `nextAction`, nothing sent, a caught refusal) |
907
- | `E5` | `test/guest-api-contract.test.ts` (execution-failure channel, in-flight `cancelled`), `test/execute.test.ts` (admission), `test/executor-admission.test.ts`, `test/quickjs-executor.test.ts` (mid-run shutdown) |
908
- | `E6`, `X8` | `test/guest-api-contract.test.ts` (unknown and inherited members, wrapped-message precedence), `test/quickjs-executor.test.ts` |
909
- | `E7` | `test/guest-api-contract.test.ts` (refusals about a `503`-named connector), `test/errors.test.ts` |
910
- | `R1`, `R2`, `R3` | `test/guest-api-contract.test.ts` (pass-through, truncation is success, envelope fits the cap and is idempotent) |
911
- | `R4`, `M6`, `M9` | verdicts; `R2`'s guard, `M1`'s strict typing, and `M2`'s collect-then-deliver are their enforcement |
912
- | `R5` | `test/guest-api-contract.test.ts`, `test/quickjs-log-limits.test.ts` |
913
- | `R6`–`R8` | `test/guest-api-contract.test.ts` (normal result keys), `test/execute.test.ts` (opt-in operation aggregates, failure paths, payload exclusion) |
914
- | `Y1`, `Y2`, `Y3` | `test/guest-api-contract.test.ts` (one attempt per call, retryable flags by code) |
915
- | `Y4` | `test/meta-tools-call.test.ts`, `test/call-admission.test.ts` (one attempt, retry hints, caller reissue) |
916
- | `L1`, `L2` | `test/guest-api-contract.test.ts` (in-flight call fails `cancelled`), `test/execute.test.ts` (cancels outstanding host calls, discovery included, and refuses discovery after the run; a cancelled wedged executor, or one whose `acquire()` ignores the signal, returns promptly and releases its lease) |
917
- | `L3`, `X1` | `test/guest-api-contract.test.ts` (short-deadline executors), `test/execute.test.ts` (the watchdog ends a never-settling executor, frees the default pool, spares a slow run, and falls back from an unusable value) |
918
- | `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (shared discovery/call budgets) |
919
- | `L5`, `L7`, `X2` | `test/quickjs-executor.test.ts` (CPU, heap), `test/execute.test.ts` and `test/executor-admission.test.ts` (bounded admission and queue) |
920
- | `L6`, `X10` | `test/quickjs-executor.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message), `test/quickjs-child-stderr.test.ts` (outer reply serialization failure settles the call) |
921
- | `V1`–`V4` | `test/guest-api-contract.test.ts` (dispatched calls, every refusal class including an address no connector owns, the friction each derives, no event for the execution itself), `test/activity.test.ts` (the shared code → friction table, the identity clamp, the one-attempt floor), `test/operator-view.test.ts`, `test/d1-activity-example.test.ts` (historical pause and approval rows still render and round-trip) |
922
- | `V5`, `W9` | `test/program-writes.test.ts` (an unawaited exempt write finished and recorded, its unknown outcome reported, counts on a failed program, the classification table), `test/invocation-pipeline.test.ts` (the gate after validation, an unrecorded refusal) |
923
- | `W10`, `W12` | `test/program-writes.test.ts` (an exempt write runs and every other write keeps `E4`, the write budget, precedence and a connector default switched off, `call_tool` still refusing, search and describe markers, construction refusals), `test/artifacts-connector.test.ts` (the shipped default and switching it off), `test/operator-ui-model.test.ts`, `test/browser/operator-ui.spec.ts` (the badge) |
924
- | `M1` | `test/guest-api-contract.test.ts` (invalid emits throw catchably, accept nothing), `test/execute-emit.test.ts` (every rejected shape) |
925
- | `M2`, `M3` | `test/guest-api-contract.test.ts` (delivery order, truncated return plus delivered blocks), `test/execute-emit.test.ts` (envelope, `structuredContent`, byte-for-byte no-emit path) |
926
- | `M4` | `test/guest-api-contract.test.ts` (discard is visible), `test/execute-emit.test.ts` (structured and plain paths), `test/quickjs-executor.test.ts` (mid-run shutdown) |
927
- | `M5`, `M7` | `test/execute-emit.test.ts` (both budgets fail the crossing block; host-call budget untouched) |
928
- | `M8` | two arms passing one case table, `test/codemode-compat.test.ts` |
929
- | `M10` | `test/execute-emit.test.ts` (aggregate present, numbers only, absent when nothing emitted) |
930
- | `X4` | `test/guest-api-contract.test.ts` (string logs only), `test/quickjs-executor.test.ts` (logs before cancellation), `test/quickjs-child-stderr.test.ts` (crash, shutdown, deadline, IPC failure, bounded parent retention), `test/quickjs-log-limits.test.ts` (unchanged successful logs) |
931
- | `X3`, `X6` | `test/quickjs-executor.test.ts` (cancels a running child, never-settling await), `test/execute.test.ts` (a wedged executor stops being awaited) |
932
- | `X7` | `P3`'s tests; the Workers superset is deliberately unused |
933
-
934
- The surface itself is checked by `test/server.test.ts` (the exact seven-tool list),
1190
+ `test/guest-contract-cases.ts` runs on QuickJS in `test/guest-api-contract-quickjs.node.test.ts`
1191
+ and on the Worker adapter with a real Miniflare Loader in `test/guest-api-contract.test.ts`.
1192
+ Titles name clauses; executor-independent clauses run in both arms.
1193
+ `test/codemode-compat.test.ts` pins upstream shape compatibility, minified upstream
1194
+ rejection, and branded adapter acceptance across module copies.
1195
+
1196
+ | Clauses | Test |
1197
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1198
+ | `P1`, `P5` | both guest-contract executors (TypeScript syntax, trailing terminators, wrapper recovery), `test/program-source.test.ts` (which semicolons and wrappers are recovered), `test/quickjs-executor.node.test.ts` (`normalizeCode`) |
1199
+ | `P2`, `X5` | `test/guest-api-contract.test.ts` (Dynamic globals plus loader-only filesystem, HTTP, environment, egress, DNS, and local `data:` boundaries), `test/guest-api-contract-quickjs.node.test.ts` (exact absent globals and blocked imports), `test/quickjs-child-stderr.node.test.ts` (a child-process environment holding only `TZ=UTC`), `test/deployment-shapes.node.test.ts` (loader-only Worker construction) |
1200
+ | `P3`, `X9` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` |
1201
+ | `P4` | `test/guest-api-contract.test.ts` (no cross-run leakage), `test/execute.test.ts` (one catalog load per connector per execution) |
1202
+ | `A1`, `A2` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (canonical addressing), `test/server.test.ts` (bounded live connector inventory) |
1203
+ | `S1`, `S1a`, `S2` | `test/guest-api-contract.test.ts` (flat page, connector guides, schema keys, unfiltered browse), `test/execute.test.ts` (guide pagination/partial/no-match behavior and `$ref`/`allOf`), `test/meta-tools-search.test.ts` (mixed complete/partial ranking, stable pagination, the two safety classes), `test/typescript-signatures.test.ts` (the TypeScript format over provider and pathological schemas, search/describe parity, observed labeling), `test/typescript-signatures-parse.node.test.ts` (every rendering parses) |
1204
+ | `S3` | `test/guest-api-contract.test.ts` (typed uncaught bound), `test/execute.test.ts` (count limits, fan-out bound) |
1205
+ | `S4` | both guest-contract executors (ordered mixed describe results with unknown-address, unknown-tool suggestion, and catalog-failure details), `test/meta-tools-search.test.ts` (top-level routing, no-suggestion, catalog-failure, and hostile-input bounds) |
1206
+ | `S5`, `S6` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`unwrapMcpResult`, fail-closed annotations, activity parity) |
1207
+ | `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (parallel calls and shared admission) |
1208
+ | `S8`, `E1`, `X11` | both guest-contract executors (caught call, discovery, utility, removed-function, and forgery cases; typed promise rejections), `test/quickjs-executor.node.test.ts` (oversized messages, private transport, forged outcomes) |
1209
+ | `S9` | `test/result-shapes.test.ts` (value exclusion, bounds, merging, LRU and time expiry, runtime isolation, read-only admission, declared precedence, definition invalidation, unwrapped MCP results, discovery provenance, copy isolation, failure isolation) |
1210
+ | `S10` | `test/resource-read.test.ts` (qualified routing, whole-connector grants, personal credentials, redaction, admission, timeout, cancellation and downstream MCP reads), both guest-contract executors (successful resource reads and caught/uncaught typed failures) |
1211
+ | `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, parallel, and uncaught validation recovery, a conflict's bounded `current`), `test/meta-tools-call.test.ts` (direct, destructive, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
1212
+ | `E3`, `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`, destructive reroute), `test/program-writes.test.ts` (the refusal's `nextAction`, nothing sent, a caught refusal) |
1213
+ | `E5` | `test/guest-api-contract.test.ts` (execution-failure channel, in-flight `cancelled`), `test/execute.test.ts` (admission), `test/executor-admission.test.ts`, `test/quickjs-executor.node.test.ts` (mid-run shutdown) |
1214
+ | `E6`, `X8` | `test/guest-api-contract.test.ts` (unknown and inherited members, retained error identities), `test/quickjs-executor.node.test.ts` |
1215
+ | `E7` | `test/guest-api-contract.test.ts` (refusals about a `503`-named connector), `test/errors.test.ts` |
1216
+ | `R1`, `R2`, `R3`, `R4` | `test/guest-api-contract.test.ts` and `test/guest-api-contract-quickjs.node.test.ts` (small pass-through, shared return paging and unavailable fallback, no repeated writes, live authority), `test/program-result-cases.ts` (reconstruction, redaction, notice survives a 2,000-character spill) |
1217
+ | `M6`, `M9` | verdicts; `R2`'s guard, `M1`'s strict typing, and `M2`'s collect-then-deliver are their enforcement |
1218
+ | `R5` | `test/guest-api-contract.test.ts`, `test/quickjs-log-limits.node.test.ts` |
1219
+ | `R6`–`R8` | `test/guest-api-contract.test.ts` (normal result keys), `test/execute.test.ts` (opt-in operation aggregates, failure paths, payload exclusion) |
1220
+ | `Y1`, `Y2`, `Y3` | `test/guest-api-contract.test.ts` (one attempt per call, retryable flags by code) |
1221
+ | `Y4` | `test/meta-tools-call.test.ts`, `test/call-admission.test.ts` (one attempt, retry hints, caller reissue) |
1222
+ | `L1`, `L2` | `test/guest-api-contract.test.ts` (in-flight call fails `cancelled`), `test/execute.test.ts` (cancels outstanding host calls, discovery included, and refuses discovery after the run; a cancelled wedged executor, or one whose `acquire()` ignores the signal, returns promptly and releases its lease) |
1223
+ | `L3`, `X1` | `test/guest-api-contract.test.ts` (short-deadline executors), `test/execute.test.ts` (the watchdog ends a never-settling executor, frees the default pool, spares a slow run, and falls back from an unusable value) |
1224
+ | `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (shared discovery/call budgets and terminal catch-and-continue loops and queued-write cancellation on both executors), `test/worker-budget-response.node.test.ts` (native handle disposal and admission recovery across completed HTTP responses) |
1225
+ | `L5`, `L7`, `X2` | `test/quickjs-executor.node.test.ts` (CPU, heap), `test/execute.test.ts` and `test/executor-admission.test.ts` (bounded admission and queue) |
1226
+ | `L6`, `X10` | `test/guest-api-contract-quickjs.node.test.ts` and `test/guest-api-contract.test.ts` (whole oversized reads and one-write pageable handles), `test/sqlite-storage.node.test.ts` and `test/d1-storage.node.test.ts` (shared stash transfers), `test/quickjs-executor.node.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message), `test/quickjs-child-stderr.node.test.ts` (outer reply serialization failure settles the call) |
1227
+ | `V1`–`V4` | `test/guest-api-contract.test.ts` (dispatched calls, every refusal class including an address no connector owns, the friction each derives, no event for the execution itself), `test/activity.test.ts` (the shared code → friction table, the identity clamp, the one-attempt floor), `test/operator-view.test.ts`, `test/sql-storage-contract.ts`, run by `test/d1-storage.node.test.ts` and `test/sqlite-storage.node.test.ts` (historical pause and approval rows still render and round-trip) |
1228
+ | `V5`, `W9` | `test/program-writes.test.ts` (an unawaited trusted-pool write finished and recorded, its unknown outcome reported, counts on a failed program, the classification table), `test/invocation-pipeline.test.ts` (the gate after validation, an unrecorded refusal) |
1229
+ | `W10`, `W12` | `test/program-writes.test.ts` (a trusted-pool write runs and every other write keeps `E4`, the write budget, pool trust and override precedence, `call_tool` still refusing, search and describe verdicts, construction refusals), `test/operator-ui-model.test.ts`, `test/browser/operator-ui.spec.ts` (the badge) |
1230
+ | `M1` | `test/guest-api-contract.test.ts` (invalid emits throw catchably, accept nothing), `test/execute-emit.test.ts` (rejected shapes, bounded native snapshots, paging before metadata traversal, and cyclic presentation refusal) |
1231
+ | `M2`, `M3` | `test/guest-api-contract.test.ts` (delivery order, truncated return plus delivered blocks), `test/execute-emit.test.ts` (envelope, `structuredContent`, byte-for-byte no-emit path) |
1232
+ | `M4` | `test/guest-api-contract.test.ts` (discard is visible), `test/execute-emit.test.ts` (structured and plain paths), `test/quickjs-executor.node.test.ts` (mid-run shutdown) |
1233
+ | `M5`, `M7` | `test/execute-emit.test.ts` (both budgets fail the crossing block; host-call budget untouched) |
1234
+ | `M8` | two arms passing one case table, `test/codemode-compat.test.ts` |
1235
+ | `M10` | `test/execute-emit.test.ts` (aggregate present, numbers only, absent when nothing emitted) |
1236
+ | `X4` | `test/guest-api-contract.test.ts` (string logs only), `test/quickjs-executor.node.test.ts` (logs before cancellation), `test/quickjs-child-stderr.node.test.ts` (crash, shutdown, deadline, IPC failure, bounded parent retention), `test/quickjs-log-limits.node.test.ts` (unchanged successful logs) |
1237
+ | `X3`, `X6` | `test/quickjs-executor.node.test.ts` (cancels a running child, never-settling await), `test/execute.test.ts` (a wedged executor stops being awaited) |
1238
+ | `X7` | `P3`'s tests; the Workers superset is deliberately unused |
1239
+
1240
+ The surface itself is checked by `test/server.test.ts` (the exact six-tool list),
935
1241
  `test/code-first-surface.test.ts` (construction, executor, removed tools, copy, and size),
936
1242
  and `test/program-writes.test.ts` (the retired pause options refused at construction).