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.
- {snulbug-0.2.5 → snulbug-0.2.6}/CHANGELOG.md +15 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/PKG-INFO +2 -1
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/devcontainers.md +2 -2
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-proxy.md +70 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/release.md +2 -2
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/.devcontainer/devcontainer.json +1 -1
- {snulbug-0.2.5 → snulbug-0.2.6}/features/snulbug/README.md +1 -1
- {snulbug-0.2.5 → snulbug-0.2.6}/features/snulbug/devcontainer-feature.json +1 -1
- {snulbug-0.2.5 → snulbug-0.2.6}/pyproject.toml +2 -1
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/__init__.py +7 -1
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/leases.py +89 -62
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/proxy.py +51 -8
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/recorder.py +3 -2
- snulbug-0.2.6/tests/test_trusted_leases.py +611 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/.gitignore +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/CODE_OF_CONDUCT.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/CONTRIBUTING.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/LICENSE +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/SECURITY.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/actions.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/always-on-runtime.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/auth-providers.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/aws-mcp.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/bundles.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/ci-policy-gates.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/comparison.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/credential-brokers.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/discovery-providers.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/event-sink-providers.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/evidence-exporters.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/getting-started.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/lua-policy-dsl.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/lua-request-api.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-auth-recipes.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-client-recipes.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-evidence.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-fabric.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-gateway.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-guide.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-impact.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-learn.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-policy.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-presets.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-protocol.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-recorder.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-schemas.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/mcp-share.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/ngrok-end-to-end.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/policy-deny-backoff.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/quickstart.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/roadmap.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/security-model.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/share-doctor-checks.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/state.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/tool-catalog-projection.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/tool-risk-analyzers.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/tunnel-providers.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/upgrading-to-0.2.4.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/upgrading-to-0.2.5.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/docs/upstream-transports.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/.gitignore +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/ci-workload-identity.toml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/group-gated-tools.toml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/auth_claim_patterns/tenant-isolation.toml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/basic/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/basic/app.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/fixtures/duplicate-delivery.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/fixtures/first-delivery.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/snapshots/duplicate.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/idempotency.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/fixtures/allowed-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/fixtures/blocked-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/fixtures/unauthenticated.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/bundles/mcp-gateway.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/mcp-client.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/mock_mcp_server.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/snulbug.env-gateway.toml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/codespace_local_gateway/snulbug.local-gateway.toml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/app.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/policies/acme.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/policies/default.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/policies/globex.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/requests/acme-invalid.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/requests/acme-valid.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/requests/globex-sandbox.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/customer_policy/requests/unknown-tenant.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/github-actions/snulbug-policy-gates.yml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/.gitignore +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/Dockerfile.gateway +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/auth-init.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/client-token-request.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/commands.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/auth/keycloak/snulbug.auth.toml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/docker-compose.yml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/get-token.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/keycloak/snulbug-demo-realm.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/mock_mcp_server.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/policy.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/requests/blocked-write-file.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/requests/read-file.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/requests/tools-list.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/keycloak_oauth_demo/snulbug.toml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/Dockerfile.gateway +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/Dockerfile.remote-peer +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/docker-compose.yml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/hypertele-client.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/hypertele-server.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/mcp-client.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/mock_mcp_server.js +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/mock_mcp_server.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/snulbug.facade.toml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_container_facade/snulbug.local.toml +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_gateway/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_gateway/app.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/.gitignore +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/requests/blocked-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/requests/safe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/requests/tools-list.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/run_demo.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/mcp_proxy_demo/upstream.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/policy_promotion/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/policy_promotion/active.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/policy_promotion/draft.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/policy_promotion/fixtures/acme-invalid.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/policy_promotion/fixtures/acme-valid.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/provider_policy_templates/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/provider_policy_templates/cloudflare-access-group-gate.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/provider_policy_templates/entra-app-role-gate.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/provider_policy_templates/github-actions-workload-gate.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/provider_policy_templates/keycloak-role-gate.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/state_replay/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/state_replay/duplicate-state.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/state_replay/idempotency.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/state_replay/request.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/app.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/policies/github.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/policies/stripe.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/policies/unknown.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/requests/github-push.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/requests/stripe-invoice.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/requests/stripe-malformed.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/examples/webhook_normalization/requests/unknown-vendor.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/features/snulbug/install.sh +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/__main__.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/_operations_smoke.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/assets/prism.css +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/assets/prism.js +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/auth_providers.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/auth_recipes.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/fixtures/authenticated.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/auth-required.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/contexts/inventory-lease.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/contexts/sandbox-change-lease.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/data-read.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/iam-delete-user.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/inventory-read.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/knowledge-search.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/sandbox-mutation.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/shell-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/absolute-path.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/allowed-path.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/traversal-path.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/tools-list.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/write-method.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/tools-list.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/batch-request.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/allowed-read.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/secret-path.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/traversal-path.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/manifest.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/policy.lua +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/bundle.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/__init__.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/common.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/evidence.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/fabric.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/policy.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/rich_reports.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/rich_share.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/schemas.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli/share.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cli_helpers.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/cloudflare_access.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/codespaces.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/config.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/confirm.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/control_events.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/controller.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/credential_health.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/credentials.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/discovery.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/events.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/evidence_exporters.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/fabric.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/fabric_control.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/fabric_members.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/fabric_runtime.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/gateway_templates.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/guide.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/http_mounts.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/http_routes.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/impact.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/inspection.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/json_schema.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/lab.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/learn.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/manifests.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_auth.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_cache.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_capabilities.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_client_requests.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_completion.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_mrtr.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_parameter_headers.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_policy_bundles.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_progress.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_protocol.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_resources.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_schema_policy.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_schemas.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_spec_conformance.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_stdio.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_stream.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_subscriptions.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_tasks.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/mcp_tools.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/middleware.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/ngrok_drift.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/policy_activation.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/policy_backoff.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/policy_review.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/presets.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/promotion.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/public_origin.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/py.typed +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/quickstart.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/redaction.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/release_qa.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/request_explanations.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/response_policy.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/runtime.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/sarif.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/scaffolds.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/schema_policy.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/share.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/share_console.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/share_doctor.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/share_runtime.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/share_session.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/simulator.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/state.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/tool_catalog.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/tool_risk.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/tunnel.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/snulbug/upstream_transports.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/LICENSE +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/dependentSchemas.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/dynamicRef.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/if-then-else.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/prefixItems.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/json_schema_2020_12/unevaluatedProperties.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/pocket_id/README.md +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/pocket_id/oauth.json +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/fixtures/stdio_subscriptions.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_auth_claim_patterns.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_auth_recipes.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_automation_invites.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_cli_helpers.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_cloudflare_access.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_codespaces.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_config.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_credential_health.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_credentials.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_customer_policy_demo.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_devcontainer_feature.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_discovery.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_events.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_evidence_exporters.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_fabric.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_fabric_controller.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_gateway_snapshot.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_http_mounts.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_http_routes.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_json_schema.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_keycloak_oauth_demo.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_leases.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_manifests.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_cache.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_gateway_demo.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_guide.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_helpers.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_impact.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_inspection.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_lab.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_learn.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_mrtr.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_parameter_headers.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_policy_bundles.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_presets.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_progress.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_protocol.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_proxy_demo.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_quickstart.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_recorder.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_resources.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_schema_policy.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_schemas.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_share.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_spec_conformance.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_stdio.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_stream.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_subscriptions.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_mcp_tools.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_middleware.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_ngrok_drift.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_operations_smoke.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_pocket_id_auth.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_policy_bundle.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_policy_promotion.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_policy_state.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_provider_policy_templates.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_proxy.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_public_origin.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_redaction_audit.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_release_qa.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_request_explanations.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_response_policy.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_sandbox_abuse.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_scaffolds.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_share_console.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_share_runtime.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_state_snapshots.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_tool_catalog.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_tool_risk.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_tunnel_audit.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_tunnel_doctor.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_tunnel_init.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_upstream_transports.py +0 -0
- {snulbug-0.2.5 → snulbug-0.2.6}/tests/test_webhook_normalization_demo.py +0 -0
- {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.
|
|
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",
|
|
@@ -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.
|
|
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.
|
{snulbug-0.2.5 → 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",
|
|
@@ -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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
506
|
-
if
|
|
507
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|