@zackbart/connecta 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +2 -1
- package/dist/activity.js +5 -5
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-routes.js +31 -5
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth.js +3 -0
- package/dist/auth/static-oauth.js +26 -18
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +292 -24
- package/dist/catalog-freshness.js +12 -0
- package/dist/catalog-service.js +373 -70
- package/dist/catalog.js +147 -58
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +43 -17
- package/dist/config.js +26 -10
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +62 -5
- package/dist/connectors/remote-mcp.js +10 -1
- package/dist/describe-config.d.ts +3 -2
- package/dist/discovery-health.js +26 -11
- package/dist/errors.d.ts +52 -3
- package/dist/errors.js +86 -3
- package/dist/execute.js +476 -200
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +2 -2
- package/dist/executors/quickjs-child.js +13 -4
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +36 -19
- package/dist/executors/quickjs.js +23 -6
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +7 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +9 -3
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -0
- package/dist/invocation.js +98 -34
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +259 -60
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +166 -51
- package/dist/node.js +3 -1
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +2 -1
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +1 -1
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +0 -13
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -0
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +7 -2
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +1 -0
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +2 -0
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +11 -5
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/index.js +2 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +2 -2
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +1 -0
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +2 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +127 -27
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +13 -4
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +55 -26
- package/dist/routes/shared.d.ts +6 -0
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/request-scope.js +36 -1
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skills.js +100 -33
- package/dist/storage/keys.js +9 -6
- package/dist/storage/sql-native.js +1 -1
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +31 -0
- package/dist/usage-guide.js +14 -13
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +28 -8
- package/documentation/architecture.md +104 -42
- package/documentation/auth.md +46 -19
- package/documentation/code-mode.md +378 -116
- package/documentation/deploying.md +10 -9
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +655 -153
- package/documentation/operating.md +5 -3
- package/documentation/providers.md +18 -6
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +1 -1
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
package/dist/auth-elicitation.js
CHANGED
|
@@ -3,7 +3,7 @@ import { RequestScope, requestOwner } from "./runtime/request-scope.js";
|
|
|
3
3
|
import { upwardStep } from "./upward.js";
|
|
4
4
|
import { authAwait, authCall, authInvoke, authFlow, authMethod, registerAuthMethod, authOwnMethod, } from "./auth/native.js";
|
|
5
5
|
import { registryProgram } from "./runtime/registry.js";
|
|
6
|
-
import { enableAuthorizationHandoffs } from "./authorization-handoff.js";
|
|
6
|
+
import { authorizationHandoff, connectLinkFields, enableAuthorizationHandoffs } from "./authorization-handoff.js";
|
|
7
7
|
import { createRequestStateCodec, inputRequired, ProtocolError, INVALID_PARAMS, PROTOCOL_VERSION_META_KEY, CLIENT_CAPABILITIES_META_KEY, } from "@modelcontextprotocol/server";
|
|
8
8
|
import { Effect } from "effect";
|
|
9
9
|
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
@@ -17,6 +17,23 @@ import { DownstreamElicitation, MAX_RELAY_STATE_CHARS, } from "./downstream-inpu
|
|
|
17
17
|
import { canonicalState as canonical, requestDigest as digest, stateObject as object, invalidRequestState as invalidState, REQUEST_STATE_TTL_MS as TTL_MS, MAX_INPUT_ROUNDS as MAX_ROUNDS, } from "./request-state.js";
|
|
18
18
|
const INPUT_KEY = "connecta_auth";
|
|
19
19
|
const MESSAGE = "Connect this service in your browser, then retry the request.";
|
|
20
|
+
// A client may answer without showing anything, and the response cannot prove
|
|
21
|
+
// otherwise. Neither text claims a person saw the prompt, and neither names a
|
|
22
|
+
// route: an agent told to call again would only reach the same refusal.
|
|
23
|
+
const REFUSED = {
|
|
24
|
+
decline: {
|
|
25
|
+
code: "auth_declined",
|
|
26
|
+
message: "The client declined the connection prompt, possibly automatically, so the user may not have seen it. Nothing was retried.",
|
|
27
|
+
},
|
|
28
|
+
cancel: {
|
|
29
|
+
code: "auth_cancelled",
|
|
30
|
+
message: "The client cancelled the connection prompt, possibly automatically, so the user may not have seen it. Nothing was retried.",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
// Only a repeat can recover a refusal whose link failed, and it asks for a new
|
|
34
|
+
// prompt, so a person's refusal stays final. Nothing on the server limits it to one.
|
|
35
|
+
const REFUSED_NO_LINK = " No connection link could be issued. If the user has not refused this connection, repeat this request once for a new link, then give it to them. Stop if that repeat also fails.";
|
|
36
|
+
const refusedPurpose = (recipient) => `Give this link to the user, unchanged, ${recipient}once, then stop. Do not prompt again, retry, or reach this service another way until they confirm. If they already refused this connection in this conversation, only mention that the link is available.`;
|
|
20
37
|
function failure(code, message) {
|
|
21
38
|
return toolError({ code, message, retryable: false }, { ok: false });
|
|
22
39
|
}
|
|
@@ -45,14 +62,26 @@ export class AuthElicitation {
|
|
|
45
62
|
context,
|
|
46
63
|
]));
|
|
47
64
|
}
|
|
48
|
-
|
|
49
|
-
return runEdge(authOwnMethod(AuthElicitation.prototype, this, "
|
|
50
|
-
state,
|
|
51
|
-
]));
|
|
65
|
+
browserCompleted(state) {
|
|
66
|
+
return runEdge(authOwnMethod(AuthElicitation.prototype, this, "browserCompleted", [state]));
|
|
52
67
|
}
|
|
53
68
|
connected(id, context) {
|
|
54
69
|
return runEdge(authOwnMethod(AuthElicitation.prototype, this, "connected", [id, context]));
|
|
55
70
|
}
|
|
71
|
+
refused(action, state, force) {
|
|
72
|
+
return runEdge(authOwnMethod(AuthElicitation.prototype, this, "refused", [action, state, force]));
|
|
73
|
+
}
|
|
74
|
+
/** The one gate before any recovery link: this caller may connect this connector here. */
|
|
75
|
+
mayConnect(id) {
|
|
76
|
+
const { registry, principal, publicUrl, canManage, unavailable, credentialUi } = this.options;
|
|
77
|
+
const connector = typeof id === "string" ? registry.getConnector(id) : undefined;
|
|
78
|
+
return (typeof id === "string" &&
|
|
79
|
+
/^[a-z0-9_-]+$/.test(id) &&
|
|
80
|
+
Boolean(connector?.startAuth || (connector?.credential && credentialUi)) &&
|
|
81
|
+
Boolean(principal && publicUrl) &&
|
|
82
|
+
!unavailable &&
|
|
83
|
+
canManage(id));
|
|
84
|
+
}
|
|
56
85
|
bind(context) {
|
|
57
86
|
this.downstream.bind(context);
|
|
58
87
|
}
|
|
@@ -114,12 +143,8 @@ export class AuthElicitation {
|
|
|
114
143
|
!(yield* upwardStep(authInvoke(self.options, "claimRetry", [previous.connector, previous.browserNonces.at(-1)])))) {
|
|
115
144
|
return failure("invalid_request_state", "Invalid or expired requestState");
|
|
116
145
|
}
|
|
117
|
-
if (previous && object(response))
|
|
118
|
-
|
|
119
|
-
return failure("auth_declined", "Connection was declined. The request was not retried.");
|
|
120
|
-
if (response.action === "cancel")
|
|
121
|
-
return failure("auth_cancelled", "Connection was cancelled. The request was not retried.");
|
|
122
|
-
}
|
|
146
|
+
if (previous && object(response) && (response.action === "decline" || response.action === "cancel"))
|
|
147
|
+
return yield* upwardStep(authInvoke(self, "refused", [response.action, previous, args.force]));
|
|
123
148
|
if (previous?.reads.length) {
|
|
124
149
|
const catalog = new CatalogService(self.options.registry, self.options.publicUrl, {
|
|
125
150
|
requestScope: self.options.requestScope,
|
|
@@ -156,18 +181,12 @@ export class AuthElicitation {
|
|
|
156
181
|
}
|
|
157
182
|
bindReplayReads(self.options.requestScope, previous.reads);
|
|
158
183
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
yield* upwardStep(authInvoke(self.options, "closeLinks", [previous.connector, previous.browserNonces]));
|
|
166
|
-
if ((yield* upwardStep(authInvoke(self, "browserProgress", [previous]))).completed &&
|
|
167
|
-
(yield* upwardStep(authInvoke(self, "connected", [previous.connector, context])))) {
|
|
168
|
-
const structuredContent = { connector: previous.connector, status: "ok" };
|
|
169
|
-
return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
170
|
-
}
|
|
184
|
+
if (previous &&
|
|
185
|
+
tool === "authorize_connector" &&
|
|
186
|
+
(yield* upwardStep(authInvoke(self, "browserCompleted", [previous]))) &&
|
|
187
|
+
(yield* upwardStep(authInvoke(self, "connected", [previous.connector, context])))) {
|
|
188
|
+
const structuredContent = { connector: previous.connector, status: "ok" };
|
|
189
|
+
return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
171
190
|
}
|
|
172
191
|
const envelope = context.mcpReq.envelope;
|
|
173
192
|
const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
|
|
@@ -216,16 +235,8 @@ export class AuthElicitation {
|
|
|
216
235
|
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
|
|
217
236
|
};
|
|
218
237
|
}
|
|
219
|
-
const {
|
|
220
|
-
|
|
221
|
-
const recoverable = connector?.startAuth || (connector?.credential && self.options.credentialUi);
|
|
222
|
-
if (typeof id !== "string" ||
|
|
223
|
-
!/^[a-z0-9_-]+$/.test(id) ||
|
|
224
|
-
!recoverable ||
|
|
225
|
-
!principal ||
|
|
226
|
-
!publicUrl ||
|
|
227
|
-
unavailable ||
|
|
228
|
-
!canManage(id))
|
|
238
|
+
const { vault, principal } = self.options;
|
|
239
|
+
if (!self.mayConnect(id) || !principal)
|
|
229
240
|
return result;
|
|
230
241
|
// Incapable hosts already received the shared explicit-authorization handoff.
|
|
231
242
|
if (!capable)
|
|
@@ -237,9 +248,9 @@ export class AuthElicitation {
|
|
|
237
248
|
...result.structuredContent,
|
|
238
249
|
error: {
|
|
239
250
|
...result.structuredContent?.error,
|
|
240
|
-
|
|
251
|
+
...connectLinkFields(error, id, error.authorizationUrl ??
|
|
241
252
|
(yield* upwardStep(authInvoke(self.options, "connectLink", [id])))
|
|
242
|
-
.url,
|
|
253
|
+
.url),
|
|
243
254
|
},
|
|
244
255
|
};
|
|
245
256
|
return {
|
|
@@ -250,13 +261,13 @@ export class AuthElicitation {
|
|
|
250
261
|
}
|
|
251
262
|
if (previous && previous.round >= MAX_ROUNDS)
|
|
252
263
|
return failure("auth_round_limit", "Connection is still required after three prompts. Connect explicitly, then start a new request.");
|
|
253
|
-
//
|
|
254
|
-
//
|
|
264
|
+
// Accept is consent to open the link, not proof it opened, so every round's
|
|
265
|
+
// link stays live and they share one reset (claimOAuthConnectStart).
|
|
255
266
|
const forced = explicit && args.force === true;
|
|
256
267
|
const link = yield* upwardStep(authInvoke(self.options, "connectLink", [
|
|
257
268
|
id,
|
|
258
|
-
forced
|
|
259
|
-
forced ?
|
|
269
|
+
forced,
|
|
270
|
+
forced ? previous?.browserNonces[0] : undefined,
|
|
260
271
|
]));
|
|
261
272
|
const state = {
|
|
262
273
|
version: 1,
|
|
@@ -285,7 +296,7 @@ export class AuthElicitation {
|
|
|
285
296
|
...result.structuredContent,
|
|
286
297
|
error: {
|
|
287
298
|
...result.structuredContent?.error,
|
|
288
|
-
|
|
299
|
+
...connectLinkFields(result.structuredContent?.error, id, link.url),
|
|
289
300
|
},
|
|
290
301
|
};
|
|
291
302
|
return {
|
|
@@ -305,6 +316,59 @@ export class AuthElicitation {
|
|
|
305
316
|
});
|
|
306
317
|
}), this.options.requestSignal).pipe(Effect.provideService(RequestScope, requestOwner(this.options.requestScope)));
|
|
307
318
|
}, AuthElicitation.prototype.run);
|
|
319
|
+
registerAuthMethod(AuthElicitation.prototype, "refused", function (action, state, force) {
|
|
320
|
+
const self = this;
|
|
321
|
+
return authAwait(authFlow(function* () {
|
|
322
|
+
const { code, message } = REFUSED[action];
|
|
323
|
+
const { connector: id, tool } = state;
|
|
324
|
+
if (!self.mayConnect(id))
|
|
325
|
+
return failure(code, message);
|
|
326
|
+
const { registry, publicUrl, canManage, unavailable, credentialHandoffUrl, routes, requestScope } = self.options;
|
|
327
|
+
try {
|
|
328
|
+
// The signed state bound these arguments, so this is the restart the
|
|
329
|
+
// caller asked for, sharing the prompt's reset if that prompt was opened.
|
|
330
|
+
const forced = tool === "authorize_connector" && force === true;
|
|
331
|
+
// The ordinary handoff, except that the link is the sealed form this
|
|
332
|
+
// client was already shown: the agent sees no principal in it.
|
|
333
|
+
const handoff = yield* upwardStep(authCall(authorizationHandoff, [
|
|
334
|
+
registry,
|
|
335
|
+
publicUrl,
|
|
336
|
+
requestScope,
|
|
337
|
+
{
|
|
338
|
+
canManageAuth: canManage,
|
|
339
|
+
credentialHandoffUrl,
|
|
340
|
+
oauthConnectUnavailable: unavailable,
|
|
341
|
+
routes,
|
|
342
|
+
oauthConnectUrl: authMethod(() => authInvoke(self.options, "connectLink", [
|
|
343
|
+
id,
|
|
344
|
+
forced,
|
|
345
|
+
forced ? state.browserNonces[0] : undefined,
|
|
346
|
+
]).pipe(Effect.map((link) => link.url))),
|
|
347
|
+
},
|
|
348
|
+
{ connector: id },
|
|
349
|
+
]));
|
|
350
|
+
const url = handoff.authorizationUrl ?? handoff.operatorUrl;
|
|
351
|
+
if (!url)
|
|
352
|
+
return failure(code, message);
|
|
353
|
+
if (handoff.authorizationUrl)
|
|
354
|
+
sentSecretsForRequest(requestScope).mintedState(handoff.authorizationUrl);
|
|
355
|
+
return toolError({
|
|
356
|
+
...handoff,
|
|
357
|
+
code,
|
|
358
|
+
message,
|
|
359
|
+
retryable: false,
|
|
360
|
+
nextAction: {
|
|
361
|
+
url,
|
|
362
|
+
purpose: refusedPurpose(handoff.authorizationUrl ? "" : "for the operator to open, "),
|
|
363
|
+
},
|
|
364
|
+
}, { ok: false });
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// A signing or storage failure cannot replace the refusal itself.
|
|
368
|
+
return failure(code, message + REFUSED_NO_LINK);
|
|
369
|
+
}
|
|
370
|
+
}), this.options.requestSignal).pipe(Effect.provideService(RequestScope, requestOwner(this.options.requestScope)));
|
|
371
|
+
}, AuthElicitation.prototype.refused);
|
|
308
372
|
registerAuthMethod(AuthElicitation.prototype, "connected", function (id, context) {
|
|
309
373
|
const self = this;
|
|
310
374
|
return authAwait(authFlow(function* () {
|
|
@@ -331,29 +395,16 @@ export class AuthElicitation {
|
|
|
331
395
|
}
|
|
332
396
|
}), this.options.requestSignal).pipe(Effect.provideService(RequestScope, requestOwner(this.options.requestScope)));
|
|
333
397
|
}, AuthElicitation.prototype.connected);
|
|
334
|
-
registerAuthMethod(AuthElicitation.prototype, "
|
|
398
|
+
registerAuthMethod(AuthElicitation.prototype, "browserCompleted", function (state) {
|
|
335
399
|
const self = this;
|
|
336
400
|
return authAwait(authFlow(function* () {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const progress = yield* upwardStep(authInvoke(self.options, "linkProgress", [
|
|
343
|
-
state.connector,
|
|
344
|
-
nonce,
|
|
345
|
-
]));
|
|
346
|
-
if (progress === "started")
|
|
347
|
-
started = nonce;
|
|
348
|
-
if (progress === "claimed")
|
|
349
|
-
claimed = nonce;
|
|
350
|
-
completed ||= progress === "started";
|
|
351
|
-
pending ||= progress === "claimed";
|
|
352
|
-
}
|
|
353
|
-
const predecessor = started ?? claimed;
|
|
354
|
-
return { ...(predecessor ? { predecessor } : {}), completed: completed && !pending };
|
|
401
|
+
// A reset never leaves "started", so no reset can be running behind this link.
|
|
402
|
+
for (const nonce of state.browserNonces)
|
|
403
|
+
if (yield* upwardStep(authInvoke(self.options, "linkStarted", [state.connector, nonce])))
|
|
404
|
+
return true;
|
|
405
|
+
return false;
|
|
355
406
|
}), this.options.requestSignal).pipe(Effect.provideService(RequestScope, requestOwner(this.options.requestScope)));
|
|
356
|
-
}, AuthElicitation.prototype.
|
|
407
|
+
}, AuthElicitation.prototype.browserCompleted);
|
|
357
408
|
registerAuthMethod(AuthElicitation.prototype, "verify", function (wire, context) {
|
|
358
409
|
const self = this;
|
|
359
410
|
return authAwait(authFlow(function* () {
|
|
@@ -380,7 +431,13 @@ export class AuthElicitation {
|
|
|
380
431
|
!Number.isInteger(state.round) ||
|
|
381
432
|
state.round < 1 ||
|
|
382
433
|
state.round > MAX_ROUNDS ||
|
|
383
|
-
![
|
|
434
|
+
![
|
|
435
|
+
"call_tool",
|
|
436
|
+
"call_destructive_tool",
|
|
437
|
+
"execute_code",
|
|
438
|
+
"execute_read_code",
|
|
439
|
+
"authorize_connector",
|
|
440
|
+
].includes(String(state.tool)) ||
|
|
384
441
|
typeof state.wireTool !== "string" ||
|
|
385
442
|
typeof state.digest !== "string" ||
|
|
386
443
|
!/^[a-f0-9]{64}$/.test(state.digest) ||
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { routePhrases } from "./agent-routes.js";
|
|
1
|
+
import { handoffInHand, routePhrases } from "./agent-routes.js";
|
|
2
2
|
import { upwardStep } from "./upward.js";
|
|
3
3
|
import { authCall, authInvoke, authFlow, registerAuth } from "./auth/native.js";
|
|
4
4
|
import { Context, Deferred, Effect, Scheduler } from "effect";
|
|
@@ -15,6 +15,38 @@ function outcomeAware(build) {
|
|
|
15
15
|
afterUnknownWrite.set(handoff, build(true));
|
|
16
16
|
return handoff;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* What to do with a handoff the error already carries. Passing it on is the
|
|
20
|
+
* whole step: calling authorize_connector would only return the same thing.
|
|
21
|
+
* After a write of unknown outcome the follow-up reconciles, never retries.
|
|
22
|
+
*/
|
|
23
|
+
function handoffAction(handoff, unknownWrite) {
|
|
24
|
+
const then = unknownWrite
|
|
25
|
+
? "After they confirm, re-read the write's target before any fresh call."
|
|
26
|
+
: "Retry after they confirm.";
|
|
27
|
+
const url = handoff.authorizationUrl ?? handoff.operatorUrl;
|
|
28
|
+
if (!url)
|
|
29
|
+
return {
|
|
30
|
+
action: "contact_operator",
|
|
31
|
+
purpose: `Tell the user the operator must fix this connector, then stop. ${then}`,
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
url,
|
|
35
|
+
purpose: `Give this link to the user, unchanged, ${handoff.authorizationUrl ? "" : "for the operator to open, "}then stop. ${then}`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const unknownWriteError = (error) => error.writeOutcome === "unknown" || error.reconciliationRequired === true;
|
|
39
|
+
/**
|
|
40
|
+
* The fields for an auth error given only a connect link, where a capable host's
|
|
41
|
+
* prompt could not carry it: the same direct step as a full handoff.
|
|
42
|
+
*/
|
|
43
|
+
export function connectLinkFields(error, connector, authorizationUrl) {
|
|
44
|
+
return {
|
|
45
|
+
...(typeof error.message === "string" ? { message: handoffInHand(error.message, connector) } : {}),
|
|
46
|
+
authorizationUrl,
|
|
47
|
+
nextAction: handoffAction({ connector, recovery: "oauth", authorizationUrl }, unknownWriteError(error)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
18
50
|
function oauthFollowUp(connectorId, unknownWrite, opts) {
|
|
19
51
|
const then = `Then ${unknownWrite ? RECONCILE : "retry the original call"}`;
|
|
20
52
|
// Without programs, no connecta.search can confirm the catalog.
|
|
@@ -95,7 +127,7 @@ const authorizationHandoffNative = /* @__PURE__ */ registerAuth(authorizationHan
|
|
|
95
127
|
recovery: "oauth",
|
|
96
128
|
status: "auth_required",
|
|
97
129
|
authorizationUrl,
|
|
98
|
-
instructions: "
|
|
130
|
+
instructions: "For the person opening this link: sign in as the user who requested this connection; Connecta verifies identity and permission before starting consent. For the agent: pass authorizationUrl on exactly as returned. " +
|
|
99
131
|
oauthFollowUp(connector.id, unknownWrite, opts),
|
|
100
132
|
}));
|
|
101
133
|
});
|
|
@@ -180,7 +212,8 @@ export function withAuthorizationHandoffEffect(scope, error) {
|
|
|
180
212
|
return {
|
|
181
213
|
...error,
|
|
182
214
|
...value,
|
|
183
|
-
message: error.message,
|
|
215
|
+
message: handoffInHand(error.message, connector),
|
|
216
|
+
nextAction: handoffAction(value, unknownWriteError(error)),
|
|
184
217
|
...(value.message ? { instructions: value.message } : {}),
|
|
185
218
|
};
|
|
186
219
|
}),
|