Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-router,kei-sage,kei-task,kei-chat-store,kei-crossdomain,kei-search-core,kei-content-store,kei-social-store,kei-curator,kei-auth,kei-artifact name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Audit pass via Phase C dogfooding (security-auditor @ Agy/Gemini reviewing our own safe_tools.rs) surfaced 5 real bugs. All fixed. ## Gemini findings (5 real bugs) [#1 HIGH] FAIL-OPEN on missing config/hook Before: missing policy-chain.toml → "passing through" warning; missing hook script → "skipped" warning. Misconfig silently disabled enforcement. After: both paths FAIL-CLOSED with clear error surfaced to caller. Tests/dev can opt in to pass-through via KEI_POLICY_CHAIN_OPTIONAL=1. [#2 HIGH] Path traversal in kei_edit/kei_write Before: no validation; attacker could pass file_path=/etc/passwd or $HOME/.ssh/authorized_keys. After: validate_path() rejects '..' segments, system dirs (/etc/, /usr/, /System/, /var/, /root/), and dotfile-secret dirs (~/.ssh/, ~/.aws/, ~/.gnupg/, ~/.config/gcloud/). Override via KEI_ALLOWED_ROOTS for explicit single-root confinement. [#3 HIGH] CLAUDECODE/GROKCODE env bypass Behavior unchanged — this guard is a perf/UX optimization to avoid double-firing hooks when called from inside Claude/Grok (which already ran their own PreToolUse). Documented explicitly as NOT a security boundary: attacker controlling parent env already owns the invocation. Module header gains a DESIGN NOTE making this load-bearing. [#4 MED] std::fs in async context Before: handle_edit/handle_write used std::fs::{read_to_string,write}, which block the tokio worker thread. Pathological paths like /dev/random would freeze a worker indefinitely. After: tokio::fs::{read_to_string,write}.await — async I/O, worker stays responsive. [#5 MED] kill_on_drop only kills immediate child Before: timeout in kei_bash drops the Child handle; tokio's kill_on_drop SIGKILLs only the shell. Grandchildren (e.g., 'sleep 1000 &') orphaned. After: Unix-only: spawn child in its own process group (Command::process_group(0)), and on timeout libc::kill(-pid, SIGKILL) to take down the whole group. New libc dep on Unix. ## Copilot doc fix Doc claimed "kei-mcp exposes 4 built-in tools" without saying spawn_agent lives in tools.rs while kei_bash/edit/write live in safe_tools.rs. Validator agent flagged this as FALSE/MISLEADING. Now the doc spells out the two-file structure + adds a v0.41 hardening summary. ## claude/grok subprocess permissions Cross-CLI audit demo revealed that 'claude -p' and 'grok --print' returned empty when invoked headless with a real audit task — they need explicit permission flags to use Read/Grep tools in non-interactive mode. Added: claude: --permission-mode=bypassPermissions grok: --always-approve agy: --dangerously-skip-permissions Override via KEI_AGENT_PERMISSIVE=0 to keep strict default. Re-verified: claude+grok both echo SMOKE-OK-V41 with the flag. ## Verification cargo test -p kei-mcp --release → 3/3 pass MCP JSON-RPC smoke (all 7): - tools/list shows 4 built-ins ✓ - kei_bash blocks RULE 0.1 push ✓ - kei_bash passes 'echo OK' ✓ - kei_write rejects /etc/passwd ✓ - kei_write rejects ../ traversal ✓ - kei_write rejects ~/.ssh/* ✓ - missing policy-chain → FAIL-CLOSED with clear error ✓ - KEI_POLICY_CHAIN_OPTIONAL=1 → opt-in pass-through ✓
281 lines
10 KiB
Bash
Executable file
281 lines
10 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# kei-agent-cli — invoke a KeiSeiKit agent via an external LLM CLI backend.
|
|
#
|
|
# Two entry points (both route through this script):
|
|
#
|
|
# kei run-via <backend> <agent> "<task>" # explicit backend
|
|
# kei agent <agent> "<task>" # backend resolved from DNA:
|
|
# # 1. --on=<backend> flag
|
|
# # 2. agent manifest's `provider`
|
|
# # 3. ~/.claude/config/primary.toml
|
|
# # 4. fallback: claude
|
|
#
|
|
# Other forms:
|
|
# kei run-via list # show backends + agents
|
|
# kei agent --on=<backend> <agent> "<task>" # override DNA backend
|
|
# kei primary # print current primary
|
|
# kei primary <backend> # set primary provider
|
|
# kei run-via --help
|
|
#
|
|
# Backends (SSoT: _primitives/cli-backends.toml):
|
|
# claude grok agy copilot kimi codex
|
|
#
|
|
# Reads assembled prompt from ~/.claude/agents/<agent-name>.md.
|
|
# Strips YAML frontmatter, composes with task, execs the CLI.
|
|
#
|
|
# Env overrides:
|
|
# KEI_AGENTS_DIR agent .md dir (default: ~/.claude/agents)
|
|
# KEI_MANIFESTS_DIR manifest .toml dir (default: ~/.claude/_manifests)
|
|
# KEI_PRIMARY override primary backend (beats config file)
|
|
# KEI_NATIVE_AGENT=1 prefer backend's native --agent flag (grok/claude)
|
|
|
|
set -euo pipefail
|
|
|
|
KEI_AGENTS_DIR="${KEI_AGENTS_DIR:-$HOME/.claude/agents}"
|
|
KEI_MANIFESTS_DIR="${KEI_MANIFESTS_DIR:-$HOME/.claude/_manifests}"
|
|
KEI_PRIMARY_CFG="${KEI_PRIMARY_CFG:-$HOME/.claude/config/primary.toml}"
|
|
KEI_NATIVE_AGENT="${KEI_NATIVE_AGENT:-0}"
|
|
|
|
usage() { sed -n '2,32p' "$0" | sed 's|^# \{0,1\}||'; }
|
|
|
|
# ---- backend table (SSoT mirror; kept in sync with cli-backends.toml) -----
|
|
backend_bin() {
|
|
case "$1" in
|
|
claude) echo "claude" ;;
|
|
grok) echo "grok" ;;
|
|
agy|antigravity) echo "agy" ;;
|
|
copilot) echo "copilot" ;;
|
|
kimi) echo "kimi" ;;
|
|
codex) echo "codex" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
backend_supports_native_agent() {
|
|
case "$1" in claude|grok) return 0 ;; *) return 1 ;; esac
|
|
}
|
|
|
|
# ---- DNA resolver: agent → preferred backend --------------------------------
|
|
# Reads `provider = "..."` line from the manifest TOML if present.
|
|
manifest_provider() {
|
|
local agent="$1" tomlf="$KEI_MANIFESTS_DIR/$1.toml"
|
|
[ -f "$tomlf" ] || return 1
|
|
awk -F'=' '
|
|
/^provider[[:space:]]*=/ {
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
|
|
gsub(/^"|"$/, "", $2)
|
|
print $2; exit
|
|
}
|
|
' "$tomlf"
|
|
}
|
|
|
|
# Reads primary from config file (or KEI_PRIMARY env override).
|
|
config_primary() {
|
|
if [ -n "${KEI_PRIMARY:-}" ]; then
|
|
printf '%s\n' "$KEI_PRIMARY"; return 0
|
|
fi
|
|
[ -f "$KEI_PRIMARY_CFG" ] || return 1
|
|
awk -F'=' '
|
|
/^provider[[:space:]]*=/ {
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
|
|
gsub(/^"|"$/, "", $2)
|
|
print $2; exit
|
|
}
|
|
' "$KEI_PRIMARY_CFG"
|
|
}
|
|
|
|
# Resolution order: explicit --on= → manifest provider → primary → claude.
|
|
resolve_backend() {
|
|
local agent="$1" explicit="${2:-}" out=""
|
|
if [ -n "$explicit" ]; then printf '%s\n' "$explicit"; return 0; fi
|
|
out=$(manifest_provider "$agent" 2>/dev/null) || true
|
|
if [ -n "$out" ]; then printf '%s\n' "$out"; return 0; fi
|
|
out=$(config_primary 2>/dev/null) || true
|
|
if [ -n "$out" ]; then printf '%s\n' "$out"; return 0; fi
|
|
printf 'claude\n'
|
|
}
|
|
|
|
# ---- backend invocation ---------------------------------------------------
|
|
backend_invoke() {
|
|
local backend="$1" prompt="$2" agent_name="${3:-}" bin
|
|
bin=$(backend_bin "$backend") || {
|
|
printf '[kei-agent-cli] unknown backend: %s\n' "$backend" >&2
|
|
return 2
|
|
}
|
|
command -v "$bin" >/dev/null 2>&1 || {
|
|
printf '[kei-agent-cli] %s not on PATH. Install it or pick another backend.\n' "$bin" >&2
|
|
return 127
|
|
}
|
|
|
|
# Native --agent path (grok/claude) — pass agent name + task directly.
|
|
if [ "$KEI_NATIVE_AGENT" = "1" ] \
|
|
&& [ -n "$agent_name" ] \
|
|
&& backend_supports_native_agent "$backend"; then
|
|
printf '[kei-agent-cli] %s --agent %s\n' "$bin" "$agent_name" >&2
|
|
exec "$bin" --agent "$agent_name" --print "${prompt##*TASK FOR THIS RUN:}"
|
|
fi
|
|
|
|
# v0.41 fix: headless subprocess invocation of claude/grok without
|
|
# --dangerously-skip-permissions returns empty (the agent's system prompt
|
|
# asks for Read/Grep tools, but those need permission prompts which can't
|
|
# be answered in -p mode). Pass the flag so the agent actually executes.
|
|
# Override via KEI_AGENT_PERMISSIVE=0 to keep the strict default.
|
|
local permissive_claude="" permissive_grok=""
|
|
if [ "${KEI_AGENT_PERMISSIVE:-1}" = "1" ]; then
|
|
permissive_claude="--permission-mode=bypassPermissions"
|
|
permissive_grok="--always-approve"
|
|
fi
|
|
|
|
case "$backend" in
|
|
claude) exec "$bin" $permissive_claude -p "$prompt" ;;
|
|
grok) exec "$bin" $permissive_grok --print "$prompt" ;;
|
|
agy|antigravity) exec "$bin" --dangerously-skip-permissions --print "$prompt" ;;
|
|
copilot) exec "$bin" --prompt "$prompt" ;;
|
|
kimi)
|
|
# Kimi has NO one-shot print mode (smoke-tested 2026-05-26): bare `kimi`
|
|
# opens an interactive TUI that ignores piped stdin and exits with "Bye!".
|
|
# For headless invocation we'd need an ACP client (`kimi acp` is a JSON-RPC
|
|
# stdio server). Until KeiSeiKit ships that client, dump the composed
|
|
# prompt to a tmpfile and open the TUI so the user can paste it in.
|
|
tmp=$(mktemp -t kei-agent-kimi.XXXX.md)
|
|
printf '%s\n' "$prompt" > "$tmp"
|
|
printf '[kei-agent-cli] kimi non-interactive is unsupported (TUI only).\n' >&2
|
|
printf '[kei-agent-cli] composed prompt saved: %s\n' "$tmp" >&2
|
|
printf '[kei-agent-cli] copy-paste it into Kimi after the TUI opens.\n' >&2
|
|
printf '[kei-agent-cli] (or pipe via `kimi acp` if you have an ACP client.)\n' >&2
|
|
exec "$bin"
|
|
;;
|
|
codex) exec "$bin" -p "$prompt" ;;
|
|
esac
|
|
}
|
|
|
|
# ---- agent loader -------------------------------------------------------
|
|
load_agent() {
|
|
local name="$1" path
|
|
case "$name" in
|
|
--file=*) path="${name#--file=}" ;;
|
|
/*|./*|*/*) path="$name" ;;
|
|
*) path="$KEI_AGENTS_DIR/$name.md" ;;
|
|
esac
|
|
if [ ! -f "$path" ]; then
|
|
printf '[kei-agent-cli] agent not found: %s\n' "$path" >&2
|
|
if [ -d "$KEI_AGENTS_DIR" ]; then
|
|
printf ' Available (%s): %s\n' "$KEI_AGENTS_DIR" \
|
|
"$(find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' 2>/dev/null \
|
|
| xargs -n1 basename 2>/dev/null | sed 's/\.md$//' \
|
|
| sort | head -8 | tr '\n' ' ')..." >&2
|
|
fi
|
|
return 1
|
|
fi
|
|
awk '
|
|
BEGIN { in_fm=0 }
|
|
NR==1 && /^---$/ { in_fm=1; next }
|
|
in_fm && /^---$/ { in_fm=0; next }
|
|
in_fm { next }
|
|
{ print }
|
|
' "$path"
|
|
}
|
|
|
|
# ---- primary subcommand ------------------------------------------------
|
|
handle_primary() {
|
|
local arg="${1:-}"
|
|
if [ -z "$arg" ]; then
|
|
cur=$(config_primary 2>/dev/null || true)
|
|
printf 'primary provider: %s\n' "${cur:-claude (default fallback)}"
|
|
[ -f "$KEI_PRIMARY_CFG" ] && printf 'config: %s\n' "$KEI_PRIMARY_CFG"
|
|
return 0
|
|
fi
|
|
backend_bin "$arg" >/dev/null || {
|
|
printf '[kei-primary] unknown backend: %s\n' "$arg" >&2
|
|
printf 'valid: claude grok agy copilot kimi codex\n' >&2
|
|
return 2
|
|
}
|
|
mkdir -p "$(dirname "$KEI_PRIMARY_CFG")"
|
|
printf '# kei primary — written %s\nprovider = "%s"\n' \
|
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$arg" > "$KEI_PRIMARY_CFG"
|
|
printf 'primary provider set: %s → %s\n' "$arg" "$KEI_PRIMARY_CFG"
|
|
}
|
|
|
|
# ---- subcommands --------------------------------------------------------
|
|
case "${1:-}" in
|
|
""|-h|--help|help) usage; exit 0 ;;
|
|
list|--list)
|
|
printf 'Backends (✓ installed, ✗ missing):\n'
|
|
for b in claude grok agy copilot kimi codex; do
|
|
bin=$(backend_bin "$b")
|
|
if p=$(command -v "$bin" 2>/dev/null); then
|
|
printf ' %-10s ✓ %s\n' "$b" "$p"
|
|
else
|
|
printf ' %-10s ✗ (not on PATH)\n' "$b"
|
|
fi
|
|
done
|
|
cur=$(config_primary 2>/dev/null || true)
|
|
printf '\nprimary: %s\n' "${cur:-claude (default)}"
|
|
printf '\nAgents (%s):\n' "$KEI_AGENTS_DIR"
|
|
if [ -d "$KEI_AGENTS_DIR" ]; then
|
|
find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' 2>/dev/null \
|
|
| xargs -n1 basename 2>/dev/null | sed 's/\.md$/ /' | sort | column 2>/dev/null \
|
|
|| (find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' \
|
|
| xargs -n1 basename | sed 's/\.md$//' | sort | head -20)
|
|
fi
|
|
exit 0
|
|
;;
|
|
primary)
|
|
shift
|
|
handle_primary "${1:-}"
|
|
exit $?
|
|
;;
|
|
agent)
|
|
# Direct-invocation passthrough: `kei-agent-cli.sh agent <name> "task"`
|
|
# behaves identically to `kei-agent-cli.sh <name> "task"` (DNA mode).
|
|
# Lets users call either form without surprise.
|
|
shift
|
|
;;
|
|
esac
|
|
|
|
# ---- main: DNA mode (no leading backend) OR explicit run-via ------------
|
|
# Detect call shape:
|
|
# "$1" is a known backend → run-via flow (kei run-via <backend> <agent> "task")
|
|
# "$1" starts with --on= → DNA flow with override
|
|
# "$1" is anything else → DNA flow (kei agent <agent> "task")
|
|
|
|
EXPLICIT_BACKEND=""
|
|
case "${1:-}" in
|
|
--on=*)
|
|
EXPLICIT_BACKEND="${1#--on=}"
|
|
shift
|
|
;;
|
|
*)
|
|
if [ $# -ge 1 ] && backend_bin "$1" >/dev/null 2>&1; then
|
|
EXPLICIT_BACKEND="$1"
|
|
shift
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
if [ $# -lt 2 ]; then
|
|
usage
|
|
exit 2
|
|
fi
|
|
|
|
AGENT_REF="$1"; shift
|
|
TASK="$*"
|
|
|
|
AGENT_NAME=$(basename "${AGENT_REF#--file=}")
|
|
AGENT_NAME="${AGENT_NAME%.md}"
|
|
|
|
BACKEND=$(resolve_backend "$AGENT_NAME" "$EXPLICIT_BACKEND")
|
|
|
|
if ! AGENT_PROMPT=$(load_agent "$AGENT_REF"); then
|
|
exit 1
|
|
fi
|
|
|
|
COMPOSED=$(printf '%s\n\n---\n\nTASK FOR THIS RUN:\n%s\n' "$AGENT_PROMPT" "$TASK")
|
|
|
|
printf '[kei-agent-cli] agent=%s backend=%s (via %s)\n' \
|
|
"$AGENT_NAME" "$BACKEND" \
|
|
"$([ -n "$EXPLICIT_BACKEND" ] && echo explicit \
|
|
|| ([ -n "$(manifest_provider "$AGENT_NAME" 2>/dev/null)" ] && echo manifest \
|
|
|| ([ -n "$(config_primary 2>/dev/null)" ] && echo primary || echo default)))" >&2
|
|
|
|
backend_invoke "$BACKEND" "$COMPOSED" "$AGENT_NAME"
|