@zackbart/connecta 0.37.0 → 0.38.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 (183) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +2 -2
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/auth/clerk-sdk/generated.js +2 -1
  8. package/dist/auth/cloudflare-access.d.ts +3 -1
  9. package/dist/auth/cloudflare-access.js +22 -11
  10. package/dist/auth/downstream-oauth.js +195 -270
  11. package/dist/auth/downstream-revocation.js +1 -1
  12. package/dist/auth/oauth-grant.js +11 -0
  13. package/dist/auth/oauth-token-response.js +1 -1
  14. package/dist/auth/vercel-agent.js +2 -1
  15. package/dist/auth-elicitation.js +2 -2
  16. package/dist/branding.d.ts +1 -15
  17. package/dist/branding.js +2 -2
  18. package/dist/bytes.js +42 -0
  19. package/dist/call-admission-context.js +2 -5
  20. package/dist/call-admission.js +5 -0
  21. package/dist/call-recovery.js +0 -3
  22. package/dist/catalog-cache.js +26 -7
  23. package/dist/catalog-fingerprint.js +2 -18
  24. package/dist/catalog-revision.js +9 -0
  25. package/dist/catalog-service.js +51 -20
  26. package/dist/config-defaults.d.ts +1 -0
  27. package/dist/config-defaults.js +1 -0
  28. package/dist/config-schema.d.ts +1 -1
  29. package/dist/config-schema.js +6 -2
  30. package/dist/config.d.ts +41 -14
  31. package/dist/config.js +21 -71
  32. package/dist/connector-caller.js +1 -1
  33. package/dist/connectors/api-connector.js +4 -3
  34. package/dist/connectors/guarded-fetch.d.ts +0 -6
  35. package/dist/connectors/guarded-fetch.js +1 -11
  36. package/dist/connectors/remote-mcp.js +93 -69
  37. package/dist/connectors/resource-uri.js +0 -4
  38. package/dist/credentials.js +3 -0
  39. package/dist/d1.d.ts +0 -89
  40. package/dist/d1.js +0 -34
  41. package/dist/downstream-input.js +2 -2
  42. package/dist/execute.js +161 -116
  43. package/dist/executor-result.js +2 -1
  44. package/dist/executors/quickjs-runtime.js +2 -25
  45. package/dist/executors/quickjs.js +1 -1
  46. package/dist/guest-runtime.js +5 -3
  47. package/dist/guest-types.d.ts +5 -5
  48. package/dist/http/client.js +1 -1
  49. package/dist/http/errors.js +2 -2
  50. package/dist/http/json-schema.js +1 -1
  51. package/dist/http/node-transport.js +0 -1
  52. package/dist/http/oauth.js +1 -1
  53. package/dist/http/paging.js +6 -9
  54. package/dist/http/provider.js +1 -1
  55. package/dist/http/safety.js +1 -1
  56. package/dist/http/services.js +1 -8
  57. package/dist/http/tools.js +1 -7
  58. package/dist/http/vendor.js +7 -7
  59. package/dist/identity.js +2 -6
  60. package/dist/index.js +1 -2
  61. package/dist/invocation-auth.js +2 -1
  62. package/dist/invocation.js +8 -2
  63. package/dist/meta-tools.js +27 -27
  64. package/dist/native-tools.js +2 -1
  65. package/dist/oauth-handoff.js +2 -2
  66. package/dist/operation-vocabulary.js +1 -1
  67. package/dist/operator-record.d.ts +8 -2
  68. package/dist/operator-record.js +18 -50
  69. package/dist/operator-ui/contract.d.ts +4 -2
  70. package/dist/operator-ui/generated.js +3 -3
  71. package/dist/operator-ui/model.d.ts +7 -2
  72. package/dist/program-writes.js +3 -8
  73. package/dist/provider-call.js +0 -7
  74. package/dist/provider.d.ts +1 -1
  75. package/dist/providers/_shared/google/kit.js +1 -6
  76. package/dist/providers/_shared/google/service-account.js +1 -3
  77. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  78. package/dist/providers/_shared/guide.js +3 -0
  79. package/dist/providers/_shared/json.js +12 -0
  80. package/dist/providers/_shared/rest/compact-index.js +1 -1
  81. package/dist/providers/_shared/rest/kit.js +1 -3
  82. package/dist/providers/_shared/rest/operation-index.js +1 -3
  83. package/dist/providers/_shared/rest/value-safety.js +26 -28
  84. package/dist/providers/breeze/api.js +2 -7
  85. package/dist/providers/breeze/index.js +4 -2
  86. package/dist/providers/breeze/tools.js +3 -4
  87. package/dist/providers/ccb/api.js +1 -6
  88. package/dist/providers/ccb/index.js +2 -1
  89. package/dist/providers/ccb/tools.js +1 -6
  90. package/dist/providers/cloudflare/api.js +2 -2
  91. package/dist/providers/cloudflare/index.js +2 -1
  92. package/dist/providers/cloudflare/safety.js +3 -3
  93. package/dist/providers/docs/tools.js +2 -10
  94. package/dist/providers/drive/tools.js +12 -25
  95. package/dist/providers/forms/tools.js +1 -9
  96. package/dist/providers/gmail/tools.js +5 -21
  97. package/dist/providers/overflow/api.js +1 -3
  98. package/dist/providers/overflow/index.js +2 -1
  99. package/dist/providers/overflow/tools.js +3 -7
  100. package/dist/providers/planning-center/api.js +2 -1
  101. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  102. package/dist/providers/planning-center/lib/json-api.js +1 -9
  103. package/dist/providers/planning-center/tools.js +2 -1
  104. package/dist/providers/sheets/tools.js +1 -9
  105. package/dist/providers/slides/tools.js +4 -13
  106. package/dist/providers/stripe/index.js +2 -1
  107. package/dist/providers/stripe/safety.js +2 -2
  108. package/dist/providers/tithely/api.js +2 -7
  109. package/dist/providers/tithely/index.js +3 -2
  110. package/dist/providers/tithely/tools.js +7 -5
  111. package/dist/providers/vercel/api.js +1 -6
  112. package/dist/providers/vercel/lib/pin.js +1 -1
  113. package/dist/providers/vercel/safety.js +1 -1
  114. package/dist/providers/vercel/tools.js +2 -1
  115. package/dist/redaction-marker.js +2 -2
  116. package/dist/registry.d.ts +4 -1
  117. package/dist/registry.js +35 -22
  118. package/dist/request-principal.js +15 -2
  119. package/dist/request-state.js +2 -2
  120. package/dist/routes/access-tokens.js +1 -1
  121. package/dist/routes/activity-disclosure.js +1 -1
  122. package/dist/routes/connect.js +2 -2
  123. package/dist/routes/credentials.js +1 -1
  124. package/dist/routes/dispatch.js +10 -0
  125. package/dist/routes/mcp.js +23 -21
  126. package/dist/routes/oauth-management.js +1 -1
  127. package/dist/routes/operator.js +53 -7
  128. package/dist/routes/shared.d.ts +8 -8
  129. package/dist/routes/shared.js +22 -15
  130. package/dist/routes/ui-config.js +6 -6
  131. package/dist/routes/ui-data.js +2 -2
  132. package/dist/routes/ui.js +9 -4
  133. package/dist/runtime/call-admission.js +1 -1
  134. package/dist/runtime/call.js +1 -1
  135. package/dist/runtime/settlement.js +1 -1
  136. package/dist/runtime/shared-read.js +2 -15
  137. package/dist/sent-secrets.d.ts +14 -6
  138. package/dist/sent-secrets.js +231 -85
  139. package/dist/server.js +4 -4
  140. package/dist/skills.js +8 -35
  141. package/dist/sqlite.d.ts +0 -19
  142. package/dist/sqlite.js +2 -83
  143. package/dist/storage/keys.js +29 -124
  144. package/dist/storage/sql-native.js +14 -111
  145. package/dist/storage/sql.d.ts +1 -24
  146. package/dist/storage/sql.js +1 -12
  147. package/dist/tool-error.js +4 -0
  148. package/dist/types.d.ts +4 -3
  149. package/dist/ui.js +3 -0
  150. package/dist/url-safety.js +0 -4
  151. package/dist/usage-guide.js +2 -2
  152. package/dist/version.d.ts +1 -1
  153. package/dist/version.js +1 -1
  154. package/dist/worker.js +1 -1
  155. package/documentation/README.md +6 -42
  156. package/documentation/architecture.md +190 -507
  157. package/documentation/auth.md +172 -58
  158. package/documentation/code-mode.md +98 -66
  159. package/documentation/deploying.md +19 -40
  160. package/documentation/integrating.md +8 -19
  161. package/documentation/meta-tools.md +23 -6
  162. package/documentation/migration-0.33.md +5 -5
  163. package/documentation/migration-0.34.md +2 -4
  164. package/documentation/migration-retired-layouts.md +74 -0
  165. package/documentation/operator-tests.md +19 -119
  166. package/documentation/operator-ui.md +46 -8
  167. package/documentation/provider-auth-migration.md +4 -10
  168. package/documentation/provider-migration-0.29.md +4 -15
  169. package/examples/worker/AGENTS.md +4 -6
  170. package/examples/worker/README.md +20 -115
  171. package/guides/connecting-agents.md +7 -30
  172. package/guides/how-it-works.md +2 -2
  173. package/guides/safety-and-trust.md +4 -1
  174. package/package.json +19 -19
  175. package/templates/node/README.md +9 -26
  176. package/templates/node/package.json +1 -1
  177. package/dist/auth/oauth-v2-migration.js +0 -135
  178. package/dist/provider-call-facts.d.ts +0 -115
  179. package/dist/provider-call.d.ts +0 -112
  180. package/dist/storage/d1-copy.js +0 -255
  181. package/dist/storage/sql-schema.d.ts +0 -6
  182. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  183. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -1,3 +1,4 @@
1
+ import { decodeBase64url } from "../bytes.js";
1
2
  import { runEdge } from "../runtime/run.js";
2
3
  import { authCall, authCatch, authFetch, authFlow, authInvoke, authMethod, authProgram, registerAuth, } from "./native.js";
3
4
  import { AGENT_NAMESPACE, agentSubjectId, trustAgentProvider } from "../agent-identity.js";
@@ -14,7 +15,7 @@ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
14
15
  function bytes(value) {
15
16
  if (!/^[A-Za-z0-9_-]+(?![\s\S])/.test(value))
16
17
  throw new Error("Invalid JWT encoding");
17
- return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (char) => char.charCodeAt(0));
18
+ return decodeBase64url(value);
18
19
  }
19
20
  function record(value) {
20
21
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -1,3 +1,4 @@
1
+ import { toolError } from "./tool-error.js";
1
2
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
2
3
  import { upwardStep } from "./upward.js";
3
4
  import { authAwait, authCall, authInvoke, authFlow, authMethod, registerAuthMethod, authOwnMethod, } from "./auth/native.js";
@@ -17,8 +18,7 @@ import { canonicalState as canonical, requestDigest as digest, stateObject as ob
17
18
  const INPUT_KEY = "connecta_auth";
18
19
  const MESSAGE = "Connect this service in your browser, then retry the request.";
19
20
  function failure(code, message) {
20
- const structuredContent = { ok: false, error: { code, message, retryable: false } };
21
- return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent, isError: true };
21
+ return toolError({ code, message, retryable: false }, { ok: false });
22
22
  }
23
23
  /** Request-local auth recovery at the MCP boundary, never inside a guest provider. */
24
24
  export class AuthElicitation {
@@ -1,7 +1,7 @@
1
1
  import type { ConnectaBranding, ConnectaTheme, UiAuthConfig } from "./types.js";
2
2
  /** Connecta's default monochrome "C" mark. */
3
3
  export declare const CONNECTA_FAVICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\">\n <style>\n .fg { fill: #000 }\n @media (prefers-color-scheme: dark) { .fg { fill: #fff } }\n </style>\n <path class=\"fg\" d=\"M27 9.4A13 13 0 1 0 27 22.6l-4.4-2.5a8 8 0 1 1 0-8.2z\"/>\n</svg>";
4
- export interface ResolvedTheme {
4
+ interface ResolvedTheme {
5
5
  accent?: string;
6
6
  radius?: string;
7
7
  fontFamily?: string;
@@ -28,26 +28,12 @@ interface ResolvedBranding {
28
28
  theme: ResolvedTheme;
29
29
  }
30
30
  export declare function resolveBranding(branding?: ConnectaBranding): ResolvedBranding;
31
- /**
32
- * Read a theme the way branding URLs are read: gate every field, drop what
33
- * fails, never throw. This runs during `createConnecta`, so a malformed value
34
- * has to fall back to the stylesheet default instead of refusing to serve the
35
- * page. `droppedThemeTokens` names the drops for the startup warning.
36
- */
37
- export declare function resolveTheme(theme?: ConnectaTheme): ResolvedTheme;
38
31
  /**
39
32
  * Names of the theme tokens the operator set that failed their gate. Same
40
33
  * contract as `droppedBrandingUrls`: rendering falls back silently, so this is
41
34
  * the only place an operator learns their value never reached the page.
42
35
  */
43
36
  export declare function droppedThemeTokens(theme?: ConnectaTheme): string[];
44
- /**
45
- * The resolved theme as a `:root` block, or "" when a deployment configured
46
- * nothing. It is emitted after the stylesheet so it overrides the defaults.
47
- * There is no escaping here: every value has already passed a gate above, and
48
- * anything that would need escaping is dropped rather than rewritten.
49
- */
50
- export declare function themeCss(theme: ResolvedTheme): string;
51
37
  /** Text or attribute value, escaped for any position in an HTML document. */
52
38
  export declare function escapeHtml(value: string): string;
53
39
  export interface PageLayout {
package/dist/branding.js CHANGED
@@ -71,7 +71,7 @@ const COLOR_SCHEMES = ["system", "light", "dark"];
71
71
  * has to fall back to the stylesheet default instead of refusing to serve the
72
72
  * page. `droppedThemeTokens` names the drops for the startup warning.
73
73
  */
74
- export function resolveTheme(theme) {
74
+ function resolveTheme(theme) {
75
75
  const accent = trimmedString(theme?.accent);
76
76
  const fontFamily = trimmedString(theme?.fontFamily);
77
77
  const monoFamily = trimmedString(theme?.monoFamily);
@@ -175,7 +175,7 @@ function accentTokens(accent) {
175
175
  * There is no escaping here: every value has already passed a gate above, and
176
176
  * anything that would need escaping is dropped rather than rewritten.
177
177
  */
178
- export function themeCss(theme) {
178
+ function themeCss(theme) {
179
179
  const derived = theme.accent ? accentTokens(theme.accent) : undefined;
180
180
  const declarations = [
181
181
  theme.accent ? `--accent:${theme.accent}` : "",
package/dist/bytes.js ADDED
@@ -0,0 +1,42 @@
1
+ /** Lowercase hexadecimal, preserving each byte's leading zero. */
2
+ export function bytesToHex(bytes) {
3
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
4
+ }
5
+ export async function sha256Hex(value) {
6
+ return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))));
7
+ }
8
+ /** UTF-8 byte count without allocating encoded bytes; stops once the limit is exceeded. */
9
+ export function utf8Length(value, limit = Infinity) {
10
+ let bytes = 0;
11
+ for (let i = 0; i < value.length; i++) {
12
+ const code = value.charCodeAt(i);
13
+ if (code < 0x80)
14
+ bytes++;
15
+ else if (code < 0x800)
16
+ bytes += 2;
17
+ else if (code >= 0xd800 &&
18
+ code <= 0xdbff &&
19
+ value.charCodeAt(i + 1) >= 0xdc00 &&
20
+ value.charCodeAt(i + 1) <= 0xdfff) {
21
+ bytes += 4;
22
+ i++;
23
+ }
24
+ else
25
+ bytes += 3;
26
+ if (bytes > limit)
27
+ return bytes;
28
+ }
29
+ return bytes;
30
+ }
31
+ export function decodeBase64(value) {
32
+ const binary = atob(value);
33
+ const bytes = new Uint8Array(binary.length);
34
+ for (let index = 0; index < binary.length; index += 1)
35
+ bytes[index] = binary.charCodeAt(index);
36
+ return bytes;
37
+ }
38
+ /** Optional padding preserves callers that pad before handing input to atob. */
39
+ export function decodeBase64url(value, pad = false) {
40
+ const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
41
+ return decodeBase64(pad ? base64 + "=".repeat((4 - (base64.length % 4)) % 4) : base64);
42
+ }
@@ -1,8 +1,5 @@
1
- import { Budget, callScopeKnown, chargeView, recordedService, setViewService } from "./runtime/call.js";
2
- import { chargeCallBudget } from "./runtime/call-admission.js";
3
- export function attachCallAdmission(ctx, permit) {
4
- setViewService(ctx, Budget, { charge: chargeCallBudget(permit, ctx.signal) });
5
- }
1
+ // The Promise view of the call's admission service; authority stays internal.
2
+ import { Budget, callScopeKnown, chargeView, recordedService } from "./runtime/call.js";
6
3
  export function chargeConnectorCall(ctx) {
7
4
  return recordedService(ctx, Budget) || callScopeKnown(ctx) ? chargeView(ctx) : Promise.resolve();
8
5
  }
@@ -195,6 +195,11 @@ export class ConnectorCallAdmissionController {
195
195
  // The waiter's own clock: the fiber's Clock for an Effect caller, the
196
196
  // live `Date.now` for a Promise one.
197
197
  const now = waiter.now();
198
+ if (now - waiter.queuedAt >= this.queueTimeoutMs) {
199
+ this.rejectedTotal++;
200
+ waiter.outcome.settle(Exit.fail(this.concurrencyLimited(`queue wait exceeded ${this.queueTimeoutMs}ms`)));
201
+ continue;
202
+ }
198
203
  this.pruneBudget(state, now);
199
204
  const retryAfterMs = this.budgetRetryAfterMs(state, now);
200
205
  if (retryAfterMs !== undefined) {
@@ -43,6 +43,3 @@ export function recordRecovery(ctx, facts) {
43
43
  recordFor(ctx);
44
44
  serviceFor(ctx, Recovery)?.recordNow?.(facts);
45
45
  }
46
- export function recoveryFor(ctx) {
47
- return ctx ? serviceFor(ctx, RecordedRecovery)?.facts : undefined;
48
- }
@@ -2,6 +2,7 @@ import { detach, runEdge } from "./runtime/run.js";
2
2
  import { Deferred, Effect } from "effect";
3
3
  import { authFlow, authCall, authInvoke, authValue, authMethod, authProgram, registerAuth, authStorage, authMap, authAwait, } from "./auth/native.js";
4
4
  import { MAX_CACHE_TTL_MS, specTypeSchemas } from "@modelcontextprotocol/client";
5
+ import { advanceCatalogRevision, catalogRevision } from "./catalog-revision.js";
5
6
  import { callerOf } from "./connector-caller.js";
6
7
  import { CONFIG_DEFAULTS } from "./config-defaults.js";
7
8
  import { Context } from "effect";
@@ -208,6 +209,17 @@ class CatalogPolicy extends Context.Service()("connecta/operation/CatalogPolicy"
208
209
  export function attachCatalogCache(ctx, value) {
209
210
  setViewService(ctx, CatalogPolicy, value);
210
211
  }
212
+ /** Revalidate retained reads without storage I/O or credential material. */
213
+ export function retainedCatalogIdentity(ctx) {
214
+ const policy = serviceFor(ctx, CatalogPolicy);
215
+ return JSON.stringify([
216
+ policy?.partition ?? JSON.stringify(callerOf(ctx) ?? null),
217
+ ctx.baseUrl,
218
+ ctx.publicUrl,
219
+ catalogRevision(policy?.storage ?? ctx.storage),
220
+ policy?.credentialRevision?.() ?? 0,
221
+ ]);
222
+ }
211
223
  const defaultOwner = {};
212
224
  const scopes = new WeakMap();
213
225
  function cacheScope(ctx, connectorId, owner = defaultOwner) {
@@ -353,13 +365,20 @@ export function invalidateCatalogCache(storage, connectorId) {
353
365
  return runEdge(authProgram(invalidateCatalogCacheNative, [storage, connectorId]));
354
366
  }
355
367
  const invalidateCatalogCacheNative = /* @__PURE__ */ registerAuth(invalidateCatalogCache, (storage, connectorId) => authFlow(function* () {
356
- (yield authStorage(storage, "set", [
357
- responseCacheKeys.generation(connectorId),
358
- crypto.randomUUID(),
359
- {
360
- ttlSeconds: GENERATION_TTL_SECONDS,
361
- },
362
- ]));
368
+ advanceCatalogRevision(storage);
369
+ try {
370
+ (yield authStorage(storage, "set", [
371
+ responseCacheKeys.generation(connectorId),
372
+ crypto.randomUUID(),
373
+ {
374
+ ttlSeconds: GENERATION_TTL_SECONDS,
375
+ },
376
+ ]));
377
+ }
378
+ finally {
379
+ // A read started while the durable fence was pending must also expire.
380
+ advanceCatalogRevision(storage);
381
+ }
363
382
  }));
364
383
  /** Keys ignore self-reported serverInfo and bind the configured connector instead. */
365
384
  export function catalogClientOptions(ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity) {
@@ -1,7 +1,8 @@
1
+ import { bytesToHex } from "./bytes.js";
1
2
  const encoder = new TextEncoder();
2
3
  async function fingerprintBytes(bytes) {
3
4
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes)));
4
- const hex = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
5
+ const hex = bytesToHex(digest);
5
6
  return `sha256:${bytes.byteLength}:${hex}`;
6
7
  }
7
8
  /** Fingerprint an already serialized catalog without parsing/reserializing it. */
@@ -12,20 +13,3 @@ export async function fingerprintSerializedCatalog(serializedTools) {
12
13
  byteLength: bytes.byteLength,
13
14
  };
14
15
  }
15
- /**
16
- * Capture every persisted/discovered field in one deterministic snapshot.
17
- * Object key order is allowed to cause a conservative extra write; changing
18
- * any serialized field must change the digest.
19
- */
20
- export async function snapshotCatalog(tools) {
21
- const serializedTools = JSON.stringify(tools);
22
- if (serializedTools === undefined) {
23
- throw new TypeError("Tool catalog is not JSON-serializable.");
24
- }
25
- const bytes = encoder.encode(serializedTools);
26
- return {
27
- tools,
28
- fingerprint: await fingerprintBytes(bytes),
29
- serializedBytes: bytes,
30
- };
31
- }
@@ -0,0 +1,9 @@
1
+ // Only counters cross request boundaries. No catalogs, identities or secrets
2
+ // are retained here; weak owners are the deployment's cache store and vault.
3
+ const revisions = new WeakMap();
4
+ export function catalogRevision(owner) {
5
+ return owner ? (revisions.get(owner) ?? 0) : 0;
6
+ }
7
+ export function advanceCatalogRevision(owner) {
8
+ revisions.set(owner, catalogRevision(owner) + 1);
9
+ }
@@ -1,6 +1,7 @@
1
1
  import { registerAuthMethod } from "./auth/native.js";
2
2
  import { withAuthorizationHandoffEffect } from "./authorization-handoff.js";
3
3
  import { carryCatalogFreshness, catalogIsFresh } from "./catalog-freshness.js";
4
+ import { retainedCatalogIdentity } from "./catalog-cache.js";
4
5
  import { cloneTool } from "./tool-safety.js";
5
6
  import { hasControlCharacters } from "./tool-name.js";
6
7
  import { Effect, Result } from "effect";
@@ -402,9 +403,10 @@ export class CatalogService {
402
403
  readOnly;
403
404
  // The request-scoped catalog cache: one shared read per connector asked
404
405
  // about, started by the first asker and joined by every later one. A
405
- // success stays for the rest of the request, so a search and the call after
406
- // it see one catalog. The cache owns a deep copy and each asker receives
407
- // another, so discovery, invocation, and guest code never hold its objects.
406
+ // success stays while its in-process invalidation fence matches, so a search
407
+ // and the call after it see one current catalog. The cache owns a deep copy;
408
+ // each asker receives another at public boundaries. Search keeps private, stable safety views
409
+ // so its lexical index can be reused without exposing cached definitions.
408
410
  // A failure, or a read cancelled because every asker
409
411
  // left it, is dropped so the next ask reads again. The service lives and
410
412
  // dies with one request, so no read here is ever awaited from another.
@@ -415,6 +417,10 @@ export class CatalogService {
415
417
  // the success for the rest of the request, recorded by the read itself
416
418
  // rather than by an asker, whose probe deadline may have ended its wait.
417
419
  catalogs = new Map();
420
+ // Key by the private source array, never connector id: a replacement read
421
+ // gets new views and indexes. Each service belongs to one request and one
422
+ // registry visibility view (caller/pool, hidden tools and classification).
423
+ searchViews = new WeakMap();
418
424
  constructor(registry, baseUrl, options = {}) {
419
425
  this.registry = registry;
420
426
  this.baseUrl = baseUrl;
@@ -452,7 +458,7 @@ export class CatalogService {
452
458
  return runEdge(this.loadConnectorEffect(id, callOptions));
453
459
  }
454
460
  loadConnectorEffect(id, callOptions = {}) {
455
- return Effect.suspend(() => duringViewEffect({ requestScope: this.requestScope, signal: callOptions.signal ?? this.requestSignal }, (signal) => this.catalog(id, { ...callOptions, signal }))).pipe(Effect.catchDefect((error) => Effect.fail(error)));
461
+ return Effect.suspend(() => duringViewEffect({ requestScope: this.requestScope, signal: callOptions.signal ?? this.requestSignal }, (signal) => Effect.map(this.catalog(id, { ...callOptions, signal }), (tools) => carryCatalogFreshness(tools, tools.map(cloneTool))))).pipe(Effect.catchDefect((error) => Effect.fail(error)));
456
462
  }
457
463
  // One connector's catalog from the request-scoped cache, reading it through
458
464
  // the registry when this request has not yet. No asker's options govern the
@@ -460,11 +466,16 @@ export class CatalogService {
460
466
  // timeout, whichever asker starts it. Each asker waits under its own signal
461
467
  // and deadline, so a short-deadline call that starts the read and a search
462
468
  // that joins it each end on their own terms, and the read is cancelled only
463
- // once every asker has gone (#571).
469
+ // once every asker has gone.
464
470
  catalog(id, callOptions) {
465
471
  return Effect.suspend(() => {
466
- let read = this.catalogs.get(id);
467
- if (!read) {
472
+ // Authored static definitions never depend on a downstream cache or
473
+ // credentials. Keep their discovery independent of credential reads.
474
+ const identity = this.registry.getConnector(id)?.staticTools
475
+ ? "static"
476
+ : retainedCatalogIdentity(this.registry.contextFor(id, this.baseUrl, this.requestScope, callOptions));
477
+ let retained = this.catalogs.get(id);
478
+ if (!retained || retained.identity !== identity) {
468
479
  const started = new SharedRead((signal) => registryProgram(this.registry, "getTools", id, this.baseUrl, this.requestScope, {
469
480
  signal,
470
481
  timeoutMs: this.probeTimeoutMs,
@@ -472,16 +483,31 @@ export class CatalogService {
472
483
  }).pipe(Effect.map((tools) => carryCatalogFreshness(tools, tools.map(cloneTool)))), (succeeded) => {
473
484
  // Evicted before the read resumes anyone, so an asker that
474
485
  // retries on hearing of the failure starts a fresh read.
475
- if (!succeeded && this.catalogs.get(id) === started) {
486
+ if (!succeeded && this.catalogs.get(id)?.read === started) {
476
487
  this.catalogs.delete(id);
477
488
  }
478
489
  }, this.requestScope);
479
- this.catalogs.set(id, started);
480
- read = started;
490
+ retained = { identity, read: started };
491
+ this.catalogs.set(id, retained);
481
492
  }
482
- return Effect.map(read.join(callOptions.signal), (tools) => carryCatalogFreshness(tools, tools.map(cloneTool)));
493
+ return retained.read.join(callOptions.signal);
483
494
  });
484
495
  }
496
+ searchView(id, tools, includeWrites, safety) {
497
+ const readOnly = !includeWrites && (this.readOnly || this.registry.getConnector(id)?.readOnly === true);
498
+ const key = `${readOnly}:${safety ?? "all"}`;
499
+ let views = this.searchViews.get(tools);
500
+ if (!views) {
501
+ views = new Map();
502
+ this.searchViews.set(tools, views);
503
+ }
504
+ let view = views.get(key);
505
+ if (!view) {
506
+ view = toolsForSafety(readOnly ? tools.filter((tool) => tool.classification === "read") : tools, safety ?? "all");
507
+ views.set(key, view);
508
+ }
509
+ return view;
510
+ }
485
511
  // Every named catalog under its own probe deadline, `concurrency` at a time,
486
512
  // each settling in its input slot: an unavailable catalog is a Failure
487
513
  // there, not a failed discovery. `route` names the surface in a timeout.
@@ -489,9 +515,7 @@ export class CatalogService {
489
515
  // that asked for it. On a read-only endpoint a write is absent from search
490
516
  // and describe, as if the connector never had it. Guides are not filtered.
491
517
  discoveryCatalogs(ids, route, includeWrites = false) {
492
- return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => !includeWrites && (this.readOnly || this.registry.getConnector(id)?.readOnly === true)
493
- ? Effect.map(this.catalog(id, { signal }), (tools) => tools.filter((tool) => tool.classification === "read"))
494
- : this.catalog(id, { signal }), {
518
+ return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => Effect.map(this.catalog(id, { signal }), (tools) => this.searchView(id, tools, includeWrites, "all")), {
495
519
  ...(this.requestSignal ? { signal: this.requestSignal } : {}),
496
520
  timeoutMs: this.probeTimeoutMs,
497
521
  timeoutError: new ConnectorCallError("timeout", `${route} probe of "${id}" timed out after ${this.probeTimeoutMs}ms`),
@@ -512,7 +536,7 @@ export class CatalogService {
512
536
  return Effect.map(this.discoveryCatalogs(visible.map((connector) => connector.id), "tools/list", includeWrites), (results) => results.map((result, index) => {
513
537
  const connector = visible[index];
514
538
  return Result.isSuccess(result)
515
- ? { connector, tools: result.success }
539
+ ? { connector, tools: carryCatalogFreshness(result.success, result.success.map(cloneTool)) }
516
540
  : { connector, error: result.failure };
517
541
  }));
518
542
  })).pipe(Effect.catchDefect((error) => Effect.fail(error)));
@@ -581,7 +605,12 @@ export class CatalogService {
581
605
  }
582
606
  return {
583
607
  ok: true,
584
- resolved: { connector, toolName, definition, classificationFresh: catalogIsFresh(tools) },
608
+ resolved: {
609
+ connector,
610
+ toolName,
611
+ definition: cloneTool(definition),
612
+ classificationFresh: catalogIsFresh(tools),
613
+ },
585
614
  catalogMs: Date.now() - started,
586
615
  };
587
616
  },
@@ -672,7 +701,7 @@ export class CatalogService {
672
701
  const connectors = absence ? [] : args.connector ? (scopedConnector ? [scopedConnector] : []) : visibleConnectors;
673
702
  // Timeout messages name the route the caller used.
674
703
  const catalogs = yield* this.discoveryCatalogs(connectors.map((connector) => connector.id), this.searchRoute);
675
- const searchableCatalogs = catalogs.map((catalog) => Result.map(catalog, (tools) => toolsForSafety(tools, safety)));
704
+ const searchableCatalogs = catalogs.map((catalog, index) => Result.map(catalog, (tools) => this.searchView(connectors[index].id, tools, true, safety)));
676
705
  const matches = [];
677
706
  let matchMode = "all";
678
707
  const statistics = lexicalCorpusStatistics(searchableCatalogs.flatMap((catalog) => (Result.isSuccess(catalog) ? [catalog.success] : [])), retrievalQuery);
@@ -784,7 +813,8 @@ export class CatalogService {
784
813
  });
785
814
  const pageMatches = matches.slice(offset, offset + limit);
786
815
  const withSchemas = args.includeSchemas;
787
- const entries = pageMatches.map((match) => {
816
+ const entries = pageMatches.map((selected) => {
817
+ const match = { ...selected, tool: cloneTool(selected.tool) };
788
818
  const output = withSchemas ? this.outputSchema(match.connector.id, match.tool) : {};
789
819
  const input = match.tool.inputSchema ?? { type: "object" };
790
820
  const signature = withSchemas === "typescript"
@@ -1143,8 +1173,8 @@ export class CatalogService {
1143
1173
  };
1144
1174
  }
1145
1175
  const tools = catalog?.success ?? [];
1146
- const tool = tools.find((item) => item.name === addressResolution.toolName);
1147
- if (!tool) {
1176
+ const selected = tools.find((item) => item.name === addressResolution.toolName);
1177
+ if (!selected) {
1148
1178
  const message = `Unknown tool "${boundedEchoText(addressResolution.toolName)}" on connector "${addressResolution.connector.id}"`;
1149
1179
  const suggestions = describeSuggestions(addressResolution.connector.id, addressResolution.toolName, tools);
1150
1180
  return {
@@ -1160,6 +1190,7 @@ export class CatalogService {
1160
1190
  },
1161
1191
  };
1162
1192
  }
1193
+ const tool = cloneTool(selected);
1163
1194
  const input = tool.inputSchema ?? { type: "object" };
1164
1195
  const output = this.outputSchema(addressResolution.connector.id, tool);
1165
1196
  const description = summarizeDescription(tool.description, args.fullDescriptions === true);
@@ -20,6 +20,7 @@ export declare const CONFIG_DEFAULTS: {
20
20
  readonly hostCallTimeoutMs: 15000;
21
21
  readonly watchdogMs: 120000;
22
22
  readonly maxWrites: 10;
23
+ readonly maxResultChars: 24000;
23
24
  };
24
25
  readonly admission: {
25
26
  readonly requests: {
@@ -27,6 +27,7 @@ export const CONFIG_DEFAULTS = {
27
27
  hostCallTimeoutMs: 15_000,
28
28
  watchdogMs: 120_000,
29
29
  maxWrites: 10,
30
+ maxResultChars: 24_000,
30
31
  },
31
32
  admission: {
32
33
  requests: {
@@ -62,7 +62,7 @@ export declare class ConfigError extends TypeError {
62
62
  }
63
63
  interface WholeOptions {
64
64
  /** Smallest accepted value. */
65
- min: 0 | 1;
65
+ min: number;
66
66
  /** Largest accepted value, when there is one. */
67
67
  max?: number;
68
68
  /** Unit named in the `max` refusal, e.g. "milliseconds". */
@@ -27,12 +27,16 @@ function ownValue(value, key) {
27
27
  return Object.prototype.hasOwnProperty.call(value, key) ? value[key] : undefined;
28
28
  }
29
29
  export function whole(options) {
30
- const kind = options.min === 1 ? "positive" : "non-negative";
30
+ const kind = options.min === 0
31
+ ? "a non-negative whole number"
32
+ : options.min === 1
33
+ ? "a positive whole number"
34
+ : `a whole number of at least ${options.min.toLocaleString("en-US")}`;
31
35
  return {
32
36
  optional: true,
33
37
  parse: (value, path) => {
34
38
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < options.min) {
35
- fail(`${path} must be a ${kind} whole number.`);
39
+ fail(`${path} must be ${kind}.`);
36
40
  }
37
41
  if (options.max !== undefined && value > options.max) {
38
42
  fail(`${path} must be at most ${options.max.toLocaleString("en-US")}` + (options.unit ? ` ${options.unit}.` : "."));
package/dist/config.d.ts CHANGED
@@ -28,11 +28,12 @@ declare const discovery: {
28
28
  declare const calls: {
29
29
  /**
30
30
  * Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
31
- * `timeoutMs`. An explicit per-call value wins. Opt-in: unset by default, so
32
- * existing long-running calls gain no surprise deadline.
31
+ * `timeoutMs`. An explicit per-call value wins. Unset by default, when those
32
+ * calls fall back to `execute.hostCallTimeoutMs`.
33
33
  *
34
- * Each call makes one attempt. `execute_code` host calls are
35
- * unaffected because they already carry their own bound.
34
+ * Each call makes one attempt. `connecta.call` inside `execute_code` uses
35
+ * it too, ahead of `execute.hostCallTimeoutMs`; `connecta.result`,
36
+ * `connecta.skill` and `connecta.read` do not.
36
37
  */
37
38
  defaultTimeoutMs: import("./config-schema.js").Field<number, number | undefined, true>;
38
39
  /**
@@ -60,9 +61,12 @@ declare const execute: {
60
61
  /** Host calls one program may make. Default 20. */
61
62
  maxHostCalls: import("./config-schema.js").Field<number, number, true>;
62
63
  /**
63
- * Deadline for each host call a program makes, in milliseconds. Default
64
- * 15_000. Raise it for providers whose legitimate calls run longer, such as
65
- * analytics queries; `call_tool`'s own `timeoutMs` is unaffected.
64
+ * Deadline for a program's `connecta.result`, `connecta.skill` and
65
+ * `connecta.read`, in milliseconds. Default 15_000. Discovery uses
66
+ * `discovery.probeTimeoutMs`. `connecta.call` and the direct call tools use it only when
67
+ * `calls.defaultTimeoutMs` is unset, and an explicit per-call `timeoutMs`
68
+ * always wins. Raise it for providers whose legitimate calls run longer,
69
+ * such as analytics queries.
66
70
  */
67
71
  hostCallTimeoutMs: import("./config-schema.js").Field<number, number, true>;
68
72
  /**
@@ -78,6 +82,14 @@ declare const execute: {
78
82
  * budget every call already spends. Default 10.
79
83
  */
80
84
  maxWrites: import("./config-schema.js").Field<number, number, true>;
85
+ /**
86
+ * Serialized characters a program may return, shared with emitted text,
87
+ * before Connecta stashes the return and answers with a paging notice.
88
+ * Default 24_000 (~6k tokens). Raise it when agents need large results whole
89
+ * and the model's context can afford them: each paging round trip costs a
90
+ * model step. From 4_000 to 1_000_000.
91
+ */
92
+ maxResultChars: import("./config-schema.js").Field<number, number, true>;
81
93
  };
82
94
  declare const codeAdmission: {
83
95
  /** Simultaneous work admitted to this pool. */
@@ -333,11 +345,12 @@ declare const connectaConfig: {
333
345
  readonly shape: {
334
346
  /**
335
347
  * Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
336
- * `timeoutMs`. An explicit per-call value wins. Opt-in: unset by default, so
337
- * existing long-running calls gain no surprise deadline.
348
+ * `timeoutMs`. An explicit per-call value wins. Unset by default, when those
349
+ * calls fall back to `execute.hostCallTimeoutMs`.
338
350
  *
339
- * Each call makes one attempt. `execute_code` host calls are
340
- * unaffected because they already carry their own bound.
351
+ * Each call makes one attempt. `connecta.call` inside `execute_code` uses
352
+ * it too, ahead of `execute.hostCallTimeoutMs`; `connecta.result`,
353
+ * `connecta.skill` and `connecta.read` do not.
341
354
  */
342
355
  defaultTimeoutMs: import("./config-schema.js").Field<number, number | undefined, true>;
343
356
  /**
@@ -371,6 +384,7 @@ declare const connectaConfig: {
371
384
  hostCallTimeoutMs?: number | undefined;
372
385
  watchdogMs?: number | undefined;
373
386
  maxWrites?: number | undefined;
387
+ maxResultChars?: number | undefined;
374
388
  }, {
375
389
  readonly maxEmittedBytes: number;
376
390
  readonly maxEmittedBlocks: number;
@@ -378,6 +392,7 @@ declare const connectaConfig: {
378
392
  readonly hostCallTimeoutMs: number;
379
393
  readonly watchdogMs: number;
380
394
  readonly maxWrites: number;
395
+ readonly maxResultChars: number;
381
396
  }, true> & {
382
397
  readonly shape: {
383
398
  /**
@@ -391,9 +406,12 @@ declare const connectaConfig: {
391
406
  /** Host calls one program may make. Default 20. */
392
407
  maxHostCalls: import("./config-schema.js").Field<number, number, true>;
393
408
  /**
394
- * Deadline for each host call a program makes, in milliseconds. Default
395
- * 15_000. Raise it for providers whose legitimate calls run longer, such as
396
- * analytics queries; `call_tool`'s own `timeoutMs` is unaffected.
409
+ * Deadline for a program's `connecta.result`, `connecta.skill` and
410
+ * `connecta.read`, in milliseconds. Default 15_000. Discovery uses
411
+ * `discovery.probeTimeoutMs`. `connecta.call` and the direct call tools use it only when
412
+ * `calls.defaultTimeoutMs` is unset, and an explicit per-call `timeoutMs`
413
+ * always wins. Raise it for providers whose legitimate calls run longer,
414
+ * such as analytics queries.
397
415
  */
398
416
  hostCallTimeoutMs: import("./config-schema.js").Field<number, number, true>;
399
417
  /**
@@ -409,6 +427,14 @@ declare const connectaConfig: {
409
427
  * budget every call already spends. Default 10.
410
428
  */
411
429
  maxWrites: import("./config-schema.js").Field<number, number, true>;
430
+ /**
431
+ * Serialized characters a program may return, shared with emitted text,
432
+ * before Connecta stashes the return and answers with a paging notice.
433
+ * Default 24_000 (~6k tokens). Raise it when agents need large results whole
434
+ * and the model's context can afford them: each paging round trip costs a
435
+ * model step. From 4_000 to 1_000_000.
436
+ */
437
+ maxResultChars: import("./config-schema.js").Field<number, number, true>;
412
438
  };
413
439
  };
414
440
  /**
@@ -654,5 +680,6 @@ export declare function executeLimits(config: ResolvedConfig): {
654
680
  hostCallTimeoutMs: number;
655
681
  watchdogMs: number;
656
682
  maxWrites: number;
683
+ maxResultChars: number;
657
684
  };
658
685
  export {};