@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
@@ -1,3 +1,4 @@
1
+ import { GUEST_FUNCTIONS } from "./guest-functions.js";
1
2
  /** Capture the single-use runner before evaluating the guest expression. */
2
3
  export function wrapGuestProgram(code) {
3
4
  return `async () => globalThis.__connecta_run()(async (connecta) => await (\n${code}\n)())`;
@@ -62,6 +63,34 @@ export function guestPromiseInitializer() {
62
63
  Object.freeze(NativePromise.prototype);
63
64
  })()`;
64
65
  }
66
+ /**
67
+ * The frozen namespace, joined from the registry. A `wrapped` or `emission`
68
+ * function uses the initializer's local of the same name; an alias shares one
69
+ * function object. Only guest source enters this string.
70
+ */
71
+ const GUEST_NAMESPACE = (() => {
72
+ const before = [];
73
+ const entries = ["__proto__: null"];
74
+ for (const fn of GUEST_FUNCTIONS) {
75
+ if (fn.budget === "once")
76
+ continue;
77
+ if (fn.budget === "emission") {
78
+ entries.push(`${fn.name}(block) {
79
+ const ref = block && typeof block === "object" ? weakGet(nativeRefs, block) : undefined;
80
+ return trackEmission(${fn.name}(ref || block));
81
+ }`);
82
+ continue;
83
+ }
84
+ const value = `hosted(${"wrapped" in fn ? fn.name : `provider.${fn.name}`})`;
85
+ if (!("alias" in fn)) {
86
+ entries.push(`${fn.name}: ${value}`);
87
+ continue;
88
+ }
89
+ before.push(` const ${fn.name} = ${value};\n`);
90
+ entries.push(fn.name, `// Undeclared alias: ${fn.alias.note}\n ${fn.alias.name}: ${fn.name}`);
91
+ }
92
+ return `${before.join("")} const namespace = freeze({\n ${entries.join(",\n ")}\n });`;
93
+ })();
65
94
  /** Capture a private initializer before a Worker evaluates its guest module. */
66
95
  export function guestInitializer() {
67
96
  return `(() => {
@@ -152,19 +181,17 @@ export function guestInitializer() {
152
181
  }
153
182
  });
154
183
  }
155
- const namespace = freeze({
156
- __proto__: null,
157
- search: provider.search,
158
- describe: provider.describe,
159
- call,
160
- read: provider.read,
161
- result: provider.result,
162
- skill: provider.skill,
163
- emit(block) {
164
- const ref = block && typeof block === "object" ? weakGet(nativeRefs, block) : undefined;
165
- return trackEmission(emit(ref || block));
166
- }
167
- });
184
+ // Result fields read off an unawaited host call flag the run, still undefined.
185
+ let unawaited = false;
186
+ const fields = {};
187
+ for (const key of ["data", "format", "content", "tools", "catalogErrors", "total", "text", "resultId", "hasMore", "nextOffset"])
188
+ fields[key] = { get() { unawaited = true; } };
189
+ const hosted = (fn) => function () {
190
+ const promise = apply(fn, undefined, arguments);
191
+ if (promise && typeof promise === "object") defineProperties(promise, fields);
192
+ return promise;
193
+ };
194
+ ${GUEST_NAMESPACE}
168
195
  globalThis.connecta = namespace;
169
196
  defineProperties(globalThis, {
170
197
  Error: { value: NativeError, writable: false, configurable: false },
@@ -174,7 +201,13 @@ export function guestInitializer() {
174
201
  return async (program) => {
175
202
  if (used) throw new NativeError("Guest runner initialization is single-use.");
176
203
  used = true;
177
- const result = await program(namespace);
204
+ let result;
205
+ try {
206
+ result = await program(namespace);
207
+ } catch (error) {
208
+ if (unawaited) try { await provider.unawaited(); } catch {}
209
+ throw error;
210
+ }
178
211
  for (let i = 0; i < pending.length; i++) {
179
212
  const report = await pending[i].settled;
180
213
  if (!report.ok && !pending[i].handled) throw report.error;
@@ -198,8 +231,15 @@ const ERROR_NAMES = new Set([
198
231
  ]);
199
232
  /** Fixed repair guidance, used only in agent-facing results. */
200
233
  export function programError(raw, code) {
201
- const message = typeof raw.message === "string" ? raw.message : "Program failed.";
202
- const name = typeof raw.name === "string" && ERROR_NAMES.has(raw.name) ? raw.name : "Error";
234
+ const text = typeof raw.message === "string" ? raw.message : "Program failed.";
235
+ // Executors may report "TypeError: …" as the message itself; name it once.
236
+ const prefix = /^([A-Za-z]*Error): /.exec(text)?.[1];
237
+ const name = typeof raw.name === "string" && ERROR_NAMES.has(raw.name)
238
+ ? raw.name
239
+ : prefix && ERROR_NAMES.has(prefix)
240
+ ? prefix
241
+ : "Error";
242
+ const message = prefix === name ? text.slice(name.length + 2) : text;
203
243
  const stack = typeof raw.stack === "string" ? raw.stack : "";
204
244
  // Compare QuickJS/V8 locations to the wrapper baseline after the source.
205
245
  const location = /:(\d+)(?::\d+)?\)?(?:\n|$)/.exec(stack);
@@ -215,7 +255,10 @@ export function programError(raw, code) {
215
255
  ? Math.max(1, Number(location[1]) - Number(baseline[1]) + code.split("\n").length + 1)
216
256
  : null;
217
257
  let hint = "Use plain JavaScript in one async () => { ... } expression and the connecta global.";
218
- if (/\brequire\b|\b[Ii]mports?\b|\bfs\b|filesystem|node:/.test(message) ||
258
+ if (raw.unawaited === true) {
259
+ hint = "connecta.* returns a Promise; await it: (await connecta.call(address, args)).data.";
260
+ }
261
+ else if (/\brequire\b|\b[Ii]mports?\b|\bfs\b|filesystem|node:/.test(message) ||
219
262
  (name === "SyntaxError" && /\bimport\b/.test(code))) {
220
263
  hint =
221
264
  "Imports, require, and filesystem access are outside the guest API. Use connecta.call to access configured services.";
@@ -224,10 +267,10 @@ export function programError(raw, code) {
224
267
  /(?:const|let|var|function|\()\s*connecta\b/.test(code)) {
225
268
  hint = "Do not shadow connecta. Use the host-provided connecta global.";
226
269
  }
227
- else if (/callTool|\bskills\b|connecta\.(?:guide|skills)/.test(message) ||
228
- (["TypeError", "ReferenceError"].includes(name) && /connecta\.(?:guide|skills)\b/.test(code))) {
270
+ else if (/callTool|\bskills\b|connecta\.guide/.test(message) ||
271
+ (["TypeError", "ReferenceError"].includes(name) && /connecta\.guide\b/.test(code))) {
229
272
  hint =
230
- "Use connecta.call(address, args), connecta.search, connecta.describe, connecta.result, and connecta.skill(name). Services are addressed by connector.tool, never guest globals.";
273
+ 'Use connecta.call(address, args), connecta.search, connecta.describe, connecta.result, and connecta.skill("usage"). Services are addressed by connector.tool, never guest globals.';
231
274
  }
232
275
  return {
233
276
  code: "program_error",
@@ -25,7 +25,8 @@ export interface GuestApi {
25
25
  text: string;
26
26
  format: "text";
27
27
  }>;
28
- emit(block: GuestNativeBlock | GuestResult): PromiseLike<void>;
28
+ /** Forwards an authored block, a block from result.content, or an inline result; a paged handle is refused. */
29
+ emit(block: GuestNativeBlock | Exclude<GuestResult, GuestResultHandle>): PromiseLike<void>;
29
30
  }
30
31
  export interface GuestResourceResult {
31
32
  contents: Array<{
@@ -103,7 +104,7 @@ export type GuestBlock = {
103
104
  mimeType: string;
104
105
  };
105
106
  type SchemaFormat = "json" | "compact" | "typescript";
106
- export interface CatalogSearchArgs {
107
+ interface CatalogSearchArgs {
107
108
  query?: string;
108
109
  connector?: string;
109
110
  safety?: "readOnly" | "approvalRequired" | "all";
@@ -113,9 +114,9 @@ export interface CatalogSearchArgs {
113
114
  includeSchemas?: SchemaFormat;
114
115
  includeSchemaKeys?: boolean;
115
116
  }
116
- export interface CatalogDescribeArgs {
117
- address?: unknown;
118
- addresses?: unknown;
117
+ interface CatalogDescribeArgs {
118
+ address?: string;
119
+ addresses?: string[];
119
120
  format?: SchemaFormat;
120
121
  fullDescriptions?: boolean;
121
122
  }
@@ -140,7 +141,7 @@ interface CatalogSearchFailure extends CatalogFailureDetail {
140
141
  }>;
141
142
  retry?: string;
142
143
  }
143
- export interface CatalogSearchPage {
144
+ interface CatalogSearchPage {
144
145
  catalogErrors: CatalogSearchFailure[];
145
146
  absence?: {
146
147
  service: string;
@@ -170,14 +171,27 @@ export interface CatalogSearchPage {
170
171
  guidance?: string;
171
172
  };
172
173
  }
173
- export type CatalogSearchResult = CatalogSearchPage & {
174
- tools: Array<CatalogSearchTool & {
175
- connectorTitle?: string;
174
+ type CatalogSearchResult = CatalogSearchPage & {
175
+ tools: CatalogSearchTool[];
176
+ /** Complete operation reads: pass args unchanged to connecta.call(address, args). */
177
+ readyCalls?: Array<{
178
+ address: string;
179
+ args: {
180
+ path: string;
181
+ };
182
+ classification: "read";
183
+ summary: string;
184
+ guideRequired?: true;
185
+ guideRequiredReasons?: GuideRequiredReason[];
186
+ }>;
187
+ /** Facts shared by a connector's rows, keyed by the id before the address's first dot. */
188
+ connectors?: Record<string, {
189
+ title?: string;
176
190
  guide?: string;
177
191
  guideSummary?: string;
178
192
  }>;
179
193
  };
180
- export interface CatalogDescription {
194
+ interface CatalogDescription {
181
195
  address: string;
182
196
  schemaFormat?: "json" | "text";
183
197
  name?: string;
@@ -0,0 +1,35 @@
1
+ // Internal provider kit (decision 0011 S5). Unreachable from src/index.ts.
2
+ //
3
+ // A provider's own argument checks record every problem before refusing, so
4
+ // one invalid_args carries every finding and an agent repairs the call in one
5
+ // turn instead of one constraint per turn.
6
+ import { ConnectorCallError, issueSummary } from "../errors.js";
7
+ import { schemaExample } from "../validate.js";
8
+ /** Every argument problem one call has, then a single refusal. */
9
+ export class ArgumentProblems {
10
+ #issues = [];
11
+ /**
12
+ * Record one problem. A refusal holds only what the published schema
13
+ * declares and fixed text: `path` names declared properties and indices
14
+ * (`<key>` for any other key), `expected` states the allowed shape or bound,
15
+ * and `received` is a JSON type. Never a submitted string or key.
16
+ */
17
+ add(path, expected, received) {
18
+ this.#issues.push({ path, expected, ...(received === undefined ? {} : { received }) });
19
+ }
20
+ /**
21
+ * The one refusal for every recorded problem, or undefined when there is
22
+ * none. With the tool's schema it carries the schema's synthesized example;
23
+ * `withheld` states why a tool offers none.
24
+ */
25
+ refusal(lead, schema, withheld) {
26
+ if (!this.#issues.length)
27
+ return undefined;
28
+ return new ConnectorCallError("invalid_args", `${lead}: ${issueSummary(this.#issues)}`, {
29
+ validation: {
30
+ issues: this.#issues,
31
+ ...(withheld ? { exampleUnavailable: withheld } : schema ? schemaExample(schema) : {}),
32
+ },
33
+ });
34
+ }
35
+ }
@@ -1,6 +1,6 @@
1
1
  import { confinementBase, REDIRECT_STATUSES, BODILESS_STATUSES } from "../http-policy.js";
2
2
  import { guardedTransport, sendGuarded } from "../guarded-transport.js";
3
- // Guarded HTTP transport (decision 0010, B.2). Unreachable from src/index.ts.
3
+ // Guarded HTTP transport. Unreachable from src/index.ts.
4
4
  //
5
5
  // The guarded transport as an Effect `HttpClient`, with the behaviour of
6
6
  // src/connectors/guarded-fetch.ts: requests confined beneath one base URL,
@@ -1,4 +1,4 @@
1
- // Internal HTTP failure contract (decision 0010, B.3). Unreachable from src/index.ts.
1
+ // Internal HTTP failure contract. Unreachable from src/index.ts.
2
2
  //
3
3
  // Every way a kit request can fail, as a tagged error, and the one mapping
4
4
  // from those to the wire `ConnectorCallError`. A provider supplies meaning
@@ -26,10 +26,11 @@
26
26
  // refusing status counts only for the request that received it. Advice to
27
27
  // send a call again is a segment of its own (`resendAdvice`), never part of
28
28
  // the message, so output composes it only where the outcome allows.
29
- // Q8 policy is decided once by the core's `settleWrite` (#917), from the
29
+ // Q8 policy is decided once by the core's `settleWrite`, from the
30
30
  // ledger entries this kit forwards its matched proof and renewals to. A
31
31
  // write answered 401 stays unknown; only renewal removes its handoff, and
32
32
  // only a reviewed authentication-before-execution declaration proves refusal.
33
+ import { routesOf } from "../agent-routes.js";
33
34
  import { Data } from "effect";
34
35
  import { ConnectorCallError, isConnectorCallErrorCode, resendAdvice, unavailableCallError, } from "../errors.js";
35
36
  import { attachFailureFacts } from "../operator-record.js";
@@ -429,7 +430,7 @@ function verdict(failure, vendor, options, dispatch = options.dispatches.find((e
429
430
  body: undefined,
430
431
  unreadable: true,
431
432
  operation: dispatch.operation ?? { method: "", template: "" },
432
- }), vendor, options.withholds?.(dispatch.operation ?? { method: "", template: "" }) === true, !dispatch.write);
433
+ }), vendor, options.withholds?.(dispatch.operation ?? { method: "", template: "" }) === true, !dispatch.write, routesOf(options));
433
434
  return providerFailureMessage(mapped, failure.detail);
434
435
  }
435
436
  const uncertainWrite = options.classification === "write" &&
@@ -455,10 +456,10 @@ function verdict(failure, vendor, options, dispatch = options.dispatches.find((e
455
456
  : unreadable, { retryable: false });
456
457
  }
457
458
  case "StatusFailure":
458
- return statusVerdict(failure, vendor, options.withholds?.(failure.operation) === true, options.dispatches.some((dispatch) => dispatch.failure === failure && !dispatch.write));
459
+ return statusVerdict(failure, vendor, options.withholds?.(failure.operation) === true, options.dispatches.some((dispatch) => dispatch.failure === failure && !dispatch.write), routesOf(options));
459
460
  }
460
461
  }
461
- function statusVerdict(failure, vendor, withheld, read) {
462
+ function statusVerdict(failure, vendor, withheld, read, routes) {
462
463
  const input = classificationInput(failure, vendor, withheld);
463
464
  const { headers, presence } = input;
464
465
  // Classification first, over facts and headers only.
@@ -477,7 +478,7 @@ function statusVerdict(failure, vendor, withheld, read) {
477
478
  withheld,
478
479
  ...(withheld ? {} : { body: failure.body }),
479
480
  });
480
- const error = new ConnectorCallError(code, words ? words(detail) : detail, {
481
+ const error = new ConnectorCallError(code, words ? words(detail, routes) : detail, {
481
482
  ...(retryable === undefined ? {} : { retryable }),
482
483
  ...(wait === undefined ? {} : { retryAfterMs: wait }),
483
484
  ...(current ? { current } : {}),
@@ -3,10 +3,29 @@
3
3
  // `fields` narrows a result to the dot paths an agent names. It runs on the
4
4
  // result a tool already built (after value safety, the privacy projection and
5
5
  // any named projection), so it can only remove. A list narrows each item.
6
+ //
7
+ // Every `fields` argument states where its paths start, its anchor, in the
8
+ // schema description, derived here from the same contract that validates it:
9
+ // a named read's result, each item of a list's `items`, or a hatch's response
10
+ // body in `data`. A path is checked against the published output schema as
11
+ // deep as that schema is closed, so a path outside the declared contract is
12
+ // refused before anything is sent. A declared path the response happens to
13
+ // lack is never refused: absent optional values and empty lists are ordinary.
6
14
  import { Schema } from "effect";
7
- import { ConnectorCallError } from "../errors.js";
8
- import { isMaxChars, isMinChars } from "./json-schema.js";
15
+ import { ArgumentProblems } from "./argument-problems.js";
16
+ import { catalogSchema, isMaxChars, isMinChars } from "./json-schema.js";
9
17
  const MAX_FIELDS = 50;
18
+ /** Choices listed in a refusal or a warning; more read as "a declared property". */
19
+ const MAX_CHOICES = 40;
20
+ const ANCHORS = {
21
+ result: "the result",
22
+ item: "each item in items",
23
+ data: "the response body in data, each item when the body is a list",
24
+ };
25
+ /** The description of a `fields` argument: its anchor, one valid example, then any tool note. */
26
+ export function fieldsDescription(anchor, example, note = "It only narrows the result.") {
27
+ return `Dot paths to keep. Paths start at ${ANCHORS[anchor]}, e.g. ${JSON.stringify([example])}. ${note}`;
28
+ }
10
29
  /** The `fields` argument: at most 50 dot paths of 1 to 200 characters. */
11
30
  export function fieldsInput(description) {
12
31
  return Schema.Array(Schema.String.check(isMinChars(1), isMaxChars(200)))
@@ -16,18 +35,97 @@ export function fieldsInput(description) {
16
35
  function record(value) {
17
36
  return value !== null && typeof value === "object" && !Array.isArray(value);
18
37
  }
38
+ /** A dot path's property names; `a[].b` and `a.b` both select `b` in each item of `a`. */
39
+ const segments = (path) => path.replace(/\[\](?=\.|$)/g, "").split(".");
40
+ /**
41
+ * Collect the properties a path segment may name at a schema position into
42
+ * `into`, false when the position is open (any key may appear: `{}`, a bare
43
+ * object or array, a record, a reference). Arrays are transparent, as
44
+ * selection narrows them item by item; a leaf adds nothing and stays closed.
45
+ * Below the first segment, JSON Schema's default applies: an object is closed
46
+ * only when it says `additionalProperties: false`. The first segment keeps the
47
+ * declared properties of the result or item as its closed set.
48
+ */
49
+ function propertiesAt(node, into, root) {
50
+ if (!record(node))
51
+ return node === false;
52
+ const additional = node["additionalProperties"];
53
+ if ("$ref" in node || "allOf" in node || (additional !== undefined && additional !== false))
54
+ return false;
55
+ const branches = [node["anyOf"], node["oneOf"]].flatMap((value) => (Array.isArray(value) ? value : []));
56
+ if (!branches.every((branch) => propertiesAt(branch, into, root)))
57
+ return false;
58
+ if (node["items"] !== undefined && !propertiesAt(node["items"], into, root))
59
+ return false;
60
+ const properties = node["properties"];
61
+ if (record(properties)) {
62
+ for (const [name, child] of Object.entries(properties))
63
+ into.set(name, [...(into.get(name) ?? []), child]);
64
+ return root || additional === false;
65
+ }
66
+ if (branches.length || node["items"] !== undefined || "enum" in node || "const" in node)
67
+ return true;
68
+ const types = [node["type"]].flat();
69
+ return node["type"] !== undefined && !types.includes("object") && !types.includes("array");
70
+ }
71
+ const choices = (names) => names.length > 0 && names.length <= MAX_CHOICES ? `one of ${names.join(", ")}` : "a declared property";
72
+ /**
73
+ * Record every `fields` path the contract cannot carry. A JSON Schema is the
74
+ * published shape at the anchor (a result, or a list's item), checked as deep
75
+ * as it is closed; a set names only the first segments, which is all a hatch's
76
+ * pinned index can promise. A finding names the entry's index, its segment
77
+ * position and the declared properties there, never the submitted text.
78
+ */
79
+ function fieldsIssues(problems, fields, contract, of) {
80
+ const root = contract instanceof Set
81
+ ? { type: "object", properties: Object.fromEntries([...contract].map((n) => [n, {}])) }
82
+ : contract;
83
+ fields?.forEach((field, index) => {
84
+ let nodes = [root];
85
+ for (const [depth, name] of segments(field).entries()) {
86
+ const at = new Map();
87
+ // Selection keeps a scalar a path runs past, so below the first segment only a closed object refuses.
88
+ if (!nodes.every((node) => propertiesAt(node, at, depth === 0)) || (depth > 0 && at.size === 0))
89
+ return;
90
+ const next = at.get(name);
91
+ if (next) {
92
+ nodes = next;
93
+ continue;
94
+ }
95
+ const declared = [...at.keys()];
96
+ problems.add(`/fields/${index}`, depth === 0
97
+ ? `a path beginning with a property of ${of}: ${choices(declared)}`
98
+ : `a declared path of ${of}; segment ${depth + 1} names a property declared at that level: ${choices(declared)}`);
99
+ return;
100
+ }
101
+ });
102
+ }
103
+ /** The single refusal naming every bad `fields` path, if any; `withheld` says why it has no example. */
104
+ export function fieldsProblem(fields, contract, of, withheld) {
105
+ const problems = new ArgumentProblems();
106
+ fieldsIssues(problems, fields, contract, of);
107
+ return problems.refusal("Invalid fields", undefined, withheld);
108
+ }
19
109
  /**
20
- * The refusal for a path whose first segment is not a property the result can
21
- * carry, if any. Named tools pass their output Schema's property names and
22
- * the hatch its operation's declared response roots, so a bad path fails
23
- * before anything is sent.
110
+ * The `fields` contract for a named read's result (`"result"`) or a list's
111
+ * item (`"item"`). `shape` is that result or item: a Schema rendered as the
112
+ * catalog renders it, or the catalog JSON Schema itself. Its first declared
113
+ * property is the description's example, so the example is always valid.
24
114
  */
25
- export function fieldsProblem(fields, names, of) {
26
- const bad = fields?.find((field) => !names.has(field.replace(/\[\](?=\.|$)/g, "").split(".")[0]));
27
- if (bad === undefined)
28
- return undefined;
29
- const choices = names.size > 0 && names.size <= 40 ? `: one of ${[...names].join(", ")}` : "";
30
- return new ConnectorCallError("invalid_args", `fields path ${bad} does not begin with a property of ${of}${choices}.`);
115
+ export function fieldsContract(anchor, shape, note, of = anchor === "item" ? "an item" : "the result") {
116
+ const schema = "ast" in shape ? catalogSchema(shape, "output") : shape;
117
+ const at = new Map();
118
+ propertiesAt(schema, at, true);
119
+ const example = [...at.keys()][0];
120
+ if (example === undefined)
121
+ throw new Error("fields needs a contract that declares at least one property.");
122
+ const description = fieldsDescription(anchor, example, note);
123
+ return {
124
+ description,
125
+ input: fieldsInput(description),
126
+ issues: (problems, fields) => fieldsIssues(problems, fields, schema, of),
127
+ problem: (fields) => fieldsProblem(fields, schema, of),
128
+ };
31
129
  }
32
130
  /** Keep only the named dot paths; arrays are narrowed item by item. */
33
131
  export function selectFields(data, fields) {
@@ -40,10 +138,91 @@ export function selectFields(data, fields) {
40
138
  return value;
41
139
  const children = new Map();
42
140
  for (const path of paths) {
43
- const [key, ...tail] = path.replace(/\[\](?=\.|$)/g, "").split(".");
141
+ const [key, ...tail] = segments(path);
44
142
  children.set(key, [...(children.get(key) ?? []), tail.join(".")]);
45
143
  }
46
144
  return Object.fromEntries([...children].flatMap(([key, tails]) => Object.hasOwn(value, key) ? [[key, tails.includes("") ? value[key] : pick(value[key], tails)]] : []));
47
145
  };
48
146
  return pick(data, fields);
49
147
  }
148
+ const empty = (value) => value === null ||
149
+ value === undefined ||
150
+ (Array.isArray(value) ? value.length === 0 : record(value) && !Object.keys(value).length);
151
+ /** Warning bounds: each warning's characters, the count kept, and every kept warning's characters together. */
152
+ export const WARNING_BUDGET = { chars: 512, warnings: 10, totalChars: 4096 };
153
+ /**
154
+ * Keep warnings within WARNING_BUDGET in order, cutting a long one with an
155
+ * ellipsis, then one count of what was omitted. Merged reviewed warnings and
156
+ * `fields` reports share the budget.
157
+ */
158
+ /** At most `max` UTF-16 units, never splitting a surrogate pair. */
159
+ function codePointSlice(text, max) {
160
+ const end = max > 0 && /[\uD800-\uDBFF]/.test(text[max - 1] ?? "") ? max - 1 : max;
161
+ return text.slice(0, end);
162
+ }
163
+ export function boundWarnings(warnings) {
164
+ const kept = [];
165
+ let total = 0;
166
+ for (const warning of warnings) {
167
+ const text = warning.length > WARNING_BUDGET.chars ? `${codePointSlice(warning, WARNING_BUDGET.chars - 1)}…` : warning;
168
+ // Leave room for the omitted count.
169
+ if (kept.length >= WARNING_BUDGET.warnings || total + text.length > WARNING_BUDGET.totalChars - 64)
170
+ break;
171
+ kept.push(text);
172
+ total += text.length;
173
+ }
174
+ const omitted = warnings.length - kept.length;
175
+ return omitted ? [...kept, `${omitted} more warning${omitted === 1 ? "" : "s"} omitted.`] : kept;
176
+ }
177
+ /** Declared names listed in one warning, within its character budget. */
178
+ function named(names) {
179
+ let text = "";
180
+ let shown = 0;
181
+ for (const name of names) {
182
+ const next = text ? `${text}, ${name}` : name;
183
+ if (next.length > WARNING_BUDGET.chars - 200)
184
+ break;
185
+ text = next;
186
+ shown += 1;
187
+ }
188
+ return shown < names.length ? `${text}${text ? " and " : ""}${names.length - shown} more` : text;
189
+ }
190
+ /**
191
+ * One warning per `fields` entry that matched nothing in a nonempty response,
192
+ * for contracts that cannot refuse it in advance. It names the entry's index,
193
+ * the segment where matching stopped and the properties the contract declares
194
+ * there, from `declared` (paths in `fields` coordinates). It never lists the
195
+ * response's own keys, which in an open map are data, nor the submitted text.
196
+ * A path some item matched, a path past a scalar (kept whole), or any path of
197
+ * an empty response, is no warning.
198
+ */
199
+ export function fieldsMisses(data, fields, declared = []) {
200
+ if (!fields?.length || empty(data))
201
+ return [];
202
+ const flat = (values) => values.flatMap((value) => (Array.isArray(value) ? flat(value) : [value]));
203
+ const contract = declared.map((path) => segments(path.replace(/^\[\]\.?/, "")));
204
+ return fields.flatMap((field, index) => {
205
+ const path = segments(field);
206
+ let values = flat([data]);
207
+ for (const [depth, name] of path.entries()) {
208
+ const holders = values.filter((value) => record(value) && Object.hasOwn(value, name));
209
+ if (holders.length) {
210
+ values = flat(holders.map((value) => value[name]));
211
+ continue;
212
+ }
213
+ // Selection keeps a scalar or null a path runs past, so only an object lacking the key is a miss.
214
+ if (!values.some(record))
215
+ return [];
216
+ const prefix = path.slice(0, depth);
217
+ const names = [
218
+ ...new Set(contract
219
+ .filter((parts) => parts.length > depth && prefix.every((part, at) => parts[at] === part))
220
+ .map((parts) => parts[depth])),
221
+ ];
222
+ return [
223
+ `fields/${index} matched nothing in this response after value safety and privacy filtering, at segment ${depth + 1}; ${names.length ? `declared there: ${named(names)}` : "the operation declares no properties there"}.`,
224
+ ];
225
+ }
226
+ return [];
227
+ });
228
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * At most 100 paths and 4 KiB of path JSON, `withheld` included, an eighth of
3
+ * the 32 KiB details budget. That holds every path of nine in ten operations
4
+ * in each maintained index; larger responses keep their shallowest paths,
5
+ * roots first. A path over 200 characters, longer than one `fields` entry may
6
+ * be, is never listed.
7
+ */
8
+ export const RESPONSE_BUDGET = { paths: 100, chars: 4096, pathChars: 200 };
9
+ /** `a[].b` → `["a", "b"]`. */
10
+ function names(path) {
11
+ return path.split(".").map((part) => part.replace(/\[\]$/, ""));
12
+ }
13
+ /**
14
+ * The capped response contract for one operation, or undefined when it
15
+ * declares no response properties (`fields` is then refused). Deterministic:
16
+ * paths keep the index's order within each depth.
17
+ */
18
+ export function responseContract(index, op, safety) {
19
+ const declared = index.fields(op).filter((field) => field.in === "response");
20
+ if (declared.length === 0)
21
+ return undefined;
22
+ const anchor = declared.some((field) => field.path.startsWith("[]")) ? "data[]" : "data";
23
+ // The coordinates `fields` validates: an item path of a list body drops its leading `[].`.
24
+ const paths = [
25
+ ...new Set(declared.map((field) => field.path.replace(/^\[\]\.?/, "")).filter((path) => /^[^.[]/.test(path))),
26
+ ];
27
+ // Every root `fields` accepts is listed, including one the index names only beneath it.
28
+ const spelled = new Map();
29
+ for (const path of [...paths.map((path) => names(path)[0]), ...paths]) {
30
+ const key = names(path).join(".");
31
+ if (!spelled.has(key))
32
+ spelled.set(key, path);
33
+ }
34
+ let withholds;
35
+ try {
36
+ withholds = safety.withholds({ method: op.method, template: op.path });
37
+ }
38
+ catch {
39
+ withholds = () => false;
40
+ }
41
+ // The hint reads paths with their list markers, a list body's items included.
42
+ const known = new Set([...spelled]
43
+ .filter(([, path]) => withholds([...(anchor === "data[]" ? ["[]"] : []), ...path.split(".")]))
44
+ .map(([key]) => key));
45
+ const depth = (path) => path.split(".").length;
46
+ const ordered = [...spelled.values()]
47
+ .map((path, at) => ({ path, at }))
48
+ .sort((a, b) => depth(a.path) - depth(b.path) || a.at - b.at);
49
+ const fields = [];
50
+ const withheld = [];
51
+ let chars = 0;
52
+ let kept = 0;
53
+ for (const { path } of ordered) {
54
+ // A path longer than a fields input can be is counted, never listed.
55
+ if (path.length > RESPONSE_BUDGET.pathChars)
56
+ continue;
57
+ const hinted = known.has(names(path).join("."));
58
+ const cost = (JSON.stringify(path).length + 1) * (hinted ? 2 : 1);
59
+ if (kept >= RESPONSE_BUDGET.paths || chars + cost > RESPONSE_BUDGET.chars)
60
+ break;
61
+ fields.push(path);
62
+ if (hinted)
63
+ withheld.push(path);
64
+ chars += cost;
65
+ kept += 1;
66
+ }
67
+ return {
68
+ anchor,
69
+ fields,
70
+ ...(withheld.length ? { withheld } : {}),
71
+ ...(kept < ordered.length ? { omitted: ordered.length - kept } : {}),
72
+ };
73
+ }