@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +403 -0
  2. package/README.md +163 -308
  3. package/dist/auth/bearer.d.ts +4 -3
  4. package/dist/auth/bearer.d.ts.map +1 -1
  5. package/dist/auth/bearer.js +10 -8
  6. package/dist/auth/bearer.js.map +1 -1
  7. package/dist/auth/clerk.d.ts +8 -7
  8. package/dist/auth/clerk.d.ts.map +1 -1
  9. package/dist/auth/clerk.js +27 -8
  10. package/dist/auth/clerk.js.map +1 -1
  11. package/dist/connector-scope.d.ts +13 -0
  12. package/dist/connector-scope.d.ts.map +1 -0
  13. package/dist/connector-scope.js +35 -0
  14. package/dist/connector-scope.js.map +1 -0
  15. package/dist/connectors/api.d.ts +5 -5
  16. package/dist/connectors/api.d.ts.map +1 -1
  17. package/dist/connectors/remote-mcp.d.ts +3 -3
  18. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  19. package/dist/connectors/remote-mcp.js +309 -10
  20. package/dist/connectors/remote-mcp.js.map +1 -1
  21. package/dist/credential-health.d.ts +20 -9
  22. package/dist/credential-health.d.ts.map +1 -1
  23. package/dist/credential-health.js +127 -63
  24. package/dist/credential-health.js.map +1 -1
  25. package/dist/credentials.d.ts +84 -1
  26. package/dist/credentials.d.ts.map +1 -1
  27. package/dist/credentials.js +109 -2
  28. package/dist/credentials.js.map +1 -1
  29. package/dist/index.d.ts +83 -82
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +101 -31
  32. package/dist/index.js.map +1 -1
  33. package/dist/meta-tools.d.ts +3 -3
  34. package/dist/meta-tools.d.ts.map +1 -1
  35. package/dist/meta-tools.js +16 -7
  36. package/dist/meta-tools.js.map +1 -1
  37. package/dist/registry.d.ts +3 -2
  38. package/dist/registry.d.ts.map +1 -1
  39. package/dist/registry.js +4 -3
  40. package/dist/registry.js.map +1 -1
  41. package/dist/server.d.ts +1 -1
  42. package/dist/server.d.ts.map +1 -1
  43. package/dist/server.js +154 -52
  44. package/dist/server.js.map +1 -1
  45. package/dist/skills.js +2 -2
  46. package/dist/skills.js.map +1 -1
  47. package/dist/toolkits.js +1 -1
  48. package/dist/types.d.ts +51 -26
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/ui.d.ts +52 -21
  51. package/dist/ui.d.ts.map +1 -1
  52. package/dist/ui.js +665 -196
  53. package/dist/ui.js.map +1 -1
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/package.json +3 -2
  57. package/src/auth/bearer.ts +10 -8
  58. package/src/auth/clerk.ts +28 -9
  59. package/src/connector-scope.ts +41 -0
  60. package/src/connectors/api.ts +5 -5
  61. package/src/connectors/remote-mcp.ts +348 -25
  62. package/src/credential-health.ts +151 -71
  63. package/src/credentials.ts +166 -3
  64. package/src/index.ts +202 -113
  65. package/src/meta-tools.ts +22 -7
  66. package/src/registry.ts +4 -3
  67. package/src/server.ts +197 -71
  68. package/src/skills.ts +2 -2
  69. package/src/toolkits.ts +1 -1
  70. package/src/types.ts +51 -26
  71. package/src/ui.ts +703 -195
  72. package/src/version.ts +1 -1
package/src/index.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { CredentialVault } from "./credentials.js";
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
- * Base64-encoded 32-byte AES key for connector credentials managed in /ui.
84
- * Keep this in the runtime's secret store, never in KV or source control.
85
- */
86
- credentialEncryptionKey?: string;
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
- * `credentialHealth.intervalSeconds` ago is not re-checked unless `force`).
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`. Iterates connectors once.
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
- // /ui renders exactly one provider's browser sign-in config — the first that
332
- // offers one, which is the same `find` the /ui route performs — and that
333
- // provider's frontendApiUrl becomes the loader's `<script src>`. Gate-or-drop
334
- // like a branding href: rendering omits the loader for a rejected value and
335
- // the dashboard then reports that Clerk could not load, a confusing symptom
336
- // without this line naming the cause. Checking only the rendered provider
337
- // keeps the claim true a later provider's uiAuth never reaches the page, so
338
- // there is nothing there to warn about.
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 defaultsboth 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: the browser sign-in loader is ` +
345
- "fetched from this origin, so it must be an absolute https URL. /ui " +
346
- "renders without the loader and cannot start a sign-in.",
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 performs no state/CSRF check: the public
351
- // /oauth/callback/<id> route would exchange any delivered code.
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} will exchange any ` +
357
- "delivered code. Implement `verifyState` (the shipped remoteMcp " +
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
- if (credentialConnectors.length > 0 && !config.credentialEncryptionKey) {
456
+ const encryptionKey = config.credentials?.encryptionKey;
457
+ if (credentialConnectors.length > 0 && !encryptionKey) {
369
458
  throw new Error(
370
- `credentialEncryptionKey is required by connector credentials: ${credentialConnectors.map((c) => c.id).join(", ")}`,
459
+ `credentials.encryptionKey is required by connector credentials: ${credentialConnectors.map((c) => c.id).join(", ")}`,
371
460
  );
372
461
  }
373
- const credentialVault = config.credentialEncryptionKey
374
- ? new CredentialVault(storage, config.credentialEncryptionKey)
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.toolCacheTtlSeconds,
381
- persistToolCatalog: config.persistToolCatalog,
382
- toolCatalogStaleSeconds: config.toolCatalogStaleSeconds,
383
- maxResultBytes: config.maxResultBytes,
384
- credentialHealth: config.credentialHealth,
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.activityReadGate,
409
- activityDeploymentId: config.activityDeploymentId,
496
+ activity: config.activity?.store,
497
+ activityReadGate: config.activity?.readGate,
498
+ activityDeploymentId: config.activity?.deploymentId,
410
499
  executor: config.executor,
411
- defaultToolTimeoutMs: config.defaultToolTimeoutMs,
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 override are
462
- * the only places a cap is set, so there is one answer to where a deployment
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
- registry.listConnectors().map(async (c) => {
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, requestScope),
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, requestScope),
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
- * — deployment config, the per-connector override, and `get_result`'s
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 in /ui). A
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
  */