@zackbart/connecta 0.39.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 +115 -0
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- 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/native.js +10 -0
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +51 -21
- package/dist/catalog-cache.js +151 -37
- 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 -7
- package/dist/connectors/remote-mcp/oauth.js +6 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +37 -8
- package/dist/connectors/remote-mcp.js +8 -5
- 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 +77 -35
- package/dist/membership.d.ts +1 -1
- package/dist/meta-tools.js +58 -36
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-tools.js +12 -5
- 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-call-facts.js +6 -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 +15 -0
- package/dist/runtime/request-reads.js +87 -10
- 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 +85 -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 +83 -2
- package/documentation/auth.md +132 -37
- package/documentation/code-mode.md +61 -46
- package/documentation/deploying.md +25 -1
- package/documentation/integrating.md +2 -2
- package/documentation/meta-tools.md +134 -15
- 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";
|
|
@@ -412,6 +414,83 @@ const invalidateCatalogCacheNative = /* @__PURE__ */ registerAuth(invalidateCata
|
|
|
412
414
|
advanceCatalogRevision(storage);
|
|
413
415
|
}
|
|
414
416
|
}));
|
|
417
|
+
// Only detached JSON facts cross requests: no context, storage, promises or
|
|
418
|
+
// callbacks. FIFO bounds retained partitions, JSON/key text (UTF-16 worst
|
|
419
|
+
// case) and structural slots. No WeakRef or timers required.
|
|
420
|
+
const catalogStores = new WeakMap();
|
|
421
|
+
const validatedCatalogs = new Map();
|
|
422
|
+
let validatedBytes = 0;
|
|
423
|
+
const VALIDATED_BYTES = 8 * 1024 * 1024;
|
|
424
|
+
function rememberValidated(key, value, bytes) {
|
|
425
|
+
bytes = bytes * 2 + key.length * 2;
|
|
426
|
+
if (bytes > VALIDATED_BYTES || validatedCatalogs.has(key))
|
|
427
|
+
return;
|
|
428
|
+
const pending = [value];
|
|
429
|
+
while (pending.length) {
|
|
430
|
+
const item = pending.pop();
|
|
431
|
+
if (!item || typeof item !== "object" || Object.isFrozen(item))
|
|
432
|
+
continue;
|
|
433
|
+
Object.freeze(item);
|
|
434
|
+
for (const child of Object.values(item)) {
|
|
435
|
+
// Charge structural overhead too: tiny JSON objects/arrays can occupy
|
|
436
|
+
// much more heap than their serialized bytes alone suggest.
|
|
437
|
+
bytes += 64;
|
|
438
|
+
if (bytes > VALIDATED_BYTES)
|
|
439
|
+
return;
|
|
440
|
+
pending.push(child);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
while (validatedCatalogs.size >= 4 || validatedBytes + bytes > VALIDATED_BYTES) {
|
|
444
|
+
const oldest = validatedCatalogs.keys().next().value;
|
|
445
|
+
validatedBytes -= validatedCatalogs.get(oldest).bytes;
|
|
446
|
+
validatedCatalogs.delete(oldest);
|
|
447
|
+
}
|
|
448
|
+
validatedCatalogs.set(key, { value, bytes });
|
|
449
|
+
validatedBytes += bytes;
|
|
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
|
+
}));
|
|
415
494
|
/** Keys ignore self-reported serverInfo and bind the configured connector instead. */
|
|
416
495
|
export function catalogClientOptions(ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity, connectionless = false) {
|
|
417
496
|
return runEdge(authProgram(catalogClientOptionsNative, [
|
|
@@ -441,26 +520,11 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
441
520
|
// canonical request owner, never the initiating call's live services.
|
|
442
521
|
const cacheMethod = (program) => authMethod((...args) => program(...args).pipe(Effect.provideService(RequestScope, request)));
|
|
443
522
|
const policy = serviceFor(ctx, CatalogPolicy);
|
|
444
|
-
const partition =
|
|
445
|
-
policy?.partition ?? JSON.stringify(callerOf(ctx) ?? null),
|
|
446
|
-
ctx.baseUrl,
|
|
447
|
-
ctx.publicUrl,
|
|
448
|
-
authPartition,
|
|
449
|
-
]);
|
|
450
|
-
const sharedPartition = JSON.stringify([
|
|
451
|
-
"host-shared",
|
|
452
|
-
policy?.sharedPartition ?? JSON.stringify([ctx.baseUrl, ctx.publicUrl, callerOf(ctx)?.pool]),
|
|
453
|
-
authPartition,
|
|
454
|
-
]);
|
|
523
|
+
const { partition, sharedPartition, configHash } = yield authCall(catalogLocation, [ctx, config, authPartition]);
|
|
455
524
|
const storage = policy?.storage ?? ctx.storage;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
policy?.defaultTtlMs ?? 300000,
|
|
460
|
-
policy?.minTtlMs ?? 0,
|
|
461
|
-
policy?.maxTtlMs ?? MAX_CACHE_TTL_MS,
|
|
462
|
-
]),
|
|
463
|
-
])).fingerprint;
|
|
525
|
+
let storeIdentity = catalogStores.get(storage);
|
|
526
|
+
if (!storeIdentity)
|
|
527
|
+
catalogStores.set(storage, (storeIdentity = crypto.randomUUID()));
|
|
464
528
|
const scope = cacheScope(ctx, connectorId, owner);
|
|
465
529
|
let unavailable = false;
|
|
466
530
|
const connectionGeneration = {};
|
|
@@ -518,7 +582,8 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
518
582
|
}));
|
|
519
583
|
const namespace = cacheMethod((operation, memoized = false) => authFlow(function* () {
|
|
520
584
|
const { stopped, generation, io } = operation;
|
|
521
|
-
if (
|
|
585
|
+
if (!memoized &&
|
|
586
|
+
resolveCredentialIdentity &&
|
|
522
587
|
(yield authCall(io, [resolveCredentialIdentity])) !== operation.credentialIdentity)
|
|
523
588
|
return undefined;
|
|
524
589
|
const current = (yield authCall(currentGeneration, [operation, memoized]));
|
|
@@ -544,7 +609,9 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
544
609
|
pair.length !== 2 ||
|
|
545
610
|
(pair[1] !== partition && (pair[1] !== "" || sharing !== "shared")))
|
|
546
611
|
return undefined;
|
|
547
|
-
//
|
|
612
|
+
// Locate with the listing's pinned identity and generation. Neither
|
|
613
|
+
// authorizes acceptance: get's post-chunk fence and set's publication
|
|
614
|
+
// re-read both, so rotation or disconnect during I/O refuses the entry.
|
|
548
615
|
const prefix = (yield authCall(namespace, [operation, true]));
|
|
549
616
|
if (!prefix || stopped())
|
|
550
617
|
return undefined;
|
|
@@ -636,12 +703,36 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
636
703
|
get: cacheMethod((key) => authFlow(function* () {
|
|
637
704
|
const operation = capture();
|
|
638
705
|
const { ctx, stopped, io } = operation;
|
|
706
|
+
const trace = traceFor(ctx.requestScope);
|
|
707
|
+
trace?.record("catalog.cache.lookup");
|
|
639
708
|
const root = (yield authCall(address, [key, operation]));
|
|
640
709
|
if (!root || stopped())
|
|
641
710
|
return undefined;
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
711
|
+
// The SDK probes private, then public. Derive both addresses from
|
|
712
|
+
// the already-bound locating generation and identity, and fetch
|
|
713
|
+
// them in one statement. The post-chunk namespace check stays fresh.
|
|
714
|
+
const listing = operation.listing;
|
|
715
|
+
if (listing && !listing.manifests?.has(root)) {
|
|
716
|
+
const slots = sharing === "shared" ? [partition, sharedPartition] : [partition];
|
|
717
|
+
const roots = [];
|
|
718
|
+
for (const slot of slots) {
|
|
719
|
+
const { fingerprint } = (yield authCall(fingerprintSerializedCatalog, [
|
|
720
|
+
JSON.stringify([key.method, slot, operation.credentialIdentity ?? null]),
|
|
721
|
+
]));
|
|
722
|
+
roots.push(responseCacheKeys.entry(responseCacheKeys.namespace(connectorId, configHash, operation.generation.value), fingerprint));
|
|
723
|
+
}
|
|
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])));
|
|
728
|
+
listing.manifests ??= new Map();
|
|
729
|
+
roots.forEach((root, i) => listing.manifests.set(root, values[i] ?? null));
|
|
730
|
+
}
|
|
731
|
+
const m = manifest(listing?.manifests?.has(root)
|
|
732
|
+
? listing.manifests.get(root)
|
|
733
|
+
: (yield authCall(io, [
|
|
734
|
+
cacheMethod(() => authStorage(storage, "get", [root])),
|
|
735
|
+
])));
|
|
645
736
|
if (stopped() ||
|
|
646
737
|
!m ||
|
|
647
738
|
(m.expiresAt <= Date.now() &&
|
|
@@ -670,23 +761,41 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
670
761
|
!(yield authCall(namespace, [operation])) ||
|
|
671
762
|
stopped())
|
|
672
763
|
return undefined;
|
|
673
|
-
const result = JSON.parse(value);
|
|
674
764
|
const method = key.method;
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
765
|
+
// root binds connector/config/generation/scope/credential; partition
|
|
766
|
+
// also binds the current owner/deployment even for public entries.
|
|
767
|
+
// Hash the chunks again above, never trust the manifest alone.
|
|
768
|
+
const reuseKey = JSON.stringify([storeIdentity, root, partition, m.fingerprint]);
|
|
769
|
+
let accepted = validatedCatalogs.get(reuseKey)?.value;
|
|
770
|
+
trace?.record(accepted ? "catalog.validated.hit" : "catalog.validated.miss");
|
|
771
|
+
if (!accepted) {
|
|
772
|
+
const result = JSON.parse(value);
|
|
773
|
+
trace?.record("catalog.decode", 0, fingerprint.byteLength);
|
|
774
|
+
const items = catalogItems(method, result);
|
|
775
|
+
if (!Array.isArray(items) ||
|
|
776
|
+
items.length > MAX_CATALOG_TOOLS ||
|
|
777
|
+
"nextCursor" in result ||
|
|
778
|
+
(result.resultType !== undefined && result.resultType !== "complete"))
|
|
779
|
+
return undefined;
|
|
780
|
+
const validated = (yield authInvoke(catalogSchema(method)["~standard"], "validate", [result]));
|
|
781
|
+
trace?.record("catalog.validate");
|
|
782
|
+
if (stopped() || validated.issues)
|
|
783
|
+
return undefined;
|
|
784
|
+
accepted = validated.value;
|
|
785
|
+
}
|
|
786
|
+
// Redaction depends on this request's sent credentials. It must
|
|
787
|
+
// still run, and returns detached objects from the frozen facts.
|
|
788
|
+
const clean = intake(ctx, method, accepted);
|
|
789
|
+
trace?.record("catalog.intake");
|
|
790
|
+
trace?.record("catalog.chunks", 0, fingerprint.byteLength, 0, 0, m.chunkCount);
|
|
791
|
+
rememberValidated(reuseKey, accepted, fingerprint.byteLength);
|
|
685
792
|
if (m.expiresAt <= Date.now()) {
|
|
793
|
+
trace?.record("catalog.cache.expired");
|
|
686
794
|
if (method === "tools/list" && (!staleTools || staleTools.manifest.fetchedAt < m.fetchedAt))
|
|
687
795
|
staleTools = { result: clean, manifest: m };
|
|
688
796
|
return undefined;
|
|
689
797
|
}
|
|
798
|
+
trace?.record("catalog.cache.hit");
|
|
690
799
|
observeCatalogFetch(ctx, m.fetchedAt, method);
|
|
691
800
|
observeCatalogExpiry(ctx, method, clean.ttlMs, m.expiresAt);
|
|
692
801
|
return { value: JSON.stringify(clean), stamp: m.stamp, expiresAt: m.expiresAt, scope: m.scope };
|
|
@@ -697,6 +806,7 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
697
806
|
const root = (yield authCall(address, [key, operation]));
|
|
698
807
|
if (!root || stopped())
|
|
699
808
|
return 0;
|
|
809
|
+
operation.listing?.manifests?.delete(root);
|
|
700
810
|
const result = JSON.parse(entry.value);
|
|
701
811
|
const method = key.method;
|
|
702
812
|
if (catalogTtlMs(ctx, result.ttlMs) === 0) {
|
|
@@ -783,10 +893,12 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
783
893
|
delete: cacheMethod((key) => authFlow(function* () {
|
|
784
894
|
const operation = capture();
|
|
785
895
|
const root = (yield authCall(address, [key, operation]));
|
|
786
|
-
if (root && !operation.stopped())
|
|
896
|
+
if (root && !operation.stopped()) {
|
|
897
|
+
operation.listing?.manifests?.delete(root);
|
|
787
898
|
(yield authInvoke(operation, "io", [
|
|
788
899
|
cacheMethod(() => authStorage(storage, "delete", [root])),
|
|
789
900
|
]));
|
|
901
|
+
}
|
|
790
902
|
})),
|
|
791
903
|
evict: cacheMethod((method) => authFlow(function* () {
|
|
792
904
|
if (isCatalogMethod(method))
|
|
@@ -860,7 +972,9 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
860
972
|
logFailure(listingCtx.logger, "catalog read failed", failureRecord({ connector: connectorId }, error));
|
|
861
973
|
}
|
|
862
974
|
}
|
|
863
|
-
|
|
975
|
+
const result = yield authCall(read, []);
|
|
976
|
+
markCatalogGeneration(listingCtx.requestScope, connectorId, listing.generation.value);
|
|
977
|
+
return result;
|
|
864
978
|
}
|
|
865
979
|
finally {
|
|
866
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<{
|