loki-mode 5.35.0 → 5.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +317 -30
- package/autonomy/run.sh +328 -7
- package/autonomy/sandbox.sh +1 -1
- package/autonomy/serve.sh +25 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +9 -5
- package/dashboard/auth.py +189 -22
- package/dashboard/requirements.txt +7 -7
- package/dashboard/secrets.py +152 -0
- package/dashboard/server.py +280 -23
- package/docs/INSTALLATION.md +1 -1
- package/package.json +1 -1
package/autonomy/run.sh
CHANGED
|
@@ -22,20 +22,32 @@
|
|
|
22
22
|
# LOKI_SKIP_PREREQS - Skip prerequisite checks (default: false)
|
|
23
23
|
# LOKI_DASHBOARD - Enable web dashboard (default: true)
|
|
24
24
|
# LOKI_DASHBOARD_PORT - Dashboard port (default: 57374)
|
|
25
|
+
# LOKI_TLS_CERT - Path to PEM certificate (enables HTTPS for dashboard)
|
|
26
|
+
# LOKI_TLS_KEY - Path to PEM private key (enables HTTPS for dashboard)
|
|
25
27
|
#
|
|
26
28
|
# Resource Monitoring (prevents system overload):
|
|
27
29
|
# LOKI_RESOURCE_CHECK_INTERVAL - Check resources every N seconds (default: 300 = 5min)
|
|
28
30
|
# LOKI_RESOURCE_CPU_THRESHOLD - CPU % threshold to warn (default: 80)
|
|
29
31
|
# LOKI_RESOURCE_MEM_THRESHOLD - Memory % threshold to warn (default: 80)
|
|
30
32
|
#
|
|
33
|
+
# Budget / Cost Limits (opt-in):
|
|
34
|
+
# LOKI_BUDGET_LIMIT - Max USD spend before auto-pause (default: empty = unlimited)
|
|
35
|
+
# Example: "50.00" pauses session when estimated cost >= $50
|
|
36
|
+
#
|
|
31
37
|
# Security & Autonomy Controls (Enterprise):
|
|
32
38
|
# LOKI_STAGED_AUTONOMY - Require approval before execution (default: false)
|
|
33
|
-
# LOKI_AUDIT_LOG - Enable audit logging (default:
|
|
39
|
+
# LOKI_AUDIT_LOG - Enable audit logging (default: true)
|
|
40
|
+
# LOKI_AUDIT_DISABLED - Disable audit logging (default: false)
|
|
34
41
|
# LOKI_MAX_PARALLEL_AGENTS - Limit concurrent agent spawning (default: 10)
|
|
35
42
|
# LOKI_SANDBOX_MODE - Run in sandboxed container (default: false, requires Docker)
|
|
36
43
|
# LOKI_ALLOWED_PATHS - Comma-separated paths agents can modify (default: all)
|
|
37
44
|
# LOKI_BLOCKED_COMMANDS - Comma-separated blocked shell commands (default: rm -rf /)
|
|
38
45
|
#
|
|
46
|
+
# OIDC / SSO Authentication (optional, works alongside token auth):
|
|
47
|
+
# LOKI_OIDC_ISSUER - OIDC issuer URL (e.g., https://accounts.google.com)
|
|
48
|
+
# LOKI_OIDC_CLIENT_ID - OIDC client/application ID
|
|
49
|
+
# LOKI_OIDC_AUDIENCE - Expected JWT audience (default: same as client_id)
|
|
50
|
+
#
|
|
39
51
|
# SDLC Phase Controls (all enabled by default, set to 'false' to skip):
|
|
40
52
|
# LOKI_PHASE_UNIT_TESTS - Run unit tests (default: true)
|
|
41
53
|
# LOKI_PHASE_API_TESTS - Functional API testing (default: true)
|
|
@@ -122,6 +134,10 @@
|
|
|
122
134
|
# Security (Enterprise):
|
|
123
135
|
# LOKI_PROMPT_INJECTION - Enable HUMAN_INPUT.md processing (default: false)
|
|
124
136
|
# Set to "true" only in trusted environments
|
|
137
|
+
#
|
|
138
|
+
# Process Supervision (opt-in):
|
|
139
|
+
# LOKI_WATCHDOG - Enable process health monitoring (default: false)
|
|
140
|
+
# LOKI_WATCHDOG_INTERVAL - Check interval in seconds (default: 30)
|
|
125
141
|
#===============================================================================
|
|
126
142
|
#
|
|
127
143
|
# Compatibility: bash 3.2+ (macOS default), bash 4+ (Linux), WSL
|
|
@@ -461,17 +477,25 @@ RESOURCE_CHECK_INTERVAL=${LOKI_RESOURCE_CHECK_INTERVAL:-300} # Check every 5 mi
|
|
|
461
477
|
RESOURCE_CPU_THRESHOLD=${LOKI_RESOURCE_CPU_THRESHOLD:-80} # CPU % threshold
|
|
462
478
|
RESOURCE_MEM_THRESHOLD=${LOKI_RESOURCE_MEM_THRESHOLD:-80} # Memory % threshold
|
|
463
479
|
|
|
480
|
+
# Budget / Cost Limit (opt-in, empty = unlimited)
|
|
481
|
+
BUDGET_LIMIT=${LOKI_BUDGET_LIMIT:-""} # USD amount, e.g., "50.00"
|
|
482
|
+
|
|
464
483
|
# Background Mode
|
|
465
484
|
BACKGROUND_MODE=${LOKI_BACKGROUND:-false} # Run in background
|
|
466
485
|
|
|
467
486
|
# Security & Autonomy Controls
|
|
468
487
|
STAGED_AUTONOMY=${LOKI_STAGED_AUTONOMY:-false} # Require plan approval
|
|
469
|
-
AUDIT_LOG_ENABLED=${LOKI_AUDIT_LOG:-
|
|
488
|
+
AUDIT_LOG_ENABLED=${LOKI_AUDIT_LOG:-true} # Enable audit logging (on by default)
|
|
470
489
|
MAX_PARALLEL_AGENTS=${LOKI_MAX_PARALLEL_AGENTS:-10} # Limit concurrent agents
|
|
471
490
|
SANDBOX_MODE=${LOKI_SANDBOX_MODE:-false} # Docker sandbox mode
|
|
472
491
|
ALLOWED_PATHS=${LOKI_ALLOWED_PATHS:-""} # Empty = all paths allowed
|
|
473
492
|
BLOCKED_COMMANDS=${LOKI_BLOCKED_COMMANDS:-"rm -rf /,dd if=,mkfs,:(){ :|:& };:"}
|
|
474
493
|
|
|
494
|
+
# Process Supervision (opt-in)
|
|
495
|
+
WATCHDOG_ENABLED=${LOKI_WATCHDOG:-"false"} # Enable process health monitoring
|
|
496
|
+
WATCHDOG_INTERVAL=${LOKI_WATCHDOG_INTERVAL:-30} # Check interval in seconds
|
|
497
|
+
LAST_WATCHDOG_CHECK=0
|
|
498
|
+
|
|
475
499
|
STATUS_MONITOR_PID=""
|
|
476
500
|
DASHBOARD_PID=""
|
|
477
501
|
RESOURCE_MONITOR_PID=""
|
|
@@ -740,6 +764,57 @@ get_iteration_duration_ms() {
|
|
|
740
764
|
fi
|
|
741
765
|
}
|
|
742
766
|
|
|
767
|
+
#===============================================================================
|
|
768
|
+
# API Key Validation
|
|
769
|
+
# Validates required API key is set for the selected provider.
|
|
770
|
+
# Supports Docker/K8s secret file mounts as fallback.
|
|
771
|
+
#===============================================================================
|
|
772
|
+
|
|
773
|
+
validate_api_keys() {
|
|
774
|
+
local provider="${LOKI_PROVIDER:-claude}"
|
|
775
|
+
local key_var=""
|
|
776
|
+
|
|
777
|
+
case "$provider" in
|
|
778
|
+
claude) key_var="ANTHROPIC_API_KEY" ;;
|
|
779
|
+
codex) key_var="OPENAI_API_KEY" ;;
|
|
780
|
+
gemini) key_var="GOOGLE_API_KEY" ;;
|
|
781
|
+
esac
|
|
782
|
+
|
|
783
|
+
if [[ -z "$key_var" ]]; then
|
|
784
|
+
return 0
|
|
785
|
+
fi
|
|
786
|
+
|
|
787
|
+
local key_value="${!key_var:-}"
|
|
788
|
+
|
|
789
|
+
# Try loading from secret file mounts (Docker/K8s)
|
|
790
|
+
if [[ -z "$key_value" ]]; then
|
|
791
|
+
local lower_name
|
|
792
|
+
lower_name=$(echo "$key_var" | tr '[:upper:]' '[:lower:]')
|
|
793
|
+
for mount_path in /run/secrets /var/run/secrets; do
|
|
794
|
+
if [[ -f "$mount_path/$lower_name" ]]; then
|
|
795
|
+
key_value=$(cat "$mount_path/$lower_name" 2>/dev/null | tr -d '[:space:]')
|
|
796
|
+
if [[ -n "$key_value" ]]; then
|
|
797
|
+
export "$key_var=$key_value"
|
|
798
|
+
log_info "Loaded $key_var from secret file: $mount_path/$lower_name"
|
|
799
|
+
break
|
|
800
|
+
fi
|
|
801
|
+
fi
|
|
802
|
+
done
|
|
803
|
+
fi
|
|
804
|
+
|
|
805
|
+
if [[ -z "$key_value" ]]; then
|
|
806
|
+
log_error "Required API key $key_var is not set for provider $provider"
|
|
807
|
+
log_error "Set via environment variable or Docker/K8s secret mount"
|
|
808
|
+
return 1
|
|
809
|
+
fi
|
|
810
|
+
|
|
811
|
+
# Log masked key for debugging
|
|
812
|
+
local masked="${key_value:0:8}...${key_value: -4}"
|
|
813
|
+
log_info "API key $key_var: $masked (${#key_value} chars)"
|
|
814
|
+
|
|
815
|
+
return 0
|
|
816
|
+
}
|
|
817
|
+
|
|
743
818
|
#===============================================================================
|
|
744
819
|
# Complexity Tier Detection (Auto-Claude pattern)
|
|
745
820
|
#===============================================================================
|
|
@@ -2097,6 +2172,19 @@ EOF
|
|
|
2097
2172
|
# Write pricing.json with provider-specific model rates
|
|
2098
2173
|
_write_pricing_json
|
|
2099
2174
|
|
|
2175
|
+
# Write budget.json if a budget limit is configured
|
|
2176
|
+
if [ -n "$BUDGET_LIMIT" ]; then
|
|
2177
|
+
cat > ".loki/metrics/budget.json" << BUDGET_EOF
|
|
2178
|
+
{
|
|
2179
|
+
"limit": $BUDGET_LIMIT,
|
|
2180
|
+
"budget_limit": $BUDGET_LIMIT,
|
|
2181
|
+
"budget_used": 0,
|
|
2182
|
+
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
2183
|
+
}
|
|
2184
|
+
BUDGET_EOF
|
|
2185
|
+
log_info "Budget limit set: \$$BUDGET_LIMIT"
|
|
2186
|
+
fi
|
|
2187
|
+
|
|
2100
2188
|
log_info "Loki directory initialized: .loki/"
|
|
2101
2189
|
}
|
|
2102
2190
|
|
|
@@ -2412,6 +2500,12 @@ write_dashboard_state() {
|
|
|
2412
2500
|
council_state=$(cat ".loki/council/state.json" 2>/dev/null || echo '{"enabled":false}')
|
|
2413
2501
|
fi
|
|
2414
2502
|
|
|
2503
|
+
# Get budget status (if configured)
|
|
2504
|
+
local budget_json="null"
|
|
2505
|
+
if [ -f ".loki/metrics/budget.json" ]; then
|
|
2506
|
+
budget_json=$(cat ".loki/metrics/budget.json" 2>/dev/null || echo "null")
|
|
2507
|
+
fi
|
|
2508
|
+
|
|
2415
2509
|
# Write comprehensive JSON state (atomic via temp file + mv)
|
|
2416
2510
|
local project_name=$(basename "$(pwd)")
|
|
2417
2511
|
local project_path=$(pwd)
|
|
@@ -2463,7 +2557,8 @@ write_dashboard_state() {
|
|
|
2463
2557
|
"procedural": $procedural_count
|
|
2464
2558
|
},
|
|
2465
2559
|
"qualityGates": $quality_gates,
|
|
2466
|
-
"council": $council_state
|
|
2560
|
+
"council": $council_state,
|
|
2561
|
+
"budget": $budget_json
|
|
2467
2562
|
}
|
|
2468
2563
|
EOF
|
|
2469
2564
|
mv "$_tmp_state" "$output_file"
|
|
@@ -3333,7 +3428,24 @@ check_staged_autonomy() {
|
|
|
3333
3428
|
}
|
|
3334
3429
|
|
|
3335
3430
|
check_command_allowed() {
|
|
3336
|
-
# Check if a command
|
|
3431
|
+
# Check if a command string contains any blocked patterns from BLOCKED_COMMANDS.
|
|
3432
|
+
#
|
|
3433
|
+
# SECURITY NOTE: This function is intentionally NOT called by run.sh because
|
|
3434
|
+
# run.sh does not directly execute arbitrary shell commands from user or agent
|
|
3435
|
+
# input. Command execution is handled by the AI CLI's own permission model:
|
|
3436
|
+
# - Claude Code: --dangerously-skip-permissions (with its own allowlist)
|
|
3437
|
+
# - Codex CLI: --full-auto or exec --dangerously-bypass-approvals-and-sandbox
|
|
3438
|
+
# - Gemini CLI: --approval-mode=yolo
|
|
3439
|
+
#
|
|
3440
|
+
# HUMAN_INPUT.md content is injected as a text prompt to the AI agent (not
|
|
3441
|
+
# executed as a shell command), and is already guarded by:
|
|
3442
|
+
# - LOKI_PROMPT_INJECTION=false by default (disabled unless explicitly enabled)
|
|
3443
|
+
# - Symlink rejection (prevents path traversal attacks)
|
|
3444
|
+
# - 1MB file size limit
|
|
3445
|
+
#
|
|
3446
|
+
# This function is retained as a utility for external callers (sandbox.sh,
|
|
3447
|
+
# custom hooks, or user scripts) that may need to validate commands against
|
|
3448
|
+
# the BLOCKED_COMMANDS list before execution.
|
|
3337
3449
|
local command="$1"
|
|
3338
3450
|
|
|
3339
3451
|
IFS=',' read -ra BLOCKED_ARRAY <<< "$BLOCKED_COMMANDS"
|
|
@@ -4623,10 +4735,20 @@ start_dashboard() {
|
|
|
4623
4735
|
export LOKI_DASHBOARD_HOST="127.0.0.1"
|
|
4624
4736
|
export LOKI_PROJECT_PATH="$project_path"
|
|
4625
4737
|
|
|
4738
|
+
# Determine URL scheme based on TLS configuration
|
|
4739
|
+
local url_scheme="http"
|
|
4740
|
+
local tls_env=""
|
|
4741
|
+
if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
|
|
4742
|
+
url_scheme="https"
|
|
4743
|
+
tls_env="LOKI_TLS_CERT=${LOKI_TLS_CERT} LOKI_TLS_KEY=${LOKI_TLS_KEY}"
|
|
4744
|
+
log_info "TLS enabled for dashboard"
|
|
4745
|
+
fi
|
|
4746
|
+
|
|
4626
4747
|
# Start the FastAPI dashboard server
|
|
4627
4748
|
# Dashboard module is at project root (parent of autonomy/)
|
|
4628
4749
|
# LOKI_SKILL_DIR tells server.py where to find static files
|
|
4629
|
-
|
|
4750
|
+
LOKI_TLS_CERT="${LOKI_TLS_CERT:-}" LOKI_TLS_KEY="${LOKI_TLS_KEY:-}" \
|
|
4751
|
+
LOKI_SKILL_DIR="${SCRIPT_DIR%/*}" PYTHONPATH="${SCRIPT_DIR%/*}" nohup python3 -m dashboard.server > "$log_file" 2>&1 &
|
|
4630
4752
|
DASHBOARD_PID=$!
|
|
4631
4753
|
|
|
4632
4754
|
# Save PID for later cleanup
|
|
@@ -4641,11 +4763,11 @@ start_dashboard() {
|
|
|
4641
4763
|
|
|
4642
4764
|
if kill -0 "$DASHBOARD_PID" 2>/dev/null; then
|
|
4643
4765
|
log_info "Dashboard started (PID: $DASHBOARD_PID)"
|
|
4644
|
-
log_info "Dashboard: ${CYAN}
|
|
4766
|
+
log_info "Dashboard: ${CYAN}${url_scheme}://127.0.0.1:$DASHBOARD_PORT/${NC}"
|
|
4645
4767
|
|
|
4646
4768
|
# Open in browser (macOS)
|
|
4647
4769
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
4648
|
-
open "
|
|
4770
|
+
open "${url_scheme}://127.0.0.1:$DASHBOARD_PORT/" 2>/dev/null || true
|
|
4649
4771
|
fi
|
|
4650
4772
|
return 0
|
|
4651
4773
|
else
|
|
@@ -4882,6 +5004,180 @@ is_completed() {
|
|
|
4882
5004
|
return 1
|
|
4883
5005
|
}
|
|
4884
5006
|
|
|
5007
|
+
# Check if estimated cost has exceeded the budget limit
|
|
5008
|
+
# Returns 0 (exceeded) or 1 (within budget / no limit set)
|
|
5009
|
+
check_budget_limit() {
|
|
5010
|
+
[[ -z "$BUDGET_LIMIT" ]] && return 1 # No limit set
|
|
5011
|
+
|
|
5012
|
+
# Validate BUDGET_LIMIT is a valid number (prevent shell injection)
|
|
5013
|
+
if ! python3 -c "float('${BUDGET_LIMIT//[^0-9.]/}')" 2>/dev/null; then
|
|
5014
|
+
log_error "BUDGET_LIMIT is not a valid number: $BUDGET_LIMIT"
|
|
5015
|
+
return 1
|
|
5016
|
+
fi
|
|
5017
|
+
|
|
5018
|
+
local current_cost=0
|
|
5019
|
+
local efficiency_dir=".loki/metrics/efficiency"
|
|
5020
|
+
|
|
5021
|
+
# Calculate cost from per-iteration efficiency files (same source as /api/cost)
|
|
5022
|
+
if [ -d "$efficiency_dir" ]; then
|
|
5023
|
+
current_cost=$(python3 -c "
|
|
5024
|
+
import json, glob
|
|
5025
|
+
total = 0.0
|
|
5026
|
+
pricing = {
|
|
5027
|
+
'opus': {'input': 5.00, 'output': 25.00},
|
|
5028
|
+
'sonnet': {'input': 3.00, 'output': 15.00},
|
|
5029
|
+
'haiku': {'input': 1.00, 'output': 5.00},
|
|
5030
|
+
'gpt-5.3-codex': {'input': 1.50, 'output': 12.00},
|
|
5031
|
+
'gemini-3-pro': {'input': 1.25, 'output': 10.00},
|
|
5032
|
+
'gemini-3-flash': {'input': 0.10, 'output': 0.40},
|
|
5033
|
+
}
|
|
5034
|
+
for f in glob.glob('${efficiency_dir}/*.json'):
|
|
5035
|
+
try:
|
|
5036
|
+
d = json.load(open(f))
|
|
5037
|
+
cost = d.get('cost_usd')
|
|
5038
|
+
if cost is not None:
|
|
5039
|
+
total += float(cost)
|
|
5040
|
+
else:
|
|
5041
|
+
model = d.get('model', 'sonnet').lower()
|
|
5042
|
+
p = pricing.get(model, pricing['sonnet'])
|
|
5043
|
+
inp = d.get('input_tokens', 0)
|
|
5044
|
+
out = d.get('output_tokens', 0)
|
|
5045
|
+
total += (inp / 1_000_000) * p['input'] + (out / 1_000_000) * p['output']
|
|
5046
|
+
except: pass
|
|
5047
|
+
print(round(total, 4))
|
|
5048
|
+
" 2>/dev/null || echo "0")
|
|
5049
|
+
fi
|
|
5050
|
+
|
|
5051
|
+
# Compare against limit
|
|
5052
|
+
local exceeded
|
|
5053
|
+
exceeded=$(python3 -c "
|
|
5054
|
+
import sys
|
|
5055
|
+
try:
|
|
5056
|
+
cost = float(sys.argv[1])
|
|
5057
|
+
limit = float(sys.argv[2])
|
|
5058
|
+
print(1 if cost >= limit else 0)
|
|
5059
|
+
except (ValueError, IndexError):
|
|
5060
|
+
print(0)
|
|
5061
|
+
" "$current_cost" "$BUDGET_LIMIT" 2>/dev/null || echo "0")
|
|
5062
|
+
|
|
5063
|
+
if [[ "$exceeded" == "1" ]]; then
|
|
5064
|
+
log_warn "BUDGET LIMIT REACHED: \$${current_cost} >= \$${BUDGET_LIMIT}"
|
|
5065
|
+
touch ".loki/PAUSE"
|
|
5066
|
+
mkdir -p ".loki/signals"
|
|
5067
|
+
echo "{\"type\":\"BUDGET_EXCEEDED\",\"limit\":${BUDGET_LIMIT},\"current\":${current_cost},\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > ".loki/signals/BUDGET_EXCEEDED"
|
|
5068
|
+
# Update budget.json with latest usage
|
|
5069
|
+
cat > ".loki/metrics/budget.json" << BUDGETUPD_EOF
|
|
5070
|
+
{
|
|
5071
|
+
"limit": $BUDGET_LIMIT,
|
|
5072
|
+
"budget_limit": $BUDGET_LIMIT,
|
|
5073
|
+
"budget_used": $current_cost,
|
|
5074
|
+
"exceeded": true,
|
|
5075
|
+
"exceeded_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
5076
|
+
}
|
|
5077
|
+
BUDGETUPD_EOF
|
|
5078
|
+
emit_event_json "budget_exceeded" \
|
|
5079
|
+
"limit=${BUDGET_LIMIT}" \
|
|
5080
|
+
"current=${current_cost}" \
|
|
5081
|
+
"iteration=$ITERATION_COUNT"
|
|
5082
|
+
return 0
|
|
5083
|
+
fi
|
|
5084
|
+
|
|
5085
|
+
# Update budget.json with current usage (not exceeded)
|
|
5086
|
+
if [ -n "$current_cost" ] && [ "$current_cost" != "0" ]; then
|
|
5087
|
+
cat > ".loki/metrics/budget.json" << BUDGETUPD_EOF
|
|
5088
|
+
{
|
|
5089
|
+
"limit": $BUDGET_LIMIT,
|
|
5090
|
+
"budget_limit": $BUDGET_LIMIT,
|
|
5091
|
+
"budget_used": $current_cost,
|
|
5092
|
+
"exceeded": false
|
|
5093
|
+
}
|
|
5094
|
+
BUDGETUPD_EOF
|
|
5095
|
+
fi
|
|
5096
|
+
|
|
5097
|
+
return 1
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5100
|
+
#===============================================================================
|
|
5101
|
+
# Watchdog: Process Supervision and Health Monitoring
|
|
5102
|
+
# Opt-in via LOKI_WATCHDOG=true. Detects crashed dashboard and agent processes.
|
|
5103
|
+
#===============================================================================
|
|
5104
|
+
|
|
5105
|
+
watchdog_check() {
|
|
5106
|
+
[[ "$WATCHDOG_ENABLED" != "true" ]] && return 0
|
|
5107
|
+
|
|
5108
|
+
# Check dashboard health
|
|
5109
|
+
local dashboard_pid_file=".loki/dashboard/dashboard.pid"
|
|
5110
|
+
if [[ -f "$dashboard_pid_file" ]]; then
|
|
5111
|
+
local dpid
|
|
5112
|
+
dpid=$(cat "$dashboard_pid_file" 2>/dev/null)
|
|
5113
|
+
if [[ -n "$dpid" ]] && ! kill -0 "$dpid" 2>/dev/null; then
|
|
5114
|
+
log_warn "WATCHDOG: Dashboard process $dpid is dead"
|
|
5115
|
+
emit_event_json "watchdog_alert" \
|
|
5116
|
+
"process=dashboard" \
|
|
5117
|
+
"pid=$dpid" \
|
|
5118
|
+
"action=detected_dead"
|
|
5119
|
+
|
|
5120
|
+
# Auto-restart dashboard if it was previously running
|
|
5121
|
+
if [[ "${ENABLE_DASHBOARD:-true}" == "true" ]]; then
|
|
5122
|
+
log_info "WATCHDOG: Restarting dashboard..."
|
|
5123
|
+
start_dashboard
|
|
5124
|
+
fi
|
|
5125
|
+
fi
|
|
5126
|
+
fi
|
|
5127
|
+
|
|
5128
|
+
# Check for zombie/dead agents
|
|
5129
|
+
local agents_file=".loki/state/agents.json"
|
|
5130
|
+
if [[ -f "$agents_file" ]]; then
|
|
5131
|
+
local dead_count=0
|
|
5132
|
+
local agent_pids
|
|
5133
|
+
agent_pids=$(python3 -c "
|
|
5134
|
+
import json, sys
|
|
5135
|
+
try:
|
|
5136
|
+
agents = json.load(open('$agents_file'))
|
|
5137
|
+
for a in agents:
|
|
5138
|
+
pid = a.get('pid')
|
|
5139
|
+
status = a.get('status', '')
|
|
5140
|
+
if pid and status not in ('terminated', 'completed', 'failed', 'crashed'):
|
|
5141
|
+
print(f\"{pid}:{a.get('id','unknown')}\")
|
|
5142
|
+
except Exception:
|
|
5143
|
+
pass
|
|
5144
|
+
" 2>/dev/null || true)
|
|
5145
|
+
|
|
5146
|
+
if [[ -n "$agent_pids" ]]; then
|
|
5147
|
+
while IFS=: read -r apid aid; do
|
|
5148
|
+
[[ -z "$apid" ]] && continue
|
|
5149
|
+
if ! kill -0 "$apid" 2>/dev/null; then
|
|
5150
|
+
dead_count=$((dead_count + 1))
|
|
5151
|
+
log_warn "WATCHDOG: Agent $aid (PID $apid) is dead"
|
|
5152
|
+
# Update agent status in agents.json
|
|
5153
|
+
python3 -c "
|
|
5154
|
+
import json
|
|
5155
|
+
try:
|
|
5156
|
+
with open('$agents_file', 'r') as f:
|
|
5157
|
+
agents = json.load(f)
|
|
5158
|
+
for a in agents:
|
|
5159
|
+
if str(a.get('pid')) == '$apid':
|
|
5160
|
+
a['status'] = 'crashed'
|
|
5161
|
+
a['crashed_at'] = '$(date -u +%Y-%m-%dT%H:%M:%SZ)'
|
|
5162
|
+
with open('$agents_file', 'w') as f:
|
|
5163
|
+
json.dump(agents, f, indent=2)
|
|
5164
|
+
except Exception:
|
|
5165
|
+
pass
|
|
5166
|
+
" 2>/dev/null || true
|
|
5167
|
+
fi
|
|
5168
|
+
done <<< "$agent_pids"
|
|
5169
|
+
|
|
5170
|
+
if [[ $dead_count -gt 0 ]]; then
|
|
5171
|
+
emit_event_json "watchdog_alert" \
|
|
5172
|
+
"process=agents" \
|
|
5173
|
+
"dead_count=$dead_count"
|
|
5174
|
+
fi
|
|
5175
|
+
fi
|
|
5176
|
+
fi
|
|
5177
|
+
|
|
5178
|
+
return 0
|
|
5179
|
+
}
|
|
5180
|
+
|
|
4885
5181
|
# Check if completion promise is fulfilled in log output
|
|
4886
5182
|
check_completion_promise() {
|
|
4887
5183
|
local log_file="$1"
|
|
@@ -5454,6 +5750,9 @@ run_autonomous() {
|
|
|
5454
5750
|
log_info "Base wait: ${BASE_WAIT}s"
|
|
5455
5751
|
log_info "Max wait: ${MAX_WAIT}s"
|
|
5456
5752
|
log_info "Autonomy mode: $AUTONOMY_MODE"
|
|
5753
|
+
if [ -n "$BUDGET_LIMIT" ]; then
|
|
5754
|
+
log_info "Budget limit: \$$BUDGET_LIMIT"
|
|
5755
|
+
fi
|
|
5457
5756
|
# Only show Claude-specific features for Claude provider
|
|
5458
5757
|
if [ "${PROVIDER_NAME:-claude}" = "claude" ]; then
|
|
5459
5758
|
log_info "Prompt repetition (Haiku): $PROMPT_REPETITION"
|
|
@@ -5493,6 +5792,23 @@ run_autonomous() {
|
|
|
5493
5792
|
2) return 0 ;; # STOP requested
|
|
5494
5793
|
esac
|
|
5495
5794
|
|
|
5795
|
+
# Check budget limit (creates PAUSE file if exceeded)
|
|
5796
|
+
if check_budget_limit; then
|
|
5797
|
+
log_warn "Session paused due to budget limit. Remove .loki/PAUSE to resume."
|
|
5798
|
+
save_state $retry "budget_exceeded" 0
|
|
5799
|
+
continue # Will hit PAUSE check on next iteration
|
|
5800
|
+
fi
|
|
5801
|
+
|
|
5802
|
+
# Watchdog: periodic process health check (opt-in via LOKI_WATCHDOG=true)
|
|
5803
|
+
if [[ "$WATCHDOG_ENABLED" == "true" ]]; then
|
|
5804
|
+
local now_epoch
|
|
5805
|
+
now_epoch=$(date +%s)
|
|
5806
|
+
if (( now_epoch - LAST_WATCHDOG_CHECK >= WATCHDOG_INTERVAL )); then
|
|
5807
|
+
watchdog_check
|
|
5808
|
+
LAST_WATCHDOG_CHECK=$now_epoch
|
|
5809
|
+
fi
|
|
5810
|
+
fi
|
|
5811
|
+
|
|
5496
5812
|
# Auto-track iteration start (for dashboard task queue)
|
|
5497
5813
|
track_iteration_start "$ITERATION_COUNT" "$prd_path"
|
|
5498
5814
|
|
|
@@ -6372,6 +6688,11 @@ main() {
|
|
|
6372
6688
|
fi
|
|
6373
6689
|
fi
|
|
6374
6690
|
|
|
6691
|
+
# Validate API keys for the selected provider
|
|
6692
|
+
if ! validate_api_keys; then
|
|
6693
|
+
exit 1
|
|
6694
|
+
fi
|
|
6695
|
+
|
|
6375
6696
|
# Check prerequisites (unless skipped)
|
|
6376
6697
|
if [ "$SKIP_PREREQS" != "true" ]; then
|
|
6377
6698
|
if ! check_prerequisites; then
|
package/autonomy/sandbox.sh
CHANGED
|
@@ -727,7 +727,7 @@ docker_desktop_sandbox_prompt() {
|
|
|
727
727
|
docker sandbox exec -w "$PROJECT_DIR" \
|
|
728
728
|
${DESKTOP_ENV_ARGS[@]+"${DESKTOP_ENV_ARGS[@]}"} \
|
|
729
729
|
"$DESKTOP_SANDBOX_NAME" \
|
|
730
|
-
bash -c "
|
|
730
|
+
bash -c "printf '%s\n' \"\$1\" > ${PROJECT_DIR}/.loki/HUMAN_INPUT.md" -- "$message"
|
|
731
731
|
|
|
732
732
|
log_success "Prompt sent to sandbox"
|
|
733
733
|
}
|
package/autonomy/serve.sh
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
# LOKI_DASHBOARD_PORT Port (default: 57374)
|
|
20
20
|
# LOKI_DASHBOARD_HOST Host (default: localhost)
|
|
21
21
|
# LOKI_API_TOKEN API token for remote access
|
|
22
|
+
# LOKI_TLS_CERT Path to PEM certificate (enables HTTPS)
|
|
23
|
+
# LOKI_TLS_KEY Path to PEM private key (enables HTTPS)
|
|
22
24
|
#===============================================================================
|
|
23
25
|
|
|
24
26
|
set -euo pipefail
|
|
@@ -30,6 +32,8 @@ API_DIR="$PROJECT_DIR/api"
|
|
|
30
32
|
# Default configuration
|
|
31
33
|
PORT="${LOKI_DASHBOARD_PORT:-57374}"
|
|
32
34
|
HOST="${LOKI_DASHBOARD_HOST:-localhost}"
|
|
35
|
+
TLS_CERT="${LOKI_TLS_CERT:-}"
|
|
36
|
+
TLS_KEY="${LOKI_TLS_KEY:-}"
|
|
33
37
|
CORS="true"
|
|
34
38
|
AUTH="true"
|
|
35
39
|
|
|
@@ -65,6 +69,8 @@ Usage:
|
|
|
65
69
|
Options:
|
|
66
70
|
--port, -p <port> Port to listen on (default: 57374)
|
|
67
71
|
--host <host> Host to bind to (default: localhost)
|
|
72
|
+
--tls-cert <path> Path to PEM certificate (enables HTTPS)
|
|
73
|
+
--tls-key <path> Path to PEM private key (enables HTTPS)
|
|
68
74
|
--no-cors Disable CORS
|
|
69
75
|
--no-auth Disable authentication
|
|
70
76
|
--generate-token Generate a new API token
|
|
@@ -74,6 +80,8 @@ Environment Variables:
|
|
|
74
80
|
LOKI_DASHBOARD_PORT Port (overridden by --port)
|
|
75
81
|
LOKI_DASHBOARD_HOST Host (overridden by --host)
|
|
76
82
|
LOKI_API_TOKEN API token for remote access
|
|
83
|
+
LOKI_TLS_CERT Path to PEM certificate (overridden by --tls-cert)
|
|
84
|
+
LOKI_TLS_KEY Path to PEM private key (overridden by --tls-key)
|
|
77
85
|
LOKI_DIR Loki installation directory
|
|
78
86
|
LOKI_DEBUG Enable debug output
|
|
79
87
|
|
|
@@ -88,6 +96,9 @@ Examples:
|
|
|
88
96
|
export LOKI_API_TOKEN=\$(loki serve --generate-token)
|
|
89
97
|
loki serve --host 0.0.0.0
|
|
90
98
|
|
|
99
|
+
# Enable HTTPS with TLS
|
|
100
|
+
loki serve --tls-cert /path/to/cert.pem --tls-key /path/to/key.pem
|
|
101
|
+
|
|
91
102
|
# Connect from another machine
|
|
92
103
|
curl -H "Authorization: Bearer \$TOKEN" http://server:57374/health
|
|
93
104
|
|
|
@@ -131,6 +142,14 @@ main() {
|
|
|
131
142
|
HOST="$2"
|
|
132
143
|
shift 2
|
|
133
144
|
;;
|
|
145
|
+
--tls-cert)
|
|
146
|
+
TLS_CERT="$2"
|
|
147
|
+
shift 2
|
|
148
|
+
;;
|
|
149
|
+
--tls-key)
|
|
150
|
+
TLS_KEY="$2"
|
|
151
|
+
shift 2
|
|
152
|
+
;;
|
|
134
153
|
--no-cors)
|
|
135
154
|
CORS="false"
|
|
136
155
|
shift
|
|
@@ -195,6 +214,12 @@ main() {
|
|
|
195
214
|
[ "$CORS" = "false" ] && server_args+=("--no-cors")
|
|
196
215
|
[ "$AUTH" = "false" ] && server_args+=("--no-auth")
|
|
197
216
|
|
|
217
|
+
# Pass TLS options if configured
|
|
218
|
+
if [ -n "$TLS_CERT" ] && [ -n "$TLS_KEY" ]; then
|
|
219
|
+
server_args+=("--tls-cert" "$TLS_CERT" "--tls-key" "$TLS_KEY")
|
|
220
|
+
log_info "TLS enabled: cert=$TLS_CERT key=$TLS_KEY"
|
|
221
|
+
fi
|
|
222
|
+
|
|
198
223
|
# Export environment variables
|
|
199
224
|
export LOKI_DIR="$PROJECT_DIR"
|
|
200
225
|
export LOKI_VERSION="$(cat "$PROJECT_DIR/VERSION" 2>/dev/null || echo "dev")"
|
package/dashboard/__init__.py
CHANGED
package/dashboard/audit.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Audit Logging Module for Loki Mode Dashboard.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Enabled by default. Disable with LOKI_AUDIT_DISABLED=true environment variable.
|
|
5
|
+
Legacy env var LOKI_ENTERPRISE_AUDIT=true always enables audit (backward compat).
|
|
6
6
|
|
|
7
7
|
Audit logs: ~/.loki/dashboard/audit/
|
|
8
8
|
"""
|
|
@@ -14,7 +14,11 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Optional
|
|
15
15
|
|
|
16
16
|
# Configuration
|
|
17
|
-
|
|
17
|
+
# Audit is ON by default. Disable with LOKI_AUDIT_DISABLED=true.
|
|
18
|
+
# Backward compat: LOKI_ENTERPRISE_AUDIT=true always forces audit ON.
|
|
19
|
+
_audit_disabled = os.environ.get("LOKI_AUDIT_DISABLED", "").lower() in ("true", "1", "yes")
|
|
20
|
+
_enterprise_force_on = os.environ.get("LOKI_ENTERPRISE_AUDIT", "").lower() in ("true", "1", "yes")
|
|
21
|
+
ENTERPRISE_AUDIT_ENABLED = _enterprise_force_on or (not _audit_disabled)
|
|
18
22
|
AUDIT_DIR = Path.home() / ".loki" / "dashboard" / "audit"
|
|
19
23
|
|
|
20
24
|
# Log rotation settings
|
|
@@ -251,5 +255,5 @@ def get_audit_summary(days: int = 7) -> dict:
|
|
|
251
255
|
|
|
252
256
|
|
|
253
257
|
def is_audit_enabled() -> bool:
|
|
254
|
-
"""Check if audit logging is enabled."""
|
|
258
|
+
"""Check if audit logging is enabled (on by default, disable with LOKI_AUDIT_DISABLED=true)."""
|
|
255
259
|
return ENTERPRISE_AUDIT_ENABLED
|