{"record":{"id":"b41fe5a099426b1d","repo":"tinyhumansai/openhuman","slug":"local-only-privacy-mode-is-active-this-action-nee","errorCode":null,"errorMessage":"Local-only privacy mode is active: this action needs external provider {label}. Switch to a local model (Ollama/LM Studio/etc.) or change privacy mode in Settings.","messagePattern":"Local-only privacy mode is active: this action needs external provider (.+?)\\. Switch to a local model \\(Ollama/LM Studio/etc\\.\\) or change privacy mode in Settings\\.","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"src/openhuman/inference/provider/factory.rs","lineNumber":645,"sourceCode":"    let mode = crate::openhuman::security::live_policy::current_privacy_mode();\n    match local_only_violation(mode, provider) {\n        None => {\n            log::debug!(\n                \"[privacy][chat-factory] privacy_mode={:?} role={} provider='{}' — inference permitted\",\n                mode,\n                role,\n                provider.trim()\n            );\n            Ok(())\n        }\n        Some(label) => {\n            log::warn!(\n                \"[privacy][chat-factory] LocalOnly BLOCK: role={} external provider='{}' ({}) refused\",\n                role,\n                provider.trim(),\n                label\n            );\n            anyhow::bail!(\n                \"Local-only privacy mode is active: this action needs external provider {label}. \\\n                 Switch to a local model (Ollama/LM Studio/etc.) or change privacy mode in Settings.\"\n            )\n        }\n    }\n}\n\n/// Egress spine (privacy epic S2, #4436): emit an [`EgressDescriptor`] for a\n/// concrete inference provider string. `provider` is expected to be already\n/// resolved (no `\"\"` / `\"cloud\"` / BYOK sentinels — those are handled before\n/// this is called). Local runtimes are marked non-external, so\n/// [`emit_external_transfer`](crate::openhuman::security::egress::emit_external_transfer)\n/// discloses them without firing the external-transfer event.\nfn emit_inference_egress(role: &str, provider: &str) {\n    let p = provider.trim();\n    if p.is_empty() || p == \"cloud\" {\n        // Defensive: a sentinel would re-resolve on recursion; don't emit here.\n        return;","sourceCodeStart":627,"sourceCodeEnd":663,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/inference/provider/factory.rs#L627-L663","documentation":"OpenHuman's privacy enforcement chokepoint (`enforce_local_only_inference` / `local_only_violation`, factory.rs:603-651) refused to build an external chat provider because the live privacy policy is `PrivacyMode::LocalOnly`. The resolved provider string is neither empty, the `cloud` sentinel (which defers), nor recognized by `is_local_provider_string` (Ollama/LM Studio/local runtimes), so it is classified as external and blocked. The label names the external destination.","triggerScenarios":"Any chat/inference request whose role resolves to an external provider string (e.g. `anthropic:claude-...`, `openai:...`) while the session's live privacy mode is LocalOnly. Fired from `enforce_local_only_inference` at the factory chokepoint before the provider is constructed.","commonSituations":"User enabled Local-only privacy mode in Settings then a background workload (summaries, embeddings, a cron role) still points at a cloud provider; a shared/parental-control profile with a local-only policy installed; switching a role to a cloud model without noticing the mode.","solutions":["Switch the offending role's provider to a local runtime (Ollama / LM Studio) in Settings, so `is_local_provider_string` accepts it.","Or change privacy mode away from Local-only in Settings if external providers are intended.","Identify which role triggered it from the `[privacy][chat-factory] LocalOnly BLOCK: role=... provider=...` warn log and fix that specific role.","For background workloads, configure them to use a local model or the exempt managed path per your privacy policy."],"exampleFix":"# config: point the role at a local runtime instead of a cloud slug\n# before\nsummary_provider = \"anthropic:claude-sonnet-4-6\"\n# after\nsummary_provider = \"ollama:llama3.1:8b\"","handlingStrategy":"validation","validationCode":"// Before requesting a workload on an external provider:\nuse crate::openhuman::config::PrivacyMode;\nuse crate::openhuman::inference::local::profile::is_local_provider_string;\nif crate::openhuman::security::live_policy::current_privacy_mode() == PrivacyMode::LocalOnly\n    && !is_local_provider_string(provider_string)\n{\n    return local_only_notice(); // don't attempt the call\n}","typeGuard":"fn provider_allowed_under_local_only(provider: &str) -> bool {\n    let p = provider.trim();\n    p.is_empty() || p == \"cloud\" || is_local_provider_string(p)\n}","tryCatchPattern":null,"preventionTips":["When enabling Local-only mode, audit every role (including background ones) for cloud providers.","Surface the live privacy mode in the UI next to provider pickers so mismatches are visible.","Remember `\"\"`/`\"cloud\"` deferred sentinels re-resolve — the concrete string is what gets checked."],"tags":["privacy","local-only","provider-gate","inference"],"backgroundTag":"privacy-policy-block","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}