stkit 17.2.2__tar.gz → 17.2.3__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.
- {stkit-17.2.2 → stkit-17.2.3}/CHANGELOG.md +12 -0
- {stkit-17.2.2 → stkit-17.2.3}/PKG-INFO +1 -1
- {stkit-17.2.2 → stkit-17.2.3}/pyproject.toml +1 -1
- {stkit-17.2.2 → stkit-17.2.3}/stk/__init__.py +1 -1
- {stkit-17.2.2 → stkit-17.2.3}/stk/app.py +6 -3
- {stkit-17.2.2 → stkit-17.2.3}/stk/mcp/__init__.py +10 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/mcp/models.py +1 -1
- {stkit-17.2.2 → stkit-17.2.3}/stk/migrations/versions/stk_0005_mcp_tokens.py +13 -10
- stkit-17.2.3/stk/migrations/versions/stk_0006_stk_mcp_token.py +69 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/utils/netguard.py +78 -24
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_mcp.py +16 -1
- stkit-17.2.3/tests/test_mcp_migration.py +62 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_netguard.py +59 -1
- {stkit-17.2.2 → stkit-17.2.3}/.gitignore +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/LICENSE +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/README.md +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/SECURITY.md +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/.env-sample +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/.gitignore +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/README.md +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/minimal/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/minimal/app.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/minimal/migrations/364232ae77d5_add_note.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/minimal/note/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/minimal/note/models.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/minimal/note/templates/note/index.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/minimal/note/views.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/minimal/settings.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/minimal/templates/layout.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/pyproject.toml +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/tests/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/tests/test_app.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/examples/minimal/uv.lock +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/__main__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/agent_login.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/checks.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/agent.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/base.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/copy_rules.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/database.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/develop.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/doctor.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/main.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/reports.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/smoke.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/users.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/cli/watch.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/extensions.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/files/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/files/models.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/files/store.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/files/views.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/mcp/server.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/mcp/views.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/migrations/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/migrations/env.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/migrations/script.py.mako +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/migrations/versions/stk_0001_initial_schema.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/migrations/versions/stk_0002_jobs.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/migrations/versions/stk_0003_files.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/migrations/versions/stk_0004_notifications.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/notification/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/notification/models.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/notification/views.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/portal/views.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/public/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/public/models.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/public/views.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/CLAUDE.md +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/Dockerfile +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/README.md +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/__app__/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/__app__/app.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/__app__/settings.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/claude/skills/stk-api/SKILL.md +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/claude/skills/stk-blueprint/SKILL.md +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/claude/skills/stk-deploy/SKILL.md +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/claude/skills/stk-lessons/SKILL.md +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/claude/skills/stk-migrate/SKILL.md +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/docker-compose.yml +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/dockerignore +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/env-sample +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/github/workflows/checks.yml +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/gitignore +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/nginx/nginx.conf +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/nginx/stk.conf +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/postgres/init-db.sh +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/pyproject.toml +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/tests/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/app/tests/test_app.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/generator.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/scaffold/templates.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/security.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/settings.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/shell.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/VERSIONS.txt +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/css/app.css +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/css/layout.css +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/css/vuetify.min.css +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.animals.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.arrows-1.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.arrows-2.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.badges.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.brand-1.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.brand-2.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.buildings.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.charts.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.communication.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.computers.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.core.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.currencies.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.database.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.design-1.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.design-2.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.development.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.devices-1.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.devices-2.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.document.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.e-commerce.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.electrical.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.extensions.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.food.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.games.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.gender.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.gestures.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.health.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.laundry.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.letters.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.logic.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.map.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.math.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.media.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.mood.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.nature.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.numbers.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.photography.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.shapes.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.sport.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.symbols.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.system-1.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.system-2.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.system-3.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.text.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.vehicles.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.version-control.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.weather.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/fonts/tabler-icons.zodiac.woff2 +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/icons/tabler-icons.min.css +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/img/auth-bg.webp +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/img/favicon.ico +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/img/stk.svg +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/axios.min.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/components/NotificationDropdown.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/components/ThemeSwitcher.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/components/TransitionExpand.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/components/VerticalNavGroup.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/components/VerticalNavLink.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/components/VerticalNavSectionTitle.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/components/index.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/config.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/vue.min.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/js/vuetify.min.js +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/static/robots.txt +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/tasks/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/tasks/models.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/401.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/404.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/500.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/_brand.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/auth_layout.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/cms/activities.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/cms/jobs.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/cms/roles.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/cms/users.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/dashboard.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/index.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/layout.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/login-layout.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/mcp/tokens.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/_macros.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/_messages.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/change_password.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/confirm_email.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/change_notice.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/change_notice.txt +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/confirmation_instructions.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/confirmation_instructions.txt +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/login_instructions.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/login_instructions.txt +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/reset_instructions.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/reset_instructions.txt +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/reset_notice.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/reset_notice.txt +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/welcome.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/email/welcome.txt +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/forgot_password.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/login_user.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/mf_recovery.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/mf_recovery_codes.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/register_user.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/reset_password.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/send_confirmation.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/send_login.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/two_factor_select.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/two_factor_setup.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/two_factor_verify_code.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/wan_register.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/wan_signin.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/templates/security/wan_verify.html +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/user/__init__.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/user/forms.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/user/models.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/user/views.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/utils/email.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/utils/ratelimit.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/stk/websocket.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_agent_operability.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_app_index.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_branding.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_builder_ux.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_cli.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_composition.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_copy_rules.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_csrf_exempt.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_error_handler.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_files.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_jobs.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_migration_drift.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_notifications.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_production_security.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_revision_imports.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_scaffolder.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_security_compatibility.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_shell.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_smoke_pages.py +0 -0
- {stkit-17.2.2 → stkit-17.2.3}/tests/test_static_versioning.py +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v17.2.3 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
Run `uv run stk db upgrade`.
|
|
6
|
+
|
|
7
|
+
- MCP is opt-in, like file uploads: add `stk.mcp` to the app's extensions. Without it stk registers no `mcp` blueprint, no `/mcp`, no `/tokens/` page and no Agent Tokens menu entry, so an app can serve its own MCP endpoint. From 17.2.0 to 17.2.2 stk claimed all of them in every app (JSON APIs).
|
|
8
|
+
- stk's MCP token table is `stk_mcp_token`, like every other core table. 17.2.0 to 17.2.2 created `mcp_token`, which failed to migrate in an app that already had a table by that name. Revision `stk_0006` moves stk's own tokens across; an app's own `mcp_token` is recognised by its columns and left alone.
|
|
9
|
+
- `stk.utils.netguard` adds `stream`, which yields the response with its body unread for large downloads and can cap decoded bytes with `max_bytes` or return a redirect with `follow_redirects=False`, and a public `pin(url)` that vets a URL without fetching it, for a client such as a headless browser. `fetch` is built on `stream`. Cookies are never carried across redirect hops (JSON APIs).
|
|
10
|
+
|
|
11
|
+
### Upgrade considerations
|
|
12
|
+
|
|
13
|
+
- An app that uses stk's MCP tools adds `stk.mcp` to its extensions, or `/mcp` and `/tokens/` return 404. Existing tokens keep working after `stk db upgrade`.
|
|
14
|
+
|
|
3
15
|
## v17.2.2 (2026-10-31)
|
|
4
16
|
|
|
5
17
|
No migration required.
|
|
@@ -27,7 +27,6 @@ from werkzeug.security import safe_join
|
|
|
27
27
|
import stk.extensions as ext
|
|
28
28
|
from stk.agent_login import agent_login_enabled, bp_agent_login
|
|
29
29
|
from stk.extensions import session
|
|
30
|
-
from stk.mcp.views import bp_mcp
|
|
31
30
|
from stk.notification.views import bp_notification
|
|
32
31
|
from stk.portal.views import portal
|
|
33
32
|
from stk.public.views import public
|
|
@@ -209,7 +208,6 @@ def register_blueprints(app):
|
|
|
209
208
|
app.register_blueprint(portal)
|
|
210
209
|
app.register_blueprint(bp_notification)
|
|
211
210
|
app.register_blueprint(ws_bp)
|
|
212
|
-
app.register_blueprint(bp_mcp)
|
|
213
211
|
if agent_login_enabled(app):
|
|
214
212
|
app.register_blueprint(bp_agent_login)
|
|
215
213
|
return None
|
|
@@ -300,7 +298,12 @@ def register_navigation(app):
|
|
|
300
298
|
app.jinja_env.globals["stk_navigation"] = lambda: [
|
|
301
299
|
*CORE_NAVIGATION_START,
|
|
302
300
|
*app.navigation,
|
|
303
|
-
*
|
|
301
|
+
*(
|
|
302
|
+
entry
|
|
303
|
+
for entry in CORE_NAVIGATION_END
|
|
304
|
+
# Agent Tokens only for apps that opted in with `stk.mcp`.
|
|
305
|
+
if entry.get("to") != "/tokens/" or "stk.mcp" in app.extensions
|
|
306
|
+
),
|
|
304
307
|
]
|
|
305
308
|
# The app's entries alone, for a layout that draws its own sidebar.
|
|
306
309
|
app.jinja_env.globals["app_navigation"] = lambda: app.navigation
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""Expose an app's own functions to AI agents over MCP at `/mcp`.
|
|
2
2
|
|
|
3
|
+
Opt in with `extensions=[stk.mcp, ...]`; without it there is no `/mcp`, no
|
|
4
|
+
`/tokens/` page, and an app is free to serve its own MCP endpoint.
|
|
5
|
+
|
|
3
6
|
A tool is an async function taking the validated arguments dict. It runs as the
|
|
4
7
|
token's user: `current_user` and `g.db_session` work as in a request handler.
|
|
5
8
|
"""
|
|
@@ -51,3 +54,10 @@ def tool(name, description, schema, *, role=None):
|
|
|
51
54
|
return fn
|
|
52
55
|
|
|
53
56
|
return decorator
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def init_app(app):
|
|
60
|
+
from stk.mcp.views import bp_mcp
|
|
61
|
+
|
|
62
|
+
app.extensions["stk.mcp"] = True
|
|
63
|
+
app.register_blueprint(bp_mcp)
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
"""MCP bearer tokens: one row per token, storing only its sha256.
|
|
1
|
+
"""MCP bearer tokens: one row per token, storing only its sha256.
|
|
2
|
+
|
|
3
|
+
Until 17.2.3 this created `mcp_token`, a name an app may already use for its own
|
|
4
|
+
table; stk_0006 moves such an stk table to `stk_mcp_token`."""
|
|
2
5
|
|
|
3
6
|
import sqlalchemy as sa
|
|
4
7
|
from alembic import op
|
|
@@ -11,7 +14,7 @@ depends_on = None
|
|
|
11
14
|
|
|
12
15
|
def upgrade() -> None:
|
|
13
16
|
op.create_table(
|
|
14
|
-
"
|
|
17
|
+
"stk_mcp_token",
|
|
15
18
|
sa.Column("id", sa.Integer(), nullable=False),
|
|
16
19
|
sa.Column("user_id", sa.Integer(), nullable=False),
|
|
17
20
|
sa.Column("name", sa.String(length=80), nullable=False),
|
|
@@ -24,19 +27,19 @@ def upgrade() -> None:
|
|
|
24
27
|
sa.ForeignKeyConstraint(
|
|
25
28
|
["user_id"],
|
|
26
29
|
["user.id"],
|
|
27
|
-
name=op.f("
|
|
30
|
+
name=op.f("fk_stk_mcp_token_user_id"),
|
|
28
31
|
ondelete="CASCADE",
|
|
29
32
|
),
|
|
30
|
-
sa.PrimaryKeyConstraint("id", name=op.f("
|
|
31
|
-
sa.UniqueConstraint("token_hash", name=op.f("
|
|
33
|
+
sa.PrimaryKeyConstraint("id", name=op.f("pk_stk_mcp_token")),
|
|
34
|
+
sa.UniqueConstraint("token_hash", name=op.f("uq_stk_mcp_token_token_hash")),
|
|
32
35
|
)
|
|
33
|
-
with op.batch_alter_table("
|
|
36
|
+
with op.batch_alter_table("stk_mcp_token", schema=None) as batch_op:
|
|
34
37
|
batch_op.create_index(
|
|
35
|
-
batch_op.f("
|
|
38
|
+
batch_op.f("ix_stk_mcp_token_user_id"), ["user_id"], unique=False
|
|
36
39
|
)
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
def downgrade() -> None:
|
|
40
|
-
with op.batch_alter_table("
|
|
41
|
-
batch_op.drop_index(batch_op.f("
|
|
42
|
-
op.drop_table("
|
|
43
|
+
with op.batch_alter_table("stk_mcp_token", schema=None) as batch_op:
|
|
44
|
+
batch_op.drop_index(batch_op.f("ix_stk_mcp_token_user_id"))
|
|
45
|
+
op.drop_table("stk_mcp_token")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Move stk's MCP tokens from `mcp_token` to `stk_mcp_token`.
|
|
2
|
+
|
|
3
|
+
stk 17.2.0 to 17.2.2 created `mcp_token`, a name an app may already use for its
|
|
4
|
+
own table. stk_0005 now creates `stk_mcp_token`; a database that ran the old
|
|
5
|
+
stk_0005 has stk's tokens in `mcp_token` instead. Only that table moves: it is
|
|
6
|
+
recognised by stk's `user_uniquifier` column, so an app's own `mcp_token` stays.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
|
|
12
|
+
revision = "stk_0006"
|
|
13
|
+
down_revision = "stk_0005"
|
|
14
|
+
branch_labels = None
|
|
15
|
+
depends_on = None
|
|
16
|
+
|
|
17
|
+
COLUMNS = (
|
|
18
|
+
"id, user_id, name, token_hash, user_uniquifier, scopes, created_at,"
|
|
19
|
+
" expires_at, last_used_at"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def upgrade() -> None:
|
|
24
|
+
inspector = sa.inspect(op.get_bind())
|
|
25
|
+
tables = set(inspector.get_table_names())
|
|
26
|
+
if "mcp_token" not in tables:
|
|
27
|
+
return
|
|
28
|
+
legacy = {c["name"] for c in inspector.get_columns("mcp_token")}
|
|
29
|
+
if "user_uniquifier" not in legacy or "token_hash" not in legacy:
|
|
30
|
+
return
|
|
31
|
+
if "stk_mcp_token" not in tables:
|
|
32
|
+
# The database ran the old stk_0005, so the new one never created this.
|
|
33
|
+
_create_stk_mcp_token()
|
|
34
|
+
op.execute(f"INSERT INTO stk_mcp_token ({COLUMNS}) SELECT {COLUMNS} FROM mcp_token")
|
|
35
|
+
# destructive: every row was copied to stk_mcp_token on the line above.
|
|
36
|
+
op.drop_table("mcp_token")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _create_stk_mcp_token():
|
|
40
|
+
# Same table as stk_0005 creates; revision modules cannot import each other.
|
|
41
|
+
op.create_table(
|
|
42
|
+
"stk_mcp_token",
|
|
43
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
44
|
+
sa.Column("user_id", sa.Integer(), nullable=False),
|
|
45
|
+
sa.Column("name", sa.String(length=80), nullable=False),
|
|
46
|
+
sa.Column("token_hash", sa.String(length=64), nullable=False),
|
|
47
|
+
sa.Column("user_uniquifier", sa.String(length=255), nullable=False),
|
|
48
|
+
sa.Column("scopes", sa.JSON(), nullable=False),
|
|
49
|
+
sa.Column("created_at", sa.DateTime(), nullable=False),
|
|
50
|
+
sa.Column("expires_at", sa.DateTime(), nullable=False),
|
|
51
|
+
sa.Column("last_used_at", sa.DateTime(), nullable=True),
|
|
52
|
+
sa.ForeignKeyConstraint(
|
|
53
|
+
["user_id"],
|
|
54
|
+
["user.id"],
|
|
55
|
+
name=op.f("fk_stk_mcp_token_user_id"),
|
|
56
|
+
ondelete="CASCADE",
|
|
57
|
+
),
|
|
58
|
+
sa.PrimaryKeyConstraint("id", name=op.f("pk_stk_mcp_token")),
|
|
59
|
+
sa.UniqueConstraint("token_hash", name=op.f("uq_stk_mcp_token_token_hash")),
|
|
60
|
+
)
|
|
61
|
+
with op.batch_alter_table("stk_mcp_token", schema=None) as batch_op:
|
|
62
|
+
batch_op.create_index(
|
|
63
|
+
batch_op.f("ix_stk_mcp_token_user_id"), ["user_id"], unique=False
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def downgrade() -> None:
|
|
68
|
+
# The old name collided with apps' own tables; there is nothing to restore.
|
|
69
|
+
pass
|
|
@@ -6,6 +6,8 @@ address between the check and the connection. Every redirect hop is re-checked."
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import ipaddress
|
|
8
8
|
import socket
|
|
9
|
+
from collections.abc import AsyncIterator
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
9
11
|
from urllib.parse import urljoin, urlsplit, urlunsplit
|
|
10
12
|
|
|
11
13
|
import httpx2
|
|
@@ -35,7 +37,7 @@ def _public(addr: str) -> bool:
|
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
def _origin(url: str) -> tuple:
|
|
38
|
-
"""(scheme, host, port) with the default port filled in; `
|
|
40
|
+
"""(scheme, host, port) with the default port filled in; `pin` rejects bad ones."""
|
|
39
41
|
parts = urlsplit(url)
|
|
40
42
|
try:
|
|
41
43
|
port = parts.port
|
|
@@ -48,8 +50,14 @@ def _origin(url: str) -> tuple:
|
|
|
48
50
|
)
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
async def
|
|
52
|
-
"""
|
|
53
|
+
async def pin(url: str) -> tuple[str, str, str]:
|
|
54
|
+
"""Check a URL and return (url with the checked IP, Host header, TLS server name).
|
|
55
|
+
|
|
56
|
+
Raise Blocked unless it is http(s), carries no credentials, and every address
|
|
57
|
+
its host resolves to is public. Connect to the returned URL, send the Host
|
|
58
|
+
header, and use the server name for SNI, so the connection goes to the
|
|
59
|
+
address that was checked. Use it alone to vet a URL another client (a
|
|
60
|
+
headless browser) will open."""
|
|
53
61
|
parts = urlsplit(url)
|
|
54
62
|
host = parts.hostname
|
|
55
63
|
if parts.scheme not in ("http", "https") or not host:
|
|
@@ -84,21 +92,45 @@ async def _pin(url: str) -> tuple[str, str, str]:
|
|
|
84
92
|
return target, host_header, host
|
|
85
93
|
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
def _capped(response: httpx2.Response, max_bytes: int) -> None:
|
|
96
|
+
"""Make every read of the body (aread, aiter_bytes/text/lines, .content after
|
|
97
|
+
aread) raise Blocked past `max_bytes` decoded bytes."""
|
|
98
|
+
if int(response.headers.get("content-length") or 0) > max_bytes:
|
|
99
|
+
raise Blocked(f"response is larger than {max_bytes} bytes")
|
|
100
|
+
read = response.aiter_bytes
|
|
101
|
+
|
|
102
|
+
async def aiter_bytes(chunk_size=None):
|
|
103
|
+
seen = 0
|
|
104
|
+
async for chunk in read(chunk_size):
|
|
105
|
+
seen += len(chunk)
|
|
106
|
+
if seen > max_bytes:
|
|
107
|
+
raise Blocked(f"response is larger than {max_bytes} bytes")
|
|
108
|
+
yield chunk
|
|
109
|
+
|
|
110
|
+
# aread, aiter_text and aiter_lines all read through self.aiter_bytes.
|
|
111
|
+
response.aiter_bytes = aiter_bytes
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@asynccontextmanager
|
|
115
|
+
async def stream(
|
|
88
116
|
url: str,
|
|
89
117
|
*,
|
|
90
118
|
headers: dict | None = None,
|
|
91
119
|
timeout: float = 10,
|
|
92
|
-
max_bytes: int =
|
|
120
|
+
max_bytes: int | None = None,
|
|
93
121
|
max_redirects: int = 5,
|
|
122
|
+
follow_redirects: bool = True,
|
|
94
123
|
transport: httpx2.AsyncBaseTransport | None = None,
|
|
95
|
-
) -> tuple[httpx2.Response, str]:
|
|
96
|
-
"""GET a user-supplied URL
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
) -> AsyncIterator[tuple[httpx2.Response, str]]:
|
|
125
|
+
"""GET a user-supplied URL; yield (response with its body unread, final url).
|
|
126
|
+
|
|
127
|
+
`timeout` bounds the whole fetch, including reading the body inside the
|
|
128
|
+
block. With `max_bytes`, reading more decoded bytes than that raises
|
|
129
|
+
Blocked. With `follow_redirects=False` a redirect is yielded as is. Raise
|
|
130
|
+
Blocked when the URL, an address or a redirect is refused or there are more
|
|
131
|
+
than `max_redirects`, TimeoutError past `timeout`, and httpx2.HTTPError for
|
|
132
|
+
transport failures. Environment proxies are ignored: a proxy would resolve
|
|
133
|
+
the name itself and undo the pin."""
|
|
102
134
|
headers = dict(headers or {})
|
|
103
135
|
origin = _origin(url)
|
|
104
136
|
async with (
|
|
@@ -108,7 +140,7 @@ async def fetch(
|
|
|
108
140
|
) as client,
|
|
109
141
|
):
|
|
110
142
|
for _ in range(max_redirects + 1):
|
|
111
|
-
target, host_header, server_name = await
|
|
143
|
+
target, host_header, server_name = await pin(url)
|
|
112
144
|
if _origin(url) != origin:
|
|
113
145
|
# Never hand the caller's credentials to an origin it did not name,
|
|
114
146
|
# including the same host over plain HTTP or another port.
|
|
@@ -128,19 +160,41 @@ async def fetch(
|
|
|
128
160
|
# them across hops.
|
|
129
161
|
client.cookies.clear()
|
|
130
162
|
try:
|
|
131
|
-
if response.has_redirect_location:
|
|
163
|
+
if follow_redirects and response.has_redirect_location:
|
|
132
164
|
url = urljoin(url, response.headers["location"])
|
|
133
165
|
continue
|
|
134
|
-
if
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
body += chunk
|
|
139
|
-
if len(body) > max_bytes:
|
|
140
|
-
raise Blocked(f"response is larger than {max_bytes} bytes")
|
|
141
|
-
# What aread() stores; .content, .text and .json() then work.
|
|
142
|
-
response._content = bytes(body)
|
|
143
|
-
return response, url
|
|
166
|
+
if max_bytes is not None:
|
|
167
|
+
_capped(response, max_bytes)
|
|
168
|
+
yield response, url
|
|
169
|
+
return
|
|
144
170
|
finally:
|
|
145
171
|
await response.aclose()
|
|
146
172
|
raise Blocked("too many redirects")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def fetch(
|
|
176
|
+
url: str,
|
|
177
|
+
*,
|
|
178
|
+
headers: dict | None = None,
|
|
179
|
+
timeout: float = 10,
|
|
180
|
+
max_bytes: int = 1_048_576,
|
|
181
|
+
max_redirects: int = 5,
|
|
182
|
+
transport: httpx2.AsyncBaseTransport | None = None,
|
|
183
|
+
) -> tuple[httpx2.Response, str]:
|
|
184
|
+
"""GET a user-supplied URL. Return (response with its body read, final url).
|
|
185
|
+
|
|
186
|
+
Same checks and errors as `stream`, with the body read and capped at
|
|
187
|
+
`max_bytes` decoded bytes (default 1 MiB)."""
|
|
188
|
+
async with stream(
|
|
189
|
+
url,
|
|
190
|
+
headers=headers,
|
|
191
|
+
timeout=timeout,
|
|
192
|
+
max_bytes=max_bytes,
|
|
193
|
+
max_redirects=max_redirects,
|
|
194
|
+
transport=transport,
|
|
195
|
+
) as (response, final_url):
|
|
196
|
+
# A body the transport already loaded (a test's MockTransport) skips the
|
|
197
|
+
# streaming guard; check what was read.
|
|
198
|
+
if len(await response.aread()) > max_bytes:
|
|
199
|
+
raise Blocked(f"response is larger than {max_bytes} bytes")
|
|
200
|
+
return response, final_url
|
|
@@ -9,6 +9,7 @@ from quart_security import current_user, hash_password
|
|
|
9
9
|
from sqlalchemy import func, select
|
|
10
10
|
|
|
11
11
|
import stk.extensions as ext
|
|
12
|
+
import stk.mcp
|
|
12
13
|
from stk.app import Stk
|
|
13
14
|
from stk.mcp import ToolError, tool
|
|
14
15
|
from stk.mcp.models import McpToken
|
|
@@ -53,7 +54,7 @@ def rpc(method, params=None, rid=1):
|
|
|
53
54
|
|
|
54
55
|
class McpTests(unittest.IsolatedAsyncioTestCase):
|
|
55
56
|
async def asyncSetUp(self):
|
|
56
|
-
self.app = Stk(__name__, config=McpConfig)
|
|
57
|
+
self.app = Stk(__name__, config=McpConfig, extensions=[stk.mcp])
|
|
57
58
|
async with ext.engine.begin() as conn:
|
|
58
59
|
await conn.run_sync(Base.metadata.create_all)
|
|
59
60
|
async with ext.async_session_factory() as db:
|
|
@@ -305,5 +306,19 @@ class McpTests(unittest.IsolatedAsyncioTestCase):
|
|
|
305
306
|
tool("test.array", "Not an object.", {"type": "array"})
|
|
306
307
|
|
|
307
308
|
|
|
309
|
+
class McpOptInTests(unittest.IsolatedAsyncioTestCase):
|
|
310
|
+
async def test_an_app_without_stk_mcp_has_no_mcp_routes_or_menu(self):
|
|
311
|
+
"""An app may serve its own /mcp; stk must not claim it uninvited."""
|
|
312
|
+
app = Stk(__name__, config=McpConfig)
|
|
313
|
+
self.assertNotIn("mcp", app.blueprints)
|
|
314
|
+
self.assertNotIn("/mcp", {r.rule for r in app.url_map.iter_rules()})
|
|
315
|
+
menu = [e.get("to") for e in app.jinja_env.globals["stk_navigation"]()]
|
|
316
|
+
self.assertNotIn("/tokens/", menu)
|
|
317
|
+
with_mcp = Stk(__name__, config=McpConfig, extensions=[stk.mcp])
|
|
318
|
+
menu = [e.get("to") for e in with_mcp.jinja_env.globals["stk_navigation"]()]
|
|
319
|
+
self.assertIn("/tokens/", menu)
|
|
320
|
+
await ext.engine.dispose()
|
|
321
|
+
|
|
322
|
+
|
|
308
323
|
if __name__ == "__main__":
|
|
309
324
|
unittest.main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""stk_0006 moves stk's own `mcp_token` to `stk_mcp_token` and leaves an app's alone."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from alembic import command
|
|
9
|
+
|
|
10
|
+
from stk.migrations import build_alembic_config
|
|
11
|
+
|
|
12
|
+
LEGACY_TOKEN = (
|
|
13
|
+
"INSERT INTO mcp_token (id, user_id, name, token_hash, user_uniquifier,"
|
|
14
|
+
" scopes, created_at, expires_at) VALUES (1, 1, 'agent', 'h', 'u', '[]',"
|
|
15
|
+
" '2026-10-31', '2027-01-29')"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class McpTokenTableMigrationTest(unittest.TestCase):
|
|
20
|
+
def setUp(self):
|
|
21
|
+
self.dir = tempfile.TemporaryDirectory()
|
|
22
|
+
self.path = Path(self.dir.name) / "db.sqlite"
|
|
23
|
+
self.config = build_alembic_config(url=f"sqlite:///{self.path}", quiet=True)
|
|
24
|
+
|
|
25
|
+
def tearDown(self):
|
|
26
|
+
self.dir.cleanup()
|
|
27
|
+
|
|
28
|
+
def sql(self, statement):
|
|
29
|
+
with sqlite3.connect(self.path) as db:
|
|
30
|
+
return db.execute(statement).fetchall()
|
|
31
|
+
|
|
32
|
+
def tables(self):
|
|
33
|
+
return {
|
|
34
|
+
r[0] for r in self.sql("select name from sqlite_master where type='table'")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def test_fresh_database_gets_only_the_prefixed_table(self):
|
|
38
|
+
command.upgrade(self.config, "heads")
|
|
39
|
+
self.assertIn("stk_mcp_token", self.tables())
|
|
40
|
+
self.assertNotIn("mcp_token", self.tables())
|
|
41
|
+
|
|
42
|
+
def test_tokens_from_the_old_table_name_move_over(self):
|
|
43
|
+
# A database that ran stk_0005 from 17.2.0 to 17.2.2.
|
|
44
|
+
command.upgrade(self.config, "stk_0004")
|
|
45
|
+
self.sql(
|
|
46
|
+
"CREATE TABLE mcp_token (id INTEGER PRIMARY KEY, user_id INTEGER, name TEXT,"
|
|
47
|
+
" token_hash TEXT, user_uniquifier TEXT, scopes JSON, created_at DATETIME,"
|
|
48
|
+
" expires_at DATETIME, last_used_at DATETIME)"
|
|
49
|
+
)
|
|
50
|
+
self.sql(LEGACY_TOKEN)
|
|
51
|
+
self.sql("UPDATE alembic_version SET version_num = 'stk_0005'")
|
|
52
|
+
command.upgrade(self.config, "heads")
|
|
53
|
+
self.assertNotIn("mcp_token", self.tables())
|
|
54
|
+
self.assertEqual(self.sql("select name from stk_mcp_token"), [("agent",)])
|
|
55
|
+
|
|
56
|
+
def test_an_apps_own_mcp_token_table_is_left_alone(self):
|
|
57
|
+
command.upgrade(self.config, "stk_0004")
|
|
58
|
+
self.sql("CREATE TABLE mcp_token (id INTEGER PRIMARY KEY, grant_id TEXT)")
|
|
59
|
+
self.sql("INSERT INTO mcp_token (id, grant_id) VALUES (7, 'g')")
|
|
60
|
+
command.upgrade(self.config, "heads")
|
|
61
|
+
self.assertEqual(self.sql("select id, grant_id from mcp_token"), [(7, "g")])
|
|
62
|
+
self.assertIn("stk_mcp_token", self.tables())
|
|
@@ -7,7 +7,7 @@ from unittest import mock
|
|
|
7
7
|
import httpx2
|
|
8
8
|
|
|
9
9
|
from stk.utils import netguard
|
|
10
|
-
from stk.utils.netguard import Blocked, fetch
|
|
10
|
+
from stk.utils.netguard import Blocked, fetch, pin, stream
|
|
11
11
|
|
|
12
12
|
PUBLIC = "93.184.215.14"
|
|
13
13
|
|
|
@@ -196,6 +196,64 @@ class NetguardTests(unittest.IsolatedAsyncioTestCase):
|
|
|
196
196
|
with self.assertRaises(TimeoutError):
|
|
197
197
|
await fetch("https://slow.example.com/", timeout=0.05)
|
|
198
198
|
|
|
199
|
+
async def test_pin_is_public_for_vetting_urls_another_client_opens(self):
|
|
200
|
+
with resolver({"site.example.com": [PUBLIC], "inside.test": ["10.0.0.2"]}):
|
|
201
|
+
self.assertEqual(
|
|
202
|
+
await pin("https://site.example.com/a?b=1"),
|
|
203
|
+
(f"https://{PUBLIC}/a?b=1", "site.example.com", "site.example.com"),
|
|
204
|
+
)
|
|
205
|
+
with self.assertRaises(Blocked):
|
|
206
|
+
await pin("https://inside.test/")
|
|
207
|
+
|
|
208
|
+
async def test_stream_yields_the_body_unread_and_caps_streamed_bytes(self):
|
|
209
|
+
async def body():
|
|
210
|
+
for _ in range(10):
|
|
211
|
+
yield b"x" * 1_000
|
|
212
|
+
|
|
213
|
+
def handler(request):
|
|
214
|
+
return httpx2.Response(200, content=body())
|
|
215
|
+
|
|
216
|
+
with resolver({"big.example.com": [PUBLIC]}):
|
|
217
|
+
async with stream(
|
|
218
|
+
"https://big.example.com/",
|
|
219
|
+
max_bytes=5_000,
|
|
220
|
+
transport=httpx2.MockTransport(handler),
|
|
221
|
+
) as (response, final):
|
|
222
|
+
self.assertEqual(final, "https://big.example.com/")
|
|
223
|
+
with self.assertRaisesRegex(Blocked, "larger than"):
|
|
224
|
+
async for _ in response.aiter_bytes():
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
async def test_stream_can_return_a_redirect_instead_of_following_it(self):
|
|
228
|
+
def handler(request):
|
|
229
|
+
return httpx2.Response(302, headers={"location": "https://b.example.com/"})
|
|
230
|
+
|
|
231
|
+
with resolver({"a.example.com": [PUBLIC]}):
|
|
232
|
+
async with stream(
|
|
233
|
+
"https://a.example.com/",
|
|
234
|
+
follow_redirects=False,
|
|
235
|
+
transport=httpx2.MockTransport(handler),
|
|
236
|
+
) as (response, final):
|
|
237
|
+
self.assertEqual(response.status_code, 302)
|
|
238
|
+
self.assertEqual(final, "https://a.example.com/")
|
|
239
|
+
|
|
240
|
+
async def test_cookies_are_not_carried_across_redirects(self):
|
|
241
|
+
seen = []
|
|
242
|
+
|
|
243
|
+
def handler(request):
|
|
244
|
+
seen.append(request)
|
|
245
|
+
if len(seen) == 1:
|
|
246
|
+
return httpx2.Response(
|
|
247
|
+
302, headers={"location": "/next", "set-cookie": "s=1; Path=/"}
|
|
248
|
+
)
|
|
249
|
+
return httpx2.Response(200, text="done")
|
|
250
|
+
|
|
251
|
+
with resolver({"a.example.com": [PUBLIC]}):
|
|
252
|
+
await fetch(
|
|
253
|
+
"https://a.example.com/", transport=httpx2.MockTransport(handler)
|
|
254
|
+
)
|
|
255
|
+
self.assertNotIn("cookie", seen[1].headers)
|
|
256
|
+
|
|
199
257
|
|
|
200
258
|
if __name__ == "__main__":
|
|
201
259
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|