@zackbart/connecta 0.6.0 → 0.7.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 +403 -0
- package/README.md +163 -308
- package/dist/auth/bearer.d.ts +4 -3
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +10 -8
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +8 -7
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +27 -8
- package/dist/auth/clerk.js.map +1 -1
- package/dist/connector-scope.d.ts +13 -0
- package/dist/connector-scope.d.ts.map +1 -0
- package/dist/connector-scope.js +35 -0
- package/dist/connector-scope.js.map +1 -0
- package/dist/connectors/api.d.ts +5 -5
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +3 -3
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +309 -10
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +20 -9
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +127 -63
- package/dist/credential-health.js.map +1 -1
- package/dist/credentials.d.ts +84 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +109 -2
- package/dist/credentials.js.map +1 -1
- package/dist/index.d.ts +83 -82
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -31
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +3 -3
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +16 -7
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +3 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -3
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +154 -52
- package/dist/server.js.map +1 -1
- package/dist/skills.js +2 -2
- package/dist/skills.js.map +1 -1
- package/dist/toolkits.js +1 -1
- package/dist/types.d.ts +51 -26
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +52 -21
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +665 -196
- 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/auth/bearer.ts +10 -8
- package/src/auth/clerk.ts +28 -9
- package/src/connector-scope.ts +41 -0
- package/src/connectors/api.ts +5 -5
- package/src/connectors/remote-mcp.ts +348 -25
- package/src/credential-health.ts +151 -71
- package/src/credentials.ts +166 -3
- package/src/index.ts +202 -113
- package/src/meta-tools.ts +22 -7
- package/src/registry.ts +4 -3
- package/src/server.ts +197 -71
- package/src/skills.ts +2 -2
- package/src/toolkits.ts +1 -1
- package/src/types.ts +51 -26
- package/src/ui.ts +703 -195
- package/src/version.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CredentialVault,
|
|
3
|
+
credentialTestRule,
|
|
4
|
+
describeCredentialTestMismatch,
|
|
5
|
+
} from "./credentials.js";
|
|
2
6
|
import { Registry } from "./registry.js";
|
|
3
7
|
import { createFetchHandler } from "./server.js";
|
|
4
8
|
import { droppedBrandingUrls, droppedUiAuthUrls } from "./ui.js";
|
|
@@ -24,6 +28,84 @@ import type {
|
|
|
24
28
|
Logger,
|
|
25
29
|
} from "./types.js";
|
|
26
30
|
|
|
31
|
+
/** Payload-free activity storage and operator-read policy. */
|
|
32
|
+
export interface ConnectaActivityConfig {
|
|
33
|
+
/**
|
|
34
|
+
* Privacy-minimal downstream tool activity storage. Writes are best-effort
|
|
35
|
+
* and never change tool results. Implement `list` to enable the Activity UI.
|
|
36
|
+
*/
|
|
37
|
+
store: ActivityStore;
|
|
38
|
+
/**
|
|
39
|
+
* Optional authorization gate for the Activity read API. MCP authentication
|
|
40
|
+
* is still required first. Omit to admit every authenticated actor.
|
|
41
|
+
*/
|
|
42
|
+
readGate?: ActivityReadGate;
|
|
43
|
+
/** Stable deployment label included in activity events, e.g. "production". */
|
|
44
|
+
deploymentId?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Operator-vault encryption and proactive credential-health tuning. */
|
|
48
|
+
export interface ConnectaCredentialsConfig {
|
|
49
|
+
/**
|
|
50
|
+
* Base64-encoded 32-byte AES key for credentials managed on /credentials.
|
|
51
|
+
* Keep this in the runtime's secret store, never in KV or source control.
|
|
52
|
+
*/
|
|
53
|
+
encryptionKey?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Tuning for proactive credential liveness checks that let a connector's
|
|
56
|
+
* status flip to `auth_required` before an agent's call fails. Defaults: one
|
|
57
|
+
* check per connector per 15 minutes, four in flight, 30 seconds each,
|
|
58
|
+
* triggered opportunistically by inbound authenticated traffic.
|
|
59
|
+
*
|
|
60
|
+
* Optional even without an encryption key because downstream OAuth connectors
|
|
61
|
+
* manage their own grants. `Connecta.checkCredentials()` runs the same checks
|
|
62
|
+
* on demand for a Worker cron trigger or Node interval.
|
|
63
|
+
*/
|
|
64
|
+
health?: CredentialHealthConfig;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
|
|
68
|
+
export interface ConnectaDiscoveryConfig {
|
|
69
|
+
/** Tool-list cache TTL (seconds). Default 300. */
|
|
70
|
+
catalogTtlSeconds?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Persist serializable remote tool catalogs in storage so cold isolates can
|
|
73
|
+
* discover tools without a downstream handshake. Default true.
|
|
74
|
+
*/
|
|
75
|
+
persistCatalog?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* How long an expired persisted catalog remains available as a fallback
|
|
78
|
+
* when a live refresh fails. Default 3600 seconds.
|
|
79
|
+
*/
|
|
80
|
+
staleCatalogSeconds?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Deadline (ms) for each downstream probe/catalog call fanned out by
|
|
83
|
+
* `list_connectors`, `search_tools`, and `describe_tools`. Defaults to
|
|
84
|
+
* 30_000. A timed-out connector degrades independently; this does not apply
|
|
85
|
+
* to tool calls or currently abort the underlying fetch.
|
|
86
|
+
*/
|
|
87
|
+
probeTimeoutMs?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Deployment-wide call deadlines and inline-result paging threshold. */
|
|
91
|
+
export interface ConnectaCallsConfig {
|
|
92
|
+
/**
|
|
93
|
+
* Deadline (ms) for `call_tool`/`batch_call` calls that pass no `timeoutMs`.
|
|
94
|
+
* An explicit per-call value wins. Opt-in: unset by default, so existing
|
|
95
|
+
* long-running calls gain no surprise deadline.
|
|
96
|
+
*
|
|
97
|
+
* This bounds one attempt, not all retries. `execute_code` host calls are
|
|
98
|
+
* unaffected because they already carry their own bound.
|
|
99
|
+
*/
|
|
100
|
+
defaultTimeoutMs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Max inline result size (bytes) before truncation and `get_result` paging.
|
|
103
|
+
* Must be a finite whole number >= 1; invalid values warn and fall back to
|
|
104
|
+
* 50_000. Connectors may override it individually.
|
|
105
|
+
*/
|
|
106
|
+
maxResultBytes?: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
27
109
|
export interface ConnectaConfig {
|
|
28
110
|
connectors: Connector[];
|
|
29
111
|
/**
|
|
@@ -58,18 +140,6 @@ export interface ConnectaConfig {
|
|
|
58
140
|
* address, or an address naming no tool on an in-code connector all throw.
|
|
59
141
|
*/
|
|
60
142
|
toolkits?: ToolkitConfig;
|
|
61
|
-
/**
|
|
62
|
-
* Privacy-minimal downstream tool activity storage. Writes are best-effort
|
|
63
|
-
* and never change tool results. Implement `list` to enable the Activity UI.
|
|
64
|
-
*/
|
|
65
|
-
activity?: ActivityStore;
|
|
66
|
-
/**
|
|
67
|
-
* Optional authorization gate for the Activity read API. MCP authentication
|
|
68
|
-
* is still required first. Omit to admit every authenticated actor.
|
|
69
|
-
*/
|
|
70
|
-
activityReadGate?: ActivityReadGate;
|
|
71
|
-
/** Stable deployment label included in activity events, e.g. "production". */
|
|
72
|
-
activityDeploymentId?: string;
|
|
73
143
|
/** Inbound auth adapters. Includes bearerToken(...); omit for open (dev). */
|
|
74
144
|
auth?: InboundAuth | InboundAuth[];
|
|
75
145
|
/** KVStorage impl. Defaults to memoryStorage(). */
|
|
@@ -79,78 +149,17 @@ export interface ConnectaConfig {
|
|
|
79
149
|
* HTTPS URL also redirects matching inbound HTTP requests to HTTPS.
|
|
80
150
|
*/
|
|
81
151
|
publicUrl?: string;
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
152
|
+
/** Payload-free tool activity storage and operator-read policy. */
|
|
153
|
+
activity?: ConnectaActivityConfig;
|
|
154
|
+
/** Operator credential vault and proactive liveness-check settings. */
|
|
155
|
+
credentials?: ConnectaCredentialsConfig;
|
|
156
|
+
/** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
|
|
157
|
+
discovery?: ConnectaDiscoveryConfig;
|
|
158
|
+
/** Deployment-wide call deadlines and result paging threshold. */
|
|
159
|
+
calls?: ConnectaCallsConfig;
|
|
87
160
|
/** Optional browser UI and OAuth result-page labels. */
|
|
88
161
|
branding?: ConnectaBranding;
|
|
89
162
|
logger?: Logger;
|
|
90
|
-
/** Tool-list cache TTL (seconds). Default 300. */
|
|
91
|
-
toolCacheTtlSeconds?: number;
|
|
92
|
-
/**
|
|
93
|
-
* Persist serializable remote tool catalogs in storage so cold isolates can
|
|
94
|
-
* discover tools without a downstream handshake. Default true.
|
|
95
|
-
*/
|
|
96
|
-
persistToolCatalog?: boolean;
|
|
97
|
-
/**
|
|
98
|
-
* How long an expired persisted catalog remains available as a fallback
|
|
99
|
-
* when a live refresh fails. Default 3600 seconds.
|
|
100
|
-
*/
|
|
101
|
-
toolCatalogStaleSeconds?: number;
|
|
102
|
-
/**
|
|
103
|
-
* Max inline result size (bytes) before call_tool/batch_call truncate and
|
|
104
|
-
* stash the full text for get_result paging. Must be a whole number of bytes
|
|
105
|
-
* >= 1; anything else (0, negative, fractional, NaN, Infinity) warns at
|
|
106
|
-
* startup and falls back to the default 50_000.
|
|
107
|
-
*/
|
|
108
|
-
maxResultBytes?: number;
|
|
109
|
-
/**
|
|
110
|
-
* Deadline (ms) applied to call_tool/batch_call calls that pass no
|
|
111
|
-
* `timeoutMs`, giving the connector both a budget (`ctx.timeoutMs`) and a
|
|
112
|
-
* cancellation signal (`ctx.signal`). An explicit per-call `timeoutMs` always
|
|
113
|
-
* wins. **Opt-in — undefined by default**, because switching it on globally
|
|
114
|
-
* would put a deadline on every call in an existing deployment and the
|
|
115
|
-
* failure mode is a working long-running call starting to time out.
|
|
116
|
-
* `execute_code` host calls are unaffected; they already carry a 15 s bound.
|
|
117
|
-
*
|
|
118
|
-
* Bounds a single attempt, not the whole call — the same as an explicit
|
|
119
|
-
* `timeoutMs` has always done. A call that also passes `maxRetries` can
|
|
120
|
-
* therefore run to roughly `(maxRetries + 1)` times this value plus backoff.
|
|
121
|
-
* `maxRetries` defaults to 0, so this is the total for every call that does
|
|
122
|
-
* not explicitly ask to retry.
|
|
123
|
-
*/
|
|
124
|
-
defaultToolTimeoutMs?: number;
|
|
125
|
-
/**
|
|
126
|
-
* Deadline (ms) applied to each individual downstream probe/catalog call that
|
|
127
|
-
* the discovery meta-tools fan out — `list_connectors` (with `probe`),
|
|
128
|
-
* `search_tools`, and `describe_tools` — so a single hung connector can no
|
|
129
|
-
* longer stall the whole meta-tool call. **Defaults to a generous 30_000**,
|
|
130
|
-
* chosen to trip only on a pathological hang, not on a realistically slow
|
|
131
|
-
* probe, so having it on by default will not break existing deployments.
|
|
132
|
-
* Bounds one downstream call, not the whole fan-out: a connector that outruns
|
|
133
|
-
* it degrades to an unavailable/errored entry while the rest are unaffected.
|
|
134
|
-
*
|
|
135
|
-
* Does NOT apply to `call_tool`/`batch_call` — those carry their own budget
|
|
136
|
-
* via `defaultToolTimeoutMs` or a per-call `timeoutMs`. Note this bounds the
|
|
137
|
-
* caller-facing wait only; the underlying fetch is not currently aborted, so
|
|
138
|
-
* real cancellation of the downstream request is a deferred follow-up.
|
|
139
|
-
*/
|
|
140
|
-
probeTimeoutMs?: number;
|
|
141
|
-
/**
|
|
142
|
-
* Tuning for the proactive credential liveness checks (issue #24) that let a
|
|
143
|
-
* connector's status flip to `auth_required` *before* an agent's call fails.
|
|
144
|
-
* Defaults are safe to leave alone: at most one check per connector per 15
|
|
145
|
-
* minutes, four in flight, 30 s each, triggered opportunistically by inbound
|
|
146
|
-
* authenticated traffic. Only connectors holding a credential connecta stores
|
|
147
|
-
* — an operator-managed `credential`, or a downstream-OAuth grant — are ever
|
|
148
|
-
* checked, and a check never calls a downstream tool.
|
|
149
|
-
*
|
|
150
|
-
* `Connecta.checkCredentials()` is the same check on demand, for a Worker cron
|
|
151
|
-
* trigger or a Node interval.
|
|
152
|
-
*/
|
|
153
|
-
credentialHealth?: CredentialHealthConfig;
|
|
154
163
|
serverInfo?: {
|
|
155
164
|
name?: string;
|
|
156
165
|
version?: string;
|
|
@@ -189,7 +198,7 @@ export interface Connecta {
|
|
|
189
198
|
*
|
|
190
199
|
* Returns one outcome per connector considered, including why a connector was
|
|
191
200
|
* skipped (`fresh` is the rate limit: a connector checked less than
|
|
192
|
-
* `
|
|
201
|
+
* `credentials.health.intervalSeconds` ago is not re-checked unless `force`).
|
|
193
202
|
* Never rejects on a connector failure — a broken connector becomes an `error`
|
|
194
203
|
* verdict. Needs a base URL for connector contexts: `publicUrl` supplies it,
|
|
195
204
|
* or pass one.
|
|
@@ -219,10 +228,64 @@ function normalizeAuth(auth: ConnectaConfig["auth"]): InboundAuth[] {
|
|
|
219
228
|
});
|
|
220
229
|
}
|
|
221
230
|
|
|
231
|
+
const LEGACY_CONFIG_MIGRATIONS = [
|
|
232
|
+
["activityReadGate", "activity.readGate"],
|
|
233
|
+
["activityDeploymentId", "activity.deploymentId"],
|
|
234
|
+
["credentialEncryptionKey", "credentials.encryptionKey"],
|
|
235
|
+
["credentialHealth", "credentials.health"],
|
|
236
|
+
["toolCacheTtlSeconds", "discovery.catalogTtlSeconds"],
|
|
237
|
+
["persistToolCatalog", "discovery.persistCatalog"],
|
|
238
|
+
["toolCatalogStaleSeconds", "discovery.staleCatalogSeconds"],
|
|
239
|
+
["probeTimeoutMs", "discovery.probeTimeoutMs"],
|
|
240
|
+
["defaultToolTimeoutMs", "calls.defaultTimeoutMs"],
|
|
241
|
+
["maxResultBytes", "calls.maxResultBytes"],
|
|
242
|
+
] as const;
|
|
243
|
+
|
|
244
|
+
const hasOwn = (value: object, key: PropertyKey): boolean =>
|
|
245
|
+
Object.prototype.hasOwnProperty.call(value, key);
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Fail closed at the public boundary: a JavaScript caller on the v0.6 shape
|
|
249
|
+
* must receive one complete migration error, never silently lose an option to
|
|
250
|
+
* a default. This runs before createConnecta reads any other config field.
|
|
251
|
+
*/
|
|
252
|
+
function assertNoLegacyConfig(config: ConnectaConfig): void {
|
|
253
|
+
const candidate = config as unknown as Record<PropertyKey, unknown>;
|
|
254
|
+
const found: Array<readonly [string, string]> = [];
|
|
255
|
+
if (hasOwn(candidate, "activity")) {
|
|
256
|
+
const activity = candidate.activity;
|
|
257
|
+
const isObject =
|
|
258
|
+
typeof activity === "object" && activity !== null;
|
|
259
|
+
// A valid v0.6 ActivityStore can itself own a backend field named `store`.
|
|
260
|
+
// Its required `record` method (including a prototype method) therefore
|
|
261
|
+
// takes precedence over the otherwise-new wrapper shape.
|
|
262
|
+
const hasLegacyRecord =
|
|
263
|
+
isObject &&
|
|
264
|
+
typeof (activity as { record?: unknown }).record === "function";
|
|
265
|
+
if (
|
|
266
|
+
activity !== undefined &&
|
|
267
|
+
(!isObject ||
|
|
268
|
+
hasLegacyRecord ||
|
|
269
|
+
!hasOwn(activity, "store"))
|
|
270
|
+
) {
|
|
271
|
+
found.push(["activity", "activity.store"]);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const migration of LEGACY_CONFIG_MIGRATIONS) {
|
|
275
|
+
if (hasOwn(candidate, migration[0])) found.push(migration);
|
|
276
|
+
}
|
|
277
|
+
if (found.length === 0) return;
|
|
278
|
+
throw new Error(
|
|
279
|
+
"Unsupported v0.6.x ConnectaConfig options. Migrate each path for v0.7.0:\n" +
|
|
280
|
+
found.map(([oldPath, newPath]) => `- ${oldPath} -> ${newPath}`).join("\n"),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
222
284
|
/**
|
|
223
285
|
* One-time construction warnings for deployment shapes that run fine but are
|
|
224
286
|
* usually unintended. Warning-only — never throws and never changes behavior;
|
|
225
|
-
* each condition emits at most one `logger.warn
|
|
287
|
+
* each deployment-wide condition emits at most one `logger.warn`, and each
|
|
288
|
+
* per-connector condition at most one per connector it names.
|
|
226
289
|
*/
|
|
227
290
|
function warnInsecureConfig(
|
|
228
291
|
config: ConnectaConfig,
|
|
@@ -328,33 +391,57 @@ function warnInsecureConfig(
|
|
|
328
391
|
);
|
|
329
392
|
}
|
|
330
393
|
|
|
331
|
-
//
|
|
332
|
-
// offers one,
|
|
333
|
-
// provider's frontendApiUrl
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
394
|
+
// Operator shells render exactly one provider's browser sign-in config — the
|
|
395
|
+
// first that offers one, matching the server route's `find` — and that
|
|
396
|
+
// provider's URLs reach the browser: frontendApiUrl as the loader's
|
|
397
|
+
// `<script src>`, signInUrl/signUpUrl as the addresses ClerkJS navigates to.
|
|
398
|
+
// Gate-or-drop like a branding href: rendering drops a rejected value and the
|
|
399
|
+
// operator page then reports that Clerk could not load or quietly signs in
|
|
400
|
+
// through Clerk's defaults — both confusing symptoms without this line naming
|
|
401
|
+
// the cause. Checking only the rendered provider keeps the claim true — a
|
|
402
|
+
// later provider's uiAuth never reaches the page, so there is nothing there
|
|
403
|
+
// to warn about.
|
|
339
404
|
const uiAuthProvider = inboundAuth.find((provider) => provider.uiAuth);
|
|
340
405
|
const droppedUiAuth = droppedUiAuthUrls(uiAuthProvider?.uiAuth);
|
|
341
406
|
if (uiAuthProvider && droppedUiAuth.length > 0) {
|
|
342
407
|
logger.warn(
|
|
343
408
|
`[connecta] inbound auth provider "${uiAuthProvider.kind}" had ` +
|
|
344
|
-
`${droppedUiAuth.join(", ")} dropped:
|
|
345
|
-
"
|
|
346
|
-
"
|
|
409
|
+
`${droppedUiAuth.join(", ")} dropped: every uiAuth URL reaches the ` +
|
|
410
|
+
"browser — as the sign-in loader's source, or as a place Clerk sends " +
|
|
411
|
+
"the operator — so each must be an absolute https URL. A dropped " +
|
|
412
|
+
"value reaches no part of the page: without frontendApiUrl the operator shell renders " +
|
|
413
|
+
"no loader and cannot start a sign-in, and without signInUrl/signUpUrl " +
|
|
414
|
+
"it signs in through Clerk's defaults.",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// A credential test hook that cannot test the declared credential shape.
|
|
419
|
+
// The shape picks the hook (see `credentialTestRule`) and the other one is
|
|
420
|
+
// never substituted, so the connector is simply not testable: /credentials offers no
|
|
421
|
+
// Test action and the route answers 400. Without this line the only way to
|
|
422
|
+
// discover the mistake is to click a button that isn't there.
|
|
423
|
+
for (const connector of config.connectors) {
|
|
424
|
+
const { mismatch } = credentialTestRule(connector);
|
|
425
|
+
if (!mismatch) continue;
|
|
426
|
+
logger.warn(
|
|
427
|
+
`[connecta] connector "${connector.id}" cannot test its credential: ` +
|
|
428
|
+
`${describeCredentialTestMismatch(mismatch)}. /credentials offers no Test ` +
|
|
429
|
+
`action and POST /ui/credentials/${connector.id}/test answers 400 ` +
|
|
430
|
+
"until the matching hook is implemented.",
|
|
347
431
|
);
|
|
348
432
|
}
|
|
349
433
|
|
|
350
|
-
// OAuth connectors whose callback
|
|
351
|
-
//
|
|
434
|
+
// OAuth connectors whose callback cannot perform a state/CSRF check. The
|
|
435
|
+
// public route refuses every callback for these connectors rather than hand
|
|
436
|
+
// an unverified code to finishAuth, so this warning explains why auth cannot
|
|
437
|
+
// complete instead of describing a vulnerability the server permits.
|
|
352
438
|
for (const connector of oauthConnectors) {
|
|
353
439
|
if (!connector.verifyState) {
|
|
354
440
|
logger.warn(
|
|
355
441
|
`[connecta] connector "${connector.id}" has an OAuth callback with no ` +
|
|
356
|
-
`state/CSRF check: /oauth/callback/${connector.id}
|
|
357
|
-
"
|
|
442
|
+
`state/CSRF check: /oauth/callback/${connector.id} refuses every ` +
|
|
443
|
+
"callback rather than exchange an unverified code. Implement " +
|
|
444
|
+
"`verifyState` to complete authorization (the shipped remoteMcp " +
|
|
358
445
|
"connector already does).",
|
|
359
446
|
);
|
|
360
447
|
}
|
|
@@ -362,26 +449,28 @@ function warnInsecureConfig(
|
|
|
362
449
|
}
|
|
363
450
|
|
|
364
451
|
export function createConnecta(config: ConnectaConfig): Connecta {
|
|
452
|
+
assertNoLegacyConfig(config);
|
|
365
453
|
const storage = config.storage ?? memoryStorage();
|
|
366
454
|
const logger = config.logger ?? defaultLogger();
|
|
367
455
|
const credentialConnectors = config.connectors.filter((c) => c.credential);
|
|
368
|
-
|
|
456
|
+
const encryptionKey = config.credentials?.encryptionKey;
|
|
457
|
+
if (credentialConnectors.length > 0 && !encryptionKey) {
|
|
369
458
|
throw new Error(
|
|
370
|
-
`
|
|
459
|
+
`credentials.encryptionKey is required by connector credentials: ${credentialConnectors.map((c) => c.id).join(", ")}`,
|
|
371
460
|
);
|
|
372
461
|
}
|
|
373
|
-
const credentialVault =
|
|
374
|
-
? new CredentialVault(storage,
|
|
462
|
+
const credentialVault = encryptionKey
|
|
463
|
+
? new CredentialVault(storage, encryptionKey)
|
|
375
464
|
: undefined;
|
|
376
465
|
const registry = new Registry(config.connectors, {
|
|
377
466
|
storage,
|
|
378
467
|
logger,
|
|
379
468
|
credentialVault,
|
|
380
|
-
toolCacheTtlSeconds: config.
|
|
381
|
-
persistToolCatalog: config.
|
|
382
|
-
toolCatalogStaleSeconds: config.
|
|
383
|
-
maxResultBytes: config.maxResultBytes,
|
|
384
|
-
credentialHealth: config.
|
|
469
|
+
toolCacheTtlSeconds: config.discovery?.catalogTtlSeconds,
|
|
470
|
+
persistToolCatalog: config.discovery?.persistCatalog,
|
|
471
|
+
toolCatalogStaleSeconds: config.discovery?.staleCatalogSeconds,
|
|
472
|
+
maxResultBytes: config.calls?.maxResultBytes,
|
|
473
|
+
credentialHealth: config.credentials?.health,
|
|
385
474
|
});
|
|
386
475
|
// Throws on every structural mistake it can see (see resolveToolkits): a
|
|
387
476
|
// typo must not become a scope the operator never wrote. Note this is about
|
|
@@ -404,12 +493,12 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
404
493
|
version: config.serverInfo?.version ?? CONNECTA_VERSION,
|
|
405
494
|
},
|
|
406
495
|
logger,
|
|
407
|
-
activity: config.activity,
|
|
408
|
-
activityReadGate: config.
|
|
409
|
-
activityDeploymentId: config.
|
|
496
|
+
activity: config.activity?.store,
|
|
497
|
+
activityReadGate: config.activity?.readGate,
|
|
498
|
+
activityDeploymentId: config.activity?.deploymentId,
|
|
410
499
|
executor: config.executor,
|
|
411
|
-
defaultToolTimeoutMs: config.
|
|
412
|
-
probeTimeoutMs: config.probeTimeoutMs,
|
|
500
|
+
defaultToolTimeoutMs: config.calls?.defaultTimeoutMs,
|
|
501
|
+
probeTimeoutMs: config.discovery?.probeTimeoutMs,
|
|
413
502
|
credentialVault,
|
|
414
503
|
deploymentInfo: config.deploymentInfo,
|
|
415
504
|
branding: config.branding,
|
package/src/meta-tools.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ActivityCallSource,
|
|
7
7
|
type ActivityRequestContext,
|
|
8
8
|
} from "./activity.js";
|
|
9
|
+
import { closeConnectorScope } from "./connector-scope.js";
|
|
9
10
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
10
11
|
import {
|
|
11
12
|
classifyCallError,
|
|
@@ -458,9 +459,9 @@ export interface SkillArgs {
|
|
|
458
459
|
* optional tenth tool, is registered separately by registerExecuteTool.)
|
|
459
460
|
*
|
|
460
461
|
* The deployment-wide result-size cap is read off the registry view rather than
|
|
461
|
-
* passed in: `ConnectaConfig.maxResultBytes` and the per-connector
|
|
462
|
-
* the only places a cap is set, so there is one answer to where a
|
|
463
|
-
* sets it (issue #44).
|
|
462
|
+
* passed in: `ConnectaConfig.calls.maxResultBytes` and the per-connector
|
|
463
|
+
* override are the only places a cap is set, so there is one answer to where a
|
|
464
|
+
* deployment sets it (issue #44).
|
|
464
465
|
*/
|
|
465
466
|
export function createMetaTools(
|
|
466
467
|
registry: RegistryView,
|
|
@@ -788,8 +789,12 @@ export function createMetaTools(
|
|
|
788
789
|
|
|
789
790
|
async listConnectors(args: ListArgs = {}): Promise<ToolResult> {
|
|
790
791
|
const probe = args.probe ?? true;
|
|
792
|
+
// Live inventory owns a short-lived scope separate from the request's
|
|
793
|
+
// call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
|
|
794
|
+
const connectors = registry.listConnectors();
|
|
795
|
+
const scope = probe ? {} : requestScope;
|
|
791
796
|
const out = await Promise.all(
|
|
792
|
-
|
|
797
|
+
connectors.map(async (c) => {
|
|
793
798
|
const statusStarted = Date.now();
|
|
794
799
|
const observed = registry.healthFor(c.id);
|
|
795
800
|
const verdict = await registry.credentialHealthFor(c.id);
|
|
@@ -799,7 +804,7 @@ export function createMetaTools(
|
|
|
799
804
|
if (probe) {
|
|
800
805
|
try {
|
|
801
806
|
status = await withTimeout(
|
|
802
|
-
registry.statusFor(c.id, baseUrl,
|
|
807
|
+
registry.statusFor(c.id, baseUrl, scope),
|
|
803
808
|
probeTimeoutMs,
|
|
804
809
|
`list_connectors probe of "${c.id}"`,
|
|
805
810
|
);
|
|
@@ -886,7 +891,7 @@ export function createMetaTools(
|
|
|
886
891
|
if (probe && status.state === "ok") {
|
|
887
892
|
try {
|
|
888
893
|
tools = await withTimeout(
|
|
889
|
-
registry.refreshTools(c.id, baseUrl,
|
|
894
|
+
registry.refreshTools(c.id, baseUrl, scope),
|
|
890
895
|
probeTimeoutMs,
|
|
891
896
|
`list_connectors catalog refresh of "${c.id}"`,
|
|
892
897
|
);
|
|
@@ -918,7 +923,17 @@ export function createMetaTools(
|
|
|
918
923
|
...(status.message ? { message: status.message } : {}),
|
|
919
924
|
};
|
|
920
925
|
}),
|
|
921
|
-
)
|
|
926
|
+
).finally(async () => {
|
|
927
|
+
if (!probe) return;
|
|
928
|
+
await Promise.all(
|
|
929
|
+
connectors.map((connector) =>
|
|
930
|
+
closeConnectorScope(
|
|
931
|
+
connector,
|
|
932
|
+
registry.contextFor(connector.id, baseUrl, scope),
|
|
933
|
+
),
|
|
934
|
+
),
|
|
935
|
+
);
|
|
936
|
+
});
|
|
922
937
|
return jsonResult({ connectors: out });
|
|
923
938
|
},
|
|
924
939
|
|
package/src/registry.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const MIN_MAX_RESULT_BYTES = 1;
|
|
|
33
33
|
/**
|
|
34
34
|
* The one definition of a usable `maxResultBytes`: a finite whole number of at
|
|
35
35
|
* least {@link MIN_MAX_RESULT_BYTES} bytes. Shared by all three intake points
|
|
36
|
-
* —
|
|
36
|
+
* — `calls.maxResultBytes`, the per-connector override, and `get_result`'s
|
|
37
37
|
* `maxBytes` argument — so a value that is valid at one is valid at all.
|
|
38
38
|
*
|
|
39
39
|
* Everything else is rejected rather than coerced, because each rejected shape
|
|
@@ -286,7 +286,7 @@ export class Registry implements RegistryView {
|
|
|
286
286
|
): void {
|
|
287
287
|
if (configured !== undefined && !isValidMaxResultBytes(configured)) {
|
|
288
288
|
logger.warn(
|
|
289
|
-
`[connecta] maxResultBytes ${configured} is not a whole number of ` +
|
|
289
|
+
`[connecta] calls.maxResultBytes ${configured} is not a whole number of ` +
|
|
290
290
|
`bytes >= ${MIN_MAX_RESULT_BYTES}: it would serve an empty, ` +
|
|
291
291
|
"oversized, or unguarded result instead of truncating. Using the " +
|
|
292
292
|
`default ${DEFAULT_MAX_RESULT_BYTES} instead.`,
|
|
@@ -612,7 +612,8 @@ export class Registry implements RegistryView {
|
|
|
612
612
|
|
|
613
613
|
/**
|
|
614
614
|
* Drop a connector's liveness verdict, because its credential just changed
|
|
615
|
-
* under us (OAuth callback completed, credential stored or removed
|
|
615
|
+
* under us (OAuth callback completed, credential stored or removed on
|
|
616
|
+
* /credentials). A
|
|
616
617
|
* stale `auth_required` must not outlive the re-authorization that fixed it —
|
|
617
618
|
* that is the difference between recovery working and needing a restart.
|
|
618
619
|
*/
|