@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.
- package/CHANGELOG.md +80 -0
- package/README.md +2 -2
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/cloudflare-access.d.ts +3 -1
- package/dist/auth/cloudflare-access.js +22 -11
- package/dist/auth/downstream-oauth.js +195 -270
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/oauth-grant.js +11 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +2 -2
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-admission.js +5 -0
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-cache.js +26 -7
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-revision.js +9 -0
- package/dist/catalog-service.js +51 -20
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +41 -14
- package/dist/config.js +21 -71
- package/dist/connector-caller.js +1 -1
- package/dist/connectors/api-connector.js +4 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/remote-mcp.js +93 -69
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/credentials.js +3 -0
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/downstream-input.js +2 -2
- package/dist/execute.js +161 -116
- package/dist/executor-result.js +2 -1
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-runtime.js +5 -3
- package/dist/guest-types.d.ts +5 -5
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +2 -2
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +1 -1
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +1 -1
- package/dist/http/safety.js +1 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +1 -7
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.js +1 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +8 -2
- package/dist/meta-tools.js +27 -27
- package/dist/native-tools.js +2 -1
- package/dist/oauth-handoff.js +2 -2
- package/dist/operation-vocabulary.js +1 -1
- package/dist/operator-record.d.ts +8 -2
- package/dist/operator-record.js +18 -50
- package/dist/operator-ui/contract.d.ts +4 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +7 -2
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +1 -3
- package/dist/providers/_shared/rest/value-safety.js +26 -28
- package/dist/providers/breeze/api.js +2 -7
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/tools.js +3 -4
- package/dist/providers/ccb/api.js +1 -6
- package/dist/providers/ccb/index.js +2 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/docs/tools.js +2 -10
- package/dist/providers/drive/tools.js +12 -25
- package/dist/providers/forms/tools.js +1 -9
- package/dist/providers/gmail/tools.js +5 -21
- package/dist/providers/overflow/api.js +1 -3
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +2 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/sheets/tools.js +1 -9
- package/dist/providers/slides/tools.js +4 -13
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/tithely/api.js +2 -7
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +1 -6
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/tools.js +2 -1
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +35 -22
- package/dist/request-principal.js +15 -2
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +1 -1
- package/dist/routes/dispatch.js +10 -0
- package/dist/routes/mcp.js +23 -21
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +53 -7
- package/dist/routes/shared.d.ts +8 -8
- package/dist/routes/shared.js +22 -15
- package/dist/routes/ui-config.js +6 -6
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -4
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +1 -1
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/sent-secrets.d.ts +14 -6
- package/dist/sent-secrets.js +231 -85
- package/dist/server.js +4 -4
- package/dist/skills.js +8 -35
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +29 -124
- package/dist/storage/sql-native.js +14 -111
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +1 -12
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +4 -3
- package/dist/ui.js +3 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/documentation/README.md +6 -42
- package/documentation/architecture.md +190 -507
- package/documentation/auth.md +172 -58
- package/documentation/code-mode.md +98 -66
- package/documentation/deploying.md +19 -40
- package/documentation/integrating.md +8 -19
- package/documentation/meta-tools.md +23 -6
- package/documentation/migration-0.33.md +5 -5
- package/documentation/migration-0.34.md +2 -4
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +19 -119
- package/documentation/operator-ui.md +46 -8
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/examples/worker/AGENTS.md +4 -6
- package/examples/worker/README.md +20 -115
- package/guides/connecting-agents.md +7 -30
- package/guides/how-it-works.md +2 -2
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- 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
|
|
18
|
+
return decodeBase64url(value);
|
|
18
19
|
}
|
|
19
20
|
function record(value) {
|
|
20
21
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
package/dist/auth-elicitation.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/dist/branding.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
import {
|
|
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
|
}
|
package/dist/call-admission.js
CHANGED
|
@@ -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) {
|
package/dist/call-recovery.js
CHANGED
package/dist/catalog-cache.js
CHANGED
|
@@ -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
|
-
(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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 =
|
|
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
|
+
}
|
package/dist/catalog-service.js
CHANGED
|
@@ -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
|
|
406
|
-
// it see one catalog. The cache owns a deep copy
|
|
407
|
-
// another
|
|
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
|
|
469
|
+
// once every asker has gone.
|
|
464
470
|
catalog(id, callOptions) {
|
|
465
471
|
return Effect.suspend(() => {
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
480
|
-
|
|
490
|
+
retained = { identity, read: started };
|
|
491
|
+
this.catalogs.set(id, retained);
|
|
481
492
|
}
|
|
482
|
-
return
|
|
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) =>
|
|
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: {
|
|
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) =>
|
|
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((
|
|
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
|
|
1147
|
-
if (!
|
|
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);
|
package/dist/config-defaults.js
CHANGED
package/dist/config-schema.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export declare class ConfigError extends TypeError {
|
|
|
62
62
|
}
|
|
63
63
|
interface WholeOptions {
|
|
64
64
|
/** Smallest accepted value. */
|
|
65
|
-
min:
|
|
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". */
|
package/dist/config-schema.js
CHANGED
|
@@ -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 ===
|
|
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
|
|
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.
|
|
32
|
-
*
|
|
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. `
|
|
35
|
-
*
|
|
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
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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.
|
|
337
|
-
*
|
|
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. `
|
|
340
|
-
*
|
|
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
|
|
395
|
-
*
|
|
396
|
-
*
|
|
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 {};
|