runsight-core 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. {runsight_core-0.2.0 → runsight_core-0.2.2}/PKG-INFO +1 -1
  2. {runsight_core-0.2.0 → runsight_core-0.2.2}/pyproject.toml +1 -1
  3. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core.egg-info/PKG-INFO +1 -1
  4. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core.egg-info/SOURCES.txt +1 -0
  5. runsight_core-0.2.2/tests/test_run_821_docker_hardening.py +513 -0
  6. {runsight_core-0.2.0 → runsight_core-0.2.2}/README.md +0 -0
  7. {runsight_core-0.2.0 → runsight_core-0.2.2}/setup.cfg +0 -0
  8. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/__init__.py +0 -0
  9. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/artifacts.py +0 -0
  10. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/__init__.py +0 -0
  11. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/base.py +0 -0
  12. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/contract.py +0 -0
  13. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/custom.py +0 -0
  14. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/deterministic/__init__.py +0 -0
  15. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/deterministic/linguistic.py +0 -0
  16. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/deterministic/performance.py +0 -0
  17. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/deterministic/string.py +0 -0
  18. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/deterministic/structural.py +0 -0
  19. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/registry.py +0 -0
  20. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/assertions/scoring.py +0 -0
  21. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/__init__.py +0 -0
  22. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/_helpers.py +0 -0
  23. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/_registry.py +0 -0
  24. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/base.py +0 -0
  25. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/code.py +0 -0
  26. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/dispatch.py +0 -0
  27. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/gate.py +0 -0
  28. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/linear.py +0 -0
  29. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/loop.py +0 -0
  30. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/registry.py +0 -0
  31. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/synthesize.py +0 -0
  32. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/blocks/workflow_block.py +0 -0
  33. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/budget_enforcement.py +0 -0
  34. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/conditions/__init__.py +0 -0
  35. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/conditions/engine.py +0 -0
  36. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/eval/__init__.py +0 -0
  37. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/eval/runner.py +0 -0
  38. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/__init__.py +0 -0
  39. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/credentials.py +0 -0
  40. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/envelope.py +0 -0
  41. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/errors.py +0 -0
  42. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/handlers.py +0 -0
  43. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/harness.py +0 -0
  44. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/interceptors.py +0 -0
  45. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/ipc.py +0 -0
  46. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/ipc_models.py +0 -0
  47. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/pool.py +0 -0
  48. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/worker.py +0 -0
  49. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/worker_proxies.py +0 -0
  50. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/worker_support.py +0 -0
  51. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/isolation/wrapper.py +0 -0
  52. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/llm/__init__.py +0 -0
  53. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/llm/client.py +0 -0
  54. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/llm/model_catalog.py +0 -0
  55. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/memory/__init__.py +0 -0
  56. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/memory/budget.py +0 -0
  57. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/memory/token_counting.py +0 -0
  58. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/memory/windowing.py +0 -0
  59. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/observer.py +0 -0
  60. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/primitives.py +0 -0
  61. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/py.typed +0 -0
  62. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/runner.py +0 -0
  63. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/security.py +0 -0
  64. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/state.py +0 -0
  65. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/tools/__init__.py +0 -0
  66. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/tools/_catalog.py +0 -0
  67. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/tools/contract.py +0 -0
  68. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/tools/delegate.py +0 -0
  69. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/tools/file_io.py +0 -0
  70. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/tools/http.py +0 -0
  71. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/workflow.py +0 -0
  72. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/yaml/__init__.py +0 -0
  73. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/yaml/discovery/__init__.py +0 -0
  74. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/yaml/discovery/_assertion.py +0 -0
  75. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/yaml/discovery/_base.py +0 -0
  76. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/yaml/discovery/_soul.py +0 -0
  77. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/yaml/discovery/_tool.py +0 -0
  78. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/yaml/discovery/_workflow.py +0 -0
  79. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/yaml/parser.py +0 -0
  80. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/yaml/registry.py +0 -0
  81. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core/yaml/schema.py +0 -0
  82. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core.egg-info/dependency_links.txt +0 -0
  83. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core.egg-info/requires.txt +0 -0
  84. {runsight_core-0.2.0 → runsight_core-0.2.2}/src/runsight_core.egg-info/top_level.txt +0 -0
  85. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_achat_budget_enforcement.py +0 -0
  86. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_artifact_store_wiring.py +0 -0
  87. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_artifacts.py +0 -0
  88. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_base_block.py +0 -0
  89. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_baseblock_artifact_helpers.py +0 -0
  90. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_block_timeout_enforcement.py +0 -0
  91. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_blocks.py +0 -0
  92. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_budget_enforcement_types.py +0 -0
  93. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_budget_limits_schema.py +0 -0
  94. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_budget_migration_remaining.py +0 -0
  95. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_budget_models.py +0 -0
  96. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_budget_session.py +0 -0
  97. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_budget_wiring.py +0 -0
  98. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_carry_context_blockresult.py +0 -0
  99. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_code_block.py +0 -0
  100. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_codeblock_sandbox_hardening.py +0 -0
  101. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_composite_observer_isolation.py +0 -0
  102. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_condition_engine.py +0 -0
  103. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_conftest_isolation_mock.py +0 -0
  104. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_context_truncation.py +0 -0
  105. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_conversation_histories.py +0 -0
  106. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_cross_feature_integration.py +0 -0
  107. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_custom_asset_tool_contract.py +0 -0
  108. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_discovery.py +0 -0
  109. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_dispatch_block_stateful.py +0 -0
  110. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_dispatch_budget_isolation.py +0 -0
  111. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_dispatch_exit_def.py +0 -0
  112. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_dispatch_synthesize_integration.py +0 -0
  113. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_dispatch_v2.py +0 -0
  114. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_e2e_block_timeout.py +0 -0
  115. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_e2e_cost_cap.py +0 -0
  116. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_e2e_dispatch_budget.py +0 -0
  117. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_e2e_warn_and_flow_timeout.py +0 -0
  118. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_fit_to_budget_phase1.py +0 -0
  119. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_fit_to_budget_phase2.py +0 -0
  120. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_gate_error_subclass.py +0 -0
  121. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_gate_file_writer_blocks.py +0 -0
  122. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_integration_blocks_workflow.py +0 -0
  123. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_integration_merge_validation.py +0 -0
  124. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_integration_runner_primitives.py +0 -0
  125. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_integration_state_blocks.py +0 -0
  126. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_integration_workflow.py +0 -0
  127. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_integration_workflow_block.py +0 -0
  128. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_integration_workflow_block_backward_compat.py +0 -0
  129. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_integration_workflow_block_e2e.py +0 -0
  130. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_integration_workflow_block_parser.py +0 -0
  131. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_integration_workflow_block_with_other_blocks.py +0 -0
  132. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_iso_001_envelope_models.py +0 -0
  133. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_iso_002_ipc_protocol.py +0 -0
  134. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_iso_003_harness.py +0 -0
  135. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_iso_004_worker.py +0 -0
  136. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_iso_005_block_migration.py +0 -0
  137. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_iso_006_dispatch_delegate.py +0 -0
  138. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_iso_007_monitoring.py +0 -0
  139. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_iso_008_credentials.py +0 -0
  140. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_iso_e2e.py +0 -0
  141. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_linearblock_stateful.py +0 -0
  142. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_loop_block.py +0 -0
  143. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_loop_break_conditions.py +0 -0
  144. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_loop_carry_context.py +0 -0
  145. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_loop_exports_schema.py +0 -0
  146. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_loop_workflow_validation.py +0 -0
  147. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_loopblock_kwargs_forwarding.py +0 -0
  148. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_loopblock_stateful_integration.py +0 -0
  149. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_model_catalog.py +0 -0
  150. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_observer.py +0 -0
  151. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_observer_soul_extension.py +0 -0
  152. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_parser_inputs_outputs.py +0 -0
  153. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_parser_workflow_block.py +0 -0
  154. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_primitives.py +0 -0
  155. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_primitives_extended.py +0 -0
  156. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_prompt_hash.py +0 -0
  157. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_registry.py +0 -0
  158. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_remove_placeholder_block.py +0 -0
  159. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_retry_config.py +0 -0
  160. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_retry_execution.py +0 -0
  161. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_retry_stateful.py +0 -0
  162. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_retryblock_migration.py +0 -0
  163. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run126_code_block_parser_and_achat.py +0 -0
  164. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run127_runner_get_client_api_key.py +0 -0
  165. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run137_async_subprocess.py +0 -0
  166. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run141_multi_provider_keys.py +0 -0
  167. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run170_complex_read_sites.py +0 -0
  168. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run177_block_result.py +0 -0
  169. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run178_write_sites_block_result.py +0 -0
  170. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run179_strict_block_result.py +0 -0
  171. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run181_read_site_migration.py +0 -0
  172. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run219_auto_registration.py +0 -0
  173. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run222_migrate_blocks.py +0 -0
  174. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run377_yaml_enabled.py +0 -0
  175. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run415_no_builtin_souls.py +0 -0
  176. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run468_parser_soul_field_forwarding.py +0 -0
  177. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run469_discover_soul_fields.py +0 -0
  178. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run569_project_root_resolution.py +0 -0
  179. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run570_kill_inline_souls.py +0 -0
  180. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run571_wire_soul_ref_to_library.py +0 -0
  181. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run572_library_soul_tool_governance.py +0 -0
  182. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run603_workflow_interface_schema.py +0 -0
  183. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run604_interface_execution.py +0 -0
  184. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run605_on_error_modes.py +0 -0
  185. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run606_runtime_depth_parity.py +0 -0
  186. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run614_integration_subworkflow.py +0 -0
  187. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run628_noise_cleanup_verification.py +0 -0
  188. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run629_dispatch_e2e.py +0 -0
  189. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run644_dispatch_runtime_rename.py +0 -0
  190. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run645_dispatch_schema_canonicalization.py +0 -0
  191. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run663_child_observer_wrapper.py +0 -0
  192. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run663_parser_round_trip.py +0 -0
  193. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run668_depends_error_routes.py +0 -0
  194. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run669_gate_shortcuts.py +0 -0
  195. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run670_error_route_runtime.py +0 -0
  196. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run671_routes_shorthand.py +0 -0
  197. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run675_block_execution_context.py +0 -0
  198. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run676_execute_block_extraction.py +0 -0
  199. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run677_workflow_run_execute_block_wiring.py +0 -0
  200. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run678_loop_execute_block_wiring.py +0 -0
  201. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run680_codeblock_exit_handle.py +0 -0
  202. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run681_linearblock_exit_conditions.py +0 -0
  203. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run682_workflowblock_loopblock_e2e.py +0 -0
  204. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run683_nested_loopblock_observer.py +0 -0
  205. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run684_exit_handle_all_block_types.py +0 -0
  206. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run685_eval_debt_integration.py +0 -0
  207. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run688_soul_assertions_cleanup.py +0 -0
  208. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run690_delete_duplicate_resolve_soul.py +0 -0
  209. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run692_inline_soul_fixture_migration.py +0 -0
  210. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run693_step_wrapper_assertions.py +0 -0
  211. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run694_eval_yaml_schema.py +0 -0
  212. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run695_eval_runner.py +0 -0
  213. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run699_eval_integration.py +0 -0
  214. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run700_eval_e2e.py +0 -0
  215. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run701_state_isolation_verification.py +0 -0
  216. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run702_mixed_pipeline_e2e.py +0 -0
  217. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run703_dispatch_in_loop_e2e.py +0 -0
  218. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run704_error_route_output_mapping_e2e.py +0 -0
  219. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run787_discovery_foundation.py +0 -0
  220. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run788_workflow_repo_soul_scanner.py +0 -0
  221. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run789_tool_scanner_callers.py +0 -0
  222. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run789_tools_router_scanner.py +0 -0
  223. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run790_workflow_repo_scanner.py +0 -0
  224. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run790_workflow_scanner.py +0 -0
  225. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run792_unified_scanner_integration.py +0 -0
  226. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run794_assertion_scanner.py +0 -0
  227. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run797_custom_assertion_registration.py +0 -0
  228. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run800_custom_assertion_eval_e2e.py +0 -0
  229. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run803_tool_pydantic_validation.py +0 -0
  230. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run817_ipc_models_extract.py +0 -0
  231. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run818_interceptors_extract.py +0 -0
  232. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run819_worker_proxies_extract.py +0 -0
  233. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_run820_worker_support_extract.py +0 -0
  234. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_runner.py +0 -0
  235. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_runner_messages.py +0 -0
  236. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_sandbox_hardening.py +0 -0
  237. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_schema.py +0 -0
  238. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_schema_validation.py +0 -0
  239. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_state.py +0 -0
  240. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_tool_integration.py +0 -0
  241. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_tool_registry.py +0 -0
  242. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_windowing.py +0 -0
  243. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_workflow.py +0 -0
  244. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_workflow_block_execute.py +0 -0
  245. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_workflow_block_recursion.py +0 -0
  246. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_workflow_defensive_observer.py +0 -0
  247. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_workflow_output_conditions.py +0 -0
  248. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_yaml_assertions_config.py +0 -0
  249. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_yaml_dx_e2e.py +0 -0
  250. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_yaml_dx_sugar.py +0 -0
  251. {runsight_core-0.2.0 → runsight_core-0.2.2}/tests/test_yaml_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runsight-core
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Runsight Agent OS Core Engine
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "runsight-core"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Runsight Agent OS Core Engine"
9
9
  requires-python = ">=3.11"
10
10
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runsight-core
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Runsight Agent OS Core Engine
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -228,6 +228,7 @@ tests/test_run817_ipc_models_extract.py
228
228
  tests/test_run818_interceptors_extract.py
229
229
  tests/test_run819_worker_proxies_extract.py
230
230
  tests/test_run820_worker_support_extract.py
231
+ tests/test_run_821_docker_hardening.py
231
232
  tests/test_runner.py
232
233
  tests/test_runner_messages.py
233
234
  tests/test_sandbox_hardening.py
@@ -0,0 +1,513 @@
1
+ """
2
+ RUN-821 — Docker hardening tests.
3
+
4
+ These tests are RED-phase: they assert on security posture that does not yet exist.
5
+ Every test here is expected to FAIL until Green implements:
6
+
7
+ 1. Dockerfile — runsight user (UID/GID 1000), git safe.directory, USER directive
8
+ 2. docker-compose.yml — init-permissions service, cap_drop, no-new-privileges,
9
+ mem_limit, cpus, init: true, depends_on
10
+ 3. docker-entrypoint.sh — fail-fast when workspace is absent (no mkdir)
11
+ 4. process-isolation.md — Layer 4 updated to reflect container hardening
12
+
13
+ Acceptance Criteria coverage:
14
+ - AC1: Process runs as UID 1000 (runsight), not root
15
+ - AC2: Memory limits via cgroup OOM (mem_limit / memswap_limit in compose)
16
+ - AC3: Zero Linux capabilities (cap_drop: ALL, no cap_add)
17
+ - AC4: no-new-privileges security_opt
18
+ - AC5: init-permissions service owns /workspace before main service
19
+ - AC6: git safe.directory configured at build time
20
+ - AC7: zombie reaping via tini (init: true in compose)
21
+ """
22
+
23
+ import re
24
+ import subprocess
25
+ import tempfile
26
+ from pathlib import Path
27
+
28
+ import pytest
29
+ import yaml
30
+
31
+ # Resolve repo root: packages/core/tests/ is 3 levels deep from root.
32
+ REPO_ROOT = Path(__file__).resolve().parents[3]
33
+
34
+ DOCKERFILE = REPO_ROOT / "Dockerfile"
35
+ COMPOSE_FILE = REPO_ROOT / "docker-compose.yml"
36
+ ENTRYPOINT = REPO_ROOT / "docker-entrypoint.sh"
37
+ PROCESS_ISOLATION_MD = (
38
+ REPO_ROOT
39
+ / "apps"
40
+ / "site"
41
+ / "src"
42
+ / "content"
43
+ / "docs"
44
+ / "docs"
45
+ / "execution"
46
+ / "process-isolation.md"
47
+ )
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Helpers
52
+ # ---------------------------------------------------------------------------
53
+
54
+
55
+ def _read_dockerfile() -> str:
56
+ return DOCKERFILE.read_text()
57
+
58
+
59
+ def _load_compose() -> dict:
60
+ return yaml.safe_load(COMPOSE_FILE.read_text())
61
+
62
+
63
+ def _read_entrypoint() -> str:
64
+ return ENTRYPOINT.read_text()
65
+
66
+
67
+ def _read_process_isolation_md() -> str:
68
+ return PROCESS_ISOLATION_MD.read_text()
69
+
70
+
71
+ def _runtime_stage_lines(dockerfile_text: str) -> list[str]:
72
+ """Return lines that belong to the runtime stage (after 'AS runtime')."""
73
+ lines = dockerfile_text.splitlines()
74
+ in_runtime = False
75
+ result = []
76
+ for line in lines:
77
+ if re.search(r"FROM\s+\S+\s+AS\s+runtime", line, re.IGNORECASE):
78
+ in_runtime = True
79
+ continue
80
+ if in_runtime and re.match(r"FROM\s+", line, re.IGNORECASE):
81
+ # Next FROM stage — stop collecting.
82
+ break
83
+ if in_runtime:
84
+ result.append(line)
85
+ return result
86
+
87
+
88
+ # ===========================================================================
89
+ # SECTION 1 — Dockerfile: non-root user, UID 1000, git safe.directory
90
+ # ===========================================================================
91
+
92
+
93
+ class TestDockerfileNonRootUser:
94
+ """AC1 / AC6 — Dockerfile must create and switch to UID 1000 user."""
95
+
96
+ def test_user_directive_exists_in_runtime_stage(self):
97
+ """A USER directive must be present in the runtime stage."""
98
+ runtime_lines = _runtime_stage_lines(_read_dockerfile())
99
+ user_directives = [ln for ln in runtime_lines if re.match(r"^\s*USER\s+", ln)]
100
+ assert user_directives, (
101
+ "No USER directive found in the runtime stage. "
102
+ "Green must add 'USER runsight' (or USER 1000) to the runtime stage."
103
+ )
104
+
105
+ def test_user_directive_names_runsight(self):
106
+ """The USER directive must reference the runsight user (not root, not numeric root)."""
107
+ runtime_lines = _runtime_stage_lines(_read_dockerfile())
108
+ user_directives = [ln for ln in runtime_lines if re.match(r"^\s*USER\s+", ln)]
109
+ assert user_directives, (
110
+ "No USER directive found in the runtime stage — cannot verify it names 'runsight'. "
111
+ "Green must add 'USER runsight' to the runtime stage."
112
+ )
113
+ for line in user_directives:
114
+ m = re.match(r"^\s*USER\s+(\S+)", line)
115
+ if m:
116
+ user_value = m.group(1).lower()
117
+ assert "runsight" in user_value or user_value == "1000", (
118
+ f"USER directive is '{m.group(1)}', expected 'runsight' or '1000'. "
119
+ "Green must use the named runsight user."
120
+ )
121
+
122
+ def test_groupadd_creates_gid_1000(self):
123
+ """groupadd must create the runsight group with GID 1000."""
124
+ dockerfile_text = _read_dockerfile()
125
+ runtime_lines = _runtime_stage_lines(dockerfile_text)
126
+ runtime_block = "\n".join(runtime_lines)
127
+ assert re.search(r"groupadd\b.*--gid\s+1000", runtime_block) or re.search(
128
+ r"groupadd\b.*-g\s+1000", runtime_block
129
+ ), (
130
+ "No groupadd with GID 1000 found in runtime stage. "
131
+ "Green must add: groupadd --gid 1000 runsight"
132
+ )
133
+
134
+ def test_useradd_creates_uid_1000(self):
135
+ """useradd must create the runsight user with UID 1000."""
136
+ runtime_block = "\n".join(_runtime_stage_lines(_read_dockerfile()))
137
+ assert re.search(r"useradd\b.*--uid\s+1000", runtime_block) or re.search(
138
+ r"useradd\b.*-u\s+1000", runtime_block
139
+ ), (
140
+ "No useradd with UID 1000 found in runtime stage. "
141
+ "Green must add: useradd --uid 1000 --gid 1000 runsight"
142
+ )
143
+
144
+ def test_useradd_references_runsight(self):
145
+ """useradd must create a user named 'runsight'."""
146
+ runtime_block = "\n".join(_runtime_stage_lines(_read_dockerfile()))
147
+ assert re.search(r"useradd\b.*runsight", runtime_block), (
148
+ "No 'useradd ... runsight' found in runtime stage. Green must name the user 'runsight'."
149
+ )
150
+
151
+ def test_git_safe_directory_configured(self):
152
+ """git config safe.directory /workspace must be set at build time."""
153
+ runtime_block = "\n".join(_runtime_stage_lines(_read_dockerfile()))
154
+ assert re.search(
155
+ r"git\s+config\s+--global\s+--add\s+safe\.directory\s+/workspace", runtime_block
156
+ ), (
157
+ "git safe.directory /workspace not configured in the Dockerfile runtime stage. "
158
+ "Green must add: RUN git config --global --add safe.directory /workspace"
159
+ )
160
+
161
+ def test_workspace_dir_created_with_correct_ownership(self):
162
+ """
163
+ /workspace must be pre-created and chowned to the runsight user
164
+ in the Dockerfile so the non-root user can write to it.
165
+ """
166
+ runtime_block = "\n".join(_runtime_stage_lines(_read_dockerfile()))
167
+ # Accept either explicit mkdir+chown or combined form.
168
+ has_mkdir = re.search(r"mkdir\s+-p\s+/workspace", runtime_block)
169
+ has_chown = re.search(
170
+ r"chown\s+.*runsight.*:.*runsight.*\s+/workspace", runtime_block
171
+ ) or re.search(r"chown\s+1000:1000\s+/workspace", runtime_block)
172
+ assert has_mkdir and has_chown, (
173
+ "Dockerfile runtime stage must mkdir /workspace and chown it to runsight:runsight (1000:1000). "
174
+ f"mkdir found: {bool(has_mkdir)}, chown found: {bool(has_chown)}"
175
+ )
176
+
177
+
178
+ # ===========================================================================
179
+ # SECTION 2 — docker-compose.yml: security settings
180
+ # ===========================================================================
181
+
182
+
183
+ class TestDockerComposeInitPermissions:
184
+ """AC5 — init-permissions service must set up /workspace before main service."""
185
+
186
+ def test_init_permissions_service_exists(self):
187
+ """An 'init-permissions' service must exist in docker-compose.yml."""
188
+ compose = _load_compose()
189
+ services = compose.get("services", {})
190
+ assert "init-permissions" in services, (
191
+ "No 'init-permissions' service found in docker-compose.yml. "
192
+ "Green must add a service that chowns /workspace to UID 1000."
193
+ )
194
+
195
+ def test_init_permissions_uses_busybox(self):
196
+ """init-permissions service must use a minimal image (busybox)."""
197
+ compose = _load_compose()
198
+ svc = compose.get("services", {}).get("init-permissions", {})
199
+ image = svc.get("image", "")
200
+ assert "busybox" in image.lower(), (
201
+ f"init-permissions uses image '{image}', expected a busybox-based image. "
202
+ "Green must use 'busybox' for the init-permissions service."
203
+ )
204
+
205
+ def test_init_permissions_runs_as_root(self):
206
+ """init-permissions service must run as root (user: '0') to chown files."""
207
+ compose = _load_compose()
208
+ svc = compose.get("services", {}).get("init-permissions", {})
209
+ user = str(svc.get("user", ""))
210
+ assert user == "0", (
211
+ f"init-permissions service user is '{user}', expected '0' (root). "
212
+ "Green must set user: '0' so it can chown /workspace."
213
+ )
214
+
215
+ def test_init_permissions_has_cap_drop_all(self):
216
+ """init-permissions service must drop all capabilities."""
217
+ compose = _load_compose()
218
+ svc = compose.get("services", {}).get("init-permissions", {})
219
+ cap_drop = svc.get("cap_drop", [])
220
+ assert "ALL" in cap_drop, (
221
+ f"init-permissions cap_drop is {cap_drop}, expected ['ALL']. "
222
+ "Green must add cap_drop: [ALL] to the init-permissions service."
223
+ )
224
+
225
+ def test_init_permissions_has_cap_add_chown_only(self):
226
+ """init-permissions must add back only CAP_CHOWN."""
227
+ compose = _load_compose()
228
+ svc = compose.get("services", {}).get("init-permissions", {})
229
+ cap_add = svc.get("cap_add", [])
230
+ assert cap_add == ["CHOWN"], (
231
+ f"init-permissions cap_add is {cap_add}, expected ['CHOWN']. "
232
+ "Green must add cap_add: [CHOWN] only."
233
+ )
234
+
235
+
236
+ class TestDockerComposeRunsightServiceSecurity:
237
+ """AC2 / AC3 / AC4 / AC7 — runsight service must be locked down."""
238
+
239
+ def test_runsight_has_cap_drop_all(self):
240
+ """runsight service must drop ALL Linux capabilities."""
241
+ compose = _load_compose()
242
+ svc = compose.get("services", {}).get("runsight", {})
243
+ cap_drop = svc.get("cap_drop", [])
244
+ assert "ALL" in cap_drop, (
245
+ f"runsight cap_drop is {cap_drop}, expected ['ALL']. "
246
+ "Green must add cap_drop: [ALL] to the runsight service."
247
+ )
248
+
249
+ def test_runsight_has_no_cap_add(self):
250
+ """
251
+ runsight service must NOT have a cap_add directive.
252
+ This test also verifies cap_drop: ALL is already present (otherwise
253
+ 'no cap_add' is trivially true for any unconfigured service).
254
+ """
255
+ compose = _load_compose()
256
+ svc = compose.get("services", {}).get("runsight", {})
257
+ # First check cap_drop is configured — makes this test load-bearing.
258
+ cap_drop = svc.get("cap_drop", [])
259
+ assert "ALL" in cap_drop, (
260
+ f"runsight cap_drop is {cap_drop}. "
261
+ "Green must add cap_drop: [ALL] before the no-cap_add assertion is meaningful."
262
+ )
263
+ cap_add = svc.get("cap_add")
264
+ assert cap_add is None or cap_add == [], (
265
+ f"runsight has cap_add: {cap_add}. "
266
+ "Green must not add any capabilities back to the runsight service."
267
+ )
268
+
269
+ def test_runsight_has_no_new_privileges(self):
270
+ """runsight service must set security_opt: no-new-privileges:true."""
271
+ compose = _load_compose()
272
+ svc = compose.get("services", {}).get("runsight", {})
273
+ security_opt = svc.get("security_opt", [])
274
+ assert "no-new-privileges:true" in security_opt, (
275
+ f"runsight security_opt is {security_opt}, "
276
+ "expected 'no-new-privileges:true'. "
277
+ "Green must add security_opt: [no-new-privileges:true]."
278
+ )
279
+
280
+ def test_runsight_has_mem_limit(self):
281
+ """runsight service must have a mem_limit set (4g or equivalent)."""
282
+ compose = _load_compose()
283
+ svc = compose.get("services", {}).get("runsight", {})
284
+ mem_limit = svc.get("mem_limit")
285
+ assert mem_limit is not None, (
286
+ "runsight service has no mem_limit. Green must add mem_limit: 4g to cap memory usage."
287
+ )
288
+ assert str(mem_limit).lower() in ("4g", "4096m", "4294967296"), (
289
+ f"runsight mem_limit is '{mem_limit}', expected '4g'. Green must set mem_limit: 4g."
290
+ )
291
+
292
+ def test_runsight_has_memswap_limit(self):
293
+ """runsight service must have memswap_limit equal to mem_limit to prevent swap usage."""
294
+ compose = _load_compose()
295
+ svc = compose.get("services", {}).get("runsight", {})
296
+ memswap = svc.get("memswap_limit")
297
+ assert memswap is not None, (
298
+ "runsight service has no memswap_limit. "
299
+ "Green must add memswap_limit: 4g to prevent OOM evasion via swap."
300
+ )
301
+ assert str(memswap).lower() in ("4g", "4096m", "4294967296"), (
302
+ f"runsight memswap_limit is '{memswap}', expected '4g'. "
303
+ "Green must set memswap_limit: 4g."
304
+ )
305
+
306
+ def test_runsight_has_cpus_limit(self):
307
+ """runsight service must have a cpus limit."""
308
+ compose = _load_compose()
309
+ svc = compose.get("services", {}).get("runsight", {})
310
+ cpus = svc.get("cpus")
311
+ assert cpus is not None, (
312
+ "runsight service has no cpus limit. Green must add cpus: 2 to cap CPU usage."
313
+ )
314
+ assert float(cpus) == 2.0, f"runsight cpus is {cpus}, expected 2. Green must set cpus: 2."
315
+
316
+ def test_runsight_has_init_true(self):
317
+ """runsight service must use init: true so tini (PID 1) reaps zombie processes."""
318
+ compose = _load_compose()
319
+ svc = compose.get("services", {}).get("runsight", {})
320
+ init_flag = svc.get("init")
321
+ assert init_flag is True, (
322
+ f"runsight 'init' flag is {init_flag!r}, expected True. "
323
+ "Green must add init: true so Docker injects tini as PID 1."
324
+ )
325
+
326
+ def test_runsight_depends_on_init_permissions(self):
327
+ """runsight service must depend on init-permissions completing successfully."""
328
+ compose = _load_compose()
329
+ svc = compose.get("services", {}).get("runsight", {})
330
+ depends_on = svc.get("depends_on", {})
331
+
332
+ # depends_on can be a list or dict form.
333
+ if isinstance(depends_on, list):
334
+ assert "init-permissions" in depends_on, (
335
+ f"runsight depends_on is {depends_on}. "
336
+ "Green must add init-permissions to depends_on."
337
+ )
338
+ elif isinstance(depends_on, dict):
339
+ assert "init-permissions" in depends_on, (
340
+ f"runsight depends_on keys are {list(depends_on.keys())}. "
341
+ "Green must add init-permissions to depends_on."
342
+ )
343
+ condition = depends_on["init-permissions"].get("condition")
344
+ assert condition == "service_completed_successfully", (
345
+ f"init-permissions condition is '{condition}', "
346
+ "expected 'service_completed_successfully'. "
347
+ "Green must use condition: service_completed_successfully."
348
+ )
349
+ else:
350
+ pytest.fail(
351
+ f"runsight depends_on is missing or malformed: {depends_on!r}. "
352
+ "Green must add depends_on: init-permissions with condition: service_completed_successfully."
353
+ )
354
+
355
+
356
+ # ===========================================================================
357
+ # SECTION 3 — docker-entrypoint.sh: fail-fast, no mkdir
358
+ # ===========================================================================
359
+
360
+
361
+ class TestEntrypointBehavior:
362
+ """AC1 / AC5 — Entrypoint must fail-fast when workspace absent; no mkdir."""
363
+
364
+ def test_entrypoint_has_no_mkdir(self):
365
+ """
366
+ The updated entrypoint must NOT contain mkdir.
367
+ Non-root containers cannot create root-owned directories at runtime.
368
+ """
369
+ entrypoint_text = _read_entrypoint()
370
+ assert "mkdir" not in entrypoint_text, (
371
+ "docker-entrypoint.sh still contains 'mkdir'. "
372
+ "Green must remove all mkdir calls — init-permissions handles ownership, "
373
+ "and the container runs as non-root."
374
+ )
375
+
376
+ def test_entrypoint_exits_nonzero_when_workspace_missing(self):
377
+ """
378
+ When RUNSIGHT_BASE_PATH does not exist the entrypoint must exit with
379
+ a non-zero status code and a meaningful error message.
380
+ """
381
+ with tempfile.TemporaryDirectory() as tmp:
382
+ missing_path = str(Path(tmp) / "nonexistent-workspace")
383
+ result = subprocess.run(
384
+ ["sh", str(ENTRYPOINT), "true"],
385
+ env={
386
+ "PATH": "/app/.venv/bin:/usr/local/bin:/usr/bin:/bin",
387
+ "RUNSIGHT_BASE_PATH": missing_path,
388
+ },
389
+ capture_output=True,
390
+ text=True,
391
+ )
392
+ assert result.returncode != 0, (
393
+ f"Entrypoint exited with code {result.returncode} (expected non-zero) "
394
+ f"when workspace '{missing_path}' does not exist. "
395
+ "Green must add a fail-fast check: if workspace is absent, exit 1."
396
+ )
397
+
398
+ def test_entrypoint_succeeds_when_workspace_exists(self):
399
+ """
400
+ When RUNSIGHT_BASE_PATH exists the entrypoint must exit with code 0
401
+ and pass through to exec.
402
+ """
403
+ with tempfile.TemporaryDirectory() as workspace:
404
+ result = subprocess.run(
405
+ ["sh", str(ENTRYPOINT), "true"],
406
+ env={
407
+ "PATH": "/app/.venv/bin:/usr/local/bin:/usr/bin:/bin",
408
+ "RUNSIGHT_BASE_PATH": workspace,
409
+ },
410
+ capture_output=True,
411
+ text=True,
412
+ )
413
+ assert result.returncode == 0, (
414
+ f"Entrypoint exited with code {result.returncode} when workspace exists. "
415
+ f"stdout: {result.stdout!r}, stderr: {result.stderr!r}"
416
+ )
417
+
418
+ def test_entrypoint_prints_scaffold_message_for_empty_workspace(self):
419
+ """
420
+ When the workspace exists but is empty the entrypoint should print a
421
+ message indicating Runsight will scaffold a new project.
422
+ """
423
+ with tempfile.TemporaryDirectory() as workspace:
424
+ result = subprocess.run(
425
+ ["sh", str(ENTRYPOINT), "true"],
426
+ env={
427
+ "PATH": "/app/.venv/bin:/usr/local/bin:/usr/bin:/bin",
428
+ "RUNSIGHT_BASE_PATH": workspace,
429
+ },
430
+ capture_output=True,
431
+ text=True,
432
+ )
433
+ combined = result.stdout + result.stderr
434
+ assert "scaffold" in combined.lower(), (
435
+ "Entrypoint did not print a scaffolding message for an empty workspace. "
436
+ f"Output was: {combined!r}"
437
+ )
438
+
439
+
440
+ # ===========================================================================
441
+ # SECTION 4 — process-isolation.md: Container hardening is Layer 1, active
442
+ # ===========================================================================
443
+
444
+
445
+ class TestProcessIsolationDocsUpdated:
446
+ """AC3 / AC2 — Documentation must reflect container hardening as Layer 1."""
447
+
448
+ def test_container_hardening_is_layer_1(self):
449
+ """Container hardening must be Layer 1 (outermost defense layer)."""
450
+ md = _read_process_isolation_md()
451
+ layer1_lines = [ln for ln in md.splitlines() if "Layer 1" in ln]
452
+ assert layer1_lines, "No 'Layer 1' line found in process-isolation.md."
453
+ layer1_text = " ".join(layer1_lines).lower()
454
+ assert "container" in layer1_text or "hardening" in layer1_text, (
455
+ f"Layer 1 does not mention container hardening: {layer1_lines}. "
456
+ "Container hardening must be the outermost layer (Layer 1)."
457
+ )
458
+
459
+ def test_layer_1_does_not_say_future(self):
460
+ """Container hardening layer must not contain 'future' — it is active."""
461
+ md = _read_process_isolation_md()
462
+ layer1_lines = [ln for ln in md.splitlines() if "Layer 1" in ln]
463
+ assert layer1_lines, "No 'Layer 1' line found in process-isolation.md."
464
+ layer1_text = " ".join(layer1_lines).lower()
465
+ assert "future" not in layer1_text, (
466
+ f"Layer 1 description still contains 'future': {layer1_lines}. "
467
+ "Container hardening is active, not future."
468
+ )
469
+
470
+ def test_layer_1_mentions_unprivileged_user(self):
471
+ """
472
+ Container hardening layer must mention the unprivileged user,
473
+ using human-readable language.
474
+ """
475
+ md = _read_process_isolation_md()
476
+ layer1_lines = [ln for ln in md.splitlines() if "Layer 1" in ln]
477
+ assert layer1_lines, "No 'Layer 1' line found in process-isolation.md."
478
+ layer1_text = " ".join(layer1_lines).lower()
479
+ assert "unprivileged" in layer1_text or "non-root" in layer1_text, (
480
+ f"Layer 1 does not mention unprivileged/non-root user: {layer1_lines}. "
481
+ "Container hardening layer must reference the unprivileged container user."
482
+ )
483
+
484
+ def test_cpu_memory_limits_row_not_unenforced(self):
485
+ """
486
+ The CPU / memory limits row in the Known Limitations table must no longer
487
+ say 'Not enforced' — it should reflect container-level enforcement.
488
+ """
489
+ md = _read_process_isolation_md()
490
+ lines = md.splitlines()
491
+ cpu_mem_lines = [ln for ln in lines if "cpu" in ln.lower() and "memory" in ln.lower()]
492
+ assert cpu_mem_lines, (
493
+ "No CPU / memory row found in process-isolation.md Known Limitations table."
494
+ )
495
+ for line in cpu_mem_lines:
496
+ assert "Not enforced" not in line, (
497
+ f"CPU / memory limits row still says 'Not enforced': {line!r}. "
498
+ "Green must update this to 'Container-level' enforcement."
499
+ )
500
+
501
+ def test_cpu_memory_limits_row_mentions_container_level(self):
502
+ """CPU / memory row must mention 'Container-level' enforcement."""
503
+ md = _read_process_isolation_md()
504
+ lines = md.splitlines()
505
+ cpu_mem_lines = [ln for ln in lines if "cpu" in ln.lower() and "memory" in ln.lower()]
506
+ assert cpu_mem_lines, (
507
+ "No CPU / memory row found in process-isolation.md Known Limitations table."
508
+ )
509
+ found = any("container" in ln.lower() for ln in cpu_mem_lines)
510
+ assert found, (
511
+ f"CPU / memory row does not mention 'Container-level': {cpu_mem_lines}. "
512
+ "Green must update the row to reflect container-level enforcement."
513
+ )
File without changes
File without changes