mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* fix(gateway): enforce authz on stateless runs * fix(gateway): guard scheduled run creation
67 lines
2.6 KiB
Python
67 lines
2.6 KiB
Python
"""Static authorization contract for HTTP routes that can create Agent runs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import re
|
|
from pathlib import Path
|
|
|
|
ROUTERS_DIR = Path(__file__).resolve().parent.parent / "app" / "gateway" / "routers"
|
|
|
|
# These scheduled-task mutations create or re-enable work that the background
|
|
# scheduler later launches through the normal Gateway run lifecycle.
|
|
SCHEDULED_RUN_ENABLING_HANDLERS = {
|
|
"create_scheduled_task",
|
|
"update_scheduled_task",
|
|
"resume_scheduled_task",
|
|
}
|
|
|
|
_ROUTE_DECORATOR_RE = re.compile(r"router\.(get|post|delete|put|patch)")
|
|
|
|
|
|
def _is_route_handler(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
return any(
|
|
isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Attribute) and isinstance(decorator.func.value, ast.Name) and _ROUTE_DECORATOR_RE.fullmatch(f"{decorator.func.value.id}.{decorator.func.attr}")
|
|
for decorator in node.decorator_list
|
|
)
|
|
|
|
|
|
def _calls_run_launcher(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
for child in ast.walk(node):
|
|
if not isinstance(child, ast.Call):
|
|
continue
|
|
if isinstance(child.func, ast.Name) and child.func.id == "start_run":
|
|
return True
|
|
if isinstance(child.func, ast.Attribute) and child.func.attr == "dispatch_task":
|
|
return True
|
|
return False
|
|
|
|
|
|
def _requires_runs_create(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
for decorator in node.decorator_list:
|
|
if not isinstance(decorator, ast.Call):
|
|
continue
|
|
if not isinstance(decorator.func, ast.Name) or decorator.func.id != "require_permission":
|
|
continue
|
|
if len(decorator.args) < 2:
|
|
continue
|
|
resource, action = decorator.args[:2]
|
|
if isinstance(resource, ast.Constant) and resource.value == "runs" and isinstance(action, ast.Constant) and action.value == "create":
|
|
return True
|
|
return False
|
|
|
|
|
|
def test_every_run_creation_route_requires_runs_create():
|
|
violations = []
|
|
for path in sorted(ROUTERS_DIR.glob("*.py")):
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) or not _is_route_handler(node):
|
|
continue
|
|
if node.name not in SCHEDULED_RUN_ENABLING_HANDLERS and not _calls_run_launcher(node):
|
|
continue
|
|
if not _requires_runs_create(node):
|
|
violations.append(f"{path.name}:{node.name}")
|
|
|
|
assert not violations, "run-creating routes without runs:create:\n" + "\n".join(violations)
|