tinyhumansai/openhuman · error
cannot persist empty identity
Error message
cannot persist empty identity
What it means
`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.
Source
Thrown at src/openhuman/channels/host/adapters.rs:284
}
}
// ---------------------------------------------------------------------------
// AllowlistStore → config.toml channel allowlist
// ---------------------------------------------------------------------------
/// Persists newly-authorized identities into the on-disk channel allowlist,
/// replicating Telegram's former `persist_allowed_identity` (load
/// `~/.openhuman/config.toml`, append to the channel's `allowed_users`, save).
pub struct ConfigAllowlistStore;
#[async_trait]
impl AllowlistStore for ConfigAllowlistStore {
async fn persist_allowed_identity(&self, channel: &str, identity: &str) -> anyhow::Result<()> {
use anyhow::Context;
let normalized = identity.trim().trim_start_matches('@').to_string();
if normalized.is_empty() {
anyhow::bail!("cannot persist empty identity");
}
let home = directories::UserDirs::new()
.map(|u| u.home_dir().to_path_buf())
.context("could not find home directory")?;
let openhuman_dir = home.join(".openhuman");
let config_path = openhuman_dir.join("config.toml");
let contents = tokio::fs::read_to_string(&config_path)
.await
.with_context(|| format!("failed to read config file: {}", config_path.display()))?;
let mut config: Config =
toml::from_str(&contents).context("failed to parse config.toml for allowlist")?;
config.config_path = config_path;
config.workspace_dir = openhuman_dir.join("workspace");
match channel {
"telegram" => {
let Some(telegram) = config.channels_config.telegram.as_mut() else {View on GitHub (pinned to 7491200858)
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".
Example fix
// before
store.persist_allowed_identity("telegram", username).await?;
// after
let normalized = username.trim().trim_start_matches('@');
if normalized.is_empty() {
return Ok(()); // nothing to persist — skip instead of erroring
}
store.persist_allowed_identity("telegram", username).await?; Defensive patterns
Strategy: validation
Validate before calling
// Guard at the call site before persisting
let normalized = identity.trim().trim_start_matches('@');
if normalized.is_empty() {
tracing::debug!("skipping empty allowlist identity for {channel}");
return Ok(());
}
store.persist_allowed_identity(channel, identity).await?; Type guard
fn is_persistable_identity(identity: &str) -> bool {
!identity.trim().trim_start_matches('@').is_empty()
} Try / catch
if let Err(e) = store.persist_allowed_identity(channel, identity).await {
if e.to_string().contains("cannot persist empty identity") {
tracing::warn!(channel, "skipped empty allowlist identity"); // benign, drop it
} else {
return Err(e);
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- telegram channel config is missing in config.toml
- allowlist persist unsupported for channel '{other}'
- OPENAI_CODEX_OAUTH_MISSING_CALLBACK_URL
- Channel connect response missing status
- no injected channel model source for '{provider_name}'; prod
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/b83ac17fb0963979.
Report an issue: GitHub.