@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
@@ -8,6 +8,7 @@
8
8
  // entries (8 at most, 16 KiB of JSON at most, with an explicit continuation
9
9
  // cursor) and details return at most 32 KiB, cutting only optional parts and
10
10
  // listing them in `truncated`; a required parameter or body field is never cut.
11
+ import { Routes } from "../agent-routes.js";
11
12
  import { Effect, Schema } from "effect";
12
13
  import { lexicalSearchContent } from "../catalog.js";
13
14
  import { registerOperationVocabulary } from "../operation-vocabulary.js";
@@ -18,7 +19,8 @@ import { providerFailureMessage } from "../write-outcome.js";
18
19
  import { redactionAltered } from "../redaction-marker.js";
19
20
  import { refuseWithheldValues } from "../providers/_shared/rest/value-safety.js";
20
21
  import { Refusal, toConnectorCallError } from "./errors.js";
21
- import { fieldsInput, fieldsProblem, selectFields } from "./fields.js";
22
+ import { boundWarnings, fieldsDescription, fieldsInput, fieldsMisses, fieldsProblem, selectFields } from "./fields.js";
23
+ import { RESPONSE_BUDGET, responseContract } from "./hatch-response.js";
22
24
  import { Integer, isMaxChars, isMinChars } from "./json-schema.js";
23
25
  import { boundCursor, StandardPage } from "./paging.js";
24
26
  import { Classification, Dispatches, IdempotencyKey, RequestSecrets, Signal } from "./services.js";
@@ -48,7 +50,6 @@ const Envelope = Schema.Struct({
48
50
  status: Integer.annotate({ description: "HTTP status." }),
49
51
  data: Schema.Unknown.pipe(Schema.annotateKey({ description: "Response body after value safety and the connection's privacy filtering." })),
50
52
  });
51
- const HATCH_FIELDS = fieldsInput("Dot paths to keep in data, applied to each item of an array, after value safety and privacy filtering.");
52
53
  function invalid(message) {
53
54
  throw new ConnectorCallError("invalid_args", message);
54
55
  }
@@ -88,7 +89,7 @@ function parameters(values) {
88
89
  if (typeof item !== "string" &&
89
90
  typeof item !== "boolean" &&
90
91
  !(typeof item === "number" && Number.isFinite(item))) {
91
- invalid(`Parameter ${name} takes a string, finite number, boolean, or a list of them.`);
92
+ invalid("Each query parameter takes a string, finite number, boolean, or a list of them.");
92
93
  }
93
94
  return [name, String(item)];
94
95
  });
@@ -160,9 +161,6 @@ export function hatch(options) {
160
161
  ];
161
162
  }));
162
163
  const Headers = Object.keys(headerFields).length ? Schema.Struct(headerFields) : undefined;
163
- const Fields = options.fieldsRoot
164
- ? fieldsInput(`Dot paths to keep in data. Envelope fields begin with ${options.fieldsRoot}, e.g. ["${options.fieldsRoot}.id"] or ["${options.fieldsRoot}[].id"], where declared.`)
165
- : HATCH_FIELDS;
166
164
  const Body = options.textBodies
167
165
  ? Schema.Union([ObjectValue, Schema.Array(Schema.Unknown), Schema.String])
168
166
  : undefined;
@@ -191,6 +189,22 @@ export function hatch(options) {
191
189
  if (!known(key) || !tool.trim())
192
190
  throw new Error(`Hatch named tool for ${key} needs an indexed operation.`);
193
191
  }
192
+ // Paths start at the vendor body. The example is a root some operation the
193
+ // tool calls declares: the envelope root, else `id`, else the first declared.
194
+ const fieldsFor = (writes) => {
195
+ const preferred = options.fieldsRoot ?? "id";
196
+ let first;
197
+ for (const [key, verdict] of verdicts) {
198
+ if (verdict === "refuse" || (verdict === "read") === writes)
199
+ continue;
200
+ const roots = index.responseRoots(operations.get(key));
201
+ if (roots?.has(preferred))
202
+ return fieldsInput(fieldsNote(preferred));
203
+ first ??= roots && [...roots][0];
204
+ }
205
+ return fieldsInput(fieldsNote(first ?? preferred));
206
+ };
207
+ const fieldsNote = (example) => fieldsDescription("data", example, `Copy declared paths from ${name}_api_details response.fields. Applied after value safety and privacy filtering.`);
194
208
  const verdictOf = (op) => verdicts.get(`${op.method} ${op.path}`) ?? "refuse";
195
209
  const refusalOf = (op) => connection.safety.refusal?.({ method: op.method, template: op.path }) ?? options.binding?.refusal?.(op);
196
210
  const discoveryRefusals = new Map();
@@ -244,13 +258,9 @@ export function hatch(options) {
244
258
  truncated: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Dot paths cut for size, including the data prefix." })),
245
259
  }
246
260
  : {}),
247
- ...(options.binding?.warnings
248
- ? {
249
- warnings: Schema.optionalKey(Schema.Array(Schema.String).annotate({
250
- description: "Reviewed warnings preserved after fields selection.",
251
- })),
252
- }
253
- : {}),
261
+ warnings: Schema.optionalKey(Schema.Array(Schema.String).annotate({
262
+ description: `${options.binding?.warnings ? "Reviewed warnings preserved after fields selection, and " : ""}fields entries that matched nothing in this nonempty response, with the properties declared where matching stopped. At most 10 warnings of 512 characters, then an omitted count.`,
263
+ })),
254
264
  });
255
265
  const hasTotals = [...pages.values()].some((adapters) => adapters.some((adapter) => adapter.total !== undefined));
256
266
  const envelope = pages.size
@@ -637,6 +647,29 @@ export function hatch(options) {
637
647
  }));
638
648
  const searchTool = tool("search");
639
649
  const detailsTool = tool("details");
650
+ // The response contract is fixed for a connection, so each operation's is built once.
651
+ const responses = new Map();
652
+ const responseOf = (op) => {
653
+ if (!responses.has(op.row))
654
+ responses.set(op.row, responseContract(index, op, connection.safety));
655
+ return responses.get(op.row);
656
+ };
657
+ const Response = Schema.optionalKey(Schema.Struct({
658
+ anchor: Schema.Literals(["data", "data[]"]).annotate({
659
+ description: "Paths start at the read result's data; data[] means each item of a list body.",
660
+ }),
661
+ fields: Schema.Array(Schema.String).annotate({
662
+ description: "Selectable declared success-response paths, valid as fields inputs unchanged. Not a readability promise: value safety may still withhold or redact any value at read time.",
663
+ }),
664
+ withheld: Schema.optionalKey(Schema.Array(Schema.String).annotate({
665
+ description: "Best-effort hint: listed paths value safety always withholds or redacts. An unlisted path may still be withheld or redacted.",
666
+ })),
667
+ omitted: Schema.optionalKey(Integer.annotate({
668
+ description: `Declared paths not listed: deeper ones past ${RESPONSE_BUDGET.paths} paths or ${RESPONSE_BUDGET.chars} chars, or over ${RESPONSE_BUDGET.pathChars} chars.`,
669
+ })),
670
+ }).annotate({
671
+ description: "Where declared response values are, not whether they are readable. Absent when the operation declares no response properties or is refused.",
672
+ }));
640
673
  const tools = [
641
674
  read(connection, {
642
675
  name: searchTool,
@@ -756,7 +789,7 @@ export function hatch(options) {
756
789
  }),
757
790
  read(connection, {
758
791
  name: detailsTool,
759
- description: `Read one ${title} API operation's request contract and refusal, including methods outside the call schemas. Reads no account data.`,
792
+ description: `Read one ${title} API operation's request contract, response fields and refusal, including methods outside the call schemas. Reads no account data.`,
760
793
  input: Schema.Struct({
761
794
  method: Method.annotate({ description: "The operation's HTTP method." }),
762
795
  path: Path.annotate({ description: "The path template from search, or a concrete path." }),
@@ -776,6 +809,7 @@ export function hatch(options) {
776
809
  body: Schema.optionalKey(ObjectValue.annotate({
777
810
  description: "Body content type and schema. When paged: this page's properties, the required names it delivers or chunks, and the schema's other keywords on the page that carries them.",
778
811
  })),
812
+ response: Response,
779
813
  verdict: Schema.Literals(["read", "write", "destructive", "refuse"]),
780
814
  ...Routing,
781
815
  revision: Schema.String,
@@ -798,8 +832,12 @@ export function hatch(options) {
798
832
  const refusal = discoveryRefusal(op);
799
833
  return { op, contract: { ...index.contract(op, args.param), ...(refusal ? { refusal } : {}) } };
800
834
  });
801
- const tail = { verdict: discoveryVerdictOf(op), ...route(op), revision: index.revision };
802
- const whole = { ...contract, ...tail };
835
+ const verdict = discoveryVerdictOf(op);
836
+ const tail = { verdict, ...route(op), revision: index.revision };
837
+ // The first page of a whole-operation answer carries the response contract; param pages do not.
838
+ const response = args.param === undefined && args.cursor === undefined && verdict !== "refuse" ? responseOf(op) : undefined;
839
+ const head = response ? { response } : {};
840
+ const whole = { ...contract, ...head, ...tail };
803
841
  if (args.cursor === undefined && bytes(whole) <= DETAILS_BUDGET_BYTES)
804
842
  return whole;
805
843
  // Bounded pages: every parameter and body property arrives whole on some page, and a
@@ -874,6 +912,7 @@ export function hatch(options) {
874
912
  },
875
913
  },
876
914
  }),
915
+ ...head,
877
916
  ...tail,
878
917
  ...(chunk ? { chunk } : {}),
879
918
  ...(truncated.length ? { truncated } : {}),
@@ -1105,7 +1144,7 @@ export function hatch(options) {
1105
1144
  const roots = index.responseRoots(op);
1106
1145
  if (!roots)
1107
1146
  invalid(`${method} ${op.path} declares no response properties, so fields cannot narrow it.`);
1108
- const problem = fieldsProblem(args.fields, roots, "data");
1147
+ const problem = fieldsProblem(args.fields, roots, "data", `Examples are withheld for API operations; read ${tool("details")} for the contract.`);
1109
1148
  if (problem) {
1110
1149
  if (options.fieldsRoot && roots.has(options.fieldsRoot))
1111
1150
  invalid(`${problem.message} Use fields:["${options.fieldsRoot}.id"] or fields:["${options.fieldsRoot}[].id"] for declared record or list fields.`);
@@ -1141,6 +1180,7 @@ export function hatch(options) {
1141
1180
  dispatches,
1142
1181
  withholds: connection.safety.withholdsErrors,
1143
1182
  signal: yield* Signal,
1183
+ routes: yield* Routes,
1144
1184
  });
1145
1185
  const diagnostic = providerFailureMessage(error, `${error.message} ${idempotency.header}: ${key}.`);
1146
1186
  return yield* Effect.fail(new Refusal({
@@ -1177,16 +1217,24 @@ export function hatch(options) {
1177
1217
  in: adapter.in,
1178
1218
  };
1179
1219
  }
1220
+ const answered = answer.data === undefined ? null : answer.data;
1221
+ // Roots were checked before dispatch; the pinned index cannot say whether a
1222
+ // deeper level is closed, so a nested miss is reported, never refused.
1223
+ const warnings = boundWarnings([
1224
+ ...(options.binding?.warnings && "warnings" in answer ? (answer.warnings ?? []) : []),
1225
+ ...fieldsMisses(answered, args.fields, index
1226
+ .fields(op)
1227
+ .filter((field) => field.in === "response")
1228
+ .map((field) => field.path)),
1229
+ ]);
1180
1230
  return {
1181
1231
  status: answer.status,
1182
- data: selectFields(answer.data === undefined ? null : answer.data, args.fields),
1232
+ data: selectFields(answered, args.fields),
1183
1233
  ...(page ? { page } : {}),
1184
1234
  ...(options.binding?.truncated && "truncated" in answer && answer.truncated?.length
1185
1235
  ? { truncated: answer.truncated }
1186
1236
  : {}),
1187
- ...(options.binding?.warnings && "warnings" in answer && answer.warnings?.length
1188
- ? { warnings: answer.warnings }
1189
- : {}),
1237
+ ...(warnings.length ? { warnings } : {}),
1190
1238
  ...(key ? { idempotencyKey: key } : {}),
1191
1239
  };
1192
1240
  });
@@ -1208,7 +1256,7 @@ export function hatch(options) {
1208
1256
  if (nonGetReads.length)
1209
1257
  readFields["body"] = Schema.optionalKey((Body ?? ObjectValue).annotate({ description: "Body for a reviewed read-only POST." }));
1210
1258
  readFields["query"] = Schema.optionalKey(Query);
1211
- readFields["fields"] = Schema.optionalKey(Fields);
1259
+ readFields["fields"] = Schema.optionalKey(fieldsFor(false));
1212
1260
  if (Headers)
1213
1261
  readFields["headers"] = Schema.optionalKey(Headers);
1214
1262
  if (options.pageCursor === "argument")
@@ -1235,7 +1283,7 @@ export function hatch(options) {
1235
1283
  body: Schema.optionalKey((Body ?? Schema.Union([ObjectValue, Schema.Array(Schema.Unknown)])).annotate({
1236
1284
  description: "Request body as JSON fields, named as details lists them; form operations are encoded for you.",
1237
1285
  })),
1238
- fields: Schema.optionalKey(Fields),
1286
+ fields: Schema.optionalKey(fieldsFor(true)),
1239
1287
  };
1240
1288
  if (Headers)
1241
1289
  writeFields["headers"] = Schema.optionalKey(Headers);
@@ -1265,6 +1313,22 @@ export function hatch(options) {
1265
1313
  run: (args) => run("write", args),
1266
1314
  }));
1267
1315
  }
1316
+ // A read search may hand back as a ready call: GET by default, no path
1317
+ // template, no required parameter or body, and served by the read tool
1318
+ // itself rather than a named tool. Anything an agent must choose or fill in
1319
+ // stays behind search and details.
1320
+ const readTool = tool("read");
1321
+ const ready = (op) => {
1322
+ if (op.method !== "GET" || defaultRead !== "GET" || op.path.includes("{"))
1323
+ return undefined;
1324
+ const routed = route(op);
1325
+ if (routed.tool !== readTool || routed.named)
1326
+ return undefined;
1327
+ const contract = index.contract(op);
1328
+ return contract.parameters.some((param) => param.required) || contract.body?.required
1329
+ ? undefined
1330
+ : { path: op.path, summary: op.summary.slice(0, SEARCH_BUDGET.summaryChars), operationId: op.operationId };
1331
+ };
1268
1332
  registerOperationVocabulary(tools[0], function* () {
1269
1333
  for (const op of index.ops) {
1270
1334
  const verdict = discoveryVerdictOf(op);
@@ -1272,6 +1336,7 @@ export function hatch(options) {
1272
1336
  text: `${op.path.slice(0, 384)} ${op.operationId.slice(0, 128)} ${op.summary.slice(0, 384)} ${op.tag.slice(0, 128)}`,
1273
1337
  route: verdict === "refuse" ? searchTool : route(op).tool,
1274
1338
  write: verdict !== "read",
1339
+ ready: verdict === "read" ? ready(op) : undefined,
1275
1340
  };
1276
1341
  }
1277
1342
  });
@@ -1,4 +1,4 @@
1
- // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ // Provider kit. Unreachable from src/index.ts.
2
2
  //
3
3
  // One Schema is a tool's type, its argument decoder, and its catalog JSON
4
4
  // Schema. Effect's generator is correct but spells some things differently
@@ -238,4 +238,3 @@ export function createNodeGuardedTransport(proxy) {
238
238
  },
239
239
  };
240
240
  }
241
- export const nodeGuardedTransport = createNodeGuardedTransport();
@@ -1,4 +1,4 @@
1
- // Refresh-once middleware (decision 0010, B.2). Unreachable from src/index.ts.
1
+ // Refresh-once middleware. Unreachable from src/index.ts.
2
2
  //
3
3
  // Refresh-once for a connection that authenticates with a token it can
4
4
  // renew: a downstream OAuth grant, a service-account assertion, a machine
@@ -23,6 +23,7 @@
23
23
  //
24
24
  // The grant itself (storage, coordination across isolates, the token
25
25
  // request, the request-scope latch) stays in src/auth/.
26
+ import { Routes } from "../agent-routes.js";
26
27
  import { Cause, Context, Effect, Option } from "effect";
27
28
  import * as HttpClient from "effect/http/HttpClient";
28
29
  import * as HttpClientResponse from "effect/http/HttpClientResponse";
@@ -70,7 +71,7 @@ export function refreshOnce(guarded, options) {
70
71
  const error = new ConnectorCallError("connector_call_failed", "The OAuth challenge exceeded its response ceiling.", { retryable: false });
71
72
  const refusal = new Refusal({
72
73
  error: !readRequest(entry ?? {}) && options?.writeUnauthorized
73
- ? options.writeUnauthorized({ _tag: "Failed", failure: error })
74
+ ? options.writeUnauthorized({ _tag: "Failed", failure: error }, yield* Routes)
74
75
  : error,
75
76
  });
76
77
  const dispatch = entry ? (yield* Dispatches).find((item) => item.request === entry) : undefined;
@@ -125,7 +126,7 @@ export function refreshOnce(guarded, options) {
125
126
  };
126
127
  // This is still the resource request's own 401. The common failure
127
128
  // mapper attaches its provenance; settleWrite decides the outcome.
128
- return yield* Effect.fail(new Refusal({ error: options.writeUnauthorized(facts) }));
129
+ return yield* Effect.fail(new Refusal({ error: options.writeUnauthorized(facts, yield* Routes) }));
129
130
  }
130
131
  return HttpClientResponse.fromWeb(request, new Response(reply.text ?? "", { status: reply.status, headers: reply.headers }));
131
132
  }
@@ -1,4 +1,5 @@
1
- // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ import { bytesToHex, decodeBase64url } from "../bytes.js";
2
+ // Provider kit. Unreachable from src/index.ts.
2
3
  //
3
4
  // List tools take one page at a time. Lookahead detects an extra row; bound
4
5
  // cursors authenticate the tool, arguments, connection and credential owner.
@@ -52,7 +53,7 @@ async function signingKey(storage) {
52
53
  return cached;
53
54
  let raw = await storage.get(continuationKeys.signingKey);
54
55
  if (raw === null) {
55
- const candidate = Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => byte.toString(16).padStart(2, "0")).join("");
56
+ const candidate = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
56
57
  raw = (await storage.compareAndSet(continuationKeys.signingKey, null, candidate))
57
58
  ? candidate
58
59
  : await storage.get(continuationKeys.signingKey);
@@ -92,7 +93,7 @@ export const boundCursor = {
92
93
  const expected = yield* digest(tool, args);
93
94
  let parsed;
94
95
  try {
95
- parsed = JSON.parse(new TextDecoder().decode(fromBase64url(token)));
96
+ parsed = JSON.parse(new TextDecoder().decode(decodeBase64url(token)));
96
97
  }
97
98
  catch {
98
99
  parsed = undefined;
@@ -108,7 +109,7 @@ export const boundCursor = {
108
109
  if (token !== base64url(JSON.stringify({ v: record.v, d: record.d, c: record.c, m: record.m })))
109
110
  return yield* Effect.fail(new Refusal({ error: new ConnectorCallError("invalid_args", STRAY_CURSOR) }));
110
111
  const key = yield* keyForCall;
111
- const mac = fromBase64url(record.m);
112
+ const mac = decodeBase64url(record.m);
112
113
  const binding = yield* ContinuationBinding;
113
114
  const claims = new TextEncoder().encode(canonicalJson([record.v, binding.connection, binding.principal, record.d, record.c]));
114
115
  const valid = yield* Effect.promise(() => crypto.subtle.verify("HMAC", key, mac, claims));
@@ -125,7 +126,7 @@ function digest(tool, args) {
125
126
  .sort()
126
127
  .map((key) => [key, args[key]]);
127
128
  const bytes = new TextEncoder().encode(canonicalJson([tool, shape]));
128
- return Effect.promise(() => crypto.subtle.digest("SHA-256", bytes)).pipe(Effect.map((hash) => Array.from(new Uint8Array(hash).slice(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("")));
129
+ return Effect.promise(() => crypto.subtle.digest("SHA-256", bytes)).pipe(Effect.map((hash) => bytesToHex(new Uint8Array(hash).slice(0, 16))));
129
130
  }
130
131
  function base64url(text) {
131
132
  return base64urlBytes(new TextEncoder().encode(text));
@@ -136,7 +137,3 @@ function base64urlBytes(bytes) {
136
137
  binary += String.fromCharCode(byte);
137
138
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
138
139
  }
139
- function fromBase64url(token) {
140
- const binary = atob(token.replace(/-/g, "+").replace(/_/g, "/"));
141
- return Uint8Array.from(binary, (char) => char.charCodeAt(0));
142
- }
@@ -1,4 +1,6 @@
1
- // Internal provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ // Internal provider kit. Unreachable from src/index.ts.
2
+ import { routePhrases, routesOf } from "../agent-routes.js";
3
+ import { credentialFailureClass } from "../credential-verification.js";
2
4
  import { Context, Effect, Option } from "effect";
3
5
  import { api } from "../connectors/api.js";
4
6
  import { OAUTH_CHALLENGE_MAX_BYTES } from "../auth/renewable-grant.js";
@@ -12,6 +14,7 @@ import { guardedClient } from "./client.js";
12
14
  import { errorMeanings, Refusal, toConnectorCallError, withheldDefect, } from "./errors.js";
13
15
  import { hatch } from "./hatch.js";
14
16
  import { takesIdempotencyKey } from "./tools.js";
17
+ import { registerSearchSynonyms } from "../operation-vocabulary.js";
15
18
  import { Grant, grantFromOAuth, refreshOnce } from "./oauth.js";
16
19
  import { resultSafety } from "./safety.js";
17
20
  import { Classification, Dispatches, Signal, WriteCapacity, fromConnectorContext, } from "./services.js";
@@ -43,7 +46,19 @@ const missingGrant = {
43
46
  };
44
47
  /** An ordinary Promise connector factory; Effect remains inside the kit. */
45
48
  export function defineApiProvider(definition) {
46
- const { displayTitle, description, authScope, vendor, baseUrl, maxResponseBytes, timeoutMs, headers, admit, transport, defaultCallAdmission, pageSize: declaredPageSize, idempotencyKey, credential, oauth, oauthWrite401, grant, grantRenewal, authenticate, errors, safety, result, inBandRefusals, probe, guide, transformGuide, hatch: declaredHatch, tools, tagConnector, ...metadata } = definition;
49
+ const { displayTitle, description, authScope, vendor, baseUrl, maxResponseBytes, timeoutMs, headers, admit, transport, defaultCallAdmission, pageSize: declaredPageSize, idempotencyKey, credential, oauth, oauthWrite401, grant, grantRenewal, authenticate, errors, safety, result, inBandRefusals, probe, guide, transformGuide, hatch: declaredHatch, tools, tagConnector, searchSynonyms, ...metadata } = definition;
50
+ if (searchSynonyms !== undefined &&
51
+ !(Array.isArray(searchSynonyms) &&
52
+ searchSynonyms.length > 0 &&
53
+ searchSynonyms.length <= 32 &&
54
+ searchSynonyms.every((group) => Array.isArray(group) &&
55
+ group.length > 1 &&
56
+ group.length <= 8 &&
57
+ new Set(group).size === group.length &&
58
+ group.every((word) => /^[a-z][a-z0-9]{0,31}$/.test(word))))) {
59
+ throw new Error(`${metadata.title} searchSynonyms must be 1–32 groups of 2–8 distinct lowercase words.`);
60
+ }
61
+ const synonyms = searchSynonyms?.map((group) => [...group]);
47
62
  if (credential !== undefined && oauth !== undefined) {
48
63
  throw new Error(`${metadata.title} declares both credential and oauth; declare one authentication method.`);
49
64
  }
@@ -125,11 +140,12 @@ export function defineApiProvider(definition) {
125
140
  ? refreshOnce(guarded, oauthWrite401
126
141
  ? {
127
142
  challengeMaxBytes: OAUTH_CHALLENGE_MAX_BYTES,
128
- writeUnauthorized: (renewal) => unsentAfterUnauthorized(`Connector "${id}"'s API`, renewal._tag === "Renewed"
143
+ writeUnauthorized: (renewal, routes) => unsentAfterUnauthorized(`Connector "${id}"'s API`, renewal._tag === "Renewed"
129
144
  ? undefined
130
145
  : renewal._tag === "Failed"
131
146
  ? renewal.failure
132
- : new ConnectorCallError("downstream_oauth_required", `Connector "${id}" requires authorization — call authorize_connector({ connector: "${id}" }) and open the returned URL.`), "downstream_oauth_required"),
147
+ : // Configured with the connection, worded during the call it refuses.
148
+ new ConnectorCallError("downstream_oauth_required", `Connector "${id}" requires authorization — ${routePhrases(routes).authorizeAndOpen(id)}.`), "downstream_oauth_required"),
133
149
  }
134
150
  : renewalModes)
135
151
  : guarded, {
@@ -164,6 +180,7 @@ export function defineApiProvider(definition) {
164
180
  dispatches: Context.get(lease.services, Dispatches),
165
181
  withholds: connection.safety.withholdsErrors,
166
182
  signal,
183
+ routes: routesOf(ctx),
167
184
  };
168
185
  return runEdge(probeProgram.pipe(Effect.provideService(WriteCapacity, new Map()), Effect.flatMap((data) => Effect.try({
169
186
  try: () => {
@@ -173,7 +190,16 @@ export function defineApiProvider(definition) {
173
190
  catch: (error) => error,
174
191
  }).pipe(Effect.catch((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error)))), Effect.catch((failure) => Effect.sync(() => {
175
192
  const error = toConnectorCallError(failure, errors, map);
176
- return probe.failure?.(error, options) ?? { ok: false, message: error.message };
193
+ const described = probe.failure?.(error, options);
194
+ // A provider's own verdict wins; a failure it leaves
195
+ // unclassified takes the class of the typed error behind it.
196
+ if (described?.ok || described?.failure)
197
+ return described;
198
+ return {
199
+ ok: false,
200
+ message: described?.message ?? error.message,
201
+ failure: credentialFailureClass(error),
202
+ };
177
203
  })), Effect.catchDefect((defect) => Effect.die(withheldDefect(defect, map, !!probe.run))), Effect.provideContext(lease.services), Effect.ensuring(Effect.sync(lease.close))), { signal: ctx.signal });
178
204
  };
179
205
  const builtTools = tools({
@@ -214,6 +240,8 @@ export function defineApiProvider(definition) {
214
240
  tools: [...builtTools, ...hatchTools],
215
241
  }));
216
242
  tagConnector?.(connector, options);
243
+ if (synonyms)
244
+ registerSearchSynonyms(connector, synonyms);
217
245
  return connector;
218
246
  },
219
247
  });
@@ -1,4 +1,4 @@
1
- // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ // Provider kit. Unreachable from src/index.ts.
2
2
  //
3
3
  // The one value-safety boundary of a kit connection. `connect` requires it,
4
4
  // and `Send` hands a provider nothing it has not passed: a named tool, a
@@ -50,6 +50,10 @@ export function resultSafety(options) {
50
50
  return dropped.size > 0 ? strip(safe) : safe;
51
51
  },
52
52
  withholdsErrors: (operation) => reviewed?.withholdsErrors(operation) === true,
53
+ withholds: (operation) => {
54
+ const reviewedHint = reviewed?.withholds?.(operation);
55
+ return (path) => path.some((segment) => dropped.has(segment.replace(/\[\]$/, ""))) || reviewedHint?.(path) === true;
56
+ },
53
57
  };
54
58
  }
55
59
  /** Bind the REST value-safety table to its pinned index; unknown operations never bypass its review. */
@@ -78,5 +82,9 @@ export function restSafety(reviewed, index) {
78
82
  const op = indexed(operation);
79
83
  return op ? reviewed.withholdsErrors(op) : true;
80
84
  },
85
+ withholds: (operation) => {
86
+ const op = indexed(operation);
87
+ return op ? reviewed.withholds(op.method, op.path) : () => false;
88
+ },
81
89
  };
82
90
  }
@@ -3,7 +3,7 @@ import { Context, Effect } from "effect";
3
3
  import { dispatchProviderRequest, providerRequest } from "../provider-call.js";
4
4
  import { callClassification } from "../call-classification.js";
5
5
  import { fromSignal } from "../runtime/run.js";
6
- import { callFromContext, ContinuationBinding, ContinuationStorage, openCall as openCoreCall, serviceFor, Signal, } from "../runtime/call.js";
6
+ import { callFromContext, ContinuationBinding, ContinuationStorage, serviceFor, Signal } from "../runtime/call.js";
7
7
  import { RequestRefused, ResendRefused } from "./errors.js";
8
8
  export { writeRequestKey } from "./write-request.js";
9
9
  export { Caller, Credentials, Secrets, RequestSecrets, Budget, Recovery, Signal, Classification, Ledger, } from "../runtime/call.js";
@@ -70,10 +70,3 @@ export function fromConnectorContext(ctx, fallback) {
70
70
  services: lease.services.pipe(Context.add(ContinuationStorage, serviceFor(ctx, ContinuationStorage) ?? ctx.storage), Context.add(ContinuationBinding, serviceFor(ctx, ContinuationBinding) ?? { connection: "", principal: null }), Context.add(Dispatches, []), Context.add(WriteCapacity, new Map([["write", new Map([[undefined, undefined]])]]))),
71
71
  };
72
72
  }
73
- export function openCall(inputs) {
74
- const lease = openCoreCall(inputs);
75
- return {
76
- ...lease,
77
- services: lease.services.pipe(Context.add(Dispatches, []), Context.add(WriteCapacity, new Map([["write", new Map([[undefined, undefined]])]]))),
78
- };
79
- }
@@ -1,4 +1,4 @@
1
- // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ // Provider kit. Unreachable from src/index.ts.
2
2
  //
3
3
  // Tool builders. A tool is a name, a description, an input Schema, an output
4
4
  // Schema, and a program; the builder renders both Schemas as catalog JSON
@@ -13,6 +13,7 @@
13
13
  // argument validation, sent-secret redaction, withheld handler text. Its
14
14
  // boundary also decides every write's outcome, from the core ledger the kit's
15
15
  // transport records into, so a kit connector declares `tracksEveryRequest`.
16
+ import { routesOf } from "../agent-routes.js";
16
17
  import { Context, Effect, Schema } from "effect";
17
18
  import { inLogicalCall } from "../provider-call.js";
18
19
  import { registerToolEntry } from "../runtime/call-entry.js";
@@ -20,11 +21,11 @@ import { ConnectorCallError } from "../errors.js";
20
21
  import { refuseWithheldValues } from "../providers/_shared/rest/value-safety.js";
21
22
  import { causeError, runEdge } from "../runtime/run.js";
22
23
  import { recordUnauthorized, Refusal, RequestRefused, toConnectorCallError, withheldDefect, } from "./errors.js";
23
- import { fieldsInput, fieldsProblem, selectFields } from "./fields.js";
24
+ import { ArgumentProblems } from "./argument-problems.js";
25
+ import { fieldsContract, selectFields } from "./fields.js";
24
26
  import { catalogSchema, Integer } from "./json-schema.js";
25
27
  import { boundCursor, Cursor, StandardPage } from "./paging.js";
26
28
  import { Classification, Dispatches, IdempotencyKey, Recovery, ReadTool, WriteCapacity, writeRequestKey, fromConnectorContext, } from "./services.js";
27
- const inputs = new WeakMap();
28
29
  const keyed = new WeakSet();
29
30
  /** Whether a kit write builder declared an idempotency key for this tool. */
30
31
  export function takesIdempotencyKey(tool) {
@@ -62,26 +63,20 @@ function originalOrder(decoded, original) {
62
63
  };
63
64
  return copy(decoded, original);
64
65
  }
65
- /** The input Schema a kit tool decodes with, for the catalog/decoder agreement check. */
66
- export function kitInput(tool) {
67
- return inputs.get(tool);
68
- }
69
- /** A Struct's own property names, for `fields`. */
70
- function propertyNames(schema, what) {
71
- const fields = schema.fields;
72
- if (!fields)
66
+ /** Refuse `fields` on anything but a Schema.Struct. */
67
+ function struct(schema, what) {
68
+ if (!schema.fields)
73
69
  throw new Error(`fields needs ${what} declared as a Schema.Struct.`);
74
- return new Set(Object.keys(fields));
70
+ return schema;
75
71
  }
76
- /** Refuse a bad `fields` path before anything is sent. */
77
- function checkFields(fields, names, of) {
78
- const error = fieldsProblem(fields, names, of);
79
- return error ? Effect.fail(new Refusal({ error })) : Effect.void;
72
+ /** A tool's own check plus every bad `fields` path, refused together before anything is sent. */
73
+ function withFields(contract, check) {
74
+ return (args, problems) => {
75
+ contract.issues(problems, args.fields);
76
+ const { fields: _fields, ...rest } = args;
77
+ check?.(rest, problems);
78
+ };
80
79
  }
81
- const fieldsExample = (names) => {
82
- const first = [...names][0];
83
- return first === undefined ? "" : `, e.g. ["${first}"]`;
84
- };
85
80
  /**
86
81
  * A read: admitted to call_tool and programs. `fields: true` (decision 0011
87
82
  * S5) appends a `fields` argument that narrows the returned record; the input
@@ -93,17 +88,17 @@ export function read(connection, spec) {
93
88
  throw new Error("A read review must be nonempty.");
94
89
  if (spec.fields === undefined)
95
90
  return build(connection, spec, "read", { readOnlyHint: true }, undefined, spec.review);
96
- const names = propertyNames(spec.output, "the output");
97
- propertyNames(spec.input, "the input");
91
+ const contract = fieldsContract("result", struct(spec.output, "the output"));
92
+ struct(spec.input, "the input");
98
93
  const input = Schema.Struct({
99
94
  ...spec.input.fields,
100
- fields: Schema.optionalKey(fieldsInput(`Dot paths to keep in the result${fieldsExample(names)}. It only narrows the result.`)),
95
+ fields: Schema.optionalKey(contract.input),
101
96
  });
102
97
  return build(connection, {
103
98
  ...spec,
104
99
  input,
100
+ check: withFields(contract, spec.check),
105
101
  run: (args) => Effect.gen(function* () {
106
- yield* checkFields(args.fields, names, "the result");
107
102
  const { fields, ...rest } = args;
108
103
  return selectFields(yield* spec.run(rest), fields);
109
104
  }),
@@ -141,7 +136,7 @@ export function list(connection, spec) {
141
136
  if (name in spec.filters)
142
137
  throw new Error(`${spec.name} filters cannot declare ${name}; the kit supplies it.`);
143
138
  }
144
- const names = propertyNames(spec.row, "the row");
139
+ const contract = fieldsContract("item", struct(spec.row, "the row"));
145
140
  const input = Schema.Struct({
146
141
  ...spec.filters,
147
142
  limit: Schema.optionalKey(allowedLimit.annotate({
@@ -149,7 +144,7 @@ export function list(connection, spec) {
149
144
  default: size,
150
145
  })),
151
146
  cursor: Schema.optionalKey(Cursor),
152
- fields: Schema.optionalKey(fieldsInput(`Dot paths to keep in each item${fieldsExample(names)}. It only narrows the result.`)),
147
+ fields: Schema.optionalKey(contract.input),
153
148
  });
154
149
  return build(connection, {
155
150
  name: spec.name,
@@ -157,8 +152,8 @@ export function list(connection, spec) {
157
152
  input,
158
153
  // `fields` narrows each item, so items render sparse; the envelope keeps items and page required.
159
154
  output: Schema.Unknown,
155
+ check: withFields(contract, undefined),
160
156
  run: (args) => Effect.gen(function* () {
161
- yield* checkFields(args.fields, names, "an item");
162
157
  // A cursor stays valid when only the projection changes between pages.
163
158
  const { fields, cursor: supplied, ...rest } = args;
164
159
  const scope = spec.cursorScope === undefined
@@ -314,7 +309,13 @@ rendered) {
314
309
  const lease = fromConnectorContext(ctx, verdict);
315
310
  const classification = Context.get(lease.services, Classification);
316
311
  const dispatches = Context.get(lease.services, Dispatches);
317
- const map = { classification, dispatches, withholds: connection.safety.withholdsErrors, signal: ctx.signal };
312
+ const map = {
313
+ classification,
314
+ dispatches,
315
+ withholds: connection.safety.withholdsErrors,
316
+ signal: ctx.signal,
317
+ routes: routesOf(ctx),
318
+ };
318
319
  return Effect.try({
319
320
  try: () => {
320
321
  if (verdict === "write" || classification === "write")
@@ -323,7 +324,22 @@ rendered) {
323
324
  catch: (error) => error,
324
325
  }).pipe(Effect.catch((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error)),
325
326
  // Its message can quote the argument; as a defect, api() withholds it.
326
- Effect.flatMap(() => decode(args).pipe(Effect.orDie)), Effect.map((decoded) => (spec.inputOrder === "original" ? originalOrder(decoded, args) : decoded)), Effect.flatMap((decoded) => Effect.gen(function* () {
327
+ Effect.flatMap(() => decode(args).pipe(Effect.orDie)), Effect.map((decoded) => (spec.inputOrder === "original" ? originalOrder(decoded, args) : decoded)), Effect.flatMap((decoded) => {
328
+ if (!spec.check)
329
+ return Effect.succeed(decoded);
330
+ const check = spec.check;
331
+ return Effect.try({
332
+ try: () => {
333
+ const problems = new ArgumentProblems();
334
+ check(decoded, problems);
335
+ const refusal = problems.refusal("Invalid arguments", tool.inputSchema);
336
+ if (refusal)
337
+ throw refusal;
338
+ return decoded;
339
+ },
340
+ catch: (error) => error,
341
+ }).pipe(Effect.catch((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error)));
342
+ }), Effect.flatMap((decoded) => Effect.gen(function* () {
327
343
  const key = idempotency?.key(decoded);
328
344
  if (key !== undefined && unsendableKey(key)) {
329
345
  return yield* Effect.fail(new RequestRefused({
@@ -350,7 +366,6 @@ rendered) {
350
366
  })));
351
367
  });
352
368
  registerToolEntry(tool, (args, ctx) => program(args, ctx, inLogicalCall(ctx)));
353
- inputs.set(tool, spec.input);
354
369
  if (idempotency)
355
370
  keyed.add(tool);
356
371
  return tool;