Port of upstream grok-build 1.0.1 (landed upstream 2026-08-06). The coordinator fire-and-forgot every spawn, so a wide fan-out (graph waves, swarm bursts, model-driven Task storms) could exhaust file descriptors. - kigi-tools task/admission: SubagentLimits — 32 concurrent per session by default (KIGI_MAX_CONCURRENT_SUBAGENTS), queue|fail behavior (KIGI_SUBAGENT_LIMIT_BEHAVIOR), zero clamped to 1, read once at the composition root; parse_positive_env helper. - Each parent session gets one spawn semaphore (map entries live for the process so a resumed session shares its predecessor's limit); handle_subagent_request holds an owned permit for the whole child run — queue awaits FIFO with a unified-log notice at saturation, fail routes through send_pre_spawn_failure. After a queued wait, a closed parent command channel (teardown) fails the spawn instead of running against a dead parent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
102 lines
3.7 KiB
Rust
102 lines
3.7 KiB
Rust
//! Environment variable helpers and process isolation for terminal execution.
|
|
//!
|
|
//! The implementations live in the lightweight [`kigi_tty_utils`] crate so
|
|
//! every workspace crate can use them without depending on `kigi-tools`; this
|
|
//! module re-exports the public API.
|
|
|
|
pub use kigi_tty_utils::{detach_from_tty, pager_env};
|
|
|
|
/// Parse a positive whole number in plain digits (no sign, no separators,
|
|
/// no scientific notation). `None` for zero or anything non-canonical.
|
|
pub fn parse_positive(value: &str) -> Option<u64> {
|
|
value.parse::<u64>().ok().filter(|&parsed| parsed > 0)
|
|
}
|
|
|
|
/// Parse a positive whole-number env value into a count. A set-but-invalid
|
|
/// value warns and reads as unset, so the caller's default applies.
|
|
pub fn parse_positive_env(var: &str, value: Option<String>) -> Option<usize> {
|
|
let value = value?;
|
|
let parsed = parse_positive(&value).and_then(|parsed| usize::try_from(parsed).ok());
|
|
if parsed.is_none() {
|
|
tracing::warn!(
|
|
var,
|
|
%value,
|
|
"env value is not a positive whole number in plain digits; using the default"
|
|
);
|
|
}
|
|
parsed
|
|
}
|
|
|
|
/// Env var set on agent-spawned terminal processes so host tools (e.g. `x ban`)
|
|
/// can distinguish agent invocations from human interactive shells.
|
|
/// Note: the CLI also uses `KIGI_AGENT` as an
|
|
/// optional agent-definition selector for launching `kigi` itself; child terminal
|
|
/// processes only need the sentinel value `"1"`.
|
|
pub const KIGI_AGENT_ENV: &str = "KIGI_AGENT";
|
|
|
|
/// Sentinel value for [`KIGI_AGENT_ENV`] on agent tool terminals.
|
|
pub const KIGI_AGENT_ENV_VALUE: &str = "1";
|
|
|
|
/// Force `KIGI_AGENT=1` on an agent terminal child so request/login env cannot
|
|
/// clear the agent marker.
|
|
pub fn apply_kigi_agent_marker(cmd: &mut tokio::process::Command) {
|
|
cmd.env(KIGI_AGENT_ENV, KIGI_AGENT_ENV_VALUE);
|
|
}
|
|
|
|
/// Expand the four plugin-path tokens (`${CLAUDE_PLUGIN_ROOT}` / `${KIGI_PLUGIN_ROOT}`
|
|
/// and `${CLAUDE_PLUGIN_DATA}` / `${KIGI_PLUGIN_DATA}`) in `s`. Each pair is expanded
|
|
/// only when its value is provided. Single source of truth for plugin agent bodies,
|
|
/// plugin skill/command bodies, and plugin MCP/hook config substitution.
|
|
pub fn substitute_plugin_tokens(
|
|
s: &str,
|
|
plugin_root: Option<&str>,
|
|
plugin_data: Option<&str>,
|
|
) -> String {
|
|
let mut out = s.to_string();
|
|
if let Some(root) = plugin_root {
|
|
out = out
|
|
.replace("${CLAUDE_PLUGIN_ROOT}", root)
|
|
.replace("${KIGI_PLUGIN_ROOT}", root);
|
|
}
|
|
if let Some(data) = plugin_data {
|
|
out = out
|
|
.replace("${CLAUDE_PLUGIN_DATA}", data)
|
|
.replace("${KIGI_PLUGIN_DATA}", data);
|
|
}
|
|
out
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{KIGI_AGENT_ENV, KIGI_AGENT_ENV_VALUE, substitute_plugin_tokens};
|
|
|
|
const ALL_TOKENS: &str = "${CLAUDE_PLUGIN_ROOT}/a ${KIGI_PLUGIN_ROOT}/b ${CLAUDE_PLUGIN_DATA}/c ${KIGI_PLUGIN_DATA}/d";
|
|
|
|
#[test]
|
|
fn expands_all_four_tokens_when_both_provided() {
|
|
let out = substitute_plugin_tokens(ALL_TOKENS, Some("/root"), Some("/data"));
|
|
assert_eq!(out, "/root/a /root/b /data/c /data/d");
|
|
}
|
|
|
|
#[test]
|
|
fn leaves_tokens_literal_when_both_none() {
|
|
let out = substitute_plugin_tokens(ALL_TOKENS, None, None);
|
|
assert_eq!(out, ALL_TOKENS);
|
|
}
|
|
|
|
#[test]
|
|
fn expands_only_root_when_data_none() {
|
|
let out = substitute_plugin_tokens(ALL_TOKENS, Some("/root"), None);
|
|
assert_eq!(
|
|
out,
|
|
"/root/a /root/b ${CLAUDE_PLUGIN_DATA}/c ${KIGI_PLUGIN_DATA}/d"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn agent_marker_constants_match_cursor_parity() {
|
|
assert_eq!(KIGI_AGENT_ENV, "KIGI_AGENT");
|
|
assert_eq!(KIGI_AGENT_ENV_VALUE, "1");
|
|
}
|
|
}
|