snulbug 0.2.5__tar.gz → 0.2.6__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 (405) hide show
  1. {snulbug-0.2.5 → snulbug-0.2.6}/CHANGELOG.md +15 -0
  2. {snulbug-0.2.5 → snulbug-0.2.6}/PKG-INFO +2 -1
  3. {snulbug-0.2.5 → snulbug-0.2.6}/docs/devcontainers.md +2 -2
  4. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-proxy.md +70 -0
  5. {snulbug-0.2.5 → snulbug-0.2.6}/docs/release.md +2 -2
  6. {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/.devcontainer/devcontainer.json +1 -1
  7. {snulbug-0.2.5 → snulbug-0.2.6}/features/snulbug/README.md +1 -1
  8. {snulbug-0.2.5 → snulbug-0.2.6}/features/snulbug/devcontainer-feature.json +1 -1
  9. {snulbug-0.2.5 → snulbug-0.2.6}/pyproject.toml +2 -1
  10. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/__init__.py +7 -1
  11. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/leases.py +89 -62
  12. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/proxy.py +51 -8
  13. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/recorder.py +3 -2
  14. snulbug-0.2.6/tests/test_trusted_leases.py +611 -0
  15. {snulbug-0.2.5 → snulbug-0.2.6}/.gitignore +0 -0
  16. {snulbug-0.2.5 → snulbug-0.2.6}/CODE_OF_CONDUCT.md +0 -0
  17. {snulbug-0.2.5 → snulbug-0.2.6}/CONTRIBUTING.md +0 -0
  18. {snulbug-0.2.5 → snulbug-0.2.6}/LICENSE +0 -0
  19. {snulbug-0.2.5 → snulbug-0.2.6}/README.md +0 -0
  20. {snulbug-0.2.5 → snulbug-0.2.6}/SECURITY.md +0 -0
  21. {snulbug-0.2.5 → snulbug-0.2.6}/docs/README.md +0 -0
  22. {snulbug-0.2.5 → snulbug-0.2.6}/docs/actions.md +0 -0
  23. {snulbug-0.2.5 → snulbug-0.2.6}/docs/always-on-runtime.md +0 -0
  24. {snulbug-0.2.5 → snulbug-0.2.6}/docs/auth-providers.md +0 -0
  25. {snulbug-0.2.5 → snulbug-0.2.6}/docs/aws-mcp.md +0 -0
  26. {snulbug-0.2.5 → snulbug-0.2.6}/docs/bundles.md +0 -0
  27. {snulbug-0.2.5 → snulbug-0.2.6}/docs/ci-policy-gates.md +0 -0
  28. {snulbug-0.2.5 → snulbug-0.2.6}/docs/comparison.md +0 -0
  29. {snulbug-0.2.5 → snulbug-0.2.6}/docs/credential-brokers.md +0 -0
  30. {snulbug-0.2.5 → snulbug-0.2.6}/docs/discovery-providers.md +0 -0
  31. {snulbug-0.2.5 → snulbug-0.2.6}/docs/event-sink-providers.md +0 -0
  32. {snulbug-0.2.5 → snulbug-0.2.6}/docs/evidence-exporters.md +0 -0
  33. {snulbug-0.2.5 → snulbug-0.2.6}/docs/getting-started.md +0 -0
  34. {snulbug-0.2.5 → snulbug-0.2.6}/docs/lua-policy-dsl.md +0 -0
  35. {snulbug-0.2.5 → snulbug-0.2.6}/docs/lua-request-api.md +0 -0
  36. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-auth-recipes.md +0 -0
  37. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-client-recipes.md +0 -0
  38. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-evidence.md +0 -0
  39. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-fabric.md +0 -0
  40. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-gateway.md +0 -0
  41. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-guide.md +0 -0
  42. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-impact.md +0 -0
  43. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-learn.md +0 -0
  44. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-policy.md +0 -0
  45. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-presets.md +0 -0
  46. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-protocol.md +0 -0
  47. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-recorder.md +0 -0
  48. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-schemas.md +0 -0
  49. {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-share.md +0 -0
  50. {snulbug-0.2.5 → snulbug-0.2.6}/docs/ngrok-end-to-end.md +0 -0
  51. {snulbug-0.2.5 → snulbug-0.2.6}/docs/policy-deny-backoff.md +0 -0
  52. {snulbug-0.2.5 → snulbug-0.2.6}/docs/quickstart.md +0 -0
  53. {snulbug-0.2.5 → snulbug-0.2.6}/docs/roadmap.md +0 -0
  54. {snulbug-0.2.5 → snulbug-0.2.6}/docs/security-model.md +0 -0
  55. {snulbug-0.2.5 → snulbug-0.2.6}/docs/share-doctor-checks.md +0 -0
  56. {snulbug-0.2.5 → snulbug-0.2.6}/docs/state.md +0 -0
  57. {snulbug-0.2.5 → snulbug-0.2.6}/docs/tool-catalog-projection.md +0 -0
  58. {snulbug-0.2.5 → snulbug-0.2.6}/docs/tool-risk-analyzers.md +0 -0
  59. {snulbug-0.2.5 → snulbug-0.2.6}/docs/tunnel-providers.md +0 -0
  60. {snulbug-0.2.5 → snulbug-0.2.6}/docs/upgrading-to-0.2.4.md +0 -0
  61. {snulbug-0.2.5 → snulbug-0.2.6}/docs/upgrading-to-0.2.5.md +0 -0
  62. {snulbug-0.2.5 → snulbug-0.2.6}/docs/upstream-transports.md +0 -0
  63. {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/.gitignore +0 -0
  64. {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/README.md +0 -0
  65. {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/ci-workload-identity.toml +0 -0
  66. {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/group-gated-tools.toml +0 -0
  67. {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/policy.lua +0 -0
  68. {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/tenant-isolation.toml +0 -0
  69. {snulbug-0.2.5 → snulbug-0.2.6}/examples/basic/README.md +0 -0
  70. {snulbug-0.2.5 → snulbug-0.2.6}/examples/basic/app.py +0 -0
  71. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/README.md +0 -0
  72. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/fixtures/duplicate-delivery.json +0 -0
  73. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/fixtures/first-delivery.json +0 -0
  74. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/manifest.json +0 -0
  75. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/policy.lua +0 -0
  76. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/snapshots/duplicate.json +0 -0
  77. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/snapshots/empty.json +0 -0
  78. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/README.md +0 -0
  79. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/fixtures/allowed-tool.json +0 -0
  80. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/fixtures/blocked-tool.json +0 -0
  81. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/fixtures/unauthenticated.json +0 -0
  82. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/manifest.json +0 -0
  83. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/policy.lua +0 -0
  84. {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/snapshots/empty.json +0 -0
  85. {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/README.md +0 -0
  86. {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/mcp-client.json +0 -0
  87. {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/mock_mcp_server.py +0 -0
  88. {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/policy.lua +0 -0
  89. {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/snulbug.env-gateway.toml +0 -0
  90. {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/snulbug.local-gateway.toml +0 -0
  91. {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/README.md +0 -0
  92. {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/app.py +0 -0
  93. {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/policies/acme.lua +0 -0
  94. {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/policies/default.lua +0 -0
  95. {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/policies/globex.lua +0 -0
  96. {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/requests/acme-invalid.json +0 -0
  97. {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/requests/acme-valid.json +0 -0
  98. {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/requests/globex-sandbox.json +0 -0
  99. {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/requests/unknown-tenant.json +0 -0
  100. {snulbug-0.2.5 → snulbug-0.2.6}/examples/github-actions/snulbug-policy-gates.yml +0 -0
  101. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/.gitignore +0 -0
  102. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/Dockerfile.gateway +0 -0
  103. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/README.md +0 -0
  104. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/README.md +0 -0
  105. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/auth-init.json +0 -0
  106. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/client-token-request.json +0 -0
  107. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/commands.json +0 -0
  108. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/snulbug.auth.toml +0 -0
  109. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/docker-compose.yml +0 -0
  110. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/get-token.py +0 -0
  111. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/keycloak/snulbug-demo-realm.json +0 -0
  112. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/mock_mcp_server.py +0 -0
  113. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/policy.snulbug/policy.lua +0 -0
  114. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/requests/blocked-write-file.json +0 -0
  115. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/requests/read-file.json +0 -0
  116. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/requests/tools-list.json +0 -0
  117. {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/snulbug.toml +0 -0
  118. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/Dockerfile.gateway +0 -0
  119. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/Dockerfile.remote-peer +0 -0
  120. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/README.md +0 -0
  121. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/docker-compose.yml +0 -0
  122. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/hypertele-client.json +0 -0
  123. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/hypertele-server.json +0 -0
  124. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/mcp-client.json +0 -0
  125. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/mock_mcp_server.js +0 -0
  126. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/mock_mcp_server.py +0 -0
  127. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/policy.lua +0 -0
  128. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/snulbug.facade.toml +0 -0
  129. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/snulbug.local.toml +0 -0
  130. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_gateway/README.md +0 -0
  131. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_gateway/app.py +0 -0
  132. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/.gitignore +0 -0
  133. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/README.md +0 -0
  134. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/requests/blocked-tool.json +0 -0
  135. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/requests/safe-tool.json +0 -0
  136. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/requests/tools-list.json +0 -0
  137. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/run_demo.py +0 -0
  138. {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/upstream.py +0 -0
  139. {snulbug-0.2.5 → snulbug-0.2.6}/examples/policy_promotion/README.md +0 -0
  140. {snulbug-0.2.5 → snulbug-0.2.6}/examples/policy_promotion/active.lua +0 -0
  141. {snulbug-0.2.5 → snulbug-0.2.6}/examples/policy_promotion/draft.lua +0 -0
  142. {snulbug-0.2.5 → snulbug-0.2.6}/examples/policy_promotion/fixtures/acme-invalid.json +0 -0
  143. {snulbug-0.2.5 → snulbug-0.2.6}/examples/policy_promotion/fixtures/acme-valid.json +0 -0
  144. {snulbug-0.2.5 → snulbug-0.2.6}/examples/provider_policy_templates/README.md +0 -0
  145. {snulbug-0.2.5 → snulbug-0.2.6}/examples/provider_policy_templates/cloudflare-access-group-gate.lua +0 -0
  146. {snulbug-0.2.5 → snulbug-0.2.6}/examples/provider_policy_templates/entra-app-role-gate.lua +0 -0
  147. {snulbug-0.2.5 → snulbug-0.2.6}/examples/provider_policy_templates/github-actions-workload-gate.lua +0 -0
  148. {snulbug-0.2.5 → snulbug-0.2.6}/examples/provider_policy_templates/keycloak-role-gate.lua +0 -0
  149. {snulbug-0.2.5 → snulbug-0.2.6}/examples/state_replay/README.md +0 -0
  150. {snulbug-0.2.5 → snulbug-0.2.6}/examples/state_replay/duplicate-state.json +0 -0
  151. {snulbug-0.2.5 → snulbug-0.2.6}/examples/state_replay/idempotency.lua +0 -0
  152. {snulbug-0.2.5 → snulbug-0.2.6}/examples/state_replay/request.json +0 -0
  153. {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/README.md +0 -0
  154. {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/app.py +0 -0
  155. {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/policies/github.lua +0 -0
  156. {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/policies/stripe.lua +0 -0
  157. {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/policies/unknown.lua +0 -0
  158. {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/requests/github-push.json +0 -0
  159. {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/requests/stripe-invoice.json +0 -0
  160. {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/requests/stripe-malformed.json +0 -0
  161. {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/requests/unknown-vendor.json +0 -0
  162. {snulbug-0.2.5 → snulbug-0.2.6}/features/snulbug/install.sh +0 -0
  163. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/__main__.py +0 -0
  164. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/_operations_smoke.py +0 -0
  165. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/assets/prism.css +0 -0
  166. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/assets/prism.js +0 -0
  167. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/auth_providers.py +0 -0
  168. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/auth_recipes.py +0 -0
  169. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/README.md +0 -0
  170. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/fixtures/authenticated.json +0 -0
  171. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/fixtures/missing-token.json +0 -0
  172. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/manifest.json +0 -0
  173. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/policy.lua +0 -0
  174. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/README.md +0 -0
  175. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/contexts/inventory-lease.json +0 -0
  176. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/contexts/sandbox-change-lease.json +0 -0
  177. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/data-read.json +0 -0
  178. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/iam-delete-user.json +0 -0
  179. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/inventory-read.json +0 -0
  180. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/knowledge-search.json +0 -0
  181. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/missing-token.json +0 -0
  182. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/sandbox-mutation.json +0 -0
  183. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/manifest.json +0 -0
  184. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/policy.lua +0 -0
  185. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/snapshots/empty.json +0 -0
  186. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/README.md +0 -0
  187. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/missing-token.json +0 -0
  188. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/safe-tool.json +0 -0
  189. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/unsafe-tool.json +0 -0
  190. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/manifest.json +0 -0
  191. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/policy.lua +0 -0
  192. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/snapshots/empty.json +0 -0
  193. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/README.md +0 -0
  194. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/missing-token.json +0 -0
  195. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/safe-tool.json +0 -0
  196. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/shell-tool.json +0 -0
  197. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/manifest.json +0 -0
  198. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/policy.lua +0 -0
  199. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/README.md +0 -0
  200. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/absolute-path.json +0 -0
  201. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/allowed-path.json +0 -0
  202. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/traversal-path.json +0 -0
  203. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/unsafe-tool.json +0 -0
  204. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/manifest.json +0 -0
  205. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/policy.lua +0 -0
  206. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/README.md +0 -0
  207. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/missing-token.json +0 -0
  208. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/safe-tool.json +0 -0
  209. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/tools-list.json +0 -0
  210. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/write-method.json +0 -0
  211. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/manifest.json +0 -0
  212. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/policy.lua +0 -0
  213. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/snapshots/empty.json +0 -0
  214. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/README.md +0 -0
  215. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/safe-tool.json +0 -0
  216. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/tools-list.json +0 -0
  217. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/unsafe-tool.json +0 -0
  218. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/manifest.json +0 -0
  219. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/policy.lua +0 -0
  220. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/README.md +0 -0
  221. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/batch-request.json +0 -0
  222. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/missing-token.json +0 -0
  223. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/safe-tool.json +0 -0
  224. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/unsafe-tool.json +0 -0
  225. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/manifest.json +0 -0
  226. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/policy.lua +0 -0
  227. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/snapshots/empty.json +0 -0
  228. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/README.md +0 -0
  229. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/allowed-read.json +0 -0
  230. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/missing-token.json +0 -0
  231. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/secret-path.json +0 -0
  232. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/traversal-path.json +0 -0
  233. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/unsafe-tool.json +0 -0
  234. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/manifest.json +0 -0
  235. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/policy.lua +0 -0
  236. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/bundle.py +0 -0
  237. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/__init__.py +0 -0
  238. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/common.py +0 -0
  239. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/evidence.py +0 -0
  240. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/fabric.py +0 -0
  241. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/policy.py +0 -0
  242. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/rich_reports.py +0 -0
  243. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/rich_share.py +0 -0
  244. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/schemas.py +0 -0
  245. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/share.py +0 -0
  246. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli_helpers.py +0 -0
  247. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cloudflare_access.py +0 -0
  248. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/codespaces.py +0 -0
  249. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/config.py +0 -0
  250. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/confirm.py +0 -0
  251. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/control_events.py +0 -0
  252. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/controller.py +0 -0
  253. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/credential_health.py +0 -0
  254. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/credentials.py +0 -0
  255. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/discovery.py +0 -0
  256. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/events.py +0 -0
  257. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/evidence_exporters.py +0 -0
  258. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/fabric.py +0 -0
  259. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/fabric_control.py +0 -0
  260. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/fabric_members.py +0 -0
  261. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/fabric_runtime.py +0 -0
  262. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/gateway_templates.py +0 -0
  263. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/guide.py +0 -0
  264. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/http_mounts.py +0 -0
  265. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/http_routes.py +0 -0
  266. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/impact.py +0 -0
  267. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/inspection.py +0 -0
  268. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/json_schema.py +0 -0
  269. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/lab.py +0 -0
  270. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/learn.py +0 -0
  271. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/manifests.py +0 -0
  272. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_auth.py +0 -0
  273. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_cache.py +0 -0
  274. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_capabilities.py +0 -0
  275. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_client_requests.py +0 -0
  276. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_completion.py +0 -0
  277. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_mrtr.py +0 -0
  278. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_parameter_headers.py +0 -0
  279. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_policy_bundles.py +0 -0
  280. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_progress.py +0 -0
  281. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_protocol.py +0 -0
  282. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_resources.py +0 -0
  283. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_schema_policy.py +0 -0
  284. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_schemas.py +0 -0
  285. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_spec_conformance.py +0 -0
  286. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_stdio.py +0 -0
  287. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_stream.py +0 -0
  288. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_subscriptions.py +0 -0
  289. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_tasks.py +0 -0
  290. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_tools.py +0 -0
  291. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/middleware.py +0 -0
  292. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/ngrok_drift.py +0 -0
  293. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/policy_activation.py +0 -0
  294. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/policy_backoff.py +0 -0
  295. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/policy_review.py +0 -0
  296. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/presets.py +0 -0
  297. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/promotion.py +0 -0
  298. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/public_origin.py +0 -0
  299. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/py.typed +0 -0
  300. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/quickstart.py +0 -0
  301. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/redaction.py +0 -0
  302. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/release_qa.py +0 -0
  303. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/request_explanations.py +0 -0
  304. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/response_policy.py +0 -0
  305. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/runtime.py +0 -0
  306. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/sarif.py +0 -0
  307. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/scaffolds.py +0 -0
  308. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/schema_policy.py +0 -0
  309. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/share.py +0 -0
  310. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/share_console.py +0 -0
  311. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/share_doctor.py +0 -0
  312. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/share_runtime.py +0 -0
  313. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/share_session.py +0 -0
  314. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/simulator.py +0 -0
  315. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/state.py +0 -0
  316. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/tool_catalog.py +0 -0
  317. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/tool_risk.py +0 -0
  318. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/tunnel.py +0 -0
  319. {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/upstream_transports.py +0 -0
  320. {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/LICENSE +0 -0
  321. {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/README.md +0 -0
  322. {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/dependentSchemas.json +0 -0
  323. {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/dynamicRef.json +0 -0
  324. {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/if-then-else.json +0 -0
  325. {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/prefixItems.json +0 -0
  326. {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/unevaluatedProperties.json +0 -0
  327. {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/pocket_id/README.md +0 -0
  328. {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/pocket_id/oauth.json +0 -0
  329. {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/stdio_subscriptions.py +0 -0
  330. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_auth_claim_patterns.py +0 -0
  331. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_auth_recipes.py +0 -0
  332. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_automation_invites.py +0 -0
  333. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_cli_helpers.py +0 -0
  334. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_cloudflare_access.py +0 -0
  335. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_codespaces.py +0 -0
  336. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_config.py +0 -0
  337. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_credential_health.py +0 -0
  338. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_credentials.py +0 -0
  339. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_customer_policy_demo.py +0 -0
  340. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_devcontainer_feature.py +0 -0
  341. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_discovery.py +0 -0
  342. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_events.py +0 -0
  343. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_evidence_exporters.py +0 -0
  344. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_fabric.py +0 -0
  345. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_fabric_controller.py +0 -0
  346. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_gateway_snapshot.py +0 -0
  347. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_http_mounts.py +0 -0
  348. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_http_routes.py +0 -0
  349. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_json_schema.py +0 -0
  350. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_keycloak_oauth_demo.py +0 -0
  351. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_leases.py +0 -0
  352. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_manifests.py +0 -0
  353. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_cache.py +0 -0
  354. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_gateway_demo.py +0 -0
  355. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_guide.py +0 -0
  356. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_helpers.py +0 -0
  357. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_impact.py +0 -0
  358. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_inspection.py +0 -0
  359. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_lab.py +0 -0
  360. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_learn.py +0 -0
  361. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_mrtr.py +0 -0
  362. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_parameter_headers.py +0 -0
  363. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_policy_bundles.py +0 -0
  364. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_presets.py +0 -0
  365. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_progress.py +0 -0
  366. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_protocol.py +0 -0
  367. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_proxy_demo.py +0 -0
  368. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_quickstart.py +0 -0
  369. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_recorder.py +0 -0
  370. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_resources.py +0 -0
  371. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_schema_policy.py +0 -0
  372. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_schemas.py +0 -0
  373. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_share.py +0 -0
  374. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_spec_conformance.py +0 -0
  375. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_stdio.py +0 -0
  376. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_stream.py +0 -0
  377. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_subscriptions.py +0 -0
  378. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_tools.py +0 -0
  379. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_middleware.py +0 -0
  380. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_ngrok_drift.py +0 -0
  381. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_operations_smoke.py +0 -0
  382. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_pocket_id_auth.py +0 -0
  383. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_policy_bundle.py +0 -0
  384. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_policy_promotion.py +0 -0
  385. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_policy_state.py +0 -0
  386. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_provider_policy_templates.py +0 -0
  387. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_proxy.py +0 -0
  388. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_public_origin.py +0 -0
  389. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_redaction_audit.py +0 -0
  390. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_release_qa.py +0 -0
  391. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_request_explanations.py +0 -0
  392. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_response_policy.py +0 -0
  393. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_sandbox_abuse.py +0 -0
  394. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_scaffolds.py +0 -0
  395. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_share_console.py +0 -0
  396. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_share_runtime.py +0 -0
  397. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_state_snapshots.py +0 -0
  398. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_tool_catalog.py +0 -0
  399. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_tool_risk.py +0 -0
  400. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_tunnel_audit.py +0 -0
  401. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_tunnel_doctor.py +0 -0
  402. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_tunnel_init.py +0 -0
  403. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_upstream_transports.py +0 -0
  404. {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_webhook_normalization_demo.py +0 -0
  405. {snulbug-0.2.5 → snulbug-0.2.6}/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.6] - 2026-10-24
10
+
11
+ Trusted internal lease selection and gateway credential-boundary hardening.
12
+
13
+ - Add trusted, request-local Python/ASGI lease-ID selection for embedding gateways, reusing identity bindings, expiry/revocation checks, policy previews, and atomic call budgets. Reject conflicting selections without recovering or injecting lease credentials.
14
+ - Consume lease credential headers at the upstream boundary, including custom header names, facade transport plugins, and streamed HTTP requests. Include configured custom lease-header names in replay/audit redaction.
15
+ - Validate supplied selections on facade discovery without consuming calls, and reject lease denials on bodyless HTTP stream opens before forwarding.
16
+ - Require certifi 2026.10.19 or later and update the lockfile to remove compromised emSign CA trust roots ([GHSA-6q48-6c7w-9gw2](https://github.com/advisories/GHSA-6q48-6c7w-9gw2)).
17
+
18
+ Upgrade note: public clients continue to send their existing lease headers, but
19
+ upstream applications no longer receive them. Keep lease enforcement at the
20
+ gateway; embedding hosts must independently authorize any internally selected
21
+ lease reference. No change to renewal/expiry configuration or MCP protocol defaults.
22
+ See the [trusted selection integration guide](docs/mcp-proxy.md#trusted-internal-lease-selection).
23
+
9
24
  ## [0.2.5] - 2026-10-23
10
25
 
11
26
  Public-origin preservation for shared MCP and HTTP gateways.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: snulbug
3
- Version: 0.2.5
3
+ Version: 0.2.6
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
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Topic :: Internet :: WWW/HTTP
24
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
25
  Requires-Python: >=3.10
26
+ Requires-Dist: certifi>=2026.10.19
26
27
  Requires-Dist: httpx<1,>=0.28
27
28
  Requires-Dist: jsonschema<5,>=4.23
28
29
  Requires-Dist: lupa>=2.5
@@ -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.5": {
14
- "version": "0.2.5",
13
+ "ghcr.io/nahuaque/snulbug/features/snulbug:0.2.6": {
14
+ "version": "0.2.6",
15
15
  "mode": "member-agent",
16
16
  "policy_profile": "tunnel-safe",
17
17
  "registry": "redis://redis:6379/0",
@@ -996,6 +996,13 @@ Send the returned `x-snulbug-lease` header with MCP requests. The proxy hot-load
996
996
  the JSON file on each call, so new leases and revocations do not require a proxy
997
997
  restart.
998
998
 
999
+ The lease credential terminates at snulbug. The default and configured lease
1000
+ headers are stripped before upstream HTTP forwarding, including streaming and
1001
+ facade transports; upstream applications do not receive the client's lease token.
1002
+ An embedding gateway can instead select an exact lease reference through the
1003
+ [trusted Python/ASGI selection API](#trusted-internal-lease-selection), without
1004
+ recovering a credential or injecting a synthetic header.
1005
+
999
1006
  Create an invite when you want snulbug to package the share URL, bearer header,
1000
1007
  lease header, and downstream client snippets together:
1001
1008
 
@@ -1644,3 +1651,66 @@ application = create_proxy_application(
1644
1651
 
1645
1652
  You can run that ASGI app with Uvicorn, Hypercorn, Daphne, or another ASGI
1646
1653
  server.
1654
+
1655
+ ### Trusted Internal Lease Selection
1656
+
1657
+ A hosting gateway that already authenticates a caller and authorizes a task can
1658
+ pass its exact lease ID in request-local ASGI context. Configure the existing
1659
+ lease store and policy normally:
1660
+
1661
+ ```python
1662
+ from snulbug import create_proxy_application, select_mcp_lease
1663
+
1664
+ proxy = create_proxy_application(
1665
+ "http://127.0.0.1:9000",
1666
+ "policy.snulbug/policy.lua",
1667
+ lease_file="leases.json",
1668
+ lease_required=True,
1669
+ )
1670
+
1671
+
1672
+ def with_task_selection(proxy, resolve_authorized_lease):
1673
+ async def application(scope, receive, send):
1674
+ if scope["type"] == "http":
1675
+ # The host authenticates the caller and authorizes this exact task.
1676
+ lease_id = await resolve_authorized_lease(scope)
1677
+ if lease_id is not None:
1678
+ scope = select_mcp_lease(scope, lease_id=lease_id)
1679
+ await proxy(scope, receive, send)
1680
+
1681
+ return application
1682
+ ```
1683
+
1684
+ `resolve_authorized_lease` belongs to the hosting application. It must return only
1685
+ an authorized reference, not an arbitrary caller-selected ID. Public headers,
1686
+ query parameters, request bodies, Lua context, and replay logs never populate
1687
+ trusted selection automatically. Do not copy any of them into this context
1688
+ without independent authentication and authorization. This is an in-process
1689
+ integration boundary, not protection against malicious Python code in the host.
1690
+
1691
+ `select_mcp_lease(scope, lease_id=...)` returns a shallow scope copy containing a
1692
+ frozen `TrustedLeaseSelection` at `TRUSTED_LEASE_SCOPE_KEY`. A trusted ASGI
1693
+ integration can also set that typed value directly; strings and JSON mappings at
1694
+ the key fail closed. Selecting a different ID on an already selected scope raises
1695
+ `ValueError`. No global or session selection is retained.
1696
+
1697
+ Selection is **not approval**. Snulbug resolves the exact ID from its configured
1698
+ store and runs the same identity bindings, expiry, revocation, tool/argument
1699
+ restrictions, and call-budget checks as header-based requests. Existing OAuth and
1700
+ Lua checks remain in force. Discovery and Lua/catalog previews do not consume
1701
+ calls; final `tools/call` admission consumes once under the existing store lock.
1702
+ No new lease, credential recovery, renewal, or TTL extension is performed.
1703
+
1704
+ If a lease header is also supplied, it must validate to the selected lease.
1705
+ Different or invalid credentials and duplicate headers are rejected, even when
1706
+ leases are optional. An unknown ID never falls back to another available lease.
1707
+ Denials use `lease.selection_conflict`, `lease.selection_invalid`, or the existing
1708
+ lease reason codes. Lease audit metadata identifies `selection = "trusted"` or
1709
+ `"header"` and retains the lease/task/invite references without a plaintext token.
1710
+
1711
+ The original scope headers remain available inside snulbug for preview and audit
1712
+ redaction, including configured custom lease-header names, but are filtered at
1713
+ the outbound boundary. Facade transport plugins
1714
+ receive a scope without the lease credential headers or trusted selection object.
1715
+ Business task selection and grant/revision permissions remain the host's concern;
1716
+ snulbug continues to own lease enforcement and accounting.
@@ -59,7 +59,7 @@ python -m zipfile -l dist/snulbug-*.whl
59
59
  8. Install from TestPyPI in a clean environment and smoke test:
60
60
 
61
61
  ```bash
62
- uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ snulbug==0.2.5 --help
62
+ uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ snulbug==0.2.6 --help
63
63
  ```
64
64
 
65
65
  9. Run the manual `Publish` workflow with `repository = pypi` when the TestPyPI
@@ -67,4 +67,4 @@ uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org
67
67
  10. Create the GitHub release and tag after the PyPI package is available.
68
68
 
69
69
  The `Publish` workflow uses `workflow_dispatch`, not a tag trigger. Creating or
70
- pushing `v0.2.5` does not publish a package or automatically create a GitHub release.
70
+ pushing `v0.2.6` 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.5": {
5
+ "ghcr.io/nahuaque/snulbug/features/snulbug:0.2.6": {
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.5": {
9
+ "ghcr.io/nahuaque/snulbug/features/snulbug:0.2.6": {
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.5",
3
+ "version": "0.2.6",
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.5"
3
+ version = "0.2.6"
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"
@@ -35,6 +35,7 @@ classifiers = [
35
35
  "Topic :: Software Development :: Libraries :: Python Modules",
36
36
  ]
37
37
  dependencies = [
38
+ "certifi>=2026.10.19",
38
39
  "httpx>=0.28,<1",
39
40
  "jsonschema>=4.23,<5",
40
41
  "lupa>=2.5",
@@ -1,6 +1,6 @@
1
1
  """Local-dev MCP policy proxy and programmable Lua ASGI policy layer."""
2
2
 
3
- __version__ = "0.2.5"
3
+ __version__ = "0.2.6"
4
4
 
5
5
  from .auth_providers import (
6
6
  AuthProvider,
@@ -201,13 +201,16 @@ from .inspection import format_mcp_inspection_report, inspect_mcp_log
201
201
  from .lab import format_mcp_auth_lab_report, run_mcp_auth_lab, run_mcp_lab
202
202
  from .learn import amend_mcp_policy, learn_mcp_policy
203
203
  from .leases import (
204
+ TRUSTED_LEASE_SCOPE_KEY,
204
205
  LeasePolicyConfig,
206
+ TrustedLeaseSelection,
205
207
  cleanup_inactive_leases,
206
208
  create_lease,
207
209
  list_leases,
208
210
  reactivate_lease,
209
211
  renew_lease,
210
212
  revoke_lease,
213
+ select_mcp_lease,
211
214
  )
212
215
  from .manifests import load_manifest, manifest_digest, sign_upstream_manifest, verify_upstream_manifest, write_manifest
213
216
  from .mcp_auth import (
@@ -500,6 +503,8 @@ __all__ = [
500
503
  "LuaMiddleware",
501
504
  "LuaRuntimeError",
502
505
  "LeasePolicyConfig",
506
+ "TrustedLeaseSelection",
507
+ "TRUSTED_LEASE_SCOPE_KEY",
503
508
  "LIFECYCLE_STATES",
504
509
  "MemoryFabricRuntimeStateStore",
505
510
  "MemoryStateStore",
@@ -764,6 +769,7 @@ __all__ = [
764
769
  "render_gateway_toml",
765
770
  "replay_record_log",
766
771
  "revoke_lease",
772
+ "select_mcp_lease",
767
773
  "revoke_mcp_share_invite",
768
774
  "revoke_mcp_share_lease",
769
775
  "run_mcp_auth_lab",
@@ -24,6 +24,7 @@ LEASE_HEADER = "x-snulbug-lease"
24
24
  LEASE_TOKEN_PREFIX = "sbl_"
25
25
  LEASE_RENEWAL_METHOD = "snulbug/lease/renew"
26
26
  LEASE_RENEWAL_HEADER = "x-snulbug-renewal"
27
+ TRUSTED_LEASE_SCOPE_KEY = "snulbug.trusted_lease"
27
28
  _STORE_LOCK = threading.RLock()
28
29
 
29
30
 
@@ -91,6 +92,35 @@ class LeasePolicyConfig:
91
92
  header: str = LEASE_HEADER
92
93
 
93
94
 
95
+ @dataclass(frozen=True)
96
+ class TrustedLeaseSelection:
97
+ """An exact lease reference selected by trusted, in-process gateway code."""
98
+
99
+ lease_id: str
100
+
101
+ def __post_init__(self) -> None:
102
+ if (
103
+ not isinstance(self.lease_id, str)
104
+ or not self.lease_id
105
+ or len(self.lease_id) > 256
106
+ or any(character.isspace() or ord(character) < 32 or ord(character) == 127 for character in self.lease_id)
107
+ ):
108
+ raise ValueError("lease_id must be a non-empty, bounded lease reference without whitespace")
109
+
110
+
111
+ def select_mcp_lease(scope: Scope, *, lease_id: str) -> Scope:
112
+ """Copy a request scope with a trusted selection; does not authorize or consume it.
113
+
114
+ Call only after the hosting gateway authorizes the selection. Never populate
115
+ this context from an unvalidated public header, query parameter, or body.
116
+ """
117
+
118
+ selection = TrustedLeaseSelection(lease_id)
119
+ if TRUSTED_LEASE_SCOPE_KEY in scope and scope[TRUSTED_LEASE_SCOPE_KEY] != selection:
120
+ raise ValueError("request already has a different or invalid trusted lease selection")
121
+ return {**scope, TRUSTED_LEASE_SCOPE_KEY: selection}
122
+
123
+
94
124
  @_lease_transaction
95
125
  def create_lease(
96
126
  path: str | Path,
@@ -411,38 +441,16 @@ def preview_mcp_lease_catalog(
411
441
  if config.lease_file is None:
412
442
  return metadata
413
443
 
414
- token = _header_value(scope, config.header)
415
- if token is None:
416
- metadata["skipped"] = "missing_header"
417
- if config.required:
444
+ _store, lease = _resolve_presented_lease(scope, config=config, metadata=metadata)
445
+ metadata["catalog_checked"] = metadata["checked"]
446
+ if lease is None:
447
+ if config.required and not metadata.get("blocked"):
418
448
  metadata["reason_code"] = "lease.missing"
419
449
  metadata["blocked"] = True
450
+ if metadata.get("blocked"):
420
451
  metadata["allowed"] = False
421
452
  return metadata
422
453
 
423
- metadata["checked"] = True
424
- metadata["catalog_checked"] = True
425
- try:
426
- store = _load_store(config.lease_file, create_missing=False)
427
- except FileNotFoundError:
428
- metadata["reason_code"] = "lease.store_missing"
429
- metadata["blocked"] = True
430
- metadata["allowed"] = False
431
- return metadata
432
- except ValueError as exc:
433
- metadata["reason_code"] = "lease.store_invalid"
434
- metadata["error"] = str(exc)
435
- metadata["blocked"] = True
436
- metadata["allowed"] = False
437
- return metadata
438
-
439
- lease = _find_lease(store, token)
440
- if lease is None:
441
- metadata["reason_code"] = "lease.invalid"
442
- metadata["blocked"] = True
443
- metadata["allowed"] = False
444
- return metadata
445
-
446
454
  invite_metadata = _lease_invite_metadata(lease.get("invite"))
447
455
  metadata.update(
448
456
  {
@@ -502,33 +510,12 @@ def _evaluate_locked_lease_policy(
502
510
  if config.lease_file is None:
503
511
  return True, metadata
504
512
 
505
- token = _header_value(scope, config.header)
506
- if token is None:
507
- metadata["skipped"] = "missing_header"
508
- if config.required and method == "tools/call":
513
+ store, lease = _resolve_presented_lease(scope, config=config, metadata=metadata)
514
+ if lease is None:
515
+ if config.required and method == "tools/call" and not metadata.get("blocked"):
509
516
  metadata["reason_code"] = "lease.missing"
510
517
  metadata["blocked"] = True
511
- return False, metadata
512
- return True, metadata
513
-
514
- metadata["checked"] = True
515
- try:
516
- store = _load_store(config.lease_file, create_missing=False)
517
- except FileNotFoundError:
518
- metadata["reason_code"] = "lease.store_missing"
519
- metadata["blocked"] = True
520
- return False, metadata
521
- except ValueError as exc:
522
- metadata["reason_code"] = "lease.store_invalid"
523
- metadata["error"] = str(exc)
524
- metadata["blocked"] = True
525
- return False, metadata
526
-
527
- lease = _find_lease(store, token)
528
- if lease is None:
529
- metadata["reason_code"] = "lease.invalid"
530
- metadata["blocked"] = True
531
- return False, metadata
518
+ return not metadata.get("blocked", False), metadata
532
519
 
533
520
  invite_metadata = _lease_invite_metadata(lease.get("invite"))
534
521
  metadata.update(
@@ -543,7 +530,7 @@ def _evaluate_locked_lease_policy(
543
530
  **_lease_auth_metadata(lease, auth_context),
544
531
  }
545
532
  )
546
- params = request.get("params")
533
+ params = request.get("params") if isinstance(request, Mapping) else None
547
534
  params = params if isinstance(params, Mapping) else {}
548
535
  tool = params.get("name")
549
536
  if isinstance(tool, str):
@@ -566,6 +553,55 @@ def _evaluate_locked_lease_policy(
566
553
  return True, metadata
567
554
 
568
555
 
556
+ def _resolve_presented_lease(
557
+ scope: Scope,
558
+ *,
559
+ config: LeasePolicyConfig,
560
+ metadata: dict[str, Any],
561
+ ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
562
+ """Resolve references inside the caller's existing consumption transaction."""
563
+
564
+ trusted = scope.get(TRUSTED_LEASE_SCOPE_KEY)
565
+ target = config.header.lower().encode("latin-1")
566
+ values = [value.decode("latin-1").strip() for name, value in scope.get("headers", []) if name.lower() == target]
567
+ reason = None
568
+ if TRUSTED_LEASE_SCOPE_KEY in scope and not isinstance(trusted, TrustedLeaseSelection):
569
+ reason = "lease.selection_invalid"
570
+ elif len(values) > 1:
571
+ reason = "lease.selection_conflict"
572
+ elif values and not values[0]:
573
+ reason = "lease.invalid"
574
+ elif not values and trusted is None:
575
+ metadata["skipped"] = "missing_header"
576
+ return None, None
577
+ metadata["checked"] = True
578
+ metadata["selection"] = "trusted" if trusted is not None else "header"
579
+ store = None
580
+ lease = None
581
+ if reason is None:
582
+ try:
583
+ store = _load_store(config.lease_file, create_missing=False)
584
+ except FileNotFoundError:
585
+ reason = "lease.store_missing"
586
+ except ValueError as exc:
587
+ reason = "lease.store_invalid"
588
+ metadata["error"] = str(exc)
589
+ else:
590
+ if trusted is not None:
591
+ matches = [item for item in _leases(store) if item.get("id") == trusted.lease_id]
592
+ lease = matches[0] if len(matches) == 1 else None
593
+ else:
594
+ lease = _find_lease(store, values[0])
595
+ if lease is None:
596
+ reason = "lease.invalid"
597
+ elif trusted is not None and values and _find_lease(store, values[0]) is not lease:
598
+ reason = "lease.selection_conflict"
599
+ if reason is not None:
600
+ metadata.update(reason_code=reason, blocked=True)
601
+ return None, None
602
+ return store, lease
603
+
604
+
569
605
  def preview_mcp_lease_coverage(
570
606
  request: Mapping[str, Any] | None,
571
607
  path: str | Path,
@@ -983,15 +1019,6 @@ def _drop_empty(value: Mapping[str, Any]) -> dict[str, Any]:
983
1019
  return {str(key): item for key, item in value.items() if item not in (None, [], {}, "")}
984
1020
 
985
1021
 
986
- def _header_value(scope: Scope, name: str) -> str | None:
987
- target = name.lower().encode("latin-1")
988
- for raw_name, raw_value in scope.get("headers", []):
989
- if raw_name.lower() == target:
990
- value = raw_value.decode("latin-1").strip()
991
- return value or None
992
- return None
993
-
994
-
995
1022
  def _tool_allowed(tool: str, allow_tools: Any) -> bool:
996
1023
  if not isinstance(allow_tools, Sequence) or isinstance(allow_tools, str | bytes | bytearray):
997
1024
  return False
@@ -33,8 +33,10 @@ from .fabric import annotate_topology_audit, build_fabric_audit_metadata
33
33
  from .fabric_control import summarize_fabric_control_state
34
34
  from .http_routes import HttpRoutesMiddleware
35
35
  from .leases import (
36
+ LEASE_HEADER,
36
37
  LEASE_RENEWAL_HEADER,
37
38
  LEASE_RENEWAL_METHOD,
39
+ TRUSTED_LEASE_SCOPE_KEY,
38
40
  LeasePolicyConfig,
39
41
  enforce_mcp_lease_policy,
40
42
  mcp_lease_error_response,
@@ -98,6 +100,7 @@ from .middleware import ASGIApp, LuaConfig, LuaMiddleware, Receive, Scope, Send
98
100
  from .policy_backoff import PolicyBackoffConfig
99
101
  from .public_origin import PublicOriginMiddleware
100
102
  from .recorder import append_record, build_request_record, record_audit_event
103
+ from .redaction import RedactionConfig, redact_secrets
101
104
  from .request_explanations import (
102
105
  upstream_failure,
103
106
  upstream_failure_message,
@@ -303,8 +306,10 @@ class ReverseProxyApp:
303
306
  scope,
304
307
  config=self.lease_policy,
305
308
  )
306
- if not lease_allowed and isinstance(request, Mapping):
307
- response = mcp_lease_error_response(request, lease_metadata)
309
+ if not lease_allowed:
310
+ response = mcp_lease_error_response(request or {}, lease_metadata)
311
+ if request is None:
312
+ response["status"] = 403
308
313
  _set_proxy_metadata(
309
314
  scope,
310
315
  {
@@ -429,6 +434,7 @@ class ReverseProxyApp:
429
434
  self._target(scope),
430
435
  credential=self.upstream_credential,
431
436
  timeout=self.config.timeout,
437
+ lease_header=self.lease_policy.header,
432
438
  )
433
439
  else:
434
440
  response = await asyncio.to_thread(self._forward, scope, body)
@@ -515,7 +521,7 @@ class ReverseProxyApp:
515
521
  connection = self._connection()
516
522
  try:
517
523
  target = self._target(scope)
518
- headers = _request_headers(scope.get("headers", []), self._upstream)
524
+ headers = _request_headers(scope.get("headers", []), self._upstream, lease_header=self.lease_policy.header)
519
525
  request = _jsonrpc_request(body)
520
526
  if is_modern_request(request):
521
527
  headers = modern_request_headers(headers, request)
@@ -1055,6 +1061,18 @@ class McpFacadeProxyApp:
1055
1061
  )
1056
1062
  return
1057
1063
 
1064
+ # Tool execution is charged later, after route admission. Discovery
1065
+ # and other methods validate a supplied selection without charging it.
1066
+ if request.get("method") != "tools/call":
1067
+ lease_allowed, lease_metadata = enforce_mcp_lease_policy(request, scope, config=self.lease_policy)
1068
+ _set_proxy_metadata(
1069
+ scope,
1070
+ {"lease": lease_metadata, "access": _composed_access_metadata(scope, lease=lease_metadata)},
1071
+ )
1072
+ if not lease_allowed:
1073
+ await _send_response(send, **mcp_lease_error_response(request, lease_metadata))
1074
+ return
1075
+
1058
1076
  progress_allowed, progress_request_metadata = enforce_mcp_progress_request_policy(
1059
1077
  request,
1060
1078
  config=self.progress_policy,
@@ -1864,6 +1882,8 @@ class McpFacadeProxyApp:
1864
1882
  body: bytes,
1865
1883
  request: Mapping[str, Any],
1866
1884
  ) -> dict[str, Any]:
1885
+ scope = {**scope, "headers": _strip_lease_headers(scope.get("headers", []), self.lease_policy.header)}
1886
+ scope.pop(TRUSTED_LEASE_SCOPE_KEY, None)
1867
1887
  transport = get_upstream_transport(upstream.transport)
1868
1888
  if is_modern_request(request) and transport.http_target(upstream) is None and upstream.transport != "stdio":
1869
1889
  return _json_response(
@@ -1887,6 +1907,7 @@ class McpFacadeProxyApp:
1887
1907
  _exact_target(routes.parsed[upstream.name]),
1888
1908
  credential=upstream.credential,
1889
1909
  timeout=self.timeout,
1910
+ lease_header=self.lease_policy.header,
1890
1911
  )
1891
1912
  if scope.get(STREAM_SCOPE_KEY) is not None
1892
1913
  else asyncio.to_thread(self._forward_http, routes, upstream, scope, body)
@@ -1912,7 +1933,9 @@ class McpFacadeProxyApp:
1912
1933
  parsed = routes.parsed[upstream.name]
1913
1934
  connection = _connection(parsed, self.timeout)
1914
1935
  try:
1915
- headers = _request_headers(scope.get("headers", []), parsed, content_length=len(body))
1936
+ headers = _request_headers(
1937
+ scope.get("headers", []), parsed, content_length=len(body), lease_header=self.lease_policy.header
1938
+ )
1916
1939
  request = _jsonrpc_request(body)
1917
1940
  if is_modern_request(request):
1918
1941
  headers = modern_request_headers(headers, request)
@@ -2007,6 +2030,7 @@ class ProxyRecorderMiddleware:
2007
2030
  policy: str | Path,
2008
2031
  record_out: str | Path | None = None,
2009
2032
  redact_records: bool = True,
2033
+ lease_header: str = LEASE_HEADER,
2010
2034
  tunnel_audit: TunnelAuditConfig | None = None,
2011
2035
  topology_audit: Mapping[str, Any] | None = None,
2012
2036
  event_dispatcher: Any = None,
@@ -2015,6 +2039,8 @@ class ProxyRecorderMiddleware:
2015
2039
  self.policy = policy
2016
2040
  self.record_out = Path(record_out) if record_out is not None else None
2017
2041
  self.redact_records = redact_records
2042
+ self.redaction_config = RedactionConfig()
2043
+ self.redaction_config.secret_keys.add(lease_header.lower())
2018
2044
  self.tunnel_audit = tunnel_audit or TunnelAuditConfig()
2019
2045
  self.topology_audit = dict(topology_audit or {})
2020
2046
  self.event_dispatcher = event_dispatcher
@@ -2047,12 +2073,13 @@ class ProxyRecorderMiddleware:
2047
2073
  metadata=metadata,
2048
2074
  redact=self.redact_records
2049
2075
  or any(name.lower() == LEASE_RENEWAL_HEADER.encode() for name, _value in scope.get("headers", [])),
2076
+ redaction_config=self.redaction_config,
2050
2077
  )
2051
2078
  # Renewal is an audited control operation, not a replayable Lua decision.
2052
2079
  if self.record_out is not None and "lease_renewal" not in metadata:
2053
2080
  append_record(self.record_out, record)
2054
2081
  if self.event_dispatcher is not None:
2055
- audit_event = record_audit_event(record)
2082
+ audit_event = redact_secrets(record_audit_event(record), self.redaction_config)
2056
2083
  self.event_dispatcher.emit(decision_console_event(record, audit_event=audit_event))
2057
2084
 
2058
2085
  def _enabled(self) -> bool:
@@ -3354,6 +3381,7 @@ def create_proxy_application(
3354
3381
  policy=policy,
3355
3382
  record_out=record_out,
3356
3383
  redact_records=redact_records,
3384
+ lease_header=lease_header,
3357
3385
  tunnel_audit=TunnelAuditConfig(provider=tunnel_provider, public_url=tunnel_public_url),
3358
3386
  topology_audit=topology_audit,
3359
3387
  event_dispatcher=event_dispatcher,
@@ -4445,9 +4473,10 @@ def _request_headers(
4445
4473
  upstream: SplitResult,
4446
4474
  *,
4447
4475
  content_length: int | None = None,
4476
+ lease_header: str = LEASE_HEADER,
4448
4477
  ) -> dict[str, str]:
4449
4478
  headers: dict[str, str] = {}
4450
- for name, value in raw_headers:
4479
+ for name, value in _strip_lease_headers(raw_headers, lease_header):
4451
4480
  lower_name = name.lower()
4452
4481
  if lower_name in HOP_BY_HOP_HEADERS or lower_name in {b"host", b"content-length", b"x-snulbug-renewal"}:
4453
4482
  continue
@@ -4458,6 +4487,11 @@ def _request_headers(
4458
4487
  return headers
4459
4488
 
4460
4489
 
4490
+ def _strip_lease_headers(raw_headers: list[tuple[bytes, bytes]], header: str) -> list[tuple[bytes, bytes]]:
4491
+ sensitive = {LEASE_HEADER.encode("ascii"), header.lower().encode("latin-1"), LEASE_RENEWAL_HEADER.encode("ascii")}
4492
+ return [(name, value) for name, value in raw_headers if name.lower() not in sensitive]
4493
+
4494
+
4461
4495
  def _strip_cloudflare_access_credentials(raw_headers: Any) -> list[tuple[bytes, bytes]]:
4462
4496
  stripped = []
4463
4497
  sensitive = {
@@ -4835,9 +4869,18 @@ def _headers_to_mapping(headers: list[tuple[bytes, bytes]]) -> dict[str, str | l
4835
4869
  return result
4836
4870
 
4837
4871
 
4838
- async def _forward_stream_http(scope: Scope, body: bytes, parsed: Any, target: str, *, credential: Any, timeout: float):
4872
+ async def _forward_stream_http(
4873
+ scope: Scope,
4874
+ body: bytes,
4875
+ parsed: Any,
4876
+ target: str,
4877
+ *,
4878
+ credential: Any,
4879
+ timeout: float,
4880
+ lease_header: str = LEASE_HEADER,
4881
+ ):
4839
4882
  request = _jsonrpc_request(body)
4840
- headers = _request_headers(scope.get("headers", []), parsed, content_length=len(body))
4883
+ headers = _request_headers(scope.get("headers", []), parsed, content_length=len(body), lease_header=lease_header)
4841
4884
  headers = modern_request_headers(headers, request)
4842
4885
  headers = apply_credential_header(headers, credential)
4843
4886
  return await scope[STREAM_SCOPE_KEY].forward_http(
@@ -6,7 +6,7 @@ from datetime import datetime, timezone
6
6
  from pathlib import Path
7
7
  from typing import Any
8
8
 
9
- from .redaction import build_audit_event, redact_secrets
9
+ from .redaction import RedactionConfig, build_audit_event, redact_secrets
10
10
  from .simulator import simulate_policy
11
11
 
12
12
  RECORD_TYPE = "snulbug.request_record"
@@ -60,6 +60,7 @@ def build_request_record(
60
60
  metadata: Mapping[str, Any] | None = None,
61
61
  recorded_at: str | None = None,
62
62
  redact: bool = False,
63
+ redaction_config: RedactionConfig | None = None,
63
64
  ) -> dict[str, Any]:
64
65
  """Build a replayable request record from an already executed policy result."""
65
66
 
@@ -80,7 +81,7 @@ def build_request_record(
80
81
  if metadata is not None:
81
82
  record["metadata"] = dict(metadata)
82
83
  if redact:
83
- record = redact_secrets(record)
84
+ record = redact_secrets(record, redaction_config)
84
85
  record["redacted"] = True
85
86
  return record
86
87