@zackbart/connecta 0.28.1 → 0.29.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 +788 -5
- package/README.md +62 -42
- package/bin/connecta.mjs +110 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1584 -1580
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +93 -71
- package/dist/auth-elicitation.js +283 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +368 -224
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +631 -0
- package/dist/config.js +503 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +84 -35
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1774 -320
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +230 -73
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +117 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +356 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +320 -54
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +137 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +434 -0
- package/documentation/architecture.md +664 -231
- package/documentation/auth.md +936 -546
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +120 -54
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +133 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/basecamp.js +0 -445
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { vettedSchemaDigest } from "./catalog-drift.js";
|
|
2
|
+
// Only built-in factories register local credential checks. This hook is not
|
|
3
|
+
// part of Connector, and a replacement callTool cannot inherit its proof.
|
|
4
|
+
const checks = new WeakMap();
|
|
5
|
+
export function registerInvocationAuth(connector, check) {
|
|
6
|
+
checks.set(connector, { callTool: connector.callTool, check });
|
|
7
|
+
}
|
|
8
|
+
/** Local credential resolution only, before entering any connector call. */
|
|
9
|
+
export async function resolveInvocationAuth(connector, ctx) {
|
|
10
|
+
const entry = checks.get(connector);
|
|
11
|
+
if (entry?.callTool === connector.callTool)
|
|
12
|
+
await entry.check(ctx);
|
|
13
|
+
}
|
|
14
|
+
const requests = new WeakMap();
|
|
15
|
+
function requestFor(scope) {
|
|
16
|
+
let facts = requests.get(scope);
|
|
17
|
+
if (!facts) {
|
|
18
|
+
facts = { entered: [], authFailures: new Map(), retryReads: new Map() };
|
|
19
|
+
requests.set(scope, facts);
|
|
20
|
+
}
|
|
21
|
+
return facts;
|
|
22
|
+
}
|
|
23
|
+
/** Bind the verdict, annotation facts and schemas used for this call. */
|
|
24
|
+
export async function classificationDigest(definition) {
|
|
25
|
+
try {
|
|
26
|
+
const schema = await vettedSchemaDigest(definition);
|
|
27
|
+
const annotations = Object.entries(definition.annotations ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
28
|
+
const bytes = new TextEncoder().encode(JSON.stringify({ classification: definition.classification, annotations, schema }));
|
|
29
|
+
return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// A digest refusal blocks recovery, not an ordinary call.
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Called by the host immediately before callTool, using its private scope. */
|
|
37
|
+
export function recordCallEntry(scope, call, definition) {
|
|
38
|
+
// Snapshot synchronously. Recovery hashes this private copy only when it
|
|
39
|
+
// needs a replay binding, without changing ordinary admission or completion.
|
|
40
|
+
requestFor(scope).entered.push({ ...call, definition: structuredClone(definition) });
|
|
41
|
+
}
|
|
42
|
+
export function bindReplayReads(scope, reads) {
|
|
43
|
+
requestFor(scope).retryReads = new Map(reads.map((read) => [read.address, read.digest]));
|
|
44
|
+
}
|
|
45
|
+
export function replayClassificationDigest(scope, address) {
|
|
46
|
+
return requestFor(scope).retryReads.get(address);
|
|
47
|
+
}
|
|
48
|
+
/** Eligibility comes from invocation control flow, never error fields. */
|
|
49
|
+
export function recordAuthFailure(scope, connector, eligible) {
|
|
50
|
+
const failures = requestFor(scope).authFailures;
|
|
51
|
+
failures.set(connector, eligible && failures.get(connector) !== false);
|
|
52
|
+
}
|
|
53
|
+
export async function authRecoveryFacts(scope, connector) {
|
|
54
|
+
const facts = requestFor(scope);
|
|
55
|
+
const writeEntered = facts.entered.some((call) => call.classification === "write");
|
|
56
|
+
const reads = new Map();
|
|
57
|
+
let unsafeEntered = writeEntered;
|
|
58
|
+
for (const call of facts.entered) {
|
|
59
|
+
const digest = await classificationDigest(call.definition);
|
|
60
|
+
unsafeEntered ||= !call.fresh || !digest || (reads.has(call.address) && reads.get(call.address) !== digest);
|
|
61
|
+
reads.set(call.address, digest);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
writeEntered,
|
|
65
|
+
unsafeEntered,
|
|
66
|
+
eligible: !unsafeEntered && facts.authFailures.get(connector) === true,
|
|
67
|
+
reads: [...reads].map(([address, digest]) => ({ address, digest })),
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/invocation.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { hasControlCharacters } from "./tool-name.js";
|
|
2
|
+
import { surfaceAllowsTool } from "./tool-safety.js";
|
|
1
3
|
import { Cause, Effect, Exit } from "effect";
|
|
2
|
-
import { isCallAdmissionError
|
|
3
|
-
import { boundedEchoText, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
|
|
4
|
-
import {
|
|
4
|
+
import { isCallAdmissionError } from "./call-admission.js";
|
|
5
|
+
import { boundedEchoText, classificationCode, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
|
|
6
|
+
import { downstreamValue } from "./mcp-result.js";
|
|
7
|
+
import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, } from "./operator-record.js";
|
|
5
8
|
import { splitAddress } from "./registry.js";
|
|
6
9
|
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
10
|
+
import { validateCatalogToolInput } from "./validate.js";
|
|
11
|
+
import { sentSecretsFor, sentSecretsForRequest, trackCredentialReads } from "./sent-secrets.js";
|
|
12
|
+
import { classificationDigest, recordAuthFailure, recordCallEntry, replayClassificationDigest, resolveInvocationAuth, } from "./invocation-auth.js";
|
|
13
|
+
import { downstreamContinuation, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
|
|
9
14
|
function defined(values) {
|
|
10
15
|
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
|
|
11
16
|
}
|
|
@@ -38,8 +43,7 @@ function admitted(registry, target, args, signal) {
|
|
|
38
43
|
args: args ?? {},
|
|
39
44
|
...defined({ signal }),
|
|
40
45
|
});
|
|
41
|
-
return Effect.tryPromise({ try: () => pending, catch: (error) => error })
|
|
42
|
-
.pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
46
|
+
return Effect.tryPromise({ try: () => pending, catch: (error) => error }).pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
43
47
|
pending.then((permit) => permit.release(), () => { });
|
|
44
48
|
})));
|
|
45
49
|
}), (permit) => Effect.sync(() => permit.release()), { interruptible: true });
|
|
@@ -54,23 +58,29 @@ function callerCancelledDetails() {
|
|
|
54
58
|
function recoveryMode(registry, connector, baseUrl) {
|
|
55
59
|
if (connector.startAuth)
|
|
56
60
|
return "oauth";
|
|
57
|
-
if (registry.credentialUiAvailable() &&
|
|
61
|
+
if (registry.credentialUiAvailable() &&
|
|
62
|
+
connector.credential &&
|
|
58
63
|
registry.contextFor(connector.id, baseUrl).credential) {
|
|
59
64
|
return "operator_config";
|
|
60
65
|
}
|
|
61
66
|
return "unavailable";
|
|
62
67
|
}
|
|
63
68
|
function isCallerCancellation(error, signal) {
|
|
64
|
-
return
|
|
65
|
-
(isCallAdmissionError(error) && error.admissionKind === "cancelled"));
|
|
69
|
+
return signal?.aborted === true || (isCallAdmissionError(error) && error.admissionKind === "cancelled");
|
|
66
70
|
}
|
|
67
71
|
/**
|
|
68
72
|
* A downstream MCP tool answered with `isError`. Classified exactly as the
|
|
69
|
-
* plain `Error
|
|
73
|
+
* plain `Error`: untyped, non-retryable, and marked so
|
|
70
74
|
* a write's outcome can tell "the service said no" from "nobody answered".
|
|
71
75
|
*/
|
|
72
76
|
class DownstreamToolError extends Error {
|
|
73
77
|
}
|
|
78
|
+
function isTimeoutFailure(error) {
|
|
79
|
+
const code = error.details?.code;
|
|
80
|
+
return (error.code === "timeout" ||
|
|
81
|
+
(error.code === "unavailable" &&
|
|
82
|
+
(code === "timeout" || code === "ETIMEDOUT" || code?.endsWith("_TIMEOUT") === true)));
|
|
83
|
+
}
|
|
74
84
|
function assertRawMcpSuccess(kind, result) {
|
|
75
85
|
if (kind !== "mcp" || result == null || typeof result !== "object")
|
|
76
86
|
return;
|
|
@@ -145,13 +155,18 @@ export class InvocationService {
|
|
|
145
155
|
pipeline(address, args, context) {
|
|
146
156
|
return Effect.gen({ self: this }, function* () {
|
|
147
157
|
const started = Date.now();
|
|
158
|
+
const argumentEcho = echoedCallArgs(args ?? {});
|
|
148
159
|
let catalogMs = 0;
|
|
149
160
|
let admissionMs = 0;
|
|
150
161
|
let connectorMs = 0;
|
|
151
162
|
let resultProcessingMs = 0;
|
|
152
163
|
let attempts = 0;
|
|
164
|
+
let resultBytes;
|
|
153
165
|
let dispatchedToConnector = false;
|
|
166
|
+
let preInvocationAuthFailure = false;
|
|
154
167
|
let answered = false;
|
|
168
|
+
const sentSecrets = sentSecretsForRequest(this.catalog.requestScope);
|
|
169
|
+
context.sentSecrets?.include(sentSecrets);
|
|
155
170
|
// A write gate's refusal that is no attempt; see WriteGateDecision.
|
|
156
171
|
let unrecorded = false;
|
|
157
172
|
let resolved;
|
|
@@ -171,20 +186,26 @@ export class InvocationService {
|
|
|
171
186
|
const identity = activityTarget
|
|
172
187
|
? {
|
|
173
188
|
connectorId: activityTarget.connector.id,
|
|
174
|
-
|
|
189
|
+
// A resolved tool's name is the downstream's choice, recorded
|
|
190
|
+
// only when it fits the tool-name grammar (INV-6).
|
|
191
|
+
toolName: resolved ? recordedToolName(resolved.definition) : activityTarget.toolName,
|
|
175
192
|
}
|
|
176
193
|
: attempted;
|
|
177
194
|
if (!identity)
|
|
178
195
|
return;
|
|
196
|
+
const toolName = hasControlCharacters(identity.toolName) ? "<unlisted>" : identity.toolName;
|
|
179
197
|
this.activity?.recordTool?.(this.activity, {
|
|
180
198
|
connectorId: identity.connectorId,
|
|
181
|
-
toolName
|
|
182
|
-
address: `${identity.connectorId}.${
|
|
199
|
+
toolName,
|
|
200
|
+
address: `${identity.connectorId}.${toolName}`,
|
|
183
201
|
source: context.source,
|
|
184
202
|
outcome,
|
|
203
|
+
...(activityTarget?.connector.authScope === "personal" ? { personal: true } : {}),
|
|
185
204
|
durationMs: Date.now() - started,
|
|
186
205
|
attempts,
|
|
187
206
|
...defined({
|
|
207
|
+
classification: resolved?.definition.classification,
|
|
208
|
+
resultBytes,
|
|
188
209
|
errorCode: classification.errorCode,
|
|
189
210
|
friction: classification.friction,
|
|
190
211
|
}),
|
|
@@ -193,7 +214,40 @@ export class InvocationService {
|
|
|
193
214
|
const enrich = (error, target) => {
|
|
194
215
|
if (!target)
|
|
195
216
|
return error;
|
|
217
|
+
if (isTimeoutFailure(error) && dispatchedToConnector && resolved?.definition.classification === "write") {
|
|
218
|
+
const echoed = argumentEcho;
|
|
219
|
+
return {
|
|
220
|
+
...error,
|
|
221
|
+
code: "write_outcome_unknown",
|
|
222
|
+
message: `${error.message} Its outcome is unknown. Check the target before repeating this call.`,
|
|
223
|
+
retryable: false,
|
|
224
|
+
connector: target.connector.id,
|
|
225
|
+
operation: `${target.connector.id}.${target.toolName}`,
|
|
226
|
+
uncertainCall: {
|
|
227
|
+
address: `${target.connector.id}.${target.toolName}`,
|
|
228
|
+
...echoed,
|
|
229
|
+
...("args" in echoed ? {} : { argsOmitted: true }),
|
|
230
|
+
},
|
|
231
|
+
retry: "Do not retry automatically. Check whether the write took effect first." +
|
|
232
|
+
("args" in echoed ? "" : " The arguments exceed the echo budget; use the exact arguments you sent."),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (error.code === "auth_required" && target.connector.startAuth) {
|
|
236
|
+
error = { ...error, code: "downstream_oauth_required" };
|
|
237
|
+
}
|
|
238
|
+
const enteredWrite = resolved?.definition.classification === "write" && dispatchedToConnector;
|
|
196
239
|
switch (error.code) {
|
|
240
|
+
case "input_required_unsupported":
|
|
241
|
+
if (context.processInputRequired)
|
|
242
|
+
return error;
|
|
243
|
+
return {
|
|
244
|
+
...error,
|
|
245
|
+
nextAction: {
|
|
246
|
+
tool: resolved?.definition.classification === "read" ? "call_tool" : "call_destructive_tool",
|
|
247
|
+
arguments: { address: `${target.connector.id}.${target.toolName}`, ...echoedCallArgs(args) },
|
|
248
|
+
purpose: "Use this direct call so the host can fulfill the downstream input request.",
|
|
249
|
+
},
|
|
250
|
+
};
|
|
197
251
|
case "destructive_tool_requires_approval": {
|
|
198
252
|
const echoed = echoedCallArgs(args);
|
|
199
253
|
return {
|
|
@@ -212,18 +266,28 @@ export class InvocationService {
|
|
|
212
266
|
};
|
|
213
267
|
}
|
|
214
268
|
case "auth_required":
|
|
269
|
+
case "downstream_oauth_required":
|
|
215
270
|
return {
|
|
216
271
|
...error,
|
|
217
272
|
connector: target.connector.id,
|
|
218
273
|
operation: `${target.connector.id}.${target.toolName}`,
|
|
219
274
|
recovery: recoveryMode(this.registry, target.connector, this.catalog.baseUrl),
|
|
275
|
+
...(enteredWrite ? { reconciliationRequired: true, retryable: false } : {}),
|
|
220
276
|
nextAction: {
|
|
221
277
|
tool: "authorize_connector",
|
|
222
278
|
arguments: { connector: target.connector.id },
|
|
223
279
|
operatorHandoff: "Give the URL and instructions it returns to the operator.",
|
|
224
280
|
},
|
|
225
|
-
retry:
|
|
226
|
-
"the operator completes recovery."
|
|
281
|
+
retry: enteredWrite
|
|
282
|
+
? "This write may have partially run. Reconcile its target before retrying after the operator completes recovery."
|
|
283
|
+
: `Retry ${target.connector.id}.${target.toolName} after the operator completes recovery.`,
|
|
284
|
+
};
|
|
285
|
+
case "provider_permission_denied":
|
|
286
|
+
return {
|
|
287
|
+
...error,
|
|
288
|
+
connector: target.connector.id,
|
|
289
|
+
operation: `${target.connector.id}.${target.toolName}`,
|
|
290
|
+
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.",
|
|
227
291
|
};
|
|
228
292
|
case "invalid_args":
|
|
229
293
|
if (!error.validation)
|
|
@@ -236,17 +300,32 @@ export class InvocationService {
|
|
|
236
300
|
query: target.toolName,
|
|
237
301
|
connector: target.connector.id,
|
|
238
302
|
}, "Inspect the current input shape if the validation findings are not sufficient."),
|
|
239
|
-
retry: `Correct the listed arguments and retry ` +
|
|
240
|
-
`${target.connector.id}.${target.toolName}.`,
|
|
303
|
+
retry: `Correct the listed arguments and retry ` + `${target.connector.id}.${target.toolName}.`,
|
|
241
304
|
};
|
|
242
305
|
default:
|
|
243
306
|
return error;
|
|
244
307
|
}
|
|
245
308
|
};
|
|
309
|
+
let stage = "catalog";
|
|
246
310
|
const failed = (error) => {
|
|
247
311
|
const diagnostics = timing();
|
|
248
312
|
const target = resolved ?? activityTarget;
|
|
249
|
-
|
|
313
|
+
if (target && (error.code === "auth_required" || error.code === "downstream_oauth_required")) {
|
|
314
|
+
recordAuthFailure(this.catalog.requestScope, target.connector.id, (resolved?.definition.classification === "read" && resolved.classificationFresh === true) ||
|
|
315
|
+
preInvocationAuthFailure ||
|
|
316
|
+
(!resolved && context.source === "call_tool"));
|
|
317
|
+
}
|
|
318
|
+
// Attach host deadline facts before write recovery changes the code.
|
|
319
|
+
if (isTimeoutFailure(error)) {
|
|
320
|
+
const elapsedMs = Date.now() - started;
|
|
321
|
+
const operation = boundedEchoText(address, 512);
|
|
322
|
+
error = {
|
|
323
|
+
...error,
|
|
324
|
+
message: `Operation "${operation}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${context.timeoutMs}ms).`,
|
|
325
|
+
details: { ...error.details, operation, stage, elapsedMs, ...defined({ deadlineMs: context.timeoutMs }) },
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const details = sentSecrets.redact(enrich(error, target));
|
|
250
329
|
const outcome = () => ({
|
|
251
330
|
ok: false,
|
|
252
331
|
durationMs: Date.now() - started,
|
|
@@ -261,47 +340,49 @@ export class InvocationService {
|
|
|
261
340
|
// leaves no event and nothing in the operator's log.
|
|
262
341
|
if (unrecorded)
|
|
263
342
|
return outcome();
|
|
264
|
-
// Activity rows
|
|
265
|
-
//
|
|
343
|
+
// Activity rows and this log line are both payload-free by
|
|
344
|
+
// construction: the line is the failure's typed record, never its
|
|
345
|
+
// message, which may be the downstream's own words (INV-6).
|
|
266
346
|
if (target && details.code !== "destructive_tool_requires_approval") {
|
|
267
|
-
this.registry
|
|
268
|
-
.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
|
|
269
|
-
.logger.warn("[connecta] call failed", {
|
|
347
|
+
logFailure(this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger, "call failed", failureRecord({
|
|
270
348
|
connector: target.connector.id,
|
|
271
|
-
|
|
349
|
+
// Named only when the catalog listed it (src/operator-record.ts).
|
|
350
|
+
tool: resolved?.definition,
|
|
272
351
|
source: context.source,
|
|
273
|
-
code: details.code,
|
|
274
|
-
// Sanitized transport diagnostics (origin and errno only, #539).
|
|
275
|
-
...(details.details ? { details: details.details } : {}),
|
|
276
352
|
attempts,
|
|
277
353
|
durationMs: Date.now() - started,
|
|
278
|
-
|
|
279
|
-
|
|
354
|
+
// Every refusal reaching `failed` is one connecta built: a
|
|
355
|
+
// thrown value's classification, a framing refusal, or a
|
|
356
|
+
// cancellation. Nothing else is read as a classification.
|
|
357
|
+
}, classifiedFailure(error)));
|
|
280
358
|
}
|
|
281
|
-
record(details.code === "timeout"
|
|
282
|
-
? "timeout"
|
|
283
|
-
: details.code === "cancelled"
|
|
284
|
-
? "cancelled"
|
|
285
|
-
: "error", { errorCode: details.code });
|
|
359
|
+
record(details.code === "timeout" ? "timeout" : details.code === "cancelled" ? "cancelled" : "error", defined({ errorCode: classificationCode(details.code) }));
|
|
286
360
|
return outcome();
|
|
287
361
|
};
|
|
288
|
-
if (context.requestSignal?.aborted) {
|
|
362
|
+
if (context.requestSignal?.aborted || context.dispatchSignal?.aborted) {
|
|
289
363
|
// Preserve the cancelled admission-attempt count without starting discovery.
|
|
290
364
|
attempts = 1;
|
|
291
365
|
return failed(callerCancelledDetails());
|
|
292
366
|
}
|
|
293
367
|
let result;
|
|
368
|
+
let inputRequiredValue;
|
|
294
369
|
let observedResult;
|
|
370
|
+
let valueFormat = "json";
|
|
295
371
|
// Everything up to the downstream result: resolution, the safety and
|
|
296
372
|
// schema refusals, admission, and the one attempt. A refusal is its
|
|
297
373
|
// success value. Its failure is an abort reason or something nobody
|
|
298
374
|
// expected, and a throw anywhere in it — a defect, to Effect — is
|
|
299
375
|
// classified like a failure, as the async body this replaced did.
|
|
300
376
|
const dispatch = (callSignal) => Effect.gen({ self: this }, function* () {
|
|
301
|
-
const
|
|
377
|
+
const admissionSignal = context.dispatchSignal
|
|
378
|
+
? AbortSignal.any([context.dispatchSignal, ...(callSignal ? [callSignal] : [])])
|
|
379
|
+
: callSignal;
|
|
380
|
+
const resolution = yield* this.catalog.resolve(address, defined({ signal: admissionSignal }));
|
|
302
381
|
catalogMs += resolution.catalogMs;
|
|
303
382
|
if (callSignal?.aborted)
|
|
304
383
|
return yield* Effect.fail(callSignal.reason);
|
|
384
|
+
if (admissionSignal?.aborted)
|
|
385
|
+
return callerCancelledDetails();
|
|
305
386
|
if (!resolution.ok) {
|
|
306
387
|
if (resolution.connector && resolution.toolName) {
|
|
307
388
|
activityTarget = {
|
|
@@ -317,30 +398,40 @@ export class InvocationService {
|
|
|
317
398
|
const target = resolution.resolved;
|
|
318
399
|
resolved = target;
|
|
319
400
|
activityTarget = target;
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
|
|
401
|
+
const write = target.definition.classification !== "read";
|
|
402
|
+
const canonicalAddress = `${target.connector.id}.${target.toolName}`;
|
|
403
|
+
const input = context.source !== "execute_code"
|
|
404
|
+
? downstreamContinuation(this.catalog.requestScope, target.connector.id, canonicalAddress)
|
|
323
405
|
: undefined;
|
|
324
|
-
|
|
406
|
+
const expectedDigest = replayClassificationDigest(this.catalog.requestScope, canonicalAddress);
|
|
407
|
+
if (expectedDigest !== undefined &&
|
|
408
|
+
(target.classificationFresh !== true ||
|
|
409
|
+
(yield* Effect.promise(() => classificationDigest(target.definition))) !== expectedDigest)) {
|
|
410
|
+
return {
|
|
411
|
+
...framingError("auth_replay_refused", "An entered read no longer has the same fresh classification. Reconcile its target before starting a new request."),
|
|
412
|
+
reconciliationRequired: true,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (!surfaceAllowsTool(target.definition.classification, context.source, context.trust)) {
|
|
325
416
|
const canonicalAddress = `${target.connector.id}.${target.toolName}`;
|
|
326
|
-
return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is
|
|
417
|
+
return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. Invoke it through call_destructive_tool so the MCP host can request approval.`);
|
|
327
418
|
}
|
|
328
419
|
// Remote MCP tools advertise their input schema in the catalog. Validate
|
|
329
420
|
// against that same request-local definition before admission or provider
|
|
330
421
|
// dispatch, so a predictable mismatch stays structured instead of being
|
|
331
422
|
// flattened into provider-specific error prose. Unsupported schemas retain
|
|
332
423
|
// validateToolInput's fail-open behavior and reach the downstream normally.
|
|
333
|
-
if (target.connector.kind === "mcp" &&
|
|
334
|
-
target.definition.inputSchema
|
|
335
|
-
const invalid = validateToolInput(target.definition.inputSchema, args ?? {}, {
|
|
424
|
+
if (target.connector.kind === "mcp" && target.definition.inputSchema) {
|
|
425
|
+
const invalid = validateCatalogToolInput(target.definition.inputSchema, args ?? {}, {
|
|
336
426
|
address: `${target.connector.id}.${target.toolName}`,
|
|
337
|
-
logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
|
|
338
|
-
|
|
427
|
+
logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
|
|
428
|
+
.logger,
|
|
429
|
+
}, { connector: target.connector.id, tool: target.definition });
|
|
339
430
|
if (invalid)
|
|
340
431
|
return classifyCallError(invalid);
|
|
341
432
|
}
|
|
342
|
-
if (
|
|
343
|
-
const decision = yield*
|
|
433
|
+
if (write && context.beforeWrite) {
|
|
434
|
+
const decision = yield* context.beforeWrite(target, args ?? {});
|
|
344
435
|
if (decision.kind === "refuse") {
|
|
345
436
|
unrecorded = decision.unrecorded === true;
|
|
346
437
|
return decision.error;
|
|
@@ -349,45 +440,102 @@ export class InvocationService {
|
|
|
349
440
|
if (callSignal?.aborted)
|
|
350
441
|
return yield* Effect.fail(callSignal.reason);
|
|
351
442
|
attempts = 1;
|
|
352
|
-
const call = () => {
|
|
353
|
-
const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs }));
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
443
|
+
const call = async () => {
|
|
444
|
+
const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs, defer: this.catalog.defer }));
|
|
445
|
+
trackCredentialReads(connectorContext);
|
|
446
|
+
sentSecrets.include(sentSecretsFor(connectorContext));
|
|
447
|
+
try {
|
|
448
|
+
if (target.connector.credential) {
|
|
449
|
+
if (!connectorContext.credential) {
|
|
450
|
+
throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
|
|
451
|
+
`authorize_connector({ connector: "${target.connector.id}" }).`);
|
|
452
|
+
}
|
|
453
|
+
if (!(await connectorContext.credential.getAll())) {
|
|
454
|
+
throw new ConnectorCallError("auth_required", `Connector "${target.connector.id}" has no stored credential. Call authorize_connector.`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
await resolveInvocationAuth(target.connector, connectorContext);
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
const code = classifyCallError(error).code;
|
|
461
|
+
preInvocationAuthFailure = code === "auth_required" || code === "downstream_oauth_required";
|
|
462
|
+
throw error;
|
|
358
463
|
}
|
|
359
464
|
// Cancellation can arrive during admission or context construction.
|
|
360
|
-
if (
|
|
361
|
-
throw
|
|
465
|
+
if (admissionSignal?.aborted)
|
|
466
|
+
throw admissionSignal.reason;
|
|
362
467
|
dispatchedToConnector = true;
|
|
363
|
-
|
|
468
|
+
recordCallEntry(this.catalog.requestScope, {
|
|
469
|
+
address: canonicalAddress,
|
|
470
|
+
classification: write ? "write" : "read",
|
|
471
|
+
fresh: target.classificationFresh === true,
|
|
472
|
+
}, target.definition);
|
|
473
|
+
return await target.connector.callTool(target.toolName, args ?? {}, connectorContext,
|
|
474
|
+
// The connector may retain or mutate its definition. Keep the
|
|
475
|
+
// invocation's classification and schema private, even during
|
|
476
|
+
// this call, and give every dispatch its own deep copy.
|
|
477
|
+
{ definition: structuredClone(target.definition), ...defined({ input }) });
|
|
364
478
|
};
|
|
365
479
|
// The permit belongs to this scope, so success, failure, and the
|
|
366
480
|
// deadline's interruption all release it. Interruption stops the
|
|
367
481
|
// wait instead of waiting for the connector, so an uncooperative one
|
|
368
482
|
// cannot hold its permit past the deadline.
|
|
369
483
|
const attempt = yield* Effect.exit(Effect.scoped(Effect.gen({ self: this }, function* () {
|
|
370
|
-
|
|
371
|
-
|
|
484
|
+
stage = "admission";
|
|
485
|
+
yield* timed((elapsed) => {
|
|
486
|
+
admissionMs += elapsed;
|
|
487
|
+
}, admitted(this.registry, target, args, admissionSignal));
|
|
488
|
+
stage = "downstream";
|
|
489
|
+
const reply = yield* timed((elapsed) => {
|
|
490
|
+
connectorMs += elapsed;
|
|
491
|
+
}, Effect.tryPromise({
|
|
372
492
|
try: () => Promise.resolve(call()),
|
|
373
493
|
catch: (error) => error,
|
|
374
494
|
}));
|
|
495
|
+
if (target.connector.kind === "mcp" && isDownstreamInputResult(reply)) {
|
|
496
|
+
if (!context.processInputRequired)
|
|
497
|
+
throw new ConnectorCallError("input_required_unsupported", "The downstream returned input_required. Use the equivalent direct MCP call to provide input.");
|
|
498
|
+
const value = yield* Effect.tryPromise({
|
|
499
|
+
try: () => Promise.resolve(context.processInputRequired(reply, target, sentSecrets)),
|
|
500
|
+
catch: (error) => error,
|
|
501
|
+
});
|
|
502
|
+
return { inputRequired: true, value };
|
|
503
|
+
}
|
|
504
|
+
assertDownstreamOutputSafe(this.catalog.requestScope, reply);
|
|
505
|
+
const raw = sentSecrets.redact(reply);
|
|
375
506
|
// isError is checked here for BOTH result shapes so every adapter
|
|
376
507
|
// reports the same downstream-failure wording, and the throw lands
|
|
377
508
|
// inside the attempt where it feeds health.
|
|
378
509
|
assertRawMcpSuccess(target.connector.kind, raw);
|
|
379
|
-
return { raw, observed:
|
|
510
|
+
return { raw, observed: sentSecrets.redact(downstreamValue(target.connector.kind, raw)) };
|
|
380
511
|
})));
|
|
381
512
|
if (Exit.isFailure(attempt)) {
|
|
382
513
|
if (callSignal?.aborted)
|
|
383
514
|
return yield* Effect.fail(callSignal.reason);
|
|
515
|
+
if (!dispatchedToConnector && admissionSignal?.aborted)
|
|
516
|
+
return callerCancelledDetails();
|
|
384
517
|
const attemptError = Cause.squash(attempt.cause);
|
|
385
518
|
answered = answeredFailure(attemptError);
|
|
386
519
|
return isCallerCancellation(attemptError, context.requestSignal)
|
|
387
520
|
? callerCancelledDetails()
|
|
388
|
-
: classifyCallError(attemptError);
|
|
521
|
+
: carryFailureFacts(attemptError, classifyCallError(sentSecrets.redact(attemptError)));
|
|
522
|
+
}
|
|
523
|
+
if ("inputRequired" in attempt.value) {
|
|
524
|
+
inputRequiredValue = { value: attempt.value.value };
|
|
525
|
+
return undefined;
|
|
389
526
|
}
|
|
390
|
-
observedResult = attempt.value.observed;
|
|
527
|
+
observedResult = attempt.value.observed.data;
|
|
528
|
+
try {
|
|
529
|
+
const serialized = attempt.value.observed.format === "text" && typeof observedResult === "string"
|
|
530
|
+
? observedResult
|
|
531
|
+
: JSON.stringify(observedResult);
|
|
532
|
+
if (serialized !== undefined)
|
|
533
|
+
resultBytes = new TextEncoder().encode(serialized).byteLength;
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
/* Unserializable values have no measurable byte count. */
|
|
537
|
+
}
|
|
538
|
+
valueFormat = attempt.value.observed.format;
|
|
391
539
|
result = context.unwrapResult ? observedResult : attempt.value.raw;
|
|
392
540
|
return undefined;
|
|
393
541
|
});
|
|
@@ -405,9 +553,11 @@ export class InvocationService {
|
|
|
405
553
|
})
|
|
406
554
|
: dispatch());
|
|
407
555
|
if (Exit.isFailure(dispatched)) {
|
|
408
|
-
|
|
556
|
+
const failure = Cause.squash(dispatched.cause);
|
|
557
|
+
const details = context.requestSignal?.aborted
|
|
409
558
|
? callerCancelledDetails()
|
|
410
|
-
: classifyCallError(
|
|
559
|
+
: carryFailureFacts(failure, classifyCallError(sentSecrets.redact(failure)));
|
|
560
|
+
return failed(details);
|
|
411
561
|
}
|
|
412
562
|
if (dispatched.value)
|
|
413
563
|
return failed(dispatched.value);
|
|
@@ -417,13 +567,19 @@ export class InvocationService {
|
|
|
417
567
|
return yield* Effect.die(new Error("Invocation completed without a resolved tool"));
|
|
418
568
|
}
|
|
419
569
|
const processResult = context.processResult;
|
|
420
|
-
const processing =
|
|
421
|
-
? Effect.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
570
|
+
const processing = inputRequiredValue
|
|
571
|
+
? Effect.succeed(inputRequiredValue.value)
|
|
572
|
+
: processResult
|
|
573
|
+
? Effect.tryPromise({
|
|
574
|
+
try: () => Promise.resolve(processResult(sentSecrets.redact(result), completed, sentSecrets, valueFormat)),
|
|
575
|
+
catch: (error) => error,
|
|
576
|
+
})
|
|
577
|
+
: Effect.succeed(result);
|
|
578
|
+
const processed = yield* Effect.exit(timed((elapsed) => {
|
|
579
|
+
resultProcessingMs += elapsed;
|
|
580
|
+
}, Effect.tap(processing, () => Effect.sync(() => {
|
|
581
|
+
if (inputRequiredValue)
|
|
582
|
+
return;
|
|
427
583
|
try {
|
|
428
584
|
this.registry.observeOutputShape(completed.connector.id, completed.definition, observedResult);
|
|
429
585
|
}
|
|
@@ -436,13 +592,14 @@ export class InvocationService {
|
|
|
436
592
|
if (Exit.isFailure(processed))
|
|
437
593
|
return unprocessable();
|
|
438
594
|
try {
|
|
439
|
-
const value = processed.value;
|
|
595
|
+
const value = sentSecrets.redact(processed.value);
|
|
440
596
|
const diagnostics = timing();
|
|
441
597
|
const friction = context.activityFriction?.(value);
|
|
442
598
|
record("success", friction ? { friction } : {});
|
|
443
599
|
return {
|
|
444
600
|
ok: true,
|
|
445
601
|
value,
|
|
602
|
+
format: valueFormat,
|
|
446
603
|
resolved: completed,
|
|
447
604
|
durationMs: Date.now() - started,
|
|
448
605
|
attempts,
|
package/dist/json-schema.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Validator } from "@cfworker/json-schema";
|
|
2
|
-
export type { OutputUnit, Schema, SchemaDraft, ValidationResult
|
|
2
|
+
export type { OutputUnit, Schema, SchemaDraft, ValidationResult } from "@cfworker/json-schema";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY } from "@modelcontextprotocol/server";
|
|
2
|
+
const RESERVED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
3
|
+
/** A null-prototype copy, so a declared key can never shadow an inherited one. */
|
|
4
|
+
function ownCapabilities(capabilities) {
|
|
5
|
+
const copy = Object.create(null);
|
|
6
|
+
for (const [key, value] of Object.entries(capabilities)) {
|
|
7
|
+
if (!RESERVED_KEYS.has(key))
|
|
8
|
+
copy[key] = value;
|
|
9
|
+
}
|
|
10
|
+
return copy;
|
|
11
|
+
}
|
|
12
|
+
/** Lift only the SDK-validated envelope, without retaining unrelated metadata. */
|
|
13
|
+
export function bindMcpClient(context, client, activity) {
|
|
14
|
+
const envelope = context.mcpReq.envelope;
|
|
15
|
+
const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
|
|
16
|
+
const info = envelope?.[CLIENT_INFO_META_KEY];
|
|
17
|
+
delete client.clientCapabilities;
|
|
18
|
+
delete client.clientInfo;
|
|
19
|
+
if (capabilities && typeof capabilities === "object" && !Array.isArray(capabilities)) {
|
|
20
|
+
client.clientCapabilities = ownCapabilities(capabilities);
|
|
21
|
+
}
|
|
22
|
+
if (info &&
|
|
23
|
+
typeof info === "object" &&
|
|
24
|
+
"name" in info &&
|
|
25
|
+
"version" in info &&
|
|
26
|
+
typeof info.name === "string" &&
|
|
27
|
+
typeof info.version === "string") {
|
|
28
|
+
client.clientInfo = { name: info.name, version: info.version };
|
|
29
|
+
}
|
|
30
|
+
if (activity) {
|
|
31
|
+
delete activity.clientInfo;
|
|
32
|
+
if (client.clientInfo)
|
|
33
|
+
activity.clientInfo = client.clientInfo;
|
|
34
|
+
}
|
|
35
|
+
}
|