runsight 0.1.0__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 (230) hide show
  1. runsight-0.1.0/.gitignore +2 -0
  2. runsight-0.1.0/PKG-INFO +20 -0
  3. runsight-0.1.0/pyproject.toml +53 -0
  4. runsight-0.1.0/src/__init__.py +0 -0
  5. runsight-0.1.0/src/runsight_api/__init__.py +0 -0
  6. runsight-0.1.0/src/runsight_api/alembic/env.py +65 -0
  7. runsight-0.1.0/src/runsight_api/alembic/script.py.mako +27 -0
  8. runsight-0.1.0/src/runsight_api/alembic/versions/001_initial_schema.py +29 -0
  9. runsight-0.1.0/src/runsight_api/alembic.ini +36 -0
  10. runsight-0.1.0/src/runsight_api/cli.py +39 -0
  11. runsight-0.1.0/src/runsight_api/core/__init__.py +0 -0
  12. runsight-0.1.0/src/runsight_api/core/config.py +76 -0
  13. runsight-0.1.0/src/runsight_api/core/context.py +47 -0
  14. runsight-0.1.0/src/runsight_api/core/di.py +25 -0
  15. runsight-0.1.0/src/runsight_api/core/logging.py +62 -0
  16. runsight-0.1.0/src/runsight_api/core/project.py +167 -0
  17. runsight-0.1.0/src/runsight_api/core/secrets.py +102 -0
  18. runsight-0.1.0/src/runsight_api/data/__init__.py +0 -0
  19. runsight-0.1.0/src/runsight_api/data/filesystem/__init__.py +13 -0
  20. runsight-0.1.0/src/runsight_api/data/filesystem/_base_yaml_repo.py +118 -0
  21. runsight-0.1.0/src/runsight_api/data/filesystem/_utils.py +26 -0
  22. runsight-0.1.0/src/runsight_api/data/filesystem/provider_repo.py +180 -0
  23. runsight-0.1.0/src/runsight_api/data/filesystem/settings_repo.py +149 -0
  24. runsight-0.1.0/src/runsight_api/data/filesystem/soul_repo.py +17 -0
  25. runsight-0.1.0/src/runsight_api/data/filesystem/step_repo.py +10 -0
  26. runsight-0.1.0/src/runsight_api/data/filesystem/task_repo.py +10 -0
  27. runsight-0.1.0/src/runsight_api/data/filesystem/workflow_repo.py +617 -0
  28. runsight-0.1.0/src/runsight_api/data/repositories/__init__.py +3 -0
  29. runsight-0.1.0/src/runsight_api/data/repositories/run_repo.py +314 -0
  30. runsight-0.1.0/src/runsight_api/domain/__init__.py +0 -0
  31. runsight-0.1.0/src/runsight_api/domain/entities/__init__.py +13 -0
  32. runsight-0.1.0/src/runsight_api/domain/entities/log.py +14 -0
  33. runsight-0.1.0/src/runsight_api/domain/entities/run.py +129 -0
  34. runsight-0.1.0/src/runsight_api/domain/entities/settings.py +21 -0
  35. runsight-0.1.0/src/runsight_api/domain/errors.py +166 -0
  36. runsight-0.1.0/src/runsight_api/domain/events.py +64 -0
  37. runsight-0.1.0/src/runsight_api/domain/value_objects.py +78 -0
  38. runsight-0.1.0/src/runsight_api/logic/__init__.py +0 -0
  39. runsight-0.1.0/src/runsight_api/logic/observers/__init__.py +0 -0
  40. runsight-0.1.0/src/runsight_api/logic/observers/artifact_cleanup_observer.py +92 -0
  41. runsight-0.1.0/src/runsight_api/logic/observers/eval_observer.py +332 -0
  42. runsight-0.1.0/src/runsight_api/logic/observers/execution_observer.py +472 -0
  43. runsight-0.1.0/src/runsight_api/logic/observers/streaming_observer.py +161 -0
  44. runsight-0.1.0/src/runsight_api/logic/services/__init__.py +0 -0
  45. runsight-0.1.0/src/runsight_api/logic/services/eval_service.py +385 -0
  46. runsight-0.1.0/src/runsight_api/logic/services/execution_service.py +527 -0
  47. runsight-0.1.0/src/runsight_api/logic/services/git_service.py +133 -0
  48. runsight-0.1.0/src/runsight_api/logic/services/model_service.py +77 -0
  49. runsight-0.1.0/src/runsight_api/logic/services/provider_service.py +278 -0
  50. runsight-0.1.0/src/runsight_api/logic/services/registry_service.py +52 -0
  51. runsight-0.1.0/src/runsight_api/logic/services/run_service.py +125 -0
  52. runsight-0.1.0/src/runsight_api/logic/services/settings_service.py +165 -0
  53. runsight-0.1.0/src/runsight_api/logic/services/soul_service.py +235 -0
  54. runsight-0.1.0/src/runsight_api/logic/services/workflow_service.py +176 -0
  55. runsight-0.1.0/src/runsight_api/main.py +212 -0
  56. runsight-0.1.0/src/runsight_api/transport/__init__.py +0 -0
  57. runsight-0.1.0/src/runsight_api/transport/deps.py +141 -0
  58. runsight-0.1.0/src/runsight_api/transport/middleware/__init__.py +0 -0
  59. runsight-0.1.0/src/runsight_api/transport/middleware/access_log.py +28 -0
  60. runsight-0.1.0/src/runsight_api/transport/middleware/error_handler.py +29 -0
  61. runsight-0.1.0/src/runsight_api/transport/middleware/request_id.py +20 -0
  62. runsight-0.1.0/src/runsight_api/transport/routers/__init__.py +0 -0
  63. runsight-0.1.0/src/runsight_api/transport/routers/dashboard.py +117 -0
  64. runsight-0.1.0/src/runsight_api/transport/routers/eval.py +21 -0
  65. runsight-0.1.0/src/runsight_api/transport/routers/git.py +299 -0
  66. runsight-0.1.0/src/runsight_api/transport/routers/models.py +88 -0
  67. runsight-0.1.0/src/runsight_api/transport/routers/runs.py +374 -0
  68. runsight-0.1.0/src/runsight_api/transport/routers/settings.py +261 -0
  69. runsight-0.1.0/src/runsight_api/transport/routers/souls.py +71 -0
  70. runsight-0.1.0/src/runsight_api/transport/routers/sse_stream.py +61 -0
  71. runsight-0.1.0/src/runsight_api/transport/routers/steps.py +126 -0
  72. runsight-0.1.0/src/runsight_api/transport/routers/tasks.py +126 -0
  73. runsight-0.1.0/src/runsight_api/transport/routers/tools.py +53 -0
  74. runsight-0.1.0/src/runsight_api/transport/routers/workflows.py +125 -0
  75. runsight-0.1.0/src/runsight_api/transport/schemas/__init__.py +0 -0
  76. runsight-0.1.0/src/runsight_api/transport/schemas/dashboard.py +28 -0
  77. runsight-0.1.0/src/runsight_api/transport/schemas/eval.py +43 -0
  78. runsight-0.1.0/src/runsight_api/transport/schemas/runs.py +89 -0
  79. runsight-0.1.0/src/runsight_api/transport/schemas/settings.py +22 -0
  80. runsight-0.1.0/src/runsight_api/transport/schemas/souls.py +64 -0
  81. runsight-0.1.0/src/runsight_api/transport/schemas/steps.py +29 -0
  82. runsight-0.1.0/src/runsight_api/transport/schemas/tasks.py +29 -0
  83. runsight-0.1.0/src/runsight_api/transport/schemas/tools.py +11 -0
  84. runsight-0.1.0/src/runsight_api/transport/schemas/workflows.py +92 -0
  85. runsight-0.1.0/tests/__init__.py +0 -0
  86. runsight-0.1.0/tests/conftest.py +20 -0
  87. runsight-0.1.0/tests/core/__init__.py +0 -0
  88. runsight-0.1.0/tests/core/test_config.py +23 -0
  89. runsight-0.1.0/tests/core/test_project.py +132 -0
  90. runsight-0.1.0/tests/core/test_run373_git_init.py +180 -0
  91. runsight-0.1.0/tests/core/test_scaffold_project.py +193 -0
  92. runsight-0.1.0/tests/data/test_base_yaml_repository.py +237 -0
  93. runsight-0.1.0/tests/data/test_baseline_query.py +308 -0
  94. runsight-0.1.0/tests/data/test_filesystem.py +258 -0
  95. runsight-0.1.0/tests/data/test_path_traversal.py +324 -0
  96. runsight-0.1.0/tests/data/test_repositories.py +30 -0
  97. runsight-0.1.0/tests/data/test_run478_run_repo_workflow_health_metrics.py +110 -0
  98. runsight-0.1.0/tests/data/test_run479_run_repo_list_metrics.py +207 -0
  99. runsight-0.1.0/tests/data/test_run480_run_repo_delete_cascade.py +239 -0
  100. runsight-0.1.0/tests/data/test_run603_workflow_interface_validation.py +215 -0
  101. runsight-0.1.0/tests/data/test_run606_snapshot_validation.py +709 -0
  102. runsight-0.1.0/tests/data/test_soul_repo.py +99 -0
  103. runsight-0.1.0/tests/data/test_step_repo.py +33 -0
  104. runsight-0.1.0/tests/data/test_task_repo.py +33 -0
  105. runsight-0.1.0/tests/domain/test_entities.py +20 -0
  106. runsight-0.1.0/tests/domain/test_entity_extra_config.py +111 -0
  107. runsight-0.1.0/tests/domain/test_node_status_enum.py +267 -0
  108. runsight-0.1.0/tests/domain/test_run127_bug_fixes.py +151 -0
  109. runsight-0.1.0/tests/domain/test_run127_create_run_workflow_name.py +52 -0
  110. runsight-0.1.0/tests/domain/test_run329_workflow_commit_sha.py +319 -0
  111. runsight-0.1.0/tests/domain/test_run334_state_transition_guards.py +471 -0
  112. runsight-0.1.0/tests/domain/test_run379_data_model.py +337 -0
  113. runsight-0.1.0/tests/domain/test_run437_soul_schema_alignment.py +94 -0
  114. runsight-0.1.0/tests/domain/test_run472_transport_soul_schema.py +82 -0
  115. runsight-0.1.0/tests/domain/test_run556_workflow_entity_enabled.py +69 -0
  116. runsight-0.1.0/tests/domain/test_run663_entity_fields.py +105 -0
  117. runsight-0.1.0/tests/domain/test_run689_soul_assertions_shim.py +38 -0
  118. runsight-0.1.0/tests/domain/test_run_node_eval_fields.py +225 -0
  119. runsight-0.1.0/tests/domain/test_sse_event_constants.py +301 -0
  120. runsight-0.1.0/tests/logic/test_artifact_cleanup_fire_and_forget.py +69 -0
  121. runsight-0.1.0/tests/logic/test_artifact_cleanup_observer.py +301 -0
  122. runsight-0.1.0/tests/logic/test_artifact_store_injection.py +138 -0
  123. runsight-0.1.0/tests/logic/test_budget_run_status.py +518 -0
  124. runsight-0.1.0/tests/logic/test_disabled_provider_policy.py +155 -0
  125. runsight-0.1.0/tests/logic/test_eval_observer.py +911 -0
  126. runsight-0.1.0/tests/logic/test_execution_observer.py +706 -0
  127. runsight-0.1.0/tests/logic/test_execution_observer_soul.py +225 -0
  128. runsight-0.1.0/tests/logic/test_execution_service.py +865 -0
  129. runsight-0.1.0/tests/logic/test_execution_service_concurrency.py +616 -0
  130. runsight-0.1.0/tests/logic/test_execution_service_main_branch_loading.py +105 -0
  131. runsight-0.1.0/tests/logic/test_google_api_key_header.py +1 -0
  132. runsight-0.1.0/tests/logic/test_provider_pulse_release_digest_eval_configs.py +46 -0
  133. runsight-0.1.0/tests/logic/test_provider_service.py +447 -0
  134. runsight-0.1.0/tests/logic/test_registry_service.py +132 -0
  135. runsight-0.1.0/tests/logic/test_run135_sim_branches.py +446 -0
  136. runsight-0.1.0/tests/logic/test_run141_execution_service_api_keys.py +153 -0
  137. runsight-0.1.0/tests/logic/test_run180_observer_block_result.py +345 -0
  138. runsight-0.1.0/tests/logic/test_run200_state_flow.py +270 -0
  139. runsight-0.1.0/tests/logic/test_run207_streaming_observer.py +563 -0
  140. runsight-0.1.0/tests/logic/test_run208_double_observer.py +446 -0
  141. runsight-0.1.0/tests/logic/test_run209_cancel_wiring.py +280 -0
  142. runsight-0.1.0/tests/logic/test_run347_wire_assertion_configs.py +304 -0
  143. runsight-0.1.0/tests/logic/test_run374_git_service.py +371 -0
  144. runsight-0.1.0/tests/logic/test_run376_save_commit.py +446 -0
  145. runsight-0.1.0/tests/logic/test_run380_branch_aware_exec.py +477 -0
  146. runsight-0.1.0/tests/logic/test_run404_workflow_id_filter.py +135 -0
  147. runsight-0.1.0/tests/logic/test_run408_async_httpx.py +588 -0
  148. runsight-0.1.0/tests/logic/test_run470_soul_update_preservation.py +104 -0
  149. runsight-0.1.0/tests/logic/test_run474_soul_service_workflow_repo_ownership.py +102 -0
  150. runsight-0.1.0/tests/logic/test_run478_workflow_health_metrics.py +174 -0
  151. runsight-0.1.0/tests/logic/test_run558_regression_logic.py +569 -0
  152. runsight-0.1.0/tests/logic/test_run573_library_soul_usage_scanning.py +693 -0
  153. runsight-0.1.0/tests/logic/test_run606_execution_service_snapshot_resolution.py +578 -0
  154. runsight-0.1.0/tests/logic/test_run607_nested_run_lifecycle.py +549 -0
  155. runsight-0.1.0/tests/logic/test_run699_eval_observer_transform.py +350 -0
  156. runsight-0.1.0/tests/logic/test_run_service.py +319 -0
  157. runsight-0.1.0/tests/logic/test_single_writer_and_session.py +129 -0
  158. runsight-0.1.0/tests/logic/test_soul_service.py +564 -0
  159. runsight-0.1.0/tests/logic/test_ssrf_provider.py +416 -0
  160. runsight-0.1.0/tests/logic/test_workflow_service.py +641 -0
  161. runsight-0.1.0/tests/test_blank_canvas_yaml_bug.py +202 -0
  162. runsight-0.1.0/tests/test_cors_config.py +137 -0
  163. runsight-0.1.0/tests/test_execution_log_persistence.py +409 -0
  164. runsight-0.1.0/tests/test_execution_service_return_type.py +49 -0
  165. runsight-0.1.0/tests/test_main.py +11 -0
  166. runsight-0.1.0/tests/test_request_id_access_log.py +233 -0
  167. runsight-0.1.0/tests/test_run134_openapi_codegen.py +240 -0
  168. runsight-0.1.0/tests/test_run203_placeholder_migration_cleanup.py +294 -0
  169. runsight-0.1.0/tests/test_run378_source_branch_filtering.py +643 -0
  170. runsight-0.1.0/tests/test_run516_ci_workspace_layout.py +217 -0
  171. runsight-0.1.0/tests/test_run517_codebones_index_regression.py +151 -0
  172. runsight-0.1.0/tests/test_run519_tracked_artifact_cleanup.py +98 -0
  173. runsight-0.1.0/tests/test_run520_tools_canonical_home.py +65 -0
  174. runsight-0.1.0/tests/test_run691_stale_soul_assertion_refs.py +112 -0
  175. runsight-0.1.0/tests/test_run705_assertions_fire_e2e.py +716 -0
  176. runsight-0.1.0/tests/test_run706_api_e2e.py +539 -0
  177. runsight-0.1.0/tests/test_run707_sse_streaming_e2e.py +902 -0
  178. runsight-0.1.0/tests/test_run739_source_audit.py +286 -0
  179. runsight-0.1.0/tests/test_run740_source_audit.py +225 -0
  180. runsight-0.1.0/tests/test_sql_pagination.py +132 -0
  181. runsight-0.1.0/tests/test_stale_run_recovery.py +233 -0
  182. runsight-0.1.0/tests/test_structlog_foundation.py +319 -0
  183. runsight-0.1.0/tests/test_structured_errors.py +385 -0
  184. runsight-0.1.0/tests/transport/test_dashboard_router.py +25 -0
  185. runsight-0.1.0/tests/transport/test_disabled_provider_souls_router.py +112 -0
  186. runsight-0.1.0/tests/transport/test_error_sanitization.py +173 -0
  187. runsight-0.1.0/tests/transport/test_eval_endpoints.py +615 -0
  188. runsight-0.1.0/tests/transport/test_git_router.py +272 -0
  189. runsight-0.1.0/tests/transport/test_git_security.py +442 -0
  190. runsight-0.1.0/tests/transport/test_model_catalog_api.py +598 -0
  191. runsight-0.1.0/tests/transport/test_run127_router_wiring.py +98 -0
  192. runsight-0.1.0/tests/transport/test_run144_polling_and_node_summary.py +355 -0
  193. runsight-0.1.0/tests/transport/test_run331_error_normalization.py +398 -0
  194. runsight-0.1.0/tests/transport/test_run338_dashboard_kpis.py +310 -0
  195. runsight-0.1.0/tests/transport/test_run341_attention.py +557 -0
  196. runsight-0.1.0/tests/transport/test_run342_eval_kpis.py +499 -0
  197. runsight-0.1.0/tests/transport/test_run363_workflow_filter.py +230 -0
  198. runsight-0.1.0/tests/transport/test_run413_response_fields.py +136 -0
  199. runsight-0.1.0/tests/transport/test_run423_sim_branch_endpoint.py +237 -0
  200. runsight-0.1.0/tests/transport/test_run478_workflow_health_router.py +245 -0
  201. runsight-0.1.0/tests/transport/test_run479_runs_list_contract.py +122 -0
  202. runsight-0.1.0/tests/transport/test_run537_canonical_runs_route.py +137 -0
  203. runsight-0.1.0/tests/transport/test_run555_node_response_enrichment.py +403 -0
  204. runsight-0.1.0/tests/transport/test_run556_patch_workflow_enabled.py +426 -0
  205. runsight-0.1.0/tests/transport/test_run557_git_file_read.py +429 -0
  206. runsight-0.1.0/tests/transport/test_run558_regression_endpoints.py +442 -0
  207. runsight-0.1.0/tests/transport/test_run608_sse_child_lifecycle.py +324 -0
  208. runsight-0.1.0/tests/transport/test_run612_child_run_queries.py +386 -0
  209. runsight-0.1.0/tests/transport/test_runs_router.py +377 -0
  210. runsight-0.1.0/tests/transport/test_settings_router.py +316 -0
  211. runsight-0.1.0/tests/transport/test_souls_router.py +241 -0
  212. runsight-0.1.0/tests/transport/test_sse_stream.py +531 -0
  213. runsight-0.1.0/tests/transport/test_steps_router.py +94 -0
  214. runsight-0.1.0/tests/transport/test_tasks_router.py +94 -0
  215. runsight-0.1.0/tests/transport/test_tools_router.py +173 -0
  216. runsight-0.1.0/tests/transport/test_workflows_router.py +485 -0
  217. runsight-0.1.0/tests/unit/__init__.py +0 -0
  218. runsight-0.1.0/tests/unit/cleanup/__init__.py +0 -0
  219. runsight-0.1.0/tests/unit/cleanup/test_decrypt_stub_cleanup.py +66 -0
  220. runsight-0.1.0/tests/unit/cleanup/test_remove_sqlite_encryption.py +338 -0
  221. runsight-0.1.0/tests/unit/core/__init__.py +0 -0
  222. runsight-0.1.0/tests/unit/core/test_secrets.py +428 -0
  223. runsight-0.1.0/tests/unit/data/__init__.py +0 -0
  224. runsight-0.1.0/tests/unit/data/filesystem/__init__.py +0 -0
  225. runsight-0.1.0/tests/unit/data/filesystem/test_provider_repo.py +518 -0
  226. runsight-0.1.0/tests/unit/data/filesystem/test_run490_workflow_repo_tool_governance.py +185 -0
  227. runsight-0.1.0/tests/unit/data/filesystem/test_settings_repo.py +181 -0
  228. runsight-0.1.0/tests/unit/logic/__init__.py +0 -0
  229. runsight-0.1.0/tests/unit/logic/test_provider_service_wiring.py +566 -0
  230. runsight-0.1.0/tests/unit/logic/test_settings_service.py +452 -0
@@ -0,0 +1,2 @@
1
+ dist/
2
+ src/runsight_api/static/
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: runsight
3
+ Version: 0.1.0
4
+ Summary: YAML-first workflow engine for AI agents
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: alembic>=1.14.0
7
+ Requires-Dist: fastapi>=0.115.0
8
+ Requires-Dist: httpx>=0.28.0
9
+ Requires-Dist: pydantic-settings>=2.0
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: ruamel-yaml>=0.18.0
12
+ Requires-Dist: runsight-core
13
+ Requires-Dist: sqlmodel>=0.0.24
14
+ Requires-Dist: structlog>=24.0.0
15
+ Requires-Dist: uvicorn[standard]>=0.34.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: httpx>=0.28.0; extra == 'dev'
18
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
19
+ Requires-Dist: pytest>=8.0; extra == 'dev'
20
+ Requires-Dist: ruff>=0.8.0; extra == 'dev'
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "runsight"
3
+ version = "0.1.0"
4
+ description = "YAML-first workflow engine for AI agents"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "fastapi>=0.115.0",
8
+ "uvicorn[standard]>=0.34.0",
9
+ "sqlmodel>=0.0.24",
10
+ "alembic>=1.14.0",
11
+ "pyyaml>=6.0",
12
+ "ruamel.yaml>=0.18.0",
13
+ "runsight-core",
14
+ "pydantic-settings>=2.0",
15
+ "httpx>=0.28.0",
16
+ "structlog>=24.0.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ runsight = "runsight_api.cli:main"
21
+
22
+ [project.optional-dependencies]
23
+ dev = [
24
+ "pytest>=8.0",
25
+ "pytest-asyncio>=0.24.0",
26
+ "httpx>=0.28.0",
27
+ "ruff>=0.8.0",
28
+ ]
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/runsight_api"]
36
+ artifacts = ["src/runsight_api/static/**"]
37
+ exclude = ["**/*.db"]
38
+
39
+ [tool.ruff]
40
+ line-length = 100
41
+ target-version = "py311"
42
+
43
+ [tool.ruff.lint.per-file-ignores]
44
+ "tests/test_run134_openapi_codegen.py" = ["F401"]
45
+
46
+ [tool.ruff.format]
47
+ quote-style = "double"
48
+ exclude = ["tests/test_run134_openapi_codegen.py"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+ pythonpath = ["src"]
53
+ asyncio_mode = "auto"
File without changes
File without changes
@@ -0,0 +1,65 @@
1
+ from logging.config import fileConfig
2
+
3
+ from alembic import context
4
+ from sqlalchemy import engine_from_config, pool
5
+ from sqlmodel import SQLModel
6
+
7
+ from runsight_api.domain.entities.log import LogEntry # noqa: F401
8
+
9
+ # Import all models so SQLModel.metadata is populated
10
+ from runsight_api.domain.entities.run import Run, RunNode # noqa: F401
11
+
12
+ config = context.config
13
+ if config.config_file_name is not None:
14
+ fileConfig(config.config_file_name)
15
+
16
+ target_metadata = SQLModel.metadata
17
+
18
+ # Naming convention for constraints (helps with batch mode migrations)
19
+ NAMING_CONVENTION = {
20
+ "ix": "ix_%(column_0_label)s",
21
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
22
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
23
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
24
+ "pk": "pk_%(table_name)s",
25
+ }
26
+
27
+
28
+ def run_migrations_offline() -> None:
29
+ """Run migrations in 'offline' mode."""
30
+ url = config.get_main_option("sqlalchemy.url")
31
+ context.configure(
32
+ url=url,
33
+ target_metadata=target_metadata,
34
+ literal_binds=True,
35
+ dialect_opts={"paramstyle": "named"},
36
+ render_as_batch=True,
37
+ )
38
+
39
+ with context.begin_transaction():
40
+ context.run_migrations()
41
+
42
+
43
+ def run_migrations_online() -> None:
44
+ """Run migrations in 'online' mode."""
45
+ connectable = engine_from_config(
46
+ config.get_section(config.config_ini_section, {}),
47
+ prefix="sqlalchemy.",
48
+ poolclass=pool.NullPool,
49
+ )
50
+
51
+ with connectable.connect() as connection:
52
+ context.configure(
53
+ connection=connection,
54
+ target_metadata=target_metadata,
55
+ render_as_batch=True,
56
+ )
57
+
58
+ with context.begin_transaction():
59
+ context.run_migrations()
60
+
61
+
62
+ if context.is_offline_mode():
63
+ run_migrations_offline()
64
+ else:
65
+ run_migrations_online()
@@ -0,0 +1,27 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ import sqlmodel
13
+ ${imports if imports else ""}
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = ${repr(up_revision)}
17
+ down_revision: Union[str, None] = ${repr(down_revision)}
18
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
19
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
20
+
21
+
22
+ def upgrade() -> None:
23
+ ${upgrades if upgrades else "pass"}
24
+
25
+
26
+ def downgrade() -> None:
27
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,29 @@
1
+ """Initial schema — baseline migration.
2
+
3
+ Revision ID: 001_initial
4
+ Revises: None
5
+ Create Date: 2026-03-22
6
+ """
7
+
8
+ from typing import Sequence, Union
9
+
10
+ import sqlmodel
11
+ from alembic import op
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision: str = "001_initial"
15
+ down_revision: Union[str, None] = None
16
+ branch_labels: Union[str, Sequence[str], None] = None
17
+ depends_on: Union[str, Sequence[str], None] = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ """Create all tables from SQLModel metadata."""
22
+ bind = op.get_bind()
23
+ sqlmodel.SQLModel.metadata.create_all(bind)
24
+
25
+
26
+ def downgrade() -> None:
27
+ """Drop all tables from SQLModel metadata."""
28
+ bind = op.get_bind()
29
+ sqlmodel.SQLModel.metadata.drop_all(bind)
@@ -0,0 +1,36 @@
1
+ [alembic]
2
+ script_location = alembic
3
+ sqlalchemy.url = sqlite:///./runsight.db
4
+
5
+ [loggers]
6
+ keys = root,sqlalchemy,alembic
7
+
8
+ [handlers]
9
+ keys = console
10
+
11
+ [formatters]
12
+ keys = generic
13
+
14
+ [logger_root]
15
+ level = WARN
16
+ handlers = console
17
+
18
+ [logger_sqlalchemy]
19
+ level = WARN
20
+ handlers =
21
+ qualname = sqlalchemy.engine
22
+
23
+ [logger_alembic]
24
+ level = INFO
25
+ handlers =
26
+ qualname = alembic
27
+
28
+ [handler_console]
29
+ class = StreamHandler
30
+ args = (sys.stderr,)
31
+ level = NOTSET
32
+ formatter = generic
33
+
34
+ [formatter_generic]
35
+ format = %(levelname)-5.5s [%(name)s] %(message)s
36
+ datefmt = %H:%M:%S
@@ -0,0 +1,39 @@
1
+ """CLI entry point for `uvx runsight` / `runsight` command."""
2
+
3
+ import sys
4
+
5
+ import uvicorn
6
+
7
+
8
+ def main() -> None:
9
+ # Parse --host and --port from argv (keep it minimal)
10
+ host = "0.0.0.0"
11
+ port = 8000
12
+
13
+ args = sys.argv[1:]
14
+ i = 0
15
+ while i < len(args):
16
+ if args[i] == "--port" and i + 1 < len(args):
17
+ port = int(args[i + 1])
18
+ i += 2
19
+ elif args[i] == "--host" and i + 1 < len(args):
20
+ host = args[i + 1]
21
+ i += 2
22
+ elif args[i] in ("--help", "-h"):
23
+ print("Usage: runsight [--host HOST] [--port PORT]")
24
+ print()
25
+ print("Start the Runsight server.")
26
+ print()
27
+ print("Options:")
28
+ print(" --host HOST Bind address (default: 0.0.0.0)")
29
+ print(" --port PORT Bind port (default: 8000)")
30
+ sys.exit(0)
31
+ else:
32
+ print(f"Unknown argument: {args[i]}", file=sys.stderr)
33
+ sys.exit(1)
34
+
35
+ print(f" Runsight running at http://localhost:{port}")
36
+ print(" Press Ctrl+C to stop")
37
+ print()
38
+
39
+ uvicorn.run("runsight_api.main:app", host=host, port=port)
File without changes
@@ -0,0 +1,76 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ from pydantic import Field, model_validator
6
+ from pydantic_settings import BaseSettings, SettingsConfigDict
7
+
8
+ from .project import resolve_base_path
9
+
10
+ _DB_URL_SENTINEL = "__auto__"
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def _default_base_path() -> str:
16
+ """Compute the default base_path using project detection.
17
+
18
+ If ``RUNSIGHT_BASE_PATH`` is set, pydantic-settings will use it directly
19
+ and this default is never called. Otherwise we run the marker / auto-detect
20
+ logic.
21
+ """
22
+ return resolve_base_path(env_value=None)
23
+
24
+
25
+ def _parse_cors_origins(raw: str) -> List[str]:
26
+ """Parse a comma-separated string into a list of origin URLs."""
27
+ return [origin.strip() for origin in raw.split(",")]
28
+
29
+
30
+ class Settings(BaseSettings):
31
+ base_path: str = Field(default_factory=_default_base_path)
32
+ db_url: str = _DB_URL_SENTINEL
33
+ debug: bool = False
34
+ host: str = "0.0.0.0"
35
+ port: int = 8000
36
+ cors_origins: str = "http://localhost:5173"
37
+ log_level: str = "INFO"
38
+ log_format: str = "json"
39
+
40
+ model_config = SettingsConfigDict(env_prefix="RUNSIGHT_")
41
+
42
+ @model_validator(mode="after")
43
+ def _resolve_db_url(self) -> "Settings":
44
+ if self.db_url == _DB_URL_SENTINEL:
45
+ db_path = Path(self.base_path).resolve() / ".runsight" / "runsight.db"
46
+ object.__setattr__(self, "db_url", f"sqlite:///{db_path}")
47
+ return self
48
+
49
+ @model_validator(mode="after")
50
+ def _split_cors_origins(self) -> "Settings":
51
+ raw = self.cors_origins
52
+ if isinstance(raw, str):
53
+ object.__setattr__(self, "cors_origins", _parse_cors_origins(raw))
54
+ return self
55
+
56
+
57
+ def ensure_project_dirs(settings: Settings) -> None:
58
+ """Ensure the custom/workflows/, .canvas/, and .runsight/ directories exist.
59
+
60
+ Called once at application startup. Resolves base_path to an absolute
61
+ path and logs the result. Creates missing directories as needed.
62
+ """
63
+ resolved = Path(settings.base_path).resolve()
64
+ logger.info("Runsight base_path resolved to: %s", resolved)
65
+
66
+ workflows_dir = resolved / "custom" / "workflows"
67
+ canvas_dir = workflows_dir / ".canvas"
68
+ runsight_dir = resolved / ".runsight"
69
+
70
+ for d in (workflows_dir, canvas_dir, runsight_dir):
71
+ if not d.exists():
72
+ d.mkdir(parents=True, exist_ok=True)
73
+ logger.info("Created missing directory: %s", d)
74
+
75
+
76
+ settings = Settings()
@@ -0,0 +1,47 @@
1
+ """ContextVars for request/execution tracing, bridged to structlog."""
2
+
3
+ import sys
4
+ from contextvars import ContextVar
5
+
6
+ import structlog
7
+
8
+ request_id: ContextVar[str] = ContextVar("request_id", default="")
9
+ run_id: ContextVar[str] = ContextVar("run_id", default="")
10
+ block_id: ContextVar[str] = ContextVar("block_id", default="")
11
+ workflow_name: ContextVar[str] = ContextVar("workflow_name", default="")
12
+
13
+ # Keep references that won't be shadowed by parameter names
14
+ _module = sys.modules[__name__]
15
+
16
+
17
+ def bind_request_context(rid: str) -> None:
18
+ """Set request_id in both ContextVar and structlog context."""
19
+ request_id.set(rid)
20
+ structlog.contextvars.bind_contextvars(request_id=rid)
21
+
22
+
23
+ def bind_execution_context(*, run_id: str, workflow_name: str) -> None:
24
+ """Set run_id and workflow_name in both ContextVar and structlog context."""
25
+ getattr(_module, "run_id").set(run_id)
26
+ getattr(_module, "workflow_name").set(workflow_name)
27
+ structlog.contextvars.bind_contextvars(run_id=run_id, workflow_name=workflow_name)
28
+
29
+
30
+ def bind_block_context(bid: str) -> None:
31
+ """Set block_id in both ContextVar and structlog context."""
32
+ block_id.set(bid)
33
+ structlog.contextvars.bind_contextvars(block_id=bid)
34
+
35
+
36
+ def clear_block_context() -> None:
37
+ """Reset block_id but leave execution context intact."""
38
+ block_id.set("")
39
+ structlog.contextvars.unbind_contextvars("block_id")
40
+
41
+
42
+ def clear_execution_context() -> None:
43
+ """Reset run_id, workflow_name, and block_id."""
44
+ run_id.set("")
45
+ workflow_name.set("")
46
+ block_id.set("")
47
+ structlog.contextvars.unbind_contextvars("run_id", "workflow_name", "block_id")
@@ -0,0 +1,25 @@
1
+ from sqlmodel import create_engine
2
+
3
+ from .config import settings
4
+
5
+ # SQLite :memory: needs check_same_thread=False for TestClient/async usage
6
+ connect_args = {}
7
+ if ":memory:" in settings.db_url:
8
+ connect_args["check_same_thread"] = False
9
+
10
+ engine = create_engine(
11
+ settings.db_url,
12
+ echo=settings.debug,
13
+ connect_args=connect_args if connect_args else {},
14
+ )
15
+
16
+
17
+ class Container:
18
+ def __init__(self):
19
+ self.engine = engine
20
+
21
+ def setup_app_state(self, app):
22
+ pass
23
+
24
+
25
+ container = Container()
@@ -0,0 +1,62 @@
1
+ """Structured logging configuration using structlog + stdlib bridge."""
2
+
3
+ import logging
4
+ import sys
5
+
6
+ import structlog
7
+
8
+
9
+ def configure_logging(log_level: str, log_format: str) -> None:
10
+ """Configure structlog with JSON or console rendering and stdlib bridge.
11
+
12
+ Args:
13
+ log_level: Python log level name (e.g. "INFO", "DEBUG").
14
+ log_format: "json" for machine-readable output, "text" for human-readable.
15
+ """
16
+ timestamper = structlog.processors.TimeStamper(fmt="iso")
17
+
18
+ shared_processors: list[structlog.types.Processor] = [
19
+ structlog.contextvars.merge_contextvars,
20
+ structlog.stdlib.add_log_level,
21
+ structlog.stdlib.add_logger_name,
22
+ timestamper,
23
+ structlog.processors.StackInfoRenderer(),
24
+ structlog.processors.format_exc_info,
25
+ ]
26
+
27
+ if log_format == "json":
28
+ renderer: structlog.types.Processor = structlog.processors.JSONRenderer()
29
+ else:
30
+ renderer = structlog.dev.ConsoleRenderer()
31
+
32
+ # Configure structlog to route through stdlib logging
33
+ structlog.configure(
34
+ processors=[
35
+ *shared_processors,
36
+ structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
37
+ ],
38
+ logger_factory=structlog.stdlib.LoggerFactory(),
39
+ wrapper_class=structlog.stdlib.BoundLogger,
40
+ cache_logger_on_first_use=False,
41
+ )
42
+
43
+ # Configure stdlib logging with structlog formatter
44
+ formatter = structlog.stdlib.ProcessorFormatter(
45
+ processors=[
46
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta,
47
+ renderer,
48
+ ],
49
+ foreign_pre_chain=shared_processors,
50
+ )
51
+
52
+ handler = logging.StreamHandler(sys.stderr)
53
+ handler.setFormatter(formatter)
54
+
55
+ root_logger = logging.getLogger()
56
+ root_logger.handlers.clear()
57
+ root_logger.addHandler(handler)
58
+ root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
59
+
60
+ # Suppress noisy loggers
61
+ logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
62
+ logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
@@ -0,0 +1,167 @@
1
+ """Project detection: resolve base_path from marker file or directory structure."""
2
+
3
+ import logging
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import yaml
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ MARKER_FILE = ".runsight-project"
13
+ MAX_WALK_DEPTH = 20
14
+
15
+
16
+ def _parse_marker(marker_path: Path) -> Optional[str]:
17
+ """Parse a .runsight-project YAML file and return the resolved base_path.
18
+
19
+ Returns None if the file is invalid or missing the base_path field.
20
+ """
21
+ try:
22
+ text = marker_path.read_text(encoding="utf-8")
23
+ data = yaml.safe_load(text)
24
+ if not isinstance(data, dict) or "base_path" not in data:
25
+ logger.warning("Marker %s missing 'base_path' field, skipping", marker_path)
26
+ return None
27
+ raw = data["base_path"]
28
+ resolved = (marker_path.parent / raw).resolve()
29
+ return str(resolved)
30
+ except Exception:
31
+ logger.warning("Failed to parse marker %s, skipping", marker_path, exc_info=True)
32
+ return None
33
+
34
+
35
+ def _find_marker(start: Path) -> Optional[str]:
36
+ """Walk up from *start* looking for a .runsight-project file.
37
+
38
+ Returns the resolved base_path string, or None.
39
+ """
40
+ current = start.resolve()
41
+ for _ in range(MAX_WALK_DEPTH):
42
+ candidate = current / MARKER_FILE
43
+ try:
44
+ found = candidate.is_file()
45
+ except OSError:
46
+ found = False
47
+ if found:
48
+ result = _parse_marker(candidate)
49
+ if result is not None:
50
+ logger.info("Found project marker at %s", candidate)
51
+ return result
52
+ parent = current.parent
53
+ if parent == current:
54
+ break
55
+ current = parent
56
+ return None
57
+
58
+
59
+ def _find_custom_workflows(start: Path) -> Optional[str]:
60
+ """Walk up from *start* looking for a custom/workflows/ directory.
61
+
62
+ Returns the parent of ``custom/`` as base_path, or None.
63
+ """
64
+ current = start.resolve()
65
+ for _ in range(MAX_WALK_DEPTH):
66
+ try:
67
+ found = (current / "custom" / "workflows").is_dir()
68
+ except OSError:
69
+ found = False
70
+ if found:
71
+ logger.info("Auto-detected custom/workflows/ at %s", current)
72
+ return str(current)
73
+ parent = current.parent
74
+ if parent == current:
75
+ break
76
+ current = parent
77
+ return None
78
+
79
+
80
+ def scaffold_project(base_path: Path) -> None:
81
+ """Create or verify the Runsight project structure at *base_path*.
82
+
83
+ Idempotent: existing files/dirs are never overwritten.
84
+ """
85
+ marker_path = base_path / MARKER_FILE
86
+ is_new = not marker_path.is_file()
87
+
88
+ # Marker file
89
+ if not marker_path.is_file():
90
+ marker_path.write_text(
91
+ yaml.dump({"version": 1, "base_path": "."}, default_flow_style=False),
92
+ encoding="utf-8",
93
+ )
94
+
95
+ # Directories
96
+ (base_path / "custom" / "workflows").mkdir(parents=True, exist_ok=True)
97
+ (base_path / "custom" / "souls").mkdir(parents=True, exist_ok=True)
98
+
99
+ # .gitignore
100
+ gitignore_path = base_path / ".gitignore"
101
+ if not gitignore_path.is_file():
102
+ gitignore_path.write_text(".canvas/\n.runsight/\n", encoding="utf-8")
103
+ else:
104
+ content = gitignore_path.read_text(encoding="utf-8")
105
+ if ".runsight/" not in content:
106
+ with gitignore_path.open("a", encoding="utf-8") as f:
107
+ if content and not content.endswith("\n"):
108
+ f.write("\n")
109
+ f.write(".runsight/\n")
110
+
111
+ # Git init if no repo exists
112
+ if not (base_path / ".git").is_dir():
113
+ subprocess.run(["git", "init"], cwd=base_path, capture_output=True, check=True)
114
+ subprocess.run(
115
+ ["git", "config", "user.email", "runsight@localhost"],
116
+ cwd=base_path,
117
+ capture_output=True,
118
+ check=True,
119
+ )
120
+ subprocess.run(
121
+ ["git", "config", "user.name", "Runsight"],
122
+ cwd=base_path,
123
+ capture_output=True,
124
+ check=True,
125
+ )
126
+ subprocess.run(["git", "add", "."], cwd=base_path, capture_output=True, check=True)
127
+ subprocess.run(
128
+ ["git", "commit", "-m", "Initial Runsight project"],
129
+ cwd=base_path,
130
+ capture_output=True,
131
+ check=True,
132
+ )
133
+
134
+ if is_new:
135
+ logger.info("Created new Runsight project at %s", base_path)
136
+ else:
137
+ logger.info("Found existing Runsight project at %s", base_path)
138
+
139
+
140
+ def resolve_base_path(env_value: Optional[str] = None) -> str:
141
+ """Resolve the project base_path using a 4-tier priority.
142
+
143
+ 1. ``RUNSIGHT_BASE_PATH`` env var (passed in as *env_value*)
144
+ 2. ``.runsight-project`` marker file (walk up from CWD)
145
+ 3. Auto-detect ``custom/workflows/`` directory in ancestors
146
+ 4. CWD as last resort
147
+ """
148
+ # Tier 1: explicit env var
149
+ if env_value is not None:
150
+ logger.info("base_path from RUNSIGHT_BASE_PATH env var: %s", env_value)
151
+ return env_value
152
+
153
+ cwd = Path.cwd()
154
+
155
+ # Tier 2: marker file
156
+ marker_result = _find_marker(cwd)
157
+ if marker_result is not None:
158
+ return marker_result
159
+
160
+ # Tier 3: auto-detect custom/workflows/
161
+ auto_result = _find_custom_workflows(cwd)
162
+ if auto_result is not None:
163
+ return auto_result
164
+
165
+ # Tier 4: CWD fallback
166
+ logger.info("No project marker or custom/workflows/ found, using CWD: %s", cwd)
167
+ return str(cwd)