Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-21) — Client-Side Rate Limiting](#whats-new-2026-06-21--client-side-rate-limiting)
- [What's new (2026-06-21) — JSON Web Tokens (JWT)](#whats-new-2026-06-21--json-web-tokens-jwt)
- [What's new (2026-06-21) — License Policy Gate](#whats-new-2026-06-21--license-policy-gate)
- [What's new (2026-06-21) — OpenVEX Vulnerability Triage](#whats-new-2026-06-21--openvex-vulnerability-triage)
Expand Down Expand Up @@ -114,6 +115,12 @@

---

## What's new (2026-06-21) — Client-Side Rate Limiting

Stay under API quotas. Full reference: [`docs/source/Eng/doc/new_features/v62_features_doc.rst`](docs/source/Eng/doc/new_features/v62_features_doc.rst).

- **`TokenBucket` / `SlidingWindowLimiter` / `throttle`** (`AC_rate_limit`, `ac_rate_limit`): `RetryPolicy`/`CircuitBreaker` recover from failures but nothing shaped the *rate* of calls. This adds a token bucket (smooth rate + burst), a sliding-window limiter (Cloudflare's O(1) weighted counter), and a leading-edge throttle decorator. Every limiter takes an injectable `clock` (and `acquire` a `sleep`) so it's fully deterministic in CI with no real delays. `AC_rate_limit` gates an action against a named bucket, returning `{acquired, tokens, wait}`.

## What's new (2026-06-21) — JSON Web Tokens (JWT)

Mint and verify bearer tokens for the APIs you automate. Full reference: [`docs/source/Eng/doc/new_features/v61_features_doc.rst`](docs/source/Eng/doc/new_features/v61_features_doc.rst).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-21) — 客户端速率限制](#本次更新-2026-06-21--客户端速率限制)
- [本次更新 (2026-06-21) — JSON Web Token(JWT)](#本次更新-2026-06-21--json-web-tokenjwt)
- [本次更新 (2026-06-21) — 许可证策略闸门](#本次更新-2026-06-21--许可证策略闸门)
- [本次更新 (2026-06-21) — OpenVEX 漏洞分级](#本次更新-2026-06-21--openvex-漏洞分级)
Expand Down Expand Up @@ -113,6 +114,12 @@

---

## 本次更新 (2026-06-21) — 客户端速率限制

守在 API 配额之内。完整参考:[`docs/source/Zh/doc/new_features/v62_features_doc.rst`](../docs/source/Zh/doc/new_features/v62_features_doc.rst)。

- **`TokenBucket` / `SlidingWindowLimiter` / `throttle`**(`AC_rate_limit`、`ac_rate_limit`):`RetryPolicy`/`CircuitBreaker` 从失败中复原,但没有任何东西塑形调用的*速率*。本功能补上 token bucket(平滑速率 + 突发)、sliding-window 限制器(Cloudflare 的 O(1) 加权计数)以及前缘 throttle 装饰器。每个限制器都接受可注入的 `clock`(`acquire` 另接受 `sleep`),因此在 CI 完全确定、没有真正延迟。`AC_rate_limit` 以具名 bucket 闸控动作,返回 `{acquired, tokens, wait}`。

## 本次更新 (2026-06-21) — JSON Web Token(JWT)

为你自动化的 API 签发与验证 bearer token。完整参考:[`docs/source/Zh/doc/new_features/v61_features_doc.rst`](../docs/source/Zh/doc/new_features/v61_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-21) — 用戶端速率限制](#本次更新-2026-06-21--用戶端速率限制)
- [本次更新 (2026-06-21) — JSON Web Token(JWT)](#本次更新-2026-06-21--json-web-tokenjwt)
- [本次更新 (2026-06-21) — 授權政策閘門](#本次更新-2026-06-21--授權政策閘門)
- [本次更新 (2026-06-21) — OpenVEX 漏洞分級](#本次更新-2026-06-21--openvex-漏洞分級)
Expand Down Expand Up @@ -113,6 +114,12 @@

---

## 本次更新 (2026-06-21) — 用戶端速率限制

守在 API 配額之內。完整參考:[`docs/source/Zh/doc/new_features/v62_features_doc.rst`](../docs/source/Zh/doc/new_features/v62_features_doc.rst)。

- **`TokenBucket` / `SlidingWindowLimiter` / `throttle`**(`AC_rate_limit`、`ac_rate_limit`):`RetryPolicy`/`CircuitBreaker` 從失敗中復原,但沒有任何東西塑形呼叫的*速率*。本功能補上 token bucket(平滑速率 + 突發)、sliding-window 限制器(Cloudflare 的 O(1) 加權計數)以及前緣 throttle 裝飾器。每個限制器都接受可注入的 `clock`(`acquire` 另接受 `sleep`),因此在 CI 完全具決定性、沒有真正延遲。`AC_rate_limit` 以具名 bucket 閘控動作,回傳 `{acquired, tokens, wait}`。

## 本次更新 (2026-06-21) — JSON Web Token(JWT)

為你自動化的 API 簽發與驗證 bearer token。完整參考:[`docs/source/Zh/doc/new_features/v61_features_doc.rst`](../docs/source/Zh/doc/new_features/v61_features_doc.rst)。
Expand Down
53 changes: 53 additions & 0 deletions docs/source/Eng/doc/new_features/v62_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Client-Side Rate Limiting
=========================

The framework had ``RetryPolicy`` / ``CircuitBreaker`` (which *recover* from
failures) and a FIFO ``work_queue``, but nothing to shape the *rate* of calls —
so a flow hammering an external API had no way to stay under a quota. This adds
the two standard limiters plus a leading-edge throttle, all with an injectable
clock so they are deterministic in tests (no real sleeping).

* :class:`TokenBucket` — a smooth rate with burst capacity (lazy refill).
* :class:`SlidingWindowLimiter` — a fixed call budget per rolling window
(Cloudflare's O(1) weighted-counter approximation).
* :func:`throttle` — a decorator that fires a function at most once per interval.

Pure standard library (``threading`` for the lock, ``time`` only as the default
clock); imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import TokenBucket, SlidingWindowLimiter, throttle

# 5 requests/second, bursts up to 10
bucket = TokenBucket(rate=5, capacity=10)
if bucket.try_acquire():
call_api() # non-blocking: skip / queue if False
bucket.acquire() # or block until a token frees up

# at most 100 calls per 60s rolling window
window = SlidingWindowLimiter(limit=100, window_s=60)
if window.try_acquire():
call_api()

@throttle(2.0) # fire at most once every 2 seconds
def on_event(payload):
...

``TokenBucket.try_acquire`` takes tokens if available; ``acquire`` blocks (with
an optional ``timeout``); ``time_until_available`` reports the wait so a
scheduler can pace itself. Every limiter accepts a ``clock=`` (and ``acquire``
a ``sleep=``) so the whole thing is exercised in CI with a fake clock — no real
delays.

Executor command
----------------

``AC_rate_limit`` takes a limiter ``name`` plus ``rate`` / ``capacity`` / ``n``
and tries to take ``n`` tokens from that named token bucket (created on first
use), returning ``{acquired, tokens, wait}`` so a flow can gate or defer an
action. The same operation is exposed as the MCP tool ``ac_rate_limit`` and as a
Script Builder command under **Flow**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v59_features_doc
doc/new_features/v60_features_doc
doc/new_features/v61_features_doc
doc/new_features/v62_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
48 changes: 48 additions & 0 deletions docs/source/Zh/doc/new_features/v62_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
用戶端速率限制
=============

框架過去有 ``RetryPolicy`` / ``CircuitBreaker``(用於從失敗中*復原*)以及 FIFO 的
``work_queue``,卻沒有任何東西能塑形呼叫的*速率* —— 因此猛打外部 API 的流程沒有辦法守在配額
之內。本功能補上兩個標準限制器,外加一個前緣觸發的 throttle,全都以可注入的時鐘實作,因此在測試
中具決定性(不需真的睡眠)。

* :class:`TokenBucket` —— 平滑速率搭配突發容量(惰性回填)。
* :class:`SlidingWindowLimiter` —— 每個滾動視窗固定的呼叫額度(Cloudflare 的 O(1) 加權計數
近似)。
* :func:`throttle` —— 一個讓函式在每個間隔內最多觸發一次的裝飾器。

純標準函式庫(``threading`` 用於鎖,``time`` 僅作為預設時鐘);不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import TokenBucket, SlidingWindowLimiter, throttle

# 每秒 5 個請求,可突發到 10
bucket = TokenBucket(rate=5, capacity=10)
if bucket.try_acquire():
call_api() # 非阻塞:False 時略過 / 排入佇列
bucket.acquire() # 或阻塞直到有 token 釋出

# 每 60 秒滾動視窗最多 100 次呼叫
window = SlidingWindowLimiter(limit=100, window_s=60)
if window.try_acquire():
call_api()

@throttle(2.0) # 每 2 秒最多觸發一次
def on_event(payload):
...

``TokenBucket.try_acquire`` 在有 token 時取用;``acquire`` 會阻塞(可選 ``timeout``);
``time_until_available`` 回報等待時間,讓排程器自行調速。每個限制器都接受 ``clock=``(``acquire``
另接受 ``sleep=``),因此整體可在 CI 以假時鐘演練 —— 沒有真正的延遲。

執行器命令
----------

``AC_rate_limit`` 接受限制器 ``name`` 以及 ``rate`` / ``capacity`` / ``n``,嘗試從該具名 token
bucket(首次使用時建立)取用 ``n`` 個 token,回傳 ``{acquired, tokens, wait}``,讓流程可閘控或
延後某個動作。同一操作亦以 MCP 工具 ``ac_rate_limit`` 以及 Script Builder 中 **Flow** 分類下的
命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v59_features_doc
doc/new_features/v60_features_doc
doc/new_features/v61_features_doc
doc/new_features/v62_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
5 changes: 5 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,10 @@
ClaimsPolicy, ExpiredTokenError, InvalidSignatureError, JwtError,
decode_jwt, encode_jwt,
)
# Client-side rate limiting (token bucket / sliding window / throttle)
from je_auto_control.utils.rate_limit import (
SlidingWindowLimiter, TokenBucket, throttle,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -798,6 +802,7 @@ def start_autocontrol_gui(*args, **kwargs):
"license_findings_to_sarif", "normalize_spdx",
"ClaimsPolicy", "ExpiredTokenError", "InvalidSignatureError", "JwtError",
"decode_jwt", "encode_jwt",
"SlidingWindowLimiter", "TokenBucket", "throttle",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,16 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
),
description="Run 'actions' (JSON view) via a named circuit breaker.",
))
specs.append(CommandSpec(
"AC_rate_limit", "Flow", "Rate Limit (Token Bucket)",
fields=(
FieldSpec("name", FieldType.STRING),
FieldSpec("rate", FieldType.FLOAT, optional=True, default=1.0),
FieldSpec("capacity", FieldType.FLOAT, optional=True, default=1.0),
FieldSpec("n", FieldType.FLOAT, optional=True, default=1.0),
),
description="Try to take 'n' tokens from a named limiter; {acquired, wait}.",
))


def _add_input_macro_specs(specs: List[CommandSpec]) -> None:
Expand Down
15 changes: 15 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2914,6 +2914,20 @@ def _check_licenses(components: Any, allow: Any = None,
return {"violations": violations, "count": len(violations)}


_RATE_LIMITERS: Dict[str, Any] = {}


def _rate_limit(name: str, rate: float = 1.0, capacity: float = 1.0,
n: float = 1.0) -> Dict[str, Any]:
"""Adapter: try to take ``n`` tokens from a named token-bucket limiter."""
from je_auto_control.utils.rate_limit import TokenBucket
bucket = _RATE_LIMITERS.setdefault(
name, TokenBucket(float(rate), float(capacity)))
acquired = bucket.try_acquire(float(n))
return {"acquired": acquired, "tokens": round(bucket.tokens, 4),
"wait": round(bucket.time_until_available(float(n)), 4)}


def _jwt_encode(claims: Any, key: str, alg: str = "HS256") -> Dict[str, Any]:
"""Adapter: sign a compact JWT from claims (a dict or JSON string)."""
import json
Expand Down Expand Up @@ -3724,6 +3738,7 @@ def __init__(self):
"AC_check_licenses": _check_licenses,
"AC_jwt_encode": _jwt_encode,
"AC_jwt_decode": _jwt_decode,
"AC_rate_limit": _rate_limit,
"AC_generate_sop": _generate_sop,
"AC_tween_drag": _tween_drag,
"AC_list_plugins": _list_plugins,
Expand Down
19 changes: 18 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3326,6 +3326,23 @@ def jwt_tools() -> List[MCPTool]:
]


def rate_limit_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_rate_limit",
description=("Try to take 'n' tokens from a named token-bucket "
"limiter ('rate' tokens/sec, 'capacity' burst). "
"Returns {acquired, tokens, wait}."),
input_schema=schema(
{"name": {"type": "string"}, "rate": {"type": "number"},
"capacity": {"type": "number"}, "n": {"type": "number"}},
["name"]),
handler=h.rate_limit,
annotations=READ_ONLY,
),
]


def saga_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -4521,7 +4538,7 @@ def media_assert_tools() -> List[MCPTool]:
locale_tools, voice_tools, coordinate_space_tools, loop_guard_tools,
process_mining_tools, asset_tools, events_tools, notify_channel_tools,
jsonpath_tools, json_schema_tools, vuln_scan_tools, vex_tools,
license_policy_tools, jwt_tools, saga_tools,
license_policy_tools, jwt_tools, rate_limit_tools, saga_tools,
decision_table_tools, locator_repair_tools,
pii_text_tools, sarif_tools,
screen_record_tools,
Expand Down
12 changes: 12 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,18 @@ def jwt_decode(token, key, algorithms=None, audience=None, leeway=0.0):
return {"ok": True, "claims": claims}


_RATE_LIMITERS = {}


def rate_limit(name, rate=1.0, capacity=1.0, n=1.0):
from je_auto_control.utils.rate_limit import TokenBucket
bucket = _RATE_LIMITERS.setdefault(
name, TokenBucket(float(rate), float(capacity)))
acquired = bucket.try_acquire(float(n))
return {"acquired": acquired, "tokens": round(bucket.tokens, 4),
"wait": round(bucket.time_until_available(float(n)), 4)}


def run_saga(steps):
from je_auto_control.utils.saga import run_saga as _run
result = _run(steps)
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/rate_limit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Client-side rate limiting: token bucket, sliding window, throttle."""
from je_auto_control.utils.rate_limit.rate_limit import (
SlidingWindowLimiter, TokenBucket, throttle,
)

__all__ = ["SlidingWindowLimiter", "TokenBucket", "throttle"]
Loading
Loading