@zackbart/connecta 0.15.0 → 0.16.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 (332) hide show
  1. package/AGENTS.md +28 -15
  2. package/CHANGELOG.md +493 -0
  3. package/README.md +19 -4
  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 +0 -1
  27. package/dist/catalog.d.ts +0 -1
  28. package/dist/catalog.js +0 -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 +0 -1
  42. package/dist/errors.js +0 -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 +5 -2
  78. package/dist/providers/cloudflare.js +435 -328
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -60
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -78
  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 -59
  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 +95 -27
  129. package/documentation/code-mode.md +12 -12
  130. package/documentation/connector-guides.md +172 -5
  131. package/documentation/connectors.md +98 -7
  132. package/documentation/linear.md +26 -9
  133. package/documentation/mcp-ui-design.md +2 -2
  134. package/documentation/mixpanel.md +48 -16
  135. package/documentation/notion.md +16 -0
  136. package/documentation/operations.md +283 -5
  137. package/documentation/operator-ui.md +120 -5
  138. package/documentation/provider-audit.md +160 -0
  139. package/documentation/provider-conventions.md +631 -0
  140. package/documentation/request-admission.md +167 -5
  141. package/documentation/rich-output-design.md +1 -2
  142. package/documentation/stripe.md +27 -12
  143. package/ethos.md +14 -9
  144. package/examples/worker/README.md +96 -12
  145. package/examples/worker/src/index.ts +46 -1
  146. package/examples/worker/wrangler.jsonc +14 -1
  147. package/package.json +4 -5
  148. package/templates/node/.dockerignore +10 -0
  149. package/templates/node/.env.example +40 -3
  150. package/templates/node/AGENTS.md +11 -1
  151. package/templates/node/Dockerfile +49 -0
  152. package/templates/node/README.md +117 -2
  153. package/templates/node/docker-compose.yml +39 -0
  154. package/templates/node/package.json +1 -1
  155. package/templates/node/src/file-activity.ts +182 -0
  156. package/templates/node/src/index.ts +80 -4
  157. package/assets/connecta-clay-hero.png +0 -0
  158. package/dist/access-tokens.d.ts.map +0 -1
  159. package/dist/access-tokens.js.map +0 -1
  160. package/dist/activity.d.ts.map +0 -1
  161. package/dist/activity.js.map +0 -1
  162. package/dist/apps-shell.d.ts.map +0 -1
  163. package/dist/apps-shell.js.map +0 -1
  164. package/dist/auth/bearer.d.ts.map +0 -1
  165. package/dist/auth/bearer.js.map +0 -1
  166. package/dist/auth/clerk.d.ts.map +0 -1
  167. package/dist/auth/clerk.js.map +0 -1
  168. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  169. package/dist/auth/downstream-oauth.js.map +0 -1
  170. package/dist/call-admission.d.ts.map +0 -1
  171. package/dist/call-admission.js.map +0 -1
  172. package/dist/catalog-fingerprint.d.ts.map +0 -1
  173. package/dist/catalog-fingerprint.js.map +0 -1
  174. package/dist/catalog-limits.d.ts.map +0 -1
  175. package/dist/catalog-limits.js.map +0 -1
  176. package/dist/catalog-service.d.ts.map +0 -1
  177. package/dist/catalog-service.js.map +0 -1
  178. package/dist/catalog.d.ts.map +0 -1
  179. package/dist/catalog.js.map +0 -1
  180. package/dist/concurrency.d.ts.map +0 -1
  181. package/dist/concurrency.js.map +0 -1
  182. package/dist/connector-scope.d.ts.map +0 -1
  183. package/dist/connector-scope.js.map +0 -1
  184. package/dist/connectors/api.d.ts.map +0 -1
  185. package/dist/connectors/api.js.map +0 -1
  186. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  187. package/dist/connectors/remote-mcp.js.map +0 -1
  188. package/dist/credentials.d.ts.map +0 -1
  189. package/dist/credentials.js.map +0 -1
  190. package/dist/errors.d.ts.map +0 -1
  191. package/dist/errors.js.map +0 -1
  192. package/dist/execute.d.ts.map +0 -1
  193. package/dist/execute.js.map +0 -1
  194. package/dist/executor-admission.d.ts.map +0 -1
  195. package/dist/executor-admission.js.map +0 -1
  196. package/dist/executor-result.d.ts.map +0 -1
  197. package/dist/executor-result.js.map +0 -1
  198. package/dist/executors/quickjs-child.d.ts.map +0 -1
  199. package/dist/executors/quickjs-child.js.map +0 -1
  200. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  201. package/dist/executors/quickjs-protocol.js.map +0 -1
  202. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  203. package/dist/executors/quickjs-runtime.js.map +0 -1
  204. package/dist/executors/quickjs.d.ts.map +0 -1
  205. package/dist/executors/quickjs.js.map +0 -1
  206. package/dist/favicon.d.ts.map +0 -1
  207. package/dist/favicon.js.map +0 -1
  208. package/dist/index.d.ts.map +0 -1
  209. package/dist/index.js.map +0 -1
  210. package/dist/invocation.d.ts.map +0 -1
  211. package/dist/invocation.js.map +0 -1
  212. package/dist/json-schema.d.ts.map +0 -1
  213. package/dist/json-schema.js.map +0 -1
  214. package/dist/mcp-result.d.ts.map +0 -1
  215. package/dist/mcp-result.js.map +0 -1
  216. package/dist/meta-tools.d.ts.map +0 -1
  217. package/dist/meta-tools.js.map +0 -1
  218. package/dist/node.d.ts.map +0 -1
  219. package/dist/node.js.map +0 -1
  220. package/dist/operator-ui/generated.d.ts.map +0 -1
  221. package/dist/operator-ui/generated.js.map +0 -1
  222. package/dist/operator-ui/model.d.ts.map +0 -1
  223. package/dist/operator-ui/model.js.map +0 -1
  224. package/dist/providers/cloudflare.d.ts.map +0 -1
  225. package/dist/providers/cloudflare.js.map +0 -1
  226. package/dist/providers/linear.d.ts.map +0 -1
  227. package/dist/providers/linear.js.map +0 -1
  228. package/dist/providers/mixpanel.d.ts.map +0 -1
  229. package/dist/providers/mixpanel.js.map +0 -1
  230. package/dist/providers/notion.d.ts.map +0 -1
  231. package/dist/providers/notion.js.map +0 -1
  232. package/dist/providers/stripe.d.ts.map +0 -1
  233. package/dist/providers/stripe.js.map +0 -1
  234. package/dist/registry.d.ts.map +0 -1
  235. package/dist/registry.js.map +0 -1
  236. package/dist/retired-toolkits.d.ts.map +0 -1
  237. package/dist/retired-toolkits.js.map +0 -1
  238. package/dist/routes/access-tokens.d.ts.map +0 -1
  239. package/dist/routes/access-tokens.js.map +0 -1
  240. package/dist/routes/activity.d.ts.map +0 -1
  241. package/dist/routes/activity.js.map +0 -1
  242. package/dist/routes/credentials.d.ts.map +0 -1
  243. package/dist/routes/credentials.js.map +0 -1
  244. package/dist/routes/mcp.d.ts.map +0 -1
  245. package/dist/routes/mcp.js.map +0 -1
  246. package/dist/routes/oauth.d.ts.map +0 -1
  247. package/dist/routes/oauth.js.map +0 -1
  248. package/dist/routes/shared.d.ts.map +0 -1
  249. package/dist/routes/shared.js.map +0 -1
  250. package/dist/routes/ui.d.ts.map +0 -1
  251. package/dist/routes/ui.js.map +0 -1
  252. package/dist/server.d.ts.map +0 -1
  253. package/dist/server.js.map +0 -1
  254. package/dist/skills.d.ts.map +0 -1
  255. package/dist/skills.js.map +0 -1
  256. package/dist/storage/file.d.ts.map +0 -1
  257. package/dist/storage/file.js.map +0 -1
  258. package/dist/storage/memory.d.ts.map +0 -1
  259. package/dist/storage/memory.js.map +0 -1
  260. package/dist/timeout.d.ts.map +0 -1
  261. package/dist/timeout.js.map +0 -1
  262. package/dist/tool-safety.d.ts.map +0 -1
  263. package/dist/tool-safety.js.map +0 -1
  264. package/dist/types.d.ts.map +0 -1
  265. package/dist/types.js.map +0 -1
  266. package/dist/ui.d.ts.map +0 -1
  267. package/dist/ui.js.map +0 -1
  268. package/dist/validate.d.ts.map +0 -1
  269. package/dist/validate.js.map +0 -1
  270. package/dist/version.d.ts.map +0 -1
  271. package/dist/version.js.map +0 -1
  272. package/examples/node/README.md +0 -52
  273. package/examples/node/src/index.ts +0 -71
  274. package/src/access-tokens.ts +0 -289
  275. package/src/activity.ts +0 -234
  276. package/src/apps-shell.ts +0 -370
  277. package/src/auth/bearer.ts +0 -61
  278. package/src/auth/clerk.ts +0 -553
  279. package/src/auth/downstream-oauth.ts +0 -575
  280. package/src/call-admission.ts +0 -521
  281. package/src/catalog-fingerprint.ts +0 -52
  282. package/src/catalog-limits.ts +0 -11
  283. package/src/catalog-service.ts +0 -1153
  284. package/src/catalog.ts +0 -790
  285. package/src/concurrency.ts +0 -39
  286. package/src/connector-scope.ts +0 -66
  287. package/src/connectors/api.ts +0 -159
  288. package/src/connectors/remote-mcp.ts +0 -1090
  289. package/src/credentials.ts +0 -428
  290. package/src/errors.ts +0 -364
  291. package/src/execute.ts +0 -1406
  292. package/src/executor-admission.ts +0 -328
  293. package/src/executor-result.ts +0 -107
  294. package/src/executors/quickjs-child.ts +0 -172
  295. package/src/executors/quickjs-protocol.ts +0 -99
  296. package/src/executors/quickjs-runtime.ts +0 -470
  297. package/src/executors/quickjs.ts +0 -776
  298. package/src/favicon.ts +0 -53
  299. package/src/index.ts +0 -666
  300. package/src/invocation.ts +0 -655
  301. package/src/json-schema.ts +0 -11
  302. package/src/mcp-result.ts +0 -43
  303. package/src/meta-tools.ts +0 -1607
  304. package/src/node.ts +0 -206
  305. package/src/operator-ui/browser.css +0 -423
  306. package/src/operator-ui/browser.ts +0 -1281
  307. package/src/operator-ui/generated.ts +0 -4
  308. package/src/operator-ui/model.ts +0 -111
  309. package/src/providers/cloudflare.ts +0 -3803
  310. package/src/providers/linear.ts +0 -301
  311. package/src/providers/mixpanel.ts +0 -228
  312. package/src/providers/notion.ts +0 -1879
  313. package/src/providers/stripe.ts +0 -306
  314. package/src/registry.ts +0 -1085
  315. package/src/retired-toolkits.ts +0 -14
  316. package/src/routes/access-tokens.ts +0 -115
  317. package/src/routes/activity.ts +0 -215
  318. package/src/routes/credentials.ts +0 -254
  319. package/src/routes/mcp.ts +0 -429
  320. package/src/routes/oauth.ts +0 -322
  321. package/src/routes/shared.ts +0 -247
  322. package/src/routes/ui.ts +0 -152
  323. package/src/server.ts +0 -195
  324. package/src/skills.ts +0 -271
  325. package/src/storage/file.ts +0 -123
  326. package/src/storage/memory.ts +0 -44
  327. package/src/timeout.ts +0 -84
  328. package/src/tool-safety.ts +0 -15
  329. package/src/types.ts +0 -523
  330. package/src/ui.ts +0 -744
  331. package/src/validate.ts +0 -387
  332. 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
@@ -786,4 +786,3 @@ export function flatSearchResult(page) {
786
786
  ...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
787
787
  };
788
788
  }
789
- //# sourceMappingURL=catalog-service.js.map
package/dist/catalog.d.ts CHANGED
@@ -72,4 +72,3 @@ export interface SchemaObjectKeys {
72
72
  * fields.
73
73
  */
74
74
  export declare function schemaObjectKeys(schema: JsonSchema | undefined): SchemaObjectKeys | undefined;
75
- //# sourceMappingURL=catalog.d.ts.map
package/dist/catalog.js CHANGED
@@ -636,4 +636,3 @@ function objectKeys(schema, defs, seen, depth) {
636
636
  }
637
637
  return undefined;
638
638
  }
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
@@ -0,0 +1,106 @@
1
+ import type { ConnectorContext } from "../types.js";
2
+ /** The methods a hand-written provider surface actually uses. */
3
+ type GuardedMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
4
+ export interface GuardedRequest {
5
+ method: GuardedMethod;
6
+ /**
7
+ * Provider-relative path beginning with one `/`, resolved against the base
8
+ * URL's own path. Never an absolute URL, and never carrying `?` or `#` —
9
+ * query parameters go in `query`, where they are encoded rather than parsed.
10
+ */
11
+ path: string;
12
+ /** Search parameters; `undefined` values are dropped, others stringified. */
13
+ query?: Record<string, string | number | boolean | undefined>;
14
+ /**
15
+ * Per-request headers. `undefined` values are dropped, and a header that
16
+ * collides with an authentication header is refused rather than allowed to
17
+ * shadow it.
18
+ */
19
+ headers?: Record<string, string | undefined>;
20
+ /** JSON request body. Serialized here, with the `Content-Type` to match. */
21
+ body?: unknown;
22
+ /**
23
+ * A pre-framed body — `FormData`, text, bytes. Mutually exclusive with
24
+ * `body`, and no `Content-Type` is supplied: multipart needs `fetch` to pick
25
+ * the boundary, and anything else knows its own type.
26
+ */
27
+ rawBody?: BodyInit;
28
+ }
29
+ /**
30
+ * One downstream response, read under the connector's byte ceiling.
31
+ *
32
+ * The accessors are the whole read surface — there is no escape to the
33
+ * underlying `Response`, because an unbounded `.text()` on it is exactly the
34
+ * mistake this type exists to remove. `status`, `ok`, and `headers` are the
35
+ * raw facts a provider mapper needs to make its own classification.
36
+ */
37
+ interface GuardedResponse {
38
+ readonly status: number;
39
+ readonly ok: boolean;
40
+ readonly headers: Headers;
41
+ /** The body as bytes. */
42
+ bytes(): Promise<Uint8Array>;
43
+ /** The body decoded as UTF-8. */
44
+ text(): Promise<string>;
45
+ /** The body parsed as JSON; `undefined` for an empty body, throws on junk. */
46
+ json(): Promise<unknown>;
47
+ }
48
+ /**
49
+ * Turn one response into the provider's own result, or throw the provider's
50
+ * own typed failure. This is where status codes acquire meaning, and it is
51
+ * always the provider's code.
52
+ */
53
+ type GuardedResponseMapper<T> = (response: GuardedResponse, ctx: ConnectorContext) => T | Promise<T>;
54
+ export interface GuardedFetchOptions {
55
+ /**
56
+ * Provider name as it appears in normalized failure prose — "Cloudflare",
57
+ * "Notion". Used for messages only; it is never parsed.
58
+ */
59
+ provider: string;
60
+ /**
61
+ * Absolute base URL. Its origin and path prefix are the confinement: every
62
+ * request resolves beneath it or is refused.
63
+ */
64
+ baseUrl: string;
65
+ /**
66
+ * Headers proving the caller's identity, resolved once per request. Throwing
67
+ * a typed `auth_required` from here is the documented way to report a
68
+ * missing credential — the helper has no opinion about what a credential is.
69
+ */
70
+ authenticate: (ctx: ConnectorContext) => Record<string, string> | Promise<Record<string, string>>;
71
+ /** Constant headers sent with every request — `Accept`, an API version. */
72
+ headers?: Record<string, string>;
73
+ /**
74
+ * Ceiling on the response body, in bytes. Required, because a default here
75
+ * would be the helper guessing on a provider's behalf: what counts as an
76
+ * absurd response is a fact about the API, not about HTTP.
77
+ */
78
+ maxResponseBytes: number;
79
+ }
80
+ /** Send one guarded request and map its response with provider knowledge. */
81
+ export type GuardedTransport = <T>(request: GuardedRequest, ctx: ConnectorContext, map: GuardedResponseMapper<T>) => Promise<T>;
82
+ /**
83
+ * Build the guarded transport one hand-written connector sends every request
84
+ * through.
85
+ *
86
+ * ```ts
87
+ * const send = guardedFetch({
88
+ * provider: "Billing",
89
+ * baseUrl: "https://billing.internal.example/v1",
90
+ * maxResponseBytes: 4 * 1024 * 1024,
91
+ * headers: { Accept: "application/json" },
92
+ * authenticate: async (ctx) => {
93
+ * const token = await ctx.credential?.get();
94
+ * if (!token) throw new ConnectorCallError("auth_required", "…");
95
+ * return { Authorization: `Bearer ${token}` };
96
+ * },
97
+ * });
98
+ *
99
+ * const invoice = await send({ method: "GET", path: `/invoices/${id}` }, ctx, (response) => {
100
+ * if (!response.ok) throw billingFailure(response.status);
101
+ * return response.json();
102
+ * });
103
+ * ```
104
+ */
105
+ export declare function guardedFetch(options: GuardedFetchOptions): GuardedTransport;
106
+ export {};