@zackbart/connecta 0.41.0 → 0.42.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 +61 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +2 -1
- package/dist/activity.js +5 -5
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-routes.js +31 -5
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth.js +3 -0
- package/dist/auth/static-oauth.js +26 -18
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +292 -24
- package/dist/catalog-freshness.js +12 -0
- package/dist/catalog-service.js +373 -70
- package/dist/catalog.js +147 -58
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +43 -17
- package/dist/config.js +26 -10
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +62 -5
- package/dist/connectors/remote-mcp.js +10 -1
- package/dist/describe-config.d.ts +3 -2
- package/dist/discovery-health.js +26 -11
- package/dist/errors.d.ts +52 -3
- package/dist/errors.js +86 -3
- package/dist/execute.js +476 -200
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +2 -2
- package/dist/executors/quickjs-child.js +13 -4
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +36 -19
- package/dist/executors/quickjs.js +23 -6
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +7 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +9 -3
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -0
- package/dist/invocation.js +98 -34
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +259 -60
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +166 -51
- package/dist/node.js +3 -1
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +2 -1
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +1 -1
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +0 -13
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -0
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +7 -2
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +1 -0
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +2 -0
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +11 -5
- 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/index.js +2 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +2 -2
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +1 -0
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +2 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +127 -27
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +13 -4
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +55 -26
- package/dist/routes/shared.d.ts +6 -0
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/request-scope.js +36 -1
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skills.js +100 -33
- package/dist/storage/keys.js +9 -6
- package/dist/storage/sql-native.js +1 -1
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +31 -0
- package/dist/usage-guide.js +14 -13
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +28 -8
- package/documentation/architecture.md +104 -42
- package/documentation/auth.md +46 -19
- package/documentation/code-mode.md +378 -116
- package/documentation/deploying.md +10 -9
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +655 -153
- package/documentation/operating.md +5 -3
- package/documentation/providers.md +18 -6
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +1 -1
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { SdkHttpError, specTypeSchemas, UnauthorizedError } from "@modelcontextp
|
|
|
5
5
|
import { redactCatalog } from "../../sent-secrets.js";
|
|
6
6
|
import { MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "../../catalog-limits.js";
|
|
7
7
|
import { catalogItems, observeCompletedCatalogRefresh, observeCatalogFetch, observeCatalogExpiry, } from "../../catalog-cache.js";
|
|
8
|
-
import { ConnectorCallError, msg } from "../../errors.js";
|
|
8
|
+
import { ConnectorCallError, isAccessRefusal, msg } from "../../errors.js";
|
|
9
9
|
import { carryFailureFacts, failureRecord, logFailure } from "../../operator-record.js";
|
|
10
10
|
import { assertDownstreamOutputSafe } from "../../downstream-input-context.js";
|
|
11
11
|
import { hasSdkPayload, ownAbortReason, requiresAuthorization } from "./errors.js";
|
|
@@ -78,6 +78,10 @@ export function remoteMcpCatalog(rt) {
|
|
|
78
78
|
listingContext(options),
|
|
79
79
|
authMethod(() => authFlow(function* () {
|
|
80
80
|
const before = toolPages;
|
|
81
|
+
// The one write that follows no cache read, which is what
|
|
82
|
+
// otherwise captures the fence this listing may lift.
|
|
83
|
+
if (options?.cacheMode === "refresh")
|
|
84
|
+
(yield authInvoke(cache, "captureFence", []));
|
|
81
85
|
let result;
|
|
82
86
|
try {
|
|
83
87
|
result = (yield authCall(listTools, [params, options]));
|
|
@@ -86,6 +90,9 @@ export function remoteMcpCatalog(rt) {
|
|
|
86
90
|
const bounded = rt.atMcpBoundary(cache.currentContext(), error, "tools/list", client.transport, [
|
|
87
91
|
cache.currentContext().signal,
|
|
88
92
|
]);
|
|
93
|
+
// Fenced as this listing was bound, whatever is current by now.
|
|
94
|
+
if (requiresAuthorization(bounded) || isAccessRefusal(bounded))
|
|
95
|
+
(yield authInvoke(cache, "fenceListing", []));
|
|
89
96
|
const transient = (error instanceof SdkHttpError && [502, 503, 504].includes(error.status)) ||
|
|
90
97
|
(bounded instanceof ConnectorCallError && bounded.code === "unavailable");
|
|
91
98
|
const fallback = transient && toolPages === before
|
|
@@ -247,32 +254,49 @@ export function remoteMcpCatalog(rt) {
|
|
|
247
254
|
}
|
|
248
255
|
}
|
|
249
256
|
state.toolDefinitions = new Map(clean.map((tool) => [tool.name, tool]));
|
|
250
|
-
return clean
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
:
|
|
275
|
-
|
|
257
|
+
return toolDefinitions(clean);
|
|
258
|
+
}));
|
|
259
|
+
// Served by discovery only, marked by the caller; the listing above stays
|
|
260
|
+
// the only source a call resolves through.
|
|
261
|
+
const peekCatalogOperation = authMethod((ctx) => authFlow(function* () {
|
|
262
|
+
if (liveListings.has(ctx))
|
|
263
|
+
return undefined;
|
|
264
|
+
const entry = (yield authCall(rt.peekCatalog, [ctx]));
|
|
265
|
+
if (!entry)
|
|
266
|
+
return undefined;
|
|
267
|
+
return {
|
|
268
|
+
tools: toolDefinitions(redactCatalog(ctx, entry.result.tools)),
|
|
269
|
+
fetchedAt: entry.fetchedAt,
|
|
270
|
+
expiresAt: entry.expiresAt,
|
|
271
|
+
};
|
|
272
|
+
}));
|
|
273
|
+
return { installCatalogIntake, listingContexts, liveListings, listToolsOperation, peekCatalogOperation };
|
|
274
|
+
}
|
|
275
|
+
function toolDefinitions(tools) {
|
|
276
|
+
return tools.map((t) => ({
|
|
277
|
+
name: t.name,
|
|
278
|
+
...(t.title !== undefined ? { title: t.title } : {}),
|
|
279
|
+
...(t.icons !== undefined
|
|
280
|
+
? {
|
|
281
|
+
icons: t.icons.filter((icon) => !/^data:/i.test(icon.src)),
|
|
282
|
+
}
|
|
283
|
+
: {}),
|
|
284
|
+
...(t.execution !== undefined ? { execution: t.execution } : {}),
|
|
285
|
+
...(t.description !== undefined ? { description: t.description } : {}),
|
|
286
|
+
...(t.inputSchema !== undefined
|
|
287
|
+
? {
|
|
288
|
+
inputSchema: t.inputSchema,
|
|
289
|
+
}
|
|
290
|
+
: {}),
|
|
291
|
+
...(t.outputSchema !== undefined
|
|
292
|
+
? {
|
|
293
|
+
outputSchema: t.outputSchema,
|
|
294
|
+
}
|
|
295
|
+
: {}),
|
|
296
|
+
...(t.annotations !== undefined
|
|
297
|
+
? {
|
|
298
|
+
annotations: t.annotations,
|
|
299
|
+
}
|
|
300
|
+
: {}),
|
|
276
301
|
}));
|
|
277
|
-
return { installCatalogIntake, listingContexts, liveListings, listToolsOperation };
|
|
278
302
|
}
|
|
@@ -12,8 +12,8 @@ import { Deferred, Effect, Exit, Scope } from "effect";
|
|
|
12
12
|
import { KvOAuthProvider, refreshCoordinatorsByPartition } from "../../auth/downstream-oauth.js";
|
|
13
13
|
import { trackOAuthStartReset } from "../../auth/oauth-start-reset.js";
|
|
14
14
|
import { sentSecretsFor } from "../../sent-secrets.js";
|
|
15
|
-
import { catalogClientOptions, prefetchCatalog,
|
|
16
|
-
import { ConnectorCallError, resendAdvice } from "../../errors.js";
|
|
15
|
+
import { catalogClientOptions, observeCatalogChange, observeCatalogExpiry, observeCatalogFetch, prefetchCatalog, retainedCatalogIdentity, } from "../../catalog-cache.js";
|
|
16
|
+
import { ConnectorCallError, isAccessRefusal, resendAdvice } from "../../errors.js";
|
|
17
17
|
import { carryFailureFacts, failureRecord, logFailure } from "../../operator-record.js";
|
|
18
18
|
import { CONNECTA_VERSION } from "../../version.js";
|
|
19
19
|
import { oauthSealerFor } from "../../oauth-sealing.js";
|
|
@@ -288,7 +288,7 @@ export function remoteMcpConnection(rt) {
|
|
|
288
288
|
* same cache through the SDK, so it skips this read. What was already read
|
|
289
289
|
* is handed to that connect, so a resolver runs once per listing.
|
|
290
290
|
*/
|
|
291
|
-
const
|
|
291
|
+
const readCatalog = authMethod((ctx, use) => authFlow(function* () {
|
|
292
292
|
const principal = principalPolicy
|
|
293
293
|
? requestPrincipalFor(principalPolicy, ctx, (reason) => new rt.CredentialRequiredError(`Connector "${id}" ${reason}.`))
|
|
294
294
|
: undefined;
|
|
@@ -340,7 +340,7 @@ export function remoteMcpConnection(rt) {
|
|
|
340
340
|
]);
|
|
341
341
|
const prior = (yield authCall(readNegotiation, [ctx, digest]));
|
|
342
342
|
preread.negotiation = { digest, prior };
|
|
343
|
-
if (prior?.kind === "modern")
|
|
343
|
+
if (prior?.kind === "modern" && use === "list")
|
|
344
344
|
return preread;
|
|
345
345
|
}
|
|
346
346
|
const cache = (yield authCall(catalogClientOptions, [
|
|
@@ -354,6 +354,10 @@ export function remoteMcpConnection(rt) {
|
|
|
354
354
|
identity,
|
|
355
355
|
true,
|
|
356
356
|
]));
|
|
357
|
+
if (use === "peek") {
|
|
358
|
+
const entry = (yield authInvoke(cache, "peekTools", [ctx]));
|
|
359
|
+
return entry ? { ...preread, peek: entry } : preread;
|
|
360
|
+
}
|
|
357
361
|
const result = (yield authInvoke(cache, "cachedTools", [ctx]));
|
|
358
362
|
if (result)
|
|
359
363
|
return { tools: result.tools };
|
|
@@ -365,6 +369,34 @@ export function remoteMcpConnection(rt) {
|
|
|
365
369
|
}
|
|
366
370
|
return preread;
|
|
367
371
|
}));
|
|
372
|
+
// A peek is the first half of the listing that may follow it: that listing
|
|
373
|
+
// takes what the peek read, once, so a discovery that peeks first still reads
|
|
374
|
+
// its stored entry and runs a credential resolver once. Nothing is taken if
|
|
375
|
+
// the catalog was invalidated or the credential changed in between, and the
|
|
376
|
+
// whole hand-off lives and dies with the request scope.
|
|
377
|
+
const peeks = new WeakMap();
|
|
378
|
+
const peekCatalog = authMethod((ctx) => authFlow(function* () {
|
|
379
|
+
// A connector known to need authorization is never listed from a cache.
|
|
380
|
+
if (stateFor(ctx).authRequired)
|
|
381
|
+
return undefined;
|
|
382
|
+
const read = (yield authCall(readCatalog, [ctx, "peek"]));
|
|
383
|
+
peeks.set(ctx.requestScope ?? ctx, { identity: retainedCatalogIdentity(ctx), read });
|
|
384
|
+
return read.peek;
|
|
385
|
+
}));
|
|
386
|
+
const cachedCatalog = authMethod((ctx) => authFlow(function* () {
|
|
387
|
+
const scope = ctx.requestScope ?? ctx;
|
|
388
|
+
const handed = peeks.get(scope);
|
|
389
|
+
peeks.delete(scope);
|
|
390
|
+
if (!handed || handed.identity !== retainedCatalogIdentity(ctx))
|
|
391
|
+
return (yield authCall(readCatalog, [ctx, "list"]));
|
|
392
|
+
const { peek, ...preread } = handed.read;
|
|
393
|
+
if (!peek || peek.expiresAt <= Date.now())
|
|
394
|
+
return preread;
|
|
395
|
+
// The listing's own view keeps the provenance a direct read would record.
|
|
396
|
+
observeCatalogFetch(ctx, peek.fetchedAt);
|
|
397
|
+
observeCatalogExpiry(ctx, "tools/list", typeof peek.result.ttlMs === "number" ? peek.result.ttlMs : peek.expiresAt - peek.fetchedAt, peek.expiresAt);
|
|
398
|
+
return { tools: peek.result.tools };
|
|
399
|
+
}));
|
|
368
400
|
const ensureConnected = authMethod((ctx, state, preread) => authFlow(function* () {
|
|
369
401
|
let joined = state.connecting;
|
|
370
402
|
if (joined)
|
|
@@ -554,6 +586,25 @@ export function remoteMcpConnection(rt) {
|
|
|
554
586
|
])));
|
|
555
587
|
if (!owned())
|
|
556
588
|
return yield* Effect.fail(rt.scopeEndedError());
|
|
589
|
+
// The generation a refused handshake is about, as it is now. Read from
|
|
590
|
+
// this connect's memo, so a connect that is not refused costs no I/O.
|
|
591
|
+
const handshakeBinding = yield* promised(authMethod(() => authInvoke(cacheOptions, "bindListing", [ctx, { credentialIdentity: undefined }]))).pipe(Effect.catch(() => Effect.succeed(undefined)));
|
|
592
|
+
if (!owned())
|
|
593
|
+
return yield* Effect.fail(rt.scopeEndedError());
|
|
594
|
+
// What the attempt sent, in memory: never the grant as it is by now. A
|
|
595
|
+
// provider that read no tokens in this epoch sent none and has no catalog.
|
|
596
|
+
const fenceHandshake = authMethod(() => authFlow(function* () {
|
|
597
|
+
let credentialIdentity;
|
|
598
|
+
if (provider) {
|
|
599
|
+
if (!provider.observedTokens || provider.observed?.epoch !== genAtStart)
|
|
600
|
+
return;
|
|
601
|
+
credentialIdentity = yield authCall(secretDigest, [ctx, JSON.stringify(provider.observedTokens)]);
|
|
602
|
+
}
|
|
603
|
+
(yield authInvoke(cacheOptions, "fenceTools", [
|
|
604
|
+
ctx,
|
|
605
|
+
{ ...handshakeBinding, credentialIdentity },
|
|
606
|
+
]));
|
|
607
|
+
}));
|
|
557
608
|
const makeClient = () => {
|
|
558
609
|
const { intake: _intake, completeCatalogRefresh: _refresh, withListing: _listing, currentContext: _context, cachedTools: _cached, ...clientOptions } = cacheOptions;
|
|
559
610
|
const client = new Client({ name: "connecta", version: CONNECTA_VERSION }, {
|
|
@@ -621,7 +672,12 @@ export function remoteMcpConnection(rt) {
|
|
|
621
672
|
}
|
|
622
673
|
}))).pipe(Effect.mapError((err) => rt.atMcpBoundary(ctx,
|
|
623
674
|
// First, while the runtime's text can still be read for it.
|
|
624
|
-
rt.withoutCredential(ctx, err, credentialValue, credentialFramed), "MCP handshake", t, [ctx.signal, connectionAbort.signal, handshakeAbort.signal]))
|
|
675
|
+
rt.withoutCredential(ctx, err, credentialValue, credentialFramed), "MCP handshake", t, [ctx.signal, connectionAbort.signal, handshakeAbort.signal])),
|
|
676
|
+
// Before the lease closes the signal this write runs under. Waiters see
|
|
677
|
+
// the failure after it, and it never replaces the refusal.
|
|
678
|
+
Effect.tapError((error) => handshakeBinding && (requiresAuthorization(error) || isAccessRefusal(error))
|
|
679
|
+
? promised(fenceHandshake).pipe(Effect.catch(() => Effect.void))
|
|
680
|
+
: Effect.void));
|
|
625
681
|
held.client = c;
|
|
626
682
|
// A probe deadline can end its scope while connect is still in flight.
|
|
627
683
|
// The transport is closed immediately by closeScope; if connect wins
|
|
@@ -709,6 +765,7 @@ export function remoteMcpConnection(rt) {
|
|
|
709
765
|
ensureConnected,
|
|
710
766
|
requestOptions,
|
|
711
767
|
cachedCatalog,
|
|
768
|
+
peekCatalog,
|
|
712
769
|
cacheOwner,
|
|
713
770
|
reset,
|
|
714
771
|
states,
|
|
@@ -155,7 +155,7 @@ export function remoteMcp(id, opts) {
|
|
|
155
155
|
Object.assign(rt, remoteMcpCatalog(rt));
|
|
156
156
|
Object.assign(rt, remoteMcpResources(rt));
|
|
157
157
|
Object.assign(rt, remoteMcpCall(rt));
|
|
158
|
-
const { listSkills, readSkill, liveListings, listToolsOperation, callToolOperation, readResourceOperation, entryFor, cacheOwner, reset, stateFor, newProvider, resourceTemplateRefusals, ensureConnected, granted, connectedProviders, CredentialRequiredError, OperatorDisconnectedError, UnresolvedTokenError, oauthSurface, activeContexts, states, isClosed, scopeEndedError, operatorDisconnectedError, authRequiredError, renewBeforeCall, } = rt;
|
|
158
|
+
const { listSkills, readSkill, liveListings, listToolsOperation, peekCatalogOperation, callToolOperation, readResourceOperation, entryFor, cacheOwner, reset, stateFor, newProvider, resourceTemplateRefusals, ensureConnected, granted, connectedProviders, CredentialRequiredError, OperatorDisconnectedError, UnresolvedTokenError, oauthSurface, activeContexts, states, isClosed, scopeEndedError, operatorDisconnectedError, authRequiredError, renewBeforeCall, } = rt;
|
|
159
159
|
// Built once from construction-time values: names, framing, and public
|
|
160
160
|
// URLs only. A static header's value and a stored credential never reach
|
|
161
161
|
// it, and a URL keeps origin and path: a CIMD document's query may carry a
|
|
@@ -261,6 +261,7 @@ export function remoteMcp(id, opts) {
|
|
|
261
261
|
}
|
|
262
262
|
: {}),
|
|
263
263
|
listTools: listToolsOperation,
|
|
264
|
+
peekCatalog: peekCatalogOperation,
|
|
264
265
|
callTool: callToolOperation,
|
|
265
266
|
readResource: readResourceOperation,
|
|
266
267
|
closeScope: authMethod((ctx) => authFlow(function* () {
|
|
@@ -386,6 +387,11 @@ export function remoteMcp(id, opts) {
|
|
|
386
387
|
ctx,
|
|
387
388
|
authMethod(() => authCall(listTools, [ctx])),
|
|
388
389
|
]));
|
|
390
|
+
const peekCatalog = connector.peekCatalog;
|
|
391
|
+
connector.peekCatalog = authMethod((ctx) => authCall(withActiveSecrets, [
|
|
392
|
+
ctx,
|
|
393
|
+
authMethod(() => authCall(peekCatalog, [ctx])),
|
|
394
|
+
]));
|
|
389
395
|
const readResource = connector.readResource;
|
|
390
396
|
connector.readResource = authMethod((uri, ctx) => authCall(withActiveSecrets, [
|
|
391
397
|
ctx,
|
|
@@ -426,6 +432,7 @@ export function remoteMcp(id, opts) {
|
|
|
426
432
|
return (yield authView(ctx, authMethod(() => authCall(operation, [...args])), requestOwned));
|
|
427
433
|
}));
|
|
428
434
|
connector.listTools = settlingOperation(connector.listTools, 0);
|
|
435
|
+
connector.peekCatalog = settlingOperation(connector.peekCatalog, 0);
|
|
429
436
|
connector.readResource = settlingOperation(connector.readResource, 1);
|
|
430
437
|
if (connector.downstreamSkills) {
|
|
431
438
|
connector.downstreamSkills.list = settlingOperation(connector.downstreamSkills.list, 0);
|
|
@@ -445,6 +452,7 @@ export function remoteMcp(id, opts) {
|
|
|
445
452
|
// a refresh flight exists. These operations do not take call admission.
|
|
446
453
|
const retain = retainOperation;
|
|
447
454
|
connector.listTools = retain(connector.listTools, 0);
|
|
455
|
+
connector.peekCatalog = retain(connector.peekCatalog, 0);
|
|
448
456
|
connector.callTool = retain(connector.callTool, 2);
|
|
449
457
|
connector.readResource = retain(connector.readResource, 1);
|
|
450
458
|
if (connector.downstreamSkills) {
|
|
@@ -460,6 +468,7 @@ export function remoteMcp(id, opts) {
|
|
|
460
468
|
connector.finishAuth = retain(connector.finishAuth, 1);
|
|
461
469
|
}
|
|
462
470
|
connector.listTools = payloadFree(connector.listTools);
|
|
471
|
+
connector.peekCatalog = payloadFree(connector.peekCatalog);
|
|
463
472
|
connector.callTool = payloadFree(connector.callTool);
|
|
464
473
|
connector.readResource = payloadFree(connector.readResource);
|
|
465
474
|
if (connector.downstreamSkills) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ClassificationOverride } from "./tool-safety.js";
|
|
1
2
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
2
3
|
import type { ConnectaConfig, ResolvedConfig } from "./config.js";
|
|
3
4
|
import { type ResolvedPool } from "./membership.js";
|
|
@@ -95,7 +96,7 @@ export interface ConnectaConfigDescription {
|
|
|
95
96
|
maxResultBytes: DescribedLimit;
|
|
96
97
|
};
|
|
97
98
|
results: Record<keyof typeof CONFIG_DEFAULTS.results, DescribedLimit>;
|
|
98
|
-
execute: Record<keyof typeof CONFIG_DEFAULTS.execute, DescribedLimit
|
|
99
|
+
execute: Record<keyof typeof CONFIG_DEFAULTS.execute, DescribedLimit<number | boolean>>;
|
|
99
100
|
requests: Record<keyof typeof CONFIG_DEFAULTS.admission.requests, DescribedLimit>;
|
|
100
101
|
};
|
|
101
102
|
/** Whether the default /mcp endpoint refuses every write, and exact classification overrides from deployment code. */
|
|
@@ -115,7 +116,7 @@ export interface ConnectaConfigDescription {
|
|
|
115
116
|
id: string;
|
|
116
117
|
pools: string[];
|
|
117
118
|
}>;
|
|
118
|
-
classification: Record<string, Record<string,
|
|
119
|
+
classification: Record<string, Record<string, ClassificationOverride>>;
|
|
119
120
|
/** Inbound auth providers in the order they are tried. */
|
|
120
121
|
auth: Array<{
|
|
121
122
|
kind: string;
|
package/dist/discovery-health.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
// the connector-health family's `discovery` key, a typed code, when it was
|
|
6
6
|
// seen, and when to try again (INV-6: never downstream text, tools or
|
|
7
7
|
// credentials). Unscoped search skips a connector whose record is current and
|
|
8
|
-
// reports it as unavailable in `catalogErrors` with `retryAfterMs
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// reports it as unavailable in `catalogErrors` with `retryAfterMs`, listing its
|
|
9
|
+
// last complete catalog, marked stale, when one exists; it never serves tools
|
|
10
|
+
// from the failed or interrupted walk (INV-8). Scoped search, describe and calls
|
|
11
|
+
// still reach the connector.
|
|
11
12
|
//
|
|
12
13
|
// Records sit beside the catalog cache, in its store and owner partition:
|
|
13
14
|
// principal, subject, caller identity and pool, origin, the admitted endpoint,
|
|
@@ -21,7 +22,9 @@
|
|
|
21
22
|
//
|
|
22
23
|
// After its retry time a record lingers, so exactly one request can claim the
|
|
23
24
|
// next probe by compare-and-set as it dispatches it; the others keep skipping
|
|
24
|
-
// until that probe settles or its lease lapses.
|
|
25
|
+
// until that probe settles or its lease lapses. Every write is conditional on
|
|
26
|
+
// the record its request read, so concurrent failures record once and a lease
|
|
27
|
+
// is never overwritten. A success, or an auth failure that proves the
|
|
25
28
|
// connector answers, clears the record. A fast failure is not recorded:
|
|
26
29
|
// asking again costs the next search nothing. Caller cancellation and a probe cut
|
|
27
30
|
// short by the discovery budget record nothing.
|
|
@@ -32,7 +35,7 @@ import { classificationCode, classifyCallError } from "./errors.js";
|
|
|
32
35
|
import { failureRecord, logFailure } from "./operator-record.js";
|
|
33
36
|
import { causeError } from "./runtime/run.js";
|
|
34
37
|
import { Storage } from "./runtime/services.js";
|
|
35
|
-
import { storageCompareAndSet, storageGetMany
|
|
38
|
+
import { storageCompareAndSet, storageGetMany } from "./runtime/storage.js";
|
|
36
39
|
import { connectorHealthKeys, CONNECTOR_HEALTH_LINGER_MS } from "./storage/keys.js";
|
|
37
40
|
/** How long unscoped search skips a connector after a failed discovery. */
|
|
38
41
|
const DISCOVERY_RETRY_MS = 60_000;
|
|
@@ -123,13 +126,22 @@ export function readDiscoveryHealth(registry, ids, baseUrl, requestScope, option
|
|
|
123
126
|
* dispatched: one compare-and-set against the record this request read, with
|
|
124
127
|
* a lease to the absolute time `leaseUntil`, past the claimant's whole
|
|
125
128
|
* discovery budget. Others keep skipping until the lease lapses or the probe
|
|
126
|
-
* settles.
|
|
129
|
+
* settles. An absent record is claimed the same way, by creating it. False when
|
|
130
|
+
* another request holds it, or the record changed.
|
|
127
131
|
*/
|
|
128
132
|
export function claimDiscoveryProbe(slot, leaseUntil) {
|
|
129
133
|
return Effect.suspend(() => {
|
|
130
|
-
const fact =
|
|
134
|
+
const fact = slot.raw === null
|
|
135
|
+
? {
|
|
136
|
+
v: 1,
|
|
137
|
+
code: "timeout",
|
|
138
|
+
observedAt: Date.now(),
|
|
139
|
+
retryAt: leaseUntil,
|
|
140
|
+
generation: slot.generation,
|
|
141
|
+
}
|
|
142
|
+
: parseFact(slot.raw);
|
|
131
143
|
if (!fact)
|
|
132
|
-
return Effect.succeed(
|
|
144
|
+
return Effect.succeed(false);
|
|
133
145
|
const claim = encodeFact({ ...fact, retryAt: leaseUntil });
|
|
134
146
|
return storageCompareAndSet(slot.key, slot.raw, claim.raw, { expiresAtMs: claim.expiresAtMs }).pipe(Effect.provideService(Storage, slot.storage), Effect.map((claimed) => {
|
|
135
147
|
if (claimed)
|
|
@@ -188,9 +200,12 @@ export function settleDiscoveryHealth(slot, outcome) {
|
|
|
188
200
|
}
|
|
189
201
|
const encoded = encodeFact(fact);
|
|
190
202
|
const skip = { code: fact.code, retryAt: fact.retryAt };
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
203
|
+
// Against the record this request read: a concurrent failure or a held
|
|
204
|
+
// lease stays, and this request learns it does not own what follows.
|
|
205
|
+
return storageCompareAndSet(slot.key, slot.raw, encoded.raw, { expiresAtMs: encoded.expiresAtMs }).pipe(Effect.provideService(Storage, slot.storage), Effect.map((recorded) => {
|
|
206
|
+
if (recorded)
|
|
207
|
+
slot.raw = encoded.raw;
|
|
208
|
+
return { slot, skip, recorded };
|
|
194
209
|
}), Effect.catchCause((cause) => Effect.sync(() => {
|
|
195
210
|
logFailure(slot.logger, "discovery health write failed", failureRecord({}, causeError(cause)));
|
|
196
211
|
return { slot, skip };
|
package/dist/errors.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type ConnectorCallErrorCode = "timeout" | "auth_required" | "downstream_o
|
|
|
33
33
|
* Adding a member fails typecheck until {@link CLASSIFICATION_CODE_TABLE}
|
|
34
34
|
* lists it.
|
|
35
35
|
*/
|
|
36
|
-
export type ClassificationCode = ConnectorCallErrorCode | ExecutorAdmissionErrorCode | "cancelled" | "unknown_address" | "unknown_tool" | "ambiguous_tool_alias" | "catalog_lookup_failed" | "result_too_large" | "destructive_tool_requires_approval" | "read_only_endpoint" | "auth_replay_refused" | "write_outcome_unknown" | "result_processing_failed" | "budget_exceeded" | "executor_failed" | "program_error";
|
|
36
|
+
export type ClassificationCode = ConnectorCallErrorCode | ExecutorAdmissionErrorCode | "cancelled" | "unknown_address" | "unknown_tool" | "ambiguous_tool_alias" | "catalog_lookup_failed" | "result_too_large" | "result_not_retained" | "destructive_tool_requires_approval" | "read_only_endpoint" | "auth_replay_refused" | "write_outcome_unknown" | "result_processing_failed" | "budget_exceeded" | "executor_failed" | "program_error";
|
|
37
37
|
/** `value` when it is a code connecta assigns, else undefined. */
|
|
38
38
|
export declare function classificationCode(value: unknown): ClassificationCode | undefined;
|
|
39
39
|
/**
|
|
@@ -101,12 +101,48 @@ export interface ArgumentRepairDetails {
|
|
|
101
101
|
* finding stays a few hundred bytes inside the 4 KiB envelope below.
|
|
102
102
|
*/
|
|
103
103
|
export declare const MAX_ARGUMENT_VALIDATION_ISSUES = 8;
|
|
104
|
+
/** Whitespace and the quotes and brackets that delimit a link in text, HTML and JSON. Each is
|
|
105
|
+
* ASCII, so a cut before one splits no codepoint in UTF-8 or UTF-16. */
|
|
106
|
+
export declare const TOKEN_END: Set<number>;
|
|
104
107
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
108
|
+
* The first `units` UTF-16 units of `value`, less any unfinished token. Every cut of text
|
|
109
|
+
* that output redaction reads later goes through this or a sibling below: a remnant shorter
|
|
110
|
+
* than a fragment matches nothing, so a credential registered after the cut must not leave
|
|
111
|
+
* its opening behind. A whole token is the only tail a cut can prove finished.
|
|
112
|
+
*/
|
|
113
|
+
export declare function tokenPrefix(value: string, units: number): string;
|
|
114
|
+
/**
|
|
115
|
+
* Clamp a string to a UTF-8 byte budget without cutting a token, appending `…` when it
|
|
116
|
+
* clipped. Short strings — the common case, and the one that has to stay exact — are
|
|
107
117
|
* returned unchanged and untagged.
|
|
108
118
|
*/
|
|
109
119
|
export declare function boundedEchoText(value: string, maxBytes?: number): string;
|
|
120
|
+
/**
|
|
121
|
+
* Clamp a name for an operator's record, which no agent reads and no output redaction
|
|
122
|
+
* follows, to a UTF-8 byte budget. Its prefix is what identifies it, so it may end inside a
|
|
123
|
+
* token; agent-facing text goes through {@link boundedEchoText}.
|
|
124
|
+
*/
|
|
125
|
+
export declare function clampedRecordName(value: string, maxBytes: number): string;
|
|
126
|
+
/**
|
|
127
|
+
* How much of a tool's own `isError` text reaches the agent. A refusal can be the
|
|
128
|
+
* only copy of what to do next: a paragraph of instructions, a request id and a
|
|
129
|
+
* signed approval URL, which alone commonly run to a couple of kilobytes. Four
|
|
130
|
+
* kilobytes holds those, and in a direct result's two copies stays well inside the
|
|
131
|
+
* default result cap; a tighter cap shrinks it further.
|
|
132
|
+
*/
|
|
133
|
+
export declare const MAX_TOOL_ERROR_BYTES = 4096;
|
|
134
|
+
/**
|
|
135
|
+
* Clamp downstream prose to a UTF-8 byte budget, marker included, without cutting
|
|
136
|
+
* a token. The cut backs off to a byte no URL or credential holds, so a link arrives
|
|
137
|
+
* whole or not at all, and no prefix of a credential registered later stays behind.
|
|
138
|
+
* Each such byte is ASCII, so no codepoint is split.
|
|
139
|
+
*/
|
|
140
|
+
export declare function clipText(value: string, maxBytes?: number): string;
|
|
141
|
+
/**
|
|
142
|
+
* Clamp text so its JSON string fits `maxChars` serialized characters, quotes and marker
|
|
143
|
+
* included, without cutting a token. Escapes make a character cost up to six.
|
|
144
|
+
*/
|
|
145
|
+
export declare function clipSerialized(value: string, maxChars: number): string;
|
|
110
146
|
/**
|
|
111
147
|
* Snapshot the caller's arguments after bounded schema-aware writeOnly omission,
|
|
112
148
|
* then apply the shared byte budget without clipping. Unresolved sensitivity
|
|
@@ -179,6 +215,8 @@ export declare class WithheldTextError extends Error {
|
|
|
179
215
|
}
|
|
180
216
|
/** Agent-visible recovery class for downstream credential failures. */
|
|
181
217
|
export type AuthRecoveryMode = "oauth" | "operator_config" | "unavailable";
|
|
218
|
+
/** The downstream said this caller has no access: affirmative evidence, unlike a failure to answer. */
|
|
219
|
+
export declare function isAccessRefusal(error: unknown): boolean;
|
|
182
220
|
/** Runtime membership in the closed connector taxonomy, checked against its exhaustive table. */
|
|
183
221
|
export declare function isConnectorCallErrorCode(value: unknown): value is ConnectorCallErrorCode;
|
|
184
222
|
/**
|
|
@@ -283,6 +321,13 @@ export interface CallErrorDetails extends Partial<AuthorizationHandoff> {
|
|
|
283
321
|
* {@link ConnectorCallError.writeOutcome}). Check its target before repeating it.
|
|
284
322
|
*/
|
|
285
323
|
writeOutcome?: "unknown";
|
|
324
|
+
/**
|
|
325
|
+
* A write's downstream tool answered `isError` itself, so `message` holds its
|
|
326
|
+
* words. They do not prove the write unapplied: its handler ran before it answered.
|
|
327
|
+
*/
|
|
328
|
+
toolAnswered?: true;
|
|
329
|
+
/** On `unknown_tool`: nearby canonical addresses on the same connector. */
|
|
330
|
+
suggestions?: string[];
|
|
286
331
|
/** The single model-facing entry point for every credential class. */
|
|
287
332
|
nextAction?: {
|
|
288
333
|
tool: "skills";
|
|
@@ -297,6 +342,10 @@ export interface CallErrorDetails extends Partial<AuthorizationHandoff> {
|
|
|
297
342
|
connector: string;
|
|
298
343
|
};
|
|
299
344
|
operatorHandoff: string;
|
|
345
|
+
} | {
|
|
346
|
+
/** An error that carries its handoff: pass this link on; no tool call is needed. */
|
|
347
|
+
url: string;
|
|
348
|
+
purpose: string;
|
|
300
349
|
} | {
|
|
301
350
|
tool: "search_tools";
|
|
302
351
|
arguments: {
|
package/dist/errors.js
CHANGED
|
@@ -29,6 +29,7 @@ const CLASSIFICATION_CODE_TABLE = {
|
|
|
29
29
|
ambiguous_tool_alias: true,
|
|
30
30
|
catalog_lookup_failed: true,
|
|
31
31
|
result_too_large: true,
|
|
32
|
+
result_not_retained: true,
|
|
32
33
|
destructive_tool_requires_approval: true,
|
|
33
34
|
read_only_endpoint: true,
|
|
34
35
|
auth_replay_refused: true,
|
|
@@ -58,7 +59,7 @@ function boundedIssueText(value, maxChars) {
|
|
|
58
59
|
if (value.length <= maxChars)
|
|
59
60
|
return { value, truncated: false };
|
|
60
61
|
return {
|
|
61
|
-
value: `${value
|
|
62
|
+
value: `${tokenPrefix(value, maxChars - 1)}…`,
|
|
62
63
|
truncated: true,
|
|
63
64
|
};
|
|
64
65
|
}
|
|
@@ -91,12 +92,43 @@ const MAX_ECHOED_BYTES = 512;
|
|
|
91
92
|
*/
|
|
92
93
|
const echoEncoder = new TextEncoder();
|
|
93
94
|
const echoDecoder = new TextDecoder();
|
|
95
|
+
/** Whitespace and the quotes and brackets that delimit a link in text, HTML and JSON. Each is
|
|
96
|
+
* ASCII, so a cut before one splits no codepoint in UTF-8 or UTF-16. */
|
|
97
|
+
export const TOKEN_END = new Set([0x09, 0x0a, 0x0d, 0x20, 0x22, 0x3c, 0x3e, 0x60]);
|
|
94
98
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
99
|
+
* The first `units` UTF-16 units of `value`, less any unfinished token. Every cut of text
|
|
100
|
+
* that output redaction reads later goes through this or a sibling below: a remnant shorter
|
|
101
|
+
* than a fragment matches nothing, so a credential registered after the cut must not leave
|
|
102
|
+
* its opening behind. A whole token is the only tail a cut can prove finished.
|
|
103
|
+
*/
|
|
104
|
+
export function tokenPrefix(value, units) {
|
|
105
|
+
if (units >= value.length)
|
|
106
|
+
return value;
|
|
107
|
+
let end = Math.max(0, units);
|
|
108
|
+
while (end > 0 && !TOKEN_END.has(value.charCodeAt(end)))
|
|
109
|
+
end--;
|
|
110
|
+
return value.slice(0, end).trimEnd();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Clamp a string to a UTF-8 byte budget without cutting a token, appending `…` when it
|
|
114
|
+
* clipped. Short strings — the common case, and the one that has to stay exact — are
|
|
97
115
|
* returned unchanged and untagged.
|
|
98
116
|
*/
|
|
99
117
|
export function boundedEchoText(value, maxBytes = MAX_ECHOED_BYTES) {
|
|
118
|
+
const bytes = echoEncoder.encode(value);
|
|
119
|
+
if (bytes.length <= maxBytes)
|
|
120
|
+
return value;
|
|
121
|
+
let end = Math.max(0, maxBytes);
|
|
122
|
+
while (end > 0 && !TOKEN_END.has(bytes[end]))
|
|
123
|
+
end--;
|
|
124
|
+
return `${echoDecoder.decode(bytes.slice(0, end)).trimEnd()}…`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Clamp a name for an operator's record, which no agent reads and no output redaction
|
|
128
|
+
* follows, to a UTF-8 byte budget. Its prefix is what identifies it, so it may end inside a
|
|
129
|
+
* token; agent-facing text goes through {@link boundedEchoText}.
|
|
130
|
+
*/
|
|
131
|
+
export function clampedRecordName(value, maxBytes) {
|
|
100
132
|
const bytes = echoEncoder.encode(value);
|
|
101
133
|
if (bytes.length <= maxBytes)
|
|
102
134
|
return value;
|
|
@@ -107,6 +139,50 @@ export function boundedEchoText(value, maxBytes = MAX_ECHOED_BYTES) {
|
|
|
107
139
|
end--;
|
|
108
140
|
return `${echoDecoder.decode(bytes.slice(0, end))}…`;
|
|
109
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* How much of a tool's own `isError` text reaches the agent. A refusal can be the
|
|
144
|
+
* only copy of what to do next: a paragraph of instructions, a request id and a
|
|
145
|
+
* signed approval URL, which alone commonly run to a couple of kilobytes. Four
|
|
146
|
+
* kilobytes holds those, and in a direct result's two copies stays well inside the
|
|
147
|
+
* default result cap; a tighter cap shrinks it further.
|
|
148
|
+
*/
|
|
149
|
+
export const MAX_TOOL_ERROR_BYTES = 4_096;
|
|
150
|
+
const CLIPPED = "… [truncated]";
|
|
151
|
+
const CLIPPED_BYTES = echoEncoder.encode(CLIPPED).length;
|
|
152
|
+
/**
|
|
153
|
+
* Clamp downstream prose to a UTF-8 byte budget, marker included, without cutting
|
|
154
|
+
* a token. The cut backs off to a byte no URL or credential holds, so a link arrives
|
|
155
|
+
* whole or not at all, and no prefix of a credential registered later stays behind.
|
|
156
|
+
* Each such byte is ASCII, so no codepoint is split.
|
|
157
|
+
*/
|
|
158
|
+
export function clipText(value, maxBytes = MAX_TOOL_ERROR_BYTES) {
|
|
159
|
+
const bytes = echoEncoder.encode(value);
|
|
160
|
+
if (bytes.length <= maxBytes)
|
|
161
|
+
return value;
|
|
162
|
+
let end = Math.max(0, maxBytes - CLIPPED_BYTES);
|
|
163
|
+
while (end > 0 && !TOKEN_END.has(bytes[end]))
|
|
164
|
+
end--;
|
|
165
|
+
return `${echoDecoder.decode(bytes.slice(0, end)).trimEnd()}${end > 0 ? " " : ""}${CLIPPED}`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Clamp text so its JSON string fits `maxChars` serialized characters, quotes and marker
|
|
169
|
+
* included, without cutting a token. Escapes make a character cost up to six.
|
|
170
|
+
*/
|
|
171
|
+
export function clipSerialized(value, maxChars) {
|
|
172
|
+
if (JSON.stringify(value).length <= maxChars)
|
|
173
|
+
return value;
|
|
174
|
+
const marked = (kept) => `${kept}${kept ? " " : ""}${CLIPPED}`;
|
|
175
|
+
let low = 0;
|
|
176
|
+
let high = Math.min(value.length, maxChars);
|
|
177
|
+
while (low < high) {
|
|
178
|
+
const mid = Math.ceil((low + high) / 2);
|
|
179
|
+
if (JSON.stringify(marked(value.slice(0, mid))).length <= maxChars)
|
|
180
|
+
low = mid;
|
|
181
|
+
else
|
|
182
|
+
high = mid - 1;
|
|
183
|
+
}
|
|
184
|
+
return marked(tokenPrefix(value, low));
|
|
185
|
+
}
|
|
110
186
|
/**
|
|
111
187
|
* Snapshot the caller's arguments after bounded schema-aware writeOnly omission,
|
|
112
188
|
* then apply the shared byte budget without clipping. Unresolved sensitivity
|
|
@@ -344,6 +420,13 @@ const RETRYABLE_BY_CODE = {
|
|
|
344
420
|
input_required_limit: false,
|
|
345
421
|
connector_call_failed: false,
|
|
346
422
|
};
|
|
423
|
+
/** The downstream said this caller has no access: affirmative evidence, unlike a failure to answer. */
|
|
424
|
+
export function isAccessRefusal(error) {
|
|
425
|
+
return (error instanceof ConnectorCallError &&
|
|
426
|
+
(error.code === "auth_required" ||
|
|
427
|
+
error.code === "downstream_oauth_required" ||
|
|
428
|
+
error.code === "provider_permission_denied"));
|
|
429
|
+
}
|
|
347
430
|
/** Runtime membership in the closed connector taxonomy, checked against its exhaustive table. */
|
|
348
431
|
export function isConnectorCallErrorCode(value) {
|
|
349
432
|
return typeof value === "string" && Object.hasOwn(RETRYABLE_BY_CODE, value);
|