@zackbart/connecta 0.8.0 → 0.8.1

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 (143) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +31 -55
  3. package/dist/activity.d.ts +0 -8
  4. package/dist/activity.d.ts.map +1 -1
  5. package/dist/activity.js +0 -1
  6. package/dist/activity.js.map +1 -1
  7. package/dist/auth/bearer.d.ts +1 -7
  8. package/dist/auth/bearer.d.ts.map +1 -1
  9. package/dist/auth/bearer.js +2 -23
  10. package/dist/auth/bearer.js.map +1 -1
  11. package/dist/auth/clerk.d.ts +2 -11
  12. package/dist/auth/clerk.d.ts.map +1 -1
  13. package/dist/auth/clerk.js +31 -46
  14. package/dist/auth/clerk.js.map +1 -1
  15. package/dist/call-admission.d.ts.map +1 -1
  16. package/dist/call-admission.js +0 -1
  17. package/dist/call-admission.js.map +1 -1
  18. package/dist/catalog-service.d.ts +1 -2
  19. package/dist/catalog-service.d.ts.map +1 -1
  20. package/dist/catalog-service.js +12 -18
  21. package/dist/catalog-service.js.map +1 -1
  22. package/dist/catalog.d.ts +6 -0
  23. package/dist/catalog.d.ts.map +1 -1
  24. package/dist/catalog.js +51 -0
  25. package/dist/catalog.js.map +1 -1
  26. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  27. package/dist/connectors/remote-mcp.js +9 -22
  28. package/dist/connectors/remote-mcp.js.map +1 -1
  29. package/dist/errors.d.ts +18 -0
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/execute.d.ts +1 -8
  33. package/dist/execute.d.ts.map +1 -1
  34. package/dist/execute.js +3 -13
  35. package/dist/execute.js.map +1 -1
  36. package/dist/executor-result.js +1 -1
  37. package/dist/executor-result.js.map +1 -1
  38. package/dist/index.d.ts +3 -82
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +17 -90
  41. package/dist/index.js.map +1 -1
  42. package/dist/invocation.d.ts +5 -0
  43. package/dist/invocation.d.ts.map +1 -1
  44. package/dist/invocation.js +46 -10
  45. package/dist/invocation.js.map +1 -1
  46. package/dist/meta-tools.d.ts +3 -11
  47. package/dist/meta-tools.d.ts.map +1 -1
  48. package/dist/meta-tools.js +105 -157
  49. package/dist/meta-tools.js.map +1 -1
  50. package/dist/operator-ui/generated.d.ts +2 -2
  51. package/dist/operator-ui/generated.d.ts.map +1 -1
  52. package/dist/operator-ui/generated.js +2 -2
  53. package/dist/operator-ui/generated.js.map +1 -1
  54. package/dist/operator-ui/model.d.ts +1 -22
  55. package/dist/operator-ui/model.d.ts.map +1 -1
  56. package/dist/operator-ui/model.js.map +1 -1
  57. package/dist/registry.d.ts +11 -178
  58. package/dist/registry.d.ts.map +1 -1
  59. package/dist/registry.js +52 -330
  60. package/dist/registry.js.map +1 -1
  61. package/dist/retired-toolkits.d.ts +3 -0
  62. package/dist/retired-toolkits.d.ts.map +1 -0
  63. package/dist/retired-toolkits.js +9 -0
  64. package/dist/retired-toolkits.js.map +1 -0
  65. package/dist/routes/activity.d.ts.map +1 -1
  66. package/dist/routes/activity.js +2 -5
  67. package/dist/routes/activity.js.map +1 -1
  68. package/dist/routes/credentials.d.ts.map +1 -1
  69. package/dist/routes/credentials.js +1 -12
  70. package/dist/routes/credentials.js.map +1 -1
  71. package/dist/routes/mcp.d.ts.map +1 -1
  72. package/dist/routes/mcp.js +32 -145
  73. package/dist/routes/mcp.js.map +1 -1
  74. package/dist/routes/oauth.d.ts.map +1 -1
  75. package/dist/routes/oauth.js +2 -7
  76. package/dist/routes/oauth.js.map +1 -1
  77. package/dist/routes/shared.d.ts +7 -28
  78. package/dist/routes/shared.d.ts.map +1 -1
  79. package/dist/routes/shared.js +10 -69
  80. package/dist/routes/shared.js.map +1 -1
  81. package/dist/routes/ui.d.ts.map +1 -1
  82. package/dist/routes/ui.js +4 -10
  83. package/dist/routes/ui.js.map +1 -1
  84. package/dist/server.d.ts.map +1 -1
  85. package/dist/server.js +1 -31
  86. package/dist/server.js.map +1 -1
  87. package/dist/skills.d.ts +3 -6
  88. package/dist/skills.d.ts.map +1 -1
  89. package/dist/skills.js +9 -38
  90. package/dist/skills.js.map +1 -1
  91. package/dist/timeout.d.ts.map +1 -1
  92. package/dist/timeout.js +2 -4
  93. package/dist/timeout.js.map +1 -1
  94. package/dist/types.d.ts +2 -59
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/ui.d.ts +1 -2
  97. package/dist/ui.d.ts.map +1 -1
  98. package/dist/ui.js +5 -35
  99. package/dist/ui.js.map +1 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/package.json +1 -1
  103. package/src/activity.ts +0 -9
  104. package/src/auth/bearer.ts +3 -32
  105. package/src/auth/clerk.ts +35 -48
  106. package/src/call-admission.ts +0 -1
  107. package/src/catalog-service.ts +23 -23
  108. package/src/catalog.ts +55 -0
  109. package/src/connectors/remote-mcp.ts +11 -23
  110. package/src/errors.ts +20 -0
  111. package/src/execute.ts +3 -18
  112. package/src/executor-result.ts +1 -1
  113. package/src/index.ts +24 -208
  114. package/src/invocation.ts +63 -10
  115. package/src/meta-tools.ts +122 -174
  116. package/src/operator-ui/browser.css +1 -54
  117. package/src/operator-ui/browser.ts +16 -150
  118. package/src/operator-ui/generated.ts +2 -2
  119. package/src/operator-ui/model.ts +1 -23
  120. package/src/registry.ts +59 -429
  121. package/src/retired-toolkits.ts +14 -0
  122. package/src/routes/activity.ts +1 -6
  123. package/src/routes/credentials.ts +0 -12
  124. package/src/routes/mcp.ts +40 -186
  125. package/src/routes/oauth.ts +1 -7
  126. package/src/routes/shared.ts +8 -98
  127. package/src/routes/ui.ts +2 -12
  128. package/src/server.ts +1 -31
  129. package/src/skills.ts +9 -38
  130. package/src/timeout.ts +2 -4
  131. package/src/types.ts +1 -60
  132. package/src/ui.ts +9 -46
  133. package/src/version.ts +1 -1
  134. package/dist/credential-health.d.ts +0 -205
  135. package/dist/credential-health.d.ts.map +0 -1
  136. package/dist/credential-health.js +0 -610
  137. package/dist/credential-health.js.map +0 -1
  138. package/dist/toolkits.d.ts +0 -124
  139. package/dist/toolkits.d.ts.map +0 -1
  140. package/dist/toolkits.js +0 -322
  141. package/dist/toolkits.js.map +0 -1
  142. package/src/credential-health.ts +0 -835
  143. package/src/toolkits.ts +0 -461
package/src/catalog.ts CHANGED
@@ -17,6 +17,61 @@ function normalized(text: string): string {
17
17
  return text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
18
18
  }
19
19
 
20
+ /**
21
+ * Conversational framing selected by the #188 research run before the #189
22
+ * holdout existed. Action-bearing terms such as get/list/search/find/create
23
+ * deliberately remain: arbitrary connector catalogs need those distinctions.
24
+ */
25
+ const CONVERSATIONAL_QUERY_WORDS = new Set([
26
+ "a",
27
+ "all",
28
+ "an",
29
+ "and",
30
+ "are",
31
+ "can",
32
+ "could",
33
+ "current",
34
+ "for",
35
+ "from",
36
+ "i",
37
+ "in",
38
+ "into",
39
+ "it",
40
+ "latest",
41
+ "let",
42
+ "me",
43
+ "most",
44
+ "of",
45
+ "on",
46
+ "our",
47
+ "please",
48
+ "right",
49
+ "show",
50
+ "that",
51
+ "the",
52
+ "this",
53
+ "to",
54
+ "up",
55
+ "want",
56
+ "when",
57
+ "which",
58
+ "with",
59
+ "you",
60
+ ]);
61
+
62
+ /**
63
+ * Remove conversational framing before the all-term/partial decision. If
64
+ * every term is framing, retain the original query rather than turning a
65
+ * search into an unfiltered catalog browse.
66
+ */
67
+ export function lexicalSearchQuery(query: string): string {
68
+ const terms = normalized(query).split(/\s+/).filter(Boolean);
69
+ const contentTerms = terms.filter(
70
+ (term) => !CONVERSATIONAL_QUERY_WORDS.has(term),
71
+ );
72
+ return contentTerms.length > 0 ? contentTerms.join(" ") : query;
73
+ }
74
+
20
75
  interface SearchDocument {
21
76
  tool: ToolDef;
22
77
  name: string;
@@ -190,8 +190,7 @@ function msg(err: unknown): string {
190
190
  * `Client.close()` only unwinds our side — it aborts the transport's controller
191
191
  * and fires `onclose`. Spec session termination is a separate DELETE carrying
192
192
  * `Mcp-Session-Id`, and without it a stateful provider keeps the session alive
193
- * until its own (often hour-long) timeout, which a periodic probe would then
194
- * accumulate several of per connector.
193
+ * until its own (often hour-long) timeout, accumulating abandoned sessions.
195
194
  *
196
195
  * Ordering is load-bearing: the SDK sends that DELETE on the transport's
197
196
  * AbortSignal, so calling this *after* close would abort the request on issue
@@ -468,6 +467,14 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
468
467
  const scopeEndedError = () =>
469
468
  new Error(`Connector "${id}" scope ended during connection.`);
470
469
 
470
+ const requestOptions = (ctx: ConnectorContext) =>
471
+ ctx.timeoutMs || ctx.signal
472
+ ? {
473
+ ...(ctx.timeoutMs ? { timeout: ctx.timeoutMs } : {}),
474
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
475
+ }
476
+ : undefined;
477
+
471
478
  /**
472
479
  * One `tools/list` request. The SDK schema is retained wholesale except for
473
480
  * accepting `null` as the common, unambiguous end-of-chain spelling. Other
@@ -485,12 +492,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
485
492
  ...(cursor === undefined ? {} : { params: { cursor } }),
486
493
  },
487
494
  CompatibleListToolsResultSchema,
488
- ctx.timeoutMs || ctx.signal
489
- ? {
490
- ...(ctx.timeoutMs ? { timeout: ctx.timeoutMs } : {}),
491
- ...(ctx.signal ? { signal: ctx.signal } : {}),
492
- }
493
- : undefined,
495
+ requestOptions(ctx),
494
496
  );
495
497
  } catch (err) {
496
498
  if (!isCursorShapeError(err)) throw err;
@@ -910,12 +912,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
910
912
  arguments: (args ?? {}) as Record<string, unknown>,
911
913
  },
912
914
  undefined,
913
- ctx.timeoutMs || ctx.signal
914
- ? {
915
- ...(ctx.timeoutMs ? { timeout: ctx.timeoutMs } : {}),
916
- ...(ctx.signal ? { signal: ctx.signal } : {}),
917
- }
918
- : undefined,
915
+ requestOptions(ctx),
919
916
  );
920
917
  } catch (err) {
921
918
  // A grant revoked after connect surfaces here, not in ensureConnected.
@@ -998,15 +995,6 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
998
995
  };
999
996
 
1000
997
  if (opts.auth?.type === "oauth") {
1001
- // The credential liveness checks (issue #24) probe only connectors that
1002
- // actually hold a stored grant: with no tokens there is nothing whose
1003
- // liveness could have lapsed, and a `status()` probe would kick off DCR +
1004
- // consent on a timer for a connector nobody has authorized yet.
1005
- connector.hasStoredCredential = async (ctx) => {
1006
- const state = stateFor(ctx);
1007
- return (await getProvider(ctx, state).tokens()) !== undefined;
1008
- };
1009
-
1010
998
  connector.verifyState = async (oauthState, ctx) => {
1011
999
  const state = stateFor(ctx);
1012
1000
  return getProvider(ctx, state).verifyState(oauthState);
package/src/errors.ts CHANGED
@@ -10,6 +10,12 @@ export type ConnectorCallErrorCode =
10
10
  | "invalid_args"
11
11
  | "connector_call_failed";
12
12
 
13
+ /** Agent-visible recovery class attached only to `auth_required` failures. */
14
+ export type AuthRecoveryMode =
15
+ | "oauth"
16
+ | "operator_config"
17
+ | "unavailable";
18
+
13
19
  const RETRYABLE_BY_CODE: Record<ConnectorCallErrorCode, boolean> = {
14
20
  timeout: true,
15
21
  rate_limited: true,
@@ -78,6 +84,20 @@ export interface CallErrorDetails {
78
84
  * window so it can schedule a re-issue.
79
85
  */
80
86
  retryAfterMs?: number;
87
+ /** Connector whose failed operation needs recovery. */
88
+ connector?: string;
89
+ /** Canonical downstream address the agent may retry after recovery. */
90
+ operation?: string;
91
+ /** Which safe recovery path `authorize_connector` will return. */
92
+ recovery?: AuthRecoveryMode;
93
+ /** The single model-facing entry point for every credential class. */
94
+ nextAction?: {
95
+ tool: "authorize_connector";
96
+ arguments: { connector: string };
97
+ operatorHandoff: string;
98
+ };
99
+ /** Explicit retry guidance; recovery never retries or mutates by itself. */
100
+ retry?: string;
81
101
  }
82
102
 
83
103
  const RETRYABLE_MESSAGE_RE =
package/src/execute.ts CHANGED
@@ -2,7 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import type { ActivityRequestContext } from "./activity.js";
4
4
  import {
5
- assertDiscoveryResultSize,
5
+ boundedDiscoveryText,
6
6
  CatalogService,
7
7
  flatSearchResult,
8
8
  } from "./catalog-service.js";
@@ -16,14 +16,12 @@ import {
16
16
  ExecutorAdmissionError,
17
17
  isAdmittingExecutor,
18
18
  } from "./executor-admission.js";
19
- import { unwrapMcpResult } from "./mcp-result.js";
20
19
  import {
21
20
  InvocationFailure,
22
21
  InvocationService,
23
22
  } from "./invocation.js";
24
23
  import type { RegistryView } from "./registry.js";
25
24
  import type {
26
- Connector,
27
25
  Executor,
28
26
  ExecutorProvider,
29
27
  Logger,
@@ -137,19 +135,6 @@ ${declarations}
137
135
  })();`;
138
136
  }
139
137
 
140
- /**
141
- * Unwrap an MCP CallToolResult so sandbox code sees plain values:
142
- * isError throws (a real exception the code can catch), structuredContent
143
- * wins when present, all-text content is JSON.parsed when possible.
144
- * Non-MCP connectors already return plain values.
145
- */
146
- export function unwrapForSandbox(
147
- kind: Connector["kind"],
148
- result: unknown,
149
- ): unknown {
150
- return unwrapMcpResult(kind, result);
151
- }
152
-
153
138
  /**
154
139
  * Expose one fixed host provider plus trusted sandbox setup that creates a
155
140
  * lazy proxy global per connector. No connector catalog is touched until code
@@ -287,7 +272,7 @@ export async function buildSandboxProviders(
287
272
  includeSchemas?: "compact" | "json";
288
273
  };
289
274
  const result = flatSearchResult(await catalog.search(args));
290
- assertDiscoveryResultSize(
275
+ boundedDiscoveryText(
291
276
  result,
292
277
  "Request a smaller limit, omit fullDescriptions, or use compact schemas.",
293
278
  );
@@ -300,7 +285,7 @@ export async function buildSandboxProviders(
300
285
  fullDescriptions?: boolean;
301
286
  };
302
287
  const result = { tools: await catalog.describe(args) };
303
- assertDiscoveryResultSize(
288
+ boundedDiscoveryText(
304
289
  result,
305
290
  'Split the address list or use format: "compact".',
306
291
  );
@@ -9,7 +9,7 @@ function msg(err: unknown): string {
9
9
  }
10
10
 
11
11
  function serializeExecuteValue(value: unknown): string {
12
- const serialized = JSON.stringify(value, null, 2);
12
+ const serialized = JSON.stringify(value);
13
13
  return serialized === undefined ? String(value) : serialized;
14
14
  }
15
15
 
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 and proactive credential-health tuning. */
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 and proactive liveness-check settings. */
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
- throw new Error(
545
- `credentials.encryptionKey is required by connector credentials: ${credentialConnectors.map((c) => c.id).join(", ")}`,
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
- // Same contract for the identity half: a binding that names a toolkit this
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,