snulbug 0.2.4__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.
- {snulbug-0.2.4 → snulbug-0.2.6}/CHANGELOG.md +30 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/PKG-INFO +2 -1
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/devcontainers.md +2 -2
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-proxy.md +134 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/ngrok-end-to-end.md +27 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/release.md +2 -2
- snulbug-0.2.6/docs/upgrading-to-0.2.5.md +97 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/codespace_local_gateway/.devcontainer/devcontainer.json +1 -1
- {snulbug-0.2.4 → snulbug-0.2.6}/features/snulbug/README.md +1 -1
- {snulbug-0.2.4 → snulbug-0.2.6}/features/snulbug/devcontainer-feature.json +1 -1
- {snulbug-0.2.4 → snulbug-0.2.6}/pyproject.toml +2 -1
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/__init__.py +7 -1
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cli/rich_share.py +5 -1
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/config.py +6 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/http_mounts.py +19 -2
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/http_routes.py +25 -5
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/leases.py +89 -62
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/ngrok_drift.py +5 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/proxy.py +59 -10
- snulbug-0.2.6/snulbug/public_origin.py +136 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/recorder.py +3 -2
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/release_qa.py +3 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/share.py +2 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/share_console.py +2 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/tunnel.py +53 -1
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_http_mounts.py +36 -3
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_http_routes.py +119 -3
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_ngrok_drift.py +12 -4
- snulbug-0.2.6/tests/test_public_origin.py +209 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_release_qa.py +2 -0
- snulbug-0.2.6/tests/test_trusted_leases.py +611 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_tunnel_init.py +68 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/.gitignore +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/CODE_OF_CONDUCT.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/CONTRIBUTING.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/LICENSE +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/SECURITY.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/actions.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/always-on-runtime.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/auth-providers.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/aws-mcp.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/bundles.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/ci-policy-gates.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/comparison.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/credential-brokers.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/discovery-providers.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/event-sink-providers.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/evidence-exporters.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/getting-started.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/lua-policy-dsl.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/lua-request-api.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-auth-recipes.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-client-recipes.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-evidence.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-fabric.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-gateway.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-guide.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-impact.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-learn.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-policy.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-presets.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-protocol.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-recorder.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-schemas.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/mcp-share.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/policy-deny-backoff.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/quickstart.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/roadmap.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/security-model.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/share-doctor-checks.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/state.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/tool-catalog-projection.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/tool-risk-analyzers.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/tunnel-providers.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/upgrading-to-0.2.4.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/docs/upstream-transports.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/auth_claim_patterns/.gitignore +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/auth_claim_patterns/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/auth_claim_patterns/ci-workload-identity.toml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/auth_claim_patterns/group-gated-tools.toml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/auth_claim_patterns/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/auth_claim_patterns/tenant-isolation.toml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/basic/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/basic/app.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/fixtures/duplicate-delivery.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/fixtures/first-delivery.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/snapshots/duplicate.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/fixtures/allowed-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/fixtures/blocked-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/fixtures/unauthenticated.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/codespace_local_gateway/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/codespace_local_gateway/mcp-client.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/codespace_local_gateway/mock_mcp_server.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/codespace_local_gateway/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/codespace_local_gateway/snulbug.env-gateway.toml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/codespace_local_gateway/snulbug.local-gateway.toml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/customer_policy/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/customer_policy/app.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/customer_policy/policies/acme.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/customer_policy/policies/default.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/customer_policy/policies/globex.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/customer_policy/requests/acme-invalid.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/customer_policy/requests/acme-valid.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/customer_policy/requests/globex-sandbox.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/customer_policy/requests/unknown-tenant.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/github-actions/snulbug-policy-gates.yml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/.gitignore +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/Dockerfile.gateway +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/auth-init.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/client-token-request.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/commands.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/snulbug.auth.toml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/docker-compose.yml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/get-token.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/keycloak/snulbug-demo-realm.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/mock_mcp_server.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/policy.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/requests/blocked-write-file.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/requests/read-file.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/requests/tools-list.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/keycloak_oauth_demo/snulbug.toml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/Dockerfile.gateway +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/Dockerfile.remote-peer +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/docker-compose.yml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/hypertele-client.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/hypertele-server.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/mcp-client.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/mock_mcp_server.js +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/mock_mcp_server.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/snulbug.facade.toml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_container_facade/snulbug.local.toml +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_gateway/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_gateway/app.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_proxy_demo/.gitignore +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_proxy_demo/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_proxy_demo/requests/blocked-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_proxy_demo/requests/safe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_proxy_demo/requests/tools-list.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_proxy_demo/run_demo.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/mcp_proxy_demo/upstream.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/policy_promotion/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/policy_promotion/active.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/policy_promotion/draft.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/policy_promotion/fixtures/acme-invalid.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/policy_promotion/fixtures/acme-valid.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/provider_policy_templates/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/provider_policy_templates/cloudflare-access-group-gate.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/provider_policy_templates/entra-app-role-gate.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/provider_policy_templates/github-actions-workload-gate.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/provider_policy_templates/keycloak-role-gate.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/state_replay/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/state_replay/duplicate-state.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/state_replay/idempotency.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/state_replay/request.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/webhook_normalization/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/webhook_normalization/app.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/webhook_normalization/policies/github.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/webhook_normalization/policies/stripe.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/webhook_normalization/policies/unknown.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/webhook_normalization/requests/github-push.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/webhook_normalization/requests/stripe-invoice.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/webhook_normalization/requests/stripe-malformed.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/examples/webhook_normalization/requests/unknown-vendor.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/features/snulbug/install.sh +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/__main__.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/_operations_smoke.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/assets/prism.css +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/assets/prism.js +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/auth_providers.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/auth_recipes.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/fixtures/authenticated.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/contexts/inventory-lease.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/contexts/sandbox-change-lease.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/data-read.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/iam-delete-user.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/inventory-read.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/knowledge-search.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/sandbox-mutation.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/shell-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/absolute-path.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/allowed-path.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/traversal-path.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/tools-list.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/write-method.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/tools-list.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/batch-request.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/allowed-read.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/secret-path.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/traversal-path.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/manifest.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/policy.lua +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/bundle.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cli/__init__.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cli/common.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cli/evidence.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cli/fabric.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cli/policy.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cli/rich_reports.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cli/schemas.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cli/share.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cli_helpers.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/cloudflare_access.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/codespaces.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/confirm.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/control_events.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/controller.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/credential_health.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/credentials.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/discovery.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/events.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/evidence_exporters.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/fabric.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/fabric_control.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/fabric_members.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/fabric_runtime.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/gateway_templates.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/guide.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/impact.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/inspection.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/json_schema.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/lab.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/learn.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/manifests.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_auth.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_cache.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_capabilities.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_client_requests.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_completion.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_mrtr.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_parameter_headers.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_policy_bundles.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_progress.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_protocol.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_resources.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_schema_policy.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_schemas.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_spec_conformance.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_stdio.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_stream.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_subscriptions.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_tasks.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/mcp_tools.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/middleware.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/policy_activation.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/policy_backoff.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/policy_review.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/presets.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/promotion.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/py.typed +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/quickstart.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/redaction.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/request_explanations.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/response_policy.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/runtime.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/sarif.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/scaffolds.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/schema_policy.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/share_doctor.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/share_runtime.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/share_session.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/simulator.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/state.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/tool_catalog.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/tool_risk.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/snulbug/upstream_transports.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/LICENSE +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/dependentSchemas.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/dynamicRef.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/if-then-else.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/prefixItems.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/unevaluatedProperties.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/fixtures/pocket_id/README.md +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/fixtures/pocket_id/oauth.json +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/fixtures/stdio_subscriptions.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_auth_claim_patterns.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_auth_recipes.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_automation_invites.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_cli_helpers.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_cloudflare_access.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_codespaces.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_config.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_credential_health.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_credentials.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_customer_policy_demo.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_devcontainer_feature.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_discovery.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_events.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_evidence_exporters.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_fabric.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_fabric_controller.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_gateway_snapshot.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_json_schema.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_keycloak_oauth_demo.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_leases.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_manifests.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_cache.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_gateway_demo.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_guide.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_helpers.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_impact.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_inspection.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_lab.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_learn.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_mrtr.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_parameter_headers.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_policy_bundles.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_presets.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_progress.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_protocol.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_proxy_demo.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_quickstart.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_recorder.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_resources.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_schema_policy.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_schemas.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_share.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_spec_conformance.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_stdio.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_stream.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_subscriptions.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_mcp_tools.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_middleware.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_operations_smoke.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_pocket_id_auth.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_policy_bundle.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_policy_promotion.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_policy_state.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_provider_policy_templates.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_proxy.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_redaction_audit.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_request_explanations.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_response_policy.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_sandbox_abuse.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_scaffolds.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_share_console.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_share_runtime.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_state_snapshots.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_tool_catalog.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_tool_risk.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_tunnel_audit.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_tunnel_doctor.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_upstream_transports.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_webhook_normalization_demo.py +0 -0
- {snulbug-0.2.4 → snulbug-0.2.6}/tests/test_webhooks.py +0 -0
|
@@ -6,6 +6,36 @@ 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
|
+
|
|
24
|
+
## [0.2.5] - 2026-10-23
|
|
25
|
+
|
|
26
|
+
Public-origin preservation for shared MCP and HTTP gateways.
|
|
27
|
+
Read the [0.2.5 upgrade notes](docs/upgrading-to-0.2.5.md) before deploying an
|
|
28
|
+
existing ngrok share or a share with required signed HTTP mount contracts.
|
|
29
|
+
|
|
30
|
+
- Forward canonical public host, scheme, and port from the configured share URL across internal hops. HTTP mounts can opt into `preserve_host = true`; backend Host remains the default and upstream TLS verification is unchanged.
|
|
31
|
+
- Generate ngrok policies that capture and validate the original public authority/scheme before forwarding and replace caller-supplied origin/audit headers. Reject invalid capture pairs at the gateway and unsafe public URLs at config load; keep observed ingress headers for audit provenance, not forwarding authority.
|
|
32
|
+
- Include effective origin and Host mode in mount health checks, doctor, status, reports, the web console, readiness reviews, and signed mount contracts. Existing ngrok policies need manual review and reattachment; no cloud changes or share regeneration are applied automatically.
|
|
33
|
+
- Add regression coverage for spoofed/duplicate headers, redirects, prefixes, ports, IPv6, MCP forwarding, health/cache invalidation, and contract drift, including installed-wheel packaging checks.
|
|
34
|
+
|
|
35
|
+
No change to the default MCP protocol or preview limitations. Application auth,
|
|
36
|
+
proxy trust, base paths, and CSRF defenses remain the mounted app's responsibility;
|
|
37
|
+
this release does not rewrite HTML, redirect locations, or cookie paths.
|
|
38
|
+
|
|
9
39
|
## [0.2.4] - 2026-10-19
|
|
10
40
|
|
|
11
41
|
Operational hardening for always-on shares and shared HTTP/MCP gateways.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: snulbug
|
|
3
|
-
Version: 0.2.
|
|
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.
|
|
14
|
-
"version": "0.2.
|
|
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",
|
|
@@ -123,6 +123,7 @@ auth = "upstream"
|
|
|
123
123
|
methods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
|
124
124
|
forward_authorization = true
|
|
125
125
|
forward_cookies = true
|
|
126
|
+
preserve_host = false
|
|
126
127
|
timeout = 30.0
|
|
127
128
|
health_path = "/healthz"
|
|
128
129
|
health_status = 200
|
|
@@ -214,6 +215,69 @@ the forwarded host, protocol, and prefix headers. Snulbug does not rewrite HTML,
|
|
|
214
215
|
redirect locations, or cookie paths. Use `strip_prefix = false` for an app that
|
|
215
216
|
already serves its prefix. WebSocket upgrades are not supported by these HTTP mounts.
|
|
216
217
|
|
|
218
|
+
### Public origin and Host preservation
|
|
219
|
+
|
|
220
|
+
The share's existing `mcp.proxy.tunnel_public_url` is the single source of truth
|
|
221
|
+
for the external scheme, authority, and port. Its MCP path is not part of the
|
|
222
|
+
origin: `https://share.example.com/mcp` yields `https://share.example.com`, and
|
|
223
|
+
the `/app` mount separately supplies `X-Forwarded-Prefix: /app`.
|
|
224
|
+
|
|
225
|
+
```toml
|
|
226
|
+
[mcp.proxy]
|
|
227
|
+
tunnel_public_url = "https://share.example.com/mcp"
|
|
228
|
+
|
|
229
|
+
[[mcp.proxy.http_routes]]
|
|
230
|
+
prefix = "/app"
|
|
231
|
+
upstream = "http://127.0.0.1:3000"
|
|
232
|
+
auth = "upstream"
|
|
233
|
+
preserve_host = true
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Merge these values into existing tables rather than adding duplicate mounts.
|
|
237
|
+
The app receives `Host: share.example.com`, `X-Forwarded-Host: share.example.com`,
|
|
238
|
+
`X-Forwarded-Proto: https`, `X-Forwarded-Port: 443`, and
|
|
239
|
+
`X-Forwarded-Prefix: /app`. Explicit non-default public ports are retained.
|
|
240
|
+
The connection destination, HTTPS SNI, and certificate verification still use
|
|
241
|
+
the configured upstream URL. Client Origin/Referer headers are not rewritten.
|
|
242
|
+
|
|
243
|
+
`preserve_host` defaults to `false`: Host remains the backend authority for apps
|
|
244
|
+
that use internal virtual hosts, while the canonical forwarding headers still
|
|
245
|
+
advertise the public URL. MCP upstreams also retain their backend Host and
|
|
246
|
+
receive canonical host/protocol/port forwarding metadata. This does not change
|
|
247
|
+
OAuth resource/audience or DPoP checks, or pass client OAuth credentials upstream.
|
|
248
|
+
|
|
249
|
+
Caller-supplied forwarding/original-origin headers do not select the public
|
|
250
|
+
origin. Generated ngrok Cloud Endpoint policies capture and check the original
|
|
251
|
+
public authority/scheme before forwarding, replacing caller-supplied capture
|
|
252
|
+
and audit headers. Snulbug rejects duplicate, incomplete, malformed, or
|
|
253
|
+
mismatched capture pairs with HTTP 400, then removes them before dispatch.
|
|
254
|
+
Those headers are consistency checks, not proof that a request came from ngrok.
|
|
255
|
+
Direct local requests without capture headers are supported and intentionally
|
|
256
|
+
advertise the same configured public origin. No extra trusted-header opt-in is
|
|
257
|
+
needed. Only one canonical origin is supported; aliases/wildcards are not
|
|
258
|
+
automatically trusted.
|
|
259
|
+
|
|
260
|
+
Without `tunnel_public_url`, HTTP mounts retain local-request forwarding
|
|
261
|
+
behavior and doctor warns that the external origin is unknown. `preserve_host =
|
|
262
|
+
true` requires an explicit public URL. Invalid public URLs, including credentials,
|
|
263
|
+
query strings, fragments, malformed authorities, and control characters, fail
|
|
264
|
+
configuration validation.
|
|
265
|
+
|
|
266
|
+
Configure the app/framework to trust forwarding headers only from snulbug's
|
|
267
|
+
immediate backend connection, with its public host in any allowed-host list.
|
|
268
|
+
Prefix stripping does not automatically configure an application's root path;
|
|
269
|
+
set the app's `/app` base path or use its supported forwarded-prefix integration.
|
|
270
|
+
Host preservation alone does not implement CSRF protection or application auth.
|
|
271
|
+
|
|
272
|
+
Doctor/status/report and the console's HTTP mount contracts show the effective
|
|
273
|
+
public origin and Host mode. Backend health checks use the same origin/Host
|
|
274
|
+
contract without client credentials. Changing the public origin or Host mode
|
|
275
|
+
invalidates cached health and readiness review and changes signed mount bindings.
|
|
276
|
+
After upgrading an existing ngrok share, review the desired policy in share
|
|
277
|
+
doctor, update the generated Cloud Endpoint policy file, and reattach it in ngrok.
|
|
278
|
+
Restart the gateway after config changes. This feature does not rewrite a live
|
|
279
|
+
Cloud Endpoint or regenerate existing shares automatically.
|
|
280
|
+
|
|
217
281
|
Restart `share run` after editing mounts. The router works with proxy and facade
|
|
218
282
|
MCP modes and any tunnel that forwards the gateway origin. For generated ngrok
|
|
219
283
|
setup, register routes at share creation so the Cloud Endpoint policy permits
|
|
@@ -932,6 +996,13 @@ Send the returned `x-snulbug-lease` header with MCP requests. The proxy hot-load
|
|
|
932
996
|
the JSON file on each call, so new leases and revocations do not require a proxy
|
|
933
997
|
restart.
|
|
934
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
|
+
|
|
935
1006
|
Create an invite when you want snulbug to package the share URL, bearer header,
|
|
936
1007
|
lease header, and downstream client snippets together:
|
|
937
1008
|
|
|
@@ -1580,3 +1651,66 @@ application = create_proxy_application(
|
|
|
1580
1651
|
|
|
1581
1652
|
You can run that ASGI app with Uvicorn, Hypercorn, Daphne, or another ASGI
|
|
1582
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.
|
|
@@ -106,6 +106,33 @@ and preserving prefixes. Existing shares need both a TOML route and an updated
|
|
|
106
106
|
Cloud Endpoint Traffic Policy; changing TOML alone leaves ngrok's old deny rule
|
|
107
107
|
in effect.
|
|
108
108
|
|
|
109
|
+
### Public Links and Redirects
|
|
110
|
+
|
|
111
|
+
The generated share sets `mcp.proxy.tunnel_public_url` to the exact public MCP
|
|
112
|
+
URL. Snulbug derives the public origin from that URL, not from the internal
|
|
113
|
+
Agent Endpoint or caller-supplied forwarding headers. Mounted apps receive the
|
|
114
|
+
public host, scheme, and port through `X-Forwarded-*`, plus their mount prefix.
|
|
115
|
+
This also applies to local requests through snulbug while the public URL is set.
|
|
116
|
+
|
|
117
|
+
For apps that need the literal public Host header, add `preserve_host = true`
|
|
118
|
+
to that existing `[[mcp.proxy.http_routes]]` table. Leave it false (the default)
|
|
119
|
+
when the backend expects its own internal host. Configure the app to trust
|
|
120
|
+
snulbug as its immediate proxy and serve its public base path, such as `/app`.
|
|
121
|
+
See [public-origin forwarding](mcp-proxy.md#public-origin-and-host-preservation).
|
|
122
|
+
|
|
123
|
+
The generated Cloud Endpoint policy captures the original host/scheme before
|
|
124
|
+
forwarding, rejects authorities/schemes different from the configured public
|
|
125
|
+
origin with HTTP 421, and replaces caller-supplied origin/audit headers.
|
|
126
|
+
Snulbug validates captured pairs and rejects invalid or mismatched values with
|
|
127
|
+
HTTP 400. These checks do not authenticate the forwarding agent or replace
|
|
128
|
+
application/MCP authentication.
|
|
129
|
+
|
|
130
|
+
Changing the public URL requires updating TOML and the generated Cloud Endpoint
|
|
131
|
+
policy and reattaching that policy in ngrok. Existing policies from older
|
|
132
|
+
versions do not gain capture rules automatically; use share doctor's desired
|
|
133
|
+
baseline and drift checks to review and update them. Do not recreate the share
|
|
134
|
+
just to update its tunnel policy or discard its invites and evidence.
|
|
135
|
+
|
|
109
136
|
## 3. Export the generated client headers
|
|
110
137
|
|
|
111
138
|
The generated share requires both bearer auth and an `x-snulbug-lease` header
|
|
@@ -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.
|
|
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.
|
|
70
|
+
pushing `v0.2.6` does not publish a package or automatically create a GitHub release.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Upgrading to 0.2.5
|
|
2
|
+
|
|
3
|
+
Snulbug 0.2.5 preserves the configured public origin across internal tunnel hops
|
|
4
|
+
and downstream MCP/HTTP forwarding. Back up the complete share directory before
|
|
5
|
+
upgrading and treat it as secret-bearing material. Do not recreate an existing
|
|
6
|
+
share with `share create --force` to update its tunnel policy.
|
|
7
|
+
|
|
8
|
+
For upgrades from before 0.2.4, also review the
|
|
9
|
+
[always-on runtime and HTTP mount upgrade notes](upgrading-to-0.2.4.md).
|
|
10
|
+
|
|
11
|
+
## Check the canonical public URL
|
|
12
|
+
|
|
13
|
+
The existing `mcp.proxy.tunnel_public_url` now determines downstream
|
|
14
|
+
`X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Forwarded-Port`. The MCP path is
|
|
15
|
+
excluded from the origin; HTTP mounts independently provide their public prefix.
|
|
16
|
+
Local requests through the gateway intentionally advertise the same configured
|
|
17
|
+
public origin, not the local/internal hostname.
|
|
18
|
+
|
|
19
|
+
Use the exact intended HTTP(S) share URL without credentials, query strings, or
|
|
20
|
+
fragments. Malformed authorities and control characters now fail configuration
|
|
21
|
+
validation. Without a public URL, HTTP mounts retain local-request forwarding
|
|
22
|
+
and doctor warns that the external origin is unknown. Aliases and wildcard hosts
|
|
23
|
+
are not automatically trusted.
|
|
24
|
+
|
|
25
|
+
## Decide whether the app needs the public Host
|
|
26
|
+
|
|
27
|
+
By default, the literal Host remains the backend authority. The canonical
|
|
28
|
+
forwarding headers are sufficient for apps that support trusted proxy headers.
|
|
29
|
+
For an app that needs the public Host for virtual hosting or URL generation,
|
|
30
|
+
merge this option into its existing mount:
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
[mcp.proxy]
|
|
34
|
+
tunnel_public_url = "https://share.example.com/mcp"
|
|
35
|
+
|
|
36
|
+
[[mcp.proxy.http_routes]]
|
|
37
|
+
prefix = "/app"
|
|
38
|
+
upstream = "http://127.0.0.1:3000"
|
|
39
|
+
auth = "upstream"
|
|
40
|
+
preserve_host = true
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Do not add duplicate tables for an existing prefix. `preserve_host = true`
|
|
44
|
+
requires an explicit public URL. The connection target, HTTPS SNI, and
|
|
45
|
+
certificate verification remain tied to the upstream URL. MCP upstreams retain
|
|
46
|
+
their own backend Host while receiving canonical forwarding metadata.
|
|
47
|
+
|
|
48
|
+
Configure the app to trust snulbug's immediate backend connection, accept its
|
|
49
|
+
public hostname, and generate URLs for `/app`. Prefix stripping does not set an
|
|
50
|
+
application's root path automatically. Snulbug does not rewrite HTML, Location
|
|
51
|
+
headers, cookie paths, Origin, or Referer. Application login, permissions, and
|
|
52
|
+
CSRF protection remain the app's responsibility; MCP leases do not guard it.
|
|
53
|
+
|
|
54
|
+
## Review and reattach the ngrok policy
|
|
55
|
+
|
|
56
|
+
Newly generated ngrok Cloud Endpoint policies capture the public host/scheme
|
|
57
|
+
before internal forwarding, reject unexpected origins with HTTP 421, and replace
|
|
58
|
+
caller-supplied forwarding/capture/audit headers. Snulbug rejects malformed,
|
|
59
|
+
duplicate, incomplete, or mismatched captured pairs with HTTP 400. These headers
|
|
60
|
+
are consistency checks, not authentication of the tunnel agent.
|
|
61
|
+
|
|
62
|
+
Existing policy files and live Cloud Endpoints are not changed automatically:
|
|
63
|
+
|
|
64
|
+
1. Confirm the intended public URL in the existing share configuration.
|
|
65
|
+
2. Run `snulbug mcp share doctor DIRECTORY` and review the desired ngrok baseline
|
|
66
|
+
and drift result. Add `--compact` for JSON, which includes `ngrok.desired_policy` when
|
|
67
|
+
inspection is available; the discovery extra supplies YAML inspection.
|
|
68
|
+
3. Review and update `tunnel/ngrok-traffic-policy.yml`, retaining any deliberately
|
|
69
|
+
customized rules, and reattach the reviewed policy to the public Cloud Endpoint.
|
|
70
|
+
4. Restart the gateway after config changes and rerun doctor. A running agent or
|
|
71
|
+
valid local YAML does not prove that the public endpoint has the new policy.
|
|
72
|
+
|
|
73
|
+
Direct requests without captured headers remain supported. Older policies do not
|
|
74
|
+
gain capture checks by upgrading Python alone, and doctor can flag them as drifted.
|
|
75
|
+
Passive console refreshes do not apply cloud changes or probe upstreams repeatedly.
|
|
76
|
+
|
|
77
|
+
## Renew reviews and contracts deliberately
|
|
78
|
+
|
|
79
|
+
Effective public origins and Host modes now participate in HTTP mount bindings.
|
|
80
|
+
Existing mount health results and readiness reviews can become stale, and a
|
|
81
|
+
required signed contract created before these fields existed can prevent startup.
|
|
82
|
+
Inspect the intended exposure, run health/doctor checks, then renew the review
|
|
83
|
+
and regenerate/sign the contract through the existing `share contract` workflow.
|
|
84
|
+
Do not bypass a required contract just to suppress drift.
|
|
85
|
+
|
|
86
|
+
## Verify before handoff
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
snulbug mcp share doctor /path/to/share
|
|
90
|
+
snulbug mcp share status /path/to/share
|
|
91
|
+
snulbug mcp share report /path/to/share
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Check a safe MCP operation and an app link/redirect through the public URL.
|
|
95
|
+
The default MCP protocol and experimental profile limitations are unchanged.
|
|
96
|
+
Local release QA and ngrok config validation do not certify live provider/client
|
|
97
|
+
interoperability. See [public-origin forwarding](mcp-proxy.md#public-origin-and-host-preservation).
|
{snulbug-0.2.4 → snulbug-0.2.6}/examples/codespace_local_gateway/.devcontainer/devcontainer.json
RENAMED
|
@@ -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
|
+
"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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
|
@@ -161,7 +161,11 @@ def http_mounts_table(mounts: Mapping[str, Any]) -> Table:
|
|
|
161
161
|
Text(f"{mount.get('prefix')}\n{mount.get('upstream')}"),
|
|
162
162
|
_value(mount.get("auth")),
|
|
163
163
|
Text(", ".join(mount.get("methods", []))),
|
|
164
|
-
Text(
|
|
164
|
+
Text(
|
|
165
|
+
f"Authorization: {mount.get('forward_authorization')}; cookies: {mount.get('forward_cookies')}\n"
|
|
166
|
+
f"Origin: {mount.get('public_origin') or 'local request'}\n"
|
|
167
|
+
f"Host: {'public' if mount.get('preserve_host') else 'upstream'}"
|
|
168
|
+
),
|
|
165
169
|
Text(("pass" if health.get("ok") else "fail") if health.get("checked") else "not checked"),
|
|
166
170
|
)
|
|
167
171
|
if not mounts.get("items"):
|
|
@@ -15,6 +15,7 @@ from .policy_backoff import (
|
|
|
15
15
|
DEFAULT_POLICY_BACKOFF_KEY_FIELDS,
|
|
16
16
|
DEFAULT_POLICY_BACKOFF_REASON_CODES,
|
|
17
17
|
)
|
|
18
|
+
from .public_origin import public_origin
|
|
18
19
|
from .tool_catalog import CATALOG_PROJECTION_MODES
|
|
19
20
|
from .upstream_transports import get_upstream_transport
|
|
20
21
|
|
|
@@ -249,6 +250,8 @@ lease_file = "leases.json"
|
|
|
249
250
|
lease_required = true
|
|
250
251
|
lease_header = "x-snulbug-lease"
|
|
251
252
|
tunnel_provider = "auto"
|
|
253
|
+
# Canonical public origin for forwarded host/protocol/port; MCP path is excluded.
|
|
254
|
+
# HTTP mounts can opt into preserve_host = true to send the public Host as well.
|
|
252
255
|
tunnel_public_url = ""
|
|
253
256
|
cloudflare_access_profile = ""
|
|
254
257
|
tailscale_profile = ""
|
|
@@ -598,6 +601,7 @@ def normalize_mcp_proxy_config(config: Mapping[str, Any], *, base_dir: str | Pat
|
|
|
598
601
|
raise ValueError("mcp.proxy.tunnel_public_url must be a string")
|
|
599
602
|
if normalized.get("tunnel_public_url") == "":
|
|
600
603
|
normalized["tunnel_public_url"] = None
|
|
604
|
+
origin = public_origin(normalized.get("tunnel_public_url"))
|
|
601
605
|
for field in ("policy", "record_out", "lease_file"):
|
|
602
606
|
value = normalized.get(field)
|
|
603
607
|
if value is not None and not isinstance(value, str | Path):
|
|
@@ -676,6 +680,8 @@ def normalize_mcp_proxy_config(config: Mapping[str, Any], *, base_dir: str | Pat
|
|
|
676
680
|
normalized["http_routes"] = normalize_http_routes(
|
|
677
681
|
normalized.get("http_routes", []), mcp_path=normalized["streamable_http_endpoint_path"]
|
|
678
682
|
)
|
|
683
|
+
if origin is None and any(route.get("preserve_host") for route in normalized["http_routes"]):
|
|
684
|
+
raise ValueError("HTTP route preserve_host requires mcp.proxy.tunnel_public_url")
|
|
679
685
|
normalized["streamable_http_allowed_origins"] = _normalize_string_list(
|
|
680
686
|
normalized.get("streamable_http_allowed_origins", []),
|
|
681
687
|
field="streamable_http_allowed_origins",
|
|
@@ -12,6 +12,7 @@ from typing import Any
|
|
|
12
12
|
import httpx
|
|
13
13
|
|
|
14
14
|
from .http_routes import http_route_contract
|
|
15
|
+
from .public_origin import public_origin
|
|
15
16
|
from .share_doctor import ShareDoctorCheck, ShareDoctorCheckResult, ShareDoctorContext
|
|
16
17
|
|
|
17
18
|
|
|
@@ -31,7 +32,9 @@ class ShareHttpMountsDoctorCheck(ShareDoctorCheck):
|
|
|
31
32
|
def http_mount_contracts(proxy: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
32
33
|
return sorted(
|
|
33
34
|
[
|
|
34
|
-
http_route_contract(
|
|
35
|
+
http_route_contract(
|
|
36
|
+
route, timeout=float(proxy.get("timeout", 30.0)), public_url=proxy.get("tunnel_public_url")
|
|
37
|
+
)
|
|
35
38
|
for route in proxy.get("http_routes", [])
|
|
36
39
|
],
|
|
37
40
|
key=lambda route: route["prefix"],
|
|
@@ -60,6 +63,13 @@ def inspect_http_mounts(proxy: Mapping[str, Any], *, live_checks: bool = False,
|
|
|
60
63
|
)
|
|
61
64
|
|
|
62
65
|
declared = contract["auth"] != "undeclared"
|
|
66
|
+
origin_message = (
|
|
67
|
+
f"{prefix}: public origin is {contract['public_origin']}; Host uses "
|
|
68
|
+
+ ("the public authority" if contract["preserve_host"] else "the upstream authority")
|
|
69
|
+
if contract["public_origin"]
|
|
70
|
+
else f"{prefix}: public origin is not configured; forwarded URLs use the local request origin"
|
|
71
|
+
)
|
|
72
|
+
check("origin", "pass" if contract["public_origin"] else "warn", origin_message)
|
|
63
73
|
check(
|
|
64
74
|
"auth",
|
|
65
75
|
"pass" if declared else "fail",
|
|
@@ -130,7 +140,14 @@ def _probe_health(contract: Mapping[str, Any], *, timeout: float) -> dict[str, A
|
|
|
130
140
|
}
|
|
131
141
|
try:
|
|
132
142
|
with httpx.Client(timeout=min(timeout, contract["timeout"]), trust_env=False, follow_redirects=False) as client:
|
|
133
|
-
|
|
143
|
+
origin = public_origin(contract["public_origin"])
|
|
144
|
+
headers = {"accept": "*/*"}
|
|
145
|
+
if origin:
|
|
146
|
+
headers.update({name.decode(): value.decode() for name, value in origin.headers()})
|
|
147
|
+
headers["x-forwarded-prefix"] = contract["prefix"]
|
|
148
|
+
if contract["preserve_host"]:
|
|
149
|
+
headers["host"] = origin.authority
|
|
150
|
+
with client.stream("GET", target, headers=headers) as response:
|
|
134
151
|
result["status"] = response.status_code
|
|
135
152
|
result["ok"] = response.status_code == contract["health_status"]
|
|
136
153
|
result["message"] = (
|
|
@@ -10,6 +10,8 @@ from urllib.parse import quote, urlsplit
|
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
|
|
13
|
+
from .public_origin import public_origin
|
|
14
|
+
|
|
13
15
|
if TYPE_CHECKING:
|
|
14
16
|
from .middleware import ASGIApp, Receive, Scope, Send
|
|
15
17
|
|
|
@@ -33,6 +35,7 @@ HTTP_ROUTE_FIELDS = {
|
|
|
33
35
|
"methods",
|
|
34
36
|
"forward_authorization",
|
|
35
37
|
"forward_cookies",
|
|
38
|
+
"preserve_host",
|
|
36
39
|
"timeout",
|
|
37
40
|
"health_path",
|
|
38
41
|
"health_status",
|
|
@@ -109,7 +112,7 @@ def _normalize_contract(route: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
109
112
|
contract = {key: value for key, value in route.items() if key not in {"prefix", "upstream", "strip_prefix"}}
|
|
110
113
|
if "auth" in contract and (not isinstance(contract["auth"], str) or contract["auth"] not in {"public", "upstream"}):
|
|
111
114
|
raise ValueError("HTTP route auth must be 'public' or 'upstream'; MCP auth does not protect HTTP mounts")
|
|
112
|
-
for key in ("forward_authorization", "forward_cookies"):
|
|
115
|
+
for key in ("forward_authorization", "forward_cookies", "preserve_host"):
|
|
113
116
|
if key in contract and not isinstance(contract[key], bool):
|
|
114
117
|
raise ValueError(f"HTTP route {key} must be a boolean")
|
|
115
118
|
if "methods" in contract:
|
|
@@ -156,9 +159,14 @@ def _normalize_contract(route: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
156
159
|
return contract
|
|
157
160
|
|
|
158
161
|
|
|
159
|
-
def http_route_contract(
|
|
162
|
+
def http_route_contract(
|
|
163
|
+
route: Mapping[str, Any], *, timeout: float = 30.0, public_url: str | None = None
|
|
164
|
+
) -> dict[str, Any]:
|
|
160
165
|
"""One effective contract for routing, inspection, and signed share bindings."""
|
|
161
166
|
|
|
167
|
+
origin = public_origin(public_url)
|
|
168
|
+
if route.get("preserve_host") and origin is None:
|
|
169
|
+
raise ValueError("HTTP route preserve_host requires mcp.proxy.tunnel_public_url")
|
|
162
170
|
return {
|
|
163
171
|
"prefix": route["prefix"],
|
|
164
172
|
"upstream": route["upstream"],
|
|
@@ -167,6 +175,8 @@ def http_route_contract(route: Mapping[str, Any], *, timeout: float = 30.0) -> d
|
|
|
167
175
|
"methods": sorted(route.get("methods", DEFAULT_HTTP_METHODS)),
|
|
168
176
|
"forward_authorization": route.get("forward_authorization", route.get("auth") != "public"),
|
|
169
177
|
"forward_cookies": route.get("forward_cookies", route.get("auth") != "public"),
|
|
178
|
+
"preserve_host": route.get("preserve_host", False),
|
|
179
|
+
"public_origin": origin.url if origin else None,
|
|
170
180
|
"timeout": route.get("timeout", timeout),
|
|
171
181
|
"health_path": route.get("health_path"),
|
|
172
182
|
"health_status": route.get("health_status", 200),
|
|
@@ -196,12 +206,16 @@ class HttpRoutesMiddleware:
|
|
|
196
206
|
mcp_path: str = "/mcp",
|
|
197
207
|
timeout: float = 30.0,
|
|
198
208
|
lease_header: str = "x-snulbug-lease",
|
|
209
|
+
public_url: str | None = None,
|
|
199
210
|
) -> None:
|
|
200
211
|
self.app = app
|
|
201
212
|
if not math.isfinite(timeout) or timeout <= 0:
|
|
202
213
|
raise ValueError("HTTP route timeout must be positive and finite")
|
|
203
214
|
self.routes = sorted(
|
|
204
|
-
[
|
|
215
|
+
[
|
|
216
|
+
http_route_contract(route, timeout=timeout, public_url=public_url)
|
|
217
|
+
for route in normalize_http_routes(routes, mcp_path=mcp_path)
|
|
218
|
+
],
|
|
205
219
|
key=lambda r: -len(r["prefix"]),
|
|
206
220
|
)
|
|
207
221
|
self.timeout = timeout
|
|
@@ -312,9 +326,15 @@ class HttpRoutesMiddleware:
|
|
|
312
326
|
or (name == b"cookie" and not route["forward_cookies"])
|
|
313
327
|
):
|
|
314
328
|
headers.append((name, value))
|
|
315
|
-
|
|
329
|
+
origin = public_origin(route["public_origin"])
|
|
330
|
+
if origin:
|
|
331
|
+
headers.extend(origin.headers())
|
|
332
|
+
if route["preserve_host"]:
|
|
333
|
+
headers.append((b"host", origin.authority.encode("ascii")))
|
|
334
|
+
elif original_host:
|
|
316
335
|
headers.append((b"x-forwarded-host", original_host))
|
|
317
|
-
|
|
336
|
+
if not origin:
|
|
337
|
+
headers.append((b"x-forwarded-proto", scope.get("scheme", "http").encode("ascii")))
|
|
318
338
|
headers.append((b"x-forwarded-prefix", route["prefix"].encode("ascii")))
|
|
319
339
|
if scope.get("client"):
|
|
320
340
|
headers.append((b"x-forwarded-for", scope["client"][0].encode("ascii")))
|