zeroclaw-labs/zeroclaw · error · anyhow::Error

Gateway responded {status}: {detail}

Error message

Gateway responded {status}: {detail}

What it means

A POST to `/admin/sop/approve` or `/admin/sop/deny` came back non-2xx. SOP routes put a typed `outcome` label in the body (e.g. `not_waiting` maps to 404, `rejected_self_approval` to 403); the CLI prefers `outcome` over `error` so the operator sees the precise reason.

Source

Thrown at src/main.rs:6975

        .map_err(|e| anyhow::Error::msg(format!("Failed to connect to gateway: {e}")))?;
    let status = resp.status();
    let out: serde_json::Value = resp.json().await.unwrap_or_default();
    if status.is_success() {
        println!(
            "{}",
            out.get("outcome").and_then(|v| v.as_str()).unwrap_or("ok")
        );
        Ok(())
    } else {
        // Non-2xx bodies from the SOP routes carry the typed `outcome` label
        // (e.g. not_waiting -> 404, rejected_self_approval -> 403), not `error`;
        // prefer it so the operator sees why, falling back to `error`.
        let detail = out
            .get("outcome")
            .and_then(|v| v.as_str())
            .or_else(|| out.get("error").and_then(|v| v.as_str()))
            .unwrap_or("request failed");
        anyhow::bail!("Gateway responded {status}: {detail}");
    }
}

#[cfg(feature = "agent-runtime")]
enum PaircodeAction {
    /// GET the current code; do not mint or revoke anything.
    Show,
    /// Issue a fresh code for an additional client; revoke nothing.
    AddClient,
    /// Revoke every paired token + clear the registry, then issue a code.
    RotateAll,
    /// Revoke a single device's token, then issue a code.
    RotateDevice(String),
}

#[cfg(feature = "agent-runtime")]
impl PaircodeAction {
    /// True when the action mints a new code (POST), false for `Show` (GET).

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Re-list waiting runs with `zeroclaw sop pending` and pass a run_id that is still waiting
  2. Treat `not_waiting` immediately after your own successful approve as idempotent completion, not a failure
  3. For `rejected_self_approval`, have a different approver act per the configured approval policy
  4. For 5xx, check the daemon logs before retrying

Example fix

# before
zeroclaw sop approve run-123   # run already decided elsewhere
# after
zeroclaw sop pending
zeroclaw sop approve <run_id from the still-waiting list>
Defensive patterns

Strategy: try-catch

Validate before calling

# refresh the waiting list right before deciding
zeroclaw sop pending | grep -q "$run_id" || { echo "run $run_id is not waiting"; exit 2; }

Try / catch

if ! zeroclaw sop approve "$run_id" 2>/tmp/sop_err; then
  case "$(cat /tmp/sop_err)" in
    *not_waiting*)             echo "already decided; treating as done" ;;        # idempotent success
    *rejected_self_approval*)  assign_second_approver "$run_id" ;;
    *"Gateway responded 5"*)   retry_after_backoff ;;
    *) cat /tmp/sop_err; exit 1 ;;
  esac
fi

Prevention

When it happens

Trigger: `zeroclaw sop approve <run_id>` for a run that already finished or was decided (`not_waiting`), approving your own run while self-approval is rejected (`rejected_self_approval`), an unknown run_id (404), or a daemon-side 5xx.

Common situations: Two operators racing to decide the same SOP run; scripts retrying an approve that already succeeded; the run being denied or timing out between `sop pending` and `sop approve`.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/c48a5056dd91c58a. Report an issue: GitHub.