@pyric/cli 0.1.0-alpha.11 → 0.1.0-alpha.13

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 (424) hide show
  1. package/dist/cli/serve.js +4 -4
  2. package/dist/cli/serve.js.map +1 -1
  3. package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
  4. package/dist/conformance/.generated/can-i-use-browser.js +1 -1
  5. package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
  6. package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
  7. package/dist/conformance/.generated/can-i-use.js +2 -2
  8. package/dist/conformance/.generated/can-i-use.js.map +1 -1
  9. package/dist/serve/bundler.d.ts +13 -8
  10. package/dist/serve/bundler.d.ts.map +1 -1
  11. package/dist/serve/bundler.js +31 -36
  12. package/dist/serve/bundler.js.map +1 -1
  13. package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
  14. package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
  15. package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
  16. package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
  17. package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
  18. package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
  19. package/dist/serve/docs-ui/docs/api-reference.md +1 -1
  20. package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
  21. package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
  22. package/dist/serve/docs-ui/docs/build/ai-logic/index.html +44 -22
  23. package/dist/serve/docs-ui/docs/build/ai-logic.md +69 -21
  24. package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
  25. package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
  26. package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
  27. package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
  28. package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
  29. package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
  30. package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
  31. package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
  32. package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
  33. package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
  34. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +2 -1
  35. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works.md +2 -0
  36. package/dist/serve/docs-ui/docs/get-started/start-building/index.html +3 -2
  37. package/dist/serve/docs-ui/docs/get-started/start-building.md +3 -1
  38. package/dist/serve/docs-ui/docs/index.json +349 -29
  39. package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
  40. package/dist/serve/docs-ui/docs/observe/resolve-runtime-status/index.html +28 -0
  41. package/dist/serve/docs-ui/docs/observe/resolve-runtime-status.md +47 -0
  42. package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
  43. package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
  44. package/dist/serve/docs-ui/docs/overview/index.html +1 -1
  45. package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
  46. package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
  47. package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
  48. package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
  49. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
  50. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
  51. package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
  52. package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
  53. package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
  54. package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
  55. package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
  56. package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
  57. package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
  58. package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
  59. package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
  60. package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
  61. package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
  62. package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
  63. package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
  64. package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
  65. package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
  66. package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
  67. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +168 -76
  68. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +156 -73
  69. package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
  70. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +11 -8
  71. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api.md +17 -4
  72. package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
  73. package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
  74. package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
  75. package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
  76. package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
  77. package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
  78. package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
  79. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
  80. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
  81. package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
  82. package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
  83. package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
  84. package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
  85. package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
  86. package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
  87. package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
  88. package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
  89. package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
  90. package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
  91. package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
  92. package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
  93. package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
  94. package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
  95. package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
  96. package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
  97. package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
  98. package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
  99. package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
  100. package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
  101. package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
  102. package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
  103. package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
  104. package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
  105. package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
  106. package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
  107. package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
  108. package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
  109. package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
  110. package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
  111. package/dist/serve/docs-ui/docs/tutorial/index.html +429 -0
  112. package/dist/serve/docs-ui/docs/tutorial.md +701 -0
  113. package/dist/serve/docs-ui/llms.txt +3 -1
  114. package/dist/serve/entries/init.d.ts.map +1 -1
  115. package/dist/serve/entries/init.js +3 -0
  116. package/dist/serve/entries/init.js.map +1 -1
  117. package/dist/serve/entries/runtime.d.ts.map +1 -1
  118. package/dist/serve/entries/runtime.js +20 -5
  119. package/dist/serve/entries/runtime.js.map +1 -1
  120. package/dist/serve/entries/worker-runtime.d.ts +2 -2
  121. package/dist/serve/entries/worker-runtime.d.ts.map +1 -1
  122. package/dist/serve/entries/worker-runtime.js +45 -10
  123. package/dist/serve/entries/worker-runtime.js.map +1 -1
  124. package/dist/serve/runtime/chip-config.d.ts +19 -0
  125. package/dist/serve/runtime/chip-config.d.ts.map +1 -0
  126. package/dist/serve/runtime/chip-config.js +19 -0
  127. package/dist/serve/runtime/chip-config.js.map +1 -0
  128. package/dist/serve/runtime/chip-install.d.ts +10 -0
  129. package/dist/serve/runtime/chip-install.d.ts.map +1 -0
  130. package/dist/serve/runtime/chip-install.js +15 -0
  131. package/dist/serve/runtime/chip-install.js.map +1 -0
  132. package/dist/serve/runtime/chip.d.ts +17 -0
  133. package/dist/serve/runtime/chip.d.ts.map +1 -0
  134. package/dist/serve/runtime/chip.js +257 -0
  135. package/dist/serve/runtime/chip.js.map +1 -0
  136. package/dist/serve/runtime/manifest.d.ts +22 -0
  137. package/dist/serve/runtime/manifest.d.ts.map +1 -0
  138. package/dist/serve/runtime/manifest.js +20 -0
  139. package/dist/serve/runtime/manifest.js.map +1 -0
  140. package/dist/serve/runtime/status.d.ts +49 -0
  141. package/dist/serve/runtime/status.d.ts.map +1 -0
  142. package/dist/serve/runtime/status.js +164 -0
  143. package/dist/serve/runtime/status.js.map +1 -0
  144. package/dist/serve/runtime/worker-connection.d.ts +4 -0
  145. package/dist/serve/runtime/worker-connection.d.ts.map +1 -0
  146. package/dist/serve/runtime/worker-connection.js +11 -0
  147. package/dist/serve/runtime/worker-connection.js.map +1 -0
  148. package/dist/serve/runtime/worker-generation.d.ts +14 -0
  149. package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
  150. package/dist/serve/runtime/worker-generation.js +56 -0
  151. package/dist/serve/runtime/worker-generation.js.map +1 -0
  152. package/dist/serve/runtime/worker-replacement.d.ts +18 -0
  153. package/dist/serve/runtime/worker-replacement.d.ts.map +1 -0
  154. package/dist/serve/runtime/worker-replacement.js +29 -0
  155. package/dist/serve/runtime/worker-replacement.js.map +1 -0
  156. package/dist/serve/standalone-assets.d.ts +2 -2
  157. package/dist/serve/standalone-assets.d.ts.map +1 -1
  158. package/dist/serve/standalone-assets.js +1 -1
  159. package/dist/serve/standalone-assets.js.map +1 -1
  160. package/dist/serve/studio-ui/assets/{RulesCodeEditor-CxVYcZXw.js → RulesCodeEditor-BDlH-5lp.js} +1 -1
  161. package/dist/serve/studio-ui/assets/{index-cpSfgQ7H.js → index-Bf9SSE8L.js} +5 -5
  162. package/dist/serve/studio-ui/assets/{seed-DeuFKi4J.js → seed-D_UjxfOU.js} +1 -1
  163. package/dist/serve/studio-ui/index.html +1 -1
  164. package/dist/serve/vite-ai-config.d.ts +25 -0
  165. package/dist/serve/vite-ai-config.d.ts.map +1 -0
  166. package/dist/serve/vite-ai-config.js +45 -0
  167. package/dist/serve/vite-ai-config.js.map +1 -0
  168. package/dist/serve/vite-plugin.d.ts +17 -36
  169. package/dist/serve/vite-plugin.d.ts.map +1 -1
  170. package/dist/serve/vite-plugin.js +38 -57
  171. package/dist/serve/vite-plugin.js.map +1 -1
  172. package/dist/serve/vite-rules-source.d.ts +8 -0
  173. package/dist/serve/vite-rules-source.d.ts.map +1 -0
  174. package/dist/serve/vite-rules-source.js +23 -0
  175. package/dist/serve/vite-rules-source.js.map +1 -0
  176. package/dist/serve/vite-worker-runtime.d.ts +20 -0
  177. package/dist/serve/vite-worker-runtime.d.ts.map +1 -0
  178. package/dist/serve/vite-worker-runtime.js +25 -0
  179. package/dist/serve/vite-worker-runtime.js.map +1 -0
  180. package/dist/serve/worker/client/connection.d.ts +7 -2
  181. package/dist/serve/worker/client/connection.d.ts.map +1 -1
  182. package/dist/serve/worker/client/connection.js +13 -16
  183. package/dist/serve/worker/client/connection.js.map +1 -1
  184. package/dist/serve/worker/client/core.d.ts +7 -0
  185. package/dist/serve/worker/client/core.d.ts.map +1 -1
  186. package/dist/serve/worker/client/core.js +28 -0
  187. package/dist/serve/worker/client/core.js.map +1 -1
  188. package/dist/serve/worker/client/runtime-control.d.ts +8 -0
  189. package/dist/serve/worker/client/runtime-control.d.ts.map +1 -0
  190. package/dist/serve/worker/client/runtime-control.js +11 -0
  191. package/dist/serve/worker/client/runtime-control.js.map +1 -0
  192. package/dist/serve/worker/entry.js +44 -2
  193. package/dist/serve/worker/entry.js.map +1 -1
  194. package/dist/serve/worker/host/core.js +1 -1
  195. package/dist/serve/worker/host/core.js.map +1 -1
  196. package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
  197. package/dist/serve/worker/host/firestore-writes.js +3 -2
  198. package/dist/serve/worker/host/firestore-writes.js.map +1 -1
  199. package/dist/serve/worker/host/studio.js +1 -1
  200. package/dist/serve/worker/host/studio.js.map +1 -1
  201. package/dist/serve/worker/host-context.d.ts +1 -1
  202. package/dist/serve/worker/host-context.d.ts.map +1 -1
  203. package/dist/serve/worker/index.d.ts +2 -1
  204. package/dist/serve/worker/index.d.ts.map +1 -1
  205. package/dist/serve/worker/index.js +2 -1
  206. package/dist/serve/worker/index.js.map +1 -1
  207. package/dist/serve/worker/protocol.d.ts +15 -1
  208. package/dist/serve/worker/protocol.d.ts.map +1 -1
  209. package/dist/serve/worker/protocol.js +2 -2
  210. package/dist/serve/worker/protocol.js.map +1 -1
  211. package/dist/serve/worker/retirement.d.ts +21 -0
  212. package/dist/serve/worker/retirement.d.ts.map +1 -0
  213. package/dist/serve/worker/retirement.js +97 -0
  214. package/dist/serve/worker/retirement.js.map +1 -0
  215. package/dist/serve/worker/serve-init.d.ts.map +1 -1
  216. package/dist/serve/worker/serve-init.js +5 -6
  217. package/dist/serve/worker/serve-init.js.map +1 -1
  218. package/dist/vite.d.ts +1 -0
  219. package/dist/vite.d.ts.map +1 -1
  220. package/dist/vite.js.map +1 -1
  221. package/package.json +6 -4
  222. package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
  223. package/src/assurance/attachment.ts +211 -0
  224. package/src/assurance/browser.ts +80 -0
  225. package/src/assurance/campaign.ts +496 -0
  226. package/src/assurance/capabilities.ts +446 -0
  227. package/src/assurance/cases.ts +43 -0
  228. package/src/assurance/index.ts +75 -0
  229. package/src/assurance/runner.ts +885 -0
  230. package/src/assurance/tool-names.ts +15 -0
  231. package/src/assurance/tools.ts +830 -0
  232. package/src/assurance/types.ts +352 -0
  233. package/src/assurance/validation.ts +434 -0
  234. package/src/bridge/client/bridge.ts +550 -0
  235. package/src/bridge/client/dispatch.ts +137 -0
  236. package/src/bridge/client.ts +45 -0
  237. package/src/bridge/protocol.ts +351 -0
  238. package/src/bridge/server/audit.ts +53 -0
  239. package/src/bridge/server/bridge.ts +597 -0
  240. package/src/bridge/server/headless.ts +160 -0
  241. package/src/bridge/server/json-schema-to-zod.ts +108 -0
  242. package/src/bridge/server/local-bridge.ts +66 -0
  243. package/src/bridge/server/logger.ts +50 -0
  244. package/src/bridge/server/mcp-contract.ts +93 -0
  245. package/src/bridge/server/mcp.ts +129 -0
  246. package/src/bridge/server/peer.ts +232 -0
  247. package/src/bridge/server/standalone.ts +347 -0
  248. package/src/bridge/server/tool-metadata.ts +99 -0
  249. package/src/bridge/server.ts +28 -0
  250. package/src/cli/can-i-use.ts +50 -0
  251. package/src/cli/cli.test.ts +493 -0
  252. package/src/cli/database-rules.ts +329 -0
  253. package/src/cli/dev-runner.ts +282 -0
  254. package/src/cli/firebase-json.ts +100 -0
  255. package/src/cli/firestore-indexes.ts +70 -0
  256. package/src/cli/index.ts +425 -0
  257. package/src/cli/init.ts +260 -0
  258. package/src/cli/mcp-proxy.ts +196 -0
  259. package/src/cli/parse-args.ts +80 -0
  260. package/src/cli/rules.ts +259 -0
  261. package/src/cli/scope.ts +67 -0
  262. package/src/cli/serve.ts +996 -0
  263. package/src/cli/service-commands.ts +73 -0
  264. package/src/cli/snapshot.ts +164 -0
  265. package/src/cli/storage-rules.ts +172 -0
  266. package/src/cli/verify.ts +416 -0
  267. package/src/conformance/.generated/can-i-use-browser.ts +113 -0
  268. package/src/conformance/.generated/can-i-use.ts +117 -0
  269. package/src/conformance/.generated/conformance-docs.ts +14 -0
  270. package/src/conformance/browser.ts +24 -0
  271. package/src/conformance/can-i-use-tool.ts +51 -0
  272. package/src/conformance/can-i-use.ts +18 -0
  273. package/src/conformance/docs.ts +8 -0
  274. package/src/conformance/index.ts +20 -0
  275. package/src/conformance/tools.ts +16 -0
  276. package/src/credentials/core/memoize-ttl.ts +142 -0
  277. package/src/credentials/core/types.ts +10 -0
  278. package/src/credentials/node/from-adc.ts +96 -0
  279. package/src/credentials/node/from-service-account.ts +133 -0
  280. package/src/credentials/node/index.ts +6 -0
  281. package/src/discover/concurrency.ts +127 -0
  282. package/src/discover/crawler-adapter.ts +142 -0
  283. package/src/discover/crawler.ts +1127 -0
  284. package/src/discover/credential-free.ts +27 -0
  285. package/src/discover/findCollectionGroup.ts +131 -0
  286. package/src/discover/firestore-source.ts +59 -0
  287. package/src/discover/index.ts +5 -0
  288. package/src/discover/merge.ts +523 -0
  289. package/src/discover/session.ts +402 -0
  290. package/src/discover/tools.ts +201 -0
  291. package/src/discover/types.ts +187 -0
  292. package/src/discover/wire.ts +324 -0
  293. package/src/functions-rtdb/child.ts +382 -0
  294. package/src/functions-rtdb/delivery.ts +7 -0
  295. package/src/functions-rtdb/discovery.ts +136 -0
  296. package/src/functions-rtdb/event.ts +64 -0
  297. package/src/functions-rtdb/execution.ts +111 -0
  298. package/src/functions-rtdb/in-memory-delivery.ts +40 -0
  299. package/src/functions-rtdb/project.ts +74 -0
  300. package/src/functions-rtdb/projection.ts +104 -0
  301. package/src/functions-rtdb/reference-pattern.ts +20 -0
  302. package/src/functions-rtdb/remote-delivery.ts +23 -0
  303. package/src/pkg-version.ts +49 -0
  304. package/src/register/esm-exports.ts +63 -0
  305. package/src/register/hooks.ts +30 -0
  306. package/src/register/index.ts +150 -0
  307. package/src/register/mapping.ts +33 -0
  308. package/src/remote/index.ts +1050 -0
  309. package/src/rtdb/crawl-snapshot.ts +118 -0
  310. package/src/rtdb/inspection.ts +137 -0
  311. package/src/rtdb/load-rules-document.ts +56 -0
  312. package/src/rtdb/rules-generation-tool.ts +38 -0
  313. package/src/rtdb/rules-json.ts +29 -0
  314. package/src/serve/activity-guard.ts +29 -0
  315. package/src/serve/activity-route.ts +164 -0
  316. package/src/serve/activity-warning.ts +26 -0
  317. package/src/serve/bridge-mount.ts +200 -0
  318. package/src/serve/bundler.ts +519 -0
  319. package/src/serve/capture-store.ts +65 -0
  320. package/src/serve/discovery.ts +199 -0
  321. package/src/serve/entries/ai.ts +292 -0
  322. package/src/serve/entries/app-backend.ts +4 -0
  323. package/src/serve/entries/app-client.ts +26 -0
  324. package/src/serve/entries/app-session-store.ts +47 -0
  325. package/src/serve/entries/app.ts +11 -0
  326. package/src/serve/entries/auth-helper-core.ts +181 -0
  327. package/src/serve/entries/auth-helper-dom.ts +134 -0
  328. package/src/serve/entries/auth-helper-runtime.ts +20 -0
  329. package/src/serve/entries/auth.ts +255 -0
  330. package/src/serve/entries/bridge-url.ts +31 -0
  331. package/src/serve/entries/database.ts +129 -0
  332. package/src/serve/entries/firestore.ts +232 -0
  333. package/src/serve/entries/init.ts +49 -0
  334. package/src/serve/entries/keepalive.ts +46 -0
  335. package/src/serve/entries/messaging-sw.ts +75 -0
  336. package/src/serve/entries/messaging.ts +96 -0
  337. package/src/serve/entries/runtime.ts +556 -0
  338. package/src/serve/entries/session-store.ts +97 -0
  339. package/src/serve/entries/storage.ts +92 -0
  340. package/src/serve/entries/tab-sync-wiring.ts +274 -0
  341. package/src/serve/entries/worker-runtime.ts +135 -0
  342. package/src/serve/init-payload.ts +42 -0
  343. package/src/serve/namespace.ts +675 -0
  344. package/src/serve/open-browser.ts +68 -0
  345. package/src/serve/rules.ts +272 -0
  346. package/src/serve/runtime/chip-config.ts +30 -0
  347. package/src/serve/runtime/chip-install.ts +27 -0
  348. package/src/serve/runtime/chip.ts +282 -0
  349. package/src/serve/runtime/manifest.ts +37 -0
  350. package/src/serve/runtime/status.ts +216 -0
  351. package/src/serve/runtime/worker-connection.ts +14 -0
  352. package/src/serve/runtime/worker-generation.ts +75 -0
  353. package/src/serve/runtime/worker-replacement.ts +42 -0
  354. package/src/serve/sandbox-marker.ts +31 -0
  355. package/src/serve/server.ts +428 -0
  356. package/src/serve/standalone-assets.ts +180 -0
  357. package/src/serve/state-store.ts +166 -0
  358. package/src/serve/studio/disk-project-store.ts +185 -0
  359. package/src/serve/studio/disk-workspace.ts +162 -0
  360. package/src/serve/studio/index.ts +20 -0
  361. package/src/serve/studio/routes.ts +237 -0
  362. package/src/serve/studio/store-types.ts +48 -0
  363. package/src/serve/studio/studio-storage.test.ts +289 -0
  364. package/src/serve/vite-ai-config.ts +77 -0
  365. package/src/serve/vite-plugin.ts +1075 -0
  366. package/src/serve/vite-rules-source.ts +28 -0
  367. package/src/serve/vite-worker-runtime.ts +51 -0
  368. package/src/serve/worker/activity-bootstrap.ts +24 -0
  369. package/src/serve/worker/client/admin-firestore.ts +38 -0
  370. package/src/serve/worker/client/ai.ts +124 -0
  371. package/src/serve/worker/client/auth.ts +489 -0
  372. package/src/serve/worker/client/connection.ts +227 -0
  373. package/src/serve/worker/client/core.ts +384 -0
  374. package/src/serve/worker/client/disconnect.ts +45 -0
  375. package/src/serve/worker/client/firestore-reads.ts +181 -0
  376. package/src/serve/worker/client/firestore-refs.ts +226 -0
  377. package/src/serve/worker/client/firestore-writes.ts +232 -0
  378. package/src/serve/worker/client/handles.ts +91 -0
  379. package/src/serve/worker/client/messaging.ts +119 -0
  380. package/src/serve/worker/client/presence.ts +174 -0
  381. package/src/serve/worker/client/rtdb.ts +311 -0
  382. package/src/serve/worker/client/rules.ts +63 -0
  383. package/src/serve/worker/client/runtime-control.ts +22 -0
  384. package/src/serve/worker/client/service-worker-connection.ts +59 -0
  385. package/src/serve/worker/client/snapshots.ts +86 -0
  386. package/src/serve/worker/client/storage.ts +195 -0
  387. package/src/serve/worker/client/studio.ts +88 -0
  388. package/src/serve/worker/client.ts +57 -0
  389. package/src/serve/worker/durable-persistence.ts +137 -0
  390. package/src/serve/worker/entry.ts +223 -0
  391. package/src/serve/worker/host/admin-firestore.ts +88 -0
  392. package/src/serve/worker/host/connection.ts +179 -0
  393. package/src/serve/worker/host/core.ts +387 -0
  394. package/src/serve/worker/host/dispatch.ts +316 -0
  395. package/src/serve/worker/host/firestore-reads.ts +117 -0
  396. package/src/serve/worker/host/firestore-writes.ts +456 -0
  397. package/src/serve/worker/host/presence.ts +312 -0
  398. package/src/serve/worker/host/rtdb.ts +136 -0
  399. package/src/serve/worker/host/rules.ts +128 -0
  400. package/src/serve/worker/host/storage.ts +312 -0
  401. package/src/serve/worker/host/studio.ts +75 -0
  402. package/src/serve/worker/host/subscriptions.ts +238 -0
  403. package/src/serve/worker/host-ai.ts +165 -0
  404. package/src/serve/worker/host-auth.ts +469 -0
  405. package/src/serve/worker/host-context.ts +273 -0
  406. package/src/serve/worker/host-events.ts +94 -0
  407. package/src/serve/worker/host-messaging.ts +240 -0
  408. package/src/serve/worker/host.ts +47 -0
  409. package/src/serve/worker/index.ts +196 -0
  410. package/src/serve/worker/presence-timing.ts +15 -0
  411. package/src/serve/worker/protocol.ts +1124 -0
  412. package/src/serve/worker/retirement.ts +125 -0
  413. package/src/serve/worker/serve-init.ts +605 -0
  414. package/src/serve/worker/service-worker-channel.ts +33 -0
  415. package/src/serve/worker/service-worker-relay.ts +86 -0
  416. package/src/serve/writer-lock.ts +53 -0
  417. package/src/verify/cases.ts +233 -0
  418. package/src/verify/fixture.ts +258 -0
  419. package/src/verify/index.ts +519 -0
  420. package/src/verify/tools.ts +108 -0
  421. package/src/version/compat-target.ts +17 -0
  422. package/src/vite.ts +18 -0
  423. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
  424. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Bridge transport helpers shared by the bridge mounts.
3
+ *
4
+ * `attachPeer` adapts a `ws` WebSocket into either a registered sandbox PEER
5
+ * (the browser-side `connectBridge` is the other end — first message
6
+ * `hello`) or a worker-relay CONSUMER (the Node-side `connectRemoteSandbox`
7
+ * — first message `attach`); `collectBody` buffers a Node request body into
8
+ * parsed JSON for the stateless MCP transport.
9
+ *
10
+ * Both are consumed by `serve/bridge-mount.ts` (the `pyric dev --bridge` and
11
+ * `pyric({ bridge })` mount) and `serve/namespace.ts` (capture route).
12
+ * They live here — not inline in a plugin file — so retiring the standalone
13
+ * bridge Vite plugin doesn't drag its consumers with it.
14
+ */
15
+ import type { IncomingMessage } from 'node:http';
16
+ import type { WebSocket } from 'ws';
17
+ import { createBridge, type Bridge } from './bridge.js';
18
+ import {
19
+ isBridgeMessage,
20
+ PEER_REPLACED_CLOSE_CODE,
21
+ PEER_REPLACED_CLOSE_REASON,
22
+ type BridgeMessage,
23
+ } from '../protocol.js';
24
+ import { cliVersion } from '../../pkg-version.js';
25
+
26
+ export function attachPeer(
27
+ bridge: ReturnType<typeof createBridge>,
28
+ ws: WebSocket,
29
+ ): void {
30
+ let disconnect: (() => void) | null = null;
31
+ /** Peer generation captured at registration — tags every inbound frame so
32
+ * a replaced tab's socket can't resolve the NEW peer's calls or deliver
33
+ * stale subscription snaps (see Bridge.peerGeneration). */
34
+ let peerGen = 0;
35
+ let helloed = false;
36
+ let consumer: ConsumerSession | null = null;
37
+
38
+ ws.on('message', (raw) => {
39
+ let msg: unknown;
40
+ try {
41
+ msg = JSON.parse(raw.toString());
42
+ } catch {
43
+ return;
44
+ }
45
+ if (!isBridgeMessage(msg)) return;
46
+ if (msg.type === 'attach') {
47
+ // Worker-relay consumer (Node client). NOT a peer: attaching never
48
+ // kicks the browser tab out of last-connection-wins.
49
+ if (helloed || consumer) return;
50
+ consumer = createConsumerSession(bridge, (out: BridgeMessage) => {
51
+ try {
52
+ ws.send(JSON.stringify(out));
53
+ } catch {}
54
+ });
55
+ consumer.handleMessage(msg); // acks with attach-ack
56
+ return;
57
+ }
58
+ if (consumer) {
59
+ consumer.handleMessage(msg);
60
+ return;
61
+ }
62
+ if (msg.type === 'hello') {
63
+ if (helloed) return;
64
+ helloed = true;
65
+ disconnect = bridge.registerSandboxPeer(
66
+ (out: BridgeMessage) => {
67
+ try {
68
+ ws.send(JSON.stringify(out));
69
+ } catch {}
70
+ },
71
+ // Harden against a malformed hello: these fields come off the wire
72
+ // and feed `new Set(...)` in the bridge core — a non-array value
73
+ // (e.g. `capabilities: 42`) would throw inside this message
74
+ // listener, escape uncaught, and crash the serve process.
75
+ Array.isArray(msg.tools) ? msg.tools : [],
76
+ msg.sandboxId,
77
+ Array.isArray(msg.capabilities) ? msg.capabilities : [],
78
+ // On replacement, close THIS socket: the browser side's onclose
79
+ // handler tears down its relayed worker subscriptions, so a
80
+ // replaced tab's SharedWorker listeners don't keep streaming
81
+ // snaps the bridge drops as stale-generation until the tab closes.
82
+ // The REPLACED close code tells that client to go STANDBY (health-
83
+ // poll for a vacant slot) instead of re-helloing — an immediate
84
+ // re-hello would kick the new peer right back, and two open tabs
85
+ // would fight over the slot forever.
86
+ () => {
87
+ try {
88
+ ws.close(PEER_REPLACED_CLOSE_CODE, PEER_REPLACED_CLOSE_REASON);
89
+ } catch {}
90
+ },
91
+ );
92
+ peerGen = bridge.peerGeneration();
93
+ try {
94
+ ws.send(
95
+ JSON.stringify({
96
+ type: 'hello-ack',
97
+ protocol: 1,
98
+ bridgeVersion: bridge.version,
99
+ }),
100
+ );
101
+ } catch {
102
+ // Socket died between hello and ack — the close handler unwinds.
103
+ }
104
+ return;
105
+ }
106
+ if (!helloed) return;
107
+ bridge.handleSandboxMessage(msg, peerGen);
108
+ });
109
+
110
+ ws.on('close', () => {
111
+ if (disconnect) disconnect();
112
+ disconnect = null;
113
+ if (consumer) consumer.dispose();
114
+ consumer = null;
115
+ });
116
+ ws.on('error', () => {});
117
+ }
118
+
119
+ // ── Worker-relay consumer session (transport-agnostic) ───────────────────
120
+
121
+ /**
122
+ * One attached worker-relay consumer (the Node `connectRemoteSandbox`
123
+ * client). Transport-agnostic — `attachPeer` adapts it onto a `ws` socket;
124
+ * tests drive `handleMessage` directly.
125
+ */
126
+ export interface ConsumerSession {
127
+ /** Handle one parsed message from the consumer. */
128
+ handleMessage(msg: BridgeMessage): void;
129
+ /** Tear down every subscription this consumer registered. */
130
+ dispose(): void;
131
+ }
132
+
133
+ /**
134
+ * Create a consumer session over `send`. Correlation ids/subIds on this leg
135
+ * are CONSUMER-minted and echoed verbatim; the bridge core mints its own ids
136
+ * for the peer leg (`dispatchWorkerOp` / `subscribeWorker`), so the two legs'
137
+ * id spaces never mix.
138
+ */
139
+ export function createConsumerSession(
140
+ bridge: Bridge,
141
+ send: (msg: BridgeMessage) => void,
142
+ ): ConsumerSession {
143
+ /** consumer subId → bridge-side unsubscribe. */
144
+ const subs = new Map<string, () => void>();
145
+
146
+ return {
147
+ handleMessage(msg: BridgeMessage): void {
148
+ switch (msg.type) {
149
+ case 'attach': {
150
+ // Idempotent re-attach: just re-ack. `serveVersion` is the
151
+ // version-skew stamp (bridge/protocol.ts) — this process's own
152
+ // @pyric/cli version, compared client-side at attach.
153
+ send({
154
+ type: 'attach-ack',
155
+ protocol: 1,
156
+ bridgeVersion: bridge.version,
157
+ peerConnected: bridge.isSandboxConnected(),
158
+ serveVersion: cliVersion(),
159
+ });
160
+ return;
161
+ }
162
+ case 'worker-op': {
163
+ bridge.dispatchWorkerOp(msg.op).then(
164
+ (value) => send({ type: 'worker-res', id: msg.id, ok: true, value }),
165
+ (err: Error & { code?: string; denialContext?: unknown }) =>
166
+ send({
167
+ type: 'worker-res',
168
+ id: msg.id,
169
+ ok: false,
170
+ error: {
171
+ code: err.code ?? 'unknown',
172
+ message: err.message,
173
+ // Structured denial context (spike gap 6) — plain JSON,
174
+ // relayed verbatim so the Node side re-attaches it.
175
+ ...(err.denialContext !== undefined ? { denialContext: err.denialContext } : {}),
176
+ },
177
+ }),
178
+ );
179
+ return;
180
+ }
181
+ case 'worker-sub': {
182
+ if (subs.has(msg.subId)) return; // idempotent
183
+ const unsubscribe = bridge.subscribeWorker(msg.sub, (value) =>
184
+ send({ type: 'worker-snap', subId: msg.subId, value }),
185
+ );
186
+ subs.set(msg.subId, unsubscribe);
187
+ return;
188
+ }
189
+ case 'worker-unsub': {
190
+ const unsubscribe = subs.get(msg.subId);
191
+ if (!unsubscribe) return;
192
+ subs.delete(msg.subId);
193
+ unsubscribe();
194
+ return;
195
+ }
196
+ case 'ping': {
197
+ send({ type: 'pong', id: msg.id });
198
+ return;
199
+ }
200
+ default:
201
+ return; // peer-only or unknown frames — ignore
202
+ }
203
+ },
204
+ dispose(): void {
205
+ for (const unsubscribe of subs.values()) unsubscribe();
206
+ subs.clear();
207
+ },
208
+ };
209
+ }
210
+
211
+ export async function collectBody(req: IncomingMessage): Promise<unknown> {
212
+ if (req.method === 'GET' || req.method === 'DELETE') return undefined;
213
+ return await new Promise<unknown>((resolve, reject) => {
214
+ let raw = '';
215
+ req.setEncoding('utf8');
216
+ req.on('data', (chunk) => {
217
+ raw += chunk;
218
+ });
219
+ req.on('end', () => {
220
+ if (!raw) {
221
+ resolve(undefined);
222
+ return;
223
+ }
224
+ try {
225
+ resolve(JSON.parse(raw));
226
+ } catch (err) {
227
+ reject(err);
228
+ }
229
+ });
230
+ req.on('error', reject);
231
+ });
232
+ }
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Standalone bridge server. Spins up an HTTP server on a known
3
+ * loopback port that handles three routes:
4
+ *
5
+ * GET /health — diagnostic
6
+ * POST /mcp — MCP-over-HTTP (StreamableHTTPServerTransport)
7
+ * GET /sandbox (Upgrade) — WebSocket the browser tab connects to
8
+ *
9
+ * Bound to 127.0.0.1 only. Refuses to start if the port is occupied.
10
+ *
11
+ * Premortem fixes baked in:
12
+ * - #A2 — per-session idle timeout (10 min default) + max-session cap.
13
+ * - #U1 — peer connect/disconnect logged; per-call logging behind
14
+ * PYRIC_VERBOSE=1.
15
+ */
16
+
17
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
18
+ import { randomUUID } from 'node:crypto';
19
+ import { WebSocketServer, WebSocket } from 'ws';
20
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
21
+ import { createBridge, type Bridge, type BridgeToolEvent } from './bridge.js';
22
+ import { buildMcpServer } from './mcp.js';
23
+ import { getDefaultMcpToolSurface } from './mcp-contract.js';
24
+ import { createAuditWriter, type AuditWriter } from './audit.js';
25
+ import {
26
+ createConsoleLogger,
27
+ createSilentLogger,
28
+ type BridgeLogger,
29
+ } from './logger.js';
30
+ import {
31
+ DEFAULT_BRIDGE_PORT,
32
+ DEFAULT_HEALTH_PATH,
33
+ DEFAULT_MCP_PATH,
34
+ DEFAULT_SANDBOX_PATH,
35
+ type BridgeMessage,
36
+ isBridgeMessage,
37
+ } from '../protocol.js';
38
+ import { pyricVersion } from '../../serve/standalone-assets.js';
39
+ import { isAllowedUpgrade } from '../../serve/server.js';
40
+
41
+ const BRIDGE_VERSION = pyricVersion();
42
+ const DEFAULT_SESSION_IDLE_MS = 10 * 60 * 1000; // 10 minutes
43
+ const DEFAULT_MAX_SESSIONS = 50;
44
+
45
+ export interface StartServerOptions {
46
+ /** Port to bind. Default: 5174. Env: PYRIC_PORT. */
47
+ port?: number;
48
+ /** Sandbox label surfaced in health and audit metadata. */
49
+ project?: string;
50
+ /** Disable the audit log writer (useful in tests). */
51
+ disableAuditLog?: boolean;
52
+ /** Extra hostnames allowed past the WS-upgrade rebinding/origin guard
53
+ * (besides the loopback set the bridge binds to). Mirrors serve's
54
+ * `--allowed-host`. */
55
+ allowedHosts?: string[];
56
+ /** Override the audit writer (testing). */
57
+ auditWriter?: AuditWriter;
58
+ /** Premortem #A2 — kill idle sessions after this many ms. Default 10 min. */
59
+ sessionIdleMs?: number;
60
+ /** Premortem #A2 — refuse new sessions when this many are active. Default 50. */
61
+ maxSessions?: number;
62
+ /** Premortem #U1 — logger; defaults to stderr `[pyric]` prefix. */
63
+ logger?: BridgeLogger;
64
+ /** Convenience: install a silent logger (tests). */
65
+ silent?: boolean;
66
+
67
+ }
68
+
69
+ export interface ServerHandle {
70
+ readonly bridge: Bridge;
71
+ readonly port: number;
72
+ readonly url: string;
73
+ readonly auditLogPath: string | null;
74
+ stop(): Promise<void>;
75
+ }
76
+
77
+ export async function startServer(
78
+ opts: StartServerOptions = {},
79
+ ): Promise<ServerHandle> {
80
+ // `??` doesn't short-circuit on NaN; Number(undefined) is NaN, which
81
+ // would silently slip through to the http server and produce
82
+ // "options.port should be >= 0 and < 65536. Received NaN."
83
+ // Resolve the env var separately so the default actually wins.
84
+ const envPort = process.env.PYRIC_PORT ? Number(process.env.PYRIC_PORT) : undefined;
85
+ const port = opts.port ?? (Number.isFinite(envPort) ? envPort : undefined) ?? DEFAULT_BRIDGE_PORT;
86
+ const project = opts.project ?? process.env.PYRIC_PROJECT ?? 'sandbox';
87
+ const logger =
88
+ opts.logger ?? (opts.silent ? createSilentLogger() : createConsoleLogger());
89
+ const sessionIdleMs = opts.sessionIdleMs ?? DEFAULT_SESSION_IDLE_MS;
90
+ const maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
91
+
92
+ // Set up audit log writer (best-effort).
93
+ const auditWriter = opts.disableAuditLog
94
+ ? null
95
+ : opts.auditWriter ?? createAuditWriter(project);
96
+
97
+ const bridge = createBridge({
98
+ project,
99
+ version: BRIDGE_VERSION,
100
+ onToolEvent: (event: BridgeToolEvent) => {
101
+ auditWriter?.write(event);
102
+ logger.verbose(
103
+ `tool ${event.tool} → ${event.result.ok ? 'ok' : 'fail'} (${event.durationMs}ms) [${event.mode}]`,
104
+ );
105
+ },
106
+ });
107
+
108
+ const { forwarded, inProcess } = getDefaultMcpToolSurface();
109
+
110
+ // Per-session transport+server map. Each MCP client connection
111
+ // gets its own pair; cleared on DELETE / idle / transport close.
112
+ type Session = {
113
+ transport: StreamableHTTPServerTransport;
114
+ close: () => Promise<void>;
115
+ /** Idle-timeout handle; reset on each request. */
116
+ idleTimer: ReturnType<typeof setTimeout> | null;
117
+ sessionId: string | null;
118
+ };
119
+ const sessions = new Map<string, Session>();
120
+ const pendingSessions = new Set<Session>();
121
+
122
+ function bumpIdle(session: Session): void {
123
+ if (session.idleTimer) clearTimeout(session.idleTimer);
124
+ session.idleTimer = setTimeout(() => {
125
+ if (session.sessionId) {
126
+ logger.verbose(`session ${session.sessionId.slice(0, 8)}… idle-closed`);
127
+ sessions.delete(session.sessionId);
128
+ }
129
+ pendingSessions.delete(session);
130
+ void session.close();
131
+ }, sessionIdleMs);
132
+ }
133
+
134
+ async function newSession(): Promise<Session> {
135
+ if (sessions.size >= maxSessions) {
136
+ const err = new Error(
137
+ `pyric bridge: refusing new session — at session cap (${maxSessions}). Existing sessions: ${sessions.size}.`,
138
+ );
139
+ (err as { statusCode?: number }).statusCode = 503;
140
+ throw err;
141
+ }
142
+ const session: Session = {
143
+ transport: null as unknown as StreamableHTTPServerTransport,
144
+ close: async () => {},
145
+ idleTimer: null,
146
+ sessionId: null,
147
+ };
148
+ session.transport = new StreamableHTTPServerTransport({
149
+ sessionIdGenerator: () => randomUUID(),
150
+ onsessioninitialized: (id) => {
151
+ session.sessionId = id;
152
+ sessions.set(id, session);
153
+ pendingSessions.delete(session);
154
+ bumpIdle(session);
155
+ logger.verbose(`session ${id.slice(0, 8)}… initialized`);
156
+ },
157
+ });
158
+ const server = buildMcpServer(bridge, { forwarded, inProcess });
159
+ session.close = async () => {
160
+ if (session.idleTimer) clearTimeout(session.idleTimer);
161
+ session.idleTimer = null;
162
+ await server.close().catch(() => {});
163
+ await session.transport.close().catch(() => {});
164
+ };
165
+ pendingSessions.add(session);
166
+ await server.connect(session.transport);
167
+ return session;
168
+ }
169
+
170
+ // ── HTTP server ─────────────────────────────────────────────────
171
+ const http = createServer(async (req, res) => {
172
+ const url = req.url ?? '/';
173
+ if (process.env.PYRIC_DEBUG) {
174
+ process.stderr.write(`[pyric debug] ${req.method} ${url}\n`);
175
+ }
176
+ if (url === DEFAULT_HEALTH_PATH && req.method === 'GET') {
177
+ const body = JSON.stringify(bridge.health());
178
+ res.writeHead(200, { 'Content-Type': 'application/json' });
179
+ res.end(body);
180
+ return;
181
+ }
182
+ if (url === DEFAULT_MCP_PATH && (req.method === 'POST' || req.method === 'GET' || req.method === 'DELETE')) {
183
+ try {
184
+ const sessionId = (req.headers['mcp-session-id'] ?? req.headers['Mcp-Session-Id']) as string | undefined;
185
+ let session: Session | undefined;
186
+ if (sessionId && sessions.has(sessionId)) {
187
+ session = sessions.get(sessionId);
188
+ if (session) bumpIdle(session);
189
+ } else {
190
+ session = await newSession();
191
+ }
192
+ if (req.method === 'DELETE' && sessionId) {
193
+ if (session) await session.close();
194
+ sessions.delete(sessionId);
195
+ res.writeHead(204);
196
+ res.end();
197
+ return;
198
+ }
199
+ await session!.transport.handleRequest(
200
+ req as IncomingMessage,
201
+ res as ServerResponse,
202
+ );
203
+ } catch (err) {
204
+ const statusCode = (err as { statusCode?: number })?.statusCode ?? 500;
205
+ const msg = err instanceof Error ? err.stack ?? err.message : String(err);
206
+ logger.error(`MCP transport error: ${msg}`);
207
+ if (!res.headersSent) {
208
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
209
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
210
+ }
211
+ }
212
+ return;
213
+ }
214
+ res.writeHead(404, { 'Content-Type': 'application/json' });
215
+ res.end(JSON.stringify({ error: 'not found', path: url }));
216
+ });
217
+
218
+ // ── WebSocket server (mounted on /sandbox) ──────────────────────
219
+ const wss = new WebSocketServer({ noServer: true });
220
+ http.on('upgrade', (req, socket, head) => {
221
+ if ((req.url ?? '') !== DEFAULT_SANDBOX_PATH) {
222
+ socket.destroy();
223
+ return;
224
+ }
225
+ // DNS-rebinding + cross-origin hijack guard (the request-time isAllowedHost
226
+ // runs on the `request` event only — `upgrade` bypasses it). The standalone
227
+ // bridge binds 127.0.0.1 only, so the allowlist is the loopback set.
228
+ if (!isAllowedUpgrade(req.headers, '127.0.0.1', opts.allowedHosts)) {
229
+ logger.error(
230
+ `refused WS upgrade — Host='${req.headers.host ?? ''}' Origin='${req.headers.origin ?? ''}' (rebinding/origin guard)`,
231
+ );
232
+ socket.destroy();
233
+ return;
234
+ }
235
+ wss.handleUpgrade(req, socket, head, (ws) => {
236
+ attachPeer(bridge, ws, logger);
237
+ });
238
+ });
239
+
240
+ // Bind to loopback only.
241
+ await new Promise<void>((resolve, reject) => {
242
+ http.once('error', reject);
243
+ http.listen(port, '127.0.0.1', () => {
244
+ http.removeListener('error', reject);
245
+ resolve();
246
+ });
247
+ });
248
+
249
+ const address = http.address();
250
+ const boundPort = typeof address === 'object' && address ? address.port : port;
251
+ const url = `http://127.0.0.1:${boundPort}`;
252
+
253
+ logger.info(`bridge ${BRIDGE_VERSION} listening on ${url} — sandbox: ${project}`);
254
+
255
+ return {
256
+ bridge,
257
+ port: boundPort,
258
+ url,
259
+ auditLogPath: auditWriter?.path ?? null,
260
+ async stop() {
261
+ // Stop accepting new WebSocket upgrades…
262
+ wss.close();
263
+ // …and forcibly terminate any existing WS clients. Without this,
264
+ // http.close() below waits for clients to disconnect on their own
265
+ // (MCP keep-alives sit forever — a Ctrl-C with any open peer
266
+ // would hang the CLI shutdown indefinitely).
267
+ for (const client of wss.clients) {
268
+ try { client.terminate(); } catch { /* already closed */ }
269
+ }
270
+ // Destroy any open HTTP keep-alive sockets. Node 18.2+.
271
+ http.closeAllConnections();
272
+ await new Promise<void>((resolve) => {
273
+ http.close(() => resolve());
274
+ });
275
+ for (const session of sessions.values()) {
276
+ await session.close().catch(() => {});
277
+ }
278
+ for (const session of pendingSessions) {
279
+ await session.close().catch(() => {});
280
+ }
281
+ sessions.clear();
282
+ pendingSessions.clear();
283
+ logger.info('bridge stopped');
284
+ },
285
+ };
286
+ }
287
+
288
+ function attachPeer(bridge: Bridge, ws: WebSocket, logger: BridgeLogger): void {
289
+ let disconnect: (() => void) | null = null;
290
+ let helloed = false;
291
+ let sandboxId: string | null = null;
292
+
293
+ ws.on('message', (raw) => {
294
+ let msg: unknown;
295
+ try {
296
+ msg = JSON.parse(raw.toString());
297
+ } catch {
298
+ return; // ignore malformed
299
+ }
300
+ if (!isBridgeMessage(msg)) return;
301
+
302
+ if (msg.type === 'hello') {
303
+ if (helloed) return; // ignore duplicate hellos
304
+ helloed = true;
305
+ sandboxId = msg.sandboxId;
306
+ logger.info(
307
+ `peer connected — sandboxId=${sandboxId.slice(0, 12)} tools=${msg.tools.length}`,
308
+ );
309
+ disconnect = bridge.registerSandboxPeer(
310
+ (out: BridgeMessage) => {
311
+ try {
312
+ ws.send(JSON.stringify(out));
313
+ } catch {
314
+ // socket likely closed; the close handler runs disconnect.
315
+ }
316
+ },
317
+ msg.tools,
318
+ msg.sandboxId,
319
+ );
320
+ ws.send(
321
+ JSON.stringify({
322
+ type: 'hello-ack',
323
+ protocol: 1,
324
+ bridgeVersion: bridge.version,
325
+ }),
326
+ );
327
+ return;
328
+ }
329
+
330
+ if (!helloed) return; // ignore messages before hello
331
+ bridge.handleSandboxMessage(msg);
332
+ });
333
+
334
+ ws.on('close', () => {
335
+ if (disconnect) {
336
+ disconnect();
337
+ logger.info(
338
+ `peer disconnected — sandboxId=${sandboxId ? sandboxId.slice(0, 12) : 'unknown'}`,
339
+ );
340
+ }
341
+ disconnect = null;
342
+ });
343
+
344
+ ws.on('error', () => {
345
+ // ws will follow up with a 'close' event; cleanup happens there.
346
+ });
347
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Extract tool metadata (name + description + parameters) from the
3
+ * existing `pyric/rules` factories without running any
4
+ * tool logic. Used by the bridge to register MCP tools whose actual
5
+ * dispatch happens against the connected browser peer.
6
+ *
7
+ * Why: `createFirestoreSimulatorTools` returns `ToolHandler[]` with
8
+ * `execute` bound to a `LocalEnvironment`. The bridge process doesn't
9
+ * have a `LocalEnvironment` — it forwards. But it does need the
10
+ * metadata to populate the MCP server's tool list. This module calls
11
+ * the factory with a stub resolver and reads only the metadata; the
12
+ * stub never gets invoked because the bridge replaces `execute`
13
+ * before dispatch.
14
+ */
15
+
16
+ import type { ToolHandler } from '@inbrowser/agent';
17
+ import {
18
+ createFirestoreSimulatorTools,
19
+ createFirestoreRulesTools,
20
+ } from 'pyric/rules/internal/node';
21
+ import { createFirestoreDataTools, createFirestoreInspectTools } from 'pyric/firestore';
22
+ import { createRtdbInspectionTools } from '../../rtdb/inspection.js';
23
+ import { createConformanceTools } from '../../conformance/tools.js';
24
+
25
+ export interface ToolMetadata {
26
+ name: string;
27
+ description: string;
28
+ parameters: Record<string, unknown>;
29
+ }
30
+
31
+ function toMetadata(handler: ToolHandler): ToolMetadata {
32
+ return {
33
+ name: handler.name,
34
+ description: handler.description,
35
+ parameters: handler.parameters as Record<string, unknown>,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Tool metadata for the sandbox bridge. Includes the
41
+ * data-plane / sandbox-management tools that the browser dispatches
42
+ * via `LocalEnvironment`. Rules tooling (lint, validate, simulate,
43
+ * test) is NOT included here — those execute in Node and are
44
+ * registered separately, since they don't need the browser sandbox.
45
+ */
46
+ export function getSandboxToolMetadata(): ToolMetadata[] {
47
+ // The resolver stub never runs: the bridge replaces execute() with
48
+ // its own forward-to-peer dispatch.
49
+ const stubResolver = async () => {
50
+ throw new Error(
51
+ 'BUG: sandbox-tool factory executor invoked on the bridge side — should have been replaced',
52
+ );
53
+ };
54
+ const handlers: ToolHandler[] = [
55
+ // Simulator / write / batch / undo / redo / events / transaction —
56
+ // 8 tools that mutate or read the in-browser sandbox state.
57
+ ...createFirestoreSimulatorTools({
58
+ resolveSandbox: stubResolver as never,
59
+ }),
60
+ // Data plane — addDoc / setDoc / getDoc / updateDoc / deleteDoc /
61
+ // collection / query / where — the modular Firestore CRUD surface
62
+ // routed through the browser sandbox so agents can drive a real app
63
+ // session rather than only the simulator.
64
+ ...createFirestoreDataTools({
65
+ resolveDb: stubResolver as never,
66
+ }),
67
+ // `sandbox_inspect` — single-call diagnostic that answers
68
+ // "what state is the sandbox in?" Born out of CLAUDE_DEBUG_SESSION.md
69
+ // (a real agent needing 51 tool calls + 72k tokens to figure out
70
+ // that rules weren't loaded). Routes to the browser sandbox like
71
+ // the other forwarded tools.
72
+ ...createFirestoreInspectTools({
73
+ resolveSandbox: stubResolver as never,
74
+ }),
75
+ ...createRtdbInspectionTools({
76
+ resolveSandbox: stubResolver as never,
77
+ }),
78
+ ];
79
+ return handlers.map(toMetadata);
80
+ }
81
+
82
+ /**
83
+ * Tooling factories that execute in-process on the bridge
84
+ * (no browser needed). Returned as live ToolHandlers — the bridge
85
+ * registers each handler's `execute` directly.
86
+ *
87
+ * `scope` is forwarded so the hosted Rules Test API verification tool can
88
+ * authenticate without changing the bridge's sandbox-only execution model.
89
+ */
90
+ export function getInProcessToolHandlers(scope?: unknown): ToolHandler[] {
91
+ // Factory accepts { scope } per packages/pyric/src/rules/tools.ts.
92
+ // The bridge does not need to own the structurally compatible type.
93
+ // Includes the rules tool surface plus the Node-only conformance query.
94
+ // No browser is needed.
95
+ return [
96
+ ...createFirestoreRulesTools({ scope } as never),
97
+ ...createConformanceTools(),
98
+ ];
99
+ }