@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.
Files changed (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. 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
+ }
@@ -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
- const structuredContent = { ok: false, error };
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
- /** One bounded, payload-free explanation of an input-schema mismatch. */
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
- /** JSON Schema keyword that rejected the argument. */
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
- /** More findings existed but were omitted from the bounded response. */
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
- /** Agent-only facts from the published schema, never operator record fields. */
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?: Array<{
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
- export declare const MAX_ARGUMENT_VALIDATION_ISSUES = 3;
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
- /** Bounded schema findings for `invalid_args`; never submitted values. */
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
- /** Bounded input-schema findings; paths and expectations, never values. */
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
- /** Drop oversized detail whole; clipping keys or examples changes their meaning. */
47
- function boundedRepair(details) {
48
- if (!details)
49
- return undefined;
50
- try {
51
- const text = JSON.stringify(details);
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 MAX_ARGUMENT_ISSUE_CODE_CHARS = 64;
67
- const MAX_ARGUMENT_ISSUE_EXPECTED_CHARS = 128;
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 boundedValidation(details) {
152
- if (!details)
139
+ function plainJson(value) {
140
+ try {
141
+ return JSON.parse(JSON.stringify(value));
142
+ }
143
+ catch {
153
144
  return undefined;
154
- let truncated = details.truncated === true || details.issues.length > MAX_ARGUMENT_VALIDATION_ISSUES;
155
- const issues = details.issues.slice(0, MAX_ARGUMENT_VALIDATION_ISSUES).map((issue) => {
156
- const path = boundedIssueText(issue.path, MAX_ARGUMENT_ISSUE_PATH_CHARS);
157
- const code = boundedIssueText(issue.code, MAX_ARGUMENT_ISSUE_CODE_CHARS);
158
- const expected = boundedIssueText(issue.expected, MAX_ARGUMENT_ISSUE_EXPECTED_CHARS);
159
- truncated ||= path.truncated || code.truncated || expected.truncated;
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.value,
162
- code: code.value,
163
- expected: expected.value,
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
- return {
167
- issues,
168
- ...(truncated ? { truncated: true } : {}),
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
- /** Bounded schema findings for `invalid_args`; never submitted values. */
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 } : {}),