@zackbart/connecta 0.34.0 → 0.35.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 (428) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/README.md +62 -105
  3. package/SECURITY.md +3 -2
  4. package/bin/connecta.mjs +41 -16
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +2 -1
  7. package/dist/access-tokens.js +66 -30
  8. package/dist/activity.d.ts +1 -1
  9. package/dist/activity.js +2 -0
  10. package/dist/agent-config.d.ts +23 -0
  11. package/dist/agent-config.js +59 -0
  12. package/dist/agent-description.js +16 -0
  13. package/dist/agent-identity.js +4 -4
  14. package/dist/agent-observation.js +39 -0
  15. package/dist/auth/clerk-sdk/client.d.ts +2 -0
  16. package/dist/auth/clerk-sdk/generated.js +554 -563
  17. package/dist/auth/clerk-transport.js +70 -3
  18. package/dist/auth/clerk.d.ts +13 -0
  19. package/dist/auth/clerk.js +653 -246
  20. package/dist/auth/cloudflare-access.js +46 -42
  21. package/dist/auth/downstream-client-metadata.js +2 -2
  22. package/dist/auth/downstream-oauth.js +2051 -1160
  23. package/dist/auth/downstream-revocation.js +88 -88
  24. package/dist/auth/native-call.js +20 -0
  25. package/dist/auth/native.js +184 -0
  26. package/dist/auth/oauth-start-reset.js +11 -5
  27. package/dist/auth/oauth-token-response.js +121 -102
  28. package/dist/auth/oauth-v2-migration.js +97 -78
  29. package/dist/auth/renewable-grant.js +10 -0
  30. package/dist/auth/static-oauth.js +663 -271
  31. package/dist/auth/vercel-agent.js +266 -0
  32. package/dist/auth-admission.js +204 -0
  33. package/dist/auth-elicitation.js +350 -241
  34. package/dist/authorization-handoff.js +164 -90
  35. package/dist/call-admission-context.js +3 -10
  36. package/dist/call-admission.js +2 -0
  37. package/dist/call-classification.js +9 -0
  38. package/dist/call-recovery.js +32 -13
  39. package/dist/canonical-json.js +8 -0
  40. package/dist/catalog-cache.js +369 -213
  41. package/dist/catalog-service.js +659 -530
  42. package/dist/catalog.js +5 -1
  43. package/dist/concurrency.js +0 -25
  44. package/dist/config-defaults.d.ts +1 -0
  45. package/dist/config-defaults.js +1 -0
  46. package/dist/config-schema.d.ts +3 -1
  47. package/dist/config-schema.js +13 -3
  48. package/dist/config.d.ts +40 -6
  49. package/dist/config.js +40 -26
  50. package/dist/connector-caller.d.ts +4 -0
  51. package/dist/connector-caller.js +17 -4
  52. package/dist/connectors/api-connector.d.ts +15 -7
  53. package/dist/connectors/api-connector.js +147 -57
  54. package/dist/connectors/guarded-fetch.d.ts +51 -17
  55. package/dist/connectors/guarded-fetch.js +215 -121
  56. package/dist/connectors/negotiation-cache.js +20 -10
  57. package/dist/connectors/option-shapes.js +1 -1
  58. package/dist/connectors/remote-mcp.d.ts +12 -30
  59. package/dist/connectors/remote-mcp.js +960 -564
  60. package/dist/d1.js +10 -243
  61. package/dist/derived-context.js +5 -0
  62. package/dist/describe-config.d.ts +5 -1
  63. package/dist/describe-config.js +3 -0
  64. package/dist/downstream-input.js +347 -260
  65. package/dist/errors.d.ts +87 -6
  66. package/dist/errors.js +114 -4
  67. package/dist/execute.js +87 -50
  68. package/dist/executor-admission.js +3 -0
  69. package/dist/executors/quickjs.js +163 -112
  70. package/dist/guarded-transport.js +50 -0
  71. package/dist/guest-runtime.js +1 -1
  72. package/dist/http/client.js +439 -0
  73. package/dist/http/errors.js +558 -0
  74. package/dist/http/fields.js +49 -0
  75. package/dist/http/hatch-paging.js +318 -0
  76. package/dist/http/hatch.js +1279 -0
  77. package/dist/http/json-schema.js +267 -0
  78. package/dist/http/node-transport.js +241 -0
  79. package/dist/http/oauth.js +162 -0
  80. package/dist/http/paging.js +228 -0
  81. package/dist/http/provider.js +221 -0
  82. package/dist/http/safety.js +82 -0
  83. package/dist/http/services.js +79 -0
  84. package/dist/http/tools.js +383 -0
  85. package/dist/http/vendor-index.js +138 -0
  86. package/dist/http/vendor.js +448 -0
  87. package/dist/http/write-request.js +66 -0
  88. package/dist/http-policy.d.ts +11 -0
  89. package/dist/http-policy.js +45 -0
  90. package/dist/inbound-credential.js +1 -1
  91. package/dist/index.d.ts +2 -2
  92. package/dist/index.js +4 -0
  93. package/dist/invocation-auth.js +30 -19
  94. package/dist/invocation.js +221 -87
  95. package/dist/mcp-result.js +0 -4
  96. package/dist/membership.d.ts +4 -0
  97. package/dist/meta-output.js +1 -1
  98. package/dist/meta-tools.js +534 -408
  99. package/dist/native-recovery.js +52 -0
  100. package/dist/native-tools.js +382 -0
  101. package/dist/node.d.ts +4 -3
  102. package/dist/node.js +45 -8
  103. package/dist/oauth-handoff.js +177 -104
  104. package/dist/oauth-partition.js +6 -20
  105. package/dist/oauth-sealing.js +6 -7
  106. package/dist/operation-vocabulary.js +101 -0
  107. package/dist/operator-record.d.ts +2 -2
  108. package/dist/operator-record.js +17 -4
  109. package/dist/operator-ui/agents.d.ts +5 -4
  110. package/dist/operator-ui/agents.js +17 -7
  111. package/dist/operator-ui/config-summary.js +8 -4
  112. package/dist/operator-ui/connection-map.js +9 -6
  113. package/dist/operator-ui/contract.d.ts +7 -3
  114. package/dist/operator-ui/endpoints.js +5 -14
  115. package/dist/operator-ui/fix-prompts.d.ts +1 -1
  116. package/dist/operator-ui/fix-prompts.js +0 -8
  117. package/dist/operator-ui/generated.js +3 -3
  118. package/dist/operator-ui/health.js +52 -0
  119. package/dist/operator-ui/model.d.ts +2 -3
  120. package/dist/operator-ui/overview.js +11 -19
  121. package/dist/operator-ui/setup-commands.js +0 -4
  122. package/dist/operator-ui/view.d.ts +5 -29
  123. package/dist/operator-ui/view.js +3 -59
  124. package/dist/program-writes.js +47 -26
  125. package/dist/provider-call-facts.d.ts +115 -0
  126. package/dist/provider-call-facts.js +182 -0
  127. package/dist/provider-call.d.ts +112 -0
  128. package/dist/provider-call.js +271 -0
  129. package/dist/provider.d.ts +71 -32
  130. package/dist/provider.js +134 -40
  131. package/dist/providers/_shared/google/hatch.js +91 -0
  132. package/dist/providers/_shared/google/kit.js +537 -0
  133. package/dist/providers/_shared/google/result-size.js +15 -2
  134. package/dist/providers/_shared/google/service-account.d.ts +2 -1
  135. package/dist/providers/_shared/google/service-account.js +8 -4
  136. package/dist/providers/_shared/google/workspace.d.ts +8 -209
  137. package/dist/providers/_shared/google/workspace.js +7 -426
  138. package/dist/providers/_shared/rest/compact-index.js +86 -0
  139. package/dist/providers/_shared/rest/dispatch.js +3 -23
  140. package/dist/providers/_shared/rest/kit.js +546 -0
  141. package/dist/providers/_shared/rest/operation-index.js +230 -14
  142. package/dist/providers/_shared/rest/response-media.js +11 -0
  143. package/dist/providers/_shared/rest/tools.js +101 -78
  144. package/dist/providers/_shared/rest/value-safety.js +353 -195
  145. package/dist/providers/_shared/sensitive-fields.js +27 -0
  146. package/dist/providers/breeze/api.js +584 -0
  147. package/dist/providers/breeze/index.d.ts +3 -3
  148. package/dist/providers/breeze/index.js +67 -1420
  149. package/dist/providers/breeze/openapi.generated.js +60 -0
  150. package/dist/providers/breeze/skill.generated.js +2 -6
  151. package/dist/providers/breeze/tools.js +696 -0
  152. package/dist/providers/ccb/api.js +496 -0
  153. package/dist/providers/ccb/index.d.ts +17 -37
  154. package/dist/providers/ccb/index.js +77 -1702
  155. package/dist/providers/ccb/lib/constants.d.ts +34 -0
  156. package/dist/providers/ccb/lib/constants.js +74 -0
  157. package/dist/providers/ccb/lib/guide.js +9 -0
  158. package/dist/providers/ccb/lib/verdicts.js +387 -0
  159. package/dist/providers/ccb/openapi.generated.js +397 -0
  160. package/dist/providers/ccb/safety.js +295 -0
  161. package/dist/providers/ccb/skill.generated.js +2 -6
  162. package/dist/providers/ccb/tools.js +853 -0
  163. package/dist/providers/cloudflare/api.js +1066 -0
  164. package/dist/providers/cloudflare/index.d.ts +7 -22
  165. package/dist/providers/cloudflare/index.js +80 -194
  166. package/dist/providers/cloudflare/lib/config.d.ts +16 -0
  167. package/dist/providers/cloudflare/lib/config.js +2 -0
  168. package/dist/providers/cloudflare/lib/discovery.js +29 -0
  169. package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
  170. package/dist/providers/cloudflare/lib/hatch.js +103 -0
  171. package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
  172. package/dist/providers/cloudflare/lib/response-media.js +75 -0
  173. package/dist/providers/cloudflare/openapi.generated.js +3299 -3208
  174. package/dist/providers/cloudflare/{value-safety.js → safety.js} +538 -18
  175. package/dist/providers/cloudflare/skill.generated.js +2 -10
  176. package/dist/providers/cloudflare/tools.js +360 -0
  177. package/dist/providers/docs/api.js +51 -0
  178. package/dist/providers/docs/index.d.ts +1 -12
  179. package/dist/providers/docs/index.js +53 -1165
  180. package/dist/providers/docs/lib/constants.d.ts +18 -0
  181. package/dist/providers/docs/lib/constants.js +18 -0
  182. package/dist/providers/docs/lib/discovery.js +26 -0
  183. package/dist/providers/docs/lib/requests.js +48 -0
  184. package/dist/providers/docs/lib/schemas.js +168 -0
  185. package/dist/providers/docs/openapi.generated.js +16 -0
  186. package/dist/providers/docs/safety.js +54 -0
  187. package/dist/providers/docs/skill.generated.js +2 -6
  188. package/dist/providers/docs/tools.js +802 -0
  189. package/dist/providers/drive/api.js +177 -0
  190. package/dist/providers/drive/index.d.ts +1 -12
  191. package/dist/providers/drive/index.js +38 -1448
  192. package/dist/providers/drive/lib/constants.d.ts +6 -0
  193. package/dist/providers/drive/lib/constants.js +6 -0
  194. package/dist/providers/drive/lib/contracts.js +532 -0
  195. package/dist/providers/drive/lib/hatch.js +197 -0
  196. package/dist/providers/drive/openapi.generated.js +77 -0
  197. package/dist/providers/drive/safety.js +90 -0
  198. package/dist/providers/drive/skill.generated.js +2 -6
  199. package/dist/providers/drive/tools.js +1290 -0
  200. package/dist/providers/forms/api.js +71 -0
  201. package/dist/providers/forms/index.d.ts +1 -12
  202. package/dist/providers/forms/index.js +49 -1145
  203. package/dist/providers/forms/lib/constants.d.ts +14 -0
  204. package/dist/providers/forms/lib/constants.js +19 -0
  205. package/dist/providers/forms/lib/hatch.js +72 -0
  206. package/dist/providers/forms/openapi.generated.js +23 -0
  207. package/dist/providers/forms/safety.js +34 -0
  208. package/dist/providers/forms/skill.generated.js +2 -8
  209. package/dist/providers/forms/tools.js +1024 -0
  210. package/dist/providers/github/api.js +150 -0
  211. package/dist/providers/github/index.d.ts +4 -5
  212. package/dist/providers/github/index.js +30 -87
  213. package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
  214. package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
  215. package/dist/providers/github/lib/discovery.js +132 -0
  216. package/dist/providers/github/lib/operations.js +58 -0
  217. package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
  218. package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
  219. package/dist/providers/github/openapi.generated.js +1216 -0
  220. package/dist/providers/github/safety.js +216 -0
  221. package/dist/providers/github/skill.generated.js +2 -5
  222. package/dist/providers/github/tools.js +506 -0
  223. package/dist/providers/gmail/api.js +114 -0
  224. package/dist/providers/gmail/index.d.ts +1 -12
  225. package/dist/providers/gmail/index.js +73 -1621
  226. package/dist/providers/gmail/lib/constants.d.ts +13 -0
  227. package/dist/providers/gmail/lib/constants.js +16 -0
  228. package/dist/providers/gmail/lib/index.js +173 -0
  229. package/dist/providers/gmail/lib/schemas.js +176 -0
  230. package/dist/providers/gmail/lib/threading.js +120 -0
  231. package/dist/providers/gmail/openapi.generated.js +92 -0
  232. package/dist/providers/gmail/safety.js +21 -0
  233. package/dist/providers/gmail/skill.generated.js +2 -6
  234. package/dist/providers/gmail/tools.js +1435 -0
  235. package/dist/providers/overflow/api.js +1008 -0
  236. package/dist/providers/overflow/index.d.ts +3 -17
  237. package/dist/providers/overflow/index.js +60 -1488
  238. package/dist/providers/overflow/skill.generated.js +2 -5
  239. package/dist/providers/overflow/tools.js +873 -0
  240. package/dist/providers/planning-center/api.js +467 -0
  241. package/dist/providers/planning-center/index.d.ts +6 -35
  242. package/dist/providers/planning-center/index.js +69 -2445
  243. package/dist/providers/planning-center/lib/constants.d.ts +58 -0
  244. package/dist/providers/planning-center/lib/constants.js +83 -0
  245. package/dist/providers/planning-center/lib/discovery.js +40 -0
  246. package/dist/providers/planning-center/lib/guide.js +39 -0
  247. package/dist/providers/planning-center/lib/json-api.js +83 -0
  248. package/dist/providers/planning-center/lib/verdicts.js +1758 -0
  249. package/dist/providers/planning-center/openapi.generated.js +1764 -0
  250. package/dist/providers/planning-center/safety.js +188 -0
  251. package/dist/providers/planning-center/skill.generated.js +2 -27
  252. package/dist/providers/planning-center/tools.js +1010 -0
  253. package/dist/providers/sheets/api.js +133 -0
  254. package/dist/providers/sheets/index.d.ts +4 -16
  255. package/dist/providers/sheets/index.js +61 -1172
  256. package/dist/providers/sheets/lib/constants.d.ts +14 -0
  257. package/dist/providers/sheets/lib/constants.js +14 -0
  258. package/dist/providers/sheets/lib/hatch.js +163 -0
  259. package/dist/providers/sheets/lib/requests.js +105 -0
  260. package/dist/providers/sheets/lib/schemas.js +138 -0
  261. package/dist/providers/sheets/openapi.generated.js +30 -0
  262. package/dist/providers/sheets/safety.js +27 -0
  263. package/dist/providers/sheets/skill.generated.js +2 -7
  264. package/dist/providers/sheets/tools.js +734 -0
  265. package/dist/providers/slides/api.js +147 -0
  266. package/dist/providers/slides/index.d.ts +2 -10
  267. package/dist/providers/slides/index.js +48 -2555
  268. package/dist/providers/slides/lib/constants.d.ts +11 -0
  269. package/dist/providers/slides/lib/constants.js +11 -0
  270. package/dist/providers/slides/lib/requests.js +60 -0
  271. package/dist/providers/slides/lib/schemas.js +565 -0
  272. package/dist/providers/slides/openapi.generated.js +18 -0
  273. package/dist/providers/slides/skill.generated.js +2 -6
  274. package/dist/providers/slides/tools.js +1968 -0
  275. package/dist/providers/stripe/api.js +401 -0
  276. package/dist/providers/stripe/index.d.ts +7 -27
  277. package/dist/providers/stripe/index.js +75 -191
  278. package/dist/providers/stripe/lib/config.d.ts +4 -0
  279. package/dist/providers/stripe/lib/config.js +2 -0
  280. package/dist/providers/stripe/openapi.generated.js +4 -3
  281. package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
  282. package/dist/providers/stripe/skill.generated.js +2 -10
  283. package/dist/providers/stripe/tools.js +77 -0
  284. package/dist/providers/tithely/api.js +531 -0
  285. package/dist/providers/tithely/index.d.ts +19 -0
  286. package/dist/providers/tithely/index.js +86 -965
  287. package/dist/providers/tithely/skill.generated.js +2 -5
  288. package/dist/providers/tithely/tools.js +426 -0
  289. package/dist/providers/vercel/api.js +551 -0
  290. package/dist/providers/vercel/index.d.ts +11 -25
  291. package/dist/providers/vercel/index.js +67 -261
  292. package/dist/providers/vercel/lib/config.d.ts +2 -0
  293. package/dist/providers/vercel/lib/config.js +2 -0
  294. package/dist/providers/vercel/lib/index.js +117 -0
  295. package/dist/providers/vercel/lib/logs.js +100 -0
  296. package/dist/providers/vercel/lib/pin.js +270 -0
  297. package/dist/providers/vercel/openapi.generated.js +398 -376
  298. package/dist/providers/vercel/{value-safety.js → safety.js} +261 -42
  299. package/dist/providers/vercel/skill.generated.js +2 -7
  300. package/dist/providers/vercel/tools.js +642 -0
  301. package/dist/redaction-identity.js +47 -0
  302. package/dist/redaction-marker.js +125 -0
  303. package/dist/registry.d.ts +30 -28
  304. package/dist/registry.js +446 -413
  305. package/dist/routes/access-tokens.js +1 -1
  306. package/dist/routes/actor-labels.js +1 -1
  307. package/dist/routes/connect.js +1 -1
  308. package/dist/routes/dispatch.js +103 -0
  309. package/dist/routes/mcp.js +170 -79
  310. package/dist/routes/oauth.js +3 -1
  311. package/dist/routes/operator.js +2 -2
  312. package/dist/routes/shared.d.ts +5 -4
  313. package/dist/routes/shared.js +244 -177
  314. package/dist/routes/skills.js +10 -8
  315. package/dist/routes/ui-config.js +27 -3
  316. package/dist/routes/ui-data.js +29 -22
  317. package/dist/routes/ui.js +1 -1
  318. package/dist/runtime/call-admission.js +0 -26
  319. package/dist/runtime/call-entry.js +30 -0
  320. package/dist/runtime/call.js +556 -0
  321. package/dist/runtime/connector-scope.js +7 -0
  322. package/dist/runtime/credential-probe.js +34 -0
  323. package/dist/runtime/executor.js +40 -0
  324. package/dist/runtime/native-storage.js +63 -0
  325. package/dist/runtime/registry.js +27 -0
  326. package/dist/runtime/request-scope.js +72 -0
  327. package/dist/runtime/run.js +15 -7
  328. package/dist/runtime/shared-read.js +34 -5
  329. package/dist/runtime/storage.js +34 -21
  330. package/dist/sent-secrets.d.ts +194 -37
  331. package/dist/sent-secrets.js +1743 -340
  332. package/dist/server.js +5 -56
  333. package/dist/skills.js +330 -224
  334. package/dist/sqlite.js +15 -14
  335. package/dist/storage/d1-copy.js +255 -0
  336. package/dist/storage/d1-driver.js +15 -0
  337. package/dist/storage/keys.js +28 -0
  338. package/dist/storage/sql-driver.js +88 -0
  339. package/dist/storage/sql-native.js +544 -0
  340. package/dist/storage/sql-schema.d.ts +6 -0
  341. package/dist/storage/sql-schema.js +15 -0
  342. package/dist/storage/sql.d.ts +14 -20
  343. package/dist/storage/sql.js +29 -535
  344. package/dist/timeout.js +0 -26
  345. package/dist/tool-safety.js +20 -10
  346. package/dist/types.d.ts +11 -99
  347. package/dist/upward.js +3 -0
  348. package/dist/usage-guide.js +3 -1
  349. package/dist/validate.d.ts +4 -2
  350. package/dist/validate.js +86 -9
  351. package/dist/version.d.ts +1 -1
  352. package/dist/version.js +1 -1
  353. package/dist/worker.js +312 -292
  354. package/dist/workspace-subject.js +5 -1
  355. package/dist/write-outcome.js +454 -34
  356. package/documentation/README.md +27 -7
  357. package/documentation/architecture.md +2550 -186
  358. package/documentation/auth.md +821 -98
  359. package/documentation/code-mode.md +56 -21
  360. package/documentation/deploying.md +164 -20
  361. package/documentation/hosted-mcp-migration.md +460 -0
  362. package/documentation/integrating.md +144 -105
  363. package/documentation/meta-tools.md +411 -34
  364. package/documentation/operating.md +28 -4
  365. package/documentation/operator-tests.md +11 -2
  366. package/documentation/operator-ui.md +103 -33
  367. package/documentation/provider-auth-migration.md +38 -63
  368. package/documentation/provider-migration-0.29.md +10 -5
  369. package/documentation/provider-migration-0.35.md +975 -0
  370. package/documentation/providers.md +965 -0
  371. package/examples/worker/AGENTS.md +23 -9
  372. package/examples/worker/README.md +254 -135
  373. package/examples/worker/alchemy.run.ts +116 -0
  374. package/examples/worker/gitignore +10 -0
  375. package/examples/worker/src/connecta.config.ts +23 -21
  376. package/examples/worker/wrangler.jsonc +14 -5
  377. package/guides/connecting-agents.md +157 -0
  378. package/guides/how-it-works.md +156 -0
  379. package/guides/safety-and-trust.md +153 -0
  380. package/package.json +27 -42
  381. package/templates/node/.env.example +3 -0
  382. package/templates/node/README.md +22 -2
  383. package/templates/node/docker-compose.yml +1 -0
  384. package/templates/node/package.json +3 -2
  385. package/templates/node/src/connecta.config.ts +8 -0
  386. package/dist/catalog-drift.d.ts +0 -134
  387. package/dist/catalog-drift.js +0 -493
  388. package/dist/providers/_shared/request-token.js +0 -83
  389. package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
  390. package/dist/providers/_shared/rest/tools.d.ts +0 -141
  391. package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
  392. package/dist/providers/basecamp/index.d.ts +0 -43
  393. package/dist/providers/basecamp/index.js +0 -997
  394. package/dist/providers/basecamp/skill.generated.js +0 -9
  395. package/dist/providers/cloudflare/rest.d.ts +0 -37
  396. package/dist/providers/cloudflare/rest.js +0 -1078
  397. package/dist/providers/github/hosted.js +0 -218
  398. package/dist/providers/github/rest.js +0 -321
  399. package/dist/providers/infisical/index.d.ts +0 -7
  400. package/dist/providers/infisical/index.js +0 -713
  401. package/dist/providers/infisical/skill.generated.js +0 -8
  402. package/dist/providers/linear/index.d.ts +0 -55
  403. package/dist/providers/linear/index.js +0 -352
  404. package/dist/providers/linear/skill.generated.js +0 -8
  405. package/dist/providers/mixpanel/index.d.ts +0 -57
  406. package/dist/providers/mixpanel/index.js +0 -468
  407. package/dist/providers/mixpanel/operations.js +0 -20
  408. package/dist/providers/mixpanel/rest.js +0 -603
  409. package/dist/providers/mixpanel/skill.generated.js +0 -10
  410. package/dist/providers/mixpanel/value-safety.js +0 -16
  411. package/dist/providers/notion/index.d.ts +0 -65
  412. package/dist/providers/notion/index.js +0 -368
  413. package/dist/providers/notion/openapi.generated.js +0 -75
  414. package/dist/providers/notion/rest.d.ts +0 -24
  415. package/dist/providers/notion/rest.js +0 -1226
  416. package/dist/providers/notion/skill.generated.js +0 -11
  417. package/dist/providers/notion/value-safety.js +0 -56
  418. package/dist/providers/revenuecat/index.d.ts +0 -49
  419. package/dist/providers/revenuecat/index.js +0 -557
  420. package/dist/providers/revenuecat/skill.generated.js +0 -11
  421. package/dist/providers/stripe/rest.d.ts +0 -14
  422. package/dist/providers/stripe/rest.js +0 -402
  423. package/dist/providers/vercel/rest.d.ts +0 -20
  424. package/dist/providers/vercel/rest.js +0 -1020
  425. package/dist/providers/wispr-flow/index.d.ts +0 -14
  426. package/dist/providers/wispr-flow/index.js +0 -117
  427. package/dist/providers/wispr-flow/skill.generated.js +0 -8
  428. /package/dist/providers/github/{key.js → lib/key.js} +0 -0
package/dist/registry.js CHANGED
@@ -1,28 +1,33 @@
1
- import { markCatalogFreshness, carryCatalogFreshness } from "./catalog-freshness.js";
1
+ import { authCall, authInvoke } from "./auth/native.js";
2
+ import { Clock, Duration, Effect, Random } from "effect";
2
3
  import { activityRequest } from "./activity-request.js";
3
- import { attachCatalogCache, catalogExpiry, customCatalogFallback, storeCustomCatalogFallback, catalogFetchedAt, invalidateCatalogCache, observeUncachedCatalogRefresh, } from "./catalog-cache.js";
4
+ import { aggregateCallAdmissionSnapshots, ConnectorCallAdmissionController, } from "./call-admission.js";
5
+ import { attachCatalogCache, catalogExpiry, catalogFetchedAt, customCatalogFallback, invalidateCatalogCache, observeUncachedCatalogRefresh, storeCustomCatalogFallback, } from "./catalog-cache.js";
6
+ import { carryCatalogFreshness, markCatalogFreshness } from "./catalog-freshness.js";
7
+ import { MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "./catalog-limits.js";
4
8
  import { CONFIG_DEFAULTS } from "./config-defaults.js";
5
- import { assertStaticToolNames, hasControlCharacters } from "./tool-name.js";
6
- import { Clock, Duration, Effect, Random } from "effect";
9
+ import { usesCallerIdentity } from "./connector-caller.js";
10
+ import { canonicalJson } from "./canonical-json.js";
7
11
  import { storedCredentialShape } from "./credential-rules.js";
8
12
  import { ConnectorCallError } from "./errors.js";
13
+ import { attachOAuthPartition, oauthPartitionIdle } from "./oauth-partition.js";
14
+ import { attachOAuthSealer, vaultOAuthSealer } from "./oauth-sealing.js";
9
15
  import { boundedStatus, failureRecord, failureStatus, logFailure, ownStatus } from "./operator-record.js";
10
- import { ConnectorCallAdmissionController, aggregateCallAdmissionSnapshots, } from "./call-admission.js";
11
- import { boundedCatalogDrift, catalogReviewOf, classifyCatalog, observeReviewedDrift, observedCatalogDrift, } from "./catalog-drift.js";
12
- import { MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "./catalog-limits.js";
13
16
  import { ObservedOutputSchemas } from "./result-shapes.js";
17
+ import { ContinuationBinding, OperationCredentials, ContinuationStorage, operationView, setViewService, duringViewEffect, } from "./runtime/call.js";
18
+ import { hasNativeStorage, registerNativeStorage } from "./runtime/native-storage.js";
19
+ import { provideRegistryPrograms, registryEdgeProgram, registryProgram } from "./runtime/registry.js";
20
+ import { causeError, runEdge } from "./runtime/run.js";
21
+ import { Storage } from "./runtime/services.js";
22
+ import { SharedRead } from "./runtime/shared-read.js";
23
+ import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageSet, storageList, } from "./runtime/storage.js";
14
24
  import { redactCatalog, sentSecretsFor, sentSecretsForRequest, trackCredentialReads } from "./sent-secrets.js";
15
25
  import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary } from "./skills.js";
16
- import { attachOAuthSealer, vaultOAuthSealer } from "./oauth-sealing.js";
17
- import { attachOAuthPartition, oauthPartitionIdle } from "./oauth-partition.js";
18
- import { attachCaller } from "./connector-caller.js";
26
+ import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashExpiryKeys, stashLedgerKeys, } from "./storage/keys.js";
27
+ import { assertStaticToolNames, hasControlCharacters } from "./tool-name.js";
28
+ import { carryToolClassification, classifyTool } from "./tool-safety.js";
19
29
  import { isWorkspaceAddress } from "./workspace-address.js";
20
30
  import { workspaceSubjectOf } from "./workspace-subject.js";
21
- import { runEdge } from "./runtime/run.js";
22
- import { SharedRead } from "./runtime/shared-read.js";
23
- import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageSet } from "./runtime/storage.js";
24
- import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashLedgerKeys, stashExpiryKeys, } from "./storage/keys.js";
25
- import { classifyTool } from "./tool-safety.js";
26
31
  const ID_RE = /^[a-z0-9_-]+$/;
27
32
  const DEFAULT_MAX_RESULT_BYTES = CONFIG_DEFAULTS.calls.maxResultBytes;
28
33
  const encoder = new TextEncoder();
@@ -74,7 +79,7 @@ export function isValidMaxResultBytes(value) {
74
79
  export function resolveMaxResultBytes(value, inherited) {
75
80
  return value !== undefined && isValidMaxResultBytes(value) ? value : inherited;
76
81
  }
77
- /** Freeze registry-owned facts so review digest memoization remains valid. */
82
+ /** Freeze registry-owned catalog facts before caching. */
78
83
  function frozenFacts(tools) {
79
84
  const pending = [tools];
80
85
  while (pending.length > 0) {
@@ -88,7 +93,7 @@ function frozenFacts(tools) {
88
93
  return tools;
89
94
  }
90
95
  function namespaced(storage, prefix) {
91
- return {
96
+ const edge = {
92
97
  capabilities: storage.capabilities,
93
98
  get: (k) => storage.get(prefix + k),
94
99
  set: (k, v, o) => storage.set(prefix + k, v, o),
@@ -96,6 +101,18 @@ function namespaced(storage, prefix) {
96
101
  list: async (keyPrefix) => (await storage.list(prefix + keyPrefix)).map((key) => key.slice(prefix.length)),
97
102
  compareAndSet: (k, expected, next, o) => storage.compareAndSet(prefix + k, expected, next, o),
98
103
  };
104
+ return hasNativeStorage(storage)
105
+ ? registerNativeStorage(edge, {
106
+ // Select against the underlying current methods at dispatch. A
107
+ // namespace created during a decoration regains the native path when
108
+ // that decoration is restored, without capturing stale capabilities.
109
+ get: (key) => storageGet(prefix + key).pipe(Effect.provideService(Storage, storage)),
110
+ set: (key, value, options) => storageSet(prefix + key, value, options).pipe(Effect.provideService(Storage, storage)),
111
+ delete: (key) => storageDelete(prefix + key).pipe(Effect.provideService(Storage, storage)),
112
+ list: (keyPrefix) => storageList(prefix + keyPrefix).pipe(Effect.provideService(Storage, storage), Effect.map((keys) => keys.map((key) => key.slice(prefix.length)))),
113
+ compareAndSet: (key, expected, next, options) => storageCompareAndSet(prefix + key, expected, next, options).pipe(Effect.provideService(Storage, storage)),
114
+ })
115
+ : edge;
99
116
  }
100
117
  const MAX_PERSONAL_REGISTRIES = 1_024;
101
118
  const MAX_ABSENT_GRANT_WARNINGS = 1_024;
@@ -229,13 +246,9 @@ export class Registry {
229
246
  catalogObservedAt = new Map();
230
247
  /** Last payload-free agent catalog access in this runtime. */
231
248
  catalogAccess = new Map();
232
- /** Count-only intake findings, including catalogs loaded from storage. */
233
- droppedToolNames = new Map();
234
- /**
235
- * Schema digest verification per reviewed facts array. Only the registry's
236
- * own deep-frozen arrays are keys, so an entry stays true while they live.
237
- */
238
- verifiedFacts = new WeakMap();
249
+ /** Bounded tokens isolating count-only observations by catalog identity. */
250
+ catalogObservationPartitions = new Map();
251
+ overrideWarningCatalogs = new WeakMap();
239
252
  observedOutputSchemas;
240
253
  /** Result-size guard cap threaded to the meta-tools. */
241
254
  maxResultBytes;
@@ -263,6 +276,9 @@ export class Registry {
263
276
  if (this.connectors.has(c.id)) {
264
277
  throw new Error(`Duplicate connector id "${c.id}"`);
265
278
  }
279
+ if (c.readOnly !== undefined && typeof c.readOnly !== "boolean") {
280
+ throw new Error(`Connector "${c.id}" readOnly must be a boolean.`);
281
+ }
266
282
  if (c.authScope !== undefined && c.authScope !== "shared" && c.authScope !== "personal") {
267
283
  throw new Error(`Invalid authScope on connector "${c.id}": expected "shared" or "personal"`);
268
284
  }
@@ -273,11 +289,6 @@ export class Registry {
273
289
  `normalization; the discovery bound is ${GUIDE_SUMMARY_LENGTH}. ` +
274
290
  "Shorten it or omit it to derive one.");
275
291
  }
276
- // Validated here, before any request (INV-11), and read once per
277
- // connector object.
278
- if (catalogReviewOf(c) && c.staticTools) {
279
- throw new Error(`Connector "${c.id}" declares both staticTools and a classification; ` + "annotate static tools directly.");
280
- }
281
292
  this.connectors.set(c.id, c);
282
293
  if (c.callAdmission !== undefined) {
283
294
  this.callAdmission.set(c.id, new ConnectorCallAdmissionController(c.id, c.callAdmission));
@@ -290,8 +301,8 @@ export class Registry {
290
301
  Object.freeze(this.classification);
291
302
  for (const [id, overrides] of Object.entries(this.classification)) {
292
303
  for (const [name, verdict] of Object.entries(overrides)) {
293
- if (!name || (verdict !== "read" && verdict !== "write")) {
294
- throw new Error(`ConnectaConfig.classification.${id}.${name}: expected "read" or "write"`);
304
+ if (!name || (verdict !== "read" && verdict !== "write" && verdict !== "hidden")) {
305
+ throw new Error(`ConnectaConfig.classification.${id}.${name}: expected "read", "write", or "hidden"`);
295
306
  }
296
307
  }
297
308
  const connector = this.connectors.get(id);
@@ -328,13 +339,11 @@ export class Registry {
328
339
  ...this.opts,
329
340
  classification: Object.fromEntries(Object.entries(this.classification).filter(([id]) => this.connectors.get(id)?.authScope === "personal")),
330
341
  storage: namespaced(this.opts.storage, scopes.principal(principalKey)),
342
+ rootStorage: this.opts.rootStorage ?? this.opts.storage,
331
343
  credentialOwner: principalKey,
332
344
  catalogStorage: this.opts.catalogStorage ?? this.opts.storage,
333
345
  constructionChecks: false,
334
346
  });
335
- // Counts only, like the connector-wide reviewed drift observation. No
336
- // principal, credentials, or catalog payload crosses this registry boundary.
337
- registry.droppedToolNames = this.droppedToolNames;
338
347
  if (this.callAdmissionClosed)
339
348
  registry.closeCallAdmission();
340
349
  this.personalRegistries.set(principalKey, registry);
@@ -378,45 +387,14 @@ export class Registry {
378
387
  const quoted = JSON.stringify(key).replace(/[\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16)}`);
379
388
  this.opts.logger.warn(`a pool grants ${quoted} but connector "${connectorId}" lists no such tool; the grant is unreachable`);
380
389
  }
381
- async storeOAuthHandoff(connectorId, state, principalKey) {
382
- const key = oauthHandoffKeys.handoff(connectorId, await sha256Hex(state));
383
- for (let attempt = 0; attempt < 32; attempt++) {
384
- const existing = await this.opts.storage.get(key);
385
- if (existing && existing !== principalKey) {
386
- throw new Error(`Connector "${connectorId}" reused one OAuth state across principals`);
387
- }
388
- // Bind ownership atomically, including renewal of the same owner's
389
- // handoff. Two owners reading a miss must not overwrite each other.
390
- if (await this.opts.storage.compareAndSet(key, existing, principalKey, {
391
- ttlSeconds: OAUTH_HANDOFF_TTL_SECONDS,
392
- }))
393
- return;
394
- }
395
- throw new Error(`OAuth handoff for "${connectorId}" is busy; retry authorization`);
390
+ storeOAuthHandoff(connectorId, state, principalKey) {
391
+ return runEdge(registryEdgeProgram(Registry.prototype, this, "storeOAuthHandoff", connectorId, state, principalKey));
396
392
  }
397
- async oauthCallbackView(connectorId, state) {
398
- if (!state)
399
- return null;
400
- const principalKey = await this.opts.storage.get(oauthHandoffKeys.handoff(connectorId, await sha256Hex(state)));
401
- const connector = this.connectors.get(connectorId);
402
- if (!connector || !principalKey)
403
- return null;
404
- return {
405
- registry: connector.authScope === "personal"
406
- ? this.scoped({
407
- connectorIds: [connectorId],
408
- subjectKey: principalKey,
409
- principalKey,
410
- })
411
- : this,
412
- principalKey,
413
- };
393
+ oauthCallbackView(connectorId, state) {
394
+ return runEdge(registryEdgeProgram(Registry.prototype, this, "oauthCallbackView", connectorId, state));
414
395
  }
415
- async consumeOAuthHandoff(connectorId, state, principalKey) {
416
- if (!state)
417
- return false;
418
- const key = oauthHandoffKeys.handoff(connectorId, await sha256Hex(state));
419
- return this.opts.storage.compareAndSet(key, principalKey, null);
396
+ consumeOAuthHandoff(connectorId, state, principalKey) {
397
+ return runEdge(registryEdgeProgram(Registry.prototype, this, "consumeOAuthHandoff", connectorId, state, principalKey));
420
398
  }
421
399
  /**
422
400
  * Refuse an unusable result cap at construction, like every other
@@ -470,18 +448,22 @@ export class Registry {
470
448
  return this.connectors.has(id);
471
449
  }
472
450
  contextFor(id, baseUrl, requestScope = {}, callOptions = {}, scope) {
473
- const credentialConfig = this.connectors.get(id)?.credential;
451
+ const connector = this.connectors.get(id);
452
+ const credentialConfig = connector?.credential;
474
453
  let credentialAccess;
454
+ let credentialProgram;
475
455
  if (this.opts.credentialVault && credentialConfig) {
476
456
  const vault = this.opts.credentialVault;
477
- const readValues = async () => {
478
- const values = await vault.getAll(id, this.opts.credentialOwner);
457
+ credentialProgram = Effect.tryPromise({
458
+ try: () => vault.getAll(id, this.opts.credentialOwner),
459
+ catch: (error) => error,
460
+ }).pipe(Effect.flatMap((values) => {
479
461
  const shape = storedCredentialShape(credentialConfig, values);
480
- if (shape.state === "mismatch") {
481
- throw new ConnectorCallError("auth_required", shape.message);
482
- }
483
- return values;
484
- };
462
+ return shape.state === "mismatch"
463
+ ? Effect.fail(new ConnectorCallError("auth_required", shape.message))
464
+ : Effect.succeed(values);
465
+ }));
466
+ const readValues = () => runEdge(credentialProgram);
485
467
  credentialAccess = {
486
468
  get: async (field = "value") => {
487
469
  const values = await readValues();
@@ -490,7 +472,7 @@ export class Registry {
490
472
  getAll: readValues,
491
473
  };
492
474
  }
493
- const context = {
475
+ const context = operationView({
494
476
  storage: namespaced(this.opts.storage, scopes.connector(id)),
495
477
  logger: this.opts.logger,
496
478
  baseUrl,
@@ -499,17 +481,19 @@ export class Registry {
499
481
  ...(credentialAccess ? { credential: credentialAccess } : {}),
500
482
  requestScope,
501
483
  ...callOptions,
502
- };
503
- attachCaller(context, scope?.caller);
484
+ }, scope?.caller);
485
+ setViewService(context, OperationCredentials, credentialProgram);
486
+ setViewService(context, ContinuationStorage, this.opts.rootStorage ?? this.opts.storage);
487
+ const owner = this.opts.credentialOwner ?? null;
488
+ setViewService(context, ContinuationBinding, {
489
+ connection: id,
490
+ // Include every admitted input a subject mapping can inspect, including
491
+ // the pool owner for an agent. This value enters only the HMAC claims.
492
+ principal: connector && usesCallerIdentity(connector) ? canonicalJson([owner, scope?.caller ?? null]) : owner,
493
+ });
504
494
  attachCatalogCache(context, {
505
495
  storage: this.opts.catalogStorage ?? this.opts.storage,
506
- partition: JSON.stringify([
507
- scope?.principalKey ?? this.opts.credentialOwner ?? null,
508
- scope?.subjectKey ?? null,
509
- scope?.caller?.identity ?? null,
510
- scope?.caller?.authenticated ?? false,
511
- scope?.caller?.pool ?? null,
512
- ]),
496
+ partition: this.catalogPartition(scope),
513
497
  sharedPartition: JSON.stringify([baseUrl, this.opts.publicUrl ?? null, scope?.caller?.pool ?? null]),
514
498
  defaultTtlMs: (this.opts.toolCacheTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds) * 1000,
515
499
  minTtlMs: (this.opts.catalogMinTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogMinTtlSeconds) * 1000,
@@ -553,26 +537,27 @@ export class Registry {
553
537
  }
554
538
  return Object.fromEntries([...snapshots].map(([id, values]) => [id, aggregateCallAdmissionSnapshots(values)]));
555
539
  }
556
- /**
557
- * The drift a connector last showed in this runtime: the registry's own
558
- * observation against a connector's `classification`, otherwise whatever
559
- * the connector's `catalogDrift()` seam reports, bounded either way.
560
- */
561
- catalogDriftOf(connector) {
562
- const report = boundedCatalogDrift(catalogReviewOf(connector) ? observedCatalogDrift(connector) : connector.catalogDrift?.());
563
- const dropped = this.droppedToolNames.get(connector.id);
564
- if (!dropped)
565
- return report;
566
- return {
567
- ...(report ?? {
568
- unclassifiedTools: 0,
569
- unservedTools: 0,
570
- annotationConflicts: 0,
571
- schemaChanges: 0,
572
- }),
573
- observedAt: dropped.observedAt,
574
- ...(dropped.count > 0 ? { droppedTools: dropped.count } : {}),
575
- };
540
+ /** The admitted identity fields used by both catalog caching and reporting. */
541
+ catalogPartition(scope) {
542
+ return JSON.stringify([
543
+ scope?.principalKey ?? this.opts.credentialOwner ?? null,
544
+ scope?.subjectKey ?? null,
545
+ scope?.caller?.identity ?? null,
546
+ scope?.caller?.authenticated ?? false,
547
+ scope?.caller?.pool ?? null,
548
+ ]);
549
+ }
550
+ /** Bounded, in-memory tokens; no principal or catalog data is persisted. */
551
+ catalogObservationPartition(id, baseUrl, scope, create = false) {
552
+ const key = JSON.stringify([id, this.catalogPartition(scope), baseUrl, this.opts.publicUrl ?? null]);
553
+ let partition = this.catalogObservationPartitions.get(key);
554
+ if (!partition && create) {
555
+ partition = {};
556
+ this.catalogObservationPartitions.set(key, partition);
557
+ if (this.catalogObservationPartitions.size > 1024)
558
+ this.catalogObservationPartitions.delete(this.catalogObservationPartitions.keys().next().value);
559
+ }
560
+ return partition;
576
561
  }
577
562
  /** Reject queued/future downstream admission; active permits release safely. */
578
563
  closeCallAdmission() {
@@ -601,88 +586,7 @@ export class Registry {
601
586
  * stop waiting after 15 seconds. Storage TTL reclaims the rows.
602
587
  */
603
588
  stashResult(id, chunks, ttlSeconds, partition = scopes.results) {
604
- return runOnPartition(Effect.gen({ self: this }, function* () {
605
- const maxBytes = this.opts.results?.maxStashBytes ?? CONFIG_DEFAULTS.results.maxStashBytes;
606
- const maxEntries = this.opts.results?.maxStashEntries ?? CONFIG_DEFAULTS.results.maxStashEntries;
607
- // The paging envelope is ASCII, so its string length is its stored byte count.
608
- const bytes = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
609
- if (bytes > maxBytes || maxEntries === 0)
610
- return false;
611
- const keys = chunks.map((_, index) => partition + resultKeys.chunk(id, index));
612
- // IDs belong to reservations, not reusable result IDs or subjects.
613
- const charge = crypto.randomUUID();
614
- const ttlMs = ttlSeconds * 1000;
615
- if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
616
- return false;
617
- const booking = yield* swapStashLedger((live, now) => {
618
- // A committed CAS with a lost response is already our booking, even
619
- // at full capacity. Reuse its deadline instead of adding a second slot.
620
- const own = live.find((entry) => entry[0] === charge);
621
- if (own)
622
- return { result: own[2] };
623
- const used = live.reduce((sum, entry) => sum + entry[1], 0);
624
- if (live.length >= maxEntries || used + bytes > maxBytes)
625
- return { result: undefined };
626
- const deadline = now + STASH_WRITE_TIMEOUT_MS + ttlMs;
627
- if (!Number.isSafeInteger(deadline))
628
- return { result: undefined };
629
- return { entries: [...live, [charge, bytes, deadline]], result: deadline };
630
- });
631
- if (booking.status === "exhausted" || booking.result === undefined)
632
- return false;
633
- const deadline = booking.result;
634
- const writeDeadline = deadline - ttlMs;
635
- const chunkExpiry = deadline - STASH_WRITE_TIMEOUT_MS;
636
- const owns = (entry) => entry[0] === charge;
637
- const settle = swapStashLedger((live) => live.some((entry) => owns(entry) && entry[2] > chunkExpiry)
638
- ? {
639
- entries: live.map((entry) => (owns(entry) ? [charge, bytes, Math.min(entry[2], chunkExpiry)] : entry)),
640
- result: undefined,
641
- }
642
- : { result: undefined });
643
- let pendingWrite = false;
644
- // A rejected or timed-out driver call may still commit after deletion.
645
- // Only a confirmed write response clears pendingWrite.
646
- const release = completeStash(Effect.gen(function* () {
647
- const deleted = yield* Effect.gen(function* () {
648
- for (const key of keys)
649
- yield* storageDelete(key);
650
- return true;
651
- }).pipe(Effect.catch(() => Effect.succeed(false)));
652
- if (pendingWrite)
653
- return;
654
- if (deleted) {
655
- const released = yield* swapStashLedger((live) => live.some(owns)
656
- ? { entries: live.filter((entry) => !owns(entry)), result: undefined }
657
- : { result: undefined });
658
- if (released.status === "applied")
659
- return;
660
- }
661
- yield* settle;
662
- }));
663
- // Trailing chunks first, then the header. The caller receives no result
664
- // ID after failure or timeout, including a late header commit.
665
- const remaining = Math.max(0, writeDeadline - (yield* Clock.currentTimeMillis));
666
- const written = yield* Effect.gen(function* () {
667
- for (let index = keys.length - 1; index >= 0; index--) {
668
- const before = yield* Clock.currentTimeMillis;
669
- if (before >= writeDeadline || before >= chunkExpiry)
670
- return false;
671
- pendingWrite = true;
672
- yield* storageSet(keys[index], chunks[index], { expiresAtMs: chunkExpiry }).pipe(Effect.tap(() => Effect.sync(() => {
673
- pendingWrite = false;
674
- })));
675
- if ((yield* Clock.currentTimeMillis) >= writeDeadline)
676
- return false;
677
- }
678
- return true;
679
- }).pipe(Effect.timeoutOrElse({ duration: Duration.millis(remaining), orElse: () => Effect.succeed(false) }), Effect.onError(() => release.pipe(Effect.ignore)));
680
- if (!written)
681
- yield* release;
682
- else
683
- yield* completeStash(settle);
684
- return written;
685
- }), this.opts);
589
+ return runOnPartition(registryEdgeProgram(Registry.prototype, this, "stashResult", id, chunks, ttlSeconds, partition), this.opts);
686
590
  }
687
591
  /**
688
592
  * Storage namespaced to the root result partition, kept separate from any
@@ -691,7 +595,7 @@ export class Registry {
691
595
  resultIdentity() {
692
596
  return { subject: null, principal: null, endpoint: "/mcp", origin: null };
693
597
  }
694
- recheckResultAccess(_address, _classification, _signal, _access) {
598
+ recheckResultAccess(_dependencies, _classification, _signal) {
695
599
  return Promise.resolve(true);
696
600
  }
697
601
  resultsStorage() {
@@ -700,11 +604,8 @@ export class Registry {
700
604
  credentialUiAvailable() {
701
605
  return Boolean(this.opts.credentialUi);
702
606
  }
703
- async bindOAuthHandoff(id, authorizationUrl, principalKey) {
704
- const state = new URL(authorizationUrl).searchParams.get("state");
705
- if (!state || !principalKey)
706
- throw new Error("OAuth requires state and an initiating user");
707
- await this.storeOAuthHandoff(id, state, principalKey);
607
+ bindOAuthHandoff(id, authorizationUrl, principalKey) {
608
+ return runEdge(registryEdgeProgram(Registry.prototype, this, "bindOAuthHandoff", id, authorizationUrl, principalKey));
708
609
  }
709
610
  observedOutputSchema(connectorId, definition) {
710
611
  return this.observedOutputSchemas.get(connectorId, definition);
@@ -740,96 +641,73 @@ export class Registry {
740
641
  ...(connector.authScope === "personal" ? { personal: true } : {}),
741
642
  }, activityRequest(ctx.requestScope));
742
643
  }
743
- async loadDownstreamTools(id, baseUrl, requestScope, callOptions = {}, scope) {
744
- const connector = this.connectors.get(id);
745
- if (!connector)
746
- throw new Error(`Unknown connector "${id}"`);
747
- if (connector.staticTools)
748
- return markCatalogFreshness(connector.staticTools, Infinity);
749
- const ctx = this.contextFor(id, baseUrl, requestScope, callOptions, scope);
750
- let listed;
751
- try {
752
- listed = await connector.listTools(ctx);
753
- }
754
- catch (error) {
755
- const fallback = error instanceof ConnectorCallError && error.code === "unavailable"
756
- ? await customCatalogFallback(ctx, id).catch(() => undefined)
757
- : undefined;
758
- if (!fallback)
759
- throw sentSecretsForRequest(ctx.requestScope ?? ctx).redact(error);
760
- listed = fallback;
761
- }
762
- const tools = redactCatalog(ctx, listed).map(({ classification: _ignored, ...fact }) => fact);
763
- if (tools.length > MAX_CATALOG_TOOLS ||
764
- encoder.encode(JSON.stringify(tools)).byteLength > MAX_SERIALIZED_CATALOG_BYTES) {
765
- throw new ConnectorCallError("connector_call_failed", "Downstream catalog exceeds the complete-catalog ceiling.", { retryable: false });
766
- }
767
- const facts = frozenFacts(structuredClone(tools));
768
- const accepted = this.acceptToolNames(facts);
769
- this.observeDroppedToolNames(connector, facts.length - accepted.length);
770
- const review = catalogReviewOf(connector);
771
- if (review)
772
- await observeReviewedDrift(connector, review, accepted, this.opts.logger);
773
- if (catalogFetchedAt(ctx) === undefined) {
774
- try {
775
- await storeCustomCatalogFallback(ctx, id, facts);
776
- }
777
- catch (error) {
778
- logFailure(this.opts.logger, "catalog refresh observation failed", failureRecord({ connector: id }, error));
779
- }
780
- try {
781
- await observeUncachedCatalogRefresh(ctx, id, facts);
644
+ loadDownstreamTools(id, baseUrl, requestScope, callOptions = {}, scope) {
645
+ return Effect.gen({ self: this }, function* () {
646
+ const connector = this.connectors.get(id);
647
+ if (!connector)
648
+ throw new Error(`Unknown connector "${id}"`);
649
+ if (connector.staticTools)
650
+ return markCatalogFreshness(connector.staticTools, Infinity);
651
+ const ctx = this.contextFor(id, baseUrl, requestScope, callOptions, scope);
652
+ const listed = yield* authInvoke(connector, "listTools", [ctx]).pipe(Effect.catch((error) => Effect.gen(function* () {
653
+ const fallback = error instanceof ConnectorCallError && error.code === "unavailable"
654
+ ? yield* authCall(customCatalogFallback, [ctx, id]).pipe(Effect.catch(() => Effect.succeed(undefined)))
655
+ : undefined;
656
+ return fallback
657
+ ? fallback
658
+ : yield* Effect.fail(sentSecretsForRequest(ctx.requestScope ?? ctx).redact(error));
659
+ })));
660
+ const tools = redactCatalog(ctx, listed).map(({ classification: _ignored, ...fact }) => fact);
661
+ if (tools.length > MAX_CATALOG_TOOLS ||
662
+ encoder.encode(JSON.stringify(tools)).byteLength > MAX_SERIALIZED_CATALOG_BYTES) {
663
+ throw new ConnectorCallError("connector_call_failed", "Downstream catalog exceeds the complete-catalog ceiling.", { retryable: false });
782
664
  }
783
- catch (error) {
784
- logFailure(this.opts.logger, "catalog refresh observation failed", failureRecord({ connector: id }, error));
665
+ const facts = frozenFacts(structuredClone(tools));
666
+ const accepted = this.acceptToolNames(facts);
667
+ if (catalogFetchedAt(ctx) === undefined) {
668
+ for (const observe of [storeCustomCatalogFallback, observeUncachedCatalogRefresh]) {
669
+ yield* authCall(observe, [ctx, id, facts]).pipe(Effect.catch((error) => Effect.sync(() => logFailure(this.opts.logger, "catalog refresh observation failed", failureRecord({ connector: id }, error)))));
670
+ }
785
671
  }
786
- }
787
- this.catalogObservedAt.set(id, catalogFetchedAt(ctx) ?? Date.now());
788
- const provenance = catalogExpiry(ctx);
789
- const freshUntil = provenance && !provenance.staleFallback ? provenance.expiresAt : 0;
790
- this.catalogAccess.set(id, {
791
- state: freshUntil > Date.now() ? "fresh" : "stale",
792
- observedAt: new Date().toISOString(),
672
+ this.catalogObservedAt.set(id, catalogFetchedAt(ctx) ?? Date.now());
673
+ const provenance = catalogExpiry(ctx);
674
+ const freshUntil = provenance && !provenance.staleFallback ? provenance.expiresAt : 0;
675
+ this.catalogAccess.set(id, {
676
+ state: freshUntil > Date.now() ? "fresh" : "stale",
677
+ observedAt: new Date().toISOString(),
678
+ });
679
+ return markCatalogFreshness(accepted, freshUntil);
793
680
  });
794
- return markCatalogFreshness(facts, freshUntil);
795
681
  }
796
682
  /** The served catalog excludes control-character names; raw caches stay complete. */
797
683
  acceptToolNames(tools) {
798
684
  const accepted = tools.filter((tool) => !hasControlCharacters(tool.name));
799
685
  return accepted.length === tools.length ? tools : accepted;
800
686
  }
801
- /** Observe intake once, never while serving an older cached listing. */
802
- observeDroppedToolNames(connector, count) {
803
- if (count > 0 || this.droppedToolNames.has(connector.id)) {
804
- this.droppedToolNames.set(connector.id, { count, observedAt: new Date().toISOString() });
805
- }
806
- }
807
687
  /**
808
688
  * One connector's catalog as served: the cached downstream facts, classified
809
- * on every read by the connector's `classification`, into fresh objects.
810
- * Safety is derived here and nowhere else, so no cache layer can carry a
811
- * stale verdict — from an older review, an older release, or a stale
812
- * fallback — and nothing a caller or decorator does to a served tool reaches
813
- * the next read.
689
+ * on every read by annotations and deployment overrides, into fresh objects.
690
+ * No cache or caller mutation can carry a stale verdict into the next read.
814
691
  */
815
- async loadTools(id, baseUrl, requestScope, callOptions = {}, scope) {
816
- const tools = await this.loadDownstreamTools(id, baseUrl, requestScope, callOptions, scope);
817
- const connector = this.connectors.get(id);
818
- if (!connector)
819
- throw new Error(`Unknown connector "${id}"`);
820
- const accepted = this.acceptToolNames(tools);
821
- const overrides = this.classification[id];
822
- this.validateClassification(id, accepted, overrides);
823
- const review = catalogReviewOf(connector);
824
- return carryCatalogFreshness(tools, review
825
- ? await classifyCatalog(review, id, accepted, this.opts.logger, this.verifiedFacts, overrides)
826
- : accepted.map((tool) => this.publishUnreviewedTool(id, tool)));
827
- }
828
- publishUnreviewedTool(id, tool) {
692
+ loadTools(id, baseUrl, requestScope, callOptions = {}, scope) {
693
+ return Effect.gen({ self: this }, function* () {
694
+ const tools = yield* this.loadDownstreamTools(id, baseUrl, requestScope, callOptions, scope);
695
+ const connector = this.connectors.get(id);
696
+ if (!connector)
697
+ throw new Error(`Unknown connector "${id}"`);
698
+ const accepted = this.acceptToolNames(tools);
699
+ const overrides = this.classification[id];
700
+ this.validateClassification(id, accepted, overrides, this.catalogObservationPartition(id, baseUrl, scope, true));
701
+ const visible = accepted.filter((tool) => overrides?.[tool.name] !== "hidden");
702
+ const served = visible.map((tool) => this.publishTool(id, tool));
703
+ return carryCatalogFreshness(tools, served);
704
+ });
705
+ }
706
+ publishTool(id, tool) {
829
707
  const override = this.classification[id]?.[tool.name];
830
- return {
708
+ return carryToolClassification(tool, {
831
709
  ...structuredClone(tool),
832
- classification: classifyTool(tool, override),
710
+ classification: classifyTool(tool, override === "hidden" ? undefined : override),
833
711
  ...(override !== undefined
834
712
  ? {
835
713
  annotations: {
@@ -839,139 +717,268 @@ export class Registry {
839
717
  },
840
718
  }
841
719
  : {}),
842
- };
720
+ });
843
721
  }
844
722
  /** Construction-only view for the secret-free static config description. */
845
723
  describeStaticTools(id) {
846
724
  const tools = this.connectors.get(id)?.staticTools;
847
- return tools?.map((tool) => this.publishUnreviewedTool(id, tool));
725
+ return tools
726
+ ?.filter((tool) => this.classification[id]?.[tool.name] !== "hidden")
727
+ .map((tool) => this.publishTool(id, tool));
848
728
  }
849
- validateClassification(id, tools, overrides) {
729
+ validateClassification(id, tools, overrides, partition) {
730
+ if (!overrides)
731
+ return;
732
+ if (partition) {
733
+ const catalog = JSON.stringify(tools);
734
+ if (this.overrideWarningCatalogs.get(partition) === catalog)
735
+ return;
736
+ this.overrideWarningCatalogs.set(partition, catalog);
737
+ }
850
738
  const known = new Set(tools.map((tool) => tool.name));
851
739
  for (const name of Object.keys(overrides ?? {})) {
852
- if (!known.has(name))
740
+ if (known.has(name))
741
+ continue;
742
+ if (this.connectors.get(id)?.staticTools) {
853
743
  throw new Error(`ConnectaConfig.classification: connector "${id}" has no tool "${name}"`);
744
+ }
745
+ // Only deployment-authored names, never downstream text. JSON quoting
746
+ // prevents control characters from forging operator log lines.
747
+ const quoted = JSON.stringify(`${id}.${name}`).replace(/[\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16)}`);
748
+ this.opts.logger.warn(`classification override ${quoted} is dormant because the runtime catalog lists no such tool`);
854
749
  }
855
750
  }
856
- async getTools(id, baseUrl, requestScope, callOptions = {}, scope) {
857
- const connector = this.connectors.get(id);
858
- if (!connector)
859
- throw new Error(`Unknown connector "${id}"`);
860
- if (!requestScope) {
861
- return this.loadTools(id, baseUrl, requestScope, callOptions, scope);
862
- }
863
- let loads = this.requestCatalogLoads.get(requestScope);
864
- if (!loads) {
865
- loads = new Map();
866
- this.requestCatalogLoads.set(requestScope, loads);
867
- }
868
- const requestLoads = loads;
869
- const key = JSON.stringify([id, scope?.principalKey, scope?.subjectKey, scope?.caller]);
870
- let load = requestLoads.get(key);
871
- if (!load) {
872
- const started = new SharedRead((signal) => this.loadTools(id, baseUrl, requestScope, { ...callOptions, signal }, scope),
873
- // Settled or cancelled, the read leaves the map before any caller
874
- // resumes, so the next one asks the caches afresh.
875
- () => {
876
- if (requestLoads.get(key) === started)
877
- requestLoads.delete(key);
878
- if (requestLoads.size === 0) {
879
- this.requestCatalogLoads.delete(requestScope);
880
- }
881
- });
882
- requestLoads.set(key, started);
883
- load = started;
884
- }
885
- return runEdge(load.join(callOptions.signal));
751
+ getTools(id, baseUrl, requestScope, callOptions = {}, scope) {
752
+ return runEdge(registryEdgeProgram(Registry.prototype, this, "getTools", id, baseUrl, requestScope, callOptions, scope));
886
753
  }
887
- async credentialDriftFor(id) {
888
- const credential = this.connectors.get(id)?.credential;
889
- const vault = this.opts.credentialVault;
890
- if (!credential || !vault)
891
- return undefined;
892
- try {
893
- const values = await vault.getAll(id, this.opts.credentialOwner);
894
- const shape = storedCredentialShape(credential, values);
895
- return shape.state === "mismatch" ? shape.message : undefined;
896
- }
897
- catch (error) {
898
- logFailure(this.opts.logger, "credential shape read failed", failureRecord({ connector: id }, error));
899
- return undefined;
900
- }
754
+ credentialDriftFor(id) {
755
+ return runEdge(registryEdgeProgram(Registry.prototype, this, "credentialDriftFor", id));
901
756
  }
902
757
  /** Age of a complete cache entry; reads never refresh it. */
903
758
  catalogAgeMs(id) {
904
759
  const observedAt = this.catalogObservedAt.get(id);
905
760
  return observedAt === undefined ? null : Math.max(0, Date.now() - observedAt);
906
761
  }
907
- /** Best-effort connector status for the operator UI. */
908
- async statusFor(id, baseUrl, requestScope = {}, callOptions = {}, scope) {
762
+ /** Pure lookup shared by status probes and operator catalog refreshes. */
763
+ withCatalogObservations(id, _baseUrl, status, _scope) {
909
764
  const connector = this.connectors.get(id);
910
765
  if (!connector)
911
766
  return ownStatus({ state: "error", message: "Unknown connector" });
912
- const ctx = this.contextFor(id, baseUrl, requestScope, callOptions, scope);
913
- // Whatever the state turns out to be, it carries the drift the last
914
- // refresh saw. Reading it is a lookup, not a probe: a connector that has
915
- // listed nothing yet reports nothing, and status never lists on its own to
916
- // make the field appear.
917
- // The report is rebuilt rather than spread through: what the seam returned
918
- // is third-party output, and status is read by the operator UI and copied
919
- // into responses.
920
- const withObservations = (status) => {
921
- const report = this.catalogDriftOf(connector);
922
- const access = this.catalogAccess.get(id);
923
- // Connector.status is an open plugin seam. Rebuild its public fields so
924
- // a connector cannot smuggle payload through either registry-owned
925
- // observation when this runtime has not made one, nor through its
926
- // message, which survives only when connecta wrote it (INV-6).
927
- return Object.assign(boundedStatus(status), {
928
- ...(report ? { catalogDrift: report } : {}),
929
- ...(access ? { catalogAccess: { ...access } } : {}),
930
- });
931
- };
932
- // Credential declarations own their readiness even when a static catalog
933
- // or a plugin status would otherwise report success. Never probe an empty slot.
934
- if (connector.credential) {
935
- try {
936
- const values = (await this.opts.credentialVault?.getAll(id, this.opts.credentialOwner)) ?? null;
937
- const shape = storedCredentialShape(connector.credential, values);
938
- if (shape.state === "missing")
939
- return withObservations(ownStatus({ state: "credential_required" }));
940
- if (shape.state === "mismatch")
941
- return withObservations(ownStatus({ state: "auth_required" }));
942
- }
943
- catch (err) {
944
- return withObservations(failureStatus(id, err));
945
- }
946
- }
947
- if (connector.status) {
948
- try {
949
- return withObservations(await connector.status(ctx));
950
- }
951
- catch (err) {
952
- return withObservations(failureStatus(id, err));
953
- }
954
- }
955
- try {
956
- await this.getTools(id, baseUrl, requestScope, callOptions, scope);
957
- return withObservations({ state: "ok" });
958
- }
959
- catch (err) {
960
- return withObservations(failureStatus(id, err));
961
- }
767
+ const access = this.catalogAccess.get(id);
768
+ // Connector.status is an open plugin seam. Rebuild its public fields so
769
+ // it cannot smuggle payload or another partition's observations (INV-6).
770
+ return Object.assign(boundedStatus(status), access ? { catalogAccess: { ...access } } : {});
771
+ }
772
+ /** Best-effort connector status for the operator UI. */
773
+ statusFor(id, baseUrl, requestScope = {}, callOptions = {}, scope) {
774
+ return runEdge(registryEdgeProgram(Registry.prototype, this, "statusFor", id, baseUrl, requestScope, callOptions, scope));
962
775
  }
963
776
  /** Rotate the storage generation; in-flight old readers cannot republish it. */
964
777
  invalidate(id) {
965
778
  void this.invalidateStored(id);
966
779
  }
967
- async invalidateStored(id) {
968
- this.catalogObservedAt.delete(id);
969
- try {
970
- await invalidateCatalogCache(this.opts.catalogStorage ?? this.opts.storage, id);
971
- }
972
- catch (error) {
973
- logFailure(this.opts.logger, "catalog invalidation failed", failureRecord({ connector: id }, error));
974
- }
780
+ invalidateStored(id) {
781
+ return runEdge(registryEdgeProgram(Registry.prototype, this, "invalidateStored", id));
782
+ }
783
+ static {
784
+ provideRegistryPrograms(Registry.prototype, {
785
+ stashResult: (self, id, chunks, ttlSeconds, partition = scopes.results) => Effect.gen({ self }, function* () {
786
+ const maxBytes = this.opts.results?.maxStashBytes ?? CONFIG_DEFAULTS.results.maxStashBytes;
787
+ const maxEntries = this.opts.results?.maxStashEntries ?? CONFIG_DEFAULTS.results.maxStashEntries;
788
+ // The paging envelope is ASCII, so its string length is its stored byte count.
789
+ const bytes = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
790
+ if (bytes > maxBytes || maxEntries === 0)
791
+ return false;
792
+ const keys = chunks.map((_, index) => partition + resultKeys.chunk(id, index));
793
+ // IDs belong to reservations, not reusable result IDs or subjects.
794
+ const charge = crypto.randomUUID();
795
+ const ttlMs = ttlSeconds * 1000;
796
+ if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
797
+ return false;
798
+ const booking = yield* swapStashLedger((live, now) => {
799
+ // A committed CAS with a lost response is already our booking, even
800
+ // at full capacity. Reuse its deadline instead of adding a second slot.
801
+ const own = live.find((entry) => entry[0] === charge);
802
+ if (own)
803
+ return { result: own[2] };
804
+ const used = live.reduce((sum, entry) => sum + entry[1], 0);
805
+ if (live.length >= maxEntries || used + bytes > maxBytes)
806
+ return { result: undefined };
807
+ const deadline = now + STASH_WRITE_TIMEOUT_MS + ttlMs;
808
+ if (!Number.isSafeInteger(deadline))
809
+ return { result: undefined };
810
+ return { entries: [...live, [charge, bytes, deadline]], result: deadline };
811
+ });
812
+ if (booking.status === "exhausted" || booking.result === undefined)
813
+ return false;
814
+ const deadline = booking.result;
815
+ const writeDeadline = deadline - ttlMs;
816
+ const chunkExpiry = deadline - STASH_WRITE_TIMEOUT_MS;
817
+ const owns = (entry) => entry[0] === charge;
818
+ const settle = swapStashLedger((live) => live.some((entry) => owns(entry) && entry[2] > chunkExpiry)
819
+ ? {
820
+ entries: live.map((entry) => owns(entry) ? [charge, bytes, Math.min(entry[2], chunkExpiry)] : entry),
821
+ result: undefined,
822
+ }
823
+ : { result: undefined });
824
+ let pendingWrite = false;
825
+ // A rejected or timed-out driver call may still commit after deletion.
826
+ // Only a confirmed write response clears pendingWrite.
827
+ const release = completeStash(Effect.gen(function* () {
828
+ const deleted = yield* Effect.gen(function* () {
829
+ for (const key of keys)
830
+ yield* storageDelete(key);
831
+ return true;
832
+ }).pipe(Effect.catch(() => Effect.succeed(false)));
833
+ if (pendingWrite)
834
+ return;
835
+ if (deleted) {
836
+ const released = yield* swapStashLedger((live) => live.some(owns)
837
+ ? { entries: live.filter((entry) => !owns(entry)), result: undefined }
838
+ : { result: undefined });
839
+ if (released.status === "applied")
840
+ return;
841
+ }
842
+ yield* settle;
843
+ }));
844
+ // Trailing chunks first, then the header. The caller receives no result
845
+ // ID after failure or timeout, including a late header commit.
846
+ const remaining = Math.max(0, writeDeadline - (yield* Clock.currentTimeMillis));
847
+ const written = yield* Effect.gen(function* () {
848
+ for (let index = keys.length - 1; index >= 0; index--) {
849
+ const before = yield* Clock.currentTimeMillis;
850
+ if (before >= writeDeadline || before >= chunkExpiry)
851
+ return false;
852
+ pendingWrite = true;
853
+ yield* storageSet(keys[index], chunks[index], { expiresAtMs: chunkExpiry }).pipe(Effect.tap(() => Effect.sync(() => {
854
+ pendingWrite = false;
855
+ })));
856
+ if ((yield* Clock.currentTimeMillis) >= writeDeadline)
857
+ return false;
858
+ }
859
+ return true;
860
+ }).pipe(Effect.timeoutOrElse({ duration: Duration.millis(remaining), orElse: () => Effect.succeed(false) }), Effect.onError(() => release.pipe(Effect.ignore)));
861
+ if (!written)
862
+ yield* release;
863
+ else
864
+ yield* completeStash(settle);
865
+ return written;
866
+ }).pipe(Effect.provideService(Storage, self.opts.storage)),
867
+ recheckResultAccess: () => Effect.succeed(true),
868
+ storeOAuthHandoff: (self, connectorId, state, principalKey) => Effect.gen(function* () {
869
+ const key = oauthHandoffKeys.handoff(connectorId, yield* Effect.tryPromise({ try: () => sha256Hex(state), catch: (error) => error }));
870
+ for (let attempt = 0; attempt < 32; attempt++) {
871
+ const existing = yield* storageGet(key);
872
+ if (existing && existing !== principalKey)
873
+ throw new Error(`Connector "${connectorId}" reused one OAuth state across principals`);
874
+ if (yield* storageCompareAndSet(key, existing, principalKey, { ttlSeconds: OAUTH_HANDOFF_TTL_SECONDS }))
875
+ return;
876
+ }
877
+ throw new Error(`OAuth handoff for "${connectorId}" is busy; retry authorization`);
878
+ }).pipe(Effect.provideService(Storage, self.opts.storage)),
879
+ oauthCallbackView: (self, connectorId, state) => Effect.gen(function* () {
880
+ if (!state)
881
+ return null;
882
+ const digest = yield* Effect.tryPromise({ try: () => sha256Hex(state), catch: (error) => error });
883
+ const principalKey = yield* storageGet(oauthHandoffKeys.handoff(connectorId, digest));
884
+ const connector = self.connectors.get(connectorId);
885
+ if (!connector || !principalKey)
886
+ return null;
887
+ return {
888
+ registry: connector.authScope === "personal"
889
+ ? self.scoped({ connectorIds: [connectorId], subjectKey: principalKey, principalKey })
890
+ : self,
891
+ principalKey,
892
+ };
893
+ }).pipe(Effect.provideService(Storage, self.opts.storage)),
894
+ consumeOAuthHandoff: (self, connectorId, state, principalKey) => Effect.gen(function* () {
895
+ if (!state)
896
+ return false;
897
+ const digest = yield* Effect.tryPromise({ try: () => sha256Hex(state), catch: (error) => error });
898
+ return yield* storageCompareAndSet(oauthHandoffKeys.handoff(connectorId, digest), principalKey, null);
899
+ }).pipe(Effect.provideService(Storage, self.opts.storage)),
900
+ bindOAuthHandoff: (self, id, authorizationUrl, principalKey) => Effect.gen(function* () {
901
+ const state = new URL(authorizationUrl).searchParams.get("state");
902
+ if (!state || !principalKey)
903
+ throw new Error("OAuth requires state and an initiating user");
904
+ yield* registryProgram(self, "storeOAuthHandoff", id, state, principalKey);
905
+ }),
906
+ getTools: (self, id, baseUrl, requestScope, callOptions = {}, scope) => Effect.suspend(() => {
907
+ if (!self.connectors.has(id))
908
+ return Effect.fail(new Error(`Unknown connector "${id}"`));
909
+ const owner = self.contextFor(id, baseUrl, requestScope, callOptions, scope);
910
+ return duringViewEffect(owner, (signal) => {
911
+ if (!requestScope)
912
+ return self.loadTools(id, baseUrl, owner.requestScope, { ...callOptions, signal }, scope);
913
+ let loads = self.requestCatalogLoads.get(requestScope);
914
+ if (!loads) {
915
+ loads = new Map();
916
+ self.requestCatalogLoads.set(requestScope, loads);
917
+ }
918
+ const requestLoads = loads;
919
+ const key = JSON.stringify([id, scope?.principalKey, scope?.subjectKey, scope?.caller]);
920
+ let load = requestLoads.get(key);
921
+ if (!load) {
922
+ const started = new SharedRead((signal) => self.loadTools(id, baseUrl, requestScope, { ...callOptions, signal }, scope), () => {
923
+ if (requestLoads.get(key) === started)
924
+ requestLoads.delete(key);
925
+ if (requestLoads.size === 0)
926
+ self.requestCatalogLoads.delete(requestScope);
927
+ }, requestScope);
928
+ requestLoads.set(key, started);
929
+ load = started;
930
+ }
931
+ return load.join(signal);
932
+ });
933
+ }),
934
+ credentialDriftFor: (self, id) => Effect.gen(function* () {
935
+ const credential = self.connectors.get(id)?.credential;
936
+ const vault = self.opts.credentialVault;
937
+ if (!credential || !vault)
938
+ return undefined;
939
+ return yield* Effect.tryPromise({
940
+ try: () => vault.getAll(id, self.opts.credentialOwner),
941
+ catch: (error) => error,
942
+ }).pipe(Effect.map((values) => {
943
+ const shape = storedCredentialShape(credential, values);
944
+ return shape.state === "mismatch" ? shape.message : undefined;
945
+ }), Effect.catchCause((cause) => Effect.sync(() => {
946
+ logFailure(self.opts.logger, "credential shape read failed", failureRecord({ connector: id }, causeError(cause)));
947
+ return undefined;
948
+ })));
949
+ }),
950
+ statusFor: (self, id, baseUrl, requestScope = {}, callOptions = {}, scope) => Effect.suspend(() => {
951
+ const connector = self.connectors.get(id);
952
+ if (!connector)
953
+ return Effect.succeed(ownStatus({ state: "error", message: "Unknown connector" }));
954
+ const ctx = self.contextFor(id, baseUrl, requestScope, callOptions, scope);
955
+ const withObservations = (status) => self.withCatalogObservations(id, baseUrl, status, scope);
956
+ return duringViewEffect(ctx, () => Effect.gen(function* () {
957
+ if (connector.credential) {
958
+ const values = yield* Effect.tryPromise({
959
+ try: () => self.opts.credentialVault?.getAll(id, self.opts.credentialOwner) ?? Promise.resolve(null),
960
+ catch: (error) => error,
961
+ });
962
+ const shape = storedCredentialShape(connector.credential, values);
963
+ if (shape.state === "missing")
964
+ return withObservations(ownStatus({ state: "credential_required" }));
965
+ if (shape.state === "mismatch")
966
+ return withObservations(ownStatus({ state: "auth_required" }));
967
+ }
968
+ if (connector.status)
969
+ return withObservations(yield* authInvoke(connector, "status", [ctx]));
970
+ yield* registryProgram(self, "getTools", id, baseUrl, requestScope, callOptions, scope);
971
+ return withObservations({ state: "ok" });
972
+ })).pipe(Effect.catchCause((cause) => Effect.succeed(withObservations(failureStatus(id, causeError(cause, ctx.signal))))));
973
+ }),
974
+ invalidateStored: (self, id) => Effect.suspend(() => {
975
+ self.catalogObservedAt.delete(id);
976
+ return Effect.tryPromise({
977
+ try: () => invalidateCatalogCache(self.opts.catalogStorage ?? self.opts.storage, id),
978
+ catch: (error) => error,
979
+ }).pipe(Effect.catch((error) => Effect.sync(() => logFailure(self.opts.logger, "catalog invalidation failed", failureRecord({ connector: id }, error)))));
980
+ }),
981
+ });
975
982
  }
976
983
  }
977
984
  class ScopedRegistryView {
@@ -1037,27 +1044,8 @@ class ScopedRegistryView {
1037
1044
  const connector = this.getConnector(parsed.connectorId);
1038
1045
  return connector ? { connector, toolName: parsed.toolName } : null;
1039
1046
  }
1040
- async getTools(...args) {
1041
- const registry = this.registryFor(args[0]);
1042
- if (!registry) {
1043
- throw new Error(`Unknown connector "${args[0]}"`);
1044
- }
1045
- const tools = await registry.getTools(args[0], args[1], args[2], args[3], this.scope);
1046
- const granted = this.scope.toolAccess?.get(args[0]);
1047
- if (!granted)
1048
- return tools;
1049
- // Every consumer — search, describe, call_tool, and a program's
1050
- // connecta.call — resolves through this list, so an ungranted tool is
1051
- // indistinguishable from one the connector never had.
1052
- const visible = tools.filter((tool) => granted.has(tool.name));
1053
- if (visible.length < granted.size) {
1054
- const present = new Set(tools.map((tool) => tool.name));
1055
- for (const name of granted) {
1056
- if (!present.has(name))
1057
- this.root.noteAbsentGrant(args[0], name);
1058
- }
1059
- }
1060
- return carryCatalogFreshness(tools, visible);
1047
+ getTools(...args) {
1048
+ return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "getTools", ...args));
1061
1049
  }
1062
1050
  contextFor(...args) {
1063
1051
  const registry = this.registryFor(args[0]);
@@ -1083,15 +1071,14 @@ class ScopedRegistryView {
1083
1071
  origin: this.scope.origin ?? null,
1084
1072
  };
1085
1073
  }
1086
- recheckResultAccess(address, classification, signal, access) {
1087
- return this.scope.currentResultAccess?.(address, classification, signal, access) ?? Promise.resolve(true);
1074
+ recheckResultAccess(dependencies, classification, signal) {
1075
+ return this.scope.currentResultAccess?.(dependencies, classification, signal) ?? Promise.resolve(true);
1088
1076
  }
1089
1077
  resultsStorage() {
1090
1078
  return this.scope.subjectKey ? this.root.scopedStorage(this.scope.subjectKey) : this.root.resultsStorage();
1091
1079
  }
1092
1080
  credentialDriftFor(id) {
1093
- const registry = this.registryFor(id);
1094
- return registry ? registry.credentialDriftFor(id) : Promise.resolve(undefined);
1081
+ return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "credentialDriftFor", id));
1095
1082
  }
1096
1083
  observedOutputSchema(connectorId, definition) {
1097
1084
  return this.registryFor(connectorId)?.observedOutputSchema(connectorId, definition);
@@ -1103,19 +1090,65 @@ class ScopedRegistryView {
1103
1090
  return this.registryFor(id)?.catalogAgeMs(id) ?? null;
1104
1091
  }
1105
1092
  statusFor(...args) {
1106
- const registry = this.registryFor(args[0]);
1107
- return registry
1108
- ? registry.statusFor(args[0], args[1], args[2], args[3], this.scope)
1109
- : Promise.resolve({ state: "error", message: "Unknown connector" });
1093
+ return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "statusFor", ...args));
1094
+ }
1095
+ withCatalogObservations(id, baseUrl, status) {
1096
+ return (this.registryFor(id)?.withCatalogObservations(id, baseUrl, status, this.scope) ??
1097
+ ownStatus({ state: "error", message: "Unknown connector" }));
1110
1098
  }
1111
1099
  invalidateStored(id) {
1112
- const registry = this.registryFor(id);
1113
- return registry ? registry.invalidateStored(id) : Promise.resolve();
1100
+ return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "invalidateStored", id));
1114
1101
  }
1115
1102
  credentialUiAvailable() {
1116
1103
  return this.root.credentialUiAvailable();
1117
1104
  }
1118
1105
  async bindOAuthHandoff(id, authorizationUrl, principalKey) {
1119
- await this.root.bindOAuthHandoff(id, authorizationUrl, principalKey ?? this.scope.principalKey);
1106
+ return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "bindOAuthHandoff", id, authorizationUrl, principalKey));
1107
+ }
1108
+ static {
1109
+ provideRegistryPrograms(ScopedRegistryView.prototype, {
1110
+ stashResult: (self, id, chunks, ttlSeconds) => registryProgram(self.root, "stashResult", id, chunks, ttlSeconds, self.scope.subjectKey ? scopes.subject(self.scope.subjectKey) : scopes.results),
1111
+ recheckResultAccess: (self, dependencies, classification, signal) => Effect.tryPromise({
1112
+ try: () => self.scope.currentResultAccess?.(dependencies, classification, signal) ?? Promise.resolve(true),
1113
+ catch: (error) => error,
1114
+ }),
1115
+ getTools: (self, ...args) => Effect.gen(function* () {
1116
+ const registry = self.registryFor(args[0]);
1117
+ if (!registry) {
1118
+ throw new Error(`Unknown connector "${args[0]}"`);
1119
+ }
1120
+ const tools = yield* registryProgram(registry, "getTools", args[0], args[1], args[2], args[3], self.scope);
1121
+ const granted = self.scope.toolAccess?.get(args[0]);
1122
+ if (!granted)
1123
+ return tools;
1124
+ // Every consumer — search, describe, call_tool, and a program's
1125
+ // connecta.call — resolves through this list, so an ungranted tool is
1126
+ // indistinguishable from one the connector never had.
1127
+ const visible = tools.filter((tool) => granted.has(tool.name));
1128
+ if (visible.length < granted.size) {
1129
+ const present = new Set(tools.map((tool) => tool.name));
1130
+ for (const name of granted) {
1131
+ if (!present.has(name))
1132
+ self.root.noteAbsentGrant(args[0], name);
1133
+ }
1134
+ }
1135
+ return carryCatalogFreshness(tools, visible);
1136
+ }),
1137
+ credentialDriftFor: (self, id) => Effect.suspend(() => {
1138
+ const registry = self.registryFor(id);
1139
+ return registry ? registryProgram(registry, "credentialDriftFor", id) : Effect.succeed(undefined);
1140
+ }),
1141
+ statusFor: (self, ...args) => Effect.suspend(() => {
1142
+ const registry = self.registryFor(args[0]);
1143
+ return registry
1144
+ ? registryProgram(registry, "statusFor", args[0], args[1], args[2], args[3], self.scope)
1145
+ : Effect.succeed({ state: "error", message: "Unknown connector" });
1146
+ }),
1147
+ invalidateStored: (self, id) => Effect.suspend(() => {
1148
+ const registry = self.registryFor(id);
1149
+ return registry ? registryProgram(registry, "invalidateStored", id) : Effect.succeed(undefined);
1150
+ }),
1151
+ bindOAuthHandoff: (self, id, authorizationUrl, principalKey) => registryProgram(self.root, "bindOAuthHandoff", id, authorizationUrl, principalKey ?? self.scope.principalKey),
1152
+ });
1120
1153
  }
1121
1154
  }