@zackbart/connecta 0.28.1 → 0.30.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 +855 -5
- package/README.md +125 -116
- package/bin/connecta.mjs +121 -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/argument-redaction.js +230 -0
- 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 +288 -0
- package/dist/authorization-handoff.js +111 -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 +409 -225
- 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 +633 -0
- package/dist/config.js +508 -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 +87 -38
- 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 +128 -0
- package/dist/downstream-input.js +325 -0
- package/dist/errors.d.ts +132 -31
- package/dist/errors.js +175 -87
- package/dist/execute.js +798 -318
- 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 +30 -13
- package/dist/executors/quickjs-child.js +15 -9
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +152 -59
- package/dist/executors/quickjs.js +49 -39
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +238 -0
- package/dist/guest-types.d.ts +218 -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 +262 -79
- 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 +61 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +419 -272
- 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 +194 -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-result.js +5 -0
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +60 -19
- 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/_shared/request-token.js +83 -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/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -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 +110 -171
- package/dist/registry.js +588 -927
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +103 -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 +365 -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 +93 -0
- package/dist/sent-secrets.js +1013 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +416 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +181 -0
- package/dist/storage/keys.js +443 -0
- package/dist/storage/memory.js +16 -4
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +543 -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 +336 -56
- 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 +146 -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 +435 -0
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +696 -231
- package/documentation/auth.md +1069 -546
- package/documentation/code-mode.md +662 -356
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +570 -216
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +97 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +65 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +293 -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 +126 -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 +140 -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,17 @@
|
|
|
1
|
+
import { withAuthorizationHandoff } from "./authorization-handoff.js";
|
|
2
|
+
import { hasControlCharacters } from "./tool-name.js";
|
|
3
|
+
import { surfaceAllowsTool } from "./tool-safety.js";
|
|
1
4
|
import { Cause, Effect, Exit } from "effect";
|
|
2
|
-
import { isCallAdmissionError
|
|
3
|
-
import { boundedEchoText, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
|
|
4
|
-
import {
|
|
5
|
+
import { isCallAdmissionError } from "./call-admission.js";
|
|
6
|
+
import { boundedEchoText, classificationCode, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
|
|
7
|
+
import { downstreamValue } from "./mcp-result.js";
|
|
8
|
+
import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, } from "./operator-record.js";
|
|
5
9
|
import { splitAddress } from "./registry.js";
|
|
6
10
|
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
11
|
+
import { validateCatalogToolInput } from "./validate.js";
|
|
12
|
+
import { sentSecretsFor, sentSecretsForRequest, trackCredentialReads } from "./sent-secrets.js";
|
|
13
|
+
import { classificationDigest, recordAuthFailure, recordCallEntry, replayClassificationDigest, resolveInvocationAuth, } from "./invocation-auth.js";
|
|
14
|
+
import { downstreamContinuation, recordDownstreamArgumentEcho, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
|
|
9
15
|
function defined(values) {
|
|
10
16
|
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
|
|
11
17
|
}
|
|
@@ -38,8 +44,7 @@ function admitted(registry, target, args, signal) {
|
|
|
38
44
|
args: args ?? {},
|
|
39
45
|
...defined({ signal }),
|
|
40
46
|
});
|
|
41
|
-
return Effect.tryPromise({ try: () => pending, catch: (error) => error })
|
|
42
|
-
.pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
47
|
+
return Effect.tryPromise({ try: () => pending, catch: (error) => error }).pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
43
48
|
pending.then((permit) => permit.release(), () => { });
|
|
44
49
|
})));
|
|
45
50
|
}), (permit) => Effect.sync(() => permit.release()), { interruptible: true });
|
|
@@ -54,23 +59,29 @@ function callerCancelledDetails() {
|
|
|
54
59
|
function recoveryMode(registry, connector, baseUrl) {
|
|
55
60
|
if (connector.startAuth)
|
|
56
61
|
return "oauth";
|
|
57
|
-
if (registry.credentialUiAvailable() &&
|
|
62
|
+
if (registry.credentialUiAvailable() &&
|
|
63
|
+
connector.credential &&
|
|
58
64
|
registry.contextFor(connector.id, baseUrl).credential) {
|
|
59
65
|
return "operator_config";
|
|
60
66
|
}
|
|
61
67
|
return "unavailable";
|
|
62
68
|
}
|
|
63
69
|
function isCallerCancellation(error, signal) {
|
|
64
|
-
return
|
|
65
|
-
(isCallAdmissionError(error) && error.admissionKind === "cancelled"));
|
|
70
|
+
return signal?.aborted === true || (isCallAdmissionError(error) && error.admissionKind === "cancelled");
|
|
66
71
|
}
|
|
67
72
|
/**
|
|
68
73
|
* A downstream MCP tool answered with `isError`. Classified exactly as the
|
|
69
|
-
* plain `Error
|
|
74
|
+
* plain `Error`: untyped, non-retryable, and marked so
|
|
70
75
|
* a write's outcome can tell "the service said no" from "nobody answered".
|
|
71
76
|
*/
|
|
72
77
|
class DownstreamToolError extends Error {
|
|
73
78
|
}
|
|
79
|
+
function isTimeoutFailure(error) {
|
|
80
|
+
const code = error.details?.code;
|
|
81
|
+
return (error.code === "timeout" ||
|
|
82
|
+
(error.code === "unavailable" &&
|
|
83
|
+
(code === "timeout" || code === "ETIMEDOUT" || code?.endsWith("_TIMEOUT") === true)));
|
|
84
|
+
}
|
|
74
85
|
function assertRawMcpSuccess(kind, result) {
|
|
75
86
|
if (kind !== "mcp" || result == null || typeof result !== "object")
|
|
76
87
|
return;
|
|
@@ -101,6 +112,7 @@ export class InvocationFailure extends Error {
|
|
|
101
112
|
recovery;
|
|
102
113
|
nextAction;
|
|
103
114
|
retry;
|
|
115
|
+
data;
|
|
104
116
|
constructor(details) {
|
|
105
117
|
super(details.message);
|
|
106
118
|
this.details = details;
|
|
@@ -113,6 +125,7 @@ export class InvocationFailure extends Error {
|
|
|
113
125
|
this.recovery = details.recovery;
|
|
114
126
|
this.nextAction = details.nextAction;
|
|
115
127
|
this.retry = details.retry;
|
|
128
|
+
this.data = details;
|
|
116
129
|
}
|
|
117
130
|
}
|
|
118
131
|
/**
|
|
@@ -145,13 +158,18 @@ export class InvocationService {
|
|
|
145
158
|
pipeline(address, args, context) {
|
|
146
159
|
return Effect.gen({ self: this }, function* () {
|
|
147
160
|
const started = Date.now();
|
|
161
|
+
let argumentEcho = {};
|
|
148
162
|
let catalogMs = 0;
|
|
149
163
|
let admissionMs = 0;
|
|
150
164
|
let connectorMs = 0;
|
|
151
165
|
let resultProcessingMs = 0;
|
|
152
166
|
let attempts = 0;
|
|
167
|
+
let resultBytes;
|
|
153
168
|
let dispatchedToConnector = false;
|
|
169
|
+
let preInvocationAuthFailure = false;
|
|
154
170
|
let answered = false;
|
|
171
|
+
const sentSecrets = sentSecretsForRequest(this.catalog.requestScope);
|
|
172
|
+
context.sentSecrets?.include(sentSecrets);
|
|
155
173
|
// A write gate's refusal that is no attempt; see WriteGateDecision.
|
|
156
174
|
let unrecorded = false;
|
|
157
175
|
let resolved;
|
|
@@ -171,20 +189,26 @@ export class InvocationService {
|
|
|
171
189
|
const identity = activityTarget
|
|
172
190
|
? {
|
|
173
191
|
connectorId: activityTarget.connector.id,
|
|
174
|
-
|
|
192
|
+
// A resolved tool's name is the downstream's choice, recorded
|
|
193
|
+
// only when it fits the tool-name grammar (INV-6).
|
|
194
|
+
toolName: resolved ? recordedToolName(resolved.definition) : activityTarget.toolName,
|
|
175
195
|
}
|
|
176
196
|
: attempted;
|
|
177
197
|
if (!identity)
|
|
178
198
|
return;
|
|
199
|
+
const toolName = hasControlCharacters(identity.toolName) ? "<unlisted>" : identity.toolName;
|
|
179
200
|
this.activity?.recordTool?.(this.activity, {
|
|
180
201
|
connectorId: identity.connectorId,
|
|
181
|
-
toolName
|
|
182
|
-
address: `${identity.connectorId}.${
|
|
202
|
+
toolName,
|
|
203
|
+
address: `${identity.connectorId}.${toolName}`,
|
|
183
204
|
source: context.source,
|
|
184
205
|
outcome,
|
|
206
|
+
...(activityTarget?.connector.authScope === "personal" ? { personal: true } : {}),
|
|
185
207
|
durationMs: Date.now() - started,
|
|
186
208
|
attempts,
|
|
187
209
|
...defined({
|
|
210
|
+
classification: resolved?.definition.classification,
|
|
211
|
+
resultBytes,
|
|
188
212
|
errorCode: classification.errorCode,
|
|
189
213
|
friction: classification.friction,
|
|
190
214
|
}),
|
|
@@ -193,9 +217,51 @@ export class InvocationService {
|
|
|
193
217
|
const enrich = (error, target) => {
|
|
194
218
|
if (!target)
|
|
195
219
|
return error;
|
|
220
|
+
if (isTimeoutFailure(error) && dispatchedToConnector && resolved?.definition.classification === "write") {
|
|
221
|
+
const echoed = argumentEcho;
|
|
222
|
+
return {
|
|
223
|
+
...error,
|
|
224
|
+
code: "write_outcome_unknown",
|
|
225
|
+
message: `${error.message} Its outcome is unknown. Check the target before repeating this call.`,
|
|
226
|
+
retryable: false,
|
|
227
|
+
connector: target.connector.id,
|
|
228
|
+
operation: `${target.connector.id}.${target.toolName}`,
|
|
229
|
+
uncertainCall: {
|
|
230
|
+
address: `${target.connector.id}.${target.toolName}`,
|
|
231
|
+
...echoed,
|
|
232
|
+
...("args" in echoed ? {} : { argsOmitted: true }),
|
|
233
|
+
},
|
|
234
|
+
retry: "Do not retry automatically. Check whether the write took effect first." +
|
|
235
|
+
(echoed.argsRedacted
|
|
236
|
+
? " Sensitive fields are omitted; use the original arguments if reconciliation requires another call."
|
|
237
|
+
: "args" in echoed
|
|
238
|
+
? ""
|
|
239
|
+
: " The arguments could not be echoed safely; use the exact arguments you sent."),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (error.code === "auth_required" && target.connector.startAuth) {
|
|
243
|
+
error = { ...error, code: "downstream_oauth_required" };
|
|
244
|
+
}
|
|
245
|
+
const enteredWrite = resolved?.definition.classification === "write" && dispatchedToConnector;
|
|
196
246
|
switch (error.code) {
|
|
247
|
+
case "input_required_unsupported":
|
|
248
|
+
if (context.processInputRequired)
|
|
249
|
+
return error;
|
|
250
|
+
return {
|
|
251
|
+
...error,
|
|
252
|
+
nextAction: {
|
|
253
|
+
tool: resolved?.definition.classification === "read" ? "call_tool" : "call_destructive_tool",
|
|
254
|
+
arguments: { address: `${target.connector.id}.${target.toolName}`, ...argumentEcho },
|
|
255
|
+
purpose: "Use this direct call so the host can fulfill the downstream input request." +
|
|
256
|
+
(argumentEcho.argsRedacted
|
|
257
|
+
? " Re-send the original arguments; sensitive fields are omitted."
|
|
258
|
+
: "args" in argumentEcho
|
|
259
|
+
? ""
|
|
260
|
+
: " Re-send the original arguments; they could not be echoed safely."),
|
|
261
|
+
},
|
|
262
|
+
};
|
|
197
263
|
case "destructive_tool_requires_approval": {
|
|
198
|
-
const echoed =
|
|
264
|
+
const echoed = argumentEcho;
|
|
199
265
|
return {
|
|
200
266
|
...error,
|
|
201
267
|
nextAction: {
|
|
@@ -205,25 +271,37 @@ export class InvocationService {
|
|
|
205
271
|
...echoed,
|
|
206
272
|
},
|
|
207
273
|
purpose: "Ask the MCP host to approve this consequential call. " +
|
|
208
|
-
(
|
|
209
|
-
? "Re-send
|
|
210
|
-
: "
|
|
274
|
+
(echoed.argsRedacted
|
|
275
|
+
? "Re-send the original arguments; sensitive fields are omitted from this hint. Add a short reason for the human reviewer."
|
|
276
|
+
: "args" in echoed
|
|
277
|
+
? "Re-send these arguments and add a short reason for the human reviewer."
|
|
278
|
+
: "Re-send the arguments you just sent; they could not be echoed safely. Add a short reason for the human reviewer."),
|
|
211
279
|
},
|
|
212
280
|
};
|
|
213
281
|
}
|
|
214
282
|
case "auth_required":
|
|
283
|
+
case "downstream_oauth_required":
|
|
215
284
|
return {
|
|
216
285
|
...error,
|
|
217
286
|
connector: target.connector.id,
|
|
218
287
|
operation: `${target.connector.id}.${target.toolName}`,
|
|
219
288
|
recovery: recoveryMode(this.registry, target.connector, this.catalog.baseUrl),
|
|
289
|
+
...(enteredWrite ? { reconciliationRequired: true, retryable: false } : {}),
|
|
220
290
|
nextAction: {
|
|
221
291
|
tool: "authorize_connector",
|
|
222
292
|
arguments: { connector: target.connector.id },
|
|
223
293
|
operatorHandoff: "Give the URL and instructions it returns to the operator.",
|
|
224
294
|
},
|
|
225
|
-
retry:
|
|
226
|
-
"the operator completes recovery."
|
|
295
|
+
retry: enteredWrite
|
|
296
|
+
? "This write may have partially run. Reconcile its target before retrying after the operator completes recovery."
|
|
297
|
+
: `Retry ${target.connector.id}.${target.toolName} after the operator completes recovery.`,
|
|
298
|
+
};
|
|
299
|
+
case "provider_permission_denied":
|
|
300
|
+
return {
|
|
301
|
+
...error,
|
|
302
|
+
connector: target.connector.id,
|
|
303
|
+
operation: `${target.connector.id}.${target.toolName}`,
|
|
304
|
+
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
305
|
};
|
|
228
306
|
case "invalid_args":
|
|
229
307
|
if (!error.validation)
|
|
@@ -236,17 +314,33 @@ export class InvocationService {
|
|
|
236
314
|
query: target.toolName,
|
|
237
315
|
connector: target.connector.id,
|
|
238
316
|
}, "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}.`,
|
|
317
|
+
retry: `Correct the listed arguments and retry ` + `${target.connector.id}.${target.toolName}.`,
|
|
241
318
|
};
|
|
242
319
|
default:
|
|
243
320
|
return error;
|
|
244
321
|
}
|
|
245
322
|
};
|
|
323
|
+
let stage = "catalog";
|
|
246
324
|
const failed = (error) => {
|
|
247
325
|
const diagnostics = timing();
|
|
248
326
|
const target = resolved ?? activityTarget;
|
|
249
|
-
|
|
327
|
+
if (target && (error.code === "auth_required" || error.code === "downstream_oauth_required")) {
|
|
328
|
+
recordAuthFailure(this.catalog.requestScope, target.connector.id, (resolved?.definition.classification === "read" && resolved.classificationFresh === true) ||
|
|
329
|
+
preInvocationAuthFailure ||
|
|
330
|
+
(!resolved && context.source === "call_tool"));
|
|
331
|
+
}
|
|
332
|
+
// Attach host deadline facts before write recovery changes the code.
|
|
333
|
+
if (isTimeoutFailure(error)) {
|
|
334
|
+
const elapsedMs = Date.now() - started;
|
|
335
|
+
const operation = boundedEchoText(address, 512);
|
|
336
|
+
error = {
|
|
337
|
+
...error,
|
|
338
|
+
message: `Operation "${operation}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${context.timeoutMs}ms).`,
|
|
339
|
+
details: { ...error.details, operation, stage, elapsedMs, ...defined({ deadlineMs: context.timeoutMs }) },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
// Downstream diagnostics were redacted under payload rules where they entered.
|
|
343
|
+
const details = sentSecrets.redact(enrich(error, target), "envelope");
|
|
250
344
|
const outcome = () => ({
|
|
251
345
|
ok: false,
|
|
252
346
|
durationMs: Date.now() - started,
|
|
@@ -261,47 +355,50 @@ export class InvocationService {
|
|
|
261
355
|
// leaves no event and nothing in the operator's log.
|
|
262
356
|
if (unrecorded)
|
|
263
357
|
return outcome();
|
|
264
|
-
// Activity rows
|
|
265
|
-
//
|
|
358
|
+
// Activity rows and this log line are both payload-free by
|
|
359
|
+
// construction: the line is the failure's typed record, never its
|
|
360
|
+
// message, which may be the downstream's own words (INV-6).
|
|
266
361
|
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", {
|
|
362
|
+
logFailure(this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger, "call failed", failureRecord({
|
|
270
363
|
connector: target.connector.id,
|
|
271
|
-
|
|
364
|
+
// Named only when the catalog listed it (src/operator-record.ts).
|
|
365
|
+
tool: resolved?.definition,
|
|
272
366
|
source: context.source,
|
|
273
|
-
code: details.code,
|
|
274
|
-
// Sanitized transport diagnostics (origin and errno only, #539).
|
|
275
|
-
...(details.details ? { details: details.details } : {}),
|
|
276
367
|
attempts,
|
|
277
368
|
durationMs: Date.now() - started,
|
|
278
|
-
|
|
279
|
-
|
|
369
|
+
// Every refusal reaching `failed` is one connecta built: a
|
|
370
|
+
// thrown value's classification, a framing refusal, or a
|
|
371
|
+
// cancellation. Nothing else is read as a classification.
|
|
372
|
+
}, classifiedFailure(error)));
|
|
280
373
|
}
|
|
281
|
-
record(details.code === "timeout"
|
|
282
|
-
? "timeout"
|
|
283
|
-
: details.code === "cancelled"
|
|
284
|
-
? "cancelled"
|
|
285
|
-
: "error", { errorCode: details.code });
|
|
374
|
+
record(details.code === "timeout" ? "timeout" : details.code === "cancelled" ? "cancelled" : "error", defined({ errorCode: classificationCode(details.code) }));
|
|
286
375
|
return outcome();
|
|
287
376
|
};
|
|
288
|
-
if (context.requestSignal?.aborted) {
|
|
377
|
+
if (context.requestSignal?.aborted || context.dispatchSignal?.aborted) {
|
|
289
378
|
// Preserve the cancelled admission-attempt count without starting discovery.
|
|
290
379
|
attempts = 1;
|
|
291
380
|
return failed(callerCancelledDetails());
|
|
292
381
|
}
|
|
293
382
|
let result;
|
|
383
|
+
let rawResult;
|
|
384
|
+
let inputRequiredValue;
|
|
294
385
|
let observedResult;
|
|
386
|
+
let valueFormat = "json";
|
|
295
387
|
// Everything up to the downstream result: resolution, the safety and
|
|
296
388
|
// schema refusals, admission, and the one attempt. A refusal is its
|
|
297
389
|
// success value. Its failure is an abort reason or something nobody
|
|
298
390
|
// expected, and a throw anywhere in it — a defect, to Effect — is
|
|
299
391
|
// classified like a failure, as the async body this replaced did.
|
|
300
392
|
const dispatch = (callSignal) => Effect.gen({ self: this }, function* () {
|
|
301
|
-
const
|
|
393
|
+
const admissionSignal = context.dispatchSignal
|
|
394
|
+
? AbortSignal.any([context.dispatchSignal, ...(callSignal ? [callSignal] : [])])
|
|
395
|
+
: callSignal;
|
|
396
|
+
const resolution = yield* this.catalog.resolve(address, defined({ signal: admissionSignal }));
|
|
302
397
|
catalogMs += resolution.catalogMs;
|
|
303
398
|
if (callSignal?.aborted)
|
|
304
399
|
return yield* Effect.fail(callSignal.reason);
|
|
400
|
+
if (admissionSignal?.aborted)
|
|
401
|
+
return callerCancelledDetails();
|
|
305
402
|
if (!resolution.ok) {
|
|
306
403
|
if (resolution.connector && resolution.toolName) {
|
|
307
404
|
activityTarget = {
|
|
@@ -317,30 +414,44 @@ export class InvocationService {
|
|
|
317
414
|
const target = resolution.resolved;
|
|
318
415
|
resolved = target;
|
|
319
416
|
activityTarget = target;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
417
|
+
argumentEcho = echoedCallArgs(args ?? {}, target.definition.inputSchema);
|
|
418
|
+
sentSecrets.arguments(args ?? {}, target.definition.inputSchema);
|
|
419
|
+
const write = target.definition.classification !== "read";
|
|
420
|
+
const canonicalAddress = `${target.connector.id}.${target.toolName}`;
|
|
421
|
+
const input = context.source !== "execute_code"
|
|
422
|
+
? downstreamContinuation(this.catalog.requestScope, target.connector.id, canonicalAddress)
|
|
323
423
|
: undefined;
|
|
324
|
-
if (
|
|
424
|
+
if (input)
|
|
425
|
+
recordDownstreamArgumentEcho(this.catalog.requestScope, argumentEcho);
|
|
426
|
+
const expectedDigest = replayClassificationDigest(this.catalog.requestScope, canonicalAddress);
|
|
427
|
+
if (expectedDigest !== undefined &&
|
|
428
|
+
(target.classificationFresh !== true ||
|
|
429
|
+
(yield* Effect.promise(() => classificationDigest(target.definition))) !== expectedDigest)) {
|
|
430
|
+
return {
|
|
431
|
+
...framingError("auth_replay_refused", "An entered read no longer has the same fresh classification. Reconcile its target before starting a new request."),
|
|
432
|
+
reconciliationRequired: true,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (!surfaceAllowsTool(target.definition.classification, context.source, context.trust)) {
|
|
325
436
|
const canonicalAddress = `${target.connector.id}.${target.toolName}`;
|
|
326
|
-
return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is
|
|
437
|
+
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
438
|
}
|
|
328
439
|
// Remote MCP tools advertise their input schema in the catalog. Validate
|
|
329
440
|
// against that same request-local definition before admission or provider
|
|
330
441
|
// dispatch, so a predictable mismatch stays structured instead of being
|
|
331
442
|
// flattened into provider-specific error prose. Unsupported schemas retain
|
|
332
443
|
// 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 ?? {}, {
|
|
444
|
+
if (target.connector.kind === "mcp" && target.definition.inputSchema) {
|
|
445
|
+
const invalid = validateCatalogToolInput(target.definition.inputSchema, args ?? {}, {
|
|
336
446
|
address: `${target.connector.id}.${target.toolName}`,
|
|
337
|
-
logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
|
|
338
|
-
|
|
447
|
+
logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
|
|
448
|
+
.logger,
|
|
449
|
+
}, { connector: target.connector.id, tool: target.definition });
|
|
339
450
|
if (invalid)
|
|
340
451
|
return classifyCallError(invalid);
|
|
341
452
|
}
|
|
342
|
-
if (
|
|
343
|
-
const decision = yield*
|
|
453
|
+
if (write && context.beforeWrite) {
|
|
454
|
+
const decision = yield* context.beforeWrite(target, args ?? {});
|
|
344
455
|
if (decision.kind === "refuse") {
|
|
345
456
|
unrecorded = decision.unrecorded === true;
|
|
346
457
|
return decision.error;
|
|
@@ -349,46 +460,104 @@ export class InvocationService {
|
|
|
349
460
|
if (callSignal?.aborted)
|
|
350
461
|
return yield* Effect.fail(callSignal.reason);
|
|
351
462
|
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
|
-
|
|
463
|
+
const call = async () => {
|
|
464
|
+
const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs, defer: this.catalog.defer }));
|
|
465
|
+
trackCredentialReads(connectorContext);
|
|
466
|
+
sentSecrets.include(sentSecretsFor(connectorContext));
|
|
467
|
+
try {
|
|
468
|
+
if (target.connector.credential) {
|
|
469
|
+
if (!connectorContext.credential) {
|
|
470
|
+
throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
|
|
471
|
+
`authorize_connector({ connector: "${target.connector.id}" }).`);
|
|
472
|
+
}
|
|
473
|
+
if (!(await connectorContext.credential.getAll())) {
|
|
474
|
+
throw new ConnectorCallError("auth_required", `Connector "${target.connector.id}" has no stored credential. Call authorize_connector.`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
await resolveInvocationAuth(target.connector, connectorContext);
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
const code = classifyCallError(error).code;
|
|
481
|
+
preInvocationAuthFailure = code === "auth_required" || code === "downstream_oauth_required";
|
|
482
|
+
throw error;
|
|
358
483
|
}
|
|
359
484
|
// Cancellation can arrive during admission or context construction.
|
|
360
|
-
if (
|
|
361
|
-
throw
|
|
485
|
+
if (admissionSignal?.aborted)
|
|
486
|
+
throw admissionSignal.reason;
|
|
362
487
|
dispatchedToConnector = true;
|
|
363
|
-
|
|
488
|
+
recordCallEntry(this.catalog.requestScope, {
|
|
489
|
+
address: canonicalAddress,
|
|
490
|
+
classification: write ? "write" : "read",
|
|
491
|
+
fresh: target.classificationFresh === true,
|
|
492
|
+
}, target.definition);
|
|
493
|
+
return await target.connector.callTool(target.toolName, args ?? {}, connectorContext,
|
|
494
|
+
// The connector may retain or mutate its definition. Keep the
|
|
495
|
+
// invocation's classification and schema private, even during
|
|
496
|
+
// this call, and give every dispatch its own deep copy.
|
|
497
|
+
{ definition: structuredClone(target.definition), ...defined({ input }) });
|
|
364
498
|
};
|
|
365
499
|
// The permit belongs to this scope, so success, failure, and the
|
|
366
500
|
// deadline's interruption all release it. Interruption stops the
|
|
367
501
|
// wait instead of waiting for the connector, so an uncooperative one
|
|
368
502
|
// cannot hold its permit past the deadline.
|
|
369
503
|
const attempt = yield* Effect.exit(Effect.scoped(Effect.gen({ self: this }, function* () {
|
|
370
|
-
|
|
371
|
-
|
|
504
|
+
stage = "admission";
|
|
505
|
+
yield* timed((elapsed) => {
|
|
506
|
+
admissionMs += elapsed;
|
|
507
|
+
}, admitted(this.registry, target, args, admissionSignal));
|
|
508
|
+
stage = "downstream";
|
|
509
|
+
const reply = yield* timed((elapsed) => {
|
|
510
|
+
connectorMs += elapsed;
|
|
511
|
+
}, Effect.tryPromise({
|
|
372
512
|
try: () => Promise.resolve(call()),
|
|
373
513
|
catch: (error) => error,
|
|
374
514
|
}));
|
|
515
|
+
if (target.connector.kind === "mcp" && isDownstreamInputResult(reply)) {
|
|
516
|
+
if (!context.processInputRequired)
|
|
517
|
+
throw new ConnectorCallError("input_required_unsupported", "The downstream returned input_required. Use the equivalent direct MCP call to provide input.");
|
|
518
|
+
const value = yield* Effect.tryPromise({
|
|
519
|
+
try: () => Promise.resolve(context.processInputRequired(reply, target, sentSecrets)),
|
|
520
|
+
catch: (error) => error,
|
|
521
|
+
});
|
|
522
|
+
return { inputRequired: true, value };
|
|
523
|
+
}
|
|
524
|
+
assertDownstreamOutputSafe(this.catalog.requestScope, reply);
|
|
525
|
+
const raw = sentSecrets.redact(reply);
|
|
375
526
|
// isError is checked here for BOTH result shapes so every adapter
|
|
376
527
|
// reports the same downstream-failure wording, and the throw lands
|
|
377
528
|
// inside the attempt where it feeds health.
|
|
378
529
|
assertRawMcpSuccess(target.connector.kind, raw);
|
|
379
|
-
return { raw, observed:
|
|
530
|
+
return { raw, observed: sentSecrets.redact(downstreamValue(target.connector.kind, raw)) };
|
|
380
531
|
})));
|
|
381
532
|
if (Exit.isFailure(attempt)) {
|
|
382
533
|
if (callSignal?.aborted)
|
|
383
534
|
return yield* Effect.fail(callSignal.reason);
|
|
535
|
+
if (!dispatchedToConnector && admissionSignal?.aborted)
|
|
536
|
+
return callerCancelledDetails();
|
|
384
537
|
const attemptError = Cause.squash(attempt.cause);
|
|
385
538
|
answered = answeredFailure(attemptError);
|
|
386
539
|
return isCallerCancellation(attemptError, context.requestSignal)
|
|
387
540
|
? callerCancelledDetails()
|
|
388
|
-
: classifyCallError(attemptError);
|
|
541
|
+
: carryFailureFacts(attemptError, classifyCallError(sentSecrets.redact(attemptError)));
|
|
542
|
+
}
|
|
543
|
+
if ("inputRequired" in attempt.value) {
|
|
544
|
+
inputRequiredValue = { value: attempt.value.value };
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
observedResult = attempt.value.observed.data;
|
|
548
|
+
try {
|
|
549
|
+
const serialized = attempt.value.observed.format === "text" && typeof observedResult === "string"
|
|
550
|
+
? observedResult
|
|
551
|
+
: JSON.stringify(observedResult);
|
|
552
|
+
if (serialized !== undefined)
|
|
553
|
+
resultBytes = new TextEncoder().encode(serialized).byteLength;
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
/* Unserializable values have no measurable byte count. */
|
|
389
557
|
}
|
|
390
|
-
|
|
391
|
-
|
|
558
|
+
valueFormat = attempt.value.observed.format;
|
|
559
|
+
rawResult = attempt.value.raw;
|
|
560
|
+
result = context.unwrapResult ? observedResult : rawResult;
|
|
392
561
|
return undefined;
|
|
393
562
|
});
|
|
394
563
|
// One deadline owns discovery, queue admission, and provider dispatch.
|
|
@@ -405,9 +574,11 @@ export class InvocationService {
|
|
|
405
574
|
})
|
|
406
575
|
: dispatch());
|
|
407
576
|
if (Exit.isFailure(dispatched)) {
|
|
408
|
-
|
|
577
|
+
const failure = Cause.squash(dispatched.cause);
|
|
578
|
+
const details = context.requestSignal?.aborted
|
|
409
579
|
? callerCancelledDetails()
|
|
410
|
-
: classifyCallError(
|
|
580
|
+
: carryFailureFacts(failure, classifyCallError(sentSecrets.redact(failure)));
|
|
581
|
+
return failed(details);
|
|
411
582
|
}
|
|
412
583
|
if (dispatched.value)
|
|
413
584
|
return failed(dispatched.value);
|
|
@@ -417,13 +588,19 @@ export class InvocationService {
|
|
|
417
588
|
return yield* Effect.die(new Error("Invocation completed without a resolved tool"));
|
|
418
589
|
}
|
|
419
590
|
const processResult = context.processResult;
|
|
420
|
-
const processing =
|
|
421
|
-
? Effect.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
591
|
+
const processing = inputRequiredValue
|
|
592
|
+
? Effect.succeed(inputRequiredValue.value)
|
|
593
|
+
: processResult
|
|
594
|
+
? Effect.tryPromise({
|
|
595
|
+
try: () => Promise.resolve(processResult(sentSecrets.redact(result), completed, sentSecrets, valueFormat, rawResult)),
|
|
596
|
+
catch: (error) => error,
|
|
597
|
+
})
|
|
598
|
+
: Effect.succeed(result);
|
|
599
|
+
const processed = yield* Effect.exit(timed((elapsed) => {
|
|
600
|
+
resultProcessingMs += elapsed;
|
|
601
|
+
}, Effect.tap(processing, () => Effect.sync(() => {
|
|
602
|
+
if (inputRequiredValue)
|
|
603
|
+
return;
|
|
427
604
|
try {
|
|
428
605
|
this.registry.observeOutputShape(completed.connector.id, completed.definition, observedResult);
|
|
429
606
|
}
|
|
@@ -436,13 +613,14 @@ export class InvocationService {
|
|
|
436
613
|
if (Exit.isFailure(processed))
|
|
437
614
|
return unprocessable();
|
|
438
615
|
try {
|
|
439
|
-
const value = processed.value;
|
|
616
|
+
const value = sentSecrets.redact(processed.value, "envelope");
|
|
440
617
|
const diagnostics = timing();
|
|
441
618
|
const friction = context.activityFriction?.(value);
|
|
442
619
|
record("success", friction ? { friction } : {});
|
|
443
620
|
return {
|
|
444
621
|
ok: true,
|
|
445
622
|
value,
|
|
623
|
+
format: valueFormat,
|
|
446
624
|
resolved: completed,
|
|
447
625
|
durationMs: Date.now() - started,
|
|
448
626
|
attempts,
|
|
@@ -453,6 +631,11 @@ export class InvocationService {
|
|
|
453
631
|
catch {
|
|
454
632
|
return unprocessable();
|
|
455
633
|
}
|
|
456
|
-
})
|
|
634
|
+
}).pipe(Effect.flatMap((outcome) => outcome.ok
|
|
635
|
+
? Effect.succeed(outcome)
|
|
636
|
+
: Effect.promise(async () => ({
|
|
637
|
+
...outcome,
|
|
638
|
+
error: await withAuthorizationHandoff(this.catalog.requestScope, outcome.error),
|
|
639
|
+
}))));
|
|
457
640
|
}
|
|
458
641
|
}
|