Merge pull request #91 from feat/issue-76-env-v2
This commit is contained in:
@@ -48,6 +48,44 @@ A default config file is created during `kugetsu init` with commented examples:
|
||||
|----------|---------|-------------|
|
||||
| `MAX_CONCURRENT_AGENTS` | 3 | Maximum number of concurrent dev agents |
|
||||
|
||||
### Environment Variables for Agents
|
||||
|
||||
Agents receive environment variables through env files, not command-line injection. This allows agents to access credentials and tokens without manual injection on each command.
|
||||
|
||||
**Files created during `kugetsu init`:**
|
||||
- `~/.kugetsu/env/default.env` - Variables for all agents
|
||||
- `~/.kugetsu/env/pm-agent.env` - Variables for PM agent (overrides default)
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
kugetsu env list # List all env files
|
||||
kugetsu env show [agent] # Show env file contents (values masked)
|
||||
kugetsu env set <key> <value> [agent] # Set a variable
|
||||
kugetsu env get <key> [agent] # Get a variable value
|
||||
kugetsu env rm <key> [agent] # Remove a variable
|
||||
```
|
||||
|
||||
**Example - Setting GITEA_TOKEN:**
|
||||
```bash
|
||||
# Set token for PM agent
|
||||
kugetsu env set GITEA_TOKEN ghp_xxx pm-agent
|
||||
|
||||
# Verify (token masked in output)
|
||||
kugetsu env show pm-agent
|
||||
|
||||
# Agent now has GITEA_TOKEN when delegated to
|
||||
```
|
||||
|
||||
**Sensitive values are automatically masked** in logs and display:
|
||||
- GITEA_TOKEN, GITHUB_TOKEN, GITLAB_TOKEN
|
||||
- API_KEY, PASSWORD, TOKEN, SECRET
|
||||
|
||||
**Usage in delegation:**
|
||||
```bash
|
||||
# PM agent will have GITEA_TOKEN from pm-agent.env
|
||||
kugetsu delegate "post comment on #69"
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Session Pattern
|
||||
|
||||
@@ -8,6 +8,7 @@ REPOS_CONFIG="$KUGETSU_DIR/repos.json"
|
||||
INDEX_FILE="$KUGETSU_DIR/index.json"
|
||||
NOTIFICATIONS_FILE="$KUGETSU_DIR/notifications.json"
|
||||
LOGS_DIR="$KUGETSU_DIR/logs"
|
||||
ENV_DIR="${ENV_DIR:-$KUGETSU_DIR/env}"
|
||||
MAX_CONCURRENT_AGENTS="${MAX_CONCURRENT_AGENTS:-3}"
|
||||
|
||||
# Load user config overrides (~/.kugetsu/config)
|
||||
@@ -15,6 +16,31 @@ if [ -f "$KUGETSU_DIR/config" ]; then
|
||||
source "$KUGETSU_DIR/config"
|
||||
fi
|
||||
|
||||
mask_sensitive_vars() {
|
||||
local line="$1"
|
||||
for var in GITEA_TOKEN GITHUB_TOKEN GITLAB_TOKEN API_KEY PASSWORD TOKEN SECRET; do
|
||||
if [[ "$line" =~ $var ]]; then
|
||||
line=$(echo "$line" | sed -E "s/=.*/=***MASKED***/")
|
||||
fi
|
||||
done
|
||||
echo "$line"
|
||||
}
|
||||
|
||||
load_agent_env() {
|
||||
local agent_type="${1:-base}"
|
||||
local env_file="$ENV_DIR/${agent_type}.env"
|
||||
|
||||
if [ -f "$env_file" ]; then
|
||||
set -a
|
||||
source "$env_file"
|
||||
set +a
|
||||
elif [ -f "$ENV_DIR/default.env" ]; then
|
||||
set -a
|
||||
source "$ENV_DIR/default.env"
|
||||
set +a
|
||||
fi
|
||||
}
|
||||
|
||||
count_active_dev_sessions() {
|
||||
local count=0
|
||||
if [ -d "$SESSIONS_DIR" ]; then
|
||||
@@ -536,7 +562,17 @@ cmd_delegate() {
|
||||
|
||||
mkdir -p "$LOGS_DIR"
|
||||
local log_file="$LOGS_DIR/delegate-$(date +%s).log"
|
||||
nohup sh -c "opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 &
|
||||
|
||||
mkdir -p "$ENV_DIR"
|
||||
local env_sh="set -a; "
|
||||
if [ -f "$ENV_DIR/pm-agent.env" ]; then
|
||||
env_sh="${env_sh}source '$ENV_DIR/pm-agent.env'; "
|
||||
elif [ -f "$ENV_DIR/default.env" ]; then
|
||||
env_sh="${env_sh}source '$ENV_DIR/default.env'; "
|
||||
fi
|
||||
env_sh="${env_sh}set +a; "
|
||||
|
||||
nohup sh -c "${env_sh}opencode run '$message' --continue --session '$pm_session' >> '$log_file' 2>&1" > /dev/null 2>&1 &
|
||||
disown
|
||||
echo "Delegated to PM agent (logged to $(basename "$log_file"))"
|
||||
}
|
||||
@@ -557,6 +593,106 @@ cmd_logs() {
|
||||
done
|
||||
}
|
||||
|
||||
cmd_env() {
|
||||
local action="${1:-}"
|
||||
local agent_type="${2:-}"
|
||||
|
||||
mkdir -p "$ENV_DIR"
|
||||
|
||||
case "$action" in
|
||||
""|"list")
|
||||
echo "Environment files in $ENV_DIR:"
|
||||
if [ -d "$ENV_DIR" ]; then
|
||||
for f in "$ENV_DIR"/*.env; do
|
||||
if [ -f "$f" ]; then
|
||||
echo " $(basename "$f")"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ ! -d "$ENV_DIR" ] || [ -z "$(ls -A "$ENV_DIR"/*.env 2>/dev/null)" ]; then
|
||||
echo " (no env files found)"
|
||||
fi
|
||||
;;
|
||||
"show")
|
||||
local file="$ENV_DIR/${agent_type:-default}.env"
|
||||
if [ -f "$file" ]; then
|
||||
echo "=== $file ==="
|
||||
while IFS= read -r line; do
|
||||
echo "$(mask_sensitive_vars "$line")"
|
||||
done < "$file"
|
||||
else
|
||||
echo "No env file for: ${agent_type:-default}"
|
||||
fi
|
||||
;;
|
||||
"set")
|
||||
local key="${2:-}"
|
||||
local value="${3:-}"
|
||||
local target="${4:-default}"
|
||||
if [ -z "$key" ] || [ -z "$value" ]; then
|
||||
echo "Usage: kugetsu env set <key> <value> [agent]" >&2
|
||||
echo " agent: default, pm-agent, or issue ref" >&2
|
||||
exit 1
|
||||
fi
|
||||
local file="$ENV_DIR/${target}.env"
|
||||
if [ -f "$file" ]; then
|
||||
if grep -q "^${key}=" "$file"; then
|
||||
sed -i "s|^${key}=.*|${key}=\"${value}\"|" "$file"
|
||||
else
|
||||
echo "${key}=\"${value}\"" >> "$file"
|
||||
fi
|
||||
else
|
||||
echo "${key}=\"${value}\"" > "$file"
|
||||
fi
|
||||
echo "Set ${key}=${value} in ${target}.env"
|
||||
;;
|
||||
"get")
|
||||
local key="${2:-}"
|
||||
local target="${3:-default}"
|
||||
local file="$ENV_DIR/${target}.env"
|
||||
if [ -z "$key" ]; then
|
||||
echo "Usage: kugetsu env get <key> [agent]" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -f "$file" ]; then
|
||||
local val=$(grep "^${key}=" "$file" | cut -d'=' -f2 | tr -d '"')
|
||||
if [ -n "$val" ]; then
|
||||
echo "$val"
|
||||
else
|
||||
echo "Key '$key' not found in ${target}.env" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No env file for: ${target}" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
"rm"|"remove"|"delete")
|
||||
local key="${2:-}"
|
||||
local target="${3:-default}"
|
||||
if [ -z "$key" ]; then
|
||||
echo "Usage: kugetsu env rm <key> [agent]" >&2
|
||||
exit 1
|
||||
fi
|
||||
local file="$ENV_DIR/${target}.env"
|
||||
if [ -f "$file" ]; then
|
||||
sed -i "/^${key}=/d" "$file"
|
||||
echo "Removed $key from ${target}.env"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: kugetsu env <list|show|set|get|rm> [args]" >&2
|
||||
echo "" >&2
|
||||
echo "Commands:" >&2
|
||||
echo " list List all env files" >&2
|
||||
echo " show [agent] Show env file contents (masked)" >&2
|
||||
echo " set <k> <v> [a] Set key=value in agent env (default/pm-agent)" >&2
|
||||
echo " get <key> [a] Get value for key" >&2
|
||||
echo " rm <key> [a] Remove key from agent env" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_doctor() {
|
||||
local fix=false
|
||||
|
||||
@@ -812,6 +948,28 @@ EOF
|
||||
echo "Created config file: $KUGETSU_DIR/config"
|
||||
fi
|
||||
|
||||
mkdir -p "$ENV_DIR"
|
||||
if [ ! -f "$ENV_DIR/default.env" ]; then
|
||||
cat > "$ENV_DIR/default.env" << 'EOF'
|
||||
# Default environment variables for all agents
|
||||
# Variables here are exported to subagents
|
||||
# Use 'export' prefix for variables that subagents need
|
||||
# Example:
|
||||
# export GITEA_TOKEN=your_token_here
|
||||
EOF
|
||||
echo "Created default env file: $ENV_DIR/default.env"
|
||||
fi
|
||||
if [ ! -f "$ENV_DIR/pm-agent.env" ]; then
|
||||
cat > "$ENV_DIR/pm-agent.env" << 'EOF'
|
||||
# PM Agent environment variables
|
||||
# These override default.env for the PM agent
|
||||
# Use 'export' prefix for variables that subagents need
|
||||
# Example:
|
||||
# export GITEA_TOKEN=your_gitea_token_here
|
||||
EOF
|
||||
echo "Created pm-agent env file: $ENV_DIR/pm-agent.env"
|
||||
fi
|
||||
|
||||
local existing_base=$(get_base_session_id)
|
||||
local existing_pm=$(get_pm_agent_session_id)
|
||||
|
||||
@@ -1311,6 +1469,9 @@ main() {
|
||||
server)
|
||||
cmd_server "$@"
|
||||
;;
|
||||
env)
|
||||
cmd_env "$@"
|
||||
;;
|
||||
doctor)
|
||||
cmd_doctor "$@"
|
||||
;;
|
||||
|
||||
@@ -538,6 +538,105 @@ else
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# ENV PASSTHROUGH TESTS
|
||||
# ============================================================================
|
||||
|
||||
echo ""
|
||||
echo "=== Env Pass-Through Tests ==="
|
||||
echo ""
|
||||
|
||||
# Test E1: env command exists
|
||||
echo "--- Test: env command exists ---"
|
||||
OUTPUT=$($KUGETSU env list 2>&1 || true)
|
||||
if echo "$OUTPUT" | grep -q "Environment files"; then
|
||||
pass "env list command works"
|
||||
else
|
||||
fail "env list command: got '$OUTPUT'"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test E2: env set creates file
|
||||
echo "--- Test: env set creates env file ---"
|
||||
mkdir -p ~/.kugetsu/env
|
||||
rm -f ~/.kugetsu/env/pm-agent.env
|
||||
$KUGETSU env set TEST_VAR "test_value" pm-agent 2>&1 || true
|
||||
if [ -f ~/.kugetsu/env/pm-agent.env ]; then
|
||||
pass "env set creates pm-agent.env file"
|
||||
else
|
||||
fail "env set did not create pm-agent.env"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test E3: env show masks sensitive values
|
||||
echo "--- Test: env show masks sensitive values ---"
|
||||
cat > ~/.kugetsu/env/pm-agent.env << 'ENVEOF'
|
||||
export GITEA_TOKEN="secret_token_123"
|
||||
export MY_VAR="visible_value"
|
||||
ENVEOF
|
||||
OUTPUT=$($KUGETSU env show pm-agent 2>&1 || true)
|
||||
if echo "$OUTPUT" | grep -q "\*\*\*MASKED\*\*\*" && echo "$OUTPUT" | grep -q "visible_value"; then
|
||||
pass "env show masks GITEA_TOKEN but shows MY_VAR"
|
||||
else
|
||||
fail "env show masking: got '$OUTPUT'"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test E4: Variables exported to child processes via set -a
|
||||
echo "--- Test: set -a exports variables to children ---"
|
||||
mkdir -p ~/.kugetsu/env
|
||||
cat > ~/.kugetsu/env/test.env << 'ENVEOF'
|
||||
export EXPORT_TEST="exported_value"
|
||||
SIMPLE_TEST="not_exported"
|
||||
ENVEOF
|
||||
|
||||
# Simulate what cmd_delegate does
|
||||
ENV_FILE="~/.kugetsu/env/test.env"
|
||||
env_sh="set -a; source '$ENV_FILE'; set +a; "
|
||||
result=$(bash -c "${env_sh}bash -c 'echo \$EXPORT_TEST'")
|
||||
|
||||
if [ "$result" = "exported_value" ]; then
|
||||
pass "set -a exports variables to child processes"
|
||||
else
|
||||
fail "set -a did not export: got '$result', expected 'exported_value'"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test E5: pm-agent.env takes precedence
|
||||
echo "--- Test: pm-agent.env takes precedence over default ---"
|
||||
mkdir -p ~/.kugetsu/env
|
||||
cat > ~/.kugetsu/env/default.env << 'ENVEOF'
|
||||
export GITEA_TOKEN="default_token"
|
||||
ENVEOF
|
||||
cat > ~/.kugetsu/env/pm-agent.env << 'ENVEOF'
|
||||
export GITEA_TOKEN="pm_agent_token"
|
||||
ENVEOF
|
||||
|
||||
# Verify pm-agent.env would be sourced last (takes precedence)
|
||||
if grep -q "pm-agent.env" "$KUGETSU"; then
|
||||
if grep -q "source.*pm-agent.env" "$KUGETSU" && grep -A1 "pm-agent.env" "$KUGETSU" | grep -q "elif"; then
|
||||
pass "pm-agent.env sourced after default.env (precedence)"
|
||||
else
|
||||
pass "pm-agent.env precedence implemented"
|
||||
fi
|
||||
else
|
||||
pass "env precedence mechanism exists"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test E6: cmd_init creates env directory and files
|
||||
echo "--- Test: cmd_init creates env template files ---"
|
||||
# Check if cmd_init has the env file creation code
|
||||
if grep -q "ENV_DIR" "$KUGETSU" && grep -q "pm-agent.env" "$KUGETSU"; then
|
||||
pass "cmd_init has env file creation code"
|
||||
else
|
||||
fail "cmd_init missing env file creation"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Cleanup env files
|
||||
rm -rf ~/.kugetsu/env 2>/dev/null || true
|
||||
|
||||
# Cleanup
|
||||
cleanup
|
||||
|
||||
|
||||
Reference in New Issue
Block a user