Hmbown/CodeWhale · error · std::io::Error
invalid evidence owner
Error message
invalid evidence owner
What it means
PermissionDenied ('invalid evidence owner') returned by read_evidence_metadata when session_artifact_absolute_path(session_id, relative) yields None — i.e. the evidence handle does not resolve to a path inside that session's artifact directory. The guard is an ownership check: artifacts written under one session cannot be read back through another session id or a hand-constructed handle.
Source
Thrown at crates/tui/src/tools/large_output_router.rs:389
}
pub fn publish_evidence_metadata(
session_id: &str,
artifact: &EvidenceArtifact,
) -> io::Result<PathBuf> {
let bytes = serde_json::to_vec_pretty(artifact)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
crate::artifacts::write_session_relative_immutable(
session_id,
&evidence_metadata_relative_path(&artifact.handle),
&bytes,
)
}
pub fn read_evidence_metadata(session_id: &str, handle: &str) -> io::Result<EvidenceArtifact> {
let relative = evidence_metadata_relative_path(handle);
let path = crate::artifacts::session_artifact_absolute_path(session_id, &relative)
.ok_or_else(|| io::Error::new(io::ErrorKind::PermissionDenied, "invalid evidence owner"))?;
let raw = std::fs::read(path)?;
serde_json::from_slice(&raw).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
}
#[must_use]
pub fn unix_millis_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or(u64::MAX)
}
#[must_use]
pub fn evidence_is_expired(artifact: &EvidenceArtifact, now_ms: u64) -> bool {
artifact.retention_state == EvidenceRetentionState::Expired
|| now_ms > artifact.retain_until_unix_msView on GitHub (pinned to 0c42157ee5)
Solutions
- Use the exact handle string returned by write_evidence_metadata (the artifact's own handle field), unmodified
- Make sure the session_id you pass is the same session that owns the artifact — re-publish the artifact into the current session if you need it there
- Never build handle/relative paths by string concatenation; treat them as opaque tokens
Example fix
// before let artifact = read_evidence_metadata(&new_session_id, &old_handle)?; // PermissionDenied // after let artifact = read_evidence_metadata(&owning_session_id, &old_handle)?; // or re-write the artifact under new_session_id first
Defensive patterns
Strategy: validation
Validate before calling
// Only attempt reads with handles minted by the write side, and only under the owning session.
fn valid_handle(h: &str) -> bool {
!h.trim().is_empty() && !h.contains("..") && !h.contains('/') && !h.contains('\\')
}
if !valid_handle(&handle) { return Err(own_error("invalid artifact handle")); } Type guard
fn is_invalid_owner(e: &std::io::Error) -> bool {
e.kind() == std::io::ErrorKind::PermissionDenied && e.to_string().contains("invalid evidence owner")
} Try / catch
match read_evidence_metadata(session_id, &handle) {
Ok(a) => Ok(a),
Err(e) if is_invalid_owner(&e) => Err(explain("artifact not owned by this session; re-publish it here first")),
Err(e) => Err(e),
} Prevention
- Treat evidence handles as opaque tokens: store the pair (session_id, handle) together
- When replaying a transcript in a new session, re-publish artifacts instead of reusing old handles
- Never construct handle strings by concatenation
When it happens
Trigger: Calling read_evidence_metadata with a handle obtained from a different session, a truncated/edited handle string, or a handle whose relative path would escape the session artifact root (path traversal) so the absolute-path resolver refuses it.
Common situations: Restoring/replaying a transcript in a new session and reusing old artifact handles; copy-pasting a handle between sessions; clients that synthesize handle strings instead of using the one returned by the write side; tampered artifact metadata.
Related errors
- external credential path changed for {}; consent covers {},
- persistent allow rules must be scoped to a workspace
- persistent command allow rules must use exact matching
- persistent allow rules must match an exact command or path
- persistent command allow rules must not be empty
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/ee00d977635fa91d.
Report an issue: GitHub.