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

  1. 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).
  2. Fix the upstream identity extraction so an absent username resolves to a canonical id instead of an empty string.
  3. 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

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


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/b83ac17fb0963979. Report an issue: GitHub.