@zackbart/connecta 0.40.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 +148 -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 +32 -5
- package/dist/activity.js +32 -6
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +33 -44
- 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-coordinator.js +4 -1
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +15 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +77 -39
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +352 -43
- package/dist/catalog-freshness.js +25 -0
- package/dist/catalog-service.js +408 -78
- package/dist/catalog.js +216 -62
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +53 -21
- package/dist/config.js +56 -17
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +113 -22
- 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 +14 -5
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +4 -2
- package/dist/describe-config.js +1 -0
- package/dist/discovery-health.js +26 -11
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +60 -4
- package/dist/errors.js +88 -3
- package/dist/execute.js +491 -202
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +3 -3
- package/dist/executors/quickjs-child.js +14 -5
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +37 -20
- package/dist/executors/quickjs.js +24 -7
- package/dist/guarded-transport.js +2 -0
- 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 +14 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +11 -3
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- 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 +15 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +167 -64
- package/dist/membership.d.ts +1 -1
- 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 +286 -73
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +177 -55
- package/dist/node.d.ts +7 -0
- package/dist/node.js +85 -4
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +104 -0
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +9 -1
- package/dist/operator-record.js +16 -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 +1 -14
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +2 -1
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +8 -3
- 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 +2 -1
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +3 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +12 -6
- 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/api.js +28 -25
- package/dist/providers/quickbooks/index.js +3 -6
- 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 +2 -1
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +2 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +3 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +142 -30
- package/dist/resource-read.js +21 -2
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +53 -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/connect.js +21 -13
- package/dist/routes/mcp.js +83 -35
- package/dist/routes/shared.d.ts +6 -0
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +38 -2
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +213 -57
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +16 -7
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +36 -0
- package/dist/url-safety.js +38 -20
- package/dist/usage-guide.js +14 -13
- package/dist/utility-recovery.js +33 -0
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +60 -15
- package/documentation/architecture.md +152 -43
- package/documentation/auth.md +160 -53
- package/documentation/code-mode.md +396 -119
- package/documentation/deploying.md +35 -10
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +761 -157
- package/documentation/operating.md +91 -4
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +34 -20
- 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 +10 -8
- 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 { 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";
|
|
@@ -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,17 +261,18 @@ 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* () {
|
|
267
268
|
// Revocation and all finalizers start before any caller guard. Cleanup
|
|
268
269
|
// retains its own bounded settlement even after the request has closed.
|
|
269
270
|
const state = entryFor(ctx);
|
|
270
|
-
const release =
|
|
271
|
+
const release = closeConnectionScope(state);
|
|
271
272
|
const cacheClosing = closeCatalogCacheScope(ctx, id, cacheOwner);
|
|
272
273
|
reset(state);
|
|
273
274
|
const closing = runEdge(Effect.all([
|
|
274
|
-
|
|
275
|
+
Effect.promise(() => release),
|
|
275
276
|
cacheClosing
|
|
276
277
|
? Effect.raceAllFirst([
|
|
277
278
|
Effect.promise(() => cacheClosing),
|
|
@@ -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) {
|
|
@@ -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. */
|
|
@@ -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;
|
|
@@ -151,6 +152,7 @@ export interface ConnectaConfigDescription {
|
|
|
151
152
|
};
|
|
152
153
|
activity: {
|
|
153
154
|
enabled: boolean;
|
|
155
|
+
operations?: true;
|
|
154
156
|
readable?: boolean;
|
|
155
157
|
deploymentId?: string;
|
|
156
158
|
store?: {
|
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/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/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.
|
|
@@ -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,13 +321,31 @@ 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?: {
|
|
333
|
+
tool: "skills";
|
|
334
|
+
arguments: Record<string, never>;
|
|
335
|
+
purpose: string;
|
|
336
|
+
} | {
|
|
337
|
+
action: "reconnect_host" | "contact_operator" | "inspect_capabilities" | "recover_result";
|
|
338
|
+
purpose: string;
|
|
339
|
+
} | {
|
|
288
340
|
tool: "authorize_connector";
|
|
289
341
|
arguments: {
|
|
290
342
|
connector: string;
|
|
291
343
|
};
|
|
292
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;
|
|
293
349
|
} | {
|
|
294
350
|
tool: "search_tools";
|
|
295
351
|
arguments: {
|
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,
|
|
@@ -28,6 +29,7 @@ const CLASSIFICATION_CODE_TABLE = {
|
|
|
28
29
|
ambiguous_tool_alias: true,
|
|
29
30
|
catalog_lookup_failed: true,
|
|
30
31
|
result_too_large: true,
|
|
32
|
+
result_not_retained: true,
|
|
31
33
|
destructive_tool_requires_approval: true,
|
|
32
34
|
read_only_endpoint: true,
|
|
33
35
|
auth_replay_refused: true,
|
|
@@ -57,7 +59,7 @@ function boundedIssueText(value, maxChars) {
|
|
|
57
59
|
if (value.length <= maxChars)
|
|
58
60
|
return { value, truncated: false };
|
|
59
61
|
return {
|
|
60
|
-
value: `${value
|
|
62
|
+
value: `${tokenPrefix(value, maxChars - 1)}…`,
|
|
61
63
|
truncated: true,
|
|
62
64
|
};
|
|
63
65
|
}
|
|
@@ -90,12 +92,43 @@ const MAX_ECHOED_BYTES = 512;
|
|
|
90
92
|
*/
|
|
91
93
|
const echoEncoder = new TextEncoder();
|
|
92
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]);
|
|
93
98
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
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
|
|
96
115
|
* returned unchanged and untagged.
|
|
97
116
|
*/
|
|
98
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) {
|
|
99
132
|
const bytes = echoEncoder.encode(value);
|
|
100
133
|
if (bytes.length <= maxBytes)
|
|
101
134
|
return value;
|
|
@@ -106,6 +139,50 @@ export function boundedEchoText(value, maxBytes = MAX_ECHOED_BYTES) {
|
|
|
106
139
|
end--;
|
|
107
140
|
return `${echoDecoder.decode(bytes.slice(0, end))}…`;
|
|
108
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
|
+
}
|
|
109
186
|
/**
|
|
110
187
|
* Snapshot the caller's arguments after bounded schema-aware writeOnly omission,
|
|
111
188
|
* then apply the shared byte budget without clipping. Unresolved sensitivity
|
|
@@ -337,11 +414,19 @@ const RETRYABLE_BY_CODE = {
|
|
|
337
414
|
conflict: false,
|
|
338
415
|
resource_template_ambiguous: false,
|
|
339
416
|
resource_match_budget_exceeded: false,
|
|
417
|
+
unsupported_capability: false,
|
|
340
418
|
input_required_unsupported: false,
|
|
341
419
|
input_required_invalid: false,
|
|
342
420
|
input_required_limit: false,
|
|
343
421
|
connector_call_failed: false,
|
|
344
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
|
+
}
|
|
345
430
|
/** Runtime membership in the closed connector taxonomy, checked against its exhaustive table. */
|
|
346
431
|
export function isConnectorCallErrorCode(value) {
|
|
347
432
|
return typeof value === "string" && Object.hasOwn(RETRYABLE_BY_CODE, value);
|