@zackbart/connecta 0.40.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 (195) hide show
  1. package/CHANGELOG.md +148 -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 +32 -5
  7. package/dist/activity.js +32 -6
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-observation.js +24 -2
  10. package/dist/agent-routes.js +33 -44
  11. package/dist/auth/clerk.d.ts +6 -1
  12. package/dist/auth/clerk.js +16 -0
  13. package/dist/auth/cloudflare-access.js +49 -7
  14. package/dist/auth/downstream-oauth/consent.js +79 -29
  15. package/dist/auth/downstream-oauth/grant-store.js +58 -7
  16. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  17. package/dist/auth/downstream-oauth/refresh-records.js +6 -5
  18. package/dist/auth/downstream-oauth.js +15 -4
  19. package/dist/auth/oauth-request-not-sent.js +4 -0
  20. package/dist/auth/static-oauth.js +77 -39
  21. package/dist/auth-elicitation.js +118 -61
  22. package/dist/authorization-handoff.js +36 -3
  23. package/dist/catalog-cache.js +352 -43
  24. package/dist/catalog-freshness.js +25 -0
  25. package/dist/catalog-service.js +408 -78
  26. package/dist/catalog.js +216 -62
  27. package/dist/config-defaults.d.ts +2 -1
  28. package/dist/config-defaults.js +4 -1
  29. package/dist/config.d.ts +53 -21
  30. package/dist/config.js +56 -17
  31. package/dist/connectors/remote-mcp/call.js +1 -1
  32. package/dist/connectors/remote-mcp/catalog.js +52 -28
  33. package/dist/connectors/remote-mcp/connection.js +113 -22
  34. package/dist/connectors/remote-mcp/oauth.js +2 -2
  35. package/dist/connectors/remote-mcp/resources.js +5 -2
  36. package/dist/connectors/remote-mcp/transport.js +24 -6
  37. package/dist/connectors/remote-mcp.js +14 -5
  38. package/dist/credential-contract.d.ts +3 -1
  39. package/dist/describe-config.d.ts +4 -2
  40. package/dist/describe-config.js +1 -0
  41. package/dist/discovery-health.js +26 -11
  42. package/dist/downstream-input.js +11 -2
  43. package/dist/errors.d.ts +60 -4
  44. package/dist/errors.js +88 -3
  45. package/dist/execute.js +491 -202
  46. package/dist/executor-admission.js +2 -0
  47. package/dist/executor-contract.js +6 -0
  48. package/dist/executor-result.js +3 -3
  49. package/dist/executors/quickjs-child.js +14 -5
  50. package/dist/executors/quickjs-runtime.d.ts +2 -0
  51. package/dist/executors/quickjs-runtime.js +37 -20
  52. package/dist/executors/quickjs.js +24 -7
  53. package/dist/guarded-transport.js +2 -0
  54. package/dist/guest-functions.js +5 -4
  55. package/dist/guest-runtime.js +37 -1
  56. package/dist/guest-types.d.ts +18 -0
  57. package/dist/http/client.js +14 -2
  58. package/dist/http/errors.js +9 -4
  59. package/dist/http/fields.js +17 -5
  60. package/dist/http/hatch.js +11 -3
  61. package/dist/http/node-transport.js +110 -4
  62. package/dist/http/oauth.js +17 -6
  63. package/dist/http/paging.js +1 -1
  64. package/dist/http/provider.js +2 -6
  65. package/dist/http/vendor.js +6 -1
  66. package/dist/index.d.ts +1 -1
  67. package/dist/index.js +15 -21
  68. package/dist/invocation-failure.js +28 -0
  69. package/dist/invocation.js +167 -64
  70. package/dist/membership.d.ts +1 -1
  71. package/dist/meta-output.js +24 -18
  72. package/dist/meta-tool-names.d.ts +1 -1
  73. package/dist/meta-tool-names.js +1 -1
  74. package/dist/meta-tools.js +286 -73
  75. package/dist/module-contracts.d.ts +3 -1
  76. package/dist/native-recovery.js +4 -1
  77. package/dist/native-tools.js +177 -55
  78. package/dist/node.d.ts +7 -0
  79. package/dist/node.js +85 -4
  80. package/dist/oauth-handoff.js +53 -43
  81. package/dist/operation-activity.js +104 -0
  82. package/dist/operation-vocabulary.js +6 -0
  83. package/dist/operator-record.d.ts +9 -1
  84. package/dist/operator-record.js +16 -0
  85. package/dist/operator-ui/generated.js +2 -2
  86. package/dist/program-result.js +2 -0
  87. package/dist/program-source.js +1 -1
  88. package/dist/program-writes.js +112 -19
  89. package/dist/provider.d.ts +1 -14
  90. package/dist/provider.js +0 -3
  91. package/dist/providers/_shared/google/kit.js +2 -2
  92. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  93. package/dist/providers/breeze/api.js +2 -2
  94. package/dist/providers/breeze/index.js +1 -1
  95. package/dist/providers/breeze/skill.generated.js +1 -1
  96. package/dist/providers/ccb/api.js +2 -2
  97. package/dist/providers/ccb/index.js +1 -1
  98. package/dist/providers/ccb/skill.generated.js +1 -1
  99. package/dist/providers/cloudflare/api.js +2 -2
  100. package/dist/providers/cloudflare/index.js +3 -1
  101. package/dist/providers/cloudflare/skill.generated.js +1 -1
  102. package/dist/providers/docs/index.js +1 -1
  103. package/dist/providers/docs/skill.generated.js +1 -1
  104. package/dist/providers/drive/index.js +1 -1
  105. package/dist/providers/drive/skill.generated.js +1 -1
  106. package/dist/providers/forms/index.js +2 -1
  107. package/dist/providers/forms/skill.generated.js +1 -1
  108. package/dist/providers/github/api.js +98 -6
  109. package/dist/providers/github/index.js +8 -3
  110. package/dist/providers/github/lib/auth.d.ts +3 -1
  111. package/dist/providers/github/lib/auth.js +43 -12
  112. package/dist/providers/github/lib/discovery.js +11 -1
  113. package/dist/providers/github/lib/operations.js +8 -0
  114. package/dist/providers/github/safety.js +39 -0
  115. package/dist/providers/github/skill.generated.js +1 -1
  116. package/dist/providers/github/tools.js +12 -5
  117. package/dist/providers/gmail/index.js +2 -1
  118. package/dist/providers/gmail/skill.generated.js +1 -1
  119. package/dist/providers/granola/index.js +1 -1
  120. package/dist/providers/granola/skill.generated.js +1 -1
  121. package/dist/providers/overflow/index.js +3 -1
  122. package/dist/providers/overflow/skill.generated.js +1 -1
  123. package/dist/providers/planning-center/index.js +12 -6
  124. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  125. package/dist/providers/planning-center/skill.generated.js +1 -1
  126. package/dist/providers/quickbooks/api.js +28 -25
  127. package/dist/providers/quickbooks/index.js +3 -6
  128. package/dist/providers/quickbooks/lib/writes.js +55 -0
  129. package/dist/providers/quickbooks/safety.js +17 -5
  130. package/dist/providers/quickbooks/skill.generated.js +1 -1
  131. package/dist/providers/sheets/index.js +2 -1
  132. package/dist/providers/sheets/skill.generated.js +1 -1
  133. package/dist/providers/slides/index.js +3 -3
  134. package/dist/providers/slides/skill.generated.js +1 -1
  135. package/dist/providers/stripe/index.js +2 -2
  136. package/dist/providers/stripe/skill.generated.js +1 -1
  137. package/dist/providers/tithely/index.js +2 -1
  138. package/dist/providers/tithely/skill.generated.js +1 -1
  139. package/dist/providers/vercel/api.js +3 -2
  140. package/dist/providers/vercel/skill.generated.js +1 -1
  141. package/dist/registry.d.ts +36 -4
  142. package/dist/registry.js +142 -30
  143. package/dist/resource-read.js +21 -2
  144. package/dist/result-preview.js +210 -0
  145. package/dist/result-seal.js +168 -0
  146. package/dist/route-names.js +53 -0
  147. package/dist/routes/activity-context.js +1 -0
  148. package/dist/routes/activity-disclosure.js +26 -4
  149. package/dist/routes/activity.js +2 -1
  150. package/dist/routes/connect.js +21 -13
  151. package/dist/routes/mcp.js +83 -35
  152. package/dist/routes/shared.d.ts +6 -0
  153. package/dist/routes/ui-config.js +1 -1
  154. package/dist/routes/ui-data.js +3 -2
  155. package/dist/runtime/call.js +15 -10
  156. package/dist/runtime/native-storage.js +12 -0
  157. package/dist/runtime/request-reads.js +66 -14
  158. package/dist/runtime/request-scope.js +38 -2
  159. package/dist/runtime/run.js +4 -1
  160. package/dist/sent-secrets.d.ts +20 -0
  161. package/dist/sent-secrets.js +49 -1
  162. package/dist/skill-origin.js +16 -0
  163. package/dist/skills.js +213 -57
  164. package/dist/storage/d1-driver.js +33 -5
  165. package/dist/storage/keys.js +16 -7
  166. package/dist/storage/sql-native.js +60 -3
  167. package/dist/storage/sql.js +6 -0
  168. package/dist/timing-trace.js +84 -0
  169. package/dist/tool-safety.d.ts +33 -0
  170. package/dist/tool-safety.js +51 -15
  171. package/dist/types.d.ts +36 -0
  172. package/dist/url-safety.js +38 -20
  173. package/dist/usage-guide.js +14 -13
  174. package/dist/utility-recovery.js +33 -0
  175. package/dist/validate.js +29 -10
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/dist/worker.js +60 -15
  179. package/documentation/architecture.md +152 -43
  180. package/documentation/auth.md +160 -53
  181. package/documentation/code-mode.md +396 -119
  182. package/documentation/deploying.md +35 -10
  183. package/documentation/hosted-mcp-migration.md +9 -3
  184. package/documentation/integrating.md +27 -5
  185. package/documentation/meta-tools.md +761 -157
  186. package/documentation/operating.md +91 -4
  187. package/documentation/operator-ui.md +51 -2
  188. package/documentation/providers.md +34 -20
  189. package/examples/worker/README.md +3 -3
  190. package/guides/how-it-works.md +2 -1
  191. package/guides/safety-and-trust.md +1 -3
  192. package/package.json +10 -8
  193. package/templates/node/AGENTS.md +3 -3
  194. package/templates/node/README.md +1 -1
  195. package/templates/node/package.json +1 -1
@@ -1,13 +1,15 @@
1
+ import { utilityRecovery } from "./utility-recovery.js";
2
+ import { traceFor } from "./timing-trace.js";
1
3
  import { routePhrases } from "./agent-routes.js";
2
4
  import { authCall, registerAuthMethod } from "./auth/native.js";
3
5
  import { withAuthorizationHandoffEffect } from "./authorization-handoff.js";
4
6
  import { hasControlCharacters } from "./tool-name.js";
5
- import { cloneTool, isWriteLike, writeRefusal } from "./tool-safety.js";
7
+ import { cloneTool, isWriteLike, READ_PROGRAM_SOURCE, readProgramRefuses, writeRefusal } from "./tool-safety.js";
6
8
  import { Cause, Context, Effect, Exit } from "effect";
7
9
  import { CallAdmissionError, isCallAdmissionError } from "./call-admission.js";
8
- import { boundedEchoText, classificationCode, classifyCallError, classifyWithoutAdvice, ConnectorCallError, echoedCallArgs, framingError, keyResendAdviceOf, resendAdviceOf, withResendAdvice, } from "./errors.js";
10
+ import { boundedEchoText, clipText, classificationCode, classifyCallError, classifyWithoutAdvice, ConnectorCallError, echoedCallArgs, framingError, keyResendAdviceOf, resendAdviceOf, withResendAdvice, } from "./errors.js";
9
11
  import { downstreamValue } from "./mcp-result.js";
10
- import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, } from "./operator-record.js";
12
+ import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, labelErrorClass, } from "./operator-record.js";
11
13
  import { splitAddress } from "./registry.js";
12
14
  import { causeError, runEdge, withDeadlineEffect } from "./runtime/run.js";
13
15
  import { schemaExample, validateCatalogToolInput } from "./validate.js";
@@ -20,7 +22,6 @@ import { forgetProviderFailure, isKnownProviderFailure, isSettledWrite, keyProte
20
22
  import { classifyWriteOutcome, settleWrite, settleWriteResult } from "./write-outcome.js";
21
23
  import { classificationDigest, recordAuthFailure, recordCallEntry, replayClassificationDigest, resolveInvocationAuthEffect, } from "./invocation-auth.js";
22
24
  import { downstreamContinuation, recordDownstreamArgumentEcho, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
23
- import { keepErrorName } from "./redaction-identity.js";
24
25
  function defined(values) {
25
26
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
26
27
  }
@@ -83,7 +84,18 @@ function isCallerCancellation(error, signal) {
83
84
  * write it is a 2xx reply after which the handler may have applied it.
84
85
  */
85
86
  class DownstreamToolError extends Error {
87
+ /** Everything it said, kept for a program's report; the message is what fits a result. */
88
+ #text;
89
+ constructor(text) {
90
+ super(clipText(text));
91
+ this.#text = text;
92
+ }
93
+ fullText() {
94
+ return this.#text;
95
+ }
86
96
  }
97
+ // Records tell the tool's own answer from a transport or runtime failure.
98
+ labelErrorClass(DownstreamToolError, "DownstreamToolError");
87
99
  function isTimeoutFailure(error) {
88
100
  const code = error.details?.code;
89
101
  return (error.code === "timeout" ||
@@ -96,10 +108,10 @@ function assertRawMcpSuccess(kind, result) {
96
108
  const mcpResult = result;
97
109
  if (!mcpResult.isError)
98
110
  return;
99
- throw new DownstreamToolError(boundedEchoText(mcpResult.content
111
+ throw new DownstreamToolError(mcpResult.content
100
112
  ?.filter((block) => block.type === "text")
101
113
  .map((block) => block.text ?? "")
102
- .join("") || "Downstream tool call failed"));
114
+ .join("\n") || "Downstream tool call failed");
103
115
  }
104
116
  /**
105
117
  * Whether a failed attempt is the downstream tool's own answer. For a read:
@@ -115,33 +127,15 @@ function answeredFailure(error, write, call) {
115
127
  return error instanceof DownstreamToolError || isKnownProviderFailure(error);
116
128
  return knownCallFailure(call, error);
117
129
  }
118
- export class InvocationFailure extends Error {
119
- details;
120
- code;
121
- retryable;
122
- retryAfterMs;
123
- connector;
124
- operation;
125
- recovery;
126
- nextAction;
127
- retry;
128
- data;
129
- constructor(details) {
130
- super(details.message);
131
- this.details = details;
132
- this.name = "InvocationFailure";
133
- this.code = details.code;
134
- this.retryable = details.retryable;
135
- this.retryAfterMs = details.retryAfterMs;
136
- this.connector = details.connector;
137
- this.operation = details.operation;
138
- this.recovery = details.recovery;
139
- this.nextAction = details.nextAction;
140
- this.retry = details.retry;
141
- this.data = details;
142
- }
143
- }
144
- keepErrorName(InvocationFailure, "InvocationFailure");
130
+ /**
131
+ * What a write gate decided about one consequential call. A dispatch goes on
132
+ * to admission and the connector; a refusal ends the call with `error`,
133
+ * recorded as an ordinary failed attempt unless `unrecorded` — a write
134
+ * refused only because its program already returned was never an attempt.
135
+ */
136
+ /** Both program routes: no direct-call continuation applies to their calls. */
137
+ const isProgram = (source) => source === "execute_code" || source === READ_PROGRAM_SOURCE;
138
+ export { InvocationFailure } from "./invocation-failure.js";
145
139
  const withoutExample = ({ example: _example, ...stated }) => stated;
146
140
  /**
147
141
  * Shared downstream invocation engine. Address/catalog resolution is delegated
@@ -190,6 +184,7 @@ export class InvocationService {
190
184
  const recoveryState = recoveryRecord();
191
185
  let preInvocationAuthFailure = false;
192
186
  let answered = false;
187
+ let failureText;
193
188
  // A dispatched failure's advice to send it again, composed by enrich
194
189
  // only once the outcome allows it (resendAdvice), and its advice to
195
190
  // reuse an idempotency key, only where that key protects the whole
@@ -200,6 +195,10 @@ export class InvocationService {
200
195
  context.sentSecrets?.include(sentSecrets);
201
196
  // A write gate's refusal that is no attempt; see WriteGateDecision.
202
197
  let unrecorded = false;
198
+ // The gate's refusal states its own deadline facts; nothing timed out here.
199
+ let gateRefused = false;
200
+ // The deadline this call runs under, once its classification is known.
201
+ let timeoutMs = context.timeoutMs;
203
202
  let resolved;
204
203
  let activityTarget;
205
204
  // The address as written, used for activity when resolution never reached
@@ -244,10 +243,11 @@ export class InvocationService {
244
243
  };
245
244
  const enrich = (error, target, settled) => {
246
245
  if (!target)
247
- return error;
246
+ return ["unavailable", "catalog_lookup_failed", "unsupported_capability"].includes(error.code)
247
+ ? { ...error, nextAction: error.nextAction ?? utilityRecovery(error.code, error.message).nextAction }
248
+ : error;
248
249
  const recovery = recoveryState.facts;
249
- const enteredWrite = dispatchedToConnector &&
250
- ((resolved !== undefined && resolved.definition.classification !== "read") || sentSourceWrite());
250
+ const enteredWrite = dispatchedToConnector && ((resolved !== undefined && isWriteLike(resolved.definition)) || sentSourceWrite());
251
251
  // A known answer alone is not dispatch evidence. Require an owned,
252
252
  // complete ledger with a positively recorded auxiliary resource read;
253
253
  // an empty ledger, custom entry or interruption proves nothing.
@@ -266,7 +266,7 @@ export class InvocationService {
266
266
  return {
267
267
  ...error,
268
268
  code: "write_outcome_unknown",
269
- message: `${error.message} Its outcome is unknown. Check the target before repeating this call.`,
269
+ message: `${error.message} It was sent once; its outcome is unknown. Check the target before repeating this call.`,
270
270
  retryable: false,
271
271
  ...(sentSourceWrite() ? { writeOutcome: "unknown" } : {}),
272
272
  connector: target.connector.id,
@@ -303,7 +303,18 @@ export class InvocationService {
303
303
  // retryable, and no advice below may tell the agent to retry it.
304
304
  const unknownWrite = mayHaveWritten && classifyWriteOutcome({ ok: false, dispatched: true, answered, error }) === "unknown";
305
305
  if (unknownWrite)
306
- error = { ...error, retryable: false, writeOutcome: "unknown" };
306
+ error = {
307
+ ...error,
308
+ retryable: false,
309
+ writeOutcome: "unknown",
310
+ ...(!recovery && error.retry === undefined
311
+ ? {
312
+ retry: error.toolAnswered
313
+ ? "Its outcome is unknown: its handler ran before it answered, so check its target before acting. Give the user any approval link or request id in the message. Connecta will not resend it; a call the tool's words ask for is a new call."
314
+ : "No safe replay action exists. Re-read the target to reconcile whether the write took effect.",
315
+ }
316
+ : {}),
317
+ };
307
318
  // The failure's advice to send it again was kept apart until now, and
308
319
  // joins its message only where the outcome allows it: never on a write
309
320
  // whose outcome is unknown, whatever redaction did to the message.
@@ -334,9 +345,22 @@ export class InvocationService {
334
345
  error = { ...error, code: "downstream_oauth_required" };
335
346
  }
336
347
  switch (error.code) {
348
+ case "unsupported_capability":
349
+ return unknownWrite
350
+ ? error
351
+ : {
352
+ ...error,
353
+ nextAction: utilityRecovery(error.code, "The requested capability is unsupported.").nextAction,
354
+ };
337
355
  case "input_required_unsupported":
338
356
  if (context.processInputRequired)
339
- return error;
357
+ return unknownWrite
358
+ ? error
359
+ : {
360
+ ...error,
361
+ nextAction: error.nextAction ??
362
+ utilityRecovery(error.code, "The requested input capability is unsupported.").nextAction,
363
+ };
340
364
  // Resending could apply the write twice: reconcile first.
341
365
  if (unknownWrite)
342
366
  return {
@@ -407,8 +431,13 @@ export class InvocationService {
407
431
  retry: "Ask the provider's resource owner or administrator to grant the required permission or scope, then retry. Repeating the call or reconnecting alone will not grant access.",
408
432
  };
409
433
  case "invalid_args":
410
- if (!error.validation || error.writeOutcome === "unknown")
434
+ if (error.writeOutcome === "unknown")
411
435
  return error;
436
+ if (!error.validation)
437
+ return {
438
+ ...error,
439
+ nextAction: this.catalog.searchRecovery({ query: target.toolName, connector: target.connector.id }, "Inspect the current input schema and correct the arguments before a fresh call."),
440
+ };
412
441
  return {
413
442
  ...error,
414
443
  // An example comes only from the published schema, never a connector's copy of the
@@ -421,12 +450,38 @@ export class InvocationService {
421
450
  },
422
451
  connector: target.connector.id,
423
452
  operation: `${target.connector.id}.${target.toolName}`,
424
- nextAction: this.catalog.searchRecovery({
425
- query: target.toolName,
426
- connector: target.connector.id,
427
- }, "Inspect the current input shape if the validation findings are not sufficient."),
453
+ // Findings naming the fix lead on their own; a search would only cost a turn.
454
+ ...(error.validation.issues?.length && error.validation.acceptedKeys && !error.validation.truncated
455
+ ? {}
456
+ : {
457
+ nextAction: this.catalog.searchRecovery({
458
+ query: target.toolName,
459
+ connector: target.connector.id,
460
+ }, "Only if the validation above is not enough: inspect the schema."),
461
+ }),
428
462
  retry: `Correct the listed arguments and retry ` + `${target.connector.id}.${target.toolName}.`,
429
463
  };
464
+ case "not_found":
465
+ if (unknownWrite)
466
+ return error;
467
+ return {
468
+ ...error,
469
+ retry: "No automatic retry action exists. Verify the resource id, account and visibility with a read before a fresh call.",
470
+ };
471
+ case "unavailable":
472
+ case "catalog_lookup_failed":
473
+ if (unknownWrite)
474
+ return error;
475
+ // The continuation nonce is spent. Let the input boundary supply
476
+ // its fresh-call action instead of generic availability advice.
477
+ if (!isProgram(context.source) &&
478
+ downstreamContinuation(this.catalog.requestScope, target.connector.id, `${target.connector.id}.${target.toolName}`))
479
+ return error;
480
+ return {
481
+ ...error,
482
+ nextAction: error.nextAction ?? utilityRecovery(error.code, error.message).nextAction,
483
+ retry: "No immediate repair action exists on this endpoint. Ask the operator to check connector availability; a read can be tried again after recovery.",
484
+ };
430
485
  default:
431
486
  return error;
432
487
  }
@@ -437,7 +492,7 @@ export class InvocationService {
437
492
  // a write. No connector failure arrives, so the logical call's
438
493
  // recorded requests alone decide whether a 401 left it unknown.
439
494
  if (dispatchedToConnector &&
440
- (resolved?.definition.classification !== "read" || sentSourceWrite()) &&
495
+ (resolved === undefined || isWriteLike(resolved.definition) || sentSourceWrite()) &&
441
496
  (error.code === "cancelled" || isTimeoutFailure(error))) {
442
497
  const departed = settleWrite(logicalCall?.sent ?? [], undefined, true);
443
498
  if (departed !== undefined) {
@@ -446,6 +501,13 @@ export class InvocationService {
446
501
  }
447
502
  }
448
503
  const diagnostics = timing();
504
+ const trace = traceFor(this.catalog.requestScope);
505
+ if (trace) {
506
+ trace.record("catalog", diagnostics.catalogMs);
507
+ trace.record("admission", diagnostics.admissionMs);
508
+ trace.record("connector", diagnostics.connectorMs);
509
+ trace.record("result", diagnostics.resultProcessingMs, resultBytes ?? 0);
510
+ }
449
511
  const target = resolved ?? activityTarget;
450
512
  if (target && (error.code === "auth_required" || error.code === "downstream_oauth_required")) {
451
513
  recordAuthFailure(this.catalog.requestScope, target.connector.id, (resolved !== undefined && !isWriteLike(resolved.definition) && resolved.classificationFresh === true) ||
@@ -455,13 +517,13 @@ export class InvocationService {
455
517
  // Read before any step below rebuilds the details: Q8's verdict is final.
456
518
  const settled = isSettledWrite(error);
457
519
  // Attach host deadline facts before write recovery changes the code.
458
- if (isTimeoutFailure(error)) {
520
+ if (isTimeoutFailure(error) && !gateRefused) {
459
521
  const elapsedMs = Date.now() - started;
460
522
  const operation = boundedEchoText(address, 512);
461
523
  error = {
462
524
  ...error,
463
- message: `Operation "${operation}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${context.timeoutMs}ms).`,
464
- details: { ...error.details, operation, stage, elapsedMs, ...defined({ deadlineMs: context.timeoutMs }) },
525
+ message: `Operation "${operation}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${timeoutMs}ms).`,
526
+ details: { ...error.details, operation, stage, elapsedMs, ...defined({ deadlineMs: timeoutMs }) },
465
527
  };
466
528
  }
467
529
  // Downstream diagnostics were redacted under payload rules where they entered.
@@ -474,6 +536,7 @@ export class InvocationService {
474
536
  ...defined({ resolved }),
475
537
  dispatched: dispatchedToConnector,
476
538
  ...(dispatchedToConnector ? { answered } : {}),
539
+ ...defined({ failureText }),
477
540
  error: details,
478
541
  });
479
542
  // A write refused only because its program had already returned
@@ -515,6 +578,11 @@ export class InvocationService {
515
578
  // expected, and a throw anywhere in it — a defect, to Effect — is
516
579
  // classified like a failure, as the async body this replaced did.
517
580
  const dispatch = (callSignal) => Effect.gen({ self: this }, function* () {
581
+ if (context.refusal) {
582
+ gateRefused = true;
583
+ return context.refusal;
584
+ }
585
+ let pastDeadline;
518
586
  const admissionSignal = context.dispatchSignal
519
587
  ? AbortSignal.any([context.dispatchSignal, ...(callSignal ? [callSignal] : [])])
520
588
  : callSignal;
@@ -541,17 +609,20 @@ export class InvocationService {
541
609
  activityTarget = target;
542
610
  argumentEcho = echoedCallArgs(args ?? {}, target.definition.inputSchema);
543
611
  sentSecrets.arguments(args ?? {}, target.definition.inputSchema);
544
- const write = target.definition.classification !== "read";
545
- const writeLike = isWriteLike(target.definition);
612
+ // Admission and presentation follow the served class. Dispatch and failure
613
+ // handling follow this: a source write served as a read still sends once.
614
+ const write = isWriteLike(target.definition);
615
+ if (write && context.writeTimeoutMs !== undefined)
616
+ timeoutMs = context.writeTimeoutMs;
546
617
  const canonicalAddress = `${target.connector.id}.${target.toolName}`;
547
- const input = context.source !== "execute_code"
618
+ const input = !isProgram(context.source)
548
619
  ? downstreamContinuation(this.catalog.requestScope, target.connector.id, canonicalAddress)
549
620
  : undefined;
550
621
  if (input)
551
622
  recordDownstreamArgumentEcho(this.catalog.requestScope, argumentEcho);
552
623
  const expectedDigest = replayClassificationDigest(this.catalog.requestScope, canonicalAddress);
553
624
  if (expectedDigest !== undefined &&
554
- (writeLike ||
625
+ (write ||
555
626
  target.classificationFresh !== true ||
556
627
  (yield* Effect.promise(() => classificationDigest(target.definition))) !== expectedDigest)) {
557
628
  return {
@@ -562,9 +633,15 @@ export class InvocationService {
562
633
  const connectorReadOnly = target.connector.readOnly === true;
563
634
  const refusal = writeRefusal(target.definition.classification, context.source, context.readOnly || connectorReadOnly);
564
635
  if (refusal === "read_only_endpoint") {
565
- return framingError("read_only_endpoint", `Tool "${canonicalAddress}" is a write, and ${connectorReadOnly ? `connector "${target.connector.id}"` : "this endpoint"} is read-only. Nothing here can send it.`);
636
+ return framingError("read_only_endpoint", `Tool "${canonicalAddress}" is a write, and ${connectorReadOnly ? `connector "${target.connector.id}"` : "this endpoint"} is read-only. No write route exists ${connectorReadOnly ? "for this connection" : "on this endpoint"}; tell the user. Only the operator can provide a writable connection.`);
566
637
  }
567
- if (refusal === "destructive_tool_requires_approval") {
638
+ // Below the sandbox: no guest code can reach past this, and catching it sends nothing.
639
+ if (context.source === READ_PROGRAM_SOURCE) {
640
+ if (readProgramRefuses(target.definition)) {
641
+ return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" can write, so ${READ_PROGRAM_SOURCE} refused it and nothing was sent. Use call_destructive_tool for this one call, or execute_code for a program that writes; the MCP host can request approval for either.`);
642
+ }
643
+ }
644
+ else if (refusal === "destructive_tool_requires_approval") {
568
645
  return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. ${routePhrases(context).writeNeedsApproval}`);
569
646
  }
570
647
  // Remote MCP tools advertise their input schema in the catalog. Validate
@@ -582,10 +659,11 @@ export class InvocationService {
582
659
  return classifyCallError(invalid);
583
660
  }
584
661
  // Source writes keep program write accounting; only effective writes spend W10 budget.
585
- if (writeLike && context.beforeWrite) {
662
+ if (write && context.beforeWrite) {
586
663
  const decision = yield* context.beforeWrite(target, args ?? {});
587
664
  if (decision.kind === "refuse") {
588
665
  unrecorded = decision.unrecorded === true;
666
+ gateRefused = true;
589
667
  return decision.error;
590
668
  }
591
669
  }
@@ -595,7 +673,7 @@ export class InvocationService {
595
673
  const call = (permit) => Effect.gen({ self: this }, function* () {
596
674
  const operationContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({
597
675
  signal: callSignal,
598
- timeoutMs: context.timeoutMs,
676
+ timeoutMs,
599
677
  defer: this.catalog.defer,
600
678
  routes: context.routes,
601
679
  }));
@@ -634,19 +712,27 @@ export class InvocationService {
634
712
  const code = classifyCallError(causeError(cause, connectorContext.signal)).code;
635
713
  preInvocationAuthFailure = code === "auth_required" || code === "downstream_oauth_required";
636
714
  })));
637
- // Cancellation can arrive during admission or context construction.
715
+ // Reflect.apply, never the function's own `call` or `apply`
716
+ // property, which anyone holding the function could replace.
717
+ const options = { definition: cloneTool(target.definition), ...defined({ input }) };
718
+ const entry = callEntry(connector, callTool);
719
+ // The final gate. Cancellation can arrive during admission or context
720
+ // construction, and resolution, admission or auth can spend the deadline.
721
+ // Nothing is awaited from here to the connector call below, so a call
722
+ // refused here is proven unsent and one let through left in time (INV-9).
638
723
  if (admissionSignal?.aborted)
639
724
  throw admissionSignal.reason;
725
+ if (context.dispatchDeadline && Date.now() >= context.dispatchDeadline.at) {
726
+ gateRefused = true;
727
+ pastDeadline = context.dispatchDeadline.refusal(canonicalAddress);
728
+ return undefined;
729
+ }
640
730
  dispatchedToConnector = true;
641
731
  recordCallEntry(this.catalog.requestScope, {
642
732
  address: canonicalAddress,
643
- classification: writeLike ? "write" : "read",
733
+ classification: write ? "write" : "read",
644
734
  fresh: target.classificationFresh === true,
645
735
  }, target.definition);
646
- // Reflect.apply, never the function's own `call` or `apply`
647
- // property, which anyone holding the function could replace.
648
- const options = { definition: cloneTool(target.definition), ...defined({ input }) };
649
- const entry = callEntry(connector, callTool);
650
736
  if (entry)
651
737
  return yield* entry(target.toolName, args ?? {}, options).pipe(Effect.provideContext(viewServices(connectorContext)));
652
738
  return yield* Effect.tryPromise({
@@ -675,6 +761,8 @@ export class InvocationService {
675
761
  const reply = yield* timed((elapsed) => {
676
762
  connectorMs += elapsed;
677
763
  }, call(permit));
764
+ if (pastDeadline)
765
+ return { pastDeadline };
678
766
  if (target.connector.kind === "mcp" && isDownstreamInputResult(reply)) {
679
767
  // The downstream suspended for input. A handler asks for
680
768
  // input from inside its run, so a write may already have
@@ -715,8 +803,15 @@ export class InvocationService {
715
803
  // An inner call's verdict that does not hold for this one is withdrawn.
716
804
  if (failureWrite && !answered)
717
805
  forgetProviderFailure(details);
806
+ // The tool's own words, which a program's write accounting keeps.
807
+ if (failureWrite && attemptError === thrown && thrown instanceof DownstreamToolError) {
808
+ details.toolAnswered = true;
809
+ failureText = thrown.fullText();
810
+ }
718
811
  return details;
719
812
  }
813
+ if ("pastDeadline" in attempt.value)
814
+ return attempt.value.pastDeadline;
720
815
  if ("inputRequired" in attempt.value) {
721
816
  inputRequiredValue = { value: attempt.value.value };
722
817
  return undefined;
@@ -745,6 +840,7 @@ export class InvocationService {
745
840
  ? withDeadlineEffect(dispatch, {
746
841
  ...defined({
747
842
  timeoutMs: context.timeoutMs,
843
+ extendMs: () => (timeoutMs ?? 0) - (context.timeoutMs ?? 0),
748
844
  signal: context.requestSignal,
749
845
  }),
750
846
  timeoutError: new ConnectorCallError("timeout", `Tool call timed out after ${context.timeoutMs}ms`),
@@ -769,7 +865,7 @@ export class InvocationService {
769
865
  // Q8 settles a result too: a write an undeclared 401 left unknown is no
770
866
  // success, whatever its handler made of the 401, and its result is not
771
867
  // processed.
772
- if (dispatchedToConnector && (completed.definition.classification !== "read" || sentSourceWrite())) {
868
+ if (dispatchedToConnector && (isWriteLike(completed.definition) || sentSourceWrite())) {
773
869
  const unknown = settleWriteResult(logicalCall?.sent ?? []);
774
870
  if (unknown) {
775
871
  failureAdvice = failureKeyAdvice = undefined;
@@ -786,7 +882,7 @@ export class InvocationService {
786
882
  resultProcessingMs += elapsed;
787
883
  }, Effect.tap(withDeadlineEffect(() => processing, {
788
884
  ...defined({
789
- timeoutMs: context.timeoutMs === undefined ? undefined : Math.max(1, context.timeoutMs - (Date.now() - started)),
885
+ timeoutMs: timeoutMs === undefined ? undefined : Math.max(1, timeoutMs - (Date.now() - started)),
790
886
  signal: context.requestSignal,
791
887
  }),
792
888
  timeoutError: new ConnectorCallError("timeout", "Result processing exceeded the tool call deadline."),
@@ -807,6 +903,13 @@ export class InvocationService {
807
903
  try {
808
904
  const value = sentSecrets.redact(processed.value, "envelope");
809
905
  const diagnostics = timing();
906
+ const trace = traceFor(this.catalog.requestScope);
907
+ if (trace) {
908
+ trace.record("catalog", diagnostics.catalogMs);
909
+ trace.record("admission", diagnostics.admissionMs);
910
+ trace.record("connector", diagnostics.connectorMs);
911
+ trace.record("result", diagnostics.resultProcessingMs, resultBytes ?? 0);
912
+ }
810
913
  const friction = context.activityFriction?.(value);
811
914
  record("success", friction ? { friction } : {});
812
915
  return {
@@ -26,7 +26,7 @@ export interface ResolvedPool {
26
26
  members: readonly Member[];
27
27
  /** Trimmed `owner`: the Workspace address agents in this pool act as. */
28
28
  owner?: string;
29
- /** Trimmed `description`, appended to this pool's MCP instructions. */
29
+ /** Trimmed `description`, placed before this pool's MCP routing instructions. */
30
30
  description?: string;
31
31
  /** Present for a native pool; absent is the six meta-tools. */
32
32
  native?: {
@@ -1,7 +1,7 @@
1
1
  import { Validator } from "@cfworker/json-schema";
2
2
  import { CATALOG_SEARCH_RESULT_SCHEMA } from "./catalog-service.js";
3
3
  import * as z from "zod";
4
- import { advertisedSchema } from "./advertised-schema.js";
4
+ import { advertisedSchema, openOutputSchema } from "./advertised-schema.js";
5
5
  const error = z
6
6
  .object({
7
7
  code: z.string(),
@@ -9,14 +9,13 @@ const error = z
9
9
  retryable: z.boolean(),
10
10
  })
11
11
  .passthrough();
12
- const format = z.enum(["json", "text"]);
13
12
  const counts = z.object({
14
13
  attempted: z.number().int().nonnegative(),
15
14
  admitted: z.number().int().nonnegative(),
16
15
  succeeded: z.number().int().nonnegative(),
17
16
  failed: z.number().int().nonnegative(),
18
17
  });
19
- export const EXECUTE_OUTPUT = advertisedSchema(z
18
+ export const EXECUTE_OUTPUT = openOutputSchema(advertisedSchema(z
20
19
  .object({
21
20
  result: z.unknown().optional(),
22
21
  error: error.optional(),
@@ -26,19 +25,14 @@ export const EXECUTE_OUTPUT = advertisedSchema(z
26
25
  emittedDiscarded: z.number().int().nonnegative().optional(),
27
26
  diagnostics: z.record(z.string(), z.unknown()).optional(),
28
27
  })
29
- .passthrough());
30
- export const SKILLS_OUTPUT = advertisedSchema(z
31
- .object({
32
- name: z.string().optional(),
33
- text: z.string().optional(),
34
- format: format.optional(),
35
- skills: z.array(z.object({ name: z.string(), uri: z.string(), description: z.string() })).optional(),
36
- error: error.optional(),
37
- })
38
- .passthrough());
39
- // Catalog publication, SDK advertisement and validation share one schema.
28
+ .passthrough()), {
29
+ type: "object",
30
+ required: ["hostCalls"],
31
+ properties: { result: {}, error: { type: "object" }, hostCalls: { type: "object" }, logs: { type: "string" } },
32
+ });
33
+ // The server validates every page against the full catalog contract; clients get the open summary.
40
34
  const searchValidator = new Validator(CATALOG_SEARCH_RESULT_SCHEMA, "2020-12", false);
41
- export const SEARCH_OUTPUT = {
35
+ export const SEARCH_OUTPUT = openOutputSchema({
42
36
  "~standard": {
43
37
  version: 1,
44
38
  vendor: "connecta",
@@ -50,12 +44,24 @@ export const SEARCH_OUTPUT = {
50
44
  output: () => CATALOG_SEARCH_RESULT_SCHEMA,
51
45
  },
52
46
  },
53
- };
54
- export const AUTHORIZE_OUTPUT = advertisedSchema(z
47
+ }, {
48
+ type: "object",
49
+ required: ["catalogErrors", "tools", "total", "offset", "limit", "hasMore"],
50
+ properties: {
51
+ catalogErrors: { type: "array" },
52
+ tools: { type: "array", items: { type: "object", required: ["name", "address"] } },
53
+ total: { type: "integer" },
54
+ offset: { type: "integer" },
55
+ limit: { type: "integer" },
56
+ hasMore: { type: "boolean" },
57
+ nextOffset: { type: "integer" },
58
+ },
59
+ });
60
+ export const AUTHORIZE_OUTPUT = openOutputSchema(advertisedSchema(z
55
61
  .object({
56
62
  connector: z.string().optional(),
57
63
  recovery: z.string().optional(),
58
64
  message: z.string().optional(),
59
65
  error: error.optional(),
60
66
  })
61
- .passthrough());
67
+ .passthrough()), { type: "object", properties: { connector: { type: "string" }, message: { type: "string" } } });
@@ -1,2 +1,2 @@
1
- /** The complete upward meta-tool set, shared with the CLI. */
1
+ /** The upward meta-tool set every meta endpoint lists, shared with the CLI. */
2
2
  export declare const META_TOOL_NAMES: readonly string[];
@@ -1,3 +1,3 @@
1
1
  import { META_TOOL_NAMES as names } from "../bin/meta-tool-names.mjs";
2
- /** The complete upward meta-tool set, shared with the CLI. */
2
+ /** The upward meta-tool set every meta endpoint lists, shared with the CLI. */
3
3
  export const META_TOOL_NAMES = names;