Skip to main content

mecha_core/
lib.rs

1//! `mecha-core` — an agent harness for local models.
2//!
3//! The library knows nothing about any particular CLI, UI, or project. It gives
4//! you four things and lets you wire them together:
5//!
6//!   * [`provider`] — talk to a model (Anthropic, or anything OpenAI-shaped)
7//!   * [`tool`] — things the agent can do, native or [`mcp`]-backed
8//!   * [`agent`] — the loop that puts those together
9//!   * [`session`] / [`batch`] — persistence and fan-out around the loop
10//!
11//! ```no_run
12//! # async fn example() -> anyhow::Result<()> {
13//! use mecha_core::{agent::Agent, agent::Conversation, config::Config};
14//! use mecha_core::sandbox::Sandbox;
15//! use mecha_core::tool::{ModeApprover, Registry, ToolCtx};
16//! use std::sync::Arc;
17//!
18//! let cfg = Config::load(&std::env::current_dir()?)?;
19//! let (_, provider_cfg) = cfg.provider(None)?;
20//!
21//! // How `shell` is confined. It decides that tool's declared capabilities,
22//! // so it is built before the registry rather than consulted at call time.
23//! let sandbox = Arc::new(Sandbox::new(cfg.sandbox.clone()));
24//!
25//! let agent = Agent::new(
26//!     mecha_core::provider::build(provider_cfg)?,
27//!     Registry::new().with_builtins(&cfg.tools, sandbox),
28//!     Arc::new(ModeApprover { mode: cfg.tools.permission_mode }),
29//!     ToolCtx {
30//!         workspace: std::env::current_dir()?,
31//!         shell_timeout: std::time::Duration::from_secs(cfg.tools.shell_timeout_secs),
32//!         security: cfg.security.clone(),
33//!         ..ToolCtx::default()
34//!     },
35//!     cfg.agent.clone(),
36//!     None,
37//! )?;
38//!
39//! // A conversation carries its own taint, so keeping it across turns keeps
40//! // the trifecta interlock honest — see `agent::Conversation`.
41//! let mut convo = Conversation::user("What changed in this repo today?");
42//! let outcome = agent.run(&mut convo, None).await?;
43//! println!("{}", outcome.text);
44//! # Ok(())
45//! # }
46//! ```
47
48pub mod agent;
49pub mod appraisal;
50pub mod backlog;
51pub mod batch;
52pub mod boredom;
53pub mod cache_lens;
54pub mod candidate;
55pub mod capture;
56pub mod charter;
57pub mod compact;
58pub mod config;
59pub mod counterfactual;
60pub mod cron;
61pub mod diagnose;
62pub mod distill;
63pub mod doctor;
64pub mod eval;
65pub mod frontdoor;
66pub mod goal;
67pub mod gossip;
68pub mod guilt;
69pub mod harness;
70pub mod homeostat;
71pub mod hooks;
72pub mod image;
73pub mod learning;
74pub mod mail_triage;
75pub mod mailbox;
76pub mod mcp;
77pub mod message;
78pub mod onboarding;
79pub mod outbox;
80pub mod outbox_source;
81pub mod permit;
82pub mod pressure;
83pub mod provider;
84pub mod quarantine;
85pub mod questions;
86pub mod replay;
87pub mod replay_run;
88pub mod runlog;
89pub mod runmarker;
90pub mod sample;
91pub mod sandbox;
92pub mod search;
93pub mod session;
94pub mod skill;
95pub mod step;
96pub mod subagent;
97pub mod surface;
98pub(crate) mod text;
99pub mod tool;
100pub mod trigger;
101pub mod work;
102
103pub use agent::{Agent, AgentEvent, RunOutcome};
104pub use config::Config;
105pub use message::{Block, Effort, Message, Role, StopReason, Usage};
106
107pub const VERSION: &str = env!("CARGO_PKG_VERSION");
108
109/// Create a directory (and its parents) and make the leaf owner-only.
110///
111/// Transcripts, staged outbox drafts, the learning store and spilled tool
112/// output all carry the user's private data — mail bodies included, now that
113/// mail is wired — so their directories get the rule the mail token files
114/// already enforce on themselves (0600). The leaf only, on purpose: parents
115/// like `~/.mecha` also hold things the user may deliberately share, and the
116/// sensitive data lives below the leaf. Idempotent, and tightens a
117/// pre-existing directory too.
118/// Is this pid still around? `kill(pid, 0)` checks without delivering
119/// anything; `EPERM` means it exists and is not ours, which still counts.
120///
121/// The range check is not defensive padding — it is the whole correctness of
122/// the function. `kill(2)` gives non-positive pids entirely different
123/// meanings: `0` is "every process in my group", `-1` is "every process I may
124/// signal" (which succeeds, always), and any other negative is a process
125/// group. A corrupt marker holding one of those would report a long-dead run
126/// as alive and leave whatever owns the marker looking permanently busy in
127/// every UI that asks. Found by a test using `u32::MAX`, which sign-flips to exactly the
128/// `-1` case.
129pub fn process_alive(pid: u32) -> bool {
130    let Ok(pid) = libc::pid_t::try_from(pid) else {
131        return false;
132    };
133    if pid <= 0 {
134        return false;
135    }
136    // SAFETY: signal 0 delivers nothing and only probes for the process.
137    let rc = unsafe { libc::kill(pid, 0) };
138    rc == 0 || std::io::Error::last_os_error().kind() == std::io::ErrorKind::PermissionDenied
139}
140
141pub fn create_private_dir(dir: &std::path::Path) -> std::io::Result<()> {
142    std::fs::create_dir_all(dir)?;
143    #[cfg(unix)]
144    {
145        use std::os::unix::fs::PermissionsExt;
146        std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
147    }
148    Ok(())
149}
150
151#[cfg(test)]
152mod process_alive_tests {
153    /// The case that found the bug, kept beside the function now that more
154    /// than one subsystem depends on it: `u32::MAX` sign-flips to `-1`, which
155    /// `kill(2)` reads as "every process I may signal" and answers yes to.
156    #[test]
157    fn a_pid_that_is_not_a_pid_is_never_alive() {
158        assert!(!super::process_alive(u32::MAX));
159        assert!(!super::process_alive(0), "0 means my whole process group");
160        assert!(
161            !super::process_alive(i32::MAX as u32),
162            "real-looking and far above any pid_max"
163        );
164        assert!(
165            super::process_alive(std::process::id()),
166            "and the negative is not vacuous: we are alive"
167        );
168    }
169}