@zackbart/connecta 0.38.0 → 0.40.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 +91 -0
- package/README.md +3 -2
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/agent-routes.js +231 -0
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2382
- package/dist/auth/native.js +37 -0
- package/dist/auth/oauth-grant.js +13 -0
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth-elicitation.js +7 -1
- package/dist/authorization-handoff.js +11 -5
- package/dist/catalog-cache.js +219 -59
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +284 -47
- package/dist/catalog.js +18 -2
- package/dist/config.d.ts +16 -11
- package/dist/config.js +9 -6
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +7 -4
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +682 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +473 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +555 -0
- package/dist/connectors/remote-mcp.js +62 -2863
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +2 -0
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +152 -114
- package/dist/executor-result.js +3 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +60 -19
- package/dist/guest-types.d.ts +19 -5
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/errors.js +5 -4
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/oauth.js +3 -2
- package/dist/http/provider.js +32 -4
- package/dist/http/safety.js +8 -0
- package/dist/http/tools.js +44 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation.js +26 -8
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +54 -31
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +5 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +135 -13
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/contract.d.ts +10 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/provider-call-facts.js +6 -2
- package/dist/providers/_shared/rest/operation-index.js +48 -50
- package/dist/providers/_shared/rest/value-safety.js +80 -0
- package/dist/providers/breeze/api.js +7 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +4 -3
- package/dist/providers/ccb/api.js +2 -1
- package/dist/providers/ccb/index.js +5 -0
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +2 -4
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +11 -16
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +10 -7
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +7 -5
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +7 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/api.js +3 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +13 -14
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +15 -13
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +3 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +4 -3
- package/dist/registry.d.ts +10 -4
- package/dist/registry.js +41 -13
- package/dist/routes/credentials.js +129 -60
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +30 -14
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +6 -1
- package/dist/routes/shared.js +7 -1
- package/dist/routes/ui-config.js +2 -2
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +16 -0
- package/dist/runtime/call.js +7 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +207 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/storage.js +12 -0
- package/dist/skills.js +37 -20
- package/dist/storage/keys.js +53 -3
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +53 -1
- package/dist/storage/sql.js +12 -2
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/usage-guide.js +16 -35
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +13 -13
- package/documentation/architecture.md +385 -20
- package/documentation/auth.md +233 -64
- package/documentation/code-mode.md +122 -39
- package/documentation/deploying.md +6 -3
- package/documentation/integrating.md +16 -2
- package/documentation/meta-tools.md +240 -49
- package/documentation/migration-0.33.md +5 -1
- package/documentation/migration-0.34.md +3 -1
- package/documentation/operator-tests.md +1 -1
- package/documentation/operator-ui.md +105 -12
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +10 -10
- package/examples/worker/README.md +4 -3
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +2 -1
- package/guides/how-it-works.md +4 -2
- package/package.json +1 -1
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
package/dist/catalog-cache.js
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
import { detach, runEdge } from "./runtime/run.js";
|
|
2
2
|
import { Deferred, Effect } from "effect";
|
|
3
|
-
import { authFlow, authCall, authInvoke, authValue, authMethod, authProgram, registerAuth, authStorage, authMap, authAwait, } from "./auth/native.js";
|
|
3
|
+
import { authFlow, authCall, authInvoke, authValue, authMethod, authProgram, registerAuth, authStorage, authStorageMany, authMap, authAwait, } from "./auth/native.js";
|
|
4
4
|
import { MAX_CACHE_TTL_MS, specTypeSchemas } from "@modelcontextprotocol/client";
|
|
5
5
|
import { advanceCatalogRevision, catalogRevision } from "./catalog-revision.js";
|
|
6
6
|
import { callerOf } from "./connector-caller.js";
|
|
7
7
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
8
8
|
import { Context } from "effect";
|
|
9
|
-
import { RequestScope } from "./runtime/request-scope.js";
|
|
9
|
+
import { ownerReads, RequestScope } from "./runtime/request-scope.js";
|
|
10
10
|
import { serviceFor, setViewService, viewServices } from "./runtime/call.js";
|
|
11
11
|
import { ConnectorCallError } from "./errors.js";
|
|
12
12
|
import { fingerprintSerializedCatalog } from "./catalog-fingerprint.js";
|
|
13
13
|
import { MAX_CATALOG_CHUNK_BYTES, MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "./catalog-limits.js";
|
|
14
14
|
import { redactCatalog, sentSecretsForRequest } from "./sent-secrets.js";
|
|
15
15
|
import { failureRecord, logFailure } from "./operator-record.js";
|
|
16
|
+
import { secretDigest } from "./storage-digest.js";
|
|
16
17
|
import { responseCacheKeys } from "./storage/keys.js";
|
|
18
|
+
import { CatalogPolicy } from "./catalog-policy.js";
|
|
19
|
+
/**
|
|
20
|
+
* An unchanged refresh leaves a baseline younger than this in place instead of
|
|
21
|
+
* rewriting it, so a baseline outlives every refresh by at least the baseline
|
|
22
|
+
* TTL minus this interval.
|
|
23
|
+
*/
|
|
24
|
+
const BASELINE_REWRITE_AGE_MS = 60 * 60 * 1000;
|
|
25
|
+
const refreshHash = (v) => typeof v === "string" && /^sha256:[0-9]+:[0-9a-f]{64}$/.test(v);
|
|
26
|
+
function refreshManifest(raw) {
|
|
27
|
+
if (!raw)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
const value = JSON.parse(raw);
|
|
31
|
+
if (refreshHash(value.digest) &&
|
|
32
|
+
refreshHash(value.fingerprint) &&
|
|
33
|
+
/^[0-9a-f-]{36}$/.test(value.revision) &&
|
|
34
|
+
Number.isInteger(value.chunkCount) &&
|
|
35
|
+
value.chunkCount >= 1 &&
|
|
36
|
+
value.chunkCount <= Math.ceil(MAX_SERIALIZED_CATALOG_BYTES / MAX_CATALOG_CHUNK_BYTES))
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
17
41
|
/** Publish one accepted catalog refresh, with its partitioned drift baseline. */
|
|
18
42
|
export function observeCompletedCatalogRefresh(ctx, refresh) {
|
|
19
43
|
return runEdge(authProgram(observeCompletedCatalogRefreshNative, [ctx, refresh]));
|
|
@@ -41,18 +65,10 @@ function refreshBaseline(storage, key, raw, io) {
|
|
|
41
65
|
return runEdge(authProgram(refreshBaselineNative, [storage, key, raw, io]));
|
|
42
66
|
}
|
|
43
67
|
const refreshBaselineNative = /* @__PURE__ */ registerAuth(refreshBaseline, (storage, key, raw, io) => authFlow(function* () {
|
|
44
|
-
|
|
68
|
+
const value = refreshManifest(raw);
|
|
69
|
+
if (!value)
|
|
45
70
|
return;
|
|
46
71
|
try {
|
|
47
|
-
const value = JSON.parse(raw);
|
|
48
|
-
const hash = (v) => typeof v === "string" && /^sha256:[0-9]+:[0-9a-f]{64}$/.test(v);
|
|
49
|
-
if (!hash(value.digest) ||
|
|
50
|
-
!hash(value.fingerprint) ||
|
|
51
|
-
!/^[0-9a-f-]{36}$/.test(value.revision) ||
|
|
52
|
-
!Number.isInteger(value.chunkCount) ||
|
|
53
|
-
value.chunkCount < 1 ||
|
|
54
|
-
value.chunkCount > Math.ceil(MAX_SERIALIZED_CATALOG_BYTES / MAX_CATALOG_CHUNK_BYTES))
|
|
55
|
-
return;
|
|
56
72
|
const chunks = [];
|
|
57
73
|
for (let index = 0; index < value.chunkCount; index++) {
|
|
58
74
|
const chunk = (yield authCall(io, [
|
|
@@ -68,7 +84,7 @@ const refreshBaselineNative = /* @__PURE__ */ registerAuth(refreshBaseline, (sto
|
|
|
68
84
|
const tools = JSON.parse(serialized);
|
|
69
85
|
if (Array.isArray(tools) &&
|
|
70
86
|
tools.length <= MAX_CATALOG_TOOLS &&
|
|
71
|
-
tools.every((tool) => tool &&
|
|
87
|
+
tools.every((tool) => tool && refreshHash(tool.name) && refreshHash(tool.fact)))
|
|
72
88
|
return { digest: value.digest, tools };
|
|
73
89
|
}
|
|
74
90
|
catch { }
|
|
@@ -86,6 +102,18 @@ function acceptCatalogRefresh(storage, key, connectorId, digest, tools, privateC
|
|
|
86
102
|
]));
|
|
87
103
|
}
|
|
88
104
|
const acceptCatalogRefreshNative = /* @__PURE__ */ registerAuth(acceptCatalogRefresh, (storage, key, connectorId, digest, tools, privateCatalog, current, io) => authFlow(function* () {
|
|
105
|
+
// An identical catalog cannot drift, and its baseline, written within
|
|
106
|
+
// the last hour, needs no rewrite: such a refresh writes nothing. The
|
|
107
|
+
// stored chunks always outlive their manifest, since a manifest is only
|
|
108
|
+
// ever written with fresh chunks.
|
|
109
|
+
const stored = (yield authCall(io, [
|
|
110
|
+
authMethod(() => authStorage(storage, "get", [key])),
|
|
111
|
+
]));
|
|
112
|
+
const baseline = refreshManifest(stored);
|
|
113
|
+
if (baseline?.digest === digest &&
|
|
114
|
+
baseline.writtenAt !== undefined &&
|
|
115
|
+
Date.now() - baseline.writtenAt < BASELINE_REWRITE_AGE_MS)
|
|
116
|
+
return;
|
|
89
117
|
const next = (yield authCall(catalogFingerprints, [tools]));
|
|
90
118
|
const serialized = JSON.stringify(next);
|
|
91
119
|
const revision = crypto.randomUUID();
|
|
@@ -103,13 +131,17 @@ const acceptCatalogRefreshNative = /* @__PURE__ */ registerAuth(acceptCatalogRef
|
|
|
103
131
|
])),
|
|
104
132
|
]));
|
|
105
133
|
}
|
|
106
|
-
const manifest = { digest, revision, chunkCount, fingerprint };
|
|
134
|
+
const manifest = { digest, revision, chunkCount, fingerprint, writtenAt: Date.now() };
|
|
107
135
|
for (let attempt = 0; attempt < 16; attempt++) {
|
|
108
136
|
if (!(yield authCall(current, [])))
|
|
109
137
|
return;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
// The first attempt publishes against the manifest read above; a lost
|
|
139
|
+
// compare-and-set reads it again.
|
|
140
|
+
const raw = attempt === 0
|
|
141
|
+
? stored
|
|
142
|
+
: (yield authCall(io, [
|
|
143
|
+
authMethod(() => authStorage(storage, "get", [key])),
|
|
144
|
+
]));
|
|
113
145
|
const previous = (yield authCall(refreshBaseline, [storage, key, raw, io]));
|
|
114
146
|
if (!(yield authCall(current, [])))
|
|
115
147
|
return;
|
|
@@ -204,8 +236,8 @@ export function observeCatalogExpiry(ctx, method, ttlMs, expiresAt, staleFallbac
|
|
|
204
236
|
// failure for five minutes. Resources never use stale inventories. A fallback
|
|
205
237
|
// carries its original expired deadline and cannot authorize auth recovery.
|
|
206
238
|
const STALE_FALLBACK_MS = 300_000;
|
|
207
|
-
|
|
208
|
-
|
|
239
|
+
/** Catalog chunks one storage read fetches together. */
|
|
240
|
+
const CHUNKS_PER_READ = 8;
|
|
209
241
|
export function attachCatalogCache(ctx, value) {
|
|
210
242
|
setViewService(ctx, CatalogPolicy, value);
|
|
211
243
|
}
|
|
@@ -380,8 +412,42 @@ const invalidateCatalogCacheNative = /* @__PURE__ */ registerAuth(invalidateCata
|
|
|
380
412
|
advanceCatalogRevision(storage);
|
|
381
413
|
}
|
|
382
414
|
}));
|
|
415
|
+
// Only detached JSON facts cross requests: no context, storage, promises or
|
|
416
|
+
// callbacks. FIFO bounds retained partitions, JSON/key text (UTF-16 worst
|
|
417
|
+
// case) and structural slots. No WeakRef or timers required.
|
|
418
|
+
const catalogStores = new WeakMap();
|
|
419
|
+
const validatedCatalogs = new Map();
|
|
420
|
+
let validatedBytes = 0;
|
|
421
|
+
const VALIDATED_BYTES = 8 * 1024 * 1024;
|
|
422
|
+
function rememberValidated(key, value, bytes) {
|
|
423
|
+
bytes = bytes * 2 + key.length * 2;
|
|
424
|
+
if (bytes > VALIDATED_BYTES || validatedCatalogs.has(key))
|
|
425
|
+
return;
|
|
426
|
+
const pending = [value];
|
|
427
|
+
while (pending.length) {
|
|
428
|
+
const item = pending.pop();
|
|
429
|
+
if (!item || typeof item !== "object" || Object.isFrozen(item))
|
|
430
|
+
continue;
|
|
431
|
+
Object.freeze(item);
|
|
432
|
+
for (const child of Object.values(item)) {
|
|
433
|
+
// Charge structural overhead too: tiny JSON objects/arrays can occupy
|
|
434
|
+
// much more heap than their serialized bytes alone suggest.
|
|
435
|
+
bytes += 64;
|
|
436
|
+
if (bytes > VALIDATED_BYTES)
|
|
437
|
+
return;
|
|
438
|
+
pending.push(child);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
while (validatedCatalogs.size >= 4 || validatedBytes + bytes > VALIDATED_BYTES) {
|
|
442
|
+
const oldest = validatedCatalogs.keys().next().value;
|
|
443
|
+
validatedBytes -= validatedCatalogs.get(oldest).bytes;
|
|
444
|
+
validatedCatalogs.delete(oldest);
|
|
445
|
+
}
|
|
446
|
+
validatedCatalogs.set(key, { value, bytes });
|
|
447
|
+
validatedBytes += bytes;
|
|
448
|
+
}
|
|
383
449
|
/** Keys ignore self-reported serverInfo and bind the configured connector instead. */
|
|
384
|
-
export function catalogClientOptions(ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity) {
|
|
450
|
+
export function catalogClientOptions(ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity, connectionless = false) {
|
|
385
451
|
return runEdge(authProgram(catalogClientOptionsNative, [
|
|
386
452
|
ctx,
|
|
387
453
|
connectorId,
|
|
@@ -391,10 +457,20 @@ export function catalogClientOptions(ctx, connectorId, config, authPartition, co
|
|
|
391
457
|
owner,
|
|
392
458
|
sharing,
|
|
393
459
|
resolveCredentialIdentity,
|
|
460
|
+
connectionless,
|
|
394
461
|
]));
|
|
395
462
|
}
|
|
396
|
-
const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOptions, (ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity) => authFlow(function* () {
|
|
463
|
+
const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOptions, (ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity, connectionless = false) => authFlow(function* () {
|
|
397
464
|
const request = Context.get(viewServices(ctx), RequestScope);
|
|
465
|
+
// Generation reads that only locate an entry are memoized for this
|
|
466
|
+
// request; every acceptance or publication check reads it fresh. A
|
|
467
|
+
// generation is never reused once replaced, so a stale one only
|
|
468
|
+
// addresses an entry that the fresh check then refuses.
|
|
469
|
+
const reads = ownerReads(request);
|
|
470
|
+
const generationRead = (memoized) => {
|
|
471
|
+
const key = responseCacheKeys.generation(connectorId);
|
|
472
|
+
return reads.read(storage, key, authStorage(storage, "get", [key]), !memoized);
|
|
473
|
+
};
|
|
398
474
|
// SDK callbacks start at their own Promise edge. Reprovide only their
|
|
399
475
|
// canonical request owner, never the initiating call's live services.
|
|
400
476
|
const cacheMethod = (program) => authMethod((...args) => program(...args).pipe(Effect.provideService(RequestScope, request)));
|
|
@@ -411,6 +487,9 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
411
487
|
authPartition,
|
|
412
488
|
]);
|
|
413
489
|
const storage = policy?.storage ?? ctx.storage;
|
|
490
|
+
let storeIdentity = catalogStores.get(storage);
|
|
491
|
+
if (!storeIdentity)
|
|
492
|
+
catalogStores.set(storage, (storeIdentity = crypto.randomUUID()));
|
|
414
493
|
const configHash = (yield authCall(fingerprintSerializedCatalog, [
|
|
415
494
|
JSON.stringify([
|
|
416
495
|
config,
|
|
@@ -443,18 +522,21 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
443
522
|
};
|
|
444
523
|
let lastStamp = 0;
|
|
445
524
|
let staleTools;
|
|
446
|
-
const currentGeneration = cacheMethod(({ stopped, io, generation }) => authFlow(function* () {
|
|
525
|
+
const currentGeneration = cacheMethod(({ stopped, io, generation }, memoized = false) => authFlow(function* () {
|
|
447
526
|
const key = responseCacheKeys.generation(connectorId);
|
|
448
527
|
for (let attempt = 0; attempt < 16; attempt++) {
|
|
449
528
|
if (stopped())
|
|
450
529
|
return undefined;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
]));
|
|
530
|
+
// A retry after a lost claim always reads fresh.
|
|
531
|
+
const current = (yield authCall(io, [cacheMethod(() => generationRead(memoized && attempt === 0))]));
|
|
454
532
|
if (stopped())
|
|
455
533
|
return undefined;
|
|
456
534
|
if (current)
|
|
457
535
|
return current;
|
|
536
|
+
// A connectionless reader never claims or recreates a generation: a
|
|
537
|
+
// missing one is a miss at every check, never a reused namespace.
|
|
538
|
+
if (connectionless)
|
|
539
|
+
return undefined;
|
|
458
540
|
const next = generation.value ?? crypto.randomUUID();
|
|
459
541
|
const claimed = (yield authCall(io, [
|
|
460
542
|
cacheMethod(() => authStorage(storage, "compareAndSet", [
|
|
@@ -471,12 +553,13 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
471
553
|
}
|
|
472
554
|
throw new Error("Catalog cache generation is busy.");
|
|
473
555
|
}));
|
|
474
|
-
const namespace = cacheMethod((operation) => authFlow(function* () {
|
|
556
|
+
const namespace = cacheMethod((operation, memoized = false) => authFlow(function* () {
|
|
475
557
|
const { stopped, generation, io } = operation;
|
|
476
|
-
if (
|
|
558
|
+
if (!memoized &&
|
|
559
|
+
resolveCredentialIdentity &&
|
|
477
560
|
(yield authCall(io, [resolveCredentialIdentity])) !== operation.credentialIdentity)
|
|
478
561
|
return undefined;
|
|
479
|
-
const current = (yield authCall(currentGeneration, [operation]));
|
|
562
|
+
const current = (yield authCall(currentGeneration, [operation, memoized]));
|
|
480
563
|
if (!current || stopped())
|
|
481
564
|
return undefined;
|
|
482
565
|
generation.value ??= current;
|
|
@@ -499,7 +582,10 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
499
582
|
pair.length !== 2 ||
|
|
500
583
|
(pair[1] !== partition && (pair[1] !== "" || sharing !== "shared")))
|
|
501
584
|
return undefined;
|
|
502
|
-
|
|
585
|
+
// Locate with the listing's pinned identity and generation. Neither
|
|
586
|
+
// authorizes acceptance: get's post-chunk fence and set's publication
|
|
587
|
+
// re-read both, so rotation or disconnect during I/O refuses the entry.
|
|
588
|
+
const prefix = (yield authCall(namespace, [operation, true]));
|
|
503
589
|
if (!prefix || stopped())
|
|
504
590
|
return undefined;
|
|
505
591
|
const digest = (yield authCall(fingerprintSerializedCatalog, [
|
|
@@ -593,24 +679,47 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
593
679
|
const root = (yield authCall(address, [key, operation]));
|
|
594
680
|
if (!root || stopped())
|
|
595
681
|
return undefined;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
682
|
+
// The SDK probes private, then public. Derive both addresses from
|
|
683
|
+
// the already-bound locating generation and identity, and fetch
|
|
684
|
+
// them in one statement. The post-chunk namespace check stays fresh.
|
|
685
|
+
const listing = operation.listing;
|
|
686
|
+
if (listing && !listing.manifests?.has(root)) {
|
|
687
|
+
const slots = sharing === "shared" ? [partition, sharedPartition] : [partition];
|
|
688
|
+
const roots = [];
|
|
689
|
+
for (const slot of slots) {
|
|
690
|
+
const { fingerprint } = (yield authCall(fingerprintSerializedCatalog, [
|
|
691
|
+
JSON.stringify([key.method, slot, operation.credentialIdentity ?? null]),
|
|
692
|
+
]));
|
|
693
|
+
roots.push(responseCacheKeys.entry(responseCacheKeys.namespace(connectorId, configHash, operation.generation.value), fingerprint));
|
|
694
|
+
}
|
|
695
|
+
const values = (yield authCall(io, [cacheMethod(() => authStorageMany(storage, roots))]));
|
|
696
|
+
listing.manifests ??= new Map();
|
|
697
|
+
roots.forEach((root, i) => listing.manifests.set(root, values[i] ?? null));
|
|
698
|
+
}
|
|
699
|
+
const m = manifest(listing?.manifests?.has(root)
|
|
700
|
+
? listing.manifests.get(root)
|
|
701
|
+
: (yield authCall(io, [
|
|
702
|
+
cacheMethod(() => authStorage(storage, "get", [root])),
|
|
703
|
+
])));
|
|
599
704
|
if (stopped() ||
|
|
600
705
|
!m ||
|
|
601
706
|
(m.expiresAt <= Date.now() &&
|
|
602
707
|
(key.method !== "tools/list" || m.expiresAt + STALE_FALLBACK_MS <= Date.now())))
|
|
603
708
|
return undefined;
|
|
709
|
+
// Up to CHUNKS_PER_READ chunks per round trip, so one answer stays a
|
|
710
|
+
// few megabytes; the generation fence below still decides whether
|
|
711
|
+
// what was read may be served.
|
|
604
712
|
const chunks = [];
|
|
605
|
-
for (let
|
|
713
|
+
for (let first = 0; first < m.chunkCount; first += CHUNKS_PER_READ) {
|
|
606
714
|
if (stopped())
|
|
607
715
|
return undefined;
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
716
|
+
const keys = Array.from({ length: Math.min(CHUNKS_PER_READ, m.chunkCount - first) }, (_, i) => responseCacheKeys.chunk(root, m.revision, first + i));
|
|
717
|
+
const read = (yield authCall(io, [cacheMethod(() => authStorageMany(storage, keys))]));
|
|
718
|
+
for (const chunk of read) {
|
|
719
|
+
if (stopped() || chunk === null || new TextEncoder().encode(chunk).byteLength > MAX_CATALOG_CHUNK_BYTES)
|
|
720
|
+
return undefined;
|
|
721
|
+
chunks.push(chunk);
|
|
722
|
+
}
|
|
614
723
|
}
|
|
615
724
|
const value = chunks.join("");
|
|
616
725
|
const fingerprint = (yield authCall(fingerprintSerializedCatalog, [value]));
|
|
@@ -620,18 +729,29 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
620
729
|
!(yield authCall(namespace, [operation])) ||
|
|
621
730
|
stopped())
|
|
622
731
|
return undefined;
|
|
623
|
-
const result = JSON.parse(value);
|
|
624
732
|
const method = key.method;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
733
|
+
// root binds connector/config/generation/scope/credential; partition
|
|
734
|
+
// also binds the current owner/deployment even for public entries.
|
|
735
|
+
// Hash the chunks again above, never trust the manifest alone.
|
|
736
|
+
const reuseKey = JSON.stringify([storeIdentity, root, partition, m.fingerprint]);
|
|
737
|
+
let accepted = validatedCatalogs.get(reuseKey)?.value;
|
|
738
|
+
if (!accepted) {
|
|
739
|
+
const result = JSON.parse(value);
|
|
740
|
+
const items = catalogItems(method, result);
|
|
741
|
+
if (!Array.isArray(items) ||
|
|
742
|
+
items.length > MAX_CATALOG_TOOLS ||
|
|
743
|
+
"nextCursor" in result ||
|
|
744
|
+
(result.resultType !== undefined && result.resultType !== "complete"))
|
|
745
|
+
return undefined;
|
|
746
|
+
const validated = (yield authInvoke(catalogSchema(method)["~standard"], "validate", [result]));
|
|
747
|
+
if (stopped() || validated.issues)
|
|
748
|
+
return undefined;
|
|
749
|
+
accepted = validated.value;
|
|
750
|
+
}
|
|
751
|
+
// Redaction depends on this request's sent credentials. It must
|
|
752
|
+
// still run, and returns detached objects from the frozen facts.
|
|
753
|
+
const clean = intake(ctx, method, accepted);
|
|
754
|
+
rememberValidated(reuseKey, accepted, fingerprint.byteLength);
|
|
635
755
|
if (m.expiresAt <= Date.now()) {
|
|
636
756
|
if (method === "tools/list" && (!staleTools || staleTools.manifest.fetchedAt < m.fetchedAt))
|
|
637
757
|
staleTools = { result: clean, manifest: m };
|
|
@@ -647,6 +767,7 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
647
767
|
const root = (yield authCall(address, [key, operation]));
|
|
648
768
|
if (!root || stopped())
|
|
649
769
|
return 0;
|
|
770
|
+
operation.listing?.manifests?.delete(root);
|
|
650
771
|
const result = JSON.parse(entry.value);
|
|
651
772
|
const method = key.method;
|
|
652
773
|
if (catalogTtlMs(ctx, result.ttlMs) === 0) {
|
|
@@ -733,10 +854,12 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
733
854
|
delete: cacheMethod((key) => authFlow(function* () {
|
|
734
855
|
const operation = capture();
|
|
735
856
|
const root = (yield authCall(address, [key, operation]));
|
|
736
|
-
if (root && !operation.stopped())
|
|
857
|
+
if (root && !operation.stopped()) {
|
|
858
|
+
operation.listing?.manifests?.delete(root);
|
|
737
859
|
(yield authInvoke(operation, "io", [
|
|
738
860
|
cacheMethod(() => authStorage(storage, "delete", [root])),
|
|
739
861
|
]));
|
|
862
|
+
}
|
|
740
863
|
})),
|
|
741
864
|
evict: cacheMethod((method) => authFlow(function* () {
|
|
742
865
|
if (isCatalogMethod(method))
|
|
@@ -752,10 +875,11 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
752
875
|
};
|
|
753
876
|
try {
|
|
754
877
|
const { stopped, io } = capture({ ctx, generation: connectionGeneration, ended: false });
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
878
|
+
// A connectionless reader only lists, and each listing reads its own
|
|
879
|
+
// generation; there are no connection-owned writes to fence.
|
|
880
|
+
if (!connectionless && !stopped()) {
|
|
881
|
+
// Memoized: a connection's writes publish only after a fresh check.
|
|
882
|
+
const current = (yield authCall(io, [cacheMethod(() => generationRead(true))]));
|
|
759
883
|
if (!stopped())
|
|
760
884
|
connectionGeneration.value = current ?? crypto.randomUUID();
|
|
761
885
|
}
|
|
@@ -792,9 +916,13 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
792
916
|
listing.credentialIdentity = yield authInvoke(operation, "io", [resolveCredentialIdentity]);
|
|
793
917
|
if (!operation.stopped()) {
|
|
794
918
|
try {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
919
|
+
// Memoized: it locates the listing's entries, which the
|
|
920
|
+
// fresh fence in get accepts or refuses.
|
|
921
|
+
const stored = (yield authInvoke(operation, "io", [
|
|
922
|
+
cacheMethod(() => generationRead(true)),
|
|
923
|
+
]));
|
|
924
|
+
const generation = stored ?? crypto.randomUUID();
|
|
925
|
+
listing.generationStored = stored !== null;
|
|
798
926
|
listing.generation.value = generation;
|
|
799
927
|
connectionGeneration.value = generation;
|
|
800
928
|
}
|
|
@@ -820,6 +948,34 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
820
948
|
void previous.then(release);
|
|
821
949
|
}
|
|
822
950
|
}));
|
|
951
|
+
// A complete, unexpired tools/list entry, read before any connection
|
|
952
|
+
// exists. This is the SDK's own two-probe read: the private slot
|
|
953
|
+
// `[serverIdentity, cachePartition]`, then a shared connector's public
|
|
954
|
+
// slot `[serverIdentity, ""]`, served only when written as public.
|
|
955
|
+
// `address` binds the configured connector rather than the self-reported
|
|
956
|
+
// identity, so the pre-connect `""` identity selects the same entries.
|
|
957
|
+
// Expired, partial, refused (`ttlMs: 0`) and invalidated entries miss,
|
|
958
|
+
// and the read writes nothing.
|
|
959
|
+
const cachedTools = cacheMethod((listingCtx) => authCall(withListing, [
|
|
960
|
+
listingCtx,
|
|
961
|
+
cacheMethod(() => authFlow(function* () {
|
|
962
|
+
// No stored generation means no entry, and claiming one is a write.
|
|
963
|
+
if (!active?.generationStored)
|
|
964
|
+
return undefined;
|
|
965
|
+
const read = (slot) => authInvoke(store, "get", [
|
|
966
|
+
{ method: "tools/list", params: "", partition: JSON.stringify(["", slot]) },
|
|
967
|
+
]);
|
|
968
|
+
let entry = (yield read(partition));
|
|
969
|
+
if (entry === undefined && sharing === "shared") {
|
|
970
|
+
const shared = (yield read(""));
|
|
971
|
+
entry = shared?.scope === "public" ? shared : undefined;
|
|
972
|
+
}
|
|
973
|
+
if (entry?.expiresAt === undefined || !(entry.expiresAt > Date.now()))
|
|
974
|
+
return undefined;
|
|
975
|
+
const result = JSON.parse(entry.value);
|
|
976
|
+
return Array.isArray(result.tools) ? result : undefined;
|
|
977
|
+
})),
|
|
978
|
+
]));
|
|
823
979
|
const fallbackTools = cacheMethod(() => authFlow(function* () {
|
|
824
980
|
const operation = capture();
|
|
825
981
|
if (!staleTools ||
|
|
@@ -837,6 +993,7 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
837
993
|
intake,
|
|
838
994
|
completeCatalogRefresh,
|
|
839
995
|
fallbackTools,
|
|
996
|
+
cachedTools,
|
|
840
997
|
responseCacheStore: store,
|
|
841
998
|
cachePartition: partition,
|
|
842
999
|
defaultCacheTtlMs: policy?.defaultTtlMs ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds * 1000,
|
|
@@ -855,7 +1012,10 @@ function customCatalogCache(ctx, connectorId) {
|
|
|
855
1012
|
}
|
|
856
1013
|
const customCatalogCacheNative = /* @__PURE__ */ registerAuth(customCatalogCache, (ctx, connectorId) => authFlow(function* () {
|
|
857
1014
|
const credential = (yield authValue(() => ctx.credential?.getAll()));
|
|
858
|
-
|
|
1015
|
+
// Credential values enter the stored partition only through a keyed digest.
|
|
1016
|
+
const auth = credential == null
|
|
1017
|
+
? (yield authCall(fingerprintSerializedCatalog, ["null"])).fingerprint
|
|
1018
|
+
: (yield authCall(secretDigest, [ctx, JSON.stringify(credential)]));
|
|
859
1019
|
const cache = (yield authCall(catalogClientOptions, [ctx, connectorId, "custom-connector", auth]));
|
|
860
1020
|
const key = {
|
|
861
1021
|
method: "tools/list",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The catalog cache's per-view policy: its store, owner partition and TTL
|
|
2
|
+
// bounds, attached to each connector context by the registry. Kept apart from
|
|
3
|
+
// the cache itself so bounded facts stored beside it (connector health, the
|
|
4
|
+
// operator page's status observations) reach the partition without the MCP
|
|
5
|
+
// client the cache adapter imports.
|
|
6
|
+
import { Context } from "effect";
|
|
7
|
+
import { callerOf } from "./connector-caller.js";
|
|
8
|
+
import { serviceFor } from "./runtime/call.js";
|
|
9
|
+
export class CatalogPolicy extends Context.Service()("connecta/operation/CatalogPolicy") {
|
|
10
|
+
}
|
|
11
|
+
/** The store and owner partition this connector's catalog cache uses, for bounded facts kept beside it. */
|
|
12
|
+
export function catalogOwnerPartition(ctx) {
|
|
13
|
+
const policy = serviceFor(ctx, CatalogPolicy);
|
|
14
|
+
return {
|
|
15
|
+
storage: policy?.storage ?? ctx.storage,
|
|
16
|
+
partition: JSON.stringify([policy?.partition ?? JSON.stringify(callerOf(ctx) ?? null), ctx.baseUrl, ctx.publicUrl]),
|
|
17
|
+
};
|
|
18
|
+
}
|