stkit 17.2.0__tar.gz → 17.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.
Files changed (236) hide show
  1. {stkit-17.2.0 → stkit-17.2.2}/CHANGELOG.md +21 -0
  2. {stkit-17.2.0 → stkit-17.2.2}/PKG-INFO +2 -2
  3. {stkit-17.2.0 → stkit-17.2.2}/pyproject.toml +2 -2
  4. {stkit-17.2.0 → stkit-17.2.2}/stk/__init__.py +1 -1
  5. {stkit-17.2.0 → stkit-17.2.2}/stk/agent_login.py +1 -1
  6. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/base.py +3 -0
  7. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/develop.py +2 -5
  8. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/users.py +3 -3
  9. {stkit-17.2.0 → stkit-17.2.2}/stk/public/views.py +1 -1
  10. {stkit-17.2.0 → stkit-17.2.2}/stk/settings.py +0 -1
  11. {stkit-17.2.0 → stkit-17.2.2}/stk/tasks/__init__.py +33 -7
  12. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/index.html +1 -1
  13. {stkit-17.2.0 → stkit-17.2.2}/stk/user/models.py +13 -1
  14. {stkit-17.2.0 → stkit-17.2.2}/tests/test_cli.py +17 -0
  15. {stkit-17.2.0 → stkit-17.2.2}/tests/test_jobs.py +17 -1
  16. {stkit-17.2.0 → stkit-17.2.2}/tests/test_security_compatibility.py +29 -1
  17. {stkit-17.2.0 → stkit-17.2.2}/.gitignore +0 -0
  18. {stkit-17.2.0 → stkit-17.2.2}/LICENSE +0 -0
  19. {stkit-17.2.0 → stkit-17.2.2}/README.md +0 -0
  20. {stkit-17.2.0 → stkit-17.2.2}/SECURITY.md +0 -0
  21. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/.env-sample +0 -0
  22. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/.gitignore +0 -0
  23. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/README.md +0 -0
  24. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/minimal/__init__.py +0 -0
  25. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/minimal/app.py +0 -0
  26. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/minimal/migrations/364232ae77d5_add_note.py +0 -0
  27. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/minimal/note/__init__.py +0 -0
  28. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/minimal/note/models.py +0 -0
  29. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/minimal/note/templates/note/index.html +0 -0
  30. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/minimal/note/views.py +0 -0
  31. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/minimal/settings.py +0 -0
  32. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/minimal/templates/layout.html +0 -0
  33. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/pyproject.toml +0 -0
  34. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/tests/__init__.py +0 -0
  35. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/tests/test_app.py +0 -0
  36. {stkit-17.2.0 → stkit-17.2.2}/examples/minimal/uv.lock +0 -0
  37. {stkit-17.2.0 → stkit-17.2.2}/stk/__main__.py +0 -0
  38. {stkit-17.2.0 → stkit-17.2.2}/stk/app.py +0 -0
  39. {stkit-17.2.0 → stkit-17.2.2}/stk/checks.py +0 -0
  40. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/__init__.py +0 -0
  41. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/agent.py +0 -0
  42. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/copy_rules.py +0 -0
  43. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/database.py +0 -0
  44. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/doctor.py +0 -0
  45. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/main.py +0 -0
  46. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/reports.py +0 -0
  47. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/smoke.py +0 -0
  48. {stkit-17.2.0 → stkit-17.2.2}/stk/cli/watch.py +0 -0
  49. {stkit-17.2.0 → stkit-17.2.2}/stk/extensions.py +0 -0
  50. {stkit-17.2.0 → stkit-17.2.2}/stk/files/__init__.py +0 -0
  51. {stkit-17.2.0 → stkit-17.2.2}/stk/files/models.py +0 -0
  52. {stkit-17.2.0 → stkit-17.2.2}/stk/files/store.py +0 -0
  53. {stkit-17.2.0 → stkit-17.2.2}/stk/files/views.py +0 -0
  54. {stkit-17.2.0 → stkit-17.2.2}/stk/mcp/__init__.py +0 -0
  55. {stkit-17.2.0 → stkit-17.2.2}/stk/mcp/models.py +0 -0
  56. {stkit-17.2.0 → stkit-17.2.2}/stk/mcp/server.py +0 -0
  57. {stkit-17.2.0 → stkit-17.2.2}/stk/mcp/views.py +0 -0
  58. {stkit-17.2.0 → stkit-17.2.2}/stk/migrations/__init__.py +0 -0
  59. {stkit-17.2.0 → stkit-17.2.2}/stk/migrations/env.py +0 -0
  60. {stkit-17.2.0 → stkit-17.2.2}/stk/migrations/script.py.mako +0 -0
  61. {stkit-17.2.0 → stkit-17.2.2}/stk/migrations/versions/stk_0001_initial_schema.py +0 -0
  62. {stkit-17.2.0 → stkit-17.2.2}/stk/migrations/versions/stk_0002_jobs.py +0 -0
  63. {stkit-17.2.0 → stkit-17.2.2}/stk/migrations/versions/stk_0003_files.py +0 -0
  64. {stkit-17.2.0 → stkit-17.2.2}/stk/migrations/versions/stk_0004_notifications.py +0 -0
  65. {stkit-17.2.0 → stkit-17.2.2}/stk/migrations/versions/stk_0005_mcp_tokens.py +0 -0
  66. {stkit-17.2.0 → stkit-17.2.2}/stk/notification/__init__.py +0 -0
  67. {stkit-17.2.0 → stkit-17.2.2}/stk/notification/models.py +0 -0
  68. {stkit-17.2.0 → stkit-17.2.2}/stk/notification/views.py +0 -0
  69. {stkit-17.2.0 → stkit-17.2.2}/stk/portal/views.py +0 -0
  70. {stkit-17.2.0 → stkit-17.2.2}/stk/public/__init__.py +0 -0
  71. {stkit-17.2.0 → stkit-17.2.2}/stk/public/models.py +0 -0
  72. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/__init__.py +0 -0
  73. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/CLAUDE.md +0 -0
  74. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/Dockerfile +0 -0
  75. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/README.md +0 -0
  76. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/__app__/__init__.py +0 -0
  77. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/__app__/app.py +0 -0
  78. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/__app__/settings.py +0 -0
  79. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/claude/skills/stk-api/SKILL.md +0 -0
  80. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/claude/skills/stk-blueprint/SKILL.md +0 -0
  81. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/claude/skills/stk-deploy/SKILL.md +0 -0
  82. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/claude/skills/stk-lessons/SKILL.md +0 -0
  83. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/claude/skills/stk-migrate/SKILL.md +0 -0
  84. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/docker-compose.yml +0 -0
  85. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/dockerignore +0 -0
  86. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/env-sample +0 -0
  87. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/github/workflows/checks.yml +0 -0
  88. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/gitignore +0 -0
  89. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/nginx/nginx.conf +0 -0
  90. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/nginx/stk.conf +0 -0
  91. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/postgres/init-db.sh +0 -0
  92. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/pyproject.toml +0 -0
  93. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/tests/__init__.py +0 -0
  94. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/app/tests/test_app.py +0 -0
  95. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/generator.py +0 -0
  96. {stkit-17.2.0 → stkit-17.2.2}/stk/scaffold/templates.py +0 -0
  97. {stkit-17.2.0 → stkit-17.2.2}/stk/security.py +0 -0
  98. {stkit-17.2.0 → stkit-17.2.2}/stk/shell.py +0 -0
  99. {stkit-17.2.0 → stkit-17.2.2}/stk/static/VERSIONS.txt +0 -0
  100. {stkit-17.2.0 → stkit-17.2.2}/stk/static/css/app.css +0 -0
  101. {stkit-17.2.0 → stkit-17.2.2}/stk/static/css/layout.css +0 -0
  102. {stkit-17.2.0 → stkit-17.2.2}/stk/static/css/vuetify.min.css +0 -0
  103. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.animals.woff2 +0 -0
  104. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.arrows-1.woff2 +0 -0
  105. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.arrows-2.woff2 +0 -0
  106. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.badges.woff2 +0 -0
  107. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.brand-1.woff2 +0 -0
  108. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.brand-2.woff2 +0 -0
  109. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.buildings.woff2 +0 -0
  110. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.charts.woff2 +0 -0
  111. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.communication.woff2 +0 -0
  112. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.computers.woff2 +0 -0
  113. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.core.woff2 +0 -0
  114. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.currencies.woff2 +0 -0
  115. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.database.woff2 +0 -0
  116. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.design-1.woff2 +0 -0
  117. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.design-2.woff2 +0 -0
  118. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.development.woff2 +0 -0
  119. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.devices-1.woff2 +0 -0
  120. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.devices-2.woff2 +0 -0
  121. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.document.woff2 +0 -0
  122. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.e-commerce.woff2 +0 -0
  123. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.electrical.woff2 +0 -0
  124. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.extensions.woff2 +0 -0
  125. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.food.woff2 +0 -0
  126. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.games.woff2 +0 -0
  127. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.gender.woff2 +0 -0
  128. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.gestures.woff2 +0 -0
  129. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.health.woff2 +0 -0
  130. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.laundry.woff2 +0 -0
  131. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.letters.woff2 +0 -0
  132. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.logic.woff2 +0 -0
  133. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.map.woff2 +0 -0
  134. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.math.woff2 +0 -0
  135. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.media.woff2 +0 -0
  136. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.mood.woff2 +0 -0
  137. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.nature.woff2 +0 -0
  138. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.numbers.woff2 +0 -0
  139. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.photography.woff2 +0 -0
  140. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.shapes.woff2 +0 -0
  141. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.sport.woff2 +0 -0
  142. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.symbols.woff2 +0 -0
  143. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.system-1.woff2 +0 -0
  144. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.system-2.woff2 +0 -0
  145. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.system-3.woff2 +0 -0
  146. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.text.woff2 +0 -0
  147. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.vehicles.woff2 +0 -0
  148. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.version-control.woff2 +0 -0
  149. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.weather.woff2 +0 -0
  150. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/fonts/tabler-icons.zodiac.woff2 +0 -0
  151. {stkit-17.2.0 → stkit-17.2.2}/stk/static/icons/tabler-icons.min.css +0 -0
  152. {stkit-17.2.0 → stkit-17.2.2}/stk/static/img/auth-bg.webp +0 -0
  153. {stkit-17.2.0 → stkit-17.2.2}/stk/static/img/favicon.ico +0 -0
  154. {stkit-17.2.0 → stkit-17.2.2}/stk/static/img/stk.svg +0 -0
  155. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/axios.min.js +0 -0
  156. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/components/NotificationDropdown.js +0 -0
  157. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/components/ThemeSwitcher.js +0 -0
  158. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/components/TransitionExpand.js +0 -0
  159. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/components/VerticalNavGroup.js +0 -0
  160. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/components/VerticalNavLink.js +0 -0
  161. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/components/VerticalNavSectionTitle.js +0 -0
  162. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/components/index.js +0 -0
  163. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/config.js +0 -0
  164. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/vue.min.js +0 -0
  165. {stkit-17.2.0 → stkit-17.2.2}/stk/static/js/vuetify.min.js +0 -0
  166. {stkit-17.2.0 → stkit-17.2.2}/stk/static/robots.txt +0 -0
  167. {stkit-17.2.0 → stkit-17.2.2}/stk/tasks/models.py +0 -0
  168. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/401.html +0 -0
  169. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/404.html +0 -0
  170. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/500.html +0 -0
  171. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/_brand.html +0 -0
  172. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/auth_layout.html +0 -0
  173. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/cms/activities.html +0 -0
  174. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/cms/jobs.html +0 -0
  175. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/cms/roles.html +0 -0
  176. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/cms/users.html +0 -0
  177. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/dashboard.html +0 -0
  178. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/layout.html +0 -0
  179. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/login-layout.html +0 -0
  180. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/mcp/tokens.html +0 -0
  181. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/_macros.html +0 -0
  182. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/_messages.html +0 -0
  183. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/change_password.html +0 -0
  184. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/confirm_email.html +0 -0
  185. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/change_notice.html +0 -0
  186. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/change_notice.txt +0 -0
  187. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/confirmation_instructions.html +0 -0
  188. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/confirmation_instructions.txt +0 -0
  189. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/login_instructions.html +0 -0
  190. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/login_instructions.txt +0 -0
  191. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/reset_instructions.html +0 -0
  192. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/reset_instructions.txt +0 -0
  193. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/reset_notice.html +0 -0
  194. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/reset_notice.txt +0 -0
  195. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/welcome.html +0 -0
  196. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/email/welcome.txt +0 -0
  197. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/forgot_password.html +0 -0
  198. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/login_user.html +0 -0
  199. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/mf_recovery.html +0 -0
  200. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/mf_recovery_codes.html +0 -0
  201. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/register_user.html +0 -0
  202. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/reset_password.html +0 -0
  203. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/send_confirmation.html +0 -0
  204. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/send_login.html +0 -0
  205. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/two_factor_select.html +0 -0
  206. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/two_factor_setup.html +0 -0
  207. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/two_factor_verify_code.html +0 -0
  208. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/wan_register.html +0 -0
  209. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/wan_signin.html +0 -0
  210. {stkit-17.2.0 → stkit-17.2.2}/stk/templates/security/wan_verify.html +0 -0
  211. {stkit-17.2.0 → stkit-17.2.2}/stk/user/__init__.py +0 -0
  212. {stkit-17.2.0 → stkit-17.2.2}/stk/user/forms.py +0 -0
  213. {stkit-17.2.0 → stkit-17.2.2}/stk/user/views.py +0 -0
  214. {stkit-17.2.0 → stkit-17.2.2}/stk/utils/email.py +0 -0
  215. {stkit-17.2.0 → stkit-17.2.2}/stk/utils/netguard.py +0 -0
  216. {stkit-17.2.0 → stkit-17.2.2}/stk/utils/ratelimit.py +0 -0
  217. {stkit-17.2.0 → stkit-17.2.2}/stk/websocket.py +0 -0
  218. {stkit-17.2.0 → stkit-17.2.2}/tests/test_agent_operability.py +0 -0
  219. {stkit-17.2.0 → stkit-17.2.2}/tests/test_app_index.py +0 -0
  220. {stkit-17.2.0 → stkit-17.2.2}/tests/test_branding.py +0 -0
  221. {stkit-17.2.0 → stkit-17.2.2}/tests/test_builder_ux.py +0 -0
  222. {stkit-17.2.0 → stkit-17.2.2}/tests/test_composition.py +0 -0
  223. {stkit-17.2.0 → stkit-17.2.2}/tests/test_copy_rules.py +0 -0
  224. {stkit-17.2.0 → stkit-17.2.2}/tests/test_csrf_exempt.py +0 -0
  225. {stkit-17.2.0 → stkit-17.2.2}/tests/test_error_handler.py +0 -0
  226. {stkit-17.2.0 → stkit-17.2.2}/tests/test_files.py +0 -0
  227. {stkit-17.2.0 → stkit-17.2.2}/tests/test_mcp.py +0 -0
  228. {stkit-17.2.0 → stkit-17.2.2}/tests/test_migration_drift.py +0 -0
  229. {stkit-17.2.0 → stkit-17.2.2}/tests/test_netguard.py +0 -0
  230. {stkit-17.2.0 → stkit-17.2.2}/tests/test_notifications.py +0 -0
  231. {stkit-17.2.0 → stkit-17.2.2}/tests/test_production_security.py +0 -0
  232. {stkit-17.2.0 → stkit-17.2.2}/tests/test_revision_imports.py +0 -0
  233. {stkit-17.2.0 → stkit-17.2.2}/tests/test_scaffolder.py +0 -0
  234. {stkit-17.2.0 → stkit-17.2.2}/tests/test_shell.py +0 -0
  235. {stkit-17.2.0 → stkit-17.2.2}/tests/test_smoke_pages.py +0 -0
  236. {stkit-17.2.0 → stkit-17.2.2}/tests/test_static_versioning.py +0 -0
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## v17.2.2 (2026-10-31)
4
+
5
+ No migration required.
6
+
7
+ - `stk add-role` and `stk cleanup-sessions` crashed with `TypeError: 'NoneType' object is not callable` because they used the database before the app was built. Every command that runs async work now builds the app first. Run `cleanup-sessions` from cron again if you had stopped it.
8
+ - `stk worker` stops on SIGTERM or Ctrl-C at a safe point: it cancels only the running job, hands it back, and exits without a traceback.
9
+ - The home page describes stk as running on Quart, not Flask.
10
+ - Requires quart-security 2.0.5: login takes the same time whether or not an email has an account, and passkey registration refuses a credential ID another user already holds (Authentication).
11
+
12
+ ## v17.2.1 (2026-10-31)
13
+
14
+ No migration required.
15
+
16
+ - Emails are stored trimmed and lowercased, and the CLI, agent login and OAuth look users up regardless of case, matching what the login form does. Before, a user created as `Ada@Example.com` from the admin page or `stk create` could not sign in, because login lowercases what it is given (Authentication).
17
+ - Requires quart-security 2.0.4, which matches login emails regardless of case and drops the unused `remember` login field and `SECURITY_FRESHNESS_GRACE_PERIOD` setting. stk's own `SECURITY_FRESHNESS_GRACE_PERIOD` is removed with it.
18
+
19
+ ### Upgrade considerations
20
+
21
+ - Existing rows keep their stored case and still sign in. Two rows whose emails differ only by case make login for that address fail: merge or rename one. To find them: `uv run stk shell -c "await sql('select lower(email), count(*) from \"user\" group by 1 having count(*) > 1')"`.
22
+ - A custom login template that renders `form.remember` must drop it.
23
+
3
24
  ## v17.2.0 (2026-10-31)
4
25
 
5
26
  Run `uv run stk db upgrade`: four new core tables (`stk_job`, `stk_file`, `stk_notification`, `mcp_token`).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: stkit
3
- Version: 17.2.0
3
+ Version: 17.2.2
4
4
  Summary: An async Quart framework optimized for AI-assisted development workflows
5
5
  Project-URL: Documentation, https://docs.stkit.dev
6
6
  Project-URL: Source, https://github.com/level09/stk
@@ -28,7 +28,7 @@ Requires-Dist: pillow>=13.0.0
28
28
  Requires-Dist: psycopg2-binary>=2.9.10
29
29
  Requires-Dist: python-dotenv>=1.2.2
30
30
  Requires-Dist: qrcode>=8.2
31
- Requires-Dist: quart-security<3,>=2.0.3
31
+ Requires-Dist: quart-security<3,>=2.0.5
32
32
  Requires-Dist: quart-session>=3.0.0
33
33
  Requires-Dist: quart>=0.20.0
34
34
  Requires-Dist: rich>=14.0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stkit"
3
- version = "17.2.0"
3
+ version = "17.2.2"
4
4
  description = "An async Quart framework optimized for AI-assisted development workflows"
5
5
  authors = [
6
6
  {name = "Level09", email = "hello@level09.com"}
@@ -30,7 +30,7 @@ dependencies = [
30
30
  "python-dotenv>=1.2.2",
31
31
  "qrcode>=8.2",
32
32
  "quart>=0.20.0",
33
- "quart-security>=2.0.3,<3",
33
+ "quart-security>=2.0.5,<3",
34
34
  "quart-session>=3.0.0",
35
35
  "rich>=14.0.0",
36
36
  "sqlalchemy[asyncio]>=2.0.41",
@@ -3,7 +3,7 @@ stk Framework
3
3
  A modern Python web framework optimized for AI-assisted development.
4
4
  """
5
5
 
6
- __version__ = "17.2.0"
6
+ __version__ = "17.2.2"
7
7
 
8
8
 
9
9
  def __getattr__(name):
@@ -69,7 +69,7 @@ async def agent_login():
69
69
  return "", 403
70
70
 
71
71
  user = (
72
- await g.db_session.execute(select(User).where(User.email == email))
72
+ await g.db_session.execute(select(User).where(User.email_is(email)))
73
73
  ).scalar_one_or_none()
74
74
  if user is None or not user.active:
75
75
  return "", 403
@@ -68,6 +68,9 @@ def cli_app():
68
68
 
69
69
  def run_async(coro):
70
70
  """Run an async function in sync context (for CLI commands)."""
71
+ # The engine and session factory exist only once the app is built; loading it
72
+ # here means no command can reach the database without it.
73
+ cli_app()
71
74
 
72
75
  async def _wrapper():
73
76
  try:
@@ -1,6 +1,5 @@
1
1
  """Starting an app, serving it locally, and running its durable jobs."""
2
2
 
3
- import asyncio
4
3
  from pathlib import Path
5
4
 
6
5
  import click
@@ -82,7 +81,5 @@ def worker(timeout):
82
81
  await work(timeout)
83
82
 
84
83
  console.print("[green]Worker started[/]")
85
- try:
86
- run_async(run())
87
- except (KeyboardInterrupt, asyncio.CancelledError):
88
- console.print("Worker stopped")
84
+ run_async(run())
85
+ console.print("Worker stopped")
@@ -125,7 +125,7 @@ def create(email, password):
125
125
  async def _run():
126
126
  async with ext.async_session_factory() as session:
127
127
  existing = (
128
- await session.execute(select(User).where(User.email == email))
128
+ await session.execute(select(User).where(User.email_is(email)))
129
129
  ).scalar_one_or_none()
130
130
  if existing is not None:
131
131
  console.print("[yellow]User already exists![/]")
@@ -153,7 +153,7 @@ def add_role(email, role):
153
153
 
154
154
  async with ext.async_session_factory() as session:
155
155
  u = (
156
- await session.execute(select(User).where(User.email == email))
156
+ await session.execute(select(User).where(User.email_is(email)))
157
157
  ).scalar_one_or_none()
158
158
 
159
159
  if u is None:
@@ -196,7 +196,7 @@ def reset(email, password):
196
196
  async def _run():
197
197
  async with ext.async_session_factory() as session:
198
198
  u = (
199
- await session.execute(select(User).where(User.email == email))
199
+ await session.execute(select(User).where(User.email_is(email)))
200
200
  ).scalar_one_or_none()
201
201
  if not u:
202
202
  console.print(f'[red]User with email "{email}" not found.[/]')
@@ -126,7 +126,7 @@ async def handle_oauth_callback(provider_name, token, user_info):
126
126
  user = account.user
127
127
  else:
128
128
  # Email ownership at an IdP does not authorize linking local credentials.
129
- existing = await g.db_session.scalar(select(User).where(User.email == email))
129
+ existing = await g.db_session.scalar(select(User).where(User.email_is(email)))
130
130
  if existing:
131
131
  return await authentication_failed()
132
132
  user = create_oauth_user(
@@ -58,7 +58,6 @@ class Config:
58
58
  SECURITY_API_ENABLED_METHODS = ["session"]
59
59
 
60
60
  SECURITY_FRESHNESS = timedelta(minutes=60)
61
- SECURITY_FRESHNESS_GRACE_PERIOD = timedelta(minutes=60)
62
61
  SECURITY_PASSWORD_LENGTH_MIN = 12
63
62
 
64
63
  SECURITY_TOTP_ISSUER = "stk"
@@ -25,6 +25,7 @@ from stk.tasks.models import Job
25
25
  logger = logging.getLogger(__name__)
26
26
 
27
27
  JOBS = {}
28
+ _running = None # the job task `stk worker` is awaiting, for a clean shutdown
28
29
  TIMEOUT = 3600
29
30
  POLL_SECONDS = 2
30
31
  RECOVER_SECONDS = 60
@@ -164,8 +165,13 @@ async def run_next(timeout=TIMEOUT):
164
165
  job_id, name, payload, attempts, max_attempts = claimed
165
166
  async with ext.async_session_factory() as session:
166
167
  try:
167
- async with asyncio.timeout(timeout):
168
- await JOBS[name](session, **payload)
168
+ global _running
169
+ _running = asyncio.create_task(JOBS[name](session, **payload))
170
+ try:
171
+ # wait_for cancels the job itself on timeout or when this is cancelled.
172
+ await asyncio.wait_for(_running, timeout)
173
+ finally:
174
+ _running = None
169
175
  done = await session.execute(
170
176
  _owned(job_id, lease).values(
171
177
  status="done", locked_until=None, last_error=None
@@ -216,11 +222,28 @@ async def recover():
216
222
 
217
223
  async def work(timeout=TIMEOUT):
218
224
  """Run due jobs one at a time until SIGTERM or Ctrl-C."""
219
- asyncio.get_running_loop().add_signal_handler(
220
- signal.SIGTERM, asyncio.current_task().cancel
221
- )
225
+ stop = asyncio.Event()
226
+
227
+ def shutdown():
228
+ # Cancel only the job, never the worker mid-query: it hands the job back
229
+ # and the loop ends at its next check.
230
+ stop.set()
231
+ if _running is not None:
232
+ _running.cancel()
233
+
234
+ loop = asyncio.get_running_loop()
235
+ for sig in (signal.SIGTERM, signal.SIGINT):
236
+ loop.add_signal_handler(sig, shutdown)
237
+ try:
238
+ await _drain(timeout, stop)
239
+ except asyncio.CancelledError:
240
+ if not stop.is_set():
241
+ raise
242
+
243
+
244
+ async def _drain(timeout, stop):
222
245
  recovered = None
223
- while True:
246
+ while not stop.is_set():
224
247
  # Also between jobs, so a queue that never empties still recovers.
225
248
  if recovered is None or datetime.now() - recovered >= timedelta(
226
249
  seconds=RECOVER_SECONDS
@@ -228,4 +251,7 @@ async def work(timeout=TIMEOUT):
228
251
  await recover()
229
252
  recovered = datetime.now()
230
253
  if not await run_next(timeout):
231
- await asyncio.sleep(POLL_SECONDS)
254
+ try:
255
+ await asyncio.wait_for(stop.wait(), POLL_SECONDS)
256
+ except TimeoutError:
257
+ pass
@@ -8,7 +8,7 @@
8
8
 
9
9
  <h1 class="text-headline-large font-weight-bold mb-3">Build faster with stk</h1>
10
10
  <p class="text-body-large text-medium-emphasis mb-8">
11
- A modern Flask framework for building web applications.
11
+ An async Python framework for building web applications, on Quart.
12
12
  </p>
13
13
 
14
14
  {% if current_user.is_authenticated %}
@@ -19,9 +19,10 @@ from sqlalchemy import (
19
19
  Table,
20
20
  UniqueConstraint,
21
21
  delete,
22
+ func,
22
23
  select,
23
24
  )
24
- from sqlalchemy.orm import declared_attr, relationship
25
+ from sqlalchemy.orm import declared_attr, relationship, validates
25
26
 
26
27
  from stk.extensions import Base
27
28
 
@@ -68,6 +69,17 @@ class User(Base, UserMixin):
68
69
  password_set = Column(Boolean, default=True, nullable=False)
69
70
  active = Column(Boolean, default=False, nullable=True)
70
71
 
72
+ @validates("email")
73
+ def _normalize_email(self, key, value):
74
+ # Login lowercases what it is given; store the same form so one address
75
+ # cannot become two accounts that differ only by case.
76
+ return value.strip().lower() if value is not None else value
77
+
78
+ @classmethod
79
+ def email_is(cls, email):
80
+ """Match an email regardless of case, including rows stored before 17.2.1."""
81
+ return func.lower(cls.email) == email.strip().lower()
82
+
71
83
  roles = relationship(
72
84
  "Role", secondary=roles_users, backref="users", lazy="selectin"
73
85
  )
@@ -61,3 +61,20 @@ class DowngradeArgumentTest(unittest.TestCase):
61
61
  result = CliRunner().invoke(main, ["db", "downgrade", "-1"])
62
62
  self.assertEqual(result.exit_code, 0, result.output)
63
63
  self.assertEqual(run.call_args.args[-1], "-1")
64
+
65
+
66
+ class DatabaseCommandTest(unittest.TestCase):
67
+ def test_run_async_builds_the_app_before_touching_the_database(self):
68
+ """`add-role` and `cleanup-sessions` crashed: nothing had built the engine."""
69
+ with (
70
+ patch("stk.cli.base.cli_app") as load,
71
+ patch("stk.tasks.cleanup_expired_sessions") as cleanup,
72
+ ):
73
+ cleanup.side_effect = lambda: _done()
74
+ result = CliRunner().invoke(main, ["cleanup-sessions"])
75
+ self.assertEqual(result.exit_code, 0, result.output)
76
+ load.assert_called()
77
+
78
+
79
+ async def _done():
80
+ return None
@@ -1,6 +1,8 @@
1
1
  """Durable jobs: enqueue, claim, retry, failure, and recovery of a dead worker's job."""
2
2
 
3
3
  import asyncio
4
+ import os
5
+ import signal
4
6
  import unittest
5
7
  from datetime import datetime, timedelta
6
8
 
@@ -11,7 +13,7 @@ from sqlalchemy import select, update
11
13
  import stk.extensions as ext
12
14
  from stk.app import Stk
13
15
  from stk.settings import TestingConfig
14
- from stk.tasks import enqueue, job, recover, run_next
16
+ from stk.tasks import enqueue, job, recover, run_next, work
15
17
  from stk.tasks.models import Job
16
18
  from stk.user.models import Activity, Base, User
17
19
  from stk.user.views import api_jobs
@@ -163,6 +165,20 @@ class JobTests(unittest.IsolatedAsyncioTestCase):
163
165
  self.assertEqual(row.status, "running")
164
166
  self.assertNotIn("lost race", await self.activities())
165
167
 
168
+ async def test_sigterm_hands_back_the_running_job_and_stops_cleanly(self):
169
+ job_id = await enqueue("jobs_test_slow")
170
+ worker = asyncio.create_task(work())
171
+ for _ in range(50):
172
+ await asyncio.sleep(0.05)
173
+ if (await self.get(job_id)).status == "running":
174
+ break
175
+ os.kill(os.getpid(), signal.SIGTERM)
176
+ await asyncio.wait_for(worker, 5) # returns, not CancelledError
177
+ row = await self.get(job_id)
178
+ self.assertEqual(
179
+ (row.status, row.attempts, row.locked_until), ("queued", 0, None)
180
+ )
181
+
166
182
  async def test_recovers_job_left_running_by_a_dead_worker(self):
167
183
  lost = await enqueue("jobs_test_record", user_id=self.user_id, action="lost")
168
184
  spent = await enqueue("jobs_test_record", user_id=self.user_id, action="spent")
@@ -5,7 +5,7 @@ import unittest
5
5
  import pyotp
6
6
  from quart import g
7
7
  from quart_security import hash_password
8
- from sqlalchemy import select
8
+ from sqlalchemy import select, update
9
9
 
10
10
  import stk.extensions as ext
11
11
  from stk.app import Stk
@@ -52,6 +52,34 @@ class SecurityCompatibilityTests(unittest.IsolatedAsyncioTestCase):
52
52
  async with client.session_transaction() as cookie:
53
53
  return dict(cookie)
54
54
 
55
+ async def test_emails_are_stored_lowercase_and_matched_regardless_of_case(self):
56
+ self.assertEqual(User(email=" New@Example.COM ").email, "new@example.com")
57
+ # A row stored before normalization keeps its case; lookups still find it.
58
+ async with ext.async_session_factory() as db:
59
+ await db.execute(
60
+ update(User)
61
+ .where(User.id == self.user_id)
62
+ .values(email="User@Example.com")
63
+ )
64
+ await db.commit()
65
+ found = await db.scalar(
66
+ select(User).where(User.email_is(" USER@example.COM"))
67
+ )
68
+ self.assertEqual(found.id, self.user_id)
69
+ client = self.app.test_client()
70
+ await client.get("/login")
71
+ async with client.session_transaction() as cookie:
72
+ csrf = cookie["_csrf_token"]
73
+ response = await client.post(
74
+ "/login",
75
+ form={
76
+ "email": " user@EXAMPLE.com ",
77
+ "password": "TestPassword123!",
78
+ "csrf_token": csrf,
79
+ },
80
+ )
81
+ self.assertEqual(response.status_code, 302)
82
+
55
83
  async def test_startup_validates_state_table_without_request(self):
56
84
  async with self.app.test_app():
57
85
  pass
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