@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
@@ -25,6 +25,8 @@ import { Integer, isMaxChars, isMinChars } from "./json-schema.js";
25
25
  import { boundCursor, StandardPage } from "./paging.js";
26
26
  import { Classification, Dispatches, IdempotencyKey, RequestSecrets, Signal } from "./services.js";
27
27
  import { read, write } from "./tools.js";
28
+ /** A page that reports more data it cannot continue must never read as the complete answer. */
29
+ const CONTINUATION_WITHHELD = "This result is incomplete: more data exists and continuation was withheld.";
28
30
  const indexedOperations = new WeakMap();
29
31
  function operationsOf(index) {
30
32
  let operations = indexedOperations.get(index);
@@ -204,7 +206,7 @@ export function hatch(options) {
204
206
  }
205
207
  return fieldsInput(fieldsNote(first ?? preferred));
206
208
  };
207
- const fieldsNote = (example) => fieldsDescription("data", example, `Copy declared paths from ${name}_api_details response.fields. Applied after value safety and privacy filtering.`);
209
+ const fieldsNote = (example) => fieldsDescription("data", example, `Copy declared paths from ${name}_api_details response.fields. A withheld value reads "[redacted]".`);
208
210
  const verdictOf = (op) => verdicts.get(`${op.method} ${op.path}`) ?? "refuse";
209
211
  const refusalOf = (op) => connection.safety.refusal?.({ method: op.method, template: op.path }) ?? options.binding?.refusal?.(op);
210
212
  const discoveryRefusals = new Map();
@@ -259,7 +261,7 @@ export function hatch(options) {
259
261
  }
260
262
  : {}),
261
263
  warnings: Schema.optionalKey(Schema.Array(Schema.String).annotate({
262
- description: `${options.binding?.warnings ? "Reviewed warnings preserved after fields selection, and " : ""}fields entries that matched nothing in this nonempty response, with the properties declared where matching stopped. At most 10 warnings of 512 characters, then an omitted count.`,
264
+ description: `${options.binding?.warnings ? "Reviewed warnings preserved after fields selection, and " : ""}fields entries that matched nothing in this nonempty response, with the properties declared where matching stopped${pages.size ? ", and a notice when continuation was withheld" : ""}. At most 10 warnings of 512 characters, then an omitted count.`,
263
265
  })),
264
266
  });
265
267
  const hasTotals = [...pages.values()].some((adapters) => adapters.some((adapter) => adapter.total !== undefined));
@@ -268,11 +270,14 @@ export function hatch(options) {
268
270
  ...callEnvelope.fields,
269
271
  page: Schema.optionalKey(Schema.Struct({
270
272
  ...StandardPage.fields,
273
+ hasMore: Schema.Boolean.annotate({
274
+ description: "True without nextCursor means this data is incomplete and continuation was withheld.",
275
+ }),
271
276
  ...(hasTotals ? { total: Schema.optionalKey(Integer.check(Schema.isGreaterThanOrEqualTo(0))) } : {}),
272
277
  param: Schema.String.annotate({
273
278
  description: options.pageCursor === "argument"
274
279
  ? "Vendor parameter restored from cursor; pass nextCursor as the top-level cursor."
275
- : "Put nextCursor in this vendor parameter, unchanged.",
280
+ : "Put nextCursor unchanged in query[param] or body[param], as page.in says, and keep every other argument.",
276
281
  }),
277
282
  in: Schema.Literals(["query", "body"]),
278
283
  })),
@@ -1223,6 +1228,7 @@ export function hatch(options) {
1223
1228
  // Roots were checked before dispatch; the pinned index cannot say whether a
1224
1229
  // deeper level is closed, so a nested miss is reported, never refused.
1225
1230
  const warnings = boundWarnings([
1231
+ ...(page?.hasMore && page.nextCursor === undefined ? [CONTINUATION_WITHHELD] : []),
1226
1232
  ...(options.binding?.warnings && "warnings" in answer ? (answer.warnings ?? []) : []),
1227
1233
  ...fieldsMisses(answered, args.fields, index
1228
1234
  .fields(op)
@@ -20,7 +20,7 @@ export const StandardPage = Schema.Struct({
20
20
  });
21
21
  /** The input field that carries a cursor back. */
22
22
  export const Cursor = Schema.String.check(Schema.isMinLength(1)).annotate({
23
- description: "Opaque nextCursor from the previous page. Pass it back unchanged.",
23
+ description: "Opaque page.nextCursor from the previous page. Pass it back unchanged with the same arguments.",
24
24
  });
25
25
  /**
26
26
  * Lookahead paging, for a vendor that returns no `has_more`: ask for one row
@@ -46,7 +46,7 @@ const missingGrant = {
46
46
  };
47
47
  /** An ordinary Promise connector factory; Effect remains inside the kit. */
48
48
  export function defineApiProvider(definition) {
49
- const { displayTitle, description, authScope, vendor, baseUrl, maxResponseBytes, timeoutMs, headers, admit, transport, defaultCallAdmission, pageSize: declaredPageSize, idempotencyKey, credential, oauth, oauthWrite401, grant, grantRenewal, authenticate, errors, safety, result, inBandRefusals, probe, guide, transformGuide, hatch: declaredHatch, tools, tagConnector, searchSynonyms, ...metadata } = definition;
49
+ const { displayTitle, description, authScope, vendor, baseUrl, maxResponseBytes, timeoutMs, headers, admit, transport, defaultCallAdmission, pageSize: declaredPageSize, idempotencyKey, credential, oauth, oauthWrite401, grant, grantRenewal, authenticate, publicIdentifierHeaders, errors, safety, result, inBandRefusals, probe, guide, transformGuide, hatch: declaredHatch, tools, tagConnector, searchSynonyms, ...metadata } = definition;
50
50
  if (searchSynonyms !== undefined &&
51
51
  !(Array.isArray(searchSynonyms) &&
52
52
  searchSynonyms.length > 0 &&
@@ -95,18 +95,13 @@ export function defineApiProvider(definition) {
95
95
  kind: "api",
96
96
  kit: {
97
97
  operations: operations(vendor),
98
- ...(pageSize ? { pageSize } : {}),
99
98
  ...(hatchOptions
100
99
  ? {
101
100
  hatch: {
102
101
  name: hatchOptions.name,
103
102
  verdicts: hatchOptions.verdicts,
104
103
  ...(hatchOptions.fieldsRoot ? { fieldsRoot: hatchOptions.fieldsRoot } : {}),
105
- ...(hatchOptions.pageCursor && Object.keys(hatchOptions.pages ?? {}).length
106
- ? { paging: hatchOptions.pageCursor }
107
- : {}),
108
104
  },
109
- refusesWithheld: true,
110
105
  }
111
106
  : {}),
112
107
  ...(meanings ? { errors: meanings } : {}),
@@ -134,6 +129,7 @@ export function defineApiProvider(definition) {
134
129
  ...(headers === undefined ? {} : { headers }),
135
130
  ...(admit === undefined ? {} : { admit }),
136
131
  authenticate: typeof authenticate === "function" ? authenticate(options) : authenticate,
132
+ ...(publicIdentifierHeaders === undefined ? {} : { publicIdentifierHeaders }),
137
133
  ...configured,
138
134
  });
139
135
  const connected = connect(vendor, oauth || machineGrant
@@ -342,6 +342,7 @@ export function connect(vendor, guarded, options) {
342
342
  method: declared.method,
343
343
  template: declared.path,
344
344
  ...(sendOptions?.flags ? { flags: sendOptions.flags } : {}),
345
+ ...(sendOptions?.context ? { context: sendOptions.context } : {}),
345
346
  };
346
347
  return Effect.flatMap(admitAddress({
347
348
  params: request.params,
@@ -370,7 +371,11 @@ export function connect(vendor, guarded, options) {
370
371
  request.form !== undefined ||
371
372
  request.emptyForm))
372
373
  return Effect.fail(new RequestRefused({ message: "A raw body cannot accompany a bulk item or another body encoding." }));
373
- return Effect.flatMap(admitAddress({ path, query: request.query }), () => Effect.flatMap(options.safety.resolve(method, path), (operation) => settle({ ...operation, ...(sendOptions?.flags ? { flags: sendOptions.flags } : {}) }, Effect.suspend(() => {
374
+ return Effect.flatMap(admitAddress({ path, query: request.query }), () => Effect.flatMap(options.safety.resolve(method, path), (operation) => settle({
375
+ ...operation,
376
+ ...(sendOptions?.flags ? { flags: sendOptions.flags } : {}),
377
+ ...(sendOptions?.context ? { context: sendOptions.context } : {}),
378
+ }, Effect.suspend(() => {
374
379
  let built = HttpClientRequest.make(method)(path, request.query ? { urlParams: parameterPairs(request.query) } : {});
375
380
  if (request.headers)
376
381
  built = HttpClientRequest.setHeaders(built, request.headers);
package/dist/index.d.ts CHANGED
@@ -41,6 +41,6 @@ export type { Registry } from "./registry.js";
41
41
  export type { RemoteMcpOptions, RemoteMcpAuth, RemoteMcpRequestAuth, RemoteMcpRedirectPolicy, } from "./connectors/remote-mcp.js";
42
42
  export type { AgentRequestPrincipal, HumanRequestPrincipal, PrincipalForwarding, PrincipalRequestToken, RequestPrincipal, RequestPrincipalPolicy, RequestToken, RequestTokenInput, } from "./request-token.js";
43
43
  export type { ApiHandlerContext, ApiOAuthAccess, ApiOAuthClientAuthentication, ApiOAuthConfig, ApiOptions, ApiTool, } from "./connectors/api.js";
44
- export type { ConnectaBranding, ConnectaTheme, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, AgentRouteNames, ConnectorContext, ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, ConnectorUsageGuide, ConnectorSkill, ConnectorSkillResourceContents, DescribedEndpoint, ConnectorStatus, CredentialFailureClass, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
44
+ export type { ConnectaBranding, ConnectaTheme, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCatalogPeek, ConnectorCredentialValues, AgentRouteNames, ConnectorContext, ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, ConnectorUsageGuide, ConnectorSkill, ConnectorSkillResourceContents, DescribedEndpoint, ConnectorStatus, CredentialFailureClass, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
45
45
  export type { ActivityActor, ActivityCatalogChange, ActivityCallSource, ActivityOutcome, ActivityPage, ActivityReadActor, ActivityReadEvent, ActivityReader, ActivityReadGate, ActivityReadPage, ActivitySink, ActivityStore, AgentFriction, ToolCallActivityEvent, } from "./activity.js";
46
46
  export { META_TOOL_NAMES } from "./meta-tool-names.js";
package/dist/index.js CHANGED
@@ -211,6 +211,11 @@ export function createConnecta(config) {
211
211
  const requestAdmission = new AdmissionController(resolved.admission.requests);
212
212
  let codeAdmission;
213
213
  let executor = resolved.executor;
214
+ if (executor.deadlineMs !== undefined && executor.deadlineMs >= resolved.execute.watchdogMs) {
215
+ logger.warn(`[connecta] The executor's ${executor.deadlineMs}ms deadline does not end before execute.watchdogMs ` +
216
+ `(${resolved.execute.watchdogMs}ms), so programs end at the watchdog. Raise execute.watchdogMs ` +
217
+ "above it if your MCP clients allow requests that long.");
218
+ }
214
219
  // Read the identity off the configured executor, before any wrapper hides
215
220
  // it behind an anonymous object literal.
216
221
  const configuredExecutorName = executorName(executor);
@@ -4,12 +4,12 @@ import { routePhrases } from "./agent-routes.js";
4
4
  import { authCall, registerAuthMethod } from "./auth/native.js";
5
5
  import { withAuthorizationHandoffEffect } from "./authorization-handoff.js";
6
6
  import { hasControlCharacters } from "./tool-name.js";
7
- import { cloneTool, isWriteLike, writeRefusal } from "./tool-safety.js";
7
+ import { cloneTool, isWriteLike, READ_PROGRAM_SOURCE, readProgramRefuses, writeRefusal } from "./tool-safety.js";
8
8
  import { Cause, Context, Effect, Exit } from "effect";
9
9
  import { CallAdmissionError, isCallAdmissionError } from "./call-admission.js";
10
- 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";
11
11
  import { downstreamValue } from "./mcp-result.js";
12
- import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, } from "./operator-record.js";
12
+ import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, labelErrorClass, } from "./operator-record.js";
13
13
  import { splitAddress } from "./registry.js";
14
14
  import { causeError, runEdge, withDeadlineEffect } from "./runtime/run.js";
15
15
  import { schemaExample, validateCatalogToolInput } from "./validate.js";
@@ -84,7 +84,18 @@ function isCallerCancellation(error, signal) {
84
84
  * write it is a 2xx reply after which the handler may have applied it.
85
85
  */
86
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
+ }
87
96
  }
97
+ // Records tell the tool's own answer from a transport or runtime failure.
98
+ labelErrorClass(DownstreamToolError, "DownstreamToolError");
88
99
  function isTimeoutFailure(error) {
89
100
  const code = error.details?.code;
90
101
  return (error.code === "timeout" ||
@@ -97,10 +108,10 @@ function assertRawMcpSuccess(kind, result) {
97
108
  const mcpResult = result;
98
109
  if (!mcpResult.isError)
99
110
  return;
100
- throw new DownstreamToolError(boundedEchoText(mcpResult.content
111
+ throw new DownstreamToolError(mcpResult.content
101
112
  ?.filter((block) => block.type === "text")
102
113
  .map((block) => block.text ?? "")
103
- .join("") || "Downstream tool call failed"));
114
+ .join("\n") || "Downstream tool call failed");
104
115
  }
105
116
  /**
106
117
  * Whether a failed attempt is the downstream tool's own answer. For a read:
@@ -116,6 +127,14 @@ function answeredFailure(error, write, call) {
116
127
  return error instanceof DownstreamToolError || isKnownProviderFailure(error);
117
128
  return knownCallFailure(call, error);
118
129
  }
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;
119
138
  export { InvocationFailure } from "./invocation-failure.js";
120
139
  const withoutExample = ({ example: _example, ...stated }) => stated;
121
140
  /**
@@ -165,6 +184,7 @@ export class InvocationService {
165
184
  const recoveryState = recoveryRecord();
166
185
  let preInvocationAuthFailure = false;
167
186
  let answered = false;
187
+ let failureText;
168
188
  // A dispatched failure's advice to send it again, composed by enrich
169
189
  // only once the outcome allows it (resendAdvice), and its advice to
170
190
  // reuse an idempotency key, only where that key protects the whole
@@ -175,6 +195,10 @@ export class InvocationService {
175
195
  context.sentSecrets?.include(sentSecrets);
176
196
  // A write gate's refusal that is no attempt; see WriteGateDecision.
177
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;
178
202
  let resolved;
179
203
  let activityTarget;
180
204
  // The address as written, used for activity when resolution never reached
@@ -223,8 +247,7 @@ export class InvocationService {
223
247
  ? { ...error, nextAction: error.nextAction ?? utilityRecovery(error.code, error.message).nextAction }
224
248
  : error;
225
249
  const recovery = recoveryState.facts;
226
- const enteredWrite = dispatchedToConnector &&
227
- ((resolved !== undefined && resolved.definition.classification !== "read") || sentSourceWrite());
250
+ const enteredWrite = dispatchedToConnector && ((resolved !== undefined && isWriteLike(resolved.definition)) || sentSourceWrite());
228
251
  // A known answer alone is not dispatch evidence. Require an owned,
229
252
  // complete ledger with a positively recorded auxiliary resource read;
230
253
  // an empty ledger, custom entry or interruption proves nothing.
@@ -243,7 +266,7 @@ export class InvocationService {
243
266
  return {
244
267
  ...error,
245
268
  code: "write_outcome_unknown",
246
- 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.`,
247
270
  retryable: false,
248
271
  ...(sentSourceWrite() ? { writeOutcome: "unknown" } : {}),
249
272
  connector: target.connector.id,
@@ -286,7 +309,9 @@ export class InvocationService {
286
309
  writeOutcome: "unknown",
287
310
  ...(!recovery && error.retry === undefined
288
311
  ? {
289
- retry: "No safe replay action exists. Re-read the target to reconcile whether the write took effect.",
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.",
290
315
  }
291
316
  : {}),
292
317
  };
@@ -425,10 +450,15 @@ export class InvocationService {
425
450
  },
426
451
  connector: target.connector.id,
427
452
  operation: `${target.connector.id}.${target.toolName}`,
428
- nextAction: this.catalog.searchRecovery({
429
- query: target.toolName,
430
- connector: target.connector.id,
431
- }, "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
+ }),
432
462
  retry: `Correct the listed arguments and retry ` + `${target.connector.id}.${target.toolName}.`,
433
463
  };
434
464
  case "not_found":
@@ -444,7 +474,7 @@ export class InvocationService {
444
474
  return error;
445
475
  // The continuation nonce is spent. Let the input boundary supply
446
476
  // its fresh-call action instead of generic availability advice.
447
- if (context.source !== "execute_code" &&
477
+ if (!isProgram(context.source) &&
448
478
  downstreamContinuation(this.catalog.requestScope, target.connector.id, `${target.connector.id}.${target.toolName}`))
449
479
  return error;
450
480
  return {
@@ -462,7 +492,7 @@ export class InvocationService {
462
492
  // a write. No connector failure arrives, so the logical call's
463
493
  // recorded requests alone decide whether a 401 left it unknown.
464
494
  if (dispatchedToConnector &&
465
- (resolved?.definition.classification !== "read" || sentSourceWrite()) &&
495
+ (resolved === undefined || isWriteLike(resolved.definition) || sentSourceWrite()) &&
466
496
  (error.code === "cancelled" || isTimeoutFailure(error))) {
467
497
  const departed = settleWrite(logicalCall?.sent ?? [], undefined, true);
468
498
  if (departed !== undefined) {
@@ -487,13 +517,13 @@ export class InvocationService {
487
517
  // Read before any step below rebuilds the details: Q8's verdict is final.
488
518
  const settled = isSettledWrite(error);
489
519
  // Attach host deadline facts before write recovery changes the code.
490
- if (isTimeoutFailure(error)) {
520
+ if (isTimeoutFailure(error) && !gateRefused) {
491
521
  const elapsedMs = Date.now() - started;
492
522
  const operation = boundedEchoText(address, 512);
493
523
  error = {
494
524
  ...error,
495
- message: `Operation "${operation}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${context.timeoutMs}ms).`,
496
- 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 }) },
497
527
  };
498
528
  }
499
529
  // Downstream diagnostics were redacted under payload rules where they entered.
@@ -506,6 +536,7 @@ export class InvocationService {
506
536
  ...defined({ resolved }),
507
537
  dispatched: dispatchedToConnector,
508
538
  ...(dispatchedToConnector ? { answered } : {}),
539
+ ...defined({ failureText }),
509
540
  error: details,
510
541
  });
511
542
  // A write refused only because its program had already returned
@@ -547,6 +578,11 @@ export class InvocationService {
547
578
  // expected, and a throw anywhere in it — a defect, to Effect — is
548
579
  // classified like a failure, as the async body this replaced did.
549
580
  const dispatch = (callSignal) => Effect.gen({ self: this }, function* () {
581
+ if (context.refusal) {
582
+ gateRefused = true;
583
+ return context.refusal;
584
+ }
585
+ let pastDeadline;
550
586
  const admissionSignal = context.dispatchSignal
551
587
  ? AbortSignal.any([context.dispatchSignal, ...(callSignal ? [callSignal] : [])])
552
588
  : callSignal;
@@ -573,17 +609,20 @@ export class InvocationService {
573
609
  activityTarget = target;
574
610
  argumentEcho = echoedCallArgs(args ?? {}, target.definition.inputSchema);
575
611
  sentSecrets.arguments(args ?? {}, target.definition.inputSchema);
576
- const write = target.definition.classification !== "read";
577
- 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;
578
617
  const canonicalAddress = `${target.connector.id}.${target.toolName}`;
579
- const input = context.source !== "execute_code"
618
+ const input = !isProgram(context.source)
580
619
  ? downstreamContinuation(this.catalog.requestScope, target.connector.id, canonicalAddress)
581
620
  : undefined;
582
621
  if (input)
583
622
  recordDownstreamArgumentEcho(this.catalog.requestScope, argumentEcho);
584
623
  const expectedDigest = replayClassificationDigest(this.catalog.requestScope, canonicalAddress);
585
624
  if (expectedDigest !== undefined &&
586
- (writeLike ||
625
+ (write ||
587
626
  target.classificationFresh !== true ||
588
627
  (yield* Effect.promise(() => classificationDigest(target.definition))) !== expectedDigest)) {
589
628
  return {
@@ -594,9 +633,15 @@ export class InvocationService {
594
633
  const connectorReadOnly = target.connector.readOnly === true;
595
634
  const refusal = writeRefusal(target.definition.classification, context.source, context.readOnly || connectorReadOnly);
596
635
  if (refusal === "read_only_endpoint") {
597
- 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"}. Ask the operator for a writable connection if writes are required.`);
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.`);
637
+ }
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
+ }
598
643
  }
599
- if (refusal === "destructive_tool_requires_approval") {
644
+ else if (refusal === "destructive_tool_requires_approval") {
600
645
  return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. ${routePhrases(context).writeNeedsApproval}`);
601
646
  }
602
647
  // Remote MCP tools advertise their input schema in the catalog. Validate
@@ -614,10 +659,11 @@ export class InvocationService {
614
659
  return classifyCallError(invalid);
615
660
  }
616
661
  // Source writes keep program write accounting; only effective writes spend W10 budget.
617
- if (writeLike && context.beforeWrite) {
662
+ if (write && context.beforeWrite) {
618
663
  const decision = yield* context.beforeWrite(target, args ?? {});
619
664
  if (decision.kind === "refuse") {
620
665
  unrecorded = decision.unrecorded === true;
666
+ gateRefused = true;
621
667
  return decision.error;
622
668
  }
623
669
  }
@@ -627,7 +673,7 @@ export class InvocationService {
627
673
  const call = (permit) => Effect.gen({ self: this }, function* () {
628
674
  const operationContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({
629
675
  signal: callSignal,
630
- timeoutMs: context.timeoutMs,
676
+ timeoutMs,
631
677
  defer: this.catalog.defer,
632
678
  routes: context.routes,
633
679
  }));
@@ -666,19 +712,27 @@ export class InvocationService {
666
712
  const code = classifyCallError(causeError(cause, connectorContext.signal)).code;
667
713
  preInvocationAuthFailure = code === "auth_required" || code === "downstream_oauth_required";
668
714
  })));
669
- // 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).
670
723
  if (admissionSignal?.aborted)
671
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
+ }
672
730
  dispatchedToConnector = true;
673
731
  recordCallEntry(this.catalog.requestScope, {
674
732
  address: canonicalAddress,
675
- classification: writeLike ? "write" : "read",
733
+ classification: write ? "write" : "read",
676
734
  fresh: target.classificationFresh === true,
677
735
  }, target.definition);
678
- // Reflect.apply, never the function's own `call` or `apply`
679
- // property, which anyone holding the function could replace.
680
- const options = { definition: cloneTool(target.definition), ...defined({ input }) };
681
- const entry = callEntry(connector, callTool);
682
736
  if (entry)
683
737
  return yield* entry(target.toolName, args ?? {}, options).pipe(Effect.provideContext(viewServices(connectorContext)));
684
738
  return yield* Effect.tryPromise({
@@ -707,6 +761,8 @@ export class InvocationService {
707
761
  const reply = yield* timed((elapsed) => {
708
762
  connectorMs += elapsed;
709
763
  }, call(permit));
764
+ if (pastDeadline)
765
+ return { pastDeadline };
710
766
  if (target.connector.kind === "mcp" && isDownstreamInputResult(reply)) {
711
767
  // The downstream suspended for input. A handler asks for
712
768
  // input from inside its run, so a write may already have
@@ -747,8 +803,15 @@ export class InvocationService {
747
803
  // An inner call's verdict that does not hold for this one is withdrawn.
748
804
  if (failureWrite && !answered)
749
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
+ }
750
811
  return details;
751
812
  }
813
+ if ("pastDeadline" in attempt.value)
814
+ return attempt.value.pastDeadline;
752
815
  if ("inputRequired" in attempt.value) {
753
816
  inputRequiredValue = { value: attempt.value.value };
754
817
  return undefined;
@@ -777,6 +840,7 @@ export class InvocationService {
777
840
  ? withDeadlineEffect(dispatch, {
778
841
  ...defined({
779
842
  timeoutMs: context.timeoutMs,
843
+ extendMs: () => (timeoutMs ?? 0) - (context.timeoutMs ?? 0),
780
844
  signal: context.requestSignal,
781
845
  }),
782
846
  timeoutError: new ConnectorCallError("timeout", `Tool call timed out after ${context.timeoutMs}ms`),
@@ -801,7 +865,7 @@ export class InvocationService {
801
865
  // Q8 settles a result too: a write an undeclared 401 left unknown is no
802
866
  // success, whatever its handler made of the 401, and its result is not
803
867
  // processed.
804
- if (dispatchedToConnector && (completed.definition.classification !== "read" || sentSourceWrite())) {
868
+ if (dispatchedToConnector && (isWriteLike(completed.definition) || sentSourceWrite())) {
805
869
  const unknown = settleWriteResult(logicalCall?.sent ?? []);
806
870
  if (unknown) {
807
871
  failureAdvice = failureKeyAdvice = undefined;
@@ -818,7 +882,7 @@ export class InvocationService {
818
882
  resultProcessingMs += elapsed;
819
883
  }, Effect.tap(withDeadlineEffect(() => processing, {
820
884
  ...defined({
821
- 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)),
822
886
  signal: context.requestSignal,
823
887
  }),
824
888
  timeoutError: new ConnectorCallError("timeout", "Result processing exceeded the tool call deadline."),
@@ -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;