steerable-agent-runtime 0.6.74__tar.gz → 0.7.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 (163) hide show
  1. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/PKG-INFO +1 -1
  2. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/pyproject.toml +1 -1
  3. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/approval.py +183 -35
  4. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/harness.py +9 -3
  5. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/storage/sqlite_store.py +39 -17
  6. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
  7. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_approval.py +343 -1
  8. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_harness.py +28 -2
  9. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_write_lease.py +30 -0
  10. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/README.md +0 -0
  11. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/setup.cfg +0 -0
  12. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/__init__.py +0 -0
  13. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/antihallucination.py +0 -0
  14. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/approval_policy.py +0 -0
  15. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/ask_user.py +0 -0
  16. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/branch.py +0 -0
  17. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/cache_control.py +0 -0
  18. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/calibration.py +0 -0
  19. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/compaction.py +0 -0
  20. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/compaction_policy.py +0 -0
  21. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/config.py +0 -0
  22. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/default.harness.json +0 -0
  23. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  24. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/delegation_gate.py +0 -0
  25. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/errors.py +0 -0
  26. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  27. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/handoff.py +0 -0
  28. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/harness_spec.py +0 -0
  29. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/history.py +0 -0
  30. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/hooks.py +0 -0
  31. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  32. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  33. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/llm/compat.py +0 -0
  34. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/llm/errors.py +0 -0
  35. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  36. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  37. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  38. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/llm/parts.py +0 -0
  39. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/llm/presets.py +0 -0
  40. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  41. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/loop.py +0 -0
  42. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/maintenance.py +0 -0
  43. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/mcp.py +0 -0
  44. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/mcp_server.py +0 -0
  45. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/model_catalog.py +0 -0
  46. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/model_info.py +0 -0
  47. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/model_resolve.py +0 -0
  48. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/native_bridge.py +0 -0
  49. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/observation_aging.py +0 -0
  50. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/orchestration.py +0 -0
  51. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/otel.py +0 -0
  52. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/plugins.py +0 -0
  53. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/pool.py +0 -0
  54. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/pricing.py +0 -0
  55. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/pseudo.py +0 -0
  56. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/recording.py +0 -0
  57. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/reminders.py +0 -0
  58. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/replay.py +0 -0
  59. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/resume.py +0 -0
  60. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/retry.py +0 -0
  61. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/sandboxed.py +0 -0
  62. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/skills.py +0 -0
  63. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/spill.py +0 -0
  64. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  65. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  66. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  67. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  68. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/subagent.py +0 -0
  69. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/todo.py +0 -0
  70. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/tokens.py +0 -0
  71. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/tool_presentation.py +0 -0
  72. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/tool_schema.py +0 -0
  73. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/tool_search.py +0 -0
  74. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/tools.py +0 -0
  75. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/tracing.py +0 -0
  76. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  77. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  78. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  79. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime/world_state.py +0 -0
  80. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
  81. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  82. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
  83. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  84. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_antihallucination.py +0 -0
  85. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_approval_policy.py +0 -0
  86. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_ask_user.py +0 -0
  87. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_branch.py +0 -0
  88. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_cache_control.py +0 -0
  89. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_cache_instrumentation.py +0 -0
  90. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_calibration.py +0 -0
  91. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_compaction.py +0 -0
  92. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_compaction_policy.py +0 -0
  93. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_compaction_strategy.py +0 -0
  94. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_config.py +0 -0
  95. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_content_parts.py +0 -0
  96. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_delegation_gate.py +0 -0
  97. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_error_taxonomy.py +0 -0
  98. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_fragment_bounds.py +0 -0
  99. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_gateway_catalog.py +0 -0
  100. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_golden.py +0 -0
  101. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_handoff.py +0 -0
  102. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_harness_spec.py +0 -0
  103. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_history.py +0 -0
  104. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_history_persistence.py +0 -0
  105. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_hooks.py +0 -0
  106. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_in_memory_storage.py +0 -0
  107. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_llm_gemini_wire.py +0 -0
  108. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_llm_responses_wire.py +0 -0
  109. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_llm_stream_e2e.py +0 -0
  110. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_llm_wire_helpers.py +0 -0
  111. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_long_session.py +0 -0
  112. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_loop.py +0 -0
  113. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_loop_cancellation.py +0 -0
  114. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_loop_replay.py +0 -0
  115. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_loop_sandbox_event.py +0 -0
  116. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_loop_terminal.py +0 -0
  117. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_maintenance.py +0 -0
  118. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_mcp.py +0 -0
  119. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_mcp_server.py +0 -0
  120. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_model_catalog.py +0 -0
  121. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_model_equivalence.py +0 -0
  122. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_model_info.py +0 -0
  123. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_model_resolve.py +0 -0
  124. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_native_default.py +0 -0
  125. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_observation_aging.py +0 -0
  126. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_orchestration.py +0 -0
  127. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_otel.py +0 -0
  128. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_parallel_tools.py +0 -0
  129. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_plugins.py +0 -0
  130. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_provider_compat.py +0 -0
  131. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_provider_presets.py +0 -0
  132. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_pseudo.py +0 -0
  133. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_pyo3_api_surface.py +0 -0
  134. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_recording.py +0 -0
  135. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_reminders.py +0 -0
  136. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_replay_crosslang.py +0 -0
  137. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_resume.py +0 -0
  138. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_retry_hooks.py +0 -0
  139. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_safety_gate.py +0 -0
  140. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_sandboxed.py +0 -0
  141. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_skills.py +0 -0
  142. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_soft_timeout.py +0 -0
  143. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_spill.py +0 -0
  144. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_sqlite_storage.py +0 -0
  145. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_steer.py +0 -0
  146. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_storage_contract.py +0 -0
  147. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_stream_strip.py +0 -0
  148. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_subagent.py +0 -0
  149. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_system_proxy.py +0 -0
  150. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_todo.py +0 -0
  151. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_todo_gate.py +0 -0
  152. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_tokens.py +0 -0
  153. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_tool_exposure.py +0 -0
  154. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_tool_hygiene.py +0 -0
  155. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_tool_presentation.py +0 -0
  156. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_tool_router.py +0 -0
  157. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_tool_schema.py +0 -0
  158. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_tool_timeout.py +0 -0
  159. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_trace_recorder.py +0 -0
  160. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_transport_jsonrpc.py +0 -0
  161. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_transport_sse.py +0 -0
  162. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_usage_attribution.py +0 -0
  163. {steerable_agent_runtime-0.6.74 → steerable_agent_runtime-0.7.0}/tests/test_world_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.74
3
+ Version: 0.7.0
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.74"
3
+ version = "0.7.0"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  license = { text = "Apache-2.0" }
6
6
  readme = "README.md"
@@ -32,7 +32,7 @@ import tempfile
32
32
  import time
33
33
  from dataclasses import dataclass
34
34
  from pathlib import Path
35
- from typing import TYPE_CHECKING, Any, Literal, Protocol, runtime_checkable
35
+ from typing import TYPE_CHECKING, Any, Literal, Protocol, cast, runtime_checkable
36
36
 
37
37
  from steerable_agent_harness.policy import ToolMode, decide_tool_mode
38
38
  from steerable_agent_protocol.generated import ToolCall, ToolResult
@@ -55,6 +55,7 @@ __all__ = [
55
55
  "Approver",
56
56
  "AutoApprover",
57
57
  "WorkspaceAutoApprover",
58
+ "declared_resolver",
58
59
  "InMemoryApprovalStore",
59
60
  "JsonApprovalStore",
60
61
  "SessionApprovalCache",
@@ -140,6 +141,33 @@ _PATH_ARG_TOOLS = {
140
141
  }
141
142
 
142
143
 
144
+ def _front_load_markers(
145
+ data: dict[str, Any] | None, approval_marker: dict[str, Any] | None
146
+ ) -> dict[str, Any]:
147
+ """Put sandbox and approval markers ahead of the tool payload.
148
+
149
+ A published loop keeps only the first 300 characters of the result
150
+ preview and, for a sibling that finished before a terminal tool in
151
+ the same batch, does not copy those markers onto the event. They
152
+ have to be readable from that prefix.
153
+ """
154
+ body = dict(data or {})
155
+ sandbox = body.pop("_sandbox", None)
156
+ approval = (
157
+ approval_marker.get("_approval")
158
+ if approval_marker is not None
159
+ else body.get("_approval")
160
+ )
161
+ body.pop("_approval", None)
162
+ ordered: dict[str, Any] = {}
163
+ if isinstance(sandbox, dict):
164
+ ordered["_sandbox"] = sandbox
165
+ if isinstance(approval, dict):
166
+ ordered["_approval"] = approval
167
+ ordered.update(body)
168
+ return ordered
169
+
170
+
143
171
  def declared_target_paths(request: ApprovalRequest) -> list[str] | None:
144
172
  """Filesystem targets of a path-scoped tool, or None when the call is not one.
145
173
 
@@ -169,31 +197,53 @@ def declared_target_paths(request: ApprovalRequest) -> list[str] | None:
169
197
  return [raw.strip()]
170
198
 
171
199
 
172
- def path_inside_writable_roots(raw: str, roots: list[str]) -> bool:
200
+ def path_inside_writable_roots(
201
+ raw: str,
202
+ roots: list[str],
203
+ *,
204
+ pathmod: Any | None = None,
205
+ base: str | None = None,
206
+ ) -> bool:
173
207
  """True when ``raw`` is the configured directory or one of its subdirectories.
174
208
 
175
- Relative paths are project-relative. A ``..`` segment can leave the
176
- project, so those still go to the prompt. Absolute paths must sit under
177
- one of ``roots``.
209
+ Relative paths are project-relative. When ``base`` is the project home,
210
+ they are joined to that directory and then compared like absolute paths,
211
+ so a symlink inside the project that points outside is not inside.
212
+ Without ``base``, a relative path is inside unless it starts with ``..``.
213
+ Absolute paths must sit under one of ``roots`` after symbolic links are
214
+ resolved, so ``/tmp`` and ``/private/tmp`` are the same directory.
215
+ Windows compares paths without letter case, so
216
+ ``c:\\work\\repo\\a.txt`` is inside ``C:\\Work\\Repo``. ``pathmod`` selects
217
+ the path implementation; production uses ``os.path``.
178
218
  """
179
- expanded = os.path.expanduser(raw.strip())
180
- if not os.path.isabs(expanded):
181
- normalized = os.path.normpath(expanded)
182
- return normalized != ".." and not normalized.startswith(".." + os.sep)
183
- target = os.path.abspath(expanded)
219
+ paths = os.path if pathmod is None else pathmod
220
+ expanded = paths.expanduser(raw.strip())
221
+ if not paths.isabs(expanded):
222
+ if isinstance(base, str) and base.strip():
223
+ expanded = paths.normpath(paths.join(paths.expanduser(base.strip()), expanded))
224
+ else:
225
+ normalized = paths.normpath(expanded)
226
+ return normalized != ".." and not normalized.startswith(".." + paths.sep)
227
+ target = paths.realpath(expanded)
184
228
  for root in roots:
185
229
  if not isinstance(root, str) or not root.strip():
186
230
  continue
187
- root_abs = os.path.abspath(os.path.expanduser(root.strip()))
231
+ root_abs = paths.realpath(paths.expanduser(root.strip()))
188
232
  try:
189
- if os.path.commonpath([target, root_abs]) == root_abs:
190
- return True
233
+ common = paths.commonpath([target, root_abs])
191
234
  except ValueError:
192
235
  continue
236
+ if paths.normcase(common) == paths.normcase(root_abs):
237
+ return True
193
238
  return False
194
239
 
195
240
 
196
- def shell_stays_in_workspace(request: ApprovalRequest, roots: list[str]) -> bool:
241
+ def shell_stays_in_workspace(
242
+ request: ApprovalRequest,
243
+ roots: list[str],
244
+ *,
245
+ base: str | None = None,
246
+ ) -> bool:
197
247
  """True when a shell call's working directory is inside a writable root.
198
248
 
199
249
  The command text is not parsed. Confinement is the OS sandbox's job:
@@ -208,7 +258,7 @@ def shell_stays_in_workspace(request: ApprovalRequest, roots: list[str]) -> bool
208
258
  return True
209
259
  if not isinstance(cwd, str) or not cwd.strip():
210
260
  return True
211
- return path_inside_writable_roots(cwd, roots)
261
+ return path_inside_writable_roots(cwd, roots, base=base)
212
262
 
213
263
 
214
264
  class WorkspaceAutoApprover:
@@ -216,13 +266,13 @@ class WorkspaceAutoApprover:
216
266
 
217
267
  Embedders may also name host-owned control tools whose effects are limited
218
268
  to internal session state and therefore need no interactive consent.
219
- Path-scoped tools (``local_write_file``, ``local_edit_file``, reads,
220
- ``present_files``) skip when every declared path is inside a writable
221
- root. Shell tools skip only when the OS exec sandbox is on and the
222
- working directory is inside those roots — the same split Codex uses
223
- between ``workspace-write`` and ``on-request``. The allow is
224
- request-scoped. The next call is judged again. Anything else falls
225
- through to the inner approver.
269
+ A ``read`` mode call with no filesystem target skips the prompt. Path-scoped
270
+ tools (``local_write_file``, ``local_edit_file``, reads, ``present_files``)
271
+ still skip only when every declared path is inside a writable root — a
272
+ ``read`` declaration does not open a path outside those roots. Shell tools
273
+ skip only when the OS exec sandbox is on and the working directory is
274
+ inside those roots. The allow is request-scoped. The next call is judged
275
+ again. Anything else falls through to the inner approver.
226
276
  """
227
277
 
228
278
  def __init__(
@@ -232,11 +282,13 @@ class WorkspaceAutoApprover:
232
282
  *,
233
283
  sandbox_enforced: bool = False,
234
284
  auto_allow_tools: tuple[str, ...] = (),
285
+ path_base: str | None = None,
235
286
  ) -> None:
236
287
  self._inner = inner
237
288
  self._roots = [root for root in writable_roots if isinstance(root, str) and root.strip()]
238
289
  self._sandbox_enforced = sandbox_enforced
239
290
  self._auto_allow_tools = frozenset(auto_allow_tools)
291
+ self._path_base = path_base.strip() if isinstance(path_base, str) and path_base.strip() else None
240
292
 
241
293
  def set_sandbox_enforced(self, enforced: bool) -> None:
242
294
  """Match a mid-turn sandbox toggle.
@@ -254,10 +306,17 @@ class WorkspaceAutoApprover:
254
306
  if (
255
307
  self._roots
256
308
  and paths is not None
257
- and all(path_inside_writable_roots(path, self._roots) for path in paths)
309
+ and all(
310
+ path_inside_writable_roots(path, self._roots, base=self._path_base)
311
+ for path in paths
312
+ )
258
313
  ):
259
314
  return ApprovalDecision("allow_once", "path is inside a writable project root")
260
- if self._sandbox_enforced and shell_stays_in_workspace(request, self._roots):
315
+ if request.mode == "read" and paths is None:
316
+ return ApprovalDecision("allow_once", "declared read-only tool")
317
+ if self._sandbox_enforced and shell_stays_in_workspace(
318
+ request, self._roots, base=self._path_base
319
+ ):
261
320
  return ApprovalDecision("allow_once", "command stays inside the workspace sandbox")
262
321
  return await self._inner.approve(request)
263
322
 
@@ -382,6 +441,86 @@ def _default_resolver(call: ToolCall, ctx: LoopContext) -> ApprovalRequest:
382
441
  )
383
442
 
384
443
 
444
+ #: Modes a host may declare on a tool. A declared value in this set is kept.
445
+ #: Missing or unknown values fall back to ``decide_tool_mode``. Keeping a
446
+ #: declared ``external`` stops a ``get_`` prefix from widening the call to read.
447
+ TOOL_MODES = frozenset(
448
+ {
449
+ "read",
450
+ "safe_write",
451
+ "destructive",
452
+ "other",
453
+ "external",
454
+ "local",
455
+ "ui",
456
+ "synthetic",
457
+ "auto_local",
458
+ }
459
+ )
460
+
461
+
462
+ def _tool_declared_name(tool: dict[str, Any]) -> str | None:
463
+ name = tool.get("name")
464
+ if isinstance(name, str) and name:
465
+ return name
466
+ function = tool.get("function")
467
+ if isinstance(function, dict):
468
+ nested = function.get("name")
469
+ if isinstance(nested, str) and nested:
470
+ return nested
471
+ return None
472
+
473
+
474
+ def _tool_declared_field(tool: dict[str, Any], key: str) -> Any:
475
+ if key in tool:
476
+ return tool.get(key)
477
+ function = tool.get("function")
478
+ if isinstance(function, dict) and key in function:
479
+ return function.get(key)
480
+ return None
481
+
482
+
483
+ def declared_resolver(
484
+ tools: list[Any] | None,
485
+ ) -> Any:
486
+ """Build an approval resolver from ``agent.chat.stream`` tools.
487
+
488
+ Each tool may carry optional ``mode`` and ``approvalCategory``, either on
489
+ the tool object or under ``function``. Tools without those fields, and
490
+ calls whose name is absent from the list, use name inference and the tool
491
+ name as the category. Old hosts that omit the fields keep the previous
492
+ behavior.
493
+ """
494
+ declared: dict[str, tuple[Any, Any]] = {}
495
+ for tool in tools or []:
496
+ if not isinstance(tool, dict):
497
+ continue
498
+ name = _tool_declared_name(tool)
499
+ if name is None:
500
+ continue
501
+ declared[name] = (
502
+ _tool_declared_field(tool, "mode"),
503
+ _tool_declared_field(tool, "approvalCategory"),
504
+ )
505
+
506
+ def resolve(call: ToolCall, ctx: LoopContext) -> ApprovalRequest:
507
+ mode, category = declared.get(call.name, (None, None))
508
+ resolved_mode = (
509
+ mode if isinstance(mode, str) and mode in TOOL_MODES else decide_tool_mode(call.name)
510
+ )
511
+ resolved_category = category if isinstance(category, str) and category else call.name
512
+ return ApprovalRequest(
513
+ tool_name=call.name,
514
+ arguments=call.arguments or {},
515
+ mode=cast(ToolMode, resolved_mode),
516
+ category=resolved_category,
517
+ round_index=ctx.round_index,
518
+ call_id=call.id,
519
+ )
520
+
521
+ return resolve
522
+
523
+
385
524
  class ToolTimeoutClock:
386
525
  """Pauses the loop's tool cap while an approval prompt is unanswered.
387
526
 
@@ -457,10 +596,11 @@ class ApprovalExecutor:
457
596
 
458
597
  async def execute(self, call: ToolCall, ctx: LoopContext) -> ToolResult:
459
598
  request = self._resolve(call, ctx)
460
- decision = self._stored_decision(request)
461
- # W2.7.2: when the decision required asking (cache miss), record the
462
- # wait on the result so the trace can show approval latency as its
463
- # own span. Cache hits carry no marker — no wait happened.
599
+ stored = self._stored_decision(request)
600
+ decision = stored
601
+ # A fresh decision records the wait so the trace can show approval
602
+ # latency. A session allow reused from the cache carries `cached`
603
+ # instead, so the tool row can say the call was already allowed.
464
604
  waited_ms: int | None = None
465
605
  if decision is None:
466
606
  ask_started = time.monotonic()
@@ -468,17 +608,24 @@ class ApprovalExecutor:
468
608
  waited_ms = int((time.monotonic() - ask_started) * 1000)
469
609
  self._persist(request, decision)
470
610
 
471
- approval_marker = (
472
- {
611
+ if stored is not None and stored.kind == "allow_for_session":
612
+ approval_marker = {
613
+ "_approval": {
614
+ "kind": "allow_for_session",
615
+ "category": request.category,
616
+ "cached": True,
617
+ }
618
+ }
619
+ elif waited_ms is not None:
620
+ approval_marker = {
473
621
  "_approval": {
474
622
  "kind": decision.kind,
475
623
  "category": request.category,
476
624
  "waitMs": waited_ms,
477
625
  }
478
626
  }
479
- if waited_ms is not None
480
- else None
481
- )
627
+ else:
628
+ approval_marker = None
482
629
 
483
630
  if decision.kind == "abort":
484
631
  raise ApprovalAborted(
@@ -503,9 +650,10 @@ class ApprovalExecutor:
503
650
  )
504
651
  ctx.consent_granted = True
505
652
  result = await self._inner.execute(call, ctx)
506
- if approval_marker is not None:
653
+ data = result.data if isinstance(result.data, dict) else None
654
+ if approval_marker is not None or (data is not None and "_sandbox" in data):
507
655
  result = result.model_copy(
508
- update={"data": {**(result.data or {}), **approval_marker}}
656
+ update={"data": _front_load_markers(data, approval_marker)}
509
657
  )
510
658
  return result
511
659
 
@@ -20,6 +20,7 @@ from __future__ import annotations
20
20
 
21
21
  from dataclasses import dataclass, field
22
22
  from pathlib import Path
23
+ from collections.abc import Callable
23
24
  from typing import Any, Protocol, runtime_checkable
24
25
 
25
26
  from .compaction import CompactionHooks
@@ -719,10 +720,12 @@ class DiscoveredNotesState:
719
720
 
720
721
  Unlike ``FilesystemState`` (one explicit path the model maintains), this
721
722
  is read-only discovery: the notes are project/user/managed-owned context
722
- the session reads, not a working file it edits.
723
+ the session reads, not a working file it edits. ``trusted`` gates the
724
+ ``project`` and ``local`` levels; ``managed`` and ``user`` are always read.
723
725
  """
724
726
 
725
727
  cwd: Path | str
728
+ trusted: Callable[[Path], bool]
726
729
  max_chars: int = 8_000
727
730
  name: str = "discovered-notes"
728
731
  assumes: str = (
@@ -737,15 +740,16 @@ class DiscoveredNotesState:
737
740
  return InMemoryStorage()
738
741
 
739
742
  def hooks(self, *, provider: LLMProvider | None = None) -> LoopHooks:
740
- return _DiscoveredNotesInjection(Path(self.cwd), self.max_chars)
743
+ return _DiscoveredNotesInjection(Path(self.cwd), self.max_chars, self.trusted)
741
744
 
742
745
 
743
746
  class _DiscoveredNotesInjection(NoopHooks):
744
747
  """pre_step slice: discover and append the notes union once per turn."""
745
748
 
746
- def __init__(self, cwd: Path, max_chars: int) -> None:
749
+ def __init__(self, cwd: Path, max_chars: int, trusted: Callable[[Path], bool]) -> None:
747
750
  self._cwd = cwd
748
751
  self._max_chars = max_chars
752
+ self._trusted = trusted
749
753
 
750
754
  async def pre_step(self, transcript: Any, ctx: Any) -> Any:
751
755
  from .hooks import PreStepAction, TranscriptAppend
@@ -755,6 +759,8 @@ class _DiscoveredNotesInjection(NoopHooks):
755
759
  sections: list[tuple[str, Path, str]] = []
756
760
  budget = self._max_chars
757
761
  for level, path in discover_notes_files(self._cwd):
762
+ if level in ("project", "local") and not self._trusted(path.parent):
763
+ continue
758
764
  if budget <= 0:
759
765
  break
760
766
  try:
@@ -17,10 +17,10 @@ Concurrency: like ``InMemoryStorage``, all mutations run under one asyncio
17
17
  lock (safe under concurrent ``await`` in a single loop). Across processes
18
18
  the database uses WAL, ``busy_timeout``, and a shared lease on the sibling
19
19
  ``*.lock`` file, so two sidecars can write different sessions. Schema
20
- migration takes that file exclusively and fails when another process has
21
- the database open. A ``user_version`` newer than this process refuses to
22
- open. Process death releases the kernel lock; the lock file is never
23
- deleted.
20
+ migration takes that file exclusively. A peer that is applying the schema
21
+ is waited on briefly; a lease that is still held, or a ``user_version``
22
+ newer than this process, refuses to open. Process death releases the
23
+ kernel lock; the lock file is never deleted.
24
24
  """
25
25
 
26
26
  from __future__ import annotations
@@ -28,6 +28,7 @@ from __future__ import annotations
28
28
  import asyncio
29
29
  import json
30
30
  import sqlite3
31
+ import time
31
32
  from collections.abc import Iterable
32
33
  from typing import Any
33
34
 
@@ -45,6 +46,11 @@ from .write_lease import WriteLease, acquire_shared_lease, acquire_write_lease
45
46
 
46
47
  SCHEMA_VERSION = 1
47
48
  _BUSY_TIMEOUT_MS = 5000
49
+ # A second process can observe version 0 while the first still holds the
50
+ # exclusive lease to create the schema. The hold is the migration itself,
51
+ # not a live sidecar, so wait out that window before refusing.
52
+ _PEER_MIGRATION_WAIT_S = 1.0
53
+ _PEER_MIGRATION_POLL_S = 0.02
48
54
 
49
55
  _SCHEMA = """
50
56
  PRAGMA journal_mode = WAL;
@@ -138,31 +144,47 @@ def _apply_schema(db: sqlite3.Connection) -> None:
138
144
  db.commit()
139
145
 
140
146
 
147
+ def _newer_than_this_program(version: int) -> StorageUpgradeBlockedError:
148
+ return StorageUpgradeBlockedError(
149
+ f"sidecar database version {version} is newer than this program ({SCHEMA_VERSION})"
150
+ )
151
+
152
+
141
153
  def _open_lease(path: str) -> WriteLease:
142
- version = _read_user_version(path)
143
- if version > SCHEMA_VERSION:
144
- raise StorageUpgradeBlockedError(
145
- f"sidecar database version {version} is newer than this program ({SCHEMA_VERSION})"
146
- )
147
- if version < SCHEMA_VERSION:
154
+ deadline = time.monotonic() + _PEER_MIGRATION_WAIT_S
155
+ while True:
156
+ version = _read_user_version(path)
157
+ if version > SCHEMA_VERSION:
158
+ raise _newer_than_this_program(version)
159
+ if version == SCHEMA_VERSION:
160
+ return acquire_shared_lease(path)
148
161
  try:
149
162
  exclusive = acquire_write_lease(path)
150
163
  except StoreAlreadyOwnedError as exc:
151
- version = _read_user_version(path)
152
- if version < SCHEMA_VERSION:
164
+ if time.monotonic() >= deadline:
165
+ version = _read_user_version(path)
166
+ if version > SCHEMA_VERSION:
167
+ raise _newer_than_this_program(version) from exc
168
+ if version == SCHEMA_VERSION:
169
+ return acquire_shared_lease(path)
153
170
  raise StorageUpgradeBlockedError(
154
171
  "sidecar database needs an upgrade but another process has it open"
155
172
  ) from exc
156
- else:
157
- try:
173
+ time.sleep(_PEER_MIGRATION_POLL_S)
174
+ continue
175
+ try:
176
+ version = _read_user_version(path)
177
+ if version > SCHEMA_VERSION:
178
+ raise _newer_than_this_program(version)
179
+ if version < SCHEMA_VERSION:
158
180
  db = _connect(path)
159
181
  try:
160
182
  _apply_schema(db)
161
183
  finally:
162
184
  db.close()
163
- finally:
164
- exclusive.release()
165
- return acquire_shared_lease(path)
185
+ finally:
186
+ exclusive.release()
187
+ return acquire_shared_lease(path)
166
188
 
167
189
 
168
190
  class SqliteStorage:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.74
3
+ Version: 0.7.0
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10