@zackbart/connecta 0.15.1 → 0.16.1

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 (335) hide show
  1. package/AGENTS.md +40 -20
  2. package/CHANGELOG.md +653 -0
  3. package/README.md +25 -5
  4. package/bin/connecta.mjs +44 -3
  5. package/dist/access-tokens.d.ts +0 -1
  6. package/dist/access-tokens.js +0 -1
  7. package/dist/activity.d.ts +50 -2
  8. package/dist/activity.js +34 -1
  9. package/dist/apps-shell.d.ts +0 -1
  10. package/dist/apps-shell.js +0 -1
  11. package/dist/auth/bearer.d.ts +0 -1
  12. package/dist/auth/bearer.js +0 -1
  13. package/dist/auth/clerk.d.ts +0 -1
  14. package/dist/auth/clerk.js +45 -9
  15. package/dist/auth/downstream-oauth.d.ts +0 -1
  16. package/dist/auth/downstream-oauth.js +0 -1
  17. package/dist/call-admission.d.ts +0 -1
  18. package/dist/call-admission.js +0 -1
  19. package/dist/catalog-drift.d.ts +99 -0
  20. package/dist/catalog-drift.js +251 -0
  21. package/dist/catalog-fingerprint.d.ts +0 -1
  22. package/dist/catalog-fingerprint.js +0 -1
  23. package/dist/catalog-limits.d.ts +0 -1
  24. package/dist/catalog-limits.js +0 -1
  25. package/dist/catalog-service.d.ts +0 -1
  26. package/dist/catalog-service.js +41 -5
  27. package/dist/catalog.d.ts +11 -1
  28. package/dist/catalog.js +20 -1
  29. package/dist/concurrency.d.ts +0 -1
  30. package/dist/concurrency.js +0 -1
  31. package/dist/connector-scope.d.ts +0 -1
  32. package/dist/connector-scope.js +0 -1
  33. package/dist/connectors/api.d.ts +29 -21
  34. package/dist/connectors/api.js +45 -19
  35. package/dist/connectors/guarded-fetch.d.ts +106 -0
  36. package/dist/connectors/guarded-fetch.js +289 -0
  37. package/dist/connectors/remote-mcp.d.ts +0 -1
  38. package/dist/connectors/remote-mcp.js +0 -1
  39. package/dist/credentials.d.ts +0 -1
  40. package/dist/credentials.js +0 -1
  41. package/dist/errors.d.ts +30 -3
  42. package/dist/errors.js +1 -1
  43. package/dist/execute.d.ts +0 -1
  44. package/dist/execute.js +0 -1
  45. package/dist/executor-admission.d.ts +11 -1
  46. package/dist/executor-admission.js +28 -1
  47. package/dist/executor-result.d.ts +0 -1
  48. package/dist/executor-result.js +0 -1
  49. package/dist/executors/quickjs-child.d.ts +0 -1
  50. package/dist/executors/quickjs-child.js +0 -1
  51. package/dist/executors/quickjs-protocol.d.ts +0 -1
  52. package/dist/executors/quickjs-protocol.js +0 -1
  53. package/dist/executors/quickjs-runtime.d.ts +0 -1
  54. package/dist/executors/quickjs-runtime.js +0 -1
  55. package/dist/executors/quickjs.d.ts +0 -1
  56. package/dist/executors/quickjs.js +2 -1
  57. package/dist/favicon.d.ts +0 -1
  58. package/dist/favicon.js +0 -1
  59. package/dist/index.d.ts +2 -3
  60. package/dist/index.js +24 -7
  61. package/dist/invocation.d.ts +0 -1
  62. package/dist/invocation.js +0 -1
  63. package/dist/json-schema.d.ts +0 -1
  64. package/dist/json-schema.js +0 -1
  65. package/dist/mcp-result.d.ts +0 -1
  66. package/dist/mcp-result.js +0 -1
  67. package/dist/meta-tools.d.ts +0 -1
  68. package/dist/meta-tools.js +0 -1
  69. package/dist/node.d.ts +0 -1
  70. package/dist/node.js +0 -1
  71. package/dist/operator-ui/generated.d.ts +2 -3
  72. package/dist/operator-ui/generated.js +3 -4
  73. package/dist/operator-ui/model.d.ts +10 -1
  74. package/dist/operator-ui/model.js +0 -1
  75. package/dist/operator-ui/view.d.ts +167 -0
  76. package/dist/operator-ui/view.js +270 -0
  77. package/dist/providers/cloudflare.d.ts +0 -1
  78. package/dist/providers/cloudflare.js +261 -299
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -61
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -79
  83. package/dist/providers/notion.d.ts +0 -1
  84. package/dist/providers/notion.js +87 -42
  85. package/dist/providers/stripe.d.ts +13 -1
  86. package/dist/providers/stripe.js +31 -60
  87. package/dist/registry.d.ts +34 -2
  88. package/dist/registry.js +85 -5
  89. package/dist/retired-toolkits.d.ts +0 -1
  90. package/dist/retired-toolkits.js +0 -1
  91. package/dist/routes/access-tokens.d.ts +0 -1
  92. package/dist/routes/access-tokens.js +0 -1
  93. package/dist/routes/activity.d.ts +0 -1
  94. package/dist/routes/activity.js +0 -1
  95. package/dist/routes/credentials.d.ts +0 -1
  96. package/dist/routes/credentials.js +0 -1
  97. package/dist/routes/mcp.d.ts +0 -1
  98. package/dist/routes/mcp.js +0 -1
  99. package/dist/routes/oauth.d.ts +0 -1
  100. package/dist/routes/oauth.js +0 -1
  101. package/dist/routes/shared.d.ts +2 -1
  102. package/dist/routes/shared.js +0 -1
  103. package/dist/routes/ui.d.ts +0 -1
  104. package/dist/routes/ui.js +0 -1
  105. package/dist/server.d.ts +0 -1
  106. package/dist/server.js +11 -1
  107. package/dist/skills.d.ts +0 -1
  108. package/dist/skills.js +0 -1
  109. package/dist/storage/file.d.ts +0 -1
  110. package/dist/storage/file.js +0 -1
  111. package/dist/storage/memory.d.ts +0 -1
  112. package/dist/storage/memory.js +0 -1
  113. package/dist/timeout.d.ts +0 -1
  114. package/dist/timeout.js +0 -1
  115. package/dist/tool-safety.d.ts +0 -1
  116. package/dist/tool-safety.js +0 -1
  117. package/dist/types.d.ts +45 -1
  118. package/dist/types.js +0 -1
  119. package/dist/ui.d.ts +0 -1
  120. package/dist/ui.js +13 -137
  121. package/dist/validate.d.ts +13 -15
  122. package/dist/validate.js +24 -11
  123. package/dist/version.d.ts +1 -2
  124. package/dist/version.js +1 -2
  125. package/documentation/architecture.md +186 -5
  126. package/documentation/auth.md +11 -0
  127. package/documentation/call-admission.md +142 -6
  128. package/documentation/cloudflare.md +92 -10
  129. package/documentation/code-first-exploration.md +2 -2
  130. package/documentation/code-mode.md +14 -14
  131. package/documentation/connector-guides.md +172 -5
  132. package/documentation/connectors.md +89 -0
  133. package/documentation/linear.md +20 -2
  134. package/documentation/mcp-ui-design.md +2 -2
  135. package/documentation/meta-tools.md +7 -1
  136. package/documentation/mixpanel.md +42 -9
  137. package/documentation/notion.md +23 -2
  138. package/documentation/operations.md +325 -5
  139. package/documentation/operator-ui.md +120 -5
  140. package/documentation/provider-audit.md +160 -0
  141. package/documentation/provider-conventions.md +667 -0
  142. package/documentation/request-admission.md +167 -5
  143. package/documentation/rich-output-design.md +1 -2
  144. package/documentation/stripe.md +15 -0
  145. package/documentation/upgrading.md +454 -0
  146. package/ethos.md +14 -9
  147. package/examples/worker/README.md +102 -12
  148. package/examples/worker/src/index.ts +46 -1
  149. package/examples/worker/wrangler.jsonc +14 -1
  150. package/package.json +10 -6
  151. package/templates/node/.dockerignore +10 -0
  152. package/templates/node/.env.example +40 -3
  153. package/templates/node/AGENTS.md +16 -1
  154. package/templates/node/Dockerfile +49 -0
  155. package/templates/node/README.md +117 -2
  156. package/templates/node/docker-compose.yml +39 -0
  157. package/templates/node/package.json +1 -1
  158. package/templates/node/src/file-activity.ts +182 -0
  159. package/templates/node/src/index.ts +80 -4
  160. package/assets/connecta-clay-hero.png +0 -0
  161. package/dist/access-tokens.d.ts.map +0 -1
  162. package/dist/access-tokens.js.map +0 -1
  163. package/dist/activity.d.ts.map +0 -1
  164. package/dist/activity.js.map +0 -1
  165. package/dist/apps-shell.d.ts.map +0 -1
  166. package/dist/apps-shell.js.map +0 -1
  167. package/dist/auth/bearer.d.ts.map +0 -1
  168. package/dist/auth/bearer.js.map +0 -1
  169. package/dist/auth/clerk.d.ts.map +0 -1
  170. package/dist/auth/clerk.js.map +0 -1
  171. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  172. package/dist/auth/downstream-oauth.js.map +0 -1
  173. package/dist/call-admission.d.ts.map +0 -1
  174. package/dist/call-admission.js.map +0 -1
  175. package/dist/catalog-fingerprint.d.ts.map +0 -1
  176. package/dist/catalog-fingerprint.js.map +0 -1
  177. package/dist/catalog-limits.d.ts.map +0 -1
  178. package/dist/catalog-limits.js.map +0 -1
  179. package/dist/catalog-service.d.ts.map +0 -1
  180. package/dist/catalog-service.js.map +0 -1
  181. package/dist/catalog.d.ts.map +0 -1
  182. package/dist/catalog.js.map +0 -1
  183. package/dist/concurrency.d.ts.map +0 -1
  184. package/dist/concurrency.js.map +0 -1
  185. package/dist/connector-scope.d.ts.map +0 -1
  186. package/dist/connector-scope.js.map +0 -1
  187. package/dist/connectors/api.d.ts.map +0 -1
  188. package/dist/connectors/api.js.map +0 -1
  189. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  190. package/dist/connectors/remote-mcp.js.map +0 -1
  191. package/dist/credentials.d.ts.map +0 -1
  192. package/dist/credentials.js.map +0 -1
  193. package/dist/errors.d.ts.map +0 -1
  194. package/dist/errors.js.map +0 -1
  195. package/dist/execute.d.ts.map +0 -1
  196. package/dist/execute.js.map +0 -1
  197. package/dist/executor-admission.d.ts.map +0 -1
  198. package/dist/executor-admission.js.map +0 -1
  199. package/dist/executor-result.d.ts.map +0 -1
  200. package/dist/executor-result.js.map +0 -1
  201. package/dist/executors/quickjs-child.d.ts.map +0 -1
  202. package/dist/executors/quickjs-child.js.map +0 -1
  203. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  204. package/dist/executors/quickjs-protocol.js.map +0 -1
  205. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  206. package/dist/executors/quickjs-runtime.js.map +0 -1
  207. package/dist/executors/quickjs.d.ts.map +0 -1
  208. package/dist/executors/quickjs.js.map +0 -1
  209. package/dist/favicon.d.ts.map +0 -1
  210. package/dist/favicon.js.map +0 -1
  211. package/dist/index.d.ts.map +0 -1
  212. package/dist/index.js.map +0 -1
  213. package/dist/invocation.d.ts.map +0 -1
  214. package/dist/invocation.js.map +0 -1
  215. package/dist/json-schema.d.ts.map +0 -1
  216. package/dist/json-schema.js.map +0 -1
  217. package/dist/mcp-result.d.ts.map +0 -1
  218. package/dist/mcp-result.js.map +0 -1
  219. package/dist/meta-tools.d.ts.map +0 -1
  220. package/dist/meta-tools.js.map +0 -1
  221. package/dist/node.d.ts.map +0 -1
  222. package/dist/node.js.map +0 -1
  223. package/dist/operator-ui/generated.d.ts.map +0 -1
  224. package/dist/operator-ui/generated.js.map +0 -1
  225. package/dist/operator-ui/model.d.ts.map +0 -1
  226. package/dist/operator-ui/model.js.map +0 -1
  227. package/dist/providers/cloudflare.d.ts.map +0 -1
  228. package/dist/providers/cloudflare.js.map +0 -1
  229. package/dist/providers/linear.d.ts.map +0 -1
  230. package/dist/providers/linear.js.map +0 -1
  231. package/dist/providers/mixpanel.d.ts.map +0 -1
  232. package/dist/providers/mixpanel.js.map +0 -1
  233. package/dist/providers/notion.d.ts.map +0 -1
  234. package/dist/providers/notion.js.map +0 -1
  235. package/dist/providers/stripe.d.ts.map +0 -1
  236. package/dist/providers/stripe.js.map +0 -1
  237. package/dist/registry.d.ts.map +0 -1
  238. package/dist/registry.js.map +0 -1
  239. package/dist/retired-toolkits.d.ts.map +0 -1
  240. package/dist/retired-toolkits.js.map +0 -1
  241. package/dist/routes/access-tokens.d.ts.map +0 -1
  242. package/dist/routes/access-tokens.js.map +0 -1
  243. package/dist/routes/activity.d.ts.map +0 -1
  244. package/dist/routes/activity.js.map +0 -1
  245. package/dist/routes/credentials.d.ts.map +0 -1
  246. package/dist/routes/credentials.js.map +0 -1
  247. package/dist/routes/mcp.d.ts.map +0 -1
  248. package/dist/routes/mcp.js.map +0 -1
  249. package/dist/routes/oauth.d.ts.map +0 -1
  250. package/dist/routes/oauth.js.map +0 -1
  251. package/dist/routes/shared.d.ts.map +0 -1
  252. package/dist/routes/shared.js.map +0 -1
  253. package/dist/routes/ui.d.ts.map +0 -1
  254. package/dist/routes/ui.js.map +0 -1
  255. package/dist/server.d.ts.map +0 -1
  256. package/dist/server.js.map +0 -1
  257. package/dist/skills.d.ts.map +0 -1
  258. package/dist/skills.js.map +0 -1
  259. package/dist/storage/file.d.ts.map +0 -1
  260. package/dist/storage/file.js.map +0 -1
  261. package/dist/storage/memory.d.ts.map +0 -1
  262. package/dist/storage/memory.js.map +0 -1
  263. package/dist/timeout.d.ts.map +0 -1
  264. package/dist/timeout.js.map +0 -1
  265. package/dist/tool-safety.d.ts.map +0 -1
  266. package/dist/tool-safety.js.map +0 -1
  267. package/dist/types.d.ts.map +0 -1
  268. package/dist/types.js.map +0 -1
  269. package/dist/ui.d.ts.map +0 -1
  270. package/dist/ui.js.map +0 -1
  271. package/dist/validate.d.ts.map +0 -1
  272. package/dist/validate.js.map +0 -1
  273. package/dist/version.d.ts.map +0 -1
  274. package/dist/version.js.map +0 -1
  275. package/examples/node/README.md +0 -52
  276. package/examples/node/src/index.ts +0 -71
  277. package/src/access-tokens.ts +0 -289
  278. package/src/activity.ts +0 -234
  279. package/src/apps-shell.ts +0 -370
  280. package/src/auth/bearer.ts +0 -61
  281. package/src/auth/clerk.ts +0 -553
  282. package/src/auth/downstream-oauth.ts +0 -575
  283. package/src/call-admission.ts +0 -521
  284. package/src/catalog-fingerprint.ts +0 -52
  285. package/src/catalog-limits.ts +0 -11
  286. package/src/catalog-service.ts +0 -1153
  287. package/src/catalog.ts +0 -790
  288. package/src/concurrency.ts +0 -39
  289. package/src/connector-scope.ts +0 -66
  290. package/src/connectors/api.ts +0 -159
  291. package/src/connectors/remote-mcp.ts +0 -1090
  292. package/src/credentials.ts +0 -428
  293. package/src/errors.ts +0 -364
  294. package/src/execute.ts +0 -1406
  295. package/src/executor-admission.ts +0 -328
  296. package/src/executor-result.ts +0 -107
  297. package/src/executors/quickjs-child.ts +0 -172
  298. package/src/executors/quickjs-protocol.ts +0 -99
  299. package/src/executors/quickjs-runtime.ts +0 -470
  300. package/src/executors/quickjs.ts +0 -776
  301. package/src/favicon.ts +0 -53
  302. package/src/index.ts +0 -666
  303. package/src/invocation.ts +0 -655
  304. package/src/json-schema.ts +0 -11
  305. package/src/mcp-result.ts +0 -43
  306. package/src/meta-tools.ts +0 -1607
  307. package/src/node.ts +0 -206
  308. package/src/operator-ui/browser.css +0 -423
  309. package/src/operator-ui/browser.ts +0 -1281
  310. package/src/operator-ui/generated.ts +0 -4
  311. package/src/operator-ui/model.ts +0 -111
  312. package/src/providers/cloudflare.ts +0 -3999
  313. package/src/providers/linear.ts +0 -302
  314. package/src/providers/mixpanel.ts +0 -229
  315. package/src/providers/notion.ts +0 -1879
  316. package/src/providers/stripe.ts +0 -307
  317. package/src/registry.ts +0 -1085
  318. package/src/retired-toolkits.ts +0 -14
  319. package/src/routes/access-tokens.ts +0 -115
  320. package/src/routes/activity.ts +0 -215
  321. package/src/routes/credentials.ts +0 -254
  322. package/src/routes/mcp.ts +0 -429
  323. package/src/routes/oauth.ts +0 -322
  324. package/src/routes/shared.ts +0 -247
  325. package/src/routes/ui.ts +0 -152
  326. package/src/server.ts +0 -195
  327. package/src/skills.ts +0 -271
  328. package/src/storage/file.ts +0 -123
  329. package/src/storage/memory.ts +0 -44
  330. package/src/timeout.ts +0 -84
  331. package/src/tool-safety.ts +0 -15
  332. package/src/types.ts +0 -523
  333. package/src/ui.ts +0 -744
  334. package/src/validate.ts +0 -387
  335. package/src/version.ts +0 -7
@@ -0,0 +1,251 @@
1
+ // Web-API only, like the rest of the core: a manifest comparison that ran on
2
+ // Node but not on Workers would leave half the deployments unable to tell a
3
+ // stale allowlist from a current one.
4
+ import { boundedEchoText } from "./errors.js";
5
+ /**
6
+ * Byte budget for the one string a drift report carries. An ISO timestamp is
7
+ * 24 bytes; anything near this bound is a plugin sending something else.
8
+ */
9
+ const MAX_OBSERVED_AT_BYTES = 64;
10
+ /** A count, or 0 when the seam returned something that is not one. */
11
+ function boundedCount(value) {
12
+ return typeof value === "number" && Number.isFinite(value)
13
+ ? Math.max(0, Math.trunc(value))
14
+ : 0;
15
+ }
16
+ /**
17
+ * Rebuild a drift report as four counts and a bounded timestamp.
18
+ *
19
+ * `Connector.catalogDrift()` sits on the open plugin seam, and what it returns
20
+ * lands in the body of unauthenticated `/health` and on connector status.
21
+ * TypeScript constrains neither an extra enumerable property nor the length of
22
+ * `observedAt` at runtime, so "counts and nothing else" is *made* true here —
23
+ * at the boundary where third-party output becomes a response — rather than
24
+ * trusted. The activity path reconstructs its five fields for the same reason.
25
+ */
26
+ export function boundedCatalogDrift(report) {
27
+ if (!report || typeof report !== "object")
28
+ return undefined;
29
+ return {
30
+ observedAt: boundedEchoText(typeof report.observedAt === "string" ? report.observedAt : "", MAX_OBSERVED_AT_BYTES),
31
+ unclassifiedTools: boundedCount(report.unclassifiedTools),
32
+ unservedTools: boundedCount(report.unservedTools),
33
+ annotationConflicts: boundedCount(report.annotationConflicts),
34
+ schemaChanges: boundedCount(report.schemaChanges),
35
+ };
36
+ }
37
+ const encoder = new TextEncoder();
38
+ /** Deterministic JSON: object keys sorted, so key order is not a schema change. */
39
+ function canonicalize(value) {
40
+ if (Array.isArray(value))
41
+ return value.map(canonicalize);
42
+ if (value === null || typeof value !== "object")
43
+ return value;
44
+ const entries = Object.entries(value)
45
+ .filter(([, item]) => item !== undefined)
46
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
47
+ return Object.fromEntries(entries.map(([key, item]) => [key, canonicalize(item)]));
48
+ }
49
+ /**
50
+ * Digest the schemas of one downstream tool.
51
+ *
52
+ * Canonical rather than literal, unlike the catalog fingerprint in
53
+ * `src/catalog-fingerprint.ts`: that one is deliberately conservative because a
54
+ * spurious cache write is cheap, while a spurious drift finding spends a
55
+ * maintainer's attention on a downstream that reordered its JSON keys.
56
+ * Description and annotations are excluded — a reworded description is P1's
57
+ * business, and an annotation change is already its own drift category.
58
+ */
59
+ export async function vettedSchemaDigest(tool) {
60
+ const bytes = encoder.encode(JSON.stringify(canonicalize({
61
+ inputSchema: tool.inputSchema ?? null,
62
+ outputSchema: tool.outputSchema ?? null,
63
+ })));
64
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
65
+ return `sha256:${[...digest]
66
+ .map((byte) => byte.toString(16).padStart(2, "0"))
67
+ .join("")}`;
68
+ }
69
+ /**
70
+ * Build a provider's manifest from the lists it already maintains.
71
+ *
72
+ * Throws when a name is classified twice, because a tool that is both a read
73
+ * and a write is a review mistake that must not boot: the annotation the
74
+ * connector would apply depends on which branch runs first, and "safe by
75
+ * default" cannot be decided by ordering.
76
+ */
77
+ export function vettedCatalog(input) {
78
+ const tools = new Map();
79
+ const digestFor = (name) => input.schemaDigests?.[name];
80
+ for (const name of input.reads) {
81
+ const digest = digestFor(name);
82
+ tools.set(name, {
83
+ verdict: "read-only",
84
+ ...(digest !== undefined ? { schemaDigest: digest } : {}),
85
+ });
86
+ }
87
+ for (const [name, verdict] of input.writes) {
88
+ if (tools.has(name)) {
89
+ throw new Error(`vettedCatalog() classified "${name}" as both a read and a write.`);
90
+ }
91
+ const digest = digestFor(name);
92
+ tools.set(name, {
93
+ verdict,
94
+ ...(digest !== undefined ? { schemaDigest: digest } : {}),
95
+ });
96
+ }
97
+ for (const name of Object.keys(input.schemaDigests ?? {})) {
98
+ if (!tools.has(name)) {
99
+ throw new Error(`vettedCatalog() recorded a schema digest for unclassified tool "${name}".`);
100
+ }
101
+ }
102
+ return { version: 1, tools };
103
+ }
104
+ /**
105
+ * Fill in downstream silence; keep reviewed destructive tools fail-closed.
106
+ *
107
+ * Silence is what a vetted classification is for, and an explicit downstream
108
+ * annotation otherwise wins in both directions. `destructiveHint: true` or
109
+ * `readOnlyHint: false` on a classified read is the downstream telling us this
110
+ * release's allowlist is stale; `readOnlyHint: true` on a name no release has
111
+ * classified says the same thing from the other side. The single place a
112
+ * vetted verdict still overrides the downstream is a name this release
113
+ * reviewed and filed destructive: there connecta knows what the tool does, and
114
+ * a claim to the contrary is a downstream bug rather than news
115
+ * ([#310](https://github.com/zackbart/connecta/issues/310),
116
+ * [#315](https://github.com/zackbart/connecta/issues/315)).
117
+ */
118
+ function applyVettedSafety(catalog, definition) {
119
+ const downstream = definition.annotations ?? {};
120
+ const record = catalog.tools.get(definition.name);
121
+ if (record?.verdict === "read-only") {
122
+ if (downstream.destructiveHint === true ||
123
+ downstream.readOnlyHint === false) {
124
+ return definition;
125
+ }
126
+ return {
127
+ ...definition,
128
+ annotations: {
129
+ ...downstream,
130
+ readOnlyHint: true,
131
+ destructiveHint: downstream.destructiveHint ?? false,
132
+ },
133
+ };
134
+ }
135
+ if (record?.verdict === "destructive") {
136
+ return {
137
+ ...definition,
138
+ annotations: {
139
+ ...downstream,
140
+ readOnlyHint: false,
141
+ destructiveHint: true,
142
+ },
143
+ };
144
+ }
145
+ // Maintained additive creates and tools this release has never seen land
146
+ // here alike. Fill-in only: a silent tool is not read-only, so drift still
147
+ // fails closed onto `call_destructive_tool`, and neither population gets a
148
+ // `destructiveHint` it has not earned. A tool that arrives explicitly
149
+ // read-only keeps that annotation — on a name no release has reviewed, the
150
+ // downstream's own word is the only evidence there is, and rewriting it
151
+ // would be an overrule rather than a fill-in.
152
+ return {
153
+ ...definition,
154
+ annotations: {
155
+ ...downstream,
156
+ readOnlyHint: downstream.readOnlyHint ?? false,
157
+ },
158
+ };
159
+ }
160
+ /**
161
+ * Whether the downstream's own annotation contradicts what a release reviewed.
162
+ *
163
+ * Only an *explicit* contradiction counts. Silence is the ordinary case the
164
+ * classification exists to fill, and an unclassified tool is already counted
165
+ * as an addition rather than twice.
166
+ */
167
+ function contradicts(record, definition) {
168
+ const downstream = definition.annotations ?? {};
169
+ if (record.verdict === "read-only") {
170
+ return (downstream.readOnlyHint === false || downstream.destructiveHint === true);
171
+ }
172
+ return downstream.readOnlyHint === true;
173
+ }
174
+ /**
175
+ * Compare a live catalog with the manifest and count what moved.
176
+ *
177
+ * Counts only, and by construction: there is nowhere here to put a tool name,
178
+ * a schema, or a downstream string, so no later surface has to remember to
179
+ * strip one. Reads nothing but the tools it was handed — the caller already
180
+ * fetched them to serve a request, and this function never fetches anything.
181
+ */
182
+ export async function detectCatalogDrift(catalog, tools) {
183
+ let unclassifiedTools = 0;
184
+ let annotationConflicts = 0;
185
+ let schemaChanges = 0;
186
+ const served = new Set();
187
+ for (const definition of tools) {
188
+ served.add(definition.name);
189
+ const record = catalog.tools.get(definition.name);
190
+ if (!record) {
191
+ unclassifiedTools += 1;
192
+ continue;
193
+ }
194
+ if (contradicts(record, definition))
195
+ annotationConflicts += 1;
196
+ // A manifest that recorded no digest for this tool cannot have an opinion
197
+ // about its schema, so it does not pay for a hash either.
198
+ if (record.schemaDigest !== undefined &&
199
+ record.schemaDigest !== (await vettedSchemaDigest(definition))) {
200
+ schemaChanges += 1;
201
+ }
202
+ }
203
+ let unservedTools = 0;
204
+ for (const name of catalog.tools.keys()) {
205
+ if (!served.has(name))
206
+ unservedTools += 1;
207
+ }
208
+ return {
209
+ unclassifiedTools,
210
+ unservedTools,
211
+ annotationConflicts,
212
+ schemaChanges,
213
+ };
214
+ }
215
+ /**
216
+ * Wrap a hosted-MCP connector in its vetted manifest: the classification the
217
+ * catalog is normalized with, and the drift check that rides the same listing.
218
+ *
219
+ * The check happens where the tools are already in hand and still unmodified —
220
+ * after the downstream answered, before the classification is applied. It adds
221
+ * no request of its own, which is the whole boundary: connecta watches a
222
+ * contract while it is serving a refresh the deployment asked for, and never
223
+ * initiates one to go looking ([#179](https://github.com/zackbart/connecta/issues/179),
224
+ * [#343](https://github.com/zackbart/connecta/issues/343)).
225
+ */
226
+ export function withVettedCatalog(connector, catalog) {
227
+ let observed;
228
+ return {
229
+ ...connector,
230
+ async listTools(ctx) {
231
+ const downstream = await connector.listTools(ctx);
232
+ try {
233
+ observed = {
234
+ observedAt: new Date().toISOString(),
235
+ ...(await detectCatalogDrift(catalog, downstream)),
236
+ };
237
+ }
238
+ catch (error) {
239
+ // A drift check is a report about a catalog, never a condition for
240
+ // serving one. Keep the last good observation rather than replacing it
241
+ // with a lie, and let the refresh through.
242
+ ctx.logger.warn(`[connecta] connector "${connector.id}" catalog drift check failed: ` +
243
+ (error instanceof Error ? error.message : String(error)));
244
+ }
245
+ return downstream.map((definition) => applyVettedSafety(catalog, definition));
246
+ },
247
+ catalogDrift() {
248
+ return observed;
249
+ },
250
+ };
251
+ }
@@ -17,4 +17,3 @@ export declare function fingerprintSerializedCatalog(serializedTools: string): P
17
17
  * any serialized field must change the digest.
18
18
  */
19
19
  export declare function snapshotCatalog(tools: ToolDef[]): Promise<CatalogSnapshot>;
20
- //# sourceMappingURL=catalog-fingerprint.d.ts.map
@@ -31,4 +31,3 @@ export async function snapshotCatalog(tools) {
31
31
  serializedBytes: bytes,
32
32
  };
33
33
  }
34
- //# sourceMappingURL=catalog-fingerprint.js.map
@@ -7,4 +7,3 @@ export declare const MAX_CATALOG_TOOLS = 100000;
7
7
  export declare const MAX_SERIALIZED_CATALOG_BYTES: number;
8
8
  /** Persisted values stay comfortably below platform per-value limits. */
9
9
  export declare const MAX_CATALOG_CHUNK_BYTES: number;
10
- //# sourceMappingURL=catalog-limits.d.ts.map
@@ -7,4 +7,3 @@ export const MAX_CATALOG_TOOLS = 100_000;
7
7
  export const MAX_SERIALIZED_CATALOG_BYTES = 32 * 1024 * 1024;
8
8
  /** Persisted values stay comfortably below platform per-value limits. */
9
9
  export const MAX_CATALOG_CHUNK_BYTES = 1024 * 1024;
10
- //# sourceMappingURL=catalog-limits.js.map
@@ -247,4 +247,3 @@ export declare function flatSearchResult(page: CatalogSearchPage): {
247
247
  hasMore: boolean;
248
248
  };
249
249
  export {};
250
- //# sourceMappingURL=catalog-service.d.ts.map
@@ -1,4 +1,4 @@
1
- import { compactDiscoverySchema, compactSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
1
+ import { compactDiscoverySchema, compactSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
2
2
  import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
3
3
  import { boundedEchoText, classifyCallError, framingError, } from "./errors.js";
4
4
  import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName, } from "./skills.js";
@@ -10,6 +10,12 @@ export const MAX_DESCRIBE_ADDRESSES = 100;
10
10
  export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
11
11
  const MAX_QUERY_TERMS = 8;
12
12
  const MAX_QUERY_TERM_LENGTH = 64;
13
+ /**
14
+ * Connector IDs a no-match search will name back. Three is enough to point at
15
+ * the connector the query already named without turning a miss into a listing
16
+ * of the deployment.
17
+ */
18
+ const MAX_IDENTITY_CONNECTORS = 3;
13
19
  const encoder = new TextEncoder();
14
20
  /** Clip one echoed query term without splitting a non-BMP code point. */
15
21
  function boundedQueryTerm(term) {
@@ -561,6 +567,36 @@ export class CatalogService {
561
567
  required: connectorGuideRequired(scopedConnector),
562
568
  }
563
569
  : undefined;
570
+ // A connector's own id — the address prefix the caller already has — and
571
+ // the title it is displayed under are the most natural first query terms,
572
+ // and neither is a document in the lexical index. Indexing them would move
573
+ // ranking for every query that already matches tools, so instead a search
574
+ // that matched nothing asks the same lexical question of connector
575
+ // identity and corrects its own sentence: the deployment plainly has this
576
+ // capability, and one scoped browse away are its tools. Unscoped only —
577
+ // guidance for an explicit scope already names that connector rather than
578
+ // claiming the deployment has nothing.
579
+ const identityConnectorIds = matches.length === 0 && !isBrowse && !unsearchableQuery && !scopedConnector
580
+ ? connectors
581
+ // The full query, not `analyzedTerms`: that cap exists to bound
582
+ // the serialized term fields, and ranking already reads every
583
+ // term. A ninth term is a real search term, and a search that
584
+ // ranked against it must not deny the connector it names.
585
+ .filter((connector) => queryTerms.some((term) => matchesLexicalTerm(connector.id, term) ||
586
+ (connector.title !== undefined &&
587
+ matchesLexicalTerm(connector.title, term))))
588
+ .map((connector) => connector.id)
589
+ : [];
590
+ const namedIdentityConnectors = identityConnectorIds
591
+ .slice(0, MAX_IDENTITY_CONNECTORS)
592
+ .map((id) => `"${id}"`)
593
+ .join(", ");
594
+ const unnamedIdentityConnectors = identityConnectorIds.length - MAX_IDENTITY_CONNECTORS;
595
+ const identityGuidance = identityConnectorIds.length === 0
596
+ ? undefined
597
+ : `No matching ${safetyLabel}capability was found${unavailableCatalogs === 0 ? "" : " in the catalogs that answered"}, but the query names configured connector${identityConnectorIds.length === 1 ? "" : "s"} ${namedIdentityConnectors}${unnamedIdentityConnectors > 0
598
+ ? ` and ${unnamedIdentityConnectors} more`
599
+ : ""}. Scope by connector and browse with an empty query to list the tools there.${filterRecovery}`;
564
600
  // A scope that resolved to nothing is the same silence one step earlier in
565
601
  // the lookup: no connector resolved, so no catalog was even attempted, so
566
602
  // no catalog failed and the unavailable path below never fires. Echo only
@@ -604,9 +640,10 @@ export class CatalogService {
604
640
  : scopedGuide?.required
605
641
  ? `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Fetch queryAnalysis.guide before calling, then refine terms or browse with an empty query.${filterRecovery}`
606
642
  : `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
607
- : unavailableCatalogs === 0
608
- ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
609
- : `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`))
643
+ : (identityGuidance ??
644
+ (unavailableCatalogs === 0
645
+ ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
646
+ : `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`))))
610
647
  : matchMode === "partial"
611
648
  ? scopedConnector
612
649
  ? `No single tool on connector "${scopedConnector.id}" matched every term. Split distinct intents into separate searches.`
@@ -786,4 +823,3 @@ export function flatSearchResult(page) {
786
823
  ...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
787
824
  };
788
825
  }
789
- //# sourceMappingURL=catalog-service.js.map
package/dist/catalog.d.ts CHANGED
@@ -18,6 +18,17 @@ export interface RankedTool {
18
18
  exactName: boolean;
19
19
  matchedTermCount: number;
20
20
  }
21
+ /**
22
+ * Whether one query term matches a whole token of arbitrary text, under the
23
+ * same inflection rules the tool index uses.
24
+ *
25
+ * Connector identity — an `id` or a `title` — is deliberately not a document in
26
+ * that index: making it one would move ranking for every query that already
27
+ * matches tools. This lets a caller ask the index's question of a string that
28
+ * never became a document, which is what the no-match analysis needs to tell
29
+ * "nothing like this exists here" from "that word is a connector".
30
+ */
31
+ export declare function matchesLexicalTerm(text: string, term: string): boolean;
21
32
  export interface LexicalCorpusStatistics {
22
33
  documentCount: number;
23
34
  documentFrequency: ReadonlyMap<string, number>;
@@ -72,4 +83,3 @@ export interface SchemaObjectKeys {
72
83
  * fields.
73
84
  */
74
85
  export declare function schemaObjectKeys(schema: JsonSchema | undefined): SchemaObjectKeys | undefined;
75
- //# sourceMappingURL=catalog.d.ts.map
package/dist/catalog.js CHANGED
@@ -166,6 +166,26 @@ function matchingTokenCandidates(term) {
166
166
  }
167
167
  return candidates;
168
168
  }
169
+ /**
170
+ * Whether one query term matches a whole token of arbitrary text, under the
171
+ * same inflection rules the tool index uses.
172
+ *
173
+ * Connector identity — an `id` or a `title` — is deliberately not a document in
174
+ * that index: making it one would move ranking for every query that already
175
+ * matches tools. This lets a caller ask the index's question of a string that
176
+ * never became a document, which is what the no-match analysis needs to tell
177
+ * "nothing like this exists here" from "that word is a connector".
178
+ */
179
+ export function matchesLexicalTerm(text, term) {
180
+ const tokens = new Set(lexicalTokens(text));
181
+ if (tokens.size === 0)
182
+ return false;
183
+ for (const candidate of matchingTokenCandidates(term)) {
184
+ if (tokens.has(candidate))
185
+ return true;
186
+ }
187
+ return false;
188
+ }
169
189
  /**
170
190
  * Compute query-specific document frequencies across every available catalog.
171
191
  * The caller does this once per search and shares the result with each
@@ -636,4 +656,3 @@ function objectKeys(schema, defs, seen, depth) {
636
656
  }
637
657
  return undefined;
638
658
  }
639
- //# sourceMappingURL=catalog.js.map
@@ -1,4 +1,3 @@
1
1
  export declare function resolveDiscoveryConcurrency(value: number | undefined): number;
2
2
  /** Run `fn` over `items` with at most `limit` operations in flight. */
3
3
  export declare function mapSettledWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<PromiseSettledResult<R>[]>;
4
- //# sourceMappingURL=concurrency.d.ts.map
@@ -28,4 +28,3 @@ export async function mapSettledWithConcurrency(items, limit, fn) {
28
28
  await Promise.all(workers);
29
29
  return settled;
30
30
  }
31
- //# sourceMappingURL=concurrency.js.map
@@ -13,4 +13,3 @@ export type DeferredWork = (promise: Promise<unknown>) => void;
13
13
  * again after this returns.
14
14
  */
15
15
  export declare function closeConnectorScope(connector: Connector, ctx: ConnectorContext, defer?: DeferredWork): Promise<void>;
16
- //# sourceMappingURL=connector-scope.d.ts.map
@@ -52,4 +52,3 @@ export async function closeConnectorScope(connector, ctx, defer) {
52
52
  // The scope is over whether or not the connector managed to clean it up.
53
53
  }
54
54
  }
55
- //# sourceMappingURL=connector-scope.js.map
@@ -1,16 +1,30 @@
1
1
  import type { Connector, ConnectorCallAdmissionPolicy, ConnectorCredentialConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, CredentialTestResult, JsonSchema, ToolAnnotations } from "../types.js";
2
2
  export interface ApiTool {
3
3
  name: string;
4
- description?: string;
5
- /** A plain JSON Schema object describing the tool input. */
4
+ /**
5
+ * Required, non-empty. Discovery has nothing else to go on: a nameless
6
+ * capability costs the agent a guess, and a guess costs a wrong call.
7
+ */
8
+ description: string;
9
+ /**
10
+ * A plain JSON Schema object describing the tool input. Optional, but what
11
+ * you supply must be a schema the validator can compile — `api()` refuses to
12
+ * construct otherwise.
13
+ */
6
14
  inputSchema?: JsonSchema;
7
15
  /** A plain JSON Schema object describing the tool's structured output. */
8
16
  outputSchema?: JsonSchema;
9
17
  /**
10
- * Standard MCP-style behavior hints. Only an explicit readOnlyHint: true
11
- * admits the tool to call_tool and execute_code.
18
+ * Standard MCP-style behavior hints, with an explicit `readOnlyHint`
19
+ * required: `true` declares a read and admits the tool to call_tool and
20
+ * execute_code, `false` declares work that must cross
21
+ * `call_destructive_tool` where a host can ask a human. Connecta never
22
+ * infers the classification from a name, a description, a schema, or the
23
+ * other annotations.
12
24
  */
13
- annotations?: ToolAnnotations;
25
+ annotations: ToolAnnotations & {
26
+ readOnlyHint: boolean;
27
+ };
14
28
  handler: (args: any, ctx: ConnectorContext) => Promise<unknown> | unknown;
15
29
  }
16
30
  export interface ApiOptions {
@@ -47,16 +61,6 @@ export interface ApiOptions {
47
61
  * on loose coercion.
48
62
  */
49
63
  validateArgs?: boolean;
50
- /**
51
- * Fail-closed on a tool whose `inputSchema` the validator cannot evaluate
52
- * (default false). The default surfaces such a schema as a one-time warning
53
- * and then passes the raw arguments through, so a broken schema never breaks
54
- * an otherwise working tool. Set true to instead reject those calls with a
55
- * non-retryable `invalid_args` ConnectorCallError, so a schema that cannot be
56
- * enforced never silently admits unvalidated input. Only consulted when
57
- * `validateArgs` is not false.
58
- */
59
- strictValidation?: boolean;
60
64
  tools: ApiTool[];
61
65
  }
62
66
  /**
@@ -64,11 +68,15 @@ export interface ApiOptions {
64
68
  * Tool inputs are plain JSON Schema objects (bring your own zod-to-json-schema
65
69
  * conversion if you prefer zod). call_tool JSON-wraps the handler's return.
66
70
  *
67
- * Arguments are validated against `inputSchema` before the handler runs
68
- * (disable with `validateArgs: false`). Remote MCP inputs are also validated,
69
- * but in the shared invocation path against the request-local downstream
70
- * catalog. These API-only controls stay here because hand-written handlers may
71
- * deliberately accept loose coercion or choose fail-closed schema handling.
71
+ * Every tool declares a description and an explicit `annotations.readOnlyHint`,
72
+ * and any `inputSchema` it carries must compile a tool that fails the
73
+ * contract throws here rather than reaching a catalog. Arguments are then
74
+ * validated against `inputSchema` before the handler runs (disable with
75
+ * `validateArgs: false`, which opts out of enforcement, not out of the schema
76
+ * being real), and a schema that only reveals itself as unenforceable on first
77
+ * use — an unresolvable `$ref`, say — fails the call rather than passing raw
78
+ * arguments through. Remote MCP inputs are also validated, but in the shared
79
+ * invocation path against the request-local downstream catalog, where a
80
+ * downstream's schema is its own affair and stays fail-open.
72
81
  */
73
82
  export declare function api(id: string, opts: ApiOptions): Connector;
74
- //# sourceMappingURL=api.d.ts.map
@@ -1,34 +1,58 @@
1
- import { precompileValidator, validateToolInput } from "../validate.js";
1
+ import { compileValidator, validateToolInput } from "../validate.js";
2
+ /**
3
+ * Enforce the construction contract for one hand-written tool.
4
+ *
5
+ * Everything here is something only the author can supply and no runtime can
6
+ * guess: what the tool does, and whether calling it needs a human's blessing.
7
+ * Guessing either one is how a deployment boots into the wrong shape, so this
8
+ * throws instead. Note what it does *not* do — it never reads a name, verb, or
9
+ * HTTP method to infer a safety class. An unclassified tool is a bug in the
10
+ * deployment, not a puzzle for connecta to solve.
11
+ */
12
+ function checkToolContract(id, tool) {
13
+ const address = `${id}.${tool.name}`;
14
+ if (typeof tool.description !== "string" || tool.description.trim() === "") {
15
+ throw new Error(`api() tool "${address}" needs a non-empty description — it is what an ` +
16
+ "agent reads to choose the tool (convention: imperative one-liner, " +
17
+ 'e.g. "Send an email via Resend").');
18
+ }
19
+ if (typeof tool.annotations?.readOnlyHint !== "boolean") {
20
+ throw new Error(`api() tool "${address}" needs an explicit annotations.readOnlyHint: ` +
21
+ "true for a read, false for work that must cross " +
22
+ "call_destructive_tool. Connecta never infers the classification " +
23
+ "from a tool name, description, schema, or other annotations.");
24
+ }
25
+ if (tool.inputSchema)
26
+ compileValidator(tool.inputSchema, { address });
27
+ }
2
28
  /**
3
29
  * A connector defined entirely in code: static tool defs + fetch handlers.
4
30
  * Tool inputs are plain JSON Schema objects (bring your own zod-to-json-schema
5
31
  * conversion if you prefer zod). call_tool JSON-wraps the handler's return.
6
32
  *
7
- * Arguments are validated against `inputSchema` before the handler runs
8
- * (disable with `validateArgs: false`). Remote MCP inputs are also validated,
9
- * but in the shared invocation path against the request-local downstream
10
- * catalog. These API-only controls stay here because hand-written handlers may
11
- * deliberately accept loose coercion or choose fail-closed schema handling.
33
+ * Every tool declares a description and an explicit `annotations.readOnlyHint`,
34
+ * and any `inputSchema` it carries must compile a tool that fails the
35
+ * contract throws here rather than reaching a catalog. Arguments are then
36
+ * validated against `inputSchema` before the handler runs (disable with
37
+ * `validateArgs: false`, which opts out of enforcement, not out of the schema
38
+ * being real), and a schema that only reveals itself as unenforceable on first
39
+ * use — an unresolvable `$ref`, say — fails the call rather than passing raw
40
+ * arguments through. Remote MCP inputs are also validated, but in the shared
41
+ * invocation path against the request-local downstream catalog, where a
42
+ * downstream's schema is its own affair and stays fail-open.
12
43
  */
13
44
  export function api(id, opts) {
45
+ for (const t of opts.tools)
46
+ checkToolContract(id, t);
14
47
  const defs = opts.tools.map((t) => ({
15
48
  name: t.name,
16
- ...(t.description !== undefined ? { description: t.description } : {}),
49
+ description: t.description,
17
50
  ...(t.inputSchema !== undefined ? { inputSchema: t.inputSchema } : {}),
18
51
  ...(t.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}),
19
- ...(t.annotations !== undefined ? { annotations: t.annotations } : {}),
52
+ annotations: t.annotations,
20
53
  }));
21
54
  const byName = new Map(opts.tools.map((t) => [t.name, t]));
22
55
  const validateArgs = opts.validateArgs ?? true;
23
- const strictValidation = opts.strictValidation ?? false;
24
- if (validateArgs) {
25
- // Compile each schema now so a validator-hostile inputSchema surfaces once
26
- // here rather than silently on its first call. Warning-only; never throws.
27
- for (const t of opts.tools) {
28
- if (t.inputSchema)
29
- precompileValidator(t.inputSchema, { address: `${id}.${t.name}` });
30
- }
31
- }
32
56
  return {
33
57
  id,
34
58
  ...(opts.title !== undefined ? { title: opts.title } : {}),
@@ -64,7 +88,10 @@ export function api(id, opts) {
64
88
  const invalid = validateToolInput(tool.inputSchema, input, {
65
89
  address: `${id}.${name}`,
66
90
  logger: ctx.logger,
67
- failClosed: strictValidation,
91
+ // Always: the schema compiled at construction, so anything that
92
+ // fails here is a schema that cannot be enforced, and a surface we
93
+ // wrote ourselves does not get to admit unvalidated input quietly.
94
+ failClosed: true,
68
95
  });
69
96
  if (invalid)
70
97
  throw invalid;
@@ -77,4 +104,3 @@ export function api(id, opts) {
77
104
  },
78
105
  };
79
106
  }
80
- //# sourceMappingURL=api.js.map