ata-coder 2.4.7__tar.gz → 2.4.9__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.
- {ata_coder-2.4.7/ata_coder.egg-info → ata_coder-2.4.9}/PKG-INFO +9 -5
- {ata_coder-2.4.7 → ata_coder-2.4.9}/README.md +7 -3
- {ata_coder-2.4.7 → ata_coder-2.4.9}/agent.py +4 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/agent_tools.py +2 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/anthropic_client.py +0 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9/ata_coder.egg-info}/PKG-INFO +9 -5
- {ata_coder-2.4.7 → ata_coder-2.4.9}/ata_coder.egg-info/SOURCES.txt +4 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/ata_coder.egg-info/requires.txt +1 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/change_tracker.py +2 -2
- {ata_coder-2.4.7 → ata_coder-2.4.9}/config.py +7 -3
- {ata_coder-2.4.7 → ata_coder-2.4.9}/llm_client.py +0 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/main.py +3 -3
- {ata_coder-2.4.7 → ata_coder-2.4.9}/memory.py +3 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/privilege.py +10 -6
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompt_template.py +1 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/pyproject.toml +11 -5
- {ata_coder-2.4.7 → ata_coder-2.4.9}/safety_guard.py +1 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/server.py +10 -80
- ata_coder-2.4.9/server_rate_limit.py +89 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/server_session.py +1 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/settings.py +0 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/setup_wizard.py +1 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/math-calculator/handler.py +1 -2
- {ata_coder-2.4.7 → ata_coder-2.4.9}/sub_agent.py +0 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_change_tracker.py +1 -1
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_memory.py +2 -2
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_tools.py +1 -1
- ata_coder-2.4.9/tools/file_ops.py +183 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/LICENSE +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/MANIFEST.in +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/__init__.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/agent_compact.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/agent_controller.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/agent_extension.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/agent_routing.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/agent_subsystems.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/agent_undo.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/ata_coder.egg-info/dependency_links.txt +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/ata_coder.egg-info/entry_points.txt +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/ata_coder.egg-info/top_level.txt +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/clawd_integration.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/commands/__init__.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/commands/_core.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/commands/_safety.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/commands/_settings.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/commands/_workflow.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/context_manager.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/core/__init__.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/core/events.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/core/queue.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/core/state.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/event_queue.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/extension.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/extensions/__init__.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/extensions/hello_skill.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/fool_proof.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/git_workflow.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/gui.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/mcp_client.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/model_registry.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/model_router.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/permissions.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/project.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/auto-mode.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/coding-rules.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/execution-guardrails.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/memory-system.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/output-style.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/safety.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/slash-commands.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/sub-agents.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/system-reminders.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/system.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/prompts/tool-policy.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/py.typed +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/repl_theme.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/repl_tracker.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/repl_ui.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/self_correct.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/server_shell.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/session.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/setup.cfg +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skill_extension.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/architect/SKILL.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/code-reviewer/SKILL.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/codecraft/SKILL.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/debugger/SKILL.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/doc-writer/SKILL.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/general-coder/SKILL.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/math-calculator/README.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/math-calculator/SKILL.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/math-calculator/prompts/system.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/math-calculator/requirements.txt +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/math-calculator/resources/constants.json +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/math-calculator/tests/test_handler.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/security-auditor/SKILL.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/test-writer/SKILL.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/weather-skill/README.md +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/weather-skill/handler.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/weather-skill/manifest.json +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/weather-skill/prompts/system_prompt.txt +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/weather-skill/prompts/user_prompt_template.txt +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/weather-skill/requirements.txt +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/weather-skill/resources/city_list.json +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/weather-skill/resources/error_messages.json +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/weather-skill/tests/test_handler.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills/weather-skill/weather_utils.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/skills.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/sub_agent_manager.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/system_prompt_builder.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/task_planner.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/terminal.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_agent.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_config.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_event_queue.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_extension.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_fibonacci.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_fool_proof.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_llm_client.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_model_registry.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_permissions.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_privilege.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_prompt_template.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_safety_guard.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_server.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_skill_handlers.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tests/test_sub_agent.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/token_counter.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tools/__init__.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tools/definitions.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tools/executor.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tools/result.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tools/strategy.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tools/subagent.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/tools/web.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/types.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/utils.py +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/web/css/style.css +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/web/index.html +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/web/js/app.js +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/web/package-lock.json +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/web/package.json +0 -0
- {ata_coder-2.4.7 → ata_coder-2.4.9}/web/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ata-coder
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.9
|
|
4
4
|
Summary: ATA Coder — AI-powered coding assistant
|
|
5
5
|
Author: ATA Coder Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Requires-Python: >=3.10
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: click>=8.0
|
|
11
|
-
Requires-Dist: httpx
|
|
11
|
+
Requires-Dist: httpx<1.0,>=0.27.0
|
|
12
12
|
Requires-Dist: colorama>=0.4.6
|
|
13
13
|
Requires-Dist: python-dotenv>=1.0.0
|
|
14
14
|
Requires-Dist: rich>=13.0.0
|
|
@@ -21,13 +21,15 @@ 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.
|
|
24
|
+
# ATA Coder v2.4.8
|
|
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.
|
|
30
|
+
> **v2.4.8** — 🤖 **Self-Bootstrapped Audit (Round 12)**: ATA Coder found 10 bugs in its own source — thread races, command injection, silent corruption. All self-found, all self-fixed.
|
|
31
|
+
>
|
|
32
|
+
> > **v2.4.7** — ⚡ **Context Memory Refactor**: O(1) token tracking, ContextManager, section-level prompt caching, pre-tokenized TF-IDF, LRU token cache. ~60% less overhead in the hot loop.
|
|
31
33
|
>
|
|
32
34
|
> > **v2.4.6** — 🔐 **OS-Native Credential Store**: API key encrypted at rest via Windows DPAPI / macOS Keychain / Linux secret-tool. Auto-migrates plaintext keys. Zero dependencies.
|
|
33
35
|
>
|
|
@@ -552,7 +554,9 @@ All 6 findings in this release were discovered by **ATA Coder scanning its own s
|
|
|
552
554
|
|
|
553
555
|
## 中文
|
|
554
556
|
|
|
555
|
-
> **v2.4.
|
|
557
|
+
> **v2.4.8** — 🤖 **自举审计(第 12 轮)**: ATA Coder 审计自身源码发现 10 个 bug — 线程竞态、命令注入、静默数据损坏。全部自发现、自修复。
|
|
558
|
+
>
|
|
559
|
+
> > **v2.4.7** — ⚡ **上下文记忆重构**: O(1) Token 追踪、ContextManager、章节级提示缓存、预分词 TF-IDF、LRU Token 缓存。热路径开销降低约 60%。
|
|
556
560
|
>
|
|
557
561
|
> > **v2.4.6** — 🔐 **操作系统凭据存储**: API Key 通过 Windows DPAPI / macOS Keychain / Linux secret-tool 加密存储。自动迁移明文密钥。零依赖。
|
|
558
562
|
>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
# ATA Coder v2.4.
|
|
1
|
+
# ATA Coder v2.4.8
|
|
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.
|
|
7
|
+
> **v2.4.8** — 🤖 **Self-Bootstrapped Audit (Round 12)**: ATA Coder found 10 bugs in its own source — thread races, command injection, silent corruption. All self-found, all self-fixed.
|
|
8
|
+
>
|
|
9
|
+
> > **v2.4.7** — ⚡ **Context Memory Refactor**: O(1) token tracking, ContextManager, section-level prompt caching, pre-tokenized TF-IDF, LRU token cache. ~60% less overhead in the hot loop.
|
|
8
10
|
>
|
|
9
11
|
> > **v2.4.6** — 🔐 **OS-Native Credential Store**: API key encrypted at rest via Windows DPAPI / macOS Keychain / Linux secret-tool. Auto-migrates plaintext keys. Zero dependencies.
|
|
10
12
|
>
|
|
@@ -529,7 +531,9 @@ All 6 findings in this release were discovered by **ATA Coder scanning its own s
|
|
|
529
531
|
|
|
530
532
|
## 中文
|
|
531
533
|
|
|
532
|
-
> **v2.4.
|
|
534
|
+
> **v2.4.8** — 🤖 **自举审计(第 12 轮)**: ATA Coder 审计自身源码发现 10 个 bug — 线程竞态、命令注入、静默数据损坏。全部自发现、自修复。
|
|
535
|
+
>
|
|
536
|
+
> > **v2.4.7** — ⚡ **上下文记忆重构**: O(1) Token 追踪、ContextManager、章节级提示缓存、预分词 TF-IDF、LRU Token 缓存。热路径开销降低约 60%。
|
|
533
537
|
>
|
|
534
538
|
> > **v2.4.6** — 🔐 **操作系统凭据存储**: API Key 通过 Windows DPAPI / macOS Keychain / Linux secret-tool 加密存储。自动迁移明文密钥。零依赖。
|
|
535
539
|
>
|
|
@@ -22,7 +22,6 @@ The agent runs a conversation loop:
|
|
|
22
22
|
import asyncio
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
|
-
import os
|
|
26
25
|
import time
|
|
27
26
|
from typing import Any, Callable
|
|
28
27
|
|
|
@@ -142,6 +141,10 @@ class CoderAgent(CompactionMixin, ToolExecutionMixin,
|
|
|
142
141
|
self.self_correct = SelfCorrectionEngine(max_retries=1)
|
|
143
142
|
self.git = GitWorkflow(self.tools.workspace)
|
|
144
143
|
|
|
144
|
+
# Per-instance self-correction depth (was a class variable shared across
|
|
145
|
+
# all agent instances — dangerous under ThreadingHTTPServer in server mode).
|
|
146
|
+
self._self_correct_depth: int = 0
|
|
147
|
+
|
|
145
148
|
self._state = AgentState()
|
|
146
149
|
self._on_event: Callable[[AgentEvent], None] | None = None
|
|
147
150
|
self._current_session_id: str = ""
|
|
@@ -17,7 +17,8 @@ class ToolExecutionMixin:
|
|
|
17
17
|
# ── Tool execution ────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
19
|
# Guard depth for self-correction retry — prevents infinite recursion.
|
|
20
|
-
|
|
20
|
+
# These are set as INSTANCE variables in CoderAgent.__init__ to avoid
|
|
21
|
+
# cross-session contamination under ThreadingHTTPServer (server mode).
|
|
21
22
|
_MAX_SELF_CORRECT_DEPTH: int = 1
|
|
22
23
|
|
|
23
24
|
async def _execute_tool(self, tool_name: str, arguments: dict[str, Any]) -> ToolResult:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ata-coder
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.9
|
|
4
4
|
Summary: ATA Coder — AI-powered coding assistant
|
|
5
5
|
Author: ATA Coder Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Requires-Python: >=3.10
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: click>=8.0
|
|
11
|
-
Requires-Dist: httpx
|
|
11
|
+
Requires-Dist: httpx<1.0,>=0.27.0
|
|
12
12
|
Requires-Dist: colorama>=0.4.6
|
|
13
13
|
Requires-Dist: python-dotenv>=1.0.0
|
|
14
14
|
Requires-Dist: rich>=13.0.0
|
|
@@ -21,13 +21,15 @@ 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.
|
|
24
|
+
# ATA Coder v2.4.8
|
|
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.
|
|
30
|
+
> **v2.4.8** — 🤖 **Self-Bootstrapped Audit (Round 12)**: ATA Coder found 10 bugs in its own source — thread races, command injection, silent corruption. All self-found, all self-fixed.
|
|
31
|
+
>
|
|
32
|
+
> > **v2.4.7** — ⚡ **Context Memory Refactor**: O(1) token tracking, ContextManager, section-level prompt caching, pre-tokenized TF-IDF, LRU token cache. ~60% less overhead in the hot loop.
|
|
31
33
|
>
|
|
32
34
|
> > **v2.4.6** — 🔐 **OS-Native Credential Store**: API key encrypted at rest via Windows DPAPI / macOS Keychain / Linux secret-tool. Auto-migrates plaintext keys. Zero dependencies.
|
|
33
35
|
>
|
|
@@ -552,7 +554,9 @@ All 6 findings in this release were discovered by **ATA Coder scanning its own s
|
|
|
552
554
|
|
|
553
555
|
## 中文
|
|
554
556
|
|
|
555
|
-
> **v2.4.
|
|
557
|
+
> **v2.4.8** — 🤖 **自举审计(第 12 轮)**: ATA Coder 审计自身源码发现 10 个 bug — 线程竞态、命令注入、静默数据损坏。全部自发现、自修复。
|
|
558
|
+
>
|
|
559
|
+
> > **v2.4.7** — ⚡ **上下文记忆重构**: O(1) Token 追踪、ContextManager、章节级提示缓存、预分词 TF-IDF、LRU Token 缓存。热路径开销降低约 60%。
|
|
556
560
|
>
|
|
557
561
|
> > **v2.4.6** — 🔐 **操作系统凭据存储**: API Key 通过 Windows DPAPI / macOS Keychain / Linux secret-tool 加密存储。自动迁移明文密钥。零依赖。
|
|
558
562
|
>
|
|
@@ -38,6 +38,7 @@ repl_ui.py
|
|
|
38
38
|
safety_guard.py
|
|
39
39
|
self_correct.py
|
|
40
40
|
server.py
|
|
41
|
+
server_rate_limit.py
|
|
41
42
|
server_session.py
|
|
42
43
|
server_shell.py
|
|
43
44
|
session.py
|
|
@@ -89,6 +90,7 @@ utils.py
|
|
|
89
90
|
./safety_guard.py
|
|
90
91
|
./self_correct.py
|
|
91
92
|
./server.py
|
|
93
|
+
./server_rate_limit.py
|
|
92
94
|
./server_session.py
|
|
93
95
|
./server_shell.py
|
|
94
96
|
./session.py
|
|
@@ -154,6 +156,7 @@ utils.py
|
|
|
154
156
|
./tools/__init__.py
|
|
155
157
|
./tools/definitions.py
|
|
156
158
|
./tools/executor.py
|
|
159
|
+
./tools/file_ops.py
|
|
157
160
|
./tools/result.py
|
|
158
161
|
./tools/strategy.py
|
|
159
162
|
./tools/subagent.py
|
|
@@ -238,6 +241,7 @@ tests/test_tools.py
|
|
|
238
241
|
tools/__init__.py
|
|
239
242
|
tools/definitions.py
|
|
240
243
|
tools/executor.py
|
|
244
|
+
tools/file_ops.py
|
|
241
245
|
tools/result.py
|
|
242
246
|
tools/strategy.py
|
|
243
247
|
tools/subagent.py
|
|
@@ -143,7 +143,7 @@ class ChangeTracker:
|
|
|
143
143
|
old_content = f.read()
|
|
144
144
|
self._backup(file_path)
|
|
145
145
|
except Exception:
|
|
146
|
-
|
|
146
|
+
logger.debug("Failed to read/backup existing file %s", file_path, exc_info=True)
|
|
147
147
|
|
|
148
148
|
change = FileChange(
|
|
149
149
|
id=self._next_id,
|
|
@@ -173,7 +173,7 @@ class ChangeTracker:
|
|
|
173
173
|
if old_content == new_content:
|
|
174
174
|
return None
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
Path(file_path)
|
|
177
177
|
# Backup before edit (for undo)
|
|
178
178
|
self._backup(file_path)
|
|
179
179
|
|
|
@@ -197,17 +197,21 @@ class AppConfig:
|
|
|
197
197
|
# Using a lazy pattern because AppConfig.load() references _from_settings()
|
|
198
198
|
# which is defined after the dataclass body in this module.
|
|
199
199
|
_config: AppConfig | None = None
|
|
200
|
+
_config_lock = threading.Lock()
|
|
200
201
|
|
|
201
202
|
|
|
202
203
|
def get_config() -> AppConfig:
|
|
203
204
|
"""Return the module-level config singleton (lazy init on first call).
|
|
204
205
|
|
|
205
|
-
After the first call the config is cached.
|
|
206
|
-
|
|
206
|
+
After the first call the config is cached. Double-checked locking
|
|
207
|
+
protects the lazy-init path when server.py runs under ThreadingHTTPServer
|
|
208
|
+
(where multiple threads may race on the first call).
|
|
207
209
|
"""
|
|
208
210
|
global _config
|
|
209
211
|
if _config is None:
|
|
210
|
-
|
|
212
|
+
with _config_lock:
|
|
213
|
+
if _config is None: # double-check
|
|
214
|
+
_config = AppConfig.load()
|
|
211
215
|
return _config
|
|
212
216
|
|
|
213
217
|
|
|
@@ -282,7 +282,6 @@ class LLMClient(BaseLLMClient):
|
|
|
282
282
|
body.pop("temperature", None)
|
|
283
283
|
|
|
284
284
|
# Retry loop for streaming (up to 2 retries for 429/5xx)
|
|
285
|
-
last_error = None
|
|
286
285
|
|
|
287
286
|
# Sanitize surrogates before JSON encoding (prevent UTF-8 encode crash)
|
|
288
287
|
from .utils import sanitize_surrogates
|
|
@@ -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.
|
|
47
|
+
__version__ = "2.4.9"
|
|
48
48
|
|
|
49
49
|
import asyncio
|
|
50
50
|
import logging
|
|
@@ -90,12 +90,12 @@ def _signal_handler(sig, frame):
|
|
|
90
90
|
try:
|
|
91
91
|
get_clawd().shutdown()
|
|
92
92
|
except Exception:
|
|
93
|
-
|
|
93
|
+
logger.debug("Clawd shutdown failed during signal handler", exc_info=True)
|
|
94
94
|
for handler in _cleanup_handlers:
|
|
95
95
|
try:
|
|
96
96
|
handler()
|
|
97
97
|
except Exception:
|
|
98
|
-
|
|
98
|
+
logger.debug("Cleanup handler %s failed", handler, exc_info=True)
|
|
99
99
|
sys.exit(1)
|
|
100
100
|
|
|
101
101
|
|
|
@@ -97,6 +97,9 @@ class Memory:
|
|
|
97
97
|
@classmethod
|
|
98
98
|
def from_frontmatter(cls, raw: str) -> "Memory | None":
|
|
99
99
|
"""Parse a markdown file with YAML frontmatter into a Memory."""
|
|
100
|
+
# Non-greedy match for the frontmatter separator; uses a negative
|
|
101
|
+
# lookahead to ensure we match the FIRST closing --- (avoiding false
|
|
102
|
+
# matches on YAML separators inside code blocks in the body).
|
|
100
103
|
match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)", raw, re.DOTALL)
|
|
101
104
|
if not match:
|
|
102
105
|
return None
|
|
@@ -167,15 +167,19 @@ def wrap_privileged_command(command: str) -> str:
|
|
|
167
167
|
)
|
|
168
168
|
|
|
169
169
|
elif os_family == OSFamily.WINDOWS:
|
|
170
|
-
# Encode command as
|
|
171
|
-
#
|
|
172
|
-
#
|
|
170
|
+
# Encode the entire command as a single base64 PowerShell script to
|
|
171
|
+
# avoid nested-quoting injection through cmd.exe. The script is
|
|
172
|
+
# executed directly by PowerShell without going through cmd.exe at all,
|
|
173
|
+
# which eliminates the shlex.quote / cmd.exe quoting mismatch.
|
|
174
|
+
# We embed the base64 script in a here-string so that special
|
|
175
|
+
# characters ($, `, ", etc.) in the original command are harmless.
|
|
173
176
|
encoded = base64.b64encode(command.encode("utf-16-le")).decode()
|
|
174
|
-
|
|
177
|
+
# Use PowerShell -EncodedCommand directly (no cmd.exe intermediary)
|
|
175
178
|
return (
|
|
176
179
|
'powershell -Command "'
|
|
177
|
-
'Start-Process -Verb RunAs -Wait -FilePath
|
|
178
|
-
f'-ArgumentList {
|
|
180
|
+
'Start-Process -Verb RunAs -Wait -FilePath powershell.exe '
|
|
181
|
+
f'-ArgumentList \\"-NoProfile -EncodedCommand {encoded}\\"'
|
|
182
|
+
'"'
|
|
179
183
|
)
|
|
180
184
|
|
|
181
185
|
return command
|
|
@@ -240,7 +240,7 @@ class PromptTemplate:
|
|
|
240
240
|
import re as _re
|
|
241
241
|
|
|
242
242
|
if_tag = _re.compile(r'\{\%\s*if\s+(.+?)\s*\%\}')
|
|
243
|
-
|
|
243
|
+
_re.compile(r'\{\%\s*endif\s*\%\}')
|
|
244
244
|
any_tag = _re.compile(r'\{\%\s*(?:if\s+.+?|endif)\s*\%\}')
|
|
245
245
|
|
|
246
246
|
def _eval_condition(cond: str) -> bool:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ata-coder"
|
|
7
|
-
version = "2.4.
|
|
7
|
+
version = "2.4.9"
|
|
8
8
|
description = "ATA Coder — AI-powered coding assistant"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -14,7 +14,7 @@ authors = [
|
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
16
|
"click>=8.0",
|
|
17
|
-
"httpx>=0.27.0",
|
|
17
|
+
"httpx>=0.27.0,<1.0",
|
|
18
18
|
"colorama>=0.4.6",
|
|
19
19
|
"python-dotenv>=1.0.0",
|
|
20
20
|
"rich>=13.0.0",
|
|
@@ -81,16 +81,22 @@ exclude = ["build", "dist", ".git", "__pycache__", ".pytest_cache", "web/node_mo
|
|
|
81
81
|
|
|
82
82
|
[tool.ruff.lint]
|
|
83
83
|
select = ["E", "F", "W"]
|
|
84
|
-
ignore = ["E501", "E402", "E701", "E722", "E741", "E731", "
|
|
84
|
+
ignore = ["E501", "E402", "E701", "E722", "E741", "E731", "F821"]
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint.per-file-ignores]
|
|
87
|
+
# repl_ui.py + terminal.py: rich/colorama imports are availability checks
|
|
88
|
+
"repl_ui.py" = ["F401"]
|
|
89
|
+
"terminal.py" = ["F401"]
|
|
90
|
+
# Skill handlers + tests: some imports are for dynamic loading or optional deps
|
|
91
|
+
"skills/*/handler.py" = ["F401"]
|
|
92
|
+
"tests/*.py" = ["F401", "F841"]
|
|
85
93
|
# E501: line too long (enforced by formatter instead)
|
|
86
94
|
# E402: imports after sys.path.insert (intentional in main/server/gui)
|
|
87
95
|
# E701: multiple statements on one line (compact error handling)
|
|
88
96
|
# E722: bare except (intentional for cleanup/shutdown paths)
|
|
89
97
|
# E741: ambiguous variable name (l for loop vars)
|
|
90
98
|
# E731: lambda assignment (trivial inline helpers, not exported)
|
|
91
|
-
# F401: unused import (optional deps guarded by try/except)
|
|
92
99
|
# F821: undefined name (optional deps guarded by try/except)
|
|
93
|
-
# F841: unused variable (test assertions via side effects)
|
|
94
100
|
|
|
95
101
|
[tool.mypy]
|
|
96
102
|
python_version = "3.10"
|
|
@@ -114,7 +114,7 @@ DESTRUCTIVE_PATTERNS = [
|
|
|
114
114
|
(r">\s*/dev/sd", RiskLevel.CRITICAL, "Direct disk write"),
|
|
115
115
|
(r">\s*/dev/nvme", RiskLevel.CRITICAL, "Direct NVMe write"),
|
|
116
116
|
(r"\bshred\s+", RiskLevel.DANGER, "Secure file deletion"),
|
|
117
|
-
(r"\$\(
|
|
117
|
+
(r"\$\([^)]+\)", RiskLevel.CAUTION, "Command substitution detected"),
|
|
118
118
|
(r"`[^`]+`", RiskLevel.CAUTION, "Backtick command substitution detected"),
|
|
119
119
|
(r"chmod\s+777\s+/", RiskLevel.CRITICAL, "World-writable root"),
|
|
120
120
|
(r"chmod\s+-R\s+777\s+/", RiskLevel.CRITICAL, "World-writable root recursive"),
|
|
@@ -22,8 +22,6 @@ Usage:
|
|
|
22
22
|
python main.py --server # From main launcher
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
import asyncio
|
|
26
|
-
import collections
|
|
27
25
|
import json
|
|
28
26
|
import logging
|
|
29
27
|
import os
|
|
@@ -70,12 +68,12 @@ class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
|
|
70
68
|
b"",
|
|
71
69
|
]))
|
|
72
70
|
except Exception:
|
|
73
|
-
|
|
71
|
+
logger.debug("Failed to send 503 response", exc_info=True)
|
|
74
72
|
finally:
|
|
75
73
|
try:
|
|
76
74
|
request.close()
|
|
77
75
|
except Exception:
|
|
78
|
-
|
|
76
|
+
logger.debug("Failed to close rejected request socket", exc_info=True)
|
|
79
77
|
return
|
|
80
78
|
# Atomic read-modify-write under lock (was: bare GIL-only read+write)
|
|
81
79
|
self._active_threads += 1
|
|
@@ -83,6 +81,10 @@ class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
|
|
83
81
|
super().process_request(request, client_address)
|
|
84
82
|
finally:
|
|
85
83
|
with self._thread_lock:
|
|
84
|
+
if self._active_threads <= 0:
|
|
85
|
+
logger.error("Thread counter underflow! _active_threads=%d — "
|
|
86
|
+
"a decrement was matched to no increment, indicating "
|
|
87
|
+
"a bug in process_request pairing.", self._active_threads)
|
|
86
88
|
self._active_threads = max(0, self._active_threads - 1)
|
|
87
89
|
from pathlib import Path
|
|
88
90
|
from typing import Any
|
|
@@ -97,6 +99,7 @@ from .config import AppConfig, get_config
|
|
|
97
99
|
from .tools import TOOL_DEFINITIONS
|
|
98
100
|
from .server_session import SessionStore
|
|
99
101
|
from .server_shell import shell_open, shell_ensure, shell_close, shell_close_all, get_shell_sessions
|
|
102
|
+
from .server_rate_limit import RateLimiter
|
|
100
103
|
from .skills import get_skill_manager
|
|
101
104
|
from .utils import brief_args
|
|
102
105
|
|
|
@@ -108,11 +111,13 @@ logger = logging.getLogger(__name__)
|
|
|
108
111
|
# ══════════════════════════════════════════════════════════════════════# HTTP Request Handler
|
|
109
112
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
110
113
|
|
|
111
|
-
class AgentAPIHandler(BaseHTTPRequestHandler):
|
|
114
|
+
class AgentAPIHandler(RateLimiter, BaseHTTPRequestHandler):
|
|
112
115
|
"""HTTP handler for the ATA Coder API.
|
|
113
116
|
|
|
114
117
|
*config* and *store* are set as class attributes by :func:`create_server`
|
|
115
118
|
before the server starts accepting requests.
|
|
119
|
+
|
|
120
|
+
Rate limiting is inherited from :class:`RateLimiter`.
|
|
116
121
|
"""
|
|
117
122
|
|
|
118
123
|
# Class-level references (set by server factory before accepting requests).
|
|
@@ -122,80 +127,6 @@ class AgentAPIHandler(BaseHTTPRequestHandler):
|
|
|
122
127
|
store: "SessionStore | None" = None
|
|
123
128
|
_ws_lock: threading.Lock = threading.Lock() # protects workspace dir reads/writes
|
|
124
129
|
|
|
125
|
-
# ── Rate limiting (class-level, shared across handler instances) ──────
|
|
126
|
-
_rate_lock: threading.Lock = threading.Lock()
|
|
127
|
-
_rate_buckets: dict[str, "collections.deque[float]"] = {} # ip → deque of timestamps
|
|
128
|
-
_rate_blocked: dict[str, float] = {} # ip → block expiry timestamp
|
|
129
|
-
_RATE_MAX_REQUESTS = 120 # max requests per window
|
|
130
|
-
_RATE_WINDOW_S = 60.0 # sliding window in seconds
|
|
131
|
-
_RATE_BLOCK_S = 300.0 # block duration after exceeding penalty threshold
|
|
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))
|
|
152
|
-
|
|
153
|
-
@classmethod
|
|
154
|
-
def _check_rate_limit(cls, client_ip: str) -> bool:
|
|
155
|
-
"""Sliding-window rate limiter with deque for O(1) cleanup.
|
|
156
|
-
|
|
157
|
-
Returns True if request is allowed.
|
|
158
|
-
"""
|
|
159
|
-
now = time.time()
|
|
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
|
-
|
|
167
|
-
# Check if IP is currently blocked (penalty tier)
|
|
168
|
-
blocked_until = cls._rate_blocked.get(client_ip, 0)
|
|
169
|
-
if now < blocked_until:
|
|
170
|
-
return False
|
|
171
|
-
if now >= blocked_until and client_ip in cls._rate_blocked:
|
|
172
|
-
del cls._rate_blocked[client_ip]
|
|
173
|
-
|
|
174
|
-
dq = cls._rate_buckets.get(client_ip)
|
|
175
|
-
if dq is None:
|
|
176
|
-
dq = collections.deque()
|
|
177
|
-
cls._rate_buckets[client_ip] = dq
|
|
178
|
-
|
|
179
|
-
# Purge expired entries — O(1) per entry via popleft
|
|
180
|
-
cutoff = now - cls._RATE_WINDOW_S
|
|
181
|
-
while dq and dq[0] <= cutoff:
|
|
182
|
-
dq.popleft()
|
|
183
|
-
|
|
184
|
-
# Penalty tier: block if request count exceeds penalty threshold
|
|
185
|
-
penalty_limit = cls._RATE_MAX_REQUESTS * cls._RATE_PENALTY_MULTIPLIER
|
|
186
|
-
if len(dq) > penalty_limit:
|
|
187
|
-
cls._rate_blocked[client_ip] = now + cls._RATE_BLOCK_S
|
|
188
|
-
logger.warning("Rate limit BLOCK: %s for %ds (%d requests in window)",
|
|
189
|
-
client_ip, cls._RATE_BLOCK_S, len(dq))
|
|
190
|
-
return False
|
|
191
|
-
|
|
192
|
-
# Standard rate limit
|
|
193
|
-
if len(dq) >= cls._RATE_MAX_REQUESTS:
|
|
194
|
-
return False
|
|
195
|
-
|
|
196
|
-
dq.append(now)
|
|
197
|
-
return True
|
|
198
|
-
|
|
199
130
|
def __init__(self, *args, **kwargs):
|
|
200
131
|
# Per-instance copies for thread-safe access under ThreadingHTTPServer
|
|
201
132
|
self.config = self.__class__.config
|
|
@@ -465,7 +396,6 @@ class AgentAPIHandler(BaseHTTPRequestHandler):
|
|
|
465
396
|
except Exception:
|
|
466
397
|
logger.debug("Failed to fetch models from API, using cache", exc_info=True)
|
|
467
398
|
# Fallback: cached model list from settings or env
|
|
468
|
-
import os
|
|
469
399
|
from .settings import get_settings
|
|
470
400
|
cached = get_settings().get("env", "ATA_CODER_MODELS_CACHE", default="") or self.config.llm.model
|
|
471
401
|
models = [{"id": m.strip(), "owned_by": ""} for m in cached.split(",") if m.strip()]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Sliding-window rate limiter for the HTTP API server.
|
|
2
|
+
|
|
3
|
+
Extracted from server.py to reduce file size and isolate concerns.
|
|
4
|
+
Used by AgentAPIHandler as a class-level mixin.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import collections
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RateLimiter:
|
|
16
|
+
"""Sliding-window rate limiter with deque for O(1) expiry + penalty tier.
|
|
17
|
+
|
|
18
|
+
Usage (as class-level mixin on a BaseHTTPRequestHandler subclass):
|
|
19
|
+
class MyHandler(RateLimiter, BaseHTTPRequestHandler):
|
|
20
|
+
...
|
|
21
|
+
allowed = self._check_rate_limit(self.client_address[0])
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# ── Configuration ──────────────────────────────────────────────────────
|
|
25
|
+
_rate_lock: threading.Lock = threading.Lock()
|
|
26
|
+
_rate_buckets: dict[str, "collections.deque[float]"] = {} # ip → deque of timestamps
|
|
27
|
+
_rate_blocked: dict[str, float] = {} # ip → block expiry timestamp
|
|
28
|
+
_RATE_MAX_REQUESTS = 120 # max requests per window
|
|
29
|
+
_RATE_WINDOW_S = 60.0 # sliding window in seconds
|
|
30
|
+
_RATE_BLOCK_S = 300.0 # block duration after exceeding penalty threshold
|
|
31
|
+
_RATE_PENALTY_MULTIPLIER = 3 # requests × this = block threshold
|
|
32
|
+
_RATE_CLEANUP_INTERVAL = 1000 # amortized: trigger cleanup every N calls
|
|
33
|
+
_rate_cleanup_counter: int = 0
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def _cleanup_rate_buckets(cls, now: float) -> None:
|
|
37
|
+
"""Remove stale IP entries whose last activity exceeds 2× the window."""
|
|
38
|
+
cutoff = now - cls._RATE_WINDOW_S * 2
|
|
39
|
+
stale = [
|
|
40
|
+
ip for ip, dq in cls._rate_buckets.items()
|
|
41
|
+
if not dq or dq[-1] <= cutoff
|
|
42
|
+
]
|
|
43
|
+
for ip in stale:
|
|
44
|
+
del cls._rate_buckets[ip]
|
|
45
|
+
if stale:
|
|
46
|
+
logger.debug("Rate limiter: pruned %d stale IP bucket(s)", len(stale))
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def _check_rate_limit(cls, client_ip: str) -> bool:
|
|
50
|
+
"""Sliding-window rate limiter. Returns True if request is allowed."""
|
|
51
|
+
now = time.time()
|
|
52
|
+
with cls._rate_lock:
|
|
53
|
+
# Periodic stale-bucket cleanup (amortized)
|
|
54
|
+
cls._rate_cleanup_counter += 1
|
|
55
|
+
if cls._rate_cleanup_counter >= cls._RATE_CLEANUP_INTERVAL:
|
|
56
|
+
cls._rate_cleanup_counter = 0
|
|
57
|
+
cls._cleanup_rate_buckets(now)
|
|
58
|
+
|
|
59
|
+
# Check if IP is currently blocked (penalty tier)
|
|
60
|
+
blocked_until = cls._rate_blocked.get(client_ip, 0)
|
|
61
|
+
if now < blocked_until:
|
|
62
|
+
return False
|
|
63
|
+
if now >= blocked_until and client_ip in cls._rate_blocked:
|
|
64
|
+
del cls._rate_blocked[client_ip]
|
|
65
|
+
|
|
66
|
+
dq = cls._rate_buckets.get(client_ip)
|
|
67
|
+
if dq is None:
|
|
68
|
+
dq = collections.deque()
|
|
69
|
+
cls._rate_buckets[client_ip] = dq
|
|
70
|
+
|
|
71
|
+
# Purge expired entries — O(1) per entry via popleft
|
|
72
|
+
cutoff = now - cls._RATE_WINDOW_S
|
|
73
|
+
while dq and dq[0] <= cutoff:
|
|
74
|
+
dq.popleft()
|
|
75
|
+
|
|
76
|
+
# Penalty tier: block if request count exceeds penalty threshold
|
|
77
|
+
penalty_limit = cls._RATE_MAX_REQUESTS * cls._RATE_PENALTY_MULTIPLIER
|
|
78
|
+
if len(dq) > penalty_limit:
|
|
79
|
+
cls._rate_blocked[client_ip] = now + cls._RATE_BLOCK_S
|
|
80
|
+
logger.warning("Rate limit BLOCK: %s for %ds (%d requests in window)",
|
|
81
|
+
client_ip, cls._RATE_BLOCK_S, len(dq))
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
# Standard rate limit
|
|
85
|
+
if len(dq) >= cls._RATE_MAX_REQUESTS:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
dq.append(now)
|
|
89
|
+
return True
|
|
@@ -541,7 +541,6 @@ def _store_credential(service: str, account: str, secret: str) -> bool:
|
|
|
541
541
|
cred_file.write_text(result.stdout.strip(), encoding="utf-8")
|
|
542
542
|
# Restrictive permissions on the credential file
|
|
543
543
|
try:
|
|
544
|
-
import stat
|
|
545
544
|
cred_file.chmod(0o600)
|
|
546
545
|
except Exception:
|
|
547
546
|
pass
|