@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
@@ -1,7 +1,143 @@
1
- # Call admission
1
+ # Downstream call admission
2
2
 
3
- > **Stub.** The old manual was retired in the phase-1 docs restructure. This
4
- > document will be rewritten as an agent-facing guide — what the subsystem is
5
- > for, how to work on it, and what it must never do once the ideas in
6
- > [ethos.md](../ethos.md) settle. The prior text lives in git history as
7
- > `docs/call-admission.md`.
3
+ One admitted MCP request can fan out into many downstream calls. A program
4
+ calls tools from loops, from `connecta.batch`, and from branches the caller
5
+ never saw. The deployment-wide request pool
6
+ ([request admission](./request-admission.md)) bounds the envelope; it cannot
7
+ see anything inside it. A provider that publishes "60 requests per minute"
8
+ needs a bound that counts calls, not requests.
9
+
10
+ So a connector may declare `callAdmission`: an optional per-runtime policy
11
+ around its own `Connector.callTool` attempts.
12
+
13
+ ```ts
14
+ const projects = api("projects", {
15
+ description: "Projects — issues and reports",
16
+ callAdmission: {
17
+ // Plural-ready public shape; this release requires exactly one rule.
18
+ rules: [{
19
+ maxConcurrency: 5,
20
+ maxQueueSize: 10, // default 32
21
+ queueTimeoutMs: 2_000, // default 5_000
22
+ retryAfterMs: 1_000, // concurrency-overload hint
23
+ budget: { kind: "rolling-window", maxCalls: 60, windowMs: 60 * 60_000 },
24
+ partitionKey({ toolName, args }) {
25
+ return projectIdFor(toolName, args);
26
+ },
27
+ }],
28
+ maxPartitions: 1_024, // default
29
+ },
30
+ tools: [/* … */],
31
+ });
32
+ ```
33
+
34
+ `api()`, `remoteMcp()`, and a hand-written `Connector` all accept it. Maintained
35
+ prebuilt connections set it themselves — but only when the provider documents a
36
+ number, which is convention
37
+ [P12](./provider-conventions.md#p12--declare-an-admission-budget-only-when-the-provider-documents-a-number).
38
+ An invented budget is a throttle with no source.
39
+
40
+ ## Policy contract
41
+
42
+ `maxConcurrency` and `budget` are independently optional, but a rule must
43
+ declare at least one — a rule that bounds nothing is a typo, not a policy.
44
+ Queue settings require `maxConcurrency`, because there is nothing to queue
45
+ behind without it, and `maxQueueSize: 0` is the fail-fast shape. Numeric bounds
46
+ are finite whole numbers: concurrency, timeouts, budget values, and
47
+ `maxPartitions` are positive; queue size and `retryAfterMs` may be zero.
48
+ `budget.kind` must be `"rolling-window"`. Everything invalid throws during
49
+ registry construction.
50
+
51
+ The container is an array so a later release can add atomic rules over
52
+ different partition dimensions, but **exactly one rule** is accepted today.
53
+ Zero or several throw rather than imply semantics the runtime does not have.
54
+ Multiple rules cannot be faked as sequential leases: consuming one rolling
55
+ token before a later rule refuses would charge a call that never reached the
56
+ provider — the exact accounting error a budget exists to prevent.
57
+
58
+ Omitting `partitionKey` gives one connector-wide partition. A callback runs
59
+ synchronously on model-supplied arguments and must return a non-secret string
60
+ of at most 128 UTF-8 bytes. A throw or an invalid return is a typed local
61
+ `connector_call_failed`; neither the callback text nor the arguments are
62
+ surfaced. The callback is operator code and may abort the caller synchronously,
63
+ so cancellation is rechecked after it returns — otherwise a cancelled call
64
+ could still consume a budget entry.
65
+
66
+ State retains the returned key, bounded timestamps, counters, signals, and
67
+ promise continuations. Nothing copies arguments into the limiter; the queued
68
+ waiter closure deliberately captures the `AbortSignal` rather than the input
69
+ object, because closing over the input would retain its `args`. At most
70
+ `maxPartitions` states are live, and an idle state is evicted only once its
71
+ active calls, queue, and rolling-window history are all empty — eviction must
72
+ not be a way to reset a live budget. Exhausted partition capacity is
73
+ `rate_limited` with the configured retry hint, not a silent unbounded map.
74
+
75
+ ## Attempt semantics
76
+
77
+ The registry owns the limiter, and `InvocationService.invoke` acquires a permit
78
+ immediately before `Connector.callTool` and releases it in `finally`. Both call
79
+ paths — top-level `call_tool` and a program's `connecta.call` or
80
+ `connecta.batch` — reach that same seam, so a program cannot buy itself a
81
+ second limit by taking the other route.
82
+
83
+ - **A retry is another attempt.** It reacquires and can consume another budget
84
+ entry, because the provider counts it that way. Backoff never holds a
85
+ concurrency permit.
86
+ - **A proactive short-window `rate_limited` refusal** participates in the
87
+ ordinary retry policy and counts as an attempt. Activity records the final
88
+ outcome and the attempt count.
89
+ - **A queued cancellation consumes nothing.** It is removed from the queue with
90
+ no rolling-window entry charged.
91
+ - **Caller cancellation is terminal.** It is non-retryable, releases its
92
+ permit, records a `cancelled` activity outcome, and is not connector-health
93
+ evidence.
94
+ - **Budget exhaustion fails immediately** as `rate_limited`, `retryable: true`,
95
+ with the exact `retryAfterMs` to the next free slot. Concurrency overflow
96
+ uses the configured hint instead, because there is no exact answer.
97
+ - **Local refusals are not connector health failures.** They are activity
98
+ errors, but no provider call happened, so poisoning the connector's health
99
+ with them would report the limiter's success as the downstream's failure.
100
+ `isCallAdmissionError` is what keeps `recordFailure` out of that path.
101
+
102
+ Only tool execution is covered. Catalog `listTools`, status probes, credential
103
+ checks, and authorization operations stay outside the budget: they are not the
104
+ calls a provider is rate-limiting, and charging discovery for them would make
105
+ a program's first search cost it capacity to act.
106
+
107
+ Queue admission and connector execution have separate clocks. `queueTimeoutMs`
108
+ bounds only the wait for a permit; a per-attempt `timeoutMs` starts after
109
+ admission. A saturated call can therefore take up to their sum. With
110
+ `diagnostics: true`, `admissionMs` reports the permit wait and `connectorMs`
111
+ the admitted attempt — which is the only way to tell "the provider is slow"
112
+ from "we are throttling ourselves".
113
+
114
+ ## Enforcement scope
115
+
116
+ This is deliberately **per-runtime**. It completely contains fan-out inside one
117
+ request, including a wide `connecta.batch` in one Worker isolate. A rolling
118
+ budget is exact inside one Node process or Worker isolate, and best-effort
119
+ across isolates, replicas, and restarts.
120
+
121
+ KV cannot coordinate the invariant without atomic operations, and this release
122
+ adds no Durable Object or other distributed coordinator. The async typed permit
123
+ is the seam a coordinator would slot into later; until then the guide says what
124
+ the bound actually is rather than implying a global one.
125
+
126
+ ## Observations
127
+
128
+ `/health` exposes payload-free aggregates at
129
+ `admission.downstreamCalls.connectors.<id>`: retained partition count, current
130
+ active and queued gauges, cumulative admitted/queued/rejected/rate-limited/
131
+ cancelled counts, and queue-wait count, total, and maximum. The open endpoint
132
+ never exposes partition keys, tool arguments, or results — a partition key can
133
+ be a customer identifier, which is precisely why it stays out of an unauthenticated
134
+ payload. Ordinary payload-free activity records the final call outcome and its
135
+ typed error code.
136
+
137
+ ## Tests that enforce this
138
+
139
+ | Invariant | Suite |
140
+ | --- | --- |
141
+ | Independent partitions, exact rolling-window reset and retry, queued cancellation charging no budget, synchronous cancel during partition derivation, validated values snapshotted rather than read from mutable config, bounded partition state and contained `partitionKey` failures, empty and multi-rule policies refused | `test/call-admission.test.ts` (controller) |
142
+ | One base-registry limiter shared by direct and program calls, batch bounds with input order preserved, cancellation threading, no dispatch or retry or health poisoning after cancellation, short proactive windows retried without poisoning health, payload-free `/health` aggregates | `test/call-admission.test.ts` (integration, Node + Workers) |
143
+ | Where provider budgets are allowed to come from at all | [provider conventions P12](./provider-conventions.md#p12--declare-an-admission-budget-only-when-the-provider-documents-a-number), [provider audit](./provider-audit.md) |
@@ -2,7 +2,7 @@
2
2
 
3
3
  Import `cloudflare()` independently from
4
4
  `@zackbart/connecta/providers/cloudflare`. It is a deliberate, hand-written
5
- surface over Cloudflare's v4 REST API. Fifty-five tools combine ergonomic,
5
+ surface over Cloudflare's v4 REST API. Fifty-two tools combine ergonomic,
6
6
  fully described operations for common work with three guarded escape hatches
7
7
  for the rest of Cloudflare's fast-moving control plane. Reads, JSON mutations,
8
8
  and raw/multipart uploads remain separate so safety routing does not depend on
@@ -23,6 +23,18 @@ const edge = cloudflare("cloudflare_prod", {
23
23
  });
24
24
  ```
25
25
 
26
+ Use the legacy user-scoped Global API Key when an existing deployment needs it:
27
+
28
+ ```ts
29
+ const legacyEdge = cloudflare("cloudflare_legacy", {
30
+ purpose: "Legacy account administration",
31
+ authentication: "globalApiKey",
32
+ });
33
+ ```
34
+
35
+ The operator UI then asks for the Cloudflare user email and Global API Key as
36
+ separate fields. The default remains a scoped API token.
37
+
26
38
  The `id` owns the ordinary connector namespaces; use a different id for every
27
39
  Cloudflare account or estate. `purpose` is required because an agent choosing
28
40
  between a production and a staging instance needs to know which one answers the
@@ -40,7 +52,7 @@ dependency would cost is real — an optional peer with its own install step and
40
52
  version skew, an import that never belongs in the root graph, and a second
41
53
  opinion about what a Cloudflare call looks like.
42
54
 
43
- The API itself does not need one. It is Bearer-token `fetch` with a uniform
55
+ The API itself does not need one. It is authenticated `fetch` with a uniform
44
56
  `{ success, errors, messages, result, result_info }` response envelope. JSON,
45
57
  raw bytes, and multipart request bodies all use Web APIs, which keeps the
46
58
  provider Workers-clean and means `@zackbart/connecta/providers/cloudflare`
@@ -51,10 +63,16 @@ must be relative.
51
63
 
52
64
  ## Credentials
53
65
 
54
- The connection declares one operator-managed credential: a scoped Cloudflare
55
- API token, sent as `Authorization: Bearer <token>`. Create it under My Profile →
56
- API Tokens → Create Token. Do not use a Global API Key — it carries every
57
- permission on the account and cannot be scoped.
66
+ The default credential is a scoped Cloudflare API token, sent as
67
+ `Authorization: Bearer <token>`. Create it under My Profile → API Tokens →
68
+ Create Token.
69
+
70
+ Set `authentication: "globalApiKey"` to use the legacy user-scoped scheme. The
71
+ credential form stores two encrypted fields and sends them as `X-Auth-Email`
72
+ and `X-Auth-Key`. The Global API Key has the same access as its Cloudflare user
73
+ across every account and zone that user can reach. It cannot be scoped, only
74
+ one can exist per user, and it has no expiry or IP limits. Cloudflare recommends
75
+ API tokens for new use, but existing Global API Keys remain supported.
58
76
 
59
77
  Grant only what the deployment needs:
60
78
 
@@ -83,10 +101,9 @@ scope. "Cache Purge" is a single permission with no Read/Write split, and
83
101
  Cloudflare's own reference renders a few labels differently between its
84
102
  Dashboard and API tabs.
85
103
 
86
- `verify_api_token` needs no permission beyond the token existing, which is what
87
- makes it the right first call when something fails. The `/credentials` Test
88
- action runs the same verification against the candidate token before it is
89
- stored.
104
+ `verify_api_token` needs no permission beyond the token existing. In legacy
105
+ mode, `verify_global_api_key` reads `/user` to confirm the email and key pair.
106
+ The `/credentials` Test action runs the matching check before storage.
90
107
 
91
108
  Cloudflare rate-limits *authentication failures* aggressively and separately
92
109
  from the global limit: a few requests with a bad token return HTTP 429 with
@@ -127,7 +144,7 @@ projections:
127
144
  | DNS/cache | list and get records | create, update, delete, targeted/full purge |
128
145
  | Workers | scripts, settings, deployments | delete a script |
129
146
  | KV | namespaces, keys, bulk values | create/rename/delete namespace, bulk write/delete |
130
- | R2 | buckets, object metadata, metrics, CORS | create/update/delete bucket, delete object, replace/delete CORS |
147
+ | R2 | buckets, object metadata, CORS | create/update/delete bucket, delete object |
131
148
  | Pages | projects, deployments, domains | retry/rollback/delete deployments, add/delete domains, purge build cache, delete project |
132
149
 
133
150
  Every named tool carries a complete hand-written input schema: closed
@@ -136,6 +153,39 @@ every constrained field, endpoint-specific pagination bounds, and a description
136
153
  on every property. `test/cloudflare-provider.test.ts` walks the surface and
137
154
  asserts those properties rather than leaving them as a claim.
138
155
 
156
+ ### What the named surface deliberately leaves out
157
+
158
+ A named tool is a permanent line item in every deployment's catalog, so the
159
+ surface was measured against the escape hatches rather than assumed to beat
160
+ them ([#350](https://github.com/zackbart/connecta/issues/350), evidence in
161
+ [`eval/current-version/results/issue-350-evidence.md`](../eval/current-version/results/issue-350-evidence.md)).
162
+ Two named tools lost that comparison, and a third followed one of them off the
163
+ surface to keep a policy pair on one route:
164
+
165
+ - **R2 CORS writes.** `set_r2_cors` declared its rule list as free-form objects
166
+ — the untyped body this connection refuses everywhere else — so its schema
167
+ validated the bucket name and waved through the part of the call that
168
+ actually fails. It also returned Cloudflare's response unprojected and was
169
+ destructive either way, which left nothing for it to beat
170
+ `cloudflare_api_mutate` on. `delete_r2_cors` did not lose that comparison:
171
+ it returned a fixed `{deleted: true}` behind a closed output schema and
172
+ refused every malformed argument locally. It went anyway, because naming only
173
+ the delete would mean one CORS policy is set through the raw route and
174
+ cleared through a named tool. Read a policy with `get_r2_cors`; change one
175
+ with `cloudflare_api_mutate` at
176
+ `PUT`/`DELETE /accounts/{accountId}/r2/buckets/{bucketName}/cors`. This is the
177
+ same split the [DNS record types](#dns-record-types) already use: structured
178
+ bodies stay readable through named tools and writable through the
179
+ approval-gated raw route.
180
+ - **R2 account metrics.** `get_r2_metrics` took one account id, put it in a
181
+ path, and returned the response untouched — `cloudflare_api_get` at
182
+ `/accounts/{accountId}/r2/metrics` does the same thing without a permanent
183
+ catalog line.
184
+
185
+ The surviving 48 named tools all refuse malformed arguments locally, which is
186
+ the one thing no escape hatch can do: a hatch's path is an opaque string, so it
187
+ can only check that a path is a path.
188
+
139
189
  ### The whole-v4 escape hatch
140
190
 
141
191
  Cloudflare adds products and endpoints faster than a curated connector should
@@ -146,21 +196,28 @@ method classification into user input:
146
196
  default; `responseType: "text" | "base64"` retrieves scripts, logs, R2
147
197
  objects, and media bodies without pretending they have a JSON envelope.
148
198
  - `cloudflare_api_mutate` accepts JSON POST, PUT, PATCH, and DELETE. It is always
149
- destructive, even when a particular POST is merely additive.
199
+ destructive, even when a particular POST is merely additive. It preserves
200
+ ordinary non-envelope JSON from endpoints such as `/graphql`.
150
201
  - `cloudflare_api_upload` accepts POST or PUT plus exactly one of raw text,
151
202
  base64 bytes, or multipart fields/files. It is always destructive and reads
152
203
  no local files.
153
204
 
154
205
  All three accept explicit endpoint-specific headers, which supports R2
155
206
  jurisdictions, conditional requests, encryption controls, and object metadata.
156
- Authentication, host selection, content type, content length, and transfer
157
- framing remain connector-owned and cannot be overridden.
207
+ `Authorization`, `Cookie`, `Host`, `Content-Length`, `Content-Type`, and
208
+ `Transfer-Encoding` remain connector-owned and are refused: authentication,
209
+ host selection, content type, and request framing are not the caller's to set.
210
+ That refused list lives in the usage guide rather than in each header property
211
+ description, because the compact renderer inlines a property description once
212
+ per tool that shares it, and repeating it three times pushed
213
+ `cloudflare_api_upload`'s compact input shape past the 1,024-byte discovery
214
+ budget.
158
215
 
159
216
  Paths are relative to `/client/v4`. Absolute URLs, protocol-relative paths,
160
217
  `..` traversal, fragments, and embedded query strings are refused locally;
161
218
  query parameters are explicit name/value pairs. These tools reuse the same
162
219
  credential, admission budget, abort signal, envelope parsing, and typed failure
163
- mapping as named tools. They do not widen the token's Cloudflare permissions.
220
+ mapping as named tools. They do not widen the configured credential's access.
164
221
 
165
222
  This is intentionally not OpenAPI ingestion: it creates three stable tools,
166
223
  not one tool per Cloudflare operation. For example, an agent can list Images at
@@ -173,8 +230,8 @@ comes from Cloudflare's API reference.
173
230
 
174
231
  ### Where the `perPage` bounds come from
175
232
 
176
- `strictValidation` is on, so an out-of-range `perPage` is refused locally
177
- before it reaches Cloudflare. That is only a favor when the bound is really
233
+ `api()` enforces every hand-written schema, so an out-of-range `perPage` is
234
+ refused locally before it reaches Cloudflare. That is only a favor when the bound is really
178
235
  Cloudflare's, so the schemas record which ones are and the descriptions say so
179
236
  out loud:
180
237
 
@@ -242,8 +299,12 @@ Paginated lists add a `page` object derived from `result_info`:
242
299
  field to branch on.
243
300
 
244
301
  Some endpoints do not work that way, and the schemas say so rather than leaving
245
- an agent to discover it. `list_r2_buckets`, `list_r2_objects`, and
246
- `list_kv_keys` paginate by cursor and return `nextCursor` instead of `page`.
302
+ an agent to discover it. `list_zone_rulesets`, `list_r2_buckets`,
303
+ `list_r2_objects`, and `list_kv_keys` paginate by cursor and return
304
+ `nextCursor` instead of `page`. On those four the `cursor` argument says the
305
+ endpoint pages by cursor rather than page number, and the `nextCursor` result
306
+ says it is the only signal and that no `page` object is coming — so the loop
307
+ condition is legible from either end of one tool, without fetching the guide.
247
308
  `list_worker_scripts` reports no counters at all and omits `page` entirely.
248
309
 
249
310
  Projected resource reads expose `raw: true` where the provider's larger object
@@ -271,8 +332,8 @@ official table mapping error codes to causes, so that set is assembled from
271
332
  community reports and probing, not from documentation. The same goes for the
272
333
  claim below that `10000` is overloaded — that is an observation about responses
273
334
  seen in practice. Treat both as well-supported readings that Cloudflare could
274
- invalidate without notice, and prefer `verify_api_token` over the code list
275
- when a diagnosis actually matters.
335
+ invalidate without notice. Prefer `verify_api_token` or
336
+ `verify_global_api_key`, as configured, when a diagnosis matters.
276
337
 
277
338
  Two ordering decisions are deliberate. The 429 branch is checked before the
278
339
  authentication codes, because Cloudflare reuses the generic `10000` code on
@@ -280,15 +341,15 @@ throttled responses and reading a rate limit as an auth failure would tell an
280
341
  agent to stop when it should wait. And `10000` is *not* itself treated as an
281
342
  auth code: Cloudflare returns it for "Authentication error" but also for
282
343
  ordinary validation failures like "Invalid pagination cursor" and
283
- "domain_name is required", so routing on it would tell an agent its token was
344
+ "domain_name is required", so routing on it would tell an agent its credential was
284
345
  broken when its arguments were. Genuine `10000` auth failures arrive with 401
285
346
  or 403 and are caught by status.
286
347
 
287
348
  Because the connection declares an operator-managed credential rather than an
288
349
  OAuth flow, an `auth_required` failure resolves to the `operator_config`
289
- recovery mode — the fix is a human updating the token, not an authorization
290
- URL the agent can open. A missing token fails that way before any request is
291
- made.
350
+ recovery mode — the fix is a human updating the credential, not an authorization
351
+ URL the agent can open. A missing credential fails that way before any request
352
+ is made.
292
353
 
293
354
  Some failures never reach Cloudflare at all. A blank scope id, a `purge_cache`
294
355
  call with no variant or two, and an `update_dns_record` with nothing to change
@@ -309,5 +370,12 @@ enforcement of it. Each runtime keeps its own counter, so N Worker isolates or
309
370
  Node processes serving one deployment can each admit up to 1,200 — and the
310
371
  dashboard traffic of a human sharing the account is counted by Cloudflare but
311
372
  not by Connecta. `maxConcurrency` is the bound that actually protects a shared
312
- token, because a single `execute_code` program can fan out far faster than the
313
- window notices.
373
+ credential, because a single `execute_code` program can fan out far faster than
374
+ the window notices.
375
+
376
+ ## Conventions
377
+
378
+ This connection is audited against
379
+ [the provider conventions](./provider-conventions.md). Its verdict per
380
+ convention, including every recorded exception, is the Cloudflare section of
381
+ [the provider audit](./provider-audit.md).
@@ -8,10 +8,9 @@ it is specified in prose first and implemented second — the same discipline th
8
8
  [MCP spec bump](./mcp-2026-07-28.md) followed.
9
9
 
10
10
  Two executors implement this document: QuickJS in a child process on Node, and
11
- `DynamicWorkerExecutor` from `@cloudflare/codemode` on Workers. Divergence
12
- between them is a bug unless it appears in
13
- [Executor exceptions](#executor-exceptions), which names the reason. Anyone can
14
- implement a third executor from this document without reading either.
11
+ `DynamicWorkerExecutor` from `@cloudflare/codemode` on Workers. Divergence between
12
+ them is a bug unless it appears in [Executor exceptions](#executor-exceptions),
13
+ which names the reason. Anyone can implement a third from this document alone.
15
14
 
16
15
  The [code-first exploration](./code-first-exploration.md) is the evidence behind
17
16
  the direction; [`ethos.md`](../ethos.md) carries the verdicts. Where its prototype
@@ -43,7 +42,7 @@ const connecta = createConnecta({
43
42
  CPU, wall-time, memory, stack, queue, result, log, and IPC bounds are configured
44
43
  on the executor. Server bundlers must keep the `@zackbart/connecta/quickjs`
45
44
  package files external so the child entry stays on disk. The
46
- [Node example](../examples/node/README.md) carries the complete setup.
45
+ [Node template](../templates/node/README.md) carries the complete setup.
47
46
 
48
47
  On Cloudflare Workers, the Worker Loader binding provides the required sandbox:
49
48
 
@@ -54,9 +53,8 @@ createConnecta({
54
53
  });
55
54
  ```
56
55
 
57
- Dynamic Workers require the Workers Paid plan. The
58
- [Worker example](../examples/worker/README.md#code-mode) carries the complete
59
- required binding and package setup.
56
+ Dynamic Workers require the Workers Paid plan. The complete required binding and
57
+ package setup is in the [Worker example](../examples/worker/README.md#code-mode).
60
58
 
61
59
  ## What an executor must implement
62
60
 
@@ -64,6 +62,7 @@ The host side of the seam is two types in `src/types.ts` and nothing else.
64
62
 
65
63
  ```ts
66
64
  interface Executor {
65
+ readonly name?: string; // what /health and doctor report
67
66
  execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
68
67
  close?(): void | Promise<void>;
69
68
  }
@@ -113,11 +112,12 @@ Connecta passes exactly one provider, named `connecta`. An executor must:
113
112
 
114
113
  Optionally implement `AdmittingExecutor` (`acquire()` returning a lease whose
115
114
  `execute` runs once) for bounded admission (`L7`) and `close()` for shutdown;
116
- connecta wraps a plain `Executor` with `withExecutorAdmission` otherwise.
115
+ connecta wraps a plain `Executor` with `withExecutorAdmission` otherwise. The
116
+ optional `name` — else a class's constructor name, which a minifier may rewrite
117
+ — is what [`/health` and `doctor`](./operations.md#the-cli) report.
117
118
 
118
- Note what is *not* on this list: [emitted output](#emitted-output) asks
119
- nothing of an executor — `connecta.emit` is just another provider function
120
- (`M8`).
119
+ Note what is *not* on this list: [emitted output](#emitted-output) asks nothing
120
+ of an executor — `connecta.emit` is just another provider function (`M8`).
121
121
 
122
122
  ## The program
123
123
 
@@ -1,7 +1,174 @@
1
1
  # Connector guides
2
2
 
3
- > **Stub.** The old manual was retired in the phase-1 docs restructure. This
4
- > document will be rewritten as an agent-facing guide what the subsystem is
5
- > for, how to work on it, and what it must never do — once the ideas in
6
- > [ethos.md](../ethos.md) settle. The prior text lives in git history as
7
- > `docs/connector-guides.md`.
3
+ Descriptions and schemas say *what* a connector's tools are. They do not say
4
+ which tool to prefer, which id format an address quirk expects, how the service
5
+ paginates, or how hard you may hammer it. A connector's optional `usageGuide`
6
+ is where that goes deployment-owned configuration, served by the `skills`
7
+ meta-tool as `connector:<id>` and returned verbatim, with a bounded `summary`
8
+ surfaced in discovery so an agent learns the guide exists at the moment it
9
+ matters. See [meta-tools](./meta-tools.md#connector-guide-selection) for the
10
+ discovery contract and [connectors](./connectors.md#catalog-contract) for how a
11
+ guide is configured.
12
+
13
+ ## The shape of a guide
14
+
15
+ `usageGuide` accepts a bare markdown string, which is still the shortest
16
+ correct configuration, or the structured form:
17
+
18
+ ```ts
19
+ const notion = remoteMcp("notion", {
20
+ url: "https://mcp.notion.com/mcp",
21
+ description: "Notion — pages, databases, comments",
22
+ auth: { type: "oauth" },
23
+ usageGuide: {
24
+ content: `# Notion usage
25
+
26
+ Search before listing: \`notion.search\` covers pages and databases in one call.
27
+
28
+ - Page ids are dashed UUIDs. Strip the trailing slug from a pasted URL first.
29
+ - Paginate with \`start_cursor\`; \`page_size\` is capped at 100.
30
+ - Writes replace blocks wholesale — read the block, merge, then write.
31
+ `,
32
+ summary: "Search before listing; dashed-UUID page ids; cursor pagination",
33
+ },
34
+ });
35
+ ```
36
+
37
+ The field is on the `Connector` interface, not on a factory, so it works
38
+ identically for `api()`, `remoteMcp()`, a prebuilt connection, and a
39
+ hand-written connector. It is deployment-owned configuration like everything
40
+ else here: an edit and a redeploy, never a runtime registration.
41
+
42
+ `content` is returned byte for byte by `skills({ name: "connector:<id>" })`.
43
+ `summary` is normalized and capped at 120 characters for discovery. Omit it and
44
+ connecta derives the same bounded line the skills listing uses: the first
45
+ meaningful body line, with frontmatter, fences, rules, comments, and table rows
46
+ skipped, a heading used only when the guide has no body, and the connector's
47
+ description as the last resort. A derived summary is usually worse than a
48
+ written one — it was written to open a document, not to answer "is this guide
49
+ relevant to what I am about to do".
50
+
51
+ `connector:<id>` is the only address for a guide, and built-in skill names are
52
+ bare identifiers, so a guide can never shadow or be shadowed by `usage`: a
53
+ connector whose id is literally `usage` is listed as `connector:usage`, and
54
+ `skills({ name: "usage" })` still returns the built-in guide. Every miss —
55
+ unknown name, unknown connector, connector with no guide — is an explicit
56
+ error. Nothing silently falls back to the generic guide, because a generic
57
+ answer to a specific question is worse than no answer.
58
+
59
+ Discovery text is conditional on the deployment actually having a guide. The
60
+ guide sentences in the `skills`, `search_tools`, `call_destructive_tool`, and
61
+ `execute_code` descriptions appear only when at least one visible connector
62
+ declares one — the connector set is fixed at construction, so this is stable
63
+ per deployment, and a deployment with no guides pays no always-loaded context
64
+ for a feature it does not use. The built-in `usage` skill is the deliberate
65
+ exception: it stays byte-identical across every deployment, including its
66
+ per-connector-guides section, so an agent that has read it once in a task never
67
+ needs a deployment-local copy of it.
68
+
69
+ ## What belongs in a guide
70
+
71
+ The test is not "is this true" but "can a schema carry it". If a schema can,
72
+ fix the schema instead — a constraint in the schema is enforced, is visible at
73
+ the moment of the call, and costs nothing to a caller who never fetches the
74
+ guide.
75
+
76
+ So a guide carries:
77
+
78
+ - **Tool preference.** Which of two plausible tools is the right one, and the
79
+ fact that decides it.
80
+ - **Sequence.** What must happen first — resolving a name to an id, listing a
81
+ parent before a child.
82
+ - **Identity and address quirks.** The id format an address expects, the
83
+ difference between the id in the URL and the id the API wants.
84
+ - **Pagination conventions.** The cursor field, the page cap, whether the
85
+ connector has more than one convention.
86
+ - **Units and aliases.** What a bare number means; what the service calls the
87
+ thing the caller calls something else.
88
+ - **Reduction advice.** Which fields matter, for a downstream whose results are
89
+ large and whose schemas you do not control.
90
+ - **Rate-limit etiquette**, with the number.
91
+
92
+ And a guide does not carry: anything readable off the schema, background on
93
+ what the service is, marketing, a tool list, or a second copy of the tool
94
+ descriptions. It is fetched into a live context window — every line that
95
+ repeats the schema is a line that displaced one that did not.
96
+
97
+ **Style.** Write for the agent, not the operator; the built-in `usage` skill
98
+ (`src/skills.ts`) is the model. Concise and imperative, leading with the
99
+ decision rather than the background ("Search before listing", not "Notion has a
100
+ search API"). Prefer short bullets to prose. Name exact tool addresses and
101
+ argument names. State a constraint with its number.
102
+
103
+ ## The `required` rule
104
+
105
+ `required: true` on the structured form means: fetch this guide before every
106
+ operation on this connector. It surfaces as `guideRequired: true` with
107
+ `guideRequiredReasons: ["connector_required"]` on discovery results.
108
+
109
+ It is an instruction, not a gate. Nothing refuses the call — connecta tells the
110
+ agent to read the guide first and then believes it, because a server-side
111
+ refusal here would be a policy engine, which
112
+ [`ethos.md`](../ethos.md) refuses.
113
+
114
+ Reserve it for connectors whose correct arguments or sequence *cannot* be
115
+ expressed by the downstream tool schema at all: generic API wrappers whose one
116
+ broad tool name carries no endpoint vocabulary, and cross-operation conventions
117
+ no single schema can state. Two categories already produce the flag on their
118
+ own and must not be hand-declared for it — an unannotated or write-capable tool
119
+ (`approval_required`) and a compact schema that was capped
120
+ (`schema_truncated`). `connector_required` and `approval_required` survive
121
+ exact schema expansion; `schema_truncated` clears once describe returns the
122
+ exact shape.
123
+
124
+ The failure mode of over-declaring is quiet and expensive: an agent that must
125
+ fetch a guide before every call pays that fetch on the calls where the schema
126
+ was already complete and unambiguous. `required` earns its cost on connectors
127
+ where the alternative is a wrong call, not on connectors where it is a slightly
128
+ slower right one.
129
+
130
+ ## Provider conventions
131
+
132
+ The maintained prebuilt connections come in two shapes, and each has its own
133
+ convention set — including the shape of its usage guide, which is one of the
134
+ few things both shapes fully own:
135
+
136
+ - [Hand-written HTTP providers](./provider-conventions.md#hand-written-http-providers)
137
+ (H1–H14) — `api()` surfaces where Connecta owns every name, schema,
138
+ projection, and error. Cloudflare and Notion.
139
+ - [Hosted-MCP proxies](./provider-conventions.md#hosted-mcp-proxies) (P1–P13) —
140
+ `remoteMcp()` wrappers where the downstream owns the catalog and Connecta
141
+ owns the endpoint, credential, classification, guide, and budget. Linear,
142
+ Stripe, and Mixpanel.
143
+
144
+ Both sets are judged by one measure: what the convention saves the model that
145
+ interacts with connecta, priced in discovery tokens, wrong-tool selection,
146
+ argument retries, or result size. The same document defines
147
+ [what a provider audit checks](./provider-conventions.md#what-the-audit-checks),
148
+ so [#342](https://github.com/zackbart/connecta/issues/342) can run against it
149
+ convention by convention rather than by taste.
150
+
151
+ Two of those conventions decide how a guide is written, and they differ by
152
+ shape. A hand-written provider's guide carries only what a schema cannot,
153
+ because it owns the schemas and should fix them instead
154
+ ([H13](./provider-conventions.md#h13--the-guide-carries-only-what-a-schema-cannot)).
155
+ A proxy's guide carries the reduction and identity-resolution advice its
156
+ schemas will never carry, because it cannot change them
157
+ ([P7](./provider-conventions.md#p7--the-guide-carries-the-reduction-advice-the-schemas-cannot),
158
+ [P8](./provider-conventions.md#p8--identity-resolution-comes-before-action)).
159
+
160
+ ## Tests that enforce this
161
+
162
+ `test/meta-tools.test.ts` owns the guide behavior end to end: the skills
163
+ listing carrying one entry per guided connector, summaries derived from the
164
+ first meaningful line and falling back to the connector description when the
165
+ guide is all markup, whitespace-only guides treated as no guide, content
166
+ returned verbatim including surrounding padding, identical content in two
167
+ deployments staying isolated, every miss erroring rather than falling back to
168
+ the generic guide with an identically labelled skills list on each branch, the
169
+ `guide` pointer in search output, and `guideRequired` appearing for
170
+ connector-required conventions, approval-bound tools, and truncated schemas —
171
+ and being absent from a search that asked for no schemas. `test/server.test.ts`
172
+ owns the conditional half: it compares a guide-free deployment's four tool
173
+ descriptions against a guided one's, and asserts the `usage` skill is
174
+ byte-identical between them.