Skip to main content

mail4agent_messenger_shell/cli/
launch.rs

1//! `m4a-agent launch --provider <codex|kimi|...> [--session ID] [--nick NICK] -- <command...>`:
2//! the per-session launcher for providers with no env-file hook (codex, kimi). It provisions this
3//! session's account (one per provider session; a fresh session id when none is given), issues a
4//! fresh capability, and starts the provider with this session's env only: `M4A_SESSION_ID`
5//! (the account's own login id), `M4A_NICK`/`M4A_BOT_NAME`, `M4A_STORE_ROOT` (the account's own
6//! root) and `M4A_SESSION_CAP`. Nothing is printed but the nick; the cap never leaves the child's
7//! environment. Exit code is the child's.
8
9use std::path::PathBuf;
10use std::process::Command;
11
12use crate::session_account::{provision_session, SESSION_CAP_ENV};
13use crate::{ProviderKind, STORE_ROOT_ENV};
14
15fn fail(msg: &str) -> ! {
16    eprintln!("m4a-agent launch: {msg}");
17    std::process::exit(64);
18}
19
20pub fn run(args: Vec<String>) {
21    let (mut provider, mut session, mut nick) = (None, None, None);
22    let mut it = args.into_iter();
23    let mut cmd: Vec<String> = Vec::new();
24    while let Some(a) = it.next() {
25        match a.as_str() {
26            "--provider" => provider = it.next(),
27            "--session" => session = it.next(),
28            "--nick" => nick = it.next(),
29            "--" => {
30                cmd.extend(it.by_ref());
31                break;
32            }
33            other => fail(&format!("unknown argument {other}")),
34        }
35    }
36    let provider = provider.as_deref().and_then(ProviderKind::parse).unwrap_or_else(|| fail("--provider is required"));
37    if cmd.is_empty() {
38        fail("usage: m4a-agent launch --provider P [--session ID] [--nick NICK] -- <command...>");
39    }
40    let base = std::env::var(crate::ACCOUNTS_ROOT_ENV)
41        .ok()
42        .filter(|v| !v.is_empty())
43        .or_else(|| std::env::var(STORE_ROOT_ENV).ok().filter(|v| !v.is_empty()))
44        .map(PathBuf::from)
45        .unwrap_or_else(|| fail("M4A_STORE_ROOT (or M4A_ACCOUNTS_ROOT) is required"));
46    let session = session.unwrap_or_else(fresh_session_id);
47    // A new process of the session gets a new cap; the old one stops working.
48    let made = provision_session(&base, provider, &session, nick.as_deref(), true).unwrap_or_else(|e| fail(&e.to_string()));
49    #[cfg(unix)]
50    if made.entry.os_uid.is_none() {
51        if let Ok(mut reg) = crate::session_account::AccountRegistry::open(&base) {
52            if let Ok(id) = crate::session_account::SessionIdentity::new(provider, &session) {
53                let _ = reg.set_os_uid(&id, crate::ipc_own_uid());
54            }
55        }
56    }
57    // Server enroll per account, by the launcher (never by the agent): only when the operator put
58    // an invite for this nick in the launcher's env and the account is not enrolled yet.
59    if made.entry.state != crate::session_account::ServerState::Enrolled {
60        if let Some(invite) = std::env::var(crate::PRODUCT_INVITE_ENV).ok().filter(|v| !v.is_empty()) {
61            let me = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("m4a-agent"));
62            let mut enroll = Command::new(me);
63            enroll.arg("enroll").env(crate::PRODUCT_INVITE_ENV, invite).env_remove(SESSION_CAP_ENV);
64            for (k, v) in made.session_env().into_iter().filter(|(k, _)| *k != SESSION_CAP_ENV) {
65                enroll.env(k, v);
66            }
67            match enroll.status() {
68                Ok(s) if s.success() => {}
69                _ => eprintln!("m4a-agent launch: enroll failed; the session starts local-only"),
70            }
71        }
72    }
73    eprintln!("m4a-agent launch: session account {} ({})", made.entry.nick, provider.id());
74    let mut child = Command::new(&cmd[0]);
75    child.args(&cmd[1..]).env_remove(SESSION_CAP_ENV).env_remove(crate::PRODUCT_INVITE_ENV);
76    child.env("M4A_PROVIDER", provider.id()).env("M4A_PROVIDER_SESSION_ID", &session);
77    for (k, v) in made.session_env() {
78        child.env(k, v);
79    }
80    match child.status() {
81        Ok(status) => std::process::exit(status.code().unwrap_or(1)),
82        Err(err) => fail(&format!("could not start {}: {}", cmd[0], err.kind())),
83    }
84}
85
86fn fresh_session_id() -> String {
87    let raw = m4a_agent::vault::random_bytes(16);
88    let h: String = raw.iter().map(|b| format!("{b:02x}")).collect();
89    format!("launch-{h}")
90}