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
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:
@@ -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,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
|
||||
|
||||
@@ -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()
|
||||
@@ -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"; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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/"
|
||||
|
||||
Reference in New Issue
Block a user