@zackbart/connecta 0.8.0 → 0.9.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 +128 -0
- package/README.md +31 -55
- package/SECURITY.md +5 -11
- package/dist/activity.d.ts +0 -8
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +0 -1
- package/dist/activity.js.map +1 -1
- package/dist/auth/bearer.d.ts +1 -7
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +2 -23
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +2 -11
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +31 -46
- package/dist/auth/clerk.js.map +1 -1
- package/dist/auth/downstream-oauth.d.ts +15 -6
- package/dist/auth/downstream-oauth.d.ts.map +1 -1
- package/dist/auth/downstream-oauth.js +60 -11
- package/dist/auth/downstream-oauth.js.map +1 -1
- package/dist/call-admission.d.ts.map +1 -1
- package/dist/call-admission.js +0 -1
- package/dist/call-admission.js.map +1 -1
- package/dist/catalog-service.d.ts +1 -2
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +12 -18
- package/dist/catalog-service.js.map +1 -1
- package/dist/catalog.d.ts +6 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +51 -0
- package/dist/catalog.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +73 -71
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/errors.d.ts +19 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +4 -9
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +41 -20
- package/dist/execute.js.map +1 -1
- package/dist/executor-result.js +1 -1
- package/dist/executor-result.js.map +1 -1
- package/dist/index.d.ts +3 -82
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -90
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +5 -0
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +46 -10
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +4 -12
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +119 -169
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/operator-ui/model.d.ts +1 -22
- package/dist/operator-ui/model.d.ts.map +1 -1
- package/dist/operator-ui/model.js.map +1 -1
- package/dist/registry.d.ts +11 -178
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +52 -330
- package/dist/registry.js.map +1 -1
- package/dist/retired-toolkits.d.ts +3 -0
- package/dist/retired-toolkits.d.ts.map +1 -0
- package/dist/retired-toolkits.js +9 -0
- package/dist/retired-toolkits.js.map +1 -0
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/activity.js +2 -5
- package/dist/routes/activity.js.map +1 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/credentials.js +1 -12
- package/dist/routes/credentials.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +94 -188
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/oauth.d.ts.map +1 -1
- package/dist/routes/oauth.js +3 -8
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/shared.d.ts +9 -30
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js +10 -69
- package/dist/routes/shared.js.map +1 -1
- package/dist/routes/ui.d.ts.map +1 -1
- package/dist/routes/ui.js +4 -10
- package/dist/routes/ui.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -31
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +3 -6
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +9 -38
- package/dist/skills.js.map +1 -1
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +2 -4
- package/dist/timeout.js.map +1 -1
- package/dist/types.d.ts +8 -61
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +1 -2
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +5 -35
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/activity.ts +0 -9
- package/src/auth/bearer.ts +3 -32
- package/src/auth/clerk.ts +35 -48
- package/src/auth/downstream-oauth.ts +106 -24
- package/src/call-admission.ts +0 -1
- package/src/catalog-service.ts +23 -23
- package/src/catalog.ts +55 -0
- package/src/connectors/remote-mcp.ts +106 -86
- package/src/errors.ts +22 -0
- package/src/execute.ts +43 -24
- package/src/executor-result.ts +1 -1
- package/src/index.ts +24 -208
- package/src/invocation.ts +63 -10
- package/src/meta-tools.ts +139 -187
- package/src/operator-ui/browser.css +1 -54
- package/src/operator-ui/browser.ts +16 -150
- package/src/operator-ui/generated.ts +2 -2
- package/src/operator-ui/model.ts +1 -23
- package/src/registry.ts +59 -429
- package/src/retired-toolkits.ts +14 -0
- package/src/routes/activity.ts +1 -6
- package/src/routes/credentials.ts +0 -12
- package/src/routes/mcp.ts +110 -230
- package/src/routes/oauth.ts +2 -8
- package/src/routes/shared.ts +10 -100
- package/src/routes/ui.ts +2 -12
- package/src/server.ts +1 -31
- package/src/skills.ts +9 -38
- package/src/timeout.ts +2 -4
- package/src/types.ts +11 -62
- package/src/ui.ts +9 -46
- package/src/version.ts +1 -1
- package/dist/credential-health.d.ts +0 -205
- package/dist/credential-health.d.ts.map +0 -1
- package/dist/credential-health.js +0 -610
- package/dist/credential-health.js.map +0 -1
- package/dist/toolkits.d.ts +0 -124
- package/dist/toolkits.d.ts.map +0 -1
- package/dist/toolkits.js +0 -322
- package/dist/toolkits.js.map +0 -1
- package/src/credential-health.ts +0 -835
- package/src/toolkits.ts +0 -461
package/src/index.ts
CHANGED
|
@@ -6,12 +6,6 @@ import {
|
|
|
6
6
|
import { Registry } from "./registry.js";
|
|
7
7
|
import { createFetchHandler } from "./server.js";
|
|
8
8
|
import { droppedBrandingUrls, droppedUiAuthUrls } from "./ui.js";
|
|
9
|
-
import {
|
|
10
|
-
resolveToolkits,
|
|
11
|
-
validateToolkitBindings,
|
|
12
|
-
type Toolkit,
|
|
13
|
-
type ToolkitConfig,
|
|
14
|
-
} from "./toolkits.js";
|
|
15
9
|
import { memoryStorage } from "./storage/memory.js";
|
|
16
10
|
import { CONNECTA_VERSION } from "./version.js";
|
|
17
11
|
import {
|
|
@@ -20,11 +14,6 @@ import {
|
|
|
20
14
|
withExecutorAdmission,
|
|
21
15
|
} from "./executor-admission.js";
|
|
22
16
|
import type { ActivityReadGate, ActivityStore } from "./activity.js";
|
|
23
|
-
import type {
|
|
24
|
-
CredentialCheckResult,
|
|
25
|
-
CredentialHealthConfig,
|
|
26
|
-
} from "./credential-health.js";
|
|
27
|
-
import type { DeferredWork } from "./connector-scope.js";
|
|
28
17
|
import type {
|
|
29
18
|
Connector,
|
|
30
19
|
ConnectaBranding,
|
|
@@ -50,24 +39,13 @@ export interface ConnectaActivityConfig {
|
|
|
50
39
|
deploymentId?: string;
|
|
51
40
|
}
|
|
52
41
|
|
|
53
|
-
/** Operator-vault encryption
|
|
42
|
+
/** Operator-vault encryption. */
|
|
54
43
|
export interface ConnectaCredentialsConfig {
|
|
55
44
|
/**
|
|
56
45
|
* Base64-encoded 32-byte AES key for credentials managed on /credentials.
|
|
57
46
|
* Keep this in the runtime's secret store, never in KV or source control.
|
|
58
47
|
*/
|
|
59
48
|
encryptionKey?: string;
|
|
60
|
-
/**
|
|
61
|
-
* Tuning for proactive credential liveness checks that let a connector's
|
|
62
|
-
* status flip to `auth_required` before an agent's call fails. Defaults: one
|
|
63
|
-
* check per connector per 15 minutes, four in flight, 30 seconds each,
|
|
64
|
-
* triggered opportunistically by inbound authenticated traffic.
|
|
65
|
-
*
|
|
66
|
-
* Optional even without an encryption key because downstream OAuth connectors
|
|
67
|
-
* manage their own grants. `Connecta.checkCredentials()` runs the same checks
|
|
68
|
-
* on demand for a Worker cron trigger or Node interval.
|
|
69
|
-
*/
|
|
70
|
-
health?: CredentialHealthConfig;
|
|
71
49
|
}
|
|
72
50
|
|
|
73
51
|
/** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
|
|
@@ -156,38 +134,6 @@ export interface ConnectaAdmissionConfig {
|
|
|
156
134
|
|
|
157
135
|
export interface ConnectaConfig {
|
|
158
136
|
connectors: Connector[];
|
|
159
|
-
/**
|
|
160
|
-
* Named scoped views over `connectors`, selected per client connection with
|
|
161
|
-
* `?toolkit=<name>` on the `/mcp` URL. One deployment belongs to one org;
|
|
162
|
-
* a toolkit is the slice of it a group of team members sees.
|
|
163
|
-
*
|
|
164
|
-
* ```ts
|
|
165
|
-
* toolkits: {
|
|
166
|
-
* support: { connectors: ["zendesk", "notion"] },
|
|
167
|
-
* exec: {
|
|
168
|
-
* connectors: ["zendesk", "notion", "gmail"],
|
|
169
|
-
* excludeTools: ["gmail.send_message"],
|
|
170
|
-
* },
|
|
171
|
-
* }
|
|
172
|
-
* ```
|
|
173
|
-
*
|
|
174
|
-
* Inside a toolkit-scoped session every meta-tool behaves as if out-of-scope
|
|
175
|
-
* connectors and tools do not exist, and an out-of-scope address fails
|
|
176
|
-
* exactly as a nonexistent one does. No `?toolkit=` ⇒ the full registry, so
|
|
177
|
-
* adding toolkits changes nothing for connections that don't ask for one; an
|
|
178
|
-
* unknown name is an error, never a silent fallback.
|
|
179
|
-
*
|
|
180
|
-
* Selection is self-service until a toolkit is BOUND to an inbound identity:
|
|
181
|
-
* pass `toolkits` to an auth adapter — `bearerToken(secret, { toolkits:
|
|
182
|
-
* ["support"] })` — and that credential may open only those toolkits, and may
|
|
183
|
-
* not connect unscoped unless it also passes `unscoped: true`. An unbound
|
|
184
|
-
* identity keeps the self-service behavior.
|
|
185
|
-
*
|
|
186
|
-
* Definitions are validated at construction: an unknown connector id, an
|
|
187
|
-
* empty connector selection, an empty `includeTools`, a malformed tool
|
|
188
|
-
* address, or an address naming no tool on an in-code connector all throw.
|
|
189
|
-
*/
|
|
190
|
-
toolkits?: ToolkitConfig;
|
|
191
137
|
/** Inbound auth adapters. Includes bearerToken(...); omit for open (dev). */
|
|
192
138
|
auth?: InboundAuth | InboundAuth[];
|
|
193
139
|
/** KVStorage impl. Defaults to memoryStorage(). */
|
|
@@ -199,7 +145,7 @@ export interface ConnectaConfig {
|
|
|
199
145
|
publicUrl?: string;
|
|
200
146
|
/** Payload-free tool activity storage and operator-read policy. */
|
|
201
147
|
activity?: ConnectaActivityConfig;
|
|
202
|
-
/** Operator credential vault
|
|
148
|
+
/** Operator credential vault settings. */
|
|
203
149
|
credentials?: ConnectaCredentialsConfig;
|
|
204
150
|
/** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
|
|
205
151
|
discovery?: ConnectaDiscoveryConfig;
|
|
@@ -235,37 +181,6 @@ export interface Connecta {
|
|
|
235
181
|
/** Web-standard fetch handler. Usable as `export default { fetch: connecta.fetch }`. */
|
|
236
182
|
fetch: (request: Request, env?: unknown, ctx?: unknown) => Promise<Response>;
|
|
237
183
|
registry: Registry;
|
|
238
|
-
/**
|
|
239
|
-
* Check the stored downstream credentials now — the scheduler-facing half of
|
|
240
|
-
* credential health (issue #24). Wire it to whatever timer the runtime has:
|
|
241
|
-
*
|
|
242
|
-
* ```ts
|
|
243
|
-
* // Cloudflare Workers (wrangler.jsonc: "triggers": { "crons": ["*\/15 * * * *"] })
|
|
244
|
-
* async scheduled(_c, env, ctx) {
|
|
245
|
-
* const defer = ctx.waitUntil.bind(ctx);
|
|
246
|
-
* ctx.waitUntil(build(env).checkCredentials({ defer }));
|
|
247
|
-
* }
|
|
248
|
-
* // Node
|
|
249
|
-
* setInterval(() => void connecta.checkCredentials(), 15 * 60_000).unref();
|
|
250
|
-
* ```
|
|
251
|
-
*
|
|
252
|
-
* Returns one outcome per connector considered, including why a connector was
|
|
253
|
-
* skipped (`fresh` is the rate limit: a connector checked less than
|
|
254
|
-
* `credentials.health.intervalSeconds` ago is not re-checked unless `force`).
|
|
255
|
-
* Never rejects on a connector failure — a broken connector becomes an `error`
|
|
256
|
-
* verdict. Needs a base URL for connector contexts: `publicUrl` supplies it,
|
|
257
|
-
* or pass one.
|
|
258
|
-
*/
|
|
259
|
-
checkCredentials: (opts?: {
|
|
260
|
-
baseUrl?: string;
|
|
261
|
-
force?: boolean;
|
|
262
|
-
ids?: string[];
|
|
263
|
-
/**
|
|
264
|
-
* Runtime continuation for bounded teardown after the check result is ready.
|
|
265
|
-
* Workers should pass `ctx.waitUntil.bind(ctx)`.
|
|
266
|
-
*/
|
|
267
|
-
defer?: DeferredWork;
|
|
268
|
-
}) => Promise<CredentialCheckResult[]>;
|
|
269
184
|
/** Drain and release configured executor resources. Idempotent. */
|
|
270
185
|
close: () => Promise<void>;
|
|
271
186
|
}
|
|
@@ -318,7 +233,6 @@ const LEGACY_CONFIG_MIGRATIONS = [
|
|
|
318
233
|
["activityReadGate", "activity.readGate"],
|
|
319
234
|
["activityDeploymentId", "activity.deploymentId"],
|
|
320
235
|
["credentialEncryptionKey", "credentials.encryptionKey"],
|
|
321
|
-
["credentialHealth", "credentials.health"],
|
|
322
236
|
["toolCacheTtlSeconds", "discovery.catalogTtlSeconds"],
|
|
323
237
|
["persistToolCatalog", "discovery.persistCatalog"],
|
|
324
238
|
["toolCatalogStaleSeconds", "discovery.staleCatalogSeconds"],
|
|
@@ -337,6 +251,23 @@ const hasOwn = (value: object, key: PropertyKey): boolean =>
|
|
|
337
251
|
*/
|
|
338
252
|
function assertNoLegacyConfig(config: ConnectaConfig): void {
|
|
339
253
|
const candidate = config as unknown as Record<PropertyKey, unknown>;
|
|
254
|
+
if (hasOwn(candidate, "toolkits")) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"ConnectaConfig.toolkits was removed in issue #178. Deploy one " +
|
|
257
|
+
"connecta instance per audience instead; see ethos.md.",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const credentials = candidate.credentials;
|
|
261
|
+
const hasNestedHealth =
|
|
262
|
+
typeof credentials === "object" &&
|
|
263
|
+
credentials !== null &&
|
|
264
|
+
hasOwn(credentials, "health");
|
|
265
|
+
if (hasOwn(candidate, "credentialHealth") || hasNestedHealth) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
"`credentials.health` and legacy `credentialHealth` were removed in " +
|
|
268
|
+
"issue #179. Credentials now fail at use; see ethos.md.",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
340
271
|
const found: Array<readonly [string, string]> = [];
|
|
341
272
|
if (hasOwn(candidate, "activity")) {
|
|
342
273
|
const activity = candidate.activity;
|
|
@@ -376,7 +307,6 @@ function assertNoLegacyConfig(config: ConnectaConfig): void {
|
|
|
376
307
|
function warnInsecureConfig(
|
|
377
308
|
config: ConnectaConfig,
|
|
378
309
|
inboundAuth: InboundAuth[],
|
|
379
|
-
toolkits: ReadonlyMap<string, Toolkit> | undefined,
|
|
380
310
|
logger: Logger,
|
|
381
311
|
): void {
|
|
382
312
|
const oauthConnectors = config.connectors.filter((c) => c.finishAuth);
|
|
@@ -407,64 +337,6 @@ function warnInsecureConfig(
|
|
|
407
337
|
);
|
|
408
338
|
}
|
|
409
339
|
|
|
410
|
-
// Toolkits that nothing binds to an identity: selection is then self-service,
|
|
411
|
-
// and the boundary organizes the surface rather than protecting it. Three
|
|
412
|
-
// distinct shapes, so three distinct warnings — an operator can only act on
|
|
413
|
-
// the one they are actually in.
|
|
414
|
-
//
|
|
415
|
-
// All are keyed off the RESOLVED toolkits, which is the same map `?toolkit=`
|
|
416
|
-
// resolves against, rather than the presence of the config key: `toolkits: {}`
|
|
417
|
-
// is a truthy object that resolves to nothing selectable, so warning about a
|
|
418
|
-
// choice no caller can make would name a risk that does not exist.
|
|
419
|
-
if (toolkits) {
|
|
420
|
-
const unbound = inboundAuth.filter((provider) => !provider.toolkitBinding);
|
|
421
|
-
if (inboundAuth.length === 0) {
|
|
422
|
-
// No auth at all ⇒ no identity exists to bind, so binding is not even the
|
|
423
|
-
// fix here. The open-mode warning above covers the wider exposure.
|
|
424
|
-
logger.warn(
|
|
425
|
-
"[connecta] toolkits are configured but there is no inbound " +
|
|
426
|
-
"authentication: with no identity to bind a toolkit to, any caller " +
|
|
427
|
-
"can choose any toolkit or omit ?toolkit= and see every connector. " +
|
|
428
|
-
"Configure `auth` (for example bearerToken(...) or Clerk), then bind " +
|
|
429
|
-
"each credential with `toolkits: [...]`.",
|
|
430
|
-
);
|
|
431
|
-
} else if (unbound.length === inboundAuth.length) {
|
|
432
|
-
// Authenticated, but every credential may still select every view. This is
|
|
433
|
-
// the shape issue #37 exists to close, and it is invisible without a line
|
|
434
|
-
// saying so: nothing fails, the teams are simply not separated.
|
|
435
|
-
logger.warn(
|
|
436
|
-
"[connecta] toolkits are configured but no inbound identity is bound " +
|
|
437
|
-
"to one: every credential `auth` admits can select any toolkit, or " +
|
|
438
|
-
"omit ?toolkit= and see the whole deployment, so a token handed to " +
|
|
439
|
-
"one team also opens the others' views. Bind each credential with " +
|
|
440
|
-
"`toolkits: [...]` on its auth adapter (add `unscoped: true` for an " +
|
|
441
|
-
"operator credential that should still see everything).",
|
|
442
|
-
);
|
|
443
|
-
} else if (unbound.length > 0) {
|
|
444
|
-
// The dangerous middle: SOME credentials are bound, which is exactly when
|
|
445
|
-
// an operator believes the deployment is separated — while one forgotten
|
|
446
|
-
// provider still opens every view and the whole deployment-wide surface.
|
|
447
|
-
// Naming the unbound providers is the point; an intentionally unrestricted
|
|
448
|
-
// credential says so with `unscoped: true` and stops appearing here.
|
|
449
|
-
const counted = new Map<string, number>();
|
|
450
|
-
for (const provider of unbound) {
|
|
451
|
-
counted.set(provider.kind, (counted.get(provider.kind) ?? 0) + 1);
|
|
452
|
-
}
|
|
453
|
-
const named = [...counted]
|
|
454
|
-
.map(([kind, count]) => (count > 1 ? `${kind} x${count}` : kind))
|
|
455
|
-
.join(", ");
|
|
456
|
-
logger.warn(
|
|
457
|
-
`[connecta] toolkits are bound on some inbound auth providers but not ` +
|
|
458
|
-
`all: ${named} ${unbound.length === 1 ? "declares" : "declare"} no ` +
|
|
459
|
-
"binding, so a caller that provider admits can still select any " +
|
|
460
|
-
"toolkit, connect unscoped, and read the deployment-wide operator " +
|
|
461
|
-
"surfaces — whatever the bound credentials beside it allow. Bind it " +
|
|
462
|
-
"too, or declare the exemption with `toolkits: [...], unscoped: true` " +
|
|
463
|
-
"if it is meant to be an operator credential.",
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
340
|
// Branding URLs that failed their scheme gate. Rendering silently falls back
|
|
469
341
|
// (a bad URL must not take the page down), so this warning is the only way an
|
|
470
342
|
// operator learns their value never reached the page.
|
|
@@ -541,8 +413,10 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
541
413
|
const credentialConnectors = config.connectors.filter((c) => c.credential);
|
|
542
414
|
const encryptionKey = config.credentials?.encryptionKey;
|
|
543
415
|
if (credentialConnectors.length > 0 && !encryptionKey) {
|
|
544
|
-
|
|
545
|
-
|
|
416
|
+
logger.warn(
|
|
417
|
+
"Operator-managed credentials are unavailable because " +
|
|
418
|
+
"credentials.encryptionKey is not configured for connectors: " +
|
|
419
|
+
credentialConnectors.map((c) => c.id).join(", "),
|
|
546
420
|
);
|
|
547
421
|
}
|
|
548
422
|
const credentialVault = encryptionKey
|
|
@@ -567,21 +441,9 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
567
441
|
...(config.calls?.maxBatchResultBytes !== undefined
|
|
568
442
|
? { maxBatchResultBytes: config.calls.maxBatchResultBytes }
|
|
569
443
|
: {}),
|
|
570
|
-
...(config.credentials?.health !== undefined
|
|
571
|
-
? { credentialHealth: config.credentials.health }
|
|
572
|
-
: {}),
|
|
573
444
|
});
|
|
574
|
-
// Throws on every structural mistake it can see (see resolveToolkits): a
|
|
575
|
-
// typo must not become a scope the operator never wrote. Note this is about
|
|
576
|
-
// the scope being *intended*, not about it being an access check — a toolkit
|
|
577
|
-
// scopes visibility, and `auth` remains the thing deciding who gets in.
|
|
578
|
-
const toolkits = resolveToolkits(config.toolkits, config.connectors);
|
|
579
445
|
const inboundAuth = normalizeAuth(config.auth);
|
|
580
|
-
|
|
581
|
-
// deployment does not declare would deny that credential every connection,
|
|
582
|
-
// with a 403 its client reports as a transport failure. Throw here instead.
|
|
583
|
-
validateToolkitBindings(inboundAuth, toolkits);
|
|
584
|
-
warnInsecureConfig(config, inboundAuth, toolkits, logger);
|
|
446
|
+
warnInsecureConfig(config, inboundAuth, logger);
|
|
585
447
|
const requestAdmission = admissionController(
|
|
586
448
|
config.admission?.requests,
|
|
587
449
|
REQUEST_ADMISSION_DEFAULTS,
|
|
@@ -637,7 +499,6 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
637
499
|
? { deploymentInfo: config.deploymentInfo }
|
|
638
500
|
: {}),
|
|
639
501
|
...(config.branding !== undefined ? { branding: config.branding } : {}),
|
|
640
|
-
...(toolkits ? { toolkits } : {}),
|
|
641
502
|
});
|
|
642
503
|
let closePromise: Promise<void> | undefined;
|
|
643
504
|
return {
|
|
@@ -649,33 +510,6 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
649
510
|
: undefined,
|
|
650
511
|
),
|
|
651
512
|
registry,
|
|
652
|
-
checkCredentials: (opts = {}) => {
|
|
653
|
-
// A scheduled check has no inbound request to derive an origin from, and
|
|
654
|
-
// a connector context without one would mint OAuth redirect URIs against
|
|
655
|
-
// a guess. Say so instead: the fix is one config line.
|
|
656
|
-
const baseUrl = opts.baseUrl ?? config.publicUrl;
|
|
657
|
-
if (!baseUrl) {
|
|
658
|
-
// Rejected, not thrown: the callers this is written for are
|
|
659
|
-
// `ctx.waitUntil(...)` and `.catch(...)` on the returned promise, and a
|
|
660
|
-
// synchronous throw escapes both — it would take down a scheduled
|
|
661
|
-
// handler instead of being reported by it.
|
|
662
|
-
return Promise.reject(
|
|
663
|
-
new Error(
|
|
664
|
-
"checkCredentials() needs a base URL: set `publicUrl` on the " +
|
|
665
|
-
"config (recommended — it is also what downstream OAuth " +
|
|
666
|
-
"callbacks use) or pass checkCredentials({ baseUrl }).",
|
|
667
|
-
),
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
return registry.checkCredentialHealth(
|
|
671
|
-
baseUrl,
|
|
672
|
-
{
|
|
673
|
-
...(opts.force !== undefined ? { force: opts.force } : {}),
|
|
674
|
-
...(opts.ids ? { ids: opts.ids } : {}),
|
|
675
|
-
},
|
|
676
|
-
opts.defer,
|
|
677
|
-
);
|
|
678
|
-
},
|
|
679
513
|
close: async () => {
|
|
680
514
|
closePromise ??= Promise.resolve().then(async () => {
|
|
681
515
|
requestAdmission.close();
|
|
@@ -705,23 +539,6 @@ export { CONNECTA_VERSION } from "./version.js";
|
|
|
705
539
|
// the class itself, the credential vault, and the meta-tool/sandbox factories
|
|
706
540
|
// are internal factoring and are deliberately not part of the API surface.
|
|
707
541
|
export type { Registry } from "./registry.js";
|
|
708
|
-
// Config-as-code shapes for `ConnectaConfig.toolkits` and the identity bindings
|
|
709
|
-
// that gate them. The resolved `Toolkit` and the `ScopedRegistry` that enforces
|
|
710
|
-
// it are internal factoring.
|
|
711
|
-
export type {
|
|
712
|
-
ToolkitBindingOptions,
|
|
713
|
-
ToolkitConfig,
|
|
714
|
-
ToolkitDefinition,
|
|
715
|
-
} from "./toolkits.js";
|
|
716
|
-
// Credential health: the config shape, and the result shape a scheduled
|
|
717
|
-
// `checkCredentials()` returns. The checker itself is internal factoring.
|
|
718
|
-
export type {
|
|
719
|
-
CredentialCheckResult,
|
|
720
|
-
CredentialCheckSkip,
|
|
721
|
-
CredentialCheckState,
|
|
722
|
-
CredentialHealthConfig,
|
|
723
|
-
CredentialHealthRecord,
|
|
724
|
-
} from "./credential-health.js";
|
|
725
542
|
|
|
726
543
|
export type {
|
|
727
544
|
RemoteMcpOptions,
|
|
@@ -750,7 +567,6 @@ export type {
|
|
|
750
567
|
ExecutorLease,
|
|
751
568
|
ExecutorProvider,
|
|
752
569
|
InboundAuth,
|
|
753
|
-
ToolkitBinding,
|
|
754
570
|
UiAuthConfig,
|
|
755
571
|
AuthResult,
|
|
756
572
|
JsonSchema,
|
package/src/invocation.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
classifyCallError,
|
|
14
14
|
ConnectorCallError,
|
|
15
15
|
messageLooksRetryable,
|
|
16
|
+
type AuthRecoveryMode,
|
|
16
17
|
type CallErrorDetails,
|
|
17
18
|
} from "./errors.js";
|
|
18
19
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
@@ -81,6 +82,21 @@ function callerCancelledDetails(): CallErrorDetails {
|
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
function recoveryMode(
|
|
86
|
+
registry: RegistryView,
|
|
87
|
+
connector: ResolvedCatalogTool["connector"],
|
|
88
|
+
baseUrl: string,
|
|
89
|
+
): AuthRecoveryMode {
|
|
90
|
+
if (connector.startAuth) return "oauth";
|
|
91
|
+
if (
|
|
92
|
+
connector.credential &&
|
|
93
|
+
registry.contextFor(connector.id, baseUrl).credential
|
|
94
|
+
) {
|
|
95
|
+
return "operator_config";
|
|
96
|
+
}
|
|
97
|
+
return "unavailable";
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
function isCallerCancellation(
|
|
85
101
|
error: unknown,
|
|
86
102
|
signal: AbortSignal | undefined,
|
|
@@ -155,6 +171,11 @@ export class InvocationFailure extends Error {
|
|
|
155
171
|
readonly code: string;
|
|
156
172
|
readonly retryable: boolean;
|
|
157
173
|
readonly retryAfterMs: number | undefined;
|
|
174
|
+
readonly connector: string | undefined;
|
|
175
|
+
readonly operation: string | undefined;
|
|
176
|
+
readonly recovery: CallErrorDetails["recovery"];
|
|
177
|
+
readonly nextAction: CallErrorDetails["nextAction"];
|
|
178
|
+
readonly retry: string | undefined;
|
|
158
179
|
|
|
159
180
|
constructor(readonly details: CallErrorDetails) {
|
|
160
181
|
super(details.message);
|
|
@@ -162,6 +183,11 @@ export class InvocationFailure extends Error {
|
|
|
162
183
|
this.code = details.code;
|
|
163
184
|
this.retryable = details.retryable;
|
|
164
185
|
this.retryAfterMs = details.retryAfterMs;
|
|
186
|
+
this.connector = details.connector;
|
|
187
|
+
this.operation = details.operation;
|
|
188
|
+
this.recovery = details.recovery;
|
|
189
|
+
this.nextAction = details.nextAction;
|
|
190
|
+
this.retry = details.retry;
|
|
165
191
|
}
|
|
166
192
|
}
|
|
167
193
|
|
|
@@ -262,13 +288,36 @@ export class InvocationService {
|
|
|
262
288
|
};
|
|
263
289
|
const failed = (error: CallErrorDetails): InvocationOutcome<T> => {
|
|
264
290
|
const diagnostics = timing();
|
|
291
|
+
const target = resolved ?? activityTarget;
|
|
292
|
+
const details =
|
|
293
|
+
error.code === "auth_required" && target
|
|
294
|
+
? {
|
|
295
|
+
...error,
|
|
296
|
+
connector: target.connector.id,
|
|
297
|
+
operation: `${target.connector.id}.${target.toolName}`,
|
|
298
|
+
recovery: recoveryMode(
|
|
299
|
+
this.registry,
|
|
300
|
+
target.connector,
|
|
301
|
+
this.catalog.baseUrl,
|
|
302
|
+
),
|
|
303
|
+
nextAction: {
|
|
304
|
+
tool: "authorize_connector" as const,
|
|
305
|
+
arguments: { connector: target.connector.id },
|
|
306
|
+
operatorHandoff:
|
|
307
|
+
"Give the URL and instructions it returns to the operator.",
|
|
308
|
+
},
|
|
309
|
+
retry:
|
|
310
|
+
`Retry ${target.connector.id}.${target.toolName} after ` +
|
|
311
|
+
"the operator completes recovery.",
|
|
312
|
+
}
|
|
313
|
+
: error;
|
|
265
314
|
record(
|
|
266
|
-
|
|
315
|
+
details.code === "timeout"
|
|
267
316
|
? "timeout"
|
|
268
|
-
:
|
|
317
|
+
: details.code === "cancelled"
|
|
269
318
|
? "cancelled"
|
|
270
319
|
: "error",
|
|
271
|
-
|
|
320
|
+
details.code,
|
|
272
321
|
);
|
|
273
322
|
return {
|
|
274
323
|
ok: false,
|
|
@@ -276,7 +325,7 @@ export class InvocationService {
|
|
|
276
325
|
attempts,
|
|
277
326
|
timing: diagnostics,
|
|
278
327
|
...(resolved ? { resolved } : {}),
|
|
279
|
-
error,
|
|
328
|
+
error: details,
|
|
280
329
|
};
|
|
281
330
|
};
|
|
282
331
|
|
|
@@ -299,12 +348,9 @@ export class InvocationService {
|
|
|
299
348
|
// against (a revoked downstream grant, say) still reads clean from the
|
|
300
349
|
// cheap `list_connectors({ probe: false })` signal.
|
|
301
350
|
//
|
|
302
|
-
// Recorded HERE rather than inside the registry's catalog fetch
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
// which `Registry.refreshTools` could not reach. A cache hit that
|
|
306
|
-
// avoids a live listTools call records nothing either way — it is not
|
|
307
|
-
// evidence of health, and success stays what it has always been: an
|
|
351
|
+
// Recorded HERE rather than inside the registry's catalog fetch because
|
|
352
|
+
// a cache hit that avoids a live listTools call records nothing — it is
|
|
353
|
+
// not evidence of health. Success stays what it has always been: an
|
|
308
354
|
// actual downstream call that returned.
|
|
309
355
|
this.registry.recordFailure(
|
|
310
356
|
resolution.connector.id,
|
|
@@ -383,6 +429,13 @@ export class InvocationService {
|
|
|
383
429
|
: {}),
|
|
384
430
|
},
|
|
385
431
|
);
|
|
432
|
+
if (resolved.connector.credential && !connectorContext.credential) {
|
|
433
|
+
throw new ConnectorCallError(
|
|
434
|
+
"auth_required",
|
|
435
|
+
"Operator-managed credential storage is not configured. Call " +
|
|
436
|
+
`authorize_connector({ connector: "${resolved.connector.id}" }).`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
386
439
|
let rejectCancelled!: (reason: unknown) => void;
|
|
387
440
|
const cancelled = controller
|
|
388
441
|
? new Promise<never>((_, reject) => {
|