feat: cloud 门禁/超时/成本/沙箱收敛为单一判定源

- 新增标准库叶子层 timeouts/gates/quota/sandbox_policy:
  审批 3600 与单轮 600 各有名常量与环境覆盖层,已建项
  expires_at 永不重算;目录/控制/合并状态门禁统一判定,
  目录违规无任何覆盖参数;配额取数与比较唯一来源
  (USD 先于无价、命中均为 >=、无价只计 actual 行);
  /tmp 隔离矩阵与失联清理判定纯函数化。
- 调用点委托:server/worker/automations/auto_memory 的
  配额 SQL 与比较、server/tree/marker 的目录解析、
  marker 的控制门、sandbox 的任务容器名正则、两处 300 秒
  陈旧阈值默认值均收敛,别名与错误文案不变。
- 统一两个 P0 口径分歧:服务端无价计数由全部 usd IS NULL
  行改为 actual-only;显式不限额用户在服务端准入亦不受
  无价上限约束(与 worker 运行时一致)。
- 不新增门禁种类与 record 模式(主线无此实现);不改
  skill_seed/skills/SKILL.md;不改 v1.1 契约与分支保护。
- 新增 test_arch_converge.py 18 项。
This commit is contained in:
host committed 2026-11-04 20:54:50 +00:00
1 parent 17fa493da7
commit facc2a249a
16 files changed
+1070 -301

No files matched your search

+92 -91
View File
@@ -75,6 +75,17 @@ from cloud.cost import CostStore, unpriced_token_cap
from cloud.models import evaluate_capability as _evaluate_capability
from cloud.models import load_routing_policy as _load_routing_policy
from cloud.routing_shadow import evaluate_shadow as _evaluate_shadow
from cloud.gates import merge_state_decision as _merge_state_decision
from cloud.gates import resolve_inside as _gate_resolve_inside
from cloud.quota import decide as _quota_decide
from cloud.quota import default_quota_usd_micros as _quota_default
from cloud.quota import monthly_usage as _quota_monthly_usage
from cloud.quota import quota_for_owner as _quota_for_owner
from cloud.timeouts import STALE_SWEEP_SECONDS as _STALE_SWEEP
from cloud.timeouts import approval_default_seconds as _approval_default
from cloud.timeouts import validate_server_timeout as _validate_timeout
# 演练注:上为 main 侧 models/routing_shadow 导入与本提交
# gates/quota/timeouts 导入的并集,两边调用点各在正文存在。
from cloud.db import connect as _db_connect
from cloud.db import create_database as _create_database
from cloud.db import init_postgres_schema as _init_postgres_schema
@@ -558,19 +569,11 @@ def _task_worktree_for_files(server, task):
def _safe_worktree_path(worktree, rel):
"""worktree 内相对路径的安全解析:空串返回 worktree 本身;
绝对路径、.. 逃逸、符号链接逃逸一律返回 None。realpath 后
再校验 commonpath,目录内的符号链接指回内部时放行(读到
的仍是 worktree 内的内容),指出去时拦住。"""
if not rel:
return worktree
if os.path.isabs(rel):
return None
base = os.path.realpath(worktree)
target = os.path.realpath(os.path.join(base, rel))
if os.path.commonpath([base, target]) != base:
return None
return target
"""worktree 内相对路径的安全解析:判定收敛到 cloud/gates.py
(空串返回 worktree 本身;绝对路径、.. 逃逸、符号链接
逃逸一律返回 None;目录内的符号链接指回内部时放行,
指出去时拦住;目录违规无任何覆盖通道)。"""
return _gate_resolve_inside(worktree, rel)
def _extract_thread_list(result):
@@ -847,7 +850,8 @@ def _build_agent_chain(task, trace_dirs):
[s.get("ts"), done.get("ts") if done else None]),
})
return agents
DEFAULT_SWEEP_THRESHOLD_SECONDS = 300
# 陈旧回收阈值默认值收敛到 cloud/timeouts.py(别名保留)。
DEFAULT_SWEEP_THRESHOLD_SECONDS = _STALE_SWEEP
# 限流默认值:每 IP / 每 API key 每分钟 120 次;环境变量
# CLOUD_RATE_LIMIT_PER_MINUTE / CLOUD_RATE_LIMIT_WINDOW_SECONDS 可调。
# 限流默认内存滑动窗口;Postgres 下改走 DB 共享计数(多 server
@@ -1625,12 +1629,9 @@ def _default_quota_usd_micros():
新号现在建号即写一行默认配额(老用户无配额行不受影响,
行为不变)。环境变量 CLOUD_DEFAULT_QUOTA_USD_MICROS 可
覆盖,默认 10_000_000($10/月);非法值按默认计。
解析收敛到 cloud/quota.py(别名保留)。
"""
try:
return max(0, int(os.environ.get(
"CLOUD_DEFAULT_QUOTA_USD_MICROS", "10000000")))
except (TypeError, ValueError):
return 10000000
return _quota_default()
def _apply_default_quota(conn, user_id):
@@ -2142,7 +2143,7 @@ def request_kill(db_path, task_id, requested_by):
conn.close()
def sweep_stale_tasks(db_path, threshold_seconds=300):
def sweep_stale_tasks(db_path, threshold_seconds=_STALE_SWEEP):
"""把 running 且心跳过期的任务改为 interrupted,返回被扫任务 id 列表。
心跳取 last_heartbeat,无心跳则用 updated_at;早于
@@ -8583,9 +8584,11 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
user_id, task["project_id"], "approve_merge"):
self._send_json(403, {"error": "forbidden"})
return
if task["status"] != "completed":
_merge_ok, _merge_reason = _merge_state_decision(
task["status"])
if not _merge_ok:
self._send_json(409, {
"error": "only completed tasks can be marked merged",
"error": _merge_reason,
"status": task["status"],
})
return
@@ -8698,7 +8701,8 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
权限:项目 create_task(建审批项是写动作,与后续消息
同级)。kind 缺省 elicitation;summary 必填;
timeout_seconds 只接受 60 到 86400 秒整数,缺省 3600,
超出范围回 400。成功记审计 approval_created。"""
超出范围回 400(校验收敛到 cloud/timeouts.py,文案
不变)。成功记审计 approval_created。"""
row = self._fetch_task(task_id)
if row is None:
self._send_json(404, {"error": "task not found"})
@@ -8720,9 +8724,10 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
detail, (dict, list, str)):
self._send_json(400, {"error": "detail must be an object"})
return
timeout = body.get("timeout_seconds", 3600)
if not isinstance(timeout, int) or isinstance(timeout, bool) \
or timeout < 60 or timeout > 86400:
timeout = body.get("timeout_seconds", _approval_default())
try:
timeout = _validate_timeout(timeout)
except ValueError:
self._send_json(
400, {"error": "timeout_seconds must be 60..86400"})
return
@@ -9121,77 +9126,61 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
from datetime import datetime, timezone
return datetime.now(timezone.utc).strftime("%Y-%m")
def _quota_status(self, user_id):
"""返回 (quota_usd_micros|None, used_usd_micros, month)。
P0 安全批后:无配额行不再等于不限额——缺行时按默认
配额处理(与 _default_quota_usd_micros() 同值,只在
返回值里合成,不在这里写行;写行由 _apply_default_quota
/ _backfill_default_quotas 负责)。只有管理员显式把
行写成 NULL 时,quota 才为 None(不限额)。"""
month = self._quota_month()
conn = self._db()
try:
_ensure_quotas_table(conn)
conn.commit()
# cost_records 由 CostStore 懒建:全新库上它还不存在,
# 直接 JOIN 会报 no such table——先实例化一次建表
# (与 _project_cost 同款做法),再跑配额查询。
_cost_store = CostStore(self.server.db_path)
try:
_cost_store._conn.close()
except Exception:
pass
row = conn.execute(
"SELECT quota_usd_micros FROM user_quotas"
" WHERE user_id = ?",
(user_id,)).fetchone()
if row is None:
quota = _default_quota_usd_micros()
else:
quota = row[0]
used = conn.execute(
"SELECT COALESCE(SUM(c.usd_micros), 0)"
" FROM cost_records c JOIN tasks t ON c.task_id = t.id"
" WHERE t.created_by = ? AND c.day LIKE ?",
(user_id, month + "%")).fetchone()[0]
return quota, int(used or 0), month
finally:
conn.close()
def _quota_snapshot(self, user_id, month=None):
"""配额快照:(quota, used_usd, unpriced_actual, month)。
def _quota_unpriced_tokens(self, user_id, month=None):
"""当月无价模型 token 数(usd_micros IS NULL 的成本行,
input + output 之和;cached 已含在 input 里,不重复计)。
合并说明(#86 × #90):两边曾各自定义过一次(#90 的
只要 user_id 且只计 source='actual',#86 的要 month 且
计全部 usd IS NULL 行),自动合并后后一个覆盖前一个,
上报调用点会 TypeError。这里统一成一个方法,以 main
(#86)的口径为准:计全部 usd IS NULL 行(含 estimated
行),上报与上限共用同一口径,不再留两套数字;month
缺省为当月,兼容两边的调用点。与 _quota_status 同款
CostStore 懒建表做法,全新库上 cost_records 不存在时
先建表再查。无配额行时调用方不应以此挡人(见
_check_unpriced_token_cap)。
取数收敛到 cloud/quota.py:配额行(无行按默认值、
显式 NULL 行为不限额)与当月用量(USD 全量 +
无价 actual-only)各只有一处 SQL。CostStore 懒建表
步骤保留(全新库上 cost_records 不存在时先建表再查)。
本函数只读:无行时不在此写行(写行仍由
_apply_default_quota / _backfill_default_quota 负责)。
"""
if month is None:
month = self._quota_month()
conn = self._db()
try:
_ensure_quotas_table(conn)
conn.commit()
_cost_store = CostStore(self.server.db_path)
try:
_cost_store._conn.close()
except Exception:
pass
row = conn.execute(
"SELECT COALESCE(SUM(c.input_tokens + c.output_tokens), 0)"
" FROM cost_records c JOIN tasks t ON c.task_id = t.id"
" WHERE t.created_by = ? AND c.day LIKE ?"
" AND c.usd_micros IS NULL",
(user_id, month + "%")).fetchone()
return int(row[0] if row is not None else 0)
quota = _quota_for_owner(conn, user_id)
used, unpriced = _quota_monthly_usage(conn, user_id, month)
return quota, used, unpriced, month
finally:
conn.close()
def _quota_status(self, user_id):
"""返回 (quota_usd_micros|None, used_usd_micros, month)。
P0 安全批后:无配额行不再等于不限额——缺行时按默认
配额处理(只在返回值里合成,不在这里写行;写行由
_apply_default_quota / _backfill_default_quota 负责)。
只有管理员显式把行写成 NULL 时,quota 才为 None
(不限额)。取数见 _quota_snapshot。"""
quota, used, _unpriced, month = self._quota_snapshot(user_id)
return quota, used, month
def _quota_unpriced_tokens(self, user_id, month=None):
"""当月无价模型 token 数(actual-only,见下)。
收敛说明(改动点,见 cloud/quota.py 模块头):
旧实现计全部 usd IS NULL 的行(含 estimated 估算行),
与 cost.py 的 P1 决议、CostStore 报表、worker、
自动化三处 actual-only 口径不一致;现统一为
actual-only:估算行不是真实花费,绝不触发上限。
input + output 之和;cached 已含在 input 里,不重复计。
month 缺省为当月。与 _quota_status 同款 CostStore
懒建表做法。无配额行时调用方不应以此挡人(见
_check_unpriced_token_cap 与 quota.decide 的
不限额语义)。
"""
_quota, _used, unpriced, _month = self._quota_snapshot(
user_id, month=month)
return unpriced
def _quota_payload(self, user_id, quota, used, month,
unpriced_actual_tokens=None):
payload = {"user_id": user_id, "month": month,
@@ -9205,14 +9194,23 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
def _check_unpriced_token_cap(self, user_id):
"""无价 token 上限准入:超限返回 (cap, used, month),否则 None。
P0 安全批后:无配额行用户同样受限(无价 token 上限与
配额行存在与否无关);USD 配额本身不计量无价模型,所
判定收敛到 cloud/quota.py:无配额行用户按默认配额受限;
显式 NULL(不限额)行用户不受限——与 worker 运行时门
一致(旧实现不论配额行一律拦,是三处不一致之一,
本轨随收敛统一)。USD 配额本身不计量无价模型,所
以这道是补 USD 口径的洞,不是第二套计费。"""
month = self._quota_month()
used = self._quota_unpriced_tokens(user_id, month)
cap = unpriced_token_cap()
if used >= cap:
return cap, used, month
# 取数经 _quota_status / _quota_unpriced_tokens(两者
# 内部共用 _quota_snapshot),比较经 quota.decide。
# _quota_status 在此保留为可 mock 的接缝(503 测试依赖
# 它抛异常时准入点 fail-closed),故 _check_* 不直连
# _quota_snapshot。
quota, _used, month = self._quota_status(user_id)
unpriced = self._quota_unpriced_tokens(user_id, month)
decision = _quota_decide(quota, _used, unpriced,
unpriced_token_cap(), month)
if decision is not None and decision["kind"] == "unpriced":
return (decision["unpriced_token_cap"],
decision["unpriced_tokens_used"], month)
return None
def _check_quota(self, user_id):
@@ -9248,7 +9246,10 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
回合结束才回传),掐断粒度是轮次边界 + 长轮询中的
30 秒检查,不是 token 级。"""
quota, used, month = self._quota_status(user_id)
if quota is not None and used >= quota:
unpriced = self._quota_unpriced_tokens(user_id, month)
decision = _quota_decide(quota, used, unpriced,
unpriced_token_cap(), month)
if decision is not None and decision["kind"] == "usd":
return quota, used, month
return None