Hmbown/CodeWhale · error · anyhow::Error
Agent Mail ownership denied: source and destination must bel
Error message
Agent Mail ownership denied: source and destination must belong to the same runtime owner and workspace
What it means
queue_agent_mail resolved both threads to AgentMailAddresses and found the source and destination do not share owner_id and workspace_id. Mail is single-owner, single-workload: envelopes never cross runtime owners or workspaces, so this is an authorization boundary failure, not a connectivity issue.
Source
Thrown at crates/tui/src/runtime_threads.rs:3620
*label = sanitize_agent_mail_text(
label,
codewhale_protocol::agent_mail::MAX_AGENT_MAIL_EVIDENCE_LABEL_BYTES,
);
}
}
request.validate().map_err(|error| anyhow!(error))?;
if request.source_thread_id == request.destination_thread_id {
bail!("Agent Mail source and destination threads must differ");
}
let source_thread = self.get_thread(&request.source_thread_id).await?;
let destination_thread = self.get_thread(&request.destination_thread_id).await?;
let source = agent_mail_address(&self.store.owner_id, &source_thread)?;
let destination = agent_mail_address(&self.store.owner_id, &destination_thread)?;
if source.owner_id != destination.owner_id
|| source.workspace_id != destination.workspace_id
{
bail!(
"Agent Mail ownership denied: source and destination must belong to the same runtime owner and workspace"
);
}
let expected_sender = agent_mail_sender_identity(&source_thread)?;
if request.sender.identity != expected_sender {
bail!(
"Agent Mail ownership denied: sender identity does not own the source task/session"
);
}
let (envelope, idempotent_replay) = {
let _mail_mutation = self.store.mail_mutation.lock();
let path = self.store.mail_path(&request.message_id)?;
if path.exists() {
let persisted = self.store.load_agent_mail(&request.message_id)?;
if !persisted.matches_send_request(&request) {
bail!(
"Agent Mail message id '{}' already exists with different delivery intent",View on GitHub (pinned to 0c42157ee5)
Solutions
- Verify both thread ids were created by the same runtime owner and workspace (list threads and compare their records)
- If the workspace legitimately changed, re-create or migrate the thread record so both sides resolve within one workspace
- Never accept destination thread ids from untrusted input - resolve and authorize them server-side
- Check for accidental cross-environment id leakage (prod id in a dev runtime)
Defensive patterns
Strategy: validation
Validate before calling
// Both threads must resolve under the same owner and workspace.
let src = manager.get_thread(&request.source_thread_id).await?;
let dst = manager.get_thread(&request.destination_thread_id).await?;
if src.owner_id != dst.owner_id || src.workspace_id != dst.workspace_id {
return Err(anyhow::anyhow!("cross-owner/cross-workspace mail is not permitted"));
} Prevention
- Only pass thread ids created by the same runtime owner/workspace
- Do not mix ids across environments (prod ids in a dev runtime)
- After workspace migrations, verify both thread records were re-keyed consistently
When it happens
Trigger: get_thread resolves either thread whose stored owner/workspace differs from the other - e.g. one id belongs to a thread created under a different owner or workspace layout after a migration, or a guessed/foreign thread id was passed. Check at runtime_threads.rs:3615-3623.
Common situations: Mixing thread ids from two runtime deployments or profiles; workspace renamed/re-keyed so one side resolves to the old workspace; test fixtures reusing ids across isolated stores.
Related errors
- Agent Mail ownership denied: sender identity does not own th
- Agent Mail ownership denied: message does not belong to this
- workflow source for `{workflow}` not found; tried {}
- external credential access is disabled for {}
- persistent allow rules must be scoped to a workspace
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/5d6ac78f5fc4ec1f.
Report an issue: GitHub.