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
- Load the quarantined proposal and read status_reason to see which pattern matched.
- If it is a true positive, rotate the credential and create a new proposal with cleaned content.
- If it is a false positive, reword the flagged text and re-propose; never hand-edit the record back to Pending.
- 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
- Keep the propose-to-apply window short so detector rule changes cannot strand proposals.
- Never hand-edit a quarantined record back to Pending; re-propose cleaned content.
- Rotate any real credential that matched before handling the proposal further.
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
- proposal rejected: {reason}
- proposal {} is {:?}, not pending
- proposal {} is stale; target SOP now exists
- proposal {} is stale; inspect and re-propose
- only completed SOP runs can be captured
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/d5aa3732ae04e544.
Report an issue: GitHub.