Skip to content

Commit 6137d2d

Browse files
committed
fix(security): harden public runtime boundaries
1 parent 2a85760 commit 6137d2d

11 files changed

Lines changed: 210 additions & 112 deletions

File tree

ksadk/builders/container_builder.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,22 @@ def _optimize_kcr_endpoint(self, registry: str) -> str:
199199
"""
200200
from ksadk.configs.settings import check_endpoint_reachable
201201

202+
registry_text = str(registry).strip()
203+
scheme, separator, reference = registry_text.partition("://")
204+
prefix = f"{scheme}://" if separator else ""
205+
target = reference if separator else registry_text
206+
host, slash, remainder = target.partition("/")
207+
normalized_host = host.lower()
208+
202209
# 已经是内网地址
203-
if "hub-vpc" in registry:
210+
if normalized_host == "hub-vpc.kce.ksyun.com":
204211
return registry
205212

206-
# 匹配企业版 KCR: hub.kce.ksyun.com
207-
if "hub.kce.ksyun.com" in registry:
213+
# 仅匹配完整 registry host,避免改写路径或攻击者域名中的同名子串。
214+
if normalized_host == "hub.kce.ksyun.com":
208215
click.echo("🔍 检测 KCR 内网连通性...")
209216
if check_endpoint_reachable("hub-vpc.kce.ksyun.com", port=443, timeout=2.0):
210-
optimized = registry.replace("hub.kce.ksyun.com", "hub-vpc.kce.ksyun.com")
217+
optimized = f"{prefix}hub-vpc.kce.ksyun.com{slash}{remainder}"
211218
click.secho(f" ✅ 优化为内网: {optimized}", fg="green")
212219
return optimized
213220
else:
@@ -522,7 +529,7 @@ def _generate_entrypoint(self, detection_result, package_name: str) -> str:
522529
entry_point="{detection_result.entry_point}",
523530
package_path="/app/{package_name}",
524531
agent_variable="{detection_result.agent_variable}",
525-
runner_class="{getattr(detection_result, 'runner_class', '')}"
532+
runner_class="{getattr(detection_result, "runner_class", "")}"
526533
)
527534
528535
logger.info(f"框架: {{detection_result.name}}")

ksadk/cli/cmd_create.py

Lines changed: 30 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -624,17 +624,17 @@ def _fix_nested_imports(file_path: Path, subdirs: list[str]) -> bool:
624624

625625

626626
def _generate_env_content(global_env: dict) -> str:
627-
"""生成 .env 文件内容"""
628-
api_key = global_env.get("OPENAI_API_KEY", "")
627+
"""Generate project-local configuration without copying credentials.
628+
629+
``init`` may read a global profile to retain non-secret endpoint defaults, but
630+
it must not persist the caller's credentials in a newly created directory.
631+
"""
629632
base_url = global_env.get("OPENAI_BASE_URL", "")
630633
model_name = global_env.get("OPENAI_MODEL_NAME", "")
631-
ks_ak = global_env.get("KSYUN_ACCESS_KEY", "")
632-
ks_sk = global_env.get("KSYUN_SECRET_KEY", "")
633634
ks_region = global_env.get("KSYUN_REGION", "cn-beijing-6")
634-
ks_account = global_env.get("KSYUN_ACCOUNT_ID", "")
635635

636-
env_content = f"""# 模型配置
637-
OPENAI_API_KEY={api_key}
636+
env_content = """# 模型配置
637+
# OPENAI_API_KEY=
638638
"""
639639
if base_url:
640640
env_content += f"OPENAI_BASE_URL={base_url}\n"
@@ -648,19 +648,10 @@ def _generate_env_content(global_env: dict) -> str:
648648
env_content += """
649649
# 金山云配置
650650
"""
651-
if ks_ak:
652-
env_content += f"KSYUN_ACCESS_KEY={ks_ak}\n"
653-
else:
654-
env_content += "# KSYUN_ACCESS_KEY=\n"
655-
if ks_sk:
656-
env_content += f"KSYUN_SECRET_KEY={ks_sk}\n"
657-
else:
658-
env_content += "# KSYUN_SECRET_KEY=\n"
651+
env_content += "# KSYUN_ACCESS_KEY=\n"
652+
env_content += "# KSYUN_SECRET_KEY=\n"
659653
env_content += f"KSYUN_REGION={ks_region}\n"
660-
if ks_account:
661-
env_content += f"KSYUN_ACCOUNT_ID={ks_account}\n"
662-
else:
663-
env_content += "# KSYUN_ACCOUNT_ID=\n"
654+
env_content += "# KSYUN_ACCOUNT_ID=\n"
664655

665656
return env_content
666657

@@ -674,12 +665,11 @@ def _generate_codex_env_content(global_env: dict) -> str:
674665
optional proxy knobs are documented rather than copied from the global profile,
675666
because their upstream key can be different from the model key.
676667
"""
677-
api_key = global_env.get("OPENAI_API_KEY", "")
678668
base_url = global_env.get("OPENAI_BASE_URL", "")
679669
model_name = global_env.get("OPENAI_MODEL_NAME", "")
680670
lines = [
681671
"# 本地 Codex 模型配置(仅用于 ksadk web,不会进入 ManagedRuntime bundle)",
682-
f"OPENAI_API_KEY={api_key}",
672+
"# OPENAI_API_KEY=",
683673
f"OPENAI_BASE_URL={base_url}" if base_url else "# OPENAI_BASE_URL=",
684674
f"OPENAI_MODEL_NAME={model_name}" if model_name else "# OPENAI_MODEL_NAME=glm-5.2",
685675
"",
@@ -1208,7 +1198,7 @@ def _wrap_agent_file(from_agent_path: Path, project_name: str, framework: str, a
12081198
if global_config_exists():
12091199
global_env = get_env_from_global_config()
12101200
if global_env:
1211-
print_info("检测到全局配置,已自动填充凭证")
1201+
print_info("检测到全局配置;项目 .env 仅保留非敏感默认值,凭证不会被复制")
12121202

12131203
# 生成 .env
12141204
(project_path / ".env").write_text(_generate_env_content(global_env), encoding="utf-8-sig")
@@ -1385,7 +1375,7 @@ def _ignore_copytree(_dir: str, names: list[str]):
13851375
if global_config_exists():
13861376
global_env = get_env_from_global_config()
13871377
if global_env:
1388-
print_info("检测到全局配置,已自动填充凭证")
1378+
print_info("检测到全局配置;项目 .env 仅保留非敏感默认值,凭证不会被复制")
13891379

13901380
# 生成 .env
13911381
(project_path / ".env").write_text(_generate_env_content(global_env), encoding="utf-8-sig")
@@ -1808,35 +1798,27 @@ def create(project_name: str, framework: str, from_agent_path: str):
18081798
if global_config_exists():
18091799
global_env = get_env_from_global_config()
18101800
if global_env:
1811-
print_info("检测到全局配置,已自动填充凭证")
1801+
print_info("检测到全局配置;项目 .env 仅保留非敏感默认值,凭证不会被复制")
18121802

18131803
# .env - 生成配置文件
1814-
# 如果有全局配置,使用全局配置的值;否则使用占位符
1815-
# 如果有全局配置,使用全局配置的值;否则使用空字符串
1816-
api_key = global_env.get("OPENAI_API_KEY", "")
1804+
# 仅继承非敏感的 endpoint/model/region 默认值;绝不复制凭证。
18171805
base_url = global_env.get("OPENAI_BASE_URL", "")
18181806
model_name = global_env.get("OPENAI_MODEL_NAME", "")
18191807

1820-
ks_ak = global_env.get("KSYUN_ACCESS_KEY", "")
1821-
ks_sk = global_env.get("KSYUN_SECRET_KEY", "")
18221808
ks_region = global_env.get("KSYUN_REGION", "cn-beijing-6")
1823-
ks_account = global_env.get("KSYUN_ACCOUNT_ID", "")
18241809

18251810
# 构建 .env 内容
18261811
if framework == "openclaw":
18271812
env_content = f"""# ======================
18281813
# OpenClaw 标准部署最小配置
18291814
# ======================
1830-
KSYUN_ACCESS_KEY={ks_ak}
1831-
KSYUN_SECRET_KEY={ks_sk}
1815+
# KSYUN_ACCESS_KEY=
1816+
# KSYUN_SECRET_KEY=
18321817
KSYUN_REGION={ks_region}
18331818
"""
1834-
if ks_account:
1835-
env_content += f"KSYUN_ACCOUNT_ID={ks_account}\n"
1836-
else:
1837-
env_content += "# KSYUN_ACCOUNT_ID=your-account-id\n"
1819+
env_content += "# KSYUN_ACCOUNT_ID=your-account-id\n"
18381820

1839-
env_content += f"\nOPENAI_API_KEY={api_key}\n"
1821+
env_content += "\n# OPENAI_API_KEY=\n"
18401822
if base_url:
18411823
env_content += f"OPENAI_BASE_URL={base_url}\n"
18421824
else:
@@ -1850,18 +1832,13 @@ def create(project_name: str, framework: str, from_agent_path: str):
18501832
env_content = f"""# ======================
18511833
# Hermes 标准部署最小配置
18521834
# ======================
1853-
KSYUN_ACCESS_KEY={ks_ak}
1854-
KSYUN_SECRET_KEY={ks_sk}
1835+
# KSYUN_ACCESS_KEY=
1836+
# KSYUN_SECRET_KEY=
18551837
KSYUN_REGION={ks_region}
18561838
"""
1857-
if ks_account:
1858-
env_content += f"KSYUN_ACCOUNT_ID={ks_account}\n"
1859-
else:
1860-
env_content += "# KSYUN_ACCOUNT_ID=your-account-id\n"
1839+
env_content += "# KSYUN_ACCOUNT_ID=your-account-id\n"
18611840

1862-
env_content += f"""
1863-
OPENAI_API_KEY={api_key}
1864-
"""
1841+
env_content += "\n# OPENAI_API_KEY=\n"
18651842
if base_url:
18661843
env_content += f"OPENAI_BASE_URL={base_url}\n"
18671844
else:
@@ -1908,14 +1885,12 @@ def create(project_name: str, framework: str, from_agent_path: str):
19081885
elif framework == "codex":
19091886
env_content = _generate_codex_env_content(global_env)
19101887
else:
1911-
langfuse_public = global_env.get("LANGFUSE_PUBLIC_KEY", "")
1912-
langfuse_secret = global_env.get("LANGFUSE_SECRET_KEY", "")
19131888
langfuse_url = global_env.get("LANGFUSE_BASE_URL", "")
19141889

1915-
env_content = f"""# ======================
1890+
env_content = """# ======================
19161891
# 模型配置 (必填, 可以从星流平台获取https://ksp.console.ksyun.com/#/apiKey)
19171892
# ======================
1918-
OPENAI_API_KEY={api_key}
1893+
# OPENAI_API_KEY=
19191894
"""
19201895

19211896
# 可选字段:如果有值则启用,否则注释掉
@@ -1934,15 +1909,8 @@ def create(project_name: str, framework: str, from_agent_path: str):
19341909
# 可观测性 (可选)
19351910
# ======================
19361911
"""
1937-
if langfuse_public:
1938-
env_content += f"LANGFUSE_PUBLIC_KEY={langfuse_public}\n"
1939-
else:
1940-
env_content += "# LANGFUSE_PUBLIC_KEY=pk-xxx\n"
1941-
1942-
if langfuse_secret:
1943-
env_content += f"LANGFUSE_SECRET_KEY={langfuse_secret}\n"
1944-
else:
1945-
env_content += "# LANGFUSE_SECRET_KEY=sk-xxx\n"
1912+
env_content += "# LANGFUSE_PUBLIC_KEY=pk-xxx\n"
1913+
env_content += "# LANGFUSE_SECRET_KEY=sk-xxx\n"
19461914

19471915
if langfuse_url:
19481916
env_content += f"LANGFUSE_BASE_URL={langfuse_url}\n"
@@ -1954,22 +1922,12 @@ def create(project_name: str, framework: str, from_agent_path: str):
19541922
# 金山云配置 (可选,需要部署时必选)
19551923
# ======================
19561924
"""
1957-
if ks_ak:
1958-
env_content += f"KSYUN_ACCESS_KEY={ks_ak}\n"
1959-
else:
1960-
env_content += "# KSYUN_ACCESS_KEY=your-api-key-here\n"
1961-
1962-
if ks_sk:
1963-
env_content += f"KSYUN_SECRET_KEY={ks_sk}\n"
1964-
else:
1965-
env_content += "# KSYUN_SECRET_KEY=your-api-secret-here\n"
1925+
env_content += "# KSYUN_ACCESS_KEY=your-api-key-here\n"
1926+
env_content += "# KSYUN_SECRET_KEY=your-api-secret-here\n"
19661927

19671928
env_content += f"KSYUN_REGION={ks_region}\n"
19681929

1969-
if ks_account:
1970-
env_content += f"KSYUN_ACCOUNT_ID={ks_account}\n"
1971-
else:
1972-
env_content += "# KSYUN_ACCOUNT_ID=your-account-id\n"
1930+
env_content += "# KSYUN_ACCOUNT_ID=your-account-id\n"
19731931

19741932
# 使用 utf-8-sig 编码 (带 BOM),确保 Windows 程序正确识别为 UTF-8
19751933
(project_path / ".env").write_text(env_content, encoding="utf-8-sig")

ksadk/model_proxy/cache.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"模型不存在"而非"endpoint 不存在"。本模块:
66
77
- 键 = ``(model, base_url, credential_scope)``;credential_scope 用 key 的稳定指纹
8-
(sha256 前 8 ),避免明文 key 入键/日志,又能区分不同 key。
8+
(BLAKE2b 前 8 字节),避免明文 key 入键/日志,又能区分不同 key。
99
- **明确判定**(supported/unsupported)长缓存(TTL);**不确定**(unknown)不缓存,
1010
下次重探——避免把一次抖动固化成长期误判。
1111
- **singleflight**:并发同键只探一次,其余等结果(用 per-key Future)。
@@ -26,8 +26,10 @@
2626

2727

2828
def _scope(key: str) -> str:
29-
"""key 的稳定指纹(sha256 前 8 位),避免明文入键/日志。"""
30-
return hashlib.sha256(key.encode("utf-8")).hexdigest()[:8]
29+
"""Return a domain-separated, non-secret cache scope for a credential."""
30+
return hashlib.blake2b(
31+
key.encode("utf-8"), digest_size=8, person=b"ksadk-capability"
32+
).hexdigest()
3133

3234

3335
@dataclass

ksadk/model_proxy/demo_webui.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
from __future__ import annotations
1414

1515
import json
16+
import logging
1617
import os
1718

1819
import uvicorn
1920
from fastapi import FastAPI, Request
2021
from fastapi.responses import HTMLResponse, StreamingResponse
2122

23+
logger = logging.getLogger(__name__)
24+
2225
# ruff: noqa: E501 内联 HTML/CSS/JS 演示字面量,长行不拆
2326

2427
_PAGE = """<!doctype html><html lang="zh"><head><meta charset="utf-8">
@@ -83,9 +86,11 @@ async def gen():
8386
if it.get("type") == "agentMessage" and it.get("text"):
8487
yield f"data: {json.dumps({'reply': it['text']}, ensure_ascii=False)}\n\n"
8588
elif m == "error":
86-
yield f"data: {json.dumps({'error': json.dumps(ev.get('params', {}).get('error', {}), ensure_ascii=False)[:200]}, ensure_ascii=False)}\n\n"
89+
logger.warning("Codex demo runtime reported an error")
90+
yield 'data: {"error":"Codex runtime failed; see local logs."}\n\n'
8791
except Exception as e: # noqa: BLE001
88-
yield f"data: {json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n"
92+
logger.warning("Codex demo runtime failed", exc_info=e)
93+
yield 'data: {"error":"Codex runtime failed; see local logs."}\n\n'
8994

9095
return StreamingResponse(gen(), media_type="text/event-stream")
9196

0 commit comments

Comments
 (0)