novacode-cli 1.2.0__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 (691) hide show
  1. novacode_cli/__init__.py +29 -0
  2. novacode_cli/__main__.py +6 -0
  3. novacode_cli/_lazy_heavy.py +99 -0
  4. novacode_cli/_version.py +3 -0
  5. novacode_cli/agent_stream.py +178 -0
  6. novacode_cli/agents/agent_file.py +250 -0
  7. novacode_cli/agents/agent_mailbox.py +120 -0
  8. novacode_cli/agents/approval_decisions.py +146 -0
  9. novacode_cli/agents/arg_repair.py +196 -0
  10. novacode_cli/agents/async_agents/__init__.py +6 -0
  11. novacode_cli/agents/async_agents/_config.py +213 -0
  12. novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
  13. novacode_cli/agents/async_agents/_specialist.py +84 -0
  14. novacode_cli/agents/async_agents/_test_commands.py +89 -0
  15. novacode_cli/agents/async_agents/_tools.py +432 -0
  16. novacode_cli/agents/async_agents/general_purpose.py +42 -0
  17. novacode_cli/agents/async_agents/langgraph.json +9 -0
  18. novacode_cli/agents/async_agents/research.py +22 -0
  19. novacode_cli/agents/async_context.py +303 -0
  20. novacode_cli/agents/async_on_demand.py +52 -0
  21. novacode_cli/agents/async_task_errors.py +75 -0
  22. novacode_cli/agents/async_workspace.py +27 -0
  23. novacode_cli/agents/attribution.py +165 -0
  24. novacode_cli/agents/browser_mode.py +119 -0
  25. novacode_cli/agents/btw_agent.py +71 -0
  26. novacode_cli/agents/caveman.py +352 -0
  27. novacode_cli/agents/caveman_support.py +370 -0
  28. novacode_cli/agents/code_mode.py +774 -0
  29. novacode_cli/agents/code_runtime.py +160 -0
  30. novacode_cli/agents/context_compaction.py +1339 -0
  31. novacode_cli/agents/core_agent.py +2710 -0
  32. novacode_cli/agents/default_subagents/__init__.py +24 -0
  33. novacode_cli/agents/default_subagents/async_subagents.py +296 -0
  34. novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
  35. novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
  36. novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
  37. novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
  38. novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
  39. novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
  40. novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
  41. novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
  42. novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
  43. novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
  44. novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
  45. novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
  46. novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
  47. novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
  48. novacode_cli/agents/default_subagents/prompt.py +88 -0
  49. novacode_cli/agents/default_subagents/subagents.py +138 -0
  50. novacode_cli/agents/dream_agent.py +93 -0
  51. novacode_cli/agents/edit_diagnostics.py +362 -0
  52. novacode_cli/agents/edit_fallback.py +62 -0
  53. novacode_cli/agents/handoff.py +166 -0
  54. novacode_cli/agents/invocation.py +152 -0
  55. novacode_cli/agents/model_budget.py +42 -0
  56. novacode_cli/agents/model_retry.py +113 -0
  57. novacode_cli/agents/model_router.py +420 -0
  58. novacode_cli/agents/openai_decisions.py +186 -0
  59. novacode_cli/agents/plan_agent/__init__.py +12 -0
  60. novacode_cli/agents/plan_agent/plan_agent.py +357 -0
  61. novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
  62. novacode_cli/agents/plan_execution.py +237 -0
  63. novacode_cli/agents/ponytail.py +66 -0
  64. novacode_cli/agents/prime.py +274 -0
  65. novacode_cli/agents/provider_caching.py +99 -0
  66. novacode_cli/agents/server_launcher.py +904 -0
  67. novacode_cli/agents/strict_tools.py +384 -0
  68. novacode_cli/agents/task_discipline.py +294 -0
  69. novacode_cli/agents/tool_call_repair.py +290 -0
  70. novacode_cli/agents/tool_offload.py +291 -0
  71. novacode_cli/agents/tool_search.py +806 -0
  72. novacode_cli/agents/tool_search_variants.py +106 -0
  73. novacode_cli/agents/tool_verdicts.py +1194 -0
  74. novacode_cli/agents/user_async_agents.py +312 -0
  75. novacode_cli/artifacts/__init__.py +10 -0
  76. novacode_cli/artifacts/registry.py +278 -0
  77. novacode_cli/artifacts/server.py +426 -0
  78. novacode_cli/assets/Nova.png +0 -0
  79. novacode_cli/assets/nova-avatar.png +0 -0
  80. novacode_cli/audio/__init__.py +301 -0
  81. novacode_cli/audio/capture.py +137 -0
  82. novacode_cli/audio/downloads.py +273 -0
  83. novacode_cli/audio/pipeline.py +391 -0
  84. novacode_cli/audio/playback.py +87 -0
  85. novacode_cli/audio/providers.py +208 -0
  86. novacode_cli/audio/speakable.py +78 -0
  87. novacode_cli/audio/stt.py +132 -0
  88. novacode_cli/audio/stt_deepgram.py +69 -0
  89. novacode_cli/audio/stt_elevenlabs.py +89 -0
  90. novacode_cli/audio/stt_parakeet.py +94 -0
  91. novacode_cli/audio/stt_whistle.py +61 -0
  92. novacode_cli/audio/summarize.py +92 -0
  93. novacode_cli/audio/tts.py +85 -0
  94. novacode_cli/audio/tts_kokoro.py +73 -0
  95. novacode_cli/audio/tts_orpheus.py +180 -0
  96. novacode_cli/audio/tts_pocket.py +131 -0
  97. novacode_cli/audio/tts_sano.py +69 -0
  98. novacode_cli/audio/vad.py +145 -0
  99. novacode_cli/backends/__init__.py +17 -0
  100. novacode_cli/backends/filesystem.py +894 -0
  101. novacode_cli/backends/native_search.py +92 -0
  102. novacode_cli/bootstrap/__init__.py +9 -0
  103. novacode_cli/bootstrap/env_snapshot.py +240 -0
  104. novacode_cli/bootstrap/steering.py +464 -0
  105. novacode_cli/bootstrap/vision_router.py +604 -0
  106. novacode_cli/brand.py +362 -0
  107. novacode_cli/cli_args.py +718 -0
  108. novacode_cli/commands/__init__.py +237 -0
  109. novacode_cli/commands/agents_commands.py +231 -0
  110. novacode_cli/commands/auth_handler.py +40 -0
  111. novacode_cli/commands/browser_use_handler.py +75 -0
  112. novacode_cli/commands/cache_handler.py +32 -0
  113. novacode_cli/commands/chat_handler.py +1357 -0
  114. novacode_cli/commands/ci_fix.py +171 -0
  115. novacode_cli/commands/claude_plan.py +331 -0
  116. novacode_cli/commands/codex_plan.py +195 -0
  117. novacode_cli/commands/commands.py +1113 -0
  118. novacode_cli/commands/create_handler.py +124 -0
  119. novacode_cli/commands/create_server.py +1887 -0
  120. novacode_cli/commands/cron_handler.py +119 -0
  121. novacode_cli/commands/dream_handler.py +203 -0
  122. novacode_cli/commands/effort_handler.py +65 -0
  123. novacode_cli/commands/evolution_handler.py +214 -0
  124. novacode_cli/commands/file_commands.py +381 -0
  125. novacode_cli/commands/handoff_handler.py +68 -0
  126. novacode_cli/commands/hooks_handler.py +269 -0
  127. novacode_cli/commands/init_handler.py +146 -0
  128. novacode_cli/commands/learning_handler.py +160 -0
  129. novacode_cli/commands/lite_mode_handler.py +76 -0
  130. novacode_cli/commands/log_commands.py +363 -0
  131. novacode_cli/commands/mcp_handler.py +29 -0
  132. novacode_cli/commands/model_handler.py +29 -0
  133. novacode_cli/commands/notifications_handler.py +158 -0
  134. novacode_cli/commands/onboard_handler.py +121 -0
  135. novacode_cli/commands/plan_handler.py +440 -0
  136. novacode_cli/commands/planner_usage.py +250 -0
  137. novacode_cli/commands/plugin_install_handler.py +38 -0
  138. novacode_cli/commands/plugins_handler.py +35 -0
  139. novacode_cli/commands/prime_handler.py +455 -0
  140. novacode_cli/commands/prompt_handler.py +149 -0
  141. novacode_cli/commands/provider_usage.py +160 -0
  142. novacode_cli/commands/ralph_events.py +106 -0
  143. novacode_cli/commands/ralph_handler.py +1293 -0
  144. novacode_cli/commands/refine_handler.py +213 -0
  145. novacode_cli/commands/research_handler.py +202 -0
  146. novacode_cli/commands/rootapp_remote.py +83 -0
  147. novacode_cli/commands/router_handler.py +36 -0
  148. novacode_cli/commands/server_commands.py +249 -0
  149. novacode_cli/commands/session_commands.py +203 -0
  150. novacode_cli/commands/share_handler.py +450 -0
  151. novacode_cli/commands/side_commands.py +366 -0
  152. novacode_cli/commands/skill_invoke.py +420 -0
  153. novacode_cli/commands/skills_commands.py +48 -0
  154. novacode_cli/commands/trace_handler.py +322 -0
  155. novacode_cli/commands/trello_handler.py +226 -0
  156. novacode_cli/commands/trello_remote.py +177 -0
  157. novacode_cli/commands/trello_server.py +1214 -0
  158. novacode_cli/commands/vision_handler.py +127 -0
  159. novacode_cli/commands/voice_handler.py +447 -0
  160. novacode_cli/commands/webhook_handler.py +134 -0
  161. novacode_cli/commands/wiki_commands.py +227 -0
  162. novacode_cli/compaction.py +753 -0
  163. novacode_cli/computation_cache.py +159 -0
  164. novacode_cli/computer/__init__.py +1 -0
  165. novacode_cli/computer/persistent.py +234 -0
  166. novacode_cli/computer/preview.py +253 -0
  167. novacode_cli/computer/runtime.py +474 -0
  168. novacode_cli/computer/windows_console.py +152 -0
  169. novacode_cli/computer/windows_qemu.py +211 -0
  170. novacode_cli/config/browser_mode.py +159 -0
  171. novacode_cli/config/catalog_models.py +95 -0
  172. novacode_cli/config/config.py +1388 -0
  173. novacode_cli/config/credentials.py +540 -0
  174. novacode_cli/config/google_oauth_auth.py +281 -0
  175. novacode_cli/config/model_capabilities.py +343 -0
  176. novacode_cli/config/model_catalog.py +275 -0
  177. novacode_cli/config/model_create.py +1325 -0
  178. novacode_cli/config/model_manager.py +695 -0
  179. novacode_cli/config/nova_config.py +1283 -0
  180. novacode_cli/config/openai_chatgpt_auth.py +384 -0
  181. novacode_cli/config/opencode_gateway.py +25 -0
  182. novacode_cli/config/plan_mode.py +188 -0
  183. novacode_cli/config/provider_auth.py +438 -0
  184. novacode_cli/config/provider_catalog.json +5127 -0
  185. novacode_cli/config/provider_registry.py +38 -0
  186. novacode_cli/config/provider_transports.py +258 -0
  187. novacode_cli/config/role_models.py +175 -0
  188. novacode_cli/context/__init__.py +50 -0
  189. novacode_cli/context/_analysis.py +799 -0
  190. novacode_cli/context/_budget.py +116 -0
  191. novacode_cli/context/_conversation.py +73 -0
  192. novacode_cli/context/_dynamic.py +264 -0
  193. novacode_cli/context/_model_config.py +380 -0
  194. novacode_cli/context/_models_dev.py +99 -0
  195. novacode_cli/context/archives.py +248 -0
  196. novacode_cli/context/estimates.py +65 -0
  197. novacode_cli/context/history.py +17 -0
  198. novacode_cli/context/knowledge_cache.py +292 -0
  199. novacode_cli/context/manager.py +101 -0
  200. novacode_cli/context/observations.py +65 -0
  201. novacode_cli/context/pressure.py +168 -0
  202. novacode_cli/context/provider_accounting.py +109 -0
  203. novacode_cli/context/snapshot.py +493 -0
  204. novacode_cli/context/strategies.py +279 -0
  205. novacode_cli/context/task_progress.py +122 -0
  206. novacode_cli/core/__init__.py +0 -0
  207. novacode_cli/core/agent_loop.py +1821 -0
  208. novacode_cli/core/autonomous_loop.py +277 -0
  209. novacode_cli/core/code_approvals.py +57 -0
  210. novacode_cli/core/input_preparation.py +276 -0
  211. novacode_cli/core/reasoning.py +67 -0
  212. novacode_cli/core/streaming.py +326 -0
  213. novacode_cli/core/subagent_tasks.py +696 -0
  214. novacode_cli/core/subagent_tracking.py +235 -0
  215. novacode_cli/core/tool_results.py +116 -0
  216. novacode_cli/core/verification_loop.py +272 -0
  217. novacode_cli/council.py +595 -0
  218. novacode_cli/council_consensus.py +115 -0
  219. novacode_cli/council_decision.py +157 -0
  220. novacode_cli/council_planner.py +1421 -0
  221. novacode_cli/council_settings.py +208 -0
  222. novacode_cli/council_web_settings.py +41 -0
  223. novacode_cli/council_workspace.py +56 -0
  224. novacode_cli/cowork/__init__.py +10 -0
  225. novacode_cli/cowork/broker_middleware.py +162 -0
  226. novacode_cli/cowork/launcher.py +102 -0
  227. novacode_cli/cowork/policy.py +284 -0
  228. novacode_cli/cowork/ui.py +826 -0
  229. novacode_cli/daemons/__init__.py +1 -0
  230. novacode_cli/daemons/registry.py +342 -0
  231. novacode_cli/desktop_notifications.py +302 -0
  232. novacode_cli/doctor.py +251 -0
  233. novacode_cli/embeddings.py +109 -0
  234. novacode_cli/entrypoint.py +56 -0
  235. novacode_cli/errors/__init__.py +19 -0
  236. novacode_cli/errors/handlers.py +648 -0
  237. novacode_cli/errors/provider_errors.py +249 -0
  238. novacode_cli/errors/taxonomy.py +80 -0
  239. novacode_cli/events.py +179 -0
  240. novacode_cli/file_ops.py +714 -0
  241. novacode_cli/git_safety.py +281 -0
  242. novacode_cli/handoff.py +383 -0
  243. novacode_cli/headless/__init__.py +18 -0
  244. novacode_cli/headless/input.py +58 -0
  245. novacode_cli/headless/output.py +224 -0
  246. novacode_cli/headless/pipe.py +532 -0
  247. novacode_cli/headless/runner.py +461 -0
  248. novacode_cli/hermes/__init__.py +39 -0
  249. novacode_cli/hermes/activation.py +297 -0
  250. novacode_cli/hermes/config.py +129 -0
  251. novacode_cli/hermes/curator.py +199 -0
  252. novacode_cli/hermes/evolution.py +374 -0
  253. novacode_cli/hermes/ledger.py +364 -0
  254. novacode_cli/hermes/memory_tiers.py +969 -0
  255. novacode_cli/hermes/middleware.py +678 -0
  256. novacode_cli/hermes/overview.py +174 -0
  257. novacode_cli/hermes/prompt_evolution.py +503 -0
  258. novacode_cli/hermes/refine_loop.py +643 -0
  259. novacode_cli/hermes/refinement_log.py +190 -0
  260. novacode_cli/hermes/review.py +674 -0
  261. novacode_cli/hermes/skill_debate.py +135 -0
  262. novacode_cli/hermes/skill_discovery.py +450 -0
  263. novacode_cli/hermes/skill_manager.py +335 -0
  264. novacode_cli/hermes/skill_proposals.py +43 -0
  265. novacode_cli/hermes/test_gaming.py +119 -0
  266. novacode_cli/hermes/tracker.py +435 -0
  267. novacode_cli/hermes/tuner.py +206 -0
  268. novacode_cli/hermes/verification_memory.py +134 -0
  269. novacode_cli/hermes/verifier.py +277 -0
  270. novacode_cli/hitl/__init__.py +22 -0
  271. novacode_cli/hitl/interrupts.py +493 -0
  272. novacode_cli/hooks.py +359 -0
  273. novacode_cli/image_utils.py +517 -0
  274. novacode_cli/init/__init__.py +7 -0
  275. novacode_cli/init/events.py +43 -0
  276. novacode_cli/input_utils.py +526 -0
  277. novacode_cli/install_commands.py +87 -0
  278. novacode_cli/integrations/__init__.py +82 -0
  279. novacode_cli/integrations/daytona.py +122 -0
  280. novacode_cli/integrations/docker.py +309 -0
  281. novacode_cli/integrations/host_path.py +111 -0
  282. novacode_cli/integrations/langsmith.py +151 -0
  283. novacode_cli/integrations/modal.py +134 -0
  284. novacode_cli/integrations/runloop.py +130 -0
  285. novacode_cli/integrations/sandbox_factory.py +1388 -0
  286. novacode_cli/integrations/sandbox_registry.py +312 -0
  287. novacode_cli/integrations/workdir_backend.py +589 -0
  288. novacode_cli/main.py +2078 -0
  289. novacode_cli/mcp/__init__.py +73 -0
  290. novacode_cli/mcp/auth.py +189 -0
  291. novacode_cli/mcp/client.py +571 -0
  292. novacode_cli/mcp/code_mode.py +5 -0
  293. novacode_cli/mcp/code_runtime.py +5 -0
  294. novacode_cli/mcp/commands.py +902 -0
  295. novacode_cli/mcp/config.py +406 -0
  296. novacode_cli/mcp/generic.py +128 -0
  297. novacode_cli/mcp/middleware.py +993 -0
  298. novacode_cli/mcp/presets.py +367 -0
  299. novacode_cli/memory/__init__.py +1 -0
  300. novacode_cli/memory/agent_memory.py +1312 -0
  301. novacode_cli/memory/builtin_lessons.py +60 -0
  302. novacode_cli/memory/dream_gates.py +192 -0
  303. novacode_cli/memory/limits.py +59 -0
  304. novacode_cli/memory/parsed_cache.py +106 -0
  305. novacode_cli/memory/recall_stats.py +97 -0
  306. novacode_cli/memory/semantic.py +116 -0
  307. novacode_cli/memory/store.py +335 -0
  308. novacode_cli/memory/wiki.py +274 -0
  309. novacode_cli/migrate.py +185 -0
  310. novacode_cli/onboarding.py +649 -0
  311. novacode_cli/onboarding_check.py +119 -0
  312. novacode_cli/path_approval.py +297 -0
  313. novacode_cli/plan_archive.py +148 -0
  314. novacode_cli/plugins/__init__.py +121 -0
  315. novacode_cli/plugins/claude_plugins.py +538 -0
  316. novacode_cli/plugins/cli.py +168 -0
  317. novacode_cli/plugins/components.py +413 -0
  318. novacode_cli/plugins/hooks.py +356 -0
  319. novacode_cli/plugins/loader.py +317 -0
  320. novacode_cli/plugins/marketplaces.py +199 -0
  321. novacode_cli/plugins/storage.py +70 -0
  322. novacode_cli/prime.py +759 -0
  323. novacode_cli/prime_factory/__init__.py +15 -0
  324. novacode_cli/prime_factory/container.py +198 -0
  325. novacode_cli/prime_factory/controller.py +565 -0
  326. novacode_cli/prime_factory/knowledge.py +261 -0
  327. novacode_cli/prime_factory/publication.py +218 -0
  328. novacode_cli/prime_factory/recovery.py +50 -0
  329. novacode_cli/prime_factory/research.py +370 -0
  330. novacode_cli/prime_factory/runner.py +608 -0
  331. novacode_cli/prime_factory/runtime.py +261 -0
  332. novacode_cli/prime_factory/state.py +201 -0
  333. novacode_cli/prime_factory/store.py +128 -0
  334. novacode_cli/prime_factory/worker.py +129 -0
  335. novacode_cli/prime_factory/workflow.py +899 -0
  336. novacode_cli/prime_factory/workspace.py +317 -0
  337. novacode_cli/prime_memory.py +290 -0
  338. novacode_cli/process_manager.py +585 -0
  339. novacode_cli/project_onboard.py +537 -0
  340. novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
  341. novacode_cli/prompts/__init__.py +168 -0
  342. novacode_cli/prompts/agent_definitions.jinja +11 -0
  343. novacode_cli/prompts/agent_generation.jinja +125 -0
  344. novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
  345. novacode_cli/prompts/claude_plan_mode.jinja +12 -0
  346. novacode_cli/prompts/coding_workflow.jinja +18 -0
  347. novacode_cli/prompts/computer_use.jinja +17 -0
  348. novacode_cli/prompts/core_agent_system.jinja +81 -0
  349. novacode_cli/prompts/delegation.jinja +15 -0
  350. novacode_cli/prompts/desktop_power.jinja +4 -0
  351. novacode_cli/prompts/file_paths.jinja +15 -0
  352. novacode_cli/prompts/file_tracker.jinja +23 -0
  353. novacode_cli/prompts/init_agents_md.jinja +56 -0
  354. novacode_cli/prompts/init_exploration.jinja +174 -0
  355. novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
  356. novacode_cli/prompts/instruction_scope.jinja +5 -0
  357. novacode_cli/prompts/learnings_extraction.jinja +45 -0
  358. novacode_cli/prompts/longterm_memory.jinja +54 -0
  359. novacode_cli/prompts/mcp.jinja +84 -0
  360. novacode_cli/prompts/memory_consolidation.jinja +40 -0
  361. novacode_cli/prompts/nova_evolution.jinja +73 -0
  362. novacode_cli/prompts/nova_review.jinja +158 -0
  363. novacode_cli/prompts/nova_skill_debate.jinja +36 -0
  364. novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
  365. novacode_cli/prompts/nova_verify.jinja +100 -0
  366. novacode_cli/prompts/onboard_dashboard.jinja +378 -0
  367. novacode_cli/prompts/plan_agent.jinja +87 -0
  368. novacode_cli/prompts/plan_scouts.jinja +5 -0
  369. novacode_cli/prompts/plan_storage.jinja +8 -0
  370. novacode_cli/prompts/planning.jinja +82 -0
  371. novacode_cli/prompts/ponytail/LICENSE +21 -0
  372. novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
  373. novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
  374. novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
  375. novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
  376. novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
  377. novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
  378. novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
  379. novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
  380. novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
  381. novacode_cli/prompts/prompt_evolution.jinja +27 -0
  382. novacode_cli/prompts/ralph_iteration.jinja +75 -0
  383. novacode_cli/prompts/refine_gate.jinja +31 -0
  384. novacode_cli/prompts/refine_plan.jinja +64 -0
  385. novacode_cli/prompts/refine_review.jinja +34 -0
  386. novacode_cli/prompts/research_delivery.jinja +9 -0
  387. novacode_cli/prompts/research_swarm.jinja +130 -0
  388. novacode_cli/prompts/response_style.jinja +5 -0
  389. novacode_cli/prompts/session_summarization.jinja +86 -0
  390. novacode_cli/prompts/skill_creation.jinja +85 -0
  391. novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
  392. novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
  393. novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
  394. novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
  395. novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
  396. novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
  397. novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
  398. novacode_cli/prompts/summarization.jinja +107 -0
  399. novacode_cli/prompts/summarization_answers.jinja +23 -0
  400. novacode_cli/prompts/summarization_questions.jinja +32 -0
  401. novacode_cli/prompts/vision_caption.jinja +21 -0
  402. novacode_cli/prompts/voice_summary.jinja +65 -0
  403. novacode_cli/prompts/wiki_file.jinja +57 -0
  404. novacode_cli/prompts/wiki_ingest.jinja +87 -0
  405. novacode_cli/recovery.py +383 -0
  406. novacode_cli/remote/__init__.py +45 -0
  407. novacode_cli/remote/approvals.py +282 -0
  408. novacode_cli/remote/async_task_clients.py +97 -0
  409. novacode_cli/remote/async_task_watcher.py +240 -0
  410. novacode_cli/remote/bridge.py +905 -0
  411. novacode_cli/remote/command_menu.py +26 -0
  412. novacode_cli/remote/command_ui.py +243 -0
  413. novacode_cli/remote/config.py +199 -0
  414. novacode_cli/remote/discord_bridge.py +530 -0
  415. novacode_cli/remote/discord_format.py +31 -0
  416. novacode_cli/remote/discord_threads.py +225 -0
  417. novacode_cli/remote/images.py +97 -0
  418. novacode_cli/remote/processor.py +601 -0
  419. novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
  420. novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
  421. novacode_cli/remote/rootapp_bot/package.json +18 -0
  422. novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
  423. novacode_cli/remote/rootapp_bridge.py +370 -0
  424. novacode_cli/remote/rootapp_setup.py +89 -0
  425. novacode_cli/remote/routing.py +103 -0
  426. novacode_cli/remote/scheduler.py +302 -0
  427. novacode_cli/remote/session.py +164 -0
  428. novacode_cli/remote/status.py +411 -0
  429. novacode_cli/remote/streaming.py +188 -0
  430. novacode_cli/remote/telegram_bridge.py +1118 -0
  431. novacode_cli/remote/telegram_format.py +215 -0
  432. novacode_cli/remote/telegram_hub.py +256 -0
  433. novacode_cli/remote/voice_notes.py +120 -0
  434. novacode_cli/remote/webhook_adapters.py +167 -0
  435. novacode_cli/remote/webhook_server.py +154 -0
  436. novacode_cli/rewind.py +150 -0
  437. novacode_cli/rooms/__init__.py +1 -0
  438. novacode_cli/rooms/bridge.py +86 -0
  439. novacode_cli/rooms/cli.py +231 -0
  440. novacode_cli/rooms/nova.py +227 -0
  441. novacode_cli/rooms/runtime.py +299 -0
  442. novacode_cli/rooms/store.py +538 -0
  443. novacode_cli/rooms/workspace.py +94 -0
  444. novacode_cli/security/__init__.py +29 -0
  445. novacode_cli/security/approval_context.py +45 -0
  446. novacode_cli/security/delegated_approval.py +68 -0
  447. novacode_cli/security/local_http.py +85 -0
  448. novacode_cli/security/middleware.py +87 -0
  449. novacode_cli/security/paths.py +35 -0
  450. novacode_cli/security/policy.py +535 -0
  451. novacode_cli/security/policy_writer.py +90 -0
  452. novacode_cli/security/prompt_injection.py +203 -0
  453. novacode_cli/security/remember.py +81 -0
  454. novacode_cli/security/rule_synthesis.py +116 -0
  455. novacode_cli/security/secret_files.py +61 -0
  456. novacode_cli/security/session_allow.py +114 -0
  457. novacode_cli/security/unicode_security.py +516 -0
  458. novacode_cli/security/validator.py +173 -0
  459. novacode_cli/server/__init__.py +9 -0
  460. novacode_cli/server/app.py +632 -0
  461. novacode_cli/server/event_adapter.py +189 -0
  462. novacode_cli/server/session_manager.py +90 -0
  463. novacode_cli/server_main.py +64 -0
  464. novacode_cli/server_runner/dev_server.py +530 -0
  465. novacode_cli/server_runner/test_runner.py +470 -0
  466. novacode_cli/session/__init__.py +1 -0
  467. novacode_cli/session/adapters/__init__.py +240 -0
  468. novacode_cli/session/adapters/claude_code.py +102 -0
  469. novacode_cli/session/adapters/codex.py +77 -0
  470. novacode_cli/session/adapters/harness_native.py +75 -0
  471. novacode_cli/session/checkpoint_coordinator.py +448 -0
  472. novacode_cli/session/context_restore.py +25 -0
  473. novacode_cli/session/history_export.py +228 -0
  474. novacode_cli/session/history_tree.py +174 -0
  475. novacode_cli/session/imported_context.py +267 -0
  476. novacode_cli/session/runtime_identity.py +32 -0
  477. novacode_cli/session/session_persistence.py +1168 -0
  478. novacode_cli/session/session_prompt_builder.py +279 -0
  479. novacode_cli/session/session_restore.py +351 -0
  480. novacode_cli/session/session_summarization.py +20 -0
  481. novacode_cli/session/transcript_journal.py +153 -0
  482. novacode_cli/sessions/__init__.py +11 -0
  483. novacode_cli/sessions/gateway.py +84 -0
  484. novacode_cli/sessions/launch.py +51 -0
  485. novacode_cli/sessions/lease.py +213 -0
  486. novacode_cli/sessions/messaging.py +255 -0
  487. novacode_cli/sessions/protocol.py +216 -0
  488. novacode_cli/sessions/resume.py +31 -0
  489. novacode_cli/sessions/supervisor.py +498 -0
  490. novacode_cli/sessions/tasks.py +73 -0
  491. novacode_cli/sessions/tui_messaging.py +413 -0
  492. novacode_cli/sessions/worker.py +652 -0
  493. novacode_cli/sessions/worktree.py +262 -0
  494. novacode_cli/shell/__init__.py +27 -0
  495. novacode_cli/shell/jobs.py +475 -0
  496. novacode_cli/shell/middleware.py +1568 -0
  497. novacode_cli/shell/os_sandbox.py +205 -0
  498. novacode_cli/shell/output.py +181 -0
  499. novacode_cli/shell/patterns.py +275 -0
  500. novacode_cli/shell/processes.py +87 -0
  501. novacode_cli/shell/utils.py +236 -0
  502. novacode_cli/skills/__init__.py +26 -0
  503. novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
  504. novacode_cli/skills/curation_middleware.py +69 -0
  505. novacode_cli/skills/executable.py +161 -0
  506. novacode_cli/skills/libraries.py +102 -0
  507. novacode_cli/skills/load.py +248 -0
  508. novacode_cli/skills/prune.py +215 -0
  509. novacode_cli/skills/refreshing_middleware.py +896 -0
  510. novacode_cli/skills/retrieval.py +248 -0
  511. novacode_cli/skills/runtime.py +118 -0
  512. novacode_cli/skills/schema.py +254 -0
  513. novacode_cli/skills/skill_creation.py +1549 -0
  514. novacode_cli/skills/skill_lock.py +75 -0
  515. novacode_cli/skills/skill_system_prompt.py +16 -0
  516. novacode_cli/skills/skills_prefs.py +126 -0
  517. novacode_cli/skills/tool_resolver.py +31 -0
  518. novacode_cli/skills/upstream.py +50 -0
  519. novacode_cli/skills/versioning.py +291 -0
  520. novacode_cli/states/Session.py +847 -0
  521. novacode_cli/states/__init__.py +31 -0
  522. novacode_cli/states/slices/__init__.py +17 -0
  523. novacode_cli/states/slices/agent_runtime.py +249 -0
  524. novacode_cli/states/slices/background_tasks.py +21 -0
  525. novacode_cli/states/slices/notifications.py +74 -0
  526. novacode_cli/states/slices/remote_bridge.py +56 -0
  527. novacode_cli/states/slices/ui_settings.py +49 -0
  528. novacode_cli/states/slices/wiki.py +25 -0
  529. novacode_cli/teammates/__init__.py +1 -0
  530. novacode_cli/teammates/controller.py +557 -0
  531. novacode_cli/teammates/graph.py +104 -0
  532. novacode_cli/teammates/registry.py +175 -0
  533. novacode_cli/teammates/service.py +254 -0
  534. novacode_cli/teammates/worker.py +19 -0
  535. novacode_cli/teams/__init__.py +1 -0
  536. novacode_cli/teams/adapters/__init__.py +1 -0
  537. novacode_cli/teams/adapters/container.py +316 -0
  538. novacode_cli/teams/adapters/inprocess.py +206 -0
  539. novacode_cli/teams/adapters/process.py +364 -0
  540. novacode_cli/teams/bootstrap.py +246 -0
  541. novacode_cli/teams/container_git.py +67 -0
  542. novacode_cli/teams/context.py +34 -0
  543. novacode_cli/teams/coordinator.py +815 -0
  544. novacode_cli/teams/gateway.py +200 -0
  545. novacode_cli/teams/host.py +64 -0
  546. novacode_cli/teams/legacy.py +52 -0
  547. novacode_cli/teams/middleware.py +176 -0
  548. novacode_cli/teams/models.py +104 -0
  549. novacode_cli/teams/output.py +47 -0
  550. novacode_cli/teams/protocol.py +50 -0
  551. novacode_cli/teams/recovery.py +80 -0
  552. novacode_cli/teams/scheduler.py +44 -0
  553. novacode_cli/teams/store.py +631 -0
  554. novacode_cli/teams/worker.py +251 -0
  555. novacode_cli/teams/workspace.py +100 -0
  556. novacode_cli/token_utils.py +256 -0
  557. novacode_cli/tools/__init__.py +104 -0
  558. novacode_cli/tools/_shared.py +97 -0
  559. novacode_cli/tools/_types.py +24 -0
  560. novacode_cli/tools/artifact_tools.py +137 -0
  561. novacode_cli/tools/code_search_tools.py +330 -0
  562. novacode_cli/tools/computer_tools.py +77 -0
  563. novacode_cli/tools/cron_tools.py +65 -0
  564. novacode_cli/tools/daemon_tool.py +290 -0
  565. novacode_cli/tools/desktop_tools.py +161 -0
  566. novacode_cli/tools/fetch_tools.py +530 -0
  567. novacode_cli/tools/http_tools.py +279 -0
  568. novacode_cli/tools/inspection_tools.py +107 -0
  569. novacode_cli/tools/job_tools.py +143 -0
  570. novacode_cli/tools/knowledge_tools.py +427 -0
  571. novacode_cli/tools/memory_tools.py +461 -0
  572. novacode_cli/tools/memory_wiki_tools.py +40 -0
  573. novacode_cli/tools/notebook_tools.py +238 -0
  574. novacode_cli/tools/oracle_tool.py +83 -0
  575. novacode_cli/tools/plan_archive_tools.py +80 -0
  576. novacode_cli/tools/plan_mode_tools.py +315 -0
  577. novacode_cli/tools/python_kernel_loop.py +165 -0
  578. novacode_cli/tools/python_kernel_tool.py +222 -0
  579. novacode_cli/tools/reflection_tools.py +34 -0
  580. novacode_cli/tools/scraper_tools.py +377 -0
  581. novacode_cli/tools/skill_tools.py +275 -0
  582. novacode_cli/tools/speak_tool.py +51 -0
  583. novacode_cli/tools/store_memory_tools.py +227 -0
  584. novacode_cli/tools/ui_tools.py +62 -0
  585. novacode_cli/tools/waiter_tools.py +408 -0
  586. novacode_cli/tools/web_tools.py +349 -0
  587. novacode_cli/tools/wiki_tools.py +152 -0
  588. novacode_cli/tools/worktree_tools.py +219 -0
  589. novacode_cli/tracking/__init__.py +10 -0
  590. novacode_cli/tracking/file_tracker.py +826 -0
  591. novacode_cli/tracking/local_metrics.py +170 -0
  592. novacode_cli/tracking/loop_guard.py +507 -0
  593. novacode_cli/tracking/request_metrics.py +60 -0
  594. novacode_cli/tracking/tracing.py +692 -0
  595. novacode_cli/tracking/usage_tree.py +677 -0
  596. novacode_cli/tracking/workspace_anchoring.py +231 -0
  597. novacode_cli/tui/__init__.py +11 -0
  598. novacode_cli/tui/animation_rate.py +9 -0
  599. novacode_cli/tui/animations.py +332 -0
  600. novacode_cli/tui/app.py +19315 -0
  601. novacode_cli/tui/auth_display.py +87 -0
  602. novacode_cli/tui/auth_screens.py +826 -0
  603. novacode_cli/tui/avatar.py +294 -0
  604. novacode_cli/tui/avatar_frames.py +21 -0
  605. novacode_cli/tui/background_tasks.py +35 -0
  606. novacode_cli/tui/browser_settings_screen.py +70 -0
  607. novacode_cli/tui/caveman_screen.py +185 -0
  608. novacode_cli/tui/council_settings_screen.py +182 -0
  609. novacode_cli/tui/council_view.py +535 -0
  610. novacode_cli/tui/decision_settings.py +268 -0
  611. novacode_cli/tui/diff_preview.py +199 -0
  612. novacode_cli/tui/gc_tuning.py +63 -0
  613. novacode_cli/tui/handoff_screen.py +206 -0
  614. novacode_cli/tui/harness.py +244 -0
  615. novacode_cli/tui/history_tree_screen.py +179 -0
  616. novacode_cli/tui/home.py +328 -0
  617. novacode_cli/tui/home_scene.py +94 -0
  618. novacode_cli/tui/icons.py +81 -0
  619. novacode_cli/tui/interface_styles.tcss +327 -0
  620. novacode_cli/tui/matrix3.py +108 -0
  621. novacode_cli/tui/mcp_tools_screen.py +131 -0
  622. novacode_cli/tui/motion.py +178 -0
  623. novacode_cli/tui/output_body.py +136 -0
  624. novacode_cli/tui/output_buffer.py +59 -0
  625. novacode_cli/tui/palette.py +334 -0
  626. novacode_cli/tui/pickers.py +237 -0
  627. novacode_cli/tui/prime_panel.py +506 -0
  628. novacode_cli/tui/provider_signin.py +213 -0
  629. novacode_cli/tui/pty_shell.py +159 -0
  630. novacode_cli/tui/reference_screens.py +444 -0
  631. novacode_cli/tui/resume_picker.py +346 -0
  632. novacode_cli/tui/rooms.py +597 -0
  633. novacode_cli/tui/screens.py +7088 -0
  634. novacode_cli/tui/session_import.py +333 -0
  635. novacode_cli/tui/session_pane.py +242 -0
  636. novacode_cli/tui/session_tab.py +42 -0
  637. novacode_cli/tui/session_transcript.py +118 -0
  638. novacode_cli/tui/shell_output.py +7 -0
  639. novacode_cli/tui/stall_watch.py +174 -0
  640. novacode_cli/tui/statusline.py +40 -0
  641. novacode_cli/tui/statusline_screen.py +91 -0
  642. novacode_cli/tui/teammate_updates.py +162 -0
  643. novacode_cli/tui/teammates.py +548 -0
  644. novacode_cli/tui/terminal.py +289 -0
  645. novacode_cli/tui/terminal_view.py +235 -0
  646. novacode_cli/tui/themes.py +118 -0
  647. novacode_cli/tui/update_notice.py +84 -0
  648. novacode_cli/tui/voice_downloads.py +113 -0
  649. novacode_cli/tui/widgets.py +2038 -0
  650. novacode_cli/tui/windows_input.py +38 -0
  651. novacode_cli/tui/workspace_approval.py +165 -0
  652. novacode_cli/ui/council_web.py +113 -0
  653. novacode_cli/ui/council_workspace.py +72 -0
  654. novacode_cli/ui/cowork_web.py +41 -0
  655. novacode_cli/ui/diff_highlight.py +160 -0
  656. novacode_cli/ui/execution.py +1396 -0
  657. novacode_cli/ui/exit_summary.py +65 -0
  658. novacode_cli/ui/hitl_approval.py +196 -0
  659. novacode_cli/ui/input_preparation.py +2 -0
  660. novacode_cli/ui/interrupt_handlers.py +215 -0
  661. novacode_cli/ui/question_prompt.py +151 -0
  662. novacode_cli/ui/status_phrases.py +157 -0
  663. novacode_cli/ui/stream_preview.py +66 -0
  664. novacode_cli/ui/streaming.py +2 -0
  665. novacode_cli/ui/subagent_tracking.py +2 -0
  666. novacode_cli/ui/ui_elements.py +1585 -0
  667. novacode_cli/ui/web_workspace.py +244 -0
  668. novacode_cli/ui_events.py +299 -0
  669. novacode_cli/update_ui.py +243 -0
  670. novacode_cli/updates.py +489 -0
  671. novacode_cli/utils/backend_patches.py +399 -0
  672. novacode_cli/utils/backend_paths.py +467 -0
  673. novacode_cli/utils/lazy_providers.py +51 -0
  674. novacode_cli/utils/model_info.py +223 -0
  675. novacode_cli/utils/pdf_extraction.py +183 -0
  676. novacode_cli/utils/startup.py +29 -0
  677. novacode_cli/utils/tasks.py +42 -0
  678. novacode_cli/utils/windows.py +9 -0
  679. novacode_cli/vixie/hook_adapter.py +127 -0
  680. novacode_cli/vixie/server.py +375 -0
  681. novacode_cli/wiki/__init__.py +15 -0
  682. novacode_cli/wiki/ask.py +56 -0
  683. novacode_cli/wiki/file.py +71 -0
  684. novacode_cli/wiki/ingest.py +184 -0
  685. novacode_cli/wiki/manager.py +261 -0
  686. novacode_cli-1.2.0.dist-info/METADATA +271 -0
  687. novacode_cli-1.2.0.dist-info/RECORD +691 -0
  688. novacode_cli-1.2.0.dist-info/WHEEL +5 -0
  689. novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
  690. novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
  691. novacode_cli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,774 @@
1
+ """Programmatic tool composition through the owning graph's tool middleware."""
2
+ # ruff: noqa: INP001, ANN401, BLE001, EM101, EM102, TRY003, TRY301
3
+ # JSON payloads are heterogeneous; the tool boundary reports arbitrary server failures.
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import inspect
9
+ import json
10
+ import re
11
+ from dataclasses import replace
12
+ from types import SimpleNamespace
13
+ from typing import TYPE_CHECKING, Any, Literal
14
+ from uuid import uuid4
15
+
16
+ from langchain_core.messages import ToolMessage
17
+ from langchain_core.tools import BaseTool, StructuredTool
18
+ from langchain_core.utils.function_calling import convert_to_openai_tool
19
+ from langchain_quickjs import CodeInterpreterMiddleware
20
+ from langchain_quickjs.middleware import _resolve_thread_id
21
+ from langgraph.types import Command
22
+ from pydantic import BaseModel, ConfigDict, Field
23
+
24
+ from novacode_cli.agents.strict_tools import _validator
25
+ from novacode_cli.agents.tool_search_variants import SEARCH_VARIANTS
26
+ from novacode_cli.config.nova_config import NovaConfig
27
+ from novacode_cli.core.tool_results import MAX_INTERMEDIATE_CHARS, capture_file_read, data_text
28
+ from novacode_cli.mcp.generic import MCPInput, build_mcp_tool, resolve_target
29
+ from novacode_cli.security.policy import get_policy
30
+
31
+ if TYPE_CHECKING:
32
+ from collections.abc import Awaitable, Callable
33
+
34
+ from langchain.agents.middleware.types import ModelRequest, ToolCallRequest
35
+ from langchain_core.messages.tool import ToolCall
36
+
37
+ MAX_ARGUMENT_CHARS = 100000
38
+ MAX_OUTPUT_CHARS = 12000
39
+
40
+ CODE_MODE_GUIDANCE = """
41
+ ## Code Mode
42
+ Use eval as your primary control surface for ordinary operations. Compose discovery,
43
+ native calls, MCP calls, filtering and bounded Promise.all fan-out in JavaScript.
44
+ Prefer one eval call per step; compose parallel work inside it with Promise.all.
45
+ Use tools.list/describe/call and mcp.list/describe/call; inspect schemas before calls.
46
+ tools.list accepts offset and limit, not query; filter its tools array in JavaScript.
47
+ JavaScript runs in QuickJS: Node.js Buffer, process and require are unavailable.
48
+ On Windows, invoke PowerShell explicitly when using its cmdlets through execute.
49
+ Use task({description, subagentType, responseSchema?}) for configured subagents,
50
+ including explicit @agent requests. tools.describe('task') lists the current roster.
51
+ eval accepts statements and top-level await; its final expression is the selected result.
52
+ Variables persist within this conversation. Keep intermediate data in JavaScript;
53
+ return a compact synthesis or explicitly print selected output with console.log.
54
+ Read text with tools.call('read_file', {file_path:'/exact/discovered/path', offset:0, limit:60}).
55
+ Nova filesystem text pages return source text, preserving newlines, without display headers
56
+ or loop-warning notes. Use that string directly for edit_file.old_string; do not add headers.
57
+ If a raw page exceeds display limits, request a smaller offset/limit instead of a full edit.
58
+ Use exact workspace virtual paths from discovery; do not reconstruct Windows host paths.
59
+ On file-not-found, use glob/ls to discover the correct path before retrying.
60
+ Keep large read batches in variables and return small passages; truncated output is incomplete
61
+ evidence. Inspect smaller slices from those variables before claiming you reviewed the source.
62
+ For Markdown edits, use quoted strings with newline escapes or arrays joined with '\\n';
63
+ backticks inside JavaScript template literals must be escaped exactly once. Never replay a
64
+ whole script after a runtime failure without checking which side effects already completed.
65
+ MCPCode is a compatibility async-function body using the same guarded runtime.
66
+ Human approvals pause the live script. Never retry completed side effects after failure.
67
+ Interaction, planning, workspace transitions and media fallbacks remain direct tools.
68
+ """
69
+
70
+
71
+ def code_mode_surface(tools: list) -> list:
72
+ """Filter schemas only; the owning ToolNode retains the complete inventory."""
73
+ from novacode_cli.config.browser_mode import browser_mode_enabled
74
+ from novacode_cli.config.plan_mode import active_plan_session
75
+
76
+ if browser_mode_enabled():
77
+ return tools
78
+
79
+ # Planning forbids interpreters; retain its existing read-only direct tool path.
80
+ if getattr(active_plan_session.get(), "plan_mode_enabled", False):
81
+ return [
82
+ t
83
+ for t in tools
84
+ if (t.get("name") if isinstance(t, dict) else t.name) not in {"eval", "MCPCode"}
85
+ ]
86
+ surface = [
87
+ t
88
+ for t in tools
89
+ if (t.get("name") if isinstance(t, dict) else t.name)
90
+ in (DIRECT_ONLY_TOOLS - {"MCPTool", "task"})
91
+ | {"read_file", "search_tools"}
92
+ | SEARCH_VARIANTS
93
+ ]
94
+ # Keep the host tool intact; only its model-facing media exception is advertised.
95
+ media_description = (
96
+ "Media fallback only: read an image or PDF through the direct multimodal path. "
97
+ "For source code, Markdown and all other text files, use eval with "
98
+ "await tools.call('read_file', {file_path:'/exact/path', offset:0, limit:60})."
99
+ )
100
+ return [
101
+ (
102
+ {**t, "description": media_description}
103
+ if isinstance(t, dict)
104
+ else t.model_copy(update={"description": media_description})
105
+ )
106
+ if (t.get("name") if isinstance(t, dict) else t.name) == "read_file"
107
+ else t
108
+ for t in surface
109
+ ]
110
+
111
+
112
+ # These require their own graph step, user interaction, or interpreter lifecycle.
113
+ DIRECT_ONLY_TOOLS = frozenset(
114
+ {
115
+ "MCPCode",
116
+ "MCPTool",
117
+ "eval",
118
+ "task",
119
+ "write_todos",
120
+ "handoff_save", # Owns foreground context; keep on the direct graph tool path.
121
+ "wait_for_task",
122
+ "start_async_task",
123
+ "check_async_task",
124
+ "ask_user_question",
125
+ "enter_plan_mode",
126
+ "exit_plan_mode",
127
+ "EnterWorktreeTool",
128
+ "ExitWorktreeTool",
129
+ "computer_screenshot", # Image results need the direct multimodal tool path.
130
+ "computer_action",
131
+ }
132
+ )
133
+
134
+
135
+ class NativeInput(BaseModel):
136
+ """Discovery and calls over the owning graph's ordinary tool inventory."""
137
+
138
+ model_config = ConfigDict(extra="forbid", strict=True)
139
+ namespace: Literal["tools"]
140
+ action: Literal["list", "describe", "call"]
141
+ tool_name: str = ""
142
+ arguments: dict[str, Any] = Field(default_factory=dict)
143
+ offset: int = Field(default=0, ge=0)
144
+ limit: int = Field(default=20, ge=1, le=50)
145
+
146
+
147
+ def _native_tools(request: ToolCallRequest) -> dict[str, BaseTool]:
148
+ """Read capabilities from this invocation; never retain another graph's tools."""
149
+ if request.runtime is None:
150
+ raise ValueError("Code Mode requires its owning agent runtime.")
151
+ available = {}
152
+ for target in request.runtime.tools:
153
+ if target.name in DIRECT_ONLY_TOOLS or (target.metadata or {}).get("nova_mcp_server"):
154
+ continue
155
+ # Most control tools declare Command returns. read_file also declares it
156
+ # for media; ordinary text reads are supported, media falls back to direct.
157
+ functions = (getattr(target, "func", None), getattr(target, "coroutine", None))
158
+ if not any(functions):
159
+ functions = (target._run, target._arun)
160
+ if target.name != "read_file" and any(
161
+ re.search(r"\bCommand\b", str(inspect.signature(fn).return_annotation))
162
+ for fn in functions
163
+ if fn is not None
164
+ ):
165
+ continue
166
+ available[target.name] = target
167
+ return available
168
+
169
+
170
+ class CodeInput(BaseModel):
171
+ """A bounded JavaScript async function body."""
172
+
173
+ model_config = ConfigDict(extra="forbid", strict=True)
174
+ code: str = Field(min_length=1, max_length=40000)
175
+
176
+
177
+ class TaskInput(BaseModel):
178
+ """The library's public JavaScript delegation API."""
179
+
180
+ model_config = ConfigDict(extra="forbid", strict=True)
181
+ namespace: Literal["task"]
182
+ description: str = Field(min_length=1)
183
+ subagent_type: str = Field(min_length=1, alias="subagentType")
184
+ response_schema: dict[str, Any] | None = Field(default=None, alias="responseSchema")
185
+ label: str | None = None
186
+
187
+
188
+ class CodeModeMiddleware(CodeInterpreterMiddleware):
189
+ """Must precede other tool middleware so nested requests retain their guards."""
190
+
191
+ def __init__(
192
+ self,
193
+ mcp: Any = None,
194
+ *,
195
+ auto_approve: bool = False,
196
+ concurrency: int = 4,
197
+ max_calls: int = 32,
198
+ cpu_timeout: float = 2,
199
+ wall_timeout: float = 60,
200
+ call_timeout: float = 30,
201
+ memory_limit: int = 64 * 1024 * 1024,
202
+ ) -> None:
203
+ """Use the owning graph's tools, optional MCP inventory and approval setting."""
204
+ super().__init__(
205
+ memory_limit=memory_limit,
206
+ timeout=cpu_timeout,
207
+ max_ptc_calls=None,
208
+ max_result_chars=MAX_OUTPUT_CHARS,
209
+ subagents=False,
210
+ mode="thread",
211
+ )
212
+ if min(concurrency, max_calls, cpu_timeout, wall_timeout, call_timeout, memory_limit) <= 0:
213
+ raise ValueError("Code Mode limits must be positive.")
214
+ self._mcp = mcp
215
+ self._catalog = build_mcp_tool(mcp) if mcp is not None else None
216
+ self._auto_approve = auto_approve
217
+ self._concurrency = concurrency
218
+ self._max_calls = max_calls
219
+ self._cpu_timeout = cpu_timeout
220
+ self._wall_timeout = wall_timeout
221
+ self._call_timeout = call_timeout
222
+ self._memory_limit = memory_limit
223
+ self._state_warnings: dict[str, str] = {}
224
+ self._execution_locks: dict[str, asyncio.Lock] = {}
225
+
226
+ def unavailable(code: str) -> str: # noqa: ARG001 -- public tool schema.
227
+ return "MCPCode requires its owning agent middleware."
228
+
229
+ self.tools = [
230
+ StructuredTool.from_function(
231
+ unavailable,
232
+ name="MCPCode",
233
+ args_schema=CodeInput,
234
+ description=(
235
+ "Run JavaScript to discover, call and process Nova tools. "
236
+ "code is an async function body: return the final small result. "
237
+ "await tools.list({offset?, limit?}); await tools.describe(tool_name); "
238
+ "await tools.call(tool_name, arguments) returns structured data. "
239
+ "task({description, subagentType, responseSchema?}) delegates "
240
+ "to a configured agent. "
241
+ "await mcp.list({server_name?, offset?, limit?}); "
242
+ "await mcp.describe(server_name, tool_name) returns the original schema; "
243
+ "await mcp.call(server_name, tool_name, arguments) returns structured data. "
244
+ "Use exact names/schema from discovery. Supports loops and Promise.all. "
245
+ "No direct filesystem, network or imports. "
246
+ "State persists in this conversation. "
247
+ "Nested calls retain permissions and pause for human approval. "
248
+ "Text file reads are supported; media reads require direct read_file calls. "
249
+ "Do not retry writes blindly after partial failures or timeouts."
250
+ ),
251
+ )
252
+ ]
253
+ self.tools.insert(
254
+ 0,
255
+ self.tools[0].model_copy(
256
+ update={
257
+ "name": "eval",
258
+ "description": (
259
+ "Primary control surface: evaluate JavaScript with top-level await. "
260
+ "Variables persist across snippets and user turns. Select results with "
261
+ "the final expression or bounded console.log output. "
262
+ + self.tools[0].description.replace(
263
+ "code is an async function body: return the final small result. ", ""
264
+ )
265
+ ),
266
+ }
267
+ ),
268
+ )
269
+
270
+ async def abefore_agent(self, state: Any, runtime: Any) -> dict[str, Any] | None:
271
+ """Report failed restoration instead of silently claiming variables survived."""
272
+ update = await super().abefore_agent(state, runtime)
273
+ if update is not None:
274
+ thread = _resolve_thread_id(self._fallback_thread_id)
275
+ await self._registry.aevict(thread)
276
+ self._state_warnings[thread] = (
277
+ "JavaScript state restoration failed; variables were cleared."
278
+ )
279
+ return update
280
+
281
+ async def aafter_agent(self, state: Any, runtime: Any) -> dict[str, Any] | None:
282
+ """Keep the installed snapshot lifecycle and surface snapshot loss."""
283
+ update = await super().aafter_agent(state, runtime)
284
+ self._execution_locks.pop(_resolve_thread_id(self._fallback_thread_id), None)
285
+ if update is not None and update.get("_quickjs_snapshot_payload") is None:
286
+ thread = _resolve_thread_id(self._fallback_thread_id)
287
+ self._state_warnings[thread] = (
288
+ "JavaScript state could not be saved; variables were cleared."
289
+ )
290
+ return update
291
+
292
+ @staticmethod
293
+ def _model_request(request: ModelRequest, *, enabled: bool) -> ModelRequest:
294
+ if enabled:
295
+ from deepagents.middleware._utils import append_to_system_message
296
+
297
+ from novacode_cli.config.browser_mode import browser_mode_enabled
298
+ from novacode_cli.config.plan_mode import active_plan_session
299
+
300
+ if browser_mode_enabled() or getattr(
301
+ active_plan_session.get(), "plan_mode_enabled", False
302
+ ):
303
+ return request
304
+
305
+ return request.override(
306
+ tools=code_mode_surface(request.tools),
307
+ system_message=append_to_system_message(request.system_message, CODE_MODE_GUIDANCE),
308
+ )
309
+ return request.override(
310
+ tools=[
311
+ t
312
+ for t in request.tools
313
+ if (t.get("name") if isinstance(t, dict) else t.name) not in {"MCPCode", "eval"}
314
+ ]
315
+ )
316
+
317
+ def wrap_model_call(self, request: ModelRequest, handler: Callable) -> Any:
318
+ """Expose Code Mode only while the saved preference is enabled."""
319
+ return handler(self._model_request(request, enabled=NovaConfig().get_code_mode_enabled()))
320
+
321
+ async def awrap_model_call(self, request: ModelRequest, handler: Callable) -> Any:
322
+ """Apply preference changes to existing graphs without rebuilding them."""
323
+ enabled = await asyncio.to_thread(lambda: NovaConfig().get_code_mode_enabled())
324
+ return await handler(self._model_request(request, enabled=enabled))
325
+
326
+ @staticmethod
327
+ def _message(request: ToolCallRequest, content: str, *, error: bool = True) -> ToolMessage:
328
+ return ToolMessage(
329
+ content=content,
330
+ name=request.tool_call["name"],
331
+ tool_call_id=request.tool_call["id"],
332
+ status="error" if error else "success",
333
+ )
334
+
335
+ def wrap_tool_call(self, request: ToolCallRequest, handler: Callable) -> Any:
336
+ """Fail closed in sync graphs; Nova's normal agent execution is async."""
337
+ if request.tool_call["name"] not in {"MCPCode", "eval"}:
338
+ return handler(request)
339
+ return self._message(request, "MCPCode requires asynchronous agent execution.")
340
+
341
+ async def awrap_tool_call( # noqa: PLR0911 -- explicit fail-closed interpreter gates.
342
+ self,
343
+ request: ToolCallRequest,
344
+ handler: Callable[[ToolCallRequest], Awaitable[Any]],
345
+ ) -> Any:
346
+ """Route each nested call through the remaining middleware and ToolNode."""
347
+ if request.tool_call["name"] not in {"MCPCode", "eval"}:
348
+ return await handler(request)
349
+ if not await asyncio.to_thread(lambda: NovaConfig().get_code_mode_enabled()):
350
+ return self._message(request, "MCP Code Mode is disabled.")
351
+ if get_policy().tool_default("eval") == "deny":
352
+ return self._message(request, "Code Mode denied by approval policy.")
353
+ if get_policy().tool_default("MCPCode") == "deny":
354
+ return self._message(request, "MCPCode denied by approval policy.")
355
+ if (
356
+ get_policy().tool_default("MCPCode") == "ask"
357
+ or get_policy().tool_default("eval") == "ask"
358
+ ) and not self._auto_approval_enabled():
359
+ return self._message(
360
+ request, "MCPCode requires approval; configure its policy before use."
361
+ )
362
+
363
+ async def execute_code(code: str) -> ToolMessage | Command:
364
+ params = CodeInput(code=code)
365
+ thread = _resolve_thread_id(self._fallback_thread_id)
366
+ lock = self._execution_locks.setdefault(thread, asyncio.Lock())
367
+ # One persistent VM per conversation; queue separate scripts, not their host calls.
368
+ async with lock:
369
+ try:
370
+ return await self._execute(request, handler, params.code)
371
+ except asyncio.CancelledError:
372
+ await self._registry.aevict(thread)
373
+ raise
374
+
375
+ # Execute inside ToolNode, so tool callbacks and downstream wrappers see
376
+ # the actual duration, result and failures, not a preliminary probe.
377
+ live_tool = next(t for t in self.tools if t.name == request.tool_call["name"]).model_copy(
378
+ update={"coroutine": execute_code}
379
+ )
380
+ try:
381
+ return await handler(request.override(tool=live_tool))
382
+ except Exception as exc:
383
+ return self._message(request, f"MCPCode failed: {type(exc).__name__}: {exc}"[:4000])
384
+
385
+ async def _execute( # noqa: PLR0915 -- guarded dispatch and bounded result reporting.
386
+ self, request: ToolCallRequest, handler: Callable, code: str
387
+ ) -> ToolMessage | Command:
388
+ from novacode_cli.agents.code_runtime import run_script
389
+
390
+ calls = failures = 0
391
+ semaphore = asyncio.Semaphore(self._concurrency)
392
+ updates: dict[str, Any] = {}
393
+
394
+ async def dispatch(raw: str) -> str:
395
+ nonlocal calls, failures
396
+ calls += 1
397
+ try:
398
+ if calls > self._max_calls:
399
+ raise ValueError("MCPCode call budget exceeded.")
400
+ if not isinstance(raw, str) or len(raw) > MAX_ARGUMENT_CHARS:
401
+ raise ValueError("MCPCode arguments exceed the size limit.")
402
+ payload = json.loads(raw)
403
+ async with semaphore:
404
+ if isinstance(payload, dict) and payload.get("namespace") == "task":
405
+ value = await self._task(request, handler, payload, updates)
406
+ elif isinstance(payload, dict) and payload.get("namespace") == "tools":
407
+ native = NativeInput.model_validate(payload)
408
+ value = await self._native(request, handler, native)
409
+ else:
410
+ params = MCPInput.model_validate(payload, strict=True)
411
+ if get_policy().tool_default("MCPTool") == "deny":
412
+ raise ValueError("MCPTool denied by approval policy.")
413
+ if self._catalog is None:
414
+ raise ValueError("No MCP tools configured. Use tools.list().")
415
+ if params.action != "call":
416
+ value = self._catalog.invoke(params.model_dump())
417
+ else:
418
+ value = await self._call(request, handler, params)
419
+ encoded = json.dumps(value, ensure_ascii=False, allow_nan=False)
420
+ if len(encoded) > MAX_INTERMEDIATE_CHARS:
421
+ raise ValueError(
422
+ "Tool result exceeds 1000000 characters; request fewer records."
423
+ )
424
+ return encoded
425
+ except Exception:
426
+ failures += 1
427
+ raise
428
+
429
+ try:
430
+ result, error = await run_script(
431
+ code,
432
+ dispatch,
433
+ cpu_timeout=self._cpu_timeout,
434
+ wall_timeout=self._wall_timeout,
435
+ memory_limit=self._memory_limit,
436
+ repl=self._registry.get(_resolve_thread_id(self._fallback_thread_id)),
437
+ runtime=request.runtime,
438
+ expression=request.tool_call["name"] == "eval",
439
+ )
440
+ if error is not None:
441
+ raise ValueError(error)
442
+ encoded = json.dumps(result, ensure_ascii=False, allow_nan=False)
443
+ truncated = len(encoded) > MAX_OUTPUT_CHARS
444
+ body = {
445
+ "result": encoded[:MAX_OUTPUT_CHARS] if truncated else result,
446
+ "truncated": truncated,
447
+ "calls": calls,
448
+ "failed_calls": failures,
449
+ }
450
+ if truncated:
451
+ body["notice"] = (
452
+ "Output is incomplete. Inspect smaller slices of your saved interpreter "
453
+ "variables; do not claim truncated source was reviewed or replay side effects."
454
+ )
455
+ warning = self._state_warnings.pop(_resolve_thread_id(self._fallback_thread_id), None)
456
+ if warning is not None:
457
+ body["state_warning"] = warning
458
+ message = self._message(request, json.dumps(body, ensure_ascii=False), error=False)
459
+ if updates:
460
+ return Command(update={**updates, "messages": [message]})
461
+ return message # noqa: TRY300 -- same error boundary covers output serialization.
462
+ except Exception as exc:
463
+ hint = (
464
+ "JavaScript syntax error with no host tool calls. For Markdown, prefer quoted "
465
+ "strings or an array of lines joined with '\\n'; escape template backticks "
466
+ "exactly once."
467
+ if calls == 0 and "SyntaxError" in str(exc)
468
+ else "Inspect completed calls before retrying; "
469
+ "external side effects are not rolled back."
470
+ )
471
+ message = self._message(
472
+ request,
473
+ json.dumps(
474
+ {
475
+ "error": f"{type(exc).__name__}: {exc}"[:4000],
476
+ "calls": calls,
477
+ "failed_calls": failures,
478
+ "hint": hint,
479
+ }
480
+ ),
481
+ )
482
+ return Command(update={**updates, "messages": [message]}) if updates else message
483
+
484
+ async def _native(
485
+ self, request: ToolCallRequest, handler: Callable, params: NativeInput
486
+ ) -> Any:
487
+ available = _native_tools(request)
488
+ if params.action == "list":
489
+ entries = [
490
+ {"name": t.name, "description": t.description[:300]} for t in available.values()
491
+ ]
492
+ return {
493
+ "tools": entries[params.offset : params.offset + params.limit],
494
+ "total": len(entries),
495
+ "offset": params.offset,
496
+ }
497
+ target = available.get(params.tool_name)
498
+ if params.action == "describe" and params.tool_name == "task":
499
+ target = next((t for t in request.runtime.tools if t.name == "task"), None)
500
+ if target is None:
501
+ raise ValueError(
502
+ "Unknown or unsupported tool; use tools.list() or call the tool directly."
503
+ )
504
+ if params.action == "describe":
505
+ return {"name": target.name, "schema": convert_to_openai_tool(target)["function"]}
506
+ return await self._invoke(request, handler, target, params.arguments)
507
+
508
+ async def _task(
509
+ self, request: ToolCallRequest, handler: Callable, payload: dict, updates: dict
510
+ ) -> Any:
511
+ """Delegate through the same guards, retaining only supported parent updates."""
512
+ import time
513
+
514
+ from langchain_quickjs._subagent import (
515
+ _ensure_schema_title,
516
+ _runtime_with_response_format,
517
+ _validate_response_schema,
518
+ )
519
+
520
+ params = TaskInput.model_validate(payload)
521
+ target = next((t for t in request.runtime.tools if t.name == "task"), None)
522
+ if target is None:
523
+ raise ValueError("No subagent task tool configured.")
524
+ if params.response_schema is not None:
525
+ _validate_response_schema(params.response_schema)
526
+ runtime = _runtime_with_response_format(
527
+ request.runtime, _ensure_schema_title(params.response_schema)
528
+ )
529
+ request = replace(request, runtime=runtime)
530
+ writer = request.runtime.stream_writer
531
+ task_id = f"mcp-code-{uuid4().hex}"
532
+ started = time.monotonic()
533
+ event = {"type": "subagent", "id": task_id, "eval_id": request.tool_call["id"]}
534
+ writer(
535
+ {
536
+ **event,
537
+ "phase": "start",
538
+ "description": params.description,
539
+ "subagent_type": params.subagent_type,
540
+ "label": (params.label or params.description)[:80],
541
+ }
542
+ )
543
+ try:
544
+ from novacode_cli.agents.code_runtime import execution_deadline
545
+
546
+ deadline = execution_deadline.get()
547
+ if deadline is None:
548
+ raise ValueError("Code Mode execution has ended.")
549
+ # Delegation uses the subagent's own execution limits.
550
+ with deadline.paused():
551
+ value = await self._invoke(
552
+ request,
553
+ handler,
554
+ target,
555
+ {
556
+ "description": params.description,
557
+ "subagent_type": params.subagent_type,
558
+ },
559
+ updates=updates,
560
+ call_id=task_id,
561
+ )
562
+ if params.response_schema is not None and isinstance(value, str):
563
+ value = json.loads(value)
564
+ if params.response_schema is not None:
565
+ _validator(json.dumps(params.response_schema), close_named=False).validate(value)
566
+ writer(
567
+ {
568
+ **event,
569
+ "phase": "complete",
570
+ "duration_ms": int((time.monotonic() - started) * 1000),
571
+ }
572
+ )
573
+ return value # noqa: TRY300 -- completion is part of the traced dispatch.
574
+ except BaseException as exc:
575
+ writer(
576
+ {
577
+ **event,
578
+ "phase": "error",
579
+ "error": str(exc),
580
+ "duration_ms": int((time.monotonic() - started) * 1000),
581
+ }
582
+ )
583
+ raise
584
+
585
+ async def _call(self, request: ToolCallRequest, handler: Callable, params: MCPInput) -> Any:
586
+ target = resolve_target(self._mcp, params)
587
+ if target is None:
588
+ raise ValueError("Unknown or disabled MCP tool; use mcp.list().")
589
+ return await self._invoke(request, handler, target, params.arguments, mcp=True)
590
+
591
+ async def _invoke( # noqa: PLR0912 -- validate and execute at the common trust boundary.
592
+ self,
593
+ request: ToolCallRequest,
594
+ handler: Callable,
595
+ target: BaseTool,
596
+ arguments: dict,
597
+ *,
598
+ mcp: bool = False,
599
+ updates: dict | None = None,
600
+ call_id: str | None = None,
601
+ ) -> Any:
602
+ schema = convert_to_openai_tool(target)["function"]["parameters"]
603
+ try:
604
+ _validator(json.dumps(schema, sort_keys=True), close_named=True).validate(arguments)
605
+ except Exception as exc:
606
+ raise ValueError(f"Invalid tool arguments: {exc}") from exc
607
+ edited = await self._authorize(request, target.name, arguments)
608
+ if edited is not None:
609
+ arguments = edited
610
+ _validator(json.dumps(schema, sort_keys=True), close_named=True).validate(arguments)
611
+ if mcp and get_policy().tool_default("MCPTool") == "deny":
612
+ raise ValueError("MCPTool denied by approval policy.")
613
+ call_id = call_id or f"mcp-code-{uuid4().hex}"
614
+ call: ToolCall = {
615
+ "name": target.name,
616
+ "args": arguments,
617
+ "id": call_id,
618
+ "type": "tool_call",
619
+ }
620
+ if request.runtime is None:
621
+ raise ValueError("MCPCode requires its owning agent runtime.")
622
+ nested = replace(
623
+ request,
624
+ tool=target,
625
+ tool_call=call,
626
+ runtime=replace(request.runtime, tool_call_id=call_id),
627
+ )
628
+ with capture_file_read(enabled=target.name == "read_file" and not mcp) as capture:
629
+ async with asyncio.timeout(
630
+ None if updates is not None else self._call_timeout
631
+ ) as deadline:
632
+ try:
633
+ result = await handler(nested)
634
+ except ValueError as exc:
635
+ if updates is not None and "matching ToolMessage in Command.update" in str(exc):
636
+ # ToolNode's diagnostic includes the child messages verbatim.
637
+ raise TypeError("Subagent returned unsupported messages.") from None
638
+ raise
639
+ # Some MCP adapters convert cancellation into a textual tool result.
640
+ if deadline.expired():
641
+ raise TimeoutError("MCP tool call timeout.")
642
+ if isinstance(result, Command) and updates is not None:
643
+ if result.goto or result.graph or not isinstance(result.update, dict):
644
+ raise TypeError("Subagent returned unsupported graph mutation.")
645
+ if set(result.update) - {"messages", "files"}:
646
+ raise TypeError("Subagent returned unsupported state mutation.")
647
+ messages = result.update.get("messages", [])
648
+ if len(messages) != 1 or not isinstance(messages[0], ToolMessage):
649
+ raise TypeError("Subagent returned unsupported messages.")
650
+ files = result.update.get("files", {})
651
+ if files:
652
+ updates.setdefault("files", {}).update(files)
653
+ result = messages[0]
654
+ if not isinstance(result, ToolMessage):
655
+ raise TypeError("Tool returned unsupported state mutation; call this tool directly.")
656
+ if result.status == "error":
657
+ raise ValueError(str(result.content))
658
+ value = self._result_value(result)
659
+ if target.name in {"read_file", "write_file", "edit_file", "ls", "glob", "grep"} and (
660
+ isinstance(value, str) and value.startswith("Error:")
661
+ ):
662
+ hint = (
663
+ " Discover the exact virtual path with glob/ls before retrying; "
664
+ "do not guess a host path."
665
+ if "not found" in value.lower()
666
+ else ""
667
+ )
668
+ raise ValueError(value + hint)
669
+ if isinstance(value, str) and value.startswith("[MCP error]"):
670
+ raise ValueError(value)
671
+ if capture is not None and (raw := capture.source_text(value)) is not None:
672
+ value = raw
673
+ return value
674
+
675
+ def _auto_approval_enabled(self) -> bool:
676
+ from novacode_cli.config.plan_mode import active_plan_session
677
+
678
+ session = active_plan_session.get()
679
+ return bool(getattr(session, "auto_approve", self._auto_approve))
680
+
681
+ async def _authorize(self, request: ToolCallRequest, name: str, arguments: dict) -> dict | None:
682
+ """Reuse session permissions and optional approval classifiers."""
683
+ from novacode_cli.core.code_approvals import current_code_approvals
684
+ from novacode_cli.security.approval_context import request_user_text
685
+ from novacode_cli.ui.hitl_approval import evaluate_tool_actions
686
+
687
+ broker = current_code_approvals.get()
688
+ if broker is not None:
689
+ broker.ensure_active()
690
+ user_request = request_user_text(request)
691
+ decisions = await asyncio.to_thread(
692
+ evaluate_tool_actions,
693
+ {"action_requests": [{"name": name, "args": arguments}]},
694
+ SimpleNamespace(auto_approve=self._auto_approval_enabled()),
695
+ user_request=user_request,
696
+ )
697
+ decision = decisions[0] if len(decisions) == 1 else None
698
+ if decision is None:
699
+ from novacode_cli.agents.code_runtime import execution_deadline
700
+
701
+ if broker is None or request.runtime is None:
702
+ raise ValueError(f"{name} requires approval; no active approval handler.")
703
+ deadline = execution_deadline.get()
704
+ if deadline is None:
705
+ raise ValueError("Code Mode execution has ended.")
706
+ with deadline.paused():
707
+ response = await broker.request(
708
+ request.runtime.stream_writer,
709
+ {
710
+ "action_requests": [{"name": name, "args": arguments}],
711
+ "review_configs": [
712
+ {
713
+ "action_name": name,
714
+ "allowed_decisions": ["approve", "edit", "reject"],
715
+ }
716
+ ],
717
+ },
718
+ )
719
+ choices = response.get("decisions", [])
720
+ decision = choices[0] if len(choices) == 1 else None
721
+ if not isinstance(decision, dict):
722
+ raise ValueError("Missing approval decision.")
723
+ edited = None
724
+ if decision.get("type") == "edit":
725
+ action = decision.get("edited_action", {})
726
+ if action.get("name") != name or not isinstance(action.get("args"), dict):
727
+ raise ValueError("Approval edits must retain the exact tool name.")
728
+ edited = action["args"]
729
+ # A decision cannot authorize permissions revoked while the script waited.
730
+ refreshed = await asyncio.to_thread(
731
+ evaluate_tool_actions,
732
+ {
733
+ "action_requests": [
734
+ {"name": name, "args": edited if edited is not None else arguments}
735
+ ]
736
+ },
737
+ SimpleNamespace(auto_approve=False),
738
+ user_request=user_request,
739
+ )
740
+ if refreshed and refreshed[0] and refreshed[0].get("type") == "reject":
741
+ raise ValueError(f"{name} denied after permission change.")
742
+ if edited is not None:
743
+ return edited
744
+ if decision.get("type") != "approve":
745
+ raise ValueError(f"{name} denied: {decision.get('reason', 'approval rejected')}")
746
+ return None
747
+
748
+ @staticmethod
749
+ def _result_value(result: ToolMessage) -> Any:
750
+ """Keep structured MCP values inside the script."""
751
+ artifact = result.artifact
752
+ if isinstance(artifact, dict) and "structured_content" in artifact:
753
+ return artifact["structured_content"]
754
+ value = (
755
+ data_text(result.content, result.additional_kwargs)
756
+ if isinstance(result.content, str)
757
+ else result.content
758
+ )
759
+ if isinstance(value, str):
760
+ try:
761
+ return json.loads(value)
762
+ except ValueError:
763
+ return value
764
+ if (
765
+ isinstance(value, list)
766
+ and len(value) == 1
767
+ and isinstance(value[0], dict)
768
+ and value[0].get("type") == "text"
769
+ ):
770
+ try:
771
+ return json.loads(value[0]["text"])
772
+ except ValueError:
773
+ return value[0]["text"]
774
+ return value