@zackbart/connecta 0.37.1 → 0.39.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 +94 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/agent-routes.js +231 -0
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2516
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/native.js +27 -0
- package/dist/auth/oauth-grant.js +24 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +9 -3
- package/dist/authorization-handoff.js +11 -5
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-cache.js +130 -43
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +285 -48
- package/dist/catalog.js +18 -2
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +57 -25
- package/dist/config.js +30 -77
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +11 -7
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +671 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +469 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +544 -0
- package/dist/connectors/remote-mcp.js +61 -2898
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +4 -2
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +172 -125
- package/dist/executor-result.js +5 -2
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +63 -20
- package/dist/guest-types.d.ts +24 -10
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +7 -6
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +4 -3
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +33 -5
- package/dist/http/safety.js +9 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +45 -30
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +21 -6
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +50 -35
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +6 -2
- package/dist/oauth-handoff.js +2 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +136 -14
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -49
- package/dist/operator-ui/contract.d.ts +12 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +49 -53
- package/dist/providers/_shared/rest/value-safety.js +106 -28
- package/dist/providers/breeze/api.js +9 -8
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +7 -7
- package/dist/providers/ccb/api.js +3 -7
- package/dist/providers/ccb/index.js +7 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +10 -3
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +4 -14
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +23 -41
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +11 -16
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +12 -26
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +8 -4
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +5 -2
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +14 -23
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +19 -26
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +5 -8
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +2 -7
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +6 -4
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +11 -5
- package/dist/registry.js +46 -20
- package/dist/request-principal.js +1 -1
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +130 -61
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +34 -17
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +6 -5
- package/dist/routes/shared.d.ts +13 -8
- package/dist/routes/shared.js +15 -9
- package/dist/routes/ui-config.js +3 -3
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +17 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +8 -3
- package/dist/runtime/native-storage.js +9 -0
- package/dist/runtime/request-reads.js +182 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/runtime/storage.js +12 -0
- package/dist/sent-secrets.d.ts +1 -2
- package/dist/sent-secrets.js +3 -8
- package/dist/server.js +4 -4
- package/dist/skills.js +44 -54
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +69 -128
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +27 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +13 -14
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +33 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +18 -37
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +19 -55
- package/documentation/architecture.md +436 -515
- package/documentation/auth.md +229 -88
- package/documentation/code-mode.md +201 -94
- package/documentation/deploying.md +25 -43
- package/documentation/integrating.md +18 -19
- package/documentation/meta-tools.md +225 -51
- package/documentation/migration-0.33.md +10 -6
- package/documentation/migration-0.34.md +5 -5
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +20 -120
- package/documentation/operator-ui.md +108 -16
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +14 -16
- package/examples/worker/README.md +24 -118
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +9 -31
- package/guides/how-it-works.md +6 -4
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Bounded per-connector discovery health, shared across requests (INV-7).
|
|
2
|
+
//
|
|
3
|
+
// One hung or failing connector would otherwise add its whole probe deadline
|
|
4
|
+
// to every unscoped search, every time. A failed discovery leaves a record:
|
|
5
|
+
// the connector-health family's `discovery` key, a typed code, when it was
|
|
6
|
+
// seen, and when to try again (INV-6: never downstream text, tools or
|
|
7
|
+
// credentials). Unscoped search skips a connector whose record is current and
|
|
8
|
+
// reports it as unavailable in `catalogErrors` with `retryAfterMs`; it never
|
|
9
|
+
// serves tools from the failed or interrupted walk (INV-8). Scoped search,
|
|
10
|
+
// describe and calls still reach the connector.
|
|
11
|
+
//
|
|
12
|
+
// Records sit beside the catalog cache, in its store and owner partition:
|
|
13
|
+
// principal, subject, caller identity and pool, origin, the admitted endpoint,
|
|
14
|
+
// the connector's value-free described configuration and the package version
|
|
15
|
+
// (INV-4, INV-5). Each record
|
|
16
|
+
// also carries the connector's catalog invalidation generation as it was when
|
|
17
|
+
// written. A credential change, OAuth connect or disconnect, or operator
|
|
18
|
+
// invalidation rotates that generation, so every partition's record lapses at
|
|
19
|
+
// once, in every isolate. A record is a bounded observation: it grants and
|
|
20
|
+
// starts nothing (INV-10).
|
|
21
|
+
//
|
|
22
|
+
// After its retry time a record lingers, so exactly one request can claim the
|
|
23
|
+
// next probe by compare-and-set as it dispatches it; the others keep skipping
|
|
24
|
+
// until that probe settles or its lease lapses. A success, or an auth failure that proves the
|
|
25
|
+
// connector answers, clears the record. A fast failure is not recorded:
|
|
26
|
+
// asking again costs the next search nothing. Caller cancellation and a probe cut
|
|
27
|
+
// short by the discovery budget record nothing.
|
|
28
|
+
import { Effect } from "effect";
|
|
29
|
+
import { healthAddress } from "./connector-health.js";
|
|
30
|
+
export { bindDiscoveryEndpoint } from "./connector-health.js";
|
|
31
|
+
import { classificationCode, classifyCallError } from "./errors.js";
|
|
32
|
+
import { failureRecord, logFailure } from "./operator-record.js";
|
|
33
|
+
import { causeError } from "./runtime/run.js";
|
|
34
|
+
import { Storage } from "./runtime/services.js";
|
|
35
|
+
import { storageCompareAndSet, storageGetMany, storageSet } from "./runtime/storage.js";
|
|
36
|
+
import { connectorHealthKeys, CONNECTOR_HEALTH_LINGER_MS } from "./storage/keys.js";
|
|
37
|
+
/** How long unscoped search skips a connector after a failed discovery. */
|
|
38
|
+
const DISCOVERY_RETRY_MS = 60_000;
|
|
39
|
+
/**
|
|
40
|
+
* A retryable failure slower than this is recorded; a faster one is cheap to
|
|
41
|
+
* ask again. A timeout, or a downstream that names its own wait, always is.
|
|
42
|
+
*/
|
|
43
|
+
const SLOW_FAILURE_MS = 1_000;
|
|
44
|
+
function parseFact(raw) {
|
|
45
|
+
if (raw === null)
|
|
46
|
+
return undefined;
|
|
47
|
+
try {
|
|
48
|
+
const value = JSON.parse(raw);
|
|
49
|
+
const code = classificationCode(value.code);
|
|
50
|
+
if (value.v !== 1 ||
|
|
51
|
+
!code ||
|
|
52
|
+
!Number.isFinite(value.observedAt) ||
|
|
53
|
+
!Number.isFinite(value.retryAt) ||
|
|
54
|
+
!(value.generation === null || typeof value.generation === "string"))
|
|
55
|
+
return undefined;
|
|
56
|
+
return { v: 1, code, observedAt: value.observedAt, retryAt: value.retryAt, generation: value.generation };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function encodeFact(fact) {
|
|
63
|
+
return { raw: JSON.stringify(fact), expiresAtMs: Math.ceil(fact.retryAt + CONNECTOR_HEALTH_LINGER_MS) };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Each connector's verdict for this discovery, in `ids` order. With `skip`, a
|
|
67
|
+
* record of the current generation comes back as `skip`: current until its
|
|
68
|
+
* `retryAt`, claimable at dispatch once that has passed. Without `skip` every
|
|
69
|
+
* connector is probed and the slot only settles the record afterwards. A
|
|
70
|
+
* storage failure is a probe with no slot: health never fails discovery.
|
|
71
|
+
*/
|
|
72
|
+
export function readDiscoveryHealth(registry, ids, baseUrl, requestScope, options) {
|
|
73
|
+
return Effect.gen(function* () {
|
|
74
|
+
// Authored static catalogs never hang, so discovery keeps no health for them.
|
|
75
|
+
const addresses = yield* Effect.forEach(ids, (id) => registry.getConnector(id)?.staticTools
|
|
76
|
+
? Effect.succeed(undefined)
|
|
77
|
+
: healthAddress(registry, id, baseUrl, requestScope), {
|
|
78
|
+
concurrency: "unbounded",
|
|
79
|
+
});
|
|
80
|
+
const verdicts = ids.map(() => ({}));
|
|
81
|
+
// One getMany per store: every record and its generation in one round trip.
|
|
82
|
+
const stores = new Map();
|
|
83
|
+
addresses.forEach((address, index) => {
|
|
84
|
+
if (!address)
|
|
85
|
+
return;
|
|
86
|
+
const indexes = stores.get(address.storage) ?? [];
|
|
87
|
+
indexes.push(index);
|
|
88
|
+
stores.set(address.storage, indexes);
|
|
89
|
+
});
|
|
90
|
+
for (const [storage, indexes] of stores) {
|
|
91
|
+
const keys = indexes.flatMap((index) => [
|
|
92
|
+
connectorHealthKeys.discovery(ids[index], addresses[index].digest),
|
|
93
|
+
addresses[index].generationKey,
|
|
94
|
+
]);
|
|
95
|
+
const values = yield* storageGetMany(keys).pipe(Effect.provideService(Storage, storage), Effect.catchCause((cause) => Effect.sync(() => {
|
|
96
|
+
const first = addresses[indexes[0]];
|
|
97
|
+
logFailure(first.logger, "discovery health read failed", failureRecord({}, causeError(cause)));
|
|
98
|
+
return undefined;
|
|
99
|
+
})));
|
|
100
|
+
if (!values)
|
|
101
|
+
continue;
|
|
102
|
+
for (const [position, index] of indexes.entries()) {
|
|
103
|
+
const address = addresses[index];
|
|
104
|
+
const slot = {
|
|
105
|
+
storage,
|
|
106
|
+
key: keys[position * 2],
|
|
107
|
+
logger: address.logger,
|
|
108
|
+
generation: values[position * 2 + 1] ?? null,
|
|
109
|
+
raw: values[position * 2] ?? null,
|
|
110
|
+
};
|
|
111
|
+
const fact = parseFact(slot.raw);
|
|
112
|
+
verdicts[index] =
|
|
113
|
+
!options.skip || !fact || fact.generation !== slot.generation
|
|
114
|
+
? { slot }
|
|
115
|
+
: { slot, skip: { code: fact.code, retryAt: fact.retryAt } };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return verdicts;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Claim the probe of a record whose retry time has passed, as the probe is
|
|
123
|
+
* dispatched: one compare-and-set against the record this request read, with
|
|
124
|
+
* a lease to the absolute time `leaseUntil`, past the claimant's whole
|
|
125
|
+
* discovery budget. Others keep skipping until the lease lapses or the probe
|
|
126
|
+
* settles. False when another request holds it, or the record changed.
|
|
127
|
+
*/
|
|
128
|
+
export function claimDiscoveryProbe(slot, leaseUntil) {
|
|
129
|
+
return Effect.suspend(() => {
|
|
130
|
+
const fact = parseFact(slot.raw);
|
|
131
|
+
if (!fact)
|
|
132
|
+
return Effect.succeed(slot.raw === null);
|
|
133
|
+
const claim = encodeFact({ ...fact, retryAt: leaseUntil });
|
|
134
|
+
return storageCompareAndSet(slot.key, slot.raw, claim.raw, { expiresAtMs: claim.expiresAtMs }).pipe(Effect.provideService(Storage, slot.storage), Effect.map((claimed) => {
|
|
135
|
+
if (claimed)
|
|
136
|
+
slot.raw = claim.raw;
|
|
137
|
+
return claimed;
|
|
138
|
+
}), Effect.catchCause(() => Effect.succeed(false)));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const REACHABLE_CODES = new Set([
|
|
142
|
+
"auth_required",
|
|
143
|
+
"downstream_oauth_required",
|
|
144
|
+
"provider_permission_denied",
|
|
145
|
+
]);
|
|
146
|
+
/**
|
|
147
|
+
* Settle one probed connector's record. `ok` and an auth failure clear a
|
|
148
|
+
* record this request saw; a recordable failure writes a fresh one. A
|
|
149
|
+
* cancelled or budget-cut probe passes `undefined` and leaves the record as
|
|
150
|
+
* it is, so a claim it held lapses on its own.
|
|
151
|
+
*/
|
|
152
|
+
export function settleDiscoveryHealth(slot, outcome) {
|
|
153
|
+
return Effect.suspend(() => {
|
|
154
|
+
let fact;
|
|
155
|
+
if (!outcome.ok) {
|
|
156
|
+
const classified = outcome.timedOut ? undefined : classifyCallError(outcome.failure, "catalog_lookup_failed");
|
|
157
|
+
const code = outcome.timedOut ? "timeout" : classificationCode(classified.code);
|
|
158
|
+
if (classified && REACHABLE_CODES.has(classified.code)) {
|
|
159
|
+
fact = undefined;
|
|
160
|
+
}
|
|
161
|
+
else if (code &&
|
|
162
|
+
(outcome.timedOut ||
|
|
163
|
+
(classified.retryable && (outcome.elapsedMs >= SLOW_FAILURE_MS || classified.retryAfterMs !== undefined)))) {
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
const window = classified?.retryAfterMs !== undefined
|
|
166
|
+
? Math.min(DISCOVERY_RETRY_MS, Math.max(1_000, classified.retryAfterMs))
|
|
167
|
+
: DISCOVERY_RETRY_MS;
|
|
168
|
+
fact = { v: 1, code, observedAt: now, retryAt: now + window, generation: slot.generation };
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// A deterministic refusal is not a health fact, and a fast failure
|
|
172
|
+
// costs the next search nothing to ask again; leave the record.
|
|
173
|
+
return Effect.succeed({ slot });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!fact) {
|
|
177
|
+
if (slot.raw === null)
|
|
178
|
+
return Effect.succeed({ slot });
|
|
179
|
+
// Clear only the record this request saw: a newer failure stays.
|
|
180
|
+
return storageCompareAndSet(slot.key, slot.raw, null).pipe(Effect.provideService(Storage, slot.storage), Effect.map((cleared) => {
|
|
181
|
+
if (cleared)
|
|
182
|
+
slot.raw = null;
|
|
183
|
+
return { slot };
|
|
184
|
+
}), Effect.catchCause((cause) => Effect.sync(() => {
|
|
185
|
+
logFailure(slot.logger, "discovery health write failed", failureRecord({}, causeError(cause)));
|
|
186
|
+
return { slot };
|
|
187
|
+
})));
|
|
188
|
+
}
|
|
189
|
+
const encoded = encodeFact(fact);
|
|
190
|
+
const skip = { code: fact.code, retryAt: fact.retryAt };
|
|
191
|
+
return storageSet(slot.key, encoded.raw, { expiresAtMs: encoded.expiresAtMs }).pipe(Effect.provideService(Storage, slot.storage), Effect.map(() => {
|
|
192
|
+
slot.raw = encoded.raw;
|
|
193
|
+
return { slot, skip };
|
|
194
|
+
}), Effect.catchCause((cause) => Effect.sync(() => {
|
|
195
|
+
logFailure(slot.logger, "discovery health write failed", failureRecord({}, causeError(cause)));
|
|
196
|
+
return { slot, skip };
|
|
197
|
+
})));
|
|
198
|
+
});
|
|
199
|
+
}
|
package/dist/downstream-input.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toolError } from "./tool-error.js";
|
|
1
2
|
import { RequestScope, requestOwner } from "./runtime/request-scope.js";
|
|
2
3
|
import { Effect } from "effect";
|
|
3
4
|
import { upwardStep } from "./upward.js";
|
|
@@ -129,8 +130,7 @@ function failure(code, message, write = false) {
|
|
|
129
130
|
writeOutcome: "unknown",
|
|
130
131
|
}
|
|
131
132
|
: { code, message, retryable: false };
|
|
132
|
-
|
|
133
|
-
return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent, isError: true };
|
|
133
|
+
return toolError(error, { ok: false });
|
|
134
134
|
}
|
|
135
135
|
/** MCP-only continuation coordination. No downstream prompt enters operator records. */
|
|
136
136
|
export class DownstreamElicitation {
|
|
@@ -210,6 +210,7 @@ export class DownstreamElicitation {
|
|
|
210
210
|
kind: "downstream",
|
|
211
211
|
principal,
|
|
212
212
|
endpoint,
|
|
213
|
+
pool: self.options.registry.resultIdentity().pool,
|
|
213
214
|
connector: pending.connector,
|
|
214
215
|
address: String(args.address),
|
|
215
216
|
target: pending.address,
|
|
@@ -370,6 +371,7 @@ export class DownstreamElicitation {
|
|
|
370
371
|
state.kind !== "downstream" ||
|
|
371
372
|
state.principal !== principal ||
|
|
372
373
|
state.endpoint !== endpoint ||
|
|
374
|
+
state.pool !== registry.resultIdentity().pool ||
|
|
373
375
|
state.connector !== wrapper.connector ||
|
|
374
376
|
typeof state.address !== "string" ||
|
|
375
377
|
typeof state.target !== "string" ||
|
package/dist/errors.d.ts
CHANGED
|
@@ -36,21 +36,51 @@ export type ConnectorCallErrorCode = "timeout" | "auth_required" | "downstream_o
|
|
|
36
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";
|
|
37
37
|
/** `value` when it is a code connecta assigns, else undefined. */
|
|
38
38
|
export declare function classificationCode(value: unknown): ClassificationCode | undefined;
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* One bounded fix for an argument. `expected` states the actual bound, allowed
|
|
41
|
+
* values or shape; `received` is the submitted JSON type, or a submitted number
|
|
42
|
+
* when its schema position is not writeOnly. Never a submitted string.
|
|
43
|
+
*/
|
|
40
44
|
export interface ArgumentValidationIssue {
|
|
41
45
|
/** JSON Pointer into the submitted arguments; "/" means the root value. */
|
|
42
46
|
path: string;
|
|
43
|
-
/**
|
|
44
|
-
code: string;
|
|
45
|
-
/** Expected shape only — never the submitted value. */
|
|
47
|
+
/** What the argument must be, e.g. `at least 5` or `one of "fast", "slow"`. */
|
|
46
48
|
expected: string;
|
|
49
|
+
/** What arrived, e.g. `string`, `missing` or `4`. */
|
|
50
|
+
received?: string;
|
|
51
|
+
/**
|
|
52
|
+
* @deprecated Accepted from custom connectors for compatibility and no longer
|
|
53
|
+
* returned; `expected` carries the constraint.
|
|
54
|
+
*/
|
|
55
|
+
code?: string;
|
|
47
56
|
}
|
|
57
|
+
/** A conditional requirement read from the published schema. */
|
|
58
|
+
export interface ArgumentConditionalRequirement {
|
|
59
|
+
path: string;
|
|
60
|
+
condition: unknown;
|
|
61
|
+
required: string[];
|
|
62
|
+
}
|
|
63
|
+
/** Every bounded argument finding and one corrected example call. */
|
|
48
64
|
export interface ArgumentValidationDetails {
|
|
49
65
|
issues: ArgumentValidationIssue[];
|
|
50
|
-
/**
|
|
66
|
+
/** The tool's top-level argument names. */
|
|
67
|
+
acceptedKeys?: string[];
|
|
68
|
+
conditionalRequirements?: ArgumentConditionalRequirement[];
|
|
69
|
+
/**
|
|
70
|
+
* One complete corrected call. Placeholder values look like `<owner>/<repo>`;
|
|
71
|
+
* an example is omitted rather than guessed.
|
|
72
|
+
*/
|
|
73
|
+
example?: unknown;
|
|
74
|
+
/** Why no example accompanies these findings. */
|
|
75
|
+
exampleUnavailable?: string;
|
|
76
|
+
/** More findings or detail existed but were omitted from the bounded response. */
|
|
51
77
|
truncated?: true;
|
|
52
78
|
}
|
|
53
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* @deprecated Pass these facts in `validation`, which connecta now returns as
|
|
81
|
+
* the single argument-recovery envelope. A `repair` passed to
|
|
82
|
+
* {@link ConnectorCallError} is folded into `validation`.
|
|
83
|
+
*/
|
|
54
84
|
export interface ArgumentRepairDetails {
|
|
55
85
|
acceptedKeys?: string[];
|
|
56
86
|
issues: Array<{
|
|
@@ -60,17 +90,17 @@ export interface ArgumentRepairDetails {
|
|
|
60
90
|
enumValues?: unknown[];
|
|
61
91
|
bounds?: Record<string, number>;
|
|
62
92
|
}>;
|
|
63
|
-
conditionalRequirements?:
|
|
64
|
-
path: string;
|
|
65
|
-
condition: unknown;
|
|
66
|
-
required: string[];
|
|
67
|
-
}>;
|
|
68
|
-
/** Synthesized from the schema and checked by the same validator. */
|
|
93
|
+
conditionalRequirements?: ArgumentConditionalRequirement[];
|
|
69
94
|
example?: unknown;
|
|
70
95
|
exampleUnavailable?: string;
|
|
71
96
|
truncated?: true;
|
|
72
97
|
}
|
|
73
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Up to eight findings per refusal: enough for every argument of a maintained
|
|
100
|
+
* tool (the widest declare fewer than eight top-level arguments) while each
|
|
101
|
+
* finding stays a few hundred bytes inside the 4 KiB envelope below.
|
|
102
|
+
*/
|
|
103
|
+
export declare const MAX_ARGUMENT_VALIDATION_ISSUES = 8;
|
|
74
104
|
/**
|
|
75
105
|
* Clamp a string to a UTF-8 byte budget, appending `…` when it clipped.
|
|
76
106
|
* Short strings — the common case, and the one that has to stay exact — are
|
|
@@ -87,6 +117,12 @@ export declare function echoedCallArgs(args: unknown, schema?: JsonSchema): {
|
|
|
87
117
|
args?: unknown;
|
|
88
118
|
argsRedacted?: true;
|
|
89
119
|
};
|
|
120
|
+
/** A JSON Schema bound keyword as an agent-readable constraint with its number. */
|
|
121
|
+
export declare function boundText(keyword: string, bound: unknown): string | undefined;
|
|
122
|
+
/** Up to twelve items, then a count. */
|
|
123
|
+
export declare function listed(items: readonly string[]): string;
|
|
124
|
+
/** One agent-readable clause per finding, for the error message. */
|
|
125
|
+
export declare function issueSummary(issues: readonly ArgumentValidationIssue[]): string;
|
|
90
126
|
/** Optional transport diagnostics; never a URL path or raw runtime message. */
|
|
91
127
|
interface UnavailableDetails {
|
|
92
128
|
/**
|
|
@@ -169,9 +205,11 @@ export declare class ConnectorCallError extends Error {
|
|
|
169
205
|
* of the wire format is `classifyCallError`'s job, not this constructor's.
|
|
170
206
|
*/
|
|
171
207
|
readonly retryAfterMs: number | undefined;
|
|
172
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* Every bounded argument finding for `invalid_args`, with one corrected
|
|
210
|
+
* example call when it can be built without guessing.
|
|
211
|
+
*/
|
|
173
212
|
readonly validation: ArgumentValidationDetails | undefined;
|
|
174
|
-
readonly repair: ArgumentRepairDetails | undefined;
|
|
175
213
|
/**
|
|
176
214
|
* Sanitized transport diagnostics, for `unavailable` and for a write whose
|
|
177
215
|
* outcome is unknown (a timeout's errno, an abort's class).
|
|
@@ -195,6 +233,7 @@ export declare class ConnectorCallError extends Error {
|
|
|
195
233
|
retryAfterMs?: number;
|
|
196
234
|
cause?: unknown;
|
|
197
235
|
validation?: ArgumentValidationDetails;
|
|
236
|
+
/** @deprecated Pass these facts in `validation`; they are folded into it. */
|
|
198
237
|
repair?: ArgumentRepairDetails;
|
|
199
238
|
details?: UnavailableDetails;
|
|
200
239
|
current?: Readonly<Record<string, number>>;
|
|
@@ -227,9 +266,8 @@ export interface CallErrorDetails extends Partial<AuthorizationHandoff> {
|
|
|
227
266
|
retryable: boolean;
|
|
228
267
|
/** Connector-known wait window; see {@link ConnectorCallError.retryAfterMs}. */
|
|
229
268
|
retryAfterMs?: number;
|
|
230
|
-
/**
|
|
269
|
+
/** Every bounded argument finding and one corrected example call. */
|
|
231
270
|
validation?: ArgumentValidationDetails;
|
|
232
|
-
repair?: ArgumentRepairDetails;
|
|
233
271
|
/** On a `conflict`: where each thing the write touched stands now. */
|
|
234
272
|
current?: Readonly<Record<string, number>>;
|
|
235
273
|
/** Connector whose failed operation needs recovery. */
|
package/dist/errors.js
CHANGED
|
@@ -43,28 +43,16 @@ export function classificationCode(value) {
|
|
|
43
43
|
? value
|
|
44
44
|
: undefined;
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return new TextEncoder().encode(text).length <= 4096
|
|
53
|
-
? JSON.parse(text)
|
|
54
|
-
: {
|
|
55
|
-
issues: [],
|
|
56
|
-
truncated: true,
|
|
57
|
-
exampleUnavailable: "Repair detail exceeds the response budget. Inspect the published inputSchema.",
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
export const MAX_ARGUMENT_VALIDATION_ISSUES = 3;
|
|
46
|
+
/**
|
|
47
|
+
* Up to eight findings per refusal: enough for every argument of a maintained
|
|
48
|
+
* tool (the widest declare fewer than eight top-level arguments) while each
|
|
49
|
+
* finding stays a few hundred bytes inside the 4 KiB envelope below.
|
|
50
|
+
*/
|
|
51
|
+
export const MAX_ARGUMENT_VALIDATION_ISSUES = 8;
|
|
65
52
|
const MAX_ARGUMENT_ISSUE_PATH_CHARS = 256;
|
|
66
|
-
const
|
|
67
|
-
const
|
|
53
|
+
const MAX_ARGUMENT_ISSUE_EXPECTED_CHARS = 256;
|
|
54
|
+
const MAX_ARGUMENT_ISSUE_RECEIVED_CHARS = 64;
|
|
55
|
+
const MAX_ARGUMENT_VALIDATION_BYTES = 4096;
|
|
68
56
|
function boundedIssueText(value, maxChars) {
|
|
69
57
|
if (value.length <= maxChars)
|
|
70
58
|
return { value, truncated: false };
|
|
@@ -148,25 +136,91 @@ export function echoedCallArgs(args, schema) {
|
|
|
148
136
|
? { argsRedacted: true }
|
|
149
137
|
: {};
|
|
150
138
|
}
|
|
151
|
-
function
|
|
152
|
-
|
|
139
|
+
function plainJson(value) {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(JSON.stringify(value));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
153
144
|
return undefined;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const BOUND_WORDS = {
|
|
148
|
+
minimum: "at least",
|
|
149
|
+
maximum: "at most",
|
|
150
|
+
exclusiveMinimum: "greater than",
|
|
151
|
+
exclusiveMaximum: "less than",
|
|
152
|
+
multipleOf: "a multiple of",
|
|
153
|
+
};
|
|
154
|
+
/** A JSON Schema bound keyword as an agent-readable constraint with its number. */
|
|
155
|
+
export function boundText(keyword, bound) {
|
|
156
|
+
const [, side, unit] = /^(min|max)(Length|Items|Properties)$/.exec(keyword) ?? [];
|
|
157
|
+
const words = side ? `at ${side === "min" ? "least" : "most"}` : BOUND_WORDS[keyword];
|
|
158
|
+
return typeof bound === "number" && words
|
|
159
|
+
? `${words} ${bound}${unit ? ` ${unit === "Length" ? "characters" : unit.toLowerCase()}` : ""}`
|
|
160
|
+
: undefined;
|
|
161
|
+
}
|
|
162
|
+
/** Up to twelve items, then a count. */
|
|
163
|
+
export function listed(items) {
|
|
164
|
+
return items.length > 12 ? `${items.slice(0, 12).join(", ")}, … (${items.length} in all)` : items.join(", ");
|
|
165
|
+
}
|
|
166
|
+
/** One agent-readable clause per finding, for the error message. */
|
|
167
|
+
export function issueSummary(issues) {
|
|
168
|
+
return issues
|
|
169
|
+
.map((issue) => `${issue.path}: expected ${issue.expected}${issue.received ? `, received ${issue.received}` : ""}`)
|
|
170
|
+
.join("; ");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Bound argument findings into the one recovery envelope. A legacy `repair`
|
|
174
|
+
* contributes its received types and schema facts. The envelope is capped at
|
|
175
|
+
* 4 KiB: optional detail goes first, then the example, then trailing findings,
|
|
176
|
+
* so what remains is always whole.
|
|
177
|
+
*/
|
|
178
|
+
function boundedValidation(details, repair) {
|
|
179
|
+
if (!details && !repair)
|
|
180
|
+
return undefined;
|
|
181
|
+
const source = { ...repair, ...details };
|
|
182
|
+
const all = (details?.issues ?? []).filter((issue, index, list) => typeof issue?.path === "string" &&
|
|
183
|
+
typeof issue.expected === "string" &&
|
|
184
|
+
list.findIndex((other) => other.path === issue.path && other.expected === issue.expected) === index);
|
|
185
|
+
let truncated = source.truncated === true || all.length > MAX_ARGUMENT_VALIDATION_ISSUES;
|
|
186
|
+
const clip = (value, max) => {
|
|
187
|
+
const bounded = boundedIssueText(value, max);
|
|
188
|
+
truncated ||= bounded.truncated;
|
|
189
|
+
return bounded.value;
|
|
190
|
+
};
|
|
191
|
+
const issues = all.slice(0, MAX_ARGUMENT_VALIDATION_ISSUES).map(({ path, expected, received }) => {
|
|
192
|
+
received ??= repair?.issues?.find((legacy) => legacy?.path === path)?.receivedType;
|
|
160
193
|
return {
|
|
161
|
-
path: path
|
|
162
|
-
|
|
163
|
-
|
|
194
|
+
path: clip(path, MAX_ARGUMENT_ISSUE_PATH_CHARS),
|
|
195
|
+
expected: clip(expected, MAX_ARGUMENT_ISSUE_EXPECTED_CHARS),
|
|
196
|
+
...(typeof received === "string" ? { received: clip(received, MAX_ARGUMENT_ISSUE_RECEIVED_CHARS) } : {}),
|
|
164
197
|
};
|
|
165
198
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
199
|
+
const result = { issues };
|
|
200
|
+
if (Array.isArray(source.acceptedKeys) && source.acceptedKeys.length)
|
|
201
|
+
result.acceptedKeys = source.acceptedKeys.filter((key) => typeof key === "string");
|
|
202
|
+
if (Array.isArray(source.conditionalRequirements) && source.conditionalRequirements.length)
|
|
203
|
+
result.conditionalRequirements = plainJson(source.conditionalRequirements);
|
|
204
|
+
const example = Object.hasOwn(source, "example") ? plainJson(source.example) : undefined;
|
|
205
|
+
if (example !== undefined)
|
|
206
|
+
result.example = example;
|
|
207
|
+
else if (typeof source.exampleUnavailable === "string")
|
|
208
|
+
result.exampleUnavailable = clip(source.exampleUnavailable, MAX_ARGUMENT_ISSUE_EXPECTED_CHARS);
|
|
209
|
+
const fits = () => echoEncoder.encode(JSON.stringify(result)).length <= MAX_ARGUMENT_VALIDATION_BYTES - 20;
|
|
210
|
+
if (!fits()) {
|
|
211
|
+
truncated = true;
|
|
212
|
+
delete result.conditionalRequirements;
|
|
213
|
+
delete result.acceptedKeys;
|
|
214
|
+
if (result.example !== undefined && !fits()) {
|
|
215
|
+
delete result.example;
|
|
216
|
+
result.exampleUnavailable = "The example exceeds the response budget. Inspect the published inputSchema.";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
while (!fits() && issues.length > 1)
|
|
220
|
+
issues.pop();
|
|
221
|
+
if (truncated)
|
|
222
|
+
result.truncated = true;
|
|
223
|
+
return result;
|
|
170
224
|
}
|
|
171
225
|
/** The network errnos `networkErrorCode` reports, and `timeout`. */
|
|
172
226
|
export const NETWORK_ERROR_CODES = new Set([
|
|
@@ -344,9 +398,11 @@ export class ConnectorCallError extends Error {
|
|
|
344
398
|
* of the wire format is `classifyCallError`'s job, not this constructor's.
|
|
345
399
|
*/
|
|
346
400
|
retryAfterMs;
|
|
347
|
-
/**
|
|
401
|
+
/**
|
|
402
|
+
* Every bounded argument finding for `invalid_args`, with one corrected
|
|
403
|
+
* example call when it can be built without guessing.
|
|
404
|
+
*/
|
|
348
405
|
validation;
|
|
349
|
-
repair;
|
|
350
406
|
/**
|
|
351
407
|
* Sanitized transport diagnostics, for `unavailable` and for a write whose
|
|
352
408
|
* outcome is unknown (a timeout's errno, an abort's class).
|
|
@@ -373,8 +429,7 @@ export class ConnectorCallError extends Error {
|
|
|
373
429
|
this.retryAfterMs = normalizeRetryAfterMs(opts.retryAfterMs);
|
|
374
430
|
this.details =
|
|
375
431
|
code === "unavailable" || opts.writeOutcome === "unknown" ? sanitizedUnavailableDetails(opts.details) : undefined;
|
|
376
|
-
this.validation = code === "invalid_args" ? boundedValidation(opts.validation) : undefined;
|
|
377
|
-
this.repair = code === "invalid_args" ? boundedRepair(opts.repair) : undefined;
|
|
432
|
+
this.validation = code === "invalid_args" ? boundedValidation(opts.validation, opts.repair) : undefined;
|
|
378
433
|
this.current = code === "conflict" ? boundedCurrent(opts.current) : undefined;
|
|
379
434
|
this.writeOutcome = opts.writeOutcome === "unknown" ? "unknown" : undefined;
|
|
380
435
|
}
|
|
@@ -482,7 +537,6 @@ export function classifyWithoutAdvice(err, fallbackCode = "connector_call_failed
|
|
|
482
537
|
retryable: err.retryable,
|
|
483
538
|
...(err.retryAfterMs !== undefined ? { retryAfterMs: err.retryAfterMs } : {}),
|
|
484
539
|
...(err.validation ? { validation: err.validation } : {}),
|
|
485
|
-
...(err.repair ? { repair: err.repair } : {}),
|
|
486
540
|
...(err.details ? { details: err.details } : {}),
|
|
487
541
|
...(err.current ? { current: err.current } : {}),
|
|
488
542
|
...(err.writeOutcome ? { writeOutcome: err.writeOutcome } : {}),
|