0d986f60bd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
集成服务器托管模型、自然语言移动与有界抓放、内置 LeKiwi URL 导入和双摄像头;同步部署契约与指定域名 iframe 白名单,保留原有物理安全、会话及调用预算防护。 更新 npm 包及锁文件版本、CHANGELOG 与发布文档。提交前 typecheck、120 项定向前端测试和 44 项后端测试通过(3 项可选跳过);真实 v2 云模型抓放仍待单独验收,不包含运行密钥或构建产物。
83 lines
2.9 KiB
Python
83 lines
2.9 KiB
Python
"""Operator-only provisioning. Never package keys, pass them in argv, or print them."""
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
from decision_server.credentials import deepseek_llm, openrouter_jev # noqa: E402
|
|
|
|
REMOTE = r"""
|
|
import json, os, pathlib, re, sys
|
|
values = json.loads(sys.stdin.read(16384))
|
|
assert set(values) == {'Deepseek_API_KEY', 'OPENROUTER_API_KEY'}
|
|
assert all(re.fullmatch(r'[A-Za-z0-9_-]{16,4096}', v) for v in values.values())
|
|
root = pathlib.Path('/opt/cadworld-sim')
|
|
secrets = root / 'secrets'
|
|
secrets.mkdir(mode=0o700, exist_ok=True)
|
|
os.chmod(str(secrets), 0o700)
|
|
path = secrets / 'model-keys.env'
|
|
payload = ''.join(k+'='+v+'\n' for k,v in sorted(values.items())).encode()
|
|
if path.exists():
|
|
if path.is_symlink() or path.read_bytes() != payload:
|
|
raise SystemExit('Existing credentials differ; explicit rotation required')
|
|
else:
|
|
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o400)
|
|
with os.fdopen(fd, 'wb') as stream:
|
|
stream.write(payload)
|
|
os.chown(str(path), 10001, 10001)
|
|
os.chmod(str(path), 0o400)
|
|
budget = root / 'budget'
|
|
budget.mkdir(mode=0o700, exist_ok=True)
|
|
os.chown(str(budget), 10001, 10001)
|
|
os.chmod(str(budget), 0o700)
|
|
print('Server-only credentials provisioned; values not displayed.')
|
|
"""
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--confirm-server-keys", action="store_true")
|
|
args = parser.parse_args()
|
|
if not args.confirm_server_keys:
|
|
parser.error("Explicit authorization required before reading the two .env variables")
|
|
keys = {
|
|
"Deepseek_API_KEY": deepseek_llm(ROOT / ".env").key,
|
|
"OPENROUTER_API_KEY": openrouter_jev(ROOT / ".env").key,
|
|
}
|
|
if not (ROOT / "website-dist/index.html").is_file():
|
|
raise SystemExit("Build website before provisioning")
|
|
for path in (ROOT / "website-dist").rglob("*"):
|
|
if path.is_file():
|
|
content = path.read_bytes()
|
|
if any(value.encode() in content for value in keys.values()):
|
|
raise SystemExit("Credential found in static build; provisioning refused")
|
|
# The remote source contains no values. Only SSH's encrypted stdin carries keys.
|
|
import shlex
|
|
|
|
result = subprocess.run(
|
|
[
|
|
"ssh",
|
|
"-o",
|
|
"BatchMode=yes",
|
|
"-o",
|
|
"StrictHostKeyChecking=yes",
|
|
"-o",
|
|
"UpdateHostKeys=no",
|
|
"root@47.93.31.109",
|
|
"python3 -c " + shlex.quote(REMOTE),
|
|
],
|
|
input=json.dumps(keys).encode(),
|
|
capture_output=True,
|
|
)
|
|
if result.returncode:
|
|
raise SystemExit("Credential provisioning failed; values/logs withheld, no automatic retry")
|
|
print("Static secret scan passed; server-only credentials provisioned (0400, read-only mount).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|