P1 security batch: quota execution checks, cost accounting, goal/coordinator semantics, PR channel, orphan signature, control-plane and console hardening

- Docker quota watchdog + fail-closed quota checks, system tasks need real owner
- max_cost_usd actually accumulated, cancel records cost, estimated rows excluded from unpriced cap, cache_write declared non-billed
- goal-resume keeps original verify on breakpoint resume, goal retry falls back to resume, goal_max_rounds capped at 100 at create and execution
- coordinator children inherit parent model/effort, orphans marked interrupted with audit
- PR jobs on docker workers return explicit needs_local_worker instead of silent empty
- worker code signature covers 6 files and is checked before claim, fail-closed
- sub2api lazy provisioning with rotation, backup whitelist + 0600, push SSRF block, repos/workers desensitized, key/project audits and limits, console buttons/quota/status wording, coordinator/automation quota reserve
This commit is contained in:
host committed 2026-10-27 16:27:50 +00:00
1 parent d254240d84
commit 1402dc67fd
20 files changed
+2616 -179

No files matched your search

+101 -8
View File
@@ -441,8 +441,28 @@ def list_gateway_models(origin, api_key, request=None):
return out
def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
"""幂等开通:已有行直接返回 key;否则建号->登录->发 key->授余额->入库。
def gateway_key_health(origin, api_key, request=None):
"""真健康检查:用已有 key 调一次 /v1/models。
返回 True(200,可用)/ False(401/403:key 已失效,或余额
为 0——网关在余额为 0 时鉴权层直接 403,两者都算死 key)/
"unknown"(其它状态或网络异常:网关本身不可用时不据此轮换,
避免把一次抖动当成 key 死亡、反复删号重建)。
"""
req = request or _default_request
try:
st, _data = req("GET", origin + "/v1/models", token=api_key)
except Exception:
return "unknown"
if st == 200:
return True
if st in (401, 403):
return False
return "unknown"
def _create_fresh_gateway_key(conn, user_id, admin=None, request=None):
"""建号->登录->发 key->授余额->入库,返回 (key, sub_id, key_id)。
入库失败时尽力清理刚建的 key 与账号(避免孤儿 key),再抛原错;
授余额失败同样走这条清理(余额为 0 的 key 在网关侧直接 403,
@@ -451,11 +471,9 @@ def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
(同一进程内第二次起免管理员登录,见 shared_admin_session)。
调用方负责 conn 的提交语义:本函数在入库后
commit(开通是独立事务,不跟调用方的业务事务绑在一起)。
调用方须保证调用前本用户无有效行(ensure/rotate 先处理
旧行),否则会主键冲突(并发撞号除外,走轮询复用)。
"""
ensure_gateway_keys_table(conn)
row = get_user_gateway_key(conn, user_id)
if row is not None:
return row["api_key"]
origin = gateway_origin()
if not origin:
raise Sub2apiError("SUB2API_BASE_URL 未配置,无法开通网关账号")
@@ -476,9 +494,16 @@ def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
time.sleep(2)
row = get_user_gateway_key(conn, user_id)
if row is not None:
return row["api_key"]
# 合并修复(P1 rebase 到 d254240):main 的 #92 竞态
# 分支原在单体 ensure 里只回一个 key 字符串;P1
# 抽出 _create_fresh 后契约是 3 元组,4 个调用点
# 都按 3 元组解包,裸字符串会被当成字符序列解包
# 而 ValueError。这里按契约回 3 元组,key_id 未知
# 故为 None(与 validate_or_rotate 的 ok 路一致)。
return (row["api_key"], row["sub2api_user_id"], None)
raise
key_id = None
user_token = None
try:
user_token, refresh = user_login(origin, email, password,
request=req)
@@ -497,7 +522,7 @@ def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
(user_id, sub_id, key, refresh, now, now))
conn.commit()
except Exception:
if key_id is not None:
if key_id is not None and user_token is not None:
try:
user_delete_key(origin, user_token, key_id, request=req)
except Exception:
@@ -507,4 +532,72 @@ def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
except Exception:
pass
raise
return key, sub_id, key_id
def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
"""幂等开通:已有行直接返回 key;否则走 _create_fresh 开通。
只保证“行存在”,不保证 key 仍有效:要校验有效性用
validate_or_rotate_gateway_key(死 key 会触发轮换重开)。
"""
ensure_gateway_keys_table(conn)
row = get_user_gateway_key(conn, user_id)
if row is not None:
return row["api_key"]
key, _sub_id, _key_id = _create_fresh_gateway_key(
conn, user_id, admin=admin, request=request)
return key
def validate_or_rotate_gateway_key(conn, user_id, admin=None, request=None):
"""懒开通/健康检查合一:返回 (status, key, sub_id, key_id)。
status:ok(旧 key 经 /v1/models 实测可用)/ provisioned
(无行,新开通)/ rotated(旧 key 经实测已死:401/403,
含余额为 0 的 403;删旧行后重开,死 key 不再被当成已开通)。
网关本身不可用(健康检查 unknown)时抛 Sub2apiError,调用方
应回 502、保留旧行不轮换;绝不在此时把“待开通”当成可用。
"""
ensure_gateway_keys_table(conn)
row = get_user_gateway_key(conn, user_id)
origin = gateway_origin()
if not origin:
raise Sub2apiError("SUB2API_BASE_URL 未配置,无法开通网关账号")
if row is None or not row.get("api_key"):
key, sub_id, key_id = _create_fresh_gateway_key(
conn, user_id, admin=admin, request=request)
return "provisioned", key, sub_id, key_id
health = gateway_key_health(origin, row["api_key"], request=request)
if health is True:
return "ok", row["api_key"], row.get("sub2api_user_id"), None
if health is False:
conn.execute(
"DELETE FROM user_gateway_keys WHERE user_id = ?",
(user_id,))
conn.commit()
key, sub_id, key_id = _create_fresh_gateway_key(
conn, user_id, admin=admin, request=request)
return "rotated", key, sub_id, key_id
raise Sub2apiError("sub2api 网关健康检查暂不可用,保留现有 key")
def rotate_user_gateway_key(conn, user_id, admin=None, request=None):
"""仅限本人调用的重新开通/轮换:删旧行后重开。
无旧行时直接走开通(与 validate 的 provisioned 等价);返回
(status, key, sub_id, key_id),调用方记审计时只可用 sub_id /
key_id,绝不记 key 明文。旧 key 即使只是“疑似失效”也会被
换掉:轮换是用户显式动作,不以健康检查为前提。
"""
ensure_gateway_keys_table(conn)
row = get_user_gateway_key(conn, user_id)
if row is not None:
conn.execute(
"DELETE FROM user_gateway_keys WHERE user_id = ?",
(user_id,))
conn.commit()
key, sub_id, key_id = _create_fresh_gateway_key(
conn, user_id, admin=admin, request=request)
return ("rotated" if row is not None else "provisioned",
key, sub_id, key_id)