{"record":{"id":"d749de358d7c9c56","repo":"zeroclaw-labs/zeroclaw","slug":"proposal-rejected-reason","errorCode":null,"errorMessage":"proposal rejected: {reason}","messagePattern":"proposal rejected: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/procedural_memory.rs","lineNumber":47,"sourceCode":"    pub target_dir: PathBuf,\n}\n\npub fn create_proposal(engine: &SopEngine, draft: ProposalDraft) -> Result<ProposalRecord> {\n    let sop_name = require_nonempty(\"sop_name\", &draft.sop_name)?;\n    let procedure_markdown = require_nonempty(\"procedure_markdown\", &draft.procedure_markdown)?;\n    let description = require_nonempty(\"description\", &draft.description)?;\n    let existing = engine.get_sop(sop_name);\n    let kind = if existing.is_some() {\n        ProposalKind::Update\n    } else {\n        ProposalKind::Create\n    };\n    let manifest_toml = match draft.manifest_toml {\n        Some(toml) if !toml.trim().is_empty() => toml,\n        _ => default_manifest_toml(sop_name, description),\n    };\n    if let Some(reason) = scan_candidate(&manifest_toml, procedure_markdown) {\n        bail!(\"proposal rejected: {reason}\");\n    }\n    validate_candidate(sop_name, &manifest_toml, procedure_markdown)?;\n    let now = now_iso8601();\n    let id = format!(\n        \"prop-{}-{}-{:08x}\",\n        slugify(sop_name),\n        now.replace(':', \"_\"),\n        rand::random::<u32>()\n    );\n    let target_content_hash = existing\n        .and_then(|sop| sop.location.as_deref())\n        .map(hash_sop_dir)\n        .transpose()?;\n    let proposal = ProposalRecord {\n        id,\n        kind,\n        status: ProposalStatus::Pending,\n        source_run_id: draft.source_run_id,","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/procedural_memory.rs#L29-L65","documentation":"Before writing any proposal, create_proposal concatenates the manifest TOML and procedure markdown and runs them through a LeakDetector (scan_candidate). If credential-like content is detected (the message lists the matched patterns), the proposal is rejected outright and nothing is persisted. This keeps procedural memory from becoming a store of harvested secrets.","triggerScenarios":"Proposing (or capturing a successful run) whose manifest or procedure markdown contains credential-looking text: 'token=<20+ char value>' style pairs, API keys, or other detector patterns. Even scrubbed run notes can trip the generic-secret detector when long high-entropy values survive redaction.","commonSituations":"Step outputs that echo environment variables or auth headers; run notes quoting CLI output containing tokens; SOP descriptions embedding connection strings; test fixtures with realistic key material.","solutions":["Remove the credential-like text from the SOP/run output and re-capture; rotate any real credential that leaked into output.","Fix the upstream steps to scrub or avoid printing secrets before their output is recorded.","Reword notes so secrets never enter step output in the first place (reference secret names, not values)."],"exampleFix":"# before: run note echoes a live token, capture is rejected\n- Step 2 ok: uploaded using token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\n\n# after: reference the secret by name only\n- Step 2 ok: uploaded using credentials from DEPLOY_TOKEN (value not logged)","handlingStrategy":"validation","validationCode":"// Best-effort pre-scan mirroring the detector: key=value pairs with long values.\nfn looks_like_credential(text: &str) -> bool {\n    for line in text.lines() {\n        let l = line.to_ascii_lowercase();\n        for key in [\"token=\", \"key=\", \"secret=\", \"password=\"] {\n            if let Some(pos) = l.find(key) {\n                let value = &line[pos + key.len()..];\n                if value.chars().take_while(|c| c.is_ascii_graphic()).count() >= 20 {\n                    return true;\n                }\n            }\n        }\n    }\n    false\n}\n\nassert!(!looks_like_credential(&markdown), \"scrub credential-like text before proposing\");","typeGuard":null,"tryCatchPattern":"match create_proposal(/* ... */) {\n    Err(e) if e.to_string().contains(\"credential-like content detected\") => {\n        // find the flagged pattern, remove/rotate it, then re-capture\n    }\n    other => other?,\n}","preventionTips":["Never let steps print secret values; log the secret's name only.","Scrub tool output before it is recorded as step results.","Rotate any real credential that reaches procedural memory paths."],"tags":["procedural-memory","proposal","secret-detection","security"],"backgroundTag":"secret-leak-detected","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}