@zackbart/connecta 0.40.0 → 0.41.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 +87 -0
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +30 -4
- package/dist/activity.js +28 -2
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +4 -41
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +12 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +51 -21
- package/dist/catalog-cache.js +62 -21
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-service.js +39 -12
- package/dist/catalog.js +103 -38
- package/dist/config.d.ts +10 -4
- package/dist/config.js +30 -7
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/connection.js +52 -18
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +4 -4
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +1 -0
- package/dist/describe-config.js +1 -0
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +8 -1
- package/dist/errors.js +2 -0
- package/dist/execute.js +20 -7
- package/dist/executor-result.js +1 -1
- package/dist/executors/quickjs-child.js +1 -1
- package/dist/executors/quickjs-runtime.js +1 -1
- package/dist/executors/quickjs.js +1 -1
- package/dist/guarded-transport.js +2 -0
- package/dist/http/client.js +7 -0
- package/dist/http/hatch.js +2 -0
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/index.js +10 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +72 -33
- package/dist/membership.d.ts +1 -1
- package/dist/meta-tools.js +27 -13
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-tools.js +11 -4
- package/dist/node.d.ts +7 -0
- package/dist/node.js +82 -3
- package/dist/operation-activity.js +103 -0
- package/dist/operator-record.d.ts +8 -0
- package/dist/operator-record.js +15 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.js +1 -1
- package/dist/providers/github/index.js +1 -1
- package/dist/providers/gmail/index.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +1 -1
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -1
- package/dist/providers/slides/index.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/api.js +1 -0
- package/dist/registry.js +16 -4
- package/dist/resource-read.js +21 -2
- package/dist/route-names.js +44 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/mcp.js +32 -13
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +2 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +132 -43
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +7 -1
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/types.d.ts +5 -0
- package/dist/url-safety.js +38 -20
- package/dist/utility-recovery.js +33 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +33 -8
- package/documentation/architecture.md +48 -1
- package/documentation/auth.md +116 -36
- package/documentation/code-mode.md +61 -46
- package/documentation/deploying.md +25 -1
- package/documentation/meta-tools.md +113 -11
- package/documentation/operating.md +86 -1
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +16 -14
- package/package.json +10 -8
- package/templates/node/package.json +1 -1
package/dist/catalog-cache.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { traceFor } from "./timing-trace.js";
|
|
2
|
+
import { markCatalogGeneration } from "./catalog-freshness.js";
|
|
1
3
|
import { detach, runEdge } from "./runtime/run.js";
|
|
2
4
|
import { Deferred, Effect } from "effect";
|
|
3
5
|
import { authFlow, authCall, authInvoke, authValue, authMethod, authProgram, registerAuth, authStorage, authStorageMany, authMap, authAwait, } from "./auth/native.js";
|
|
@@ -446,6 +448,49 @@ function rememberValidated(key, value, bytes) {
|
|
|
446
448
|
validatedCatalogs.set(key, { value, bytes });
|
|
447
449
|
validatedBytes += bytes;
|
|
448
450
|
}
|
|
451
|
+
const catalogLocation = authMethod((ctx, config, authPartition) => authFlow(function* () {
|
|
452
|
+
const policy = serviceFor(ctx, CatalogPolicy);
|
|
453
|
+
const partition = JSON.stringify([
|
|
454
|
+
policy?.partition ?? JSON.stringify(callerOf(ctx) ?? null),
|
|
455
|
+
ctx.baseUrl,
|
|
456
|
+
ctx.publicUrl,
|
|
457
|
+
authPartition,
|
|
458
|
+
]);
|
|
459
|
+
const sharedPartition = JSON.stringify([
|
|
460
|
+
"host-shared",
|
|
461
|
+
policy?.sharedPartition ?? JSON.stringify([ctx.baseUrl, ctx.publicUrl, callerOf(ctx)?.pool]),
|
|
462
|
+
authPartition,
|
|
463
|
+
]);
|
|
464
|
+
const configHash = (yield authCall(fingerprintSerializedCatalog, [
|
|
465
|
+
JSON.stringify([
|
|
466
|
+
config,
|
|
467
|
+
policy?.defaultTtlMs ?? 300000,
|
|
468
|
+
policy?.minTtlMs ?? 0,
|
|
469
|
+
policy?.maxTtlMs ?? MAX_CACHE_TTL_MS,
|
|
470
|
+
]),
|
|
471
|
+
])).fingerprint;
|
|
472
|
+
return { partition, sharedPartition, configHash };
|
|
473
|
+
}));
|
|
474
|
+
/** Prefetch candidates only; authority, identity and acceptance stay fresh. */
|
|
475
|
+
export const prefetchCatalog = authMethod((ctx, connectorId, config, authPartition, sharing, credentialIdentity, negotiationKey) => authFlow(function* () {
|
|
476
|
+
const { partition, sharedPartition, configHash } = yield authCall(catalogLocation, [ctx, config, authPartition]);
|
|
477
|
+
const slots = sharing === "shared" ? [partition, sharedPartition] : [partition];
|
|
478
|
+
const suffixes = [];
|
|
479
|
+
for (const slot of slots)
|
|
480
|
+
suffixes.push((yield authCall(fingerprintSerializedCatalog, [
|
|
481
|
+
JSON.stringify(["tools/list", slot, credentialIdentity ?? null]),
|
|
482
|
+
])).fingerprint);
|
|
483
|
+
const storage = serviceFor(ctx, CatalogPolicy)?.storage ?? ctx.storage;
|
|
484
|
+
yield ownerReads(Context.get(viewServices(ctx), RequestScope)).prefetch([
|
|
485
|
+
{ storage: ctx.storage, key: negotiationKey },
|
|
486
|
+
{ storage, key: responseCacheKeys.generation(connectorId) },
|
|
487
|
+
], (storage, keys) => authStorageMany(storage, keys), {
|
|
488
|
+
storage,
|
|
489
|
+
generationKey: responseCacheKeys.generation(connectorId),
|
|
490
|
+
prefix: responseCacheKeys.locatorPrefix(connectorId, configHash),
|
|
491
|
+
suffixes,
|
|
492
|
+
});
|
|
493
|
+
}));
|
|
449
494
|
/** Keys ignore self-reported serverInfo and bind the configured connector instead. */
|
|
450
495
|
export function catalogClientOptions(ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity, connectionless = false) {
|
|
451
496
|
return runEdge(authProgram(catalogClientOptionsNative, [
|
|
@@ -475,29 +520,11 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
475
520
|
// canonical request owner, never the initiating call's live services.
|
|
476
521
|
const cacheMethod = (program) => authMethod((...args) => program(...args).pipe(Effect.provideService(RequestScope, request)));
|
|
477
522
|
const policy = serviceFor(ctx, CatalogPolicy);
|
|
478
|
-
const partition =
|
|
479
|
-
policy?.partition ?? JSON.stringify(callerOf(ctx) ?? null),
|
|
480
|
-
ctx.baseUrl,
|
|
481
|
-
ctx.publicUrl,
|
|
482
|
-
authPartition,
|
|
483
|
-
]);
|
|
484
|
-
const sharedPartition = JSON.stringify([
|
|
485
|
-
"host-shared",
|
|
486
|
-
policy?.sharedPartition ?? JSON.stringify([ctx.baseUrl, ctx.publicUrl, callerOf(ctx)?.pool]),
|
|
487
|
-
authPartition,
|
|
488
|
-
]);
|
|
523
|
+
const { partition, sharedPartition, configHash } = yield authCall(catalogLocation, [ctx, config, authPartition]);
|
|
489
524
|
const storage = policy?.storage ?? ctx.storage;
|
|
490
525
|
let storeIdentity = catalogStores.get(storage);
|
|
491
526
|
if (!storeIdentity)
|
|
492
527
|
catalogStores.set(storage, (storeIdentity = crypto.randomUUID()));
|
|
493
|
-
const configHash = (yield authCall(fingerprintSerializedCatalog, [
|
|
494
|
-
JSON.stringify([
|
|
495
|
-
config,
|
|
496
|
-
policy?.defaultTtlMs ?? 300000,
|
|
497
|
-
policy?.minTtlMs ?? 0,
|
|
498
|
-
policy?.maxTtlMs ?? MAX_CACHE_TTL_MS,
|
|
499
|
-
]),
|
|
500
|
-
])).fingerprint;
|
|
501
528
|
const scope = cacheScope(ctx, connectorId, owner);
|
|
502
529
|
let unavailable = false;
|
|
503
530
|
const connectionGeneration = {};
|
|
@@ -676,6 +703,8 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
676
703
|
get: cacheMethod((key) => authFlow(function* () {
|
|
677
704
|
const operation = capture();
|
|
678
705
|
const { ctx, stopped, io } = operation;
|
|
706
|
+
const trace = traceFor(ctx.requestScope);
|
|
707
|
+
trace?.record("catalog.cache.lookup");
|
|
679
708
|
const root = (yield authCall(address, [key, operation]));
|
|
680
709
|
if (!root || stopped())
|
|
681
710
|
return undefined;
|
|
@@ -692,7 +721,10 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
692
721
|
]));
|
|
693
722
|
roots.push(responseCacheKeys.entry(responseCacheKeys.namespace(connectorId, configHash, operation.generation.value), fingerprint));
|
|
694
723
|
}
|
|
695
|
-
|
|
724
|
+
yield reads.prefetch(roots.map((key) => ({ storage, key })), (storage, keys) => authStorageMany(storage, keys));
|
|
725
|
+
const values = [];
|
|
726
|
+
for (const root of roots)
|
|
727
|
+
values.push(yield reads.read(storage, root, authStorage(storage, "get", [root])));
|
|
696
728
|
listing.manifests ??= new Map();
|
|
697
729
|
roots.forEach((root, i) => listing.manifests.set(root, values[i] ?? null));
|
|
698
730
|
}
|
|
@@ -735,8 +767,10 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
735
767
|
// Hash the chunks again above, never trust the manifest alone.
|
|
736
768
|
const reuseKey = JSON.stringify([storeIdentity, root, partition, m.fingerprint]);
|
|
737
769
|
let accepted = validatedCatalogs.get(reuseKey)?.value;
|
|
770
|
+
trace?.record(accepted ? "catalog.validated.hit" : "catalog.validated.miss");
|
|
738
771
|
if (!accepted) {
|
|
739
772
|
const result = JSON.parse(value);
|
|
773
|
+
trace?.record("catalog.decode", 0, fingerprint.byteLength);
|
|
740
774
|
const items = catalogItems(method, result);
|
|
741
775
|
if (!Array.isArray(items) ||
|
|
742
776
|
items.length > MAX_CATALOG_TOOLS ||
|
|
@@ -744,6 +778,7 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
744
778
|
(result.resultType !== undefined && result.resultType !== "complete"))
|
|
745
779
|
return undefined;
|
|
746
780
|
const validated = (yield authInvoke(catalogSchema(method)["~standard"], "validate", [result]));
|
|
781
|
+
trace?.record("catalog.validate");
|
|
747
782
|
if (stopped() || validated.issues)
|
|
748
783
|
return undefined;
|
|
749
784
|
accepted = validated.value;
|
|
@@ -751,12 +786,16 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
751
786
|
// Redaction depends on this request's sent credentials. It must
|
|
752
787
|
// still run, and returns detached objects from the frozen facts.
|
|
753
788
|
const clean = intake(ctx, method, accepted);
|
|
789
|
+
trace?.record("catalog.intake");
|
|
790
|
+
trace?.record("catalog.chunks", 0, fingerprint.byteLength, 0, 0, m.chunkCount);
|
|
754
791
|
rememberValidated(reuseKey, accepted, fingerprint.byteLength);
|
|
755
792
|
if (m.expiresAt <= Date.now()) {
|
|
793
|
+
trace?.record("catalog.cache.expired");
|
|
756
794
|
if (method === "tools/list" && (!staleTools || staleTools.manifest.fetchedAt < m.fetchedAt))
|
|
757
795
|
staleTools = { result: clean, manifest: m };
|
|
758
796
|
return undefined;
|
|
759
797
|
}
|
|
798
|
+
trace?.record("catalog.cache.hit");
|
|
760
799
|
observeCatalogFetch(ctx, m.fetchedAt, method);
|
|
761
800
|
observeCatalogExpiry(ctx, method, clean.ttlMs, m.expiresAt);
|
|
762
801
|
return { value: JSON.stringify(clean), stamp: m.stamp, expiresAt: m.expiresAt, scope: m.scope };
|
|
@@ -933,7 +972,9 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
933
972
|
logFailure(listingCtx.logger, "catalog read failed", failureRecord({ connector: connectorId }, error));
|
|
934
973
|
}
|
|
935
974
|
}
|
|
936
|
-
|
|
975
|
+
const result = yield authCall(read, []);
|
|
976
|
+
markCatalogGeneration(listingCtx.requestScope, connectorId, listing.generation.value);
|
|
977
|
+
return result;
|
|
937
978
|
}
|
|
938
979
|
finally {
|
|
939
980
|
listing.ended = true;
|
|
@@ -11,3 +11,16 @@ export function carryCatalogFreshness(source, copy) {
|
|
|
11
11
|
export function catalogIsFresh(tools) {
|
|
12
12
|
return (deadlines.get(tools) ?? 0) > Date.now();
|
|
13
13
|
}
|
|
14
|
+
// Primitive accepted-generation evidence lives only beside the current request.
|
|
15
|
+
const generations = new WeakMap();
|
|
16
|
+
export function markCatalogGeneration(scope, connector, generation) {
|
|
17
|
+
if (!scope || generation === undefined)
|
|
18
|
+
return;
|
|
19
|
+
let values = generations.get(scope);
|
|
20
|
+
if (!values)
|
|
21
|
+
generations.set(scope, (values = new Map()));
|
|
22
|
+
values.set(connector, generation);
|
|
23
|
+
}
|
|
24
|
+
export function catalogGenerationFor(scope, connector) {
|
|
25
|
+
return generations.get(scope)?.get(connector);
|
|
26
|
+
}
|
package/dist/catalog-service.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { utilityRecovery } from "./utility-recovery.js";
|
|
2
|
+
import { traceFor } from "./timing-trace.js";
|
|
3
|
+
import { utf8Length } from "./bytes.js";
|
|
4
|
+
import { catalogOwnerPartition } from "./catalog-policy.js";
|
|
1
5
|
import { registerAuthMethod } from "./auth/native.js";
|
|
2
6
|
import { META_ROUTES, routeToolInPlace } from "./agent-routes.js";
|
|
3
7
|
import { withAuthorizationHandoffEffect } from "./authorization-handoff.js";
|
|
4
|
-
import { carryCatalogFreshness, catalogIsFresh } from "./catalog-freshness.js";
|
|
8
|
+
import { carryCatalogFreshness, catalogIsFresh, catalogGenerationFor } from "./catalog-freshness.js";
|
|
5
9
|
import { retainedCatalogIdentity } from "./catalog-cache.js";
|
|
6
10
|
import { cloneTool } from "./tool-safety.js";
|
|
7
11
|
import { hasControlCharacters } from "./tool-name.js";
|
|
8
12
|
import { Effect, Result } from "effect";
|
|
9
|
-
import { compactDiscoverySchema, compactDescriptionSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, typescriptSignature, } from "./catalog.js";
|
|
13
|
+
import { prepareLexicalCatalog, carryLexicalCatalog, compactDiscoverySchema, compactDescriptionSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, typescriptSignature, } from "./catalog.js";
|
|
10
14
|
import { resolveDiscoveryConcurrency } from "./concurrency.js";
|
|
11
15
|
import { boundedEchoText, classifyCallError, ConnectorCallError, framingError } from "./errors.js";
|
|
12
16
|
import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName } from "./skills.js";
|
|
@@ -20,6 +24,8 @@ import { keepErrorName } from "./redaction-identity.js";
|
|
|
20
24
|
import { operationVocabularyMatches, searchSynonyms } from "./operation-vocabulary.js";
|
|
21
25
|
/** The slice of an unscoped search's budget kept for recording what its probes saw. */
|
|
22
26
|
const SETTLE_RESERVE_MS = 500;
|
|
27
|
+
const lexicalStores = new WeakMap();
|
|
28
|
+
let nextLexicalStore = 0;
|
|
23
29
|
/** How long a probe claim outlives its request's discovery budget, for settlement and clock skew. */
|
|
24
30
|
const CLAIM_MARGIN_MS = 2_000;
|
|
25
31
|
/** A connector unscoped search skipped because its discovery failed recently. */
|
|
@@ -299,19 +305,17 @@ const catalogFailureProperties = {
|
|
|
299
305
|
},
|
|
300
306
|
},
|
|
301
307
|
retry: { type: "string" },
|
|
308
|
+
// Handoff fields above carry the detailed authorization contract. Keep
|
|
309
|
+
// additive action advice compact in the always-loaded search schema.
|
|
302
310
|
nextAction: {
|
|
303
311
|
type: "object",
|
|
304
|
-
required: ["tool"
|
|
305
|
-
additionalProperties: false,
|
|
312
|
+
anyOf: [{ required: ["tool"] }, { required: ["action"] }],
|
|
306
313
|
properties: {
|
|
307
314
|
tool: { const: "authorize_connector" },
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
properties: { connector: { type: "string" } },
|
|
313
|
-
},
|
|
314
|
-
operatorHandoff: { type: "string" },
|
|
315
|
+
action: { enum: ["contact_operator", "inspect_capabilities"] },
|
|
316
|
+
purpose: STRING,
|
|
317
|
+
operatorHandoff: STRING,
|
|
318
|
+
arguments: { type: "object", properties: { connector: STRING } },
|
|
315
319
|
},
|
|
316
320
|
},
|
|
317
321
|
};
|
|
@@ -533,6 +537,7 @@ export class CatalogService {
|
|
|
533
537
|
timeoutMs: this.probeTimeoutMs,
|
|
534
538
|
...(this.defer ? { defer: this.defer } : {}),
|
|
535
539
|
}).pipe(Effect.map((tools) => {
|
|
540
|
+
traceFor(this.requestScope)?.record("catalog.accepted", 0, utf8Length(JSON.stringify(tools)), 0, 0, tools.length);
|
|
536
541
|
// An authored catalog lists its declared descriptions for this request's surface.
|
|
537
542
|
const connector = this.registry.getConnector(id);
|
|
538
543
|
return carryCatalogFreshness(tools, tools.map((tool) => {
|
|
@@ -565,6 +570,7 @@ export class CatalogService {
|
|
|
565
570
|
let view = views.get(key);
|
|
566
571
|
if (!view) {
|
|
567
572
|
view = toolsForSafety(readOnly ? tools.filter((tool) => tool.classification === "read") : tools, safety ?? "all");
|
|
573
|
+
carryLexicalCatalog(tools, view);
|
|
568
574
|
views.set(key, view);
|
|
569
575
|
}
|
|
570
576
|
return view;
|
|
@@ -793,6 +799,9 @@ export class CatalogService {
|
|
|
793
799
|
message: `Connector "${connector.id}" was skipped because its catalog lookup failed recently (${failure.code}). Search with connector "${connector.id}" to try it now.`,
|
|
794
800
|
retryable: true,
|
|
795
801
|
retryAfterMs: failure.retryAfterMs,
|
|
802
|
+
...(["unavailable", "catalog_lookup_failed", "unsupported_capability"].includes(failure.code)
|
|
803
|
+
? { nextAction: utilityRecovery(failure.code, "Catalog lookup is unavailable.").nextAction }
|
|
804
|
+
: {}),
|
|
796
805
|
};
|
|
797
806
|
}
|
|
798
807
|
const classified = classifyCallError(failure, "catalog_lookup_failed");
|
|
@@ -830,6 +839,9 @@ export class CatalogService {
|
|
|
830
839
|
error.retry =
|
|
831
840
|
"Ask the provider's resource owner or administrator to grant the required permission or scope, then retry. Reconnecting alone will not grant access.";
|
|
832
841
|
}
|
|
842
|
+
if (["unavailable", "catalog_lookup_failed", "unsupported_capability"].includes(error.code)) {
|
|
843
|
+
error.nextAction = utilityRecovery(error.code, error.message).nextAction;
|
|
844
|
+
}
|
|
833
845
|
return yield* withAuthorizationHandoffEffect(this.requestScope, error);
|
|
834
846
|
});
|
|
835
847
|
}
|
|
@@ -877,7 +889,22 @@ export class CatalogService {
|
|
|
877
889
|
const connectors = absence ? [] : args.connector ? (scopedConnector ? [scopedConnector] : []) : visibleConnectors;
|
|
878
890
|
// Timeout messages name the route the caller used.
|
|
879
891
|
const catalogs = yield* this.searchCatalogs(connectors.map((connector) => connector.id), args.connector !== undefined);
|
|
880
|
-
const searchableCatalogs = catalogs.map((catalog, index) => Result.map(catalog, (tools) =>
|
|
892
|
+
const searchableCatalogs = catalogs.map((catalog, index) => Result.map(catalog, (tools) => {
|
|
893
|
+
const id = connectors[index].id;
|
|
894
|
+
const ctx = this.registry.contextFor(id, this.baseUrl, this.requestScope);
|
|
895
|
+
const { storage, partition } = catalogOwnerPartition(ctx);
|
|
896
|
+
let store = lexicalStores.get(storage);
|
|
897
|
+
if (store === undefined)
|
|
898
|
+
lexicalStores.set(storage, (store = ++nextLexicalStore));
|
|
899
|
+
prepareLexicalCatalog(tools, JSON.stringify([
|
|
900
|
+
store,
|
|
901
|
+
id,
|
|
902
|
+
partition,
|
|
903
|
+
retainedCatalogIdentity(ctx),
|
|
904
|
+
catalogGenerationFor(this.requestScope, id),
|
|
905
|
+
]));
|
|
906
|
+
return this.searchView(id, tools, true, safety);
|
|
907
|
+
}));
|
|
881
908
|
const matches = [];
|
|
882
909
|
let matchMode = "all";
|
|
883
910
|
// Provider-declared synonyms widen term matching within their own catalog only.
|
package/dist/catalog.js
CHANGED
|
@@ -164,38 +164,93 @@ export function lexicalSearchContent(query) {
|
|
|
164
164
|
return contentTerms.join(" ");
|
|
165
165
|
}
|
|
166
166
|
const searchIndexes = new WeakMap();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
167
|
+
// Only immutable token strings and numeric positions cross requests. The key
|
|
168
|
+
// holds exactly what buildIndex reads, in order, so schemas cost nothing here;
|
|
169
|
+
// it avoids hash collisions and charges its UTF-16 storage as well.
|
|
170
|
+
const retainedIndexes = new Map();
|
|
171
|
+
const INDEX_BYTES = 8 * 1024 * 1024;
|
|
172
|
+
let retainedBytes = 0;
|
|
173
|
+
function buildIndex(tools) {
|
|
174
|
+
const nameTokenDocuments = new Map();
|
|
175
|
+
const descriptionTokenDocuments = new Map();
|
|
176
|
+
const addTokens = (target, tokens, position) => {
|
|
177
|
+
for (const token of new Set(tokens)) {
|
|
178
|
+
const documents = target.get(token) ?? [];
|
|
179
|
+
documents.push(position);
|
|
180
|
+
target.set(token, documents);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const documents = tools.map((tool, position) => {
|
|
184
|
+
const nameTokens = lexicalTokens(tool.name);
|
|
185
|
+
const descriptionTokens = [...new Set(lexicalTokens(tool.description ?? ""))];
|
|
186
|
+
addTokens(nameTokenDocuments, nameTokens, position);
|
|
187
|
+
addTokens(descriptionTokenDocuments, descriptionTokens, position);
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
name: nameTokens.join(" "),
|
|
190
|
+
nameTokens: Object.freeze(nameTokens),
|
|
191
|
+
descriptionTokens: Object.freeze(descriptionTokens),
|
|
190
192
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
});
|
|
194
|
+
for (const map of [nameTokenDocuments, descriptionTokenDocuments])
|
|
195
|
+
for (const positions of map.values())
|
|
196
|
+
Object.freeze(positions);
|
|
197
|
+
// Maps remain private to this module and are never mutated after construction.
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
documents: Object.freeze(documents),
|
|
200
|
+
nameTokenDocuments,
|
|
201
|
+
descriptionTokenDocuments,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function bindIndex(tools, index) {
|
|
205
|
+
const view = { index, tools: new Map(tools.map((tool, position) => [position, tool])) };
|
|
206
|
+
searchIndexes.set(tools, view);
|
|
207
|
+
return view;
|
|
208
|
+
}
|
|
209
|
+
/** Only call after complete catalog acceptance, redaction and classification. */
|
|
210
|
+
export function prepareLexicalCatalog(tools, partition) {
|
|
211
|
+
if (searchIndexes.has(tools))
|
|
212
|
+
return;
|
|
213
|
+
const key = JSON.stringify([partition, tools.map((tool) => [tool.name, tool.description ?? ""])]);
|
|
214
|
+
const retained = retainedIndexes.get(key);
|
|
215
|
+
if (retained) {
|
|
216
|
+
bindIndex(tools, retained.index);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const index = buildIndex(tools);
|
|
220
|
+
bindIndex(tools, index);
|
|
221
|
+
let bytes = key.length * 2 + 128;
|
|
222
|
+
for (const doc of index.documents) {
|
|
223
|
+
bytes += 128 + doc.name.length * 2;
|
|
224
|
+
for (const tokens of [doc.nameTokens, doc.descriptionTokens])
|
|
225
|
+
for (const token of tokens)
|
|
226
|
+
bytes += 32 + token.length * 2;
|
|
197
227
|
}
|
|
198
|
-
|
|
228
|
+
for (const map of [index.nameTokenDocuments, index.descriptionTokenDocuments])
|
|
229
|
+
for (const [token, positions] of map)
|
|
230
|
+
bytes += 96 + token.length * 2 + positions.length * 8;
|
|
231
|
+
if (bytes > INDEX_BYTES)
|
|
232
|
+
return;
|
|
233
|
+
while (retainedIndexes.size >= 4 || retainedBytes + bytes > INDEX_BYTES) {
|
|
234
|
+
const oldest = retainedIndexes.keys().next().value;
|
|
235
|
+
retainedBytes -= retainedIndexes.get(oldest).bytes;
|
|
236
|
+
retainedIndexes.delete(oldest);
|
|
237
|
+
}
|
|
238
|
+
retainedIndexes.set(key, { index, bytes });
|
|
239
|
+
retainedBytes += bytes;
|
|
240
|
+
}
|
|
241
|
+
/** Project immutable positions onto this request's currently visible objects. */
|
|
242
|
+
export function carryLexicalCatalog(source, visible) {
|
|
243
|
+
const view = searchIndexes.get(source);
|
|
244
|
+
if (!view || source === visible)
|
|
245
|
+
return;
|
|
246
|
+
const allowed = new Set(visible);
|
|
247
|
+
searchIndexes.set(visible, {
|
|
248
|
+
index: view.index,
|
|
249
|
+
tools: new Map([...view.tools].filter(([, tool]) => allowed.has(tool))),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function indexFor(tools) {
|
|
253
|
+
return searchIndexes.get(tools) ?? bindIndex(tools, buildIndex(tools));
|
|
199
254
|
}
|
|
200
255
|
/**
|
|
201
256
|
* Whole-token equality is the ordinary lexical match. A deliberately narrow
|
|
@@ -289,11 +344,15 @@ export function lexicalCorpusStatistics(toolSets, query, synonymsFor) {
|
|
|
289
344
|
for (const tools of toolSets) {
|
|
290
345
|
const index = indexFor(tools);
|
|
291
346
|
for (const candidate of synonymCandidates(term, synonymsFor?.(tools))) {
|
|
292
|
-
for (const
|
|
293
|
-
|
|
347
|
+
for (const position of index.index.nameTokenDocuments.get(candidate) ?? []) {
|
|
348
|
+
const tool = index.tools.get(position);
|
|
349
|
+
if (tool)
|
|
350
|
+
termNameMatches.add(tool);
|
|
294
351
|
}
|
|
295
|
-
for (const
|
|
296
|
-
|
|
352
|
+
for (const position of index.index.descriptionTokenDocuments.get(candidate) ?? []) {
|
|
353
|
+
const tool = index.tools.get(position);
|
|
354
|
+
if (tool)
|
|
355
|
+
termDescriptionMatches.add(tool);
|
|
297
356
|
}
|
|
298
357
|
}
|
|
299
358
|
}
|
|
@@ -365,13 +424,19 @@ export function rankTools(tools, query, mode = "all", statistics = lexicalCorpus
|
|
|
365
424
|
const exactNamePhrase = normalized(exactNameQuery);
|
|
366
425
|
const terms = [...new Set(phrase.split(/\s+/).filter(Boolean))];
|
|
367
426
|
const ranked = [];
|
|
368
|
-
indexFor(tools)
|
|
369
|
-
|
|
427
|
+
const view = indexFor(tools);
|
|
428
|
+
let order = 0;
|
|
429
|
+
view.index.documents.forEach((doc, position) => {
|
|
430
|
+
const tool = view.tools.get(position);
|
|
431
|
+
if (!tool)
|
|
432
|
+
return;
|
|
433
|
+
const visibleOrder = order++;
|
|
434
|
+
const scored = scoreDocument({ ...doc, tool }, phrase, terms, mode, statistics);
|
|
370
435
|
if (scored !== null) {
|
|
371
436
|
ranked.push({
|
|
372
|
-
tool
|
|
437
|
+
tool,
|
|
373
438
|
score: scored.score,
|
|
374
|
-
order,
|
|
439
|
+
order: visibleOrder,
|
|
375
440
|
exactName: queryContainsExactName(doc, exactNamePhrase),
|
|
376
441
|
matchedTermCount: scored.matchedTermCount,
|
|
377
442
|
});
|
package/dist/config.d.ts
CHANGED
|
@@ -211,7 +211,7 @@ declare const pool: {
|
|
|
211
211
|
* What this pool is for, in plain words for the model: appended to the MCP
|
|
212
212
|
* `initialize` instructions at `/mcp/<name>` and `/agent/<name>`, so a
|
|
213
213
|
* caller learns its scope ("Team tier: shared tools only."). Trimmed, 1 to
|
|
214
|
-
* 500
|
|
214
|
+
* 500 UTF-16 code units, no line breaks or control characters. It grants and hides nothing; plain
|
|
215
215
|
* `/mcp` never shows it.
|
|
216
216
|
*/
|
|
217
217
|
description: import("./config-schema.js").Field<string, string | undefined, true>;
|
|
@@ -283,8 +283,9 @@ declare const connectaConfig: {
|
|
|
283
283
|
*/
|
|
284
284
|
storage: import("./config-schema.js").Field<KVStorage, KVStorage, true>;
|
|
285
285
|
/**
|
|
286
|
-
* Public base URL. Defaults to the request origin per-request
|
|
287
|
-
*
|
|
286
|
+
* Public base URL. Defaults to the request origin per-request, except that
|
|
287
|
+
* clerkAuth requires it here or as its own `publicUrl`. Configuring an HTTPS
|
|
288
|
+
* URL also redirects matching inbound HTTP requests to HTTPS.
|
|
288
289
|
*/
|
|
289
290
|
publicUrl: import("./config-schema.js").Field<string, string | undefined, true>;
|
|
290
291
|
/**
|
|
@@ -294,7 +295,10 @@ declare const connectaConfig: {
|
|
|
294
295
|
allowedOrigins: import("./config-schema.js").Field<readonly string[] | "*", readonly string[] | "*" | undefined, true>;
|
|
295
296
|
/** Optional recorder and reader, created by activityHistory() from /activity. */
|
|
296
297
|
activity: import("./config-schema.js").Field<ActivityModule, ActivityModule | undefined, true>;
|
|
297
|
-
/**
|
|
298
|
+
/**
|
|
299
|
+
* Replaceable owner-partitioned credential storage. Omit for config-owned
|
|
300
|
+
* secrets. With any OAuth connector it must implement `seal` and `open`.
|
|
301
|
+
*/
|
|
298
302
|
vault: import("./config-schema.js").Field<CredentialVault, CredentialVault | undefined, true>;
|
|
299
303
|
/** Optional connection UI, created by operatorUi() from /ui. */
|
|
300
304
|
ui: import("./config-schema.js").Field<OperatorSurface, OperatorSurface | undefined, true>;
|
|
@@ -542,6 +546,8 @@ declare const connectaConfig: {
|
|
|
542
546
|
};
|
|
543
547
|
};
|
|
544
548
|
};
|
|
549
|
+
/** Payload-free request timing logs; 0 disables tracing, 1 samples every MCP request. */
|
|
550
|
+
timingTraceSampleRate: import("./config-schema.js").Field<number, number, true>;
|
|
545
551
|
/** Diagnostic output. Use "silent" to disable all diagnostic logging. */
|
|
546
552
|
logger: import("./config-schema.js").Field<Logger | "silent", Logger, true>;
|
|
547
553
|
serverInfo: import("./config-schema.js").Field<{
|
package/dist/config.js
CHANGED
|
@@ -189,7 +189,7 @@ function assertFlag(value, path) {
|
|
|
189
189
|
if (typeof value !== "boolean")
|
|
190
190
|
throw new ConfigError(`${path} must be true or false.`);
|
|
191
191
|
}
|
|
192
|
-
/** Longest pool `description`, in
|
|
192
|
+
/** Longest pool `description`, in UTF-16 code units, after trimming. */
|
|
193
193
|
const MAX_POOL_DESCRIPTION_LENGTH = 500;
|
|
194
194
|
/**
|
|
195
195
|
* A pool description is one line of prose for the model. C0 and C1 controls
|
|
@@ -209,9 +209,9 @@ function assertPoolDescription(value, path) {
|
|
|
209
209
|
const trimmed = typeof value === "string" ? value.trim() : undefined;
|
|
210
210
|
if (trimmed === undefined ||
|
|
211
211
|
trimmed.length === 0 ||
|
|
212
|
-
|
|
212
|
+
trimmed.length > MAX_POOL_DESCRIPTION_LENGTH ||
|
|
213
213
|
hasLineBreakOrControlCharacter(trimmed)) {
|
|
214
|
-
throw new ConfigError(`${path} must be text of 1 to ${MAX_POOL_DESCRIPTION_LENGTH}
|
|
214
|
+
throw new ConfigError(`${path} must be text of 1 to ${MAX_POOL_DESCRIPTION_LENGTH} UTF-16 code units after trimming, with no line breaks or control characters.`);
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
/** Shape only; each reference resolves against auth and agents in createConnecta. */
|
|
@@ -263,7 +263,7 @@ const pool = {
|
|
|
263
263
|
* What this pool is for, in plain words for the model: appended to the MCP
|
|
264
264
|
* `initialize` instructions at `/mcp/<name>` and `/agent/<name>`, so a
|
|
265
265
|
* caller learns its scope ("Team tier: shared tools only."). Trimmed, 1 to
|
|
266
|
-
* 500
|
|
266
|
+
* 500 UTF-16 code units, no line breaks or control characters. It grants and hides nothing; plain
|
|
267
267
|
* `/mcp` never shows it.
|
|
268
268
|
*/
|
|
269
269
|
description: opaque({ check: assertPoolDescription }),
|
|
@@ -396,8 +396,9 @@ const connectaConfig = {
|
|
|
396
396
|
resolve: (value) => value ?? memoryStorage(),
|
|
397
397
|
}),
|
|
398
398
|
/**
|
|
399
|
-
* Public base URL. Defaults to the request origin per-request
|
|
400
|
-
*
|
|
399
|
+
* Public base URL. Defaults to the request origin per-request, except that
|
|
400
|
+
* clerkAuth requires it here or as its own `publicUrl`. Configuring an HTTPS
|
|
401
|
+
* URL also redirects matching inbound HTTP requests to HTTPS.
|
|
401
402
|
*/
|
|
402
403
|
publicUrl: opaque({ check: httpUrl }),
|
|
403
404
|
/**
|
|
@@ -419,7 +420,10 @@ const connectaConfig = {
|
|
|
419
420
|
}
|
|
420
421
|
},
|
|
421
422
|
}),
|
|
422
|
-
/**
|
|
423
|
+
/**
|
|
424
|
+
* Replaceable owner-partitioned credential storage. Omit for config-owned
|
|
425
|
+
* secrets. With any OAuth connector it must implement `seal` and `open`.
|
|
426
|
+
*/
|
|
423
427
|
vault: opaque({
|
|
424
428
|
check: (value) => {
|
|
425
429
|
if (!hasMethods(value, ["get", "getAll", "set", "setAll", "metadata", "delete"]) ||
|
|
@@ -464,6 +468,14 @@ const connectaConfig = {
|
|
|
464
468
|
* saturation.
|
|
465
469
|
*/
|
|
466
470
|
admission: object(admission),
|
|
471
|
+
/** Payload-free request timing logs; 0 disables tracing, 1 samples every MCP request. */
|
|
472
|
+
timingTraceSampleRate: opaque({
|
|
473
|
+
check: (value, path) => {
|
|
474
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1)
|
|
475
|
+
throw new ConfigError(`${path} must be a finite number between 0 and 1.`);
|
|
476
|
+
},
|
|
477
|
+
resolve: (value) => value ?? 0,
|
|
478
|
+
}),
|
|
467
479
|
/** Diagnostic output. Use "silent" to disable all diagnostic logging. */
|
|
468
480
|
logger: opaque({
|
|
469
481
|
check: (value, path) => {
|
|
@@ -528,10 +540,21 @@ function assertCoherent(config) {
|
|
|
528
540
|
config.discovery.catalogMaxTtlSeconds > 86_400) {
|
|
529
541
|
throw new ConfigError("ConnectaConfig.discovery requires catalogMinTtlSeconds <= catalogMaxTtlSeconds <= 86400.");
|
|
530
542
|
}
|
|
543
|
+
const { vault } = config;
|
|
544
|
+
const oauth = config.connectors.find((connector) => connector.startAuth);
|
|
545
|
+
if (oauth && vault && (typeof vault.seal !== "function" || typeof vault.open !== "function")) {
|
|
546
|
+
throw new ConfigError(`ConnectaConfig.vault must implement both seal and open: connector "${oauth.id}" stores downstream OAuth ` +
|
|
547
|
+
"tokens, client registration and PKCE verifiers through it. Use encryptedCredentialVault(storage, key) " +
|
|
548
|
+
"from @zackbart/connecta/credentials, or add both methods.");
|
|
549
|
+
}
|
|
531
550
|
if (config.agents.some((agent) => !agent.vercel) && !config.accessTokens) {
|
|
532
551
|
throw new ConfigError("ConnectaConfig.agents needs accessTokens: accessTokens(storage) from @zackbart/connecta/auth/access-tokens " +
|
|
533
552
|
"holds each agent's token slot.");
|
|
534
553
|
}
|
|
554
|
+
if (!config.publicUrl && config.auth.some((provider) => provider.requiresPublicUrl)) {
|
|
555
|
+
throw new ConfigError("ConnectaConfig.publicUrl is required with clerkAuth, which checks tokens and sessions against it: " +
|
|
556
|
+
"set it, or clerkAuth({ publicUrl }), to this deployment's URL, http://localhost:<port> for local development.");
|
|
557
|
+
}
|
|
535
558
|
}
|
|
536
559
|
/**
|
|
537
560
|
* Validate a configuration and apply every default. Unknown keys are refused
|
|
@@ -55,7 +55,7 @@ export function remoteMcpCall(rt) {
|
|
|
55
55
|
}
|
|
56
56
|
: state.toolDefinitions.get(name);
|
|
57
57
|
if (toolDefinition?.execution?.taskSupport === "required") {
|
|
58
|
-
throw new ConnectorCallError("
|
|
58
|
+
throw new ConnectorCallError("unsupported_capability", `Tool "${name}" requires task-based execution, which Connecta does not support.`);
|
|
59
59
|
}
|
|
60
60
|
let output;
|
|
61
61
|
const outputError = (code, message) => rt.atMcpBoundary(ctx, new ProtocolError(code, message), "tools/call", client.transport, [ctx.signal]);
|