{
  "type": "object",
  "properties": {
    "action": {
      "type": "string",
      "description": "Action to perform",
      "enum": [
        "create_world",
        "load_scene",
        "replace_scene_mjcf",
        "patch_scene_mjcf",
        "reset",
        "get_state",
        "destroy",
        "add_robot",
        "remove_robot",
        "list_robots",
        "get_robot_state",
        "list_bodies",
        "add_object",
        "remove_object",
        "move_object",
        "list_objects",
        "add_camera",
        "remove_camera",
        "list_cameras",
        "run_policy",
        "start_policy",
        "stop_policy",
        "list_policies_running",
        "render",
        "render_depth",
        "get_world_point",
        "get_contacts",
        "step",
        "set_gravity",
        "set_timestep",
        "randomize",
        "set_obs_noise",
        "start_recording",
        "stop_recording",
        "get_recording_status",
        "open_viewer",
        "close_viewer",
        "list_urdfs",
        "register_urdf",
        "get_features",
        "replay_episode",
        "eval_policy",
        "save_state",
        "load_state",
        "apply_force",
        "raycast",
        "multi_raycast",
        "get_jacobian",
        "get_energy",
        "get_mass_matrix",
        "inverse_dynamics",
        "get_body_state",
        "set_joint_positions",
        "set_joint_velocities",
        "get_sensor_data",
        "set_body_properties",
        "set_geom_properties",
        "get_contact_forces",
        "forward_kinematics",
        "get_total_mass",
        "get_ground_height",
        "export_xml",
        "render_all",
        "start_cameras_recording",
        "stop_cameras_recording",
        "get_cameras_recording_status",
        "list_benchmarks",
        "register_benchmark_from_file",
        "register_builtin_benchmarks",
        "evaluate_benchmark",
        "attach_bodies",
        "detach_bodies",
        "actuate_robot",
        "zero_dynamics",
        "move_to",
        "set_gripper",
        "rotate_wrist"
      ]
    },
    "scene_path": {
      "type": "string",
      "description": "Path to MJCF/URDF scene file"
    },
    "timestep": {
      "type": "number"
    },
    "gravity": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 3,
      "maxItems": 3
    },
    "ground_plane": {
      "type": "boolean"
    },
    "terrain": {
      "type": "string",
      "description": "create_world: heightfield terrain kind ('rough' = bumps, 'stairs' = step plateaus along +x, 'pyramid' = concentric step plateaus toward the centre, 'slope' = constant-grade incline); omit for a flat ground plane. MuJoCo backend only."
    },
    "difficulty": {
      "type": "number",
      "description": "create_world: terrain curriculum knob - scales the heightfield's peak elevation (1.0 = full height, <1 gentler, >1 harsher) to ramp terrain magnitude across resets without changing the terrain kind. Only meaningful together with 'terrain'; difficulty != 1.0 with no terrain is rejected. MuJoCo backend only."
    },
    "urdf_path": {
      "type": "string",
      "description": "Path to URDF/MJCF file"
    },
    "robot_name": {
      "type": "string"
    },
    "data_config": {
      "type": "string",
      "description": "Robot model registry key for add_robot (e.g. 'so101', 'panda', 'aloha'). Use the BARE name from list_urdfs -- not 'so101_default'/'so101_sim' (those are auto-stripped but the bare key is canonical). Auto-resolves to the MJCF/URDF on disk."
    },
    "keyframe": {
      "type": "string",
      "description": "Optional add_robot spawn pose: the name of a <keyframe> in the robot's source model (e.g. 'home' for panda/ur5e/fr3, 'neutral_pose' for aloha) to spawn in that canonical configuration instead of all-zeros. Restored on reset(). Omit for the default zero pose; an unknown name errors and lists the available keyframes."
    },
    "name": {
      "type": "string",
      "description": "Instance label. For add_object/add_camera it's the object/camera name. For add_robot it's OPTIONAL -- the label you'll use later in run_policy(robot_name=...); if omitted it's auto-derived from data_config and auto-numbered on collision (so101, so101_2, ...)."
    },
    "shape": {
      "type": "string",
      "enum": [
        "box",
        "sphere",
        "cylinder",
        "capsule",
        "ellipsoid",
        "mesh",
        "plane"
      ]
    },
    "position": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 3,
      "maxItems": 3
    },
    "orientation": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 4,
      "maxItems": 4,
      "description": "Orientation as a unit quaternion in [w, x, y, z] order - scalar first, MuJoCo's convention. Applied verbatim to the body quat, so an [x, y, z, w] vector has the same accepted length and is silently used as a different rotation."
    },
    "size": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "description": "Vector extent. The convention differs per action and the two are not interchangeable. add_object: FULL extents in meters per axis (not MuJoCo half-extents), as the shape consumes them - box/ellipsoid [x,y,z], cylinder/capsule [diameter,unused,height], sphere [diameter], plane [x] or [x,y] half-widths; mesh ignores it; at most 3 components; a partial vector (e.g. [0.5] for a box) is rejected, not padded - omit size entirely for the 5 cm default. set_geom_properties: the compiled geom's own MuJoCo geom_size components, exactly as many as its type defines and each > 0 - sphere [radius], capsule/cylinder [radius, half-length], box [three half-extents], ellipsoid [three semi-axes], plane [x half-extent, y half-extent, grid spacing]; mesh/height-field/SDF geoms define none, so size is refused for them. The same value therefore means different things: size=[0.2,0.2,0.2] builds a 20 cm box via add_object and resizes that same box to 40 cm via set_geom_properties, and add_object's [diameter,unused,height] capsule triple is refused there (pass [radius, half-length])."
    },
    "color": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 3,
      "maxItems": 4,
      "description": "Colour in 0..1: [r, g, b] (opaque alpha added) or [r, g, b, a]. Any other component count, including an empty list, is rejected - omit color entirely for the default mid-grey."
    },
    "mass": {
      "type": "number",
      "description": "Body mass in kg for add_object (default 0.1). Dynamic objects need a finite value > 0 and >= MuJoCo's mjMINVAL (1e-15); anything else is rejected, since a non-finite mass turns the whole world's state to nan on the next step. Ignored when is_static=true (MuJoCo derives the mass from the geom density)."
    },
    "is_static": {
      "type": "boolean"
    },
    "mesh_path": {
      "type": "string"
    },
    "material": {
      "type": "object",
      "description": "Optional visual material/texture for add_object (MuJoCo backend). Keys: reflectance|specular|shininess (float 0..1; specular=0,shininess=0 = matte), texture (absolute image path) OR builtin (checker|gradient|flat) with rgb1/rgb2 ([r,g,b]) + texdim (int, default 512), texrepeat ([u,v]). Omit for a flat color-only surface. Invalid texture path, unknown builtin, empty {}, rgb1/rgb2/texdim without builtin, or any key outside the list above fails loudly (never silently ignored).",
      "properties": {
        "reflectance": { "type": "number" },
        "specular": { "type": "number" },
        "shininess": { "type": "number" },
        "texture": { "type": "string" },
        "builtin": { "type": "string", "enum": ["checker", "gradient", "flat"] },
        "rgb1": { "type": "array", "items": { "type": "number" } },
        "rgb2": { "type": "array", "items": { "type": "number" } },
        "texdim": { "type": "integer" },
        "texrepeat": { "type": "array", "items": { "type": "number" } }
      }
    },
    "target": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 3,
      "maxItems": 3,
      "description": "Camera target point"
    },
    "parent_body": {
      "type": "string",
      "description": "Optional body name to mount the camera ON (e.g. 'so101/gripper' for a wrist cam). When set, position/target are in that body's LOCAL frame and the camera tracks the body as it moves. Empty/omitted = world-fixed camera."
    },
    "fov": {
      "type": "number",
      "description": "Camera field of view"
    },
    "width": {
      "type": "integer"
    },
    "height": {
      "type": "integer"
    },
    "policy_provider": {
      "type": "string",
      "description": "Policy provider name (e.g. groot, lerobot_local, lerobot_async, cosmos3, mock). See list_providers()."
    },
    "instruction": {
      "type": "string"
    },
    "duration": {
      "type": "number"
    },
    "action_horizon": {
      "type": "integer"
    },
    "control_frequency": {
      "type": "number"
    },
    "camera_name": {
      "type": "string"
    },
    "pixels": {
      "type": "array",
      "items": {
        "type": "array",
        "items": {
          "type": "number"
        }
      },
      "description": "get_world_point: pixel coordinates [[u, v], ...] to ground to world xyz (u = column from the left, v = row from the top; integer-valued, within the rendered resolution). Pick pixels ON the visible surface of the target (avoid rims/edges/reflections/background) and sample several on the same surface - the returned 'point' is the median over the valid samples; pixels with no depth are dropped (see n_valid). Re-localize after any robot/camera/object motion."
    },
    "n_steps": {
      "type": "integer"
    },
    "output_path": {
      "type": "string",
      "description": "Destination file path. render: PNG still, confined to the render sandbox (~/.strands_robots/renders, or STRANDS_ROBOTS_RENDER_ROOT); set STRANDS_ROBOTS_RENDER_ALLOW_ABS=1 to write outside it. export_xml: MJCF scene, any absolute path accepted. run_policy/start_policy/eval_policy/evaluate_benchmark: rollout MP4. Every sink rejects '..' traversal, a symlinked target, backslash separators and shell metacharacters."
    },
    "fps": {
      "type": "integer",
      "description": "Frames per second: video rate for run_policy record_video, dataset rate for start_recording. Must be a positive whole number."
    },
    "randomize_colors": {
      "type": "boolean"
    },
    "randomize_lighting": {
      "type": "boolean"
    },
    "randomize_physics": {
      "type": "boolean"
    },
    "randomize_positions": {
      "type": "boolean"
    },
    "position_noise": {
      "type": "number"
    },
    "seed": {
      "type": "integer",
      "description": "Random seed"
    },
    "joint_pos_std": {
      "type": "number",
      "description": "set_obs_noise: std (radians) of Gaussian noise added to joint positions in get_observation and get_robot_state. Zero (the default) disables it. Must be finite and non-negative."
    },
    "joint_vel_std": {
      "type": "number",
      "description": "set_obs_noise: std (rad/s) of Gaussian noise added to per-joint velocities. Zero (the default) disables it. Must be finite and non-negative."
    },
    "camera_jitter_px": {
      "type": "number",
      "description": "set_obs_noise: max pixel shift applied to rendered frames, uniform in [-px, px] per axis. Zero (the default) disables it. Must be finite and non-negative."
    },
    "repo_id": {
      "type": "string",
      "description": "HuggingFace dataset repo ID"
    },
    "push_to_hub": {
      "type": "boolean",
      "description": "Auto-push dataset to HuggingFace Hub on stop_recording"
    },
    "vcodec": {
      "type": "string",
      "description": "Video codec for dataset recording (h264, hevc, libsvtav1)"
    },
    "task": {
      "type": "string",
      "description": "Task description for dataset recording"
    },
    "episode": {
      "type": "integer",
      "description": "Episode index for replay_episode"
    },
    "root": {
      "type": "string",
      "description": "Local dataset root directory"
    },
    "speed": {
      "type": "number",
      "description": "Replay speed multiplier (1.0 = original)"
    },
    "n_episodes": {
      "type": "integer",
      "description": "Number of eval episodes"
    },
    "max_steps": {
      "type": "integer",
      "description": "Max steps per eval episode; for move_to / rotate_wrist: max control ticks before returning a not-reached error"
    },
    "tol": {
      "type": "number",
      "description": "move_to / rotate_wrist convergence tolerance: meters for move_to (EE POSITION only - use orientation_tol for the rotation, default 0.01), radians for rotate_wrist (joint angle, default 0.02)"
    },
    "orientation_tol": {
      "type": "number",
      "description": "move_to orientation convergence tolerance in RADIANS (default 0.1, about 5.7 deg). Bounds the rotational half of a pose request, which 'tol' (meters) cannot: when 'orientation' is given, move_to converges to within tol meters AND orientation_tol radians and reports both errors. Only valid alongside 'orientation' - passing it for a position-only move is refused, not ignored"
    },
    "state": {
      "type": "string",
      "enum": ["open", "close"],
      "description": "set_gripper set-point: 'open'/'close' drive the gripper actuator(s) to the set-point-range end declared in the robot registry's gripper metadata when present, else the default convention: 'open' = HIGH end, 'close' = LOW end (SO-100/SO-101 jaw closes toward the low end). The range is the actuator ctrlrange, falling back to the driven joint's limits when the MJCF left the ctrlrange unset"
    },
    "steps": {
      "type": "integer",
      "description": "set_gripper: control ticks to hold the set-point (each tick advances a few physics substeps); default 12"
    },
    "target_yaw": {
      "type": "number",
      "description": "rotate_wrist: wrist-yaw joint set-point in radians (must lie within the joint's range)"
    },
    "success_fn": {
      "type": "string",
      "description": "Success function ('contact')"
    },
    "fast_mode": {
      "type": "boolean",
      "description": "Skip sleep between actions for faster data collection"
    },
    "x": {
      "type": "number",
      "description": "World x coordinate (for get_ground_height)"
    },
    "y": {
      "type": "number",
      "description": "World y coordinate (for get_ground_height)"
    },
    "body_name": {
      "type": "string",
      "description": "Target body name"
    },
    "site_name": {
      "type": "string",
      "description": "Site name for Jacobian"
    },
    "geom_name": {
      "type": "string",
      "description": "Geom name"
    },
    "geom_id": {
      "type": "integer",
      "description": "Geom ID (alternative to geom_name)"
    },
    "friction": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 3,
      "maxItems": 3,
      "description": "set_geom_properties only: the geom's three MuJoCo friction coefficients [sliding, torsional, rolling], each finite and >= 0. Pass all three - a partial vector is rejected, not padded, since there is no value to invent for an omitted coefficient. Takes effect on the next step."
    },
    "force": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 3,
      "maxItems": 3,
      "description": "Force vector [fx, fy, fz] in Newtons"
    },
    "torque_vec": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 3,
      "maxItems": 3,
      "description": "Torque vector [tx, ty, tz] in N\u00b7m"
    },
    "point": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 3,
      "maxItems": 3,
      "description": "Point of force application [x, y, z]"
    },
    "origin": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 3,
      "maxItems": 3,
      "description": "Ray origin [x, y, z]"
    },
    "direction": {
      "type": "array",
      "items": {
        "type": "number"
      },
      "minItems": 3,
      "maxItems": 3,
      "description": "Ray direction [dx, dy, dz]"
    },
    "directions": {
      "type": "array",
      "items": {
        "type": "array",
        "items": {
          "type": "number"
        }
      },
      "description": "Multiple ray directions for multi_raycast - one [dx, dy, dz] per ray, at least one. The batch is all-or-nothing: a direction that cannot be cast refuses it, so a rejected ray is never reported as a miss"
    },
    "exclude_body": {
      "type": "integer",
      "description": "Body ID whose geoms a ray passes through (-1 = exclude nothing); any other value must be a body id the model defines - read one from list_bodies"
    },
    "include_static": {
      "type": "boolean",
      "description": "Include static geoms in raycast"
    },
    "positions": {
      "type": "object",
      "description": "Joint name \u2192 position mapping for set_joint_positions"
    },
    "velocities": {
      "type": "object",
      "description": "Joint name \u2192 velocity mapping for set_joint_velocities"
    },
    "sensor_name": {
      "type": "string",
      "description": "Specific sensor name (or omit for all)"
    },
    "checkpoint_name": {
      "type": "string",
      "description": "Named checkpoint for save_state/load_state"
    },
    "policy_config": {
      "type": "object",
      "description": "Provider-specific config dict forwarded to strands_robots.policies.create_policy. Contents depend on policy_provider. For 'groot': host, port, api_token, observation_mapping, action_mapping. For 'lerobot_local': pretrained_name_or_path, device, trust_remote_code, actions_per_step, use_processor, processor_overrides, observation_mapping, action_mapping. For 'mock': {} is fine.",
      "additionalProperties": true
    },
    "stop_when": {
      "type": "object",
      "description": "run_policy: optional semantic early-return clause -- the rollout ends as soon as the condition holds in the sim, instead of only when the step budget runs out. Same predicate DSL as a benchmark spec's success: clause: a single bool predicate call like {'predicate': 'grasped', 'body': 'cube', 'gripper_prefix': 'so100'} or {'predicate': 'body_above_z', 'body': 'cube', 'z': 0.2}, or a group {'all': [<call>, ...]} / {'any': [<call>, ...]}. Predicates resolve through the closed registry (never eval/exec); an unknown name or a float-valued reward term is rejected up front with the valid list. Checked after every applied action (also on the async-RTC path, so the stop lands within one control step). Composes with an active recording: frames are captured up to the stop. The result json reports stopped_reason ('predicate'|'budget'|'cancelled'; 'error' on failures) and steps_used so you can decide whether to retry.",
      "additionalProperties": true
    },
    "cameras": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "List of camera names. Omit to use every camera in the scene. Used by render_all / start_cameras_recording."
    },
    "output_dir": {
      "type": "string",
      "description": "Directory for start_cameras_recording output. Defaults to a platform-appropriate temp directory (tempfile.gettempdir()/strands_robots/recordings)."
    },
    "xml": {
      "type": "string",
      "description": "Raw MJCF XML string. Used by replace_scene_mjcf."
    },
    "ops": {
      "type": "array",
      "description": "List of structured MJCF edit ops for action='patch_scene_mjcf'. Each op is a dict like {'op': 'add_body', 'parent': 'world', 'name': 'foo', 'pos': [0,0,1]}, {'op': 'add_geom', 'body': 'foo', 'type': 'sphere', 'size': [0.1]}, {'op': 'add_site', 'body': 'foo', 'name': 'tip', 'pos': [0,0,0.2]}, {'op': 'set_body_pos', 'name': 'foo', 'pos': [1,0,1]}, {'op': 'set_body_quat', 'name': 'foo', 'quat': [1,0,0,0]}, or {'op': 'delete_body', 'name': 'foo'}. Each op accepts only the keys it reads (add_body: parent, name, pos, quat; add_geom: body, type, size, rgba, name, pos, quat; add_site: body, name, pos, size, rgba; set_body_pos: name, pos; set_body_quat: name, quat; delete_body: name) - any other key is rejected, since an unread key would leave the op running on its default (a misspelled 'pos' would move the body to the origin). Numeric fields: 'pos' takes exactly 3 finite components, 'quat' 4, 'rgba' 3 (RGB, completed with an opaque alpha) or 4, and 'size' the count its shape consumes. Applied atomically: if any op fails, the whole batch is rolled back.",
      "items": {
        "type": "object"
      }
    },
    "benchmark_name": {
      "type": "string",
      "description": "Name of a registered BenchmarkProtocol (for register_benchmark_from_file / evaluate_benchmark)"
    },
    "spec_path": {
      "type": "string",
      "description": "Path to a declarative benchmark spec file (.json, .yaml, .yml). Used by register_benchmark_from_file."
    },
    "parent": {
      "type": "string",
      "description": "Parent body name for attach_bodies/detach_bodies (robot bodies are namespaced '<robot>/<body>'; call list_bodies to discover names)"
    },
    "child": {
      "type": "string",
      "description": "Child body name to attach/detach (mode='kinematic' requires it to be a dynamic body with a freejoint)"
    },
    "mode": {
      "type": "string",
      "enum": ["weld", "kinematic"],
      "description": "attach_bodies mode: 'weld' adds an equality constraint holding the current relative pose; 'kinematic' teleport-follows the parent every physics step. Neither is a physical grasp."
    },
    "torquescale": {
      "type": "number",
      "description": "attach_bodies weld-only: torque-to-force ratio of the constraint (finite, > 0; default 1.0)"
    },
    "kp": {
      "type": ["number", "object"],
      "description": "actuate_robot position gain: a number applies to every hinge/slide joint; a {joint_name: kp} object actuates only the listed joints. Values must be finite and > 0."
    },
    "damping": {
      "type": "number",
      "description": "actuate_robot per-joint damping floor (finite, >= 0; default 2.0)"
    },
    "armature": {
      "type": "number",
      "description": "actuate_robot per-joint armature floor (finite, >= 0; default 0.01)"
    },
    "gravity_compensation": {
      "type": "boolean",
      "description": "actuate_robot: apply gravcomp=1 to the robot's bodies (default true)"
    },
    "disable_self_collision": {
      "type": "boolean",
      "description": "actuate_robot: zero contype/conaffinity on ALL the robot's geoms so planner-ignored self-contacts cannot block motion (default false)"
    }
  },
  "required": [
    "action"
  ]
}
