KeiSeiKit-1.0/_primitives/_rust/kei-runtime/src/main.rs
Parfii-bot 913d62c280 fix(security): RCE allowlist + WebSocket auth + SSH option-injection
Group D — three independent security primitives hardening (post-audit 2026-05-02).

kei-runtime — atom invoke RCE allowlist:
- invoke.rs: is_safe_crate_name validator (regex ^kei-[a-z][a-z0-9-]+$);
             rejects /, \\, .., :, absolute paths, empty, >128 chars.
             InvalidAtom error variant.
             stdout/stderr capped at 16 MiB (was unbounded).
- main.rs: InvalidAtom mapped to exit code 2.
- tests/invoke_exit_codes_smoke.rs: invoke_unsafe_crate_name_exits_2 added.
- Closes: any user able to write atoms/*.md with crate_name: "rm" or "sudo"
           triggered arbitrary command execution.

kei-graph-stream — WebSocket bearer + Origin:
- auth.rs (new, 142 LOC): token load + bearer extraction + Origin allowlist +
                            ConstantTimeEq compare; 8 unit tests.
- ws.rs: ws_handler validates Origin + bearer before upgrade (403/401 on failure).
- main.rs: --public-bind-i-accept-the-leak flag required for non-loopback bind;
            else bail!() with explicit error.
- tests/smoke.rs: rewritten with Origin + bearer headers via connect_async_with_config.
- Closes: WebSocket /stream had zero auth, zero Origin check; browser CSWSH could
           subscribe to agent activity broadcast; KEI_GRAPH_STREAM_BIND env silently
           accepted any SocketAddr.

kei-compute-baremetal — SSH option injection (CVE-2023-51385 class):
- ssh.rs: is_safe_user + is_safe_host validators (alphanumeric + -_.; reject leading -;
           max 64 chars; no @, :, /, \\, space).
- ssh.rs: -- sentinel before user@host argv (OpenSSH 9.6+ stops flag parsing).
- ssh.rs: StrictHostKeyChecking=yes default; KEI_BAREMETAL_ACCEPT_NEW=1 for TOFU.
- error.rs: InvalidRegion variant.
- provider.rs: validators applied in target_for_spec + target_for_handle.
- Closes: spec.region "-oProxyCommand=evil" triggered local RCE before TCP connect.

Test results: 29 passed; 0 failed across all three crates. cargo check clean.

Findings: RCE allowlist (Wave-A) + WebSocket auth (Wave-B) + SSH injection (Wave-B)
were unique-per-retest discoveries. None present in original wave-1 audit.

Note: kei-compute-baremetal/src/provider.rs at 300 LOC (was 268; +32 from validators).
Pre-existing >200 LOC violation, fix scope was security-additions only. Follow-up:
split provider.rs into provider.rs (<200) + provider_tests.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:40:24 +08:00

165 lines
5.2 KiB
Rust

//! kei-runtime — CLI dispatcher.
//!
//! Subcommands: list-atoms | invoke | schema-lint | pipe (stub).
//! Default --root: `~/.claude/agents/_primitives/_rust`.
use clap::{Parser, Subcommand};
use kei_runtime::invoke::InvokeError;
use kei_runtime::{discover, invoke, lint};
use std::path::PathBuf;
use std::process::ExitCode;
/// Exit code returned when `invoke` lands on a not-yet-wired atom.
/// Distinct from exit 2 (atom rejected input) so CI can branch.
/// Chosen in the EX_USAGE range per `sysexits.h` convention.
const EXIT_INVOKE_NOT_IMPLEMENTED: u8 = 64;
#[derive(Parser)]
#[command(name = "kei-runtime", version, about = "Atom invocation runtime + schema linter")]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// List atoms discovered under --root.
ListAtoms {
#[arg(long)]
root: Option<PathBuf>,
#[arg(long = "crate")]
crate_name: Option<String>,
#[arg(long)]
kind: Option<String>,
},
/// Invoke one atom (MVP stub — see docs).
Invoke {
atom_id: String,
#[arg(long)]
input: String,
#[arg(long)]
root: Option<PathBuf>,
},
/// Lint every `atoms/*.md` under --root for schema correctness.
SchemaLint {
#[arg(long)]
root: Option<PathBuf>,
#[arg(long = "crate")]
crate_name: Option<String>,
},
/// Execute a pipeline (not yet implemented).
Pipe { dag: PathBuf },
}
fn main() -> ExitCode {
let cli = Cli::parse();
match cli.cmd {
Cmd::ListAtoms { root, crate_name, kind } => {
run_list_atoms(resolve_root(root), crate_name, kind)
}
Cmd::Invoke { atom_id, input, root } => run_invoke(resolve_root(root), atom_id, input),
Cmd::SchemaLint { root, crate_name } => run_lint(resolve_root(root), crate_name),
Cmd::Pipe { dag: _ } => {
println!("pipe: not yet implemented");
ExitCode::SUCCESS
}
}
}
fn resolve_root(arg: Option<PathBuf>) -> PathBuf {
if let Some(p) = arg {
return p;
}
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(".claude/agents/_primitives/_rust")
}
fn run_list_atoms(root: PathBuf, crate_name: Option<String>, kind: Option<String>) -> ExitCode {
let atoms = discover::walk_atoms(&root);
for a in atoms {
if let Some(c) = &crate_name {
if a.crate_name != *c {
continue;
}
}
if let Some(k) = &kind {
if a.kind.as_str() != k.as_str() {
continue;
}
}
println!("{}\t{}\t{}", a.full_id, a.kind.as_str(), a.md_path.display());
}
ExitCode::SUCCESS
}
fn run_invoke(root: PathBuf, atom_id: String, input_arg: String) -> ExitCode {
let input_text = match load_input(&input_arg) {
Ok(s) => s,
Err(e) => {
eprintln!("input: {e}");
return ExitCode::from(1);
}
};
match invoke::invoke(&root, &atom_id, &input_text) {
Ok(out) => {
println!("{}", serde_json::to_string(&out).unwrap_or_default());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{e}");
ExitCode::from(invoke_exit_code(&e))
}
}
}
/// Map typed invoke errors to exit codes per locked §Runtime schema.
///
/// - `AtomNotFound|InputParse|InputInvalid|MissingInputSchema|InvalidAtom` → 2 (atom error)
/// - `AtomFailed { code, .. }` → passthrough child exit code
/// - `SubprocessError|OutputParse` → 1 (IO / malformed output)
/// - `BinaryNotFound` → 127 (POSIX command-not-found)
/// - `NotImplemented` → 64 (legacy escape)
fn invoke_exit_code(err: &InvokeError) -> u8 {
match err {
InvokeError::AtomNotFound(_)
| InvokeError::InputParse(_)
| InvokeError::InputInvalid(_)
| InvokeError::MissingInputSchema(_)
| InvokeError::InvalidAtom(_) => 2,
InvokeError::AtomFailed { code, .. } => {
let c = *code;
if (0..=255).contains(&c) { c as u8 } else { 1 }
}
InvokeError::SubprocessError(_) | InvokeError::OutputParse(_) => 1,
InvokeError::BinaryNotFound { .. } => 127,
InvokeError::NotImplemented { .. } => EXIT_INVOKE_NOT_IMPLEMENTED,
}
}
fn load_input(arg: &str) -> Result<String, String> {
if let Some(path) = arg.strip_prefix('@') {
std::fs::read_to_string(path).map_err(|e| format!("read {path}: {e}"))
} else {
Ok(arg.to_string())
}
}
fn run_lint(root: PathBuf, crate_filter: Option<String>) -> ExitCode {
let report = lint::schema_lint(&root);
print_lint(&report, crate_filter.as_deref());
if report.failed.is_empty() {
ExitCode::SUCCESS
} else {
ExitCode::from(2)
}
}
fn print_lint(report: &lint::LintReport, crate_filter: Option<&str>) {
let keep = |label: &str| crate_filter.is_none_or(|f| label.contains(f));
for label in report.passed.iter().filter(|l| keep(l)) {
println!("PASS\t{label}");
}
for (label, errs) in report.failed.iter().filter(|(l, _)| keep(l)) {
println!("FAIL\t{label}\t{}", errs.join(" | "));
}
}