@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,44 +1,62 @@
1
1
  # Meta-tools
2
2
 
3
3
  Connecta keeps one small tool surface in model context and resolves downstream
4
- tools behind it. `search_tools` finds addresses, the call tools enforce safety
5
- annotations, `execute_code` runs read-only work as a program, and `get_result`
4
+ tools behind it. `search_tools` finds addresses, the call tools enforce the
5
+ stored classification, `execute_code` runs programs under pool trust, and `connecta.result` inside programs
6
6
  pages bounded results.
7
7
 
8
8
  This guide is the contract an MCP client sees. The in-program `connecta.*` API
9
9
  those tools imply belongs to [code mode](./code-mode.md); inbound identity and
10
10
  credential administration belong to [auth](./auth.md).
11
11
 
12
- ## The seven tools
12
+ ## Task routes
13
13
 
14
- Every deployment requires an executor, so `tools/list` is exactly seven. No
15
- configuration adds an eighth or removes one, so a client's cached tool list
14
+ Start with [Operating an endpoint](./operating.md) for a workflow, or
15
+ [the agent index](./README.md) for setup and repository tasks. This reference
16
+ owns the top-level MCP contract; it does not require reading the core internals.
17
+
18
+ | Task | Sections |
19
+ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | Choose a tool or construct its arguments | [Six-tool table](#the-six-tools), [call routing](#routing-between-the-call-surfaces), [downstream input](#downstream-input-on-direct-calls) |
21
+ | Find an operation and its schema | [Discovery context](#discovery-context), [TypeScript signatures](#typescript-signatures), [lexical discovery](#lexical-discovery) |
22
+ | Load provider instructions or native skills | [Connector guides](#connector-guides-and-skills), [native extension](#native-skills-extension) |
23
+ | Interpret a direct response or page a large result | [Representation](#result-representation), [truncation](#truncated-direct-call-results), [paging](#paging-with-connectaresult) |
24
+ | Recover a refused or invalid call | [Authorization](#authorization-recovery), [routing](#routing-recovery), [arguments](#argument-recovery), [echo budgets](#echo-budgets) |
25
+ | Change the contract | [Source and tests](#source-and-tests), [guest API](./code-mode.md#verification) |
26
+
27
+ ## The six tools
28
+
29
+ Every deployment requires an executor, so `tools/list` is exactly six. No
30
+ configuration adds a seventh or removes one, so a client's cached tool list
16
31
  never depends on the storage behind the deployment. There was an eighth,
17
32
  `resume_execution`, while programs paused at writes; it left with pausing
18
33
  ([#672](https://github.com/zackbart/connecta/issues/672)). `execute_code`
19
- keeps its read-only hint because the only writes a program sends are the ones
20
- the deployment's own config exempted from asking
21
- ([code mode](./code-mode.md#writes) `W12`) — a decision made in code, not by
22
- the model, and not by a downstream annotation.
23
-
24
- | Tool | Arguments | Returns |
25
- | --- | --- | --- |
26
- | `execute_code` | `code`, `diagnostics?` | the program's reduced return value, plus a `diagnostics` block when asked |
27
- | `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json" \| "typescript"` | `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`, plus `queryAnalysis` on a partial or failed search |
28
- | `call_tool` | `address`, `args?`, `resultMode?: "mcp" \| "value"`, `timeoutMs?`, `diagnostics?` | the downstream result, bounded as [result representation](#result-representation) describes |
29
- | `call_destructive_tool` | the same, plus `reason?` | the same |
30
- | `authorize_connector` | `connector`, `force?` | the class-specific handoff in [authorization recovery](#authorization-recovery) |
31
- | `get_result` | `id`, `offset?`, `maxBytes?` | a one-line JSON header `{ resultId, offset, bytes, totalBytes, hasMore, nextAction? }`, a newline, then the page as raw text ([paging](#paging-with-get_result)) |
32
- | `skills` | `name?` | the listing when `name` is absent, that skill's markdown when it is present |
34
+ is annotated `readOnlyHint: true, destructiveHint: false` on a `read-only`
35
+ endpoint, and `readOnlyHint: false, destructiveHint: true` on a `trusted`
36
+ endpoint. Approval belongs to the host. Pool trust never changes connector
37
+ visibility or tool grants. The four tools with structured-only success results
38
+ advertise `outputSchema`. The two direct-call tools omit it because their default
39
+ mode returns native `content`. Default-mode successes declare
40
+ `_meta["dev.connecta/format"]: "json" | "text"`; value mode places the value in
41
+ `data` and declares `format` in its complete result envelope.
42
+
43
+ | Tool | Arguments | Returns |
44
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
45
+ | `execute_code` | `code`, `diagnostics?` | `{ result?, error?, hostCalls }`, plus diagnostics when asked |
46
+ | `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json" \| "typescript"` | `{ catalogErrors, tools, total, offset, limit, hasMore }`, plus `queryAnalysis` on a partial or failed search |
47
+ | `call_tool` | `address`, `args?`, `resultMode?: "mcp" \| "value"`, `timeoutMs?`, `diagnostics?` | the downstream result, bounded as [result representation](#result-representation) describes |
48
+ | `call_destructive_tool` | the same, plus `reason?` | the same |
49
+ | `authorize_connector` | `connector`, `force?` | the class-specific handoff in [authorization recovery](#authorization-recovery) |
50
+ | `skills` | `name?` | the listing when `name` is absent, that skill's markdown when it is present |
33
51
 
34
52
  `limit` defaults to 8 and is capped at 100, as is one `connecta.describe` batch.
35
- `get_result.offset` is a whole number of bytes ≥ 0 defaulting to 0 and
53
+ `connecta.result`'s `offset` is a whole number of bytes ≥ 0 defaulting to 0 and
36
54
  `maxBytes` a whole number ≥ 1 that defaults to, and is clamped to, the inline
37
55
  cap of the call that stashed the result — 24,000 bytes unless
38
56
  `calls.maxResultBytes` or a per-connector override says otherwise
39
57
  ([why 24,000](#truncated-direct-call-results)). A value outside those domains is
40
58
  an input error; a valid `maxBytes` above the cap is an upper bound, not an
41
- error ([why](#paging-with-get_result)). `reason` is at most 500 characters of context for the host's human
59
+ error ([why](#paging-with-connectaresult)). `reason` is at most 500 characters of context for the host's human
42
60
  approval view; Connecta neither treats it as authority nor sends it downstream,
43
61
  and an empty or whitespace-only one reads as no reason rather than as grounds to
44
62
  refuse a consequential call.
@@ -50,14 +68,11 @@ aggregates for a caller measuring a workflow: a `diagnostics` block from
50
68
  and the measurements never contain program source, arguments, values, addresses,
51
69
  credentials, logs, or raw error text.
52
70
 
53
- Connecta's own tools carry the annotations it demands of downstream tools:
54
- read-only hints on all but `authorize_connector`, which mutates stored auth
55
- state, and `call_destructive_tool`, the one that sends writes, which is
56
- destructive. Otherwise a host that gates on annotations would prompt for every
57
- search, and a connecta aggregated behind another connecta would be refused by
58
- its own policy. `call_destructive_tool` is where the prompt belongs: its
59
- arguments are the exact write, so the human approves what will be sent rather
60
- than a program that might send anything. Its `reason` is dropped before
71
+ Connecta's discovery tools carry read-only hints. `authorize_connector`
72
+ requests interactive authentication changes; `call_destructive_tool` sends
73
+ writes and is annotated destructive. `execute_code` reports the selected pool's
74
+ trust in its description and annotations. Its host may approve the whole
75
+ program on a trusted endpoint. `call_destructive_tool.reason` is dropped before
61
76
  anything runs.
62
77
 
63
78
  ## Routing between the call surfaces
@@ -74,36 +89,25 @@ a small sample for inspection before continuing in another call, which avoids
74
89
  repeated guesses at text formats and collection roots without restoring a
75
90
  mandatory discovery-only round trip. A one-time write can return the only copy
76
91
  of its result. Inspect and reduce the full value before a program returns, or
77
- use a direct `call_destructive_tool` result and page it with `get_result` after
78
- the write runs once. A program return has no page handle; sampling or slicing
79
- a write's output there can discard the answer.
80
-
81
- Writes do not take that route. A program that reaches a tool not explicitly
82
- annotated read-only refuses it before validation and before anything is sent —
83
- unless the deployment's config exempts that tool, and then it runs unasked —
84
- with `destructive_tool_requires_approval` and a `nextAction` naming
85
- `call_destructive_tool` and the canonical address. Each write is its own
86
- top-level call, and the host's permission prompt on it is the only approval
87
- there is. So "close the stale issues and post a summary" is one program to find
88
- the issues and one `call_destructive_tool` per write; the program does the
89
- reading, and the host sees every write it is asked about. Programs once paused
90
- at a write for a resume call to approve and replay; hosts approve one tool call
91
- at a time, so under always-allow that prompt approved nothing and cost a round
92
- trip and a replay
93
- ([#672](https://github.com/zackbart/connecta/issues/672)). Top-level discovery
94
- stays for catalog inspection and for finding the address of a write, and the
95
- MCP instructions say so.
96
-
97
- The three discovery routes use deliberately different envelopes. These are their
98
- smallest successful one-tool shapes:
92
+ page the completed direct-call result or oversized program return with
93
+ `connecta.result` after the write runs once. Sampling or slicing a write's output
94
+ inside the program can still discard the answer.
95
+
96
+ A `trusted` pool lets the same program discover and dispatch writes.
97
+ A `read-only` pool refuses writes before validation or dispatch with
98
+ `destructive_tool_requires_approval` and a `nextAction` naming
99
+ `call_destructive_tool` and the canonical address. Each write then uses its
100
+ own top-level call. Connecta never pauses, replays, or approves a program.
101
+ Top-level discovery remains available for catalog inspection and write routing.
102
+
103
+ Both search routes return the flat page specified under
104
+ [lexical discovery](#lexical-discovery), using the catalog's shared output schema.
105
+ Describe returns an ordered tool list without pagination. Optional row metadata
106
+ is omitted in this example:
99
107
 
100
108
  ```js
101
- // Top-level search_tools
102
- { connectors: [{ id: "ci", tools: [{ name: "get_run", address: "ci.get_run" }] }], total: 1, offset: 0, limit: 8, hasMore: false }
103
-
104
- // Inside execute_code
105
- { tools: [{ name: "get_run", address: "ci.get_run" }], total: 1, offset: 0, limit: 8, hasMore: false } // connecta.search
106
- { tools: [{ name: "get_run", address: "ci.get_run", inputSchema: "{ runId: integer }" }] } // connecta.describe
109
+ // connecta.describe defaults to JSON
110
+ { tools: [{ name: "get_run", address: "ci.get_run", schemaFormat: "json", inputSchema: { type: "object" } }] }
107
111
  ```
108
112
 
109
113
  Live connector probing is not a fourth: the operator pages and `/health` own it.
@@ -119,21 +123,20 @@ account and environment hints cannot eat the inventory. It reads only the
119
123
  configured registry: no catalog load, no credential probe, no capability, and no
120
124
  replacement for canonical discovery or addressing. Program search results carry
121
125
  the same bounded `connectorTitle` per tool, so choosing an account or
122
- environment costs no provider read. Context, not a ranking input and not proof
123
- of live access.
126
+ environment costs no provider read. The inventory does not probe live access.
124
127
 
125
128
  Read-only lookup belongs in `connecta.search` inside the program; top-level
126
129
  `search_tools` stays for explicit catalog inspection and approval-required
127
130
  discovery. Both take the same arguments.
128
131
 
129
- | Argument | What it does |
130
- | --- | --- |
131
- | `query` | two to four action/object terms; empty or whitespace-only browses |
132
- | `connector` | scopes to one id, loading that catalog alone instead of fanning out across every configured connector. Set it when the integration is obvious, omit it when the right one is genuinely ambiguous |
133
- | `safety` | `"readOnly"` for what runs unasked, `"approvalRequired"` for the complementary set that crosses `call_destructive_tool`, omitted or `"all"` for the complete configured catalog. A config-exempt write stays `"approvalRequired"` and its row says `approval: "exempt"`: a program calls it without asking, and `call_tool` still refuses it ([code mode](./code-mode.md#writes) `W12`) |
134
- | `limit` / `offset` | page the ranked results; omit `limit` initially so the default eight-result page stays small |
135
- | `includeSchemas` | `"compact"` for the rendered routing view, `"json"` for the exact schema, `"typescript"` for a function signature ([below](#typescript-signatures)) |
136
- | `fullDescriptions` | unabridged tool purposes, at the obvious cost |
132
+ | Argument | What it does |
133
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
134
+ | `query` | two to four action/object terms; empty or whitespace-only browses |
135
+ | `connector` | scopes to one id, loading that catalog alone instead of fanning out across every configured connector. Set it when the integration is obvious, omit it when the right one is genuinely ambiguous |
136
+ | `safety` | `"readOnly"` selects stored `read` verdicts; `"approvalRequired"` selects stored `write` verdicts; omitted or `"all"` selects both. Each row carries `classification: "read" \| "write"`. These legacy filter names do not describe host approval policy or pool trust. |
137
+ | `limit` / `offset` | page the ranked results; omit `limit` initially so the default eight-result page stays small |
138
+ | `includeSchemas` | `"compact"` for the rendered routing view, `"json"` for the exact schema, `"typescript"` for a function signature ([below](#typescript-signatures)) |
139
+ | `fullDescriptions` | unabridged tool purposes, at the obvious cost |
137
140
 
138
141
  Neither `connector` nor `safety` grants authority or changes invocation
139
142
  admission; they select what discovery shows and nothing else.
@@ -170,7 +173,7 @@ describe, when the exact constraints matter.
170
173
  ### TypeScript signatures
171
174
 
172
175
  `includeSchemas: "typescript"` replaces both schema fields with one
173
- `signature`: the function `connecta.call(address, args)` resolves to, written
176
+ `signature`: the downstream function whose result is `(await connecta.call(address, args)).data`, written
174
177
  as a TypeScript type because agents write code against types more reliably
175
178
  than against JSON Schema ([executor](https://github.com/UsefulSoftwareCo/executor)'s
176
179
  discovery made the same bet).
@@ -182,8 +185,8 @@ discovery made the same bet).
182
185
  ```
183
186
 
184
187
  It is something to read, not something that runs: programs stay JavaScript,
185
- and the refusal of erasable TypeScript in `execute_code` stands
186
- ([ethos](../ethos.md#decisions)). The output half is the provider's declared
188
+ and `execute_code` accepts no erasable TypeScript
189
+ ([decision history](https://github.com/zackbart/connecta/blob/main/decisions/0001-ethos-verdict-table.md)). The output half is the provider's declared
187
190
  schema, else an `S9` observation that opens with the
188
191
  `/* observed, not declared */` marker and keeps the `outputSchemaSource`
189
192
  field, else `unknown` — runtime evidence never reads as a contract, even when
@@ -210,7 +213,8 @@ declared schema exactly as they do for compact.
210
213
  A connector may attach a deployment-owned guide as markdown, keeping the
211
214
  original `usageGuide: string` configuration, or as
212
215
  `{ content, summary?, required? }` — which registers no connector and creates no
213
- shared runtime template. `content` is the markdown `skills` returns verbatim.
216
+ shared runtime template. The registry prepends Agent Skills frontmatter to
217
+ `content` and preserves the guide body, including its whitespace.
214
218
  `summary` is normalized and refuses construction over 120 characters; absent,
215
219
  Connecta derives the same bounded fallback the skills listing uses from the
216
220
  guide's first meaningful body paragraph. `required: true` is reserved for
@@ -221,11 +225,11 @@ Search and describe results carry a `guide: "connector:<id>"` pointer and a
221
225
  `guideSummary`. A matching tool also carries `guideRequired: true` and
222
226
  `guideRequiredReasons` when Connecta can prove review is necessary:
223
227
 
224
- | Reason | Raised by | Survives exact schema expansion |
225
- | --- | --- | --- |
226
- | `connector_required` | the explicit `required: true` above | yes |
227
- | `approval_required` | an unannotated or write-capable tool | yes |
228
- | `schema_truncated` | a requested compact shape was capped | no — the describe that returns the exact shape clears it |
228
+ | Reason | Raised by | Survives exact schema expansion |
229
+ | -------------------- | ------------------------------------ | -------------------------------------------------------- |
230
+ | `connector_required` | the explicit `required: true` above | yes |
231
+ | `approval_required` | an unannotated or write-capable tool | yes |
232
+ | `schema_truncated` | a requested compact shape was capped | no — the describe that returns the exact shape clears it |
229
233
 
230
234
  Describe reports whatever reasons remain in the same two fields. The boolean is
231
235
  an instruction, not a server-side gate: nothing refuses the call, so the agent
@@ -253,29 +257,101 @@ guides stay scoped to the deployment that listed them even when two deployments
253
257
  use identical content, and a deployment with no connector guides receives none of
254
258
  the short conditional guide pointers in its tool definitions.
255
259
 
260
+ ### Native Skills extension
261
+
262
+ Connecta declares `resources: {}` and `io.modelcontextprotocol/skills`, then
263
+ serves `skills/list`, `skills/get`, `resources/list` and `resources/read` from
264
+ the same registry as `skills` and `connecta.skill`. These methods return
265
+ `resultType: "complete"`, `ttlMs: 0` and `cacheScope: "private"`. Listing pages
266
+ contain at most five complete skills: usage first, required connector guides
267
+ next, other connector guides, downstream skills, then the investigation guide.
268
+ Subsequent pages use `cursor`/`nextCursor`. Local lookup works without listing.
269
+
270
+ The local document URIs are `skill://connecta/usage/SKILL.md` and
271
+ `skill://connecta/connectors/<slug>/SKILL.md`. A valid Agent Skills connector ID
272
+ is its slug; other IDs use `connector-` followed by a stable SHA-256 prefix.
273
+ The suffix makes each
274
+ document a valid Agent Skills file; the roots `skill://connecta/usage` and
275
+ `skill://connecta/connectors/<connector-id>` also resolve. Provider guides are
276
+ built from the generated `src/providers/*/SKILL.md` content with the existing
277
+ connection context and deployment instructions. Local manifests contain a
278
+ SHA-256 digest and UTF-8 byte size of the complete document. `usage` and
279
+ `investigate` remain compatibility names; `connector:<id>` remains a
280
+ one-release alias for its canonical document.
281
+
282
+ Set `remoteMcp(id, { url, skills: true })` to opt into downstream skills.
283
+ Each downstream URI maps under `skill://downstream/<connector-id>/` with an
284
+ encoded origin and the original path tree, preserving relative references.
285
+ The originating authority is repeated as a path segment so an authority-rooted
286
+ skill retains its directory name. All frontmatter fields, manifests and digests
287
+ are preserved except for the namespace prefix on URIs. Listing never retrieves
288
+ file bodies. Reads accept only files advertised in a complete manifest, or the
289
+ main file of a dynamic skill. Non-file URI shapes, query/fragment URIs and
290
+ traversal segments fail closed. No URI is fetched directly over HTTP.
291
+
292
+ Local guides follow connector visibility. Downstream skills require a
293
+ whole-connector grant in the caller's admitted pool; a tool-only grant cannot
294
+ authorize arbitrary files. Each list/read uses that caller's context and
295
+ credential ownership. Downstream skills use uncached `skills/list` and explicit
296
+ `resources/read` requests on the same request-scoped connection as ordinary
297
+ resource reads. The SDK's auth-partitioned resource inventories remain separate
298
+ from skill manifests; neither skill catalogs nor file bodies enter its response
299
+ cache. A failed or malformed opted-in listing fails the entire skills catalog,
300
+ with no partial cache. Known local guides remain readable during an outage.
301
+
302
+ Downstream files are proxied byte-exact, including CRLF and binary supporting
303
+ files, within 512 files and 16 MiB per skill. Catalog metadata is bounded to
304
+ 1,024 entries and 8 MiB. The existing request-scoped agent boundary redacts
305
+ sent credential echoes, including a credential in a binary file's wire
306
+ encoding. Binary echoes remain valid base64; mixed UTF-8/JSON escapes and an
307
+ encoding that itself echoes a credential cause the file to be withheld as an
308
+ encoded placeholder. Unmatched binary bytes remain exact. That is the sole
309
+ exception to byte-exactness: digests remain the downstream's originals, so a host's integrity check rejects altered
310
+ content. Refreshing a manifest cannot make credential-bearing content safe to
311
+ load. URI and digest fields containing a sent credential are refused rather
312
+ than repaired, including credentials sent by later catalog or read operations.
313
+ Skill content never reaches operator logs, activity or status. Upward
314
+ `resources/read` is skill-only; downstream general resource reads are the
315
+ separate Phase 3 item 7 program API.
316
+
317
+ The wire contract follows the [Skills extension](https://modelcontextprotocol.io/extensions/skills/overview)
318
+ and [2026-07-28 Resources](https://modelcontextprotocol.io/specification/2026-07-28/server/resources).
319
+
256
320
  ## Result representation
257
321
 
258
- For object results, `structuredContent` is the canonical full-fidelity value and
322
+ For structured envelopes, `structuredContent` is the canonical full-fidelity value and
259
323
  `content` carries the same complete value as compact JSON for clients that only
260
324
  consume text. Keeping both follows MCP's backwards-compatibility guidance;
261
325
  dropping the text copy waits on host-forwarding measurements showing supported
262
- clients do not need it. Plain-text guidance and errors stay text-only, and a
263
- downstream MCP tool's native content blocks pass through in MCP result mode.
264
- When no text block exists and `structuredContent` is present, Connecta appends a
326
+ clients do not need it. Direct-call failures set `isError: true` and carry the
327
+ same `{ ok: false, error, durationMs, attempts }` envelope in both forms, whether
328
+ `resultMode` is omitted, `"mcp"`, or `"value"`. Plain-text guidance stays
329
+ text-only. Successful direct calls with `resultMode` omitted or `"mcp"` return
330
+ their data only in `content`, with the downstream format in
331
+ `_meta["dev.connecta/format"]`. They omit `structuredContent`, including on
332
+ truncated results, so clients that prefer it cannot replace data or a preview
333
+ with metadata. API results keep their serialized value; downstream MCP native
334
+ content blocks pass through. Value-mode successes retain
335
+ `{ ok: true, data, format, durationMs, attempts }` in both forms, including when
336
+ `data` is a truncation notice. Direct-call tools advertise no `outputSchema`:
337
+ MCP requires a tool declaring one to supply conforming structured results, which
338
+ default native content does not promise.
339
+ When downstream MCP content has no text block and downstream `structuredContent`
340
+ is present, Connecta appends a
265
341
  text block carrying its compact JSON and then applies the same content size
266
342
  guard, which preserves structured-only results including `null`, arrays, and
267
343
  scalars. An existing text mirror stays unchanged; Connecta adds no second copy.
268
344
  Newly stashed JSON and downstream content envelopes use compact serialization,
269
- and a lone text block stashes as its own text, so `get_result` offsets and
345
+ and a lone text block stashes as its own text, so `connecta.result` offsets and
270
346
  totals describe exactly that text.
271
347
 
272
- | Bound | Value |
273
- | --- | --- |
274
- | Stashed result TTL | 15 minutes |
275
- | `results.maxStashBytes` | 8 MiB per `createConnecta` runtime |
276
- | `results.maxStashEntries` | 64 per runtime |
277
- | Top-level discovery result ceiling | 256,000 UTF-8 bytes |
278
- | Downstream MCP `isError` text | 512 UTF-8 bytes plus an `…` marker |
348
+ | Bound | Value |
349
+ | ---------------------------------- | ---------------------------------- |
350
+ | Stashed result TTL | 15 minutes |
351
+ | `results.maxStashBytes` | 8 MiB per shared storage ledger |
352
+ | `results.maxStashEntries` | 64 per shared storage ledger |
353
+ | Top-level discovery result ceiling | 256,000 UTF-8 bytes |
354
+ | Downstream MCP `isError` text | 512 UTF-8 bytes plus an `…` marker |
279
355
 
280
356
  The discovery ceiling counts text, `structuredContent`, and JSON escaping
281
357
  together, because measuring one copy would advertise half the bytes the adapter
@@ -286,28 +362,46 @@ subjects and pools; they count the stored ASCII paging envelope, base64 overhead
286
362
  included, not only the result text. Capacity is reserved before each storage
287
363
  write so concurrent requests cannot oversubscribe it, and a full stash refuses
288
364
  new entries. A later attempt deletes expired entries before reusing their
289
- capacity; a failed deletion keeps the charge. These bounds cover writes by this
290
- runtime — not other processes, Worker isolates, or entries a previous runtime
291
- left behind.
365
+ capacity; a failed deletion keeps the charge. These bounds cover all processes and Worker isolates sharing the store.
366
+ Compare-and-set reserves each charge in one storage ledger before any chunk
367
+ is written; chunk TTLs cannot exceed the charge deadline.
292
368
 
293
369
  Results belong to the authenticated subject whenever auth supplies a subject or
294
370
  user id, independently of activity configuration, under the provider's namespace
295
371
  when it has one and `connecta:auth:<provider kind>` otherwise. Keep subject ids
296
372
  distinct within that namespace. An explicit principal is the fallback subject
297
373
  when neither id is supplied, and open deployments and auth providers that supply
298
- no identity share one partition.
374
+ no identity share one partition. Each new stash also binds the principal,
375
+ endpoint/pool, request origin, connector, tool, and classification. Program-return
376
+ stashes bind every contributing call, resource or skill read, and catalog lookup,
377
+ including dependencies inherited from pages read during the run. Downstream skill and
378
+ resource content requires a whole-connector grant on every page. Local guides and catalog
379
+ metadata require the contributing connector or tool to remain visible, including
380
+ caught catalog-load errors, unknown-tool failures, and recovery messages that
381
+ list visible connectors or local guides. Returns with no downstream calls still
382
+ require matching identity, origin and endpoint, current auth, and pool membership. A page must
383
+ match those bindings and pass current auth, connector/tool grants, pool membership,
384
+ and trust checks. Old entries lacking bindings fail closed. A random UUID is a
385
+ handle, never an access grant.
299
386
 
300
387
  New entries store UTF-8 bytes in a base64 envelope split across storage keys,
301
388
  48 KiB of result text per chunk — widening past roughly 1.5 MB so no result
302
- occupies more than 33 keys, because every chunk is also a write. `get_result`
389
+ occupies more than 33 keys, because every chunk is also a write. `connecta.result`
303
390
  reads and decodes only the chunks a page covers plus a few boundary bytes, so
304
391
  paging a 1.2 MB result costs the same per page as paging a 300 KB one; it
305
- neither encodes nor reads the whole result per page. Pre-upgrade entries remain readable during their TTL — the earlier
306
- single-key envelope reads one full value per page, and raw text before that
307
- also pays one full encoding. Offsets and `totalBytes` always describe the
392
+ neither encodes nor reads the whole result per page. Entries without the required identity and invocation bindings fail closed,
393
+ including pre-upgrade single-key envelopes and raw-text entries. Offsets and `totalBytes` always describe the
308
394
  original UTF-8 text, not the envelope. A supplied offset inside a character
309
395
  moves back to its start; page ends also align to character boundaries, and a
310
396
  page smaller than one character widens just enough to make progress.
397
+ Decoding preserves every payload code point, including U+FEFF at the start of
398
+ the result or any page, so joining pages reproduces the original UTF-8 text.
399
+ Oversized raw text containing unpaired UTF-16 surrogates cannot use this UTF-8
400
+ page/offset contract. It returns a successful truncation notice explaining that
401
+ the text cannot be paged and suggesting a JSON value request for recovery,
402
+ without a stash id, paging action, or altered preview.
403
+ Completed writes remain successful and must not be repeated. JSON values still
404
+ round-trip such strings through their escaped JSON representation.
311
405
 
312
406
  A per-call `timeoutMs` covers catalog resolution, admission, and connector
313
407
  execution under one deadline. The admission queue's own timeout may expire
@@ -320,10 +414,11 @@ retryable timeout while Connecta prepares its response.
320
414
  A `call_tool` or `call_destructive_tool` result over its cap — the connector's
321
415
  `maxResultBytes`, else `calls.maxResultBytes`, else 24,000 bytes — comes back
322
416
  as one text block. Its first line is the truncation notice, one line of compact
323
- JSON; everything after the first newline is the preview:
417
+ JSON; everything after the first newline is the preview. `structuredContent`
418
+ is absent, so the notice cannot hide the preview in clients that prefer it:
324
419
 
325
420
  ```text
326
- {"truncated":true,"resultId":"…","totalBytes":161420,"hint":"This write already ran: do not call it again to see its result. Bytes 0-24000 of 161420 follow; page the rest with get_result using nextAction.","nextAction":{"tool":"get_result","arguments":{"id":"…","offset":24000}}}
421
+ {"truncated":true,"resultId":"…","totalBytes":161420,"hint":"This write already ran: do not call it again to see its result. Bytes 0-24000 of 161420 follow; page the rest with connecta.result using nextAction.","nextOffset":24000,"nextAction":{"tool":"execute_code","arguments":{"code":"async () => await connecta.result(\"…\", { offset: 24000 })"}}}
327
422
  {"ts":"2026-09-16T21:31:11.759Z","actor":"sam.ortiz@example.com",…
328
423
  ```
329
424
 
@@ -344,8 +439,8 @@ three models re-ran a billable export three times trying to read its result.
344
439
  The default cap sits under both lines with room to spare. Preview plus notice
345
440
  stays under 25,000 bytes, which is under 50,000 characters for any text, because
346
441
  a character is at least one UTF-8 byte, and under 25,000 tokens for any text,
347
- because a token covers at least one byte. A `get_result` page obeys the same
348
- cap, and 24,000 matches the program result boundary in
442
+ because a token covers at least one byte. A `connecta.result` page is bounded by the same
443
+ inline byte cap, and 24,000 matches the program result boundary in
349
444
  [code mode](./code-mode.md). A deployment whose
350
445
  clients take more can raise the cap, and one whose clients take less can lower
351
446
  it, per connector if need be; a result between 24,000 and 50,000 bytes that
@@ -354,7 +449,7 @@ the notice whatever its threshold, since the notice is the first few hundred
354
449
  characters. One that rejects outright — Claude Code with
355
450
  `MAX_MCP_OUTPUT_TOKENS` set below 12,500 — needs a cap under twice its limit.
356
451
 
357
- The preview is the head of what `get_result` pages, cut on a character
452
+ The preview is the head of what `connecta.result` pages, cut on a character
358
453
  boundary, so `nextAction.arguments.offset` continues exactly where it stops. A
359
454
  lone text block — including the one synthesized from `structuredContent` — is
360
455
  measured, stashed, previewed, and paged as its text: wrapping it in a content
@@ -373,130 +468,297 @@ The approved write happened; repeating it to look again is how one export
373
468
  becomes three. For an explicitly read-only call, repeating is the better route,
374
469
  so the hint offers it first: to find something specific, repeat the read inside
375
470
  `execute_code` with `connecta.call` and filter or search the result there; to
376
- read it in full, page with `get_result`. Inside a program the inline cap does
471
+ read it in full, page with `connecta.result`. Inside a program the inline cap does
377
472
  not apply to a host call's result, only to what the program returns (`L6` in
378
473
  [code mode](./code-mode.md) bounds a host call far higher), so the reduction
379
474
  sees the whole result at once. When the hint named paging alone, the eval's
380
475
  weakest model read a 185 KB CI log in 24,000-byte pages, skipped the range
381
476
  holding the real failure, and named the flaky test near the top; before the
382
477
  notice was visible, it had reduced the log in a program and found the failure.
383
- `nextAction` stays the page handle for either kind of call, because paging is
478
+ When paging is available, `nextAction` is the page handle for either kind of call, because paging is
384
479
  the one next step connecta can spell out exactly, while a reduction is a
385
480
  program the agent has to write. `resultId` stays beside the exact `nextAction`,
386
481
  so the handle is actionable without copying an identifier out of prose.
387
- Program results and oversized discovery responses carry no such route: paging a
388
- program's return value is a refused shape, because a program can shrink
389
- anything before it returns.
482
+ Oversized program returns carry the same route, with a bounded preview after
483
+ its notice fields. They stash the compact JSON return and page from offset 0;
484
+ see [program returns](./code-mode.md#results-and-projection). A run that completed
485
+ any write gets the same no-repeat warning. Oversized discovery responses carry
486
+ no paging route.
487
+
488
+ ### Paging with connecta.result
489
+
490
+ Paging is a guest operation, never another top-level tool. Program calls also
491
+ return handles for oversized values; see [program result limits](./code-mode.md#cancellation-and-limits).
492
+ `connecta.result(handle, { page: 0 })` accepts those handles alongside direct
493
+ result IDs. Numbered pages are zero-based and mutually exclusive with offset.
494
+ Keep maxBytes fixed while incrementing pages. A direct call's
495
+ notice carries a `resultId`, `nextOffset`, and an executable `execute_code`
496
+ recovery action. Read and reduce the result in one program:
390
497
 
391
- ### Paging with get_result
498
+ ```js
499
+ async () => {
500
+ let text = "", offset = 0, page;
501
+ do {
502
+ page = await connecta.result("result-id", { offset, maxBytes: 8_000 });
503
+ text += page.text;
504
+ offset = page.nextOffset;
505
+ } while (page.hasMore);
506
+ return JSON.parse(text).items.map(item => item.id);
507
+ }
508
+ ```
392
509
 
393
- A page is the truncated result's shape again: one text block whose first line
394
- is a JSON header and whose remainder is the page, raw.
510
+ A page is `{ resultId, offset, bytes, totalBytes, hasMore, nextOffset?,
511
+ format: "text", text }`. The page size is clamped to the original call's inline
512
+ cap and a 32 KiB program bridge ceiling, with UTF-8 alignment and forward progress. Reassemble inside the sandbox;
513
+ returning pages unchanged can hit the program result cap. The admitted subject
514
+ and principal, endpoint/pool, request origin, connector, and tool bindings must
515
+ match. The host rechecks current access and trust before returning each page; a
516
+ read-only endpoint cannot read a write stash. Caller arguments select no partition.
395
517
 
396
- ```text
397
- {"resultId":"…","offset":24000,"bytes":24000,"totalBytes":161420,"hasMore":true,"nextAction":{"tool":"get_result","arguments":{"id":"…","offset":48000}}}
398
- {"ts":"2026-09-17T23:42:55.357Z","actor":"noor.haddad@example.com",…
399
- ```
518
+ A write on a read-only endpoint returns an inline truncation notice and any
519
+ usable bounded preview. It says paging is unavailable and the write already
520
+ ran; it carries no result handle or recovery action and stores no unreachable
521
+ stash. Write paging remains available on trusted endpoints.
400
522
 
401
- `bytes` is what this page returned; `hasMore` is false on the last page, which
402
- carries no `nextAction`. The page is the stashed text as-is — a lone text
403
- block's own text, a value's compact JSON, several blocks' serialized content
404
- array — and never a JSON string holding it. Pages used to arrive as
405
- `{ offset, nextOffset?, totalBytes, text }`, which escaped every quote and
406
- newline in the page: a second layer over JSON payloads that made them hard to
407
- read and swelled a 20,000-byte page of JSON lines to 25,000 characters or more.
408
-
409
- `maxBytes` is an upper bound clamped to the inline cap of the call that stashed
410
- the result — the connector's override when it had one, recorded in the stash
411
- entry — and a clamped request is answered, not refused, with `bytes` saying how
412
- much came back. Clients cut an oversized page exactly as they cut the original
413
- result. In the eval, agents that could finally see the handle asked for pages of
414
- 50,000 bytes and more, and Claude Code rejected every one of those answers
415
- outright, leaving an agent that had just been told not to repeat a write with
416
- nothing to read. So no response to a truncated call or to a page request
417
- exceeds the cap plus a header of a few hundred bytes. Value mode's unpageable
418
- preview is cut short enough that its JSON escaping still fits. Entries stashed
419
- before the cap was recorded page at the deployment cap.
420
-
421
- A refused or failed stash write cannot undo a downstream success. Both call tools
422
- then return the same layout with a paging-unavailable notice and no `resultId`
423
- or paging action — the notice first, then the preview where one is usable, and
424
- for a write the same warning not to repeat it. Activity records success, and
425
- the operator logger receives a fixed warning naming the connector and tool
426
- without storage error prose. For read-only work, reduce the result inside
427
- `execute_code` — repeating an approved write is not a way to recover its output. Other result-processing failures use a fixed
428
- `result_processing_failed` message and are never retryable.
523
+ A refused stash write cannot undo a downstream success. Direct calls and program returns produce
524
+ a paging-unavailable notice, without a handle or recovery action. A write's
525
+ notice still says it already ran. Activity records success; operator logs receive
526
+ a fixed warning without storage error prose. Never repeat a write to recover output.
429
527
 
430
528
  ## Lexical discovery
431
529
 
432
- `search_tools` tokenizes tool names and descriptions at punctuation and
433
- camel-case boundaries. Exact whole-token matches carry the most weight, and a
434
- small set of inflectional variants preserves singular/plural and verb-form
435
- recall without admitting arbitrary mid-word substrings. Each query term is
436
- weighted by its document frequency across the catalogs available to that search,
437
- so a rare domain term outranks a ubiquitous action while action terms still
438
- distinguish `get`, `list`, `search`, and write operations. Complete matches rank
439
- before ordinary partial matches; a partial candidate whose complete normalized
440
- tool name occurs in the normalized raw query competes with complete matches by
441
- score, and other candidates covering at least two terms fill the remaining page.
442
- Conversational cleanup applies to scoring terms only, never to the exact-name
443
- phrase check. If no tool covers every non-conversational term, the same scorer
444
- preserves the wider any-term fallback and marks the result
445
- `matchMode: "partial"`.
446
-
447
- Tool rows expose neither lexical scores nor per-result query coverage. Select
448
- from the returned purpose, address, schema, safety, and output shape; page-level
449
- `queryAnalysis` is the recovery path when no single result covers every term or
450
- none exists. It reports `representedTerms` (in the current page),
451
- `otherResultTerms` (only in another result), and `unmatchedTerms` (no lexical
452
- match in the catalogs that answered), covers at most eight distinct terms of at
453
- most 64 displayed characters each while marking longer input `truncated`, and
454
- never changes lexical ranking. A non-empty query that normalizes to no ASCII
455
- lexical terms returns no tools rather than unrelated browse results, with the
456
- clipped raw query in `unmatchedTerms`; a mixed query searches with its ASCII
457
- terms, and unsupported characters become neither false matches nor coverage
458
- terms.
459
-
460
- What the analysis says next depends on why the page is thin:
461
-
462
- | Case | Fields and guidance |
463
- | --- | --- |
464
- | Partial match | no single tool covered every term; split distinct intents |
465
- | True negative | no matching capability is configured; refine, connector-scope, or browse |
466
- | True negative naming a configured connector | the same, plus up to three such connectors named by id and a pointer to a scoped browse |
467
- | Unscoped search or browse with some connector unavailable | `unavailableConnectorCount` alone |
468
- | Scoped to an unavailable connector | `unavailableConnectorCount`, `catalogError`, guidance |
469
- | Scoped to an unconfigured id | `connectorScope`, `unknownConnector`, omit-the-connector guidance |
470
- | Connector configured and genuinely exposing no tools | no analysis |
471
-
472
- Three of those rows earn their asymmetry. An unavailable catalog downgrades the
473
- claim, because a search may not report that nothing is configured when it does
474
- not know. Connector identity is deliberately absent from the lexical index —
475
- indexing it would move ranking for every query that already matches tools — so a
476
- query naming one gets exactly one extra sentence and nothing else: no ranking
477
- change, no result, no new field. And a browse scoped to an unavailable connector
478
- must not serialize like a connector that genuinely has no tools, or the advice to
479
- browse lands in silence that reads like an answer.
480
-
481
- `catalogError` is the bounded classified failure — `code`, `message`,
482
- `retryable`, and any `retryAfterMs` — so a caller can tell a transient outage
483
- from one an operator must clear, and nothing else the call-path classifier knows,
484
- because a discovery read is not a call. Only an explicitly scoped search gets it:
485
- one connector's failure is not another search's context, and a scope that was
486
- never configured gets neither it nor a count, since nothing was attempted.
530
+ `search_tools` and in-program `connecta.search` return the same flat discovery
531
+ page. Select tool rows from `page.tools`.
532
+ With the same schema format, schema-key option, and scoped registry, both paths
533
+ return identical data. Top-level search omits schemas unless requested; programs
534
+ default to JSON schemas and schema-key metadata.
535
+
536
+ ```json
537
+ {
538
+ "catalogErrors": [],
539
+ "tools": [
540
+ {
541
+ "name": "get_issue",
542
+ "address": "linear.get_issue",
543
+ "connectorTitle": "Linear Production",
544
+ "classification": "read",
545
+ "schemaFormat": "json",
546
+ "inputSchema": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] }
547
+ }
548
+ ],
549
+ "total": 1,
550
+ "offset": 0,
551
+ "limit": 8,
552
+ "hasMore": false
553
+ }
554
+ ```
555
+
556
+ `catalogErrors` is always present and serialized before tools. It contains
557
+ bounded typed failures from visible catalogs, including on an unscoped search
558
+ with healthy matches. Credential and permission failures appear first. OAuth
559
+ and operator credential failures carry the codes and `recovery`, `nextAction`,
560
+ and retry instructions described below. `provider_permission_denied` asks the
561
+ resource owner to grant access and carries no reconnect action. Discovery never
562
+ starts recovery. Failure messages use fixed text derived from the error code
563
+ and connector id, never connector-supplied error text. Other failures retain
564
+ `code`, `message`, `retryable`, and any
565
+ `retryAfterMs`. A scoped search also keeps its bounded failure subset under
566
+ `queryAnalysis.catalogError` for clients that already use it.
567
+
568
+ Tool rows carry the address, configured display title when present, guide and
569
+ guide summary when available, and stored classification. Requested schemas have
570
+ `schemaFormat: "json"` for JSON Schema values or `schemaFormat: "text"` for
571
+ compact schema strings and TypeScript signatures. Compact schemas are text;
572
+ `inputSchema.properties` is meaningful only in JSON mode. Pagination counts
573
+ tools across connectors: `nextOffset` is present only when another page remains.
574
+
575
+ Search tokenizes tool names and descriptions at punctuation and camel-case
576
+ boundaries. Whole-token matches and a small set of inflectional variants preserve
577
+ recall without admitting arbitrary mid-word substrings. Document frequency weights
578
+ rare domain terms above ubiquitous action terms. Exact tool names and canonical
579
+ addresses rank first, so `get_issue` beats `get_issue_status` even when a
580
+ connector is named `issue`. Exact configured connector IDs, full display titles,
581
+ and recognized service names rank next. Complete lexical matches then precede
582
+ partial matches, with score and catalog order breaking ties.
583
+ An exact connector ID or title query browses that connector's tools, even when
584
+ individual tool names or descriptions mention that identity. Connector identity
585
+ terms that never occur in its tools need not be repeated in every tool description.
586
+
587
+ An explicit unknown `connector` scope returns no tools and an `absence` record:
588
+ `{ service, message, configuredConnectors }`. The message says
589
+ `No connector for "X" is configured for this endpoint.` Unscoped queries naming
590
+ recognized services such as GitHub, Linear, Mixpanel, Supabase, or PostHog do the
591
+ same when no visible connector ID or title names that service. This service-name
592
+ vocabulary is query interpretation, not an inventory of configured providers.
593
+ For other service names, use the explicit `connector` scope. An unavailable
594
+ catalog is a catalog failure, never an absence. Absence records and catalog
595
+ errors use only the endpoint/pool and grant intersection; a connector hidden by
596
+ that intersection is indistinguishable from an unconfigured one.
597
+
598
+ If no tool covers every search term, wider any-term fallback remains available
599
+ for ordinary action/object searches and the page carries `matchMode: "partial"`.
600
+ A service absence suppresses that fallback, so `GitHub get issue` cannot return
601
+ Linear issues or Mixpanel/Supabase projects. Tool rows expose no scores or
602
+ per-result query coverage. `queryAnalysis` partitions at most eight terms of at
603
+ most 64 characters into `representedTerms`, `otherResultTerms`, and
604
+ `unmatchedTerms`, marking longer input `truncated`. Its guidance distinguishes
605
+ partial matches, unavailable catalogs, unknown scopes, and genuine no-match
606
+ results. A non-empty query with no ASCII lexical terms returns no tools with
607
+ bounded no-match analysis. An empty or whitespace-only query browses.
608
+
609
+ ## Downstream input on direct calls
610
+
611
+ On MCP 2026-07-28, `call_tool` and `call_destructive_tool` relay a downstream
612
+ `input_required` through connecta's own `requestState`. This requires an
613
+ authenticated principal and a vault with `requestStateKey`, `seal`, and `open`.
614
+ The original request must be retried with identical arguments. Connecta
615
+ continues only the same connector and resolved address, after the normal
616
+ identity, pool, classification, schema, and admission checks.
617
+
618
+ The signed `createRequestStateCodec` envelope contains a version-2
619
+ `downstream` discriminator, connector id, and vault ciphertext. The encrypted
620
+ payload binds the principal, endpoint including its pool path, meta-tool,
621
+ submitted address and resolved target, arguments digest, round, original expiry,
622
+ and one-use nonce. It carries the opaque downstream state byte-exact, bounded
623
+ private state history from earlier rounds, and a map
624
+ from connecta's numbered `downstream/<connector>/<index>` keys to the original
625
+ downstream keys and elicitation modes. Neither the opaque state nor the original
626
+ keys appear in agent output. Each round consumes a TTL-bound storage CAS before
627
+ dispatch; tampering, expiry, changed arguments, cross-principal/pool reuse, and
628
+ concurrent or repeated consumption are refused.
629
+
630
+ Only well-formed form and URL elicitation declared by the client on that request
631
+ is forwarded. An empty `elicitation: {}` declaration means form support, as in
632
+ the spec. Capabilities are checked again on retry. Unknown response keys are
633
+ ignored; recognized bare responses are validated and renamed back before being
634
+ sent downstream. Accept, decline, and cancel all continue the pending downstream
635
+ call, so the downstream can finish or terminate it. URL acknowledgements and
636
+ decline/cancel carry only their action. Sampling and roots remain unsupported
637
+ under [#703](https://github.com/zackbart/connecta/issues/703).
638
+
639
+ URL elicitation retains the downstream-provided URL unchanged. It must be HTTPS,
640
+ have no URL userinfo or control/space characters, contain no credential known to
641
+ the sent-secret boundary, and remain byte-identical through agent redaction.
642
+ Connecta refuses unsafe URLs instead of rewriting them. Messages begin
643
+ with `Downstream <connector>:` so the host can display who supplied the prompt;
644
+ the host owns consent and browser navigation. Connecta never adds credentials
645
+ or follows the URL. Messages pass the ordinary agent-output redaction boundary.
646
+ A form is refused if that boundary would change its schema, including property
647
+ names, enum values, or annotations; the host never receives a rewritten answer
648
+ contract. Prompts, raw continuation results, discovery, and continuation-time
649
+ catalogs that echo any round's opaque state are refused before redaction, paging,
650
+ stashing, error shaping, schema observation, or cache publication and reuse.
651
+ This includes bounded percent-decoded and JSON-escaped views, encoded echoes,
652
+ short state, and serialized
653
+ numbers, booleans, and null. No prompt, state,
654
+ response, arguments, or raw error reaches activity, logs, or status.
655
+
656
+ Connecta's confidentiality guarantee covers its own handling and persistence of
657
+ `requestState`. Checks for state echoed in downstream content are defense in
658
+ depth; a downstream's deliberate disclosure of its own state is outside that
659
+ guarantee. Binary blobs are not decoded to look for echoes.
660
+
661
+ A write returning `input_required` has not completed its operation: the
662
+ [MRTR spec](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr)
663
+ defines that result as awaiting input before completion. Sending the original
664
+ opaque state and input responses is a continuation, not an automatic replay.
665
+ The sealed arguments digest prevents using that continuation for fresh write
666
+ arguments. A timeout or other failure during a continuation leaves its nonce
667
+ spent, so it cannot authorize a second attempt. Write continuations stop at an
668
+ HTTP 401 or redirect before the SDK can refresh authorization or resend the call.
669
+ HTTP 403 scope escalation also remains disabled. Read continuations retain normal
670
+ OAuth refresh behavior.
671
+
672
+ Auth recovery and downstream input are distinct state variants, with only one
673
+ active in a round. An auth retry can reach downstream input within the same
674
+ three-prompt, ten-minute window. A downstream continuation that fails auth ends
675
+ with the ordinary failure; it does not replace its pending state with an auth
676
+ replay. Downstream payloads and input responses are capped at 64 KiB, with at
677
+ most 16 inputs and 256 characters per downstream key. The combined private
678
+ state history is also capped at 64 KiB. Sealed wire state is
679
+ capped at 128 KiB. `input_required_invalid`, `input_required_limit`,
680
+ `input_required_unsupported`, and `input_required_round_limit` are non-retryable
681
+ typed failures.
682
+
683
+ Programs and in-process meta-tools return `input_required_unsupported`, with a
684
+ `nextAction` naming the equivalent direct call and bounded original arguments.
685
+ Programs never receive downstream state or prompts, and never restart to
686
+ fulfill downstream input.
487
687
 
488
688
  ## Authorization recovery
489
689
 
490
- Every typed `auth_required` call failure uses the same envelope:
690
+ On MCP 2026-07-28, a host declaring `elicitation.url` receives
691
+ `resultType: "input_required"` when an eligible call or program needs recoverable
692
+ connector authentication and the admitted identity may manage that connector.
693
+ Read-classified calls may re-run after a mid-handler auth failure only when
694
+ their classification came from a catalog accepted within its TTL. Stale
695
+ fallback catalogs cannot authorize post-entry recovery. A write may
696
+ elicit only when Connecta's own pre-invocation credential resolution
697
+ reports a missing grant, a missing credential slot, or required consent before
698
+ refresh. Its handler and transport have not been entered. Programs also require
699
+ that every entered call has a fresh read classification during this run.
700
+ The `connecta_auth` input request uses `elicitation/create`,
701
+ `mode: "url"`, fixed copy, and an identity-checked `/connect/<id>` link built
702
+ from configured `publicUrl`. OAuth consent stays in the browser; a credential
703
+ slot uses the authenticated operator UI. `authorize_connector` uses the same
704
+ flow for explicit connect and restart. There is no legacy shim.
705
+
706
+ The host echoes `requestState` and its bare `inputResponses.connecta_auth`
707
+ response on the original `tools/call`. Accept re-runs the original call; it
708
+ does not prove browser consent completed. Decline or cancel ends without
709
+ dispatch, with `auth_declined` or `auth_cancelled`. Three prompts are allowed
710
+ within one ten-minute retry window, then `auth_round_limit` ends the flow.
711
+ Each state can be consumed once; concurrent or repeated retries are refused
712
+ before dispatch. A Continue link from a pending restart requires that restart
713
+ to start successfully before the browser can consume it.
714
+ Host-owned invocation facts record the auth failure's origin and entry into
715
+ all calls, including each classification, catalog freshness and classification
716
+ digest. Raw fetches and custom connector transports cannot establish recovery
717
+ eligibility. Each accept retry rechecks all previously entered reads before
718
+ restarting a program or direct call. A write classification, changed digest, or
719
+ stale catalog returns `auth_replay_refused` and reconciliation guidance.
720
+ Each round also checks its own invocation facts. Normal stale-fallback calls
721
+ retain their existing behavior.
722
+ An auth failure after a write handler or transport was entered keeps its
723
+ ordinary error code,
724
+ `retryable: false`, `reconciliationRequired: true`, and manual `/connect` guidance
725
+ where available. The write may have partially run; reconcile its target before
726
+ retrying after connection. Handler-authored error fields cannot clear this guard.
727
+
728
+ State integrity, expiry, principal, endpoint, and tool failures produce the
729
+ SDK's JSON-RPC `-32602` with `data.reason: "invalid_request_state"` before the
730
+ handler runs. Changed arguments or code produce a typed `invalid_request_state`
731
+ tool failure before dispatch. Retry state grants no access; each request still
732
+ passes current identity, pool, connector visibility, and management checks.
733
+ Hosts without URL support keep the recovery envelope below, with a `/connect`
734
+ URL when this deployment can issue one. Without an interactive provider,
735
+ signing vault, or configured public URL, automatic elicitation is unavailable.
736
+
737
+ `downstream_oauth_required` means the connector needs an OAuth grant.
738
+ `auth_required` means its operator-managed credentials or configuration need
739
+ repair. Both carry the recovery envelope below. `provider_permission_denied`
740
+ means the provider refused a permission or scope; ask its resource owner or
741
+ administrator to grant access before retrying. It does not invite a reconnect.
742
+
743
+ A host rejected before MCP dispatch gets HTTP 401 or 403 with
744
+ `error.code: "host_auth_required"`, `recovery: "host_connection"`, and the auth
745
+ adapter's original `WWW-Authenticate` challenge. Custom streaming or non-JSON
746
+ responses keep their body and expose `Connecta-Error-Code` and
747
+ `Connecta-Recovery` headers instead. Repair the host's connecta
748
+ connection or endpoint grants. This cannot be fixed by `authorize_connector`.
749
+
750
+ An OAuth call failure uses this envelope:
491
751
 
492
752
  ```json
493
753
  {
494
- "code": "auth_required",
754
+ "code": "downstream_oauth_required",
495
755
  "message": "...",
496
756
  "retryable": false,
497
757
  "connector": "service",
498
758
  "operation": "service.read",
499
759
  "recovery": "oauth",
760
+ "authorizationUrl": "https://connecta.example/connect/service?h=...",
761
+ "instructions": "Copy authorizationUrl exactly as returned and sign in as the initiating user.",
500
762
  "nextAction": {
501
763
  "tool": "authorize_connector",
502
764
  "arguments": { "connector": "service" },
@@ -506,11 +768,16 @@ Every typed `auth_required` call failure uses the same envelope:
506
768
  }
507
769
  ```
508
770
 
509
- `recovery` is `oauth`, `operator_config`, or `unavailable`. Call
510
- `authorize_connector` only after this error. It returns the class-specific
511
- handoff:
771
+ `recovery` is `oauth`, `operator_config`, or `unavailable`. On hosts without URL
772
+ elicitation, auth errors carry the same handoff fields that `authorize_connector`
773
+ returns for the admitted user, connector, and pool. Direct calls include them in
774
+ `error`; program `connecta.call` failures include them in `error.data` and
775
+ `error.details`. Search `catalogErrors` and scoped `queryAnalysis.catalogError`,
776
+ and describe `errorDetails`, carry them too. When an auth error includes a
777
+ handoff, give it to the user or operator. The class-specific handoff is:
512
778
 
513
- - `oauth`: an `authorizationUrl` and consent instructions;
779
+ - `oauth`: an `authorizationUrl` to connecta's `/connect/<connector>?h=...` route
780
+ and instructions to sign in as the initiating user;
514
781
  - `operator_config`: an `operatorUrl` to the mounted connection UI, plus the
515
782
  declared credential label and field names/guidance; or
516
783
  - `unavailable`: an honest deployment/configuration message.
@@ -520,12 +787,28 @@ The class follows what the connector declares, not how it was authored: a
520
787
  and no OAuth flow, so it uses `operator_config` when both vault and UI are
521
788
  configured. Without either it returns `unavailable`, never a dead UI link.
522
789
 
523
- The tool accepts no secret. `force` applies only to OAuth and may discard its
524
- stored grant before restarting consent. Static credential values are written
790
+ Errors mint at most one handoff per connector per direct call or program run,
791
+ including concurrent and repeated call or catalog errors. The request reuses the
792
+ same signed URL; a later request can issue a fresh one. Issuing a link is
793
+ stateless and starts no consent. Errors never request `force`; use an explicit
794
+ `authorize_connector` call for a restart or a fresh expired link. URL-elicitation
795
+ hosts keep the existing prompt and retry flow.
796
+
797
+ The tool accepts no secret and starts no downstream consent. `force` applies
798
+ only to OAuth and requests a restart when the verified browser opens the signed
799
+ link. That visit may discard the stored grant before restarting consent. Static credential values are written
525
800
  only through the same-origin interactive-user credential route, and only for a
526
801
  connector visible to that user with the relevant shared or personal management
527
- permission; OAuth start, `force` included, requires that permission too. Core
528
- callbacks work without the UI for authorized interactive callers. After OAuth
802
+ permission; Issuing an OAuth link, `force` included, requires that permission too. The
803
+ `/connect` visit and callback both verify the same initiating user under Clerk or
804
+ Cloudflare Access, including for shared connectors.
805
+
806
+ Copy `authorizationUrl` exactly as returned. Do not decode or re-encode its opaque,
807
+ unpadded base64url handoff token. If the link is invalid or expired, request a fresh link with
808
+ `authorize_connector`. The URL expires after fifteen minutes and carries no browser authentication.
809
+ A signing credential vault is required; deployments without either interactive provider return `unavailable`
810
+ with a clear configuration message. Status reads stay passive and never expose
811
+ a downstream authorization-server URL. These routes work without the UI. After OAuth
529
812
  consent or a human update, retry the original operation; a static update is read
530
813
  from the vault on the next call and needs no redeploy.
531
814
 
@@ -540,7 +823,7 @@ the same arguments when the miss happened inside `execute_code`, which has no
540
823
  way to call a tool. `call_tool` reaching an unannotated, write-capable, or
541
824
  destructive tool returns `nextAction` for `call_destructive_tool` with the
542
825
  canonical address, and so does the same call inside a program, unless config
543
- exempts the tool there. Nothing is executed by these records.
826
+ marks that pool trusted. Nothing is executed by these records.
544
827
 
545
828
  `connecta.describe` keeps its failures inline instead, so one miss cannot
546
829
  discard the other schemas; each failed entry carries a human `error`, typed
@@ -560,20 +843,43 @@ invented address once produced a 200 KB refusal against a 1 KB result cap. Every
560
843
  caller-authored string a refusal repeats therefore gets 512 UTF-8 bytes, and
561
844
  what happens over budget differs by field.
562
845
 
563
- | Field | Over 512 UTF-8 bytes |
564
- | --- | --- |
565
- | `args` on `call_tool` and `connecta.call` | dropped whole; `purpose` says to re-send what was just sent |
566
- | the attempted address | clamped with a trailing `…` |
567
- | unknown `get_result.id`, `authorize_connector.connector`, `skills.name` | clamped the same way |
568
- | `search_tools.connector` | rejected with `invalid_args` before catalog lookup |
569
-
570
- Arguments go all or nothing because the agent already holds what it sent, and
571
- half of it would describe a call nobody made. The address gets the opposite rule
846
+ | Field | Over 512 UTF-8 bytes |
847
+ | ----------------------------------------------------------------------------- | ----------------------------------------------------------- |
848
+ | `args` on `call_tool` and `connecta.call` | dropped whole; `purpose` says to re-send what was just sent |
849
+ | the attempted address | clamped with a trailing `…` |
850
+ | unknown `connecta.result(id)`, `authorize_connector.connector`, `skills.name` | clamped the same way |
851
+ | `search_tools.connector` | rejected with `invalid_args` before catalog lookup |
852
+
853
+ Arguments are schema-filtered before budgeting. `writeOnly: true` values are
854
+ omitted at any depth, including array items and prefixes, local `$ref`/`$defs`,
855
+ and `allOf`. `oneOf`/`anyOf` alternatives must agree on sensitivity. Unresolved
856
+ or remote references, sensitive `patternProperties`/`additionalProperties`
857
+ schemas, unsupported sensitivity rules, and traversal limits omit the entire
858
+ echo. A private array element omits its containing array to keep indices intact.
859
+ `argsRedacted: true` marks partial or withheld echoes. Use them only to identify
860
+ the target for reconciliation; any new call requires the original arguments.
861
+ The filtered snapshot is never clipped: over-budget arguments are dropped whole.
862
+ The address gets the opposite rule
572
863
  because it is the thing being corrected: a clipped one still identifies the
573
864
  mistake, and a short one — every real one — comes back exact and untagged. A
574
865
  scope is rejected outright because a clipped one could select a different
575
866
  connector. A failed result-storage read returns typed `unavailable` without
576
867
  exposing backend error text.
868
+ Submitted `writeOnly` values also enter the call's sent-secret output redaction
869
+ set, including property names inside private object subtrees. Downstream
870
+ result and error text quoting them, in raw or credential-supported encoded
871
+ forms, is redacted before direct-call output, program returns, logs, and emits,
872
+ or result stash persistence; paging in a later request reads already-redacted
873
+ bytes. A program's `connecta.read` resources, connector usage guides, and
874
+ downstream `connecta.skill` files take the same rules before the program sees
875
+ them. JSON stays valid: a matching number or boolean becomes `"[redacted]"`.
876
+ Empty private strings are exempt. A private value shorter than eight characters
877
+ redacts only structured fields that equal it, so identifiers and approval state
878
+ remain, and withholds prose that contains it, including error messages, while
879
+ typed classification, retry facts, and write outcome remain.
880
+ Work limits withhold the affected text without failing the call. See
881
+ [output redaction](./auth.md#what-a-servers-errors-may-say)
882
+ for the shared encoding, budget, and argument-filter contract.
577
883
 
578
884
  Activity records each of these refusals with the coarse `friction` class derived
579
885
  from its typed error code — `tool_not_found`, `schema_retry`,
@@ -601,3 +907,51 @@ the compact schema is needed, routed like any other miss: a program to
601
907
  the listed arguments and reissue the original operation. Which keyword a finding
602
908
  names, and what the local validator declines to evaluate, is
603
909
  [code mode](./code-mode.md#errors)'s `E8`.
910
+
911
+ `repair` adds agent-only detail from that tool's published schema: accepted
912
+ keys, received JSON types, enum values, numeric/length/item bounds, and known
913
+ conditional requirements such as `dependentRequired` and `if`/`then`/`else`.
914
+ Its `example` is synthesized without submitted values and checked against the
915
+ same validator. Complex or unsatisfiable schemas get `exampleUnavailable`
916
+ rather than an invalid example. Synthesis detects recursive references and
917
+ shares a 128-node work budget. Alternative branches do not emit dependency
918
+ advice unless the branch condition is known. Repair detail has a 4 KiB budget; oversized
919
+ detail is withheld whole with `truncated: true`. Operator records still contain
920
+ only checked failure facts, never findings or examples.
921
+
922
+ Unknown-address errors list `configuredConnectors` from the current endpoint's
923
+ scoped registry, including an empty list when none are accessible. They never
924
+ list connectors from another pool or identity's grants.
925
+
926
+ A write that times out after dispatch returns `write_outcome_unknown` with
927
+ `retryable: false` and an agent-only `uncertainCall: { address, args?, argsOmitted?, argsRedacted? }`.
928
+ No write is automatically replayed. `args` may be partial reconciliation context
929
+ with `argsRedacted: true`. A withheld echo or arguments over the 512-byte budget
930
+ carry `argsOmitted: true`; sensitive or unresolved schemas also carry
931
+ `argsRedacted: true`. When `args` is absent or redacted, use the original
932
+ arguments if reconciliation requires another call. A timeout before dispatch remains `timeout`. Trusted
933
+ program write accounting keeps this detail even if the guest catches the
934
+ failure or returns early. Multiple uncertain writes use `uncertainCalls`,
935
+ bounded to ten calls with `uncertainCallsTruncated` when more exist.
936
+
937
+ Retryability comes from typed errors, HTTP status, registered OAuth error codes,
938
+ SDK timeout classes, and runtime network codes. Untyped prose is non-retryable:
939
+ "timeout", "temporarily unavailable", "rate limit", or "503" in a body, tool
940
+ name, or URL never changes the verdict. JSON-RPC `ProtocolError.data` is dropped
941
+ at the downstream boundary, even when the agent may see the error's message.
942
+
943
+ ## Source and tests
944
+
945
+ Handlers and schemas live in [meta-tools.ts](https://github.com/zackbart/connecta/blob/main/src/meta-tools.ts).
946
+ [Catalog service](https://github.com/zackbart/connecta/blob/main/src/catalog-service.ts)
947
+ and [invocation](https://github.com/zackbart/connecta/blob/main/src/invocation.ts)
948
+ serve both direct tools and programs. [Skills](https://github.com/zackbart/connecta/blob/main/src/skills.ts)
949
+ owns the supplied usage instructions and connector guides.
950
+
951
+ [Schema tests](https://github.com/zackbart/connecta/blob/main/test/meta-tool-schemas.test.ts),
952
+ [search tests](https://github.com/zackbart/connecta/blob/main/test/meta-tools-search.test.ts),
953
+ [call tests](https://github.com/zackbart/connecta/blob/main/test/meta-tools-call.test.ts),
954
+ [skills tests](https://github.com/zackbart/connecta/blob/main/test/skills-extension.test.ts),
955
+ and [surface tests](https://github.com/zackbart/connecta/blob/main/test/code-first-surface.test.ts)
956
+ check the published contract. Guest parity and clause evidence are in
957
+ [code-mode verification](./code-mode.md#verification).