Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion registry/schema/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ export function catalogEntryFromPolicy(
media: {
author: authorMedia,
registry,
primary: registry ? "registry" : authorMedia.length > 0 ? "author" : "none",
primary: authorMedia.length > 0 ? "author" : registry ? "registry" : "none",
},
});
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/evidence_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
RELEASE_TAG = "registry-evidence"
FORMAT_VERSION = 2
# The release index container remains format v2 so it can retain historical
# blobs, while report/evidence keys use the v3 semantic identity namespace in
# blobs, while report/evidence keys use the v4 semantic identity namespace in
# simulation/evidence.py.
EVIDENCE_FORMAT = "uduck-evidence-v2"
# Wall-clock fields are useful transiently but must not affect content
Expand Down
4 changes: 2 additions & 2 deletions simulation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ content-addressed Release blob

## ExecutionSpec

An `ExecutionSpec` must state the entry id, exact artifact URL and SHA-256, supported model, runner contract, reviewed recipe, source identity, and resolved manifest. The current runner owns one flat `flat-v1` scene with the official 61-observation/14-action Microduck contract. Recipes state the start preset, scenario, duration, explicit schedule, checks, and provenance.
An `ExecutionSpec` must state the entry id, exact artifact URL and SHA-256, supported model, runner contract, reviewed recipe, source identity, and resolved manifest. The current runner owns one flat `flat-v1` scene with the official 61-observation/14-action Microduck contract. Recipes state the start preset, scenario, duration, explicit schedule, checks, and provenance. A recipe may also declare a source-bound policy handoff; the runner downloads and verifies that artifact, then switches the same physical simulation state at the declared deadline.

Preflight runs before download or inference. It verifies the runner, model, scene, start state, duration, schedule, contract, and HTTPS artifact URL. It rejects malformed or out-of-range commands; it never clips them and never substitutes defaults.

Expand All @@ -45,7 +45,7 @@ Exit code 0 means the diagnostic passed or was not-covered; 1 means measured che

## Evidence identity

`simulation/evidence.py` computes an entry-specific v3 identity from the immutable source, execution-relevant manifest fields, that entry's resolved recipe/status, the executable runner code, the asset lock, dependency pins, and the environment contract. Editorial curation does not enter the digest. The evidence key additionally binds the artifact SHA-256.
`simulation/evidence.py` computes an entry-specific v4 identity from the immutable source, execution-relevant manifest fields, that entry's resolved recipe/status (including any source-bound policy handoff), the executable runner code, the asset lock, dependency pins, and the environment contract. Editorial curation does not enter the digest. The evidence key additionally binds the artifact SHA-256.

The evidence store archives deterministic reports and media as `<blob_sha256>.tar.gz` assets in the `registry-evidence` GitHub Release. Its mutable index maps current entry ids to immutable blobs while retaining historical blobs. Hydration accepts only an exact current entry identity and exact authored artifact hash.

Expand Down
4 changes: 2 additions & 2 deletions simulation/evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
IDENTITY_VERSION = "uduck-execution-inputs-v3"
EVIDENCE_VERSION = "uduck-evidence-v3"
IDENTITY_VERSION = "uduck-execution-inputs-v4"
EVIDENCE_VERSION = "uduck-evidence-v4"
EVIDENCE_ENV = "uduck-evidence-env-v1:ubuntu-24.04:python3.12:mujoco==3.12.0:onnxruntime==1.29.0:numpy==2.5.2:pillow==12.3.0"


Expand Down
57 changes: 57 additions & 0 deletions simulation/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,22 @@
from __future__ import annotations

from dataclasses import dataclass
from math import isfinite
from typing import Any


@dataclass(frozen=True)
class ExecutionHandoff:
"""A source-bound policy selected after the primary policy window ends."""

at_s: float
name: str
artifact_url: str
artifact_sha256: str
action_scale: float
source: dict[str, Any]


@dataclass(frozen=True)
class ExecutionSpec:
"""All inputs needed by the deterministic registry runner.
Expand All @@ -22,6 +35,7 @@ class ExecutionSpec:
recipe: dict[str, Any]
source: dict[str, Any]
manifest: dict[str, Any] | None
handoff: ExecutionHandoff | None = None


def artifact_url(source: dict[str, Any]) -> str:
Expand All @@ -35,6 +49,45 @@ def artifact_url(source: dict[str, Any]) -> str:
return f"https://huggingface.co/{prefix}{repo}/resolve/{revision}/{artifact_path}"


def _handoff_from_recipe(recipe: dict[str, Any]) -> ExecutionHandoff | None:
value = recipe.get("handoff")
if value is None:
return None
if not isinstance(value, dict):
return None
source = value.get("source")
at_s = value.get("at_s")
name = value.get("name")
action_scale = value.get("action_scale")
if (
not isinstance(source, dict)
or not isinstance(at_s, (int, float))
or isinstance(at_s, bool)
or not isfinite(float(at_s))
or float(at_s) <= 0
or not isinstance(name, str)
or not name
or isinstance(action_scale, bool)
or not isinstance(action_scale, (int, float))
or not isfinite(float(action_scale))
or float(action_scale) <= 0
or not isinstance(source.get("artifact_sha256"), str)
):
return None
try:
url = artifact_url(source)
except (KeyError, TypeError):
return None
return ExecutionHandoff(
at_s=float(at_s),
name=name,
artifact_url=url,
artifact_sha256=source["artifact_sha256"],
action_scale=float(action_scale),
source=source,
)


def execution_spec_from_policy(policy: dict[str, Any], resolved: dict[str, Any]) -> ExecutionSpec | None:
"""Build an executable spec from resolved policy data, or return ``None``."""

Expand Down Expand Up @@ -80,6 +133,9 @@ def execution_spec_from_policy(policy: dict[str, Any], resolved: dict[str, Any])
model = recipe.get("model")
if not isinstance(model, str):
return None
handoff = _handoff_from_recipe(recipe)
if recipe.get("handoff") is not None and handoff is None:
return None
return ExecutionSpec(
entry_id=str(policy["id"]),
artifact_url=artifact_url(source),
Expand All @@ -89,4 +145,5 @@ def execution_spec_from_policy(policy: dict[str, Any], resolved: dict[str, Any])
recipe=recipe,
source=source,
manifest=manifest,
handoff=handoff,
)
80 changes: 64 additions & 16 deletions simulation/execution_recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
UPSTREAM_MANIFEST_URL = f"https://github.com/pollen-robotics/microduck/blob/{UPSTREAM_PIN}/docs/policy-manifest.md"
UPSTREAM_CHEATSHEET_URL = f"https://github.com/pollen-robotics/microduck/blob/{UPSTREAM_PIN}/docs/robot/cheatsheet.md"
UPSTREAM_CONTROL_URL = f"https://github.com/pollen-robotics/microduck/blob/{UPSTREAM_PIN}/robotd/src/control.rs"
ROULADE_RECOVERY_TAIL_S = 3.0

POLLEN_POLICY_REPO = "pollen-robotics/microduck-policies"
POLLEN_POLICY_REVISION = "088524a64e2557dc453256b6071dbb9d23888802"
Expand All @@ -42,6 +43,7 @@
POLLEN_MANIFEST_URL = f"https://huggingface.co/{POLLEN_POLICY_REPO}/blob/{POLLEN_POLICY_REVISION}/{POLLEN_MANIFEST_PATH}"
POLLEN_ARTIFACT_SHA256 = {
"alpha_walking.onnx": "e36332d383997d51401897734cd3e79cf5038406feddb18b4d57ecfb141daa6c",
"alpha_stand.onnx": "1569268713e40deea795dd2922dba50d3621e15a872855408b6b1b125b1c094b",
"alpha_ground_pick.onnx": "ffbf5109982ff999b0ba53afe86b9ae731bbec679d67fb7f8ab4c52152c88872",
"roller.onnx": "cf05651d2708a2f9364212e86b866c97a70ace8131c492500105e8f28bf99afd",
"roller_crouch.onnx": "a1a084be240469c76ac9d3fa44d4792f16d4b1da60398b3ecd3cfc5e2244d990",
Expand Down Expand Up @@ -204,6 +206,26 @@ def _official_phase_recipe(manifest: dict[str, Any], source: dict[str, Any], *,
}


def _official_stand_handoff(at_s: float) -> dict[str, Any]:
return {
"at_s": at_s,
"name": "stand",
"source": _pollen_source("alpha_stand.onnx"),
"action_scale": 1.0,
"provenance": _provenance(
"Exact pinned Pollen alpha stand artifact and the pinned robotd skill-expiry selection",
UPSTREAM_CONTROL_URL,
"At the released episodic skill deadline, the zero external twist hands control back to the exact pinned stand policy; this is part of the registry diagnostic execution, not publisher hardware evidence.",
policy_set_revision=POLLEN_POLICY_REVISION,
manifest_sha256=POLLEN_MANIFEST_SHA256,
artifact_path="alpha_stand.onnx",
artifact_sha256=POLLEN_ARTIFACT_SHA256["alpha_stand.onnx"],
action_scale=1.0,
selection="zero external command selects stand after the active skill expires",
),
}


def _official_recipe(manifest: dict[str, Any], source: dict[str, Any]) -> dict[str, Any] | None:
artifact_path = source.get("artifact_path")
if not isinstance(artifact_path, str) or artifact_path not in POLLEN_ARTIFACT_SHA256:
Expand Down Expand Up @@ -257,32 +279,58 @@ def _official_recipe(manifest: dict[str, Any], source: dict[str, Any]) -> dict[s
return None
if manifest.get("command") not in (None, {}):
return None
duration = float(manifest["duration_s"])
command_duration = float(manifest["duration_s"])
recovery_tail = ROULADE_RECOVERY_TAIL_S if artifact_path == "roulade.onnx" else 0.0
capture_duration = command_duration + recovery_tail
checks = ["recover_upright"] if artifact_path == "roulade.onnx" else ["no_fall", "ends_upright"]
return {
handoff = _official_stand_handoff(command_duration) if artifact_path == "roulade.onnx" else None
scope = (
"Registry diagnostic rollout of the exact policy command window followed by the pinned stand-policy handoff under flat-v1; final checks cover the full capture horizon and this does not establish intended-task success or hardware verification."
if handoff is not None
else "Registry diagnostic rollout of the exact policy command window under flat-v1; this does not establish intended-task success or hardware verification."
)
provenance = _provenance(
"Exact per-file Pollen schema-2 manifest and the pinned robotd zero-command skill contract",
UPSTREAM_MANIFEST_URL,
scope,
policy_set_revision=POLLEN_POLICY_REVISION,
manifest_sha256=POLLEN_MANIFEST_SHA256,
artifact_path=artifact_path,
command=[0.0, 0.0, 0.0],
command_semantics="Selecting an ordinary constant episodic skill is the trigger; the upstream runtime feeds the all-zero twist.",
action_scale=1.0,
action_scale_source=UPSTREAM_CONTROL_URL,
chain=bool(manifest.get("chain", False)),
command_duration_s=command_duration,
post_command_settle_s=recovery_tail,
capture_duration_s=capture_duration,
)
if handoff is not None:
provenance.update({
"handoff_artifact_path": "alpha_stand.onnx",
"handoff_artifact_sha256": POLLEN_ARTIFACT_SHA256["alpha_stand.onnx"],
})
recipe = {
"runner": RUNNER,
"model": MODEL,
"scene": SCENE,
"start": deepcopy(START),
"scenario": "oneshot_zero",
"duration_s": duration,
# duration_s remains the runner's full rollout horizon. The
# explicit fields keep the upstream activation window distinct
# from the registry-owned recovery/evaluation tail.
"duration_s": capture_duration,
"command_duration_s": command_duration,
"post_command_settle_s": recovery_tail,
"capture_duration_s": capture_duration,
"checks": checks,
"action_scale": 1.0,
"chain": bool(manifest.get("chain", False)),
"provenance": _provenance(
"Exact per-file Pollen schema-2 manifest and the pinned robotd zero-command skill contract",
UPSTREAM_MANIFEST_URL,
"Registry diagnostic rollout of the exact policy window under flat-v1; this does not establish intended-task success or hardware verification.",
policy_set_revision=POLLEN_POLICY_REVISION,
manifest_sha256=POLLEN_MANIFEST_SHA256,
artifact_path=artifact_path,
command=[0.0, 0.0, 0.0],
command_semantics="Selecting an ordinary constant episodic skill is the trigger; the upstream runtime feeds the all-zero twist.",
action_scale=1.0,
action_scale_source=UPSTREAM_CONTROL_URL,
chain=bool(manifest.get("chain", False)),
),
"provenance": provenance,
}
if handoff is not None:
recipe["handoff"] = handoff
return recipe

if artifact_path == "alpha_sitstand.onnx":
command = manifest.get("command")
Expand Down
52 changes: 48 additions & 4 deletions simulation/microduck_sim/preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,50 @@ def preflight_execution(spec: "ExecutionSpec") -> PreflightResult:
if duration_value is None or not 0 < duration_value <= 30:
errors.append("execution duration_s must be finite, positive, and at most 30 seconds")

capture_duration = recipe.get("capture_duration_s", duration)
capture_value = float(capture_duration) if _finite(capture_duration) else None
if capture_value is None or not 0 < capture_value <= 30:
errors.append("execution capture_duration_s must be finite, positive, and at most 30 seconds")
elif duration_value is not None and abs(capture_value - duration_value) > 1e-9:
errors.append("execution capture_duration_s must equal execution duration_s")

command_duration = recipe.get("command_duration_s", duration)
command_value = float(command_duration) if _finite(command_duration) else None
if command_value is None or command_value <= 0:
errors.append("execution command_duration_s must be finite and positive")

settle = recipe.get("post_command_settle_s", 0.0)
settle_value = float(settle) if _finite(settle) else None
if settle_value is None or settle_value < 0:
errors.append("execution post_command_settle_s must be finite and non-negative")
if command_value is not None and settle_value is not None and capture_value is not None and abs(command_value + settle_value - capture_value) > 1e-9:
errors.append("execution command duration plus settle tail must equal capture duration")
has_settle_tail = command_value is not None and settle_value is not None and settle_value > 0
schedule_value = command_value if has_settle_tail else duration_value
schedule_name = "command_duration_s" if has_settle_tail else "duration_s"

handoff = getattr(spec, "handoff", None)
if recipe.get("handoff") is not None and handoff is None:
errors.append("execution recipe handoff could not be assembled into the ExecutionSpec")
if handoff is not None:
if not _finite(handoff.at_s) or not 0 < handoff.at_s < (capture_value or 0):
errors.append("execution handoff at_s must be inside the capture horizon")
if command_value is not None and abs(handoff.at_s - command_value) > 1e-9:
errors.append("execution handoff at_s must equal command_duration_s")
if not isinstance(handoff.name, str) or not handoff.name:
errors.append("execution handoff name must be non-empty")
if not _finite(handoff.action_scale) or handoff.action_scale <= 0:
errors.append("execution handoff action_scale must be finite and positive")
if not isinstance(handoff.artifact_sha256, str) or len(handoff.artifact_sha256) != 64 or any(char not in "0123456789abcdef" for char in handoff.artifact_sha256):
errors.append("execution handoff artifact SHA-256 is invalid")
if not isinstance(handoff.artifact_url, str) or not handoff.artifact_url.startswith("https://"):
errors.append("execution handoff artifact_url must be an HTTPS URL")
if (
not isinstance(handoff.source, dict)
or handoff.source.get("artifact_sha256") != handoff.artifact_sha256
):
errors.append("execution handoff source hash does not match the handoff artifact hash")

segments = recipe.get("segments")
if scenario == "velocity":
if not isinstance(segments, list) or not segments:
Expand All @@ -109,8 +153,8 @@ def preflight_execution(spec: "ExecutionSpec") -> PreflightResult:
else:
total += float(segment_duration)
errors.extend(_velocity_errors(segment.get("vx"), segment.get("vy"), segment.get("wz"), prefix))
if duration_value is not None and abs(total - duration_value) > 1e-9:
errors.append(f"execution segments cover {total:g}s but execution duration_s={duration_value:g}s")
if schedule_value is not None and abs(total - schedule_value) > 1e-9:
errors.append(f"execution segments cover {total:g}s but execution {schedule_name}={schedule_value:g}s")
elif scenario == "command_schedule":
if not isinstance(segments, list) or not segments:
errors.append("execution segments are required for the command_schedule scenario")
Expand All @@ -133,8 +177,8 @@ def preflight_execution(spec: "ExecutionSpec") -> PreflightResult:
for axis, value in enumerate(command):
if not _finite(value) or value < -3 or value > 3:
errors.append(f"{prefix}.command[{axis}] must be finite and in [-3, 3]")
if duration_value is not None and abs(total - duration_value) > 1e-9:
errors.append(f"execution segments cover {total:g}s but execution duration_s={duration_value:g}s")
if schedule_value is not None and abs(total - schedule_value) > 1e-9:
errors.append(f"execution segments cover {total:g}s but execution {schedule_name}={schedule_value:g}s")
elif "segments" in recipe:
errors.append("execution segments are only valid with velocity and command_schedule scenarios")

Expand Down
Loading
Loading