use std::path::{Path, PathBuf};
use std::process::ExitStatus;
pub type Result<T> = std::result::Result<T, MindError>;
pub const METADATA_SIZE_LIMIT: u64 = 8 * 1024 * 1024;
pub fn read_capped_metadata(path: &Path) -> Result<String> {
use std::io::Read as _;
let file = std::fs::File::open(path).map_err(|e| MindError::io(path, e))?;
let mut buf = Vec::new();
file.take(METADATA_SIZE_LIMIT + 1)
.read_to_end(&mut buf)
.map_err(|e| MindError::io(path, e))?;
if buf.len() as u64 > METADATA_SIZE_LIMIT {
return Err(MindError::MetadataTooLarge {
path: path.to_path_buf(),
limit: METADATA_SIZE_LIMIT,
});
}
String::from_utf8(buf).map_err(|e| {
MindError::io(
path,
std::io::Error::new(std::io::ErrorKind::InvalidData, e),
)
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ItemKind {
Skill,
Agent,
Rule,
Tool,
}
impl ItemKind {
pub fn as_str(self) -> &'static str {
match self {
ItemKind::Skill => "skill",
ItemKind::Agent => "agent",
ItemKind::Rule => "rule",
ItemKind::Tool => "tool",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"skill" => Some(ItemKind::Skill),
"agent" => Some(ItemKind::Agent),
"rule" => Some(ItemKind::Rule),
"tool" => Some(ItemKind::Tool),
_ => None,
}
}
pub fn dir(self) -> &'static str {
match self {
ItemKind::Skill => "skills",
ItemKind::Agent => "agents",
ItemKind::Rule => "rules",
ItemKind::Tool => "tools",
}
}
pub fn from_dir(s: &str) -> Option<Self> {
match s {
"skills" => Some(ItemKind::Skill),
"agents" => Some(ItemKind::Agent),
"rules" => Some(ItemKind::Rule),
"tools" => Some(ItemKind::Tool),
_ => None,
}
}
pub const LINKABLE: [ItemKind; 3] = [ItemKind::Skill, ItemKind::Agent, ItemKind::Rule];
pub fn parse_kinds(strs: &[String]) -> Result<Vec<ItemKind>> {
strs.iter()
.map(|s| ItemKind::parse(s).ok_or_else(|| MindError::UnknownKind { kind: s.clone() }))
.collect()
}
}
impl std::fmt::Display for ItemKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BadRefReason {
NoMatch,
ToolNoBin,
AmbiguousKind,
CrossSource,
InvalidRef,
}
impl std::fmt::Display for BadRefReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BadRefReason::NoMatch => f.write_str("does not match any item"),
BadRefReason::ToolNoBin => {
f.write_str("names a tool with no resolvable entrypoint (bin)")
}
BadRefReason::AmbiguousKind => {
f.write_str("is ambiguous across kinds; add a kind qualifier")
}
BadRefReason::CrossSource => {
f.write_str("crosses sources; a requires entry is intra-source only")
}
BadRefReason::InvalidRef => f.write_str("is not a valid item ref"),
}
}
}
fn format_conflicts(conflicts: &[(String, String, String)]) -> String {
conflicts
.iter()
.map(|(k, n, s)| format!(" {k}:{n} (already installed from '{s}')"))
.collect::<Vec<_>>()
.join("\n")
}
#[derive(Debug, thiserror::Error)]
pub enum MindError {
#[error("could not locate the home directory")]
HomeDirNotFound,
#[error("I/O error at '{path}': {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to (de)serialize {what}: {source}")]
Json {
what: String,
#[source]
source: serde_json::Error,
},
#[error("invalid mind.toml at {path}: {source}")]
Toml {
path: PathBuf,
#[source]
source: toml::de::Error,
},
#[error("invalid config at {path}: {msg}")]
ConfigToml { path: PathBuf, msg: String },
#[error("failed to write {path}: {source}")]
TomlWrite {
path: PathBuf,
#[source]
source: toml::ser::Error,
},
#[error(
"'{path}' is not a configured agent home (lobe); run `mind config lobes list` to see configured lobes"
)]
UnknownLobe { path: String },
#[error("'{kind}' is not a valid item kind (expected one of: skill, agent, rule, tool)")]
UnknownKind { kind: String },
#[error(
"'{name}' is not a known lobe preset (expected one of: gemini, codex, universal, windsurf)"
)]
UnknownPreset { name: String },
#[error("`config lobes add` needs a path or `--preset <name>`")]
LobeTargetRequired,
#[error("mind.toml at {path}: {msg}")]
MindToml { path: PathBuf, msg: String },
#[error("{path}: {msg}")]
Manifest { path: PathBuf, msg: String },
#[error(
"'{path}' exceeds the {} MiB size cap for a hand-authored metadata file (mind.toml, an \
item's frontmatter, or a plugin/marketplace manifest); trim the file, or move large \
content out of it, and try again",
limit / (1024 * 1024)
)]
MetadataTooLarge { path: PathBuf, limit: u64 },
#[error(
"'{spec}' is not a valid repo spec (expected 'owner/repo', a github shorthand, a git URL, \
or a local path: '/abs/path', './rel/path', '../rel/path', or 'file:///abs/path')"
)]
InvalidRepoSpec { spec: String },
#[error(
"'{spec}' is not a usable repo spec: its {part} part '{value}' {reason}; each of host, owner, and repo must be a single path component"
)]
UnsafeRepoSpec {
spec: String,
part: &'static str,
value: String,
reason: &'static str,
},
#[error(
"'{url}' is not a valid item-link URL ({reason}); expected '<repo-url>/tree/<ref>/<skill-dir>' or '<repo-url>/blob/<ref>/<skill-dir>/SKILL.md' (GitLab's '/-/tree/' and '/-/blob/' forms also work)"
)]
BadItemLink { url: String, reason: String },
#[error(
"'{name}' is not a valid item ref (expected 'name', 'skill:name', 'agent:name', 'rule:name', or 'owner/repo#name')"
)]
InvalidItemRef { name: String },
#[error(
"'{prefix}' cannot be used as a namespace prefix: it is a reserved item-kind word (skill, agent, rule, tool), which would make a prefixed name indistinguishable from a kind-qualified ref"
)]
ReservedPrefix { prefix: String },
#[error(
"'{prefix}' cannot be used as a namespace prefix: it must be a single safe path component (no `/`, `\\`, `:`, `.`, `..`, leading `~`, NUL, or control characters)"
)]
UnsafePrefix { prefix: String },
#[error(
"cannot change the namespace of source '{src_name}': the following items are installed ({items}); run `mind forget <item>` for each before changing the namespace",
items = items.join(", ")
)]
NamespaceLocked {
src_name: String,
items: Vec<String>,
},
#[error("source '{name}' is already melded (from {url})")]
SourceExists { name: String, url: String },
#[error(
"no source named '{name}' is melded; run `mind recall --sources` to list melded sources"
)]
SourceNotFound { name: String },
#[error("'{pattern}' is not a valid glob selector: {source}")]
InvalidPattern {
pattern: String,
#[source]
source: glob::PatternError,
},
#[error("'{query}' matches multiple sources: {}; use the full owner/repo", candidates.join(", "))]
AmbiguousSource {
query: String,
candidates: Vec<String>,
},
#[error(
"no item matches '{query}'{}",
if *sources == 0 {
"; no sources are melded yet -- run `mind meld <repo>` to add one".to_string()
} else {
format!(" across {sources} melded source(s); run `mind probe` to search available items")
}
)]
ItemNotFound { query: String, sources: usize },
#[error("'{query}' is ambiguous; matches: {}", candidates.join(", "))]
AmbiguousItem {
query: String,
candidates: Vec<String>,
},
#[error("'{name}' is not installed")]
NotInstalled { name: String },
#[error("sync failed for {failed} of {total} source(s); see the messages above")]
SyncFailed { failed: usize, total: usize },
#[error(
"source '{source_name}' requires mind >= {required}, but this is mind {running}; run `mind evolve`"
)]
IncompatibleVersion {
source_name: String,
required: String,
running: String,
},
#[error(
"'{path}' already exists and is not managed by mind; remove it (or `mind forget` the item) before installing, or re-run with `--force` to overwrite"
)]
LinkOccupied { path: String },
#[error("{item}: reference {referent} {reason} in source '{in_source}'")]
BadReference {
item: String,
referent: String,
reason: BadRefReason,
in_source: String,
},
#[error("git {} failed for {url}{}: {}",
args.join(" "),
status_suffix(*status),
if stderr.is_empty() { "<no stderr>" } else { stderr })]
Git {
url: String,
args: Vec<String>,
status: Option<ExitStatus>,
stderr: String,
},
#[error(
"melding '{super_source}' produced no discoverable items: it has no items of its own and every nested source failed to register"
)]
CuratorAllNestedFailed { super_source: String },
#[error("git executable not found on PATH; install git to meld and sync sources")]
GitNotFound,
#[error("conflicting pin flags: {first} and {second} cannot both be given; supply at most one")]
ConflictingPin { first: String, second: String },
#[error(
"invalid --pin value '{value}': expected HEAD, a ref (tag/sha/branch), 'branch=<name>', or 'tag=<name>'"
)]
BadPinSpec { value: String },
#[error("source '{source_name}': scan root '{root}' is not a directory in the clone")]
InvalidRoot { source_name: String, root: String },
#[error(
"source '{source_name}': linked path '{path}' is not a skill directory in the clone (no SKILL.md)"
)]
LinkNotASkill { source_name: String, path: String },
#[error(
"source '{source_name}': {kind} '{name}' appears under more than one scan root; roots must not yield the same item"
)]
DuplicateItem {
source_name: String,
kind: ItemKind,
name: String,
},
#[error("review found {hard} hard error(s); see the findings above")]
ReviewFailed { hard: usize },
#[error("source '{identity}' is not permitted by the managed policy's allowlist")]
SourceNotAllowed { identity: String },
#[error(
"local-path and file:// melds are forbidden by the managed policy \
([sources].allow-local = false)"
)]
LocalMeldForbidden { identity: String },
#[error(
"source '{identity}' must be pinned to a tag or ref: the managed policy forbids floating branches"
)]
UnpinnedSourceForbidden { identity: String },
#[error("invalid managed policy at {path}: {reason}")]
InvalidPolicy { path: String, reason: String },
#[error(
"the agent homes are locked by the managed policy ([lobes].lock); `config lobes {action}` is refused"
)]
LobesLocked { action: String },
#[error(
"install hook for source '{identity}' failed{}: {}\n command: {command}",
status_suffix(*status),
if *printed_output { "(see output above)" } else if stderr.is_empty() { "(no output)" } else { stderr.as_str() }
)]
HookFailed {
identity: String,
command: String,
status: Option<ExitStatus>,
stderr: String,
printed_output: bool,
},
#[error("no prebuilt `mind` binary for this platform ({os}/{arch}); build from source instead")]
UnsupportedPlatform { os: String, arch: String },
#[error("failed to download {url}: {reason}")]
DownloadFailed { url: String, reason: String },
#[error("the downloaded release archive did not contain a 'mind' binary")]
ReleaseAssetEmpty,
#[error(
"cannot replace the running binary at {path}: it is not writable; reinstall with elevated privileges (e.g. sudo) or, for a Homebrew install, run `brew upgrade mind`"
)]
TargetNotWritable { path: String },
#[error("'{path}' is not a directory")]
NotADirectory { path: String },
#[error("{action} needs confirmation; re-run with --yes (or in an interactive terminal)")]
ConfirmationRequired { action: String },
#[error(
"'{path}' is not a git repository; absorb requires a git destination (use --to to choose one)"
)]
DestinationNotRepo { path: String },
#[error("invalid ref value '{value}': {reason}")]
InvalidRef { value: String, reason: String },
#[error("destination already has {kind}:{name} at {dest_path}; use --force to overwrite")]
AbsorbCollision {
kind: String,
name: String,
dest_path: String,
},
#[error("{}", agent_collision_message(name, existing, incoming))]
AgentCollision {
name: String,
existing: String,
incoming: String,
},
#[error("{}", skill_collision_message(conflicts, suggested))]
SkillCollision {
conflicts: Vec<(String, String, String)>,
suggested: String,
},
#[error(
"unsafe effective name '{name}': contains path-traversal characters or resolves to a relative component (`.`/`..`); refusing to build store or link paths from it"
)]
UnsafeName { name: String },
#[error(
"digest mismatch for {url}: expected {expected}, got {actual}; the download may be corrupted or tampered with"
)]
DigestMismatch {
url: String,
expected: String,
actual: String,
},
#[error(
"build provenance verification failed for the downloaded release archive: {reason}; \
evolve is aborting without replacing the running binary. If this release predates \
build-provenance attestations, install it another way (resources/install.sh or a \
manual download) instead of `mind evolve`"
)]
AttestationVerificationFailed { reason: String },
#[error("{detail}")]
SelfUpdatePolicy { detail: String },
#[error(
"{what} uses schema version {found} but this mind only supports up to version {supported}; run `mind evolve` to read it"
)]
StateTooNew {
what: &'static str,
found: u32,
supported: u32,
},
#[error(
"--event build is valid only for an item target (a source has no build hook); use <source>#<item> to target an item"
)]
BuildEventRequiresItemTarget,
#[error("hook '{label}' was aborted by user; not running remaining hooks")]
HookAborted { label: String },
#[error("lobe base directory does not exist: {}", path.display())]
LobeBaseMissing { path: PathBuf },
#[error(
"refusing to use clone path '{path}' for source '{identity}': it resolves outside the managed sources tree"
)]
UnsafeClonePath { path: PathBuf, identity: String },
#[error("{}", ambiguous_hook_target_message(target, item_forms))]
AmbiguousHookTarget {
target: String,
item_forms: Vec<String>,
},
#[error("{}", linked_source_gone_message(source_name, path))]
LinkedSourceGone { source_name: String, path: String },
#[error("{}", hooks_not_run_message(target, event, *skipped, resolved))]
HooksNotRun {
target: String,
event: String,
skipped: usize,
resolved: Vec<String>,
},
}
pub(crate) fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', r"'\''"))
}
fn ambiguous_hook_target_message(target: &str, item_forms: &[String]) -> String {
let source_arg = shell_quote(&format!("source:{target}"));
let item_form = item_forms.first().map(String::as_str).unwrap_or("");
let item_arg = shell_quote(item_form);
format!(
"'{target}' is ambiguous: it names both the registered source '{target}' and installed item(s) matching the same string; \
to target the source, run `mind hooks run {source_arg}`; to target an item, use a kind-qualified ref, e.g. `mind hooks run {item_arg}`"
)
}
fn hooks_not_run_message(target: &str, event: &str, skipped: usize, resolved: &[String]) -> String {
debug_assert!(
!resolved.is_empty(),
"hooks_not_run_message: resolved must never be empty -- both construction sites \
(run_source_hooks/run_item_hooks) only build MindError::HooksNotRun once at least \
one contributor pushed a resolved identity"
);
match resolved {
[one] => {
let quoted = shell_quote(one);
format!(
"hooks run '{one}': {skipped} hook(s) had work to do but were skipped for want \
of consent (not a terminal); re-run unattended with:\n mind hooks run {quoted} \
--event {event} --dangerously-skip-install-hook-check"
)
}
_ => {
let names = if resolved.is_empty() {
String::new()
} else {
let quoted: Vec<String> = resolved.iter().map(|n| shell_quote(n)).collect();
format!(" ({})", quoted.join(", "))
};
format!(
"hooks run '{target}': {skipped} hook(s) had work to do but were skipped for \
want of consent (not a terminal), across {} matched target(s){names}; re-run \
each individually with 'mind hooks run <name> --event {event} \
--dangerously-skip-install-hook-check', substituting each resolved name above \
for <name>",
resolved.len()
)
}
}
}
fn linked_source_gone_message(source_name: &str, path: &str) -> String {
let quoted = shell_quote(source_name);
format!(
"source '{source_name}': linked working tree '{path}' is gone; run `mind unmeld {quoted}` to drop it, or restore the directory"
)
}
fn agent_collision_message(name: &str, existing: &str, incoming: &str) -> String {
let forget_arg = shell_quote(&format!("agent:{name}"));
format!(
"agent '{name}' from source '{incoming}' conflicts with the installed agent from \
'{existing}': both link as agents/{name}.md in the agent home -- \
run `mind forget {forget_arg}` (or the prefixed name) to remove the existing agent first"
)
}
fn skill_collision_message(conflicts: &[(String, String, String)], suggested: &str) -> String {
let ns = shell_quote(suggested);
format!(
"name collision: the following items from the incoming source conflict with \
already-installed items:\n{}\nrun `mind meld --namespace {ns} <repo>` \
to namespace the incoming source",
format_conflicts(conflicts)
)
}
fn status_suffix(status: Option<ExitStatus>) -> String {
match status.and_then(|s| s.code()) {
Some(code) => format!(" (exit {code})"),
None => String::new(),
}
}
impl MindError {
pub fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
MindError::Io {
path: path.into(),
source,
}
}
pub fn json(what: impl Into<String>, source: serde_json::Error) -> Self {
MindError::Json {
what: what.into(),
source,
}
}
pub fn kind(&self) -> &'static str {
match self {
MindError::HomeDirNotFound => "home-dir-not-found",
MindError::Io { .. } => "io",
MindError::Json { .. } => "json",
MindError::Toml { .. } => "toml",
MindError::ConfigToml { .. } => "config-toml",
MindError::TomlWrite { .. } => "toml-write",
MindError::UnknownLobe { .. } => "unknown-lobe",
MindError::UnknownKind { .. } => "unknown-kind",
MindError::UnknownPreset { .. } => "unknown-preset",
MindError::LobeTargetRequired => "lobe-target-required",
MindError::MindToml { .. } => "mind-toml",
MindError::Manifest { .. } => "manifest",
MindError::MetadataTooLarge { .. } => "metadata-too-large",
MindError::InvalidRepoSpec { .. } => "invalid-repo-spec",
MindError::UnsafeRepoSpec { .. } => "unsafe-repo-spec",
MindError::BadItemLink { .. } => "bad-item-link",
MindError::InvalidItemRef { .. } => "invalid-item-ref",
MindError::ReservedPrefix { .. } => "reserved-prefix",
MindError::UnsafePrefix { .. } => "unsafe-prefix",
MindError::NamespaceLocked { .. } => "namespace-locked",
MindError::SourceExists { .. } => "source-exists",
MindError::SourceNotFound { .. } => "source-not-found",
MindError::InvalidPattern { .. } => "invalid-pattern",
MindError::AmbiguousSource { .. } => "ambiguous-source",
MindError::ItemNotFound { .. } => "item-not-found",
MindError::AmbiguousItem { .. } => "ambiguous-item",
MindError::NotInstalled { .. } => "not-installed",
MindError::SyncFailed { .. } => "sync-failed",
MindError::IncompatibleVersion { .. } => "incompatible-version",
MindError::LinkOccupied { .. } => "link-occupied",
MindError::BadReference { .. } => "bad-reference",
MindError::Git { .. } => "git",
MindError::CuratorAllNestedFailed { .. } => "curator-all-nested-failed",
MindError::GitNotFound => "git-not-found",
MindError::ConflictingPin { .. } => "conflicting-pin",
MindError::BadPinSpec { .. } => "bad-pin-spec",
MindError::InvalidRoot { .. } => "invalid-root",
MindError::LinkNotASkill { .. } => "link-not-a-skill",
MindError::DuplicateItem { .. } => "duplicate-item",
MindError::ReviewFailed { .. } => "review-failed",
MindError::SourceNotAllowed { .. } => "source-not-allowed",
MindError::LocalMeldForbidden { .. } => "local-meld-forbidden",
MindError::UnpinnedSourceForbidden { .. } => "unpinned-source-forbidden",
MindError::InvalidPolicy { .. } => "invalid-policy",
MindError::LobesLocked { .. } => "lobes-locked",
MindError::HookFailed { .. } => "hook-failed",
MindError::UnsupportedPlatform { .. } => "unsupported-platform",
MindError::DownloadFailed { .. } => "download-failed",
MindError::ReleaseAssetEmpty => "release-asset-empty",
MindError::TargetNotWritable { .. } => "target-not-writable",
MindError::NotADirectory { .. } => "not-a-directory",
MindError::ConfirmationRequired { .. } => "confirmation-required",
MindError::DestinationNotRepo { .. } => "destination-not-repo",
MindError::InvalidRef { .. } => "invalid-ref",
MindError::AbsorbCollision { .. } => "absorb-collision",
MindError::AgentCollision { .. } => "agent-collision",
MindError::SkillCollision { .. } => "skill-collision",
MindError::UnsafeName { .. } => "unsafe-name",
MindError::DigestMismatch { .. } => "digest-mismatch",
MindError::AttestationVerificationFailed { .. } => "attestation-verification-failed",
MindError::SelfUpdatePolicy { .. } => "self-update-policy",
MindError::StateTooNew { .. } => "state-too-new",
MindError::BuildEventRequiresItemTarget => "build-event-requires-item-target",
MindError::HookAborted { .. } => "hook-aborted",
MindError::LobeBaseMissing { .. } => "lobe-base-missing",
MindError::UnsafeClonePath { .. } => "unsafe-clone-path",
MindError::AmbiguousHookTarget { .. } => "ambiguous-hook-target",
MindError::LinkedSourceGone { .. } => "linked-source-gone",
MindError::HooksNotRun { .. } => "hooks-not-run",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
#[test]
fn shell_quote_round_trips_a_malicious_identity_through_a_real_shell() {
if Command::new("sh").arg("-c").arg("true").status().is_err() {
return;
}
let evil = "x; touch /tmp/mind-hook-106-pwned; echo $(id) `whoami` it's-evil";
let quoted = shell_quote(evil);
let output = Command::new("sh")
.arg("-c")
.arg(format!("printf '%s' {quoted}"))
.output()
.expect("sh -c must run");
assert!(
output.status.success(),
"the quoted identity must parse as a single shell argument: {quoted:?}\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
evil,
"the shell must see the identity back EXACTLY as given, proving none of \
';', '$(...)', backtick, or the embedded quote were interpreted rather \
than passed through literally: quoted form was {quoted:?}"
);
let sentinel = std::path::Path::new("/tmp/mind-hook-106-pwned");
assert!(
!sentinel.exists(),
"the embedded 'touch' must not have executed"
);
let _ = std::fs::remove_file(sentinel);
}
#[test]
fn shell_quote_neutralizes_every_boundary_identity_through_a_real_shell() {
if Command::new("sh").arg("-c").arg("true").status().is_err() {
return;
}
let cases = [
("lone-quote", "'", "'"),
("trailing-backslash", r"github.com/a/b\", r"github.com/a/b\"),
(
"newline",
"a\ntouch /tmp/mind-hq-nl\nb",
"a\ntouch /tmp/mind-hq-nl\nb",
),
(
"double-quote",
"a\"; touch /tmp/mind-hq-dq; \"b",
"a\"; touch /tmp/mind-hq-dq; \"b",
),
(
"dollar-paren",
"$(touch /tmp/mind-hq-dp)",
"$(touch /tmp/mind-hq-dp)",
),
(
"backtick",
"`touch /tmp/mind-hq-bt`",
"`touch /tmp/mind-hq-bt`",
),
];
for (name, payload, expected) in cases {
let quoted = shell_quote(payload);
let out = Command::new("sh")
.arg("-c")
.arg(format!("printf '%s' {quoted}"))
.output()
.unwrap_or_else(|e| panic!("[{name}] sh failed: {e}"));
assert!(
out.status.success(),
"[{name}] quoted form must parse as one argument: {quoted:?} stderr {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8_lossy(&out.stdout),
expected,
"[{name}] the shell must return the identity literally: quoted {quoted:?}"
);
}
for p in [
"/tmp/mind-hq-nl",
"/tmp/mind-hq-dq",
"/tmp/mind-hq-dp",
"/tmp/mind-hq-bt",
] {
let path = std::path::Path::new(p);
let existed = path.exists();
let _ = std::fs::remove_file(path);
assert!(!existed, "an injected touch executed and dropped {p}");
}
}
#[test]
fn shell_quote_is_harmless_for_a_plain_identity() {
assert_eq!(
shell_quote("github.com/jaemk/agents"),
"'github.com/jaemk/agents'"
);
}
#[test]
fn shell_quote_escapes_embedded_single_quotes() {
assert_eq!(shell_quote("it's"), r"'it'\''s'");
}
#[test]
fn hooks_not_run_message_shell_quotes_the_single_resolved_identity() {
let evil = "evil; rm -rf /tmp/x".to_string();
let msg =
hooks_not_run_message("target-as-typed", "install", 1, std::slice::from_ref(&evil));
assert!(
msg.contains(&shell_quote(&evil)),
"the remedy must carry the shell-quoted identity: {msg}"
);
assert!(
!msg.contains("run evil; rm -rf /tmp/x --event"),
"the remedy must never place the identity bare (unquoted) into the \
runnable command: {msg}"
);
}
#[test]
fn hooks_not_run_message_shell_quotes_every_resolved_identity_in_the_list() {
let a = "one; touch /tmp/a".to_string();
let b = "two`whoami`".to_string();
let c = "x'; rm -rf ~; echo '".to_string();
let msg = hooks_not_run_message("glob*", "install", 3, &[a.clone(), b.clone(), c.clone()]);
assert!(msg.contains(&shell_quote(&a)), "{msg}");
assert!(msg.contains(&shell_quote(&b)), "{msg}");
assert!(msg.contains(&shell_quote(&c)), "{msg}");
assert!(!msg.contains("(one; touch /tmp/a"), "{msg}");
assert!(
!msg.contains("'x'; rm -rf ~; echo ''"),
"must never carry the broken bare `'{{n}}'` interpolation of the \
single-quote-carrying identity: {msg}"
);
}
#[test]
#[should_panic(expected = "resolved must never be empty")]
fn hooks_not_run_message_asserts_resolved_is_never_empty() {
let _ = hooks_not_run_message("target", "install", 0, &[]);
}
#[test]
fn lobe_errors_render_actionable_messages() {
let unknown_kind = MindError::UnknownKind {
kind: "wizard".into(),
}
.to_string();
assert!(unknown_kind.contains("wizard"), "{unknown_kind}");
assert!(
unknown_kind.contains("skill") && unknown_kind.contains("tool"),
"UnknownKind must list the valid kinds: {unknown_kind}"
);
let unknown_preset = MindError::UnknownPreset {
name: "emacs".into(),
}
.to_string();
assert!(unknown_preset.contains("emacs"), "{unknown_preset}");
assert!(
unknown_preset.contains("gemini")
&& unknown_preset.contains("codex")
&& unknown_preset.contains("universal")
&& unknown_preset.contains("windsurf"),
"UnknownPreset must list the valid presets: {unknown_preset}"
);
assert!(
!unknown_preset.contains("antigravity"),
"UnknownPreset must not mention removed presets: {unknown_preset}"
);
let needs_target = MindError::LobeTargetRequired.to_string();
assert!(
needs_target.contains("path") && needs_target.contains("--preset"),
"LobeTargetRequired must mention both a path and --preset: {needs_target}"
);
}
#[test]
fn parse_kinds_accepts_known_rejects_unknown() {
let ok = ItemKind::parse_kinds(&["skill".into(), "agent".into(), "rule".into()]).unwrap();
assert_eq!(ok, vec![ItemKind::Skill, ItemKind::Agent, ItemKind::Rule]);
let err = ItemKind::parse_kinds(&["skill".into(), "wizard".into()]).unwrap_err();
assert!(
matches!(err, MindError::UnknownKind { ref kind } if kind == "wizard"),
"the first unknown kind must surface as UnknownKind: {err:?}"
);
}
#[test]
fn namespace_locked_displays_items_and_forget_hint() {
let e = MindError::NamespaceLocked {
src_name: "github.com/acme/agents".into(),
items: vec!["skill:review".into(), "agent:dev".into()],
}
.to_string();
assert!(e.contains("github.com/acme/agents"), "{e}");
assert!(
e.contains("skill:review") && e.contains("agent:dev"),
"must list every installed item: {e}"
);
assert!(e.contains("forget"), "must direct the user to forget: {e}");
assert!(e.contains("namespace"), "must mention the namespace: {e}");
}
#[test]
fn hook_failed_displays_identity_and_command() {
let e = MindError::HookFailed {
identity: "github.com/acme/tools".into(),
command: "make install".into(),
status: None,
stderr: "boom".into(),
printed_output: false,
};
let msg = e.to_string();
assert!(msg.contains("github.com/acme/tools"), "msg: {msg}");
assert!(msg.contains("make install"), "msg: {msg}");
assert!(msg.contains("boom"), "msg: {msg}");
}
#[test]
fn hook_failed_silent_exit_renders_no_output() {
let e = MindError::HookFailed {
identity: "github.com/acme/tools".into(),
command: "exit 1".into(),
status: None,
stderr: String::new(),
printed_output: false,
};
let msg = e.to_string();
assert!(
msg.contains("(no output)"),
"silent failure must say '(no output)', not 'see the hook's output above': {msg}"
);
assert!(
!msg.contains("see the hook"),
"must not point to framed output when nothing was printed: {msg}"
);
}
#[test]
fn hook_failed_with_stderr_renders_stderr_not_no_output() {
let e = MindError::HookFailed {
identity: "github.com/acme/tools".into(),
command: "make install".into(),
status: None,
stderr: "some diagnostic".into(),
printed_output: false,
};
let msg = e.to_string();
assert!(
msg.contains("some diagnostic"),
"stderr content must appear in the message: {msg}"
);
assert!(
!msg.contains("(no output)"),
"must not say '(no output)' when stderr was captured: {msg}"
);
}
#[test]
fn hook_failed_with_printed_output_renders_see_output_above_not_no_output() {
let e = MindError::HookFailed {
identity: "github.com/acme/tools".into(),
command: "make install".into(),
status: None,
stderr: String::new(),
printed_output: true,
};
let msg = e.to_string();
assert!(
msg.contains("(see output above)"),
"printed_output=true must say '(see output above)': {msg}"
);
assert!(
!msg.contains("(no output)"),
"must not say '(no output)' when output was already shown: {msg}"
);
assert!(
msg.contains("github.com/acme/tools"),
"missing identity: {msg}"
);
assert!(msg.contains("make install"), "missing command: {msg}");
}
#[test]
fn skill_collision_renders_conflict_list_and_namespace_hint() {
let e = MindError::SkillCollision {
conflicts: vec![
(
"skill".into(),
"review".into(),
"github.com/acme/agents".into(),
),
(
"rule".into(),
"style".into(),
"github.com/acme/rules".into(),
),
],
suggested: "acme".into(),
};
let msg = e.to_string();
assert!(
msg.contains("name collision"),
"must contain 'name collision': {msg}"
);
assert!(
msg.contains("skill:review"),
"must list skill:review: {msg}"
);
assert!(msg.contains("rule:style"), "must list rule:style: {msg}");
assert!(
msg.contains("github.com/acme/agents"),
"must name the existing source: {msg}"
);
assert!(
msg.contains("--namespace 'acme'"),
"must suggest --namespace with the shell-quoted repo name (CLI-225): {msg}"
);
}
#[test]
fn hook_failed_printed_output_priority_over_stderr_content() {
let e = MindError::HookFailed {
identity: "github.com/acme/tools".into(),
command: "make install".into(),
status: None,
stderr: "some content".into(),
printed_output: true,
};
let msg = e.to_string();
assert!(
msg.contains("(see output above)"),
"printed_output=true must take priority: {msg}"
);
assert!(
!msg.contains("(no output)"),
"must not say '(no output)': {msg}"
);
}
#[test]
fn item_not_found_no_sources_hints_meld() {
let e = MindError::ItemNotFound {
query: "review".into(),
sources: 0,
}
.to_string();
assert!(e.contains("review"), "must include query: {e}");
assert!(
e.contains("meld"),
"no-sources hint must mention `meld`: {e}"
);
assert!(
!e.contains("sync"),
"no-sources path must not suggest `sync`: {e}"
);
}
#[test]
fn item_not_found_with_sources_hints_probe_not_sync() {
let e = MindError::ItemNotFound {
query: "review".into(),
sources: 3,
}
.to_string();
assert!(e.contains("review"), "must include query: {e}");
assert!(e.contains("3"), "must include source count: {e}");
assert!(e.contains("probe"), "must mention `probe`: {e}");
assert!(
!e.contains("sync"),
"with sources must not mention `sync`: {e}"
);
assert!(
!e.contains("mind meld") && !e.contains("meld <"),
"with sources must not suggest `meld`: {e}"
);
}
#[test]
fn source_not_found_hints_recall_sources() {
let e = MindError::SourceNotFound {
name: "jaemk/minds".into(),
}
.to_string();
assert!(e.contains("jaemk/minds"), "must include the name: {e}");
assert!(
e.contains("mind recall --sources"),
"must hint the exact next command: {e}"
);
}
#[test]
fn unknown_lobe_hints_config_lobes_list() {
let e = MindError::UnknownLobe {
path: "/no/such/lobe".into(),
}
.to_string();
assert!(e.contains("/no/such/lobe"), "must include the path: {e}");
assert!(
e.contains("mind config lobes list"),
"must hint the exact next command: {e}"
);
}
#[test]
fn link_occupied_includes_force_hint() {
let e = MindError::LinkOccupied {
path: "/home/user/.claude/skills/foo".into(),
}
.to_string();
assert!(
e.contains("--force"),
"LinkOccupied must mention --force: {e}"
);
assert!(
e.contains("/home/user/.claude/skills/foo"),
"must include the path: {e}"
);
}
#[test]
fn digest_mismatch_includes_url_and_digests() {
let e = MindError::DigestMismatch {
url: "https://example.com/mind-0.1.0.tar.gz".into(),
expected: "abc123".into(),
actual: "def456".into(),
}
.to_string();
assert!(e.contains("abc123"), "must include expected digest: {e}");
assert!(e.contains("def456"), "must include actual digest: {e}");
assert!(
e.contains("https://example.com/mind-0.1.0.tar.gz"),
"must include URL: {e}"
);
}
#[test]
fn attestation_verification_failed_names_reason_and_kind() {
let e = MindError::AttestationVerificationFailed {
reason: "no attestations found".into(),
};
let msg = e.to_string();
assert!(
msg.contains("no attestations found"),
"must include the reason: {msg}"
);
assert!(
msg.contains("aborting"),
"must say evolve is aborting: {msg}"
);
assert_eq!(e.kind(), "attestation-verification-failed");
}
#[test]
fn state_too_new_names_file_and_versions() {
let e = MindError::StateTooNew {
what: "sources.json",
found: 3,
supported: 1,
}
.to_string();
assert!(e.contains("sources.json"), "must name the file: {e}");
assert!(e.contains("3"), "must name the found version: {e}");
assert!(e.contains("1"), "must name the supported version: {e}");
assert!(e.contains("mind evolve"), "must suggest `mind evolve`: {e}");
}
#[test]
fn incompatible_version_remedy_points_at_evolve() {
let e = MindError::IncompatibleVersion {
source_name: "github.com/acme/agents".into(),
required: "0.30.0".into(),
running: "0.22.0".into(),
}
.to_string();
assert!(e.contains("github.com/acme/agents"), "{e}");
assert!(e.contains("0.30.0") && e.contains("0.22.0"), "{e}");
assert!(
e.contains("mind evolve"),
"must suggest `mind evolve`, not `mind upgrade`: {e}"
);
assert!(
!e.contains("upgrade mind"),
"must not say 'upgrade mind' (upgrade upgrades items, not the binary): {e}"
);
}
#[test]
fn unsafe_prefix_error_mentions_prefix() {
let e = MindError::UnsafePrefix {
prefix: "../evil".into(),
}
.to_string();
assert!(e.contains("../evil"), "must include the prefix: {e}");
}
#[test]
fn kind_slugs_are_stable() {
assert_eq!(
MindError::ItemNotFound {
query: "x".into(),
sources: 0
}
.kind(),
"item-not-found"
);
assert_eq!(
MindError::DigestMismatch {
url: "u".into(),
expected: "e".into(),
actual: "a".into()
}
.kind(),
"digest-mismatch"
);
assert_eq!(
MindError::SelfUpdatePolicy { detail: "d".into() }.kind(),
"self-update-policy"
);
assert_eq!(MindError::HomeDirNotFound.kind(), "home-dir-not-found");
assert_eq!(MindError::GitNotFound.kind(), "git-not-found");
assert_eq!(MindError::ReleaseAssetEmpty.kind(), "release-asset-empty");
assert_eq!(MindError::LobeTargetRequired.kind(), "lobe-target-required");
assert_eq!(
MindError::BuildEventRequiresItemTarget.kind(),
"build-event-requires-item-target"
);
assert_eq!(
MindError::HookAborted { label: "h".into() }.kind(),
"hook-aborted"
);
assert_eq!(
MindError::BadItemLink {
url: "u".into(),
reason: "r".into()
}
.kind(),
"bad-item-link"
);
assert_eq!(
MindError::LinkedSourceGone {
source_name: "s".into(),
path: "p".into(),
}
.kind(),
"linked-source-gone"
);
assert_eq!(
MindError::HooksNotRun {
target: "t".into(),
event: "install".into(),
skipped: 1,
resolved: vec!["t".into()],
}
.kind(),
"hooks-not-run"
);
let samples: &[(&str, &MindError)] = &[
(
"item-not-found",
&MindError::ItemNotFound {
query: "x".into(),
sources: 0,
},
),
("home-dir-not-found", &MindError::HomeDirNotFound),
("git-not-found", &MindError::GitNotFound),
];
for (expected, err) in samples {
let slug = err.kind();
assert_eq!(slug, *expected, "slug mismatch for variant");
assert!(
!slug.is_empty(),
"slug must be non-empty for variant {expected}"
);
assert!(
slug.chars().all(|c| c.is_ascii_lowercase() || c == '-'),
"slug must be lowercase kebab-case: {slug}"
);
}
}
#[test]
fn linked_source_gone_names_source_and_remedy() {
let e = MindError::LinkedSourceGone {
source_name: "local/tmp/starter".into(),
path: "/tmp/starter".into(),
};
let msg = e.to_string();
assert!(
msg.contains("local/tmp/starter"),
"must name the source: {msg}"
);
assert!(msg.contains("/tmp/starter"), "must name the path: {msg}");
assert!(
msg.contains("mind unmeld 'local/tmp/starter'"),
"must give the copy-pasteable, shell-quoted unmeld remedy (CLI-225): {msg}"
);
}
#[test]
fn hooks_not_run_names_target_count_and_remedy() {
let e = MindError::HooksNotRun {
target: "myrepo".into(),
event: "install".into(),
skipped: 2,
resolved: vec!["myrepo".into()],
};
let msg = e.to_string();
assert!(msg.contains("myrepo"), "must name the target: {msg}");
assert!(msg.contains('2'), "must name the skipped count: {msg}");
assert!(
msg.contains(
"mind hooks run 'myrepo' --event install --dangerously-skip-install-hook-check"
),
"must give the copy-pasteable, shell-quoted remedy: {msg}"
);
}
#[test]
fn hooks_not_run_remedy_carries_the_selected_event() {
let e = MindError::HooksNotRun {
target: "myrepo".into(),
event: "uninstall".into(),
skipped: 1,
resolved: vec!["myrepo".into()],
};
let msg = e.to_string();
assert!(
msg.contains(
"mind hooks run 'myrepo' --event uninstall --dangerously-skip-install-hook-check"
),
"the remedy must re-select the uninstall event: {msg}"
);
assert!(
!msg.contains("--event install"),
"the remedy must not name a different event than the run selected: {msg}"
);
}
#[test]
fn hooks_not_run_several_resolved_targets_lists_names_not_one_fake_command() {
let e = MindError::HooksNotRun {
target: "*".into(),
event: "install".into(),
skipped: 3,
resolved: vec!["github.com/a/one".into(), "github.com/a/two".into()],
};
let msg = e.to_string();
assert!(msg.contains('3'), "must name the skipped count: {msg}");
assert!(
msg.contains("github.com/a/one") && msg.contains("github.com/a/two"),
"must list every resolved identity: {msg}"
);
assert!(
!msg.contains("mind hooks run * --event"),
"must never echo the raw glob selector back into a runnable-looking \
remedy command: {msg}"
);
assert!(
!msg.contains(
"mind hooks run github.com/a/one --event install \
--dangerously-skip-install-hook-check"
) && !msg.contains(
"mind hooks run github.com/a/two --event install \
--dangerously-skip-install-hook-check"
),
"must not synthesize a single command for either resolved name either \
(both had work, so neither alone is the whole remedy): {msg}"
);
}
#[test]
fn invalid_repo_spec_names_local_path_forms() {
let e = MindError::InvalidRepoSpec {
spec: "bogus".into(),
};
let msg = e.to_string();
assert!(msg.contains("./rel/path"), "must mention './': {msg}");
assert!(msg.contains("../rel/path"), "must mention '../': {msg}");
assert!(
msg.contains("/abs/path"),
"must mention an absolute path: {msg}"
);
assert!(msg.contains("file://"), "must mention 'file://': {msg}");
}
#[test]
fn lobe_base_missing_displays_path() {
let path = std::path::PathBuf::from("/nonexistent/myproject");
let e = MindError::LobeBaseMissing { path: path.clone() };
let msg = e.to_string();
assert!(
msg.contains("/nonexistent/myproject"),
"must include the path: {msg}"
);
assert!(
msg.contains("does not exist"),
"must say directory does not exist: {msg}"
);
assert_eq!(e.kind(), "lobe-base-missing", "kind slug must be stable");
}
#[test]
fn unsafe_clone_path_names_path_and_identity() {
let e = MindError::UnsafeClonePath {
path: PathBuf::from("/home/user/evil"),
identity: "../../victim".into(),
};
let msg = e.to_string();
assert!(
msg.contains("/home/user/evil"),
"must include the offending path: {msg}"
);
assert!(
msg.contains("../../victim"),
"must include the source identity: {msg}"
);
assert!(
msg.contains("sources tree"),
"must say it resolves outside the sources tree: {msg}"
);
assert_eq!(e.kind(), "unsafe-clone-path", "kind slug must be stable");
}
#[test]
fn ambiguous_hook_target_names_both_disambiguated_forms() {
let e = MindError::AmbiguousHookTarget {
target: "local/base/repo#dev".into(),
item_forms: vec!["local/base/repo#skill:dev".into()],
};
let msg = e.to_string();
assert!(
msg.contains("local/base/repo#dev"),
"must name the ambiguous target: {msg}"
);
assert!(
msg.contains("source:local/base/repo#dev"),
"must name the source-targeting escape: {msg}"
);
assert!(
msg.contains("local/base/repo#skill:dev"),
"must name the kind-qualified item escape: {msg}"
);
assert_eq!(
e.kind(),
"ambiguous-hook-target",
"kind slug must be stable"
);
}
#[test]
fn ambiguous_hook_target_shell_quotes_both_runnable_examples() {
use std::process::Command;
let target = "x'; touch /tmp/mind-hook-105-pwned; echo '`id`$(id)".to_string();
let item_form =
"local/base/x'; touch /tmp/mind-hook-105-item-pwned; echo '#skill:x".to_string();
let e = MindError::AmbiguousHookTarget {
target: target.clone(),
item_forms: vec![item_form.clone()],
};
let msg = e.to_string();
let quoted_source = shell_quote(&format!("source:{target}"));
assert!(
msg.contains(&format!("mind hooks run {quoted_source}")),
"the source-targeting example must be shell-quoted: {msg}"
);
assert!(
!msg.contains(&format!("mind hooks run 'source:{target}'")),
"must never fall back to the broken bare `'source:{{target}}'` \
interpolation: {msg}"
);
let quoted_item = shell_quote(&item_form);
assert!(
msg.contains(&format!("mind hooks run {quoted_item}")),
"the item-targeting example must be shell-quoted: {msg}"
);
assert!(
!msg.contains(&format!("mind hooks run '{item_form}'")),
"must never fall back to the broken bare `'{{item_form}}'` \
interpolation: {msg}"
);
if Command::new("sh").arg("-c").arg("true").status().is_err() {
return;
}
for (sentinel_path, command) in [
(
"/tmp/mind-hook-105-pwned",
format!("mind hooks run {quoted_source}"),
),
(
"/tmp/mind-hook-105-item-pwned",
format!("mind hooks run {quoted_item}"),
),
] {
let sentinel = std::path::Path::new(sentinel_path);
let _ = std::fs::remove_file(sentinel);
let probe = command.replacen("mind hooks run", "printf '%s\\n'", 1);
let out = Command::new("sh")
.arg("-c")
.arg(&probe)
.output()
.expect("sh -c must run");
assert!(
out.status.success(),
"the quoted example must parse as one shell argument: {probe:?}\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!sentinel.exists(),
"the injected 'touch' must not have executed via the printed \
example: {probe:?}"
);
let _ = std::fs::remove_file(sentinel);
}
}
#[test]
fn hooks_not_run_message_whole_framed_remedy_is_inert_when_pasted_with_the_frame() {
use std::process::Command;
if Command::new("sh").arg("-c").arg("true").status().is_err() {
return;
}
let sentinel = std::path::Path::new("/tmp/mind-hook-106-whole-frame-pwned");
let _ = std::fs::remove_file(sentinel);
let evil = "$(touch /tmp/mind-hook-106-whole-frame-pwned)`touch /tmp/mind-hook-106-whole-frame-pwned`".to_string();
let msg = hooks_not_run_message("target", "install", 1, std::slice::from_ref(&evil));
let lead_at = msg
.find("re-run unattended with:")
.expect("message has a remedy frame");
let framed_remedy = &msg[lead_at..];
let _ = Command::new("sh").arg("-c").arg(framed_remedy).output();
assert!(
!sentinel.exists(),
"pasting the whole framed remedy (frame included) must not execute \
the embedded command substitution: {framed_remedy:?}"
);
let _ = std::fs::remove_file(sentinel);
}
#[test]
fn self_update_policy_displays_detail() {
let disabled = MindError::SelfUpdatePolicy {
detail: "self-update is disabled by the managed policy".into(),
}
.to_string();
assert!(
disabled.contains("disabled by the managed policy"),
"disabled detail must appear: {disabled}"
);
let mismatch = MindError::SelfUpdatePolicy {
detail:
"managed policy pins self-update to 0.14.0; --version 0.15.0 conflicts with the pin"
.into(),
}
.to_string();
assert!(mismatch.contains("0.14.0"), "must name the pin: {mismatch}");
assert!(
mismatch.contains("0.15.0"),
"must name the requested version: {mismatch}"
);
assert!(
mismatch.contains("conflicts"),
"must say 'conflicts': {mismatch}"
);
}
fn assert_command_inert(command: &str, verb_prefix: &str, sentinel: &str, expected_arg: &str) {
assert!(
command.starts_with(verb_prefix),
"extracted command must start with the verb: {command:?}"
);
if Command::new("sh").arg("-c").arg("true").status().is_err() {
return;
}
let sentinel_path = std::path::Path::new(sentinel);
let _ = std::fs::remove_file(sentinel_path);
let probe = command.replacen(verb_prefix, "printf '%s\\n'", 1);
let out = Command::new("sh")
.arg("-c")
.arg(&probe)
.output()
.expect("sh -c must run");
assert!(
!sentinel_path.exists(),
"the injected command fired via the printed remedy: {probe:?}"
);
let _ = std::fs::remove_file(sentinel_path);
assert!(
out.status.success(),
"the quoted remedy must parse as shell arguments: {probe:?}\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
String::from_utf8_lossy(&out.stdout).contains(expected_arg),
"the identity must survive as one literal argument: stdout {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn linked_source_gone_remedy_shell_quotes_the_source_identity() {
let evil = "s'; touch /tmp/mind-lsg-pwned; echo '`id`$(id)";
let e = MindError::LinkedSourceGone {
source_name: evil.to_string(),
path: "/gone".into(),
};
let msg = e.to_string();
let command = format!("mind unmeld {}", shell_quote(evil));
assert!(
msg.contains(&command),
"the unmeld remedy must carry the shell-quoted identity: {msg}"
);
assert!(
!msg.contains(&format!("mind unmeld '{evil}'")),
"must never fall back to the broken single-quote-framed raw name: {msg}"
);
assert_command_inert(&command, "mind unmeld", "/tmp/mind-lsg-pwned", evil);
}
#[test]
fn agent_collision_remedy_shell_quotes_the_forget_argument() {
let evil = "dev'; touch /tmp/mind-agentcol-pwned; echo '`id`";
let e = MindError::AgentCollision {
name: evil.to_string(),
existing: "github.com/a/one".into(),
incoming: "github.com/a/two".into(),
};
let msg = e.to_string();
let command = format!("mind forget {}", shell_quote(&format!("agent:{evil}")));
assert!(
msg.contains(&command),
"the forget remedy must carry the shell-quoted agent ref: {msg}"
);
assert!(
!msg.contains(&format!("mind forget agent:{evil}")),
"must never splice the raw agent name into the runnable command: {msg}"
);
assert_command_inert(
&command,
"mind forget",
"/tmp/mind-agentcol-pwned",
&format!("agent:{evil}"),
);
}
#[test]
fn skill_collision_remedy_shell_quotes_the_suggested_prefix() {
let evil = "fork'; touch /tmp/mind-skillcol-pwned; echo '`id`";
let e = MindError::SkillCollision {
conflicts: vec![(
"skill".into(),
"review".into(),
"github.com/acme/agents".into(),
)],
suggested: evil.to_string(),
};
let msg = e.to_string();
assert!(
msg.contains(&format!(
"mind meld --namespace {} <repo>",
shell_quote(evil)
)),
"the meld remedy must carry the shell-quoted prefix: {msg}"
);
assert!(
!msg.contains(&format!("--namespace {evil}")),
"must never interpolate the raw prefix into the runnable command: {msg}"
);
let command = format!("mind meld --namespace {}", shell_quote(evil));
assert_command_inert(&command, "mind meld", "/tmp/mind-skillcol-pwned", evil);
}
use std::sync::atomic::{AtomicU32, Ordering};
static CAP_N: AtomicU32 = AtomicU32::new(0);
fn cap_tmp(label: &str) -> PathBuf {
let n = CAP_N.fetch_add(1, Ordering::SeqCst);
std::env::temp_dir().join(format!("mind-metacap-{}-{label}-{n}", std::process::id()))
}
#[test]
fn read_capped_metadata_reads_a_normal_file() {
let path = cap_tmp("ok");
std::fs::write(&path, "hello metadata").unwrap();
let text = read_capped_metadata(&path).expect("small file must read fine");
assert_eq!(text, "hello metadata");
let _ = std::fs::remove_file(&path);
}
#[test]
fn read_capped_metadata_refuses_an_oversized_file_naming_it() {
let path = cap_tmp("big");
let file = std::fs::File::create(&path).unwrap();
file.set_len(METADATA_SIZE_LIMIT + 1).unwrap();
drop(file);
let err = read_capped_metadata(&path).expect_err("oversized file must be refused");
match &err {
MindError::MetadataTooLarge { path: p, limit } => {
assert_eq!(p, &path, "error must name the offending file");
assert_eq!(*limit, METADATA_SIZE_LIMIT, "error must name the limit");
}
other => panic!("expected MetadataTooLarge, got: {other:?}"),
}
let msg = err.to_string();
assert!(
msg.contains(&path.display().to_string()),
"message must name the file: {msg}"
);
assert!(msg.contains("8 MiB"), "message must name the limit: {msg}");
assert!(
msg.contains("trim") || msg.contains("move"),
"message must say what to do: {msg}"
);
assert_eq!(err.kind(), "metadata-too-large", "kind slug must be stable");
let _ = std::fs::remove_file(&path);
}
#[test]
fn read_capped_metadata_a_file_exactly_at_the_limit_is_accepted() {
let path = cap_tmp("exact");
let file = std::fs::File::create(&path).unwrap();
file.set_len(METADATA_SIZE_LIMIT).unwrap();
drop(file);
let text = read_capped_metadata(&path).expect("a file exactly at the cap must be OK");
assert_eq!(text.len() as u64, METADATA_SIZE_LIMIT);
let _ = std::fs::remove_file(&path);
}
#[test]
fn read_capped_metadata_missing_file_is_plain_io_error() {
let path = cap_tmp("missing");
let err = read_capped_metadata(&path).unwrap_err();
assert!(
matches!(err, MindError::Io { .. }),
"missing file must be a plain Io error: {err:?}"
);
}
}