@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/routes/mcp.ts CHANGED
@@ -7,14 +7,11 @@ import {
7
7
  type AdmissionLease,
8
8
  } from "../executor-admission.js";
9
9
  import { registerMetaTools } from "../meta-tools.js";
10
- import { ScopedRegistry, type Registry, type RegistryView } from "../registry.js";
10
+ import type { RegistryView } from "../registry.js";
11
11
  import { CONNECTA_INSTRUCTIONS } from "../skills.js";
12
- import { TOOLKIT_NAME_RE, type Toolkit } from "../toolkits.js";
13
- import type { Logger, ToolkitBinding } from "../types.js";
12
+ import type { Logger } from "../types.js";
14
13
  import {
15
- MAX_ECHOED_TOOLKIT_NAME,
16
14
  authorize,
17
- loggableValue,
18
15
  type RouteContext,
19
16
  type RuntimeExecutionContext,
20
17
  type ServerOptions,
@@ -142,167 +139,45 @@ function releaseAdmissionWithResponse(
142
139
  });
143
140
  }
144
141
 
145
- /** What one MCP connection may see: the full registry, or one toolkit's view. */
146
- interface McpScope {
147
- registry: RegistryView;
148
- /** Set only under `?toolkit=`; recorded on activity events. */
149
- toolkitId?: string;
150
- }
151
-
152
- /** The one refusal a bound identity ever sees. Constant on purpose — see below. */
153
- const TOOLKIT_FORBIDDEN_BODY = JSON.stringify({
154
- jsonrpc: "2.0",
155
- id: null,
156
- error: {
157
- code: -32600,
158
- message:
159
- "Not permitted to use the requested toolkit. This credential is bound " +
160
- "to a specific toolkit — check the ?toolkit= value in this deployment's " +
161
- "MCP endpoint URL with the operator.",
162
- },
163
- });
164
-
165
- /**
166
- * 403 for every binding refusal, with a body that does not depend on WHY.
167
- *
168
- * A bound identity asking for a toolkit it may not open, for a toolkit that does
169
- * not exist, or for no toolkit at all gets byte-identical responses, so a team
170
- * credential cannot be used to enumerate the org's other teams — the boundary
171
- * would leak the very structure it exists to hide. The operator log below is
172
- * where the three cases are told apart.
173
- */
174
- function toolkitForbidden(): Response {
175
- return new Response(TOOLKIT_FORBIDDEN_BODY, {
176
- status: 403,
177
- headers: {
178
- "Content-Type": "application/json",
179
- "Cache-Control": "no-store",
180
- },
181
- });
182
- }
183
-
184
- /** How a rejected connection is named in the operator log. */
185
- function identityLabel(actor: ActivityActor): string {
186
- return actor.id ? `${actor.kind} ${loggableValue(actor.id)}` : actor.kind;
187
- }
188
-
189
142
  /**
190
- * Resolve `?toolkit=<name>` into the registry view this connection may see,
191
- * enforcing the caller's toolkit binding (documentation/toolkits.md) on the way.
192
- *
193
- * For an UNBOUND identity (no binding configured — the pre-#37 shape):
194
- *
195
- * - absent → the full registry, byte-identical to a deployment with no toolkits
196
- * - known → a `ScopedRegistry` over that toolkit (the one visibility boundary)
197
- * - anything else, including `?toolkit=` with an empty value → an explicit
198
- * 404. Never a silent fallback to the full registry.
143
+ * 404 for any `?toolkit=` value, kept after the feature's retirement (#178).
199
144
  *
200
- * For a BOUND identity, membership is checked FIRST and refusal is a flat 403:
201
- * a toolkit outside the binding, an unknown name, and (without `unscoped`) an
202
- * omitted `?toolkit=` are all refused before any `ScopedRegistry` is built, and
203
- * all three produce the same response.
204
- *
205
- * Neither error enumerates the configured toolkits: the name selects a scope, so
206
- * a wrong guess gets a flat refusal, not a directory.
207
- *
208
- * Because of that — and because SDK clients treat a 404/403 on the transport
209
- * endpoint as a transport failure and discard the body — every rejection is also
210
- * logged operator-side (issue #47), which is the channel that actually reaches a
211
- * human. The log line may name the configured or bound toolkits; the response
212
- * still may not.
145
+ * Toolkits are gone, but the URLs that named them are not: clients were handed
146
+ * MCP endpoint URLs with `?toolkit=` baked in, and nothing about upgrading the
147
+ * server rotates them. Silently serving those clients the full registry would
148
+ * turn a former scoping boundary into fail-open — so a request still sending
149
+ * the param gets the same explicit 404 an unknown toolkit got before, plus an
150
+ * operator-side log line, which (as ever issue #47) is the channel that
151
+ * actually reaches a human.
213
152
  */
214
- function resolveToolkitScope(
215
- url: URL,
216
- registry: Registry,
217
- toolkits: ReadonlyMap<string, Toolkit> | undefined,
218
- logger: Logger,
219
- identity: { actor: ActivityActor; binding?: ToolkitBinding },
220
- ):
221
- | { ok: true; scope: McpScope }
222
- | { ok: false; response: Response } {
223
- const requested = url.searchParams.get("toolkit");
224
- const binding = identity.binding;
225
- const scopeFor = (toolkit: Toolkit) => ({
226
- ok: true as const,
227
- scope: {
228
- registry: new ScopedRegistry(registry, toolkit),
229
- toolkitId: toolkit.name,
230
- },
231
- });
232
-
233
- if (binding) {
234
- const who = identityLabel(identity.actor);
235
- const bound = `Bound toolkits: ${binding.toolkits.join(", ") || "(none)"}${
236
- binding.unscoped ? ", plus unscoped access" : ""
237
- }.`;
238
- if (requested === null) {
239
- if (binding.unscoped) return { ok: true, scope: { registry } };
240
- logger.warn(
241
- `[connecta] refused an unscoped /mcp connection from ${who} with 403: ` +
242
- "its toolkit binding does not allow the full registry. " +
243
- bound +
244
- " The client sees a transport-level failure and never the reason, so " +
245
- "give it an MCP endpoint URL with a ?toolkit= value it is bound to.",
246
- );
247
- return { ok: false, response: toolkitForbidden() };
248
- }
249
- const permitted = binding.toolkits.includes(requested);
250
- const toolkit = permitted ? toolkits?.get(requested) : undefined;
251
- if (toolkit) return scopeFor(toolkit);
252
- logger.warn(
253
- `[connecta] refused an /mcp connection from ${who} with 403: it asked ` +
254
- `for toolkit ${loggableValue(requested)}, which ` +
255
- (permitted
256
- ? "its binding allows but this deployment does not configure"
257
- : "its toolkit binding does not include") +
258
- ". " +
259
- bound +
260
- " The client sees a transport-level failure and never the reason, so " +
261
- "check the ?toolkit= value in its MCP endpoint URL.",
262
- );
263
- return { ok: false, response: toolkitForbidden() };
264
- }
265
-
266
- if (requested === null) return { ok: true, scope: { registry } };
267
- const toolkit = toolkits?.get(requested);
268
- if (toolkit) return scopeFor(toolkit);
269
- const configured = toolkits && toolkits.size > 0 ? [...toolkits.keys()] : [];
153
+ function toolkitRetired(logger: Logger): Response {
270
154
  logger.warn(
271
- "[connecta] rejected an /mcp connection asking for unknown toolkit " +
272
- `${loggableValue(requested)} with 404. ` +
273
- (configured.length > 0
274
- ? `Configured toolkits: ${configured.join(", ")}.`
275
- : "This deployment configures no toolkits, so no ?toolkit= value is accepted.") +
276
- " The client sees a transport-level failure and never the reason, so " +
277
- "check the ?toolkit= value in its MCP endpoint URL.",
155
+ "[connecta] rejected an /mcp connection carrying ?toolkit= with 404: " +
156
+ "toolkits were retired in issue #178 (see ethos.md) one deployment " +
157
+ "serves one audience. The client sees a transport-level failure and " +
158
+ "never the reason, so remove the ?toolkit= value from its MCP endpoint " +
159
+ "URL, or point it at the deployment for its audience.",
278
160
  );
279
- const label =
280
- requested.length <= MAX_ECHOED_TOOLKIT_NAME &&
281
- TOOLKIT_NAME_RE.test(requested)
282
- ? `"${requested}"`
283
- : "requested";
284
- return {
285
- ok: false,
286
- response: new Response(
287
- JSON.stringify({
288
- jsonrpc: "2.0",
289
- id: null,
290
- error: {
291
- code: -32600,
292
- message:
293
- `Unknown toolkit ${label}. Check the ?toolkit= value in this ` +
294
- "deployment's MCP endpoint URL with the operator.",
295
- },
296
- }),
297
- {
298
- status: 404,
299
- headers: {
300
- "Content-Type": "application/json",
301
- "Cache-Control": "no-store",
302
- },
161
+ return new Response(
162
+ JSON.stringify({
163
+ jsonrpc: "2.0",
164
+ id: null,
165
+ error: {
166
+ code: -32600,
167
+ message:
168
+ "This deployment does not accept ?toolkit=. Toolkits were retired " +
169
+ "in issue #178 — remove the ?toolkit= value from the MCP endpoint " +
170
+ "URL, or ask the operator for the deployment serving this audience.",
303
171
  },
304
- ),
305
- };
172
+ }),
173
+ {
174
+ status: 404,
175
+ headers: {
176
+ "Content-Type": "application/json",
177
+ "Cache-Control": "no-store",
178
+ },
179
+ },
180
+ );
306
181
  }
307
182
 
308
183
  async function serveMcp(
@@ -310,7 +185,7 @@ async function serveMcp(
310
185
  opts: ServerOptions,
311
186
  baseUrl: string,
312
187
  actor: ActivityActor,
313
- scope: McpScope,
188
+ registry: RegistryView,
314
189
  runtimeContext?: RuntimeExecutionContext,
315
190
  ): Promise<Response> {
316
191
  // Fresh McpServer + transport per request (SDK ≥1.26 requirement), stateless.
@@ -326,16 +201,12 @@ async function serveMcp(
326
201
  ...(opts.activityDeploymentId
327
202
  ? { deploymentId: opts.activityDeploymentId }
328
203
  : {}),
329
- ...(scope.toolkitId ? { toolkitId: scope.toolkitId } : {}),
330
204
  ...(runtimeContext?.waitUntil
331
205
  ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
332
206
  : {}),
333
207
  logger: opts.logger,
334
208
  }
335
209
  : undefined;
336
- // `scope.registry` is the connection's VIEW — the full registry, or one
337
- // toolkit's ScopedRegistry. Nothing below may reach for `opts.registry`.
338
- const registry = scope.registry;
339
210
  registerMetaTools(server, registry, {
340
211
  baseUrl,
341
212
  ...(activity ? { activity } : {}),
@@ -399,10 +270,8 @@ export function createMcpRoute(
399
270
  const {
400
271
  path,
401
272
  request,
402
- url,
403
273
  baseUrl,
404
274
  runtimeContext,
405
- sweepCredentials,
406
275
  } = context;
407
276
  if (path !== "/mcp") return null;
408
277
  let admission: AdmissionLease;
@@ -433,9 +302,7 @@ export function createMcpRoute(
433
302
  throw error;
434
303
  }
435
304
  try {
436
- // Authenticate BEFORE resolving ?toolkit=: an unauthenticated caller
437
- // must not be able to probe which toolkit names exist.
438
- const authz = await authorize(request, baseUrl, opts.auth, opts.logger);
305
+ const authz = await authorize(request, baseUrl, opts.auth);
439
306
  if (!authz.ok) {
440
307
  return releaseAdmissionWithResponse(
441
308
  withMcpCors(authz.response),
@@ -443,26 +310,13 @@ export function createMcpRoute(
443
310
  request.signal,
444
311
  );
445
312
  }
446
- const selected = resolveToolkitScope(
447
- url,
448
- opts.registry,
449
- opts.toolkits,
450
- opts.logger,
451
- {
452
- actor: authz.actor,
453
- ...(authz.toolkitBinding
454
- ? { binding: authz.toolkitBinding }
455
- : {}),
456
- },
457
- );
458
- if (!selected.ok) {
313
+ if (new URL(request.url).searchParams.has("toolkit")) {
459
314
  return releaseAdmissionWithResponse(
460
- withMcpCors(selected.response),
315
+ withMcpCors(toolkitRetired(opts.logger)),
461
316
  admission,
462
317
  request.signal,
463
318
  );
464
319
  }
465
- sweepCredentials();
466
320
  return releaseAdmissionWithResponse(
467
321
  withMcpCors(
468
322
  await serveMcp(
@@ -470,7 +324,7 @@ export function createMcpRoute(
470
324
  opts,
471
325
  baseUrl,
472
326
  authz.actor,
473
- selected.scope,
327
+ opts.registry,
474
328
  runtimeContext,
475
329
  ),
476
330
  ),
@@ -30,7 +30,6 @@ async function handleOAuthManagementRequest(
30
30
  request,
31
31
  baseUrl,
32
32
  opts.auth,
33
- opts.logger,
34
33
  "OAuth management",
35
34
  );
36
35
  if (!admin.ok) return admin.response;
@@ -61,11 +60,10 @@ async function handleOAuthManagementRequest(
61
60
  operationError = error;
62
61
  }
63
62
 
64
- // The old grant and its catalog verdict are invalid after either operation,
63
+ // The old grant and its cached catalog are invalid after either operation,
65
64
  // including a partially failed physical cleanup whose epoch fence succeeded.
66
65
  try {
67
66
  await opts.registry.invalidateStored(connectorId);
68
- await opts.registry.clearCredentialHealth(connectorId);
69
67
  } catch (error) {
70
68
  operationError ??= error;
71
69
  }
@@ -313,10 +311,6 @@ export async function routeOAuthCallback(
313
311
  try {
314
312
  await connector.finishAuth(code, connectorContext);
315
313
  await opts.registry.invalidateStored(id);
316
- // Recovery, without a restart: the grant this connector was reported dead
317
- // for has just been replaced, so drop the verdict rather than let a stale
318
- // `auth_required` survive until the next scheduled check.
319
- await opts.registry.clearCredentialHealth(id);
320
314
  return html(
321
315
  `Connected "${id}". You can close this window.`,
322
316
  200,
@@ -4,13 +4,11 @@ import type { CredentialVault } from "../credentials.js";
4
4
  import type { DeferredWork } from "../connector-scope.js";
5
5
  import type { AdmissionController } from "../executor-admission.js";
6
6
  import type { Registry } from "../registry.js";
7
- import { resolveIdentityBinding, type Toolkit } from "../toolkits.js";
8
7
  import type {
9
8
  ConnectaBranding,
10
9
  Executor,
11
10
  InboundAuth,
12
11
  Logger,
13
- ToolkitBinding,
14
12
  } from "../types.js";
15
13
  import { operatorPageForPath } from "../ui.js";
16
14
 
@@ -40,11 +38,6 @@ export interface ServerOptions {
40
38
  credentialVault?: CredentialVault;
41
39
  /** Optional browser UI and OAuth result-page labels. */
42
40
  branding?: ConnectaBranding;
43
- /**
44
- * Validated named scopes, selected per connection with `?toolkit=<name>` on
45
- * `/mcp`. Omit (or leave empty) and every connection sees the full registry.
46
- */
47
- toolkits?: ReadonlyMap<string, Toolkit>;
48
41
  }
49
42
 
50
43
  export interface RuntimeExecutionContext {
@@ -59,7 +52,6 @@ export interface RouteContext {
59
52
  opts: ServerOptions;
60
53
  defer: DeferredWork | undefined;
61
54
  runtimeContext: RuntimeExecutionContext | undefined;
62
- sweepCredentials(): void;
63
55
  }
64
56
 
65
57
  export function msg(err: unknown): string {
@@ -80,13 +72,13 @@ export function privateJson(
80
72
  const MAX_LOGGED_VALUE_LENGTH = 64;
81
73
 
82
74
  /**
83
- * Bounded, escaped form of a caller-influenced value (a rejected toolkit name,
84
- * identity id, or OAuth callback connector id) for the operator log. Goes
75
+ * Bounded, escaped form of a caller-influenced value (an identity id or OAuth
76
+ * callback connector id) for the operator log. Goes
85
77
  * through JSON.stringify so a caller-controlled newline or control character
86
78
  * cannot forge a log line, plus a hand-rolled escape for U+2028/U+2029, which
87
79
  * JSON.stringify leaves raw even though a log reader treats them as line
88
- * terminators. Truncated to a small shared cap (also the toolkit response's echo
89
- * limit), so an oversized value cannot flood the log either.
80
+ * terminators. Truncated to a small shared cap so an oversized value cannot
81
+ * flood the log either.
90
82
  */
91
83
  export function loggableValue(requested: string): string {
92
84
  const bounded = requested.slice(0, MAX_LOGGED_VALUE_LENGTH);
@@ -97,9 +89,6 @@ export function loggableValue(requested: string): string {
97
89
  return escaped + (bounded.length < requested.length ? " (truncated)" : "");
98
90
  }
99
91
 
100
- /** Length beyond which a rejected toolkit name is not echoed back. */
101
- export const MAX_ECHOED_TOOLKIT_NAME = MAX_LOGGED_VALUE_LENGTH;
102
-
103
92
  const ACTIVITY_ACTOR_NAMESPACE_RE = /^[\x21-\x7e]{1,256}$/;
104
93
 
105
94
  export function activityActorNamespace(
@@ -111,29 +100,16 @@ export function activityActorNamespace(
111
100
  : undefined;
112
101
  }
113
102
 
114
- /**
115
- * Refusal for an identity whose toolkit binding cannot be trusted — a malformed
116
- * declaration, or a malformed per-identity binding out of `authorize`. The
117
- * caller is authenticated, so this is a 403, and it is deliberately opaque: the
118
- * cause is an operator bug, and the operator reads it in the log, not the client.
119
- */
120
- function unusableBinding(): Response {
121
- return privateJson({ error: "forbidden" }, { status: 403 });
122
- }
123
-
124
103
  export async function authorize(
125
104
  request: Request,
126
105
  baseUrl: string,
127
106
  auth: InboundAuth[],
128
- logger: Logger,
129
107
  ): Promise<
130
108
  | {
131
109
  ok: true;
132
110
  actor: ActivityActor;
133
111
  /** True only when the admitting provider can also authorize UI mutation. */
134
112
  uiAdminEligible?: boolean;
135
- /** The admitting identity's toolkit binding (documentation/toolkits.md). */
136
- toolkitBinding?: ToolkitBinding;
137
113
  }
138
114
  | { ok: false; response: Response }
139
115
  > {
@@ -145,24 +121,6 @@ export async function authorize(
145
121
  const result = await provider.authorize(request, baseUrl);
146
122
  if (result.ok) {
147
123
  const subjectId = result.subjectId ?? result.userId;
148
- // Re-validate both halves and cap the per-identity one by the provider's
149
- // declaration (see resolveIdentityBinding). A binding that does not
150
- // type-check at runtime refuses the request rather than evaporating:
151
- // dropping it would hand the caller the full registry, which is the one
152
- // outcome a binding exists to prevent.
153
- const binding = resolveIdentityBinding(
154
- provider.toolkitBinding,
155
- result.toolkitBinding,
156
- );
157
- if (!binding.ok) {
158
- logger.warn(
159
- `[connecta] refused a request admitted by inbound auth provider ` +
160
- `"${provider.kind}" with 403: ${binding.reason}. Until it is fixed ` +
161
- "this provider cannot admit anyone, because connecta cannot tell " +
162
- "which toolkits the identity may use.",
163
- );
164
- return { ok: false, response: unusableBinding() };
165
- }
166
124
  const actorNamespace = activityActorNamespace(provider);
167
125
  return {
168
126
  ok: true,
@@ -176,7 +134,6 @@ export async function authorize(
176
134
  ...(result.userId && provider.uiAuth?.kind === "clerk"
177
135
  ? { uiAdminEligible: true }
178
136
  : {}),
179
- ...(binding.binding ? { toolkitBinding: binding.binding } : {}),
180
137
  };
181
138
  }
182
139
  lastResponse = result.response;
@@ -195,49 +152,20 @@ export async function authorize(
195
152
  };
196
153
  }
197
154
 
198
- /**
199
- * True when this identity is confined to one or more toolkits — bound, without
200
- * `unscoped`. Such a credential belongs to a team's agent, not to the operator
201
- * running the deployment, so the deployment-wide operator surfaces (`/ui/data`,
202
- * `/ui/activity`, the credential API) refuse it: their payloads describe every
203
- * connector in the org, which is exactly what the binding exists to withhold.
204
- */
205
- export function isToolkitRestricted(
206
- binding: ToolkitBinding | undefined,
207
- ): boolean {
208
- return Boolean(binding && !binding.unscoped);
209
- }
210
-
211
- /** The refusal the deployment-wide operator surfaces give a bound identity. */
212
- export function restrictedOperatorSurface(): Response {
213
- return privateJson(
214
- {
215
- error:
216
- "this credential is bound to a toolkit and may not read " +
217
- "deployment-wide operator data",
218
- },
219
- { status: 403 },
220
- );
221
- }
222
-
223
155
  export async function authorizeUiAdmin(
224
156
  request: Request,
225
157
  baseUrl: string,
226
158
  auth: InboundAuth[],
227
- logger: Logger,
228
159
  purpose = "credential management",
229
160
  ): Promise<{ ok: true; userId: string } | { ok: false; response: Response }> {
230
161
  // Operator mutation is intentionally narrower than /mcp and /ui/data: only
231
162
  // an interactive Clerk provider may admit it. A static bearer token is useful
232
163
  // for headless tool calls but must not become a deployment-admin key.
233
164
  //
234
- // EVERY Clerk provider gets a turn, the way the /mcp gate does, because the
235
- // documented per-team pattern is several `clerkAuth(...)`s that differ only in
236
- // `gate` and `toolkits` (documentation/toolkits.md). Stopping at the first would make
237
- // admission depend on config order: the team-bound provider listed first
238
- // would refuse the operator outright, and a refusal here — a failed gate, a
239
- // missing user, a toolkit-bound identity — is exactly the case where a later
240
- // provider is the one meant to admit. The last refusal is returned if none do.
165
+ // Every Clerk provider gets a turn, the way the /mcp gate does. Stopping at
166
+ // the first would make admission depend on config order: a failed gate or
167
+ // missing user may simply mean a later provider is the one meant to admit.
168
+ // The last refusal is returned if none do.
241
169
  const providers = auth.filter(
242
170
  (candidate) => candidate.uiAuth?.kind === "clerk",
243
171
  );
@@ -264,24 +192,6 @@ export async function authorizeUiAdmin(
264
192
  );
265
193
  continue;
266
194
  }
267
- const binding = resolveIdentityBinding(
268
- provider.toolkitBinding,
269
- result.toolkitBinding,
270
- );
271
- if (!binding.ok) {
272
- logger.warn(
273
- `[connecta] refused an operator-mutation request admitted by inbound auth ` +
274
- `provider "${provider.kind}" with 403: ${binding.reason}.`,
275
- );
276
- lastResponse = unusableBinding();
277
- continue;
278
- }
279
- // A toolkit-bound identity is a team's credential, not a vault admin key:
280
- // credentials are deployment-wide, so writing one reaches every toolkit.
281
- if (isToolkitRestricted(binding.binding)) {
282
- lastResponse = restrictedOperatorSurface();
283
- continue;
284
- }
285
195
  return { ok: true, userId: result.userId };
286
196
  }
287
197
  return {
package/src/routes/ui.ts CHANGED
@@ -8,9 +8,7 @@ import {
8
8
  } from "../ui.js";
9
9
  import {
10
10
  authorize,
11
- isToolkitRestricted,
12
11
  privateJson,
13
- restrictedOperatorSurface,
14
12
  type RouteContext,
15
13
  } from "./shared.js";
16
14
 
@@ -54,8 +52,7 @@ function uiScriptNonce(): string {
54
52
  export async function routeUi(
55
53
  context: RouteContext,
56
54
  ): Promise<Response | null> {
57
- const { request, url, path, baseUrl, opts, defer, sweepCredentials } =
58
- context;
55
+ const { request, url, path, baseUrl, opts, defer } = context;
59
56
  if (request.method === "GET" && path === "/favicon.svg") {
60
57
  return new Response(opts.branding?.favicon?.svg ?? CONNECTA_FAVICON_SVG, {
61
58
  headers: {
@@ -119,14 +116,8 @@ export async function routeUi(
119
116
  }
120
117
  if (path !== "/ui/data") return null;
121
118
 
122
- const authz = await authorize(request, baseUrl, opts.auth, opts.logger);
119
+ const authz = await authorize(request, baseUrl, opts.auth);
123
120
  if (!authz.ok) return authz.response;
124
- if (isToolkitRestricted(authz.toolkitBinding)) {
125
- return restrictedOperatorSurface();
126
- }
127
- // After the restriction check, not before: an identity that may not
128
- // read this surface should not get to trigger background work from it.
129
- sweepCredentials();
130
121
  const eligibleClerkOperator = authz.uiAdminEligible === true;
131
122
  const credentialManagement = credentialManagementCapability({
132
123
  eligibleClerkOperator,
@@ -144,7 +135,6 @@ export async function routeUi(
144
135
  eligibleClerkOperator ? opts.credentialVault : undefined,
145
136
  Boolean(opts.activity?.list),
146
137
  credentialManagement,
147
- opts.toolkits,
148
138
  defer,
149
139
  eligibleClerkOperator,
150
140
  opts.discoveryConcurrency,
package/src/server.ts CHANGED
@@ -29,7 +29,7 @@ export function createFetchHandler(
29
29
  request: Request,
30
30
  runtimeContext?: RuntimeExecutionContext,
31
31
  ) => Promise<Response> {
32
- const { registry, auth, publicUrl } = opts;
32
+ const { auth, publicUrl, registry } = opts;
33
33
  const routeMcp = createMcpRoute(opts);
34
34
 
35
35
  return async function fetch(
@@ -43,35 +43,6 @@ export function createFetchHandler(
43
43
  ? runtimeContext.waitUntil.bind(runtimeContext)
44
44
  : undefined;
45
45
 
46
- /**
47
- * Piggyback a DUE credential liveness sweep on traffic that has already been
48
- * authenticated (issue #24). Started beside the request and never awaited by
49
- * it: it must not add latency or change a result, so it is handed to
50
- * `ctx.waitUntil` where the runtime has one (Workers, and the Node adapter's
51
- * shim) to settle after the response. The registry answers `undefined`
52
- * unless a sweep is actually due, so the ordinary request pays nothing.
53
- */
54
- const sweepCredentials = (): void => {
55
- // Belt and braces: a rejected sweep is already absorbed below, and this
56
- // catches the synchronous half — arming the gate, or a connector list that
57
- // throws while deciding whether anything is due. Nothing about a
58
- // background health check may turn a served request into a 500.
59
- try {
60
- const sweep = registry.sweepCredentialHealthIfDue(baseUrl, defer);
61
- if (!sweep) return;
62
- const settled = sweep.then(
63
- () => {},
64
- (err) => {
65
- opts.logger.warn("[connecta] credential health sweep failed", err);
66
- },
67
- );
68
- if (defer) defer(settled);
69
- else void settled;
70
- } catch (err) {
71
- opts.logger.warn("[connecta] credential health sweep failed", err);
72
- }
73
- };
74
-
75
46
  // Container and orchestrator probes reach /health over plain HTTP on
76
47
  // loopback, where no proxy has set X-Forwarded-Proto. Redirecting them to
77
48
  // the public origin would make an internal liveness check depend on
@@ -113,7 +84,6 @@ export function createFetchHandler(
113
84
  opts,
114
85
  defer,
115
86
  runtimeContext,
116
- sweepCredentials,
117
87
  };
118
88
 
119
89
  const route = async (): Promise<Response> => {