iac-code 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. {iac_code-0.4.0 → iac_code-0.5.0}/MANIFEST.in +1 -1
  2. {iac_code-0.4.0 → iac_code-0.5.0}/PKG-INFO +2 -2
  3. {iac_code-0.4.0 → iac_code-0.5.0}/README.md +2 -2
  4. {iac_code-0.4.0 → iac_code-0.5.0}/pyproject.toml +1 -0
  5. iac_code-0.5.0/src/iac_code/__init__.py +2 -0
  6. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/executor.py +18 -2
  7. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/persistence.py +2 -3
  8. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/push.py +1 -0
  9. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/push_queue.py +17 -4
  10. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/push_secrets.py +9 -1
  11. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/task_store.py +7 -6
  12. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transports/redis_streams.py +8 -1
  13. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transports/unix.py +18 -2
  14. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/http_sse.py +53 -3
  15. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/server.py +55 -5
  16. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/session.py +26 -2
  17. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/slash_registry.py +11 -8
  18. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/tools.py +39 -4
  19. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/agent/agent_loop.py +483 -15
  20. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/agent/agent_tool.py +80 -25
  21. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/agent/agent_types.py +11 -9
  22. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/agent/message.py +58 -0
  23. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/agent/system_prompt.py +48 -6
  24. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/cli/main.py +6 -0
  25. iac_code-0.5.0/src/iac_code/cli/update.py +38 -0
  26. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/__init__.py +22 -3
  27. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/auth.py +2 -0
  28. iac_code-0.5.0/src/iac_code/commands/memory.py +241 -0
  29. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/model.py +25 -10
  30. iac_code-0.5.0/src/iac_code/commands/prompt.py +693 -0
  31. iac_code-0.5.0/src/iac_code/commands/status.py +203 -0
  32. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/tasks.py +2 -1
  33. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/config.py +3 -2
  34. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/i18n/__init__.py +13 -1
  35. iac_code-0.5.0/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.mo +0 -0
  36. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po +1335 -696
  37. iac_code-0.5.0/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.mo +0 -0
  38. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po +1338 -696
  39. iac_code-0.5.0/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.mo +0 -0
  40. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po +1341 -696
  41. iac_code-0.5.0/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.mo +0 -0
  42. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po +1305 -696
  43. iac_code-0.5.0/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.mo +0 -0
  44. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po +1338 -696
  45. iac_code-0.5.0/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.mo +0 -0
  46. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1303 -696
  47. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/memory/memory_manager.py +24 -0
  48. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/memory/memory_tools.py +17 -11
  49. iac_code-0.5.0/src/iac_code/memory/project_memory.py +149 -0
  50. iac_code-0.5.0/src/iac_code/memory/recall.py +452 -0
  51. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/anthropic_provider.py +28 -8
  52. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/base.py +1 -0
  53. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/dashscope_provider.py +21 -2
  54. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/manager.py +101 -29
  55. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/openai_provider.py +12 -1
  56. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/openrouter_provider.py +12 -9
  57. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/agent_factory.py +33 -4
  58. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/context_manager.py +76 -3
  59. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/permissions/loader.py +1 -0
  60. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/permissions/pipeline.py +32 -2
  61. iac_code-0.5.0/src/iac_code/services/permissions/trusted_roots.py +21 -0
  62. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/providers/aliyun.py +48 -25
  63. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/providers/aliyun_oauth.py +23 -7
  64. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/session_index.py +43 -1
  65. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/__init__.py +2 -1
  66. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/identity.py +27 -1
  67. iac_code-0.5.0/src/iac_code/services/token_counter.py +149 -0
  68. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/update_checker.py +8 -3
  69. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/auto_trigger.py +16 -3
  70. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/__init__.py +4 -0
  71. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/SKILL.md +4 -0
  72. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/auto_trigger.py +10 -0
  73. iac_code-0.5.0/src/iac_code/skills/bundled/pac_aliyun/SKILL.md +65 -0
  74. iac_code-0.5.0/src/iac_code/skills/bundled/pac_aliyun/__init__.py +20 -0
  75. iac_code-0.5.0/src/iac_code/skills/bundled/pac_aliyun/auto_trigger.py +87 -0
  76. iac_code-0.5.0/src/iac_code/skills/bundled/pac_aliyun/references/infraguard-policy-generation.md +75 -0
  77. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/renderer.py +119 -30
  78. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/skill_tool.py +6 -1
  79. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/state/app_state.py +1 -1
  80. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tasks/task_state.py +20 -5
  81. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tasks/task_tools.py +2 -1
  82. iac_code-0.5.0/src/iac_code/tools/bash/argv_safety.py +1112 -0
  83. iac_code-0.5.0/src/iac_code/tools/bash/path_validation.py +249 -0
  84. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/bash/permissions.py +33 -28
  85. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/bash/readonly_commands.py +9 -24
  86. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/bash/safety_checks.py +10 -53
  87. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/ros_stack.py +313 -139
  88. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/base_stack.py +111 -49
  89. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/types.py +10 -1
  90. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/edit_file.py +24 -0
  91. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/glob.py +56 -0
  92. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/grep.py +127 -4
  93. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/list_files.py +24 -5
  94. iac_code-0.5.0/src/iac_code/tools/path_safety.py +218 -0
  95. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/read_file.py +99 -19
  96. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/tool_executor.py +1 -9
  97. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/web_fetch.py +54 -6
  98. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/write_file.py +24 -0
  99. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/types/permissions.py +1 -0
  100. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/types/stream_events.py +9 -0
  101. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/core/input_history.py +5 -5
  102. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/core/prompt_input.py +10 -8
  103. iac_code-0.5.0/src/iac_code/ui/dialogs/memory.py +150 -0
  104. iac_code-0.5.0/src/iac_code/ui/dialogs/memory_editor.py +341 -0
  105. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/dialogs/resume_picker.py +9 -7
  106. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/renderer.py +251 -47
  107. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/repl.py +162 -54
  108. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/suggestions/command_provider.py +15 -7
  109. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/suggestions/shell_history_provider.py +36 -2
  110. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/file_security.py +19 -0
  111. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code.egg-info/PKG-INFO +2 -2
  112. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code.egg-info/SOURCES.txt +13 -0
  113. iac_code-0.4.0/src/iac_code/__init__.py +0 -2
  114. iac_code-0.4.0/src/iac_code/commands/memory.py +0 -85
  115. iac_code-0.4.0/src/iac_code/commands/status.py +0 -98
  116. iac_code-0.4.0/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.mo +0 -0
  117. iac_code-0.4.0/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.mo +0 -0
  118. iac_code-0.4.0/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.mo +0 -0
  119. iac_code-0.4.0/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.mo +0 -0
  120. iac_code-0.4.0/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.mo +0 -0
  121. iac_code-0.4.0/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.mo +0 -0
  122. iac_code-0.4.0/src/iac_code/services/token_counter.py +0 -81
  123. iac_code-0.4.0/src/iac_code/tools/bash/path_validation.py +0 -118
  124. {iac_code-0.4.0 → iac_code-0.5.0}/LICENSE +0 -0
  125. {iac_code-0.4.0 → iac_code-0.5.0}/setup.cfg +0 -0
  126. {iac_code-0.4.0 → iac_code-0.5.0}/setup.py +0 -0
  127. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/__init__.py +0 -0
  128. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/agent_card.py +0 -0
  129. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/app.py +0 -0
  130. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/artifacts.py +0 -0
  131. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/client.py +0 -0
  132. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/events.py +0 -0
  133. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/exposure.py +0 -0
  134. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/metrics.py +0 -0
  135. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/parts.py +0 -0
  136. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/push_worker.py +0 -0
  137. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/router.py +0 -0
  138. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/signing.py +0 -0
  139. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transport.py +0 -0
  140. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transports/__init__.py +0 -0
  141. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transports/base.py +0 -0
  142. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transports/dispatcher.py +0 -0
  143. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transports/grpc.py +0 -0
  144. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transports/grpc_jsonrpc.py +0 -0
  145. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transports/http.py +0 -0
  146. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transports/stdio.py +0 -0
  147. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/transports/websocket.py +0 -0
  148. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/a2a/types.py +0 -0
  149. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/__init__.py +0 -0
  150. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/convert.py +0 -0
  151. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/mcp.py +0 -0
  152. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/metrics.py +0 -0
  153. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/state.py +0 -0
  154. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/types.py +0 -0
  155. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/acp/version.py +0 -0
  156. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/agent/__init__.py +0 -0
  157. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/cli/__init__.py +0 -0
  158. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/cli/headless.py +0 -0
  159. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/cli/install_git_bash.py +0 -0
  160. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/cli/output_formats.py +0 -0
  161. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/clear.py +0 -0
  162. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/compact.py +0 -0
  163. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/debug.py +0 -0
  164. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/effort.py +0 -0
  165. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/exit.py +0 -0
  166. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/help.py +0 -0
  167. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/registry.py +0 -0
  168. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/rename.py +0 -0
  169. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/resume.py +0 -0
  170. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/commands/skills.py +0 -0
  171. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/memory/__init__.py +0 -0
  172. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/__init__.py +0 -0
  173. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/azure_openai_provider.py +0 -0
  174. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/deepseek_provider.py +0 -0
  175. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/gemini_provider.py +0 -0
  176. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/kimi_provider.py +0 -0
  177. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/lmstudio_provider.py +0 -0
  178. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/minimax_provider.py +0 -0
  179. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/modelscope_provider.py +0 -0
  180. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/ollama_provider.py +0 -0
  181. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/registry.py +0 -0
  182. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/retry.py +0 -0
  183. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/siliconflow_provider.py +0 -0
  184. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/stream_watchdog.py +0 -0
  185. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/thinking.py +0 -0
  186. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/volcengine_provider.py +0 -0
  187. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/providers/zhipu_provider.py +0 -0
  188. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/__init__.py +0 -0
  189. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/capabilities/__init__.py +0 -0
  190. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/capabilities/auto_detect.py +0 -0
  191. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/capabilities/multimodal.py +0 -0
  192. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/cloud_credentials.py +0 -0
  193. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/permissions/__init__.py +0 -0
  194. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/permissions/storage.py +0 -0
  195. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/providers/__init__.py +0 -0
  196. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/qwenpaw_source.py +0 -0
  197. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/session_metadata.py +0 -0
  198. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/session_resolver.py +0 -0
  199. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/session_storage.py +0 -0
  200. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/session_usage.py +0 -0
  201. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/attributes.py +0 -0
  202. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/client.py +0 -0
  203. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/config.py +0 -0
  204. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/constants.py +0 -0
  205. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/content_serializer.py +0 -0
  206. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/events.py +0 -0
  207. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/fallback.py +0 -0
  208. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/metrics.py +0 -0
  209. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/names.py +0 -0
  210. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/sanitize.py +0 -0
  211. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/sink.py +0 -0
  212. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/tracing.py +0 -0
  213. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/telemetry/types.py +0 -0
  214. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/services/token_budget.py +0 -0
  215. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/__init__.py +0 -0
  216. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/__init__.py +0 -0
  217. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +0 -0
  218. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +0 -0
  219. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +0 -0
  220. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +0 -0
  221. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +0 -0
  222. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +0 -0
  223. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/references/ros-template.md +0 -0
  224. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/references/template-parameter-recommendation.md +0 -0
  225. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +0 -0
  226. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +0 -0
  227. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +0 -0
  228. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/bundled/simplify.py +0 -0
  229. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/discovery.py +0 -0
  230. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/frontmatter.py +0 -0
  231. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/listing.py +0 -0
  232. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/loader.py +0 -0
  233. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/management.py +0 -0
  234. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/processor.py +0 -0
  235. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/settings.py +0 -0
  236. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/skills/skill_definition.py +0 -0
  237. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/state/__init__.py +0 -0
  238. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tasks/__init__.py +0 -0
  239. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tasks/notification_queue.py +0 -0
  240. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/__init__.py +0 -0
  241. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/base.py +0 -0
  242. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/bash/__init__.py +0 -0
  243. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/bash/bash_tool.py +0 -0
  244. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/bash/command_parser.py +0 -0
  245. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/bash/mode_validation.py +0 -0
  246. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/bash/rule_matching.py +0 -0
  247. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/__init__.py +0 -0
  248. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/__init__.py +0 -0
  249. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/aliyun_api.py +0 -0
  250. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/aliyun_doc_search.py +0 -0
  251. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/api_hooks.py +0 -0
  252. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/endpoints.yml +0 -0
  253. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/hooks/__init__.py +0 -0
  254. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/hooks/ros_parameters.py +0 -0
  255. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/hooks/ros_validate.py +0 -0
  256. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/ros_client.py +0 -0
  257. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/ros_stack_instances.py +0 -0
  258. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/ros_yaml.py +0 -0
  259. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/aliyun/user_agent.py +0 -0
  260. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/base_api.py +0 -0
  261. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/cloud/registry.py +0 -0
  262. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/tools/result_storage.py +0 -0
  263. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/types/__init__.py +0 -0
  264. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/types/skill_source.py +0 -0
  265. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/__init__.py +0 -0
  266. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/banner.py +0 -0
  267. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/components/__init__.py +0 -0
  268. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/components/dialog.py +0 -0
  269. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/components/divider.py +0 -0
  270. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/components/fuzzy_picker.py +0 -0
  271. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/components/progress_bar.py +0 -0
  272. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/components/search_box.py +0 -0
  273. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/components/select.py +0 -0
  274. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/components/status_icon.py +0 -0
  275. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/components/tabs.py +0 -0
  276. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/core/__init__.py +0 -0
  277. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/core/in_place_render.py +0 -0
  278. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/core/key_event.py +0 -0
  279. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/core/raw_input.py +0 -0
  280. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/core/raw_input_win.py +0 -0
  281. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/core/screen.py +0 -0
  282. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/dialogs/__init__.py +0 -0
  283. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/dialogs/global_search.py +0 -0
  284. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/dialogs/history_search.py +0 -0
  285. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/dialogs/model_picker.py +0 -0
  286. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/dialogs/quick_open.py +0 -0
  287. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/dialogs/skills_picker.py +0 -0
  288. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/keybindings/__init__.py +0 -0
  289. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/keybindings/manager.py +0 -0
  290. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/spinner.py +0 -0
  291. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/suggestions/__init__.py +0 -0
  292. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/suggestions/aggregator.py +0 -0
  293. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/suggestions/directory_provider.py +0 -0
  294. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/suggestions/file_provider.py +0 -0
  295. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/suggestions/skill_provider.py +0 -0
  296. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/suggestions/token_extractor.py +0 -0
  297. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/suggestions/types.py +0 -0
  298. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/ui/transcript_view.py +0 -0
  299. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/__init__.py +0 -0
  300. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/background_housekeeping.py +0 -0
  301. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/cleanup.py +0 -0
  302. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/console.py +0 -0
  303. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/image/__init__.py +0 -0
  304. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/image/clipboard.py +0 -0
  305. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/image/format_detect.py +0 -0
  306. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/image/pasted_content.py +0 -0
  307. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/image/processor.py +0 -0
  308. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/image/resizer.py +0 -0
  309. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/image/store.py +0 -0
  310. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/json_utils.py +0 -0
  311. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/log.py +0 -0
  312. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/platform.py +0 -0
  313. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/project_paths.py +0 -0
  314. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/signals.py +0 -0
  315. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/tool_input_parser.py +0 -0
  316. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code/utils/windows_paths.py +0 -0
  317. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code.egg-info/dependency_links.txt +0 -0
  318. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code.egg-info/entry_points.txt +0 -0
  319. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code.egg-info/requires.txt +0 -0
  320. {iac_code-0.4.0 → iac_code-0.5.0}/src/iac_code.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  include LICENSE
2
2
  include README.md
3
3
  include pyproject.toml
4
- recursive-include src/iac_code *.yml *.yaml *.json *.md *.mo *.po
4
+ recursive-include src/iac_code *.yml *.yaml *.json *.md *.rego *.mo *.po
5
5
 
6
6
  prune tests
7
7
  prune htmlcov
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iac_code
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Your AI-powered Infrastructure as Code assistant
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.10
@@ -21,7 +21,7 @@ License-File: LICENSE
21
21
  <img src="website/static/img/logo-with-front.png" alt="iac-code" width="200">
22
22
  </p>
23
23
  <p align="center">
24
- <em>AI-powered Infrastructure as Code assistant for Alibaba Cloud (ROS / Terraform) through natural language interaction.</em>
24
+ <em>AI-powered Infrastructure as Code assistant for cloud infrastructure through natural language interaction. Currently supports Alibaba Cloud ROS and Terraform workflows.</em>
25
25
  </p>
26
26
  <p align="center">
27
27
  <a href="https://github.com/aliyun/iac-code/actions/workflows/test.yml"><img src="https://github.com/aliyun/iac-code/actions/workflows/test.yml/badge.svg" alt="Test"></a>
@@ -2,7 +2,7 @@
2
2
  <img src="website/static/img/logo-with-front.png" alt="iac-code" width="200">
3
3
  </p>
4
4
  <p align="center">
5
- <em>AI-powered Infrastructure as Code assistant for Alibaba Cloud (ROS / Terraform) through natural language interaction.</em>
5
+ <em>AI-powered Infrastructure as Code assistant for cloud infrastructure through natural language interaction. Currently supports Alibaba Cloud ROS and Terraform workflows.</em>
6
6
  </p>
7
7
  <p align="center">
8
8
  <a href="https://github.com/aliyun/iac-code/actions/workflows/test.yml"><img src="https://github.com/aliyun/iac-code/actions/workflows/test.yml/badge.svg" alt="Test"></a>
@@ -73,4 +73,4 @@ See the [Contributing Guide](https://aliyun.github.io/iac-code/getting-started/c
73
73
 
74
74
  | [DingTalk](https://qr.dingtalk.com/action/joingroup?code=v1,k1,ubm/77U7qRh/STFZUNBP26X4PNg2z6+uhiPcLGtDNfU=&_dt_no_comment=1&origin=11) | [Discord](https://discord.gg/qECFuFBwF) |
75
75
  | :----------------------------------------------------------: | :----------------------------------------------------------: |
76
- | [<img src="website/static/img/qrcode-dingtalk.jpg" width="120" height="120" alt="DingTalk">](https://qr.dingtalk.com/action/joingroup?code=v1,k1,ubm/77U7qRh/STFZUNBP26X4PNg2z6+uhiPcLGtDNfU=&_dt_no_comment=1&origin=11) | [<img src="website/static/img/qrcode-discord.jpg" width="120" height="120" alt="Discord">](https://discord.gg/qECFuFBwF) |
76
+ | [<img src="website/static/img/qrcode-dingtalk.jpg" width="120" height="120" alt="DingTalk">](https://qr.dingtalk.com/action/joingroup?code=v1,k1,ubm/77U7qRh/STFZUNBP26X4PNg2z6+uhiPcLGtDNfU=&_dt_no_comment=1&origin=11) | [<img src="website/static/img/qrcode-discord.jpg" width="120" height="120" alt="Discord">](https://discord.gg/qECFuFBwF) |
@@ -91,6 +91,7 @@ version = {attr = "iac_code.__version__"}
91
91
  "**/*.yaml",
92
92
  "**/*.json",
93
93
  "**/*.md",
94
+ "**/*.rego",
94
95
  "**/*.mo",
95
96
  "**/*.po",
96
97
  ]
@@ -0,0 +1,2 @@
1
+ __version__ = "0.5.0"
2
+ __release_date__ = "2026-06-11"
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import contextlib
4
5
  import logging
5
6
  import os
6
7
  import uuid
@@ -27,7 +28,7 @@ from iac_code.a2a.types import (
27
28
  )
28
29
  from iac_code.services.agent_factory import AgentFactoryOptions, create_agent_runtime
29
30
  from iac_code.services.session_storage import SessionStorage
30
- from iac_code.services.telemetry import use_session_id
31
+ from iac_code.services.telemetry import use_session_id, use_user_id
31
32
 
32
33
  logger = logging.getLogger(__name__)
33
34
  _CONTEXT_LOCK_ACQUIRE_TIMEOUT_SECONDS = 1
@@ -87,6 +88,7 @@ class IacCodeA2AExecutor(AgentExecutor):
87
88
  getattr(context, "message", None), "metadata", None
88
89
  )
89
90
  cwd = self._resolve_cwd(metadata)
91
+ user_id = self._resolve_user_id(metadata)
90
92
  prompt = self._prompt_from_context(context, cwd=cwd)
91
93
  except Exception as exc:
92
94
  if _is_retryable_executor_error(exc):
@@ -222,7 +224,8 @@ class IacCodeA2AExecutor(AgentExecutor):
222
224
  context_id=context_id,
223
225
  state=TaskState.TASK_STATE_WORKING,
224
226
  )
225
- with use_session_id(ctx.session_id):
227
+ user_id_ctx = use_user_id(user_id) if user_id else contextlib.nullcontext()
228
+ with use_session_id(ctx.session_id), user_id_ctx:
226
229
  async for event in runtime.agent_loop.run_streaming(prompt):
227
230
  text_chunk = await publish_stream_event(
228
231
  event_queue,
@@ -348,6 +351,19 @@ class IacCodeA2AExecutor(AgentExecutor):
348
351
  resolved_cwd.mkdir(parents=True, exist_ok=True)
349
352
  return str(resolved_cwd)
350
353
 
354
+ def _resolve_user_id(self, metadata: Any | None) -> str | None:
355
+ if metadata is not None and hasattr(metadata, "DESCRIPTOR"):
356
+ metadata = MessageToDict(metadata, preserving_proto_field_name=False)
357
+ if not isinstance(metadata, Mapping):
358
+ return None
359
+ raw_iac_meta = metadata.get("iac_code")
360
+ if not isinstance(raw_iac_meta, Mapping):
361
+ return None
362
+ raw_user_id = raw_iac_meta.get("user_id")
363
+ if isinstance(raw_user_id, str) and raw_user_id.strip():
364
+ return raw_user_id.strip()
365
+ return None
366
+
351
367
  def _prompt_from_context(self, context: RequestContext, *, cwd: str) -> str:
352
368
  message = getattr(context, "message", None)
353
369
  if not isinstance(message, Message):
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import cast
9
9
 
10
10
  from iac_code.a2a.types import validate_protocol_id
11
+ from iac_code.utils.file_security import atomic_write_text
11
12
 
12
13
  _INTERRUPTED_RESTORE_STATES = {"submitted", "working", "auth-required"}
13
14
 
@@ -175,9 +176,7 @@ class A2APersistenceStore:
175
176
 
176
177
  @staticmethod
177
178
  def _write_json(path: Path, data: dict[str, object]) -> None:
178
- tmp = path.with_suffix(".json.tmp")
179
- tmp.write_text(json.dumps(data, ensure_ascii=False, sort_keys=True), encoding="utf-8")
180
- tmp.replace(path)
179
+ atomic_write_text(path, json.dumps(data, ensure_ascii=False, sort_keys=True))
181
180
 
182
181
  @staticmethod
183
182
  def _read_json(path: Path) -> dict[str, object] | None:
@@ -262,6 +262,7 @@ class A2APushNotifier:
262
262
  config = self.load_config(task_id)
263
263
  if config is None:
264
264
  return False
265
+ validate_push_callback_url(config.callback_url)
265
266
  payload = {"taskId": task_id, "contextId": context_id, "state": state}
266
267
  for attempt in range(self._max_attempts):
267
268
  try:
@@ -50,6 +50,7 @@ class A2APushJob:
50
50
  "configId": self.config_id,
51
51
  "url": self.url,
52
52
  "payload": self.payload,
53
+ "headers": redact_push_headers(self.headers),
53
54
  "attempt": self.attempt,
54
55
  "nextAttemptAt": self.next_attempt_at,
55
56
  "lastError": self.last_error,
@@ -137,7 +138,10 @@ class LocalFileA2APushQueue:
137
138
  current = time.time() if now is None else now
138
139
  self._recover_expired_inflight(current)
139
140
  for path in sorted(self.pending_dir.glob("*.json")):
140
- job = self._read(path)
141
+ try:
142
+ job = self._read(path)
143
+ except FileNotFoundError:
144
+ continue
141
145
  if job.next_attempt_at > current:
142
146
  continue
143
147
  target = self.inflight_dir / path.name
@@ -146,7 +150,10 @@ class LocalFileA2APushQueue:
146
150
  next_attempt_at=current + self._inflight_timeout_seconds,
147
151
  last_error=job.last_error,
148
152
  )
149
- path.replace(target)
153
+ try:
154
+ path.replace(target)
155
+ except FileNotFoundError:
156
+ continue
150
157
  self._write(target, leased)
151
158
  return self._read(target)
152
159
  return None
@@ -265,8 +272,14 @@ class RedisStreamsA2APushQueue:
265
272
  members = await self._redis.zrangebyscore(self._retry_key, "-inf", now, start=0, num=10)
266
273
  for member in members:
267
274
  encoded = _decode_redis_field(member)
268
- await self._redis.xadd(self._stream, {"job": encoded})
269
- await self._redis.zrem(self._retry_key, member)
275
+ pipe = self._redis.pipeline() if hasattr(self._redis, "pipeline") else None
276
+ if pipe is not None:
277
+ pipe.xadd(self._stream, {"job": encoded})
278
+ pipe.zrem(self._retry_key, member)
279
+ await pipe.execute()
280
+ else:
281
+ await self._redis.xadd(self._stream, {"job": encoded})
282
+ await self._redis.zrem(self._retry_key, member)
270
283
 
271
284
  async def _claim_expired(self) -> A2APushJob | None:
272
285
  xautoclaim = getattr(self._redis, "xautoclaim", None)
@@ -23,6 +23,7 @@ class A2APushSecretKeyring:
23
23
  self._env_managed = False
24
24
  self._active_key_id = ""
25
25
  self._keys: dict[str, str] = {}
26
+ self._created_at: dict[str, int] = {}
26
27
 
27
28
  @property
28
29
  def active_key_id(self) -> str:
@@ -55,6 +56,7 @@ class A2APushSecretKeyring:
55
56
  if key_id in self._keys:
56
57
  raise A2APushSecretError(f"A2A push secret encryption key already exists: {key_id}")
57
58
  self._keys[key_id] = Fernet.generate_key().decode("ascii")
59
+ self._created_at[key_id] = int(time.time())
58
60
  self._active_key_id = key_id
59
61
  self._write()
60
62
  return key_id
@@ -74,6 +76,7 @@ class A2APushSecretKeyring:
74
76
  return
75
77
  self._active_key_id = _new_key_id()
76
78
  self._keys = {self._active_key_id: Fernet.generate_key().decode("ascii")}
79
+ self._created_at = {self._active_key_id: int(time.time())}
77
80
  self._loaded = True
78
81
  self._write()
79
82
 
@@ -86,6 +89,11 @@ class A2APushSecretKeyring:
86
89
  for item in keys
87
90
  if isinstance(item, dict) and item.get("id") and item.get("fernetKey")
88
91
  }
92
+ self._created_at = {
93
+ str(item["id"]): int(item["createdAt"])
94
+ for item in keys
95
+ if isinstance(item, dict) and item.get("id") and item.get("createdAt") is not None
96
+ }
89
97
  self._active_key_id = str(data.get("activeKeyId") or "")
90
98
  if not self._active_key_id or self._active_key_id not in self._keys:
91
99
  raise A2APushSecretError("A2A push secret keyring does not contain its active key")
@@ -96,7 +104,7 @@ class A2APushSecretKeyring:
96
104
  data = {
97
105
  "activeKeyId": self._active_key_id,
98
106
  "keys": [
99
- {"id": key_id, "fernetKey": key, "createdAt": int(time.time())}
107
+ {"id": key_id, "fernetKey": key, "createdAt": self._created_at.get(key_id, int(time.time()))}
100
108
  for key_id, key in sorted(self._keys.items())
101
109
  ],
102
110
  }
@@ -51,12 +51,13 @@ class A2ATaskStore(TaskStore):
51
51
  async def save(self, task: Task, context: ServerCallContext | None = None) -> None:
52
52
  owner = self._owner(context)
53
53
  task_id = validate_protocol_id(task.id)
54
- owner_tasks = self._sdk_tasks.setdefault(owner, {})
55
- previous = owner_tasks.get(task_id)
56
- if previous is not None:
57
- self._remove_sdk_task_from_index(owner, task_id, previous.context_id)
58
- owner_tasks[task_id] = _copy_task(task)
59
- self._sdk_tasks_by_context.setdefault(owner, {}).setdefault(task.context_id, set()).add(task_id)
54
+ async with self._mutation_lock:
55
+ owner_tasks = self._sdk_tasks.setdefault(owner, {})
56
+ previous = owner_tasks.get(task_id)
57
+ if previous is not None:
58
+ self._remove_sdk_task_from_index(owner, task_id, previous.context_id)
59
+ owner_tasks[task_id] = _copy_task(task)
60
+ self._sdk_tasks_by_context.setdefault(owner, {}).setdefault(task.context_id, set()).add(task_id)
60
61
 
61
62
  async def delete(self, task_id: str, context: ServerCallContext | None = None) -> None:
62
63
  owner = self._owner(context)
@@ -97,13 +97,18 @@ class RedisStreamsA2AClient:
97
97
 
98
98
  async def _read_responses(self, correlation_id: str) -> AsyncIterator[RedisStreamsMessage]:
99
99
  deadline = asyncio.get_running_loop().time() + self._timeout_seconds
100
+ # Start from "0-0" instead of "$" to avoid a race where the server
101
+ # responds before the first xread call. After each batch we advance
102
+ # the cursor to the last entry ID we saw, which is the standard
103
+ # Redis Streams consumer pattern.
104
+ last_id = "0-0"
100
105
  consecutive_errors = 0
101
106
  max_consecutive_errors = 3
102
107
  while asyncio.get_running_loop().time() < deadline:
103
108
  try:
104
109
  remaining = deadline - asyncio.get_running_loop().time()
105
110
  rows = await asyncio.wait_for(
106
- self._redis.xread({self._response_stream: "$"}, count=1, block=100),
111
+ self._redis.xread({self._response_stream: last_id}, count=10, block=100),
107
112
  timeout=max(0.001, min(0.2, remaining)),
108
113
  )
109
114
  consecutive_errors = 0
@@ -121,6 +126,8 @@ class RedisStreamsA2AClient:
121
126
  continue
122
127
  for _stream, entries in rows:
123
128
  for entry_id, fields in entries:
129
+ # Advance cursor past this entry so we don't re-read it.
130
+ last_id = entry_id if isinstance(entry_id, str) else entry_id.decode("utf-8")
124
131
  message = parse_redis_entry(entry_id, fields)
125
132
  if message.correlation_id == correlation_id:
126
133
  yield message
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import contextlib
5
+ import os
6
+ import stat
5
7
  from collections.abc import AsyncIterator
6
8
  from pathlib import Path
7
9
  from typing import Any
@@ -10,10 +12,19 @@ from iac_code.a2a.transports.dispatcher import A2ARuntimeComponents
10
12
  from iac_code.a2a.transports.stdio import StdioA2AClient, StdioA2AServer
11
13
 
12
14
 
15
+ def _is_socket(path: Path) -> bool:
16
+ try:
17
+ return stat.S_ISSOCK(os.lstat(path).st_mode)
18
+ except OSError:
19
+ return False
20
+
21
+
13
22
  def validate_socket_path(socket_path: str) -> Path:
14
23
  path = Path(socket_path)
15
24
  if not path.parent.exists():
16
25
  raise ValueError(f"Unix socket parent does not exist: {path.parent}")
26
+ if path.exists() and not _is_socket(path):
27
+ raise ValueError(f"Path exists and is not a Unix socket: {path}")
17
28
  return path
18
29
 
19
30
 
@@ -22,11 +33,15 @@ class UnixA2AServer:
22
33
  self._components = components
23
34
  self._socket_path = validate_socket_path(socket_path)
24
35
  self._server: asyncio.AbstractServer | None = None
36
+ self._owns_socket = False
25
37
 
26
38
  async def serve(self) -> None:
27
39
  if self._socket_path.exists():
40
+ if not _is_socket(self._socket_path):
41
+ raise ValueError(f"Path exists and is not a Unix socket: {self._socket_path}")
28
42
  self._socket_path.unlink()
29
43
  self._server = await asyncio.start_unix_server(self._handle_client, path=str(self._socket_path))
44
+ self._owns_socket = True
30
45
  async with self._server:
31
46
  await self._server.serve_forever()
32
47
 
@@ -43,8 +58,9 @@ class UnixA2AServer:
43
58
  if self._server is not None:
44
59
  self._server.close()
45
60
  await self._server.wait_closed()
46
- with contextlib.suppress(FileNotFoundError):
47
- self._socket_path.unlink()
61
+ if self._owns_socket:
62
+ with contextlib.suppress(FileNotFoundError):
63
+ self._socket_path.unlink()
48
64
  await self._components.aclose()
49
65
 
50
66
 
@@ -247,9 +247,39 @@ class HTTPConnectionBridge:
247
247
  self._initialized.set()
248
248
  else:
249
249
  try:
250
- self._sse_queue.put_nowait(message)
251
- except asyncio.QueueFull:
252
- logger.warning("SSE queue full for connection %s, discarding message", self.connection_id[:8])
250
+ # Apply backpressure: block until space is available
251
+ # (up to a timeout) rather than silently discarding.
252
+ await asyncio.wait_for(self._sse_queue.put(message), timeout=30.0)
253
+ except asyncio.TimeoutError:
254
+ # Client is not consuming events within a reasonable
255
+ # window — send an error notification and tear down.
256
+ logger.error(
257
+ "SSE queue full for connection %s: client not consuming events, closing",
258
+ self.connection_id[:8],
259
+ )
260
+ error_event = json.dumps(
261
+ {
262
+ "jsonrpc": "2.0",
263
+ "method": "notifications/cancelled",
264
+ "params": {
265
+ "reason": "SSE queue full: client is not consuming events",
266
+ },
267
+ }
268
+ )
269
+ # Make room by discarding the oldest pending event so
270
+ # the error notification reaches the client.
271
+ try:
272
+ self._sse_queue.get_nowait()
273
+ except asyncio.QueueEmpty:
274
+ pass
275
+ try:
276
+ self._sse_queue.put_nowait(error_event)
277
+ except asyncio.QueueFull:
278
+ pass
279
+ # Break out of the read loop — the finally block will
280
+ # send the sentinel and trigger connection teardown.
281
+ fatal_error = True
282
+ break
253
283
  except asyncio.CancelledError:
254
284
  raise
255
285
  except Exception:
@@ -290,11 +320,31 @@ _connections: dict[str, HTTPConnectionBridge] = {}
290
320
 
291
321
  _ACP_CONN_HEADER = "Acp-Connection-Id"
292
322
  _INIT_TIMEOUT = 30.0
323
+ # Maximum allowed request body size (4 MiB). Requests exceeding this limit
324
+ # are rejected with 413 to prevent unbounded memory consumption.
325
+ _MAX_BODY_BYTES = 4 * 1024 * 1024
293
326
 
294
327
 
295
328
  async def _handle_post(request: Request) -> Response:
296
329
  """Handle ``POST /acp`` — JSON-RPC requests from the client."""
330
+ # Enforce body size limit to prevent unbounded memory consumption.
331
+ content_length = request.headers.get("content-length")
332
+ if content_length is not None:
333
+ try:
334
+ if int(content_length) > _MAX_BODY_BYTES:
335
+ return JSONResponse(
336
+ {"error": "Request body too large"},
337
+ status_code=413,
338
+ )
339
+ except (ValueError, OverflowError):
340
+ pass
341
+
297
342
  body = await request.body()
343
+ if len(body) > _MAX_BODY_BYTES:
344
+ return JSONResponse(
345
+ {"error": "Request body too large"},
346
+ status_code=413,
347
+ )
298
348
  message = body.decode("utf-8")
299
349
 
300
350
  try:
@@ -3,13 +3,17 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import contextlib
5
5
  import logging
6
+ import os
7
+ import sys
6
8
  import time
7
9
  import uuid
10
+ from pathlib import Path
8
11
  from typing import Any
9
12
 
10
13
  import acp
11
14
 
12
15
  from iac_code import __version__
16
+ from iac_code.a2a.parts import allowed_cwd_roots, is_relative_to
13
17
  from iac_code.acp.metrics import ACPMetrics
14
18
  from iac_code.acp.session import ACPSession, Message, _is_auth_error
15
19
  from iac_code.acp.slash_registry import ACP_SUPPORTED_COMMANDS
@@ -23,7 +27,11 @@ from iac_code.services.agent_factory import AgentFactoryOptions, create_agent_ru
23
27
  from iac_code.services.session_index import SessionEntry, SessionIndex
24
28
  from iac_code.services.session_resolver import ResolutionStatus, resolve_session_argument
25
29
  from iac_code.services.session_storage import SessionStorage
26
- from iac_code.utils.project_paths import format_resume_command, same_project_path
30
+ from iac_code.utils.project_paths import (
31
+ _looks_like_windows_path,
32
+ format_resume_command,
33
+ same_project_path,
34
+ )
27
35
 
28
36
  SESSION_IDLE_TIMEOUT = 3600 # 1 hour
29
37
  CLEANUP_INTERVAL = 300 # 5 minutes
@@ -31,6 +39,10 @@ CLEANUP_INTERVAL = 300 # 5 minutes
31
39
  logger = logging.getLogger(__name__)
32
40
 
33
41
 
42
+ def _runtime_command_memory_manager(runtime: object) -> object | None:
43
+ return getattr(runtime, "legacy_memory_manager", None) or getattr(runtime, "memory_manager", None)
44
+
45
+
34
46
  class ACPServer:
35
47
  def __init__(self) -> None:
36
48
  self.conn: acp.Client | None = None
@@ -101,6 +113,36 @@ class ACPServer:
101
113
  raise acp.RequestError.invalid_params({"session_id": "Session not found"})
102
114
  return session
103
115
 
116
+ @staticmethod
117
+ def _validate_cwd(cwd: str) -> str:
118
+ """Validate the working directory path.
119
+
120
+ Ensures the path is absolute and falls within one of the allowed CWD
121
+ roots (same policy as the A2A executor). Returns the original *cwd*
122
+ string on success (not resolved) so that session storage lookups
123
+ remain consistent. Raises :class:`acp.RequestError` on failure.
124
+ """
125
+ if not cwd:
126
+ raise acp.RequestError.invalid_params({"cwd": "cwd must be an absolute path"})
127
+ # Accept both POSIX and Windows absolute paths. ``os.path.isabs``
128
+ # treats rooted POSIX-style paths (``/tmp``) as absolute on Windows
129
+ # too, while ``_looks_like_windows_path`` catches drive-letter forms
130
+ # (``C:\foo``) on POSIX hosts.
131
+ is_windows_path = _looks_like_windows_path(cwd)
132
+ if not os.path.isabs(cwd) and not is_windows_path:
133
+ raise acp.RequestError.invalid_params({"cwd": "cwd must be an absolute path"})
134
+ # When a Windows-style path is received on a non-Windows host, we
135
+ # cannot resolve it against the local filesystem; skip the
136
+ # root-containment and is_dir checks for that case only.
137
+ if is_windows_path and sys.platform != "win32":
138
+ return cwd
139
+ resolved = Path(cwd).resolve()
140
+ if not any(is_relative_to(resolved, root) for root in allowed_cwd_roots()):
141
+ raise acp.RequestError.invalid_params({"cwd": "cwd is outside allowed roots"})
142
+ if resolved.exists() and not resolved.is_dir():
143
+ raise acp.RequestError.invalid_params({"cwd": "cwd is not a directory"})
144
+ return cwd
145
+
104
146
  async def initialize(
105
147
  self,
106
148
  protocol_version: int,
@@ -144,6 +186,8 @@ class ACPServer:
144
186
  if self.conn is None:
145
187
  raise acp.RequestError.internal_error({"error": "ACP client not connected"})
146
188
 
189
+ cwd = self._validate_cwd(cwd)
190
+
147
191
  # Convert MCP server configs from ACP protocol types to internal dicts
148
192
  mcp_configs = _convert_mcp_servers(mcp_servers)
149
193
 
@@ -161,7 +205,7 @@ class ACPServer:
161
205
  self.conn,
162
206
  mcp_configs=mcp_configs,
163
207
  metrics=self.metrics,
164
- memory_manager=getattr(runtime, "memory_manager", None),
208
+ memory_manager=_runtime_command_memory_manager(runtime),
165
209
  )
166
210
  self.sessions[session.id] = session
167
211
  self.metrics.record_session_created()
@@ -268,6 +312,8 @@ class ACPServer:
268
312
  if self.conn is None:
269
313
  raise acp.RequestError.internal_error({"error": "ACP client not connected"})
270
314
 
315
+ cwd = self._validate_cwd(cwd)
316
+
271
317
  # 1. Already active in memory — return immediately
272
318
  if session_id in self.sessions:
273
319
  model = load_saved_model() or DEFAULT_MODEL
@@ -303,7 +349,7 @@ class ACPServer:
303
349
  self.conn,
304
350
  mcp_configs=mcp_configs,
305
351
  metrics=self.metrics,
306
- memory_manager=getattr(runtime, "memory_manager", None),
352
+ memory_manager=_runtime_command_memory_manager(runtime),
307
353
  )
308
354
  self.sessions[session_id] = session
309
355
  self.metrics.record_session_created()
@@ -334,6 +380,8 @@ class ACPServer:
334
380
  if self.conn is None:
335
381
  raise acp.RequestError.internal_error({"error": "ACP client not connected"})
336
382
 
383
+ cwd = self._validate_cwd(cwd)
384
+
337
385
  # 1. Collect history from the source session
338
386
  history: list[Message] = []
339
387
  if session_id in self.sessions:
@@ -372,7 +420,7 @@ class ACPServer:
372
420
  self.conn,
373
421
  mcp_configs=mcp_configs,
374
422
  metrics=self.metrics,
375
- memory_manager=getattr(runtime, "memory_manager", None),
423
+ memory_manager=_runtime_command_memory_manager(runtime),
376
424
  )
377
425
  self.sessions[new_session_id] = session
378
426
  self.metrics.record_session_created()
@@ -396,6 +444,8 @@ class ACPServer:
396
444
  mcp_servers: list[MCPServer] | None = None,
397
445
  **kwargs: Any,
398
446
  ) -> acp.schema.ResumeSessionResponse:
447
+ cwd = self._validate_cwd(cwd)
448
+
399
449
  # 1. If session is still active in memory by exact id, enforce project ownership before returning.
400
450
  active_session = self.sessions.get(session_id)
401
451
  if active_session is not None:
@@ -482,7 +532,7 @@ class ACPServer:
482
532
  self.conn,
483
533
  mcp_configs=mcp_configs,
484
534
  metrics=self.metrics,
485
- memory_manager=getattr(runtime, "memory_manager", None),
535
+ memory_manager=_runtime_command_memory_manager(runtime),
486
536
  )
487
537
  self.sessions[resolved_session_id] = session
488
538
  self.metrics.record_session_created()
@@ -18,7 +18,14 @@ from iac_code.acp.slash_registry import ACPSlashRegistry
18
18
  from iac_code.acp.state import TurnState
19
19
  from iac_code.acp.tools import ACPTerminalBashTool
20
20
  from iac_code.acp.types import ACPContentBlock
21
- from iac_code.agent.message import Message, TextBlock, ThinkingBlock, ToolResultBlock, ToolUseBlock
21
+ from iac_code.agent.message import (
22
+ Message,
23
+ TextBlock,
24
+ ThinkingBlock,
25
+ ToolResultBlock,
26
+ ToolUseBlock,
27
+ is_recalled_memory_message,
28
+ )
22
29
  from iac_code.services.telemetry import use_session_id
23
30
  from iac_code.state.app_state import lookup_permission, record_permission
24
31
  from iac_code.types.permissions import PermissionDecision
@@ -64,6 +71,9 @@ def _history_message_to_updates(msg: Message) -> list[Any]:
64
71
  ``ToolCallProgress``.
65
72
  * **user** tool-result blocks are emitted as completed ``ToolCallProgress``.
66
73
  """
74
+ if is_recalled_memory_message(msg):
75
+ return []
76
+
67
77
  updates: list[Any] = []
68
78
  content = msg.content
69
79
 
@@ -157,6 +167,20 @@ _PREFIX_ALLOW_RULE = "allow_rule:"
157
167
  _PREFIX_DENY_RULE = "deny_rule:"
158
168
 
159
169
 
170
+ def _tool_supports_blanket_allow(agent_loop, tool_name: str) -> bool:
171
+ """Return False only when the registered tool explicitly disables blanket allow."""
172
+ registry = getattr(agent_loop, "tool_registry", None)
173
+ get_tool = getattr(registry, "get", None)
174
+ if get_tool is None:
175
+ return True
176
+
177
+ tool = get_tool(tool_name)
178
+ if tool is None:
179
+ return True
180
+
181
+ return bool(getattr(tool, "supports_blanket_allow", True))
182
+
183
+
160
184
  class ACPSession:
161
185
  def __init__(
162
186
  self,
@@ -457,7 +481,7 @@ class ACPSession:
457
481
  kind="allow_always",
458
482
  )
459
483
  )
460
- else:
484
+ elif _tool_supports_blanket_allow(self.agent_loop, tool_name):
461
485
  options.append(
462
486
  acp.schema.PermissionOption(
463
487
  option_id=_OPTION_ALLOW_ALWAYS,