@pyric/cli 0.1.0-alpha.18 → 0.1.0-alpha.20

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 (356) hide show
  1. package/README.md +14 -9
  2. package/dist/bridge/server/mcp-contract.d.ts +2 -2
  3. package/dist/bridge/server/mcp-contract.d.ts.map +1 -1
  4. package/dist/bridge/server/mcp-contract.js +3 -0
  5. package/dist/bridge/server/mcp-contract.js.map +1 -1
  6. package/dist/cli/index.d.ts +3 -2
  7. package/dist/cli/index.d.ts.map +1 -1
  8. package/dist/cli/index.js +51 -42
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/mcp-proxy.d.ts.map +1 -1
  11. package/dist/cli/mcp-proxy.js +3 -3
  12. package/dist/cli/mcp-proxy.js.map +1 -1
  13. package/dist/cli/parse-args.d.ts +1 -1
  14. package/dist/cli/parse-args.d.ts.map +1 -1
  15. package/dist/cli/parse-args.js +51 -15
  16. package/dist/cli/parse-args.js.map +1 -1
  17. package/dist/cli/pyric-config.d.ts +24 -0
  18. package/dist/cli/pyric-config.d.ts.map +1 -0
  19. package/dist/cli/pyric-config.js +28 -0
  20. package/dist/cli/pyric-config.js.map +1 -0
  21. package/dist/cli/rules.d.ts +1 -0
  22. package/dist/cli/rules.d.ts.map +1 -1
  23. package/dist/cli/rules.js +18 -4
  24. package/dist/cli/rules.js.map +1 -1
  25. package/dist/cli/{dev-runner.d.ts → sandbox-runner.d.ts} +33 -36
  26. package/dist/cli/sandbox-runner.d.ts.map +1 -0
  27. package/dist/cli/{dev-runner.js → sandbox-runner.js} +90 -55
  28. package/dist/cli/sandbox-runner.js.map +1 -0
  29. package/dist/cli/serve.d.ts +6 -4
  30. package/dist/cli/serve.d.ts.map +1 -1
  31. package/dist/cli/serve.js +99 -40
  32. package/dist/cli/serve.js.map +1 -1
  33. package/dist/cli/service-command-records/storage-rules-resolve.d.ts +4 -0
  34. package/dist/cli/service-command-records/storage-rules-resolve.d.ts.map +1 -0
  35. package/dist/cli/service-command-records/storage-rules-resolve.js +3 -0
  36. package/dist/cli/service-command-records/storage-rules-resolve.js.map +1 -0
  37. package/dist/cli/service-commands.generated.d.ts +3 -0
  38. package/dist/cli/service-commands.generated.d.ts.map +1 -1
  39. package/dist/cli/service-commands.generated.js +2 -0
  40. package/dist/cli/service-commands.generated.js.map +1 -1
  41. package/dist/cli/snapshot.js +5 -5
  42. package/dist/cli/snapshot.js.map +1 -1
  43. package/dist/cli/verify.js +1 -1
  44. package/dist/cli/verify.js.map +1 -1
  45. package/dist/functions-rtdb/development-runtime.js +1 -1
  46. package/dist/functions-rtdb/development-runtime.js.map +1 -1
  47. package/dist/functions-rtdb/project.js +8 -8
  48. package/dist/functions-rtdb/project.js.map +1 -1
  49. package/dist/next/guard.js +1 -1
  50. package/dist/next/guard.js.map +1 -1
  51. package/dist/remote/index.d.ts +1 -1
  52. package/dist/remote/index.js +9 -9
  53. package/dist/remote/index.js.map +1 -1
  54. package/dist/serve/bundler.js +10 -10
  55. package/dist/serve/bundler.js.map +1 -1
  56. package/dist/serve/entries/ai.d.ts.map +1 -1
  57. package/dist/serve/entries/ai.js +27 -1
  58. package/dist/serve/entries/ai.js.map +1 -1
  59. package/dist/serve/entries/auth-helper-dom.js +1 -1
  60. package/dist/serve/entries/auth-helper-dom.js.map +1 -1
  61. package/dist/serve/entries/auth-helper-runtime.js +1 -1
  62. package/dist/serve/entries/auth-helper-runtime.js.map +1 -1
  63. package/dist/serve/entries/firestore.js +3 -3
  64. package/dist/serve/entries/firestore.js.map +1 -1
  65. package/dist/serve/entries/keepalive.js +1 -1
  66. package/dist/serve/entries/keepalive.js.map +1 -1
  67. package/dist/serve/entries/runtime.js +21 -14
  68. package/dist/serve/entries/runtime.js.map +1 -1
  69. package/dist/serve/entries/storage.js +1 -1
  70. package/dist/serve/entries/storage.js.map +1 -1
  71. package/dist/serve/entries/worker-runtime.js +1 -1
  72. package/dist/serve/entries/worker-runtime.js.map +1 -1
  73. package/dist/serve/init-payload.d.ts +4 -0
  74. package/dist/serve/init-payload.d.ts.map +1 -1
  75. package/dist/serve/namespace.d.ts +6 -0
  76. package/dist/serve/namespace.d.ts.map +1 -1
  77. package/dist/serve/namespace.js +36 -5
  78. package/dist/serve/namespace.js.map +1 -1
  79. package/dist/serve/rules.d.ts +3 -3
  80. package/dist/serve/rules.d.ts.map +1 -1
  81. package/dist/serve/rules.js +56 -25
  82. package/dist/serve/rules.js.map +1 -1
  83. package/dist/serve/runtime/chip.d.ts.map +1 -1
  84. package/dist/serve/runtime/chip.js +32 -1
  85. package/dist/serve/runtime/chip.js.map +1 -1
  86. package/dist/serve/runtime/manifest.d.ts +1 -1
  87. package/dist/serve/runtime/manifest.d.ts.map +1 -1
  88. package/dist/serve/runtime/manifest.js +1 -1
  89. package/dist/serve/runtime/manifest.js.map +1 -1
  90. package/dist/serve/runtime/status.d.ts.map +1 -1
  91. package/dist/serve/runtime/status.js +23 -0
  92. package/dist/serve/runtime/status.js.map +1 -1
  93. package/dist/serve/sandbox-session.d.ts +1 -0
  94. package/dist/serve/sandbox-session.d.ts.map +1 -1
  95. package/dist/serve/sandbox-session.js +10 -4
  96. package/dist/serve/sandbox-session.js.map +1 -1
  97. package/dist/serve/server.d.ts +7 -0
  98. package/dist/serve/server.d.ts.map +1 -1
  99. package/dist/serve/server.js +15 -1
  100. package/dist/serve/server.js.map +1 -1
  101. package/dist/serve/site-ui/404.html +1 -1
  102. package/dist/serve/site-ui/_astro/{RulesCodeEditor.CzwHdCi9.js → RulesCodeEditor.XtDmHOlN.js} +1 -1
  103. package/dist/serve/site-ui/_astro/_example_.B9DG9HLJ.css +1 -0
  104. package/dist/serve/site-ui/_astro/_slug_.Da5VLhW1.css +1 -0
  105. package/dist/serve/site-ui/_astro/_studio_.BnfYCKoI.css +1 -0
  106. package/dist/serve/site-ui/_astro/app.B52xr-DK.js +1 -0
  107. package/dist/serve/site-ui/_astro/app.DWBraPUw.js +12 -0
  108. package/dist/serve/site-ui/_astro/app.pMSnN6Qw.css +1 -0
  109. package/dist/serve/site-ui/_astro/chess-showcase.B72XYeoh.js +354 -0
  110. package/dist/serve/site-ui/_astro/{example-runtime.B0byNQOl.js → example-runtime.C25_lx6Z.js} +2 -26
  111. package/dist/serve/site-ui/_astro/index.Tb-31efc.css +1 -0
  112. package/dist/serve/site-ui/_astro/index.UkCBr-_0.css +1 -0
  113. package/dist/serve/site-ui/_astro/sandbox-controls.DEpnKg-O.js +1169 -0
  114. package/dist/serve/site-ui/_astro/seed.Co0yUJY5.js +20 -0
  115. package/dist/serve/site-ui/_astro/writes.rpjrFzmu.js +102 -0
  116. package/dist/serve/site-ui/auth/index.html +5 -3
  117. package/dist/serve/site-ui/david-east.jpg +0 -0
  118. package/dist/serve/site-ui/docs/_rhythm/index.html +6 -3
  119. package/dist/serve/site-ui/docs/_rhythm.md +1 -1
  120. package/dist/serve/site-ui/docs/agent/set-up-your-agent/index.html +45 -17
  121. package/dist/serve/site-ui/docs/agent/set-up-your-agent.md +20 -17
  122. package/dist/serve/site-ui/docs/agent/watch-and-review/index.html +6 -5
  123. package/dist/serve/site-ui/docs/agent/watch-and-review.md +1 -5
  124. package/dist/serve/site-ui/docs/agent/work-with-an-agent/index.html +8 -5
  125. package/dist/serve/site-ui/docs/agent/work-with-an-agent.md +4 -4
  126. package/dist/serve/site-ui/docs/ai-compat/index.html +5 -2
  127. package/dist/serve/site-ui/docs/api-reference/index.html +6 -2
  128. package/dist/serve/site-ui/docs/api-reference.md +1 -0
  129. package/dist/serve/site-ui/docs/app-compat/index.html +5 -2
  130. package/dist/serve/site-ui/docs/auth-compat/index.html +5 -2
  131. package/dist/serve/site-ui/docs/build/ai-logic/index.html +5 -2
  132. package/dist/serve/site-ui/docs/build/authentication/index.html +7 -4
  133. package/dist/serve/site-ui/docs/build/authentication.md +2 -2
  134. package/dist/serve/site-ui/docs/build/cloud-firestore/index.html +34 -53
  135. package/dist/serve/site-ui/docs/build/cloud-firestore.md +40 -36
  136. package/dist/serve/site-ui/docs/build/cloud-messaging/index.html +5 -2
  137. package/dist/serve/site-ui/docs/build/cloud-storage/index.html +6 -3
  138. package/dist/serve/site-ui/docs/build/cloud-storage.md +1 -1
  139. package/dist/serve/site-ui/docs/build/realtime-database/index.html +9 -6
  140. package/dist/serve/site-ui/docs/build/realtime-database.md +4 -4
  141. package/dist/serve/site-ui/docs/conformance-scores/index.html +5 -2
  142. package/dist/serve/site-ui/docs/create-pyric-reference-api/index.html +6 -3
  143. package/dist/serve/site-ui/docs/create-pyric-reference-api.md +1 -1
  144. package/dist/serve/site-ui/docs/database-compat/index.html +5 -2
  145. package/dist/serve/site-ui/docs/examples/chess/index.html +649 -250
  146. package/dist/serve/site-ui/docs/examples/chess.md +45 -16
  147. package/dist/serve/site-ui/docs/examples/index.html +6 -3
  148. package/dist/serve/site-ui/docs/firestore-compat/index.html +5 -2
  149. package/dist/serve/site-ui/docs/functions-rtdb-compat/index.html +5 -2
  150. package/dist/serve/site-ui/docs/get-started/how-the-swap-works/index.html +9 -6
  151. package/dist/serve/site-ui/docs/get-started/how-the-swap-works.md +5 -5
  152. package/dist/serve/site-ui/docs/get-started/nextjs/index.html +9 -6
  153. package/dist/serve/site-ui/docs/get-started/nextjs.md +4 -4
  154. package/dist/serve/site-ui/docs/get-started/start-building/index.html +41 -10
  155. package/dist/serve/site-ui/docs/get-started/start-building.md +21 -10
  156. package/dist/serve/site-ui/docs/get-started/vite/index.html +6 -3
  157. package/dist/serve/site-ui/docs/get-started/vite.md +1 -1
  158. package/dist/serve/site-ui/docs/index.json +203 -168
  159. package/dist/serve/site-ui/docs/messaging-compat/index.html +5 -2
  160. package/dist/serve/site-ui/docs/observe/resolve-runtime-status/index.html +5 -2
  161. package/dist/serve/site-ui/docs/observe/see-whats-happening/index.html +6 -4
  162. package/dist/serve/site-ui/docs/observe/see-whats-happening.md +2 -4
  163. package/dist/serve/site-ui/docs/observe/shape-your-data/index.html +9 -6
  164. package/dist/serve/site-ui/docs/observe/shape-your-data.md +4 -4
  165. package/dist/serve/site-ui/docs/overview/index.html +16 -10
  166. package/dist/serve/site-ui/docs/overview.md +19 -11
  167. package/dist/serve/site-ui/docs/pyric-admin-app-reference-api/index.html +5 -2
  168. package/dist/serve/site-ui/docs/pyric-admin-auth-reference-api/index.html +5 -2
  169. package/dist/serve/site-ui/docs/pyric-admin-database-reference-api/index.html +5 -2
  170. package/dist/serve/site-ui/docs/pyric-admin-firestore-reference-api/index.html +8 -5
  171. package/dist/serve/site-ui/docs/pyric-admin-firestore-reference-api.md +3 -3
  172. package/dist/serve/site-ui/docs/pyric-admin-messaging-reference-api/index.html +5 -2
  173. package/dist/serve/site-ui/docs/pyric-admin-reference-api/index.html +501 -0
  174. package/dist/serve/site-ui/docs/pyric-admin-reference-api.md +587 -0
  175. package/dist/serve/site-ui/docs/pyric-admin-storage-reference-api/index.html +5 -2
  176. package/dist/serve/site-ui/docs/pyric-ai-reference-api/index.html +5 -2
  177. package/dist/serve/site-ui/docs/pyric-ai-scripting-reference-api/index.html +5 -2
  178. package/dist/serve/site-ui/docs/pyric-app-reference-api/index.html +5 -2
  179. package/dist/serve/site-ui/docs/pyric-auth-reference-api/index.html +5 -2
  180. package/dist/serve/site-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +5 -2
  181. package/dist/serve/site-ui/docs/pyric-cli-assurance-reference-api/index.html +5 -2
  182. package/dist/serve/site-ui/docs/pyric-cli-bridge-client-reference-api/index.html +5 -2
  183. package/dist/serve/site-ui/docs/pyric-cli-bridge-reference-api/index.html +5 -2
  184. package/dist/serve/site-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +5 -2
  185. package/dist/serve/site-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +5 -2
  186. package/dist/serve/site-ui/docs/pyric-cli-conformance-reference-api/index.html +5 -2
  187. package/dist/serve/site-ui/docs/pyric-cli-credentials-node-reference-api/index.html +5 -2
  188. package/dist/serve/site-ui/docs/pyric-cli-discover-reference-api/index.html +5 -2
  189. package/dist/serve/site-ui/docs/pyric-cli-next-reference-api/index.html +5 -2
  190. package/dist/serve/site-ui/docs/pyric-cli-register-app-bridge-reference-api/index.html +5 -2
  191. package/dist/serve/site-ui/docs/pyric-cli-register-reference-api/index.html +5 -2
  192. package/dist/serve/site-ui/docs/pyric-cli-remote-reference-api/index.html +6 -3
  193. package/dist/serve/site-ui/docs/pyric-cli-remote-reference-api.md +1 -1
  194. package/dist/serve/site-ui/docs/pyric-cli-serve-worker-reference-api/index.html +5 -2
  195. package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api/index.html +5 -2
  196. package/dist/serve/site-ui/docs/pyric-cli-vite-reference-api/index.html +5 -2
  197. package/dist/serve/site-ui/docs/pyric-database-reference-api/index.html +166 -95
  198. package/dist/serve/site-ui/docs/pyric-database-reference-api.md +107 -1
  199. package/dist/serve/site-ui/docs/pyric-firestore-reference-api/index.html +5 -2
  200. package/dist/serve/site-ui/docs/pyric-messaging-reference-api/index.html +10 -4
  201. package/dist/serve/site-ui/docs/pyric-messaging-reference-api.md +5 -2
  202. package/dist/serve/site-ui/docs/pyric-messaging-sw-reference-api/index.html +10 -4
  203. package/dist/serve/site-ui/docs/pyric-messaging-sw-reference-api.md +5 -2
  204. package/dist/serve/site-ui/docs/pyric-rules-reference-api/index.html +5 -2
  205. package/dist/serve/site-ui/docs/pyric-sandbox-database-reference-api/index.html +5 -2
  206. package/dist/serve/site-ui/docs/pyric-sandbox-firestore-reference-api/index.html +6 -3
  207. package/dist/serve/site-ui/docs/pyric-sandbox-firestore-reference-api.md +2 -2
  208. package/dist/serve/site-ui/docs/pyric-sandbox-reference-api/index.html +5 -2
  209. package/dist/serve/site-ui/docs/pyric-storage-reference-api/index.html +5 -2
  210. package/dist/serve/site-ui/docs/pyric-ui-agents-reference-api/index.html +5 -2
  211. package/dist/serve/site-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +5 -2
  212. package/dist/serve/site-ui/docs/pyric-ui-auth-reference-api/index.html +5 -2
  213. package/dist/serve/site-ui/docs/pyric-ui-events-hooks-reference-api/index.html +5 -2
  214. package/dist/serve/site-ui/docs/pyric-ui-events-reference-api/index.html +5 -2
  215. package/dist/serve/site-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +5 -2
  216. package/dist/serve/site-ui/docs/pyric-ui-firestore-reference-api/index.html +5 -2
  217. package/dist/serve/site-ui/docs/pyric-ui-primitives-reference-api/index.html +5 -2
  218. package/dist/serve/site-ui/docs/pyric-ui-rtdb-reference-api/index.html +5 -2
  219. package/dist/serve/site-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +5 -2
  220. package/dist/serve/site-ui/docs/pyric-ui-rules-reference-api/index.html +5 -2
  221. package/dist/serve/site-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +5 -2
  222. package/dist/serve/site-ui/docs/pyric-ui-storage-reference-api/index.html +5 -2
  223. package/dist/serve/site-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +5 -2
  224. package/dist/serve/site-ui/docs/pyric-ui-traffic-reference-api/index.html +5 -2
  225. package/dist/serve/site-ui/docs/reference/cli/index.html +153 -0
  226. package/dist/serve/site-ui/docs/reference/cli.md +114 -0
  227. package/dist/serve/site-ui/docs/rules-compat/index.html +5 -2
  228. package/dist/serve/site-ui/docs/secure/audit-your-rules/index.html +5 -2
  229. package/dist/serve/site-ui/docs/secure/firestore-rules-limits/index.html +5 -2
  230. package/dist/serve/site-ui/docs/secure/read-a-denial/index.html +6 -3
  231. package/dist/serve/site-ui/docs/secure/read-a-denial.md +1 -1
  232. package/dist/serve/site-ui/docs/secure/rtdb-rules-in-typescript/index.html +5 -2
  233. package/dist/serve/site-ui/docs/secure/rules-standard-library/index.html +5 -2
  234. package/dist/serve/site-ui/docs/secure/secure-it-with-rules/index.html +5 -2
  235. package/dist/serve/site-ui/docs/secure/simulate-and-lint/index.html +5 -2
  236. package/dist/serve/site-ui/docs/secure/write-a-rules-test-suite/index.html +5 -2
  237. package/dist/serve/site-ui/docs/ship/ship-to-production/index.html +7 -4
  238. package/dist/serve/site-ui/docs/ship/ship-to-production.md +2 -2
  239. package/dist/serve/site-ui/docs/ship/test-in-node/index.html +9 -6
  240. package/dist/serve/site-ui/docs/ship/test-in-node.md +4 -4
  241. package/dist/serve/site-ui/docs/storage-compat/index.html +5 -2
  242. package/dist/serve/site-ui/docs/trust/how-we-know-it-matches-firebase/index.html +5 -2
  243. package/dist/serve/site-ui/docs/trust/rules-standard-library/index.html +5 -2
  244. package/dist/serve/site-ui/docs/trust/versioning-and-compatibility/index.html +5 -2
  245. package/dist/serve/site-ui/docs/tutorial/index.html +10 -7
  246. package/dist/serve/site-ui/docs/tutorial.md +4 -4
  247. package/dist/serve/site-ui/examples/chess/index.html +4 -2
  248. package/dist/serve/site-ui/firestore/index.html +5 -3
  249. package/dist/serve/site-ui/hero-charcoal.jpg +0 -0
  250. package/dist/serve/site-ui/index.html +9 -7
  251. package/dist/serve/site-ui/llms.txt +4 -3
  252. package/dist/serve/site-ui/pyric-campfire-full.png +0 -0
  253. package/dist/serve/site-ui/pyric-campfire.png +0 -0
  254. package/dist/serve/site-ui/rtdb/index.html +5 -3
  255. package/dist/serve/site-ui/settings/index.html +5 -3
  256. package/dist/serve/site-ui/storage/index.html +5 -3
  257. package/dist/serve/site-ui/studio/index.html +13 -0
  258. package/dist/serve/site-ui/studio-routes.json +1 -1
  259. package/dist/serve/site-ui/traffic/index.html +5 -3
  260. package/dist/serve/studio/routes.d.ts +9 -0
  261. package/dist/serve/studio/routes.d.ts.map +1 -1
  262. package/dist/serve/studio/routes.js +47 -7
  263. package/dist/serve/studio/routes.js.map +1 -1
  264. package/dist/serve/vite-ai-config.d.ts.map +1 -1
  265. package/dist/serve/vite-ai-config.js +38 -8
  266. package/dist/serve/vite-ai-config.js.map +1 -1
  267. package/dist/serve/vite-module-swap.d.ts.map +1 -1
  268. package/dist/serve/vite-module-swap.js +0 -28
  269. package/dist/serve/vite-module-swap.js.map +1 -1
  270. package/dist/serve/vite-rules-source.d.ts.map +1 -1
  271. package/dist/serve/vite-rules-source.js +28 -7
  272. package/dist/serve/vite-rules-source.js.map +1 -1
  273. package/dist/serve/vite-sandbox-generation.d.ts +1 -1
  274. package/dist/serve/vite-sandbox-generation.d.ts.map +1 -1
  275. package/dist/serve/vite-sandbox-generation.js +1 -1
  276. package/dist/serve/vite-sandbox-generation.js.map +1 -1
  277. package/dist/serve/worker/client/disconnect.js +1 -1
  278. package/dist/serve/worker/client/disconnect.js.map +1 -1
  279. package/dist/serve/worker/client/presence.d.ts.map +1 -1
  280. package/dist/serve/worker/client/presence.js +18 -5
  281. package/dist/serve/worker/client/presence.js.map +1 -1
  282. package/dist/serve/worker/host-ai.d.ts.map +1 -1
  283. package/dist/serve/worker/host-ai.js +15 -0
  284. package/dist/serve/worker/host-ai.js.map +1 -1
  285. package/dist/serve/worker/protocol.d.ts +4 -0
  286. package/dist/serve/worker/protocol.d.ts.map +1 -1
  287. package/dist/serve/worker/protocol.js.map +1 -1
  288. package/dist/serve/worker/serve-init.d.ts.map +1 -1
  289. package/dist/serve/worker/serve-init.js +7 -1
  290. package/dist/serve/worker/serve-init.js.map +1 -1
  291. package/package.json +5 -5
  292. package/src/bridge/server/mcp-contract.ts +3 -0
  293. package/src/cli/cli.test.ts +2 -2
  294. package/src/cli/index.ts +51 -42
  295. package/src/cli/mcp-proxy.ts +10 -11
  296. package/src/cli/parse-args.ts +61 -15
  297. package/src/cli/pyric-config.ts +45 -0
  298. package/src/cli/rules.ts +31 -4
  299. package/src/cli/{dev-runner.ts → sandbox-runner.ts} +117 -72
  300. package/src/cli/serve.ts +115 -47
  301. package/src/cli/service-command-records/storage-rules-resolve.ts +4 -0
  302. package/src/cli/service-commands.generated.ts +2 -0
  303. package/src/cli/snapshot.ts +5 -5
  304. package/src/cli/verify.ts +1 -1
  305. package/src/functions-rtdb/development-runtime.ts +1 -1
  306. package/src/functions-rtdb/project.ts +8 -8
  307. package/src/next/guard.ts +1 -1
  308. package/src/remote/index.ts +9 -9
  309. package/src/serve/bundler.ts +10 -10
  310. package/src/serve/entries/ai.ts +27 -1
  311. package/src/serve/entries/auth-helper-dom.ts +1 -1
  312. package/src/serve/entries/auth-helper-runtime.ts +1 -1
  313. package/src/serve/entries/firestore.ts +3 -3
  314. package/src/serve/entries/keepalive.ts +1 -1
  315. package/src/serve/entries/runtime.ts +19 -14
  316. package/src/serve/entries/storage.ts +1 -1
  317. package/src/serve/entries/worker-runtime.ts +1 -1
  318. package/src/serve/init-payload.ts +4 -0
  319. package/src/serve/namespace.ts +44 -5
  320. package/src/serve/rules.ts +61 -26
  321. package/src/serve/runtime/chip.ts +38 -1
  322. package/src/serve/runtime/manifest.ts +1 -1
  323. package/src/serve/runtime/status.ts +24 -0
  324. package/src/serve/sandbox-session.ts +10 -4
  325. package/src/serve/server.ts +18 -1
  326. package/src/serve/studio/routes.ts +63 -7
  327. package/src/serve/vite-ai-config.ts +37 -7
  328. package/src/serve/vite-module-swap.ts +0 -28
  329. package/src/serve/vite-rules-source.ts +29 -7
  330. package/src/serve/vite-sandbox-generation.ts +1 -1
  331. package/src/serve/worker/client/disconnect.ts +1 -1
  332. package/src/serve/worker/client/presence.ts +25 -5
  333. package/src/serve/worker/host-ai.ts +15 -0
  334. package/src/serve/worker/protocol.ts +2 -1
  335. package/src/serve/worker/serve-init.ts +5 -1
  336. package/README.md.orig +0 -141
  337. package/dist/cli/dev-runner.d.ts.map +0 -1
  338. package/dist/cli/dev-runner.js.map +0 -1
  339. package/dist/serve/site-ui/_astro/_example_.jevZ7OdV.css +0 -1
  340. package/dist/serve/site-ui/_astro/_slug_.C6QfVOxJ.css +0 -1
  341. package/dist/serve/site-ui/_astro/_slug_.CXh9yTP1.css +0 -1
  342. package/dist/serve/site-ui/_astro/_studio_.B6b_G1ev.css +0 -1
  343. package/dist/serve/site-ui/_astro/app.B7pBzz5N.js +0 -11
  344. package/dist/serve/site-ui/_astro/app.BsFsMjn0.css +0 -1
  345. package/dist/serve/site-ui/_astro/app.boex5SnQ.js +0 -1
  346. package/dist/serve/site-ui/_astro/chess-showcase.DDqE-QGe.js +0 -1
  347. package/dist/serve/site-ui/_astro/chess-source-explorer.C5k9c5yK.js +0 -1
  348. package/dist/serve/site-ui/_astro/chess-v2.CFHmd3UL.js +0 -354
  349. package/dist/serve/site-ui/_astro/index.uwPvjH_1.js +0 -1
  350. package/dist/serve/site-ui/_astro/jsx-runtime.D_zvdyIk.js +0 -9
  351. package/dist/serve/site-ui/_astro/reads.Dh3z0fJy.js +0 -1
  352. package/dist/serve/site-ui/_astro/sandbox-controls.C2IqtoSw.js +0 -1252
  353. package/dist/serve/site-ui/_astro/seed.CCmj00su.js +0 -20
  354. package/dist/serve/site-ui/docs/trust/storage-rules-stdlib-research/index.html +0 -582
  355. package/dist/serve/site-ui/docs/trust/storage-rules-stdlib-research.md +0 -448
  356. package/dist/serve/site-ui/examples/firestore-first-write/index.html +0 -2
@@ -18,7 +18,7 @@ import { collectBody } from '../bridge/server/peer.js';
18
18
  import { StateFileError, type StateSection, type StateStore } from './state-store.js';
19
19
  import { createWriterLock, type WriterLock } from './writer-lock.js';
20
20
  import { createStudioRoutes, type StudioRouteOptions } from './studio/index.js';
21
- import { pipeFileToResponse, type ServeLogger } from './server.js';
21
+ import { pipeFileToResponse, getHeader, isAllowedHost, isAllowedOrigin, type ServeLogger } from './server.js';
22
22
  import type { InitPayload } from './init-payload.js';
23
23
  import { handleActivity } from './activity-route.js';
24
24
  import { createSiteTreeHandler } from './site-tree.js';
@@ -107,6 +107,12 @@ export interface NamespaceOptions {
107
107
  * (disk-backed `WorkspaceStore`/`ProjectStore`, plus the SSE watch stream)
108
108
  * that `@pyric/studio`'s `local` mode talks to. */
109
109
  studio?: StudioRouteOptions;
110
+ /** Per-boot session capability token required on workspace and project endpoints. */
111
+ sessionToken?: string;
112
+ /** Bound host for DNS rebinding guard. */
113
+ boundHost?: string;
114
+ /** Extra allowed hostnames. */
115
+ allowedHosts?: string[];
110
116
  /** The OpenAI-compatible upstream `/__pyric/ai-proxy` forwards to
111
117
  * (pyric/ai — cdd-deltas #98.2). Falls back to the
112
118
  * `PYRIC_AI_PROXY_UPSTREAM` env var, then `http://localhost:11434/v1`
@@ -471,8 +477,20 @@ async function handleDenials(
471
477
  }
472
478
 
473
479
  export function createPyricNamespace(opts: NamespaceOptions) {
474
- const writerLock = createWriterLock();
475
- const studioRoutes = opts.studio ? createStudioRoutes(opts.studio) : null;
480
+ const stateWriterLock = createWriterLock();
481
+ const studioWriterLock = opts.studio?.writerLock ?? createWriterLock();
482
+ const sessionToken = opts.sessionToken ?? randomBytes(24).toString('base64url');
483
+ let studioRoutes: ((req: IncomingMessage, res: ServerResponse, url: URL) => Promise<boolean>) | null = null;
484
+ if (opts.studio) {
485
+ const studioOptions: StudioRouteOptions = {
486
+ ...opts.studio,
487
+ sessionToken,
488
+ writerLock: studioWriterLock,
489
+ boundHost: opts.boundHost,
490
+ allowedHosts: opts.allowedHosts,
491
+ };
492
+ studioRoutes = createStudioRoutes(studioOptions);
493
+ }
476
494
  const siteTree = opts.siteUiDir
477
495
  ? createSiteTreeHandler(opts.siteUiDir, opts.workerVersion)
478
496
  : null;
@@ -489,7 +507,7 @@ export function createPyricNamespace(opts: NamespaceOptions) {
489
507
  return studioRoutes(req, res, url);
490
508
  }
491
509
  if (opts.state && url.pathname === '/__pyric/state') {
492
- return handleState(opts.state!, writerLock, req, res, url).then(() => true);
510
+ return handleState(opts.state!, stateWriterLock, req, res, url).then(() => true);
493
511
  }
494
512
  if (opts.capture && url.pathname === '/__pyric/capture') {
495
513
  return handleCapture(opts.capture, req, res).then(() => true);
@@ -498,6 +516,21 @@ export function createPyricNamespace(opts: NamespaceOptions) {
498
516
  return handleActivity(opts.activity, req, res, activityToken!).then(() => true);
499
517
  }
500
518
  if (opts.events && url.pathname === '/__pyric/events') {
519
+ const hostHeader = getHeader(req, 'host');
520
+ const originHeader = getHeader(req, 'origin');
521
+ if (!isAllowedHost(hostHeader, opts.boundHost ?? 'localhost', opts.allowedHosts)) {
522
+ res.writeHead(403, { 'content-type': 'text/plain' }).end('Forbidden: host not allowed');
523
+ return true;
524
+ }
525
+ if (originHeader && !isAllowedOrigin(originHeader, opts.boundHost ?? 'localhost', opts.allowedHosts)) {
526
+ res.writeHead(403, { 'content-type': 'text/plain' }).end('Forbidden: origin mismatch');
527
+ return true;
528
+ }
529
+ const reqToken = getHeader(req, 'x-pyric-session-token') ?? url.searchParams.get('token');
530
+ if (sessionToken && reqToken && reqToken !== sessionToken) {
531
+ res.writeHead(401, { 'content-type': 'text/plain' }).end('Unauthorized: invalid session capability token');
532
+ return true;
533
+ }
501
534
  opts.events.handle(req, res);
502
535
  return true;
503
536
  }
@@ -509,7 +542,13 @@ export function createPyricNamespace(opts: NamespaceOptions) {
509
542
  }
510
543
  if (url.pathname === '/__pyric/init.json') {
511
544
  res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
512
- res.end(JSON.stringify({ ...opts.initPayload(), ...(activityToken ? { activityToken } : {}) }));
545
+ res.end(
546
+ JSON.stringify({
547
+ ...opts.initPayload(),
548
+ sessionToken,
549
+ ...(activityToken ? { activityToken } : {}),
550
+ }),
551
+ );
513
552
  return true;
514
553
  }
515
554
  if (url.pathname.startsWith('/__pyric/sdk/')) {
@@ -56,7 +56,7 @@ export function prepareRulesSource(raw: string, sourcePath: string): string {
56
56
  const resolved = resolveModulesBrowser(raw);
57
57
  if (!resolved.success) {
58
58
  throw new Error(
59
- `pyric dev: ${sourcePath} uses 2+modules but module resolution failed: ${resolved.error.message}`,
59
+ `pyric sandbox: ${sourcePath} uses 2+modules but module resolution failed: ${resolved.error.message}`,
60
60
  );
61
61
  }
62
62
  source = resolved.data.resolved;
@@ -65,13 +65,13 @@ export function prepareRulesSource(raw: string, sourcePath: string): string {
65
65
  if (lint.parseError) {
66
66
  const { line, column } = lint.parseError;
67
67
  throw new Error(
68
- `pyric dev: ${sourcePath} failed to parse (line ${line}, col ${column}) fix the rules before serving.`,
68
+ `pyric sandbox: ${sourcePath} failed to parse (line ${line}, col ${column}). Fix the rules before serving.`,
69
69
  );
70
70
  }
71
71
  const errors = lint.warnings.filter((w) => w.severity === 'error');
72
72
  if (errors.length > 0) {
73
73
  throw new Error(
74
- `pyric dev: ${sourcePath} has ${errors.length} rules error(s):\n` +
74
+ `pyric sandbox: ${sourcePath} has ${errors.length} rules error(s):\n` +
75
75
  errors.map((e) => ` - ${e.message}`).join('\n'),
76
76
  );
77
77
  }
@@ -98,7 +98,7 @@ export async function loadProjectRules(
98
98
  } catch (e) {
99
99
  if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
100
100
  if (configured) {
101
- throw new Error(`pyric dev: firebase.json points firestore.rules at ${path}, but it does not exist.`);
101
+ throw new Error(`pyric sandbox: firebase.json points firestore.rules at ${path}, but it does not exist.`);
102
102
  }
103
103
  return { rules: null, rulesHash: null, sourcePath: null };
104
104
  }
@@ -109,19 +109,29 @@ export async function loadProjectRules(
109
109
  }
110
110
 
111
111
  /**
112
- * Validate a raw storage-rules source. `pyric/storage`'s parser throws a
113
- * plain `SyntaxError` (no line/col like the firestore lint path) — wrap it
114
- * into the same actionable "fix before serving" message shape.
112
+ * Resolve and validate a raw Storage rules source. Modular sources are
113
+ * authored in storage.modules.rules and resolved in memory before the
114
+ * sandbox sees them; firebase.json can continue to point at storage.rules.
115
115
  */
116
116
  export function prepareStorageRulesSource(raw: string, sourcePath: string): string {
117
+ let source = raw;
118
+ if (/^\s*rules_version\s*=\s*['"]2\+modules['"]/m.test(raw)) {
119
+ const resolved = resolveModulesBrowser(raw, { sourceFile: sourcePath });
120
+ if (!resolved.success) {
121
+ throw new Error(
122
+ `pyric sandbox: ${sourcePath} uses 2+modules but module resolution failed: ${resolved.error.message}`,
123
+ );
124
+ }
125
+ source = resolved.data.resolved;
126
+ }
117
127
  try {
118
- parseStorageRules(raw);
128
+ parseStorageRules(source);
119
129
  } catch (e) {
120
130
  throw new Error(
121
- `pyric dev: ${sourcePath} failed to parse: ${e instanceof Error ? e.message : String(e)} fix the rules before serving.`,
131
+ `pyric sandbox: ${sourcePath} failed to parse: ${e instanceof Error ? e.message : String(e)}. Fix the rules before serving.`,
122
132
  );
123
133
  }
124
- return raw;
134
+ return source;
125
135
  }
126
136
 
127
137
  /**
@@ -149,7 +159,7 @@ export async function loadProjectStorageRules(
149
159
  } catch (e) {
150
160
  if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
151
161
  if (configured) {
152
- throw new Error(`pyric dev: firebase.json points storage.rules at ${path}, but it does not exist.`);
162
+ throw new Error(`pyric sandbox: firebase.json points storage.rules at ${path}, but it does not exist.`);
153
163
  }
154
164
  return { rules: null, rulesHash: null, sourcePath: null };
155
165
  }
@@ -159,27 +169,52 @@ export async function loadProjectStorageRules(
159
169
  return { rules, rulesHash: rulesHashOf(rules), sourcePath: path };
160
170
  }
161
171
 
172
+ function normalizeDatabaseEntries(block: unknown): Array<Record<string, unknown>> {
173
+ if (!block) {
174
+ return [];
175
+ }
176
+ if (Array.isArray(block)) {
177
+ return block.filter((item): item is Record<string, unknown> => typeof item === 'object' && item !== null);
178
+ }
179
+ if (typeof block === 'object' && block !== null) {
180
+ return [block as Record<string, unknown>];
181
+ }
182
+ return [];
183
+ }
184
+
185
+ function hasConfiguredRules(entry: Record<string, unknown>): entry is Record<string, unknown> & { rules: string } {
186
+ return typeof entry.rules === 'string';
187
+ }
188
+
189
+ function hasConfiguredUrl(entry: Record<string, unknown>): entry is Record<string, unknown> & { url: string } {
190
+ return typeof entry.url === 'string';
191
+ }
192
+
162
193
  export async function loadProjectDatabaseRules(
163
194
  cwd: string,
164
195
  config: FirebaseJson | null,
165
196
  ): Promise<LoadedDatabaseRules> {
166
- const configured = config?.database?.rules;
167
- if (!configured) {
168
- return {
169
- rules: null,
170
- rulesHash: null,
171
- sourcePath: null,
172
- databaseUrl: config?.database?.url ?? null,
173
- };
174
- }
175
-
176
- const path = isAbsolute(configured) ? configured : join(cwd, configured);
197
+ const entries = normalizeDatabaseEntries(config?.database);
198
+ const configuredEntry = entries.find(hasConfiguredRules);
199
+ const configured = configuredEntry?.rules;
200
+ const rel = configured ?? 'database.rules.json';
201
+ const path = isAbsolute(rel) ? rel : join(cwd, rel);
202
+ const urlEntry = entries.find(hasConfiguredUrl);
203
+ const databaseUrl = urlEntry?.url ?? null;
177
204
  let raw: string;
178
205
  try {
179
206
  raw = await readFile(path, 'utf8');
180
207
  } catch (e) {
181
208
  if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
182
- throw new Error(`pyric dev: firebase.json points database.rules at ${path}, but it does not exist.`);
209
+ if (configured) {
210
+ throw new Error(`pyric sandbox: firebase.json points database.rules at ${path}, but it does not exist.`);
211
+ }
212
+ return {
213
+ rules: null,
214
+ rulesHash: null,
215
+ sourcePath: null,
216
+ databaseUrl,
217
+ };
183
218
  }
184
219
  throw e;
185
220
  }
@@ -188,18 +223,18 @@ export async function loadProjectDatabaseRules(
188
223
  try {
189
224
  parsed = JSON.parse(stripJsonComments(raw));
190
225
  } catch (e) {
191
- throw new Error(`pyric dev: ${path} failed to parse as RTDB rules JSON: ${e instanceof Error ? e.message : String(e)}`);
226
+ throw new Error(`pyric sandbox: ${path} failed to parse as RTDB rules JSON: ${e instanceof Error ? e.message : String(e)}`);
192
227
  }
193
228
  const rules = parseRtdbRulesJson(
194
229
  parsed,
195
- () => new Error(`pyric dev: ${path} must contain a top-level "rules" object.`),
230
+ () => new Error(`pyric sandbox: ${path} must contain a top-level "rules" object.`),
196
231
  );
197
232
 
198
233
  return {
199
234
  rules,
200
235
  rulesHash: rulesHashOf(raw),
201
236
  sourcePath: path,
202
- databaseUrl: config?.database?.url ?? null,
237
+ databaseUrl,
203
238
  };
204
239
  }
205
240
 
@@ -18,6 +18,32 @@ export interface PyricRuntimeChip {
18
18
  dispose(): void;
19
19
  }
20
20
 
21
+ interface AiEngineDisplay {
22
+ primary: string;
23
+ subline: string | null;
24
+ }
25
+
26
+ function aiEngineState(): AiEngineDisplay {
27
+ const engine = (globalThis as { __PYRIC_AI_ENGINE__?: { kind?: string; model?: string } }).__PYRIC_AI_ENGINE__;
28
+ if (engine?.kind === 'gemini') {
29
+ return {
30
+ primary: 'gemini (production)',
31
+ subline: 'gemini-3.5-flash-lite → gemini-flash-lite-latest',
32
+ };
33
+ }
34
+ if (engine?.kind === 'openai') {
35
+ const modelLabel = engine.model ? ` (${engine.model})` : '';
36
+ return {
37
+ primary: `openai (proxy${modelLabel})`,
38
+ subline: null,
39
+ };
40
+ }
41
+ return {
42
+ primary: 'sandbox (scripted)',
43
+ subline: null,
44
+ };
45
+ }
46
+
21
47
  const styles = `
22
48
  :host {
23
49
  --pyric-bg: #1e1e24;
@@ -86,8 +112,11 @@ const styles = `
86
112
  .icon-button[data-copy-failed] { color: var(--pyric-error); }
87
113
  .icon { height: 15px; width: 15px; }
88
114
  .worker-state { border-top: 1px solid var(--pyric-border-soft); color: var(--pyric-muted); font-size: 10px; justify-content: space-between; min-height: 34px; padding: 7px 12px; }
115
+ .worker-state-col { border-top: 1px solid var(--pyric-border-soft); color: var(--pyric-muted); font-size: 10px; display: flex; flex-direction: column; min-height: 34px; padding: 7px 12px; }
116
+ .worker-state-row { align-items: center; display: flex; justify-content: space-between; width: 100%; }
117
+ .worker-state-subline { color: #89899f; font: 9px/1.4 ui-monospace, monospace; margin-top: 4px; overflow-wrap: anywhere; text-align: right; width: 100%; }
89
118
  .worker-state .available { color: var(--pyric-warning); }
90
- .worker-state .state-label { align-items: center; display: flex; gap: 7px; }
119
+ .worker-state .state-label, .worker-state-col .state-label { align-items: center; display: flex; gap: 7px; white-space: nowrap; }
91
120
  .mini-dot { background: var(--pyric-accent); border-radius: 50%; height: 6px; width: 6px; }
92
121
  .available .mini-dot { background: var(--pyric-warning); }
93
122
  .epochs { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -203,6 +232,7 @@ export function mountPyricRuntimeChip(options: PyricRuntimeChipOptions): PyricRu
203
232
  const epochs = snapshot.updateAvailable
204
233
  ? `${snapshot.runningEpoch?.slice(0, 8) ?? 'unknown'} → ${snapshot.servedEpoch?.slice(0, 8) ?? 'unknown'}`
205
234
  : snapshot.runningEpoch?.slice(0, 8) ?? '';
235
+ const aiState = aiEngineState();
206
236
 
207
237
  view.innerHTML = `${open ? `
208
238
  <section class="panel" role="dialog" aria-label="Pyric runtime">
@@ -214,6 +244,13 @@ export function mountPyricRuntimeChip(options: PyricRuntimeChipOptions): PyricRu
214
244
  </div>
215
245
  </header>
216
246
  <div class="worker-state"><span class="state-label${snapshot.updateAvailable ? ' available' : ''}"><span class="mini-dot"></span>${workerLabel}</span><span class="epochs">${epochs}</span></div>
247
+ <div class="worker-state-col" data-ai-status>
248
+ <div class="worker-state-row">
249
+ <span class="state-label"><span class="mini-dot"></span>AI engine</span>
250
+ <span class="epochs">${aiState.primary}</span>
251
+ </div>
252
+ ${aiState.subline ? `<div class="worker-state-subline">${aiState.subline}</div>` : ''}
253
+ </div>
217
254
  <div class="errors" data-error-viewport>${renderErrors(snapshot, Boolean(clipboard))}</div>
218
255
  <div class="actions">
219
256
  <button class="button update" type="button" data-update-worker ${snapshot.updateAvailable ? '' : 'disabled'} aria-disabled="${snapshot.updateAvailable && !snapshot.updatingWorker ? 'false' : 'true'}">${snapshot.updatingWorker ? 'Updating…' : 'Update worker'}</button>
@@ -1,7 +1,7 @@
1
1
  /** Stable routes and identity used by the served runtime and its UI. */
2
2
  export const PYRIC_WORKER_URL = '/__pyric/sdk/worker.js';
3
3
  export const PYRIC_WORKER_NAME = 'pyric-shared-worker';
4
- export const PYRIC_STUDIO_URL = '/__pyric/ui/';
4
+ export const PYRIC_STUDIO_URL = '/__pyric/ui/studio';
5
5
 
6
6
  export interface PyricRuntimeManifest {
7
7
  studioUrl: string;
@@ -108,6 +108,13 @@ function isListenerPhaseErroredEvent(event: SandboxEvent): boolean {
108
108
  return isListenerKind && isErroredPhase && hasErrorPayload;
109
109
  }
110
110
 
111
+ function isAiRejectedEvent(event: SandboxEvent): boolean {
112
+ const isServiceMutationKind = event.kind === 'service_mutation';
113
+ const isAiService = event.service === 'ai';
114
+ const isRejectedOp = 'op' in event && event.op === 'request_rejected';
115
+ return isServiceMutationKind && isAiService && isRejectedOp;
116
+ }
117
+
111
118
  function sandboxError(event: SandboxEvent): PyricRuntimeError | null {
112
119
  if (isRuntimeErrorEvent(event)) {
113
120
  const hasPathProperty = 'path' in event && typeof event.path === 'string';
@@ -127,6 +134,23 @@ function sandboxError(event: SandboxEvent): PyricRuntimeError | null {
127
134
  message: errorPayload?.message ? errorPayload.message : 'Sandbox runtime error',
128
135
  };
129
136
  }
137
+ if (isAiRejectedEvent(event)) {
138
+ const detail = 'detail' in event ? (event.detail as Record<string, unknown> | undefined) : undefined;
139
+ const code = typeof detail?.code === 'string' ? detail.code : 'AI_ERROR';
140
+ const message = typeof detail?.message === 'string' ? detail.message : 'AI request rejected';
141
+ const hasPathProperty = 'path' in event && typeof event.path === 'string';
142
+ const targetPath = hasPathProperty ? event.path : '(ai)';
143
+ return {
144
+ id: event.id,
145
+ source: 'sandbox',
146
+ at: event.at,
147
+ service: 'ai',
148
+ method: 'generateContent',
149
+ path: targetPath,
150
+ code,
151
+ message: `AI_ERROR: ${message}`,
152
+ };
153
+ }
130
154
  if (isListenerErroredEvent(event)) {
131
155
  const errorPayload = (event as { error: { code?: string; message: string } }).error;
132
156
  return {
@@ -35,6 +35,7 @@ export interface SandboxSessionOptions {
35
35
  bridgeUrl?: () => string | null;
36
36
  ai?: InitPayload['ai'];
37
37
  aiProxyUpstream?: string;
38
+ permissive?: boolean;
38
39
  logger?: ServeLogger;
39
40
  activity?: (incident: ActivityIncident) => void;
40
41
  }
@@ -97,6 +98,13 @@ export async function createSandboxSession(
97
98
  databaseRules: database.rules,
98
99
  databaseRulesHash: database.rulesHash,
99
100
  };
101
+ if (!database.sourcePath) {
102
+ if (options.permissive) {
103
+ options.logger?.note(' ⓘ RTDB permissive mode active — client reads/writes are open by default.');
104
+ } else {
105
+ options.logger?.note(' ⚠ no database.rules.json found — client RTDB reads/writes default to DENY (matching production Firebase). Use --permissive for open prototyping.');
106
+ }
107
+ }
100
108
  const events = createEventHub();
101
109
  const capture: CaptureStore | undefined = (options.capture ?? true)
102
110
  ? createCaptureStore(options.projectDir)
@@ -164,6 +172,7 @@ export async function createSandboxSession(
164
172
  : seedUsers,
165
173
  messaging: true,
166
174
  ai: options.ai ?? null,
175
+ permissive: Boolean(options.permissive),
167
176
  });
168
177
 
169
178
  const summary: SandboxSessionSummary = {
@@ -221,16 +230,13 @@ export async function createSandboxSession(
221
230
  }
222
231
  };
223
232
  const reloadDatabaseRules = async (): Promise<RulesReloadResult> => {
224
- const isMissingDatabaseRules = database.sourcePath === null;
225
- if (isMissingDatabaseRules) {
226
- return { kind: 'not-configured' };
227
- }
228
233
  try {
229
234
  const updated = await loadProjectDatabaseRules(options.projectDir, options.firebaseConfig);
230
235
  const isMissingUpdatedRules = updated.rules === null || updated.rulesHash === null;
231
236
  if (isMissingUpdatedRules) {
232
237
  return { kind: 'not-configured' };
233
238
  }
239
+ database.sourcePath = updated.sourcePath;
234
240
  live.databaseRules = updated.rules;
235
241
  live.databaseRulesHash = updated.rulesHash;
236
242
  events.broadcast('rtdb-rules-update', { rules: updated.rules, rulesHash: updated.rulesHash });
@@ -134,6 +134,23 @@ export function isAllowedUpgrade(
134
134
  );
135
135
  }
136
136
 
137
+ /**
138
+ * Extract a header value case-insensitively from either Node's IncomingMessage headers
139
+ * or Fetch API Headers object.
140
+ */
141
+ export function getHeader(
142
+ req: { headers?: Record<string, string | string[] | undefined> | Headers },
143
+ name: string,
144
+ ): string | undefined {
145
+ const headers = req?.headers;
146
+ if (!headers) return undefined;
147
+ if (typeof (headers as Headers).get === 'function') {
148
+ return (headers as Headers).get(name) ?? undefined;
149
+ }
150
+ const val = (headers as Record<string, string | string[] | undefined>)[name.toLowerCase()];
151
+ return Array.isArray(val) ? val.join(', ') : val;
152
+ }
153
+
137
154
  export interface ServeHandle {
138
155
  port: number;
139
156
  host: string;
@@ -249,7 +266,7 @@ async function handleRequest(
249
266
  if (!isAllowedHost(req.headers.host, opts.host ?? 'localhost', opts.allowedHosts)) {
250
267
  logger.note(` ✖ 403 blocked Host '${req.headers.host}' (DNS-rebinding guard)`);
251
268
  res.writeHead(403, { 'content-type': 'text/plain' }).end(
252
- `pyric dev: refused request for Host '${req.headers.host ?? ''}'. ` +
269
+ `pyric sandbox: refused request for Host '${req.headers.host ?? ''}'. ` +
253
270
  'Only localhost is allowed; use --allowed-host to add one.',
254
271
  );
255
272
  return;
@@ -33,12 +33,22 @@ import type {
33
33
  WorkspaceChange,
34
34
  WorkspaceStore,
35
35
  } from './store-types.js';
36
+ import { isAllowedHost, isAllowedOrigin, getHeader } from '../server.js';
37
+ import type { WriterLock } from '../writer-lock.js';
36
38
 
37
39
  export interface StudioRouteOptions {
38
40
  /** The single-project file tree served at `/__pyric/workspace`. */
39
41
  workspace?: WorkspaceStore;
40
42
  /** The project list served at `/__pyric/projects`. */
41
43
  projects?: ProjectStore;
44
+ /** Per-boot session capability token required on workspace & project endpoints. */
45
+ sessionToken?: string;
46
+ /** Single-writer lock validator for file mutation & deletion endpoints. */
47
+ writerLock?: WriterLock;
48
+ /** Bound host for DNS rebinding guard. */
49
+ boundHost?: string;
50
+ /** Extra hostnames allowed for DNS rebinding/origin guard. */
51
+ allowedHosts?: string[];
42
52
  }
43
53
 
44
54
  /** Collect the raw request body as a UTF-8 string. */
@@ -207,6 +217,10 @@ async function handleProjects(
207
217
  .end('method not allowed');
208
218
  }
209
219
 
220
+ function isWorkspaceMutation(method?: string): boolean {
221
+ return method === 'PUT' || method === 'POST' || method === 'PATCH' || method === 'DELETE';
222
+ }
223
+
210
224
  /**
211
225
  * Build a namespace fragment for the Studio storage routes. Returns a handler
212
226
  * shaped like the other `/__pyric/*` fragments: it resolves `true` when it
@@ -219,13 +233,55 @@ export function createStudioRoutes(opts: StudioRouteOptions) {
219
233
  url: URL,
220
234
  ): Promise<boolean> => {
221
235
  try {
222
- if (opts.workspace && url.pathname.startsWith('/__pyric/workspace')) {
223
- await handleWorkspace(opts.workspace, req, res, url);
224
- return true;
225
- }
226
- if (opts.projects && url.pathname.startsWith('/__pyric/projects')) {
227
- await handleProjects(opts.projects, req, res, url);
228
- return true;
236
+ const isWorkspaceRoute = opts.workspace && url.pathname.startsWith('/__pyric/workspace');
237
+ const isProjectsRoute = opts.projects && url.pathname.startsWith('/__pyric/projects');
238
+
239
+ if (isWorkspaceRoute || isProjectsRoute) {
240
+ const hostHeader = getHeader(req, 'host');
241
+ const originHeader = getHeader(req, 'origin');
242
+
243
+ // 1. Host and Origin guard (Requirement 1)
244
+ if (!isAllowedHost(hostHeader, opts.boundHost ?? 'localhost', opts.allowedHosts)) {
245
+ res.writeHead(403, { 'content-type': 'text/plain' }).end('Forbidden: host not allowed');
246
+ return true;
247
+ }
248
+ if (originHeader && !isAllowedOrigin(originHeader, opts.boundHost ?? 'localhost', opts.allowedHosts)) {
249
+ res.writeHead(403, { 'content-type': 'text/plain' }).end('Forbidden: origin mismatch');
250
+ return true;
251
+ }
252
+
253
+ // 2. Per-boot session capability token guard (Requirement 2)
254
+ if (opts.sessionToken) {
255
+ const reqToken =
256
+ getHeader(req, 'x-pyric-session-token') ??
257
+ url.searchParams.get('token');
258
+ if (!reqToken || reqToken !== opts.sessionToken) {
259
+ res.writeHead(401, { 'content-type': 'text/plain' }).end('Unauthorized: invalid session capability token');
260
+ return true;
261
+ }
262
+ }
263
+
264
+ // 3. Single-writer lock guard for mutation endpoints (Requirement 4)
265
+ if (opts.writerLock && isWorkspaceMutation(req.method)) {
266
+ const writerId = getHeader(req, 'x-pyric-writer');
267
+ if (!writerId) {
268
+ res.writeHead(423, { 'content-type': 'text/plain' }).end('Locked: missing active writer lock header');
269
+ return true;
270
+ }
271
+ if (!opts.writerLock.claim(writerId, Date.now())) {
272
+ res.writeHead(423, { 'content-type': 'text/plain' }).end('Locked: another tab holds the writer lock');
273
+ return true;
274
+ }
275
+ }
276
+
277
+ if (isWorkspaceRoute) {
278
+ await handleWorkspace(opts.workspace!, req, res, url);
279
+ return true;
280
+ }
281
+ if (isProjectsRoute) {
282
+ await handleProjects(opts.projects!, req, res, url);
283
+ return true;
284
+ }
229
285
  }
230
286
  } catch (e) {
231
287
  if (!res.headersSent) sendError(res, e);
@@ -44,7 +44,18 @@ export function loadViteAiEnv(
44
44
  const resolvedEnvDir = envDir === undefined
45
45
  ? resolvedRoot
46
46
  : path.resolve(resolvedRoot, envDir);
47
- return loadEnv(mode, resolvedEnvDir, '');
47
+ const loaded = loadEnv(mode, resolvedEnvDir, '');
48
+ for (const [key, val] of Object.entries(loaded)) {
49
+ const isAuthKey =
50
+ key === 'GEMINI_API_KEY' ||
51
+ key === 'GOOGLE_GENAI_API_KEY' ||
52
+ key === 'VITE_GEMINI_API_KEY' ||
53
+ key === 'PYRIC_API_KEY';
54
+ if (isAuthKey && process.env[key] === undefined) {
55
+ process.env[key] = val;
56
+ }
57
+ }
58
+ return loaded;
48
59
  }
49
60
 
50
61
  /** Convert a public declarative engine into the JSON-safe worker wire shape. */
@@ -65,6 +76,21 @@ export function engineConfigToWire(engine: PyricAiEngineConfig): AiEngineConfigW
65
76
  }
66
77
  return result as AiEngineConfigWire;
67
78
  }
79
+ const isGeminiEngine = engine.kind === 'gemini';
80
+ if (isGeminiEngine) {
81
+ const result: Record<string, unknown> = {
82
+ kind: 'gemini',
83
+ };
84
+ const hasBaseUrl = engine.baseUrl !== undefined;
85
+ if (hasBaseUrl) {
86
+ result.baseUrl = engine.baseUrl;
87
+ }
88
+ const hasApiKey = engine.apiKey !== undefined;
89
+ if (hasApiKey) {
90
+ result.apiKey = engine.apiKey;
91
+ }
92
+ return result as AiEngineConfigWire;
93
+ }
68
94
  const result: Record<string, unknown> = {
69
95
  kind: 'scripted',
70
96
  };
@@ -123,12 +149,16 @@ export function resolveViteAiConfig(
123
149
  }
124
150
 
125
151
  let engineWire: AiEngineConfigWire | undefined = undefined;
126
- if (!isProductionMode) {
127
- if (explicitEngine !== undefined) {
128
- engineWire = engineConfigToWire(explicitEngine);
129
- } else if (model !== undefined) {
130
- engineWire = engineConfigToWire({ kind: 'openai', baseUrl: AI_PROXY_PATH, model });
131
- }
152
+ if (isProductionMode) {
153
+ const apiKey = env.GEMINI_API_KEY ?? env.GOOGLE_GENAI_API_KEY ?? env.VITE_GEMINI_API_KEY;
154
+ engineWire = {
155
+ kind: 'gemini',
156
+ ...(apiKey !== undefined && apiKey.trim() !== '' ? { apiKey: apiKey.trim() } : {}),
157
+ };
158
+ } else if (explicitEngine !== undefined) {
159
+ engineWire = engineConfigToWire(explicitEngine);
160
+ } else if (model !== undefined) {
161
+ engineWire = engineConfigToWire({ kind: 'openai', baseUrl: AI_PROXY_PATH, model });
132
162
  }
133
163
 
134
164
  let proxyUpstream: string | undefined = undefined;
@@ -86,20 +86,6 @@ export function createViteModuleSwap(
86
86
 
87
87
  const match = FIREBASE_SPECIFIER.exec(args.path);
88
88
  const subpath = match !== null && match[1] !== undefined ? match[1] : '';
89
- const isProductionAiMode = aiMode() === 'production';
90
- if (isProductionAiMode) {
91
- const isAiSubpath = subpath === 'ai';
92
- if (isAiSubpath) {
93
- return null;
94
- }
95
- const isAppSubpath = subpath === 'app';
96
- const passthroughEntry = entries['app-ai-passthrough'];
97
- const hasPassthroughEntry = passthroughEntry !== undefined;
98
- const shouldUsePassthroughBridge = isAppSubpath && hasPassthroughEntry;
99
- if (shouldUsePassthroughBridge) {
100
- return { path: passthroughEntry };
101
- }
102
- }
103
89
 
104
90
  const isServedSubpath = SERVED_FIREBASE_SUBPATHS.has(subpath);
105
91
  if (isServedSubpath) {
@@ -173,20 +159,6 @@ export function createViteModuleSwap(
173
159
  const isFirebaseSpecifier = firebaseMatch !== null;
174
160
  if (isFirebaseSpecifier) {
175
161
  const subpath = firebaseMatch[1] !== undefined ? firebaseMatch[1] : '';
176
- const isProductionAiMode = aiMode() === 'production';
177
- if (isProductionAiMode) {
178
- const isAiSubpath = subpath === 'ai';
179
- if (isAiSubpath) {
180
- return null;
181
- }
182
- const isAppSubpath = subpath === 'app';
183
- const passthroughEntry = entries['app-ai-passthrough'];
184
- const hasPassthroughEntry = passthroughEntry !== undefined;
185
- const shouldUsePassthroughBridge = isAppSubpath && hasPassthroughEntry;
186
- if (shouldUsePassthroughBridge) {
187
- return passthroughEntry;
188
- }
189
- }
190
162
  const isServedSubpath = SERVED_FIREBASE_SUBPATHS.has(subpath);
191
163
  if (isServedSubpath) {
192
164
  const key = entryKey(subpath);