@zackbart/connecta 0.40.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +30 -4
- package/dist/activity.js +28 -2
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +4 -41
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +12 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +51 -21
- package/dist/catalog-cache.js +62 -21
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-service.js +39 -12
- package/dist/catalog.js +103 -38
- package/dist/config.d.ts +10 -4
- package/dist/config.js +30 -7
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/connection.js +52 -18
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +4 -4
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +1 -0
- package/dist/describe-config.js +1 -0
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +8 -1
- package/dist/errors.js +2 -0
- package/dist/execute.js +20 -7
- package/dist/executor-result.js +1 -1
- package/dist/executors/quickjs-child.js +1 -1
- package/dist/executors/quickjs-runtime.js +1 -1
- package/dist/executors/quickjs.js +1 -1
- package/dist/guarded-transport.js +2 -0
- package/dist/http/client.js +7 -0
- package/dist/http/hatch.js +2 -0
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/index.js +10 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +72 -33
- package/dist/membership.d.ts +1 -1
- package/dist/meta-tools.js +27 -13
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-tools.js +11 -4
- package/dist/node.d.ts +7 -0
- package/dist/node.js +82 -3
- package/dist/operation-activity.js +103 -0
- package/dist/operator-record.d.ts +8 -0
- package/dist/operator-record.js +15 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.js +1 -1
- package/dist/providers/github/index.js +1 -1
- package/dist/providers/gmail/index.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +1 -1
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -1
- package/dist/providers/slides/index.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/api.js +1 -0
- package/dist/registry.js +16 -4
- package/dist/resource-read.js +21 -2
- package/dist/route-names.js +44 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/mcp.js +32 -13
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +2 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +132 -43
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +7 -1
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/types.d.ts +5 -0
- package/dist/url-safety.js +38 -20
- package/dist/utility-recovery.js +33 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +33 -8
- package/documentation/architecture.md +48 -1
- package/documentation/auth.md +116 -36
- package/documentation/code-mode.md +61 -46
- package/documentation/deploying.md +25 -1
- package/documentation/meta-tools.md +113 -11
- package/documentation/operating.md +86 -1
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +16 -14
- package/package.json +10 -8
- package/templates/node/package.json +1 -1
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// served without connecting (INV-10).
|
|
4
4
|
import { routePhrases } from "../../agent-routes.js";
|
|
5
5
|
import { authView } from "../../auth/native-call.js";
|
|
6
|
-
import { authFlow, authCall, authInvoke, authMethod
|
|
7
|
-
import { openRequest, viewServices, viewSignal
|
|
6
|
+
import { authFlow, authCall, authInvoke, authMethod } from "../../auth/native.js";
|
|
7
|
+
import { openRequest, viewServices, viewSignal } from "../../runtime/call.js";
|
|
8
8
|
import { secretDigest } from "../../storage-digest.js";
|
|
9
9
|
import { downstreamClientMetadata, downstreamRedirectUri, remoteClientAuthMethod, selfHostedClientUrl, } from "../../auth/downstream-client-metadata.js";
|
|
10
10
|
import { Client, SdkErrorCode, SdkHttpError, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
@@ -12,7 +12,7 @@ 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, observeCatalogChange } from "../../catalog-cache.js";
|
|
15
|
+
import { catalogClientOptions, prefetchCatalog, observeCatalogChange } from "../../catalog-cache.js";
|
|
16
16
|
import { ConnectorCallError, resendAdvice } from "../../errors.js";
|
|
17
17
|
import { carryFailureFacts, failureRecord, logFailure } from "../../operator-record.js";
|
|
18
18
|
import { CONNECTA_VERSION } from "../../version.js";
|
|
@@ -23,13 +23,42 @@ import { downstreamInputCapabilities } from "../../downstream-input-context.js";
|
|
|
23
23
|
import { detach, runEdge } from "../../runtime/run.js";
|
|
24
24
|
import { requestPrincipalFor } from "../../request-principal.js";
|
|
25
25
|
import { callerOf } from "../../connector-caller.js";
|
|
26
|
-
import {
|
|
27
|
-
import { negotiationKeys, responseCacheKeys } from "../../storage/keys.js";
|
|
26
|
+
import { negotiationKeys } from "../../storage/keys.js";
|
|
28
27
|
import { forgetNegotiation, readNegotiation, storeNegotiation } from "../negotiation-cache.js";
|
|
29
28
|
import { carriesIllegalHeaderChar, credentialHeaderValue } from "./config.js";
|
|
30
29
|
import { AUTH_OR_THROTTLE, promised, recordWireErrors, requiresAuthorization } from "./errors.js";
|
|
31
30
|
import { MAX_TOOL_PAGES } from "./catalog.js";
|
|
32
31
|
import { SKILLS_EXTENSION } from "./resources.js";
|
|
32
|
+
/** Retain every abandoned lease release until the final scope close joins it. */
|
|
33
|
+
export function retainCleanup(state, release) {
|
|
34
|
+
const pending = detach(release);
|
|
35
|
+
state.cleanup.add(pending);
|
|
36
|
+
if (state.closing)
|
|
37
|
+
state.closing = Promise.all([state.closing, pending]).then(() => { });
|
|
38
|
+
// Register at the release itself as well: a late connect can acquire a
|
|
39
|
+
// session after the owner's first close has already settled.
|
|
40
|
+
try {
|
|
41
|
+
state.defer?.(pending);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Runtime hooks are best-effort; the bounded cleanup is already running.
|
|
45
|
+
}
|
|
46
|
+
void pending.then(() => state.cleanup.delete(pending));
|
|
47
|
+
return pending;
|
|
48
|
+
}
|
|
49
|
+
/** Scope.closeUnsafe returns nothing on a second close, even during release. */
|
|
50
|
+
export function closeConnectionScope(state) {
|
|
51
|
+
if (!state.closing) {
|
|
52
|
+
const release = Scope.closeUnsafe(state.scope, Exit.void);
|
|
53
|
+
if (release)
|
|
54
|
+
retainCleanup(state, release);
|
|
55
|
+
state.closing = (async () => {
|
|
56
|
+
while (state.cleanup.size)
|
|
57
|
+
await Promise.all(state.cleanup);
|
|
58
|
+
})();
|
|
59
|
+
}
|
|
60
|
+
return state.closing;
|
|
61
|
+
}
|
|
33
62
|
/** One connector's connection states, credential reads and connect attempts. */
|
|
34
63
|
export function remoteMcpConnection(rt) {
|
|
35
64
|
const { requestAuth, forwarding, id, staticClient, clientMetadataUrl, opts, oauthScope, authorizationParams, destination, isOauth, extraHeaders, credentialHeader, credentialScheme, skillsEnabled, principalPolicy, credentialAuth, } = rt;
|
|
@@ -105,6 +134,9 @@ export function remoteMcpConnection(rt) {
|
|
|
105
134
|
if (!state) {
|
|
106
135
|
state = {
|
|
107
136
|
scope: Scope.makeUnsafe(),
|
|
137
|
+
closing: null,
|
|
138
|
+
cleanup: new Set(),
|
|
139
|
+
defer: ctx.defer,
|
|
108
140
|
lease: null,
|
|
109
141
|
client: null,
|
|
110
142
|
transport: null,
|
|
@@ -120,10 +152,8 @@ export function remoteMcpConnection(rt) {
|
|
|
120
152
|
states.set(key, state);
|
|
121
153
|
const owned = state;
|
|
122
154
|
owner.own(() => {
|
|
123
|
-
|
|
155
|
+
void closeConnectionScope(owned);
|
|
124
156
|
reset(owned);
|
|
125
|
-
if (closing)
|
|
126
|
-
detach(closing);
|
|
127
157
|
});
|
|
128
158
|
}
|
|
129
159
|
return state;
|
|
@@ -191,7 +221,7 @@ export function remoteMcpConnection(rt) {
|
|
|
191
221
|
reset(state);
|
|
192
222
|
const release = lease && Scope.closeUnsafe(lease, Exit.void);
|
|
193
223
|
if (release)
|
|
194
|
-
|
|
224
|
+
retainCleanup(state, release);
|
|
195
225
|
};
|
|
196
226
|
const waitForConnection = (attempt, signal) => runEdge(Deferred.await(attempt), signal ? { signal } : {});
|
|
197
227
|
const oauthConfigFor = (ctx) => isOauth
|
|
@@ -274,6 +304,7 @@ export function remoteMcpConnection(rt) {
|
|
|
274
304
|
try {
|
|
275
305
|
let epoch = "";
|
|
276
306
|
let identity;
|
|
307
|
+
let pinnedIdentity;
|
|
277
308
|
if (isOauth) {
|
|
278
309
|
// Read-only grant snapshots: no in-place sealing, epoch binding or
|
|
279
310
|
// refresh. Every identity check during cache acceptance re-reads the
|
|
@@ -284,6 +315,7 @@ export function remoteMcpConnection(rt) {
|
|
|
284
315
|
epoch = snapshot.epoch;
|
|
285
316
|
if (provider.isOperatorDisconnectedEpoch(epoch) || !snapshot.tokens)
|
|
286
317
|
return preread;
|
|
318
|
+
pinnedIdentity = yield authCall(secretDigest, [ctx, JSON.stringify(snapshot.tokens)]);
|
|
287
319
|
const bound = epoch;
|
|
288
320
|
identity = authMethod(() => authFlow(function* () {
|
|
289
321
|
const current = (yield authInvoke(provider, "peekGrant", []));
|
|
@@ -297,13 +329,15 @@ export function remoteMcpConnection(rt) {
|
|
|
297
329
|
const digest = (yield authCall(negotiationKey, [ctx, credentialDigest, epoch]));
|
|
298
330
|
// Both reads only locate cached data. Acceptance and dispatch keep
|
|
299
331
|
// their fresh generation and credential checks.
|
|
300
|
-
yield
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
332
|
+
yield authCall(prefetchCatalog, [
|
|
333
|
+
ctx,
|
|
334
|
+
id,
|
|
335
|
+
(yield authCall(catalogConfigFor, [ctx])),
|
|
336
|
+
JSON.stringify([epoch, credentialDigest]),
|
|
337
|
+
catalogSharing,
|
|
338
|
+
pinnedIdentity,
|
|
339
|
+
negotiationKeys.verdict(digest),
|
|
340
|
+
]);
|
|
307
341
|
const prior = (yield authCall(readNegotiation, [ctx, digest]));
|
|
308
342
|
preread.negotiation = { digest, prior };
|
|
309
343
|
if (prior?.kind === "modern")
|
|
@@ -644,13 +678,13 @@ export function remoteMcpConnection(rt) {
|
|
|
644
678
|
state.authRequired = true;
|
|
645
679
|
const release = Scope.closeUnsafe(lease, Exit.void);
|
|
646
680
|
if (release)
|
|
647
|
-
|
|
681
|
+
retainCleanup(state, release);
|
|
648
682
|
}
|
|
649
683
|
else {
|
|
650
684
|
// Whoever abandoned this attempt closed the lease already, while
|
|
651
685
|
// the connect was in flight. A session it acquired after that
|
|
652
686
|
// close still owes its DELETE.
|
|
653
|
-
|
|
687
|
+
retainCleanup(state, rt.closeConnection(held.client, held.transport, ctx.logger));
|
|
654
688
|
}
|
|
655
689
|
if (err instanceof UnauthorizedError) {
|
|
656
690
|
return Effect.fail(carryFailureFacts(err, rt.authRequiredError(ctx)));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The downstream OAuth surface: bounded SDK flows, early renewal before a
|
|
2
2
|
// call, unchallenged consent starts, and the connector's auth operations.
|
|
3
|
+
import { retainCleanup } from "./connection.js";
|
|
3
4
|
import { authView } from "../../auth/native-call.js";
|
|
4
5
|
import { authFlow, authCall, authInvoke, authMethod } from "../../auth/native.js";
|
|
5
6
|
import { viewSignal } from "../../runtime/call.js";
|
|
@@ -10,7 +11,6 @@ import { byteReadResponse } from "../../byte-read-response.js";
|
|
|
10
11
|
import { requestSentSecrets, sentSecretsFor } from "../../sent-secrets.js";
|
|
11
12
|
import { ConnectorCallError, resendAdvice, WithheldTextError } from "../../errors.js";
|
|
12
13
|
import { attachFailureFacts, carryFailureFacts, OAUTH_ERROR_CODES } from "../../operator-record.js";
|
|
13
|
-
import { detach } from "../../runtime/run.js";
|
|
14
14
|
import { retryAfterMs } from "../../http-policy.js";
|
|
15
15
|
import { sendCredentialPost } from "../../guarded-transport.js";
|
|
16
16
|
import { UnboundOAuthFlowsError, downstreamCallError, errorKind, hasSdkPayload, ownAbortReason, requiresAuthorization, startMessage, withheldAs, } from "./errors.js";
|
|
@@ -376,7 +376,7 @@ export function remoteMcpOAuth(rt) {
|
|
|
376
376
|
]));
|
|
377
377
|
}
|
|
378
378
|
finally {
|
|
379
|
-
|
|
379
|
+
retainCleanup(state, rt.closeConnection(null, t, ctx.logger));
|
|
380
380
|
}
|
|
381
381
|
})), true));
|
|
382
382
|
connector.verifyState = authMethod((oauthState, ctx) => authFlow(function* () {
|
|
@@ -345,8 +345,11 @@ export function remoteMcpResources(rt) {
|
|
|
345
345
|
if (client.getProtocolEra() !== "modern" ||
|
|
346
346
|
!skillObject(capabilities?.resources) ||
|
|
347
347
|
!skillObject(extension) ||
|
|
348
|
+
// The released Beta specification defines no revision setting.
|
|
349
|
+
// Never enable a future breaking revision under the same ID.
|
|
350
|
+
extension.revision !== undefined ||
|
|
348
351
|
(extension.directoryRead !== undefined && typeof extension.directoryRead !== "boolean")) {
|
|
349
|
-
throw new ConnectorCallError("
|
|
352
|
+
throw new ConnectorCallError("unsupported_capability", "The downstream did not declare a valid Skills extension and resources capability.");
|
|
350
353
|
}
|
|
351
354
|
const result = (yield authCall(run, [client, state]));
|
|
352
355
|
skillScopeOpen(ctx, state);
|
|
@@ -415,7 +418,7 @@ export function remoteMcpResources(rt) {
|
|
|
415
418
|
rt.listingContexts.set(options, ctx);
|
|
416
419
|
try {
|
|
417
420
|
if (!client.getServerCapabilities()?.resources)
|
|
418
|
-
throw
|
|
421
|
+
throw new ConnectorCallError("unsupported_capability", "The downstream did not declare resources support.");
|
|
419
422
|
const resources = (yield authInvoke(client, "listResources", [undefined, options]));
|
|
420
423
|
const templates = (yield authInvoke(client, "listResourceTemplates", [
|
|
421
424
|
undefined,
|
|
@@ -18,7 +18,7 @@ import { learnedUrlRefusal } from "../../url-safety.js";
|
|
|
18
18
|
import { RequestScope } from "../../runtime/request-scope.js";
|
|
19
19
|
import { Context } from "effect";
|
|
20
20
|
import { downstreamWriteContinuation } from "../../downstream-input-context.js";
|
|
21
|
-
import { guardedTransport, sendGuarded, sendCredentialPost } from "../../guarded-transport.js";
|
|
21
|
+
import { learnedDestination, guardedTransport, sendGuarded, sendCredentialPost, } from "../../guarded-transport.js";
|
|
22
22
|
import { dispatchProviderRequest, recordProviderResponse, knownProviderFailure, providerRequest, renewedAfterUnauthorized, } from "../../provider-call.js";
|
|
23
23
|
import { recordRequestField } from "../../provider-call-facts.js";
|
|
24
24
|
import { AUTH_OR_THROTTLE, errorBodyText, jsonRpcErrorBody, keepsModernVerdict, promised } from "./errors.js";
|
|
@@ -128,11 +128,25 @@ labelErrorClass(RemoteMcpDestinationError, "RemoteMcpDestinationError");
|
|
|
128
128
|
*/
|
|
129
129
|
export function learnedUrlSafeFetch(connectorId, configured, baseFetch) {
|
|
130
130
|
return authMethod((input, init) => authFlow(function* () {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
let target = new URL(input);
|
|
132
|
+
for (let hop = 0; hop < 10; hop++) {
|
|
133
|
+
const reason = learnedUrlRefusal(configured, target);
|
|
134
|
+
if (reason !== undefined)
|
|
135
|
+
throw new RemoteMcpDestinationError(connectorId, reason);
|
|
136
|
+
const marked = { ...init, redirect: "manual", [learnedDestination]: configured };
|
|
137
|
+
const response = (yield authCall(baseFetch, [target, marked]));
|
|
138
|
+
if (!REDIRECT_STATUSES.has(response.status) ||
|
|
139
|
+
!response.headers.has("location") ||
|
|
140
|
+
(init?.method ?? "GET").toUpperCase() !== "GET")
|
|
141
|
+
return byteReadResponse(response);
|
|
142
|
+
const location = response.headers.get("location");
|
|
143
|
+
void response.body?.cancel().catch(() => { });
|
|
144
|
+
const redirected = new URL(location, target);
|
|
145
|
+
if (redirected.origin !== target.origin)
|
|
146
|
+
throw new RemoteMcpDestinationError(connectorId, "cross-origin redirect refused");
|
|
147
|
+
target = redirected;
|
|
134
148
|
}
|
|
135
|
-
|
|
149
|
+
throw new RemoteMcpDestinationError(connectorId, "redirect limit exceeded");
|
|
136
150
|
}));
|
|
137
151
|
}
|
|
138
152
|
/**
|
|
@@ -289,7 +303,11 @@ export function remoteMcpTransport(rt) {
|
|
|
289
303
|
if (customFetch && new URL(input instanceof Request ? input.url : String(input)).origin !== destination.origin)
|
|
290
304
|
throw new ConnectorCallError("connector_call_failed", `Connector "${id}" refused a request outside its origin; nothing was sent.`, { retryable: false });
|
|
291
305
|
// The deployment's fetch owns the wire: each write is handed over once.
|
|
292
|
-
const sender = flight?.write
|
|
306
|
+
const sender = flight?.write
|
|
307
|
+
? (customSender ?? guardedTransport(true))
|
|
308
|
+
: init?.[learnedDestination]
|
|
309
|
+
? guardedTransport(false)
|
|
310
|
+
: undefined;
|
|
293
311
|
if (flight?.write) {
|
|
294
312
|
if (flight.sent) {
|
|
295
313
|
if (flight.request && flight.authorization)
|
|
@@ -5,7 +5,7 @@ import { authFlow, authCall, authInvoke, authValue, authMethod, authCatch } from
|
|
|
5
5
|
import { viewServices, viewSignal } from "../runtime/call.js";
|
|
6
6
|
import { declareSelfHostedClient } from "../auth/downstream-client-metadata.js";
|
|
7
7
|
import { isInputRequiredResult } from "@modelcontextprotocol/client";
|
|
8
|
-
import { Duration, Effect
|
|
8
|
+
import { Duration, Effect } from "effect";
|
|
9
9
|
import { byteReadResponse } from "../byte-read-response.js";
|
|
10
10
|
import { redactSentSecrets, requestSentSecrets, sentSecretsFor } from "../sent-secrets.js";
|
|
11
11
|
import { closeCatalogCacheScope } from "../catalog-cache.js";
|
|
@@ -28,7 +28,7 @@ import { providerBoundary } from "../provider-call.js";
|
|
|
28
28
|
import { downstreamCallError, hasSdkPayload, payloadFree, remoteMcpErrors } from "./remote-mcp/errors.js";
|
|
29
29
|
import { LOCAL_CLOSE_BUDGET_MS, REDIRECT_STATUSES, remoteMcpTransport } from "./remote-mcp/transport.js";
|
|
30
30
|
import { remoteMcpConfig } from "./remote-mcp/config.js";
|
|
31
|
-
import { remoteMcpConnection } from "./remote-mcp/connection.js";
|
|
31
|
+
import { closeConnectionScope, remoteMcpConnection } from "./remote-mcp/connection.js";
|
|
32
32
|
import { remoteMcpOAuth } from "./remote-mcp/oauth.js";
|
|
33
33
|
import { remoteMcpCatalog } from "./remote-mcp/catalog.js";
|
|
34
34
|
import { remoteMcpResources } from "./remote-mcp/resources.js";
|
|
@@ -267,11 +267,11 @@ export function remoteMcp(id, opts) {
|
|
|
267
267
|
// Revocation and all finalizers start before any caller guard. Cleanup
|
|
268
268
|
// retains its own bounded settlement even after the request has closed.
|
|
269
269
|
const state = entryFor(ctx);
|
|
270
|
-
const release =
|
|
270
|
+
const release = closeConnectionScope(state);
|
|
271
271
|
const cacheClosing = closeCatalogCacheScope(ctx, id, cacheOwner);
|
|
272
272
|
reset(state);
|
|
273
273
|
const closing = runEdge(Effect.all([
|
|
274
|
-
|
|
274
|
+
Effect.promise(() => release),
|
|
275
275
|
cacheClosing
|
|
276
276
|
? Effect.raceAllFirst([
|
|
277
277
|
Effect.promise(() => cacheClosing),
|
|
@@ -39,7 +39,9 @@ export interface CredentialVault {
|
|
|
39
39
|
* Encrypt downstream OAuth state (tokens, a registered client, a PKCE
|
|
40
40
|
* verifier) before it enters storage. `purpose` is the storage key the value
|
|
41
41
|
* is written under, so the result is bound to one connector, owner, and key.
|
|
42
|
-
*
|
|
42
|
+
* Construction refuses a vault without both `seal` and `open` when any
|
|
43
|
+
* connector has `startAuth`, so that state is never left plaintext beside
|
|
44
|
+
* credentials this vault encrypts.
|
|
43
45
|
*/
|
|
44
46
|
seal?(connectorId: string, purpose: string, plaintext: string, owner?: string): Promise<string>;
|
|
45
47
|
/** Sign browser OAuth handoffs with a purpose-specific key; nothing is stored. */
|
package/dist/describe-config.js
CHANGED
|
@@ -269,6 +269,7 @@ export function describeConfig(input) {
|
|
|
269
269
|
? {
|
|
270
270
|
enabled: true,
|
|
271
271
|
readable: typeof config.activity.store.list === "function",
|
|
272
|
+
...(config.activity.operations ? { operations: true } : {}),
|
|
272
273
|
...(str(config.activity.deploymentId) !== undefined ? { deploymentId: config.activity.deploymentId } : {}),
|
|
273
274
|
store: {
|
|
274
275
|
kind: storeKind(activityStore),
|
package/dist/downstream-input.js
CHANGED
|
@@ -316,12 +316,21 @@ export class DownstreamElicitation {
|
|
|
316
316
|
// The nonce is spent even when the downstream failure would normally
|
|
317
317
|
// allow a retry. Preserve write reconciliation and auth prerequisites.
|
|
318
318
|
const echoed = downstreamArgumentEcho(self.options.requestScope);
|
|
319
|
+
const recoveryError = { ...error };
|
|
320
|
+
const freshRead = state.tool === "call_tool" &&
|
|
321
|
+
error.code !== "provider_permission_denied" &&
|
|
322
|
+
error.nextAction === undefined &&
|
|
323
|
+
error.recovery === undefined &&
|
|
324
|
+
error.writeOutcome !== "unknown" &&
|
|
325
|
+
error.uncertainCall === undefined;
|
|
326
|
+
if (freshRead)
|
|
327
|
+
delete recoveryError.retry;
|
|
319
328
|
const structuredContent = {
|
|
320
329
|
...result.structuredContent,
|
|
321
330
|
error: {
|
|
322
|
-
...
|
|
331
|
+
...recoveryError,
|
|
323
332
|
retryable: false,
|
|
324
|
-
...(
|
|
333
|
+
...(freshRead
|
|
325
334
|
? {
|
|
326
335
|
nextAction: {
|
|
327
336
|
tool: state.tool,
|
package/dist/errors.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type ConnectorCallErrorCode = "timeout" | "auth_required" | "downstream_o
|
|
|
24
24
|
* change, and retries with the base `current` reports — repeating the same
|
|
25
25
|
* call can never succeed, and waiting will not help either.
|
|
26
26
|
*/
|
|
27
|
-
| "conflict" | "resource_template_ambiguous" | "resource_match_budget_exceeded" | "input_required_unsupported" | "input_required_invalid" | "input_required_limit" | "connector_call_failed";
|
|
27
|
+
| "conflict" | "resource_template_ambiguous" | "resource_match_budget_exceeded" | "unsupported_capability" | "input_required_unsupported" | "input_required_invalid" | "input_required_limit" | "connector_call_failed";
|
|
28
28
|
/**
|
|
29
29
|
* Every classification code connecta assigns: a connector's typed codes, the
|
|
30
30
|
* executor's admission codes, and the refusals connecta frames itself.
|
|
@@ -285,6 +285,13 @@ export interface CallErrorDetails extends Partial<AuthorizationHandoff> {
|
|
|
285
285
|
writeOutcome?: "unknown";
|
|
286
286
|
/** The single model-facing entry point for every credential class. */
|
|
287
287
|
nextAction?: {
|
|
288
|
+
tool: "skills";
|
|
289
|
+
arguments: Record<string, never>;
|
|
290
|
+
purpose: string;
|
|
291
|
+
} | {
|
|
292
|
+
action: "reconnect_host" | "contact_operator" | "inspect_capabilities" | "recover_result";
|
|
293
|
+
purpose: string;
|
|
294
|
+
} | {
|
|
288
295
|
tool: "authorize_connector";
|
|
289
296
|
arguments: {
|
|
290
297
|
connector: string;
|
package/dist/errors.js
CHANGED
|
@@ -15,6 +15,7 @@ const CLASSIFICATION_CODE_TABLE = {
|
|
|
15
15
|
conflict: true,
|
|
16
16
|
resource_template_ambiguous: true,
|
|
17
17
|
resource_match_budget_exceeded: true,
|
|
18
|
+
unsupported_capability: true,
|
|
18
19
|
input_required_unsupported: true,
|
|
19
20
|
input_required_invalid: true,
|
|
20
21
|
input_required_limit: true,
|
|
@@ -337,6 +338,7 @@ const RETRYABLE_BY_CODE = {
|
|
|
337
338
|
conflict: false,
|
|
338
339
|
resource_template_ambiguous: false,
|
|
339
340
|
resource_match_budget_exceeded: false,
|
|
341
|
+
unsupported_capability: false,
|
|
340
342
|
input_required_unsupported: false,
|
|
341
343
|
input_required_invalid: false,
|
|
342
344
|
input_required_limit: false,
|
package/dist/execute.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { operationActivity } from "./operation-activity.js";
|
|
2
|
+
import { SkillLookupError } from "./skills.js";
|
|
3
|
+
import { skillLookupRecovery, utilityRecovery } from "./utility-recovery.js";
|
|
1
4
|
import { META_ROUTES, routeNames } from "./agent-routes.js";
|
|
2
5
|
import { authCall, authFlow, authInvoke, authMethod } from "./auth/native.js";
|
|
3
6
|
import { nativeRecovery } from "./native-recovery.js";
|
|
@@ -342,7 +345,9 @@ function serializedDiagnosticBytes(value) {
|
|
|
342
345
|
}
|
|
343
346
|
}
|
|
344
347
|
function guestFailure(code, message, retryable = false) {
|
|
345
|
-
return new InvocationFailure(
|
|
348
|
+
return new InvocationFailure(["unavailable", "unsupported_capability", "input_required_unsupported"].includes(code)
|
|
349
|
+
? utilityRecovery(code, message, retryable)
|
|
350
|
+
: { code, message, retryable });
|
|
346
351
|
}
|
|
347
352
|
/**
|
|
348
353
|
* E1/X11: bound typed host failure metadata, including JSON escapes, before transport.
|
|
@@ -724,7 +729,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
724
729
|
if (typeof id !== "string" ||
|
|
725
730
|
(!options && options !== undefined) ||
|
|
726
731
|
(options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
|
|
727
|
-
throw
|
|
732
|
+
throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? })."));
|
|
728
733
|
}
|
|
729
734
|
const args = { ...(options ?? {}) };
|
|
730
735
|
if (Object.keys(args).some((key) => !["page", "offset", "maxBytes"].includes(key)) ||
|
|
@@ -737,7 +742,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
737
742
|
(typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
|
|
738
743
|
(args.maxBytes !== undefined &&
|
|
739
744
|
(typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
|
|
740
|
-
throw
|
|
745
|
+
throw new InvocationFailure(utilityRecovery("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }) with whole page/offset >= 0 and maxBytes >= 1; page and offset are mutually exclusive."));
|
|
741
746
|
}
|
|
742
747
|
// A page must fit every shipped bridge even when the connector cap is raised.
|
|
743
748
|
args.maxBytes = Math.min(args.maxBytes ?? PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_PAGE_BYTES);
|
|
@@ -768,6 +773,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
768
773
|
if (typeof skillName !== "string")
|
|
769
774
|
throw guestFailure("invalid_args", 'Use connecta.skill("usage") or connecta.skill("connector:<id>") with an exact skill name; the skills tool lists names.');
|
|
770
775
|
const skills = new SkillsRegistry(registry, baseUrl, {
|
|
776
|
+
readOnly: limits.readOnly,
|
|
771
777
|
requestScope,
|
|
772
778
|
requestSignal: utilitySignal ?? hostAccessSignal,
|
|
773
779
|
onRead: (connector, source) => limits.resultDependencies?.push({
|
|
@@ -780,7 +786,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
780
786
|
});
|
|
781
787
|
return { name: skillName, format: "text", text: await skills.text(skillName) };
|
|
782
788
|
},
|
|
783
|
-
catch: (err) => err instanceof
|
|
789
|
+
catch: (err) => err instanceof SkillLookupError
|
|
790
|
+
? new InvocationFailure(skillLookupRecovery(err.message))
|
|
791
|
+
: err instanceof ConnectorCallError
|
|
792
|
+
? guestFailure(err.code, "Skills are unavailable.", err.retryable)
|
|
793
|
+
: err,
|
|
784
794
|
}),
|
|
785
795
|
read: (uri) => Effect.suspend(() => {
|
|
786
796
|
const started = Date.now();
|
|
@@ -923,7 +933,10 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
923
933
|
timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
|
|
924
934
|
})
|
|
925
935
|
: invoke;
|
|
926
|
-
const
|
|
936
|
+
const operationEffect = activity?.recordOperation && ["search", "describe", "result", "skill"].includes(name)
|
|
937
|
+
? operationActivity(activity, registry, name, guarded)
|
|
938
|
+
: guarded;
|
|
939
|
+
const settled = runEdge(operationEffect.pipe(Effect.catch((error) => {
|
|
927
940
|
if (deadline && hostAccessSignal?.aborted)
|
|
928
941
|
return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
|
|
929
942
|
if (deadline && error instanceof InvocationFailure && error.code === "timeout") {
|
|
@@ -1230,7 +1243,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1230
1243
|
}
|
|
1231
1244
|
return play(normalizeProgramSource(code), diagnostics, options.signal, requestScope, hostCalls, programWrites);
|
|
1232
1245
|
});
|
|
1233
|
-
return response.pipe(Effect.provideService(RequestScope, owner), Effect.map((unfinished) => {
|
|
1246
|
+
return operationActivity(activity, registry, "program", response.pipe(Effect.provideService(RequestScope, owner), Effect.map((unfinished) => {
|
|
1234
1247
|
// Every response, including intake refusals, crosses accounting and
|
|
1235
1248
|
// exactly one envelope check at this operation boundary.
|
|
1236
1249
|
const result = redactAgentOutput(sentSecrets, programWrites.finish(unfinished));
|
|
@@ -1241,7 +1254,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1241
1254
|
if (result.content[0]?.type === "text")
|
|
1242
1255
|
result.content[0].text = JSON.stringify(result.structuredContent);
|
|
1243
1256
|
return result;
|
|
1244
|
-
}), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error)));
|
|
1257
|
+
}), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error))), () => ({ ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded }));
|
|
1245
1258
|
}));
|
|
1246
1259
|
}
|
|
1247
1260
|
/** An execution that never produced an ExecuteResult, as the model sees it. */
|
package/dist/executor-result.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { prepareExecuteResultForTransport } from "../executor-result.js";
|
|
2
|
-
import { InvocationFailure } from "../invocation.js";
|
|
2
|
+
import { InvocationFailure } from "../invocation-failure.js";
|
|
3
3
|
import { msg } from "../errors.js";
|
|
4
4
|
import { MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_RESULT_BYTES, QUICKJS_RESULT_CHUNK_CHARS, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
|
|
5
5
|
import { executeQuickJs, prepareQuickJs } from "./quickjs-runtime.js";
|
|
@@ -9,7 +9,7 @@ import { utf8Length } from "../bytes.js";
|
|
|
9
9
|
// continuations from a host-side loop. Values cross the boundary as JSON, so
|
|
10
10
|
// provider args/results must be JSON-serializable.
|
|
11
11
|
import { getQuickJS } from "quickjs-emscripten";
|
|
12
|
-
import { InvocationFailure } from "../invocation.js";
|
|
12
|
+
import { InvocationFailure } from "../invocation-failure.js";
|
|
13
13
|
import { msg } from "../errors.js";
|
|
14
14
|
import { withoutProgramTerminator } from "../program-source.js";
|
|
15
15
|
import { hostCallLabel, MAX_QUICKJS_LOG_TRANSPORT_BYTES, serializedBytes } from "./quickjs-protocol.js";
|
|
@@ -18,7 +18,7 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
import { Deferred, Duration, Effect, Exit, Schedule, Scope } from "effect";
|
|
19
19
|
import { AdmissionController, ExecutorAdmissionError, ExecutorExecutionError } from "../executor-admission.js";
|
|
20
20
|
import { msg } from "../errors.js";
|
|
21
|
-
import { InvocationFailure } from "../invocation.js";
|
|
21
|
+
import { InvocationFailure } from "../invocation-failure.js";
|
|
22
22
|
import { brandExecutor } from "../executor-contract.js";
|
|
23
23
|
import { MAX_EXECUTE_LOG_CHARS } from "../executor-result.js";
|
|
24
24
|
import { admit } from "../runtime/admission.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// The plain runtime slot keeps the HTTP kit and Node sender off the root
|
|
2
2
|
// and Worker graphs. /node installs its sender when listen() starts serving.
|
|
3
3
|
import { ConnectorCallError } from "./errors.js";
|
|
4
|
+
/** Internal request marker: only downstream-learned URL fetches set this. */
|
|
5
|
+
export const learnedDestination = Symbol("connecta.learnedDestination");
|
|
4
6
|
let owned;
|
|
5
7
|
/** Runtime configuration, never request data. Returns the previous slot for isolated fixtures. */
|
|
6
8
|
export function configureGuardedTransport(transport) {
|
package/dist/http/client.js
CHANGED
|
@@ -30,6 +30,12 @@ import { RedirectRefused, Refusal, RequestRefused, ResendRefused, ResponseTooLar
|
|
|
30
30
|
import { Classification, Dispatches, IdempotencyKey, Ledger, RequestFacts, Secrets, Signal, WriteCapacity, reserveWrite, writeRequestKey, } from "./services.js";
|
|
31
31
|
const decoder = new TextDecoder();
|
|
32
32
|
const responseEntries = new WeakMap();
|
|
33
|
+
// Private request identity for middleware observing a rejected response. Never
|
|
34
|
+
// attach headers or body to the failure or its payload-free ledger entry.
|
|
35
|
+
const failedRequests = new WeakMap();
|
|
36
|
+
export function failedResponseRequest(failure) {
|
|
37
|
+
return failedRequests.get(failure);
|
|
38
|
+
}
|
|
33
39
|
/** The core's physical request, also available to direct HttpClient middleware. */
|
|
34
40
|
export function responseRequest(response) {
|
|
35
41
|
return responseEntries.get(response);
|
|
@@ -216,6 +222,7 @@ export function guardedClient(options) {
|
|
|
216
222
|
const refused = (failure) => {
|
|
217
223
|
dispatch.failure = failure;
|
|
218
224
|
responseEntries.set(failure, entry);
|
|
225
|
+
failedRequests.set(failure, request);
|
|
219
226
|
return Effect.fail(failure);
|
|
220
227
|
};
|
|
221
228
|
const response = yield* Effect.tryPromise({
|
package/dist/http/hatch.js
CHANGED
|
@@ -1171,6 +1171,8 @@ export function hatch(options) {
|
|
|
1171
1171
|
const send = options.binding
|
|
1172
1172
|
? options.binding.send(call(), sendOptions)
|
|
1173
1173
|
: connection.sendPath(request, sendOptions);
|
|
1174
|
+
// Both send paths share the binding result contract. Keep its optional
|
|
1175
|
+
// metadata typed even when the plain HTTP branch omits those properties.
|
|
1174
1176
|
const answer = yield* send.pipe(Effect.catch((failure) => Effect.gen(function* () {
|
|
1175
1177
|
if (!key || args.idempotencyKey)
|
|
1176
1178
|
return yield* Effect.fail(failure);
|