@zackbart/connecta 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +31 -55
  3. package/SECURITY.md +5 -11
  4. package/dist/activity.d.ts +0 -8
  5. package/dist/activity.d.ts.map +1 -1
  6. package/dist/activity.js +0 -1
  7. package/dist/activity.js.map +1 -1
  8. package/dist/auth/bearer.d.ts +1 -7
  9. package/dist/auth/bearer.d.ts.map +1 -1
  10. package/dist/auth/bearer.js +2 -23
  11. package/dist/auth/bearer.js.map +1 -1
  12. package/dist/auth/clerk.d.ts +2 -11
  13. package/dist/auth/clerk.d.ts.map +1 -1
  14. package/dist/auth/clerk.js +31 -46
  15. package/dist/auth/clerk.js.map +1 -1
  16. package/dist/auth/downstream-oauth.d.ts +15 -6
  17. package/dist/auth/downstream-oauth.d.ts.map +1 -1
  18. package/dist/auth/downstream-oauth.js +60 -11
  19. package/dist/auth/downstream-oauth.js.map +1 -1
  20. package/dist/call-admission.d.ts.map +1 -1
  21. package/dist/call-admission.js +0 -1
  22. package/dist/call-admission.js.map +1 -1
  23. package/dist/catalog-service.d.ts +1 -2
  24. package/dist/catalog-service.d.ts.map +1 -1
  25. package/dist/catalog-service.js +12 -18
  26. package/dist/catalog-service.js.map +1 -1
  27. package/dist/catalog.d.ts +6 -0
  28. package/dist/catalog.d.ts.map +1 -1
  29. package/dist/catalog.js +51 -0
  30. package/dist/catalog.js.map +1 -1
  31. package/dist/connectors/remote-mcp.d.ts +1 -1
  32. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  33. package/dist/connectors/remote-mcp.js +73 -71
  34. package/dist/connectors/remote-mcp.js.map +1 -1
  35. package/dist/errors.d.ts +19 -1
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +1 -0
  38. package/dist/errors.js.map +1 -1
  39. package/dist/execute.d.ts +4 -9
  40. package/dist/execute.d.ts.map +1 -1
  41. package/dist/execute.js +41 -20
  42. package/dist/execute.js.map +1 -1
  43. package/dist/executor-result.js +1 -1
  44. package/dist/executor-result.js.map +1 -1
  45. package/dist/index.d.ts +3 -82
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +17 -90
  48. package/dist/index.js.map +1 -1
  49. package/dist/invocation.d.ts +5 -0
  50. package/dist/invocation.d.ts.map +1 -1
  51. package/dist/invocation.js +46 -10
  52. package/dist/invocation.js.map +1 -1
  53. package/dist/meta-tools.d.ts +4 -12
  54. package/dist/meta-tools.d.ts.map +1 -1
  55. package/dist/meta-tools.js +119 -169
  56. package/dist/meta-tools.js.map +1 -1
  57. package/dist/operator-ui/generated.d.ts +2 -2
  58. package/dist/operator-ui/generated.d.ts.map +1 -1
  59. package/dist/operator-ui/generated.js +2 -2
  60. package/dist/operator-ui/generated.js.map +1 -1
  61. package/dist/operator-ui/model.d.ts +1 -22
  62. package/dist/operator-ui/model.d.ts.map +1 -1
  63. package/dist/operator-ui/model.js.map +1 -1
  64. package/dist/registry.d.ts +11 -178
  65. package/dist/registry.d.ts.map +1 -1
  66. package/dist/registry.js +52 -330
  67. package/dist/registry.js.map +1 -1
  68. package/dist/retired-toolkits.d.ts +3 -0
  69. package/dist/retired-toolkits.d.ts.map +1 -0
  70. package/dist/retired-toolkits.js +9 -0
  71. package/dist/retired-toolkits.js.map +1 -0
  72. package/dist/routes/activity.d.ts.map +1 -1
  73. package/dist/routes/activity.js +2 -5
  74. package/dist/routes/activity.js.map +1 -1
  75. package/dist/routes/credentials.d.ts.map +1 -1
  76. package/dist/routes/credentials.js +1 -12
  77. package/dist/routes/credentials.js.map +1 -1
  78. package/dist/routes/mcp.d.ts.map +1 -1
  79. package/dist/routes/mcp.js +94 -188
  80. package/dist/routes/mcp.js.map +1 -1
  81. package/dist/routes/oauth.d.ts.map +1 -1
  82. package/dist/routes/oauth.js +3 -8
  83. package/dist/routes/oauth.js.map +1 -1
  84. package/dist/routes/shared.d.ts +9 -30
  85. package/dist/routes/shared.d.ts.map +1 -1
  86. package/dist/routes/shared.js +10 -69
  87. package/dist/routes/shared.js.map +1 -1
  88. package/dist/routes/ui.d.ts.map +1 -1
  89. package/dist/routes/ui.js +4 -10
  90. package/dist/routes/ui.js.map +1 -1
  91. package/dist/server.d.ts.map +1 -1
  92. package/dist/server.js +1 -31
  93. package/dist/server.js.map +1 -1
  94. package/dist/skills.d.ts +3 -6
  95. package/dist/skills.d.ts.map +1 -1
  96. package/dist/skills.js +9 -38
  97. package/dist/skills.js.map +1 -1
  98. package/dist/timeout.d.ts.map +1 -1
  99. package/dist/timeout.js +2 -4
  100. package/dist/timeout.js.map +1 -1
  101. package/dist/types.d.ts +8 -61
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/ui.d.ts +1 -2
  104. package/dist/ui.d.ts.map +1 -1
  105. package/dist/ui.js +5 -35
  106. package/dist/ui.js.map +1 -1
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/package.json +3 -2
  110. package/src/activity.ts +0 -9
  111. package/src/auth/bearer.ts +3 -32
  112. package/src/auth/clerk.ts +35 -48
  113. package/src/auth/downstream-oauth.ts +106 -24
  114. package/src/call-admission.ts +0 -1
  115. package/src/catalog-service.ts +23 -23
  116. package/src/catalog.ts +55 -0
  117. package/src/connectors/remote-mcp.ts +106 -86
  118. package/src/errors.ts +22 -0
  119. package/src/execute.ts +43 -24
  120. package/src/executor-result.ts +1 -1
  121. package/src/index.ts +24 -208
  122. package/src/invocation.ts +63 -10
  123. package/src/meta-tools.ts +139 -187
  124. package/src/operator-ui/browser.css +1 -54
  125. package/src/operator-ui/browser.ts +16 -150
  126. package/src/operator-ui/generated.ts +2 -2
  127. package/src/operator-ui/model.ts +1 -23
  128. package/src/registry.ts +59 -429
  129. package/src/retired-toolkits.ts +14 -0
  130. package/src/routes/activity.ts +1 -6
  131. package/src/routes/credentials.ts +0 -12
  132. package/src/routes/mcp.ts +110 -230
  133. package/src/routes/oauth.ts +2 -8
  134. package/src/routes/shared.ts +10 -100
  135. package/src/routes/ui.ts +2 -12
  136. package/src/server.ts +1 -31
  137. package/src/skills.ts +9 -38
  138. package/src/timeout.ts +2 -4
  139. package/src/types.ts +11 -62
  140. package/src/ui.ts +9 -46
  141. package/src/version.ts +1 -1
  142. package/dist/credential-health.d.ts +0 -205
  143. package/dist/credential-health.d.ts.map +0 -1
  144. package/dist/credential-health.js +0 -610
  145. package/dist/credential-health.js.map +0 -1
  146. package/dist/toolkits.d.ts +0 -124
  147. package/dist/toolkits.d.ts.map +0 -1
  148. package/dist/toolkits.js +0 -322
  149. package/dist/toolkits.js.map +0 -1
  150. package/src/credential-health.ts +0 -835
  151. package/src/toolkits.ts +0 -461
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> => {
package/src/skills.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import type { Connector } from "./types.js";
2
2
 
3
3
  export const CONNECTA_INSTRUCTIONS =
4
- 'Connecta exposes many integrations behind meta-tools. When an address is unknown, start with search_tools and includeSchemas="compact"; use describe_tools only when that schema is insufficient. Use call_tool for one explicitly read-only call, batch_call for 2–10 independent explicitly read-only calls, and execute_code (when available) only for dependent read-only steps, loops, joins, or reducing large results. Unannotated, write-capable, and destructive tools must use call_destructive_tool individually. Use authorize_connector only after auth_required and get_result only for truncated results. For the detailed workflow, call skills({ name: "usage" }) once per task.';
4
+ 'Connecta exposes many integrations behind meta-tools. When an address is unknown, start with search_tools and includeSchemas="compact"; use describe_tools only when that schema is insufficient. Use call_tool for one explicitly read-only call, batch_call for 2–10 independent explicitly read-only calls, and execute_code (when available) only for dependent read-only steps, loops, joins, or reducing large results. Unannotated, write-capable, and destructive tools must use call_destructive_tool individually. Use authorize_connector only after auth_required and get_result only for truncated results. Fetch skills({ name: "usage" }) when this routing workflow is unfamiliar.';
5
5
 
6
6
  export const USAGE_SKILL = `# Connecta usage
7
7
 
8
8
  ## Choose the smallest execution tool
9
9
 
10
+ Use exact addresses returned by discovery; never invent one.
11
+
10
12
  - Unknown address: \`search_tools({ query, includeSchemas: "compact" })\`.
11
13
  - Schema still unclear: \`describe_tools({ addresses: [...] })\`.
12
14
  - One explicitly read-only call: \`call_tool\`.
@@ -14,45 +16,17 @@ export const USAGE_SKILL = `# Connecta usage
14
16
  - Dependent read-only calls, loops, joins, branching, or large-result reduction: \`execute_code\` when available.
15
17
  - Any unannotated, write-capable, or destructive call: \`call_destructive_tool\`, individually and only after reviewing its schema and consequences.
16
18
  - Truncated result: retry with \`fields\` when possible; otherwise page it with \`get_result\`.
17
- - \`auth_required\`: use \`authorize_connector\`, have the operator complete consent, then confirm with \`list_connectors\`.
19
+ - \`auth_required\`: use \`authorize_connector\`, give its recovery handoff to the operator, then retry the original call.
18
20
 
19
- Use \`list_connectors({ probe: false })\` for a fast inventory. Use \`probe: true\` only when diagnosing live health or authorization. The fast inventory already reports a connector whose stored credential failed a proactive check as \`auth_required\` (with \`credentialCheck\` and the URL to open), so trust it and authorize up front rather than probing to confirm.
21
+ Use \`list_connectors({ probe: false })\` for a fast inventory based on recent call observations and local credential-shape drift. Use \`probe: true\` only when diagnosing live health or authorization.
20
22
 
21
23
  ## Code mode
22
24
 
23
- Use code mode when a later call depends on an earlier result, when joining across connectors, or when filtering or aggregating data in the sandbox will substantially shrink the response. Use \`Promise.all\` or \`connecta.batch\` for independent calls inside one execution.
24
-
25
- Do not use code mode for one straightforward call, for independent calls already handled by \`batch_call\`, or for any tool not explicitly annotated \`readOnlyHint: true\`. Code mode has a bounded host-call budget and per-call deadline. Return only the reduced value the agent needs; do not return a large upstream payload unchanged.
26
-
27
- ## Examples
28
-
29
- These addresses are illustrative; always use the exact address returned by \`search_tools\`.
25
+ Use code mode when calls depend on earlier results, when joining connectors, or when sandbox filtering or aggregation will substantially shrink the response. Use \`Promise.all\` or \`connecta.batch\` for independent calls inside one execution.
30
26
 
31
- Single call:
32
- \`\`\`json
33
- { "address": "crm.get_account", "args": { "id": "acct_123" }, "resultMode": "value" }
34
- \`\`\`
27
+ Connector namespace calls and \`connecta.call\` use the same read-only gate and throw on downstream errors. Catch only failures the workflow can handle; let authorization failures return to the agent for recovery.
35
28
 
36
- Independent calls:
37
- \`\`\`json
38
- { "calls": [
39
- { "address": "crm.get_account", "args": { "id": "acct_123" } },
40
- { "address": "billing.list_invoices", "args": { "status": "open" } }
41
- ] }
42
- \`\`\`
43
-
44
- Dependent code with reduction:
45
- \`\`\`js
46
- async () => {
47
- const accounts = await crm.search_accounts({ query: "renewal" });
48
- const details = await Promise.all(
49
- accounts.results.slice(0, 5).map((account) =>
50
- crm.get_account({ id: account.id })
51
- )
52
- );
53
- return details.map(({ id, name, status }) => ({ id, name, status }));
54
- }
55
- \`\`\`
29
+ Do not use code mode for one call, independent calls already handled by \`batch_call\`, or any tool lacking \`readOnlyHint: true\`. Host calls and time are bounded. Return only the reduced value the agent needs.
56
30
  `;
57
31
 
58
32
  /**
@@ -173,10 +147,7 @@ export interface SkillListing {
173
147
  /**
174
148
  * Every fetchable skill: the built-in guides plus one entry per connector that
175
149
  * carries a usage guide. Derived from the connector list passed in — the single
176
- * place guide visibility is decided. The `skills` meta-tool passes its
177
- * connection's `registry.listConnectors()`, so a toolkit-scoped session lists
178
- * only in-scope guides, and `resolveSkill` below reports an out-of-scope
179
- * `connector:<id>` exactly as it reports an unknown connector.
150
+ * place guide visibility is decided.
180
151
  */
181
152
  export function listSkills(connectors: readonly Connector[]): SkillListing[] {
182
153
  const listing: SkillListing[] = AVAILABLE_SKILLS.map((skill) => ({
package/src/timeout.ts CHANGED
@@ -1,7 +1,5 @@
1
- // The deadline vocabulary shared by every non-call downstream probe: the
2
- // discovery meta-tools' catalog fan-out (src/meta-tools.ts) and the credential
3
- // liveness checks (src/credential-health.ts). One definition so a "probe" means
4
- // the same thing, and is bounded the same way, wherever one is issued.
1
+ // The deadline vocabulary shared by downstream discovery probes and tool calls.
2
+ // One definition keeps those waits bounded consistently.
5
3
 
6
4
  /**
7
5
  * Generous default bound for a single downstream probe/catalog call. High enough
package/src/types.ts CHANGED
@@ -174,7 +174,7 @@ export interface ConnectorContext {
174
174
  timeoutMs?: number;
175
175
  }
176
176
 
177
- export type ConnectorStatusState = "ok" | "auth_required" | "error";
177
+ type ConnectorStatusState = "ok" | "auth_required" | "error";
178
178
 
179
179
  export interface ConnectorStatus {
180
180
  state: ConnectorStatusState;
@@ -226,19 +226,6 @@ export interface Connector {
226
226
  values: ConnectorCredentialValues,
227
227
  ctx: ConnectorContext,
228
228
  ): Promise<CredentialTestResult>;
229
- /**
230
- * Optional: whether this connector currently holds a stored downstream
231
- * credential — an OAuth grant it persisted, typically. Read only by the
232
- * credential liveness checks: a connector with nothing stored has no
233
- * credential whose liveness could be in question, and probing it anyway would
234
- * start an authorization flow nobody asked for.
235
- *
236
- * Implement it on connectors that manage their own credential storage (the
237
- * shipped `remoteMcp` does, for `auth: { type: "oauth" }`). Connectors whose
238
- * credential lives in connecta's vault (`credential` above) need not: the
239
- * vault answers for them. Must not perform downstream I/O.
240
- */
241
- hasStoredCredential?(ctx: ConnectorContext): Promise<boolean>;
242
229
  /**
243
230
  * Statically-known tool defs, exposed by in-code connectors (`api()`) for
244
231
  * startup convention checks. Remote connectors omit this — their tools are
@@ -289,8 +276,16 @@ export interface Connector {
289
276
  * pending URL could complete consent with their own account.
290
277
  */
291
278
  verifyState?(state: string | null, ctx: ConnectorContext): Promise<boolean>;
292
- /** Optional: complete a downstream OAuth flow (called by /oauth/callback/<id>). */
293
- finishAuth?(code: string, ctx: ConnectorContext): Promise<void>;
279
+ /**
280
+ * Optional: complete a downstream OAuth flow (called by
281
+ * /oauth/callback/<id>). `callbackParams` preserves the authorization
282
+ * server's RFC 9207 `iss` response parameter for SDK validation.
283
+ */
284
+ finishAuth?(
285
+ code: string,
286
+ ctx: ConnectorContext,
287
+ callbackParams?: URLSearchParams,
288
+ ): Promise<void>;
294
289
  /**
295
290
  * Optional: serve a connector-owned HTTP route — for example a signed
296
291
  * download link minted by one of the connector's tools. Called only after
@@ -384,50 +379,12 @@ export interface ExecutorLease {
384
379
  release(): void;
385
380
  }
386
381
 
387
- /**
388
- * Which toolkits one inbound identity may open — the membership half of the
389
- * deployment=org / toolkit=team framing (documentation/toolkits.md). A mapping, never a
390
- * policy engine: one identity → the toolkit names it may select, plus whether
391
- * it may connect with no `?toolkit=` at all.
392
- *
393
- * An identity with NO binding is unbound and keeps the pre-binding behavior:
394
- * any declared toolkit, or the full registry. A binding is enforced at connect
395
- * time, before any scoped registry is constructed.
396
- */
397
- export interface ToolkitBinding {
398
- /** Toolkit names this identity may select with `?toolkit=<name>`. */
399
- readonly toolkits: readonly string[];
400
- /**
401
- * Whether this identity may also connect with no `?toolkit=` and see the full
402
- * registry (and read the deployment-wide operator surfaces). Defaults to
403
- * false: binding a credential to a toolkit means binding it.
404
- */
405
- readonly unscoped?: boolean;
406
- }
407
-
408
382
  /** Result of an inbound-auth check. */
409
383
  export type AuthResult =
410
384
  | {
411
385
  ok: true;
412
386
  userId?: string;
413
387
  subjectId?: string;
414
- /**
415
- * Toolkit binding resolved for THIS identity — the seam for an adapter
416
- * that maps its own users (or an IdP claim) to views. Omit to inherit the
417
- * provider's `toolkitBinding`.
418
- *
419
- * When the provider also declares one, the declaration is a **CEILING**,
420
- * not a default: connecta intersects the two, and grants `unscoped` only
421
- * if both do. A per-identity binding can therefore narrow the credential's
422
- * view but never widen it — otherwise an adapter reading a user-writable
423
- * claim would let the user name their own toolkits. When the provider
424
- * declares nothing, this binding is used as given.
425
- *
426
- * Validated on arrival (a malformed one refuses the request with 403
427
- * rather than being ignored), but never checked against the configured
428
- * toolkits, which is only possible for the static declaration at startup.
429
- */
430
- toolkitBinding?: ToolkitBinding;
431
388
  }
432
389
  | { ok: false; response: Response };
433
390
 
@@ -527,14 +484,6 @@ export interface InboundAuth {
527
484
  * provider instead of asking the operator to paste a static bearer secret.
528
485
  */
529
486
  uiAuth?: UiAuthConfig;
530
- /**
531
- * Optional toolkit binding for every identity this provider admits
532
- * (documentation/toolkits.md). Declared statically so `createConnecta` can validate the
533
- * names against `ConnectaConfig.toolkits` and throw on a typo — a binding
534
- * nobody wrote is not one an operator can reason about. An `authorize` result
535
- * may narrow it per identity with its own `toolkitBinding`.
536
- */
537
- toolkitBinding?: ToolkitBinding;
538
487
  /** Serve/short-circuit .well-known + OPTIONS. Return null when not handled. */
539
488
  handleMetadata?(
540
489
  request: Request,
package/src/ui.ts CHANGED
@@ -16,7 +16,6 @@ import {
16
16
  type CredentialManagementCapability,
17
17
  type UiConnector,
18
18
  type UiData,
19
- type UiToolkit,
20
19
  type UiTool,
21
20
  } from "./operator-ui/model.js";
22
21
  import {
@@ -24,8 +23,11 @@ import {
24
23
  OPERATOR_UI_SCRIPT,
25
24
  } from "./operator-ui/generated.js";
26
25
  import type { Registry } from "./registry.js";
27
- import type { Toolkit } from "./toolkits.js";
28
- import type { ConnectaBranding, UiAuthConfig } from "./types.js";
26
+ import type {
27
+ ConnectaBranding,
28
+ ConnectorStatus,
29
+ UiAuthConfig,
30
+ } from "./types.js";
29
31
  import { CONNECTA_VERSION } from "./version.js";
30
32
 
31
33
  export {
@@ -310,7 +312,6 @@ export async function buildUiData(
310
312
  credentialManagement: CredentialManagementCapability = credentialVault
311
313
  ? "available"
312
314
  : "requires_clerk",
313
- toolkits?: ReadonlyMap<string, Toolkit>,
314
315
  defer?: DeferredWork,
315
316
  oauthManagement = false,
316
317
  discoveryConcurrency?: number,
@@ -322,8 +323,10 @@ export async function buildUiData(
322
323
  connectorSet,
323
324
  concurrency,
324
325
  async (c): Promise<UiConnector> => {
325
- const status = await registry.statusFor(c.id, baseUrl, requestScope);
326
- const credentialCheck = await registry.credentialHealthFor(c.id);
326
+ const drift = await registry.credentialDriftFor(c.id);
327
+ const status: ConnectorStatus = drift
328
+ ? { state: "auth_required", message: drift }
329
+ : await registry.statusFor(c.id, baseUrl, requestScope);
327
330
  let tools: UiTool[] = [];
328
331
  // `status()` on an unauthenticated remote connector starts OAuth and
329
332
  // stores its state + PKCE verifier. Probing listTools immediately
@@ -447,17 +450,6 @@ export async function buildUiData(
447
450
  toolCount: tools.length,
448
451
  tools,
449
452
  ...(c.disconnectAuth && c.startAuth ? { oauth: true } : {}),
450
- ...(credentialCheck
451
- ? {
452
- credentialCheck: {
453
- state: credentialCheck.state,
454
- checkedAt: credentialCheck.checkedAt,
455
- ...(credentialCheck.message
456
- ? { message: credentialCheck.message }
457
- : {}),
458
- },
459
- }
460
- : {}),
461
453
  ...(credential ? { credential } : {}),
462
454
  };
463
455
  },
@@ -476,29 +468,10 @@ export async function buildUiData(
476
468
  if (result.status === "rejected") throw result.reason;
477
469
  return result.value;
478
470
  });
479
- const toolkitData: UiToolkit[] = [...(toolkits?.values() ?? [])].map(
480
- (toolkit) => ({
481
- name: toolkit.name,
482
- connectors: [...toolkit.connectors],
483
- includeTools: [...toolkit.includeTools],
484
- excludeTools: [...toolkit.excludeTools],
485
- toolCount: connectors.reduce(
486
- (count, connector) =>
487
- count +
488
- (toolkit.hasConnector(connector.id)
489
- ? connector.tools.filter((tool) =>
490
- toolkit.hasTool(connector.id, tool.name),
491
- ).length
492
- : 0),
493
- 0,
494
- ),
495
- }),
496
- );
497
471
  return {
498
472
  serverInfo,
499
473
  connectaVersion: CONNECTA_VERSION,
500
474
  connectors,
501
- toolkits: toolkitData,
502
475
  activityEnabled,
503
476
  credentialManagement,
504
477
  oauthManagement,
@@ -668,16 +641,6 @@ ${clerkScript}
668
641
  <div id="list" class="connector-tools" aria-busy="false"></div>
669
642
  </div>
670
643
  </section>
671
- <section class="section pgrid" aria-labelledby="toolkitLedgerHeading">
672
- <h2 class="pcap" id="toolkitLedgerHeading">Toolkits</h2>
673
- <div class="pbody">
674
- <p class="toolkit-copy meta">
675
- Read-only views from deployment config. Change the config and redeploy
676
- to update them.
677
- </p>
678
- <div id="toolkitList" class="toolkit-ledger"></div>
679
- </div>
680
- </section>
681
644
  </section>
682
645
 
683
646
  <section id="credentialsView"${page === "credentials" ? "" : ' class="hidden"'}>
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.8.0";
7
+ export const CONNECTA_VERSION = "0.9.0";
@@ -1,205 +0,0 @@
1
- import type { CredentialVault } from "./credentials.js";
2
- import { type DeferredWork } from "./connector-scope.js";
3
- import type { Connector, ConnectorContext, ConnectorStatusState, KVStorage, Logger } from "./types.js";
4
- /** Verdict of one liveness check. Same vocabulary as `ConnectorStatus.state`. */
5
- export type CredentialCheckState = ConnectorStatusState;
6
- /** The stored verdict of the most recent liveness check of one connector. */
7
- export interface CredentialHealthRecord {
8
- state: CredentialCheckState;
9
- /** ISO timestamp of the check that produced this record. */
10
- checkedAt: string;
11
- /** Why, for a non-ok state — the connector's own reason, verbatim. */
12
- message?: string;
13
- /** Consent URL to open, when the connector reported one. */
14
- authorizationUrl?: string;
15
- }
16
- /**
17
- * Why a connector was not checked.
18
- *
19
- * - `not_found` — no connector with that id is registered. Only reachable
20
- * through an explicit `ids` request, and reported rather than dropped so a
21
- * typo in a scheduled check is visible instead of silent.
22
- * - `not_checkable` — it stores no credential connecta manages, or exposes no
23
- * usable way to ask: neither `status()` nor a credential test hook the
24
- * declared credential shape can use (`credentialTestRule`), against a value
25
- * actually stored under it.
26
- * - `no_credential` — checkable, but nothing is stored yet: there is no
27
- * credential whose liveness could be in question, and probing would start an
28
- * OAuth flow nobody asked for.
29
- * - `fresh` — checked less than `intervalSeconds` ago (by any isolate — the
30
- * record is persisted), so this is the rate limit doing its job.
31
- * - `in_flight` — another check of this connector is already running.
32
- */
33
- export type CredentialCheckSkip = "not_found" | "not_checkable" | "no_credential" | "fresh" | "in_flight";
34
- /** One connector's outcome in a sweep. */
35
- export interface CredentialCheckResult {
36
- connectorId: string;
37
- /**
38
- * The record now in force. Present for a completed check, and for a `fresh`
39
- * skip (where the still-valid record is what the skip deferred to).
40
- */
41
- record?: CredentialHealthRecord;
42
- /** Set when no check ran; `record` is then whatever was already stored. */
43
- skipped?: CredentialCheckSkip;
44
- /**
45
- * The check ran, but its verdict was thrown away: the credential it judged
46
- * was replaced or removed while it was in flight (see `clear`). `record` is
47
- * what the check saw, not what is stored — nothing is.
48
- */
49
- discarded?: true;
50
- /** How long the check took, when one ran. */
51
- latencyMs?: number;
52
- }
53
- /** Deployment-wide tuning for credential liveness checks. */
54
- export interface CredentialHealthConfig {
55
- /**
56
- * Minimum seconds between checks of the same connector, across isolates (the
57
- * verdict is persisted, so a Worker cron isolate and a request isolate share
58
- * one clock). Default 900 (15 minutes). This is the bound on downstream cost:
59
- * repeated status reads never each trigger a check.
60
- */
61
- intervalSeconds?: number;
62
- /** Max checks in flight at once during one sweep. Default 4. */
63
- concurrency?: number;
64
- /** Per-check deadline. Default 30 000, the probe default. */
65
- timeoutMs?: number;
66
- /**
67
- * Let inbound authenticated `/mcp` and `/ui/data` traffic trigger a *due*
68
- * sweep in the background (`ctx.waitUntil` where the runtime has it). Default
69
- * true — it is the trigger that makes stale-credential detection work with no
70
- * scheduler wired at all, and it cannot slow a request down or change a
71
- * result. Set false to check only from `Connecta.checkCredentials()`.
72
- */
73
- onRequest?: boolean;
74
- }
75
- /** What the checker needs from the registry, without depending on it. */
76
- export interface CredentialHealthDeps {
77
- listConnectors(): Connector[];
78
- getConnector(id: string): Connector | undefined;
79
- contextFor(id: string, baseUrl: string, requestScope?: object): ConnectorContext;
80
- storage: KVStorage;
81
- logger: Logger;
82
- credentialVault?: CredentialVault;
83
- }
84
- export interface CredentialCheckOptions {
85
- /** Check even connectors whose verdict is still fresh. */
86
- force?: boolean;
87
- /** Restrict the sweep to these connector ids. Default: every connector. */
88
- ids?: string[];
89
- /**
90
- * @deprecated Ignored. Credential checks always create and close their own
91
- * probe scope; no core path supplies an existing request scope.
92
- */
93
- requestScope?: object;
94
- }
95
- /**
96
- * Runs and caches credential liveness checks. One instance per `Registry`.
97
- *
98
- * Cost is bounded four ways, because a status surface an agent polls must never
99
- * become a way to hammer a downstream auth endpoint:
100
- *
101
- * 1. **Eligibility** — only connectors holding a credential of ours are probed
102
- * at all (`isCheckableConnector`), and only when something is actually stored.
103
- * 2. **Freshness (cross-isolate)** — a persisted verdict younger than
104
- * `intervalSeconds` short-circuits the check, so every isolate and every
105
- * trigger share one budget.
106
- * 3. **Sweep gate (per isolate)** — `sweepIfDue` runs at most one traffic-
107
- * triggered sweep per interval per isolate, and never two at once, so a burst
108
- * of requests costs one sweep, not one per request.
109
- * 4. **Deadline + fan-out bound** — each check is bounded by `timeoutMs` and at
110
- * most `concurrency` run together (the same shape as the
111
- * `discovery.probeTimeoutMs` bound on the discovery fan-out, issue #19).
112
- */
113
- export declare class CredentialHealthChecker {
114
- private readonly deps;
115
- private readonly store;
116
- private readonly intervalMs;
117
- private readonly concurrency;
118
- private readonly timeoutMs;
119
- private readonly onRequest;
120
- /** Per-connector checks in flight in THIS isolate. */
121
- private readonly inFlight;
122
- /** Earliest a traffic-triggered sweep may run again in this isolate. */
123
- private nextSweepAt;
124
- private sweeping;
125
- constructor(deps: CredentialHealthDeps, config?: CredentialHealthConfig);
126
- /**
127
- * The stored verdict, if any. No downstream I/O — and no storage read at all
128
- * for a connector that stores no credential of ours, since only a checkable
129
- * connector can ever have had a verdict written. That keeps
130
- * `list_connectors({ probe: false })` exactly as cheap as it was for the
131
- * deployments this feature does not apply to.
132
- */
133
- healthFor(connectorId: string): Promise<CredentialHealthRecord | undefined>;
134
- /**
135
- * Record a liveness verdict observed elsewhere — today, the live status a
136
- * `list_connectors({ probe: true })` just performed. Filtered by the same
137
- * eligibility rule as a check, so this stays a record of *credential* health
138
- * rather than a general status cache, and so it also counts against the
139
- * freshness budget: an operator who just probed live does not get swept again
140
- * moments later.
141
- */
142
- record(connectorId: string, record: CredentialHealthRecord): Promise<void>;
143
- /** Forget a connector's verdict — its credential just changed under us. */
144
- clear(connectorId: string): Promise<void>;
145
- /** Whether any connector in this deployment could be checked at all. */
146
- hasCheckableConnectors(): boolean;
147
- /**
148
- * Check every (or the named) connector's stored credential and return one
149
- * outcome per connector considered. Never rejects: a connector that throws,
150
- * hangs past `timeoutMs`, or cannot be persisted becomes an `error` verdict.
151
- */
152
- check(baseUrl: string, opts?: CredentialCheckOptions, defer?: DeferredWork): Promise<CredentialCheckResult[]>;
153
- /**
154
- * The traffic-triggered sweep: a promise to hand to `ctx.waitUntil`, or
155
- * `undefined` when nothing is due (the common case, and free — no I/O). The
156
- * gate is armed BEFORE the sweep starts, so a burst of concurrent requests
157
- * produces one sweep.
158
- */
159
- sweepIfDue(baseUrl: string, defer?: DeferredWork): Promise<CredentialCheckResult[]> | undefined;
160
- private checkOne;
161
- private recordOrNothing;
162
- private runCheck;
163
- /**
164
- * `isCheckableConnector` re-asked against what is actually stored: the hook
165
- * the declared shape selects, bound to a value that fits it (see
166
- * {@link testHookFor}), or a `status()` to fall back on. Neither ⇒ there is no
167
- * honest question to put to this connector.
168
- */
169
- private canAsk;
170
- /**
171
- * Ask the connector whether the credential it holds still works — with no
172
- * downstream mutation and no tool call. A credential test is preferred for a
173
- * vault credential because it validates the stored value itself; `status()` is
174
- * the downstream-OAuth answer (it refreshes the grant, which is the liveness
175
- * question for a token).
176
- */
177
- private probe;
178
- private settle;
179
- }
180
- /**
181
- * Whether a liveness verdict may DECIDE a connector's cached status.
182
- *
183
- * Only `auth_required` ever does, and only while nothing better has happened
184
- * since. Two separate judgements:
185
- *
186
- * 1. **`error` is not credential evidence.** A check that timed out, threw, or
187
- * got a 502 from the provider's status endpoint failed to *complete* — it
188
- * learned nothing about the credential. Letting it set the status would flip
189
- * a connector whose calls are fine to `error` for a whole interval on a DNS
190
- * blip. Error verdicts stay visible in `credentialCheck` (an operator wants
191
- * to know checks are failing) but the status keeps coming from observed real
192
- * calls, which is evidence.
193
- * 2. **A successful real call retires the verdict, except static shape drift.**
194
- * Traffic beats a background probe, so a `lastSuccessAt` at or after
195
- * `checkedAt` normally means the credential demonstrably works. Stored-shape
196
- * drift is different: a credential-independent tool can succeed without
197
- * making a missing declared field appear, so only replacement/removal clears
198
- * that verdict.
199
- *
200
- * `auth_required` deliberately outranks an observed real-call *failure*: both
201
- * say something is wrong, and only one of them carries the URL that fixes it.
202
- * The failure stays visible as `lastError`.
203
- */
204
- export declare function credentialVerdictApplies(record: CredentialHealthRecord | undefined, lastSuccessAt: string | undefined): boolean;
205
- //# sourceMappingURL=credential-health.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"credential-health.d.ts","sourceRoot":"","sources":["../src/credential-health.ts"],"names":[],"mappings":"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"}