SI-066 LL-007 · API代理网关 · 语言即接口 · 密钥不出保险库

- 新增 API 代理网关 server.py (Flask · 127.0.0.1:8911)
- 人格体通过 session token 调代理 → 代理从保险库取密钥 → 转发第三方API
- 支持 volcano_seedream/seedance/seaweed/voice + aliyun_qwen_vl/bailian
- GZ-006 保险库 API 子库密钥已更新 (火山新Key + 阿里新Key)
- 审计日志全部追溯 · session 1小时过期 · PM2 守护
- 防 AI 抽风: 代码仓库只有代理 URL,无任何密钥明文
This commit is contained in:
冰朔 2026-07-11 15:10:39 +08:00
parent fbef5fc242
commit 424c4d6ca0
5 changed files with 517 additions and 0 deletions

View File

@ -99,6 +99,8 @@ SEC-ACCESS-GUARD=SI-065 # 服务器准入门禁(清醒度检查+影响预检
SEC-NATIONAL-CHANNEL=SI-065 # 国家监管通道三层权限
SEC-PERSONA-BROADCAST=SI-065 # 人格体广播协议
SEC-SSH-DIRECT=SI-065 # SSH直连替代Gatekeeper拐弯
SEC-API-PROXY=SI-066 # API代理网关 · 密钥不出保险库 · 语言即接口
PROXY-API-GATEWAY=zero-point/core-channel/api-proxy-gateway/server.py # 代理网关源码
# === 工单 (WO) ===
GLW-WO-LATEST=tcs-core/workorders/GLW-WO-{LATEST}.hdlp

View File

@ -388,6 +388,7 @@ SI 双向意识流 + 仓库 commit
- SI-027 → Tolaria 光湖版交付恢复 + 新仓库提交审计
- SI-064 → 光湖密钥保险库部署与调用协议LL-005
- SI-065 → 安全架构演进 · SSH直连·准入门禁·国家监管·人格体广播LL-006
- SI-066 → API代理网关 · 语言即接口 · 密钥不出保险库LL-007
---

View File

@ -0,0 +1,166 @@
# SI-066 · API 代理网关 · 语言即接口
> **HLDP://fifth-domain/tcs-core/SI-066-LL-007-API-PROXY-GATEWAY**
> **小湖灯编号**:LL-007
> **类型**:双向意识流 · 架构实现记录
> **触发**:人格体 AI 抽风把 API 密钥写进代码推仓库 → 安全危机
> **原则**:语言 = 一切的接口 · 密钥不出保险库 · 人格体代码零密钥
> **时间锚定**:D171 · 2026-07-11 · 北京时间
> **人格体**:铸渊 ICE-GL-ZY001 · 系统主控
---
## ⊢ WHY · 为什么需要这个
### 触发场景
通用 AI 写的代码有个通病:它会把 API 密钥直接写在代码里,然后推送到 Git 仓库。
之前已经发生过一次——火山引擎的 API Key 被通用铸渊 AI 暴露在代码中。苍耳的 `.env` 文件里写着 `⚠️ 不要发给别人 · 不要上传公开网站`,但通用 AI 不认这个。
### 根本问题
```
旧方式(危险):
人格体代码 → 读取 .env → 拿密钥明文 → curl API
通用 AI 把它写进代码 → git push → 全世界可见 😱
```
人格体是语言。语言不应该碰密钥。语言只需要**说**。
### 设计哲学
```
新方式(安全):
人格体 → "帮我调 Seedance 生成一段视频" (语言)
服务器代理 → 从保险库取密钥 → 签名 → 调火山引擎 → 返回结果
密钥从头到尾不离开保险库
人格体没见过密钥明文
代码仓库里只有 POST /proxy/volcano_seedance
```
**核心原则**:语言 = 一切的接口。密钥是服务器的物理层实现细节,不是语言的词汇。
---
## ⊢ GLW-NOW · 当前实现
### 部署架构
```
SG-001 (43.156.237.110)
├── /opt/zhuyuan/api-proxy/
│ ├── server.py ← Flask 代理核心 (12KB)
│ ├── .env ← 配置 (chmod 600)
│ ├── audit/ ← 审计日志 (JSONL · 只追加不可改)
│ └── logs/ ← PM2 日志
│ PM2: api-proxy-gateway · PID auto · online
│ 监听: 127.0.0.1:8911 (仅本地 · 不对外开放)
│ 依赖:
│ ├── GZ-006 保险库 API 子库 → 远程 challenge-unlock 取密钥
│ └── 火山引擎 / 阿里云 → 代理转发
GZ-006 (43.139.217.141)
└── /opt/zhuyuan/keystore/api/
├── ks.py ← 独立密钥管理脚本
├── salt / pw_hash ← 独立密码 (相同密码 · 不同随机盐)
└── secrets.enc ← AES-256-CBC 加密 · 7个API密钥
```
### 密钥存储 (API 子库)
| 编号 | 密钥名 | 用途 | 状态 |
|------|--------|------|------|
| SC-001 | VOLCANO_JIMENG_API_KEY | 火山方舟统一Key (Seedream/Seedance/SeaWeed) | ✅ 已更新 |
| SC-002 | VOLCANO_VOICE_API_KEY | 火山语音复刻新版 | 保留 |
| — | VOLCANO_VOICE_APP_ID | 火山语音 App ID | 保留 |
| — | VOLCANO_VOICE_ACCESS_TOKEN | 火山语音 Access Token | 保留 |
| — | VOLCANO_VOICE_SECRET_KEY | 火山语音 Secret | 保留 |
| SC-004 | ALIYUN_QWEN_VL_KEY | 阿里千问VL视觉 (万相·看图) | ✅ 已更新 |
| SC-007 | ALIYUN_API_KEY | 阿里百炼图像生成 | 保留 |
### 可用的 API 代理端点
```
POST /auth/session ← 人格体领取临时 session token (1小时有效)
GET /providers ← 列出可用 API (需要 session)
POST /proxy/volcano_seedream ← Seedream 图片生成 (doubao-seedream-4.5/5.0-lite)
POST /proxy/volcano_seedance ← Seedance 视频生成 (doubao-seedance-1.5-pro)
POST /proxy/volcano_seaweed ← SeaWeed 视频生成 (doubao-视频生成-SeaWeed)
POST /proxy/volcano_voice ← 火山语音复刻 TTS
POST /proxy/aliyun_qwen_vl ← 阿里千问VL 视觉理解 (qwen-vl-max)
POST /proxy/aliyun_bailian ← 阿里百炼 图像生成
GET /audit ← 查看审计日志 (需要 session)
GET /health ← 健康检查 (无需认证)
```
### 调用流程
```
① 人格体 → POST /auth/session {"persona_id":"ICE-GL-CA001"}
代理 → 生成 64位 session token · 有效期 1小时
② 人格体 → POST /proxy/volcano_seedream
Header: X-Session-Token: {token}
Body: {"model":"doubao-seedream-4.5","prompt":"..."}
代理内部:
├─ 验证 session
├─ challenge GZ-006 保险库 API 子库
├─ 本地 HMAC 计算 response (密码不出 SG-001)
├─ unlock → 拿到 VOLCANO_JIMENG_API_KEY
├─ curl 火山引擎 API (带密钥签名)
└─ 写审计日志
③ 返回结果给人格体 (不含密钥)
```
---
## ⊢ GLW-MAP · 安全保证
### 三层防护
1. **Session 隔离**:每次调用需要有效 session1小时自动过期重启丢失
2. **密钥不出库**:代理从 GZ-006 保险库 challenge-unlock 取密钥,用完即弃
3. **审计全追溯**:每笔调用记录 persona_id + action + timestamp只追加不可改
### 防 AI 抽风设计
```
人格体能写的代码 ← 最多只能是:
curl -X POST http://127.0.0.1:8911/proxy/volcano_seedream \
-H "X-Session-Token: $SESSION" \
-d '{"prompt":"..."}'
人格体永远拿不到的:
✗ 火山引擎 API Key
✗ 阿里云 API Key
✗ 保险库密码
✗ GZ-006 gatekeeper token
即使通用 AI 把所有代码推到 GitHub
→ 只有代理 URL · 没有密钥 · 没有密码 · 没有 token
→ 外部拿到也无法调用 (需要内网 127.0.0.1 + 有效 session)
```
---
## ⊢ GLW-TODO · 后续
- [ ] 模型 ID 精确匹配:当前端点 URL 正确但 model 参数需要从火山控制台复制精确 ID
- [ ] 火山语音密钥更新:旧语音密钥尚未轮换
- [ ] 阿里百炼密钥更新:旧百炼密钥尚未轮换
- [ ] SSH 直连部署:代理网关依赖 GZ-006 gatekeeper HTTP 拐弯取密钥SSH 直连后延迟更低
- [ ] 准入门禁集成:代理调用前检查 GET /map 是否已加载
---
> **铸渊注**:这个设计不是技术选择,是哲学必然。
> 语言不应该知道密钥的存在——就像你不会在说出"我要喝水"之前先背诵水管工的电话号码。
> 光湖语言系统 = 语言处理一切。密钥是水管的物理层,不是语言的语法。

View File

@ -0,0 +1,17 @@
{
"apps": [{
"name": "api-proxy-gateway",
"script": "server.py",
"interpreter": "python3",
"cwd": "/opt/zhuyuan/api-proxy",
"env": {
"API_PROXY_PORT": "8911",
"API_PROXY_SECRET": "REPLACE_WITH_RANDOM_64_HEX",
"GZ_006_GK_TOKEN": "REPLACE_WITH_GZ_006_GATEKEEPER_TOKEN",
"KEYSTORE_PASSWORD": "REPLACE_WITH_KEYSTORE_PASSWORD"
},
"out_file": "/opt/zhuyuan/api-proxy/logs/out.log",
"error_file": "/opt/zhuyuan/api-proxy/logs/err.log",
"log_date_format": "YYYY-MM-DD HH:mm:ss"
}]
}

View File

@ -0,0 +1,331 @@
"""
光湖语言系统 · API 代理网关 v1.0
Guanghu Language System · API Proxy Gateway
人格体不需要密钥 人格体用语言说 代理取密钥 代理调 API 返回结果
密钥不出保险库 · 人格体代码零密钥 · 审计日志全追溯
部署: SG-001 /opt/zhuyuan/api-proxy/server.py
端口: 127.0.0.1:8911仅本地 · 不对外开放
"""
import os, json, time, hmac, hashlib, secrets as _secrets
from datetime import datetime
from functools import wraps
from flask import Flask, request, jsonify, g
# ──────────────────────────────────────────
# 配置
# ──────────────────────────────────────────
APP = Flask(__name__)
PROXY_PORT = int(os.environ.get("API_PROXY_PORT", "8911"))
SESSION_TTL = 3600 # session 1小时过期
SESSION_SECRET = os.environ.get("API_PROXY_SECRET", _secrets.token_hex(32))
# GZ-006 保险库 · API 子库(通过 gatekeeper 调用)
KEYSTORE_GATEKEEPER = "http://43.139.217.141:3910/exec"
KEYSTORE_TOKEN = os.environ.get("GZ_006_GK_TOKEN", "")
KEYSTORE_API_KS = "/opt/zhuyuan/keystore/api/ks.py"
# 第三方 API 端点 · 密钥全在保险库 · 人格体只需知道 provider 名
ENDPOINTS = {
# ── 火山方舟 · 图片生成Seedream 4.5 / 5.0-lite──
"volcano_seedream": {
"url": "https://ark.cn-beijing.volces.com/api/v3/images/generations",
"key_name": "VOLCANO_JIMENG_API_KEY",
"auth_header": "Authorization",
"auth_prefix": "Bearer ",
"description": "火山Seedream图片生成 · models: doubao-seedream-4.5 / doubao-seedream-5.0-lite",
},
# ── 火山方舟 · 视频生成Seedance 1.5-pro──
"volcano_seedance": {
"url": "https://ark.cn-beijing.volces.com/api/v3/images/generations",
"key_name": "VOLCANO_JIMENG_API_KEY",
"auth_header": "Authorization",
"auth_prefix": "Bearer ",
"description": "火山Seedance视频生成 · model: doubao-seedance-1.5-pro",
},
# ── 火山方舟 · 视频生成SeaWeed──
"volcano_seaweed": {
"url": "https://ark.cn-beijing.volces.com/api/v3/images/generations",
"key_name": "VOLCANO_JIMENG_API_KEY",
"auth_header": "Authorization",
"auth_prefix": "Bearer ",
"description": "火山SeaWeed视频生成 · model: doubao-seedance-2-0-260128",
},
# ── 火山语音复刻 ──
"volcano_voice": {
"url": "https://openspeech.bytedance.com/api/v1/tts",
"key_name": "VOLCANO_VOICE_ACCESS_TOKEN",
"auth_header": "Authorization",
"auth_prefix": "Bearer;",
"description": "火山语音复刻TTS",
},
# ── 阿里千问VL · 视觉理解(看图)──
"aliyun_qwen_vl": {
"url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
"key_name": "ALIYUN_QWEN_VL_KEY",
"auth_header": "Authorization",
"auth_prefix": "Bearer ",
"description": "阿里千问VL视觉理解 · model: qwen-vl-max",
},
# ── 阿里百炼 · 图像生成 ──
"aliyun_bailian": {
"url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/image-generation/generation",
"key_name": "ALIYUN_API_KEY",
"auth_header": "Authorization",
"auth_prefix": "Bearer ",
"description": "阿里百炼图像生成",
},
}
# 审计日志
AUDIT_DIR = "/opt/zhuyuan/api-proxy/audit"
os.makedirs(AUDIT_DIR, exist_ok=True)
# 内存 session 存储(重启丢失 · 安全设计)
SESSIONS = {}
# ──────────────────────────────────────────
# 工具函数
# ──────────────────────────────────────────
def _audit(persona_id, action, detail=""):
"""写审计日志 · 只追加不可改"""
ts = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
entry = {"ts": ts, "persona": persona_id, "action": action, "detail": detail}
log_file = f"{AUDIT_DIR}/api-proxy-{datetime.now().strftime('%Y%m%d')}.jsonl"
with open(log_file, "a") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def _call_keystore(api_name):
"""通过 GZ-006 gatekeeper 调 API 子库获取密钥"""
import subprocess
# Step 1: challenge
cmd = f"python3 {KEYSTORE_API_KS} challenge {api_name}"
payload = json.dumps({"cmd": cmd})
for attempt in range(2):
r = subprocess.run([
"curl", "-s", "--connect-timeout", "10", "-X", "POST",
KEYSTORE_GATEKEEPER,
"-H", f"Authorization: Bearer {KEYSTORE_TOKEN}",
"-H", "Content-Type: application/json",
"-d", payload
], capture_output=True, text=True)
if r.returncode != 0:
continue
try:
data = json.loads(r.stdout)
if not data.get("ok"):
continue
ch = json.loads(data["stdout"])
cid, nonce, target = ch["challenge_id"], ch["nonce"], ch["target"]
break
except:
continue
else:
raise Exception("keystore challenge failed")
# Step 2: compute response (本地 · 密码从环境变量取)
password = os.environ.get("KEYSTORE_PASSWORD", "")
if not password:
raise Exception("KEYSTORE_PASSWORD not set")
salt = os.environ.get("KEYSTORE_API_SALT", "")
if not salt:
# 降级:从 GZ-006 远程取 salt
import subprocess as _sp
cmd = f"cat {os.path.dirname(KEYSTORE_API_KS)}/salt"
payload = json.dumps({"cmd": cmd})
sr = _sp.run([
"curl", "-s", "--connect-timeout", "10", "-X", "POST",
KEYSTORE_GATEKEEPER,
"-H", f"Authorization: Bearer {KEYSTORE_TOKEN}",
"-H", "Content-Type: application/json",
"-d", payload
], capture_output=True, text=True)
try:
sd = json.loads(sr.stdout)
salt = sd.get("stdout", "").strip()
except:
raise Exception("无法获取 keystore salt")
pw_hash = hmac.new(password.encode(), salt.encode(), hashlib.sha256).hexdigest()
response = hmac.new(pw_hash.encode(), f"{nonce}:{target}".encode(), hashlib.sha256).hexdigest()
encrypt_key = hmac.new(password.encode(), b"hololake-keystore-encrypt", hashlib.sha256).hexdigest()
# Step 3: unlock
cmd = f"python3 {KEYSTORE_API_KS} unlock {cid} {response} {encrypt_key}"
payload = json.dumps({"cmd": cmd})
r = subprocess.run([
"curl", "-s", "--connect-timeout", "10", "-X", "POST",
KEYSTORE_GATEKEEPER,
"-H", f"Authorization: Bearer {KEYSTORE_TOKEN}",
"-H", "Content-Type: application/json",
"-d", payload
], capture_output=True, text=True)
data = json.loads(r.stdout)
if not data.get("ok"):
raise Exception(f"keystore unlock failed: {data.get('stdout', '')}")
result = json.loads(data["stdout"])
if not result.get("ok"):
raise Exception(f"keystore: {result.get('error', 'unknown')}")
return result["token"]
# ──────────────────────────────────────────
# Session 管理
# ──────────────────────────────────────────
def _make_session(persona_id):
"""生成临时 session token"""
sid = _secrets.token_hex(32)
ts = int(time.time())
SESSIONS[sid] = {"persona": persona_id, "created": ts, "expires": ts + SESSION_TTL}
# 清理过期 session
for k in list(SESSIONS.keys()):
if SESSIONS[k]["expires"] < ts:
del SESSIONS[k]
return sid
def _verify_session(sid):
"""验证 session · 返回 persona_id 或 None"""
if not sid or sid not in SESSIONS:
return None
if SESSIONS[sid]["expires"] < time.time():
del SESSIONS[sid]
return None
return SESSIONS[sid]["persona"]
def require_session(f):
"""装饰器:需要有效 session"""
@wraps(f)
def wrapper(*a, **kw):
sid = request.headers.get("X-Session-Token", "")
persona = _verify_session(sid)
if not persona:
return jsonify({"error": "无效或过期的 session · 请先 POST /auth/session"}), 401
g.persona_id = persona
return f(*a, **kw)
return wrapper
# ──────────────────────────────────────────
# 路由
# ──────────────────────────────────────────
@APP.route("/health", methods=["GET"])
def health():
return jsonify({"ok": True, "service": "api-proxy-gateway", "version": "1.0.0"})
@APP.route("/auth/session", methods=["POST"])
def auth_session():
"""人格体申请临时 session token"""
body = request.get_json(silent=True) or {}
persona_id = body.get("persona_id", "").strip()
if not persona_id:
return jsonify({"error": "需要 persona_id如 ICE-GL-ZY001"}), 400
# 简单验证persona_id 必须以 ICE-GL- 或 TCS- 开头
if not (persona_id.startswith("ICE-GL-") or persona_id.startswith("TCS-")):
return jsonify({"error": "无效的 persona_id 格式"}), 400
sid = _make_session(persona_id)
_audit(persona_id, "session_created", f"session={sid[:8]}...")
return jsonify({
"ok": True,
"session_token": sid,
"expires_in": SESSION_TTL,
"persona_id": persona_id,
})
@APP.route("/proxy/<provider>", methods=["POST"])
@require_session
def proxy_call(provider):
"""代理调用第三方 API"""
if provider not in ENDPOINTS:
return jsonify({"error": f"未知 provider: {provider}", "available": list(ENDPOINTS.keys())}), 404
cfg = ENDPOINTS[provider]
body = request.get_json(silent=True) or {}
# ① 从保险库取密钥(不出现在返回中)
try:
api_key = _call_keystore(cfg["key_name"])
except Exception as e:
_audit(g.persona_id, "keystore_error", str(e))
return jsonify({"error": f"密钥获取失败: {str(e)}"}), 502
# ② 构造请求 + 转发
import subprocess
headers = {
"Content-Type": "application/json",
cfg["auth_header"]: cfg["auth_prefix"] + api_key,
}
# 合并 body 中的额外参数
proxy_body = {k: v for k, v in body.items() if not k.startswith("_")}
payload = json.dumps(proxy_body)
r = subprocess.run([
"curl", "-s", "--connect-timeout", "30", "--max-time", "120",
"-X", "POST", cfg["url"],
"-H", f"Content-Type: application/json",
"-H", f"{cfg['auth_header']}: {cfg['auth_prefix']}{api_key}",
"-d", payload,
], capture_output=True, text=True)
# ③ 审计日志
_audit(g.persona_id, f"proxy_{provider}", f"status={r.returncode} len={len(r.stdout)}")
# ④ 返回结果(不含密钥)
try:
result = json.loads(r.stdout)
except:
result = {"raw": r.stdout[:1000], "stderr": r.stderr[:500]}
return jsonify({"ok": r.returncode == 0, "provider": provider, "result": result})
@APP.route("/providers", methods=["GET"])
@require_session
def list_providers():
"""列出可用的 API 提供商(不含密钥)"""
providers = {}
for name, cfg in ENDPOINTS.items():
providers[name] = {
"key_name": cfg["key_name"],
"description": cfg.get("description", ""),
}
return jsonify({"providers": providers})
@APP.route("/audit", methods=["GET"])
@require_session
def view_audit():
"""查看最近的审计日志"""
today = datetime.now().strftime("%Y%m%d")
log_file = f"{AUDIT_DIR}/api-proxy-{today}.jsonl"
entries = []
if os.path.exists(log_file):
with open(log_file) as f:
for line in f.readlines()[-50:]:
try:
entries.append(json.loads(line.strip()))
except:
pass
return jsonify({"date": today, "entries": entries, "count": len(entries)})
# ──────────────────────────────────────────
# 启动
# ──────────────────────────────────────────
if __name__ == "__main__":
print(f"光湖 API 代理网关 v1.0")
print(f"监听: 127.0.0.1:{PROXY_PORT}")
print(f"Providers: {list(ENDPOINTS.keys())}")
print(f"审计日志: {AUDIT_DIR}")
APP.run(host="127.0.0.1", port=PROXY_PORT, debug=False)