qwen-sci 0.2.2__py3-none-any.whl

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 (549) hide show
  1. qwen_sci-0.2.2.dist-info/METADATA +492 -0
  2. qwen_sci-0.2.2.dist-info/RECORD +549 -0
  3. qwen_sci-0.2.2.dist-info/WHEEL +4 -0
  4. qwen_sci-0.2.2.dist-info/entry_points.txt +6 -0
  5. qwen_sci-0.2.2.dist-info/licenses/LICENSE +21 -0
  6. src/1.txt +1 -0
  7. src/__init__.py +1 -0
  8. src/__main__.py +5 -0
  9. src/agents/__init__.py +1 -0
  10. src/agents/experiment_design_agent/__init__.py +268 -0
  11. src/agents/experiment_design_agent/artifacts.py +747 -0
  12. src/agents/experiment_design_agent/cache.py +319 -0
  13. src/agents/experiment_design_agent/completeness.py +130 -0
  14. src/agents/experiment_design_agent/contracts.py +771 -0
  15. src/agents/experiment_design_agent/counterexample_analyzer.py +187 -0
  16. src/agents/experiment_design_agent/design_evidence_paper_screener.py +470 -0
  17. src/agents/experiment_design_agent/discipline_catalog.py +178 -0
  18. src/agents/experiment_design_agent/evidence_cards.py +338 -0
  19. src/agents/experiment_design_agent/evidence_planner.py +452 -0
  20. src/agents/experiment_design_agent/formal_reasoning_planner.py +884 -0
  21. src/agents/experiment_design_agent/fulltext_acquisition.py +863 -0
  22. src/agents/experiment_design_agent/idea_adapter.py +100 -0
  23. src/agents/experiment_design_agent/idea_intake.py +87 -0
  24. src/agents/experiment_design_agent/llm_json.py +328 -0
  25. src/agents/experiment_design_agent/orchestrator.py +1220 -0
  26. src/agents/experiment_design_agent/reasoning_context.py +183 -0
  27. src/agents/experiment_design_agent/reasoning_validation.py +546 -0
  28. src/agents/experiment_design_agent/run.py +481 -0
  29. src/agents/experiment_design_agent/run_logging.py +292 -0
  30. src/agents/experiment_design_agent/scope_gate.py +198 -0
  31. src/agents/experiment_design_agent/study_type_composer.py +1360 -0
  32. src/agents/experiment_design_agent/survey_evidence.py +1381 -0
  33. src/agents/experiment_design_agent/template_router.py +195 -0
  34. src/agents/experiment_design_agent/variable_claim_extractor.py +93 -0
  35. src/agents/idea_agent/__init__.py +1 -0
  36. src/agents/idea_agent/agent/__init__.py +6 -0
  37. src/agents/idea_agent/agent/artifacts.py +308 -0
  38. src/agents/idea_agent/agent/base.py +122 -0
  39. src/agents/idea_agent/agent/ligagent.py +488 -0
  40. src/agents/idea_agent/agent/mcts.py +2060 -0
  41. src/agents/idea_agent/agent/prompts/__init__.py +48 -0
  42. src/agents/idea_agent/agent/prompts/advanced_analysis.py +235 -0
  43. src/agents/idea_agent/agent/prompts/algorithm_alignment.py +55 -0
  44. src/agents/idea_agent/agent/prompts/algorithm_structuring.py +48 -0
  45. src/agents/idea_agent/agent/prompts/component_extraction.py +62 -0
  46. src/agents/idea_agent/agent/prompts/component_novelty_evaluation.py +54 -0
  47. src/agents/idea_agent/agent/prompts/experiment_findings_extraction.py +39 -0
  48. src/agents/idea_agent/agent/prompts/idea_fusion.py +295 -0
  49. src/agents/idea_agent/agent/prompts/idea_introduction.py +30 -0
  50. src/agents/idea_agent/agent/prompts/idea_result_alignment.py +39 -0
  51. src/agents/idea_agent/agent/prompts/input_interpreter.py +52 -0
  52. src/agents/idea_agent/agent/prompts/keynote_ops.py +78 -0
  53. src/agents/idea_agent/agent/prompts/mcts_evaluation.py +235 -0
  54. src/agents/idea_agent/agent/prompts/mcts_generation.py +140 -0
  55. src/agents/idea_agent/agent/prompts/mechanism_commit_query.py +99 -0
  56. src/agents/idea_agent/agent/prompts/memory_selection.py +0 -0
  57. src/agents/idea_agent/agent/prompts/prompt_modes.py +16 -0
  58. src/agents/idea_agent/agent/prompts/rag_query.py +34 -0
  59. src/agents/idea_agent/agent/prompts/re_analysis_replan.py +48 -0
  60. src/agents/idea_agent/agent/prompts/reference_grounding.py +34 -0
  61. src/agents/idea_agent/agent/prompts/root_domain_classification.py +32 -0
  62. src/agents/idea_agent/agent/prompts/scientific_debate.py +88 -0
  63. src/agents/idea_agent/agent/prompts/scientific_materialization.py +45 -0
  64. src/agents/idea_agent/agent/prompts/skill_instantiation.py +193 -0
  65. src/agents/idea_agent/agent/prompts/theory_transfer_query.py +93 -0
  66. src/agents/idea_agent/agent/prompts/topic_background.py +15 -0
  67. src/agents/idea_agent/agent/skills/DEFAULT_SKILL_TEMPLATES.json +263 -0
  68. src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/SKILL.md +40 -0
  69. src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/references/contrast_patterns.md +22 -0
  70. src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/SKILL.md +40 -0
  71. src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/references/mechanism_cards.md +24 -0
  72. src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/SKILL.md +39 -0
  73. src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/references/hierarchy_patterns.md +22 -0
  74. src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/SKILL.md +39 -0
  75. src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/references/mechanism_commit_patterns.md +24 -0
  76. src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/SKILL.md +39 -0
  77. src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/references/coordination_patterns.md +22 -0
  78. src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/SKILL.md +41 -0
  79. src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/references/speculation_patterns.md +22 -0
  80. src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/SKILL.md +39 -0
  81. src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/references/modularity_patterns.md +22 -0
  82. src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/SKILL.md +40 -0
  83. src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/references/transfer_patterns.md +24 -0
  84. src/agents/idea_agent/example/idea_result.json +900 -0
  85. src/agents/idea_agent/run.py +276 -0
  86. src/agents/idea_agent/tests/test_advanced_analysis_contract.py +90 -0
  87. src/agents/idea_agent/tests/test_gap_hypothesis_bridge.py +166 -0
  88. src/agents/idea_agent/tests/test_idea_debate.py +480 -0
  89. src/agents/idea_agent/tests/test_idea_result_v5.py +99 -0
  90. src/agents/idea_agent/tests/test_idea_synthesis.py +92 -0
  91. src/agents/idea_agent/tests/test_mature_idea_sources.py +27 -0
  92. src/agents/idea_agent/tests/test_multimodal_idea_integration.py +386 -0
  93. src/agents/idea_agent/tests/test_profile_materialization_and_scoring.py +219 -0
  94. src/agents/idea_agent/tests/test_profile_native_skill_rendering.py +122 -0
  95. src/agents/idea_agent/tests/test_result_persistence.py +183 -0
  96. src/agents/idea_agent/tests/test_scientific_intervention_profiles.py +241 -0
  97. src/agents/idea_agent/tests/test_scientific_rubric_and_prompts.py +559 -0
  98. src/agents/idea_agent/tests/test_strict_json_object_responses.py +48 -0
  99. src/agents/idea_agent/utils/core/__init__.py +1 -0
  100. src/agents/idea_agent/utils/core/ablation_inputs.py +60 -0
  101. src/agents/idea_agent/utils/core/chat_errors.py +95 -0
  102. src/agents/idea_agent/utils/core/chat_router.py +130 -0
  103. src/agents/idea_agent/utils/core/chat_transport.py +188 -0
  104. src/agents/idea_agent/utils/core/config_loader.py +166 -0
  105. src/agents/idea_agent/utils/core/json_utils.py +28 -0
  106. src/agents/idea_agent/utils/core/logger.py +287 -0
  107. src/agents/idea_agent/utils/core/progress.py +58 -0
  108. src/agents/idea_agent/utils/core/response_parsing.py +57 -0
  109. src/agents/idea_agent/utils/core/run_inputs.py +198 -0
  110. src/agents/idea_agent/utils/mcts/__init__.py +1 -0
  111. src/agents/idea_agent/utils/mcts/component_novelty.py +342 -0
  112. src/agents/idea_agent/utils/mcts/defect_registry.py +302 -0
  113. src/agents/idea_agent/utils/mcts/idea_routes.py +61 -0
  114. src/agents/idea_agent/utils/mcts/idea_taste_presets.py +312 -0
  115. src/agents/idea_agent/utils/mcts/mcts_helpers.py +738 -0
  116. src/agents/idea_agent/utils/mcts/mcts_runtime.py +3000 -0
  117. src/agents/idea_agent/utils/mcts/scientific_intervention_ontology.py +1561 -0
  118. src/agents/idea_agent/utils/mcts/scientific_rubric.py +211 -0
  119. src/agents/idea_agent/utils/mcts/skill_parsing.py +92 -0
  120. src/agents/idea_agent/utils/papers/__init__.py +1 -0
  121. src/agents/idea_agent/utils/papers/paper_graph_vector_store.py +499 -0
  122. src/agents/idea_agent/utils/papers/paper_repository.py +372 -0
  123. src/agents/idea_agent/utils/prompting/__init__.py +1 -0
  124. src/agents/idea_agent/utils/prompting/prompt_views.py +637 -0
  125. src/agents/idea_agent/utils/workflow/__init__.py +1 -0
  126. src/agents/idea_agent/utils/workflow/advanced_analysis_contract.py +244 -0
  127. src/agents/idea_agent/utils/workflow/gap_hypothesis_bridge.py +461 -0
  128. src/agents/idea_agent/utils/workflow/idea_contract.py +426 -0
  129. src/agents/idea_agent/utils/workflow/idea_debate.py +1238 -0
  130. src/agents/idea_agent/utils/workflow/idea_diversity.py +163 -0
  131. src/agents/idea_agent/utils/workflow/idea_fusion.py +889 -0
  132. src/agents/idea_agent/utils/workflow/idea_helpers.py +478 -0
  133. src/agents/idea_agent/utils/workflow/idea_portfolio.py +422 -0
  134. src/agents/idea_agent/utils/workflow/idea_synthesis.py +482 -0
  135. src/agents/idea_agent/utils/workflow/ligagent_flow.py +581 -0
  136. src/agents/idea_agent/utils/workflow/ligagent_handlers.py +1836 -0
  137. src/agents/idea_agent/utils/workflow/ligagent_helpers.py +1331 -0
  138. src/agents/idea_agent/utils/workflow/ligagent_utils.py +322 -0
  139. src/agents/idea_agent/utils/workflow/mature_idea_sources.py +648 -0
  140. src/agents/idea_agent/utils/workflow/multimodal_data_anchoring.py +578 -0
  141. src/agents/idea_agent/utils/workflow/result_persistence.py +774 -0
  142. src/agents/idea_agent/utils/workflow/stage_contract.py +76 -0
  143. src/agents/idea_agent/utils/workflow/workflow_runtime.py +288 -0
  144. src/agents/research_plan_author/__init__.py +50 -0
  145. src/agents/research_plan_author/artifacts.py +270 -0
  146. src/agents/research_plan_author/authoring_blueprint.py +1246 -0
  147. src/agents/research_plan_author/bibtex_renderer.py +252 -0
  148. src/agents/research_plan_author/contract_repair.py +131 -0
  149. src/agents/research_plan_author/contracts.py +745 -0
  150. src/agents/research_plan_author/cross_section_editor.py +232 -0
  151. src/agents/research_plan_author/document_quality.py +987 -0
  152. src/agents/research_plan_author/idea_evolution.py +223 -0
  153. src/agents/research_plan_author/input_loader.py +76 -0
  154. src/agents/research_plan_author/latex_compiler.py +260 -0
  155. src/agents/research_plan_author/latex_safety.py +159 -0
  156. src/agents/research_plan_author/llm_json.py +74 -0
  157. src/agents/research_plan_author/markdown_renderer.py +275 -0
  158. src/agents/research_plan_author/pdf_validator.py +149 -0
  159. src/agents/research_plan_author/render.py +166 -0
  160. src/agents/research_plan_author/render_artifacts.py +187 -0
  161. src/agents/research_plan_author/run.py +765 -0
  162. src/agents/research_plan_author/run_logging.py +209 -0
  163. src/agents/research_plan_author/section_cache.py +184 -0
  164. src/agents/research_plan_author/section_composer.py +1764 -0
  165. src/agents/research_plan_author/section_router.py +209 -0
  166. src/agents/research_plan_author/semantic_validator.py +254 -0
  167. src/agents/research_plan_author/source_bundle.py +46 -0
  168. src/agents/research_plan_author/source_registry.py +208 -0
  169. src/agents/research_plan_author/survey_source_loader.py +137 -0
  170. src/agents/research_plan_author/template_adapter.py +173 -0
  171. src/agents/research_plan_author/template_profile.py +204 -0
  172. src/agents/research_plan_author/tex_renderer.py +496 -0
  173. src/agents/research_plan_author/theory_presentation.py +134 -0
  174. src/agents/research_plan_author/theory_spine.py +703 -0
  175. src/agents/survey_agent/Dockerfile +54 -0
  176. src/agents/survey_agent/README.md +350 -0
  177. src/agents/survey_agent/config/deep_survey.yaml +217 -0
  178. src/agents/survey_agent/config/deep_survey_batch.yaml +176 -0
  179. src/agents/survey_agent/config/deep_survey_fast.yaml +183 -0
  180. src/agents/survey_agent/config/evaluate_baseline.yaml +197 -0
  181. src/agents/survey_agent/config/outcomeRAG.yaml +159 -0
  182. src/agents/survey_agent/env_report.md +348 -0
  183. src/agents/survey_agent/modules/__init__.py +0 -0
  184. src/agents/survey_agent/modules/code_collector.py +2194 -0
  185. src/agents/survey_agent/modules/code_report_generator.py +1282 -0
  186. src/agents/survey_agent/modules/data_manager.py +1713 -0
  187. src/agents/survey_agent/modules/database.py +167 -0
  188. src/agents/survey_agent/modules/fulltext_download_cache.py +462 -0
  189. src/agents/survey_agent/modules/fulltext_http.py +134 -0
  190. src/agents/survey_agent/modules/fulltext_resolution.py +749 -0
  191. src/agents/survey_agent/modules/judge.py +434 -0
  192. src/agents/survey_agent/modules/outcome_RAG.py +236 -0
  193. src/agents/survey_agent/modules/paper_graph_retriever.py +1075 -0
  194. src/agents/survey_agent/modules/pe.py +1729 -0
  195. src/agents/survey_agent/modules/pseudo_creator.py +782 -0
  196. src/agents/survey_agent/modules/pseudo_reviser.py +537 -0
  197. src/agents/survey_agent/modules/refine_agent.py +1952 -0
  198. src/agents/survey_agent/modules/survey_generator.py +5691 -0
  199. src/agents/survey_agent/modules/survey_visualizer.py +1817 -0
  200. src/agents/survey_agent/modules/visual_prompts.py +327 -0
  201. src/agents/survey_agent/modules/work_analyzer.py +2750 -0
  202. src/agents/survey_agent/modules/work_collector.py +5649 -0
  203. src/agents/survey_agent/scripts/baseline_evaluation.py +411 -0
  204. src/agents/survey_agent/scripts/run_deep_survey.py +224 -0
  205. src/agents/survey_agent/scripts/run_deep_survey_adapter.py +407 -0
  206. src/agents/survey_agent/scripts/run_deep_survey_adapter_with_input.py +409 -0
  207. src/agents/survey_agent/scripts/run_deep_survey_batch.py +228 -0
  208. src/agents/survey_agent/scripts/run_deep_survey_batch_arg.py +243 -0
  209. src/agents/survey_agent/scripts/run_deep_survey_batch_settings.py +218 -0
  210. src/agents/survey_agent/scripts/run_keynotes_gen.py +128 -0
  211. src/agents/survey_agent/scripts/run_work_clusters.py +140 -0
  212. src/agents/survey_agent/utils/__init__.py +0 -0
  213. src/agents/survey_agent/utils/ablation_utils.py +317 -0
  214. src/agents/survey_agent/utils/api_call.py +2518 -0
  215. src/agents/survey_agent/utils/chat_utils.py +82 -0
  216. src/agents/survey_agent/utils/config_utils.py +70 -0
  217. src/agents/survey_agent/utils/convert_to_md.py +29 -0
  218. src/agents/survey_agent/utils/err_info.py +24 -0
  219. src/agents/survey_agent/utils/file_utils.py +176 -0
  220. src/agents/survey_agent/utils/gpu_utils.py +145 -0
  221. src/agents/survey_agent/utils/graph_visualizer.py +279 -0
  222. src/agents/survey_agent/utils/html_utils.py +181 -0
  223. src/agents/survey_agent/utils/latex_generator_intra_cluster.py +529 -0
  224. src/agents/survey_agent/utils/latex_generator_relation_table.py +193 -0
  225. src/agents/survey_agent/utils/mineru_section_packer.py +334 -0
  226. src/agents/survey_agent/utils/mineru_utils.py +396 -0
  227. src/agents/survey_agent/utils/paper.py +144 -0
  228. src/agents/survey_agent/utils/read_yaml.py +15 -0
  229. src/agents/survey_agent/utils/repo_utils.py +167 -0
  230. src/agents/survey_agent/utils/rich_logger.py +77 -0
  231. src/agents/survey_agent/utils/step2v2.py +874 -0
  232. src/agents/survey_agent/utils/step2v2_extractor.py +856 -0
  233. src/agents/survey_agent/utils/topic_survey_storage.py +309 -0
  234. src/agents/survey_agent/utils/utils.py +118 -0
  235. src/agents/survey_agent/utils/work.py +2 -0
  236. src/cli.py +1611 -0
  237. src/config/__init__.py +213 -0
  238. src/config/default.yaml +1122 -0
  239. src/harness/LICENSE +21 -0
  240. src/harness/VENDORED.md +14 -0
  241. src/harness/src/__init__.py +0 -0
  242. src/harness/src/openharness/__init__.py +0 -0
  243. src/harness/src/openharness/__main__.py +6 -0
  244. src/harness/src/openharness/api/__init__.py +21 -0
  245. src/harness/src/openharness/api/client.py +281 -0
  246. src/harness/src/openharness/api/codex_client.py +407 -0
  247. src/harness/src/openharness/api/copilot_auth.py +240 -0
  248. src/harness/src/openharness/api/copilot_client.py +134 -0
  249. src/harness/src/openharness/api/errors.py +19 -0
  250. src/harness/src/openharness/api/openai_client.py +577 -0
  251. src/harness/src/openharness/api/provider.py +186 -0
  252. src/harness/src/openharness/api/registry.py +437 -0
  253. src/harness/src/openharness/api/usage.py +17 -0
  254. src/harness/src/openharness/auth/__init__.py +29 -0
  255. src/harness/src/openharness/auth/external.py +610 -0
  256. src/harness/src/openharness/auth/flows.py +189 -0
  257. src/harness/src/openharness/auth/manager.py +482 -0
  258. src/harness/src/openharness/auth/storage.py +269 -0
  259. src/harness/src/openharness/autopilot/__init__.py +23 -0
  260. src/harness/src/openharness/autopilot/service.py +2239 -0
  261. src/harness/src/openharness/autopilot/types.py +91 -0
  262. src/harness/src/openharness/bridge/__init__.py +20 -0
  263. src/harness/src/openharness/bridge/manager.py +106 -0
  264. src/harness/src/openharness/bridge/session_runner.py +46 -0
  265. src/harness/src/openharness/bridge/types.py +37 -0
  266. src/harness/src/openharness/bridge/work_secret.py +41 -0
  267. src/harness/src/openharness/channels/UPSTREAM +6 -0
  268. src/harness/src/openharness/channels/__init__.py +22 -0
  269. src/harness/src/openharness/channels/adapter.py +131 -0
  270. src/harness/src/openharness/channels/bus/__init__.py +6 -0
  271. src/harness/src/openharness/channels/bus/events.py +38 -0
  272. src/harness/src/openharness/channels/bus/queue.py +44 -0
  273. src/harness/src/openharness/channels/impl/__init__.py +6 -0
  274. src/harness/src/openharness/channels/impl/base.py +141 -0
  275. src/harness/src/openharness/channels/impl/dingtalk.py +445 -0
  276. src/harness/src/openharness/channels/impl/discord.py +311 -0
  277. src/harness/src/openharness/channels/impl/email.py +410 -0
  278. src/harness/src/openharness/channels/impl/feishu.py +1342 -0
  279. src/harness/src/openharness/channels/impl/manager.py +257 -0
  280. src/harness/src/openharness/channels/impl/matrix.py +700 -0
  281. src/harness/src/openharness/channels/impl/mochat.py +897 -0
  282. src/harness/src/openharness/channels/impl/qq.py +141 -0
  283. src/harness/src/openharness/channels/impl/slack.py +284 -0
  284. src/harness/src/openharness/channels/impl/telegram.py +525 -0
  285. src/harness/src/openharness/channels/impl/whatsapp.py +159 -0
  286. src/harness/src/openharness/cli.py +2551 -0
  287. src/harness/src/openharness/commands/__init__.py +21 -0
  288. src/harness/src/openharness/commands/registry.py +2783 -0
  289. src/harness/src/openharness/config/__init__.py +34 -0
  290. src/harness/src/openharness/config/paths.py +160 -0
  291. src/harness/src/openharness/config/schema.py +119 -0
  292. src/harness/src/openharness/config/settings.py +1104 -0
  293. src/harness/src/openharness/coordinator/__init__.py +12 -0
  294. src/harness/src/openharness/coordinator/agent_definitions.py +975 -0
  295. src/harness/src/openharness/coordinator/coordinator_mode.py +520 -0
  296. src/harness/src/openharness/engine/__init__.py +80 -0
  297. src/harness/src/openharness/engine/cost_tracker.py +24 -0
  298. src/harness/src/openharness/engine/messages.py +222 -0
  299. src/harness/src/openharness/engine/query.py +1188 -0
  300. src/harness/src/openharness/engine/query_engine.py +313 -0
  301. src/harness/src/openharness/engine/stream_events.py +90 -0
  302. src/harness/src/openharness/hooks/__init__.py +50 -0
  303. src/harness/src/openharness/hooks/events.py +20 -0
  304. src/harness/src/openharness/hooks/executor.py +242 -0
  305. src/harness/src/openharness/hooks/hot_reload.py +31 -0
  306. src/harness/src/openharness/hooks/loader.py +68 -0
  307. src/harness/src/openharness/hooks/schemas.py +66 -0
  308. src/harness/src/openharness/hooks/types.py +38 -0
  309. src/harness/src/openharness/keybindings/__init__.py +14 -0
  310. src/harness/src/openharness/keybindings/default_bindings.py +11 -0
  311. src/harness/src/openharness/keybindings/loader.py +22 -0
  312. src/harness/src/openharness/keybindings/parser.py +18 -0
  313. src/harness/src/openharness/keybindings/resolver.py +13 -0
  314. src/harness/src/openharness/mcp/__init__.py +79 -0
  315. src/harness/src/openharness/mcp/client.py +298 -0
  316. src/harness/src/openharness/mcp/config.py +16 -0
  317. src/harness/src/openharness/mcp/types.py +76 -0
  318. src/harness/src/openharness/memory/__init__.py +25 -0
  319. src/harness/src/openharness/memory/agent.py +91 -0
  320. src/harness/src/openharness/memory/manager.py +183 -0
  321. src/harness/src/openharness/memory/memdir.py +52 -0
  322. src/harness/src/openharness/memory/migrate.py +137 -0
  323. src/harness/src/openharness/memory/paths.py +22 -0
  324. src/harness/src/openharness/memory/relevance.py +145 -0
  325. src/harness/src/openharness/memory/scan.py +116 -0
  326. src/harness/src/openharness/memory/schema.py +443 -0
  327. src/harness/src/openharness/memory/search.py +71 -0
  328. src/harness/src/openharness/memory/team.py +90 -0
  329. src/harness/src/openharness/memory/types.py +33 -0
  330. src/harness/src/openharness/memory/usage.py +153 -0
  331. src/harness/src/openharness/output_styles/__init__.py +5 -0
  332. src/harness/src/openharness/output_styles/loader.py +42 -0
  333. src/harness/src/openharness/permissions/__init__.py +26 -0
  334. src/harness/src/openharness/permissions/checker.py +200 -0
  335. src/harness/src/openharness/permissions/modes.py +13 -0
  336. src/harness/src/openharness/personalization/__init__.py +6 -0
  337. src/harness/src/openharness/personalization/extractor.py +138 -0
  338. src/harness/src/openharness/personalization/rules.py +64 -0
  339. src/harness/src/openharness/personalization/session_hook.py +65 -0
  340. src/harness/src/openharness/platforms.py +86 -0
  341. src/harness/src/openharness/plugins/__init__.py +53 -0
  342. src/harness/src/openharness/plugins/bundled/__init__.py +0 -0
  343. src/harness/src/openharness/plugins/installer.py +39 -0
  344. src/harness/src/openharness/plugins/loader.py +730 -0
  345. src/harness/src/openharness/plugins/schemas.py +24 -0
  346. src/harness/src/openharness/plugins/types.py +59 -0
  347. src/harness/src/openharness/prompts/__init__.py +14 -0
  348. src/harness/src/openharness/prompts/claudemd.py +48 -0
  349. src/harness/src/openharness/prompts/context.py +187 -0
  350. src/harness/src/openharness/prompts/environment.py +135 -0
  351. src/harness/src/openharness/prompts/system_prompt.py +109 -0
  352. src/harness/src/openharness/sandbox/Dockerfile +6 -0
  353. src/harness/src/openharness/sandbox/__init__.py +33 -0
  354. src/harness/src/openharness/sandbox/adapter.py +148 -0
  355. src/harness/src/openharness/sandbox/docker_backend.py +232 -0
  356. src/harness/src/openharness/sandbox/docker_image.py +103 -0
  357. src/harness/src/openharness/sandbox/path_validator.py +37 -0
  358. src/harness/src/openharness/sandbox/session.py +63 -0
  359. src/harness/src/openharness/services/__init__.py +30 -0
  360. src/harness/src/openharness/services/autodream/__init__.py +34 -0
  361. src/harness/src/openharness/services/autodream/backup.py +104 -0
  362. src/harness/src/openharness/services/autodream/lock.py +138 -0
  363. src/harness/src/openharness/services/autodream/prompt.py +128 -0
  364. src/harness/src/openharness/services/autodream/service.py +313 -0
  365. src/harness/src/openharness/services/compact/__init__.py +1725 -0
  366. src/harness/src/openharness/services/cron.py +137 -0
  367. src/harness/src/openharness/services/cron_scheduler.py +595 -0
  368. src/harness/src/openharness/services/lsp/__init__.py +216 -0
  369. src/harness/src/openharness/services/memory_extract/__init__.py +261 -0
  370. src/harness/src/openharness/services/oauth/__init__.py +0 -0
  371. src/harness/src/openharness/services/session_backend.py +97 -0
  372. src/harness/src/openharness/services/session_memory/__init__.py +139 -0
  373. src/harness/src/openharness/services/session_storage.py +230 -0
  374. src/harness/src/openharness/services/token_estimation.py +15 -0
  375. src/harness/src/openharness/services/tool_outputs.py +55 -0
  376. src/harness/src/openharness/skills/__init__.py +44 -0
  377. src/harness/src/openharness/skills/_frontmatter.py +97 -0
  378. src/harness/src/openharness/skills/bundled/__init__.py +75 -0
  379. src/harness/src/openharness/skills/bundled/content/commit.md +25 -0
  380. src/harness/src/openharness/skills/bundled/content/debug.md +25 -0
  381. src/harness/src/openharness/skills/bundled/content/diagnose.md +35 -0
  382. src/harness/src/openharness/skills/bundled/content/plan.md +39 -0
  383. src/harness/src/openharness/skills/bundled/content/review.md +33 -0
  384. src/harness/src/openharness/skills/bundled/content/simplify.md +26 -0
  385. src/harness/src/openharness/skills/bundled/content/skill-creator.md +175 -0
  386. src/harness/src/openharness/skills/bundled/content/test.md +38 -0
  387. src/harness/src/openharness/skills/loader.py +229 -0
  388. src/harness/src/openharness/skills/registry.py +29 -0
  389. src/harness/src/openharness/skills/types.py +24 -0
  390. src/harness/src/openharness/state/__init__.py +6 -0
  391. src/harness/src/openharness/state/app_state.py +30 -0
  392. src/harness/src/openharness/state/store.py +40 -0
  393. src/harness/src/openharness/swarm/__init__.py +70 -0
  394. src/harness/src/openharness/swarm/in_process.py +693 -0
  395. src/harness/src/openharness/swarm/lockfile.py +24 -0
  396. src/harness/src/openharness/swarm/mailbox.py +522 -0
  397. src/harness/src/openharness/swarm/permission_sync.py +1168 -0
  398. src/harness/src/openharness/swarm/registry.py +410 -0
  399. src/harness/src/openharness/swarm/spawn_utils.py +229 -0
  400. src/harness/src/openharness/swarm/subprocess_backend.py +171 -0
  401. src/harness/src/openharness/swarm/team_lifecycle.py +910 -0
  402. src/harness/src/openharness/swarm/types.py +398 -0
  403. src/harness/src/openharness/swarm/worktree.py +315 -0
  404. src/harness/src/openharness/tasks/__init__.py +18 -0
  405. src/harness/src/openharness/tasks/local_agent_task.py +28 -0
  406. src/harness/src/openharness/tasks/local_shell_task.py +17 -0
  407. src/harness/src/openharness/tasks/manager.py +475 -0
  408. src/harness/src/openharness/tasks/stop_task.py +11 -0
  409. src/harness/src/openharness/tasks/types.py +32 -0
  410. src/harness/src/openharness/themes/__init__.py +21 -0
  411. src/harness/src/openharness/themes/builtin.py +89 -0
  412. src/harness/src/openharness/themes/loader.py +55 -0
  413. src/harness/src/openharness/themes/schema.py +54 -0
  414. src/harness/src/openharness/tools/__init__.py +107 -0
  415. src/harness/src/openharness/tools/agent_tool.py +141 -0
  416. src/harness/src/openharness/tools/ask_user_question_tool.py +46 -0
  417. src/harness/src/openharness/tools/base.py +80 -0
  418. src/harness/src/openharness/tools/bash_tool.py +259 -0
  419. src/harness/src/openharness/tools/brief_tool.py +33 -0
  420. src/harness/src/openharness/tools/config_tool.py +54 -0
  421. src/harness/src/openharness/tools/cron_create_tool.py +105 -0
  422. src/harness/src/openharness/tools/cron_delete_tool.py +32 -0
  423. src/harness/src/openharness/tools/cron_list_tool.py +69 -0
  424. src/harness/src/openharness/tools/cron_toggle_tool.py +37 -0
  425. src/harness/src/openharness/tools/enter_plan_mode_tool.py +28 -0
  426. src/harness/src/openharness/tools/enter_worktree_tool.py +80 -0
  427. src/harness/src/openharness/tools/exit_plan_mode_tool.py +28 -0
  428. src/harness/src/openharness/tools/exit_worktree_tool.py +42 -0
  429. src/harness/src/openharness/tools/file_edit_tool.py +95 -0
  430. src/harness/src/openharness/tools/file_read_tool.py +72 -0
  431. src/harness/src/openharness/tools/file_write_tool.py +87 -0
  432. src/harness/src/openharness/tools/glob_tool.py +175 -0
  433. src/harness/src/openharness/tools/grep_tool.py +374 -0
  434. src/harness/src/openharness/tools/image_generation_tool.py +447 -0
  435. src/harness/src/openharness/tools/image_to_text_tool.py +237 -0
  436. src/harness/src/openharness/tools/list_mcp_resources_tool.py +36 -0
  437. src/harness/src/openharness/tools/lsp_tool.py +154 -0
  438. src/harness/src/openharness/tools/mcp_auth_tool.py +71 -0
  439. src/harness/src/openharness/tools/mcp_tool.py +72 -0
  440. src/harness/src/openharness/tools/notebook_edit_tool.py +97 -0
  441. src/harness/src/openharness/tools/read_mcp_resource_tool.py +38 -0
  442. src/harness/src/openharness/tools/remote_trigger_tool.py +74 -0
  443. src/harness/src/openharness/tools/send_message_tool.py +58 -0
  444. src/harness/src/openharness/tools/skill_tool.py +43 -0
  445. src/harness/src/openharness/tools/sleep_tool.py +32 -0
  446. src/harness/src/openharness/tools/task_create_tool.py +56 -0
  447. src/harness/src/openharness/tools/task_get_tool.py +33 -0
  448. src/harness/src/openharness/tools/task_list_tool.py +35 -0
  449. src/harness/src/openharness/tools/task_output_tool.py +35 -0
  450. src/harness/src/openharness/tools/task_stop_tool.py +30 -0
  451. src/harness/src/openharness/tools/task_update_tool.py +50 -0
  452. src/harness/src/openharness/tools/team_create_tool.py +31 -0
  453. src/harness/src/openharness/tools/team_delete_tool.py +30 -0
  454. src/harness/src/openharness/tools/todo_write_tool.py +46 -0
  455. src/harness/src/openharness/tools/tool_search_tool.py +38 -0
  456. src/harness/src/openharness/tools/web_fetch_tool.py +117 -0
  457. src/harness/src/openharness/tools/web_search_tool.py +119 -0
  458. src/harness/src/openharness/ui/__init__.py +5 -0
  459. src/harness/src/openharness/ui/app.py +320 -0
  460. src/harness/src/openharness/ui/backend_host.py +941 -0
  461. src/harness/src/openharness/ui/coordinator_drain.py +198 -0
  462. src/harness/src/openharness/ui/input.py +32 -0
  463. src/harness/src/openharness/ui/output.py +265 -0
  464. src/harness/src/openharness/ui/permission_dialog.py +14 -0
  465. src/harness/src/openharness/ui/protocol.py +247 -0
  466. src/harness/src/openharness/ui/react_launcher.py +179 -0
  467. src/harness/src/openharness/ui/runtime.py +799 -0
  468. src/harness/src/openharness/ui/textual_app.py +495 -0
  469. src/harness/src/openharness/utils/__init__.py +0 -0
  470. src/harness/src/openharness/utils/file_lock.py +86 -0
  471. src/harness/src/openharness/utils/fs.py +98 -0
  472. src/harness/src/openharness/utils/helpers.py +77 -0
  473. src/harness/src/openharness/utils/network_guard.py +340 -0
  474. src/harness/src/openharness/utils/shell.py +147 -0
  475. src/harness/src/openharness/vim/__init__.py +5 -0
  476. src/harness/src/openharness/vim/transitions.py +8 -0
  477. src/harness/src/openharness/voice/__init__.py +7 -0
  478. src/harness/src/openharness/voice/keyterms.py +10 -0
  479. src/harness/src/openharness/voice/stream_stt.py +8 -0
  480. src/harness/src/openharness/voice/voice_mode.py +44 -0
  481. src/llm/__init__.py +29 -0
  482. src/llm/image_generation.py +402 -0
  483. src/llm/provider_registry.py +419 -0
  484. src/llm/runtime_env.py +64 -0
  485. src/llm/structured_output.py +131 -0
  486. src/llm/vision.py +198 -0
  487. src/memory/__init__.py +2 -0
  488. src/memory/api/__init__.py +1 -0
  489. src/memory/api/base_memory_system_api.py +98 -0
  490. src/memory/api/base_symbolic_memory_system_api.py +190 -0
  491. src/memory/api/base_vector_memory_system_api.py +116 -0
  492. src/memory/api/faiss_memory_system_api.py +228 -0
  493. src/memory/api/slot_process_api.py +827 -0
  494. src/memory/api/symbolic_memory_system_api.py +572 -0
  495. src/memory/memory_system/__init__.py +48 -0
  496. src/memory/memory_system/component_taxonomy.py +430 -0
  497. src/memory/memory_system/llm.py +437 -0
  498. src/memory/memory_system/models.py +168 -0
  499. src/memory/memory_system/user_prompt.py +418 -0
  500. src/memory/memory_system/utils.py +282 -0
  501. src/memory/memory_system/vectorstore.py +311 -0
  502. src/memory/memory_system/working_slot.py +99 -0
  503. src/pipeline/__init__.py +129 -0
  504. src/pipeline/contracts.py +531 -0
  505. src/pipeline/discipline_taxonomy.py +916 -0
  506. src/pipeline/evidence_coverage_ledger.py +1150 -0
  507. src/pipeline/evidence_refinement.py +620 -0
  508. src/pipeline/experiment_to_symbolic.py +137 -0
  509. src/pipeline/multimodal_evidence/__init__.py +28 -0
  510. src/pipeline/multimodal_evidence/capabilities.py +107 -0
  511. src/pipeline/multimodal_evidence/claims.py +135 -0
  512. src/pipeline/multimodal_evidence/contract.py +291 -0
  513. src/pipeline/multimodal_evidence/data_sh_compiler.py +505 -0
  514. src/pipeline/multimodal_evidence/handoff_binding.py +135 -0
  515. src/pipeline/multimodal_evidence/manifest.py +254 -0
  516. src/pipeline/multimodal_evidence/native_analysis.py +479 -0
  517. src/pipeline/multimodal_evidence/perception.py +277 -0
  518. src/pipeline/multimodal_evidence/query_binding.py +65 -0
  519. src/pipeline/multimodal_evidence/rendering.py +120 -0
  520. src/pipeline/multimodal_evidence/safety.py +42 -0
  521. src/pipeline/multimodal_evidence/sampling.py +69 -0
  522. src/pipeline/multimodal_evidence/service.py +148 -0
  523. src/pipeline/multimodal_evidence/survey_integration.py +460 -0
  524. src/pipeline/paper_identity.py +52 -0
  525. src/pipeline/research_design_inventory.py +338 -0
  526. src/pipeline/research_domain_resolution.py +669 -0
  527. src/pipeline/research_identity.py +548 -0
  528. src/pipeline/research_question_contract.py +646 -0
  529. src/pipeline/retrieval_lanes.py +889 -0
  530. src/pipeline/run_loop.py +862 -0
  531. src/pipeline/science_events.py +30 -0
  532. src/pipeline/science_manifests.py +602 -0
  533. src/pipeline/science_run.py +918 -0
  534. src/pipeline/science_stages.py +709 -0
  535. src/pipeline/science_workflow.py +807 -0
  536. src/pipeline/sh_cluster_projection.py +306 -0
  537. src/pipeline/sh_graph_provenance.py +648 -0
  538. src/pipeline/subhypothesis_decomposition.py +392 -0
  539. src/pipeline/subhypothesis_evidence.py +856 -0
  540. src/pipeline/survey_evidence_plan.py +858 -0
  541. src/pipeline/survey_gap_adjudication.py +647 -0
  542. src/pipeline/survey_gap_candidates.py +840 -0
  543. src/pipeline/survey_gap_ledger.py +683 -0
  544. src/pipeline/survey_gap_triage.py +401 -0
  545. src/pipeline/survey_handoff_persistence.py +481 -0
  546. src/pipeline/survey_handoff_projection.py +457 -0
  547. src/pipeline/survey_idea_handoff.py +1112 -0
  548. src/pipeline/survey_idea_loader.py +289 -0
  549. src/research_run_ids.py +11 -0
@@ -0,0 +1,1168 @@
1
+ """Permission sync protocol for leader-worker coordination in OpenHarness swarms.
2
+
3
+ Provides both file-based (pending/resolved directories) and mailbox-based
4
+ permission request/response coordination between swarm workers and the leader.
5
+
6
+ File-based flow (directory storage):
7
+ 1. Worker calls ``write_permission_request()`` → pending/{id}.json
8
+ 2. Leader calls ``read_pending_permissions()`` to list pending requests
9
+ 3. Leader calls ``resolve_permission()`` → moves to resolved/{id}.json
10
+ 4. Worker calls ``read_resolved_permission(id)`` or ``poll_for_response(id)``
11
+
12
+ Mailbox-based flow:
13
+ 1. Worker calls ``send_permission_request_via_mailbox()``
14
+ 2. Leader polls mailbox, sends response via ``send_permission_response_via_mailbox()``
15
+ 3. Worker calls ``poll_permission_response()`` on its own mailbox
16
+
17
+ Paths:
18
+ ~/.openharness/teams/<teamName>/permissions/pending/<id>.json
19
+ ~/.openharness/teams/<teamName>/permissions/resolved/<id>.json
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import json
26
+ import os
27
+ import random
28
+ import string
29
+ import time
30
+ import uuid
31
+ from dataclasses import dataclass, field
32
+ from pathlib import Path
33
+ from typing import TYPE_CHECKING, Any, Literal
34
+
35
+ from openharness.swarm.lockfile import exclusive_file_lock
36
+ from openharness.swarm.mailbox import (
37
+ MailboxMessage,
38
+ TeammateMailbox,
39
+ create_permission_request_message,
40
+ create_permission_response_message,
41
+ create_sandbox_permission_request_message,
42
+ create_sandbox_permission_response_message,
43
+ get_team_dir,
44
+ write_to_mailbox,
45
+ )
46
+
47
+ if TYPE_CHECKING:
48
+ from openharness.permissions.checker import PermissionChecker
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Environment helpers
53
+ # ---------------------------------------------------------------------------
54
+
55
+
56
+ def _get_team_name() -> str | None:
57
+ return os.environ.get("CLAUDE_CODE_TEAM_NAME")
58
+
59
+
60
+ def _get_agent_id() -> str | None:
61
+ return os.environ.get("CLAUDE_CODE_AGENT_ID")
62
+
63
+
64
+ def _get_agent_name() -> str | None:
65
+ return os.environ.get("CLAUDE_CODE_AGENT_NAME")
66
+
67
+
68
+ def _get_teammate_color() -> str | None:
69
+ return os.environ.get("CLAUDE_CODE_AGENT_COLOR")
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Read-only tool heuristic
74
+ # ---------------------------------------------------------------------------
75
+
76
+ _READ_ONLY_TOOLS: frozenset[str] = frozenset(
77
+ {
78
+ "read_file",
79
+ "glob",
80
+ "grep",
81
+ "web_fetch",
82
+ "web_search",
83
+ "task_get",
84
+ "task_list",
85
+ "task_output",
86
+ "cron_list",
87
+ }
88
+ )
89
+
90
+
91
+ def _is_read_only(tool_name: str) -> bool:
92
+ """Return True for tools that are considered safe/read-only."""
93
+ return tool_name in _READ_ONLY_TOOLS
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Data structures
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ @dataclass
102
+ class SwarmPermissionRequest:
103
+ """Permission request forwarded from a worker to the team leader.
104
+
105
+ All fields are present to match the TS SwarmPermissionRequestSchema.
106
+ """
107
+
108
+ id: str
109
+ """Unique identifier for this request."""
110
+
111
+ worker_id: str
112
+ """The requesting worker's agent ID (CLAUDE_CODE_AGENT_ID)."""
113
+
114
+ worker_name: str
115
+ """The requesting worker's agent name (CLAUDE_CODE_AGENT_NAME)."""
116
+
117
+ team_name: str
118
+ """Team name for routing."""
119
+
120
+ tool_name: str
121
+ """Name of the tool requiring permission (e.g. 'Bash', 'Edit')."""
122
+
123
+ tool_use_id: str
124
+ """Original tool-use ID from the worker's execution context."""
125
+
126
+ description: str
127
+ """Human-readable description of the requested operation."""
128
+
129
+ input: dict[str, Any]
130
+ """Serialized tool input parameters."""
131
+
132
+ # Optional / defaulted fields
133
+ permission_suggestions: list[Any] = field(default_factory=list)
134
+ """Suggested rule updates produced by the worker's local permission system."""
135
+
136
+ worker_color: str | None = None
137
+ """The requesting worker's assigned color (CLAUDE_CODE_AGENT_COLOR)."""
138
+
139
+ status: Literal["pending", "approved", "rejected"] = "pending"
140
+ """Current status of the request."""
141
+
142
+ resolved_by: Literal["worker", "leader"] | None = None
143
+ """Who resolved the request."""
144
+
145
+ resolved_at: float | None = None
146
+ """Timestamp (seconds since epoch) when the request was resolved."""
147
+
148
+ feedback: str | None = None
149
+ """Optional rejection reason or leader comment."""
150
+
151
+ updated_input: dict[str, Any] | None = None
152
+ """Modified input if changed by the resolver."""
153
+
154
+ permission_updates: list[Any] | None = None
155
+ """'Always allow' rules applied during resolution."""
156
+
157
+ created_at: float = field(default_factory=time.time)
158
+ """Timestamp when request was created."""
159
+
160
+ def to_dict(self) -> dict[str, Any]:
161
+ return {
162
+ "id": self.id,
163
+ "worker_id": self.worker_id,
164
+ "worker_name": self.worker_name,
165
+ "team_name": self.team_name,
166
+ "tool_name": self.tool_name,
167
+ "tool_use_id": self.tool_use_id,
168
+ "description": self.description,
169
+ "input": self.input,
170
+ "permission_suggestions": self.permission_suggestions,
171
+ "worker_color": self.worker_color,
172
+ "status": self.status,
173
+ "resolved_by": self.resolved_by,
174
+ "resolved_at": self.resolved_at,
175
+ "feedback": self.feedback,
176
+ "updated_input": self.updated_input,
177
+ "permission_updates": self.permission_updates,
178
+ "created_at": self.created_at,
179
+ }
180
+
181
+ @classmethod
182
+ def from_dict(cls, data: dict[str, Any]) -> "SwarmPermissionRequest":
183
+ return cls(
184
+ id=data["id"],
185
+ worker_id=data.get("worker_id", data.get("workerId", "")),
186
+ worker_name=data.get("worker_name", data.get("workerName", "")),
187
+ team_name=data.get("team_name", data.get("teamName", "")),
188
+ tool_name=data.get("tool_name", data.get("toolName", "")),
189
+ tool_use_id=data.get("tool_use_id", data.get("toolUseId", "")),
190
+ description=data.get("description", ""),
191
+ input=data.get("input", {}),
192
+ permission_suggestions=data.get(
193
+ "permission_suggestions",
194
+ data.get("permissionSuggestions", []),
195
+ ),
196
+ worker_color=data.get("worker_color", data.get("workerColor")),
197
+ status=data.get("status", "pending"),
198
+ resolved_by=data.get("resolved_by", data.get("resolvedBy")),
199
+ resolved_at=data.get("resolved_at", data.get("resolvedAt")),
200
+ feedback=data.get("feedback"),
201
+ updated_input=data.get("updated_input", data.get("updatedInput")),
202
+ permission_updates=data.get(
203
+ "permission_updates", data.get("permissionUpdates")
204
+ ),
205
+ created_at=data.get("created_at", data.get("createdAt", time.time())),
206
+ )
207
+
208
+
209
+ @dataclass
210
+ class PermissionResolution:
211
+ """Resolution data returned when leader/worker resolves a request."""
212
+
213
+ decision: Literal["approved", "rejected"]
214
+ """Decision: approved or rejected."""
215
+
216
+ resolved_by: Literal["worker", "leader"]
217
+ """Who resolved the request."""
218
+
219
+ feedback: str | None = None
220
+ """Optional feedback message if rejected."""
221
+
222
+ updated_input: dict[str, Any] | None = None
223
+ """Optional updated input if the resolver modified it."""
224
+
225
+ permission_updates: list[Any] | None = None
226
+ """Permission updates to apply (e.g. 'always allow' rules)."""
227
+
228
+
229
+ @dataclass
230
+ class PermissionResponse:
231
+ """Legacy response type for worker polling (backward compatibility)."""
232
+
233
+ request_id: str
234
+ """ID of the request this responds to."""
235
+
236
+ decision: Literal["approved", "denied"]
237
+ """Decision: approved or denied."""
238
+
239
+ timestamp: str
240
+ """ISO timestamp when response was created."""
241
+
242
+ feedback: str | None = None
243
+ """Optional feedback message if denied."""
244
+
245
+ updated_input: dict[str, Any] | None = None
246
+ """Optional updated input if the resolver modified it."""
247
+
248
+ permission_updates: list[Any] | None = None
249
+ """Permission updates to apply."""
250
+
251
+
252
+ @dataclass
253
+ class SwarmPermissionResponse:
254
+ """Response sent from the leader back to the requesting worker."""
255
+
256
+ request_id: str
257
+ """ID of the ``SwarmPermissionRequest`` this responds to."""
258
+
259
+ allowed: bool
260
+ """True if the tool use is approved."""
261
+
262
+ feedback: str | None = None
263
+ """Optional rejection reason or leader comment."""
264
+
265
+ updated_rules: list[dict[str, Any]] = field(default_factory=list)
266
+ """Permission-rule updates the leader decided to apply."""
267
+
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # Request ID generation
271
+ # ---------------------------------------------------------------------------
272
+
273
+
274
+ def generate_request_id() -> str:
275
+ """Generate a unique permission request ID.
276
+
277
+ Format: ``perm-{timestamp_ms}-{random7}``, matching the TS implementation:
278
+ ``perm-${Date.now()}-${Math.random().toString(36).substring(2, 9)}``
279
+ """
280
+ ts = int(time.time() * 1000)
281
+ rand = "".join(random.choices(string.ascii_lowercase + string.digits, k=7))
282
+ return f"perm-{ts}-{rand}"
283
+
284
+
285
+ def generate_sandbox_request_id() -> str:
286
+ """Generate a unique sandbox permission request ID.
287
+
288
+ Format: ``sandbox-{timestamp_ms}-{random7}``.
289
+ """
290
+ ts = int(time.time() * 1000)
291
+ rand = "".join(random.choices(string.ascii_lowercase + string.digits, k=7))
292
+ return f"sandbox-{ts}-{rand}"
293
+
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # Permission directory helpers
297
+ # ---------------------------------------------------------------------------
298
+
299
+
300
+ def get_permission_dir(team_name: str) -> Path:
301
+ """Return ~/.openharness/teams/{teamName}/permissions/"""
302
+ return get_team_dir(team_name) / "permissions"
303
+
304
+
305
+ def _get_pending_dir(team_name: str) -> Path:
306
+ return get_permission_dir(team_name) / "pending"
307
+
308
+
309
+ def _get_resolved_dir(team_name: str) -> Path:
310
+ return get_permission_dir(team_name) / "resolved"
311
+
312
+
313
+ def _ensure_permission_dirs(team_name: str) -> None:
314
+ for d in (
315
+ get_permission_dir(team_name),
316
+ _get_pending_dir(team_name),
317
+ _get_resolved_dir(team_name),
318
+ ):
319
+ d.mkdir(parents=True, exist_ok=True)
320
+
321
+
322
+ def _pending_request_path(team_name: str, request_id: str) -> Path:
323
+ return _get_pending_dir(team_name) / f"{request_id}.json"
324
+
325
+
326
+ def _resolved_request_path(team_name: str, request_id: str) -> Path:
327
+ return _get_resolved_dir(team_name) / f"{request_id}.json"
328
+
329
+
330
+ # ---------------------------------------------------------------------------
331
+ # Factory
332
+ # ---------------------------------------------------------------------------
333
+
334
+
335
+ def create_permission_request(
336
+ tool_name: str,
337
+ tool_use_id: str,
338
+ tool_input: dict[str, Any],
339
+ description: str = "",
340
+ permission_suggestions: list[Any] | None = None,
341
+ team_name: str | None = None,
342
+ worker_id: str | None = None,
343
+ worker_name: str | None = None,
344
+ worker_color: str | None = None,
345
+ ) -> SwarmPermissionRequest:
346
+ """Build a new :class:`SwarmPermissionRequest` with a generated ID.
347
+
348
+ Missing worker/team fields are read from environment variables
349
+ (``CLAUDE_CODE_AGENT_ID``, ``CLAUDE_CODE_AGENT_NAME``,
350
+ ``CLAUDE_CODE_TEAM_NAME``, ``CLAUDE_CODE_AGENT_COLOR``).
351
+
352
+ Args:
353
+ tool_name: Name of the tool requesting permission.
354
+ tool_use_id: Original tool-use ID from the execution context.
355
+ tool_input: The tool's input parameters.
356
+ description: Optional human-readable description of the operation.
357
+ permission_suggestions: Optional list of suggested permission-rule dicts.
358
+ team_name: Team name (falls back to ``CLAUDE_CODE_TEAM_NAME``).
359
+ worker_id: Worker agent ID (falls back to ``CLAUDE_CODE_AGENT_ID``).
360
+ worker_name: Worker agent name (falls back to ``CLAUDE_CODE_AGENT_NAME``).
361
+ worker_color: Worker color (falls back to ``CLAUDE_CODE_AGENT_COLOR``).
362
+
363
+ Returns:
364
+ A new :class:`SwarmPermissionRequest` in *pending* state.
365
+
366
+ Raises:
367
+ ValueError: if team_name, worker_id, or worker_name cannot be resolved.
368
+ """
369
+ resolved_team = team_name or _get_team_name() or ""
370
+ resolved_id = worker_id or _get_agent_id() or ""
371
+ resolved_name = worker_name or _get_agent_name() or ""
372
+ resolved_color = worker_color or _get_teammate_color()
373
+
374
+ return SwarmPermissionRequest(
375
+ id=generate_request_id(),
376
+ worker_id=resolved_id,
377
+ worker_name=resolved_name,
378
+ worker_color=resolved_color,
379
+ team_name=resolved_team,
380
+ tool_name=tool_name,
381
+ tool_use_id=tool_use_id,
382
+ description=description,
383
+ input=tool_input,
384
+ permission_suggestions=permission_suggestions or [],
385
+ status="pending",
386
+ created_at=time.time(),
387
+ )
388
+
389
+
390
+ # ---------------------------------------------------------------------------
391
+ # File-based storage: write / read / resolve / cleanup
392
+ # ---------------------------------------------------------------------------
393
+
394
+
395
+ def _sync_write_permission_request(
396
+ request: SwarmPermissionRequest,
397
+ ) -> SwarmPermissionRequest:
398
+ _ensure_permission_dirs(request.team_name)
399
+ pending_path = _pending_request_path(request.team_name, request.id)
400
+ lock_path = _get_pending_dir(request.team_name) / ".lock"
401
+ tmp_path = pending_path.with_suffix(".json.tmp")
402
+
403
+ with exclusive_file_lock(lock_path):
404
+ tmp_path.write_text(json.dumps(request.to_dict(), indent=2), encoding="utf-8")
405
+ os.replace(tmp_path, pending_path)
406
+ return request
407
+
408
+
409
+ async def write_permission_request(
410
+ request: SwarmPermissionRequest,
411
+ ) -> SwarmPermissionRequest:
412
+ """Write *request* to the pending directory with file locking.
413
+
414
+ Called by worker agents when they need permission approval from the leader.
415
+
416
+ Args:
417
+ request: The permission request to persist.
418
+
419
+ Returns:
420
+ The written request (same object, for convenience).
421
+ """
422
+ loop = asyncio.get_event_loop()
423
+ return await loop.run_in_executor(None, _sync_write_permission_request, request)
424
+
425
+
426
+ async def read_pending_permissions(
427
+ team_name: str | None = None,
428
+ ) -> list[SwarmPermissionRequest]:
429
+ """Read all pending permission requests for a team.
430
+
431
+ Called by the team leader to see what requests need attention. Requests
432
+ are returned sorted oldest-first.
433
+
434
+ Args:
435
+ team_name: Team name (falls back to ``CLAUDE_CODE_TEAM_NAME``).
436
+
437
+ Returns:
438
+ List of pending :class:`SwarmPermissionRequest` objects.
439
+ """
440
+ team = team_name or _get_team_name()
441
+ if not team:
442
+ return []
443
+
444
+ pending_dir = _get_pending_dir(team)
445
+ if not pending_dir.exists():
446
+ return []
447
+
448
+ requests: list[SwarmPermissionRequest] = []
449
+ for path in sorted(pending_dir.glob("*.json")):
450
+ if path.name == ".lock":
451
+ continue
452
+ try:
453
+ data = json.loads(path.read_text(encoding="utf-8"))
454
+ requests.append(SwarmPermissionRequest.from_dict(data))
455
+ except (json.JSONDecodeError, KeyError):
456
+ continue
457
+
458
+ requests.sort(key=lambda r: r.created_at)
459
+ return requests
460
+
461
+
462
+ async def read_resolved_permission(
463
+ request_id: str,
464
+ team_name: str | None = None,
465
+ ) -> SwarmPermissionRequest | None:
466
+ """Read a resolved permission request by ID.
467
+
468
+ Called by workers to check if their request has been resolved.
469
+
470
+ Args:
471
+ request_id: The permission request ID to look up.
472
+ team_name: Team name (falls back to ``CLAUDE_CODE_TEAM_NAME``).
473
+
474
+ Returns:
475
+ The resolved :class:`SwarmPermissionRequest`, or ``None`` if not yet
476
+ resolved.
477
+ """
478
+ team = team_name or _get_team_name()
479
+ if not team:
480
+ return None
481
+
482
+ resolved_path = _resolved_request_path(team, request_id)
483
+ if not resolved_path.exists():
484
+ return None
485
+
486
+ try:
487
+ data = json.loads(resolved_path.read_text(encoding="utf-8"))
488
+ return SwarmPermissionRequest.from_dict(data)
489
+ except (json.JSONDecodeError, KeyError, OSError):
490
+ return None
491
+
492
+
493
+ def _sync_resolve_permission(
494
+ request_id: str,
495
+ resolution: PermissionResolution,
496
+ team: str,
497
+ ) -> bool:
498
+ _ensure_permission_dirs(team)
499
+ pending_path = _pending_request_path(team, request_id)
500
+ resolved_path = _resolved_request_path(team, request_id)
501
+ lock_path = _get_pending_dir(team) / ".lock"
502
+ tmp_path = resolved_path.with_suffix(".json.tmp")
503
+
504
+ with exclusive_file_lock(lock_path):
505
+ if not pending_path.exists():
506
+ return False
507
+
508
+ try:
509
+ data = json.loads(pending_path.read_text(encoding="utf-8"))
510
+ request = SwarmPermissionRequest.from_dict(data)
511
+ except (json.JSONDecodeError, KeyError):
512
+ return False
513
+
514
+ resolved_request = SwarmPermissionRequest(
515
+ id=request.id,
516
+ worker_id=request.worker_id,
517
+ worker_name=request.worker_name,
518
+ worker_color=request.worker_color,
519
+ team_name=request.team_name,
520
+ tool_name=request.tool_name,
521
+ tool_use_id=request.tool_use_id,
522
+ description=request.description,
523
+ input=request.input,
524
+ permission_suggestions=request.permission_suggestions,
525
+ status="approved" if resolution.decision == "approved" else "rejected",
526
+ resolved_by=resolution.resolved_by,
527
+ resolved_at=time.time(),
528
+ feedback=resolution.feedback,
529
+ updated_input=resolution.updated_input,
530
+ permission_updates=resolution.permission_updates,
531
+ created_at=request.created_at,
532
+ )
533
+
534
+ tmp_path.write_text(
535
+ json.dumps(resolved_request.to_dict(), indent=2), encoding="utf-8"
536
+ )
537
+ os.replace(tmp_path, resolved_path)
538
+ try:
539
+ pending_path.unlink()
540
+ except OSError:
541
+ pass
542
+
543
+ return True
544
+
545
+
546
+ async def resolve_permission(
547
+ request_id: str,
548
+ resolution: PermissionResolution,
549
+ team_name: str | None = None,
550
+ ) -> bool:
551
+ """Resolve a permission request, moving it from pending/ to resolved/.
552
+
553
+ Called by the team leader (or worker in self-resolution cases).
554
+
555
+ Args:
556
+ request_id: The permission request ID to resolve.
557
+ resolution: The resolution data (decision, resolvedBy, etc.).
558
+ team_name: Team name (falls back to ``CLAUDE_CODE_TEAM_NAME``).
559
+
560
+ Returns:
561
+ ``True`` if the request was found and resolved, ``False`` otherwise.
562
+ """
563
+ team = team_name or _get_team_name()
564
+ if not team:
565
+ return False
566
+ loop = asyncio.get_event_loop()
567
+ return await loop.run_in_executor(
568
+ None, _sync_resolve_permission, request_id, resolution, team
569
+ )
570
+
571
+
572
+ def _sync_cleanup_old_resolutions(team: str, max_age_seconds: float) -> int:
573
+ resolved_dir = _get_resolved_dir(team)
574
+ if not resolved_dir.exists():
575
+ return 0
576
+
577
+ now = time.time()
578
+ cleaned = 0
579
+
580
+ for path in resolved_dir.glob("*.json"):
581
+ try:
582
+ data = json.loads(path.read_text(encoding="utf-8"))
583
+ resolved_at = data.get("resolved_at") or data.get("created_at", 0)
584
+ if now - resolved_at >= max_age_seconds:
585
+ path.unlink()
586
+ cleaned += 1
587
+ except (json.JSONDecodeError, KeyError, OSError):
588
+ try:
589
+ path.unlink()
590
+ cleaned += 1
591
+ except OSError:
592
+ pass
593
+
594
+ return cleaned
595
+
596
+
597
+ async def cleanup_old_resolutions(
598
+ team_name: str | None = None,
599
+ max_age_seconds: float = 3600.0,
600
+ ) -> int:
601
+ """Clean up old resolved permission files.
602
+
603
+ Called periodically to prevent file accumulation.
604
+
605
+ Args:
606
+ team_name: Team name (falls back to ``CLAUDE_CODE_TEAM_NAME``).
607
+ max_age_seconds: Maximum age in seconds (default: 1 hour).
608
+
609
+ Returns:
610
+ Number of files removed.
611
+ """
612
+ team = team_name or _get_team_name()
613
+ if not team:
614
+ return 0
615
+ loop = asyncio.get_event_loop()
616
+ return await loop.run_in_executor(
617
+ None, _sync_cleanup_old_resolutions, team, max_age_seconds
618
+ )
619
+
620
+
621
+ async def delete_resolved_permission(
622
+ request_id: str,
623
+ team_name: str | None = None,
624
+ ) -> bool:
625
+ """Delete a resolved permission file after a worker has processed it.
626
+
627
+ Args:
628
+ request_id: The permission request ID.
629
+ team_name: Team name (falls back to ``CLAUDE_CODE_TEAM_NAME``).
630
+
631
+ Returns:
632
+ ``True`` if the file was found and deleted, ``False`` otherwise.
633
+ """
634
+ team = team_name or _get_team_name()
635
+ if not team:
636
+ return False
637
+
638
+ resolved_path = _resolved_request_path(team, request_id)
639
+ try:
640
+ resolved_path.unlink()
641
+ return True
642
+ except FileNotFoundError:
643
+ return False
644
+ except OSError:
645
+ return False
646
+
647
+
648
+ # ---------------------------------------------------------------------------
649
+ # Legacy / backward-compat helpers
650
+ # ---------------------------------------------------------------------------
651
+
652
+
653
+ async def poll_for_response(
654
+ request_id: str,
655
+ _agent_name: str | None = None,
656
+ team_name: str | None = None,
657
+ ) -> PermissionResponse | None:
658
+ """Poll for a permission response (worker-side convenience function).
659
+
660
+ Converts the resolved request into the simpler legacy response format.
661
+
662
+ Args:
663
+ request_id: The permission request ID to check.
664
+ _agent_name: Unused; kept for API compatibility.
665
+ team_name: Team name (falls back to ``CLAUDE_CODE_TEAM_NAME``).
666
+
667
+ Returns:
668
+ A :class:`PermissionResponse`, or ``None`` if not yet resolved.
669
+ """
670
+ from datetime import datetime, timezone
671
+
672
+ resolved = await read_resolved_permission(request_id, team_name)
673
+ if not resolved:
674
+ return None
675
+
676
+ ts = resolved.resolved_at or resolved.created_at
677
+ return PermissionResponse(
678
+ request_id=resolved.id,
679
+ decision="approved" if resolved.status == "approved" else "denied",
680
+ timestamp=datetime.fromtimestamp(ts, tz=timezone.utc).strftime(
681
+ "%Y-%m-%dT%H:%M:%S.%f"
682
+ )[:-3]
683
+ + "Z",
684
+ feedback=resolved.feedback,
685
+ updated_input=resolved.updated_input,
686
+ permission_updates=resolved.permission_updates,
687
+ )
688
+
689
+
690
+ async def remove_worker_response(
691
+ request_id: str,
692
+ _agent_name: str | None = None,
693
+ team_name: str | None = None,
694
+ ) -> None:
695
+ """Remove a worker's response after processing (alias for delete_resolved_permission)."""
696
+ await delete_resolved_permission(request_id, team_name)
697
+
698
+
699
+ # Alias: submitPermissionRequest → writePermissionRequest
700
+ submit_permission_request = write_permission_request
701
+
702
+
703
+ # ---------------------------------------------------------------------------
704
+ # Team leader / worker role detection
705
+ # ---------------------------------------------------------------------------
706
+
707
+
708
+ def is_team_leader(team_name: str | None = None) -> bool:
709
+ """Return True if the current agent is a team leader.
710
+
711
+ Team leaders don't have an agent ID set, or their ID is 'team-lead'.
712
+ """
713
+ team = team_name or _get_team_name()
714
+ if not team:
715
+ return False
716
+ agent_id = _get_agent_id()
717
+ return not agent_id or agent_id == "team-lead"
718
+
719
+
720
+ def is_swarm_worker() -> bool:
721
+ """Return True if the current agent is a worker in a swarm."""
722
+ team_name = _get_team_name()
723
+ agent_id = _get_agent_id()
724
+ return bool(team_name) and bool(agent_id) and not is_team_leader()
725
+
726
+
727
+ # ---------------------------------------------------------------------------
728
+ # Leader name lookup
729
+ # ---------------------------------------------------------------------------
730
+
731
+
732
+ async def get_leader_name(team_name: str | None = None) -> str | None:
733
+ """Get the leader's agent name from the team file.
734
+
735
+ This is needed to address permission requests to the leader's mailbox.
736
+
737
+ Args:
738
+ team_name: Team name (falls back to ``CLAUDE_CODE_TEAM_NAME``).
739
+
740
+ Returns:
741
+ The leader's name string, or ``None`` if the team file is missing.
742
+ Falls back to ``'team-lead'`` if the lead member is not found.
743
+ """
744
+ from openharness.swarm.team_lifecycle import read_team_file_async
745
+
746
+ team = team_name or _get_team_name()
747
+ if not team:
748
+ return None
749
+
750
+ team_file = await read_team_file_async(team)
751
+ if not team_file:
752
+ return None
753
+
754
+ lead_id = team_file.lead_agent_id
755
+ if lead_id and lead_id in team_file.members:
756
+ return team_file.members[lead_id].name
757
+
758
+ return "team-lead"
759
+
760
+
761
+ # ---------------------------------------------------------------------------
762
+ # Mailbox-based permission send/receive
763
+ # ---------------------------------------------------------------------------
764
+
765
+
766
+ async def send_permission_request_via_mailbox(
767
+ request: SwarmPermissionRequest,
768
+ ) -> bool:
769
+ """Send a permission request to the leader via the mailbox system.
770
+
771
+ This is the mailbox-based approach for forwarding permission requests.
772
+ Writes a ``permission_request`` message to the leader's mailbox.
773
+
774
+ Args:
775
+ request: The permission request to send.
776
+
777
+ Returns:
778
+ ``True`` if the message was sent successfully.
779
+ """
780
+ leader_name = await get_leader_name(request.team_name)
781
+ if not leader_name:
782
+ return False
783
+
784
+ try:
785
+ msg = create_permission_request_message(
786
+ sender=request.worker_name,
787
+ recipient=leader_name,
788
+ request_data={
789
+ "request_id": request.id,
790
+ "agent_id": request.worker_name,
791
+ "tool_name": request.tool_name,
792
+ "tool_use_id": request.tool_use_id,
793
+ "description": request.description,
794
+ "input": request.input,
795
+ "permission_suggestions": request.permission_suggestions,
796
+ },
797
+ )
798
+
799
+ await write_to_mailbox(
800
+ leader_name,
801
+ {
802
+ "from": request.worker_name,
803
+ "text": json.dumps(msg.payload),
804
+ "timestamp": time.strftime(
805
+ "%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()
806
+ ),
807
+ "color": request.worker_color,
808
+ },
809
+ request.team_name,
810
+ )
811
+ return True
812
+ except OSError:
813
+ return False
814
+
815
+
816
+ async def send_permission_response_via_mailbox(
817
+ worker_name: str,
818
+ resolution: PermissionResolution,
819
+ request_id: str,
820
+ team_name: str | None = None,
821
+ ) -> bool:
822
+ """Send a permission response to a worker via the mailbox system.
823
+
824
+ Called by the leader when approving/denying a permission request.
825
+
826
+ Args:
827
+ worker_name: The worker's name to send the response to.
828
+ resolution: The permission resolution.
829
+ request_id: The original request ID.
830
+ team_name: Team name (falls back to ``CLAUDE_CODE_TEAM_NAME``).
831
+
832
+ Returns:
833
+ ``True`` if the message was sent successfully.
834
+ """
835
+ team = team_name or _get_team_name()
836
+ if not team:
837
+ return False
838
+
839
+ sender_name = _get_agent_name() or "team-lead"
840
+ subtype = "success" if resolution.decision == "approved" else "error"
841
+
842
+ try:
843
+ msg = create_permission_response_message(
844
+ sender=sender_name,
845
+ recipient=worker_name,
846
+ response_data={
847
+ "request_id": request_id,
848
+ "subtype": subtype,
849
+ "error": resolution.feedback if subtype == "error" else None,
850
+ "updated_input": resolution.updated_input,
851
+ "permission_updates": resolution.permission_updates,
852
+ },
853
+ )
854
+
855
+ await write_to_mailbox(
856
+ worker_name,
857
+ {
858
+ "from": sender_name,
859
+ "text": json.dumps(msg.payload),
860
+ "timestamp": time.strftime(
861
+ "%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()
862
+ ),
863
+ },
864
+ team,
865
+ )
866
+ return True
867
+ except OSError:
868
+ return False
869
+
870
+
871
+ # ---------------------------------------------------------------------------
872
+ # Sandbox permission mailbox helpers
873
+ # ---------------------------------------------------------------------------
874
+
875
+
876
+ async def send_sandbox_permission_request_via_mailbox(
877
+ host: str,
878
+ request_id: str,
879
+ team_name: str | None = None,
880
+ ) -> bool:
881
+ """Send a sandbox permission request to the leader via the mailbox system.
882
+
883
+ Called by workers when sandbox runtime needs network access approval.
884
+
885
+ Args:
886
+ host: The host requesting network access.
887
+ request_id: Unique ID for this request.
888
+ team_name: Optional team name.
889
+
890
+ Returns:
891
+ ``True`` if the message was sent successfully.
892
+ """
893
+ team = team_name or _get_team_name()
894
+ if not team:
895
+ return False
896
+
897
+ leader_name = await get_leader_name(team)
898
+ if not leader_name:
899
+ return False
900
+
901
+ worker_id = _get_agent_id()
902
+ worker_name = _get_agent_name()
903
+ worker_color = _get_teammate_color()
904
+
905
+ if not worker_id or not worker_name:
906
+ return False
907
+
908
+ try:
909
+ msg = create_sandbox_permission_request_message(
910
+ sender=worker_name,
911
+ recipient=leader_name,
912
+ request_data={
913
+ "requestId": request_id,
914
+ "workerId": worker_id,
915
+ "workerName": worker_name,
916
+ "workerColor": worker_color,
917
+ "host": host,
918
+ },
919
+ )
920
+
921
+ await write_to_mailbox(
922
+ leader_name,
923
+ {
924
+ "from": worker_name,
925
+ "text": json.dumps(msg.payload),
926
+ "timestamp": time.strftime(
927
+ "%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()
928
+ ),
929
+ "color": worker_color,
930
+ },
931
+ team,
932
+ )
933
+ return True
934
+ except OSError:
935
+ return False
936
+
937
+
938
+ async def send_sandbox_permission_response_via_mailbox(
939
+ worker_name: str,
940
+ request_id: str,
941
+ host: str,
942
+ allow: bool,
943
+ team_name: str | None = None,
944
+ ) -> bool:
945
+ """Send a sandbox permission response to a worker via the mailbox system.
946
+
947
+ Called by the leader when approving/denying a sandbox network access request.
948
+
949
+ Args:
950
+ worker_name: The worker's name to send the response to.
951
+ request_id: The original request ID.
952
+ host: The host that was approved/denied.
953
+ allow: Whether the connection is allowed.
954
+ team_name: Optional team name.
955
+
956
+ Returns:
957
+ ``True`` if the message was sent successfully.
958
+ """
959
+ team = team_name or _get_team_name()
960
+ if not team:
961
+ return False
962
+
963
+ sender_name = _get_agent_name() or "team-lead"
964
+
965
+ try:
966
+ msg = create_sandbox_permission_response_message(
967
+ sender=sender_name,
968
+ recipient=worker_name,
969
+ response_data={
970
+ "requestId": request_id,
971
+ "host": host,
972
+ "allow": allow,
973
+ },
974
+ )
975
+
976
+ await write_to_mailbox(
977
+ worker_name,
978
+ {
979
+ "from": sender_name,
980
+ "text": json.dumps(msg.payload),
981
+ "timestamp": time.strftime(
982
+ "%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()
983
+ ),
984
+ },
985
+ team,
986
+ )
987
+ return True
988
+ except OSError:
989
+ return False
990
+
991
+
992
+ # ---------------------------------------------------------------------------
993
+ # Worker helpers: send request / poll response (original mailbox-only approach)
994
+ # ---------------------------------------------------------------------------
995
+
996
+
997
+ async def send_permission_request(
998
+ request: SwarmPermissionRequest,
999
+ team_name: str,
1000
+ worker_id: str,
1001
+ leader_id: str = "leader",
1002
+ ) -> None:
1003
+ """Serialize *request* and write it to the leader's mailbox.
1004
+
1005
+ This is the original structured-payload approach. For new code prefer
1006
+ :func:`send_permission_request_via_mailbox`.
1007
+
1008
+ Args:
1009
+ request: The permission request to forward.
1010
+ team_name: The swarm team name used for mailbox routing.
1011
+ worker_id: The sending worker's agent ID.
1012
+ leader_id: The leader's agent ID (default ``"leader"``).
1013
+ """
1014
+ payload: dict[str, Any] = {
1015
+ "request_id": request.id,
1016
+ "tool_name": request.tool_name,
1017
+ "tool_use_id": request.tool_use_id,
1018
+ "input": request.input,
1019
+ "description": request.description,
1020
+ "permission_suggestions": request.permission_suggestions,
1021
+ "worker_id": worker_id,
1022
+ }
1023
+ msg = MailboxMessage(
1024
+ id=str(uuid.uuid4()),
1025
+ type="permission_request",
1026
+ sender=worker_id,
1027
+ recipient=leader_id,
1028
+ payload=payload,
1029
+ timestamp=time.time(),
1030
+ )
1031
+ leader_mailbox = TeammateMailbox(team_name, leader_id)
1032
+ await leader_mailbox.write(msg)
1033
+
1034
+
1035
+ async def poll_permission_response(
1036
+ team_name: str,
1037
+ worker_id: str,
1038
+ request_id: str,
1039
+ timeout: float = 60.0,
1040
+ ) -> SwarmPermissionResponse | None:
1041
+ """Poll the worker's own mailbox until a matching ``permission_response`` arrives.
1042
+
1043
+ Checks every 0.5 s up to *timeout* seconds. When a response matching
1044
+ *request_id* is found, the message is marked read and the decoded
1045
+ :class:`SwarmPermissionResponse` is returned.
1046
+
1047
+ Args:
1048
+ team_name: The swarm team name.
1049
+ worker_id: The worker's agent ID (owns this mailbox).
1050
+ request_id: The ``SwarmPermissionRequest.id`` to match against.
1051
+ timeout: Maximum seconds to wait before returning ``None``.
1052
+
1053
+ Returns:
1054
+ A :class:`SwarmPermissionResponse`, or ``None`` on timeout.
1055
+ """
1056
+ worker_mailbox = TeammateMailbox(team_name, worker_id)
1057
+ deadline = time.monotonic() + timeout
1058
+
1059
+ while time.monotonic() < deadline:
1060
+ messages = await worker_mailbox.read_all(unread_only=True)
1061
+ for msg in messages:
1062
+ if msg.type == "permission_response":
1063
+ payload = msg.payload
1064
+ if payload.get("request_id") == request_id:
1065
+ await worker_mailbox.mark_read(msg.id)
1066
+ return SwarmPermissionResponse(
1067
+ request_id=payload["request_id"],
1068
+ allowed=bool(payload.get("allowed", False)),
1069
+ feedback=payload.get("feedback"),
1070
+ updated_rules=payload.get("updated_rules", []),
1071
+ )
1072
+ await asyncio.sleep(0.5)
1073
+
1074
+ return None
1075
+
1076
+
1077
+ # ---------------------------------------------------------------------------
1078
+ # Leader helper: evaluate and send response
1079
+ # ---------------------------------------------------------------------------
1080
+
1081
+
1082
+ async def handle_permission_request(
1083
+ request: SwarmPermissionRequest,
1084
+ checker: "PermissionChecker",
1085
+ ) -> SwarmPermissionResponse:
1086
+ """Evaluate *request* using the existing :class:`PermissionChecker`.
1087
+
1088
+ Read-only tools are auto-approved without consulting the checker. For
1089
+ all other tools the checker's ``evaluate`` method is called; if the tool
1090
+ is allowed or only requires confirmation (and nothing blocks it), it is
1091
+ approved; otherwise it is denied.
1092
+
1093
+ Args:
1094
+ request: The incoming permission request from a worker.
1095
+ checker: An already-configured :class:`~openharness.permissions.checker.PermissionChecker`.
1096
+
1097
+ Returns:
1098
+ A :class:`SwarmPermissionResponse` with the decision.
1099
+ """
1100
+ if _is_read_only(request.tool_name):
1101
+ return SwarmPermissionResponse(
1102
+ request_id=request.id,
1103
+ allowed=True,
1104
+ feedback=None,
1105
+ )
1106
+
1107
+ file_path: str | None = (
1108
+ request.input.get("file_path") # type: ignore[assignment]
1109
+ or request.input.get("path")
1110
+ or None
1111
+ )
1112
+ command: str | None = request.input.get("command") # type: ignore[assignment]
1113
+
1114
+ decision = checker.evaluate(
1115
+ request.tool_name,
1116
+ is_read_only=False,
1117
+ file_path=file_path,
1118
+ command=command,
1119
+ )
1120
+
1121
+ allowed = decision.allowed
1122
+ feedback: str | None = None if allowed else decision.reason
1123
+
1124
+ return SwarmPermissionResponse(
1125
+ request_id=request.id,
1126
+ allowed=allowed,
1127
+ feedback=feedback,
1128
+ )
1129
+
1130
+
1131
+ # ---------------------------------------------------------------------------
1132
+ # Leader helper: write response back to a worker's mailbox
1133
+ # ---------------------------------------------------------------------------
1134
+
1135
+
1136
+ async def send_permission_response(
1137
+ response: SwarmPermissionResponse,
1138
+ team_name: str,
1139
+ worker_id: str,
1140
+ leader_id: str = "leader",
1141
+ ) -> None:
1142
+ """Write *response* to the worker's mailbox.
1143
+
1144
+ This is the original structured-payload approach. For new code prefer
1145
+ :func:`send_permission_response_via_mailbox`.
1146
+
1147
+ Args:
1148
+ response: The resolution to send.
1149
+ team_name: The swarm team name.
1150
+ worker_id: The target worker's agent ID.
1151
+ leader_id: The sending leader's agent ID (default ``"leader"``).
1152
+ """
1153
+ payload: dict[str, Any] = {
1154
+ "request_id": response.request_id,
1155
+ "allowed": response.allowed,
1156
+ "feedback": response.feedback,
1157
+ "updated_rules": response.updated_rules,
1158
+ }
1159
+ msg = MailboxMessage(
1160
+ id=str(uuid.uuid4()),
1161
+ type="permission_response",
1162
+ sender=leader_id,
1163
+ recipient=worker_id,
1164
+ payload=payload,
1165
+ timestamp=time.time(),
1166
+ )
1167
+ worker_mailbox = TeammateMailbox(team_name, worker_id)
1168
+ await worker_mailbox.write(msg)