ata-coder 2.4.4__tar.gz → 2.4.5__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 (140) hide show
  1. {ata_coder-2.4.4/ata_coder.egg-info → ata_coder-2.4.5}/PKG-INFO +7 -3
  2. {ata_coder-2.4.4 → ata_coder-2.4.5}/README.md +6 -2
  3. {ata_coder-2.4.4 → ata_coder-2.4.5}/agent.py +4 -9
  4. {ata_coder-2.4.4 → ata_coder-2.4.5/ata_coder.egg-info}/PKG-INFO +7 -3
  5. {ata_coder-2.4.4 → ata_coder-2.4.5}/ata_coder.egg-info/SOURCES.txt +2 -0
  6. {ata_coder-2.4.4 → ata_coder-2.4.5}/config.py +3 -0
  7. {ata_coder-2.4.4 → ata_coder-2.4.5}/extension.py +38 -38
  8. {ata_coder-2.4.4 → ata_coder-2.4.5}/main.py +31 -14
  9. ata_coder-2.4.5/py.typed +0 -0
  10. {ata_coder-2.4.4 → ata_coder-2.4.5}/pyproject.toml +1 -1
  11. {ata_coder-2.4.4 → ata_coder-2.4.5}/server.py +84 -34
  12. {ata_coder-2.4.4 → ata_coder-2.4.5}/server_session.py +31 -1
  13. {ata_coder-2.4.4 → ata_coder-2.4.5}/setup_wizard.py +1 -1
  14. {ata_coder-2.4.4 → ata_coder-2.4.5}/sub_agent.py +3 -8
  15. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_server.py +1 -1
  16. {ata_coder-2.4.4 → ata_coder-2.4.5}/tools/executor.py +1 -1
  17. {ata_coder-2.4.4 → ata_coder-2.4.5}/tools/web.py +60 -7
  18. {ata_coder-2.4.4 → ata_coder-2.4.5}/utils.py +13 -0
  19. {ata_coder-2.4.4 → ata_coder-2.4.5}/LICENSE +0 -0
  20. {ata_coder-2.4.4 → ata_coder-2.4.5}/MANIFEST.in +0 -0
  21. {ata_coder-2.4.4 → ata_coder-2.4.5}/__init__.py +0 -0
  22. {ata_coder-2.4.4 → ata_coder-2.4.5}/agent_compact.py +0 -0
  23. {ata_coder-2.4.4 → ata_coder-2.4.5}/agent_controller.py +0 -0
  24. {ata_coder-2.4.4 → ata_coder-2.4.5}/agent_extension.py +0 -0
  25. {ata_coder-2.4.4 → ata_coder-2.4.5}/agent_routing.py +0 -0
  26. {ata_coder-2.4.4 → ata_coder-2.4.5}/agent_subsystems.py +0 -0
  27. {ata_coder-2.4.4 → ata_coder-2.4.5}/agent_tools.py +0 -0
  28. {ata_coder-2.4.4 → ata_coder-2.4.5}/agent_undo.py +0 -0
  29. {ata_coder-2.4.4 → ata_coder-2.4.5}/anthropic_client.py +0 -0
  30. {ata_coder-2.4.4 → ata_coder-2.4.5}/ata_coder.egg-info/dependency_links.txt +0 -0
  31. {ata_coder-2.4.4 → ata_coder-2.4.5}/ata_coder.egg-info/entry_points.txt +0 -0
  32. {ata_coder-2.4.4 → ata_coder-2.4.5}/ata_coder.egg-info/requires.txt +0 -0
  33. {ata_coder-2.4.4 → ata_coder-2.4.5}/ata_coder.egg-info/top_level.txt +0 -0
  34. {ata_coder-2.4.4 → ata_coder-2.4.5}/change_tracker.py +0 -0
  35. {ata_coder-2.4.4 → ata_coder-2.4.5}/clawd_integration.py +0 -0
  36. {ata_coder-2.4.4 → ata_coder-2.4.5}/commands/__init__.py +0 -0
  37. {ata_coder-2.4.4 → ata_coder-2.4.5}/commands/_core.py +0 -0
  38. {ata_coder-2.4.4 → ata_coder-2.4.5}/commands/_safety.py +0 -0
  39. {ata_coder-2.4.4 → ata_coder-2.4.5}/commands/_settings.py +0 -0
  40. {ata_coder-2.4.4 → ata_coder-2.4.5}/commands/_workflow.py +0 -0
  41. {ata_coder-2.4.4 → ata_coder-2.4.5}/core/__init__.py +0 -0
  42. {ata_coder-2.4.4 → ata_coder-2.4.5}/core/events.py +0 -0
  43. {ata_coder-2.4.4 → ata_coder-2.4.5}/core/queue.py +0 -0
  44. {ata_coder-2.4.4 → ata_coder-2.4.5}/core/state.py +0 -0
  45. {ata_coder-2.4.4 → ata_coder-2.4.5}/event_queue.py +0 -0
  46. {ata_coder-2.4.4 → ata_coder-2.4.5}/extensions/__init__.py +0 -0
  47. {ata_coder-2.4.4 → ata_coder-2.4.5}/extensions/hello_skill.py +0 -0
  48. {ata_coder-2.4.4 → ata_coder-2.4.5}/fool_proof.py +0 -0
  49. {ata_coder-2.4.4 → ata_coder-2.4.5}/git_workflow.py +0 -0
  50. {ata_coder-2.4.4 → ata_coder-2.4.5}/gui.py +0 -0
  51. {ata_coder-2.4.4 → ata_coder-2.4.5}/llm_client.py +0 -0
  52. {ata_coder-2.4.4 → ata_coder-2.4.5}/mcp_client.py +0 -0
  53. {ata_coder-2.4.4 → ata_coder-2.4.5}/memory.py +0 -0
  54. {ata_coder-2.4.4 → ata_coder-2.4.5}/model_registry.py +0 -0
  55. {ata_coder-2.4.4 → ata_coder-2.4.5}/model_router.py +0 -0
  56. {ata_coder-2.4.4 → ata_coder-2.4.5}/permissions.py +0 -0
  57. {ata_coder-2.4.4 → ata_coder-2.4.5}/privilege.py +0 -0
  58. {ata_coder-2.4.4 → ata_coder-2.4.5}/project.py +0 -0
  59. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompt_template.py +0 -0
  60. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/auto-mode.md +0 -0
  61. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/coding-rules.md +0 -0
  62. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/execution-guardrails.md +0 -0
  63. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/memory-system.md +0 -0
  64. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/output-style.md +0 -0
  65. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/safety.md +0 -0
  66. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/slash-commands.md +0 -0
  67. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/sub-agents.md +0 -0
  68. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/system-reminders.md +0 -0
  69. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/system.md +0 -0
  70. {ata_coder-2.4.4 → ata_coder-2.4.5}/prompts/tool-policy.md +0 -0
  71. {ata_coder-2.4.4 → ata_coder-2.4.5}/repl_theme.py +0 -0
  72. {ata_coder-2.4.4 → ata_coder-2.4.5}/repl_tracker.py +0 -0
  73. {ata_coder-2.4.4 → ata_coder-2.4.5}/repl_ui.py +0 -0
  74. {ata_coder-2.4.4 → ata_coder-2.4.5}/safety_guard.py +0 -0
  75. {ata_coder-2.4.4 → ata_coder-2.4.5}/self_correct.py +0 -0
  76. {ata_coder-2.4.4 → ata_coder-2.4.5}/server_shell.py +0 -0
  77. {ata_coder-2.4.4 → ata_coder-2.4.5}/session.py +0 -0
  78. {ata_coder-2.4.4 → ata_coder-2.4.5}/settings.py +0 -0
  79. {ata_coder-2.4.4 → ata_coder-2.4.5}/setup.cfg +0 -0
  80. {ata_coder-2.4.4 → ata_coder-2.4.5}/skill_extension.py +0 -0
  81. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/architect/SKILL.md +0 -0
  82. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/code-reviewer/SKILL.md +0 -0
  83. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/codecraft/SKILL.md +0 -0
  84. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/debugger/SKILL.md +0 -0
  85. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/doc-writer/SKILL.md +0 -0
  86. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/general-coder/SKILL.md +0 -0
  87. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/math-calculator/README.md +0 -0
  88. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/math-calculator/SKILL.md +0 -0
  89. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/math-calculator/handler.py +0 -0
  90. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/math-calculator/prompts/system.md +0 -0
  91. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/math-calculator/requirements.txt +0 -0
  92. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/math-calculator/resources/constants.json +0 -0
  93. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/math-calculator/tests/test_handler.py +0 -0
  94. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/security-auditor/SKILL.md +0 -0
  95. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/test-writer/SKILL.md +0 -0
  96. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/weather-skill/README.md +0 -0
  97. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/weather-skill/handler.py +0 -0
  98. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/weather-skill/manifest.json +0 -0
  99. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/weather-skill/prompts/system_prompt.txt +0 -0
  100. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/weather-skill/prompts/user_prompt_template.txt +0 -0
  101. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/weather-skill/requirements.txt +0 -0
  102. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/weather-skill/resources/city_list.json +0 -0
  103. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/weather-skill/resources/error_messages.json +0 -0
  104. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/weather-skill/tests/test_handler.py +0 -0
  105. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills/weather-skill/weather_utils.py +0 -0
  106. {ata_coder-2.4.4 → ata_coder-2.4.5}/skills.py +0 -0
  107. {ata_coder-2.4.4 → ata_coder-2.4.5}/sub_agent_manager.py +0 -0
  108. {ata_coder-2.4.4 → ata_coder-2.4.5}/system_prompt_builder.py +0 -0
  109. {ata_coder-2.4.4 → ata_coder-2.4.5}/task_planner.py +0 -0
  110. {ata_coder-2.4.4 → ata_coder-2.4.5}/terminal.py +0 -0
  111. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_agent.py +0 -0
  112. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_change_tracker.py +0 -0
  113. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_config.py +0 -0
  114. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_event_queue.py +0 -0
  115. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_extension.py +0 -0
  116. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_fibonacci.py +0 -0
  117. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_fool_proof.py +0 -0
  118. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_llm_client.py +0 -0
  119. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_memory.py +0 -0
  120. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_model_registry.py +0 -0
  121. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_permissions.py +0 -0
  122. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_privilege.py +0 -0
  123. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_prompt_template.py +0 -0
  124. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_safety_guard.py +0 -0
  125. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_skill_handlers.py +0 -0
  126. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_sub_agent.py +0 -0
  127. {ata_coder-2.4.4 → ata_coder-2.4.5}/tests/test_tools.py +0 -0
  128. {ata_coder-2.4.4 → ata_coder-2.4.5}/token_counter.py +0 -0
  129. {ata_coder-2.4.4 → ata_coder-2.4.5}/tools/__init__.py +0 -0
  130. {ata_coder-2.4.4 → ata_coder-2.4.5}/tools/definitions.py +0 -0
  131. {ata_coder-2.4.4 → ata_coder-2.4.5}/tools/result.py +0 -0
  132. {ata_coder-2.4.4 → ata_coder-2.4.5}/tools/strategy.py +0 -0
  133. {ata_coder-2.4.4 → ata_coder-2.4.5}/tools/subagent.py +0 -0
  134. {ata_coder-2.4.4 → ata_coder-2.4.5}/types.py +0 -0
  135. {ata_coder-2.4.4 → ata_coder-2.4.5}/web/css/style.css +0 -0
  136. {ata_coder-2.4.4 → ata_coder-2.4.5}/web/index.html +0 -0
  137. {ata_coder-2.4.4 → ata_coder-2.4.5}/web/js/app.js +0 -0
  138. {ata_coder-2.4.4 → ata_coder-2.4.5}/web/package-lock.json +0 -0
  139. {ata_coder-2.4.4 → ata_coder-2.4.5}/web/package.json +0 -0
  140. {ata_coder-2.4.4 → ata_coder-2.4.5}/web/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ata-coder
3
- Version: 2.4.4
3
+ Version: 2.4.5
4
4
  Summary: ATA Coder — AI-powered coding assistant
5
5
  Author: ATA Coder Team
6
6
  License-Expression: MIT
@@ -21,13 +21,17 @@ Requires-Dist: pytest-timeout>=2.0; extra == "dev"
21
21
  Requires-Dist: tiktoken>=0.5.0; extra == "dev"
22
22
  Dynamic: license-file
23
23
 
24
- # ATA Coder v2.4.3
24
+ # ATA Coder v2.4.5
25
25
 
26
26
  **AI-powered coding assistant — async, AST-aware, single config file.**
27
27
 
28
28
  [English](#english) | [中文](#中文)
29
29
 
30
- > **v2.4.3** — 🧠 **Comprehensive Refactoring**: Memory overhaul, unified token counting, safety pipeline, tool call optimization. 60+ issues fixed.
30
+ > **v2.4.5** — 🛡️ **Comprehensive Bug & Security Fix**: 19 bugs fixed — thread safety, SSRF IPv6, rate limiter leak, auth hardening, DRY refactoring, CI coverage. 12 files changed.
31
+ >
32
+ > > **v2.4.4** — 🔒 **Security Hardening**: 19 fixes across safety, storage, and reliability.
33
+ >
34
+ > > **v2.4.3** — 🧠 **Comprehensive Refactoring**: Memory overhaul, unified token counting, safety pipeline, tool call optimization. 60+ issues fixed.
31
35
  >
32
36
  > > **v2.4.2** — 🐾 **Clawd working state + Window tokens**: Clawd shows 'working' immediately. Status line shows window tokens (~120k) not cumulative (7.8M). Surrogate-safe session saves.
33
37
  >
@@ -1,10 +1,14 @@
1
- # ATA Coder v2.4.3
1
+ # ATA Coder v2.4.5
2
2
 
3
3
  **AI-powered coding assistant — async, AST-aware, single config file.**
4
4
 
5
5
  [English](#english) | [中文](#中文)
6
6
 
7
- > **v2.4.3** — 🧠 **Comprehensive Refactoring**: Memory overhaul, unified token counting, safety pipeline, tool call optimization. 60+ issues fixed.
7
+ > **v2.4.5** — 🛡️ **Comprehensive Bug & Security Fix**: 19 bugs fixed — thread safety, SSRF IPv6, rate limiter leak, auth hardening, DRY refactoring, CI coverage. 12 files changed.
8
+ >
9
+ > > **v2.4.4** — 🔒 **Security Hardening**: 19 fixes across safety, storage, and reliability.
10
+ >
11
+ > > **v2.4.3** — 🧠 **Comprehensive Refactoring**: Memory overhaul, unified token counting, safety pipeline, tool call optimization. 60+ issues fixed.
8
12
  >
9
13
  > > **v2.4.2** — 🐾 **Clawd working state + Window tokens**: Clawd shows 'working' immediately. Status line shows window tokens (~120k) not cumulative (7.8M). Surrogate-safe session saves.
10
14
  >
@@ -27,8 +27,7 @@ import time
27
27
  from typing import Any, Callable
28
28
 
29
29
  from .config import AppConfig
30
- from .llm_client import LLMClient, SYSTEM_PROMPT
31
- from .anthropic_client import AnthropicClient
30
+ from .llm_client import SYSTEM_PROMPT
32
31
  from .tools import ToolExecutor, TOOL_DEFINITIONS, ToolResult
33
32
  from .types import Message
34
33
  from .agent_subsystems import AgentSubsystems
@@ -84,13 +83,9 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
84
83
  ):
85
84
  self.config = config or AppConfig.load()
86
85
 
87
- # Choose client: Anthropic or OpenAI format
88
- if self.config.llm.use_anthropic:
89
- self.llm = AnthropicClient(self.config.llm)
90
- self._use_anthropic = True
91
- else:
92
- self.llm = LLMClient(self.config.llm)
93
- self._use_anthropic = False
86
+ # Choose client: Anthropic or OpenAI format (factory eliminates duplication)
87
+ from .utils import create_llm_client
88
+ self.llm, self._use_anthropic = create_llm_client(self.config.llm)
94
89
 
95
90
  self.tools = tool_executor or ToolExecutor(self.config.agent)
96
91
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ata-coder
3
- Version: 2.4.4
3
+ Version: 2.4.5
4
4
  Summary: ATA Coder — AI-powered coding assistant
5
5
  Author: ATA Coder Team
6
6
  License-Expression: MIT
@@ -21,13 +21,17 @@ Requires-Dist: pytest-timeout>=2.0; extra == "dev"
21
21
  Requires-Dist: tiktoken>=0.5.0; extra == "dev"
22
22
  Dynamic: license-file
23
23
 
24
- # ATA Coder v2.4.3
24
+ # ATA Coder v2.4.5
25
25
 
26
26
  **AI-powered coding assistant — async, AST-aware, single config file.**
27
27
 
28
28
  [English](#english) | [中文](#中文)
29
29
 
30
- > **v2.4.3** — 🧠 **Comprehensive Refactoring**: Memory overhaul, unified token counting, safety pipeline, tool call optimization. 60+ issues fixed.
30
+ > **v2.4.5** — 🛡️ **Comprehensive Bug & Security Fix**: 19 bugs fixed — thread safety, SSRF IPv6, rate limiter leak, auth hardening, DRY refactoring, CI coverage. 12 files changed.
31
+ >
32
+ > > **v2.4.4** — 🔒 **Security Hardening**: 19 fixes across safety, storage, and reliability.
33
+ >
34
+ > > **v2.4.3** — 🧠 **Comprehensive Refactoring**: Memory overhaul, unified token counting, safety pipeline, tool call optimization. 60+ issues fixed.
31
35
  >
32
36
  > > **v2.4.2** — 🐾 **Clawd working state + Window tokens**: Clawd shows 'working' immediately. Status line shows window tokens (~120k) not cumulative (7.8M). Surrogate-safe session saves.
33
37
  >
@@ -29,6 +29,7 @@ permissions.py
29
29
  privilege.py
30
30
  project.py
31
31
  prompt_template.py
32
+ py.typed
32
33
  pyproject.toml
33
34
  repl_theme.py
34
35
  repl_tracker.py
@@ -79,6 +80,7 @@ utils.py
79
80
  ./privilege.py
80
81
  ./project.py
81
82
  ./prompt_template.py
83
+ ./py.typed
82
84
  ./repl_theme.py
83
85
  ./repl_tracker.py
84
86
  ./repl_ui.py
@@ -231,6 +231,9 @@ def _from_settings(attr: str, default: Any = "") -> Any:
231
231
  logger.debug("Settings property %r not found, using default %r", attr, default)
232
232
  return default
233
233
  except Exception:
234
+ # Catch-all for unexpected error types (e.g., OSError on corrupt file,
235
+ # TypeError from malformed data). These are logged at WARNING with full
236
+ # traceback so they don't go unnoticed, but the system stays running.
234
237
  logger = logging.getLogger(__name__)
235
238
  logger.warning(
236
239
  "Failed to read settings.%s — using default %r. "
@@ -1,14 +1,14 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- 正式扩展 API — 统一的插件系统。
3
+ Extension API — unified plugin system.
4
4
 
5
- 提供:
6
- - Extension 基类: 生命周期钩子 (load / unload / activate / deactivate)
7
- - ExtensionManager: 扩展发现、注册、激活、卸载
8
- - @extension 装饰器: 声明式注册
9
- - ExtensionPoint: 标记类, 用于定义可扩展点
5
+ Provides:
6
+ - Extension base class: lifecycle hooks (load / unload / activate / deactivate)
7
+ - ExtensionManager: extension discovery, registration, activation, unloading
8
+ - @extension decorator: declarative registration
9
+ - ExtensionPoint: marker class for defining extension points
10
10
 
11
- 使用示例:
11
+ Usage example:
12
12
 
13
13
  from .extension import Extension, extension
14
14
 
@@ -21,7 +21,7 @@
21
21
  def get_prompt(self) -> str:
22
22
  return "You are an expert in..."
23
23
 
24
- # 注册到全局管理器
24
+ # Register with the global manager
25
25
  from .extension import get_extension_manager
26
26
  get_extension_manager().register(MySkill())
27
27
  """
@@ -88,17 +88,17 @@ class ExtensionType:
88
88
 
89
89
  class ExtensionPoint:
90
90
  """
91
- 标记一个可扩展点。扩展可以通过名称向该点注册回调。
91
+ Marker for an extension point. Extensions can register callbacks by name.
92
92
 
93
- 用法:
93
+ Usage:
94
94
 
95
- # 定义一个扩展点
95
+ # Define an extension point
96
96
  ON_SYSTEM_PROMPT = ExtensionPoint("system_prompt")
97
97
 
98
- # 扩展注册回调
98
+ # Extension registers a callback
99
99
  ON_SYSTEM_PROMPT.register(my_callable)
100
100
 
101
- # 触发所有已注册的回调
101
+ # Fire all registered callbacks
102
102
  results = ON_SYSTEM_PROMPT.trigger(prompt="...")
103
103
  """
104
104
 
@@ -181,23 +181,23 @@ class ExtensionPoint:
181
181
 
182
182
  class Extension(ABC):
183
183
  """
184
- 扩展基类。所有 ATA Coder 扩展的基类。
184
+ Extension base class. Base class for all ATA Coder extensions.
185
185
 
186
- 生命周期:
187
- 1. __init__() — 实例化
188
- 2. on_load(manager) — 被管理器加载时调用
189
- 3. on_activate() — 被激活时调用
190
- 4. on_deactivate() — 被停用时调用
191
- 5. on_unload() — 被卸载时调用
186
+ Lifecycle:
187
+ 1. __init__() — instantiation
188
+ 2. on_load(manager) — called when loaded by the manager
189
+ 3. on_activate() — called when activated
190
+ 4. on_deactivate() — called when deactivated
191
+ 5. on_unload() — called when unloaded
192
192
 
193
- 子类必须设置:
193
+ Subclasses MUST set:
194
194
  - meta: ExtensionMeta
195
195
 
196
- 子类可选覆盖:
196
+ Subclasses MAY override:
197
197
  - on_load() / on_unload() / on_activate() / on_deactivate()
198
- - get_tools() → 返回工具定义列表
199
- - get_prompt() → 返回系统提示字符串
200
- - validate() → 验证扩展是否可用
198
+ - get_tools() → list of tool definitions
199
+ - get_prompt() → system prompt string
200
+ - validate() → verify extension is usable
201
201
  """
202
202
 
203
203
  meta: ExtensionMeta
@@ -208,27 +208,27 @@ class Extension(ABC):
208
208
  cls.meta = ExtensionMeta(name=cls.__name__)
209
209
 
210
210
  def on_load(self, manager: "ExtensionManager") -> None:
211
- """扩展被加载到管理器时调用。"""
211
+ """Extension was loaded into a manager."""
212
212
 
213
213
  def on_unload(self) -> None:
214
- """扩展被卸载时调用。"""
214
+ """Extension is being unloaded."""
215
215
 
216
216
  def on_activate(self) -> None:
217
- """扩展被激活时调用。"""
217
+ """Extension was activated."""
218
218
 
219
219
  def on_deactivate(self) -> None:
220
- """扩展被停用时调用。"""
220
+ """Extension was deactivated."""
221
221
 
222
222
  def get_tools(self) -> list[dict[str, Any]]:
223
- """返回此扩展提供的工具定义列表。"""
223
+ """Return the tool definitions provided by this extension."""
224
224
  return []
225
225
 
226
226
  def get_prompt(self) -> str:
227
- """返回此扩展提供的系统提示片段。"""
227
+ """Return the system prompt fragment provided by this extension."""
228
228
  return ""
229
229
 
230
230
  def get_middleware(self) -> list[Callable]:
231
- """返回此扩展提供的中间件列表。"""
231
+ """Return the middleware list provided by this extension."""
232
232
  return []
233
233
 
234
234
  def validate(self) -> tuple[bool, str]:
@@ -250,9 +250,9 @@ class Extension(ABC):
250
250
 
251
251
  class ExtensionManager:
252
252
  """
253
- 扩展管理器发现、加载、激活和管理扩展。
253
+ Extension manager discovers, loads, activates, and manages extensions.
254
254
 
255
- 用法:
255
+ Usage:
256
256
 
257
257
  mgr = ExtensionManager()
258
258
  mgr.discover("./extensions/")
@@ -587,9 +587,9 @@ def extension(
587
587
  **kwargs: Any,
588
588
  ) -> Callable:
589
589
  """
590
- 类装饰器声明一个扩展。
590
+ Class decorator declare an extension.
591
591
 
592
- 用法:
592
+ Usage:
593
593
 
594
594
  @extension(name="my-skill", version="1.0.0",
595
595
  tags=["skill"], priority=10)
@@ -641,7 +641,7 @@ _extension_manager: ExtensionManager | None = None
641
641
 
642
642
 
643
643
  def get_extension_manager() -> ExtensionManager:
644
- """获取全局扩展管理器单例。"""
644
+ """Get the global ExtensionManager singleton."""
645
645
  global _extension_manager
646
646
  if _extension_manager is None:
647
647
  _extension_manager = ExtensionManager()
@@ -649,6 +649,6 @@ def get_extension_manager() -> ExtensionManager:
649
649
 
650
650
 
651
651
  def reset_extension_manager() -> None:
652
- """重置全局扩展管理器(主要用于测试)"""
652
+ """Reset the global extension manager (mainly for testing)."""
653
653
  global _extension_manager
654
654
  _extension_manager = None
@@ -44,7 +44,7 @@ if sys.platform == 'win32':
44
44
  _patched_init.__ata_patched__ = True
45
45
  _sp.Popen.__init__ = _patched_init
46
46
 
47
- __version__ = "2.4.4"
47
+ __version__ = "2.4.5"
48
48
 
49
49
  import asyncio
50
50
  import logging
@@ -122,17 +122,29 @@ def _init_subsystems(config, **kwargs) -> dict:
122
122
  workspace = config.agent.workspace_dir
123
123
  errors: list[str] = []
124
124
 
125
+ def _init_subsystem(name, factory, critical=True):
126
+ """Initialize a single subsystem with consistent error handling.
127
+
128
+ Critical subsystems raise on failure; non-critical log a warning
129
+ and return None.
130
+ """
131
+ try:
132
+ return factory()
133
+ except Exception as e:
134
+ if critical:
135
+ logger.exception("%s init failed", name)
136
+ errors.append(f" {name}: {e}")
137
+ else:
138
+ logger.warning("%s unavailable: %s", name, e)
139
+ return None
140
+
125
141
  # ── Critical: agent cannot function without these ──────────────────
126
142
  for name, factory in [
127
143
  ("skills", lambda: get_skill_manager(kwargs.get("skills_dir"))),
128
144
  ("memory", lambda: get_memory_store(kwargs.get("memory_dir"))),
129
145
  ("permissions", lambda: PermissionStore()),
130
146
  ]:
131
- try:
132
- result[name] = factory()
133
- except Exception as e:
134
- logger.exception("%s init failed", name)
135
- errors.append(f" {name}: {e}")
147
+ result[name] = _init_subsystem(name, factory, critical=True)
136
148
 
137
149
  # ── Non-critical: nice-to-have, degrade gracefully ─────────────────
138
150
  for name, factory in [
@@ -140,11 +152,7 @@ def _init_subsystems(config, **kwargs) -> dict:
140
152
  ("templates", lambda: _try_init_templates(kwargs.get("prompts_dir"))),
141
153
  ("project", lambda: ProjectDetector(workspace).detect()),
142
154
  ]:
143
- try:
144
- result[name] = factory()
145
- except Exception as e:
146
- logger.warning("%s unavailable: %s", name, e)
147
- result[name] = None
155
+ result[name] = _init_subsystem(name, factory, critical=False)
148
156
 
149
157
  # MCP is special: only init if config provided
150
158
  result["mcp"] = _try_init_mcp(kwargs.get("mcp_config"))
@@ -163,11 +171,20 @@ def _try_init_templates(prompts_dir: str | None):
163
171
  return TemplateManager(prompts_dir)
164
172
 
165
173
 
166
- def _try_init_mcp(mcp_config: str | None):
167
- if not mcp_config:
174
+ def _try_init_mcp(mcp_config_path: str | None):
175
+ """Initialize MCP client from a JSON/YAML config file path.
176
+
177
+ Args:
178
+ mcp_config_path: Path to an MCP configuration file (JSON or YAML).
179
+ If None or empty, MCP is not initialized.
180
+
181
+ Returns:
182
+ MCPClient instance or None.
183
+ """
184
+ if not mcp_config_path:
168
185
  return None
169
186
  from .mcp_client import MCPClient, load_mcp_config
170
- return MCPClient(load_mcp_config(mcp_config))
187
+ return MCPClient(load_mcp_config(mcp_config_path))
171
188
 
172
189
 
173
190
  # ── Config override ─────────────────────────────────────────────────────
File without changes
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ata-coder"
7
- version = "2.4.4"
7
+ version = "2.4.5"
8
8
  description = "ATA Coder — AI-powered coding assistant"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -47,37 +47,43 @@ class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
47
47
  daemon_threads = True # threads exit when server shuts down
48
48
  _max_threads: int = 64
49
49
 
50
+ def __init__(self, *args, **kwargs):
51
+ super().__init__(*args, **kwargs)
52
+ self._thread_lock = threading.Lock()
53
+ self._active_threads = 0
54
+
50
55
  def process_request(self, request, client_address):
51
56
  """Override to enforce a maximum thread count."""
52
57
  # ThreadingMixIn uses a thread per request; we cap the pool.
53
- active = getattr(self, '_active_threads', 0)
54
- if active >= self._max_threads:
55
- # Too many active threads — reject with 503
56
- CRLF = b"\r\n"
57
- try:
58
- request.sendall(CRLF.join([
59
- b"HTTP/1.1 503 Service Unavailable",
60
- b"Content-Type: application/json",
61
- b"Connection: close",
62
- b"Content-Length: 60",
63
- b"",
64
- b'{"error":"Server busy - too many concurrent requests"}',
65
- b"",
66
- ]))
67
- except Exception:
68
- pass
69
- finally:
58
+ with self._thread_lock:
59
+ if self._active_threads >= self._max_threads:
60
+ # Too many active threads — reject with 503
61
+ CRLF = b"\r\n"
70
62
  try:
71
- request.close()
63
+ request.sendall(CRLF.join([
64
+ b"HTTP/1.1 503 Service Unavailable",
65
+ b"Content-Type: application/json",
66
+ b"Connection: close",
67
+ b"Content-Length: 60",
68
+ b"",
69
+ b'{"error":"Server busy - too many concurrent requests"}',
70
+ b"",
71
+ ]))
72
72
  except Exception:
73
73
  pass
74
- return
75
- # Track active count (best-effort — Python GIL keeps this safe)
76
- self._active_threads = active + 1
74
+ finally:
75
+ try:
76
+ request.close()
77
+ except Exception:
78
+ pass
79
+ return
80
+ # Atomic read-modify-write under lock (was: bare GIL-only read+write)
81
+ self._active_threads += 1
77
82
  try:
78
83
  super().process_request(request, client_address)
79
84
  finally:
80
- self._active_threads = max(0, getattr(self, '_active_threads', 1) - 1)
85
+ with self._thread_lock:
86
+ self._active_threads = max(0, self._active_threads - 1)
81
87
  from pathlib import Path
82
88
  from typing import Any
83
89
  from urllib.parse import urlparse
@@ -109,9 +115,11 @@ class AgentAPIHandler(BaseHTTPRequestHandler):
109
115
  before the server starts accepting requests.
110
116
  """
111
117
 
112
- # Class-level references (set by server factory)
113
- config: AppConfig = None
114
- store: SessionStore = None
118
+ # Class-level references (set by server factory before accepting requests).
119
+ # These are None until create_server() assigns them. mypy note: declared as
120
+ # Optional because they start as None; asserted non-None at usage sites.
121
+ config: "AppConfig | None" = None
122
+ store: "SessionStore | None" = None
115
123
  _ws_lock: threading.Lock = threading.Lock() # protects workspace dir reads/writes
116
124
 
117
125
  # ── Rate limiting (class-level, shared across handler instances) ──────
@@ -122,6 +130,25 @@ class AgentAPIHandler(BaseHTTPRequestHandler):
122
130
  _RATE_WINDOW_S = 60.0 # sliding window in seconds
123
131
  _RATE_BLOCK_S = 300.0 # block duration after exceeding penalty threshold
124
132
  _RATE_PENALTY_MULTIPLIER = 3 # requests × this = block threshold
133
+ _RATE_CLEANUP_INTERVAL = 1000 # amortized: trigger cleanup every N calls
134
+ _rate_cleanup_counter: int = 0
135
+
136
+ @classmethod
137
+ def _cleanup_rate_buckets(cls, now: float) -> None:
138
+ """Remove stale IP entries whose last activity exceeds 2× the window.
139
+
140
+ Without this, IPs that stay under the rate limit forever accumulate
141
+ in _rate_buckets and leak memory over long-running server processes.
142
+ """
143
+ cutoff = now - cls._RATE_WINDOW_S * 2
144
+ stale = [
145
+ ip for ip, dq in cls._rate_buckets.items()
146
+ if not dq or dq[-1] <= cutoff
147
+ ]
148
+ for ip in stale:
149
+ del cls._rate_buckets[ip]
150
+ if stale:
151
+ logger.debug("Rate limiter: pruned %d stale IP bucket(s)", len(stale))
125
152
 
126
153
  @classmethod
127
154
  def _check_rate_limit(cls, client_ip: str) -> bool:
@@ -131,6 +158,12 @@ class AgentAPIHandler(BaseHTTPRequestHandler):
131
158
  """
132
159
  now = time.time()
133
160
  with cls._rate_lock:
161
+ # Periodic stale-bucket cleanup (amortized — triggered every N calls)
162
+ cls._rate_cleanup_counter += 1
163
+ if cls._rate_cleanup_counter >= cls._RATE_CLEANUP_INTERVAL:
164
+ cls._rate_cleanup_counter = 0
165
+ cls._cleanup_rate_buckets(now)
166
+
134
167
  # Check if IP is currently blocked (penalty tier)
135
168
  blocked_until = cls._rate_blocked.get(client_ip, 0)
136
169
  if now < blocked_until:
@@ -184,15 +217,21 @@ class AgentAPIHandler(BaseHTTPRequestHandler):
184
217
  """
185
218
  expected = os.environ.get("ATA_CODER_API_TOKEN", "")
186
219
  if not expected:
187
- # No token configured — restrict to localhost only
188
- client_host = self.client_address[0]
189
- if client_host in ("127.0.0.1", "::1", "localhost"):
190
- return True
191
- logger.warning(
192
- "Remote request from %s rejected: no ATA_CODER_API_TOKEN configured. "
193
- "Set ATA_CODER_API_TOKEN env var to allow remote access.",
194
- client_host,
195
- )
220
+ # No token configured — require explicit --no-auth flag to allow
221
+ # localhost access without authentication. Otherwise, generate a
222
+ # random token so even localhost requires proof of access.
223
+ if os.environ.get("ATA_CODER_NO_AUTH", "").lower() in ("1", "true", "yes"):
224
+ client_host = self.client_address[0]
225
+ if client_host in ("127.0.0.1", "::1", "localhost"):
226
+ return True
227
+ logger.warning(
228
+ "Remote request from %s rejected: ATA_CODER_NO_AUTH only "
229
+ "allows localhost. Set ATA_CODER_API_TOKEN for remote access.",
230
+ client_host,
231
+ )
232
+ return False
233
+ # Secure default: even localhost must present the auto-generated token
234
+ # (printed once at server startup via create_server)
196
235
  return False
197
236
  token = (self.headers.get("Authorization", "")
198
237
  .removeprefix("Bearer ").strip())
@@ -954,6 +993,17 @@ def create_server(
954
993
 
955
994
  config = config or get_config()
956
995
 
996
+ # Auto-generate API token if none configured (secure default)
997
+ if not os.environ.get("ATA_CODER_API_TOKEN") and os.environ.get("ATA_CODER_NO_AUTH", "").lower() not in ("1", "true", "yes"):
998
+ auto_token = secrets.token_urlsafe(24)
999
+ os.environ["ATA_CODER_API_TOKEN"] = auto_token
1000
+ logger.info(
1001
+ "🔐 No ATA_CODER_API_TOKEN set — auto-generated: %s\n"
1002
+ " Pass this token as 'Authorization: Bearer %s' header.\n"
1003
+ " Set ATA_CODER_NO_AUTH=1 to disable auth for localhost-only use.",
1004
+ auto_token, auto_token,
1005
+ )
1006
+
957
1007
  AgentAPIHandler.config = config
958
1008
  AgentAPIHandler.store = SessionStore()
959
1009
 
@@ -113,11 +113,41 @@ class SessionStore:
113
113
  if os.environ.get("ATA_CODER_ALLOW_ALL", "").lower() in ("1", "true", "yes"):
114
114
  perms.set_category_rule("shell", PermissionMode.ALLOW)
115
115
  perms.set_category_rule("write", PermissionMode.ALLOW)
116
- logger.warning(
116
+ # Audit logger: records every allow-all decision for forensic trace
117
+ audit_logger = logging.getLogger("ata_coder.audit.allow_all")
118
+ audit_logger.setLevel(logging.WARNING)
119
+ audit_logger.warning(
117
120
  "⚠️ ATA_CODER_ALLOW_ALL=1 — ALL shell & write operations "
118
121
  "will be silently allowed without permission prompts. "
119
122
  "This is intended for headless/automated environments only."
120
123
  )
124
+ # Wrap the permission store to emit audit log entries for every
125
+ # check that would have required confirmation in interactive mode.
126
+ _orig_check = perms.check
127
+ def _audited_check(tool_name: str, arguments: dict | None = None) -> bool:
128
+ result = _orig_check(tool_name, arguments)
129
+ from .permissions import tool_category
130
+ category = tool_category(tool_name)
131
+ if result and category in ("shell", "write"):
132
+ audit_logger.info(
133
+ "ALLOW_ALL: %s | %s | %s",
134
+ category, tool_name, str(arguments or {})[:200]
135
+ )
136
+ # CRITICAL safety: hard-block destructive patterns even in allow-all mode.
137
+ # Patterns like rm -rf /, mkfs, dd writes, fork bombs are never auto-allowed.
138
+ if category == "shell" and result:
139
+ from .safety_guard import analyze_command, RiskLevel
140
+ cmd = str((arguments or {}).get("command", ""))
141
+ if cmd:
142
+ risk = analyze_command(cmd)
143
+ if risk.risk == RiskLevel.CRITICAL:
144
+ audit_logger.critical(
145
+ "ALLOW_ALL BLOCKED (CRITICAL): %s | %s",
146
+ tool_name, risk.description or cmd[:200]
147
+ )
148
+ return False
149
+ return result
150
+ perms.check = _audited_check
121
151
  elif os.environ.get("ATA_CODER_API_TOKEN", ""):
122
152
  # Token auth is configured — trust the caller's auth layer,
123
153
  # but still require explicit allow-all for write/shell.
@@ -96,7 +96,7 @@ def run_setup_wizard() -> None:
96
96
  print()
97
97
 
98
98
 
99
- __version__ = "2.4.4"
99
+ __version__ = "2.4.5"
100
100
 
101
101
 
102
102
  def print_banner() -> None:
@@ -18,7 +18,7 @@ from dataclasses import dataclass, field
18
18
  from typing import Callable, Optional
19
19
 
20
20
  from .config import AppConfig, LLMConfig
21
- from .llm_client import LLMClient
21
+ # LLMClient/AnthropicClient created via create_llm_client() factory in utils.py
22
22
  from .tools import ToolExecutor, TOOL_DEFINITIONS, ToolResult
23
23
 
24
24
  logger = logging.getLogger(__name__)
@@ -69,13 +69,8 @@ class SubAgent:
69
69
  thinking_strength=config.llm.thinking_strength,
70
70
  use_anthropic=config.llm.use_anthropic,
71
71
  )
72
- if llm_config.use_anthropic:
73
- from .anthropic_client import AnthropicClient
74
- self._llm = AnthropicClient(llm_config)
75
- self._use_anthropic = True
76
- else:
77
- self._llm = LLMClient(llm_config)
78
- self._use_anthropic = False
72
+ from .utils import create_llm_client
73
+ self._llm, self._use_anthropic = create_llm_client(llm_config)
79
74
 
80
75
  # Independent tool executor
81
76
  self._tools = ToolExecutor(config.agent)
@@ -39,7 +39,7 @@ class TestSessionStore:
39
39
  store, config = store
40
40
  sid, agent = store.create(config)
41
41
  assert isinstance(sid, str)
42
- assert len(sid) == 12
42
+ assert len(sid) == 32 # uuid.uuid4().hex = 32 chars
43
43
  assert agent is not None
44
44
 
45
45
  def test_create_adds_to_list(self, store):
@@ -61,7 +61,7 @@ class ToolExecutor(WebToolsMixin, SubAgentToolsMixin):
61
61
  self._edit_callback: Callable[[str, str], None] | None = None
62
62
  # File read cache: path → (mtime, cached_at, content).
63
63
  self._file_cache: dict[str, tuple[float, float, str]] = {}
64
- self._file_cache_max_entries = 50
64
+ self._file_cache_max_entries = 200
65
65
  self._FILE_CACHE_TTL = 30.0 # seconds before a cache entry is revalidated
66
66
  self._cache_dir: Path | None = None
67
67
  # Sub-agent manager (set by agent)