tinyhumansai/openhuman · warning

invalid approvalMode '{other}' (expected required|not_requir

Error message

invalid approvalMode '{other}' (expected required|not_required|null)

What it means

CardPatch parsing: args["approvalMode"] is a string but not one of the three accepted literals (todo.rs:258). Matching is case-sensitive: exactly "required", "not_required", or JSON null (null clears the override). Any other spelling — including camelCase variants — is rejected.

Source

Thrown at src/openhuman/agent/tools/todo.rs:258

fn optional_string(args: &serde_json::Value, key: &str) -> Option<String> {
    args.get(key)
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
}

fn patch_from_args(args: &serde_json::Value) -> anyhow::Result<CardPatch> {
    let status: Option<TaskCardStatus> = match args.get("status").and_then(|v| v.as_str()) {
        Some(s) => Some(ops::parse_status(s).map_err(anyhow::Error::msg)?),
        None => None,
    };
    let approval_mode = match args.get("approvalMode") {
        Some(value) if value.is_null() => Some(None),
        Some(value) => match value.as_str() {
            Some("required") => Some(Some(TaskApprovalMode::Required)),
            Some("not_required") => Some(Some(TaskApprovalMode::NotRequired)),
            Some(other) => {
                return Err(anyhow::anyhow!(
                    "invalid approvalMode '{other}' (expected required|not_required|null)"
                ))
            }
            None => {
                return Err(anyhow::anyhow!(
                    "invalid approvalMode type (expected required|not_required|null)"
                ))
            }
        },
        None => None,
    };
    Ok(CardPatch {
        content: None,
        status,
        objective: optional_string(args, "objective"),
        plan: optional_string_array(args, "plan")?,
        assigned_agent: optional_string(args, "assignedAgent"),
        allowed_tools: optional_string_array(args, "allowedTools")?,

View on GitHub (pinned to a221052e0d)

Solutions

  1. Use exactly "required" or "not_required" (lowercase, snake_case), or omit the field to leave it unchanged
  2. Use JSON null to explicitly clear an existing approval-mode override
  3. Map UI/enum names to the wire literals in wrappers before invoking

Example fix

// before
{ "op": "edit", "id": "t1", "approvalMode": "notRequired" }

// after
{ "op": "edit", "id": "t1", "approvalMode": "not_required" }
Defensive patterns

Strategy: type-guard

Validate before calling

const OK = new Set(["required", "not_required"]);
const a = args as Record<string, unknown>;
if ("approvalMode" in a && a.approvalMode !== null
    && (typeof a.approvalMode !== "string" || !OK.has(a.approvalMode))) {
  throw new Error("approvalMode must be 'required' | 'not_required' | null");
}

Type guard

function isApprovalMode(v: unknown): v is "required" | "not_required" | null {
  return v === null || v === "required" || v === "not_required";
}

Try / catch

if e.to_string().contains("invalid approvalMode") {
    return Ok(ToolResult::error("approvalMode accepts exactly 'required', 'not_required', or null (case-sensitive)"));
}

Prevention

When it happens

Trigger: Passing "ApprovalMode", "notRequired", "none", "auto", or "REQUIRED"; model-generated camelCase mirroring the JSON key name instead of the value literals.

Common situations: Models echoing the key casing for the value; wrappers forwarding UI enum names that differ from the wire literals.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/b8e8383e518fc89b. Report an issue: GitHub.