korvid 0.1.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 (458) hide show
  1. korvid-0.1.2/.claude/hooks/protect-files.sh +11 -0
  2. korvid-0.1.2/.claude/settings.json +37 -0
  3. korvid-0.1.2/.github/dependabot.yml +76 -0
  4. korvid-0.1.2/.github/scripts/wait-aks-settled.sh +76 -0
  5. korvid-0.1.2/.github/workflows/ci.yml +160 -0
  6. korvid-0.1.2/.github/workflows/codeql.yml +33 -0
  7. korvid-0.1.2/.github/workflows/k8s-contract.yml +286 -0
  8. korvid-0.1.2/.github/workflows/release.yml +524 -0
  9. korvid-0.1.2/.gitignore +12 -0
  10. korvid-0.1.2/.pre-commit-config.yaml +46 -0
  11. korvid-0.1.2/AGENTS.md +141 -0
  12. korvid-0.1.2/CLAUDE.md +1 -0
  13. korvid-0.1.2/LICENSE +202 -0
  14. korvid-0.1.2/Makefile +16 -0
  15. korvid-0.1.2/PKG-INFO +292 -0
  16. korvid-0.1.2/README.md +240 -0
  17. korvid-0.1.2/SECURITY.md +48 -0
  18. korvid-0.1.2/deploy/eval/ollama.yaml +100 -0
  19. korvid-0.1.2/docs/agent.md +482 -0
  20. korvid-0.1.2/docs/airgap.md +158 -0
  21. korvid-0.1.2/docs/assets/demo.gif +0 -0
  22. korvid-0.1.2/docs/demo/README.md +26 -0
  23. korvid-0.1.2/docs/demo/demo.py +310 -0
  24. korvid-0.1.2/docs/demo/demo.tape +68 -0
  25. korvid-0.1.2/docs/dev/README.md +28 -0
  26. korvid-0.1.2/docs/dev/agent-decisions.md +279 -0
  27. korvid-0.1.2/docs/dev/contract-tests.md +122 -0
  28. korvid-0.1.2/docs/dev/plans/2026-07-24-phase1-foundation.md +1790 -0
  29. korvid-0.1.2/docs/dev/plans/2026-07-24-plan23-logs-universal-resources.md +355 -0
  30. korvid-0.1.2/docs/dev/plans/2026-07-24-plan4-agent-runtime-slice1.md +608 -0
  31. korvid-0.1.2/docs/dev/plans/2026-07-25-plan4-slice2-auth-setup.md +1012 -0
  32. korvid-0.1.2/docs/dev/plans/2026-07-25-plan4-slice3-ui-drive.md +92 -0
  33. korvid-0.1.2/docs/dev/quality-gates.md +76 -0
  34. korvid-0.1.2/docs/dev/specs/2026-07-23-korvid-tui-design.md +299 -0
  35. korvid-0.1.2/docs/dev/specs/2026-07-24-korvid-engineering-standards.md +242 -0
  36. korvid-0.1.2/docs/dev/specs/2026-08-04-local-model-aks-evaluation-design.md +181 -0
  37. korvid-0.1.2/docs/dev/specs/2026-08-06-large-cluster-performance-qualification-design.md +381 -0
  38. korvid-0.1.2/docs/dev/specs/2026-08-09-configurable-agent-prompts-design.md +347 -0
  39. korvid-0.1.2/docs/dev/ui-controllers.md +183 -0
  40. korvid-0.1.2/docs/evals/methodology.md +340 -0
  41. korvid-0.1.2/docs/evals/scenarios.md +79 -0
  42. korvid-0.1.2/docs/evals/scoreboard.md +318 -0
  43. korvid-0.1.2/docs/helm-operators.md +175 -0
  44. korvid-0.1.2/docs/keybindings.md +85 -0
  45. korvid-0.1.2/docs/mcp.md +182 -0
  46. korvid-0.1.2/docs/ops.md +259 -0
  47. korvid-0.1.2/docs/performance.md +114 -0
  48. korvid-0.1.2/docs/provider-plugins.md +425 -0
  49. korvid-0.1.2/docs/release-notes/v0.1.2.md +122 -0
  50. korvid-0.1.2/docs/release.md +291 -0
  51. korvid-0.1.2/docs/threat-model.md +329 -0
  52. korvid-0.1.2/docs/tui.md +156 -0
  53. korvid-0.1.2/docs/windows.md +48 -0
  54. korvid-0.1.2/pyproject.toml +159 -0
  55. korvid-0.1.2/scripts/release/build-constraints.in +2 -0
  56. korvid-0.1.2/scripts/release/build-constraints.txt +15 -0
  57. korvid-0.1.2/scripts/release/bundle.py +119 -0
  58. korvid-0.1.2/scripts/release/check_artifacts.py +190 -0
  59. korvid-0.1.2/scripts/release/check_dry_run.py +81 -0
  60. korvid-0.1.2/scripts/release/check_sbom.py +56 -0
  61. korvid-0.1.2/scripts/release/check_source.py +63 -0
  62. korvid-0.1.2/scripts/release/check_version.py +45 -0
  63. korvid-0.1.2/scripts/release/compare_assets.py +42 -0
  64. korvid-0.1.2/scripts/release/metadata.py +41 -0
  65. korvid-0.1.2/scripts/release/offline_verify.py +203 -0
  66. korvid-0.1.2/scripts/release/release_manifest.py +43 -0
  67. korvid-0.1.2/scripts/release/smoke_install.py +439 -0
  68. korvid-0.1.2/scripts/release/version_format.py +23 -0
  69. korvid-0.1.2/src/korvid/__init__.py +3 -0
  70. korvid-0.1.2/src/korvid/__main__.py +1233 -0
  71. korvid-0.1.2/src/korvid/agent/__init__.py +0 -0
  72. korvid-0.1.2/src/korvid/agent/context.py +61 -0
  73. korvid-0.1.2/src/korvid/agent/credentials.py +16 -0
  74. korvid-0.1.2/src/korvid/agent/events.py +66 -0
  75. korvid-0.1.2/src/korvid/agent/evidence.py +228 -0
  76. korvid-0.1.2/src/korvid/agent/navigation.py +125 -0
  77. korvid-0.1.2/src/korvid/agent/outbound.py +916 -0
  78. korvid-0.1.2/src/korvid/agent/profiles.py +231 -0
  79. korvid-0.1.2/src/korvid/agent/prompts.py +203 -0
  80. korvid-0.1.2/src/korvid/agent/provider.py +91 -0
  81. korvid-0.1.2/src/korvid/agent/provider_plugin.py +302 -0
  82. korvid-0.1.2/src/korvid/agent/runtime.py +1131 -0
  83. korvid-0.1.2/src/korvid/agent/setup.py +67 -0
  84. korvid-0.1.2/src/korvid/cli.py +26 -0
  85. korvid-0.1.2/src/korvid/core/__init__.py +0 -0
  86. korvid-0.1.2/src/korvid/core/audit.py +325 -0
  87. korvid-0.1.2/src/korvid/core/config.py +881 -0
  88. korvid-0.1.2/src/korvid/core/debugimage.py +287 -0
  89. korvid-0.1.2/src/korvid/core/errors.py +14 -0
  90. korvid-0.1.2/src/korvid/core/filters.py +278 -0
  91. korvid-0.1.2/src/korvid/core/findings.py +113 -0
  92. korvid-0.1.2/src/korvid/core/keybindings.py +171 -0
  93. korvid-0.1.2/src/korvid/core/logbuffer.py +82 -0
  94. korvid-0.1.2/src/korvid/core/logexport.py +82 -0
  95. korvid-0.1.2/src/korvid/core/mcp.py +49 -0
  96. korvid-0.1.2/src/korvid/core/portforward.py +824 -0
  97. korvid-0.1.2/src/korvid/core/private_export.py +45 -0
  98. korvid-0.1.2/src/korvid/core/pvc_analysis.py +635 -0
  99. korvid-0.1.2/src/korvid/core/redaction.py +480 -0
  100. korvid-0.1.2/src/korvid/core/secrets.py +104 -0
  101. korvid-0.1.2/src/korvid/core/service_analysis.py +242 -0
  102. korvid-0.1.2/src/korvid/core/sorting.py +122 -0
  103. korvid-0.1.2/src/korvid/core/store.py +76 -0
  104. korvid-0.1.2/src/korvid/core/transfer.py +648 -0
  105. korvid-0.1.2/src/korvid/core/watch.py +140 -0
  106. korvid-0.1.2/src/korvid/evals/__init__.py +6 -0
  107. korvid-0.1.2/src/korvid/evals/__main__.py +554 -0
  108. korvid-0.1.2/src/korvid/evals/fake_kube.py +253 -0
  109. korvid-0.1.2/src/korvid/evals/grader.py +511 -0
  110. korvid-0.1.2/src/korvid/evals/journey.py +222 -0
  111. korvid-0.1.2/src/korvid/evals/journey_runner.py +432 -0
  112. korvid-0.1.2/src/korvid/evals/journeys/compare-namespaces.yaml +202 -0
  113. korvid-0.1.2/src/korvid/evals/journeys/healthy-stop.yaml +77 -0
  114. korvid-0.1.2/src/korvid/evals/journeys/logs-to-events.yaml +67 -0
  115. korvid-0.1.2/src/korvid/evals/journeys/namespace-triage.yaml +245 -0
  116. korvid-0.1.2/src/korvid/evals/journeys/rbac-evidence-gap.yaml +152 -0
  117. korvid-0.1.2/src/korvid/evals/journeys/rollout-owner-chain.yaml +245 -0
  118. korvid-0.1.2/src/korvid/evals/journeys/triage-and-correct.yaml +204 -0
  119. korvid-0.1.2/src/korvid/evals/journeys/tui-follow.yaml +108 -0
  120. korvid-0.1.2/src/korvid/evals/journeys_cli.py +203 -0
  121. korvid-0.1.2/src/korvid/evals/live_journey.py +237 -0
  122. korvid-0.1.2/src/korvid/evals/live_journeys/triage-and-correct.yaml +55 -0
  123. korvid-0.1.2/src/korvid/evals/runner.py +503 -0
  124. korvid-0.1.2/src/korvid/evals/scenario.py +287 -0
  125. korvid-0.1.2/src/korvid/evals/scenarios/bad-command-crash.yaml +56 -0
  126. korvid-0.1.2/src/korvid/evals/scenarios/crashloop-app-panic.yaml +83 -0
  127. korvid-0.1.2/src/korvid/evals/scenarios/crashloop-dependency-unreachable.yaml +65 -0
  128. korvid-0.1.2/src/korvid/evals/scenarios/crashloop-missing-env.yaml +62 -0
  129. korvid-0.1.2/src/korvid/evals/scenarios/healthy-deployment.yaml +118 -0
  130. korvid-0.1.2/src/korvid/evals/scenarios/healthy-restart-history.yaml +62 -0
  131. korvid-0.1.2/src/korvid/evals/scenarios/healthy-service-endpoints.yaml +111 -0
  132. korvid-0.1.2/src/korvid/evals/scenarios/image-pull-auth.yaml +67 -0
  133. korvid-0.1.2/src/korvid/evals/scenarios/image-pull-typo.yaml +61 -0
  134. korvid-0.1.2/src/korvid/evals/scenarios/init-container-failing.yaml +71 -0
  135. korvid-0.1.2/src/korvid/evals/scenarios/job-backoff-limit-exceeded.yaml +76 -0
  136. korvid-0.1.2/src/korvid/evals/scenarios/liveness-probe-failing.yaml +70 -0
  137. korvid-0.1.2/src/korvid/evals/scenarios/missing-configmap-mount.yaml +58 -0
  138. korvid-0.1.2/src/korvid/evals/scenarios/missing-secret-env.yaml +57 -0
  139. korvid-0.1.2/src/korvid/evals/scenarios/node-pressure-eviction.yaml +49 -0
  140. korvid-0.1.2/src/korvid/evals/scenarios/oom-killed.yaml +70 -0
  141. korvid-0.1.2/src/korvid/evals/scenarios/pending-insufficient-cpu.yaml +42 -0
  142. korvid-0.1.2/src/korvid/evals/scenarios/pending-node-selector.yaml +44 -0
  143. korvid-0.1.2/src/korvid/evals/scenarios/pvc-pending-no-storageclass.yaml +70 -0
  144. korvid-0.1.2/src/korvid/evals/scenarios/pvc-wait-for-first-consumer.yaml +80 -0
  145. korvid-0.1.2/src/korvid/evals/scenarios/quota-blocked-scheduling.yaml +77 -0
  146. korvid-0.1.2/src/korvid/evals/scenarios/readiness-probe-failing.yaml +63 -0
  147. korvid-0.1.2/src/korvid/evals/scenarios/service-endpoints-not-ready.yaml +123 -0
  148. korvid-0.1.2/src/korvid/evals/scenarios/service-selector-mismatch.yaml +79 -0
  149. korvid-0.1.2/src/korvid/evals/scenarios/stuck-rollout.yaml +148 -0
  150. korvid-0.1.2/src/korvid/evals/scripted.py +43 -0
  151. korvid-0.1.2/src/korvid/evals/serving.py +196 -0
  152. korvid-0.1.2/src/korvid/k8s/__init__.py +0 -0
  153. korvid-0.1.2/src/korvid/k8s/client.py +1768 -0
  154. korvid-0.1.2/src/korvid/k8s/columns.py +119 -0
  155. korvid-0.1.2/src/korvid/k8s/components.py +218 -0
  156. korvid-0.1.2/src/korvid/k8s/csp.py +99 -0
  157. korvid-0.1.2/src/korvid/k8s/discovery.py +42 -0
  158. korvid-0.1.2/src/korvid/k8s/drain.py +302 -0
  159. korvid-0.1.2/src/korvid/k8s/dryrun.py +106 -0
  160. korvid-0.1.2/src/korvid/k8s/errors.py +29 -0
  161. korvid-0.1.2/src/korvid/k8s/helm.py +281 -0
  162. korvid-0.1.2/src/korvid/k8s/helmcli.py +475 -0
  163. korvid-0.1.2/src/korvid/k8s/logs.py +21 -0
  164. korvid-0.1.2/src/korvid/k8s/managed.py +159 -0
  165. korvid-0.1.2/src/korvid/k8s/metrics.py +177 -0
  166. korvid-0.1.2/src/korvid/k8s/models.py +952 -0
  167. korvid-0.1.2/src/korvid/k8s/olm.py +180 -0
  168. korvid-0.1.2/src/korvid/k8s/portforward.py +84 -0
  169. korvid-0.1.2/src/korvid/k8s/reads.py +61 -0
  170. korvid-0.1.2/src/korvid/k8s/relations.py +33 -0
  171. korvid-0.1.2/src/korvid/k8s/telemetry.py +19 -0
  172. korvid-0.1.2/src/korvid/k8s/telepresence.py +272 -0
  173. korvid-0.1.2/src/korvid/k8s/writes.py +201 -0
  174. korvid-0.1.2/src/korvid/mcp/__init__.py +0 -0
  175. korvid-0.1.2/src/korvid/mcp/server.py +626 -0
  176. korvid-0.1.2/src/korvid/providers/__init__.py +0 -0
  177. korvid-0.1.2/src/korvid/providers/configurator.py +211 -0
  178. korvid-0.1.2/src/korvid/providers/entra.py +56 -0
  179. korvid-0.1.2/src/korvid/providers/github_copilot.py +163 -0
  180. korvid-0.1.2/src/korvid/providers/net.py +89 -0
  181. korvid-0.1.2/src/korvid/providers/ollama.py +321 -0
  182. korvid-0.1.2/src/korvid/providers/openai_compat.py +171 -0
  183. korvid-0.1.2/src/korvid/providers/plugin_registry.py +355 -0
  184. korvid-0.1.2/src/korvid/providers/registry.py +253 -0
  185. korvid-0.1.2/src/korvid/providers/static_creds.py +23 -0
  186. korvid-0.1.2/src/korvid/providers/token_store.py +133 -0
  187. korvid-0.1.2/src/korvid/py.typed +0 -0
  188. korvid-0.1.2/src/korvid/tools/__init__.py +0 -0
  189. korvid-0.1.2/src/korvid/tools/diagnose.py +348 -0
  190. korvid-0.1.2/src/korvid/tools/executor.py +1895 -0
  191. korvid-0.1.2/src/korvid/tools/follow.py +164 -0
  192. korvid-0.1.2/src/korvid/tools/proposals.py +400 -0
  193. korvid-0.1.2/src/korvid/tools/registry.py +1189 -0
  194. korvid-0.1.2/src/korvid/tools/structured.py +282 -0
  195. korvid-0.1.2/src/korvid/ui/__init__.py +0 -0
  196. korvid-0.1.2/src/korvid/ui/app.py +8060 -0
  197. korvid-0.1.2/src/korvid/ui/command.py +139 -0
  198. korvid-0.1.2/src/korvid/ui/debug.py +271 -0
  199. korvid-0.1.2/src/korvid/ui/drain.py +273 -0
  200. korvid-0.1.2/src/korvid/ui/forward_controller.py +678 -0
  201. korvid-0.1.2/src/korvid/ui/helm_controller.py +759 -0
  202. korvid-0.1.2/src/korvid/ui/hints.py +334 -0
  203. korvid-0.1.2/src/korvid/ui/logformat.py +119 -0
  204. korvid-0.1.2/src/korvid/ui/messages.py +140 -0
  205. korvid-0.1.2/src/korvid/ui/navigation.py +67 -0
  206. korvid-0.1.2/src/korvid/ui/operator_controller.py +642 -0
  207. korvid-0.1.2/src/korvid/ui/shell.py +208 -0
  208. korvid-0.1.2/src/korvid/ui/shell_controller.py +939 -0
  209. korvid-0.1.2/src/korvid/ui/theme.py +91 -0
  210. korvid-0.1.2/src/korvid/ui/transfer.py +237 -0
  211. korvid-0.1.2/src/korvid/ui/ui_surface.py +136 -0
  212. korvid-0.1.2/src/korvid/ui/view_state.py +99 -0
  213. korvid-0.1.2/src/korvid/ui/widgets/__init__.py +0 -0
  214. korvid-0.1.2/src/korvid/ui/widgets/agent_panel.py +483 -0
  215. korvid-0.1.2/src/korvid/ui/widgets/agent_setup_screen.py +445 -0
  216. korvid-0.1.2/src/korvid/ui/widgets/command_bar.py +87 -0
  217. korvid-0.1.2/src/korvid/ui/widgets/confirm_screen.py +306 -0
  218. korvid-0.1.2/src/korvid/ui/widgets/containers_screen.py +131 -0
  219. korvid-0.1.2/src/korvid/ui/widgets/describe_screen.py +493 -0
  220. korvid-0.1.2/src/korvid/ui/widgets/filter_bar.py +46 -0
  221. korvid-0.1.2/src/korvid/ui/widgets/helm_chart_search.py +176 -0
  222. korvid-0.1.2/src/korvid/ui/widgets/helm_install.py +410 -0
  223. korvid-0.1.2/src/korvid/ui/widgets/helm_repos.py +230 -0
  224. korvid-0.1.2/src/korvid/ui/widgets/help_screen.py +211 -0
  225. korvid-0.1.2/src/korvid/ui/widgets/hierarchy_screen.py +363 -0
  226. korvid-0.1.2/src/korvid/ui/widgets/hint_detail.py +164 -0
  227. korvid-0.1.2/src/korvid/ui/widgets/hint_strip.py +179 -0
  228. korvid-0.1.2/src/korvid/ui/widgets/log_pane.py +439 -0
  229. korvid-0.1.2/src/korvid/ui/widgets/logo.py +63 -0
  230. korvid-0.1.2/src/korvid/ui/widgets/namespace_picker.py +33 -0
  231. korvid-0.1.2/src/korvid/ui/widgets/operator_install.py +210 -0
  232. korvid-0.1.2/src/korvid/ui/widgets/path_picker.py +258 -0
  233. korvid-0.1.2/src/korvid/ui/widgets/payload_inspector.py +76 -0
  234. korvid-0.1.2/src/korvid/ui/widgets/pick_screen.py +87 -0
  235. korvid-0.1.2/src/korvid/ui/widgets/port_forward_screen.py +332 -0
  236. korvid-0.1.2/src/korvid/ui/widgets/resize_prompt.py +171 -0
  237. korvid-0.1.2/src/korvid/ui/widgets/resource_table.py +1079 -0
  238. korvid-0.1.2/src/korvid/ui/widgets/secret_screen.py +230 -0
  239. korvid-0.1.2/src/korvid/ui/widgets/status_bar.py +112 -0
  240. korvid-0.1.2/src/korvid/ui/widgets/telepresence_screen.py +115 -0
  241. korvid-0.1.2/src/korvid/ui/widgets/top_bar.py +257 -0
  242. korvid-0.1.2/src/korvid/ui/widgets/transfer_screen.py +260 -0
  243. korvid-0.1.2/src/korvid/ui/write_gate.py +278 -0
  244. korvid-0.1.2/tach.toml +33 -0
  245. korvid-0.1.2/tests/__init__.py +0 -0
  246. korvid-0.1.2/tests/agent/__init__.py +0 -0
  247. korvid-0.1.2/tests/agent/test_context.py +47 -0
  248. korvid-0.1.2/tests/agent/test_credentials.py +18 -0
  249. korvid-0.1.2/tests/agent/test_events.py +16 -0
  250. korvid-0.1.2/tests/agent/test_evidence.py +744 -0
  251. korvid-0.1.2/tests/agent/test_interrupt.py +237 -0
  252. korvid-0.1.2/tests/agent/test_navigation.py +193 -0
  253. korvid-0.1.2/tests/agent/test_outbound.py +1710 -0
  254. korvid-0.1.2/tests/agent/test_plugin_runtime.py +283 -0
  255. korvid-0.1.2/tests/agent/test_profiles.py +444 -0
  256. korvid-0.1.2/tests/agent/test_provider.py +97 -0
  257. korvid-0.1.2/tests/agent/test_provider_plugin.py +907 -0
  258. korvid-0.1.2/tests/agent/test_runtime.py +3827 -0
  259. korvid-0.1.2/tests/contract/__init__.py +8 -0
  260. korvid-0.1.2/tests/contract/conftest.py +190 -0
  261. korvid-0.1.2/tests/contract/janitor.py +73 -0
  262. korvid-0.1.2/tests/contract/test_create_replace_contract.py +105 -0
  263. korvid-0.1.2/tests/contract/test_delete_contract.py +69 -0
  264. korvid-0.1.2/tests/contract/test_helm_contract.py +93 -0
  265. korvid-0.1.2/tests/contract/test_node_ops_contract.py +147 -0
  266. korvid-0.1.2/tests/contract/test_rbac_contract.py +143 -0
  267. korvid-0.1.2/tests/contract/test_resize_contract.py +68 -0
  268. korvid-0.1.2/tests/contract/test_scale_rollout_contract.py +99 -0
  269. korvid-0.1.2/tests/core/__init__.py +0 -0
  270. korvid-0.1.2/tests/core/test_audit.py +400 -0
  271. korvid-0.1.2/tests/core/test_config.py +1617 -0
  272. korvid-0.1.2/tests/core/test_debugimage.py +453 -0
  273. korvid-0.1.2/tests/core/test_errors.py +18 -0
  274. korvid-0.1.2/tests/core/test_filters.py +317 -0
  275. korvid-0.1.2/tests/core/test_keybindings.py +150 -0
  276. korvid-0.1.2/tests/core/test_logbuffer.py +120 -0
  277. korvid-0.1.2/tests/core/test_logexport.py +171 -0
  278. korvid-0.1.2/tests/core/test_portforward.py +1258 -0
  279. korvid-0.1.2/tests/core/test_private_export.py +76 -0
  280. korvid-0.1.2/tests/core/test_pvc_analysis.py +870 -0
  281. korvid-0.1.2/tests/core/test_redaction.py +713 -0
  282. korvid-0.1.2/tests/core/test_secrets.py +153 -0
  283. korvid-0.1.2/tests/core/test_service_analysis.py +269 -0
  284. korvid-0.1.2/tests/core/test_sorting.py +234 -0
  285. korvid-0.1.2/tests/core/test_store.py +146 -0
  286. korvid-0.1.2/tests/core/test_transfer.py +392 -0
  287. korvid-0.1.2/tests/core/test_transfer_stream.py +526 -0
  288. korvid-0.1.2/tests/core/test_watch.py +402 -0
  289. korvid-0.1.2/tests/evals/__init__.py +0 -0
  290. korvid-0.1.2/tests/evals/test_cli.py +403 -0
  291. korvid-0.1.2/tests/evals/test_fake_kube.py +533 -0
  292. korvid-0.1.2/tests/evals/test_grader.py +891 -0
  293. korvid-0.1.2/tests/evals/test_journey.py +779 -0
  294. korvid-0.1.2/tests/evals/test_journey_runner.py +407 -0
  295. korvid-0.1.2/tests/evals/test_journeys_cli.py +192 -0
  296. korvid-0.1.2/tests/evals/test_live_journey.py +174 -0
  297. korvid-0.1.2/tests/evals/test_runner.py +1017 -0
  298. korvid-0.1.2/tests/evals/test_scenario.py +279 -0
  299. korvid-0.1.2/tests/evals/test_scenarios_pack.py +301 -0
  300. korvid-0.1.2/tests/evals/test_serving.py +140 -0
  301. korvid-0.1.2/tests/evals/test_serving_manifest.py +50 -0
  302. korvid-0.1.2/tests/evals/test_tool_surface_arm.py +229 -0
  303. korvid-0.1.2/tests/fixtures/__init__.py +0 -0
  304. korvid-0.1.2/tests/fixtures/provider_plugin/__init__.py +0 -0
  305. korvid-0.1.2/tests/fixtures/provider_plugin/company_provider.py +80 -0
  306. korvid-0.1.2/tests/fixtures/provider_plugin/site_helpers.py +53 -0
  307. korvid-0.1.2/tests/fixtures/provider_plugin/unselected_provider.py +7 -0
  308. korvid-0.1.2/tests/k8s/__init__.py +0 -0
  309. korvid-0.1.2/tests/k8s/test_client.py +2353 -0
  310. korvid-0.1.2/tests/k8s/test_client_node_ops.py +281 -0
  311. korvid-0.1.2/tests/k8s/test_client_previews.py +374 -0
  312. korvid-0.1.2/tests/k8s/test_client_resize.py +169 -0
  313. korvid-0.1.2/tests/k8s/test_columns.py +123 -0
  314. korvid-0.1.2/tests/k8s/test_components.py +326 -0
  315. korvid-0.1.2/tests/k8s/test_csp.py +175 -0
  316. korvid-0.1.2/tests/k8s/test_discovery.py +196 -0
  317. korvid-0.1.2/tests/k8s/test_drain.py +388 -0
  318. korvid-0.1.2/tests/k8s/test_dryrun.py +93 -0
  319. korvid-0.1.2/tests/k8s/test_helm.py +549 -0
  320. korvid-0.1.2/tests/k8s/test_helmcli.py +836 -0
  321. korvid-0.1.2/tests/k8s/test_logs.py +298 -0
  322. korvid-0.1.2/tests/k8s/test_managed.py +267 -0
  323. korvid-0.1.2/tests/k8s/test_metrics.py +283 -0
  324. korvid-0.1.2/tests/k8s/test_models.py +1551 -0
  325. korvid-0.1.2/tests/k8s/test_models_trouble.py +394 -0
  326. korvid-0.1.2/tests/k8s/test_olm.py +180 -0
  327. korvid-0.1.2/tests/k8s/test_portforward.py +75 -0
  328. korvid-0.1.2/tests/k8s/test_relations.py +47 -0
  329. korvid-0.1.2/tests/k8s/test_telepresence.py +337 -0
  330. korvid-0.1.2/tests/mcp/__init__.py +0 -0
  331. korvid-0.1.2/tests/mcp/test_server.py +1141 -0
  332. korvid-0.1.2/tests/performance/__init__.py +0 -0
  333. korvid-0.1.2/tests/performance/cli.py +548 -0
  334. korvid-0.1.2/tests/performance/live.py +1612 -0
  335. korvid-0.1.2/tests/performance/manifests.py +147 -0
  336. korvid-0.1.2/tests/performance/metrics.py +816 -0
  337. korvid-0.1.2/tests/performance/profile.py +203 -0
  338. korvid-0.1.2/tests/performance/profiles/aks-1k.json +15 -0
  339. korvid-0.1.2/tests/performance/profiles/aks-live-1k.json +15 -0
  340. korvid-0.1.2/tests/performance/profiles/burst-50k.json +15 -0
  341. korvid-0.1.2/tests/performance/profiles/smoke-1k.json +13 -0
  342. korvid-0.1.2/tests/performance/profiles/steady-10k.json +13 -0
  343. korvid-0.1.2/tests/performance/replay.py +668 -0
  344. korvid-0.1.2/tests/performance/test_cli.py +998 -0
  345. korvid-0.1.2/tests/performance/test_live.py +2728 -0
  346. korvid-0.1.2/tests/performance/test_manifests.py +175 -0
  347. korvid-0.1.2/tests/performance/test_metrics.py +783 -0
  348. korvid-0.1.2/tests/performance/test_profile.py +173 -0
  349. korvid-0.1.2/tests/performance/test_replay.py +530 -0
  350. korvid-0.1.2/tests/performance/test_workload.py +63 -0
  351. korvid-0.1.2/tests/performance/workload.py +103 -0
  352. korvid-0.1.2/tests/platforms.py +47 -0
  353. korvid-0.1.2/tests/providers/__init__.py +0 -0
  354. korvid-0.1.2/tests/providers/test_configurator.py +513 -0
  355. korvid-0.1.2/tests/providers/test_entra.py +51 -0
  356. korvid-0.1.2/tests/providers/test_github_copilot.py +185 -0
  357. korvid-0.1.2/tests/providers/test_net.py +292 -0
  358. korvid-0.1.2/tests/providers/test_ollama.py +721 -0
  359. korvid-0.1.2/tests/providers/test_openai_compat.py +325 -0
  360. korvid-0.1.2/tests/providers/test_plugin_registry.py +1101 -0
  361. korvid-0.1.2/tests/providers/test_registry.py +870 -0
  362. korvid-0.1.2/tests/providers/test_token_store.py +320 -0
  363. korvid-0.1.2/tests/test_cli.py +121 -0
  364. korvid-0.1.2/tests/test_main_recovery.py +328 -0
  365. korvid-0.1.2/tests/test_main_wiring.py +1996 -0
  366. korvid-0.1.2/tests/test_optional_extras.py +166 -0
  367. korvid-0.1.2/tests/test_platforms.py +268 -0
  368. korvid-0.1.2/tests/test_release_scripts.py +1934 -0
  369. korvid-0.1.2/tests/test_sanity.py +16 -0
  370. korvid-0.1.2/tests/tools/__init__.py +0 -0
  371. korvid-0.1.2/tests/tools/test_diagnose.py +553 -0
  372. korvid-0.1.2/tests/tools/test_executor.py +4000 -0
  373. korvid-0.1.2/tests/tools/test_follow.py +178 -0
  374. korvid-0.1.2/tests/tools/test_list_resources.py +368 -0
  375. korvid-0.1.2/tests/tools/test_proposals.py +436 -0
  376. korvid-0.1.2/tests/tools/test_registry.py +602 -0
  377. korvid-0.1.2/tests/tools/test_structured.py +313 -0
  378. korvid-0.1.2/tests/tools/test_write_tools.py +251 -0
  379. korvid-0.1.2/tests/ui/__init__.py +0 -0
  380. korvid-0.1.2/tests/ui/test_adaptive_footer.py +487 -0
  381. korvid-0.1.2/tests/ui/test_agent_follow.py +203 -0
  382. korvid-0.1.2/tests/ui/test_agent_interrupt.py +517 -0
  383. korvid-0.1.2/tests/ui/test_agent_off.py +206 -0
  384. korvid-0.1.2/tests/ui/test_agent_panel.py +525 -0
  385. korvid-0.1.2/tests/ui/test_agent_setup_screen.py +648 -0
  386. korvid-0.1.2/tests/ui/test_agent_ui_drive.py +910 -0
  387. korvid-0.1.2/tests/ui/test_agent_wiring.py +937 -0
  388. korvid-0.1.2/tests/ui/test_agent_write.py +641 -0
  389. korvid-0.1.2/tests/ui/test_app.py +1187 -0
  390. korvid-0.1.2/tests/ui/test_busy_bird.py +183 -0
  391. korvid-0.1.2/tests/ui/test_column_sorting.py +574 -0
  392. korvid-0.1.2/tests/ui/test_command.py +261 -0
  393. korvid-0.1.2/tests/ui/test_confirm_screen.py +494 -0
  394. korvid-0.1.2/tests/ui/test_containers_screen.py +253 -0
  395. korvid-0.1.2/tests/ui/test_csp_footer.py +89 -0
  396. korvid-0.1.2/tests/ui/test_ctx_switch.py +1261 -0
  397. korvid-0.1.2/tests/ui/test_cursor_stability.py +141 -0
  398. korvid-0.1.2/tests/ui/test_custom_columns.py +221 -0
  399. korvid-0.1.2/tests/ui/test_debug_controller.py +218 -0
  400. korvid-0.1.2/tests/ui/test_describe.py +416 -0
  401. korvid-0.1.2/tests/ui/test_describe_search.py +374 -0
  402. korvid-0.1.2/tests/ui/test_drain_controller.py +215 -0
  403. korvid-0.1.2/tests/ui/test_drilldown.py +784 -0
  404. korvid-0.1.2/tests/ui/test_dryrun_preview.py +275 -0
  405. korvid-0.1.2/tests/ui/test_filter_enhancements.py +141 -0
  406. korvid-0.1.2/tests/ui/test_helm_actions.py +1649 -0
  407. korvid-0.1.2/tests/ui/test_helm_chart_search.py +233 -0
  408. korvid-0.1.2/tests/ui/test_helm_install_prompt.py +664 -0
  409. korvid-0.1.2/tests/ui/test_helm_repo_screen.py +283 -0
  410. korvid-0.1.2/tests/ui/test_helm_view.py +248 -0
  411. korvid-0.1.2/tests/ui/test_help_screen.py +324 -0
  412. korvid-0.1.2/tests/ui/test_hierarchy_nav.py +956 -0
  413. korvid-0.1.2/tests/ui/test_hierarchy_screen.py +405 -0
  414. korvid-0.1.2/tests/ui/test_hint_controller.py +211 -0
  415. korvid-0.1.2/tests/ui/test_hint_detail.py +119 -0
  416. korvid-0.1.2/tests/ui/test_hint_strip.py +234 -0
  417. korvid-0.1.2/tests/ui/test_hint_wiring.py +666 -0
  418. korvid-0.1.2/tests/ui/test_keybindings.py +176 -0
  419. korvid-0.1.2/tests/ui/test_log_pane.py +1616 -0
  420. korvid-0.1.2/tests/ui/test_logformat.py +225 -0
  421. korvid-0.1.2/tests/ui/test_mcp_follow.py +252 -0
  422. korvid-0.1.2/tests/ui/test_mcp_ui_context.py +304 -0
  423. korvid-0.1.2/tests/ui/test_messages.py +34 -0
  424. korvid-0.1.2/tests/ui/test_metrics_wiring.py +210 -0
  425. korvid-0.1.2/tests/ui/test_navigation.py +70 -0
  426. korvid-0.1.2/tests/ui/test_node_ops.py +738 -0
  427. korvid-0.1.2/tests/ui/test_node_shell.py +902 -0
  428. korvid-0.1.2/tests/ui/test_olm_view.py +716 -0
  429. korvid-0.1.2/tests/ui/test_operator_install.py +204 -0
  430. korvid-0.1.2/tests/ui/test_operator_uninstall.py +486 -0
  431. korvid-0.1.2/tests/ui/test_payload_inspector.py +182 -0
  432. korvid-0.1.2/tests/ui/test_pick_screen.py +58 -0
  433. korvid-0.1.2/tests/ui/test_port_forward.py +2113 -0
  434. korvid-0.1.2/tests/ui/test_proposals_ui.py +1283 -0
  435. korvid-0.1.2/tests/ui/test_protected_contexts.py +259 -0
  436. korvid-0.1.2/tests/ui/test_resize_flow.py +393 -0
  437. korvid-0.1.2/tests/ui/test_resize_prompt.py +207 -0
  438. korvid-0.1.2/tests/ui/test_resource_table_cells.py +353 -0
  439. korvid-0.1.2/tests/ui/test_secret_screen.py +479 -0
  440. korvid-0.1.2/tests/ui/test_shell.py +1504 -0
  441. korvid-0.1.2/tests/ui/test_shell_reservation.py +360 -0
  442. korvid-0.1.2/tests/ui/test_split_pane.py +757 -0
  443. korvid-0.1.2/tests/ui/test_table_column_widths.py +286 -0
  444. korvid-0.1.2/tests/ui/test_table_diff_update.py +269 -0
  445. korvid-0.1.2/tests/ui/test_table_row_memo.py +233 -0
  446. korvid-0.1.2/tests/ui/test_telepresence.py +354 -0
  447. korvid-0.1.2/tests/ui/test_theme.py +108 -0
  448. korvid-0.1.2/tests/ui/test_top_bar.py +352 -0
  449. korvid-0.1.2/tests/ui/test_transfer.py +655 -0
  450. korvid-0.1.2/tests/ui/test_transfer_controller.py +249 -0
  451. korvid-0.1.2/tests/ui/test_transfer_picker.py +808 -0
  452. korvid-0.1.2/tests/ui/test_view_state_seam.py +169 -0
  453. korvid-0.1.2/tests/ui/test_waits.py +65 -0
  454. korvid-0.1.2/tests/ui/test_widget_accessors.py +48 -0
  455. korvid-0.1.2/tests/ui/test_write_confirm_characterization.py +281 -0
  456. korvid-0.1.2/tests/ui/test_write_ops.py +1531 -0
  457. korvid-0.1.2/tests/ui/waits.py +49 -0
  458. korvid-0.1.2/uv.lock +2726 -0
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ INPUT=$(cat)
3
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
4
+ PROTECTED=("uv.lock" ".github/workflows/" "tach.toml" ".pre-commit-config.yaml")
5
+ for pattern in "${PROTECTED[@]}"; do
6
+ if [[ "$FILE_PATH" == *"$pattern"* ]]; then
7
+ echo "Blocked: $FILE_PATH is a protected gate file — ask the human to change it" >&2
8
+ exit 2
9
+ fi
10
+ done
11
+ exit 0
@@ -0,0 +1,37 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Edit|Write",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "file=$(jq -r '.tool_input.file_path // empty'); if echo \"$file\" | grep -q '\\.py$'; then uv run ruff check --fix --quiet \"$file\" 2>/dev/null; uv run ruff format --quiet \"$file\" 2>/dev/null; fi; exit 0"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PreToolUse": [
15
+ {
16
+ "matcher": "Edit|Write",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "Stop": [
26
+ {
27
+ "matcher": "",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "uv run pytest -x -q --tb=short 2>&1 | tail -20"
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ }
37
+ }
@@ -0,0 +1,76 @@
1
+ version: 2
2
+
3
+ updates:
4
+ # Python dependencies, resolved through uv.lock.
5
+ #
6
+ # Runtime vulnerabilities already fail CI: the `security` job audits the
7
+ # locked runtime resolution with pip-audit. Nothing audits the dev group,
8
+ # because dev packages do not ship in the wheel - but they do run in CI
9
+ # with the repository checked out, and nine GitPython advisories sat open
10
+ # because an alert with no path to a pull request is not a process.
11
+ - package-ecosystem: uv
12
+ directory: /
13
+ schedule:
14
+ interval: weekly
15
+ open-pull-requests-limit: 5
16
+ ignore:
17
+ # mcp 2.0 is a breaking release: the Server type parameters, the
18
+ # registration surface, `request_context`, the Tool schema field name
19
+ # and the streamable_http_client return arity all changed. That
20
+ # migration touches the path the approval gate sits on, so it is
21
+ # tracked as #260 rather than done as a bump. Remove this entry in
22
+ # the same change that closes it.
23
+ - dependency-name: mcp
24
+ update-types: [version-update:semver-major]
25
+ groups:
26
+ # One pull request per group keeps the review loop bounded: a routine
27
+ # dev-tool bump does not deserve the same attention as a runtime one.
28
+ #
29
+ # Grouped by name rather than by `dependency-type`: the uv updater
30
+ # reports every dependency as production, so a development group would
31
+ # match nothing and the runtime group would quietly absorb the dev
32
+ # tools it exists to separate. The runtime list is the project
33
+ # dependencies plus every extra - keep it in step with pyproject.toml.
34
+ runtime:
35
+ patterns:
36
+ - textual
37
+ - kubernetes_asyncio
38
+ - kubernetes-asyncio
39
+ - pyyaml
40
+ - rich
41
+ - regex
42
+ - httpx
43
+ - keyring
44
+ - mcp
45
+ - anyio
46
+ - starlette
47
+ - uvicorn
48
+ - azure-identity
49
+ dev-tooling:
50
+ patterns: ["*"]
51
+ exclude-patterns:
52
+ - textual
53
+ - kubernetes_asyncio
54
+ - kubernetes-asyncio
55
+ - pyyaml
56
+ - rich
57
+ - regex
58
+ - httpx
59
+ - keyring
60
+ - mcp
61
+ - anyio
62
+ - starlette
63
+ - uvicorn
64
+ - azure-identity
65
+
66
+ # Action pins. Every `uses:` in this repository is pinned to a commit SHA,
67
+ # which is what makes a supply-chain review meaningful and also what makes
68
+ # the pins invisible to a human reader - they cannot tell a current pin
69
+ # from a two-year-old one without looking each up.
70
+ - package-ecosystem: github-actions
71
+ directory: /
72
+ schedule:
73
+ interval: weekly
74
+ groups:
75
+ actions:
76
+ patterns: ["*"]
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bash
2
+ # Wait until the AKS cluster has no operation in flight.
3
+ #
4
+ # Azure flips `powerState.code` as soon as it accepts a start/stop request,
5
+ # while `provisioningState` stays Starting/Stopping/Updating until the
6
+ # operation actually completes. Any other operation issued in that window is
7
+ # rejected with "there's an in-progress ... operation", so both the start and
8
+ # the stop paths have to wait for the real thing.
9
+ #
10
+ # Usage: wait-aks-settled.sh <resource-group> <cluster> [expected-power-state]
11
+ #
12
+ # Exits 0 once provisioningState is Succeeded (and, when given, powerState
13
+ # matches). Exits 1 if that has not happened within WAIT_AKS_TIMEOUT seconds
14
+ # (default 600), printing the last observed pair.
15
+ #
16
+ # Query failures are retried through the whole budget by default, because the
17
+ # caller after a stop is a barrier: giving up early would release the
18
+ # workflow's concurrency group while the stop is still in flight, which is the
19
+ # collision this exists to prevent. Set WAIT_AKS_MAX_QUERY_FAILURES to abort
20
+ # after N consecutive failures instead - only appropriate for an advisory
21
+ # pre-flight check whose failure does not skip anything.
22
+ set -uo pipefail
23
+
24
+ group=${1:?resource group required}
25
+ cluster=${2:?cluster name required}
26
+ want_power=${3:-}
27
+ max_fails=${WAIT_AKS_MAX_QUERY_FAILURES:-0}
28
+
29
+ deadline=$(( SECONDS + ${WAIT_AKS_TIMEOUT:-600} ))
30
+ power=""
31
+ prov=""
32
+ fails=0
33
+ err_file=$(mktemp)
34
+ trap 'rm -f "$err_file"' EXIT
35
+
36
+ while :; do
37
+ if out=$(az aks show -g "$group" -n "$cluster" \
38
+ --query '[powerState.code,provisioningState]' -o tsv 2>"$err_file"); then
39
+ fails=0
40
+ read -r power prov < <(printf '%s' "$out" | tr '\n' ' ') || true
41
+ else
42
+ # An expired login, a wrong subscription or a deleted cluster is
43
+ # permanent, so every failure is logged rather than hidden. Whether to
44
+ # give up early is the caller's call: a barrier must keep waiting, since
45
+ # returning early would release the concurrency group with an operation
46
+ # still running.
47
+ fails=$(( fails + 1 ))
48
+ power=""
49
+ prov=""
50
+ echo "az aks show failed: $(tr '\n' ' ' <"$err_file")"
51
+ if [ "$max_fails" -gt 0 ] && [ "$fails" -ge "$max_fails" ]; then
52
+ echo "::error title=Cluster query failed::az aks show failed $fails times in a row for $cluster: $(tr '\n' ' ' <"$err_file")"
53
+ exit 1
54
+ fi
55
+ fi
56
+ if [ "${prov:-}" = "Succeeded" ] && { [ -z "$want_power" ] || [ "${power:-}" = "$want_power" ]; }; then
57
+ exit 0
58
+ fi
59
+ case "${prov:-}" in
60
+ Failed|Canceled)
61
+ # Terminal: nothing is in flight, so polling to the deadline would only
62
+ # delay the report and then blame the wrong thing.
63
+ echo "::error title=Cluster in a terminal state::$cluster provisioningState=$prov (power=${power:-unknown}); no operation is in flight."
64
+ exit 1
65
+ ;;
66
+ esac
67
+ # Poll once more *at* the deadline rather than sleeping past it and failing
68
+ # on a stale read: an operation that settles during the final sleep still
69
+ # counts as settled within the advertised budget.
70
+ [ "$SECONDS" -lt "$deadline" ] || break
71
+ echo "waiting for $cluster to settle (${power:-unknown}/${prov:-unknown})"
72
+ sleep 20
73
+ done
74
+
75
+ echo "::error title=Cluster busy::$cluster is ${power:-unknown}/${prov:-unknown} after ${WAIT_AKS_TIMEOUT:-600}s; another operation has not finished."
76
+ exit 1
@@ -0,0 +1,160 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ # Supersede a PR's older runs when new commits arrive. Non-PR events get a
11
+ # unique group: GitHub cancels a *pending* run when a newer one joins the
12
+ # group even with cancel-in-progress off, so sharing a group on main would
13
+ # let a burst of merges discard the middle commit's verification.
14
+ concurrency:
15
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
16
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
17
+
18
+ jobs:
19
+ # Docs are load-bearing for the suite - tests assert README, SECURITY.md,
20
+ # docs/keybindings.md and docs/windows.md still match the code and the
21
+ # release workflow - so a docs edit must still be tested. It does not need
22
+ # testing four times: those contracts are not Python-version or OS
23
+ # specific. Classify the change here so the redundant legs can skip the
24
+ # 20-30 minute pytest step while still reporting (they are required checks,
25
+ # and a required check that never runs blocks the merge forever).
26
+ changes:
27
+ runs-on: ubuntu-latest
28
+ # Reading the PR's file list needs more than the workflow-level
29
+ # `contents: read`; without this the API call 404s on every PR.
30
+ permissions:
31
+ contents: read
32
+ pull-requests: read
33
+ outputs:
34
+ code: ${{ steps.classify.outputs.code }}
35
+ steps:
36
+ - name: Classify the change
37
+ id: classify
38
+ env:
39
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40
+ PR: ${{ github.event.pull_request.number }}
41
+ REPO: ${{ github.repository }}
42
+ run: |
43
+ set -uo pipefail
44
+ # Fail closed: anything we cannot positively identify as docs-only
45
+ # gets the full matrix. That includes a failed API call - exiting
46
+ # non-zero here would *skip* the dependent test jobs rather than run
47
+ # them, and a skipped required check never reports.
48
+ if [ -z "${PR:-}" ]; then
49
+ echo "code=true" >> "$GITHUB_OUTPUT"
50
+ echo "not a pull request; running everything"
51
+ exit 0
52
+ fi
53
+ if ! files=$(gh api --paginate "repos/$REPO/pulls/$PR/files" --jq '.[].filename'); then
54
+ echo "code=true" >> "$GITHUB_OUTPUT"
55
+ echo "could not read the file list; running everything"
56
+ exit 0
57
+ fi
58
+ echo "$files"
59
+ # Docs-only means every path is under docs/ or is a root .md file.
60
+ # grep must consume the whole list: `grep -q` stops at the first hit
61
+ # and SIGPIPEs the writer, which under pipefail fails the pipeline
62
+ # and would read as docs-only - skipping tests on a code change.
63
+ non_docs=$(printf '%s\n' "$files" | { grep -cvE '^(docs/.*|[^/]+\.md)$' || true; })
64
+ if [ "${non_docs:-1}" -gt 0 ]; then
65
+ echo "code=true" >> "$GITHUB_OUTPUT"
66
+ else
67
+ echo "code=false" >> "$GITHUB_OUTPUT"
68
+ echo "docs-only: one full test pass instead of four"
69
+ fi
70
+
71
+ test:
72
+ needs: changes
73
+ runs-on: korvid-runners
74
+ strategy:
75
+ matrix:
76
+ python-version: ["3.11", "3.12", "3.13"]
77
+ steps:
78
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
79
+ with:
80
+ persist-credentials: false
81
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
82
+ with:
83
+ python-version: ${{ matrix.python-version }}
84
+ enable-cache: true
85
+ - run: uv sync --locked --dev --all-extras
86
+ - run: uv run ruff check src/ tests/
87
+ - run: uv run ruff format --check src/ tests/
88
+ - run: uv run mypy
89
+ - run: uv run tach check
90
+ - run: uvx deptry src/
91
+ # A docs-only change still gets a full suite run, on one version.
92
+ - if: needs.changes.outputs.code == 'true' || matrix.python-version == '3.12'
93
+ run: uv run pytest --cov --cov-fail-under=80
94
+
95
+ windows-test:
96
+ needs: changes
97
+ runs-on: windows-latest
98
+ steps:
99
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
100
+ with:
101
+ persist-credentials: false
102
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
103
+ with:
104
+ python-version: "3.12"
105
+ enable-cache: true
106
+ - run: uv sync --locked --dev --all-extras
107
+ # Windows verifies OS-specific behaviour; docs contracts are not that.
108
+ - if: needs.changes.outputs.code == 'true'
109
+ run: uv run pytest -q
110
+
111
+ pre-commit:
112
+ runs-on: korvid-runners
113
+ steps:
114
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
115
+ with:
116
+ persist-credentials: false
117
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
118
+ with:
119
+ enable-cache: true
120
+ - run: uv sync --locked --dev --all-extras
121
+ - run: uv run pre-commit run --all-files --show-diff-on-failure
122
+
123
+ security:
124
+ runs-on: korvid-runners
125
+ steps:
126
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
127
+ with:
128
+ persist-credentials: false
129
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
130
+ with:
131
+ enable-cache: true
132
+ # Dependency vulnerability audit against the locked resolution.
133
+ # --all-extras, because `korvid[agent]` and `korvid[mcp]` are supported
134
+ # installs: auditing the base set alone leaves an httpx or starlette
135
+ # advisory invisible until release verification catches it.
136
+ - run: uv export --frozen --all-extras --no-emit-project --no-dev -o requirements.txt
137
+ - run: uvx pip-audit -r requirements.txt --disable-pip
138
+ # Static analysis of GitHub Actions workflows themselves
139
+ - run: uvx zizmor --min-severity medium .github/workflows/
140
+
141
+ dependency-review:
142
+ if: github.event_name == 'pull_request'
143
+ runs-on: korvid-runners
144
+ steps:
145
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
146
+ with:
147
+ persist-credentials: false
148
+ - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
149
+ with:
150
+ fail-on-severity: high
151
+
152
+ ty-experimental:
153
+ runs-on: korvid-runners
154
+ continue-on-error: true
155
+ steps:
156
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
157
+ with:
158
+ persist-credentials: false
159
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
160
+ - run: uvx ty check src/ || true
@@ -0,0 +1,33 @@
1
+ name: CodeQL
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ branches: [main]
7
+ schedule:
8
+ - cron: "17 4 * * 1"
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ # Same policy as CI: supersede a PR's older analyses, and keep every
14
+ # push/schedule analysis in its own group so a queued one is never displaced.
15
+ concurrency:
16
+ group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
17
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
18
+
19
+ jobs:
20
+ analyze:
21
+ runs-on: korvid-runners
22
+ permissions:
23
+ contents: read
24
+ security-events: write
25
+ steps:
26
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
27
+ with:
28
+ persist-credentials: false
29
+ - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
30
+ with:
31
+ languages: python
32
+ queries: security-extended
33
+ - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
@@ -0,0 +1,286 @@
1
+ name: K8s contract tests
2
+ # Live-cluster contract suite (issue #109). Runs the destructive
3
+ # write-path contract tests against the dedicated, stopped-at-rest
4
+ # AKS cluster `aks-korvid-contract-test`. Never runs for pull requests
5
+ # or forks: the Azure identity is scoped to the protected
6
+ # `aks-contract-test` environment, which only `main` may use.
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ concurrency:
17
+ group: korvid-k8s-contract
18
+ cancel-in-progress: false
19
+
20
+ env:
21
+ # The one cluster this workflow is allowed to touch. The guard steps
22
+ # fail closed when the (mutable) environment variables drift from
23
+ # these fixed, documented test-only names.
24
+ EXPECTED_RESOURCE_GROUP: rg-korvid-contract-test
25
+ EXPECTED_CLUSTER_NAME: aks-korvid-contract-test
26
+
27
+ jobs:
28
+ contract:
29
+ # GitHub-hosted ephemeral runner — deliberately NOT the shared
30
+ # self-hosted `korvid-runners` pool, so cluster credentials never
31
+ # touch persistent shared infrastructure.
32
+ runs-on: ubuntu-latest
33
+ environment: aks-contract-test
34
+ timeout-minutes: 45
35
+ permissions:
36
+ contents: read
37
+ id-token: write
38
+ env:
39
+ AKS_RESOURCE_GROUP: ${{ vars.AKS_RESOURCE_GROUP }}
40
+ AKS_CLUSTER_NAME: ${{ vars.AKS_CLUSTER_NAME }}
41
+ # Named only in the guard's repair hint. Fixed rather than a variable so
42
+ # a drifting setting cannot send someone to scale the system pool.
43
+ WORKLOAD_NODEPOOL: workload
44
+ steps:
45
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
+ with:
47
+ persist-credentials: false
48
+
49
+ - name: Guard — refuse any target other than the documented test cluster
50
+ run: |
51
+ if [ "$AKS_RESOURCE_GROUP" != "$EXPECTED_RESOURCE_GROUP" ] || \
52
+ [ "$AKS_CLUSTER_NAME" != "$EXPECTED_CLUSTER_NAME" ]; then
53
+ echo "::error title=Cluster identity mismatch::environment variables point at $AKS_RESOURCE_GROUP/$AKS_CLUSTER_NAME, expected $EXPECTED_RESOURCE_GROUP/$EXPECTED_CLUSTER_NAME. Refusing to run destructive tests."
54
+ exit 1
55
+ fi
56
+
57
+ - uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
58
+ with:
59
+ client-id: ${{ vars.AZURE_CLIENT_ID }}
60
+ tenant-id: ${{ vars.AZURE_TENANT_ID }}
61
+ subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
62
+
63
+ - name: Guard — verify test-only tags before touching the cluster
64
+ run: |
65
+ purpose=$(az aks show -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" \
66
+ --query 'tags.purpose' -o tsv)
67
+ production=$(az aks show -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" \
68
+ --query 'tags."production-use"' -o tsv)
69
+ if [ "$purpose" != "korvid-contract-testing" ] || [ "$production" != "prohibited" ]; then
70
+ echo "::error title=Not a test-only cluster::$AKS_CLUSTER_NAME is missing the purpose=korvid-contract-testing / production-use=prohibited tags. Refusing to run destructive tests."
71
+ exit 1
72
+ fi
73
+
74
+ - name: Start AKS cluster
75
+ run: |
76
+ # A preceding run's stop can still be finishing even though
77
+ # powerState already reads Stopped; Azure rejects a start until it
78
+ # settles. Not a barrier - failing here fails this job and cleanup
79
+ # still runs - so give up early on a broken query rather than
80
+ # burning ten minutes on it.
81
+ WAIT_AKS_MAX_QUERY_FAILURES=3 \
82
+ .github/scripts/wait-aks-settled.sh "$AKS_RESOURCE_GROUP" "$AKS_CLUSTER_NAME"
83
+ state=$(az aks show -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" --query powerState.code -o tsv)
84
+ if [ "$state" != "Running" ]; then
85
+ az aks start -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME"
86
+ fi
87
+
88
+ - uses: azure/use-kubelogin@0ce7c36141aa27d4934872cf00b0120804c98a29 # v1.3
89
+ with:
90
+ kubelogin-version: v0.2.15
91
+ env:
92
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
93
+
94
+ - name: Fetch user credentials (no --admin, isolated kubeconfig)
95
+ run: |
96
+ export KUBECONFIG="$RUNNER_TEMP/contract-kubeconfig"
97
+ az aks get-credentials -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" \
98
+ --overwrite-existing -f "$KUBECONFIG"
99
+ kubelogin convert-kubeconfig -l azurecli --kubeconfig "$KUBECONFIG"
100
+ chmod 600 "$KUBECONFIG"
101
+ echo "KUBECONFIG=$KUBECONFIG" >> "$GITHUB_ENV"
102
+
103
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
104
+ with:
105
+ python-version: "3.13"
106
+ enable-cache: true
107
+
108
+ - run: uv sync --locked --dev --all-extras
109
+
110
+ - name: Install helm
111
+ run: |
112
+ HELM_VERSION=v3.19.0
113
+ curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" \
114
+ | tar -xz -C "$RUNNER_TEMP" linux-amd64/helm
115
+ sudo install "$RUNNER_TEMP/linux-amd64/helm" /usr/local/bin/helm
116
+
117
+ - name: Janitor — remove stale fixtures from interrupted runs
118
+ run: uv run python -m tests.contract.janitor
119
+
120
+ - name: Guard — a schedulable disposable node must exist
121
+ run: |
122
+ set -euo pipefail
123
+ # The suite needs one node that satisfies both of its requirements at
124
+ # once: the node-operation contracts only touch
125
+ # korvid.dev/disposable=true and fail closed rather than fall back to
126
+ # a system node, while every test pod carries a
127
+ # korvid.dev/pool=workload nodeSelector. Those labels riding the same
128
+ # node is a convention of the pool setup, not something enforced
129
+ # here, so both are required together — a node with only one of them
130
+ # would let this guard pass and leave pods Pending.
131
+ #
132
+ # With no such node the suite produces six failures across four files
133
+ # — "no disposable workload node found" and pod-scheduling timeouts —
134
+ # none of which names the cause. It stayed red for eight commits.
135
+ #
136
+ # Asked of the cluster rather than of Azure: a stopped or
137
+ # still-starting cluster reports agentPools[].count as 0 even when
138
+ # the pool is populated, so the control-plane number is not evidence.
139
+ #
140
+ # Checked, not repaired: the workflow identity holds a deliberately
141
+ # minimal role with agentPools/read and no write, and widening it so
142
+ # this job could scale the pool would also let it reshape the system
143
+ # pool that every other guard here exists to protect.
144
+ SELECTOR=korvid.dev/disposable=true,korvid.dev/pool=workload
145
+
146
+ # Prove the node list is reachable first. Otherwise an expired token,
147
+ # an RBAC denial, or an unreachable API server spends the whole
148
+ # deadline and then blames an empty pool, sending the reader after a
149
+ # scale command that cannot help.
150
+ if ! nodes=$(kubectl get nodes -o name 2>&1); then
151
+ echo "::error title=Cannot query nodes::Listing nodes on $AKS_CLUSTER_NAME failed, so the disposable-node check could not run: $nodes"
152
+ exit 1
153
+ fi
154
+
155
+ # `kubectl wait` requires *every* node matching the selector to
156
+ # satisfy the condition, but this guard needs only one somewhere to
157
+ # schedule. A second pool node, or one cycling through an image
158
+ # upgrade, would otherwise burn the whole deadline and report "no
159
+ # schedulable node" while a perfectly good one was sitting there.
160
+ #
161
+ # Ready alone is not enough: a crashed node-ops test leaves the node
162
+ # cordoned, and a cordoned node would pass a Ready-only check and
163
+ # leave every test pod Pending. This runs after the janitor, which
164
+ # uncordons disposable nodes, so anything still cordoned here is
165
+ # something the janitor could not fix and a human has to look at.
166
+ deadline=$(( SECONDS + 300 ))
167
+ while :; do
168
+ if kubectl get nodes -l "$SELECTOR" \
169
+ -o jsonpath='{range .items[*]}{.metadata.name}{"=ready:"}{range .status.conditions[?(@.type=="Ready")]}{.status}{end}{" unschedulable:"}{.spec.unschedulable}{"\n"}{end}' \
170
+ 2>/dev/null | grep -q '=ready:True unschedulable:$'; then
171
+ kubectl get nodes -l "$SELECTOR"
172
+ exit 0
173
+ fi
174
+ [ "$SECONDS" -lt "$deadline" ] || break
175
+ sleep 5
176
+ done
177
+ echo "::error title=No schedulable disposable node::$AKS_CLUSTER_NAME has no Ready, uncordoned node matching $SELECTOR after 5 minutes, so the node-operation and resize contracts have nowhere to schedule. If a previous run left the node cordoned, uncordon it. If the $WORKLOAD_NODEPOOL pool is empty, restore it by starting the cluster first — this run's cleanup job stops it, and Azure rejects every operation except start on a stopped cluster: az aks start -g $AKS_RESOURCE_GROUP -n $AKS_CLUSTER_NAME && az aks nodepool scale -g $AKS_RESOURCE_GROUP --cluster-name $AKS_CLUSTER_NAME -n $WORKLOAD_NODEPOOL --node-count 1 && az aks stop -g $AKS_RESOURCE_GROUP -n $AKS_CLUSTER_NAME"
178
+ kubectl get nodes -o wide --show-labels || true
179
+ exit 1
180
+
181
+ - name: Run contract suite
182
+ timeout-minutes: 30
183
+ env:
184
+ KORVID_CONTRACT_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}
185
+ run: uv run pytest -p no:randomly -m contract tests/contract/ -v
186
+
187
+ - name: Collect diagnostics
188
+ if: failure() || cancelled()
189
+ run: |
190
+ mkdir -p diag
191
+ kubectl get events -A --sort-by=.lastTimestamp > diag/events.txt || true
192
+ kubectl get pods -A -o wide > diag/pods.txt || true
193
+ kubectl describe pods -A > diag/pods-describe.txt || true
194
+ kubectl get nodes -o wide > diag/nodes.txt || true
195
+ kubectl get ns -l app.kubernetes.io/managed-by=korvid-contract -o yaml > diag/namespaces.yaml || true
196
+
197
+ - name: Upload diagnostics
198
+ if: failure() || cancelled()
199
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
200
+ with:
201
+ name: contract-diagnostics
202
+ path: diag/
203
+ retention-days: 14
204
+
205
+ - name: Delete kubeconfig
206
+ if: always()
207
+ run: |
208
+ if [ -n "${KUBECONFIG:-}" ] && [ -f "$KUBECONFIG" ]; then
209
+ rm -f "$KUBECONFIG"
210
+ fi
211
+
212
+ # Separate job so the stop runs even when the contract job hits its
213
+ # timeout-minutes (GitHub kills the job before later always() steps,
214
+ # which would leave the stopped-at-rest cluster running and billing).
215
+ stop-cluster:
216
+ needs: contract
217
+ if: always()
218
+ runs-on: ubuntu-latest
219
+ environment: aks-contract-test
220
+ timeout-minutes: 20
221
+ permissions:
222
+ contents: read
223
+ id-token: write
224
+ env:
225
+ AKS_RESOURCE_GROUP: ${{ vars.AKS_RESOURCE_GROUP }}
226
+ AKS_CLUSTER_NAME: ${{ vars.AKS_CLUSTER_NAME }}
227
+ steps:
228
+ # The settle helper lives in the repo, so this job needs the tree too.
229
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
230
+ with:
231
+ persist-credentials: false
232
+
233
+ - name: Guard — refuse any target other than the documented test cluster
234
+ run: |
235
+ if [ "$AKS_RESOURCE_GROUP" != "$EXPECTED_RESOURCE_GROUP" ] || \
236
+ [ "$AKS_CLUSTER_NAME" != "$EXPECTED_CLUSTER_NAME" ]; then
237
+ echo "::error title=Cluster identity mismatch::environment variables point at $AKS_RESOURCE_GROUP/$AKS_CLUSTER_NAME, expected $EXPECTED_RESOURCE_GROUP/$EXPECTED_CLUSTER_NAME. Refusing to stop an unexpected cluster."
238
+ exit 1
239
+ fi
240
+
241
+ - uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
242
+ with:
243
+ client-id: ${{ vars.AZURE_CLIENT_ID }}
244
+ tenant-id: ${{ vars.AZURE_TENANT_ID }}
245
+ subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
246
+
247
+ - name: Stop AKS cluster
248
+ run: |
249
+ # A test may have left an operation in flight (a nodepool scale, a
250
+ # failed run mid-write) and Azure would reject the stop outright.
251
+ # Advisory only: it must never prevent the stop, because a step that
252
+ # dies here leaves the cluster running with no recovery hint - so a
253
+ # failed or timed-out wait falls through and lets the stop try
254
+ # anyway, and its own error is the report.
255
+ WAIT_AKS_TIMEOUT=300 WAIT_AKS_MAX_QUERY_FAILURES=3 \
256
+ .github/scripts/wait-aks-settled.sh "$AKS_RESOURCE_GROUP" "$AKS_CLUSTER_NAME" \
257
+ || echo "cluster did not settle before the stop; attempting it anyway"
258
+ # Azure rejects a stop on a cluster that is not running, which is the
259
+ # normal state when the contract job failed before starting it.
260
+ state=$(az aks show -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" --query powerState.code -o tsv 2>/dev/null || echo unknown)
261
+ if [ "$state" = "Stopped" ]; then
262
+ echo "$AKS_CLUSTER_NAME is already stopped"
263
+ elif ! az aks stop -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" --no-wait; then
264
+ # The state read above can be `unknown` when the query itself
265
+ # failed, so a rejection here may just mean it was already stopped.
266
+ # Check before crying wolf: a red cleanup job for a cluster that is
267
+ # costing nothing trains people to ignore this error.
268
+ recheck=$(az aks show -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" --query powerState.code -o tsv 2>/dev/null || echo unknown)
269
+ if [ "$recheck" = "Stopped" ]; then
270
+ echo "$AKS_CLUSTER_NAME was already stopped"
271
+ else
272
+ echo "::error title=AKS stop rejected::aks-korvid-contract-test would not accept a stop and reads $recheck; idle compute may be accruing. Stop it manually once its current operation finishes: az aks stop -g $AKS_RESOURCE_GROUP -n $AKS_CLUSTER_NAME"
273
+ exit 1
274
+ fi
275
+ fi
276
+ # powerState flips to Stopped as soon as Azure accepts the request,
277
+ # while provisioningState stays Stopping until the operation really
278
+ # finishes. Waiting on powerState alone let this job exit seconds
279
+ # after asking, so the next merge's run hit "there's an in-progress
280
+ # stop managed cluster operation" on its own start. This one is a
281
+ # barrier, so it retries query failures through the whole budget.
282
+ if ! WAIT_AKS_TIMEOUT=600 .github/scripts/wait-aks-settled.sh \
283
+ "$AKS_RESOURCE_GROUP" "$AKS_CLUSTER_NAME" Stopped; then
284
+ echo "::error title=AKS stop failed::aks-korvid-contract-test did not settle at Stopped; idle compute may be accruing. Stop it manually: az aks stop -g $AKS_RESOURCE_GROUP -n $AKS_CLUSTER_NAME"
285
+ exit 1
286
+ fi