@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.
- package/dist/cli/serve.js +4 -4
- package/dist/cli/serve.js.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +2 -2
- package/dist/conformance/.generated/can-i-use.js.map +1 -1
- package/dist/serve/bundler.d.ts +13 -8
- package/dist/serve/bundler.d.ts.map +1 -1
- package/dist/serve/bundler.js +31 -36
- package/dist/serve/bundler.js.map +1 -1
- package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
- package/dist/serve/docs-ui/docs/api-reference.md +1 -1
- package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/ai-logic/index.html +44 -22
- package/dist/serve/docs-ui/docs/build/ai-logic.md +69 -21
- package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
- package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
- package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +2 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works.md +2 -0
- package/dist/serve/docs-ui/docs/get-started/start-building/index.html +3 -2
- package/dist/serve/docs-ui/docs/get-started/start-building.md +3 -1
- package/dist/serve/docs-ui/docs/index.json +349 -29
- package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/resolve-runtime-status/index.html +28 -0
- package/dist/serve/docs-ui/docs/observe/resolve-runtime-status.md +47 -0
- package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
- package/dist/serve/docs-ui/docs/overview/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
- package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +168 -76
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +156 -73
- package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +11 -8
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api.md +17 -4
- package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
- package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
- package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
- package/dist/serve/docs-ui/docs/tutorial/index.html +429 -0
- package/dist/serve/docs-ui/docs/tutorial.md +701 -0
- package/dist/serve/docs-ui/llms.txt +3 -1
- package/dist/serve/entries/init.d.ts.map +1 -1
- package/dist/serve/entries/init.js +3 -0
- package/dist/serve/entries/init.js.map +1 -1
- package/dist/serve/entries/runtime.d.ts.map +1 -1
- package/dist/serve/entries/runtime.js +20 -5
- package/dist/serve/entries/runtime.js.map +1 -1
- package/dist/serve/entries/worker-runtime.d.ts +2 -2
- package/dist/serve/entries/worker-runtime.d.ts.map +1 -1
- package/dist/serve/entries/worker-runtime.js +45 -10
- package/dist/serve/entries/worker-runtime.js.map +1 -1
- package/dist/serve/runtime/chip-config.d.ts +19 -0
- package/dist/serve/runtime/chip-config.d.ts.map +1 -0
- package/dist/serve/runtime/chip-config.js +19 -0
- package/dist/serve/runtime/chip-config.js.map +1 -0
- package/dist/serve/runtime/chip-install.d.ts +10 -0
- package/dist/serve/runtime/chip-install.d.ts.map +1 -0
- package/dist/serve/runtime/chip-install.js +15 -0
- package/dist/serve/runtime/chip-install.js.map +1 -0
- package/dist/serve/runtime/chip.d.ts +17 -0
- package/dist/serve/runtime/chip.d.ts.map +1 -0
- package/dist/serve/runtime/chip.js +257 -0
- package/dist/serve/runtime/chip.js.map +1 -0
- package/dist/serve/runtime/manifest.d.ts +22 -0
- package/dist/serve/runtime/manifest.d.ts.map +1 -0
- package/dist/serve/runtime/manifest.js +20 -0
- package/dist/serve/runtime/manifest.js.map +1 -0
- package/dist/serve/runtime/status.d.ts +49 -0
- package/dist/serve/runtime/status.d.ts.map +1 -0
- package/dist/serve/runtime/status.js +164 -0
- package/dist/serve/runtime/status.js.map +1 -0
- package/dist/serve/runtime/worker-connection.d.ts +4 -0
- package/dist/serve/runtime/worker-connection.d.ts.map +1 -0
- package/dist/serve/runtime/worker-connection.js +11 -0
- package/dist/serve/runtime/worker-connection.js.map +1 -0
- package/dist/serve/runtime/worker-generation.d.ts +14 -0
- package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
- package/dist/serve/runtime/worker-generation.js +56 -0
- package/dist/serve/runtime/worker-generation.js.map +1 -0
- package/dist/serve/runtime/worker-replacement.d.ts +18 -0
- package/dist/serve/runtime/worker-replacement.d.ts.map +1 -0
- package/dist/serve/runtime/worker-replacement.js +29 -0
- package/dist/serve/runtime/worker-replacement.js.map +1 -0
- package/dist/serve/standalone-assets.d.ts +2 -2
- package/dist/serve/standalone-assets.d.ts.map +1 -1
- package/dist/serve/standalone-assets.js +1 -1
- package/dist/serve/standalone-assets.js.map +1 -1
- package/dist/serve/studio-ui/assets/{RulesCodeEditor-CxVYcZXw.js → RulesCodeEditor-BDlH-5lp.js} +1 -1
- package/dist/serve/studio-ui/assets/{index-cpSfgQ7H.js → index-Bf9SSE8L.js} +5 -5
- package/dist/serve/studio-ui/assets/{seed-DeuFKi4J.js → seed-D_UjxfOU.js} +1 -1
- package/dist/serve/studio-ui/index.html +1 -1
- package/dist/serve/vite-ai-config.d.ts +25 -0
- package/dist/serve/vite-ai-config.d.ts.map +1 -0
- package/dist/serve/vite-ai-config.js +45 -0
- package/dist/serve/vite-ai-config.js.map +1 -0
- package/dist/serve/vite-plugin.d.ts +17 -36
- package/dist/serve/vite-plugin.d.ts.map +1 -1
- package/dist/serve/vite-plugin.js +38 -57
- package/dist/serve/vite-plugin.js.map +1 -1
- package/dist/serve/vite-rules-source.d.ts +8 -0
- package/dist/serve/vite-rules-source.d.ts.map +1 -0
- package/dist/serve/vite-rules-source.js +23 -0
- package/dist/serve/vite-rules-source.js.map +1 -0
- package/dist/serve/vite-worker-runtime.d.ts +20 -0
- package/dist/serve/vite-worker-runtime.d.ts.map +1 -0
- package/dist/serve/vite-worker-runtime.js +25 -0
- package/dist/serve/vite-worker-runtime.js.map +1 -0
- package/dist/serve/worker/client/connection.d.ts +7 -2
- package/dist/serve/worker/client/connection.d.ts.map +1 -1
- package/dist/serve/worker/client/connection.js +13 -16
- package/dist/serve/worker/client/connection.js.map +1 -1
- package/dist/serve/worker/client/core.d.ts +7 -0
- package/dist/serve/worker/client/core.d.ts.map +1 -1
- package/dist/serve/worker/client/core.js +28 -0
- package/dist/serve/worker/client/core.js.map +1 -1
- package/dist/serve/worker/client/runtime-control.d.ts +8 -0
- package/dist/serve/worker/client/runtime-control.d.ts.map +1 -0
- package/dist/serve/worker/client/runtime-control.js +11 -0
- package/dist/serve/worker/client/runtime-control.js.map +1 -0
- package/dist/serve/worker/entry.js +44 -2
- package/dist/serve/worker/entry.js.map +1 -1
- package/dist/serve/worker/host/core.js +1 -1
- package/dist/serve/worker/host/core.js.map +1 -1
- package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
- package/dist/serve/worker/host/firestore-writes.js +3 -2
- package/dist/serve/worker/host/firestore-writes.js.map +1 -1
- package/dist/serve/worker/host/studio.js +1 -1
- package/dist/serve/worker/host/studio.js.map +1 -1
- package/dist/serve/worker/host-context.d.ts +1 -1
- package/dist/serve/worker/host-context.d.ts.map +1 -1
- package/dist/serve/worker/index.d.ts +2 -1
- package/dist/serve/worker/index.d.ts.map +1 -1
- package/dist/serve/worker/index.js +2 -1
- package/dist/serve/worker/index.js.map +1 -1
- package/dist/serve/worker/protocol.d.ts +15 -1
- package/dist/serve/worker/protocol.d.ts.map +1 -1
- package/dist/serve/worker/protocol.js +2 -2
- package/dist/serve/worker/protocol.js.map +1 -1
- package/dist/serve/worker/retirement.d.ts +21 -0
- package/dist/serve/worker/retirement.d.ts.map +1 -0
- package/dist/serve/worker/retirement.js +97 -0
- package/dist/serve/worker/retirement.js.map +1 -0
- package/dist/serve/worker/serve-init.d.ts.map +1 -1
- package/dist/serve/worker/serve-init.js +5 -6
- package/dist/serve/worker/serve-init.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js.map +1 -1
- package/package.json +6 -4
- package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
- package/src/assurance/attachment.ts +211 -0
- package/src/assurance/browser.ts +80 -0
- package/src/assurance/campaign.ts +496 -0
- package/src/assurance/capabilities.ts +446 -0
- package/src/assurance/cases.ts +43 -0
- package/src/assurance/index.ts +75 -0
- package/src/assurance/runner.ts +885 -0
- package/src/assurance/tool-names.ts +15 -0
- package/src/assurance/tools.ts +830 -0
- package/src/assurance/types.ts +352 -0
- package/src/assurance/validation.ts +434 -0
- package/src/bridge/client/bridge.ts +550 -0
- package/src/bridge/client/dispatch.ts +137 -0
- package/src/bridge/client.ts +45 -0
- package/src/bridge/protocol.ts +351 -0
- package/src/bridge/server/audit.ts +53 -0
- package/src/bridge/server/bridge.ts +597 -0
- package/src/bridge/server/headless.ts +160 -0
- package/src/bridge/server/json-schema-to-zod.ts +108 -0
- package/src/bridge/server/local-bridge.ts +66 -0
- package/src/bridge/server/logger.ts +50 -0
- package/src/bridge/server/mcp-contract.ts +93 -0
- package/src/bridge/server/mcp.ts +129 -0
- package/src/bridge/server/peer.ts +232 -0
- package/src/bridge/server/standalone.ts +347 -0
- package/src/bridge/server/tool-metadata.ts +99 -0
- package/src/bridge/server.ts +28 -0
- package/src/cli/can-i-use.ts +50 -0
- package/src/cli/cli.test.ts +493 -0
- package/src/cli/database-rules.ts +329 -0
- package/src/cli/dev-runner.ts +282 -0
- package/src/cli/firebase-json.ts +100 -0
- package/src/cli/firestore-indexes.ts +70 -0
- package/src/cli/index.ts +425 -0
- package/src/cli/init.ts +260 -0
- package/src/cli/mcp-proxy.ts +196 -0
- package/src/cli/parse-args.ts +80 -0
- package/src/cli/rules.ts +259 -0
- package/src/cli/scope.ts +67 -0
- package/src/cli/serve.ts +996 -0
- package/src/cli/service-commands.ts +73 -0
- package/src/cli/snapshot.ts +164 -0
- package/src/cli/storage-rules.ts +172 -0
- package/src/cli/verify.ts +416 -0
- package/src/conformance/.generated/can-i-use-browser.ts +113 -0
- package/src/conformance/.generated/can-i-use.ts +117 -0
- package/src/conformance/.generated/conformance-docs.ts +14 -0
- package/src/conformance/browser.ts +24 -0
- package/src/conformance/can-i-use-tool.ts +51 -0
- package/src/conformance/can-i-use.ts +18 -0
- package/src/conformance/docs.ts +8 -0
- package/src/conformance/index.ts +20 -0
- package/src/conformance/tools.ts +16 -0
- package/src/credentials/core/memoize-ttl.ts +142 -0
- package/src/credentials/core/types.ts +10 -0
- package/src/credentials/node/from-adc.ts +96 -0
- package/src/credentials/node/from-service-account.ts +133 -0
- package/src/credentials/node/index.ts +6 -0
- package/src/discover/concurrency.ts +127 -0
- package/src/discover/crawler-adapter.ts +142 -0
- package/src/discover/crawler.ts +1127 -0
- package/src/discover/credential-free.ts +27 -0
- package/src/discover/findCollectionGroup.ts +131 -0
- package/src/discover/firestore-source.ts +59 -0
- package/src/discover/index.ts +5 -0
- package/src/discover/merge.ts +523 -0
- package/src/discover/session.ts +402 -0
- package/src/discover/tools.ts +201 -0
- package/src/discover/types.ts +187 -0
- package/src/discover/wire.ts +324 -0
- package/src/functions-rtdb/child.ts +382 -0
- package/src/functions-rtdb/delivery.ts +7 -0
- package/src/functions-rtdb/discovery.ts +136 -0
- package/src/functions-rtdb/event.ts +64 -0
- package/src/functions-rtdb/execution.ts +111 -0
- package/src/functions-rtdb/in-memory-delivery.ts +40 -0
- package/src/functions-rtdb/project.ts +74 -0
- package/src/functions-rtdb/projection.ts +104 -0
- package/src/functions-rtdb/reference-pattern.ts +20 -0
- package/src/functions-rtdb/remote-delivery.ts +23 -0
- package/src/pkg-version.ts +49 -0
- package/src/register/esm-exports.ts +63 -0
- package/src/register/hooks.ts +30 -0
- package/src/register/index.ts +150 -0
- package/src/register/mapping.ts +33 -0
- package/src/remote/index.ts +1050 -0
- package/src/rtdb/crawl-snapshot.ts +118 -0
- package/src/rtdb/inspection.ts +137 -0
- package/src/rtdb/load-rules-document.ts +56 -0
- package/src/rtdb/rules-generation-tool.ts +38 -0
- package/src/rtdb/rules-json.ts +29 -0
- package/src/serve/activity-guard.ts +29 -0
- package/src/serve/activity-route.ts +164 -0
- package/src/serve/activity-warning.ts +26 -0
- package/src/serve/bridge-mount.ts +200 -0
- package/src/serve/bundler.ts +519 -0
- package/src/serve/capture-store.ts +65 -0
- package/src/serve/discovery.ts +199 -0
- package/src/serve/entries/ai.ts +292 -0
- package/src/serve/entries/app-backend.ts +4 -0
- package/src/serve/entries/app-client.ts +26 -0
- package/src/serve/entries/app-session-store.ts +47 -0
- package/src/serve/entries/app.ts +11 -0
- package/src/serve/entries/auth-helper-core.ts +181 -0
- package/src/serve/entries/auth-helper-dom.ts +134 -0
- package/src/serve/entries/auth-helper-runtime.ts +20 -0
- package/src/serve/entries/auth.ts +255 -0
- package/src/serve/entries/bridge-url.ts +31 -0
- package/src/serve/entries/database.ts +129 -0
- package/src/serve/entries/firestore.ts +232 -0
- package/src/serve/entries/init.ts +49 -0
- package/src/serve/entries/keepalive.ts +46 -0
- package/src/serve/entries/messaging-sw.ts +75 -0
- package/src/serve/entries/messaging.ts +96 -0
- package/src/serve/entries/runtime.ts +556 -0
- package/src/serve/entries/session-store.ts +97 -0
- package/src/serve/entries/storage.ts +92 -0
- package/src/serve/entries/tab-sync-wiring.ts +274 -0
- package/src/serve/entries/worker-runtime.ts +135 -0
- package/src/serve/init-payload.ts +42 -0
- package/src/serve/namespace.ts +675 -0
- package/src/serve/open-browser.ts +68 -0
- package/src/serve/rules.ts +272 -0
- package/src/serve/runtime/chip-config.ts +30 -0
- package/src/serve/runtime/chip-install.ts +27 -0
- package/src/serve/runtime/chip.ts +282 -0
- package/src/serve/runtime/manifest.ts +37 -0
- package/src/serve/runtime/status.ts +216 -0
- package/src/serve/runtime/worker-connection.ts +14 -0
- package/src/serve/runtime/worker-generation.ts +75 -0
- package/src/serve/runtime/worker-replacement.ts +42 -0
- package/src/serve/sandbox-marker.ts +31 -0
- package/src/serve/server.ts +428 -0
- package/src/serve/standalone-assets.ts +180 -0
- package/src/serve/state-store.ts +166 -0
- package/src/serve/studio/disk-project-store.ts +185 -0
- package/src/serve/studio/disk-workspace.ts +162 -0
- package/src/serve/studio/index.ts +20 -0
- package/src/serve/studio/routes.ts +237 -0
- package/src/serve/studio/store-types.ts +48 -0
- package/src/serve/studio/studio-storage.test.ts +289 -0
- package/src/serve/vite-ai-config.ts +77 -0
- package/src/serve/vite-plugin.ts +1075 -0
- package/src/serve/vite-rules-source.ts +28 -0
- package/src/serve/vite-worker-runtime.ts +51 -0
- package/src/serve/worker/activity-bootstrap.ts +24 -0
- package/src/serve/worker/client/admin-firestore.ts +38 -0
- package/src/serve/worker/client/ai.ts +124 -0
- package/src/serve/worker/client/auth.ts +489 -0
- package/src/serve/worker/client/connection.ts +227 -0
- package/src/serve/worker/client/core.ts +384 -0
- package/src/serve/worker/client/disconnect.ts +45 -0
- package/src/serve/worker/client/firestore-reads.ts +181 -0
- package/src/serve/worker/client/firestore-refs.ts +226 -0
- package/src/serve/worker/client/firestore-writes.ts +232 -0
- package/src/serve/worker/client/handles.ts +91 -0
- package/src/serve/worker/client/messaging.ts +119 -0
- package/src/serve/worker/client/presence.ts +174 -0
- package/src/serve/worker/client/rtdb.ts +311 -0
- package/src/serve/worker/client/rules.ts +63 -0
- package/src/serve/worker/client/runtime-control.ts +22 -0
- package/src/serve/worker/client/service-worker-connection.ts +59 -0
- package/src/serve/worker/client/snapshots.ts +86 -0
- package/src/serve/worker/client/storage.ts +195 -0
- package/src/serve/worker/client/studio.ts +88 -0
- package/src/serve/worker/client.ts +57 -0
- package/src/serve/worker/durable-persistence.ts +137 -0
- package/src/serve/worker/entry.ts +223 -0
- package/src/serve/worker/host/admin-firestore.ts +88 -0
- package/src/serve/worker/host/connection.ts +179 -0
- package/src/serve/worker/host/core.ts +387 -0
- package/src/serve/worker/host/dispatch.ts +316 -0
- package/src/serve/worker/host/firestore-reads.ts +117 -0
- package/src/serve/worker/host/firestore-writes.ts +456 -0
- package/src/serve/worker/host/presence.ts +312 -0
- package/src/serve/worker/host/rtdb.ts +136 -0
- package/src/serve/worker/host/rules.ts +128 -0
- package/src/serve/worker/host/storage.ts +312 -0
- package/src/serve/worker/host/studio.ts +75 -0
- package/src/serve/worker/host/subscriptions.ts +238 -0
- package/src/serve/worker/host-ai.ts +165 -0
- package/src/serve/worker/host-auth.ts +469 -0
- package/src/serve/worker/host-context.ts +273 -0
- package/src/serve/worker/host-events.ts +94 -0
- package/src/serve/worker/host-messaging.ts +240 -0
- package/src/serve/worker/host.ts +47 -0
- package/src/serve/worker/index.ts +196 -0
- package/src/serve/worker/presence-timing.ts +15 -0
- package/src/serve/worker/protocol.ts +1124 -0
- package/src/serve/worker/retirement.ts +125 -0
- package/src/serve/worker/serve-init.ts +605 -0
- package/src/serve/worker/service-worker-channel.ts +33 -0
- package/src/serve/worker/service-worker-relay.ts +86 -0
- package/src/serve/writer-lock.ts +53 -0
- package/src/verify/cases.ts +233 -0
- package/src/verify/fixture.ts +258 -0
- package/src/verify/index.ts +519 -0
- package/src/verify/tools.ts +108 -0
- package/src/version/compat-target.ts +17 -0
- package/src/vite.ts +18 -0
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
- 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
|
+
}
|