KeiSeiKit-1.0/_primitives/_rust/kei-spawn/src/ledger_sh.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

199 lines
6.8 KiB
Rust

//! Thin subprocess wrapper around the `kei-ledger` binary.
//!
//! kei-ledger is a bin-only crate (no lib.rs at the time kei-spawn was
//! introduced). We shell to it rather than replicate SQL — same process
//! model users expect, same DB file, same env contract (`KEI_LEDGER_DB`).
//!
//! Every call surfaces stderr on failure so orchestrator sees the real
//! ledger error (branch too long, duplicate id, etc.), not a wrapped one.
//!
//! # Security — `$PATH` hijack (CWE-427)
//!
//! The final fallback in [`ledger_bin`] is the bare name `"kei-ledger"`,
//! which `std::process::Command` resolves by walking `$PATH`. On a shared
//! or compromised machine an attacker-controlled directory earlier on
//! `$PATH` can provide a rogue `kei-ledger` that silently captures ledger
//! writes. To mitigate:
//!
//! * Set `KEI_LEDGER_BIN` to an **absolute path** in production / CI
//! (e.g. `/usr/local/bin/kei-ledger` or the cargo-install path), or
//! * Run integration tests via `cargo test` which populates the
//! `CARGO_BIN_EXE_kei-ledger` env var with the workspace-built binary.
//!
//! The env-override path is checked first in [`ledger_bin`] precisely so
//! a trusted operator can pin the binary and sidestep `$PATH` resolution.
use anyhow::{anyhow, Result};
use std::process::Command;
/// Resolve `kei-ledger` executable. Env override → CARGO env (tests) → PATH.
///
/// Lookup order:
/// 1. `KEI_LEDGER_BIN` — operator-pinned absolute path (recommended for
/// production; mitigates `$PATH` hijack per CWE-427, see module docs).
/// 2. `CARGO_BIN_EXE_kei-ledger` — set by `cargo test` for the workspace
/// binary under integration testing.
/// 3. `"kei-ledger"` — last-resort bare name; resolved via `$PATH` by
/// `std::process::Command`. Acceptable on single-user dev machines;
/// pin via `KEI_LEDGER_BIN` in any multi-user or CI context.
pub fn ledger_bin() -> String {
if let Ok(b) = std::env::var("KEI_LEDGER_BIN") {
return b;
}
// CARGO_BIN_EXE_kei-ledger is set for integration tests under workspace.
if let Ok(b) = std::env::var("CARGO_BIN_EXE_kei-ledger") {
return b;
}
"kei-ledger".into()
}
/// Test / sandbox escape hatch: when set, every ledger call is a no-op.
/// Integration tests use this to avoid needing the real kei-ledger binary
/// on PATH. Production callers MUST NOT set this env var.
fn is_noop() -> bool {
std::env::var("KEI_SPAWN_LEDGER_NOOP")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
/// Run `kei-ledger fork` with DNA + worktree metadata.
pub fn fork(
id: &str,
branch: &str,
parent: Option<&str>,
spec_sha: &str,
worktree: Option<&str>,
dna: Option<&str>,
) -> Result<()> {
if is_noop() {
let _ = (id, branch, parent, spec_sha, worktree, dna);
return Ok(());
}
let mut cmd = Command::new(ledger_bin());
cmd.args(["fork", id, branch, "--spec-sha", spec_sha]);
if let Some(p) = parent {
cmd.args(["--parent", p]);
}
if let Some(w) = worktree {
cmd.args(["--worktree", w]);
}
if let Some(d) = dna {
cmd.args(["--dna", d]);
}
run(&mut cmd, "fork")
}
/// Run `kei-ledger done <id> --summary <s>`.
pub fn done(id: &str, summary: &str) -> Result<()> {
if is_noop() {
let _ = (id, summary);
return Ok(());
}
let mut cmd = Command::new(ledger_bin());
cmd.args(["done", id, "--summary", summary]);
run(&mut cmd, "done")
}
/// Run `kei-ledger fail <id> --reason <r>`.
pub fn fail(id: &str, reason: &str) -> Result<()> {
if is_noop() {
let _ = (id, reason);
return Ok(());
}
let mut cmd = Command::new(ledger_bin());
cmd.args(["fail", id, "--reason", reason]);
run(&mut cmd, "fail")
}
/// Run `kei-ledger list --status running`. Returns raw stdout lines.
pub fn list_running() -> Result<String> {
if is_noop() {
return Ok(String::from("(noop: KEI_SPAWN_LEDGER_NOOP=1)\n"));
}
let mut cmd = Command::new(ledger_bin());
cmd.args(["list", "--status", "running"]);
let out = cmd.output().map_err(|e| anyhow!("spawn kei-ledger: {e}"))?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
return Err(anyhow!("kei-ledger list failed: {stderr}"));
}
Ok(String::from_utf8_lossy(&out.stdout).to_string())
}
fn run(cmd: &mut Command, stage: &str) -> Result<()> {
let out = cmd
.output()
.map_err(|e| anyhow!("spawn kei-ledger {stage}: {e}"))?;
if out.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
Err(anyhow!("kei-ledger {stage} failed: {stderr}"))
}
#[cfg(test)]
mod tests {
//! Regression coverage for [`ledger_bin`] lookup precedence.
//!
//! Env vars are process-global; serialize with a local mutex so
//! parallel cargo-test workers don't trample each other.
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_env<F: FnOnce()>(kei_bin: Option<&str>, cargo_bin: Option<&str>, f: F) {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev_kei = std::env::var("KEI_LEDGER_BIN").ok();
let prev_cargo = std::env::var("CARGO_BIN_EXE_kei-ledger").ok();
match kei_bin {
Some(v) => std::env::set_var("KEI_LEDGER_BIN", v),
None => std::env::remove_var("KEI_LEDGER_BIN"),
}
match cargo_bin {
Some(v) => std::env::set_var("CARGO_BIN_EXE_kei-ledger", v),
None => std::env::remove_var("CARGO_BIN_EXE_kei-ledger"),
}
f();
match prev_kei {
Some(v) => std::env::set_var("KEI_LEDGER_BIN", v),
None => std::env::remove_var("KEI_LEDGER_BIN"),
}
match prev_cargo {
Some(v) => std::env::set_var("CARGO_BIN_EXE_kei-ledger", v),
None => std::env::remove_var("CARGO_BIN_EXE_kei-ledger"),
}
}
#[test]
fn ledger_bin_env_overrides_default() {
with_env(Some("/opt/pinned/kei-ledger"), None, || {
assert_eq!(ledger_bin(), "/opt/pinned/kei-ledger");
});
}
#[test]
fn ledger_bin_cargo_env_used_when_kei_unset() {
with_env(None, Some("/tmp/cargo-built/kei-ledger"), || {
assert_eq!(ledger_bin(), "/tmp/cargo-built/kei-ledger");
});
}
#[test]
fn ledger_bin_falls_back_to_bare_name() {
with_env(None, None, || {
assert_eq!(ledger_bin(), "kei-ledger");
});
}
#[test]
fn ledger_bin_env_wins_over_cargo_env() {
with_env(
Some("/opt/pinned/kei-ledger"),
Some("/tmp/cargo-built/kei-ledger"),
|| {
assert_eq!(ledger_bin(), "/opt/pinned/kei-ledger");
},
);
}
}