snulbug 0.2.1__tar.gz → 0.2.2__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.1 → snulbug-0.2.2}/CHANGELOG.md +14 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/PKG-INFO +3 -1
- {snulbug-0.2.1 → snulbug-0.2.2}/README.md +2 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/SECURITY.md +10 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/devcontainers.md +2 -2
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-proxy.md +4 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-share.md +88 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/release.md +4 -3
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/codespace_local_gateway/.devcontainer/devcontainer.json +1 -1
- {snulbug-0.2.1 → snulbug-0.2.2}/features/snulbug/README.md +1 -1
- {snulbug-0.2.1 → snulbug-0.2.2}/features/snulbug/devcontainer-feature.json +1 -1
- {snulbug-0.2.1 → snulbug-0.2.2}/pyproject.toml +1 -1
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/__init__.py +3 -1
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cli/share.py +22 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/leases.py +227 -6
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_auth.py +8 -1
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/proxy.py +133 -5
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/redaction.py +3 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/share.py +195 -6
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/share_console.py +67 -0
- snulbug-0.2.2/tests/test_automation_invites.py +538 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/.gitignore +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/CODE_OF_CONDUCT.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/CONTRIBUTING.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/LICENSE +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/actions.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/auth-providers.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/aws-mcp.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/bundles.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/ci-policy-gates.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/comparison.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/credential-brokers.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/discovery-providers.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/event-sink-providers.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/evidence-exporters.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/getting-started.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/lua-policy-dsl.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/lua-request-api.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-auth-recipes.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-client-recipes.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-evidence.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-fabric.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-gateway.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-guide.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-impact.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-learn.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-policy.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-presets.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-protocol.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-recorder.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/mcp-schemas.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/ngrok-end-to-end.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/policy-deny-backoff.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/quickstart.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/roadmap.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/security-model.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/share-doctor-checks.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/state.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/tool-catalog-projection.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/tool-risk-analyzers.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/tunnel-providers.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/docs/upstream-transports.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/auth_claim_patterns/.gitignore +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/auth_claim_patterns/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/auth_claim_patterns/ci-workload-identity.toml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/auth_claim_patterns/group-gated-tools.toml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/auth_claim_patterns/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/auth_claim_patterns/tenant-isolation.toml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/basic/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/basic/app.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/idempotency.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/idempotency.snulbug/fixtures/duplicate-delivery.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/idempotency.snulbug/fixtures/first-delivery.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/idempotency.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/idempotency.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/idempotency.snulbug/snapshots/duplicate.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/idempotency.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/mcp-gateway.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/mcp-gateway.snulbug/fixtures/allowed-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/mcp-gateway.snulbug/fixtures/blocked-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/mcp-gateway.snulbug/fixtures/unauthenticated.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/mcp-gateway.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/mcp-gateway.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/bundles/mcp-gateway.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/codespace_local_gateway/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/codespace_local_gateway/mcp-client.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/codespace_local_gateway/mock_mcp_server.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/codespace_local_gateway/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/codespace_local_gateway/snulbug.env-gateway.toml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/codespace_local_gateway/snulbug.local-gateway.toml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/customer_policy/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/customer_policy/app.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/customer_policy/policies/acme.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/customer_policy/policies/default.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/customer_policy/policies/globex.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/customer_policy/requests/acme-invalid.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/customer_policy/requests/acme-valid.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/customer_policy/requests/globex-sandbox.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/customer_policy/requests/unknown-tenant.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/github-actions/snulbug-policy-gates.yml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/.gitignore +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/Dockerfile.gateway +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/auth/keycloak/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/auth/keycloak/auth-init.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/auth/keycloak/client-token-request.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/auth/keycloak/commands.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/auth/keycloak/snulbug.auth.toml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/docker-compose.yml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/get-token.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/keycloak/snulbug-demo-realm.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/mock_mcp_server.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/policy.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/requests/blocked-write-file.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/requests/read-file.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/requests/tools-list.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/keycloak_oauth_demo/snulbug.toml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/Dockerfile.gateway +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/Dockerfile.remote-peer +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/docker-compose.yml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/hypertele-client.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/hypertele-server.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/mcp-client.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/mock_mcp_server.js +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/mock_mcp_server.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/snulbug.facade.toml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_container_facade/snulbug.local.toml +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_gateway/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_gateway/app.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_proxy_demo/.gitignore +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_proxy_demo/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_proxy_demo/requests/blocked-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_proxy_demo/requests/safe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_proxy_demo/requests/tools-list.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_proxy_demo/run_demo.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/mcp_proxy_demo/upstream.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/policy_promotion/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/policy_promotion/active.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/policy_promotion/draft.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/policy_promotion/fixtures/acme-invalid.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/policy_promotion/fixtures/acme-valid.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/provider_policy_templates/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/provider_policy_templates/cloudflare-access-group-gate.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/provider_policy_templates/entra-app-role-gate.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/provider_policy_templates/github-actions-workload-gate.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/provider_policy_templates/keycloak-role-gate.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/state_replay/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/state_replay/duplicate-state.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/state_replay/idempotency.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/state_replay/request.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/webhook_normalization/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/webhook_normalization/app.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/webhook_normalization/policies/github.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/webhook_normalization/policies/stripe.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/webhook_normalization/policies/unknown.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/webhook_normalization/requests/github-push.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/webhook_normalization/requests/stripe-invoice.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/webhook_normalization/requests/stripe-malformed.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/examples/webhook_normalization/requests/unknown-vendor.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/features/snulbug/install.sh +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/__main__.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/assets/prism.css +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/assets/prism.js +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/auth_providers.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/auth_recipes.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/auth-required.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/auth-required.snulbug/fixtures/authenticated.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/auth-required.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/auth-required.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/auth-required.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/contexts/inventory-lease.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/contexts/sandbox-change-lease.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/data-read.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/iam-delete-user.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/inventory-read.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/knowledge-search.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/fixtures/sandbox-mutation.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/aws-mcp.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/local-dev-safe.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/fixtures/shell-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/no-shell-tools.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/absolute-path.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/allowed-path.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/traversal-path.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/project-path-allowlist.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/tools-list.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/fixtures/write-method.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/read-only-local-dev.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/tools-list.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tool-allowlist.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/batch-request.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/safe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/tunnel-safe.snulbug/snapshots/empty.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/allowed-read.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/missing-token.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/secret-path.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/traversal-path.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/fixtures/unsafe-tool.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/manifest.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/builtin_presets/mcp/workspace-firewall.snulbug/policy.lua +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/bundle.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cli/__init__.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cli/common.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cli/evidence.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cli/fabric.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cli/policy.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cli/rich_reports.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cli/rich_share.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cli/schemas.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cli_helpers.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/cloudflare_access.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/codespaces.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/config.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/confirm.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/control_events.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/controller.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/credentials.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/discovery.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/events.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/evidence_exporters.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/fabric.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/fabric_control.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/fabric_members.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/fabric_runtime.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/gateway_templates.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/guide.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/http_routes.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/impact.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/inspection.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/json_schema.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/lab.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/learn.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/manifests.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_cache.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_capabilities.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_client_requests.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_completion.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_mrtr.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_parameter_headers.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_policy_bundles.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_progress.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_protocol.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_resources.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_schema_policy.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_schemas.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_spec_conformance.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_stdio.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_stream.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_subscriptions.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_tasks.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/mcp_tools.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/middleware.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/policy_activation.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/policy_backoff.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/policy_review.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/presets.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/promotion.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/py.typed +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/quickstart.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/recorder.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/release_qa.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/response_policy.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/runtime.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/sarif.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/scaffolds.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/schema_policy.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/share_doctor.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/share_session.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/simulator.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/state.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/tool_catalog.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/tool_risk.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/tunnel.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/snulbug/upstream_transports.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/fixtures/json_schema_2020_12/LICENSE +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/fixtures/json_schema_2020_12/README.md +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/fixtures/json_schema_2020_12/dependentSchemas.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/fixtures/json_schema_2020_12/dynamicRef.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/fixtures/json_schema_2020_12/if-then-else.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/fixtures/json_schema_2020_12/prefixItems.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/fixtures/json_schema_2020_12/unevaluatedProperties.json +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/fixtures/stdio_subscriptions.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_auth_claim_patterns.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_auth_recipes.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_cli_helpers.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_cloudflare_access.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_codespaces.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_config.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_credentials.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_customer_policy_demo.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_devcontainer_feature.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_discovery.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_events.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_evidence_exporters.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_fabric.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_fabric_controller.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_http_routes.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_json_schema.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_keycloak_oauth_demo.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_leases.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_manifests.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_cache.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_gateway_demo.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_guide.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_helpers.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_impact.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_inspection.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_lab.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_learn.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_mrtr.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_parameter_headers.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_policy_bundles.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_presets.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_progress.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_protocol.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_proxy_demo.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_quickstart.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_recorder.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_resources.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_schema_policy.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_schemas.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_share.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_spec_conformance.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_stdio.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_stream.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_subscriptions.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_mcp_tools.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_middleware.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_policy_bundle.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_policy_promotion.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_policy_state.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_provider_policy_templates.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_proxy.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_redaction_audit.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_release_qa.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_response_policy.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_sandbox_abuse.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_scaffolds.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_share_console.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_state_snapshots.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_tool_catalog.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_tool_risk.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_tunnel_audit.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_tunnel_doctor.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_tunnel_init.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_upstream_transports.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_webhook_normalization_demo.py +0 -0
- {snulbug-0.2.1 → snulbug-0.2.2}/tests/test_webhooks.py +0 -0
|
@@ -6,6 +6,20 @@ The format is based on Keep a Changelog, and this project follows semantic versi
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.2] - 2026-10-16
|
|
10
|
+
|
|
11
|
+
Support always-on MCP bots with renewable automation invites, without introducing
|
|
12
|
+
a separate permission or approval system.
|
|
13
|
+
|
|
14
|
+
- Added renewable automation invites using the existing backing lease, with bounded consumer renewal/restart recovery, rotating access tokens, identity bindings, persistent usage budgets, and console/CLI setup snippets.
|
|
15
|
+
- Serialized lease mutations across local workers so renewal, revocation, cleanup, and concurrent call consumption cannot overwrite each other's state.
|
|
16
|
+
- Added renewal visibility and refresh-stable invite forms in the web console, secret-safe renewal audit events, and share closeout that revokes invites as well as the session lease.
|
|
17
|
+
|
|
18
|
+
Automation renewal is opt-in. Access leases remain short-lived; the owner's
|
|
19
|
+
renewal authorization has a separate hard expiry. Clients must adopt refreshed
|
|
20
|
+
lease headers, and revocation or exhausted budgets prevent recovery. No change
|
|
21
|
+
to the default MCP protocol version or the experimental profile's limitations.
|
|
22
|
+
|
|
9
23
|
## [0.2.1] - 2026-10-15
|
|
10
24
|
|
|
11
25
|
Expose web applications and APIs alongside MCP through one gateway and ngrok tunnel.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: snulbug
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -63,6 +63,8 @@ It gives you a tight loop for agent-tool safety:
|
|
|
63
63
|
- review MCP-native capability requests and approve them into normal task leases
|
|
64
64
|
- use task-scoped leases for temporary tool/path grants, optionally bound to
|
|
65
65
|
OAuth subject, tenant, client, group, issuer, or auth profile
|
|
66
|
+
- give always-on bots [renewable automation invites](docs/mcp-share.md#renewable-automation-invites)
|
|
67
|
+
with short-lived leases, consumer renewal, and restart recovery
|
|
66
68
|
- turn OAuth identity into MCP-specific tool permissions
|
|
67
69
|
- pin facade upstream identity with signed manifests
|
|
68
70
|
|
|
@@ -20,6 +20,8 @@ It gives you a tight loop for agent-tool safety:
|
|
|
20
20
|
- review MCP-native capability requests and approve them into normal task leases
|
|
21
21
|
- use task-scoped leases for temporary tool/path grants, optionally bound to
|
|
22
22
|
OAuth subject, tenant, client, group, issuer, or auth profile
|
|
23
|
+
- give always-on bots [renewable automation invites](docs/mcp-share.md#renewable-automation-invites)
|
|
24
|
+
with short-lived leases, consumer renewal, and restart recovery
|
|
23
25
|
- turn OAuth identity into MCP-specific tool permissions
|
|
24
26
|
- pin facade upstream identity with signed manifests
|
|
25
27
|
|
|
@@ -59,6 +59,16 @@ Limits:
|
|
|
59
59
|
schemas pass through unless your Lua policy blocks them separately.
|
|
60
60
|
- Leases are bearer-style capabilities. Store `leases.json` locally, rotate or
|
|
61
61
|
revoke leases after sharing, and prefer short TTLs for public tunnel use.
|
|
62
|
+
- Renewable automation invites add a separate, reusable renewal credential with
|
|
63
|
+
an owner-set hard deadline. Treat it as a long-lived bot secret: theft together
|
|
64
|
+
with the required gateway authentication permits recovery within the approved
|
|
65
|
+
capability and call-budget bounds until revocation or expiry. It does not
|
|
66
|
+
grant console access. Renewal retains the existing lease's usage count and
|
|
67
|
+
identity bindings; revocation disables recovery. Access-token rotation has a
|
|
68
|
+
bounded overlap of at most 30 seconds. Lease operations are serialized on the
|
|
69
|
+
local lease file; independent gateway hosts must not treat copied JSON stores
|
|
70
|
+
as a shared revocation authority. The owner console's invite secret store
|
|
71
|
+
contains plaintext setup credentials and must remain private.
|
|
62
72
|
|
|
63
73
|
### Malicious or compromised MCP server
|
|
64
74
|
|
|
@@ -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.2": {
|
|
14
|
+
"version": "0.2.2",
|
|
15
15
|
"mode": "member-agent",
|
|
16
16
|
"policy_profile": "tunnel-safe",
|
|
17
17
|
"registry": "redis://redis:6379/0",
|
|
@@ -920,6 +920,10 @@ snulbug mcp evidence impact traces/session.jsonl --lease leases.json --report-ou
|
|
|
920
920
|
In facade mode, leases use the client-facing tool name, such as
|
|
921
921
|
`files.read_file` or `git.status`.
|
|
922
922
|
|
|
923
|
+
For unattended consumers, [renewable automation invites](mcp-share.md#renewable-automation-invites)
|
|
924
|
+
extend the same task lease with bounded consumer renewal and restart recovery.
|
|
925
|
+
They retain the existing Lua, OAuth identity, revocation, and call-budget checks.
|
|
926
|
+
|
|
923
927
|
## Tool Schema Firewall
|
|
924
928
|
|
|
925
929
|
When `schema_validation = true`, snulbug learns each MCP tool's `inputSchema`
|
|
@@ -123,6 +123,94 @@ traffic is observed. The table records evidence source, confidence, risk level,
|
|
|
123
123
|
categories, and signals such as command-capable arguments, network/path
|
|
124
124
|
arguments, destructive annotations, and open argument schemas.
|
|
125
125
|
|
|
126
|
+
## Renewable Automation Invites
|
|
127
|
+
|
|
128
|
+
For an always-on bot, enable **Renewable automation invite** in the console's
|
|
129
|
+
Invites tab, or extend the normal invite command:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
snulbug mcp share invite create .snulbug/shares/my-share \
|
|
133
|
+
--recipient nightly-bot --task "Inspect project" \
|
|
134
|
+
--capability project_readonly \
|
|
135
|
+
--renewable --ttl 30m --renewal-ttl 7d
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`--ttl` controls the short-lived access lease. `--renewal-ttl` is the owner's
|
|
139
|
+
hard authorization deadline, measured from invite creation, not each renewal.
|
|
140
|
+
The backing lease keeps its ID, capabilities, identity bindings, usage count,
|
|
141
|
+
and total `--max-calls` budget. Renewal does not approve policy changes or
|
|
142
|
+
replenish an exhausted budget. Use the existing Lua rate limits for recurring
|
|
143
|
+
request limits; this is not a new daily quota system.
|
|
144
|
+
|
|
145
|
+
The setup packet includes a renewal credential, a curl command, and a Python
|
|
146
|
+
integration example. Store the renewal credential as a bot secret, separately
|
|
147
|
+
from the short-lived `x-snulbug-lease` token. It is reusable until its deadline
|
|
148
|
+
or revocation, is not an OAuth refresh token, and cannot administer the console
|
|
149
|
+
or create other invites. It is shown only through the existing owner CLI or
|
|
150
|
+
secret-gated console. Protect the share directory and its `invite-secrets.json`.
|
|
151
|
+
|
|
152
|
+
Consumers use ordinary HTTPS against the **same MCP URL**, so ngrok and other
|
|
153
|
+
generated tunnel routes need no additional public endpoint:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
export SNULBUG_MCP_URL="https://your-tunnel.example/mcp"
|
|
157
|
+
export SNULBUG_INVITE_ID="invite_..."
|
|
158
|
+
export SNULBUG_RENEWAL_TOKEN="sbr_..."
|
|
159
|
+
# Set SNULBUG_BEARER_TOKEN to the bearer token from the invite packet.
|
|
160
|
+
curl --fail-with-body -sS "$SNULBUG_MCP_URL" \
|
|
161
|
+
-H "Authorization: Bearer ${SNULBUG_BEARER_TOKEN}" \
|
|
162
|
+
-H "x-snulbug-renewal: ${SNULBUG_RENEWAL_TOKEN}" \
|
|
163
|
+
-H 'Content-Type: application/json' \
|
|
164
|
+
-H 'Accept: application/json, text/event-stream' \
|
|
165
|
+
-d "{\"jsonrpc\":\"2.0\",\"id\":\"renew\",\"method\":\"snulbug/lease/renew\",\"params\":{\"invite_id\":\"${SNULBUG_INVITE_ID}\"}}"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`snulbug/lease/renew` is a snulbug JSON-RPC extension, not a standard MCP method
|
|
169
|
+
or an upstream tool. Its only parameter is `invite_id`; the consumer cannot
|
|
170
|
+
choose a longer TTL, larger budget, or different capabilities. The gateway
|
|
171
|
+
terminates the request without forwarding the renewal credential upstream.
|
|
172
|
+
Errors use `error.data.reason_code`, even when the HTTP status is 200.
|
|
173
|
+
|
|
174
|
+
On success, `result.headers` contains the current lease header;
|
|
175
|
+
`result.lease.expires_at` and `result.lease.renewal.renew_after` tell the bot
|
|
176
|
+
when to refresh its MCP client's headers. Call again at `renew_after`, with
|
|
177
|
+
jitter and backoff for network failures. Retries inside the same lease window
|
|
178
|
+
return the same access token. A renewal rotates it with at most 30 seconds of
|
|
179
|
+
overlap for in-flight requests, bounded by the old token's original expiry.
|
|
180
|
+
After a process restart or an expired access lease, the **same operation**
|
|
181
|
+
recovers access while the owner authorization remains valid. No old lease token
|
|
182
|
+
or installed snulbug client is required. Fixed-header clients need a refresh
|
|
183
|
+
integration; a static client configuration does not renew itself.
|
|
184
|
+
|
|
185
|
+
OAuth shares still require a valid access token obtained from the configured
|
|
186
|
+
identity provider. Bind automation invites using `--allow-issuer` plus
|
|
187
|
+
`--allow-subject` or `--allow-client-id`; optionally add `--allow-tenant`.
|
|
188
|
+
Use that access token as the renewal request's `Authorization` header and the
|
|
189
|
+
separate invite renewal credential as `x-snulbug-renewal`. Required OAuth scopes,
|
|
190
|
+
DPoP requirements, and identity bindings still apply. Tool scope maps, claim
|
|
191
|
+
policies, and Lua continue to govern every ordinary MCP tool call.
|
|
192
|
+
For DPoP-required shares, the consumer must also sign and attach a fresh proof
|
|
193
|
+
for each renewal request using its existing OAuth/DPoP client integration; the
|
|
194
|
+
plain curl/Python examples do not mint proofs or OAuth access tokens.
|
|
195
|
+
|
|
196
|
+
Revoking the invite or backing lease disables renewal and restart recovery.
|
|
197
|
+
Closing the share with its default revocation option revokes its invites too.
|
|
198
|
+
Even `invite revoke --keep-lease` revokes renewal authority; it leaves only the
|
|
199
|
+
already-issued access lease usable until expiry. Removed capability labels,
|
|
200
|
+
exhausted budgets, and expired renewal authorizations fail closed. Create a new
|
|
201
|
+
invite for a new approval; operator reactivation cannot reset a renewable lease.
|
|
202
|
+
Cleanup retains expired access leases while their renewal authorization is
|
|
203
|
+
active. The console shows authorization expiry, access expiry, next renewal,
|
|
204
|
+
and last renewal/count, and keeps setup snippets synchronized with token rotation.
|
|
205
|
+
Renewal decisions go to the existing audit/event sinks, not the Lua replay log:
|
|
206
|
+
renewing a credential is a control operation, not a simulated policy decision.
|
|
207
|
+
|
|
208
|
+
Renewal and lease consumption use the existing JSON lease store with file locks
|
|
209
|
+
and atomic replacement, including across local worker processes. Persist that
|
|
210
|
+
file and the share directory across gateway restarts. This does not introduce
|
|
211
|
+
a distributed lease database; separate hosts need a shared authority rather
|
|
212
|
+
than independent copies of `leases.json`.
|
|
213
|
+
|
|
126
214
|
## Generated files
|
|
127
215
|
|
|
128
216
|
By default, the command writes under `.snulbug/shares/share-*` and creates:
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
1. Finalize `CHANGELOG.md`, with a dated release section and upgrade notes. Keep
|
|
4
4
|
protocol preview limitations distinct from package release readiness.
|
|
5
5
|
2. Bump `version` in `pyproject.toml` and `snulbug/__init__.py`, update the
|
|
6
|
-
devcontainer feature manifest and its docs/example references
|
|
6
|
+
devcontainer feature manifest and its docs/example references (including
|
|
7
|
+
`examples/codespace_local_gateway/.devcontainer/devcontainer.json`), and run `uv lock`.
|
|
7
8
|
3. Run verification:
|
|
8
9
|
|
|
9
10
|
```bash
|
|
@@ -48,7 +49,7 @@ python -m zipfile -l dist/snulbug-*.whl
|
|
|
48
49
|
8. Install from TestPyPI in a clean environment and smoke test:
|
|
49
50
|
|
|
50
51
|
```bash
|
|
51
|
-
uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ snulbug==0.2.
|
|
52
|
+
uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ snulbug==0.2.2 --help
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
9. Run the manual `Publish` workflow with `repository = pypi` when the TestPyPI
|
|
@@ -56,4 +57,4 @@ uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org
|
|
|
56
57
|
10. Create the GitHub release and tag after the PyPI package is available.
|
|
57
58
|
|
|
58
59
|
The `Publish` workflow uses `workflow_dispatch`, not a tag trigger. Creating or
|
|
59
|
-
pushing `v0.2.
|
|
60
|
+
pushing `v0.2.2` does not publish a package or automatically create a GitHub release.
|
{snulbug-0.2.1 → snulbug-0.2.2}/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.2": {
|
|
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.2": {
|
|
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.2",
|
|
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
|
"""Local-dev MCP policy proxy and programmable Lua ASGI policy layer."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.2"
|
|
4
4
|
|
|
5
5
|
from .auth_providers import (
|
|
6
6
|
AuthProvider,
|
|
@@ -206,6 +206,7 @@ from .leases import (
|
|
|
206
206
|
create_lease,
|
|
207
207
|
list_leases,
|
|
208
208
|
reactivate_lease,
|
|
209
|
+
renew_lease,
|
|
209
210
|
revoke_lease,
|
|
210
211
|
)
|
|
211
212
|
from .manifests import load_manifest, manifest_digest, sign_upstream_manifest, verify_upstream_manifest, write_manifest
|
|
@@ -746,6 +747,7 @@ __all__ = [
|
|
|
746
747
|
"record_audit_event",
|
|
747
748
|
"record_policy_request",
|
|
748
749
|
"reactivate_lease",
|
|
750
|
+
"renew_lease",
|
|
749
751
|
"reconcile_fabric_controller",
|
|
750
752
|
"reconcile_policy_activation",
|
|
751
753
|
"register_fabric_member",
|
|
@@ -627,6 +627,12 @@ def handle_mcp_share_command(args: argparse.Namespace, parser: argparse.Argument
|
|
|
627
627
|
ttl=args.ttl,
|
|
628
628
|
max_calls=args.max_calls,
|
|
629
629
|
client_name=args.client_name,
|
|
630
|
+
renewable=args.renewable,
|
|
631
|
+
renewal_ttl=args.renewal_ttl,
|
|
632
|
+
allow_subjects=args.allow_subject,
|
|
633
|
+
allow_issuers=args.allow_issuer,
|
|
634
|
+
allow_tenants=args.allow_tenant,
|
|
635
|
+
allow_client_ids=args.allow_client_id,
|
|
630
636
|
)
|
|
631
637
|
elif args.share_invite_command == "list":
|
|
632
638
|
result = list_mcp_share_invites(args.directory, include_revoked=not args.active_only)
|
|
@@ -1488,6 +1494,22 @@ def _add_share_invite_args(parser: argparse.ArgumentParser) -> None:
|
|
|
1488
1494
|
invite_create.add_argument("--ttl", default="30m", help="invite lease TTL, such as 30m, 2h, or 1d")
|
|
1489
1495
|
invite_create.add_argument("--max-calls", type=int, help="maximum number of allowed tools/call uses")
|
|
1490
1496
|
invite_create.add_argument("--client-name", help="MCP client config server name")
|
|
1497
|
+
invite_create.add_argument(
|
|
1498
|
+
"--renewable", action="store_true", help="allow this automation to renew/recover its short-lived lease"
|
|
1499
|
+
)
|
|
1500
|
+
invite_create.add_argument(
|
|
1501
|
+
"--renewal-ttl", default="7d", help="hard renewal authorization lifetime; defaults to 7d"
|
|
1502
|
+
)
|
|
1503
|
+
invite_create.add_argument(
|
|
1504
|
+
"--allow-subject", action="append", default=[], help="bind the invite to this authenticated subject"
|
|
1505
|
+
)
|
|
1506
|
+
invite_create.add_argument(
|
|
1507
|
+
"--allow-issuer", action="append", default=[], help="bind the invite to this OAuth issuer"
|
|
1508
|
+
)
|
|
1509
|
+
invite_create.add_argument("--allow-tenant", action="append", default=[], help="bind the invite to this tenant")
|
|
1510
|
+
invite_create.add_argument(
|
|
1511
|
+
"--allow-client-id", action="append", default=[], help="bind the invite to this OAuth workload client"
|
|
1512
|
+
)
|
|
1491
1513
|
add_compact_arg(invite_create)
|
|
1492
1514
|
|
|
1493
1515
|
invite_list = invite_subparsers.add_parser("list", help="list MCP share invites without revealing tokens")
|
|
@@ -1,21 +1,70 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
+
import hmac
|
|
4
5
|
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
5
8
|
import posixpath
|
|
6
9
|
import secrets
|
|
7
10
|
import shlex
|
|
8
|
-
|
|
11
|
+
import threading
|
|
12
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
13
|
+
from contextlib import contextmanager
|
|
9
14
|
from dataclasses import dataclass
|
|
10
15
|
from datetime import datetime, timedelta, timezone
|
|
16
|
+
from functools import wraps
|
|
11
17
|
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
18
|
+
from typing import Any, Iterator
|
|
13
19
|
from urllib.parse import urlparse
|
|
14
20
|
|
|
15
21
|
from .middleware import Scope
|
|
16
22
|
|
|
17
23
|
LEASE_HEADER = "x-snulbug-lease"
|
|
18
24
|
LEASE_TOKEN_PREFIX = "sbl_"
|
|
25
|
+
LEASE_RENEWAL_METHOD = "snulbug/lease/renew"
|
|
26
|
+
LEASE_RENEWAL_HEADER = "x-snulbug-renewal"
|
|
27
|
+
_STORE_LOCK = threading.RLock()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@contextmanager
|
|
31
|
+
def _store_transaction(path: str | Path) -> Iterator[None]:
|
|
32
|
+
"""Serialize read/modify/write across gateway workers and console threads."""
|
|
33
|
+
|
|
34
|
+
store_path = Path(path)
|
|
35
|
+
store_path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
with _STORE_LOCK, store_path.with_name(store_path.name + ".lock").open("a+b") as lock:
|
|
37
|
+
if os.name == "nt": # pragma: no cover - Windows CI
|
|
38
|
+
import msvcrt
|
|
39
|
+
|
|
40
|
+
if not lock.seek(0, 2):
|
|
41
|
+
lock.write(b"\0")
|
|
42
|
+
lock.flush()
|
|
43
|
+
lock.seek(0)
|
|
44
|
+
msvcrt.locking(lock.fileno(), msvcrt.LK_LOCK, 1)
|
|
45
|
+
try:
|
|
46
|
+
yield
|
|
47
|
+
finally:
|
|
48
|
+
lock.seek(0)
|
|
49
|
+
msvcrt.locking(lock.fileno(), msvcrt.LK_UNLCK, 1)
|
|
50
|
+
else:
|
|
51
|
+
import fcntl
|
|
52
|
+
|
|
53
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
|
54
|
+
try:
|
|
55
|
+
yield
|
|
56
|
+
finally:
|
|
57
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _lease_transaction(function):
|
|
61
|
+
@wraps(function)
|
|
62
|
+
def locked(path, *args, **kwargs):
|
|
63
|
+
with _store_transaction(path):
|
|
64
|
+
return function(path, *args, **kwargs)
|
|
65
|
+
|
|
66
|
+
return locked
|
|
67
|
+
|
|
19
68
|
|
|
20
69
|
PATH_KEYS = {
|
|
21
70
|
"path",
|
|
@@ -42,6 +91,7 @@ class LeasePolicyConfig:
|
|
|
42
91
|
header: str = LEASE_HEADER
|
|
43
92
|
|
|
44
93
|
|
|
94
|
+
@_lease_transaction
|
|
45
95
|
def create_lease(
|
|
46
96
|
path: str | Path,
|
|
47
97
|
*,
|
|
@@ -61,6 +111,9 @@ def create_lease(
|
|
|
61
111
|
max_calls: int | None = None,
|
|
62
112
|
token: str | None = None,
|
|
63
113
|
invite: Mapping[str, Any] | None = None,
|
|
114
|
+
renewal_ttl: str | int | float | None = None,
|
|
115
|
+
renewal_token: str | None = None,
|
|
116
|
+
renewal_bearer: str | None = None,
|
|
64
117
|
) -> dict[str, Any]:
|
|
65
118
|
"""Create a task-scoped lease and return the one-time plaintext token."""
|
|
66
119
|
|
|
@@ -99,6 +152,29 @@ def create_lease(
|
|
|
99
152
|
invite_metadata = _lease_invite_metadata(invite)
|
|
100
153
|
if invite_metadata:
|
|
101
154
|
lease["invite"] = invite_metadata
|
|
155
|
+
if renewal_ttl is not None:
|
|
156
|
+
if not invite_metadata.get("id"):
|
|
157
|
+
raise ValueError("renewable leases must belong to a share invite")
|
|
158
|
+
lease_seconds = _parse_ttl(ttl)
|
|
159
|
+
authorization_seconds = _parse_ttl(renewal_ttl)
|
|
160
|
+
if not math.isfinite(lease_seconds) or not math.isfinite(authorization_seconds):
|
|
161
|
+
raise ValueError("renewal and lease TTL must be finite")
|
|
162
|
+
if authorization_seconds < lease_seconds:
|
|
163
|
+
raise ValueError("renewal authorization TTL must be at least the lease TTL")
|
|
164
|
+
if not renewal_bearer and not (allow_issuers and (allow_subjects or allow_client_ids)):
|
|
165
|
+
raise ValueError("renewal requires bearer authentication or an issuer-bound subject/client identity")
|
|
166
|
+
renewal_token = renewal_token or "sbr_" + secrets.token_urlsafe(32)
|
|
167
|
+
lease_token = lease_token_for_renewal(renewal_token, lease["id"], 0)
|
|
168
|
+
lease["token_hash"] = _token_hash(lease_token)
|
|
169
|
+
lease["renewal"] = {
|
|
170
|
+
"authorization_expires_at": _format_time(now + timedelta(seconds=authorization_seconds)),
|
|
171
|
+
"lease_ttl_seconds": lease_seconds,
|
|
172
|
+
"token_hash": _token_hash(renewal_token),
|
|
173
|
+
"bearer_hash": _token_hash(renewal_bearer) if renewal_bearer else None,
|
|
174
|
+
"generation": 0,
|
|
175
|
+
"renewal_count": 0,
|
|
176
|
+
"last_renewed_at": None,
|
|
177
|
+
}
|
|
102
178
|
store_path = Path(path)
|
|
103
179
|
store = _load_store(store_path, create_missing=True)
|
|
104
180
|
store.setdefault("leases", []).append(lease)
|
|
@@ -109,6 +185,7 @@ def create_lease(
|
|
|
109
185
|
"token": lease_token,
|
|
110
186
|
"headers": {LEASE_HEADER: lease_token},
|
|
111
187
|
"file": str(store_path),
|
|
188
|
+
**({"renewal_token": renewal_token} if renewal_ttl is not None else {}),
|
|
112
189
|
}
|
|
113
190
|
|
|
114
191
|
|
|
@@ -123,25 +200,28 @@ def list_leases(path: str | Path, *, include_inactive: bool = True) -> dict[str,
|
|
|
123
200
|
return {"ok": True, "file": str(store_path), "leases": leases}
|
|
124
201
|
|
|
125
202
|
|
|
203
|
+
@_lease_transaction
|
|
126
204
|
def cleanup_inactive_leases(path: str | Path) -> dict[str, Any]:
|
|
127
205
|
"""Remove revoked or expired leases from a lease store."""
|
|
128
206
|
|
|
129
207
|
store_path = Path(path)
|
|
130
208
|
store = _load_store(store_path, create_missing=True)
|
|
131
209
|
leases = list(_leases(store))
|
|
132
|
-
kept = [lease for lease in leases if _lease_view(lease)["active"]]
|
|
133
|
-
removed = [lease for lease in leases if
|
|
210
|
+
kept = [lease for lease in leases if _lease_view(lease)["active"] or _renewal_view(lease).get("active")]
|
|
211
|
+
removed = [lease for lease in leases if lease not in kept]
|
|
134
212
|
store["leases"] = kept
|
|
135
213
|
_write_store(store_path, store)
|
|
136
214
|
return {
|
|
137
215
|
"ok": True,
|
|
138
216
|
"file": str(store_path),
|
|
139
217
|
"removed_count": len(removed),
|
|
140
|
-
"active_count":
|
|
218
|
+
"active_count": sum(bool(_lease_view(lease)["active"]) for lease in kept),
|
|
219
|
+
"retained_renewable_count": sum(not _lease_view(lease)["active"] for lease in kept),
|
|
141
220
|
"leases": [_lease_view(lease) for lease in kept],
|
|
142
221
|
}
|
|
143
222
|
|
|
144
223
|
|
|
224
|
+
@_lease_transaction
|
|
145
225
|
def revoke_lease(path: str | Path, lease_id: str) -> dict[str, Any]:
|
|
146
226
|
"""Revoke a lease by id."""
|
|
147
227
|
|
|
@@ -156,6 +236,17 @@ def revoke_lease(path: str | Path, lease_id: str) -> dict[str, Any]:
|
|
|
156
236
|
return {"ok": False, "file": str(store_path), "error": f"lease not found: {lease_id}"}
|
|
157
237
|
|
|
158
238
|
|
|
239
|
+
@_lease_transaction
|
|
240
|
+
def revoke_invite_renewal(path: str | Path, invite_id: str) -> None:
|
|
241
|
+
store_path = Path(path)
|
|
242
|
+
store = _load_store(store_path, create_missing=True)
|
|
243
|
+
for lease in _leases(store):
|
|
244
|
+
if lease.get("invite", {}).get("id") == invite_id and isinstance(lease.get("renewal"), dict):
|
|
245
|
+
lease["renewal"]["revoked_at"] = lease["renewal"].get("revoked_at") or _format_time(_now())
|
|
246
|
+
_write_store(store_path, store)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@_lease_transaction
|
|
159
250
|
def reactivate_lease(
|
|
160
251
|
path: str | Path,
|
|
161
252
|
lease_id: str,
|
|
@@ -174,6 +265,8 @@ def reactivate_lease(
|
|
|
174
265
|
now = _now()
|
|
175
266
|
for lease in _leases(store):
|
|
176
267
|
if lease.get("id") == lease_id:
|
|
268
|
+
if lease.get("renewal"):
|
|
269
|
+
raise ValueError("create a new automation invite instead of reactivating a renewable lease")
|
|
177
270
|
lease["revoked_at"] = None
|
|
178
271
|
lease["expires_at"] = _format_time(now + timedelta(seconds=_parse_ttl(ttl)))
|
|
179
272
|
lease["reactivated_at"] = _format_time(now)
|
|
@@ -194,6 +287,88 @@ def reactivate_lease(
|
|
|
194
287
|
return {"ok": False, "file": str(store_path), "error": f"lease not found: {lease_id}"}
|
|
195
288
|
|
|
196
289
|
|
|
290
|
+
def lease_token_for_renewal(renewal_token: str, lease_id: str, generation: int) -> str:
|
|
291
|
+
"""Recover the current access token without persisting plaintext in the lease store."""
|
|
292
|
+
|
|
293
|
+
payload = f"snulbug.lease.v1:{lease_id}:{generation}".encode()
|
|
294
|
+
return LEASE_TOKEN_PREFIX + hmac.new(renewal_token.encode(), payload, hashlib.sha256).hexdigest()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@_lease_transaction
|
|
298
|
+
def renew_lease(
|
|
299
|
+
path: str | Path,
|
|
300
|
+
*,
|
|
301
|
+
invite_id: str,
|
|
302
|
+
renewal_token: str,
|
|
303
|
+
bearer: str | None = None,
|
|
304
|
+
auth_context: Mapping[str, Any] | None = None,
|
|
305
|
+
policy_capabilities: Sequence[str] | Callable[[], Sequence[str]] | None = None,
|
|
306
|
+
) -> dict[str, Any]:
|
|
307
|
+
"""Renew or recover an invite's existing lease, never its permissions or budget."""
|
|
308
|
+
|
|
309
|
+
def denied(reason: str) -> dict[str, Any]:
|
|
310
|
+
return {"ok": False, "reason_code": reason}
|
|
311
|
+
|
|
312
|
+
store = _load_store(Path(path), create_missing=False)
|
|
313
|
+
lease = next((item for item in _leases(store) if item.get("invite", {}).get("id") == invite_id), None)
|
|
314
|
+
renewal = lease.get("renewal") if lease else None
|
|
315
|
+
if (
|
|
316
|
+
not isinstance(renewal, dict)
|
|
317
|
+
or not renewal_token
|
|
318
|
+
or not hmac.compare_digest(str(renewal.get("token_hash") or ""), _token_hash(renewal_token))
|
|
319
|
+
):
|
|
320
|
+
return denied("lease.renewal_invalid")
|
|
321
|
+
if lease.get("revoked_at") or renewal.get("revoked_at"):
|
|
322
|
+
return denied("lease.revoked")
|
|
323
|
+
now = _now()
|
|
324
|
+
until = _parse_time(renewal.get("authorization_expires_at"))
|
|
325
|
+
if until is None or until <= now:
|
|
326
|
+
return denied("lease.renewal_expired")
|
|
327
|
+
auth = auth_context or {}
|
|
328
|
+
if renewal.get("bearer_hash") and auth.get("enabled") is not True:
|
|
329
|
+
if not bearer or not hmac.compare_digest(str(renewal["bearer_hash"]), _token_hash(bearer)):
|
|
330
|
+
return denied("lease.renewal_auth_required")
|
|
331
|
+
elif auth.get("enabled") is not True:
|
|
332
|
+
return denied("lease.auth_missing")
|
|
333
|
+
auth_denial = _lease_auth_denial_reason(lease, auth)
|
|
334
|
+
if auth_denial:
|
|
335
|
+
return denied(str(auth_denial["reason_code"]))
|
|
336
|
+
if callable(policy_capabilities):
|
|
337
|
+
policy_capabilities = policy_capabilities()
|
|
338
|
+
if policy_capabilities is not None and not set(lease.get("capabilities", [])).issubset(policy_capabilities):
|
|
339
|
+
return denied("lease.renewal_capability_removed")
|
|
340
|
+
if isinstance(lease.get("max_calls"), int) and int(lease.get("use_count") or 0) >= lease["max_calls"]:
|
|
341
|
+
return denied("lease.max_calls_exceeded")
|
|
342
|
+
|
|
343
|
+
expires = _parse_time(lease.get("expires_at"))
|
|
344
|
+
seconds = float(renewal["lease_ttl_seconds"])
|
|
345
|
+
# Retrying/restarting inside a healthy lease window returns the same token.
|
|
346
|
+
# A short overlap lets in-flight requests finish when the next window opens.
|
|
347
|
+
next_expiry = min(until, now + timedelta(seconds=seconds))
|
|
348
|
+
changed = expires is None or ((expires - now).total_seconds() <= seconds / 2 and next_expiry > expires)
|
|
349
|
+
if changed:
|
|
350
|
+
if expires is not None and expires > now:
|
|
351
|
+
renewal["previous_token_hash"] = lease["token_hash"]
|
|
352
|
+
renewal["previous_token_expires_at"] = _format_time(min(expires, now + timedelta(seconds=30)))
|
|
353
|
+
else:
|
|
354
|
+
renewal.pop("previous_token_hash", None)
|
|
355
|
+
renewal.pop("previous_token_expires_at", None)
|
|
356
|
+
renewal["generation"] = int(renewal.get("generation") or 0) + 1
|
|
357
|
+
renewal["renewal_count"] = int(renewal.get("renewal_count") or 0) + 1
|
|
358
|
+
renewal["last_renewed_at"] = _format_time(now)
|
|
359
|
+
lease["expires_at"] = _format_time(next_expiry)
|
|
360
|
+
token = lease_token_for_renewal(renewal_token, str(lease["id"]), renewal["generation"])
|
|
361
|
+
lease["token_hash"] = _token_hash(token)
|
|
362
|
+
_write_store(Path(path), store)
|
|
363
|
+
token = lease_token_for_renewal(renewal_token, str(lease["id"]), int(renewal.get("generation") or 0))
|
|
364
|
+
return {
|
|
365
|
+
"ok": True,
|
|
366
|
+
"reason_code": "lease.renewed" if changed else "lease.recovered",
|
|
367
|
+
"lease": _lease_view(lease),
|
|
368
|
+
"token": token,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
197
372
|
def enforce_mcp_lease_policy(
|
|
198
373
|
request: Mapping[str, Any] | None,
|
|
199
374
|
scope: Scope,
|
|
@@ -300,6 +475,19 @@ def _evaluate_mcp_lease_policy(
|
|
|
300
475
|
*,
|
|
301
476
|
config: LeasePolicyConfig,
|
|
302
477
|
consume: bool,
|
|
478
|
+
) -> tuple[bool, dict[str, Any]]:
|
|
479
|
+
if consume and config.lease_file is not None:
|
|
480
|
+
with _store_transaction(config.lease_file):
|
|
481
|
+
return _evaluate_locked_lease_policy(request, scope, config=config, consume=consume)
|
|
482
|
+
return _evaluate_locked_lease_policy(request, scope, config=config, consume=consume)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _evaluate_locked_lease_policy(
|
|
486
|
+
request: Mapping[str, Any] | None,
|
|
487
|
+
scope: Scope,
|
|
488
|
+
*,
|
|
489
|
+
config: LeasePolicyConfig,
|
|
490
|
+
consume: bool,
|
|
303
491
|
) -> tuple[bool, dict[str, Any]]:
|
|
304
492
|
method = request.get("method") if isinstance(request, Mapping) else None
|
|
305
493
|
auth_context = _scope_auth_context(scope)
|
|
@@ -581,6 +769,7 @@ def _write_store(path: Path, store: Mapping[str, Any]) -> None:
|
|
|
581
769
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
582
770
|
tmp = path.with_name(f"{path.name}.tmp")
|
|
583
771
|
tmp.write_text(json.dumps(store, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
772
|
+
tmp.chmod(0o600)
|
|
584
773
|
tmp.replace(path)
|
|
585
774
|
|
|
586
775
|
|
|
@@ -592,7 +781,15 @@ def _leases(store: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
|
592
781
|
def _find_lease(store: Mapping[str, Any], token: str) -> dict[str, Any] | None:
|
|
593
782
|
digest = _token_hash(token)
|
|
594
783
|
for lease in _leases(store):
|
|
595
|
-
if lease.get("token_hash")
|
|
784
|
+
if hmac.compare_digest(str(lease.get("token_hash") or ""), digest):
|
|
785
|
+
return lease
|
|
786
|
+
renewal = lease.get("renewal", {})
|
|
787
|
+
previous_until = _parse_time(renewal.get("previous_token_expires_at"))
|
|
788
|
+
if (
|
|
789
|
+
previous_until is not None
|
|
790
|
+
and previous_until > _now()
|
|
791
|
+
and hmac.compare_digest(str(renewal.get("previous_token_hash") or ""), digest)
|
|
792
|
+
):
|
|
596
793
|
return lease
|
|
597
794
|
return None
|
|
598
795
|
|
|
@@ -625,6 +822,30 @@ def _lease_view(lease: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
625
822
|
"use_count": int(lease.get("use_count") or 0),
|
|
626
823
|
"last_used_at": lease.get("last_used_at"),
|
|
627
824
|
"last_tool": lease.get("last_tool"),
|
|
825
|
+
**({"renewal": _renewal_view(lease)} if lease.get("renewal") else {}),
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _renewal_view(lease: Mapping[str, Any]) -> dict[str, Any]:
|
|
830
|
+
renewal = lease.get("renewal")
|
|
831
|
+
if not isinstance(renewal, Mapping):
|
|
832
|
+
return {}
|
|
833
|
+
until = _parse_time(renewal.get("authorization_expires_at"))
|
|
834
|
+
expires = _parse_time(lease.get("expires_at"))
|
|
835
|
+
ttl = float(renewal.get("lease_ttl_seconds") or 0)
|
|
836
|
+
return {
|
|
837
|
+
"active": bool(
|
|
838
|
+
not lease.get("revoked_at") and not renewal.get("revoked_at") and until is not None and until > _now()
|
|
839
|
+
),
|
|
840
|
+
"revoked_at": renewal.get("revoked_at"),
|
|
841
|
+
"authorization_expires_at": renewal.get("authorization_expires_at"),
|
|
842
|
+
"lease_ttl_seconds": ttl,
|
|
843
|
+
"generation": int(renewal.get("generation") or 0),
|
|
844
|
+
"renewal_count": int(renewal.get("renewal_count") or 0),
|
|
845
|
+
"last_renewed_at": renewal.get("last_renewed_at"),
|
|
846
|
+
"renew_after": _format_time(expires if expires == until else expires - timedelta(seconds=ttl / 2))
|
|
847
|
+
if expires
|
|
848
|
+
else None,
|
|
628
849
|
}
|
|
629
850
|
|
|
630
851
|
|