@zackbart/connecta 0.41.0 → 0.42.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 (148) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/connecta.mjs +26 -17
  3. package/bin/meta-tool-names.d.mts +3 -1
  4. package/bin/meta-tool-names.mjs +4 -1
  5. package/bin/version.mjs +1 -1
  6. package/dist/activity.d.ts +2 -1
  7. package/dist/activity.js +5 -5
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-routes.js +31 -5
  10. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  11. package/dist/auth/downstream-oauth.js +3 -0
  12. package/dist/auth/static-oauth.js +26 -18
  13. package/dist/auth-elicitation.js +118 -61
  14. package/dist/authorization-handoff.js +36 -3
  15. package/dist/catalog-cache.js +292 -24
  16. package/dist/catalog-freshness.js +12 -0
  17. package/dist/catalog-service.js +373 -70
  18. package/dist/catalog.js +147 -58
  19. package/dist/config-defaults.d.ts +2 -1
  20. package/dist/config-defaults.js +4 -1
  21. package/dist/config.d.ts +43 -17
  22. package/dist/config.js +26 -10
  23. package/dist/connectors/remote-mcp/catalog.js +52 -28
  24. package/dist/connectors/remote-mcp/connection.js +62 -5
  25. package/dist/connectors/remote-mcp.js +10 -1
  26. package/dist/describe-config.d.ts +3 -2
  27. package/dist/discovery-health.js +26 -11
  28. package/dist/errors.d.ts +52 -3
  29. package/dist/errors.js +86 -3
  30. package/dist/execute.js +476 -200
  31. package/dist/executor-admission.js +2 -0
  32. package/dist/executor-contract.js +6 -0
  33. package/dist/executor-result.js +2 -2
  34. package/dist/executors/quickjs-child.js +13 -4
  35. package/dist/executors/quickjs-runtime.d.ts +2 -0
  36. package/dist/executors/quickjs-runtime.js +36 -19
  37. package/dist/executors/quickjs.js +23 -6
  38. package/dist/guest-functions.js +5 -4
  39. package/dist/guest-runtime.js +37 -1
  40. package/dist/guest-types.d.ts +18 -0
  41. package/dist/http/client.js +7 -2
  42. package/dist/http/errors.js +9 -4
  43. package/dist/http/fields.js +17 -5
  44. package/dist/http/hatch.js +9 -3
  45. package/dist/http/paging.js +1 -1
  46. package/dist/http/provider.js +2 -6
  47. package/dist/http/vendor.js +6 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +5 -0
  50. package/dist/invocation.js +98 -34
  51. package/dist/meta-output.js +24 -18
  52. package/dist/meta-tool-names.d.ts +1 -1
  53. package/dist/meta-tool-names.js +1 -1
  54. package/dist/meta-tools.js +259 -60
  55. package/dist/native-recovery.js +4 -1
  56. package/dist/native-tools.js +166 -51
  57. package/dist/node.js +3 -1
  58. package/dist/oauth-handoff.js +53 -43
  59. package/dist/operation-activity.js +2 -1
  60. package/dist/operation-vocabulary.js +6 -0
  61. package/dist/operator-record.d.ts +1 -1
  62. package/dist/operator-record.js +1 -0
  63. package/dist/operator-ui/generated.js +2 -2
  64. package/dist/program-result.js +2 -0
  65. package/dist/program-source.js +1 -1
  66. package/dist/program-writes.js +112 -19
  67. package/dist/provider.d.ts +0 -13
  68. package/dist/provider.js +0 -3
  69. package/dist/providers/_shared/google/kit.js +2 -2
  70. package/dist/providers/breeze/api.js +2 -2
  71. package/dist/providers/breeze/skill.generated.js +1 -1
  72. package/dist/providers/ccb/api.js +2 -2
  73. package/dist/providers/ccb/skill.generated.js +1 -1
  74. package/dist/providers/cloudflare/api.js +2 -2
  75. package/dist/providers/cloudflare/index.js +2 -0
  76. package/dist/providers/cloudflare/skill.generated.js +1 -1
  77. package/dist/providers/docs/skill.generated.js +1 -1
  78. package/dist/providers/drive/skill.generated.js +1 -1
  79. package/dist/providers/forms/index.js +1 -0
  80. package/dist/providers/forms/skill.generated.js +1 -1
  81. package/dist/providers/github/api.js +98 -6
  82. package/dist/providers/github/index.js +7 -2
  83. package/dist/providers/github/lib/auth.d.ts +3 -1
  84. package/dist/providers/github/lib/auth.js +43 -12
  85. package/dist/providers/github/lib/discovery.js +11 -1
  86. package/dist/providers/github/lib/operations.js +8 -0
  87. package/dist/providers/github/safety.js +39 -0
  88. package/dist/providers/github/skill.generated.js +1 -1
  89. package/dist/providers/github/tools.js +12 -5
  90. package/dist/providers/gmail/index.js +1 -0
  91. package/dist/providers/gmail/skill.generated.js +1 -1
  92. package/dist/providers/granola/skill.generated.js +1 -1
  93. package/dist/providers/overflow/index.js +2 -0
  94. package/dist/providers/overflow/skill.generated.js +1 -1
  95. package/dist/providers/planning-center/index.js +11 -5
  96. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  97. package/dist/providers/planning-center/skill.generated.js +1 -1
  98. package/dist/providers/quickbooks/index.js +2 -5
  99. package/dist/providers/quickbooks/skill.generated.js +1 -1
  100. package/dist/providers/sheets/index.js +1 -0
  101. package/dist/providers/sheets/skill.generated.js +1 -1
  102. package/dist/providers/slides/index.js +2 -2
  103. package/dist/providers/slides/skill.generated.js +1 -1
  104. package/dist/providers/stripe/skill.generated.js +1 -1
  105. package/dist/providers/tithely/index.js +1 -0
  106. package/dist/providers/tithely/skill.generated.js +1 -1
  107. package/dist/providers/vercel/api.js +2 -2
  108. package/dist/providers/vercel/skill.generated.js +1 -1
  109. package/dist/registry.d.ts +36 -4
  110. package/dist/registry.js +127 -27
  111. package/dist/result-preview.js +210 -0
  112. package/dist/result-seal.js +168 -0
  113. package/dist/route-names.js +13 -4
  114. package/dist/routes/connect.js +21 -13
  115. package/dist/routes/mcp.js +55 -26
  116. package/dist/routes/shared.d.ts +6 -0
  117. package/dist/runtime/call.js +15 -10
  118. package/dist/runtime/request-scope.js +36 -1
  119. package/dist/runtime/run.js +4 -1
  120. package/dist/sent-secrets.d.ts +20 -0
  121. package/dist/sent-secrets.js +49 -1
  122. package/dist/skills.js +100 -33
  123. package/dist/storage/keys.js +9 -6
  124. package/dist/storage/sql-native.js +1 -1
  125. package/dist/tool-safety.d.ts +33 -0
  126. package/dist/tool-safety.js +51 -15
  127. package/dist/types.d.ts +31 -0
  128. package/dist/usage-guide.js +14 -13
  129. package/dist/validate.js +29 -10
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/dist/worker.js +28 -8
  133. package/documentation/architecture.md +104 -42
  134. package/documentation/auth.md +46 -19
  135. package/documentation/code-mode.md +378 -116
  136. package/documentation/deploying.md +10 -9
  137. package/documentation/hosted-mcp-migration.md +9 -3
  138. package/documentation/integrating.md +27 -5
  139. package/documentation/meta-tools.md +655 -153
  140. package/documentation/operating.md +5 -3
  141. package/documentation/providers.md +18 -6
  142. package/examples/worker/README.md +3 -3
  143. package/guides/how-it-works.md +2 -1
  144. package/guides/safety-and-trust.md +1 -3
  145. package/package.json +1 -1
  146. package/templates/node/AGENTS.md +3 -3
  147. package/templates/node/README.md +1 -1
  148. package/templates/node/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { Effect } from "effect";
1
2
  import { operationActivity } from "./operation-activity.js";
2
3
  import { SkillLookupError } from "./skills.js";
3
4
  import { skillLookupRecovery, utilityRecovery } from "./utility-recovery.js";
@@ -6,26 +7,29 @@ import { toolError } from "./tool-error.js";
6
7
  import { upwardStep } from "./upward.js";
7
8
  import { authCall, authInvoke, authFlow, authMethod, authStorage, registerAuth } from "./auth/native.js";
8
9
  import { registryProgram } from "./runtime/registry.js";
9
- import { runEdge } from "./runtime/run.js";
10
+ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
10
11
  import { authorizationHandoff, authorizeConnector, bindAuthorizationHandoffs } from "./authorization-handoff.js";
11
12
  import { bindMcpClient } from "./mcp-client-context.js";
12
13
  import { CONFIG_DEFAULTS } from "./config-defaults.js";
13
14
  import * as z from "zod";
14
15
  import { advertisedSchema } from "./advertised-schema.js";
15
- import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError, flatSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
16
+ import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
16
17
  import { resolveDiscoveryConcurrency } from "./concurrency.js";
17
- import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
18
+ import { boundedEchoText, clipText, ConnectorCallError, MAX_TOOL_ERROR_BYTES, msg, } from "./errors.js";
18
19
  import { failureRecord, logFailure } from "./operator-record.js";
20
+ import { RESULT_ENVELOPE_V4 } from "./result-seal.js";
19
21
  import { guardExecuteResultValue, MAX_EXECUTE_RESULT_CHARS, serializeResultText } from "./executor-result.js";
20
22
  import { InvocationService } from "./invocation.js";
21
23
  import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
22
24
  import { hasConnectorGuides, SkillsRegistry } from "./skills.js";
23
25
  import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs } from "./timeout.js";
24
- import { AUTHORIZE_OUTPUT, SEARCH_OUTPUT, SKILLS_OUTPUT } from "./meta-output.js";
26
+ import { isWriteLike } from "./tool-safety.js";
27
+ import { AUTHORIZE_OUTPUT, SEARCH_OUTPUT } from "./meta-output.js";
25
28
  import { RESULT_TTL_SECONDS, resultKeys } from "./storage/keys.js";
26
29
  import { agentOutputOperations, sentSecretsForRequest } from "./sent-secrets.js";
27
30
  import { captureDownstreamInput } from "./downstream-input.js";
28
31
  import { PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES } from "./program-result.js";
32
+ import { previewResult } from "./result-preview.js";
29
33
  export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT };
30
34
  const enc = new TextEncoder();
31
35
  // Each page is a slice of payload bytes, so a leading U+FEFF is data.
@@ -116,8 +120,8 @@ function alignEndToCharBoundary(bytes, offset, end, total) {
116
120
  return e;
117
121
  }
118
122
  /** Every readable stash carries host-admitted bindings; older entries fail closed. */
119
- const RESULT_ENVELOPE_V4 = "connecta-result-v4:";
120
123
  const RESULT_ENVELOPE_V4_HEADER = /^connecta-result-v4:([A-Za-z0-9+/=]+):(\d+):(\d+):(\d+):/;
124
+ const RESULT_REFUSED_V4_HEADER = /^connecta-result-refused-v4:([A-Za-z0-9+/=]+):$/;
121
125
  /**
122
126
  * Smallest chunk of result text stored under one key. A multiple of three so
123
127
  * every chunk's base64 stands alone and a byte offset inside it lands on a
@@ -137,6 +141,30 @@ const RESULT_MAX_CHUNKS = 32;
137
141
  function resultChunkBytes(totalBytes) {
138
142
  return Math.max(RESULT_CHUNK_BYTES, Math.ceil(totalBytes / RESULT_MAX_CHUNKS / 3) * 3);
139
143
  }
144
+ const isBinary = (item) => ArrayBuffer.isView(item) || item instanceof ArrayBuffer;
145
+ /**
146
+ * JSON text, and whether a binary value went into it: as written (the holder's value, before
147
+ * any toJSON) or as a toJSON returned it. Whatever JSON makes of one, redaction checks its bytes
148
+ * only while they are binary. Text results have no binary values to lose.
149
+ */
150
+ function serializedWithBinary(value) {
151
+ let binary = false;
152
+ const text = JSON.stringify(value, function (key, item) {
153
+ binary ||= isBinary(this[key]) || isBinary(item);
154
+ return item;
155
+ });
156
+ return { text: text === undefined ? String(value) : text, binary };
157
+ }
158
+ /**
159
+ * Give back a stash nothing can name any more; its layout follows from its size. Sealing
160
+ * writes a header for whatever the ledger still holds, so the id leaves the ledger first.
161
+ * Once sealing has begun it owns the entry, which keeps its charge until it expires.
162
+ */
163
+ export function releaseStashedResult(registry, held, id, totalBytes) {
164
+ return Effect.suspend(() => held && !held.discard(id)
165
+ ? Effect.void
166
+ : registryProgram(registry, "releaseResult", id, Math.max(1, Math.ceil(totalBytes / resultChunkBytes(totalBytes)))));
167
+ }
140
168
  /**
141
169
  * Base64 of `bytes`, in three-byte-aligned batches so the argument list of one
142
170
  * spread never grows with the result. Alignment matters: an unaligned batch
@@ -149,6 +177,26 @@ function base64Of(bytes) {
149
177
  }
150
178
  return out;
151
179
  }
180
+ /** Calls a whole-result loop leaves the program, so it can also act on what it finds. */
181
+ const LOOP_SPARE_HOST_CALLS = 2;
182
+ /**
183
+ * Host calls to read every page of a retained result at the default page size.
184
+ * A page can stop up to three bytes short to keep a character whole, so the
185
+ * count assumes every one does.
186
+ */
187
+ const resultPages = (cap, totalBytes) => Math.ceil(totalBytes / Math.max(1, Math.min(cap, PROGRAM_RESULT_PAGE_BYTES) - 3));
188
+ /** Whether a program with this many calls left can walk every page of a retained result. */
189
+ const loopFits = (cap, totalBytes, calls) => resultPages(cap, totalBytes) + LOOP_SPARE_HOST_CALLS <= calls;
190
+ /**
191
+ * A read handle's advice, sized to the calls its program has left. Overrunning
192
+ * them ends the run and discards everything it read, so a result that can't fit
193
+ * pages across programs, which carry the offset and reduction between them.
194
+ */
195
+ function readHandleHint(id, cap, totalBytes, calls) {
196
+ if (loopFits(cap, totalBytes, calls.left))
197
+ return "Loop connecta.result(handle, { offset }) until !hasMore, setting offset to nextOffset.";
198
+ return `Needs up to ${resultPages(cap, totalBytes)} host calls; this program has ${calls.left} left, and overrunning returns nothing. Call connecta.result(${JSON.stringify(id)}, { offset }) at most ${Math.max(0, calls.left - LOOP_SPARE_HOST_CALLS)} times here and ${Math.max(1, calls.max - LOOP_SPARE_HOST_CALLS)} per new program, return your reduction and nextOffset, and continue from both within ${RESULT_TTL_SECONDS / 60} minutes.`;
199
+ }
152
200
  /**
153
201
  * The page a notice's nextAction asks a program to read: it fits the program's
154
202
  * return even if every byte escapes to six JSON characters, with room for the
@@ -158,6 +206,28 @@ function programPageBytes(results) {
158
206
  const fits = Math.floor(((results.programChars ?? MAX_EXECUTE_RESULT_CHARS) - 1024) / 6);
159
207
  return Math.max(4, Math.min(fits, results.cap, PROGRAM_RESULT_PAGE_BYTES));
160
208
  }
209
+ /** A retained result its run could not seal: a typed answer, and repeating this page cannot help. */
210
+ function resultNotRetained(write, id) {
211
+ const label = `"${boundedEchoText(id, 96)}"`;
212
+ return write
213
+ ? {
214
+ code: "result_not_retained",
215
+ message: `Result ${label} is not retrievable: it was not retained past the run that produced it. ${WRITE_ALREADY_RAN}`,
216
+ retryable: false,
217
+ nextAction: {
218
+ action: "recover_result",
219
+ purpose: "Read the write's target for its outcome, or report the missing output. Never repeat the write.",
220
+ },
221
+ }
222
+ : {
223
+ code: "result_not_retained",
224
+ message: `Result ${label} was not retained past the run that produced it. Rerun the read for a fresh result.`,
225
+ retryable: false,
226
+ nextAction: { action: "recover_result", purpose: "Rerun the read in a new call or program." },
227
+ };
228
+ }
229
+ /** nextAction for a whole read: runs as written, and is meant to be edited first. */
230
+ const reduceWhole = (id, results) => `async () => { const { data } = await connecta.result(${JSON.stringify(id)}, { all: true }); return /* reduce data to what you need */ JSON.stringify(data).slice(0, ${programPageBytes(results)}); }`;
161
231
  /** Opens a write's notice, ahead of any paging instruction. */
162
232
  const WRITE_ALREADY_RAN = "This write already ran: do not call it again to see its result.";
163
233
  /** UTF-8 cannot represent lone UTF-16 surrogates without changing them. */
@@ -186,7 +256,9 @@ function isWellFormedText(text) {
186
256
  * it is the one thing its notice forbids.
187
257
  */
188
258
  const readReduceFirst = (routes) => `To find something specific, repeat this read inside ${routes.program} with connecta.call and filter or search the result there, returning only what matters`;
189
- function pagingHint(results, totalBytes, preview) {
259
+ function pagingHint(results, id, totalBytes, preview) {
260
+ if (results.readBytes !== undefined && totalBytes <= results.readBytes)
261
+ return `${results.write ? WRITE_ALREADY_RAN + " " : ""}Read this completed return with connecta.result(id, { all: true }) inside ${results.routes.program} and reduce it, as nextAction does.`;
190
262
  if (results.pagingTool === "read_result") {
191
263
  const shown = preview.kind === "prefix"
192
264
  ? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest`
@@ -203,6 +275,11 @@ function pagingHint(results, totalBytes, preview) {
203
275
  : preview.kind === "text-of-envelope"
204
276
  ? ["Its text follows.", `to read the full ${totalBytes}-byte content array as JSON, page it`]
205
277
  : [`The result is ${totalBytes} bytes.`, "to read it in full, page it"];
278
+ // Stop fetching again when one program can reach every retained byte: the reduction
279
+ // still sees the whole value, and the downstream is asked only once. Beyond that the
280
+ // loop would end in budget_exceeded with nothing returned, so a program repeats the read.
281
+ if (loopFits(results.cap, totalBytes, results.programHostCalls ?? CONFIG_DEFAULTS.execute.maxHostCalls))
282
+ return `${shown} The full result is retained for ${RESULT_TTL_SECONDS / 60} minutes. To find something in it, loop connecta.result(${JSON.stringify(id)}, { offset }) from offset 0 until !hasMore, setting offset to nextOffset, in one ${results.routes.program} program; filter there and return the matches. To read on, run nextAction.`;
206
283
  return `${shown} ${readReduceFirst(results.routes)}; ${full} with connecta.result using nextAction.`;
207
284
  }
208
285
  const body = preview.kind === "prefix"
@@ -242,7 +319,8 @@ const stashResult = function (bytes, results, preview, rawText) {
242
319
  // reader below maps a byte offset back to chunk index and base64 quad.
243
320
  const chunkBytes = resultChunkBytes(totalBytes);
244
321
  const binding = base64Of(enc.encode(JSON.stringify(results.binding)));
245
- const chunks = [`${RESULT_ENVELOPE_V4}${binding}:${totalBytes}:${chunkBytes}:${results.cap}:`];
322
+ const head = `${RESULT_ENVELOPE_V4}${binding}:${totalBytes}:${chunkBytes}:${results.cap}:`;
323
+ const chunks = [head];
246
324
  for (let offset = 0; offset < bytes.length; offset += chunkBytes) {
247
325
  const chunk = base64Of(bytes.subarray(offset, offset + chunkBytes));
248
326
  if (offset === 0)
@@ -250,9 +328,30 @@ const stashResult = function (bytes, results, preview, rawText) {
250
328
  else
251
329
  chunks.push(chunk);
252
330
  }
253
- if (!(yield* upwardStep(authInvoke(results, "set", [id, chunks, RESULT_TTL_SECONDS])))) {
331
+ const held = results.held;
332
+ // Taken before the booking, so a held header can never outlive the chunks it describes.
333
+ const expiresAtMs = Date.now() + RESULT_TTL_SECONDS * 1000;
334
+ if (held && !held.results.accepts())
335
+ throw new Error("The run already sealed its results");
336
+ const stashed = yield* upwardStep(authInvoke(results, "set", [id, chunks, RESULT_TTL_SECONDS, ...(held ? [{ holdFirst: true }] : [])]));
337
+ if (!stashed)
254
338
  throw new Error("Result stash capacity exhausted");
255
- }
339
+ if (held &&
340
+ !held.results.add({
341
+ id,
342
+ binding,
343
+ total: totalBytes,
344
+ chunkBytes,
345
+ cap: results.cap,
346
+ first: chunks[0].slice(head.length),
347
+ chunks: chunks.length,
348
+ passed: held.passed,
349
+ format: held.format,
350
+ binary: held.binary,
351
+ write: results.binding.classification === "write",
352
+ expiresAtMs,
353
+ }))
354
+ throw new Error("The run already sealed its results");
256
355
  }
257
356
  catch {
258
357
  // Paging is advisory after a completed call, including an approved write.
@@ -277,7 +376,7 @@ const stashResult = function (bytes, results, preview, rawText) {
277
376
  truncated: true,
278
377
  resultId: id,
279
378
  totalBytes,
280
- hint: pagingHint(results, totalBytes, preview),
379
+ hint: pagingHint(results, id, totalBytes, preview),
281
380
  nextOffset: preview.kind === "prefix" ? preview.bytes : 0,
282
381
  nextAction: results.pagingTool === "read_result"
283
382
  ? {
@@ -285,32 +384,37 @@ const stashResult = function (bytes, results, preview, rawText) {
285
384
  arguments: { id, offset: preview.kind === "prefix" ? preview.bytes : 0 },
286
385
  }
287
386
  : {
288
- tool: "execute_code",
387
+ tool: results.routes.program,
289
388
  // A byte may cost six JSON characters when escaped. Leave room for the
290
389
  // result id, page counters, and continuation metadata in the returned page.
291
390
  arguments: {
292
- code: `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${programPageBytes(results)} })`,
391
+ code: results.readBytes !== undefined && totalBytes <= results.readBytes
392
+ ? reduceWhole(id, results)
393
+ : `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${programPageBytes(results)} })`,
293
394
  },
294
395
  },
295
396
  };
296
397
  });
297
398
  };
298
399
  /** Program calls use the direct-call envelope, ledger, expiry and authority. */
299
- export function pageProgramResult(registry, baseUrl, resolved, value, format, secrets, requestScope, wireBytes = PROGRAM_RESULT_WIRE_BYTES) {
300
- return runEdge(pageProgramResultNative(registry, baseUrl, resolved, value, format, secrets, requestScope, wireBytes));
400
+ export function pageProgramResult(registry, baseUrl, resolved, value, format, secrets, requestScope, hostCalls, wireBytes = PROGRAM_RESULT_WIRE_BYTES, routes = META_ROUTES, held, readBytes) {
401
+ return runEdge(pageProgramResultNative(registry, baseUrl, resolved, value, format, secrets, requestScope, hostCalls, wireBytes, routes, held, readBytes));
301
402
  }
302
- const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult, function (registry, baseUrl, resolved, value, format, secrets, requestScope, wireBytes = PROGRAM_RESULT_WIRE_BYTES) {
403
+ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult, function (registry, baseUrl, resolved, value, format, secrets, requestScope, hostCalls, wireBytes = PROGRAM_RESULT_WIRE_BYTES, routes = META_ROUTES, held, readBytes) {
303
404
  return authFlow(function* () {
405
+ const passed = held?.secrets.revision ?? 0;
304
406
  const data = secrets.redact(value);
305
407
  const wire = JSON.stringify({ data, format });
306
408
  if (enc.encode(wire).length <= wireBytes)
307
409
  return { data, format };
308
- const text = secrets.text(format === "text" ? String(data) : serializeResultText(data));
410
+ const serialized = format === "text" ? { text: String(data), binary: false } : serializedWithBinary(data);
411
+ const text = secrets.text(serialized.text);
309
412
  const bytes = enc.encode(text);
310
413
  const write = resolved.definition.classification !== "read";
414
+ const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, registry.maxResultBytes);
311
415
  const notice = yield* upwardStep(stashResult(bytes, {
312
- // Program-call pages name only execute_code, listed wherever programs run.
313
- routes: META_ROUTES,
416
+ // Pages name the program route the run is on, listed wherever it runs.
417
+ routes,
314
418
  binding: {
315
419
  identity: registry.resultIdentity(),
316
420
  baseUrl,
@@ -319,8 +423,9 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
319
423
  classification: write ? "write" : "read",
320
424
  },
321
425
  write,
322
- cap: resolveMaxResultBytes(resolved.connector.maxResultBytes, registry.maxResultBytes),
323
- set: authMethod((id, chunks, ttl) => registryProgram(registry, "stashResult", id, chunks, ttl)),
426
+ cap,
427
+ ...(held ? { held: { results: held, passed, format, binary: serialized.binary } } : {}),
428
+ set: authMethod((id, chunks, ttl, options) => registryProgram(registry, "stashResult", id, chunks, ttl, options)),
324
429
  warn: () => logFailure(registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger, "result paging unavailable", failureRecord({ connector: resolved.connector.id, tool: resolved.definition })),
325
430
  }, { kind: "none" }, format === "text" ? text : undefined));
326
431
  return {
@@ -330,28 +435,35 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
330
435
  totalBytes: bytes.length,
331
436
  truncated: true,
332
437
  hint: notice.resultId
333
- ? `${write ? WRITE_ALREADY_RAN + " " : ""}Page this result with connecta.result(handle); do not repeat the call for output.`
438
+ ? readBytes !== undefined && bytes.length <= readBytes
439
+ ? `${write ? `${WRITE_ALREADY_RAN} ` : ""}Read it whole with connecta.result(handle, { all: true }), then reduce it.`
440
+ : write
441
+ ? `${WRITE_ALREADY_RAN} Page this result with connecta.result(handle); do not repeat the call for output.`
442
+ : readHandleHint(notice.resultId, cap, bytes.length, hostCalls)
334
443
  : notice.hint,
335
444
  };
336
445
  });
337
446
  });
338
447
  /** Program returns share the direct stash, with all host-admitted call dependencies. */
339
- export function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars) {
340
- return runEdge(guardProgramReturnNative(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars));
448
+ export function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars, held, storeBy, readBytes, routes) {
449
+ return runEdge(guardProgramReturnNative(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars, held, storeBy, readBytes, routes));
341
450
  }
342
- const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn, function (value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars) {
451
+ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn, function (value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars, held,
452
+ // Stalled storage costs the paging, never the answer.
453
+ storeBy, readBytes, routes = META_ROUTES) {
343
454
  return authFlow(function* () {
344
- const serialized = serializeResultText(value);
345
- if (serialized.length <= maxChars)
455
+ if (serializeResultText(value).length <= maxChars)
346
456
  return value;
347
- const text = secrets.text(serialized);
457
+ const passed = held?.secrets.revision ?? 0;
458
+ const serialized = serializedWithBinary(value);
459
+ const text = secrets.text(serialized.text);
348
460
  const bytes = enc.encode(text);
349
461
  const write = dependencies.some((dependency) => dependency.classification === "write");
350
462
  const cap = Math.min(registry.maxResultBytes, ...dependencies.map((dependency) => resolveMaxResultBytes(registry.getConnector(dependency.connector)?.maxResultBytes, registry.maxResultBytes)));
351
463
  const results = {
352
464
  secrets,
353
- // A program return names only execute_code, which every surface running programs lists.
354
- routes: META_ROUTES,
465
+ // A return names the program route the run is on, listed wherever it runs.
466
+ routes,
355
467
  binding: {
356
468
  identity: registry.resultIdentity(),
357
469
  baseUrl,
@@ -361,7 +473,14 @@ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn
361
473
  write: completedWrite,
362
474
  cap,
363
475
  ...(programChars !== undefined ? { programChars } : {}),
364
- set: authMethod((id, chunks, ttl) => registryProgram(registry, "stashResult", id, chunks, ttl)),
476
+ ...(readBytes !== undefined ? { readBytes } : {}),
477
+ ...(held ? { held: { results: held, passed, format: "json", binary: serialized.binary } } : {}),
478
+ set: authMethod((id, chunks, ttl, options) => storeBy === undefined
479
+ ? registryProgram(registry, "stashResult", id, chunks, ttl, options)
480
+ : withDeadlineEffect(() => registryProgram(registry, "stashResult", id, chunks, ttl, options), {
481
+ timeoutMs: Math.max(1, storeBy - Date.now()),
482
+ timeoutError: new Error("Result storage did not answer."),
483
+ })),
365
484
  warn: () => logFailure(logger, "result paging unavailable", failureRecord({})),
366
485
  };
367
486
  const notice = yield* upwardStep(stashResult(bytes, results, { kind: "none" }, text));
@@ -386,6 +505,39 @@ function noticeFirst(notice, preview) {
386
505
  content: [{ type: "text", text: `${JSON.stringify(notice)}\n${preview}` }],
387
506
  };
388
507
  }
508
+ /**
509
+ * The view of a read past its cap, or undefined for its first bytes. A call that
510
+ * may have written is never reshaped, whatever a deployment override calls it: its
511
+ * result is its only copy, and repeating it to look again is forbidden (INV-9).
512
+ */
513
+ function previewOf(text, bytes, results, format) {
514
+ if (!results.programTool || results.write)
515
+ return undefined;
516
+ // The program this offers has to reach every byte within the run's host calls.
517
+ if (!loopFits(results.cap, bytes.length, results.programHostCalls ?? CONFIG_DEFAULTS.execute.maxHostCalls))
518
+ return undefined;
519
+ return previewResult(text, format, results.cap);
520
+ }
521
+ /**
522
+ * A preview's notice. It says what the body holds and what it leaves out, and
523
+ * since a host may prompt for or refuse the program, what to do without it.
524
+ */
525
+ function previewNotice(view, id, totalBytes, tool) {
526
+ return {
527
+ truncated: true,
528
+ resultId: id,
529
+ totalBytes,
530
+ ...view.fields,
531
+ hint: `${view.shown} All of it is retained for ${RESULT_TTL_SECONDS / 60} minutes: run nextAction in ${tool}, editing its last line to ${view.noun === "rows" ? "filter, join or count the rows" : "search for what you need"}, instead of reading pages by hand. If ${tool} is refused or unavailable, the rest can't be read here: narrow this read with its own filter, limit or fields arguments, or say what was not read.`,
532
+ nextOffset: 0,
533
+ nextAction: {
534
+ tool,
535
+ arguments: {
536
+ code: `async () => { let text = "", offset = 0, page; do { page = await connecta.result(${JSON.stringify(id)}, { offset }); text += page.text; offset = page.nextOffset; } while (page.hasMore); ${view.reduce} }`,
537
+ },
538
+ },
539
+ };
540
+ }
389
541
  /**
390
542
  * Return `text` as a single content block; if it exceeds `cap` bytes, stash the
391
543
  * full text and return a JSON truncation notice pointing at connecta.result,
@@ -394,7 +546,7 @@ function noticeFirst(notice, preview) {
394
546
  * `text` already encoded, so a caller that had to measure it to make this
395
547
  * decision doesn't encode it twice.
396
548
  */
397
- const guardEncoded = function (text, bytes, results, cap) {
549
+ const guardEncoded = function (text, bytes, results, cap, format) {
398
550
  return authFlow(function* () {
399
551
  if (bytes.length <= cap) {
400
552
  return {
@@ -411,11 +563,17 @@ const guardEncoded = function (text, bytes, results, cap) {
411
563
  kind: "prefix",
412
564
  bytes: head.length,
413
565
  }));
566
+ // A preview needs the retained copy its program reads, so a failed stash keeps the first bytes.
567
+ const view = notice.resultId === undefined ? undefined : previewOf(text, bytes, results, format);
568
+ if (view && notice.resultId !== undefined) {
569
+ const shown = previewNotice(view, notice.resultId, bytes.length, results.programTool);
570
+ return { result: noticeFirst(shown, view.body), truncated: true };
571
+ }
414
572
  return { result: noticeFirst(notice, dec.decode(head)), truncated: true };
415
573
  });
416
574
  };
417
575
  /** {@link guardEncoded} over a string that has not been measured yet. */
418
- const guardText = function (text, results, cap) {
576
+ const guardText = function (text, results, cap, format) {
419
577
  return authFlow(function* () {
420
578
  // `JSON.stringify`'s type says `string` where its behavior says `string |
421
579
  // undefined`, so TypeScript alone does not keep a non-string out of here.
@@ -424,7 +582,7 @@ const guardText = function (text, results, cap) {
424
582
  // through it the way issue #42 describes.
425
583
  const serialized = typeof text === "string" ? text : serializeResultText(text);
426
584
  const body = results.secrets?.text(serialized) ?? serialized;
427
- return yield* upwardStep(guardEncoded(body, enc.encode(body), results, cap));
585
+ return yield* upwardStep(guardEncoded(body, enc.encode(body), results, cap, format));
428
586
  });
429
587
  };
430
588
  /** Store an oversized JSON value and replace it with a page handle. */
@@ -480,7 +638,7 @@ function escapedHeadOf(bytes, cap) {
480
638
  * the head of a half-written base64 image is of no use to anyone. Either way
481
639
  * the full envelope is stashed and pages through `connecta.result`.
482
640
  */
483
- const guardContent = function (content, results, cap) {
641
+ const guardContent = function (content, results, cap, format) {
484
642
  return authFlow(function* () {
485
643
  content = results.secrets?.redact({ content }).content ?? content;
486
644
  const only = content.length === 1 ? content[0] : undefined;
@@ -489,7 +647,7 @@ const guardContent = function (content, results, cap) {
489
647
  // Under the cap the block passes through untouched, annotations included.
490
648
  if (bytes.length <= cap)
491
649
  return { result: { content }, truncated: false };
492
- return yield* upwardStep(guardEncoded(only.text, bytes, results, cap));
650
+ return yield* upwardStep(guardEncoded(only.text, bytes, results, cap, format));
493
651
  }
494
652
  let text;
495
653
  try {
@@ -546,6 +704,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
546
704
  // Already normalized and warned about at registry construction.
547
705
  const globalCap = registry.maxResultBytes;
548
706
  const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs) ?? CONFIG_DEFAULTS.execute.hostCallTimeoutMs;
707
+ const responseCeilingMs = normalizeTimeoutMs(opts.responseCeilingMs) ?? CONFIG_DEFAULTS.execute.watchdogMs;
549
708
  const probeTimeoutMs = normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
550
709
  const discoveryConcurrency = resolveDiscoveryConcurrency(opts.discoveryConcurrency);
551
710
  // createMetaTools() is called once per inbound MCP request. Sharing this
@@ -589,7 +748,22 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
589
748
  }
590
749
  const runCallNative = /* @__PURE__ */ registerAuth(runCall, function (call, source) {
591
750
  return authFlow(function* () {
592
- const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
751
+ // A deadline past the client's would trade a typed result for its transport timeout.
752
+ const now = Date.now();
753
+ const ceilingAt = (opts.requestStartedAt ?? now) + responseCeilingMs;
754
+ const remainingMs = ceilingAt - now;
755
+ const timeoutMs = Math.min(normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs, Math.max(1, remainingMs));
756
+ const pastCeiling = () => ({
757
+ code: "timeout",
758
+ message: `This request used its ${responseCeilingMs}ms response ceiling before ${boundedEchoText(call.address, 512)} started, so it was not sent. Send it again.`,
759
+ retryable: true,
760
+ details: {
761
+ operation: boundedEchoText(call.address, 512),
762
+ stage: "response ceiling",
763
+ elapsedMs: responseCeilingMs - (ceilingAt - Date.now()),
764
+ deadlineMs: responseCeilingMs,
765
+ },
766
+ });
593
767
  const outcome = yield* upwardStep(authInvoke(invocation, "invoke", [
594
768
  call.address,
595
769
  call.args ?? {},
@@ -599,7 +773,11 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
599
773
  sentSecrets,
600
774
  readOnly: opts.readOnly,
601
775
  routes,
602
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
776
+ timeoutMs,
777
+ // Auth or queueing spent the time, and a 1 ms deadline could still lose
778
+ // to a dispatch. A known "not sent" beats an answer the client won't hear.
779
+ ...(remainingMs <= 0 ? { refusal: pastCeiling() } : {}),
780
+ dispatchDeadline: { at: ceilingAt, refusal: pastCeiling },
603
781
  ...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
604
782
  unwrapResult: call.resultMode === "value",
605
783
  ...(opts.downstreamInput
@@ -635,10 +813,12 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
635
813
  tool: resolved.definition.name,
636
814
  classification: resolved.definition.classification === "read" ? "read" : "write",
637
815
  },
638
- write: resolved.definition.classification !== "read",
816
+ write: isWriteLike(resolved.definition),
639
817
  cap,
640
818
  ...(opts.pagingTool ? { pagingTool: opts.pagingTool } : {}),
819
+ ...(opts.programTool ? { programTool: opts.programTool } : {}),
641
820
  ...(opts.maxProgramResultChars !== undefined ? { programChars: opts.maxProgramResultChars } : {}),
821
+ ...(opts.maxProgramHostCalls !== undefined ? { programHostCalls: opts.maxProgramHostCalls } : {}),
642
822
  set: authMethod((id, value, ttlSeconds) => registryProgram(registry, "stashResult", id, value, ttlSeconds)),
643
823
  // The catalog entry, never `toolName`: a record names a tool
644
824
  // only through the grammar check (src/operator-record.ts).
@@ -667,14 +847,14 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
667
847
  },
668
848
  ];
669
849
  }
670
- const guarded = yield* upwardStep(guardContent(content, results, cap));
850
+ const guarded = yield* upwardStep(guardContent(content, results, cap, format));
671
851
  // Native blocks and bounded previews are the result. A format-only
672
852
  // structuredContent hides them in clients that prefer that field.
673
853
  guarded.result._meta = { "dev.connecta/format": format };
674
854
  return processed(guarded.result, guarded.truncated);
675
855
  }
676
856
  const value = result;
677
- const guarded = yield* upwardStep(guardText(serializeResultText(value), results, cap));
857
+ const guarded = yield* upwardStep(guardText(serializeResultText(value), results, cap, format));
678
858
  guarded.result._meta = { "dev.connecta/format": format };
679
859
  return processed(guarded.result, guarded.truncated, { value });
680
860
  })),
@@ -703,10 +883,11 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
703
883
  // both copies and JSON escaping when the bounded provider reason is large.
704
884
  if (!recoveryRequired) {
705
885
  const cap = resolveMaxResultBytes(outcome.resolved?.connector.maxResultBytes, globalCap);
706
- let budget = 512;
886
+ const { message } = outcome.error;
887
+ let budget = 2 * MAX_TOOL_ERROR_BYTES;
707
888
  while (enc.encode(JSON.stringify(failedResult)).length > cap && budget > 0) {
708
889
  budget = Math.floor(budget / 2);
709
- outcome.error.message = boundedEchoText(outcome.error.message, budget);
890
+ outcome.error.message = clipText(message, budget);
710
891
  failedResult = makeFailedResult();
711
892
  }
712
893
  }
@@ -755,7 +936,8 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
755
936
  };
756
937
  }
757
938
  const text = yield* upwardStep(authInvoke(skills, "text", [args.name]));
758
- return { content: [{ type: "text", text }], structuredContent: { name: args.name, format: "text", text } };
939
+ // Guidance is text-only: a structured copy would double every fetch on the wire (result-representation).
940
+ return { content: [{ type: "text", text }] };
759
941
  }
760
942
  catch (error) {
761
943
  if (error instanceof SkillLookupError)
@@ -826,14 +1008,16 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
826
1008
  options.signal?.throwIfAborted();
827
1009
  return value;
828
1010
  }));
829
- const stored = yield* upwardStep(authCall(read, [resultKeys.chunk(args.id, 0)]));
1011
+ const stored = opts.heldResults?.stored(args.id) ??
1012
+ (yield* upwardStep(authCall(read, [resultKeys.chunk(args.id, 0)])));
830
1013
  if (stored === false)
831
1014
  return unavailableResult();
832
1015
  if (stored === null) {
833
1016
  return toolError(utilityRecovery("not_found", `Unknown or expired result id "${boundedEchoText(args.id, 96)}"`));
834
1017
  }
835
1018
  const denied = () => toolError(utilityRecovery("not_found", `Unknown or expired result id "${boundedEchoText(args.id, 96)}"`));
836
- const header = RESULT_ENVELOPE_V4_HEADER.exec(stored);
1019
+ const refused = RESULT_REFUSED_V4_HEADER.exec(stored);
1020
+ const header = refused ?? RESULT_ENVELOPE_V4_HEADER.exec(stored);
837
1021
  if (!header)
838
1022
  return denied();
839
1023
  let binding;
@@ -863,6 +1047,8 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
863
1047
  !["read", "write"].includes(dependency.classification))) ||
864
1048
  !["read", "write"].includes(binding.classification))
865
1049
  return denied();
1050
+ if (refused)
1051
+ return toolError(resultNotRetained(binding.classification === "write", args.id));
866
1052
  const allowed = authMethod(() => authFlow(function* () {
867
1053
  options.onStage?.("access validation");
868
1054
  options.signal?.throwIfAborted();
@@ -1018,13 +1204,13 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
1018
1204
  // lives once in the server instructions. A client that drops instructions
1019
1205
  // still routes correctly from these scopes plus execute_code's terse lead.
1020
1206
  // Only the meta surface lists these tools, so they use its names.
1021
- const SEARCH_DESC = (readOnly) => `${readOnly ? "Catalog inspection; this read-only endpoint omits write tools." : `Catalog inspection, or finding a write before ${META_ROUTES.write}.`} For read work, search inside one ${META_ROUTES.program} program that also calls and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" selects reads; "approvalRequired" selects the fail-closed write complement; neither grants authority. Empty query browses. Read catalogErrors and absence before selecting a tool. Compact schemas have schemaFormat="text".`;
1022
- const CALL_DESC = (readOnly) => `Call one known-address read: a tool explicitly annotated readOnlyHint: true. Other read work: ${META_ROUTES.program}. ${readOnly ? "This endpoint is read-only and refuses every write." : `Unannotated or write-capable tools fail closed to ${META_ROUTES.write}.`} A truncated result carries a connecta.result action.`;
1023
- const CALL_DESTRUCTIVE_DESC = "Call any tool not explicitly annotated readOnlyHint: true. Include a short reason for the human reviewer after checking the schema and consequences. The reason grants no authority and is not sent downstream.";
1207
+ const SEARCH_DESC = (readOnly, program) => `${readOnly ? "Inspect the catalog; this read-only endpoint omits write tools." : `Inspect the catalog, or find a write before ${META_ROUTES.write}.`} For read work, search inside one ${program} program that also calls and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; set connector when known; an empty query browses. safety filters by classification and grants nothing. Read catalogErrors and absence before choosing a tool.`;
1208
+ const CALL_DESC = (readOnly, program) => `Call one known-address read: a tool explicitly annotated readOnlyHint: true. Other read work: ${program}. ${readOnly ? "This endpoint is read-only and refuses every write." : `Unannotated or write-capable tools fail closed to ${META_ROUTES.write}.`} A truncated result carries nextAction.`;
1209
+ const CALL_DESTRUCTIVE_DESC = "Call one tool not explicitly annotated readOnlyHint: true. Check its schema and consequences, then include a short reason for the human reviewer; it grants nothing and is not sent downstream. Sent once: never resend it to see output.";
1024
1210
  /** A read-only endpoint keeps the six-tool surface, so this tool says why it never writes here. */
1025
1211
  const CALL_DESTRUCTIVE_READ_ONLY_DESC = `This endpoint is read-only: search and describe omit write tools, and every write call here is refused. Reads still run; prefer ${META_ROUTES.read} or ${META_ROUTES.program} for them.`;
1026
- const AUTHORIZE_DESC = "Use after auth_required. Returns an OAuth or operator-credential handoff, or required deployment configuration. A configured credential reports presence only: if a call just failed with an authentication or operator-configuration error, give the retained handoff to the operator; otherwise proceed with the call. force=true requests an OAuth restart when the verified user opens the /connect URL; this tool never accepts credentials.";
1027
- const SKILLS_DESC = "List or fetch on-demand guidance. Fetch usage only when the always-loaded instructions are insufficient or a program needs repair.";
1212
+ const AUTHORIZE_DESC = "Use after auth_required when the error has no authorizationUrl or operatorUrl, or for a fresh link; otherwise pass that on. Returns an OAuth or operator-credential handoff, or required deployment configuration. A configured credential reports presence only: if a call just failed on authentication or configuration, give the retained handoff to the operator; otherwise proceed with the call. force=true restarts OAuth when the verified user opens the /connect URL. Never accepts credentials.";
1213
+ const SKILLS_DESC = "List or fetch guidance by exact name. Fetch usage only when guidance is missing or a program needs repair.";
1028
1214
  /**
1029
1215
  * Sentences appended to a meta-tool description only when this connection
1030
1216
  * actually has connector guides. Tool descriptions are always-loaded context,
@@ -1034,9 +1220,9 @@ const SKILLS_DESC = "List or fetch on-demand guidance. Fetch usage only when the
1034
1220
  * Registration is per connection and reads the configured connector set.
1035
1221
  */
1036
1222
  const GUIDE_NOTES = {
1037
- skills: " Also lists this deployment's connector guides by exact name.",
1038
- search: " A result with guideRequired: true requires its exact named connector guide before the call.",
1039
- destructive: " Fetch any exact connector guide named by discovery before the call.",
1223
+ skills: " Includes connector guides.",
1224
+ search: " guideRequired: true means fetch its named guide before the call.",
1225
+ destructive: " Fetch any connector guide discovery names first.",
1040
1226
  };
1041
1227
  /** `base`, plus its guide note when any VISIBLE connector carries a guide. */
1042
1228
  function describedFor(registry, base, note) {
@@ -1061,7 +1247,10 @@ const READ_ONLY_LOCAL = {
1061
1247
  openWorldHint: false,
1062
1248
  };
1063
1249
  const CALL_INPUT_SCHEMA = {
1064
- address: z.string().meta({ "x-mcp-header": "Address" }),
1250
+ // Error text only: neither message reaches the advertised schema or widens what is accepted.
1251
+ address: z
1252
+ .string({ error: 'expected a string such as "connector.tool". Send { address, args }.' })
1253
+ .meta({ "x-mcp-header": "Address" }),
1065
1254
  args: z.record(z.string(), z.unknown()).optional(),
1066
1255
  resultMode: z.enum(["mcp", "value"]).optional(),
1067
1256
  timeoutMs: z.number().int().positive().optional(),
@@ -1081,7 +1270,13 @@ const SEARCH_INPUT = advertisedSchema(z.object({
1081
1270
  fullDescriptions: z.boolean().optional(),
1082
1271
  includeSchemas: z.enum(["compact", "json", "typescript"]).optional(),
1083
1272
  }));
1084
- const CALL_INPUT = advertisedSchema(z.strictObject(CALL_INPUT_SCHEMA));
1273
+ /** MCP's own `tools/call` spells these `name` and `arguments`; say so at the point of failure. */
1274
+ const CALL_SHAPE = {
1275
+ error: (issue) => issue.code === "unrecognized_keys"
1276
+ ? `Unrecognized key${issue.keys?.length === 1 ? "" : "s"}: ${issue.keys?.map((key) => JSON.stringify(key)).join(", ")}. Send { address, args }, with the tool's arguments in args.`
1277
+ : undefined,
1278
+ };
1279
+ const CALL_INPUT = advertisedSchema(z.strictObject(CALL_INPUT_SCHEMA, CALL_SHAPE));
1085
1280
  const CALL_DESTRUCTIVE_INPUT = advertisedSchema(z.strictObject({
1086
1281
  ...CALL_INPUT_SCHEMA,
1087
1282
  // Bounded above, but with no lower bound: a model that sends `""` or
@@ -1089,8 +1284,8 @@ const CALL_DESTRUCTIVE_INPUT = advertisedSchema(z.strictObject({
1089
1284
  // call over a cosmetic field the host merely displays is the wrong
1090
1285
  // trade. It is normalized to absent in the handler instead.
1091
1286
  reason: z.string().max(500).optional(),
1092
- }));
1093
- const READ_RESULT_DESC = "Page by resultId and nextAction from the truncation notice. UTF-8 byte ranges: continue from nextOffset while hasMore. Never repeat a write for output.";
1287
+ }, CALL_SHAPE));
1288
+ const READ_RESULT_DESC = "Page a truncated result: run nextAction, then continue from nextOffset while hasMore. Never repeat a write for output.";
1094
1289
  const READ_RESULT_INPUT = advertisedSchema(z.strictObject({
1095
1290
  id: z.string(),
1096
1291
  offset: z.number().int().nonnegative().optional(),
@@ -1112,6 +1307,8 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
1112
1307
  export function registerMetaTools(server, registry, ctx) {
1113
1308
  const mt = createMetaTools(registry, ctx.baseUrl, {
1114
1309
  maxProgramResultChars: ctx.maxProgramResultChars,
1310
+ maxProgramHostCalls: ctx.maxProgramHostCalls,
1311
+ programTool: ctx.programTool,
1115
1312
  routes: ctx.routes,
1116
1313
  ...(ctx.native && !ctx.native.executeCode ? { pagingTool: "read_result" } : {}),
1117
1314
  ...(ctx.native ? { activitySource: "native" } : {}),
@@ -1119,6 +1316,8 @@ export function registerMetaTools(server, registry, ctx) {
1119
1316
  requestScope: ctx.requestScope,
1120
1317
  readOnly: ctx.readOnly,
1121
1318
  defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
1319
+ responseCeilingMs: ctx.responseCeilingMs,
1320
+ requestStartedAt: ctx.requestStartedAt,
1122
1321
  probeTimeoutMs: ctx.probeTimeoutMs,
1123
1322
  discoveryConcurrency: ctx.discoveryConcurrency,
1124
1323
  activity: ctx.activity,
@@ -1130,6 +1329,7 @@ export function registerMetaTools(server, registry, ctx) {
1130
1329
  requestSignal: ctx.requestSignal,
1131
1330
  defer: ctx.defer,
1132
1331
  });
1332
+ const routeProgram = ctx.routes?.program ?? META_ROUTES.program;
1133
1333
  const bindRequest = (request) => {
1134
1334
  if (ctx.client)
1135
1335
  bindMcpClient(request, ctx.client, ctx.activity);
@@ -1138,7 +1338,6 @@ export function registerMetaTools(server, registry, ctx) {
1138
1338
  description: describedFor(registry, SKILLS_DESC, "skills"),
1139
1339
  ...(ctx.native ? { _meta: { "dev.connecta/surface": { surface: "native", ...ctx.native } } } : {}),
1140
1340
  inputSchema: SKILLS_INPUT,
1141
- outputSchema: SKILLS_OUTPUT,
1142
1341
  annotations: READ_ONLY_LOCAL,
1143
1342
  }, authMethod((args, request) => authFlow(function* () {
1144
1343
  bindRequest(request);
@@ -1193,7 +1392,7 @@ export function registerMetaTools(server, registry, ctx) {
1193
1392
  });
1194
1393
  }
1195
1394
  server.registerTool("search_tools", {
1196
- description: describedFor(registry, SEARCH_DESC(ctx.readOnly === true), "search"),
1395
+ description: describedFor(registry, SEARCH_DESC(ctx.readOnly === true, routeProgram), "search"),
1197
1396
  inputSchema: SEARCH_INPUT,
1198
1397
  outputSchema: SEARCH_OUTPUT,
1199
1398
  annotations: READ_ONLY_REMOTE,
@@ -1202,7 +1401,7 @@ export function registerMetaTools(server, registry, ctx) {
1202
1401
  return yield* upwardStep(authInvoke(mt, "searchTools", [args]));
1203
1402
  })));
1204
1403
  server.registerTool("call_tool", {
1205
- description: CALL_DESC(ctx.readOnly === true),
1404
+ description: CALL_DESC(ctx.readOnly === true, routeProgram),
1206
1405
  inputSchema: CALL_INPUT,
1207
1406
  // Default mode returns native content, so no structured result is promised.
1208
1407
  // call_tool admits only tools that are themselves explicitly read-only;