@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.
- package/CHANGELOG.md +148 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +32 -5
- package/dist/activity.js +32 -6
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +33 -44
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +15 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +77 -39
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +352 -43
- package/dist/catalog-freshness.js +25 -0
- package/dist/catalog-service.js +408 -78
- package/dist/catalog.js +216 -62
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +53 -21
- package/dist/config.js +56 -17
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +113 -22
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +14 -5
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +4 -2
- package/dist/describe-config.js +1 -0
- package/dist/discovery-health.js +26 -11
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +60 -4
- package/dist/errors.js +88 -3
- package/dist/execute.js +491 -202
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +3 -3
- package/dist/executors/quickjs-child.js +14 -5
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +37 -20
- package/dist/executors/quickjs.js +24 -7
- package/dist/guarded-transport.js +2 -0
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +14 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +11 -3
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +15 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +167 -64
- package/dist/membership.d.ts +1 -1
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +286 -73
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +177 -55
- package/dist/node.d.ts +7 -0
- package/dist/node.js +85 -4
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +104 -0
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +9 -1
- package/dist/operator-record.js +16 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +1 -14
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +2 -1
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +8 -3
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +2 -1
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +3 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +12 -6
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +3 -6
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +2 -1
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +2 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +3 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +142 -30
- package/dist/resource-read.js +21 -2
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +53 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +83 -35
- package/dist/routes/shared.d.ts +6 -0
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +38 -2
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +213 -57
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +16 -7
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +36 -0
- package/dist/url-safety.js +38 -20
- package/dist/usage-guide.js +14 -13
- package/dist/utility-recovery.js +33 -0
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +60 -15
- package/documentation/architecture.md +152 -43
- package/documentation/auth.md +160 -53
- package/documentation/code-mode.md +396 -119
- package/documentation/deploying.md +35 -10
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +761 -157
- package/documentation/operating.md +91 -4
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +34 -20
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +10 -8
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
package/dist/invocation.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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}
|
|
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 = {
|
|
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
|
|
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 (
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
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 ${
|
|
464
|
-
details: { ...error.details, operation, stage, elapsedMs, ...defined({ deadlineMs:
|
|
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
|
-
|
|
545
|
-
|
|
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
|
|
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
|
-
(
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
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
|
|
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:
|
|
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 {
|
package/dist/membership.d.ts
CHANGED
|
@@ -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`,
|
|
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?: {
|
package/dist/meta-output.js
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
1
|
+
/** The upward meta-tool set every meta endpoint lists, shared with the CLI. */
|
|
2
2
|
export declare const META_TOOL_NAMES: readonly string[];
|
package/dist/meta-tool-names.js
CHANGED