{"record":{"id":"b83ac17fb0963979","repo":"tinyhumansai/openhuman","slug":"cannot-persist-empty-identity","errorCode":null,"errorMessage":"cannot persist empty identity","messagePattern":"cannot persist empty identity","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/channels/host/adapters.rs","lineNumber":284,"sourceCode":"    }\n}\n\n// ---------------------------------------------------------------------------\n// AllowlistStore → config.toml channel allowlist\n// ---------------------------------------------------------------------------\n\n/// Persists newly-authorized identities into the on-disk channel allowlist,\n/// replicating Telegram's former `persist_allowed_identity` (load\n/// `~/.openhuman/config.toml`, append to the channel's `allowed_users`, save).\npub struct ConfigAllowlistStore;\n\n#[async_trait]\nimpl AllowlistStore for ConfigAllowlistStore {\n    async fn persist_allowed_identity(&self, channel: &str, identity: &str) -> anyhow::Result<()> {\n        use anyhow::Context;\n        let normalized = identity.trim().trim_start_matches('@').to_string();\n        if normalized.is_empty() {\n            anyhow::bail!(\"cannot persist empty identity\");\n        }\n\n        let home = directories::UserDirs::new()\n            .map(|u| u.home_dir().to_path_buf())\n            .context(\"could not find home directory\")?;\n        let openhuman_dir = home.join(\".openhuman\");\n        let config_path = openhuman_dir.join(\"config.toml\");\n        let contents = tokio::fs::read_to_string(&config_path)\n            .await\n            .with_context(|| format!(\"failed to read config file: {}\", config_path.display()))?;\n        let mut config: Config =\n            toml::from_str(&contents).context(\"failed to parse config.toml for allowlist\")?;\n        config.config_path = config_path;\n        config.workspace_dir = openhuman_dir.join(\"workspace\");\n\n        match channel {\n            \"telegram\" => {\n                let Some(telegram) = config.channels_config.telegram.as_mut() else {","sourceCodeStart":266,"sourceCodeEnd":302,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/channels/host/adapters.rs#L266-L302","documentation":"`ConfigAllowlistStore::persist_allowed_identity` persists a newly authorized identity into `~/.openhuman/config.toml`'s channel allowlist (replicating Telegram's former `persist_allowed_identity`). Before touching the file it normalizes the handle — `trim()` then strip one leading `@` — and if the result is empty it refuses. This prevents blank entries from ever being written into `allowed_users`, where they would authorize nobody and corrupt the deny-by-default allowlist.","triggerScenarios":"Calling `persist_allowed_identity(channel, identity)` where identity is `\"\"`, whitespace-only, `\"@\"`, or `\"@ \"` — i.e. the normalized form is empty. Typically the provider event carried an empty username and the adapter forwarded it unvalidated.","commonSituations":"Telegram messages where `from.username` is absent (users without a public handle) and the adapter passes an empty default; test harnesses passing placeholder handles; refactors that drop or reorder the normalization step.","solutions":["Normalize and check the identity at the call site, skipping persistence when empty (fall back to a real platform id such as the numeric chat id).","Fix the upstream identity extraction so an absent username resolves to a canonical id instead of an empty string.","In tests, inject a non-empty identity such as \"@alice\"."],"exampleFix":"// before\nstore.persist_allowed_identity(\"telegram\", username).await?;\n\n// after\nlet normalized = username.trim().trim_start_matches('@');\nif normalized.is_empty() {\n    return Ok(()); // nothing to persist — skip instead of erroring\n}\nstore.persist_allowed_identity(\"telegram\", username).await?;","handlingStrategy":"validation","validationCode":"// Guard at the call site before persisting\nlet normalized = identity.trim().trim_start_matches('@');\nif normalized.is_empty() {\n    tracing::debug!(\"skipping empty allowlist identity for {channel}\");\n    return Ok(());\n}\nstore.persist_allowed_identity(channel, identity).await?;","typeGuard":"fn is_persistable_identity(identity: &str) -> bool {\n    !identity.trim().trim_start_matches('@').is_empty()\n}","tryCatchPattern":"if let Err(e) = store.persist_allowed_identity(channel, identity).await {\n    if e.to_string().contains(\"cannot persist empty identity\") {\n        tracing::warn!(channel, \"skipped empty allowlist identity\"); // benign, drop it\n    } else {\n        return Err(e);\n    }\n}","preventionTips":["Normalize and check identities at the adapter boundary, before any store call","Fall back to a stable platform id (e.g. Telegram numeric chat id) when the username is absent","Never forward Option::unwrap_or_default() of a username field into persistence"],"tags":["channels","allowlist","telegram","input-validation"],"backgroundTag":"empty-input-validation","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}