feat: release v1.0.2 LeKiwi 语言控制与网站嵌入
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 云模型抓放仍待单独验收,不包含运行密钥或构建产物。
This commit is contained in:
2026-09-24 15:29:49 +08:00
parent f3a8a38acd
commit 0d986f60bd
93 changed files with 5166 additions and 1094 deletions
+2 -1
View File
@@ -8,6 +8,7 @@ RUN pip install --no-index --find-links=/opt/wheels --require-hashes -r /opt/req
COPY vendor/codex /usr/local/bin/codex
RUN chmod 755 /usr/local/bin/codex && codex --version && useradd --uid 10001 --no-create-home --shell /usr/sbin/nologin cadworld
COPY decision_server/ /app/decision_server/
COPY contracts/lekiwi-agent-v1.schema.json /app/contracts/lekiwi-agent-v1.schema.json
# prepare_bundle.py includes only the three explicitly approved runtime contracts.
COPY contracts/ /app/contracts/
USER 10001:10001
ENTRYPOINT ["python", "-m", "decision_server"]
+9
View File
@@ -9,6 +9,15 @@ services:
- 0.0.0.0
- --trusted-proxy
- 172.30.88.1
- --website-key-file
- /run/secrets/model-keys.env
- --website-budget-file
- /var/lib/cadworld/budget.sqlite
- --max-inference
- '2'
volumes:
- /opt/cadworld-sim/secrets/model-keys.env:/run/secrets/model-keys.env:ro
- /opt/cadworld-sim/budget:/var/lib/cadworld:rw
ports:
- '127.0.0.1:8768:8768'
restart: unless-stopped
+82
View File
@@ -0,0 +1,82 @@
"""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()
+3 -2
View File
@@ -27,8 +27,9 @@ server {
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy no-referrer always;
add_header X-Frame-Options DENY always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'none'" always;
# CSP allowlist supports the approved sibling-origin iframe; X-Frame-Options
# DENY/SAMEORIGIN would block it in legacy clients (ALLOW-FROM is obsolete).
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'self' https://cadworld.robotquan.com; form-action 'none'" always;
# Hash assets cache; all headers remain inherited (no nested add_header).
set $cadworld_cache "no-cache";
if ($uri ~ ^/assets/) { set $cadworld_cache "public, max-age=31536000, immutable"; }
+6 -4
View File
@@ -35,10 +35,12 @@ def main():
and source.suffix in (".py", ".json", ".txt", ".md")
):
copy(source, output / "app" / relative)
copy(
ROOT / "contracts/lekiwi-agent-v1.schema.json",
output / "app/contracts/lekiwi-agent-v1.schema.json",
)
for name in (
"lekiwi-agent-v1.schema.json",
"lekiwi-language-task-v2.schema.json",
"lekiwi-language-scene-v2.json",
):
copy(ROOT / "contracts" / name, output / "app/contracts" / name)
for source in (ROOT / "deploy/cadworld").iterdir():
if source.is_file():
copy(source, output / "deploy" / source.name)
+5
View File
@@ -24,6 +24,11 @@ PY
old=''
[[ ! -f $root/current.txt ]] || read -r old < "$root/current.txt"
docker build --network none --pull=false -t "cadworld-decision:$release" "$stage/app"
# Fail before replacing the live service if a runtime contract/module was omitted.
# No server keys, network, volumes or inference are available to this check.
docker run --rm --network none --read-only --cap-drop ALL \
--security-opt no-new-privileges --entrypoint python "cadworld-decision:$release" \
-c 'import decision_server.web_server; from decision_server.language_tasks import resolve_target; assert resolve_target("B", [])[1] == "table"; print("Runtime imports and v2 contracts verified")'
# No current traffic is changed before the build and static manifest succeed.
mkdir -p "$site/releases/$release"
cp -a "$stage/static/." "$site/releases/$release/"