@zackbart/connecta 0.15.1 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/AGENTS.md +40 -20
  2. package/CHANGELOG.md +653 -0
  3. package/README.md +25 -5
  4. package/bin/connecta.mjs +44 -3
  5. package/dist/access-tokens.d.ts +0 -1
  6. package/dist/access-tokens.js +0 -1
  7. package/dist/activity.d.ts +50 -2
  8. package/dist/activity.js +34 -1
  9. package/dist/apps-shell.d.ts +0 -1
  10. package/dist/apps-shell.js +0 -1
  11. package/dist/auth/bearer.d.ts +0 -1
  12. package/dist/auth/bearer.js +0 -1
  13. package/dist/auth/clerk.d.ts +0 -1
  14. package/dist/auth/clerk.js +45 -9
  15. package/dist/auth/downstream-oauth.d.ts +0 -1
  16. package/dist/auth/downstream-oauth.js +0 -1
  17. package/dist/call-admission.d.ts +0 -1
  18. package/dist/call-admission.js +0 -1
  19. package/dist/catalog-drift.d.ts +99 -0
  20. package/dist/catalog-drift.js +251 -0
  21. package/dist/catalog-fingerprint.d.ts +0 -1
  22. package/dist/catalog-fingerprint.js +0 -1
  23. package/dist/catalog-limits.d.ts +0 -1
  24. package/dist/catalog-limits.js +0 -1
  25. package/dist/catalog-service.d.ts +0 -1
  26. package/dist/catalog-service.js +41 -5
  27. package/dist/catalog.d.ts +11 -1
  28. package/dist/catalog.js +20 -1
  29. package/dist/concurrency.d.ts +0 -1
  30. package/dist/concurrency.js +0 -1
  31. package/dist/connector-scope.d.ts +0 -1
  32. package/dist/connector-scope.js +0 -1
  33. package/dist/connectors/api.d.ts +29 -21
  34. package/dist/connectors/api.js +45 -19
  35. package/dist/connectors/guarded-fetch.d.ts +106 -0
  36. package/dist/connectors/guarded-fetch.js +289 -0
  37. package/dist/connectors/remote-mcp.d.ts +0 -1
  38. package/dist/connectors/remote-mcp.js +0 -1
  39. package/dist/credentials.d.ts +0 -1
  40. package/dist/credentials.js +0 -1
  41. package/dist/errors.d.ts +30 -3
  42. package/dist/errors.js +1 -1
  43. package/dist/execute.d.ts +0 -1
  44. package/dist/execute.js +0 -1
  45. package/dist/executor-admission.d.ts +11 -1
  46. package/dist/executor-admission.js +28 -1
  47. package/dist/executor-result.d.ts +0 -1
  48. package/dist/executor-result.js +0 -1
  49. package/dist/executors/quickjs-child.d.ts +0 -1
  50. package/dist/executors/quickjs-child.js +0 -1
  51. package/dist/executors/quickjs-protocol.d.ts +0 -1
  52. package/dist/executors/quickjs-protocol.js +0 -1
  53. package/dist/executors/quickjs-runtime.d.ts +0 -1
  54. package/dist/executors/quickjs-runtime.js +0 -1
  55. package/dist/executors/quickjs.d.ts +0 -1
  56. package/dist/executors/quickjs.js +2 -1
  57. package/dist/favicon.d.ts +0 -1
  58. package/dist/favicon.js +0 -1
  59. package/dist/index.d.ts +2 -3
  60. package/dist/index.js +24 -7
  61. package/dist/invocation.d.ts +0 -1
  62. package/dist/invocation.js +0 -1
  63. package/dist/json-schema.d.ts +0 -1
  64. package/dist/json-schema.js +0 -1
  65. package/dist/mcp-result.d.ts +0 -1
  66. package/dist/mcp-result.js +0 -1
  67. package/dist/meta-tools.d.ts +0 -1
  68. package/dist/meta-tools.js +0 -1
  69. package/dist/node.d.ts +0 -1
  70. package/dist/node.js +0 -1
  71. package/dist/operator-ui/generated.d.ts +2 -3
  72. package/dist/operator-ui/generated.js +3 -4
  73. package/dist/operator-ui/model.d.ts +10 -1
  74. package/dist/operator-ui/model.js +0 -1
  75. package/dist/operator-ui/view.d.ts +167 -0
  76. package/dist/operator-ui/view.js +270 -0
  77. package/dist/providers/cloudflare.d.ts +0 -1
  78. package/dist/providers/cloudflare.js +261 -299
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -61
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -79
  83. package/dist/providers/notion.d.ts +0 -1
  84. package/dist/providers/notion.js +87 -42
  85. package/dist/providers/stripe.d.ts +13 -1
  86. package/dist/providers/stripe.js +31 -60
  87. package/dist/registry.d.ts +34 -2
  88. package/dist/registry.js +85 -5
  89. package/dist/retired-toolkits.d.ts +0 -1
  90. package/dist/retired-toolkits.js +0 -1
  91. package/dist/routes/access-tokens.d.ts +0 -1
  92. package/dist/routes/access-tokens.js +0 -1
  93. package/dist/routes/activity.d.ts +0 -1
  94. package/dist/routes/activity.js +0 -1
  95. package/dist/routes/credentials.d.ts +0 -1
  96. package/dist/routes/credentials.js +0 -1
  97. package/dist/routes/mcp.d.ts +0 -1
  98. package/dist/routes/mcp.js +0 -1
  99. package/dist/routes/oauth.d.ts +0 -1
  100. package/dist/routes/oauth.js +0 -1
  101. package/dist/routes/shared.d.ts +2 -1
  102. package/dist/routes/shared.js +0 -1
  103. package/dist/routes/ui.d.ts +0 -1
  104. package/dist/routes/ui.js +0 -1
  105. package/dist/server.d.ts +0 -1
  106. package/dist/server.js +11 -1
  107. package/dist/skills.d.ts +0 -1
  108. package/dist/skills.js +0 -1
  109. package/dist/storage/file.d.ts +0 -1
  110. package/dist/storage/file.js +0 -1
  111. package/dist/storage/memory.d.ts +0 -1
  112. package/dist/storage/memory.js +0 -1
  113. package/dist/timeout.d.ts +0 -1
  114. package/dist/timeout.js +0 -1
  115. package/dist/tool-safety.d.ts +0 -1
  116. package/dist/tool-safety.js +0 -1
  117. package/dist/types.d.ts +45 -1
  118. package/dist/types.js +0 -1
  119. package/dist/ui.d.ts +0 -1
  120. package/dist/ui.js +13 -137
  121. package/dist/validate.d.ts +13 -15
  122. package/dist/validate.js +24 -11
  123. package/dist/version.d.ts +1 -2
  124. package/dist/version.js +1 -2
  125. package/documentation/architecture.md +186 -5
  126. package/documentation/auth.md +11 -0
  127. package/documentation/call-admission.md +142 -6
  128. package/documentation/cloudflare.md +92 -10
  129. package/documentation/code-first-exploration.md +2 -2
  130. package/documentation/code-mode.md +14 -14
  131. package/documentation/connector-guides.md +172 -5
  132. package/documentation/connectors.md +89 -0
  133. package/documentation/linear.md +20 -2
  134. package/documentation/mcp-ui-design.md +2 -2
  135. package/documentation/meta-tools.md +7 -1
  136. package/documentation/mixpanel.md +42 -9
  137. package/documentation/notion.md +23 -2
  138. package/documentation/operations.md +325 -5
  139. package/documentation/operator-ui.md +120 -5
  140. package/documentation/provider-audit.md +160 -0
  141. package/documentation/provider-conventions.md +667 -0
  142. package/documentation/request-admission.md +167 -5
  143. package/documentation/rich-output-design.md +1 -2
  144. package/documentation/stripe.md +15 -0
  145. package/documentation/upgrading.md +454 -0
  146. package/ethos.md +14 -9
  147. package/examples/worker/README.md +102 -12
  148. package/examples/worker/src/index.ts +46 -1
  149. package/examples/worker/wrangler.jsonc +14 -1
  150. package/package.json +10 -6
  151. package/templates/node/.dockerignore +10 -0
  152. package/templates/node/.env.example +40 -3
  153. package/templates/node/AGENTS.md +16 -1
  154. package/templates/node/Dockerfile +49 -0
  155. package/templates/node/README.md +117 -2
  156. package/templates/node/docker-compose.yml +39 -0
  157. package/templates/node/package.json +1 -1
  158. package/templates/node/src/file-activity.ts +182 -0
  159. package/templates/node/src/index.ts +80 -4
  160. package/assets/connecta-clay-hero.png +0 -0
  161. package/dist/access-tokens.d.ts.map +0 -1
  162. package/dist/access-tokens.js.map +0 -1
  163. package/dist/activity.d.ts.map +0 -1
  164. package/dist/activity.js.map +0 -1
  165. package/dist/apps-shell.d.ts.map +0 -1
  166. package/dist/apps-shell.js.map +0 -1
  167. package/dist/auth/bearer.d.ts.map +0 -1
  168. package/dist/auth/bearer.js.map +0 -1
  169. package/dist/auth/clerk.d.ts.map +0 -1
  170. package/dist/auth/clerk.js.map +0 -1
  171. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  172. package/dist/auth/downstream-oauth.js.map +0 -1
  173. package/dist/call-admission.d.ts.map +0 -1
  174. package/dist/call-admission.js.map +0 -1
  175. package/dist/catalog-fingerprint.d.ts.map +0 -1
  176. package/dist/catalog-fingerprint.js.map +0 -1
  177. package/dist/catalog-limits.d.ts.map +0 -1
  178. package/dist/catalog-limits.js.map +0 -1
  179. package/dist/catalog-service.d.ts.map +0 -1
  180. package/dist/catalog-service.js.map +0 -1
  181. package/dist/catalog.d.ts.map +0 -1
  182. package/dist/catalog.js.map +0 -1
  183. package/dist/concurrency.d.ts.map +0 -1
  184. package/dist/concurrency.js.map +0 -1
  185. package/dist/connector-scope.d.ts.map +0 -1
  186. package/dist/connector-scope.js.map +0 -1
  187. package/dist/connectors/api.d.ts.map +0 -1
  188. package/dist/connectors/api.js.map +0 -1
  189. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  190. package/dist/connectors/remote-mcp.js.map +0 -1
  191. package/dist/credentials.d.ts.map +0 -1
  192. package/dist/credentials.js.map +0 -1
  193. package/dist/errors.d.ts.map +0 -1
  194. package/dist/errors.js.map +0 -1
  195. package/dist/execute.d.ts.map +0 -1
  196. package/dist/execute.js.map +0 -1
  197. package/dist/executor-admission.d.ts.map +0 -1
  198. package/dist/executor-admission.js.map +0 -1
  199. package/dist/executor-result.d.ts.map +0 -1
  200. package/dist/executor-result.js.map +0 -1
  201. package/dist/executors/quickjs-child.d.ts.map +0 -1
  202. package/dist/executors/quickjs-child.js.map +0 -1
  203. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  204. package/dist/executors/quickjs-protocol.js.map +0 -1
  205. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  206. package/dist/executors/quickjs-runtime.js.map +0 -1
  207. package/dist/executors/quickjs.d.ts.map +0 -1
  208. package/dist/executors/quickjs.js.map +0 -1
  209. package/dist/favicon.d.ts.map +0 -1
  210. package/dist/favicon.js.map +0 -1
  211. package/dist/index.d.ts.map +0 -1
  212. package/dist/index.js.map +0 -1
  213. package/dist/invocation.d.ts.map +0 -1
  214. package/dist/invocation.js.map +0 -1
  215. package/dist/json-schema.d.ts.map +0 -1
  216. package/dist/json-schema.js.map +0 -1
  217. package/dist/mcp-result.d.ts.map +0 -1
  218. package/dist/mcp-result.js.map +0 -1
  219. package/dist/meta-tools.d.ts.map +0 -1
  220. package/dist/meta-tools.js.map +0 -1
  221. package/dist/node.d.ts.map +0 -1
  222. package/dist/node.js.map +0 -1
  223. package/dist/operator-ui/generated.d.ts.map +0 -1
  224. package/dist/operator-ui/generated.js.map +0 -1
  225. package/dist/operator-ui/model.d.ts.map +0 -1
  226. package/dist/operator-ui/model.js.map +0 -1
  227. package/dist/providers/cloudflare.d.ts.map +0 -1
  228. package/dist/providers/cloudflare.js.map +0 -1
  229. package/dist/providers/linear.d.ts.map +0 -1
  230. package/dist/providers/linear.js.map +0 -1
  231. package/dist/providers/mixpanel.d.ts.map +0 -1
  232. package/dist/providers/mixpanel.js.map +0 -1
  233. package/dist/providers/notion.d.ts.map +0 -1
  234. package/dist/providers/notion.js.map +0 -1
  235. package/dist/providers/stripe.d.ts.map +0 -1
  236. package/dist/providers/stripe.js.map +0 -1
  237. package/dist/registry.d.ts.map +0 -1
  238. package/dist/registry.js.map +0 -1
  239. package/dist/retired-toolkits.d.ts.map +0 -1
  240. package/dist/retired-toolkits.js.map +0 -1
  241. package/dist/routes/access-tokens.d.ts.map +0 -1
  242. package/dist/routes/access-tokens.js.map +0 -1
  243. package/dist/routes/activity.d.ts.map +0 -1
  244. package/dist/routes/activity.js.map +0 -1
  245. package/dist/routes/credentials.d.ts.map +0 -1
  246. package/dist/routes/credentials.js.map +0 -1
  247. package/dist/routes/mcp.d.ts.map +0 -1
  248. package/dist/routes/mcp.js.map +0 -1
  249. package/dist/routes/oauth.d.ts.map +0 -1
  250. package/dist/routes/oauth.js.map +0 -1
  251. package/dist/routes/shared.d.ts.map +0 -1
  252. package/dist/routes/shared.js.map +0 -1
  253. package/dist/routes/ui.d.ts.map +0 -1
  254. package/dist/routes/ui.js.map +0 -1
  255. package/dist/server.d.ts.map +0 -1
  256. package/dist/server.js.map +0 -1
  257. package/dist/skills.d.ts.map +0 -1
  258. package/dist/skills.js.map +0 -1
  259. package/dist/storage/file.d.ts.map +0 -1
  260. package/dist/storage/file.js.map +0 -1
  261. package/dist/storage/memory.d.ts.map +0 -1
  262. package/dist/storage/memory.js.map +0 -1
  263. package/dist/timeout.d.ts.map +0 -1
  264. package/dist/timeout.js.map +0 -1
  265. package/dist/tool-safety.d.ts.map +0 -1
  266. package/dist/tool-safety.js.map +0 -1
  267. package/dist/types.d.ts.map +0 -1
  268. package/dist/types.js.map +0 -1
  269. package/dist/ui.d.ts.map +0 -1
  270. package/dist/ui.js.map +0 -1
  271. package/dist/validate.d.ts.map +0 -1
  272. package/dist/validate.js.map +0 -1
  273. package/dist/version.d.ts.map +0 -1
  274. package/dist/version.js.map +0 -1
  275. package/examples/node/README.md +0 -52
  276. package/examples/node/src/index.ts +0 -71
  277. package/src/access-tokens.ts +0 -289
  278. package/src/activity.ts +0 -234
  279. package/src/apps-shell.ts +0 -370
  280. package/src/auth/bearer.ts +0 -61
  281. package/src/auth/clerk.ts +0 -553
  282. package/src/auth/downstream-oauth.ts +0 -575
  283. package/src/call-admission.ts +0 -521
  284. package/src/catalog-fingerprint.ts +0 -52
  285. package/src/catalog-limits.ts +0 -11
  286. package/src/catalog-service.ts +0 -1153
  287. package/src/catalog.ts +0 -790
  288. package/src/concurrency.ts +0 -39
  289. package/src/connector-scope.ts +0 -66
  290. package/src/connectors/api.ts +0 -159
  291. package/src/connectors/remote-mcp.ts +0 -1090
  292. package/src/credentials.ts +0 -428
  293. package/src/errors.ts +0 -364
  294. package/src/execute.ts +0 -1406
  295. package/src/executor-admission.ts +0 -328
  296. package/src/executor-result.ts +0 -107
  297. package/src/executors/quickjs-child.ts +0 -172
  298. package/src/executors/quickjs-protocol.ts +0 -99
  299. package/src/executors/quickjs-runtime.ts +0 -470
  300. package/src/executors/quickjs.ts +0 -776
  301. package/src/favicon.ts +0 -53
  302. package/src/index.ts +0 -666
  303. package/src/invocation.ts +0 -655
  304. package/src/json-schema.ts +0 -11
  305. package/src/mcp-result.ts +0 -43
  306. package/src/meta-tools.ts +0 -1607
  307. package/src/node.ts +0 -206
  308. package/src/operator-ui/browser.css +0 -423
  309. package/src/operator-ui/browser.ts +0 -1281
  310. package/src/operator-ui/generated.ts +0 -4
  311. package/src/operator-ui/model.ts +0 -111
  312. package/src/providers/cloudflare.ts +0 -3999
  313. package/src/providers/linear.ts +0 -302
  314. package/src/providers/mixpanel.ts +0 -229
  315. package/src/providers/notion.ts +0 -1879
  316. package/src/providers/stripe.ts +0 -307
  317. package/src/registry.ts +0 -1085
  318. package/src/retired-toolkits.ts +0 -14
  319. package/src/routes/access-tokens.ts +0 -115
  320. package/src/routes/activity.ts +0 -215
  321. package/src/routes/credentials.ts +0 -254
  322. package/src/routes/mcp.ts +0 -429
  323. package/src/routes/oauth.ts +0 -322
  324. package/src/routes/shared.ts +0 -247
  325. package/src/routes/ui.ts +0 -152
  326. package/src/server.ts +0 -195
  327. package/src/skills.ts +0 -271
  328. package/src/storage/file.ts +0 -123
  329. package/src/storage/memory.ts +0 -44
  330. package/src/timeout.ts +0 -84
  331. package/src/tool-safety.ts +0 -15
  332. package/src/types.ts +0 -523
  333. package/src/ui.ts +0 -744
  334. package/src/validate.ts +0 -387
  335. package/src/version.ts +0 -7
@@ -1,521 +0,0 @@
1
- import { ConnectorCallError } from "./errors.js";
2
- import type {
3
- ConnectorCallAdmissionInput,
4
- ConnectorCallAdmissionPolicy,
5
- ConnectorCallAdmissionRule,
6
- } from "./types.js";
7
-
8
- const DEFAULT_MAX_QUEUE_SIZE = 32;
9
- const DEFAULT_QUEUE_TIMEOUT_MS = 5_000;
10
- const DEFAULT_RETRY_AFTER_MS = 1_000;
11
- const DEFAULT_MAX_PARTITIONS = 1_024;
12
- const MAX_PARTITION_KEY_BYTES = 128;
13
- const DEFAULT_PARTITION_KEY = "";
14
- const enc = new TextEncoder();
15
-
16
- export type CallAdmissionFailureKind =
17
- | "concurrency"
18
- | "budget"
19
- | "cancelled"
20
- | "closed"
21
- | "partition";
22
-
23
- /**
24
- * A locally-produced connector-call failure. Extending ConnectorCallError
25
- * preserves the public error envelope while letting call paths avoid recording
26
- * a refusal as evidence that the downstream provider is unhealthy.
27
- */
28
- export class CallAdmissionError extends ConnectorCallError {
29
- constructor(
30
- readonly admissionKind: CallAdmissionFailureKind,
31
- code: ConstructorParameters<typeof ConnectorCallError>[0],
32
- message: string,
33
- opts: ConstructorParameters<typeof ConnectorCallError>[2] = {},
34
- ) {
35
- super(code, message, opts);
36
- this.name = "CallAdmissionError";
37
- }
38
- }
39
-
40
- export function isCallAdmissionError(
41
- error: unknown,
42
- ): error is CallAdmissionError {
43
- return error instanceof CallAdmissionError;
44
- }
45
-
46
- export interface CallAdmissionPermit {
47
- /** Time spent in the concurrency queue. Zero for immediate admission. */
48
- readonly waitMs: number;
49
- /** Idempotent. */
50
- release(): void;
51
- }
52
-
53
- interface Waiter {
54
- queuedAt: number;
55
- resolve: (permit: CallAdmissionPermit) => void;
56
- reject: (error: CallAdmissionError) => void;
57
- signal?: AbortSignal;
58
- onAbort?: () => void;
59
- timer?: ReturnType<typeof setTimeout>;
60
- }
61
-
62
- interface PartitionState {
63
- active: number;
64
- waiters: Waiter[];
65
- admittedAt: number[];
66
- }
67
-
68
- export interface ConnectorCallAdmissionSnapshot {
69
- rules: number;
70
- partitions: number;
71
- active: number;
72
- queued: number;
73
- closed: boolean;
74
- totals: {
75
- admitted: number;
76
- queued: number;
77
- rejected: number;
78
- rateLimited: number;
79
- cancelled: number;
80
- };
81
- queueWaitMs: {
82
- count: number;
83
- total: number;
84
- max: number;
85
- };
86
- }
87
-
88
- function positiveWhole(value: number, name: string): number {
89
- if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
90
- throw new TypeError(`${name} must be a positive whole number.`);
91
- }
92
- return value;
93
- }
94
-
95
- function nonNegativeWhole(value: number, name: string): number {
96
- if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
97
- throw new TypeError(`${name} must be a non-negative whole number.`);
98
- }
99
- return value;
100
- }
101
-
102
- /**
103
- * Per-runtime, per-connector call admission. State contains only bounded
104
- * partition keys, counters, timestamps, signals, and promise continuations;
105
- * tool arguments never enter the controller.
106
- */
107
- export class ConnectorCallAdmissionController {
108
- private readonly maxConcurrency: number | undefined;
109
- private readonly maxQueueSize: number;
110
- private readonly queueTimeoutMs: number;
111
- private readonly retryAfterMs: number;
112
- private readonly maxPartitions: number;
113
- private readonly budget:
114
- | { maxCalls: number; windowMs: number }
115
- | undefined;
116
- private readonly partitionKey:
117
- | ConnectorCallAdmissionRule["partitionKey"]
118
- | undefined;
119
- private readonly partitions = new Map<string, PartitionState>();
120
- private closed = false;
121
- private admittedTotal = 0;
122
- private queuedTotal = 0;
123
- private rejectedTotal = 0;
124
- private rateLimitedTotal = 0;
125
- private cancelledTotal = 0;
126
- private queueWaitCount = 0;
127
- private queueWaitTotalMs = 0;
128
- private queueWaitMaxMs = 0;
129
-
130
- constructor(
131
- readonly connectorId: string,
132
- policy: ConnectorCallAdmissionPolicy,
133
- ) {
134
- if (!Array.isArray(policy.rules) || policy.rules.length !== 1) {
135
- throw new TypeError(
136
- `connector "${connectorId}" callAdmission.rules must contain exactly one rule in this release.`,
137
- );
138
- }
139
- const rule = policy.rules[0];
140
- if (
141
- rule.maxConcurrency === undefined &&
142
- rule.budget === undefined
143
- ) {
144
- throw new TypeError(
145
- `connector "${connectorId}" callAdmission rule must declare maxConcurrency or budget.`,
146
- );
147
- }
148
- this.maxConcurrency =
149
- rule.maxConcurrency === undefined
150
- ? undefined
151
- : positiveWhole(
152
- rule.maxConcurrency,
153
- `connector "${connectorId}" callAdmission maxConcurrency`,
154
- );
155
- if (
156
- this.maxConcurrency === undefined &&
157
- (rule.maxQueueSize !== undefined ||
158
- rule.queueTimeoutMs !== undefined ||
159
- rule.retryAfterMs !== undefined)
160
- ) {
161
- throw new TypeError(
162
- `connector "${connectorId}" callAdmission queue settings require maxConcurrency.`,
163
- );
164
- }
165
- this.maxQueueSize = nonNegativeWhole(
166
- rule.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
167
- `connector "${connectorId}" callAdmission maxQueueSize`,
168
- );
169
- this.queueTimeoutMs = positiveWhole(
170
- rule.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS,
171
- `connector "${connectorId}" callAdmission queueTimeoutMs`,
172
- );
173
- this.retryAfterMs = nonNegativeWhole(
174
- rule.retryAfterMs ?? DEFAULT_RETRY_AFTER_MS,
175
- `connector "${connectorId}" callAdmission retryAfterMs`,
176
- );
177
- this.maxPartitions = positiveWhole(
178
- policy.maxPartitions ?? DEFAULT_MAX_PARTITIONS,
179
- `connector "${connectorId}" callAdmission maxPartitions`,
180
- );
181
- if (rule.budget) {
182
- if (rule.budget.kind !== "rolling-window") {
183
- throw new TypeError(
184
- `connector "${connectorId}" callAdmission budget kind must be "rolling-window".`,
185
- );
186
- }
187
- const maxCalls = positiveWhole(
188
- rule.budget.maxCalls,
189
- `connector "${connectorId}" callAdmission budget.maxCalls`,
190
- );
191
- const windowMs = positiveWhole(
192
- rule.budget.windowMs,
193
- `connector "${connectorId}" callAdmission budget.windowMs`,
194
- );
195
- this.budget = { maxCalls, windowMs };
196
- } else {
197
- this.budget = undefined;
198
- }
199
- this.partitionKey = rule.partitionKey;
200
- }
201
-
202
- acquire(
203
- input: Readonly<ConnectorCallAdmissionInput> & { signal?: AbortSignal },
204
- ): Promise<CallAdmissionPermit> {
205
- // Copy the signal out before constructing any waiter closure. Referencing
206
- // `input` from a queued callback would retain its `args`, defeating the
207
- // limiter's payload-free state contract.
208
- const signal = input.signal;
209
- if (this.closed) {
210
- return Promise.reject(
211
- new CallAdmissionError(
212
- "closed",
213
- "unavailable",
214
- `Connector "${this.connectorId}" call admission is closed.`,
215
- ),
216
- );
217
- }
218
- if (signal?.aborted) {
219
- this.cancelledTotal++;
220
- return Promise.reject(this.cancelled(signal));
221
- }
222
-
223
- let key: string;
224
- try {
225
- key = this.partitionKey
226
- ? this.partitionKey({
227
- toolName: input.toolName,
228
- args: input.args,
229
- })
230
- : DEFAULT_PARTITION_KEY;
231
- } catch (cause) {
232
- this.rejectedTotal++;
233
- return Promise.reject(
234
- new CallAdmissionError(
235
- "partition",
236
- "connector_call_failed",
237
- `Connector "${this.connectorId}" call-admission partitionKey threw.`,
238
- { cause },
239
- ),
240
- );
241
- }
242
- if (
243
- typeof key !== "string" ||
244
- enc.encode(key).length > MAX_PARTITION_KEY_BYTES
245
- ) {
246
- this.rejectedTotal++;
247
- return Promise.reject(
248
- new CallAdmissionError(
249
- "partition",
250
- "connector_call_failed",
251
- `Connector "${this.connectorId}" call-admission partitionKey must return a string of at most ${MAX_PARTITION_KEY_BYTES} UTF-8 bytes.`,
252
- ),
253
- );
254
- }
255
- // A partition callback is operator code and may synchronously abort the
256
- // caller. Recheck after it returns so that cancellation cannot consume a
257
- // budget entry or concurrency slot.
258
- if (signal?.aborted) {
259
- this.cancelledTotal++;
260
- return Promise.reject(this.cancelled(signal));
261
- }
262
-
263
- const now = Date.now();
264
- let state = this.partitions.get(key);
265
- if (!state) {
266
- this.evictIdlePartitions(now);
267
- if (this.partitions.size >= this.maxPartitions) {
268
- this.rejectedTotal++;
269
- return Promise.reject(
270
- new CallAdmissionError(
271
- "partition",
272
- "rate_limited",
273
- `Connector "${this.connectorId}" call-admission partition capacity is exhausted.`,
274
- { retryAfterMs: this.retryAfterMs },
275
- ),
276
- );
277
- }
278
- state = {
279
- active: 0,
280
- waiters: [],
281
- admittedAt: [],
282
- };
283
- this.partitions.set(key, state);
284
- }
285
- this.pruneBudget(state, now);
286
- const budgetRetryAfterMs = this.budgetRetryAfterMs(state, now);
287
- if (budgetRetryAfterMs !== undefined) {
288
- this.rateLimitedTotal++;
289
- return Promise.reject(this.budgetLimited(budgetRetryAfterMs));
290
- }
291
- if (
292
- this.maxConcurrency === undefined ||
293
- state.active < this.maxConcurrency
294
- ) {
295
- return Promise.resolve(this.admit(state, now, 0));
296
- }
297
- if (state.waiters.length >= this.maxQueueSize) {
298
- this.rejectedTotal++;
299
- return Promise.reject(this.concurrencyLimited("queue is full"));
300
- }
301
-
302
- return new Promise<CallAdmissionPermit>((resolve, reject) => {
303
- const waiter: Waiter = {
304
- queuedAt: now,
305
- resolve,
306
- reject,
307
- ...(signal ? { signal } : {}),
308
- };
309
- waiter.onAbort = () => {
310
- if (!this.removeWaiter(state!, waiter)) return;
311
- this.cleanupWaiter(waiter);
312
- this.cancelledTotal++;
313
- reject(this.cancelled(signal!));
314
- this.maybeDeletePartition(key, state!, Date.now());
315
- };
316
- waiter.timer = setTimeout(() => {
317
- if (!this.removeWaiter(state!, waiter)) return;
318
- this.cleanupWaiter(waiter);
319
- this.rejectedTotal++;
320
- reject(
321
- this.concurrencyLimited(
322
- `queue wait exceeded ${this.queueTimeoutMs}ms`,
323
- ),
324
- );
325
- this.maybeDeletePartition(key, state!, Date.now());
326
- }, this.queueTimeoutMs);
327
- state!.waiters.push(waiter);
328
- this.queuedTotal++;
329
- signal?.addEventListener("abort", waiter.onAbort, { once: true });
330
- // Close the check-to-listener race: an abort before registration is not
331
- // replayed by AbortSignal, so inspect it once after the waiter is fully
332
- // removable and its timer is installed.
333
- if (signal?.aborted) waiter.onAbort();
334
- });
335
- }
336
-
337
- snapshot(): ConnectorCallAdmissionSnapshot {
338
- let active = 0;
339
- let queued = 0;
340
- for (const state of this.partitions.values()) {
341
- active += state.active;
342
- queued += state.waiters.length;
343
- }
344
- return {
345
- rules: 1,
346
- partitions: this.partitions.size,
347
- active,
348
- queued,
349
- closed: this.closed,
350
- totals: {
351
- admitted: this.admittedTotal,
352
- queued: this.queuedTotal,
353
- rejected: this.rejectedTotal,
354
- rateLimited: this.rateLimitedTotal,
355
- cancelled: this.cancelledTotal,
356
- },
357
- queueWaitMs: {
358
- count: this.queueWaitCount,
359
- total: this.queueWaitTotalMs,
360
- max: this.queueWaitMaxMs,
361
- },
362
- };
363
- }
364
-
365
- close(): void {
366
- if (this.closed) return;
367
- this.closed = true;
368
- for (const state of this.partitions.values()) {
369
- for (const waiter of state.waiters.splice(0)) {
370
- this.cleanupWaiter(waiter);
371
- waiter.reject(
372
- new CallAdmissionError(
373
- "closed",
374
- "unavailable",
375
- `Connector "${this.connectorId}" call admission is closed.`,
376
- ),
377
- );
378
- }
379
- }
380
- }
381
-
382
- private admit(
383
- state: PartitionState,
384
- now: number,
385
- waitMs: number,
386
- ): CallAdmissionPermit {
387
- state.active++;
388
- if (this.budget) state.admittedAt.push(now);
389
- this.admittedTotal++;
390
- if (waitMs > 0) {
391
- this.queueWaitCount++;
392
- this.queueWaitTotalMs += waitMs;
393
- this.queueWaitMaxMs = Math.max(this.queueWaitMaxMs, waitMs);
394
- }
395
- let released = false;
396
- return {
397
- waitMs,
398
- release: () => {
399
- if (released) return;
400
- released = true;
401
- if (state.active > 0) state.active--;
402
- this.pump(state);
403
- },
404
- };
405
- }
406
-
407
- private pump(state: PartitionState): void {
408
- if (this.closed || this.maxConcurrency === undefined) return;
409
- while (
410
- state.active < this.maxConcurrency &&
411
- state.waiters.length > 0
412
- ) {
413
- const waiter = state.waiters.shift()!;
414
- this.cleanupWaiter(waiter);
415
- if (waiter.signal?.aborted) {
416
- this.cancelledTotal++;
417
- waiter.reject(this.cancelled(waiter.signal));
418
- continue;
419
- }
420
- const now = Date.now();
421
- this.pruneBudget(state, now);
422
- const retryAfterMs = this.budgetRetryAfterMs(state, now);
423
- if (retryAfterMs !== undefined) {
424
- this.rateLimitedTotal++;
425
- waiter.reject(this.budgetLimited(retryAfterMs));
426
- continue;
427
- }
428
- const waitMs = Math.max(0, now - waiter.queuedAt);
429
- waiter.resolve(this.admit(state, now, waitMs));
430
- }
431
- }
432
-
433
- private pruneBudget(state: PartitionState, now: number): void {
434
- const budget = this.budget;
435
- if (!budget || state.admittedAt.length === 0) return;
436
- let expired = 0;
437
- while (expired < state.admittedAt.length) {
438
- const admittedAt = state.admittedAt[expired];
439
- if (admittedAt === undefined || admittedAt + budget.windowMs > now) {
440
- break;
441
- }
442
- expired++;
443
- }
444
- if (expired > 0) state.admittedAt.splice(0, expired);
445
- }
446
-
447
- private budgetRetryAfterMs(
448
- state: PartitionState,
449
- now: number,
450
- ): number | undefined {
451
- const budget = this.budget;
452
- if (!budget || state.admittedAt.length < budget.maxCalls) {
453
- return undefined;
454
- }
455
- const oldestAdmission = state.admittedAt[0];
456
- if (oldestAdmission === undefined) return undefined;
457
- return Math.max(0, oldestAdmission + budget.windowMs - now);
458
- }
459
-
460
- private evictIdlePartitions(now: number): void {
461
- for (const [key, state] of this.partitions) {
462
- this.maybeDeletePartition(key, state, now);
463
- }
464
- }
465
-
466
- private maybeDeletePartition(
467
- key: string,
468
- state: PartitionState,
469
- now: number,
470
- ): void {
471
- this.pruneBudget(state, now);
472
- if (
473
- state.active === 0 &&
474
- state.waiters.length === 0 &&
475
- state.admittedAt.length === 0
476
- ) {
477
- this.partitions.delete(key);
478
- }
479
- }
480
-
481
- private removeWaiter(state: PartitionState, waiter: Waiter): boolean {
482
- const index = state.waiters.indexOf(waiter);
483
- if (index < 0) return false;
484
- state.waiters.splice(index, 1);
485
- return true;
486
- }
487
-
488
- private cleanupWaiter(waiter: Waiter): void {
489
- if (waiter.timer !== undefined) clearTimeout(waiter.timer);
490
- if (waiter.onAbort) {
491
- waiter.signal?.removeEventListener("abort", waiter.onAbort);
492
- }
493
- }
494
-
495
- private concurrencyLimited(reason: string): CallAdmissionError {
496
- return new CallAdmissionError(
497
- "concurrency",
498
- "rate_limited",
499
- `Connector "${this.connectorId}" call concurrency ${reason}.`,
500
- { retryAfterMs: this.retryAfterMs },
501
- );
502
- }
503
-
504
- private budgetLimited(retryAfterMs: number): CallAdmissionError {
505
- return new CallAdmissionError(
506
- "budget",
507
- "rate_limited",
508
- `Connector "${this.connectorId}" rolling call budget is exhausted.`,
509
- { retryAfterMs },
510
- );
511
- }
512
-
513
- private cancelled(signal: AbortSignal): CallAdmissionError {
514
- return new CallAdmissionError(
515
- "cancelled",
516
- "timeout",
517
- `Connector "${this.connectorId}" call was cancelled before admission.`,
518
- { cause: signal.reason },
519
- );
520
- }
521
- }
@@ -1,52 +0,0 @@
1
- import type { ToolDef } from "./types.js";
2
-
3
- const encoder = new TextEncoder();
4
-
5
- export interface CatalogSnapshot {
6
- tools: ToolDef[];
7
- /** SHA-256 over the exact serialized tool array, prefixed with its size. */
8
- fingerprint: string;
9
- /** UTF-8 form reused by hashing, size enforcement, and chunking. */
10
- serializedBytes: Uint8Array;
11
- }
12
-
13
- async function fingerprintBytes(bytes: Uint8Array): Promise<string> {
14
- const digest = new Uint8Array(
15
- await crypto.subtle.digest("SHA-256", bytes),
16
- );
17
- const hex = [...digest]
18
- .map((byte) => byte.toString(16).padStart(2, "0"))
19
- .join("");
20
- return `sha256:${bytes.byteLength}:${hex}`;
21
- }
22
-
23
- /** Fingerprint an already serialized catalog without parsing/reserializing it. */
24
- export async function fingerprintSerializedCatalog(
25
- serializedTools: string,
26
- ): Promise<{ fingerprint: string; byteLength: number }> {
27
- const bytes = encoder.encode(serializedTools);
28
- return {
29
- fingerprint: await fingerprintBytes(bytes),
30
- byteLength: bytes.byteLength,
31
- };
32
- }
33
-
34
- /**
35
- * Capture every persisted/discovered field in one deterministic snapshot.
36
- * Object key order is allowed to cause a conservative extra write; changing
37
- * any serialized field must change the digest.
38
- */
39
- export async function snapshotCatalog(
40
- tools: ToolDef[],
41
- ): Promise<CatalogSnapshot> {
42
- const serializedTools = JSON.stringify(tools);
43
- if (serializedTools === undefined) {
44
- throw new TypeError("Tool catalog is not JSON-serializable.");
45
- }
46
- const bytes = encoder.encode(serializedTools);
47
- return {
48
- tools,
49
- fingerprint: await fingerprintBytes(bytes),
50
- serializedBytes: bytes,
51
- };
52
- }
@@ -1,11 +0,0 @@
1
- /** Complete-catalog ceiling shared by dynamically listed connector catalogs. */
2
- export const MAX_CATALOG_TOOLS = 100_000;
3
-
4
- /**
5
- * Large enough to cross a single Workers KV value boundary while keeping the
6
- * serialized form at one quarter of a 128 MiB Worker heap.
7
- */
8
- export const MAX_SERIALIZED_CATALOG_BYTES = 32 * 1024 * 1024;
9
-
10
- /** Persisted values stay comfortably below platform per-value limits. */
11
- export const MAX_CATALOG_CHUNK_BYTES = 1024 * 1024;