Files
Mujoco_WASM/decision_server/protocol.py
T
chenlin 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
feat: release v1.0.2 LeKiwi 语言控制与网站嵌入
集成服务器托管模型、自然语言移动与有界抓放、内置 LeKiwi URL 导入和双摄像头;同步部署契约与指定域名 iframe 白名单,保留原有物理安全、会话及调用预算防护。

更新 npm 包及锁文件版本、CHANGELOG 与发布文档。提交前 typecheck、120 项定向前端测试和 44 项后端测试通过(3 项可选跳过);真实 v2 云模型抓放仍待单独验收,不包含运行密钥或构建产物。
2026-09-24 15:29:49 +08:00

204 lines
6.6 KiB
Python

"""Strict data-only validation of the same versioned contract used by the browser."""
import json
import math
from copy import deepcopy
from pathlib import Path
SCHEMA = json.loads(
(Path(__file__).resolve().parents[1] / "contracts/lekiwi-agent-v1.schema.json").read_text()
)
VERSION = "lekiwi-agent-v1"
LANGUAGE_VERSION = "lekiwi-language-v2"
LANGUAGE_SCHEMA = json.loads(
(
Path(__file__).resolve().parents[1] / "contracts/lekiwi-language-task-v2.schema.json"
).read_text()
)
def schema_for(version):
if version == VERSION:
return SCHEMA
if version == LANGUAGE_VERSION:
return LANGUAGE_SCHEMA
raise DecisionError("unsupported_version")
PRECONDITIONS = dict(
zip(
SCHEMA["$defs"]["Plan"]["properties"]["steps"]["items"]["properties"]["skill"]["enum"],
[
"scene-ready",
"base-stopped",
"tcp-above",
"aligned",
"dual-contact",
"verified-grasp",
"verified-grasp",
"transported",
"supported",
"released",
"retreat",
],
strict=True,
)
)
SKILLS = list(PRECONDITIONS)
LANGUAGE_PRECONDITIONS = {"stow": "scene-ready", "approach": "arm-safe", **PRECONDITIONS}
LANGUAGE_SKILLS = list(LANGUAGE_PRECONDITIONS)
class DecisionError(Exception):
"""Public error code only: upstream bodies/headers must never become logs or UI errors."""
def __init__(self, code, status=400):
super().__init__(code)
self.code = code
self.status = status
def loads(text):
def pairs(items):
result = {}
for key, value in items:
if key in result:
raise DecisionError("duplicate_json_key")
result[key] = value
return result
def invalid(_):
raise DecisionError("nonfinite_json")
try:
return json.loads(text, object_pairs_hook=pairs, parse_constant=invalid)
except (ValueError, TypeError, RecursionError) as exc:
raise DecisionError("invalid_json") from exc
def check(node, value, schema=SCHEMA):
def fail():
raise DecisionError("contract_mismatch")
if "$ref" in node:
return check(schema["$defs"][node["$ref"].removeprefix("#/$defs/")], value, schema)
if "const" in node and (type(value) is not type(node["const"]) or value != node["const"]):
fail()
if "enum" in node and value not in node["enum"]:
fail()
kind = node.get("type")
if kind in ("number", "integer"):
if type(value) not in (int, float) or not math.isfinite(value):
fail()
if kind == "integer" and value != int(value):
fail()
if not node.get("minimum", -math.inf) <= value <= node.get("maximum", math.inf):
fail()
elif kind == "boolean":
if type(value) is not bool:
fail()
elif kind == "string":
if not isinstance(value, str):
fail()
if not node.get("minLength", 0) <= len(value) <= node.get("maxLength", math.inf):
fail()
elif kind == "array":
if not isinstance(value, list):
fail()
if not node.get("minItems", 0) <= len(value) <= node.get("maxItems", math.inf):
fail()
if node.get("uniqueItems") and len({json.dumps(v) for v in value}) != len(value):
fail()
for item in value:
check(node["items"], item, schema)
elif kind == "object":
if not isinstance(value, dict):
fail()
props = node.get("properties", {})
if set(node.get("required", [])) - value.keys():
fail()
if node.get("additionalProperties") is False and value.keys() - props.keys():
fail()
for key in value.keys() & props.keys():
check(props[key], value[key], schema)
def validate(name, value, version=VERSION):
try:
if len(json.dumps(value, ensure_ascii=False, allow_nan=False)) > 65536:
raise DecisionError("message_too_large", 413)
schema = schema_for(version)
check(schema["$defs"][name], value, schema)
except (ValueError, TypeError, OverflowError, RecursionError) as exc:
raise DecisionError("contract_mismatch") from exc
return deepcopy(value)
def output_schema(name, version=VERSION):
"""Equivalent schema with explicit string types for strict API implementations."""
def expand(node):
result = deepcopy(node)
if "type" not in result and ("const" in result or "enum" in result):
result["type"] = "string" # All enum/const nodes in v1 are strings.
for key, value in result.items():
if isinstance(value, dict):
result[key] = {k: expand(v) if isinstance(v, dict) else v for k, v in value.items()}
if isinstance(result.get("items"), dict):
result["items"] = expand(node["items"])
return result
return expand(schema_for(version)["$defs"][name])
def remaining_skills(value, version=VERSION):
schema_for(version)
skills = LANGUAGE_SKILLS if version == LANGUAGE_VERSION else SKILLS
if (
not isinstance(value, list)
or not value
or value not in [skills[i:] for i in range(len(skills))]
):
raise DecisionError("invalid_remaining_skills")
return value
def validate_plan(value, remaining, version=VERSION):
value = validate("Plan", value, version)
if [step["skill"] for step in value["steps"]] != remaining_skills(remaining, version):
raise DecisionError("invalid_plan_order")
preconditions = LANGUAGE_PRECONDITIONS if version == LANGUAGE_VERSION else PRECONDITIONS
if any(step["precondition"] != preconditions[step["skill"]] for step in value["steps"]):
raise DecisionError("invalid_precondition")
return value
def candidates(value, version=VERSION):
schema_for(version)
skills = LANGUAGE_SKILLS if version == LANGUAGE_VERSION else SKILLS
if (
not isinstance(value, list)
or not 1 <= len(value) <= 4
or any(type(v) is not str or v not in [*skills, "stop"] for v in value)
or len(set(value)) != len(value)
):
raise DecisionError("invalid_candidates")
return value
def validate_jev(value, choices, version=VERSION):
value = validate("JevDecision", value, version)
if value["choice"] not in candidates(choices, version):
raise DecisionError("invalid_choice")
return value
def fields(value, required, optional=()):
if (
not isinstance(value, dict)
or set(required) - value.keys()
or value.keys() - set(required) - set(optional)
):
raise DecisionError("invalid_fields")
return value