@zackbart/connecta 0.24.3 → 0.25.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/AGENTS.md +58 -30
- package/CHANGELOG.md +317 -1
- package/README.md +5 -6
- package/dist/advertised-schema.js +53 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +141 -105
- package/dist/catalog.js +258 -33
- package/dist/concurrency.js +24 -23
- package/dist/connector-scope.js +6 -37
- package/dist/connectors/api.d.ts +3 -3
- package/dist/connectors/api.js +10 -2
- package/dist/connectors/guarded-fetch.js +34 -4
- package/dist/connectors/remote-mcp.d.ts +6 -5
- package/dist/connectors/remote-mcp.js +325 -245
- package/dist/credential-contract.d.ts +9 -0
- package/dist/credential-rules.js +2 -2
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +45 -3
- package/dist/errors.d.ts +11 -3
- package/dist/errors.js +2 -1
- package/dist/execute.js +363 -257
- package/dist/executor-admission.js +67 -50
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +251 -210
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +63 -21
- package/dist/invocation.js +324 -285
- package/dist/meta-tools.js +304 -114
- package/dist/oauth-callback-outcome.js +94 -0
- package/dist/oauth-sealing.js +29 -0
- package/dist/operator-ui/fix-prompts.js +87 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +46 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +109 -2
- package/dist/program-source.js +205 -0
- package/dist/providers/cloudflare.d.ts +14 -2
- package/dist/providers/cloudflare.js +107 -16
- package/dist/providers/linear.d.ts +26 -4
- package/dist/providers/linear.js +19 -4
- package/dist/providers/mixpanel.d.ts +16 -3
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/notion.d.ts +8 -1
- package/dist/providers/notion.js +85 -12
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +70 -5
- package/dist/providers/stripe.d.ts +10 -3
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +12 -2
- package/dist/registry.d.ts +13 -5
- package/dist/registry.js +317 -258
- package/dist/routes/activity.js +120 -137
- package/dist/routes/credentials.js +174 -163
- package/dist/routes/mcp.js +129 -70
- package/dist/routes/oauth-management.js +60 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +82 -0
- package/dist/routes/shared.d.ts +2 -0
- package/dist/routes/ui-data.js +210 -0
- package/dist/routes/ui.js +64 -32
- package/dist/runtime/admission.js +43 -0
- package/dist/runtime/call-admission.js +67 -0
- package/dist/runtime/connector-scope.js +72 -0
- package/dist/runtime/run.js +174 -0
- package/dist/runtime/services.js +130 -0
- package/dist/runtime/storage.js +66 -0
- package/dist/server.js +111 -89
- package/dist/skills.js +1 -1
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +17 -47
- package/dist/types.d.ts +64 -7
- package/dist/ui.d.ts +16 -2
- package/dist/ui.js +68 -176
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +368 -182
- package/documentation/auth.md +277 -176
- package/documentation/code-mode.md +479 -329
- package/documentation/meta-tools.md +494 -417
- package/ethos.md +44 -47
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +62 -14
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +18 -21
- package/examples/worker/wrangler.jsonc +16 -4
- package/package.json +11 -4
- package/templates/node/.env.example +3 -3
- package/templates/node/AGENTS.md +5 -4
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +23 -22
- package/dist/auth/downstream-oauth.d.ts +0 -200
- package/dist/catalog-fingerprint.d.ts +0 -19
- package/dist/catalog-limits.d.ts +0 -9
- package/dist/catalog-service.d.ts +0 -263
- package/dist/catalog.d.ts +0 -87
- package/dist/concurrency.d.ts +0 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -92
- package/dist/credential-rules.d.ts +0 -85
- package/dist/execute.d.ts +0 -154
- package/dist/executor-result.d.ts +0 -10
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -67
- package/dist/favicon.d.ts +0 -1
- package/dist/identity.d.ts +0 -4
- package/dist/invocation.d.ts +0 -74
- package/dist/mcp-result.d.ts +0 -8
- package/dist/meta-tools.d.ts +0 -129
- package/dist/operator-ui/generated.d.ts +0 -2
- package/dist/operator-ui/view.d.ts +0 -151
- package/dist/result-shapes.d.ts +0 -13
- package/dist/retired-toolkits.d.ts +0 -2
- package/dist/routes/activity.d.ts +0 -2
- package/dist/routes/credentials.d.ts +0 -2
- package/dist/routes/mcp.d.ts +0 -10
- package/dist/routes/oauth-management.d.ts +0 -2
- package/dist/routes/oauth.d.ts +0 -2
- package/dist/routes/ui.d.ts +0 -2
- package/dist/server.d.ts +0 -9
- package/dist/skills.d.ts +0 -44
- package/dist/timeout.d.ts +0 -21
- package/dist/tool-safety.d.ts +0 -9
- package/documentation/call-admission.md +0 -158
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -431
- package/documentation/linear.md +0 -193
- package/documentation/mixpanel.md +0 -160
- package/documentation/notion.md +0 -308
- package/documentation/operations.md +0 -359
- package/documentation/operator-ui.md +0 -135
- package/documentation/optional-modules-upgrade.md +0 -243
- package/documentation/provider-conventions.md +0 -729
- package/documentation/request-admission.md +0 -204
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -254
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -768
- package/documentation/vercel.md +0 -241
package/dist/invocation.js
CHANGED
|
@@ -1,21 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Cause, Effect, Exit } from "effect";
|
|
2
|
+
import { isCallAdmissionError, } from "./call-admission.js";
|
|
2
3
|
import { boundedEchoText, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
|
|
3
4
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
4
5
|
import { splitAddress } from "./registry.js";
|
|
6
|
+
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
5
7
|
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
6
|
-
import { withDeadline } from "./timeout.js";
|
|
7
8
|
import { validateToolInput } from "./validate.js";
|
|
8
9
|
function defined(values) {
|
|
9
10
|
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
/** Add the effect's wall time to `bucket` however it ends, interruption included. */
|
|
13
|
+
export function timed(bucket, effect) {
|
|
14
|
+
return Effect.suspend(() => {
|
|
15
|
+
const started = Date.now();
|
|
16
|
+
return Effect.ensuring(effect, Effect.sync(() => bucket(Date.now() - started)));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The connector's call permit, owned by the enclosing Scope.
|
|
21
|
+
*
|
|
22
|
+
* Admission goes through `registry.admitCall`, a Promise, on purpose. The
|
|
23
|
+
* permit queue is shared by every request in the isolate, and a queued
|
|
24
|
+
* waiter is resumed from inside the request that released the slot. Were
|
|
25
|
+
* this fiber to wait on the controller's Effect program itself
|
|
26
|
+
* (`acquireCallScoped`), it would be resumed there and run this call's
|
|
27
|
+
* downstream I/O in the releasing request, which workerd refuses. Awaiting
|
|
28
|
+
* admitCall's promise resumes it in its own request instead.
|
|
29
|
+
*
|
|
30
|
+
* The wait stays interruptible, so a deadline ends it. A permit granted just
|
|
31
|
+
* as the wait was interrupted still arrives, later, and is released on
|
|
32
|
+
* arrival rather than holding its slot forever.
|
|
33
|
+
*/
|
|
34
|
+
function admitted(registry, target, args, signal) {
|
|
35
|
+
return Effect.acquireRelease(Effect.suspend(() => {
|
|
36
|
+
const pending = registry.admitCall(target.connector.id, {
|
|
37
|
+
toolName: target.toolName,
|
|
38
|
+
args: args ?? {},
|
|
39
|
+
...defined({ signal }),
|
|
40
|
+
});
|
|
41
|
+
return Effect.tryPromise({ try: () => pending, catch: (error) => error })
|
|
42
|
+
.pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
43
|
+
pending.then((permit) => permit.release(), () => { });
|
|
44
|
+
})));
|
|
45
|
+
}), (permit) => Effect.sync(() => permit.release()), { interruptible: true });
|
|
19
46
|
}
|
|
20
47
|
function callerCancelledDetails() {
|
|
21
48
|
return {
|
|
@@ -76,6 +103,11 @@ export class InvocationFailure extends Error {
|
|
|
76
103
|
* Shared downstream invocation engine. Address/catalog resolution is delegated
|
|
77
104
|
* to the request-local CatalogService; every remaining call semantic lives
|
|
78
105
|
* here so MCP and code mode cannot drift independently.
|
|
106
|
+
*
|
|
107
|
+
* One call is one fiber: resolve, refuse or validate, admit, dispatch, then
|
|
108
|
+
* process the result, with every way the call can go wrong turned into an
|
|
109
|
+
* outcome. Only a throw while recording that outcome, or a bug, rejects
|
|
110
|
+
* `invoke`.
|
|
79
111
|
*/
|
|
80
112
|
export class InvocationService {
|
|
81
113
|
registry;
|
|
@@ -86,301 +118,308 @@ export class InvocationService {
|
|
|
86
118
|
this.catalog = catalog;
|
|
87
119
|
this.activity = activity;
|
|
88
120
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return;
|
|
118
|
-
this.activity?.recordTool?.(this.activity, {
|
|
119
|
-
connectorId: identity.connectorId,
|
|
120
|
-
toolName: identity.toolName,
|
|
121
|
-
address: `${identity.connectorId}.${identity.toolName}`,
|
|
122
|
-
source: context.source,
|
|
123
|
-
outcome,
|
|
124
|
-
durationMs: Date.now() - started,
|
|
125
|
-
attempts,
|
|
126
|
-
...defined({
|
|
127
|
-
errorCode: classification.errorCode,
|
|
128
|
-
friction: classification.friction,
|
|
129
|
-
}),
|
|
121
|
+
invoke(address, args, context) {
|
|
122
|
+
return runEdge(this.pipeline(address, args, context));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `invoke` without the Promise, for a caller already running a fiber —
|
|
126
|
+
* code mode's host calls yield it rather than crossing a second edge. It
|
|
127
|
+
* never fails: every outcome, refusals included, is its success value.
|
|
128
|
+
*/
|
|
129
|
+
pipeline(address, args, context) {
|
|
130
|
+
return Effect.gen({ self: this }, function* () {
|
|
131
|
+
const started = Date.now();
|
|
132
|
+
let catalogMs = 0;
|
|
133
|
+
let admissionMs = 0;
|
|
134
|
+
let connectorMs = 0;
|
|
135
|
+
let resultProcessingMs = 0;
|
|
136
|
+
let attempts = 0;
|
|
137
|
+
let resolved;
|
|
138
|
+
let activityTarget;
|
|
139
|
+
// The address as written, used for activity when resolution never reached
|
|
140
|
+
// a connector. Only its two halves are recorded — the same fields activity
|
|
141
|
+
// has always carried — so no new class of payload enters the log.
|
|
142
|
+
const attempted = splitAddress(address);
|
|
143
|
+
const timing = () => ({
|
|
144
|
+
catalogMs,
|
|
145
|
+
admissionMs,
|
|
146
|
+
connectorMs,
|
|
147
|
+
resultProcessingMs,
|
|
148
|
+
totalMs: Date.now() - started,
|
|
130
149
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
...echoed,
|
|
145
|
-
},
|
|
146
|
-
purpose: "Ask the MCP host to approve this consequential call. " +
|
|
147
|
-
("args" in echoed
|
|
148
|
-
? "Re-send these arguments and add a short reason for the human reviewer."
|
|
149
|
-
: "Re-send the arguments you just sent — they are too large to echo back — and add a short reason for the human reviewer."),
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
case "auth_required":
|
|
154
|
-
return {
|
|
155
|
-
...error,
|
|
156
|
-
connector: target.connector.id,
|
|
157
|
-
operation: `${target.connector.id}.${target.toolName}`,
|
|
158
|
-
recovery: recoveryMode(this.registry, target.connector, this.catalog.baseUrl),
|
|
159
|
-
nextAction: {
|
|
160
|
-
tool: "authorize_connector",
|
|
161
|
-
arguments: { connector: target.connector.id },
|
|
162
|
-
operatorHandoff: "Give the URL and instructions it returns to the operator.",
|
|
163
|
-
},
|
|
164
|
-
retry: `Retry ${target.connector.id}.${target.toolName} after ` +
|
|
165
|
-
"the operator completes recovery.",
|
|
166
|
-
};
|
|
167
|
-
case "invalid_args":
|
|
168
|
-
if (!error.validation)
|
|
169
|
-
return error;
|
|
170
|
-
return {
|
|
171
|
-
...error,
|
|
172
|
-
connector: target.connector.id,
|
|
173
|
-
operation: `${target.connector.id}.${target.toolName}`,
|
|
174
|
-
nextAction: this.catalog.searchRecovery({
|
|
175
|
-
query: target.toolName,
|
|
176
|
-
connector: target.connector.id,
|
|
177
|
-
}, "Inspect the current input shape if the validation findings are not sufficient."),
|
|
178
|
-
retry: `Correct the listed arguments and retry ` +
|
|
179
|
-
`${target.connector.id}.${target.toolName}.`,
|
|
180
|
-
};
|
|
181
|
-
default:
|
|
182
|
-
return error;
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
const failed = (error) => {
|
|
186
|
-
const diagnostics = timing();
|
|
187
|
-
const target = resolved ?? activityTarget;
|
|
188
|
-
const details = enrich(error, target);
|
|
189
|
-
// Activity rows stay payload-free by construction; the operator's log is
|
|
190
|
-
// where the downstream reason goes, bounded and without arguments.
|
|
191
|
-
if (target && details.code !== "destructive_tool_requires_approval") {
|
|
192
|
-
this.registry
|
|
193
|
-
.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
|
|
194
|
-
.logger.warn("[connecta] call failed", {
|
|
195
|
-
connector: target.connector.id,
|
|
196
|
-
tool: target.toolName,
|
|
150
|
+
const record = (outcome, classification = {}) => {
|
|
151
|
+
const identity = activityTarget
|
|
152
|
+
? {
|
|
153
|
+
connectorId: activityTarget.connector.id,
|
|
154
|
+
toolName: activityTarget.toolName,
|
|
155
|
+
}
|
|
156
|
+
: attempted;
|
|
157
|
+
if (!identity)
|
|
158
|
+
return;
|
|
159
|
+
this.activity?.recordTool?.(this.activity, {
|
|
160
|
+
connectorId: identity.connectorId,
|
|
161
|
+
toolName: identity.toolName,
|
|
162
|
+
address: `${identity.connectorId}.${identity.toolName}`,
|
|
197
163
|
source: context.source,
|
|
198
|
-
|
|
199
|
-
// Sanitized transport diagnostics (origin and errno only, #539).
|
|
200
|
-
...(details.details ? { details: details.details } : {}),
|
|
201
|
-
attempts,
|
|
164
|
+
outcome,
|
|
202
165
|
durationMs: Date.now() - started,
|
|
203
|
-
|
|
166
|
+
attempts,
|
|
167
|
+
...defined({
|
|
168
|
+
errorCode: classification.errorCode,
|
|
169
|
+
friction: classification.friction,
|
|
170
|
+
}),
|
|
204
171
|
});
|
|
205
|
-
}
|
|
206
|
-
record(details.code === "timeout"
|
|
207
|
-
? "timeout"
|
|
208
|
-
: details.code === "cancelled"
|
|
209
|
-
? "cancelled"
|
|
210
|
-
: "error", { errorCode: details.code });
|
|
211
|
-
return {
|
|
212
|
-
ok: false,
|
|
213
|
-
durationMs: Date.now() - started,
|
|
214
|
-
attempts,
|
|
215
|
-
timing: diagnostics,
|
|
216
|
-
...defined({ resolved }),
|
|
217
|
-
error: details,
|
|
218
172
|
};
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
173
|
+
const enrich = (error, target) => {
|
|
174
|
+
if (!target)
|
|
175
|
+
return error;
|
|
176
|
+
switch (error.code) {
|
|
177
|
+
case "destructive_tool_requires_approval": {
|
|
178
|
+
const echoed = echoedCallArgs(args);
|
|
179
|
+
return {
|
|
180
|
+
...error,
|
|
181
|
+
nextAction: {
|
|
182
|
+
tool: "call_destructive_tool",
|
|
183
|
+
arguments: {
|
|
184
|
+
address: `${target.connector.id}.${target.toolName}`,
|
|
185
|
+
...echoed,
|
|
186
|
+
},
|
|
187
|
+
purpose: "Ask the MCP host to approve this consequential call. " +
|
|
188
|
+
("args" in echoed
|
|
189
|
+
? "Re-send these arguments and add a short reason for the human reviewer."
|
|
190
|
+
: "Re-send the arguments you just sent — they are too large to echo back — and add a short reason for the human reviewer."),
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
case "auth_required":
|
|
195
|
+
return {
|
|
196
|
+
...error,
|
|
197
|
+
connector: target.connector.id,
|
|
198
|
+
operation: `${target.connector.id}.${target.toolName}`,
|
|
199
|
+
recovery: recoveryMode(this.registry, target.connector, this.catalog.baseUrl),
|
|
200
|
+
nextAction: {
|
|
201
|
+
tool: "authorize_connector",
|
|
202
|
+
arguments: { connector: target.connector.id },
|
|
203
|
+
operatorHandoff: "Give the URL and instructions it returns to the operator.",
|
|
204
|
+
},
|
|
205
|
+
retry: `Retry ${target.connector.id}.${target.toolName} after ` +
|
|
206
|
+
"the operator completes recovery.",
|
|
207
|
+
};
|
|
208
|
+
case "invalid_args":
|
|
209
|
+
if (!error.validation)
|
|
210
|
+
return error;
|
|
211
|
+
return {
|
|
212
|
+
...error,
|
|
213
|
+
connector: target.connector.id,
|
|
214
|
+
operation: `${target.connector.id}.${target.toolName}`,
|
|
215
|
+
nextAction: this.catalog.searchRecovery({
|
|
216
|
+
query: target.toolName,
|
|
217
|
+
connector: target.connector.id,
|
|
218
|
+
}, "Inspect the current input shape if the validation findings are not sufficient."),
|
|
219
|
+
retry: `Correct the listed arguments and retry ` +
|
|
220
|
+
`${target.connector.id}.${target.toolName}.`,
|
|
221
|
+
};
|
|
222
|
+
default:
|
|
223
|
+
return error;
|
|
241
224
|
}
|
|
242
|
-
|
|
243
|
-
|
|
225
|
+
};
|
|
226
|
+
const failed = (error) => {
|
|
227
|
+
const diagnostics = timing();
|
|
228
|
+
const target = resolved ?? activityTarget;
|
|
229
|
+
const details = enrich(error, target);
|
|
230
|
+
// Activity rows stay payload-free by construction; the operator's log is
|
|
231
|
+
// where the downstream reason goes, bounded and without arguments.
|
|
232
|
+
if (target && details.code !== "destructive_tool_requires_approval") {
|
|
233
|
+
this.registry
|
|
234
|
+
.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope)
|
|
235
|
+
.logger.warn("[connecta] call failed", {
|
|
236
|
+
connector: target.connector.id,
|
|
237
|
+
tool: target.toolName,
|
|
238
|
+
source: context.source,
|
|
239
|
+
code: details.code,
|
|
240
|
+
// Sanitized transport diagnostics (origin and errno only, #539).
|
|
241
|
+
...(details.details ? { details: details.details } : {}),
|
|
242
|
+
attempts,
|
|
243
|
+
durationMs: Date.now() - started,
|
|
244
|
+
message: String(details.message ?? "").slice(0, 300),
|
|
245
|
+
});
|
|
244
246
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger,
|
|
264
|
-
});
|
|
265
|
-
if (invalid)
|
|
266
|
-
return classifyCallError(invalid);
|
|
267
|
-
}
|
|
268
|
-
try {
|
|
269
|
-
context.beforeDispatch?.();
|
|
270
|
-
}
|
|
271
|
-
catch (error) {
|
|
272
|
-
return error instanceof InvocationFailure
|
|
273
|
-
? error.details
|
|
274
|
-
: classifyCallError(error);
|
|
275
|
-
}
|
|
276
|
-
if (callSignal?.aborted)
|
|
277
|
-
throw callSignal.reason;
|
|
278
|
-
attempts = 1;
|
|
279
|
-
let permit;
|
|
280
|
-
let attemptError;
|
|
281
|
-
let attemptFailed = false;
|
|
282
|
-
try {
|
|
283
|
-
permit = await timed((elapsed) => { admissionMs += elapsed; }, () => this.registry.admitCall(target.connector.id, {
|
|
284
|
-
toolName: target.toolName,
|
|
285
|
-
args: args ?? {},
|
|
286
|
-
...defined({ signal: callSignal }),
|
|
287
|
-
}));
|
|
288
|
-
const raw = await timed((elapsed) => { connectorMs += elapsed; }, () => {
|
|
289
|
-
const call = () => {
|
|
290
|
-
const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs }));
|
|
291
|
-
if (target.connector.credential &&
|
|
292
|
-
!connectorContext.credential) {
|
|
293
|
-
throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
|
|
294
|
-
`authorize_connector({ connector: "${target.connector.id}" }).`);
|
|
295
|
-
}
|
|
296
|
-
// Cancellation can arrive during admission or context construction.
|
|
297
|
-
if (callSignal?.aborted)
|
|
298
|
-
throw callSignal.reason;
|
|
299
|
-
return target.connector.callTool(target.toolName, args ?? {}, connectorContext);
|
|
300
|
-
};
|
|
301
|
-
// Race cancellation here too, so an uncooperative connector cannot
|
|
302
|
-
// retain its admission permit after the enclosing deadline expires.
|
|
303
|
-
return callSignal
|
|
304
|
-
? withDeadline(call, {
|
|
305
|
-
signal: callSignal,
|
|
306
|
-
timeoutError: new ConnectorCallError("timeout", "Tool call timed out"),
|
|
307
|
-
})
|
|
308
|
-
: call();
|
|
309
|
-
});
|
|
310
|
-
// isError is checked here for BOTH result shapes so every adapter
|
|
311
|
-
// reports the same downstream-failure wording, and the throw lands
|
|
312
|
-
// inside the attempt where it feeds health.
|
|
313
|
-
assertRawMcpSuccess(target.connector.kind, raw);
|
|
314
|
-
observedResult = unwrapMcpResult(target.connector.kind, raw);
|
|
315
|
-
result = context.unwrapResult ? observedResult : raw;
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
attemptFailed = true;
|
|
319
|
-
attemptError = error;
|
|
320
|
-
}
|
|
321
|
-
finally {
|
|
322
|
-
permit?.release();
|
|
247
|
+
record(details.code === "timeout"
|
|
248
|
+
? "timeout"
|
|
249
|
+
: details.code === "cancelled"
|
|
250
|
+
? "cancelled"
|
|
251
|
+
: "error", { errorCode: details.code });
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
durationMs: Date.now() - started,
|
|
255
|
+
attempts,
|
|
256
|
+
timing: diagnostics,
|
|
257
|
+
...defined({ resolved }),
|
|
258
|
+
error: details,
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
if (context.requestSignal?.aborted) {
|
|
262
|
+
// Preserve the cancelled admission-attempt count without starting discovery.
|
|
263
|
+
attempts = 1;
|
|
264
|
+
return failed(callerCancelledDetails());
|
|
323
265
|
}
|
|
324
|
-
|
|
266
|
+
let result;
|
|
267
|
+
let observedResult;
|
|
268
|
+
// Everything up to the downstream result: resolution, the safety and
|
|
269
|
+
// schema refusals, admission, and the one attempt. A refusal is its
|
|
270
|
+
// success value. Its failure is an abort reason or something nobody
|
|
271
|
+
// expected, and a throw anywhere in it — a defect, to Effect — is
|
|
272
|
+
// classified like a failure, as the async body this replaced did.
|
|
273
|
+
const dispatch = (callSignal) => Effect.gen({ self: this }, function* () {
|
|
274
|
+
const resolution = yield* this.catalog.resolve(address, defined({ signal: callSignal }));
|
|
275
|
+
catalogMs += resolution.catalogMs;
|
|
325
276
|
if (callSignal?.aborted)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
277
|
+
return yield* Effect.fail(callSignal.reason);
|
|
278
|
+
if (!resolution.ok) {
|
|
279
|
+
if (resolution.connector && resolution.toolName) {
|
|
280
|
+
activityTarget = {
|
|
281
|
+
connector: resolution.connector,
|
|
282
|
+
toolName: resolution.toolName,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (resolution.cause && context.requestSignal?.aborted) {
|
|
286
|
+
return callerCancelledDetails();
|
|
287
|
+
}
|
|
288
|
+
return resolution.error;
|
|
289
|
+
}
|
|
290
|
+
const target = resolution.resolved;
|
|
291
|
+
resolved = target;
|
|
292
|
+
activityTarget = target;
|
|
293
|
+
if (!isExplicitlyReadOnly(target.definition) && !context.allowDestructive) {
|
|
294
|
+
const canonicalAddress = `${target.connector.id}.${target.toolName}`;
|
|
295
|
+
return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`);
|
|
296
|
+
}
|
|
297
|
+
// Remote MCP tools advertise their input schema in the catalog. Validate
|
|
298
|
+
// against that same request-local definition before admission or provider
|
|
299
|
+
// dispatch, so a predictable mismatch stays structured instead of being
|
|
300
|
+
// flattened into provider-specific error prose. Unsupported schemas retain
|
|
301
|
+
// validateToolInput's fail-open behavior and reach the downstream normally.
|
|
302
|
+
if (target.connector.kind === "mcp" &&
|
|
303
|
+
target.definition.inputSchema) {
|
|
304
|
+
const invalid = validateToolInput(target.definition.inputSchema, args ?? {}, {
|
|
305
|
+
address: `${target.connector.id}.${target.toolName}`,
|
|
306
|
+
logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger,
|
|
307
|
+
});
|
|
308
|
+
if (invalid)
|
|
309
|
+
return classifyCallError(invalid);
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
context.beforeDispatch?.();
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
return error instanceof InvocationFailure
|
|
316
|
+
? error.details
|
|
317
|
+
: classifyCallError(error);
|
|
318
|
+
}
|
|
319
|
+
if (callSignal?.aborted)
|
|
320
|
+
return yield* Effect.fail(callSignal.reason);
|
|
321
|
+
attempts = 1;
|
|
322
|
+
const call = () => {
|
|
323
|
+
const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs }));
|
|
324
|
+
if (target.connector.credential &&
|
|
325
|
+
!connectorContext.credential) {
|
|
326
|
+
throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
|
|
327
|
+
`authorize_connector({ connector: "${target.connector.id}" }).`);
|
|
328
|
+
}
|
|
329
|
+
// Cancellation can arrive during admission or context construction.
|
|
330
|
+
if (callSignal?.aborted)
|
|
331
|
+
throw callSignal.reason;
|
|
332
|
+
return target.connector.callTool(target.toolName, args ?? {}, connectorContext);
|
|
333
|
+
};
|
|
334
|
+
// The permit belongs to this scope, so success, failure, and the
|
|
335
|
+
// deadline's interruption all release it. Interruption stops the
|
|
336
|
+
// wait instead of waiting for the connector, so an uncooperative one
|
|
337
|
+
// cannot hold its permit past the deadline.
|
|
338
|
+
const attempt = yield* Effect.exit(Effect.scoped(Effect.gen({ self: this }, function* () {
|
|
339
|
+
yield* timed((elapsed) => { admissionMs += elapsed; }, admitted(this.registry, target, args, callSignal));
|
|
340
|
+
const raw = yield* timed((elapsed) => { connectorMs += elapsed; }, Effect.tryPromise({
|
|
341
|
+
try: () => Promise.resolve(call()),
|
|
342
|
+
catch: (error) => error,
|
|
343
|
+
}));
|
|
344
|
+
// isError is checked here for BOTH result shapes so every adapter
|
|
345
|
+
// reports the same downstream-failure wording, and the throw lands
|
|
346
|
+
// inside the attempt where it feeds health.
|
|
347
|
+
assertRawMcpSuccess(target.connector.kind, raw);
|
|
348
|
+
return { raw, observed: unwrapMcpResult(target.connector.kind, raw) };
|
|
349
|
+
})));
|
|
350
|
+
if (Exit.isFailure(attempt)) {
|
|
351
|
+
if (callSignal?.aborted)
|
|
352
|
+
return yield* Effect.fail(callSignal.reason);
|
|
353
|
+
const attemptError = Cause.squash(attempt.cause);
|
|
354
|
+
return isCallerCancellation(attemptError, context.requestSignal)
|
|
355
|
+
? callerCancelledDetails()
|
|
356
|
+
: classifyCallError(attemptError);
|
|
357
|
+
}
|
|
358
|
+
observedResult = attempt.value.observed;
|
|
359
|
+
result = context.unwrapResult ? observedResult : attempt.value.raw;
|
|
360
|
+
return undefined;
|
|
361
|
+
});
|
|
362
|
+
// One deadline owns discovery, queue admission, and provider dispatch.
|
|
363
|
+
// Expiry interrupts dispatch wherever it is, so nothing it started runs
|
|
364
|
+
// on afterwards, and only this fiber records the outcome: a late
|
|
365
|
+
// cancellation cannot append a second activity event.
|
|
366
|
+
const dispatched = yield* Effect.exit(context.timeoutMs || context.requestSignal
|
|
367
|
+
? withDeadlineEffect(dispatch, {
|
|
338
368
|
...defined({
|
|
339
369
|
timeoutMs: context.timeoutMs,
|
|
340
370
|
signal: context.requestSignal,
|
|
341
371
|
}),
|
|
342
372
|
timeoutError: new ConnectorCallError("timeout", `Tool call timed out after ${context.timeoutMs}ms`),
|
|
343
373
|
})
|
|
344
|
-
:
|
|
345
|
-
if (
|
|
346
|
-
return failed(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
:
|
|
374
|
+
: dispatch());
|
|
375
|
+
if (Exit.isFailure(dispatched)) {
|
|
376
|
+
return failed(context.requestSignal?.aborted
|
|
377
|
+
? callerCancelledDetails()
|
|
378
|
+
: classifyCallError(Cause.squash(dispatched.cause)));
|
|
379
|
+
}
|
|
380
|
+
if (dispatched.value)
|
|
381
|
+
return failed(dispatched.value);
|
|
382
|
+
// A dispatch that returned no refusal resolved a concrete tool.
|
|
383
|
+
const completed = resolved;
|
|
384
|
+
if (!completed) {
|
|
385
|
+
return yield* Effect.die(new Error("Invocation completed without a resolved tool"));
|
|
386
|
+
}
|
|
387
|
+
const processResult = context.processResult;
|
|
388
|
+
const processing = processResult
|
|
389
|
+
? Effect.tryPromise({
|
|
390
|
+
try: () => Promise.resolve(processResult(result, completed)),
|
|
391
|
+
catch: (error) => error,
|
|
392
|
+
})
|
|
393
|
+
: Effect.succeed(result);
|
|
394
|
+
const processed = yield* Effect.exit(timed((elapsed) => { resultProcessingMs += elapsed; }, Effect.tap(processing, () => Effect.sync(() => {
|
|
362
395
|
try {
|
|
363
396
|
this.registry.observeOutputShape(completed.connector.id, completed.definition, observedResult);
|
|
364
397
|
}
|
|
365
398
|
catch {
|
|
366
399
|
// Shape learning is advisory. It cannot change a completed call.
|
|
367
400
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
401
|
+
}))));
|
|
402
|
+
// Past this point the call has happened, so nothing may invite a retry.
|
|
403
|
+
const unprocessable = () => failed(framingError("result_processing_failed", "The downstream call completed, but its result could not be processed. Do not repeat the call to recover its result."));
|
|
404
|
+
if (Exit.isFailure(processed))
|
|
405
|
+
return unprocessable();
|
|
406
|
+
try {
|
|
407
|
+
const value = processed.value;
|
|
408
|
+
const diagnostics = timing();
|
|
409
|
+
const friction = context.activityFriction?.(value);
|
|
410
|
+
record("success", friction ? { friction } : {});
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
value,
|
|
414
|
+
resolved: completed,
|
|
415
|
+
durationMs: Date.now() - started,
|
|
416
|
+
attempts,
|
|
417
|
+
timing: diagnostics,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return unprocessable();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
385
424
|
}
|
|
386
425
|
}
|