snulbug 0.2.0__tar.gz → 0.2.1__tar.gz

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 (382) hide show
  1. {snulbug-0.2.0 → snulbug-0.2.1}/CHANGELOG.md +15 -0
  2. {snulbug-0.2.0 → snulbug-0.2.1}/PKG-INFO +2 -2
  3. {snulbug-0.2.0 → snulbug-0.2.1}/docs/devcontainers.md +2 -2
  4. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-proxy.md +62 -0
  5. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-share.md +6 -0
  6. {snulbug-0.2.0 → snulbug-0.2.1}/docs/ngrok-end-to-end.md +26 -0
  7. {snulbug-0.2.0 → snulbug-0.2.1}/docs/release.md +3 -3
  8. {snulbug-0.2.0 → snulbug-0.2.1}/examples/codespace_local_gateway/.devcontainer/devcontainer.json +1 -1
  9. {snulbug-0.2.0 → snulbug-0.2.1}/features/snulbug/README.md +1 -1
  10. {snulbug-0.2.0 → snulbug-0.2.1}/features/snulbug/devcontainer-feature.json +1 -1
  11. {snulbug-0.2.0 → snulbug-0.2.1}/pyproject.toml +2 -2
  12. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/__init__.py +1 -1
  13. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cli/share.py +16 -0
  14. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/config.py +5 -0
  15. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/gateway_templates.py +3 -0
  16. snulbug-0.2.1/snulbug/http_routes.py +239 -0
  17. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/proxy.py +9 -0
  18. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/quickstart.py +6 -1
  19. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/share.py +2 -0
  20. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/tunnel.py +43 -5
  21. snulbug-0.2.1/tests/test_http_routes.py +462 -0
  22. {snulbug-0.2.0 → snulbug-0.2.1}/.gitignore +0 -0
  23. {snulbug-0.2.0 → snulbug-0.2.1}/CODE_OF_CONDUCT.md +0 -0
  24. {snulbug-0.2.0 → snulbug-0.2.1}/CONTRIBUTING.md +0 -0
  25. {snulbug-0.2.0 → snulbug-0.2.1}/LICENSE +0 -0
  26. {snulbug-0.2.0 → snulbug-0.2.1}/README.md +0 -0
  27. {snulbug-0.2.0 → snulbug-0.2.1}/SECURITY.md +0 -0
  28. {snulbug-0.2.0 → snulbug-0.2.1}/docs/README.md +0 -0
  29. {snulbug-0.2.0 → snulbug-0.2.1}/docs/actions.md +0 -0
  30. {snulbug-0.2.0 → snulbug-0.2.1}/docs/auth-providers.md +0 -0
  31. {snulbug-0.2.0 → snulbug-0.2.1}/docs/aws-mcp.md +0 -0
  32. {snulbug-0.2.0 → snulbug-0.2.1}/docs/bundles.md +0 -0
  33. {snulbug-0.2.0 → snulbug-0.2.1}/docs/ci-policy-gates.md +0 -0
  34. {snulbug-0.2.0 → snulbug-0.2.1}/docs/comparison.md +0 -0
  35. {snulbug-0.2.0 → snulbug-0.2.1}/docs/credential-brokers.md +0 -0
  36. {snulbug-0.2.0 → snulbug-0.2.1}/docs/discovery-providers.md +0 -0
  37. {snulbug-0.2.0 → snulbug-0.2.1}/docs/event-sink-providers.md +0 -0
  38. {snulbug-0.2.0 → snulbug-0.2.1}/docs/evidence-exporters.md +0 -0
  39. {snulbug-0.2.0 → snulbug-0.2.1}/docs/getting-started.md +0 -0
  40. {snulbug-0.2.0 → snulbug-0.2.1}/docs/lua-policy-dsl.md +0 -0
  41. {snulbug-0.2.0 → snulbug-0.2.1}/docs/lua-request-api.md +0 -0
  42. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-auth-recipes.md +0 -0
  43. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-client-recipes.md +0 -0
  44. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-evidence.md +0 -0
  45. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-fabric.md +0 -0
  46. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-gateway.md +0 -0
  47. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-guide.md +0 -0
  48. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-impact.md +0 -0
  49. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-learn.md +0 -0
  50. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-policy.md +0 -0
  51. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-presets.md +0 -0
  52. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-protocol.md +0 -0
  53. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-recorder.md +0 -0
  54. {snulbug-0.2.0 → snulbug-0.2.1}/docs/mcp-schemas.md +0 -0
  55. {snulbug-0.2.0 → snulbug-0.2.1}/docs/policy-deny-backoff.md +0 -0
  56. {snulbug-0.2.0 → snulbug-0.2.1}/docs/quickstart.md +0 -0
  57. {snulbug-0.2.0 → snulbug-0.2.1}/docs/roadmap.md +0 -0
  58. {snulbug-0.2.0 → snulbug-0.2.1}/docs/security-model.md +0 -0
  59. {snulbug-0.2.0 → snulbug-0.2.1}/docs/share-doctor-checks.md +0 -0
  60. {snulbug-0.2.0 → snulbug-0.2.1}/docs/state.md +0 -0
  61. {snulbug-0.2.0 → snulbug-0.2.1}/docs/tool-catalog-projection.md +0 -0
  62. {snulbug-0.2.0 → snulbug-0.2.1}/docs/tool-risk-analyzers.md +0 -0
  63. {snulbug-0.2.0 → snulbug-0.2.1}/docs/tunnel-providers.md +0 -0
  64. {snulbug-0.2.0 → snulbug-0.2.1}/docs/upstream-transports.md +0 -0
  65. {snulbug-0.2.0 → snulbug-0.2.1}/examples/auth_claim_patterns/.gitignore +0 -0
  66. {snulbug-0.2.0 → snulbug-0.2.1}/examples/auth_claim_patterns/README.md +0 -0
  67. {snulbug-0.2.0 → snulbug-0.2.1}/examples/auth_claim_patterns/ci-workload-identity.toml +0 -0
  68. {snulbug-0.2.0 → snulbug-0.2.1}/examples/auth_claim_patterns/group-gated-tools.toml +0 -0
  69. {snulbug-0.2.0 → snulbug-0.2.1}/examples/auth_claim_patterns/policy.lua +0 -0
  70. {snulbug-0.2.0 → snulbug-0.2.1}/examples/auth_claim_patterns/tenant-isolation.toml +0 -0
  71. {snulbug-0.2.0 → snulbug-0.2.1}/examples/basic/README.md +0 -0
  72. {snulbug-0.2.0 → snulbug-0.2.1}/examples/basic/app.py +0 -0
  73. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/idempotency.snulbug/README.md +0 -0
  74. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/idempotency.snulbug/fixtures/duplicate-delivery.json +0 -0
  75. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/idempotency.snulbug/fixtures/first-delivery.json +0 -0
  76. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/idempotency.snulbug/manifest.json +0 -0
  77. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/idempotency.snulbug/policy.lua +0 -0
  78. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/idempotency.snulbug/snapshots/duplicate.json +0 -0
  79. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/idempotency.snulbug/snapshots/empty.json +0 -0
  80. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/mcp-gateway.snulbug/README.md +0 -0
  81. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/mcp-gateway.snulbug/fixtures/allowed-tool.json +0 -0
  82. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/mcp-gateway.snulbug/fixtures/blocked-tool.json +0 -0
  83. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/mcp-gateway.snulbug/fixtures/unauthenticated.json +0 -0
  84. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/mcp-gateway.snulbug/manifest.json +0 -0
  85. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/mcp-gateway.snulbug/policy.lua +0 -0
  86. {snulbug-0.2.0 → snulbug-0.2.1}/examples/bundles/mcp-gateway.snulbug/snapshots/empty.json +0 -0
  87. {snulbug-0.2.0 → snulbug-0.2.1}/examples/codespace_local_gateway/README.md +0 -0
  88. {snulbug-0.2.0 → snulbug-0.2.1}/examples/codespace_local_gateway/mcp-client.json +0 -0
  89. {snulbug-0.2.0 → snulbug-0.2.1}/examples/codespace_local_gateway/mock_mcp_server.py +0 -0
  90. {snulbug-0.2.0 → snulbug-0.2.1}/examples/codespace_local_gateway/policy.lua +0 -0
  91. {snulbug-0.2.0 → snulbug-0.2.1}/examples/codespace_local_gateway/snulbug.env-gateway.toml +0 -0
  92. {snulbug-0.2.0 → snulbug-0.2.1}/examples/codespace_local_gateway/snulbug.local-gateway.toml +0 -0
  93. {snulbug-0.2.0 → snulbug-0.2.1}/examples/customer_policy/README.md +0 -0
  94. {snulbug-0.2.0 → snulbug-0.2.1}/examples/customer_policy/app.py +0 -0
  95. {snulbug-0.2.0 → snulbug-0.2.1}/examples/customer_policy/policies/acme.lua +0 -0
  96. {snulbug-0.2.0 → snulbug-0.2.1}/examples/customer_policy/policies/default.lua +0 -0
  97. {snulbug-0.2.0 → snulbug-0.2.1}/examples/customer_policy/policies/globex.lua +0 -0
  98. {snulbug-0.2.0 → snulbug-0.2.1}/examples/customer_policy/requests/acme-invalid.json +0 -0
  99. {snulbug-0.2.0 → snulbug-0.2.1}/examples/customer_policy/requests/acme-valid.json +0 -0
  100. {snulbug-0.2.0 → snulbug-0.2.1}/examples/customer_policy/requests/globex-sandbox.json +0 -0
  101. {snulbug-0.2.0 → snulbug-0.2.1}/examples/customer_policy/requests/unknown-tenant.json +0 -0
  102. {snulbug-0.2.0 → snulbug-0.2.1}/examples/github-actions/snulbug-policy-gates.yml +0 -0
  103. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/.gitignore +0 -0
  104. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/Dockerfile.gateway +0 -0
  105. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/README.md +0 -0
  106. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/auth/keycloak/README.md +0 -0
  107. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/auth/keycloak/auth-init.json +0 -0
  108. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/auth/keycloak/client-token-request.json +0 -0
  109. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/auth/keycloak/commands.json +0 -0
  110. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/auth/keycloak/snulbug.auth.toml +0 -0
  111. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/docker-compose.yml +0 -0
  112. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/get-token.py +0 -0
  113. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/keycloak/snulbug-demo-realm.json +0 -0
  114. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/mock_mcp_server.py +0 -0
  115. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/policy.snulbug/policy.lua +0 -0
  116. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/requests/blocked-write-file.json +0 -0
  117. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/requests/read-file.json +0 -0
  118. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/requests/tools-list.json +0 -0
  119. {snulbug-0.2.0 → snulbug-0.2.1}/examples/keycloak_oauth_demo/snulbug.toml +0 -0
  120. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/Dockerfile.gateway +0 -0
  121. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/Dockerfile.remote-peer +0 -0
  122. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/README.md +0 -0
  123. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/docker-compose.yml +0 -0
  124. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/hypertele-client.json +0 -0
  125. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/hypertele-server.json +0 -0
  126. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/mcp-client.json +0 -0
  127. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/mock_mcp_server.js +0 -0
  128. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/mock_mcp_server.py +0 -0
  129. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/policy.lua +0 -0
  130. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/snulbug.facade.toml +0 -0
  131. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_container_facade/snulbug.local.toml +0 -0
  132. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_gateway/README.md +0 -0
  133. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_gateway/app.py +0 -0
  134. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_proxy_demo/.gitignore +0 -0
  135. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_proxy_demo/README.md +0 -0
  136. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_proxy_demo/requests/blocked-tool.json +0 -0
  137. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_proxy_demo/requests/safe-tool.json +0 -0
  138. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_proxy_demo/requests/tools-list.json +0 -0
  139. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_proxy_demo/run_demo.py +0 -0
  140. {snulbug-0.2.0 → snulbug-0.2.1}/examples/mcp_proxy_demo/upstream.py +0 -0
  141. {snulbug-0.2.0 → snulbug-0.2.1}/examples/policy_promotion/README.md +0 -0
  142. {snulbug-0.2.0 → snulbug-0.2.1}/examples/policy_promotion/active.lua +0 -0
  143. {snulbug-0.2.0 → snulbug-0.2.1}/examples/policy_promotion/draft.lua +0 -0
  144. {snulbug-0.2.0 → snulbug-0.2.1}/examples/policy_promotion/fixtures/acme-invalid.json +0 -0
  145. {snulbug-0.2.0 → snulbug-0.2.1}/examples/policy_promotion/fixtures/acme-valid.json +0 -0
  146. {snulbug-0.2.0 → snulbug-0.2.1}/examples/provider_policy_templates/README.md +0 -0
  147. {snulbug-0.2.0 → snulbug-0.2.1}/examples/provider_policy_templates/cloudflare-access-group-gate.lua +0 -0
  148. {snulbug-0.2.0 → snulbug-0.2.1}/examples/provider_policy_templates/entra-app-role-gate.lua +0 -0
  149. {snulbug-0.2.0 → snulbug-0.2.1}/examples/provider_policy_templates/github-actions-workload-gate.lua +0 -0
  150. {snulbug-0.2.0 → snulbug-0.2.1}/examples/provider_policy_templates/keycloak-role-gate.lua +0 -0
  151. {snulbug-0.2.0 → snulbug-0.2.1}/examples/state_replay/README.md +0 -0
  152. {snulbug-0.2.0 → snulbug-0.2.1}/examples/state_replay/duplicate-state.json +0 -0
  153. {snulbug-0.2.0 → snulbug-0.2.1}/examples/state_replay/idempotency.lua +0 -0
  154. {snulbug-0.2.0 → snulbug-0.2.1}/examples/state_replay/request.json +0 -0
  155. {snulbug-0.2.0 → snulbug-0.2.1}/examples/webhook_normalization/README.md +0 -0
  156. {snulbug-0.2.0 → snulbug-0.2.1}/examples/webhook_normalization/app.py +0 -0
  157. {snulbug-0.2.0 → snulbug-0.2.1}/examples/webhook_normalization/policies/github.lua +0 -0
  158. {snulbug-0.2.0 → snulbug-0.2.1}/examples/webhook_normalization/policies/stripe.lua +0 -0
  159. {snulbug-0.2.0 → snulbug-0.2.1}/examples/webhook_normalization/policies/unknown.lua +0 -0
  160. {snulbug-0.2.0 → snulbug-0.2.1}/examples/webhook_normalization/requests/github-push.json +0 -0
  161. {snulbug-0.2.0 → snulbug-0.2.1}/examples/webhook_normalization/requests/stripe-invoice.json +0 -0
  162. {snulbug-0.2.0 → snulbug-0.2.1}/examples/webhook_normalization/requests/stripe-malformed.json +0 -0
  163. {snulbug-0.2.0 → snulbug-0.2.1}/examples/webhook_normalization/requests/unknown-vendor.json +0 -0
  164. {snulbug-0.2.0 → snulbug-0.2.1}/features/snulbug/install.sh +0 -0
  165. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/__main__.py +0 -0
  166. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/assets/prism.css +0 -0
  167. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/assets/prism.js +0 -0
  168. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/auth_providers.py +0 -0
  169. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/auth_recipes.py +0 -0
  170. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/auth-required.snulbug/README.md +0 -0
  171. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/auth-required.snulbug/fixtures/authenticated.json +0 -0
  172. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/auth-required.snulbug/fixtures/missing-token.json +0 -0
  173. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/auth-required.snulbug/manifest.json +0 -0
  174. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/auth-required.snulbug/policy.lua +0 -0
  175. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/README.md +0 -0
  176. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/contexts/inventory-lease.json +0 -0
  177. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/contexts/sandbox-change-lease.json +0 -0
  178. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/data-read.json +0 -0
  179. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/iam-delete-user.json +0 -0
  180. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/inventory-read.json +0 -0
  181. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/knowledge-search.json +0 -0
  182. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/missing-token.json +0 -0
  183. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/sandbox-mutation.json +0 -0
  184. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/manifest.json +0 -0
  185. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/policy.lua +0 -0
  186. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/snapshots/empty.json +0 -0
  187. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/README.md +0 -0
  188. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/missing-token.json +0 -0
  189. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/safe-tool.json +0 -0
  190. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/unsafe-tool.json +0 -0
  191. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/manifest.json +0 -0
  192. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/policy.lua +0 -0
  193. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/snapshots/empty.json +0 -0
  194. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/README.md +0 -0
  195. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/missing-token.json +0 -0
  196. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/safe-tool.json +0 -0
  197. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/shell-tool.json +0 -0
  198. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/manifest.json +0 -0
  199. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/policy.lua +0 -0
  200. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/README.md +0 -0
  201. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/absolute-path.json +0 -0
  202. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/allowed-path.json +0 -0
  203. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/traversal-path.json +0 -0
  204. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/unsafe-tool.json +0 -0
  205. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/manifest.json +0 -0
  206. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/policy.lua +0 -0
  207. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/README.md +0 -0
  208. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/missing-token.json +0 -0
  209. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/safe-tool.json +0 -0
  210. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/tools-list.json +0 -0
  211. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/write-method.json +0 -0
  212. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/manifest.json +0 -0
  213. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/policy.lua +0 -0
  214. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/snapshots/empty.json +0 -0
  215. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/README.md +0 -0
  216. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/safe-tool.json +0 -0
  217. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/tools-list.json +0 -0
  218. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/unsafe-tool.json +0 -0
  219. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/manifest.json +0 -0
  220. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/policy.lua +0 -0
  221. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/README.md +0 -0
  222. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/batch-request.json +0 -0
  223. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/missing-token.json +0 -0
  224. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/safe-tool.json +0 -0
  225. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/unsafe-tool.json +0 -0
  226. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/manifest.json +0 -0
  227. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/policy.lua +0 -0
  228. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/snapshots/empty.json +0 -0
  229. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/README.md +0 -0
  230. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/allowed-read.json +0 -0
  231. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/missing-token.json +0 -0
  232. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/secret-path.json +0 -0
  233. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/traversal-path.json +0 -0
  234. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/unsafe-tool.json +0 -0
  235. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/manifest.json +0 -0
  236. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/policy.lua +0 -0
  237. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/bundle.py +0 -0
  238. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cli/__init__.py +0 -0
  239. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cli/common.py +0 -0
  240. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cli/evidence.py +0 -0
  241. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cli/fabric.py +0 -0
  242. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cli/policy.py +0 -0
  243. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cli/rich_reports.py +0 -0
  244. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cli/rich_share.py +0 -0
  245. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cli/schemas.py +0 -0
  246. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cli_helpers.py +0 -0
  247. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/cloudflare_access.py +0 -0
  248. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/codespaces.py +0 -0
  249. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/confirm.py +0 -0
  250. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/control_events.py +0 -0
  251. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/controller.py +0 -0
  252. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/credentials.py +0 -0
  253. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/discovery.py +0 -0
  254. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/events.py +0 -0
  255. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/evidence_exporters.py +0 -0
  256. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/fabric.py +0 -0
  257. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/fabric_control.py +0 -0
  258. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/fabric_members.py +0 -0
  259. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/fabric_runtime.py +0 -0
  260. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/guide.py +0 -0
  261. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/impact.py +0 -0
  262. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/inspection.py +0 -0
  263. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/json_schema.py +0 -0
  264. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/lab.py +0 -0
  265. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/learn.py +0 -0
  266. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/leases.py +0 -0
  267. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/manifests.py +0 -0
  268. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_auth.py +0 -0
  269. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_cache.py +0 -0
  270. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_capabilities.py +0 -0
  271. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_client_requests.py +0 -0
  272. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_completion.py +0 -0
  273. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_mrtr.py +0 -0
  274. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_parameter_headers.py +0 -0
  275. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_policy_bundles.py +0 -0
  276. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_progress.py +0 -0
  277. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_protocol.py +0 -0
  278. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_resources.py +0 -0
  279. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_schema_policy.py +0 -0
  280. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_schemas.py +0 -0
  281. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_spec_conformance.py +0 -0
  282. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_stdio.py +0 -0
  283. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_stream.py +0 -0
  284. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_subscriptions.py +0 -0
  285. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_tasks.py +0 -0
  286. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/mcp_tools.py +0 -0
  287. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/middleware.py +0 -0
  288. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/policy_activation.py +0 -0
  289. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/policy_backoff.py +0 -0
  290. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/policy_review.py +0 -0
  291. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/presets.py +0 -0
  292. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/promotion.py +0 -0
  293. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/py.typed +0 -0
  294. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/recorder.py +0 -0
  295. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/redaction.py +0 -0
  296. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/release_qa.py +0 -0
  297. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/response_policy.py +0 -0
  298. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/runtime.py +0 -0
  299. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/sarif.py +0 -0
  300. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/scaffolds.py +0 -0
  301. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/schema_policy.py +0 -0
  302. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/share_console.py +0 -0
  303. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/share_doctor.py +0 -0
  304. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/share_session.py +0 -0
  305. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/simulator.py +0 -0
  306. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/state.py +0 -0
  307. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/tool_catalog.py +0 -0
  308. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/tool_risk.py +0 -0
  309. {snulbug-0.2.0 → snulbug-0.2.1}/snulbug/upstream_transports.py +0 -0
  310. {snulbug-0.2.0 → snulbug-0.2.1}/tests/fixtures/json_schema_2020_12/LICENSE +0 -0
  311. {snulbug-0.2.0 → snulbug-0.2.1}/tests/fixtures/json_schema_2020_12/README.md +0 -0
  312. {snulbug-0.2.0 → snulbug-0.2.1}/tests/fixtures/json_schema_2020_12/dependentSchemas.json +0 -0
  313. {snulbug-0.2.0 → snulbug-0.2.1}/tests/fixtures/json_schema_2020_12/dynamicRef.json +0 -0
  314. {snulbug-0.2.0 → snulbug-0.2.1}/tests/fixtures/json_schema_2020_12/if-then-else.json +0 -0
  315. {snulbug-0.2.0 → snulbug-0.2.1}/tests/fixtures/json_schema_2020_12/prefixItems.json +0 -0
  316. {snulbug-0.2.0 → snulbug-0.2.1}/tests/fixtures/json_schema_2020_12/unevaluatedProperties.json +0 -0
  317. {snulbug-0.2.0 → snulbug-0.2.1}/tests/fixtures/stdio_subscriptions.py +0 -0
  318. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_auth_claim_patterns.py +0 -0
  319. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_auth_recipes.py +0 -0
  320. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_cli_helpers.py +0 -0
  321. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_cloudflare_access.py +0 -0
  322. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_codespaces.py +0 -0
  323. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_config.py +0 -0
  324. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_credentials.py +0 -0
  325. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_customer_policy_demo.py +0 -0
  326. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_devcontainer_feature.py +0 -0
  327. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_discovery.py +0 -0
  328. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_events.py +0 -0
  329. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_evidence_exporters.py +0 -0
  330. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_fabric.py +0 -0
  331. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_fabric_controller.py +0 -0
  332. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_json_schema.py +0 -0
  333. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_keycloak_oauth_demo.py +0 -0
  334. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_leases.py +0 -0
  335. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_manifests.py +0 -0
  336. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_cache.py +0 -0
  337. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_gateway_demo.py +0 -0
  338. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_guide.py +0 -0
  339. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_helpers.py +0 -0
  340. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_impact.py +0 -0
  341. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_inspection.py +0 -0
  342. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_lab.py +0 -0
  343. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_learn.py +0 -0
  344. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_mrtr.py +0 -0
  345. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_parameter_headers.py +0 -0
  346. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_policy_bundles.py +0 -0
  347. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_presets.py +0 -0
  348. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_progress.py +0 -0
  349. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_protocol.py +0 -0
  350. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_proxy_demo.py +0 -0
  351. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_quickstart.py +0 -0
  352. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_recorder.py +0 -0
  353. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_resources.py +0 -0
  354. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_schema_policy.py +0 -0
  355. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_schemas.py +0 -0
  356. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_share.py +0 -0
  357. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_spec_conformance.py +0 -0
  358. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_stdio.py +0 -0
  359. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_stream.py +0 -0
  360. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_subscriptions.py +0 -0
  361. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_mcp_tools.py +0 -0
  362. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_middleware.py +0 -0
  363. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_policy_bundle.py +0 -0
  364. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_policy_promotion.py +0 -0
  365. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_policy_state.py +0 -0
  366. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_provider_policy_templates.py +0 -0
  367. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_proxy.py +0 -0
  368. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_redaction_audit.py +0 -0
  369. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_release_qa.py +0 -0
  370. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_response_policy.py +0 -0
  371. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_sandbox_abuse.py +0 -0
  372. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_scaffolds.py +0 -0
  373. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_share_console.py +0 -0
  374. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_state_snapshots.py +0 -0
  375. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_tool_catalog.py +0 -0
  376. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_tool_risk.py +0 -0
  377. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_tunnel_audit.py +0 -0
  378. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_tunnel_doctor.py +0 -0
  379. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_tunnel_init.py +0 -0
  380. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_upstream_transports.py +0 -0
  381. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_webhook_normalization_demo.py +0 -0
  382. {snulbug-0.2.0 → snulbug-0.2.1}/tests/test_webhooks.py +0 -0
@@ -6,6 +6,21 @@ The format is based on Keep a Changelog, and this project follows semantic versi
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.1] - 2026-10-15
10
+
11
+ Expose web applications and APIs alongside MCP through one gateway and ngrok tunnel.
12
+
13
+ - Added `[[mcp.proxy.http_routes]]` mounts with longest-prefix routing, segment-boundary matching, configurable prefix stripping, and reserved MCP/gateway paths.
14
+ - Added repeatable `--http-route PREFIX=URL` flags to share creation and quickstart, with matching ngrok Traffic Policy rules that apply MCP edge checks only to the MCP endpoint.
15
+ - Added streaming HTTP uploads, responses, and SSE with disconnect cleanup, ordinary HTTP headers/cookies/content encodings, and plain HTTP upstream errors; included local proxy/facade regression coverage and setup documentation.
16
+ - Raised the PyJWT minimum to 2.15 and updated the lockfile to exclude the versions flagged by the current JWT security advisories.
17
+
18
+ HTTP mounts bypass MCP authentication, leases, Lua, redaction, and MCP evidence
19
+ logging; each backend owns its authentication. Applications must configure their
20
+ public base path. WebSockets and rewriting HTML, redirects, or cookie paths are
21
+ outside this slice. See [shared HTTP routes](docs/mcp-proxy.md#shared-http-routes)
22
+ and the [ngrok walkthrough](docs/ngrok-end-to-end.md).
23
+
9
24
  ## [0.2.0] - 2026-09-06
10
25
 
11
26
  This release expands MCP protocol support while keeping **2025-11-25 as the default**.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: snulbug
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A local-dev MCP policy proxy with Lua policies, replay logs, and learned least-privilege bundles
5
5
  Project-URL: Homepage, https://github.com/nahuaque/snulbug
6
6
  Project-URL: Repository, https://github.com/nahuaque/snulbug
@@ -26,7 +26,7 @@ Requires-Python: >=3.10
26
26
  Requires-Dist: httpx<1,>=0.28
27
27
  Requires-Dist: jsonschema<5,>=4.23
28
28
  Requires-Dist: lupa>=2.5
29
- Requires-Dist: pyjwt[crypto]>=2.8
29
+ Requires-Dist: pyjwt[crypto]>=2.15
30
30
  Requires-Dist: referencing<1,>=0.35
31
31
  Requires-Dist: rich>=15.0.0
32
32
  Requires-Dist: tomli>=2.0; python_version < '3.11'
@@ -10,8 +10,8 @@ servers with a shared control plane.
10
10
  ```json
11
11
  {
12
12
  "features": {
13
- "ghcr.io/nahuaque/snulbug/features/snulbug:0.2.0": {
14
- "version": "0.2.0",
13
+ "ghcr.io/nahuaque/snulbug/features/snulbug:0.2.1": {
14
+ "version": "0.2.1",
15
15
  "mode": "member-agent",
16
16
  "policy_profile": "tunnel-safe",
17
17
  "registry": "redis://redis:6379/0",
@@ -109,6 +109,68 @@ curl -sS "${NGROK_URL}/mcp" \
109
109
  -d '{"jsonrpc":"2.0","id":"tools-list","method":"tools/list","params":{}}'
110
110
  ```
111
111
 
112
+ ## Shared HTTP Routes
113
+
114
+ One gateway listener can expose MCP at `/mcp` and ordinary HTTP backends at
115
+ other prefixes. Add mounts to the share's `snulbug.toml`:
116
+
117
+ ```toml
118
+ [[mcp.proxy.http_routes]]
119
+ prefix = "/app"
120
+ upstream = "http://127.0.0.1:3000"
121
+ strip_prefix = true
122
+
123
+ [[mcp.proxy.http_routes]]
124
+ prefix = "/api"
125
+ upstream = "http://127.0.0.1:4000"
126
+ strip_prefix = false
127
+ ```
128
+
129
+ With prefix stripping (the default), `/app/notes?q=1` forwards to
130
+ `http://127.0.0.1:3000/notes?q=1`. With `strip_prefix = false`, `/api/items`
131
+ forwards to `http://127.0.0.1:4000/api/items`. A path in the upstream URL is
132
+ prepended to the forwarded path. Prefix matches use segment boundaries:
133
+ `/app` matches `/app` and `/app/...`, but not `/application`. The longest
134
+ matching prefix wins. The MCP prefix, `/.well-known`, and `/snulbug` are reserved.
135
+
136
+ These requests bypass the MCP middleware, including its authentication, task
137
+ leases, Lua policies, body/response limits, redaction, and replay/audit logs.
138
+ Each HTTP backend owns authentication and request validation. Application
139
+ `Authorization` and cookies pass through; snulbug lease and internal headers
140
+ are removed. Responses preserve status, content encoding, duplicate cookies,
141
+ and end-to-end headers. Uploads and responses stream with backpressure and
142
+ disconnect cleanup; the proxy timeout applies to upstream inactivity.
143
+
144
+ Configure web applications to generate assets, redirects, and cookies for their
145
+ public prefix (for example `/app/`), using their base URL/root-path setting or
146
+ the forwarded host, protocol, and prefix headers. Snulbug does not rewrite HTML,
147
+ redirect locations, or cookie paths. Use `strip_prefix = false` for an app that
148
+ already serves its prefix. WebSocket upgrades are not supported by these HTTP mounts.
149
+
150
+ Restart `share run` after editing mounts. The router works with proxy and facade
151
+ MCP modes and any tunnel that forwards the gateway origin. For generated ngrok
152
+ setup, register routes at share creation so the Cloud Endpoint policy permits
153
+ them:
154
+
155
+ ```bash
156
+ snulbug mcp share create \
157
+ --directory .snulbug/shares/shared-demo \
158
+ --provider ngrok \
159
+ --url https://YOUR-NGROK-CLOUD-ENDPOINT/mcp \
160
+ --upstream http://127.0.0.1:9000 \
161
+ --http-route /app=http://127.0.0.1:3000 \
162
+ --http-route /api=http://127.0.0.1:4000
163
+ snulbug mcp share run .snulbug/shares/shared-demo
164
+ ```
165
+
166
+ `--http-route` is repeatable and also available on `share quickstart`. CLI-created
167
+ routes strip their prefix; change that field in TOML when needed. The generated
168
+ ngrok Traffic Policy permits only the MCP endpoint and registered HTTP prefixes,
169
+ and applies its bearer/method/JSON guards only to MCP. Attach the policy to the
170
+ public Cloud Endpoint as described in the [ngrok walkthrough](ngrok-end-to-end.md).
171
+ If prefixes change later, update and reattach the Cloud Endpoint policy as well
172
+ as restarting the gateway; editing TOML alone does not update ngrok's cloud policy.
173
+
112
174
  ## Streamable HTTP edge hardening
113
175
 
114
176
  `snulbug` enforces MCP Streamable HTTP safety checks at the gateway boundary
@@ -563,6 +563,12 @@ Point clients at `mcp-client.facade.json` for this facade recipe.
563
563
 
564
564
  ## Public tunnel providers
565
565
 
566
+ Shares can also mount HTTP applications on the same tunnel. Add repeatable
567
+ `--http-route /app=http://127.0.0.1:3000` flags to `share create`; MCP stays at
568
+ `/mcp`, while the app route bypasses MCP auth/policy and uses the backend's own
569
+ authentication. Generated ngrok policies permit these prefixes and scope MCP
570
+ checks to `/mcp`. See [shared HTTP routes](mcp-proxy.md#shared-http-routes).
571
+
566
572
  The share command also works with existing tunnel providers:
567
573
 
568
574
  ```bash
@@ -72,6 +72,32 @@ The share writes the important files here:
72
72
  `mcp-client.json` contains the bearer token and lease token for the generated
73
73
  session. Treat it as secret-bearing material.
74
74
 
75
+ ### Optional: Share a Webapp on the Same Tunnel
76
+
77
+ To expose an app on port 3000, add this flag to the `share create` command in
78
+ step 2 before creating the share:
79
+
80
+ ```bash
81
+ --http-route /app=http://127.0.0.1:3000
82
+ ```
83
+
84
+ The generated gateway config and ngrok Traffic Policy now allow `/app` and
85
+ `/app/...` alongside `/mcp`. After starting the app, gateway, and ngrok and
86
+ attaching the generated policy, test both hops without MCP credentials:
87
+
88
+ ```bash
89
+ curl -i http://127.0.0.1:8080/app/
90
+ curl -i "${NGROK_PUBLIC_ORIGIN}/app/"
91
+ ```
92
+
93
+ The route strips `/app` before forwarding, so the app receives `/`. Configure
94
+ its public base path as `/app/` for asset links and redirects. App traffic
95
+ bypasses all MCP authentication/policy/recording; the app owns its authentication.
96
+ See [shared HTTP routes](mcp-proxy.md#shared-http-routes) for multiple backends
97
+ and preserving prefixes. Existing shares need both a TOML route and an updated
98
+ Cloud Endpoint Traffic Policy; changing TOML alone leaves ngrok's old deny rule
99
+ in effect.
100
+
75
101
  ## 3. Export the generated client headers
76
102
 
77
103
  The generated share requires both bearer auth and an `x-snulbug-lease` header
@@ -24,7 +24,7 @@ The release QA suite runs:
24
24
  - isolated built-wheel CLI/module smoke tests outside the source checkout
25
25
  - installed-wheel protocol discovery and offline JSON Schema 2020-12 checks
26
26
 
27
- These checks do not certify client/provider E2E interoperability. For 0.2.0,
27
+ These checks do not certify client/provider E2E interoperability. For 0.2.x,
28
28
  2025-11-25 remains the default and 2026-07-28 remains an opt-in preview.
29
29
 
30
30
  Before publishing, include all new modules, docs, tests, and fixtures in the
@@ -48,7 +48,7 @@ python -m zipfile -l dist/snulbug-*.whl
48
48
  8. Install from TestPyPI in a clean environment and smoke test:
49
49
 
50
50
  ```bash
51
- uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ snulbug==0.2.0 --help
51
+ uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ snulbug==0.2.1 --help
52
52
  ```
53
53
 
54
54
  9. Run the manual `Publish` workflow with `repository = pypi` when the TestPyPI
@@ -56,4 +56,4 @@ uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org
56
56
  10. Create the GitHub release and tag after the PyPI package is available.
57
57
 
58
58
  The `Publish` workflow uses `workflow_dispatch`, not a tag trigger. Creating or
59
- pushing `v0.2.0` does not publish a package or automatically create a GitHub release.
59
+ pushing `v0.2.1` does not publish a package or automatically create a GitHub release.
@@ -2,7 +2,7 @@
2
2
  "name": "snulbug Codespace MCP member",
3
3
  "features": {
4
4
  "ghcr.io/devcontainers/features/python:1": {},
5
- "ghcr.io/nahuaque/snulbug/features/snulbug:0.2.0": {
5
+ "ghcr.io/nahuaque/snulbug/features/snulbug:0.2.1": {
6
6
  "install_source": "github",
7
7
  "github_ref": "main",
8
8
  "mode": "member-agent",
@@ -6,7 +6,7 @@ workspace helpers for Codespaces/devcontainers.
6
6
  ```json
7
7
  {
8
8
  "features": {
9
- "ghcr.io/nahuaque/snulbug/features/snulbug:0.2.0": {
9
+ "ghcr.io/nahuaque/snulbug/features/snulbug:0.2.1": {
10
10
  "install_source": "github",
11
11
  "github_ref": "main",
12
12
  "mode": "member-agent",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "snulbug",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "name": "snulbug",
5
5
  "description": "Install snulbug and optional helpers for MCP policy gateways and remote fabric members in devcontainers.",
6
6
  "documentationURL": "https://github.com/nahuaque/snulbug/blob/main/docs/devcontainers.md",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "snulbug"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "A local-dev MCP policy proxy with Lua policies, replay logs, and learned least-privilege bundles"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -38,7 +38,7 @@ dependencies = [
38
38
  "httpx>=0.28,<1",
39
39
  "jsonschema>=4.23,<5",
40
40
  "lupa>=2.5",
41
- "pyjwt[crypto]>=2.8",
41
+ "pyjwt[crypto]>=2.15",
42
42
  "referencing>=0.35,<1",
43
43
  "rich>=15.0.0",
44
44
  "tomli>=2.0; python_version < '3.11'",
@@ -1,6 +1,6 @@
1
1
  """Local-dev MCP policy proxy and programmable Lua ASGI policy layer."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
4
4
 
5
5
  from .auth_providers import (
6
6
  AuthProvider,
@@ -20,6 +20,7 @@ from ..cli_helpers import (
20
20
  write_result_output,
21
21
  write_sarif_output,
22
22
  )
23
+ from ..http_routes import parse_http_route
23
24
  from .common import read_required_env
24
25
 
25
26
  PROVIDERS = ("generic", "ngrok", "cloudflare", "tailscale", "pinggy", "ssh", "holepunch")
@@ -445,6 +446,7 @@ def handle_mcp_share_command(args: argparse.Namespace, parser: argparse.Argument
445
446
  config_output=args.config_output,
446
447
  traces_dir=args.traces_dir,
447
448
  upstream=args.upstream,
449
+ http_routes=args.http_route,
448
450
  token=args.token,
449
451
  token_env=args.token_env,
450
452
  allowed_tools=args.allow_tool or None,
@@ -506,6 +508,7 @@ def handle_mcp_share_command(args: argparse.Namespace, parser: argparse.Argument
506
508
  provider=args.provider,
507
509
  preset=args.preset,
508
510
  upstream=args.upstream,
511
+ http_routes=args.http_route,
509
512
  hostname=args.hostname,
510
513
  public_url=args.url,
511
514
  ngrok_internal_url=args.ngrok_internal_url,
@@ -940,6 +943,7 @@ def _add_quickstart_args(parser: argparse.ArgumentParser) -> None:
940
943
  parser.add_argument("--config-output", type=Path, default=Path("snulbug.toml"), help="config file path")
941
944
  parser.add_argument("--traces-dir", type=Path, default=Path("traces"), help="trace directory path")
942
945
  parser.add_argument("--upstream", default="http://127.0.0.1:9000", help="upstream MCP HTTP server URL")
946
+ _add_http_route_arg(parser)
943
947
  add_token_arg(parser, help="bearer token to render into generated policy")
944
948
  add_token_env_arg(parser, help="context key used by generated policy for env-derived token lookup")
945
949
  parser.add_argument("--allow-tool", action="append", default=[], help="allowed MCP tool name")
@@ -1169,6 +1173,17 @@ def _add_cloudflare_access_setup_args(parser: argparse.ArgumentParser) -> None:
1169
1173
  )
1170
1174
 
1171
1175
 
1176
+ def _add_http_route_arg(parser: argparse.ArgumentParser) -> None:
1177
+ parser.add_argument(
1178
+ "--http-route",
1179
+ action="append",
1180
+ type=parse_http_route,
1181
+ default=[],
1182
+ metavar="PREFIX=URL",
1183
+ help="HTTP backend outside MCP auth/policy, e.g. /app=http://127.0.0.1:3000; strips prefix; repeatable",
1184
+ )
1185
+
1186
+
1172
1187
  def _add_share_create_args(parser: argparse.ArgumentParser) -> None:
1173
1188
  parser.add_argument("--directory", type=Path, help="share session directory")
1174
1189
  parser.add_argument(
@@ -1179,6 +1194,7 @@ def _add_share_create_args(parser: argparse.ArgumentParser) -> None:
1179
1194
  )
1180
1195
  parser.add_argument("--preset", default="tunnel-safe", help="MCP policy preset")
1181
1196
  parser.add_argument("--upstream", default="http://127.0.0.1:9000", help="upstream MCP HTTP server")
1197
+ _add_http_route_arg(parser)
1182
1198
  parser.add_argument(
1183
1199
  "--hostname",
1184
1200
  help="provider hostname to use when --url is omitted; for --provider ssh this is the SSH target user@host",
@@ -8,6 +8,7 @@ from .credentials import attach_upstream_credentials, normalize_fabric_credentia
8
8
  from .discovery import apply_fabric_discovery
9
9
  from .events import normalize_event_sink_configs
10
10
  from .gateway_templates import render_toml_array_table
11
+ from .http_routes import normalize_http_routes
11
12
  from .mcp_protocol import DEFAULT_MCP_PROTOCOL_VERSION
12
13
  from .policy_backoff import (
13
14
  DEFAULT_POLICY_BACKOFF_EXCLUDE_REASON_CODES,
@@ -81,6 +82,7 @@ DEFAULT_MCP_PROXY_CONFIG = {
81
82
  "upstream": "http://127.0.0.1:9000",
82
83
  "upstream_credential": None,
83
84
  "upstreams": [],
85
+ "http_routes": [],
84
86
  "policy": "policy.snulbug/policy.lua",
85
87
  "host": "127.0.0.1",
86
88
  "port": 8080,
@@ -671,6 +673,9 @@ def normalize_mcp_proxy_config(config: Mapping[str, Any], *, base_dir: str | Pat
671
673
  raise ValueError("mcp.proxy.resource_subscription_policy_action must be 'allow', 'warn', or 'block'")
672
674
  if not normalized["streamable_http_endpoint_path"].startswith("/"):
673
675
  normalized["streamable_http_endpoint_path"] = f"/{normalized['streamable_http_endpoint_path']}"
676
+ normalized["http_routes"] = normalize_http_routes(
677
+ normalized.get("http_routes", []), mcp_path=normalized["streamable_http_endpoint_path"]
678
+ )
674
679
  normalized["streamable_http_allowed_origins"] = _normalize_string_list(
675
680
  normalized.get("streamable_http_allowed_origins", []),
676
681
  field="streamable_http_allowed_origins",
@@ -13,6 +13,7 @@ class GatewayTemplate:
13
13
 
14
14
  proxy: Mapping[str, Any] = field(default_factory=dict)
15
15
  upstreams: Sequence[Mapping[str, Any]] = ()
16
+ http_routes: Sequence[Mapping[str, Any]] = ()
16
17
  auth: Mapping[str, Any] | None = None
17
18
  event_sinks: Sequence[Mapping[str, Any]] = ()
18
19
  fabric: Mapping[str, Any] | None = None
@@ -39,6 +40,8 @@ def render_gateway_toml(template: GatewayTemplate, *, header: str | None = None)
39
40
  _append_block(lines, render_toml_table("mcp.proxy", template.proxy))
40
41
  for upstream in template.upstreams:
41
42
  _append_block(lines, render_toml_array_table("mcp.proxy.upstreams", upstream))
43
+ for route in template.http_routes:
44
+ _append_block(lines, render_toml_array_table("mcp.proxy.http_routes", route))
42
45
  if template.auth is not None:
43
46
  _append_block(lines, render_toml_table("mcp.auth", template.auth))
44
47
  for sink in template.event_sinks:
@@ -0,0 +1,239 @@
1
+ """Explicit HTTP mounts outside the MCP middleware stack."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import math
7
+ from collections.abc import Mapping, Sequence
8
+ from typing import TYPE_CHECKING, Any
9
+ from urllib.parse import quote, urlsplit
10
+
11
+ import httpx
12
+
13
+ if TYPE_CHECKING:
14
+ from .middleware import ASGIApp, Receive, Scope, Send
15
+
16
+ HOP_HEADERS = {
17
+ b"connection",
18
+ b"keep-alive",
19
+ b"proxy-authenticate",
20
+ b"proxy-authorization",
21
+ b"proxy-connection",
22
+ b"te",
23
+ b"trailer",
24
+ b"transfer-encoding",
25
+ b"upgrade",
26
+ }
27
+
28
+
29
+ class _ClientDisconnected(Exception):
30
+ pass
31
+
32
+
33
+ def path_matches(path: str, prefix: str) -> bool:
34
+ return path == prefix or path.startswith(prefix + "/")
35
+
36
+
37
+ def normalize_http_routes(routes: Any, *, mcp_path: str = "/mcp") -> list[dict[str, Any]]:
38
+ if not isinstance(routes, (list, tuple)):
39
+ raise ValueError("mcp.proxy.http_routes must be an array of tables")
40
+ reserved = (mcp_path.rstrip("/") or "/", "/.well-known", "/snulbug")
41
+ result = []
42
+ prefixes = set()
43
+ for route in routes:
44
+ if not isinstance(route, Mapping) or set(route) - {"prefix", "upstream", "strip_prefix"}:
45
+ raise ValueError("HTTP routes support only prefix, upstream, and strip_prefix")
46
+ prefix = route.get("prefix")
47
+ if (
48
+ not isinstance(prefix, str)
49
+ or not prefix.startswith("/")
50
+ or not prefix.isascii()
51
+ or any(char.isspace() or ord(char) < 32 or ord(char) == 127 for char in prefix)
52
+ or any(char in prefix for char in ("?", "#", "%", "\\"))
53
+ or quote(prefix, safe="/:@!$&'()*+,;=-._~") != prefix
54
+ or "//" in prefix
55
+ or any(segment in {".", ".."} for segment in prefix.split("/"))
56
+ ):
57
+ raise ValueError("HTTP route prefix must be an absolute, unescaped URL path")
58
+ prefix = prefix.rstrip("/")
59
+ if not prefix or any(
60
+ item == "/" or path_matches(prefix, item) or path_matches(item, prefix) for item in reserved
61
+ ):
62
+ raise ValueError("HTTP route prefix overlaps MCP or reserved gateway paths")
63
+ if prefix in prefixes:
64
+ raise ValueError(f"duplicate HTTP route prefix: {prefix}")
65
+ upstream = route.get("upstream")
66
+ if not isinstance(upstream, str) or any(char.isspace() or ord(char) < 32 for char in upstream):
67
+ raise ValueError("HTTP route upstream must be an absolute http:// or https:// URL")
68
+ try:
69
+ parsed = urlsplit(upstream)
70
+ port = parsed.port
71
+ httpx.URL(upstream)
72
+ except (ValueError, httpx.InvalidURL) as exc:
73
+ raise ValueError("invalid HTTP route upstream URL") from exc
74
+ if (
75
+ parsed.scheme not in {"http", "https"}
76
+ or not parsed.hostname
77
+ or parsed.username is not None
78
+ or parsed.password is not None
79
+ or parsed.query
80
+ or parsed.fragment
81
+ or "\\" in upstream
82
+ or port == 0
83
+ ):
84
+ raise ValueError("HTTP route upstream requires HTTP(S), without credentials, query, or fragment")
85
+ strip = route.get("strip_prefix", True)
86
+ if not isinstance(strip, bool):
87
+ raise ValueError("HTTP route strip_prefix must be a boolean")
88
+ prefixes.add(prefix)
89
+ result.append({"prefix": prefix, "upstream": upstream.rstrip("/"), "strip_prefix": strip})
90
+ return result
91
+
92
+
93
+ def parse_http_route(value: str) -> dict[str, Any]:
94
+ prefix, separator, upstream = value.partition("=")
95
+ if not separator:
96
+ raise ValueError("--http-route requires PREFIX=URL, for example /app=http://127.0.0.1:3000")
97
+ return normalize_http_routes([{"prefix": prefix, "upstream": upstream}])[0]
98
+
99
+
100
+ def _end_to_end_headers(headers: Sequence[tuple[bytes, bytes]]) -> list[tuple[bytes, bytes]]:
101
+ connection_tokens = {
102
+ token.strip().lower() for name, value in headers if name.lower() == b"connection" for token in value.split(b",")
103
+ }
104
+ return [(name.lower(), value) for name, value in headers if name.lower() not in HOP_HEADERS | connection_tokens]
105
+
106
+
107
+ class HttpRoutesMiddleware:
108
+ def __init__(
109
+ self,
110
+ app: ASGIApp,
111
+ routes: Sequence[Mapping[str, Any]],
112
+ *,
113
+ mcp_path: str = "/mcp",
114
+ timeout: float = 30.0,
115
+ lease_header: str = "x-snulbug-lease",
116
+ ) -> None:
117
+ self.app = app
118
+ self.routes = sorted(normalize_http_routes(routes, mcp_path=mcp_path), key=lambda r: -len(r["prefix"]))
119
+ if not math.isfinite(timeout) or timeout <= 0:
120
+ raise ValueError("HTTP route timeout must be positive and finite")
121
+ self.timeout = timeout
122
+ self.lease_header = lease_header.lower().encode("ascii")
123
+
124
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
125
+ route = next((route for route in self.routes if path_matches(scope.get("path", ""), route["prefix"])), None)
126
+ if scope["type"] not in {"http", "websocket"} or route is None:
127
+ await self.app(scope, receive, send)
128
+ return
129
+ if scope["type"] == "websocket":
130
+ await send({"type": "websocket.close", "code": 1008, "reason": "HTTP routes do not support WebSockets"})
131
+ return
132
+ raw_path = scope.get("raw_path", scope["path"].encode("utf-8"))
133
+ prefix = route["prefix"].encode("ascii")
134
+ if any(part in {".", ".."} for part in scope["path"].split("/")) or not (
135
+ raw_path == prefix or raw_path.startswith(prefix + b"/")
136
+ ):
137
+ await self._error(send, 400, b"Invalid route path")
138
+ return
139
+ body_done = asyncio.Event()
140
+ proxy = asyncio.create_task(self._forward(route, scope, raw_path, body_done, receive, send))
141
+ disconnected = asyncio.create_task(self._wait_disconnect(receive, body_done))
142
+ try:
143
+ done, _ = await asyncio.wait({proxy, disconnected}, return_when=asyncio.FIRST_COMPLETED)
144
+ if proxy in done:
145
+ await proxy
146
+ finally:
147
+ for task in (proxy, disconnected):
148
+ if not task.done():
149
+ task.cancel()
150
+ await asyncio.gather(proxy, disconnected, return_exceptions=True)
151
+
152
+ async def _wait_disconnect(self, receive: Receive, body_done: asyncio.Event) -> None:
153
+ await body_done.wait()
154
+ while (await receive())["type"] != "http.disconnect":
155
+ pass
156
+
157
+ async def _forward(self, route, scope, raw_path, body_done, receive, send) -> None:
158
+ first = await receive()
159
+ if first["type"] == "http.disconnect":
160
+ return
161
+
162
+ async def body():
163
+ message = first
164
+ while True:
165
+ if message["type"] == "http.disconnect":
166
+ raise _ClientDisconnected
167
+ if not message.get("more_body", False):
168
+ body_done.set()
169
+ yield message.get("body", b"")
170
+ if not message.get("more_body", False):
171
+ return
172
+ message = await receive()
173
+
174
+ content = body()
175
+ if not first.get("body") and not first.get("more_body", False):
176
+ content = b""
177
+ body_done.set()
178
+ base = httpx.URL(route["upstream"])
179
+ suffix = raw_path[len(route["prefix"]) :] if route["strip_prefix"] else raw_path
180
+ target_path = base.raw_path.rstrip(b"/") + (suffix or b"/")
181
+ query = scope.get("query_string", b"")
182
+ target = base.copy_with(raw_path=target_path + (b"?" + query if query else b""))
183
+ headers = self._request_headers(scope, route)
184
+ started = False
185
+ try:
186
+ async with httpx.AsyncClient(timeout=self.timeout, trust_env=False, follow_redirects=False) as client:
187
+ async with client.stream(scope["method"], target, headers=headers, content=content) as response:
188
+ await send(
189
+ {
190
+ "type": "http.response.start",
191
+ "status": response.status_code,
192
+ "headers": _end_to_end_headers(response.headers.raw),
193
+ }
194
+ )
195
+ started = True
196
+ async for chunk in response.aiter_raw():
197
+ await send({"type": "http.response.body", "body": chunk, "more_body": True})
198
+ await send({"type": "http.response.body", "body": b"", "more_body": False})
199
+ except _ClientDisconnected:
200
+ return
201
+ except (httpx.HTTPError, OSError) as exc:
202
+ if started:
203
+ raise
204
+ if isinstance(exc, httpx.TimeoutException):
205
+ await self._error(send, 504, b"HTTP route upstream timed out")
206
+ else:
207
+ await self._error(send, 502, b"HTTP route upstream unavailable")
208
+
209
+ def _request_headers(self, scope: Scope, route: Mapping[str, Any]) -> list[tuple[bytes, bytes]]:
210
+ headers = []
211
+ original_host = b""
212
+ for name, value in _end_to_end_headers(scope.get("headers", [])):
213
+ if name == b"host":
214
+ original_host = value
215
+ elif not (
216
+ name.startswith((b"x-snulbug-", b"x-forwarded-")) or name in {self.lease_header, b"forwarded", b"dpop"}
217
+ ):
218
+ headers.append((name, value))
219
+ if original_host:
220
+ headers.append((b"x-forwarded-host", original_host))
221
+ headers.append((b"x-forwarded-proto", scope.get("scheme", "http").encode("ascii")))
222
+ headers.append((b"x-forwarded-prefix", route["prefix"].encode("ascii")))
223
+ if scope.get("client"):
224
+ headers.append((b"x-forwarded-for", scope["client"][0].encode("ascii")))
225
+ return headers
226
+
227
+ @staticmethod
228
+ async def _error(send: Send, status: int, body: bytes) -> None:
229
+ await send(
230
+ {
231
+ "type": "http.response.start",
232
+ "status": status,
233
+ "headers": [
234
+ (b"content-type", b"text/plain; charset=utf-8"),
235
+ (b"content-length", str(len(body)).encode("ascii")),
236
+ ],
237
+ }
238
+ )
239
+ await send({"type": "http.response.body", "body": body})
@@ -31,6 +31,7 @@ from .credentials import apply_credential_header, credential_metadata, normalize
31
31
  from .events import build_event_dispatcher, decision_console_event
32
32
  from .fabric import annotate_topology_audit, build_fabric_audit_metadata
33
33
  from .fabric_control import summarize_fabric_control_state
34
+ from .http_routes import HttpRoutesMiddleware
34
35
  from .leases import (
35
36
  LeasePolicyConfig,
36
37
  enforce_mcp_lease_policy,
@@ -2944,6 +2945,7 @@ def create_proxy_application(
2944
2945
  *,
2945
2946
  upstream_credential: Mapping[str, Any] | None = None,
2946
2947
  upstreams: Sequence[FacadeUpstream | Mapping[str, Any]] | None = None,
2948
+ http_routes: Sequence[Mapping[str, Any]] = (),
2947
2949
  state_store: PolicyStateStore | None = None,
2948
2950
  state_limits: StateLimits | None = None,
2949
2951
  trace: bool = True,
@@ -3187,6 +3189,10 @@ def create_proxy_application(
3187
3189
  topology_audit=topology_audit,
3188
3190
  control_state_provider=fabric_control_state_provider,
3189
3191
  )
3192
+ if http_routes:
3193
+ app = HttpRoutesMiddleware(
3194
+ app, http_routes, mcp_path=streamable_http_config.endpoint_path, timeout=timeout, lease_header=lease_header
3195
+ )
3190
3196
  return app
3191
3197
 
3192
3198
 
@@ -3196,6 +3202,7 @@ def run_proxy(
3196
3202
  policy: str | Path,
3197
3203
  upstream_credential: Mapping[str, Any] | None = None,
3198
3204
  upstreams: Sequence[FacadeUpstream | Mapping[str, Any]] | None = None,
3205
+ http_routes: Sequence[Mapping[str, Any]] = (),
3199
3206
  host: str = "127.0.0.1",
3200
3207
  port: int = 8080,
3201
3208
  state: str = "memory",
@@ -3276,6 +3283,7 @@ def run_proxy(
3276
3283
  policy,
3277
3284
  upstream_credential=upstream_credential,
3278
3285
  upstreams=upstreams,
3286
+ http_routes=http_routes,
3279
3287
  state_store=_state_store(state),
3280
3288
  trace=trace,
3281
3289
  max_body_bytes=max_body_bytes,
@@ -3365,6 +3373,7 @@ def proxy_config_run_kwargs(
3365
3373
  "upstream": proxy_config["upstream"],
3366
3374
  "upstream_credential": proxy_config.get("upstream_credential"),
3367
3375
  "upstreams": proxy_config["upstreams"],
3376
+ "http_routes": proxy_config.get("http_routes", []),
3368
3377
  "policy": proxy_config["policy"],
3369
3378
  "host": proxy_config["host"],
3370
3379
  "port": proxy_config["port"],