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

proposal {} quarantined: {reason}

Error message

proposal {} quarantined: {reason}

What it means

apply_proposal re-runs scan_candidate over the stored proposal content at apply time (defense in depth beyond the capture-time scan). If the LeakDetector finds credential-like content, the proposal is persisted as Quarantined with the detection reason in status_reason, and the apply bails. The proposal is preserved for inspection but is no longer appliable.

Source

Thrown at crates/zeroclaw-runtime/src/sop/procedural_memory.rs:182

            engine.save_proposal(&proposal)?;
            bail!("proposal {} is stale; target SOP now exists", proposal.id);
        }
        (_, Some(expected)) if current_target_hash.as_ref() != Some(expected) => {
            proposal.status = ProposalStatus::Stale;
            proposal.updated_at = now_iso8601();
            proposal.status_reason = Some("target SOP changed since proposal capture".into());
            engine.save_proposal(&proposal)?;
            bail!("proposal {} is stale; inspect and re-propose", proposal.id);
        }
        _ => {}
    }

    if let Some(reason) = scan_candidate(&proposal.manifest_toml, &proposal.procedure_markdown) {
        proposal.status = ProposalStatus::Quarantined;
        proposal.updated_at = now_iso8601();
        proposal.status_reason = Some(reason.clone());
        engine.save_proposal(&proposal)?;
        bail!("proposal {} quarantined: {reason}", proposal.id);
    }

    validate_candidate(
        &proposal.sop_name,
        &proposal.manifest_toml,
        &proposal.procedure_markdown,
    )?;
    let rollback = write_rollback(&sops_root, &target_dir, &proposal.id)?;
    atomic_write_sop(
        &target_dir,
        &proposal.manifest_toml,
        &proposal.procedure_markdown,
    )?;

    proposal.status = ProposalStatus::Applied;
    proposal.updated_at = now_iso8601();
    proposal.applied_at = Some(proposal.updated_at.clone());
    proposal.applied_by = applied_by;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Load the quarantined proposal and read status_reason to see which pattern matched.
  2. If it is a true positive, rotate the credential and create a new proposal with cleaned content.
  3. If it is a false positive, reword the flagged text and re-propose; never hand-edit the record back to Pending.
  4. Re-apply promptly after capture to shrink the window for rule-skew mismatches.

Example fix

// before: apply assumes pending proposals are always appliable
apply_proposal(&engine, install_root, id, None).await?;

// after: handle quarantine explicitly
match apply_proposal(&engine, install_root, id, None).await {
    Ok(out) => out,
    Err(e) if e.to_string().contains("quarantined") => {
        let p = engine.load_proposal(id)?.context("proposal missing")?;
        tracing::warn!(reason = ?p.status_reason, "proposal quarantined; re-propose cleaned content");
        return Err(e);
    }
    Err(e) => return Err(e),
}
Defensive patterns

Strategy: validation

Validate before calling

// Pre-scan the stored proposal content with the same heuristic before applying.
fn looks_like_credential(text: &str) -> bool {
    for line in text.lines() {
        let l = line.to_ascii_lowercase();
        for key in ["token=", "key=", "secret=", "password="] {
            if let Some(pos) = l.find(key) {
                let value = &line[pos + key.len()..];
                if value.chars().take_while(|c| c.is_ascii_graphic()).count() >= 20 {
                    return true;
                }
            }
        }
    }
    false
}

let p = engine.load_proposal(id)?.context("proposal missing")?;
if looks_like_credential(&format!("{}\n{}", p.manifest_toml, p.procedure_markdown)) {
    anyhow::bail!("clean the proposal content and re-propose");
}
apply_proposal(&engine, install_root, id, None).await?;

Try / catch

match apply_proposal(&engine, install_root, id, None).await {
    Err(e) if e.to_string().contains("quarantined") => {
        // reload the record: status_reason names the matched pattern; re-propose cleaned content
        let p = engine.load_proposal(id)?.context("proposal missing")?;
        tracing::warn!(reason = ?p.status_reason, "quarantined at apply time");
        return Err(e);
    }
    other => other?,
}

Prevention

When it happens

Trigger: Applying a proposal whose manifest/procedure content contains a credential-like pattern that was introduced after capture or that the capture-time scan missed (e.g. detector updates, or content edited into the record).

Common situations: LeakDetector rules tightened between capture and apply; step outputs whose secrets survived scrubbing (long token= values); proposals held in review while their content aged against newer detection rules.

Related errors


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