loki-mode 5.6.0 → 5.6.1
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 +18 -6
- package/VERSION +1 -1
- package/autonomy/run.sh +50 -15
- package/autonomy/sandbox.sh +875 -30
- package/package.json +2 -2
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with zero human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v5.6.
|
|
6
|
+
# Loki Mode v5.6.1
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -210,21 +210,33 @@ When running with `autonomy/run.sh`, you can intervene:
|
|
|
210
210
|
| Method | Effect |
|
|
211
211
|
|--------|--------|
|
|
212
212
|
| `touch .loki/PAUSE` | Pauses after current session |
|
|
213
|
-
| `echo "instructions" > .loki/HUMAN_INPUT.md` | Injects directive
|
|
213
|
+
| `echo "instructions" > .loki/HUMAN_INPUT.md` | Injects directive (requires `LOKI_PROMPT_INJECTION=true`) |
|
|
214
214
|
| `touch .loki/STOP` | Stops immediately |
|
|
215
215
|
| Ctrl+C (once) | Pauses, shows options |
|
|
216
216
|
| Ctrl+C (twice) | Exits immediately |
|
|
217
217
|
|
|
218
|
+
### Security: Prompt Injection (v5.6.1)
|
|
219
|
+
|
|
220
|
+
**DISABLED by default** for enterprise security. Prompt injection via `HUMAN_INPUT.md` is blocked unless explicitly enabled.
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Enable prompt injection (only in trusted environments)
|
|
224
|
+
LOKI_PROMPT_INJECTION=true loki start ./prd.md
|
|
225
|
+
|
|
226
|
+
# Or for sandbox mode
|
|
227
|
+
LOKI_PROMPT_INJECTION=true loki sandbox prompt "start the app"
|
|
228
|
+
```
|
|
229
|
+
|
|
218
230
|
### Hints vs Directives
|
|
219
231
|
|
|
220
232
|
| Type | File | Behavior |
|
|
221
233
|
|------|------|----------|
|
|
222
234
|
| **Hint** | `.loki/CONTINUITY.md` "Mistakes & Learnings" | Passive memory - remembered but not acted upon |
|
|
223
|
-
| **Directive** | `.loki/HUMAN_INPUT.md` | Active instruction
|
|
235
|
+
| **Directive** | `.loki/HUMAN_INPUT.md` | Active instruction (requires `LOKI_PROMPT_INJECTION=true`) |
|
|
224
236
|
|
|
225
|
-
**Example directive** (
|
|
237
|
+
**Example directive** (only works with `LOKI_PROMPT_INJECTION=true`):
|
|
226
238
|
```bash
|
|
227
|
-
echo "Check all .astro files for missing BaseLayout imports.
|
|
239
|
+
echo "Check all .astro files for missing BaseLayout imports." > .loki/HUMAN_INPUT.md
|
|
228
240
|
```
|
|
229
241
|
|
|
230
242
|
---
|
|
@@ -241,4 +253,4 @@ Auto-detected or force with `LOKI_COMPLEXITY`:
|
|
|
241
253
|
|
|
242
254
|
---
|
|
243
255
|
|
|
244
|
-
**v5.6.
|
|
256
|
+
**v5.6.1 | Prompt injection disabled by default (enterprise security) | ~250 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.6.
|
|
1
|
+
5.6.1
|
package/autonomy/run.sh
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
# shellcheck disable=SC2034 # Many variables are used by sourced scripts
|
|
3
|
+
# shellcheck disable=SC2155 # Declare and assign separately (acceptable in this codebase)
|
|
4
|
+
# shellcheck disable=SC2329 # Functions may be invoked indirectly or via dynamic dispatch
|
|
5
|
+
# shellcheck disable=SC2086 # Word splitting is intentional in some contexts
|
|
2
6
|
#===============================================================================
|
|
3
7
|
# Loki Mode - Autonomous Runner
|
|
4
8
|
# Single script that handles prerequisites, setup, and autonomous execution
|
|
@@ -106,6 +110,10 @@
|
|
|
106
110
|
# STOP file: touch .loki/STOP - stops immediately
|
|
107
111
|
# Ctrl+C (once): Pauses execution, shows options
|
|
108
112
|
# Ctrl+C (twice): Exits immediately
|
|
113
|
+
#
|
|
114
|
+
# Security (Enterprise):
|
|
115
|
+
# LOKI_PROMPT_INJECTION - Enable HUMAN_INPUT.md processing (default: false)
|
|
116
|
+
# Set to "true" only in trusted environments
|
|
109
117
|
#===============================================================================
|
|
110
118
|
|
|
111
119
|
set -uo pipefail
|
|
@@ -513,7 +521,9 @@ if [ "$BASH_VERSION_MAJOR" -ge 4 ] 2>/dev/null; then
|
|
|
513
521
|
declare -A WORKTREE_PATHS
|
|
514
522
|
else
|
|
515
523
|
# Fallback: parallel mode will check and warn
|
|
524
|
+
# shellcheck disable=SC2178
|
|
516
525
|
WORKTREE_PIDS=""
|
|
526
|
+
# shellcheck disable=SC2178
|
|
517
527
|
WORKTREE_PATHS=""
|
|
518
528
|
fi
|
|
519
529
|
|
|
@@ -939,6 +949,7 @@ import_github_issues() {
|
|
|
939
949
|
# Append to pending.json with temp file cleanup on error
|
|
940
950
|
local temp_file
|
|
941
951
|
temp_file=$(mktemp)
|
|
952
|
+
# shellcheck disable=SC2064
|
|
942
953
|
trap "rm -f '$temp_file'" RETURN
|
|
943
954
|
if jq ".tasks += [$task_json]" "$pending_file" > "$temp_file" && mv "$temp_file" "$pending_file"; then
|
|
944
955
|
log_info "Imported issue #$number: $title"
|
|
@@ -1320,8 +1331,8 @@ remove_worktree() {
|
|
|
1320
1331
|
fi
|
|
1321
1332
|
}
|
|
1322
1333
|
|
|
1323
|
-
unset WORKTREE_PATHS[$stream_name]
|
|
1324
|
-
unset WORKTREE_PIDS[$stream_name]
|
|
1334
|
+
unset "WORKTREE_PATHS[$stream_name]"
|
|
1335
|
+
unset "WORKTREE_PIDS[$stream_name]"
|
|
1325
1336
|
|
|
1326
1337
|
log_info "Removed worktree: $stream_name"
|
|
1327
1338
|
}
|
|
@@ -1642,7 +1653,7 @@ run_parallel_orchestrator() {
|
|
|
1642
1653
|
local pid="${WORKTREE_PIDS[$stream]}"
|
|
1643
1654
|
if ! kill -0 "$pid" 2>/dev/null; then
|
|
1644
1655
|
log_warn "Session ended: $stream"
|
|
1645
|
-
unset WORKTREE_PIDS[$stream]
|
|
1656
|
+
unset "WORKTREE_PIDS[$stream]"
|
|
1646
1657
|
fi
|
|
1647
1658
|
done
|
|
1648
1659
|
|
|
@@ -4076,19 +4087,43 @@ check_human_intervention() {
|
|
|
4076
4087
|
return 1
|
|
4077
4088
|
fi
|
|
4078
4089
|
|
|
4079
|
-
# Check for HUMAN_INPUT.md
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
# Move to processed
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4090
|
+
# Check for HUMAN_INPUT.md (prompt injection)
|
|
4091
|
+
# Security: Check it's a regular file (not symlink) to prevent symlink attacks
|
|
4092
|
+
if [ -f "$loki_dir/HUMAN_INPUT.md" ] && [ ! -L "$loki_dir/HUMAN_INPUT.md" ]; then
|
|
4093
|
+
# Security: Prompt injection disabled by default for enterprise security
|
|
4094
|
+
if [ "${LOKI_PROMPT_INJECTION:-false}" != "true" ]; then
|
|
4095
|
+
log_warn "HUMAN_INPUT.md detected but prompt injection is DISABLED"
|
|
4096
|
+
log_warn "To enable, set LOKI_PROMPT_INJECTION=true (only in trusted environments)"
|
|
4097
|
+
# Move to rejected instead of processed
|
|
4098
|
+
mkdir -p "$loki_dir/logs" 2>/dev/null
|
|
4099
|
+
mv "$loki_dir/HUMAN_INPUT.md" "$loki_dir/logs/human-input-REJECTED-$(date +%Y%m%d-%H%M%S).md" 2>/dev/null || rm -f "$loki_dir/HUMAN_INPUT.md"
|
|
4100
|
+
else
|
|
4101
|
+
# Security: Check file size (1MB limit)
|
|
4102
|
+
local file_size
|
|
4103
|
+
file_size=$(stat -f%z "$loki_dir/HUMAN_INPUT.md" 2>/dev/null || stat -c%s "$loki_dir/HUMAN_INPUT.md" 2>/dev/null || echo "0")
|
|
4104
|
+
if [ "$file_size" -gt 1048576 ]; then
|
|
4105
|
+
log_warn "HUMAN_INPUT.md exceeds 1MB size limit, rejecting"
|
|
4106
|
+
mkdir -p "$loki_dir/logs" 2>/dev/null
|
|
4107
|
+
mv "$loki_dir/HUMAN_INPUT.md" "$loki_dir/logs/human-input-REJECTED-TOOLARGE-$(date +%Y%m%d-%H%M%S).md" 2>/dev/null || rm -f "$loki_dir/HUMAN_INPUT.md"
|
|
4108
|
+
else
|
|
4109
|
+
local human_input=$(cat "$loki_dir/HUMAN_INPUT.md")
|
|
4110
|
+
if [ -n "$human_input" ]; then
|
|
4111
|
+
log_info "Human input detected:"
|
|
4112
|
+
echo "$human_input"
|
|
4113
|
+
echo ""
|
|
4114
|
+
# Move to processed
|
|
4115
|
+
mkdir -p "$loki_dir/logs" 2>/dev/null
|
|
4116
|
+
mv "$loki_dir/HUMAN_INPUT.md" "$loki_dir/logs/human-input-$(date +%Y%m%d-%H%M%S).md"
|
|
4117
|
+
# Inject into next prompt
|
|
4118
|
+
export LOKI_HUMAN_INPUT="$human_input"
|
|
4119
|
+
return 0
|
|
4120
|
+
fi
|
|
4121
|
+
fi
|
|
4091
4122
|
fi
|
|
4123
|
+
elif [ -L "$loki_dir/HUMAN_INPUT.md" ]; then
|
|
4124
|
+
# Security: Reject symlinks
|
|
4125
|
+
log_warn "HUMAN_INPUT.md is a symlink - rejected for security"
|
|
4126
|
+
rm -f "$loki_dir/HUMAN_INPUT.md"
|
|
4092
4127
|
fi
|
|
4093
4128
|
|
|
4094
4129
|
# Check for STOP file (immediate stop)
|
package/autonomy/sandbox.sh
CHANGED
|
@@ -40,7 +40,13 @@ BOLD='\033[1m'
|
|
|
40
40
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
41
41
|
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
|
|
42
42
|
PROJECT_DIR="${LOKI_PROJECT_DIR:-$(pwd)}"
|
|
43
|
-
|
|
43
|
+
# Normalize PROJECT_DIR (remove trailing slash)
|
|
44
|
+
PROJECT_DIR="${PROJECT_DIR%/}"
|
|
45
|
+
|
|
46
|
+
# Container name includes path hash to avoid collisions between similarly-named projects
|
|
47
|
+
# macOS uses md5 instead of md5sum
|
|
48
|
+
PROJECT_HASH=$(echo "$PROJECT_DIR" | md5sum 2>/dev/null | cut -c1-8 || md5 2>/dev/null | cut -c1-8 || echo "$$")
|
|
49
|
+
CONTAINER_NAME="loki-sandbox-$(basename "$PROJECT_DIR" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')-${PROJECT_HASH}"
|
|
44
50
|
|
|
45
51
|
# Sandbox settings
|
|
46
52
|
SANDBOX_IMAGE="${LOKI_SANDBOX_IMAGE:-loki-mode:sandbox}"
|
|
@@ -53,6 +59,9 @@ SANDBOX_READONLY="${LOKI_SANDBOX_READONLY:-false}"
|
|
|
53
59
|
API_PORT="${LOKI_API_PORT:-9898}"
|
|
54
60
|
DASHBOARD_PORT="${LOKI_DASHBOARD_PORT:-57374}"
|
|
55
61
|
|
|
62
|
+
# Security: Prompt injection disabled by default for enterprise security
|
|
63
|
+
PROMPT_INJECTION_ENABLED="${LOKI_PROMPT_INJECTION:-false}"
|
|
64
|
+
|
|
56
65
|
#===============================================================================
|
|
57
66
|
# Utility Functions
|
|
58
67
|
#===============================================================================
|
|
@@ -73,6 +82,65 @@ log_error() {
|
|
|
73
82
|
echo -e "${RED}[SANDBOX]${NC} $1" >&2
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
# Check if a port is available
|
|
86
|
+
check_port_available() {
|
|
87
|
+
local port="$1"
|
|
88
|
+
if command -v lsof &>/dev/null; then
|
|
89
|
+
! lsof -i ":$port" &>/dev/null
|
|
90
|
+
elif command -v nc &>/dev/null; then
|
|
91
|
+
! nc -z localhost "$port" 2>/dev/null
|
|
92
|
+
else
|
|
93
|
+
# Assume available if we can't check
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Validate project directory
|
|
99
|
+
validate_project_dir() {
|
|
100
|
+
if [[ ! -d "$PROJECT_DIR" ]]; then
|
|
101
|
+
log_error "Project directory does not exist: $PROJECT_DIR"
|
|
102
|
+
return 1
|
|
103
|
+
fi
|
|
104
|
+
if [[ ! -r "$PROJECT_DIR" ]]; then
|
|
105
|
+
log_error "Project directory is not readable: $PROJECT_DIR"
|
|
106
|
+
return 1
|
|
107
|
+
fi
|
|
108
|
+
if [[ "$SANDBOX_READONLY" != "true" ]] && [[ ! -w "$PROJECT_DIR" ]]; then
|
|
109
|
+
log_warn "Project directory is not writable: $PROJECT_DIR"
|
|
110
|
+
log_info "Consider using LOKI_SANDBOX_READONLY=true"
|
|
111
|
+
fi
|
|
112
|
+
return 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Warn about API keys based on provider
|
|
116
|
+
warn_missing_api_keys() {
|
|
117
|
+
local provider="$1"
|
|
118
|
+
case "$provider" in
|
|
119
|
+
claude)
|
|
120
|
+
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
|
|
121
|
+
log_warn "ANTHROPIC_API_KEY not set - Claude commands will fail inside container"
|
|
122
|
+
fi
|
|
123
|
+
;;
|
|
124
|
+
codex)
|
|
125
|
+
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
|
126
|
+
log_warn "OPENAI_API_KEY not set - Codex commands will fail inside container"
|
|
127
|
+
fi
|
|
128
|
+
;;
|
|
129
|
+
gemini)
|
|
130
|
+
if [[ -z "${GOOGLE_API_KEY:-}" ]]; then
|
|
131
|
+
log_warn "GOOGLE_API_KEY not set - Gemini commands will fail inside container"
|
|
132
|
+
fi
|
|
133
|
+
;;
|
|
134
|
+
esac
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Cleanup handler for signals
|
|
138
|
+
cleanup_container() {
|
|
139
|
+
log_warn "Interrupted - cleaning up container..."
|
|
140
|
+
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
|
141
|
+
exit 130
|
|
142
|
+
}
|
|
143
|
+
|
|
76
144
|
check_docker() {
|
|
77
145
|
if ! command -v docker &> /dev/null; then
|
|
78
146
|
log_error "Docker not found. Install Docker to use sandbox mode."
|
|
@@ -107,6 +175,344 @@ ensure_image() {
|
|
|
107
175
|
fi
|
|
108
176
|
}
|
|
109
177
|
|
|
178
|
+
#===============================================================================
|
|
179
|
+
# Git Worktree Sandbox (Fallback for non-Docker environments)
|
|
180
|
+
#===============================================================================
|
|
181
|
+
|
|
182
|
+
# Worktree sandbox settings
|
|
183
|
+
WORKTREE_PREFIX="loki-sandbox"
|
|
184
|
+
WORKTREE_BASE="${LOKI_WORKTREE_BASE:-${TMPDIR:-/tmp}}"
|
|
185
|
+
WORKTREE_STATE_FILE="${PROJECT_DIR}/.loki/sandbox/worktree-state.json"
|
|
186
|
+
|
|
187
|
+
# Check if Docker is available (non-fatal version)
|
|
188
|
+
is_docker_available() {
|
|
189
|
+
command -v docker &>/dev/null && docker info &>/dev/null 2>&1
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Check if git worktree is available
|
|
193
|
+
is_git_available() {
|
|
194
|
+
command -v git &>/dev/null && git rev-parse --git-dir &>/dev/null 2>&1
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Detect which sandbox mode to use
|
|
198
|
+
detect_sandbox_mode() {
|
|
199
|
+
local requested="${1:-auto}"
|
|
200
|
+
|
|
201
|
+
case "$requested" in
|
|
202
|
+
docker)
|
|
203
|
+
if is_docker_available; then
|
|
204
|
+
echo "docker"
|
|
205
|
+
else
|
|
206
|
+
log_error "Docker requested but not available"
|
|
207
|
+
return 1
|
|
208
|
+
fi
|
|
209
|
+
;;
|
|
210
|
+
worktree)
|
|
211
|
+
if is_git_available; then
|
|
212
|
+
echo "worktree"
|
|
213
|
+
else
|
|
214
|
+
log_error "Git not available for worktree sandbox"
|
|
215
|
+
return 1
|
|
216
|
+
fi
|
|
217
|
+
;;
|
|
218
|
+
auto|*)
|
|
219
|
+
if is_docker_available; then
|
|
220
|
+
echo "docker"
|
|
221
|
+
elif is_git_available; then
|
|
222
|
+
log_warn "Docker not available - using worktree sandbox (soft isolation)"
|
|
223
|
+
echo "worktree"
|
|
224
|
+
else
|
|
225
|
+
log_error "Neither Docker nor Git available for sandbox mode"
|
|
226
|
+
return 1
|
|
227
|
+
fi
|
|
228
|
+
;;
|
|
229
|
+
esac
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Create a worktree sandbox
|
|
233
|
+
create_worktree_sandbox() {
|
|
234
|
+
local prd_path="${1:-}"
|
|
235
|
+
local provider="${LOKI_PROVIDER:-claude}"
|
|
236
|
+
|
|
237
|
+
local timestamp=$(date +%Y%m%d-%H%M%S)
|
|
238
|
+
local sandbox_name="${WORKTREE_PREFIX}-${timestamp}"
|
|
239
|
+
local sandbox_branch="${WORKTREE_PREFIX}-${timestamp}"
|
|
240
|
+
local sandbox_path="${WORKTREE_BASE}/${sandbox_name}"
|
|
241
|
+
|
|
242
|
+
log_warn ""
|
|
243
|
+
log_warn "=========================================="
|
|
244
|
+
log_warn " WORKTREE SANDBOX - SOFT ISOLATION ONLY"
|
|
245
|
+
log_warn "=========================================="
|
|
246
|
+
log_warn ""
|
|
247
|
+
log_warn "This provides workspace isolation but NOT:"
|
|
248
|
+
log_warn " - Filesystem isolation (can access any file)"
|
|
249
|
+
log_warn " - Network isolation (full network access)"
|
|
250
|
+
log_warn " - Process isolation (no resource limits)"
|
|
251
|
+
log_warn ""
|
|
252
|
+
log_warn "For full isolation, install Docker."
|
|
253
|
+
log_warn ""
|
|
254
|
+
|
|
255
|
+
log_info "Creating worktree sandbox..."
|
|
256
|
+
log_info " Location: $sandbox_path"
|
|
257
|
+
log_info " Branch: $sandbox_branch"
|
|
258
|
+
|
|
259
|
+
# Check for existing sandbox
|
|
260
|
+
if [[ -f "$WORKTREE_STATE_FILE" ]]; then
|
|
261
|
+
local existing_path=$(jq -r '.sandbox_path // empty' "$WORKTREE_STATE_FILE" 2>/dev/null)
|
|
262
|
+
if [[ -n "$existing_path" ]] && [[ -d "$existing_path" ]]; then
|
|
263
|
+
log_warn "Existing sandbox found: $existing_path"
|
|
264
|
+
log_info "Use 'loki sandbox stop' to stop it first."
|
|
265
|
+
return 1
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# Create branch for sandbox
|
|
270
|
+
if ! git branch "$sandbox_branch" HEAD 2>/dev/null; then
|
|
271
|
+
log_error "Failed to create sandbox branch. Are you in a git repository?"
|
|
272
|
+
return 1
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
# Create worktree
|
|
276
|
+
if ! git worktree add "$sandbox_path" "$sandbox_branch" 2>/dev/null; then
|
|
277
|
+
git branch -D "$sandbox_branch" 2>/dev/null
|
|
278
|
+
log_error "Failed to create worktree at $sandbox_path"
|
|
279
|
+
return 1
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# Set up sandbox environment
|
|
283
|
+
mkdir -p "$sandbox_path/.loki/"{state,logs,signals,queue,memory}
|
|
284
|
+
|
|
285
|
+
# Copy essential files
|
|
286
|
+
for file in ".loki/CONTINUITY.md" ".loki/config.yaml" "SKILL.md" "CLAUDE.md"; do
|
|
287
|
+
if [[ -f "$PROJECT_DIR/$file" ]]; then
|
|
288
|
+
mkdir -p "$(dirname "$sandbox_path/$file")"
|
|
289
|
+
cp "$PROJECT_DIR/$file" "$sandbox_path/$file" 2>/dev/null || true
|
|
290
|
+
fi
|
|
291
|
+
done
|
|
292
|
+
|
|
293
|
+
# Create sandbox marker
|
|
294
|
+
cat > "$sandbox_path/.loki/SANDBOX_MODE" << EOF
|
|
295
|
+
ISOLATION_TYPE=worktree
|
|
296
|
+
CREATED_AT=$(date -Iseconds)
|
|
297
|
+
PARENT_DIR=$PROJECT_DIR
|
|
298
|
+
EOF
|
|
299
|
+
|
|
300
|
+
# Save state
|
|
301
|
+
mkdir -p "$(dirname "$WORKTREE_STATE_FILE")"
|
|
302
|
+
cat > "$WORKTREE_STATE_FILE" << EOF
|
|
303
|
+
{
|
|
304
|
+
"sandbox_path": "$sandbox_path",
|
|
305
|
+
"sandbox_branch": "$sandbox_branch",
|
|
306
|
+
"created_at": "$(date -Iseconds)",
|
|
307
|
+
"provider": "$provider",
|
|
308
|
+
"prd_path": "$prd_path",
|
|
309
|
+
"status": "created",
|
|
310
|
+
"isolation_type": "worktree"
|
|
311
|
+
}
|
|
312
|
+
EOF
|
|
313
|
+
|
|
314
|
+
log_success "Worktree sandbox created: $sandbox_path"
|
|
315
|
+
return 0
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
# Start loki in worktree sandbox
|
|
319
|
+
start_worktree_sandbox() {
|
|
320
|
+
local prd_path="${1:-}"
|
|
321
|
+
local provider="${LOKI_PROVIDER:-claude}"
|
|
322
|
+
|
|
323
|
+
# Create sandbox if needed
|
|
324
|
+
if ! [[ -f "$WORKTREE_STATE_FILE" ]]; then
|
|
325
|
+
create_worktree_sandbox "$prd_path" || return 1
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
local sandbox_path=$(jq -r '.sandbox_path' "$WORKTREE_STATE_FILE")
|
|
329
|
+
|
|
330
|
+
if [[ ! -d "$sandbox_path" ]]; then
|
|
331
|
+
log_error "Sandbox path does not exist: $sandbox_path"
|
|
332
|
+
rm -f "$WORKTREE_STATE_FILE"
|
|
333
|
+
return 1
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
log_info "Starting Loki in worktree sandbox..."
|
|
337
|
+
log_info " Path: $sandbox_path"
|
|
338
|
+
log_info " Provider: $provider"
|
|
339
|
+
|
|
340
|
+
# Build loki command
|
|
341
|
+
local loki_cmd="$SKILL_DIR/autonomy/run.sh"
|
|
342
|
+
if [[ -n "$prd_path" ]]; then
|
|
343
|
+
if [[ -f "$sandbox_path/$prd_path" ]]; then
|
|
344
|
+
loki_cmd="$loki_cmd $prd_path"
|
|
345
|
+
elif [[ -f "$prd_path" ]]; then
|
|
346
|
+
cp "$prd_path" "$sandbox_path/" 2>/dev/null || true
|
|
347
|
+
loki_cmd="$loki_cmd $(basename "$prd_path")"
|
|
348
|
+
fi
|
|
349
|
+
fi
|
|
350
|
+
loki_cmd="$loki_cmd --provider $provider"
|
|
351
|
+
|
|
352
|
+
# Set environment
|
|
353
|
+
export LOKI_SANDBOX_MODE=true
|
|
354
|
+
export LOKI_SANDBOX_TYPE=worktree
|
|
355
|
+
export LOKI_NOTIFICATIONS=false
|
|
356
|
+
|
|
357
|
+
log_info ""
|
|
358
|
+
log_info "Commands (in another terminal):"
|
|
359
|
+
log_info " loki sandbox status - Check status"
|
|
360
|
+
log_info " loki sandbox prompt 'msg' - Send prompt"
|
|
361
|
+
log_info " loki sandbox stop - Stop sandbox"
|
|
362
|
+
log_info ""
|
|
363
|
+
|
|
364
|
+
# Run loki in sandbox directory
|
|
365
|
+
cd "$sandbox_path" && $loki_cmd
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# Stop worktree sandbox
|
|
369
|
+
stop_worktree_sandbox() {
|
|
370
|
+
if [[ ! -f "$WORKTREE_STATE_FILE" ]]; then
|
|
371
|
+
log_warn "No active worktree sandbox found"
|
|
372
|
+
return 0
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
local sandbox_path=$(jq -r '.sandbox_path' "$WORKTREE_STATE_FILE")
|
|
376
|
+
local sandbox_branch=$(jq -r '.sandbox_branch' "$WORKTREE_STATE_FILE")
|
|
377
|
+
|
|
378
|
+
log_info "Stopping worktree sandbox..."
|
|
379
|
+
|
|
380
|
+
# Send stop signal
|
|
381
|
+
if [[ -d "$sandbox_path" ]]; then
|
|
382
|
+
touch "$sandbox_path/.loki/STOP" 2>/dev/null || true
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
# Cleanup
|
|
386
|
+
if [[ "${LOKI_SANDBOX_CLEANUP:-true}" == "true" ]]; then
|
|
387
|
+
log_info "Cleaning up worktree..."
|
|
388
|
+
|
|
389
|
+
if [[ -n "$sandbox_path" ]] && [[ -d "$sandbox_path" ]]; then
|
|
390
|
+
git worktree remove "$sandbox_path" --force 2>/dev/null || rm -rf "$sandbox_path" 2>/dev/null
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
if [[ -n "$sandbox_branch" ]]; then
|
|
394
|
+
git branch -D "$sandbox_branch" 2>/dev/null || true
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
git worktree prune 2>/dev/null || true
|
|
398
|
+
rm -f "$WORKTREE_STATE_FILE"
|
|
399
|
+
|
|
400
|
+
log_success "Worktree sandbox cleaned up"
|
|
401
|
+
else
|
|
402
|
+
log_info "Sandbox preserved at: $sandbox_path"
|
|
403
|
+
log_info "Run 'loki sandbox cleanup' to remove."
|
|
404
|
+
fi
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
# Worktree sandbox status
|
|
408
|
+
worktree_sandbox_status() {
|
|
409
|
+
if [[ ! -f "$WORKTREE_STATE_FILE" ]]; then
|
|
410
|
+
log_info "No active worktree sandbox"
|
|
411
|
+
return 0
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
local sandbox_path=$(jq -r '.sandbox_path' "$WORKTREE_STATE_FILE")
|
|
415
|
+
local sandbox_branch=$(jq -r '.sandbox_branch' "$WORKTREE_STATE_FILE")
|
|
416
|
+
local created_at=$(jq -r '.created_at' "$WORKTREE_STATE_FILE")
|
|
417
|
+
|
|
418
|
+
echo ""
|
|
419
|
+
echo -e "${BOLD}Worktree Sandbox Status${NC}"
|
|
420
|
+
echo "========================"
|
|
421
|
+
echo ""
|
|
422
|
+
echo -e " Path: ${CYAN}$sandbox_path${NC}"
|
|
423
|
+
echo -e " Branch: $sandbox_branch"
|
|
424
|
+
echo -e " Created: $created_at"
|
|
425
|
+
|
|
426
|
+
if [[ -d "$sandbox_path" ]]; then
|
|
427
|
+
local disk_usage=$(du -sh "$sandbox_path" 2>/dev/null | cut -f1)
|
|
428
|
+
echo -e " Disk: $disk_usage"
|
|
429
|
+
|
|
430
|
+
if [[ -f "$sandbox_path/.loki/STOP" ]]; then
|
|
431
|
+
echo -e " Status: ${YELLOW}Stopping${NC}"
|
|
432
|
+
else
|
|
433
|
+
echo -e " Status: ${GREEN}Active${NC}"
|
|
434
|
+
fi
|
|
435
|
+
else
|
|
436
|
+
echo -e " Status: ${RED}Missing${NC}"
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
echo ""
|
|
440
|
+
echo -e "${YELLOW}[SOFT ISOLATION]${NC} - No filesystem/network/process isolation"
|
|
441
|
+
echo ""
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
# Send prompt to worktree sandbox
|
|
445
|
+
worktree_sandbox_prompt() {
|
|
446
|
+
local prompt="$*"
|
|
447
|
+
|
|
448
|
+
# Security check: prompt injection disabled by default
|
|
449
|
+
if [[ "$PROMPT_INJECTION_ENABLED" != "true" ]]; then
|
|
450
|
+
log_error "Prompt injection is disabled for security"
|
|
451
|
+
log_info ""
|
|
452
|
+
log_info "To enable, set LOKI_PROMPT_INJECTION=true"
|
|
453
|
+
log_info " Example: LOKI_PROMPT_INJECTION=true loki sandbox prompt 'your message'"
|
|
454
|
+
log_info ""
|
|
455
|
+
log_warn "WARNING: Only enable in trusted environments"
|
|
456
|
+
return 1
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
if [[ -z "$prompt" ]]; then
|
|
460
|
+
log_error "Usage: loki sandbox prompt <your message>"
|
|
461
|
+
return 1
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
if [[ ! -f "$WORKTREE_STATE_FILE" ]]; then
|
|
465
|
+
log_error "No active worktree sandbox"
|
|
466
|
+
return 1
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
local sandbox_path=$(jq -r '.sandbox_path' "$WORKTREE_STATE_FILE")
|
|
470
|
+
|
|
471
|
+
if [[ ! -d "$sandbox_path" ]]; then
|
|
472
|
+
log_error "Sandbox path does not exist"
|
|
473
|
+
return 1
|
|
474
|
+
fi
|
|
475
|
+
|
|
476
|
+
# Use printf to safely write prompt without interpretation
|
|
477
|
+
printf '%s\n' "$prompt" > "$sandbox_path/.loki/HUMAN_INPUT.md"
|
|
478
|
+
log_success "Prompt sent to worktree sandbox"
|
|
479
|
+
log_info "Check $sandbox_path/.loki/logs/ for response"
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# Cleanup orphaned worktrees
|
|
483
|
+
cleanup_worktrees() {
|
|
484
|
+
log_info "Scanning for orphaned sandbox worktrees..."
|
|
485
|
+
|
|
486
|
+
local found=0
|
|
487
|
+
while IFS= read -r line; do
|
|
488
|
+
if [[ "$line" == *"$WORKTREE_PREFIX"* ]]; then
|
|
489
|
+
log_info " Found: $line"
|
|
490
|
+
((found++))
|
|
491
|
+
fi
|
|
492
|
+
done < <(git worktree list 2>/dev/null)
|
|
493
|
+
|
|
494
|
+
if [[ $found -eq 0 ]]; then
|
|
495
|
+
log_info "No sandbox worktrees found"
|
|
496
|
+
return 0
|
|
497
|
+
fi
|
|
498
|
+
|
|
499
|
+
log_info "Pruning worktrees..."
|
|
500
|
+
git worktree prune 2>/dev/null || true
|
|
501
|
+
|
|
502
|
+
# Clean up orphaned branches
|
|
503
|
+
local branches=$(git branch --list "${WORKTREE_PREFIX}*" 2>/dev/null)
|
|
504
|
+
while IFS= read -r branch; do
|
|
505
|
+
branch=$(echo "$branch" | tr -d '* ')
|
|
506
|
+
if [[ -n "$branch" ]]; then
|
|
507
|
+
log_info " Removing branch: $branch"
|
|
508
|
+
git branch -D "$branch" 2>/dev/null || true
|
|
509
|
+
fi
|
|
510
|
+
done <<< "$branches"
|
|
511
|
+
|
|
512
|
+
rm -f "$WORKTREE_STATE_FILE" 2>/dev/null
|
|
513
|
+
log_success "Cleanup complete"
|
|
514
|
+
}
|
|
515
|
+
|
|
110
516
|
#===============================================================================
|
|
111
517
|
# Container Management
|
|
112
518
|
#===============================================================================
|
|
@@ -115,9 +521,36 @@ start_sandbox() {
|
|
|
115
521
|
local prd_path="${1:-}"
|
|
116
522
|
local provider="${LOKI_PROVIDER:-claude}"
|
|
117
523
|
|
|
524
|
+
# Set up signal handler to cleanup on Ctrl+C
|
|
525
|
+
trap cleanup_container INT TERM
|
|
526
|
+
|
|
118
527
|
check_docker
|
|
528
|
+
validate_project_dir || return 1
|
|
119
529
|
ensure_image
|
|
120
530
|
|
|
531
|
+
# Check port availability
|
|
532
|
+
if ! check_port_available "$API_PORT"; then
|
|
533
|
+
log_error "Port $API_PORT is already in use"
|
|
534
|
+
log_info "Set LOKI_API_PORT to use a different port"
|
|
535
|
+
return 1
|
|
536
|
+
fi
|
|
537
|
+
if ! check_port_available "$DASHBOARD_PORT"; then
|
|
538
|
+
log_error "Port $DASHBOARD_PORT is already in use"
|
|
539
|
+
log_info "Set LOKI_DASHBOARD_PORT to use a different port"
|
|
540
|
+
return 1
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
# Warn about missing API keys
|
|
544
|
+
warn_missing_api_keys "$provider"
|
|
545
|
+
|
|
546
|
+
# Warn about network=none implications
|
|
547
|
+
if [[ "$SANDBOX_NETWORK" == "none" ]]; then
|
|
548
|
+
log_warn "Network disabled (--network=none)"
|
|
549
|
+
log_warn " - Git remote operations will fail"
|
|
550
|
+
log_warn " - Package installations will fail"
|
|
551
|
+
log_warn " - API calls to AI providers will fail"
|
|
552
|
+
fi
|
|
553
|
+
|
|
121
554
|
# Check if already running
|
|
122
555
|
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
123
556
|
log_warn "Sandbox already running: $CONTAINER_NAME"
|
|
@@ -177,9 +610,9 @@ start_sandbox() {
|
|
|
177
610
|
docker_args+=("--volume" "$PROJECT_DIR:/workspace:rw")
|
|
178
611
|
fi
|
|
179
612
|
|
|
180
|
-
# Mount git config (read-only)
|
|
613
|
+
# Mount git config (read-only) - mount to /home/loki since container runs as user loki
|
|
181
614
|
if [[ -f "$HOME/.gitconfig" ]]; then
|
|
182
|
-
docker_args+=("--volume" "$HOME/.gitconfig:/
|
|
615
|
+
docker_args+=("--volume" "$HOME/.gitconfig:/home/loki/.gitconfig:ro")
|
|
183
616
|
fi
|
|
184
617
|
|
|
185
618
|
# SSH agent forwarding (more secure than mounting .ssh directory)
|
|
@@ -195,9 +628,9 @@ start_sandbox() {
|
|
|
195
628
|
log_warn "SSH agent not available. Git operations may require manual auth."
|
|
196
629
|
fi
|
|
197
630
|
|
|
198
|
-
# Mount GitHub CLI config (read-only)
|
|
631
|
+
# Mount GitHub CLI config (read-only) - mount to /home/loki since container runs as user loki
|
|
199
632
|
if [[ -d "$HOME/.config/gh" ]]; then
|
|
200
|
-
docker_args+=("--volume" "$HOME/.config/gh:/
|
|
633
|
+
docker_args+=("--volume" "$HOME/.config/gh:/home/loki/.config/gh:ro")
|
|
201
634
|
fi
|
|
202
635
|
|
|
203
636
|
# Pass API keys as environment variables (more secure than mounting files)
|
|
@@ -231,6 +664,14 @@ start_sandbox() {
|
|
|
231
664
|
"--publish" "$DASHBOARD_PORT:57374"
|
|
232
665
|
)
|
|
233
666
|
|
|
667
|
+
# Expose additional ports for testing (e.g., LOKI_EXTRA_PORTS="3000:3000,8080:8080")
|
|
668
|
+
if [[ -n "${LOKI_EXTRA_PORTS:-}" ]]; then
|
|
669
|
+
IFS=',' read -ra EXTRA_PORTS <<< "$LOKI_EXTRA_PORTS"
|
|
670
|
+
for port_mapping in "${EXTRA_PORTS[@]}"; do
|
|
671
|
+
docker_args+=("--publish" "$port_mapping")
|
|
672
|
+
done
|
|
673
|
+
fi
|
|
674
|
+
|
|
234
675
|
# Working directory
|
|
235
676
|
docker_args+=("--workdir" "/workspace")
|
|
236
677
|
|
|
@@ -240,9 +681,12 @@ start_sandbox() {
|
|
|
240
681
|
# Build loki command
|
|
241
682
|
local loki_cmd="loki start"
|
|
242
683
|
if [[ -n "$prd_path" ]]; then
|
|
243
|
-
# Convert to container path
|
|
244
|
-
local
|
|
245
|
-
|
|
684
|
+
# Convert to container path (handle paths with spaces)
|
|
685
|
+
local relative_prd
|
|
686
|
+
relative_prd=$(realpath --relative-to="$PROJECT_DIR" "$prd_path" 2>/dev/null || basename "$prd_path")
|
|
687
|
+
local container_prd="/workspace/${relative_prd}"
|
|
688
|
+
# Quote path to handle spaces
|
|
689
|
+
loki_cmd="$loki_cmd \"$container_prd\""
|
|
246
690
|
fi
|
|
247
691
|
loki_cmd="$loki_cmd --provider $provider"
|
|
248
692
|
|
|
@@ -257,11 +701,20 @@ start_sandbox() {
|
|
|
257
701
|
log_info "Access:"
|
|
258
702
|
log_info " Dashboard: http://localhost:$DASHBOARD_PORT"
|
|
259
703
|
log_info " API: http://localhost:$API_PORT"
|
|
704
|
+
if [[ -n "${LOKI_EXTRA_PORTS:-}" ]]; then
|
|
705
|
+
log_info " Extra: $LOKI_EXTRA_PORTS"
|
|
706
|
+
fi
|
|
260
707
|
log_info ""
|
|
261
708
|
log_info "Commands:"
|
|
262
|
-
log_info " loki sandbox logs
|
|
263
|
-
log_info " loki sandbox shell
|
|
264
|
-
log_info " loki sandbox stop
|
|
709
|
+
log_info " loki sandbox logs - View logs"
|
|
710
|
+
log_info " loki sandbox shell - Open shell in container"
|
|
711
|
+
log_info " loki sandbox stop - Stop sandbox"
|
|
712
|
+
log_info ""
|
|
713
|
+
log_info "Testing (when ready):"
|
|
714
|
+
log_info " loki sandbox phase - Check SDLC phase & testing tips"
|
|
715
|
+
log_info " loki sandbox test - Run tests"
|
|
716
|
+
log_info " loki sandbox serve - Start dev server"
|
|
717
|
+
log_info " loki sandbox prompt 'msg' - Send real-time prompt"
|
|
265
718
|
}
|
|
266
719
|
|
|
267
720
|
stop_sandbox() {
|
|
@@ -273,11 +726,20 @@ stop_sandbox() {
|
|
|
273
726
|
# Try graceful stop first (touch STOP file)
|
|
274
727
|
docker exec "$CONTAINER_NAME" touch /workspace/.loki/STOP 2>/dev/null || true
|
|
275
728
|
|
|
276
|
-
# Wait
|
|
277
|
-
|
|
729
|
+
# Wait for graceful shutdown (check every second for up to 10 seconds)
|
|
730
|
+
local waited=0
|
|
731
|
+
while [ $waited -lt 10 ]; do
|
|
732
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
733
|
+
log_success "Sandbox stopped gracefully"
|
|
734
|
+
return 0
|
|
735
|
+
fi
|
|
736
|
+
sleep 1
|
|
737
|
+
((waited++))
|
|
738
|
+
done
|
|
278
739
|
|
|
279
740
|
# Force stop if still running
|
|
280
|
-
|
|
741
|
+
log_info "Force stopping container..."
|
|
742
|
+
docker stop --time 5 "$CONTAINER_NAME" 2>/dev/null || true
|
|
281
743
|
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
|
282
744
|
|
|
283
745
|
log_success "Sandbox stopped"
|
|
@@ -286,6 +748,300 @@ stop_sandbox() {
|
|
|
286
748
|
fi
|
|
287
749
|
}
|
|
288
750
|
|
|
751
|
+
# Send a prompt/directive to the running sandbox
|
|
752
|
+
sandbox_prompt() {
|
|
753
|
+
local prompt="$*"
|
|
754
|
+
|
|
755
|
+
# Security check: prompt injection disabled by default
|
|
756
|
+
if [[ "$PROMPT_INJECTION_ENABLED" != "true" ]]; then
|
|
757
|
+
log_error "Prompt injection is disabled for security"
|
|
758
|
+
log_info ""
|
|
759
|
+
log_info "To enable, set LOKI_PROMPT_INJECTION=true"
|
|
760
|
+
log_info " Example: LOKI_PROMPT_INJECTION=true loki sandbox prompt 'your message'"
|
|
761
|
+
log_info ""
|
|
762
|
+
log_warn "WARNING: Only enable in trusted environments"
|
|
763
|
+
return 1
|
|
764
|
+
fi
|
|
765
|
+
|
|
766
|
+
if [[ -z "$prompt" ]]; then
|
|
767
|
+
log_error "Usage: loki sandbox prompt <your message>"
|
|
768
|
+
log_info "Example: loki sandbox prompt 'start the dev server and show me the URL'"
|
|
769
|
+
return 1
|
|
770
|
+
fi
|
|
771
|
+
|
|
772
|
+
check_docker
|
|
773
|
+
|
|
774
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
775
|
+
log_error "Sandbox is not running. Start it first with: loki sandbox start"
|
|
776
|
+
return 1
|
|
777
|
+
fi
|
|
778
|
+
|
|
779
|
+
log_info "Sending prompt to sandbox..."
|
|
780
|
+
|
|
781
|
+
# Write to HUMAN_INPUT.md inside the container
|
|
782
|
+
# Use heredoc to avoid command injection via single quotes in prompt
|
|
783
|
+
docker exec "$CONTAINER_NAME" bash -c 'cat > /workspace/.loki/HUMAN_INPUT.md' <<< "$prompt"
|
|
784
|
+
|
|
785
|
+
log_success "Prompt sent. Loki will process it in the next iteration."
|
|
786
|
+
log_info ""
|
|
787
|
+
log_info "Watch the response with: loki sandbox logs"
|
|
788
|
+
log_info "Or check status with: loki sandbox status"
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
# Run a command inside the sandbox and show output
|
|
792
|
+
sandbox_run() {
|
|
793
|
+
local cmd="$*"
|
|
794
|
+
|
|
795
|
+
if [[ -z "$cmd" ]]; then
|
|
796
|
+
log_error "Usage: loki sandbox run <command>"
|
|
797
|
+
log_info "Example: loki sandbox run 'npm run dev'"
|
|
798
|
+
return 1
|
|
799
|
+
fi
|
|
800
|
+
|
|
801
|
+
check_docker
|
|
802
|
+
|
|
803
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
804
|
+
log_error "Sandbox is not running"
|
|
805
|
+
return 1
|
|
806
|
+
fi
|
|
807
|
+
|
|
808
|
+
log_info "Running command in sandbox: $cmd"
|
|
809
|
+
docker exec -it "$CONTAINER_NAME" bash -c "cd /workspace && $cmd"
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
# Start a dev server inside the sandbox
|
|
813
|
+
sandbox_serve() {
|
|
814
|
+
local port="${1:-3000}"
|
|
815
|
+
|
|
816
|
+
check_docker
|
|
817
|
+
|
|
818
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
819
|
+
log_error "Sandbox is not running"
|
|
820
|
+
return 1
|
|
821
|
+
fi
|
|
822
|
+
|
|
823
|
+
log_info "Looking for dev server configuration..."
|
|
824
|
+
|
|
825
|
+
# Detect project type and start appropriate server
|
|
826
|
+
local serve_cmd=""
|
|
827
|
+
|
|
828
|
+
if docker exec "$CONTAINER_NAME" test -f /workspace/package.json; then
|
|
829
|
+
# Check for common dev server scripts
|
|
830
|
+
local has_dev=$(docker exec "$CONTAINER_NAME" jq -r '.scripts.dev // empty' /workspace/package.json 2>/dev/null)
|
|
831
|
+
local has_start=$(docker exec "$CONTAINER_NAME" jq -r '.scripts.start // empty' /workspace/package.json 2>/dev/null)
|
|
832
|
+
|
|
833
|
+
if [[ -n "$has_dev" ]]; then
|
|
834
|
+
serve_cmd="npm run dev"
|
|
835
|
+
elif [[ -n "$has_start" ]]; then
|
|
836
|
+
serve_cmd="npm start"
|
|
837
|
+
fi
|
|
838
|
+
elif docker exec "$CONTAINER_NAME" test -f /workspace/requirements.txt; then
|
|
839
|
+
# Python project
|
|
840
|
+
if docker exec "$CONTAINER_NAME" test -f /workspace/manage.py; then
|
|
841
|
+
serve_cmd="python manage.py runserver 0.0.0.0:$port"
|
|
842
|
+
elif docker exec "$CONTAINER_NAME" test -f /workspace/app.py; then
|
|
843
|
+
serve_cmd="python app.py"
|
|
844
|
+
fi
|
|
845
|
+
fi
|
|
846
|
+
|
|
847
|
+
if [[ -z "$serve_cmd" ]]; then
|
|
848
|
+
log_warn "Could not auto-detect dev server"
|
|
849
|
+
log_info "Try: loki sandbox run 'your-dev-command'"
|
|
850
|
+
return 1
|
|
851
|
+
fi
|
|
852
|
+
|
|
853
|
+
log_success "Starting dev server: $serve_cmd"
|
|
854
|
+
log_info ""
|
|
855
|
+
log_info "Access the app at:"
|
|
856
|
+
log_info " http://localhost:$port"
|
|
857
|
+
log_info ""
|
|
858
|
+
log_info "Press Ctrl+C to stop the server (sandbox continues running)"
|
|
859
|
+
|
|
860
|
+
docker exec -it "$CONTAINER_NAME" bash -c "cd /workspace && $serve_cmd"
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
# Run tests inside the sandbox
|
|
864
|
+
sandbox_test() {
|
|
865
|
+
local test_type="${1:-all}"
|
|
866
|
+
|
|
867
|
+
check_docker
|
|
868
|
+
|
|
869
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
870
|
+
log_error "Sandbox is not running"
|
|
871
|
+
return 1
|
|
872
|
+
fi
|
|
873
|
+
|
|
874
|
+
log_info "Running tests in sandbox..."
|
|
875
|
+
|
|
876
|
+
# Detect project type and test command
|
|
877
|
+
local test_cmd=""
|
|
878
|
+
|
|
879
|
+
if docker exec "$CONTAINER_NAME" test -f /workspace/package.json; then
|
|
880
|
+
case "$test_type" in
|
|
881
|
+
unit)
|
|
882
|
+
test_cmd=$(docker exec "$CONTAINER_NAME" jq -r '.scripts["test:unit"] // .scripts.test // "npm test"' /workspace/package.json 2>/dev/null)
|
|
883
|
+
;;
|
|
884
|
+
integration)
|
|
885
|
+
test_cmd=$(docker exec "$CONTAINER_NAME" jq -r '.scripts["test:integration"] // empty' /workspace/package.json 2>/dev/null)
|
|
886
|
+
[[ -z "$test_cmd" ]] && test_cmd="npm run test:integration"
|
|
887
|
+
;;
|
|
888
|
+
e2e)
|
|
889
|
+
if docker exec "$CONTAINER_NAME" test -d /workspace/node_modules/.bin/playwright; then
|
|
890
|
+
test_cmd="npx playwright test"
|
|
891
|
+
elif docker exec "$CONTAINER_NAME" test -d /workspace/node_modules/.bin/cypress; then
|
|
892
|
+
test_cmd="npx cypress run"
|
|
893
|
+
else
|
|
894
|
+
test_cmd=$(docker exec "$CONTAINER_NAME" jq -r '.scripts["test:e2e"] // empty' /workspace/package.json 2>/dev/null)
|
|
895
|
+
fi
|
|
896
|
+
;;
|
|
897
|
+
all|*)
|
|
898
|
+
test_cmd=$(docker exec "$CONTAINER_NAME" jq -r '.scripts.test // "npm test"' /workspace/package.json 2>/dev/null)
|
|
899
|
+
;;
|
|
900
|
+
esac
|
|
901
|
+
elif docker exec "$CONTAINER_NAME" test -f /workspace/requirements.txt; then
|
|
902
|
+
case "$test_type" in
|
|
903
|
+
unit)
|
|
904
|
+
test_cmd="pytest tests/unit/ -v"
|
|
905
|
+
;;
|
|
906
|
+
integration)
|
|
907
|
+
test_cmd="pytest tests/integration/ -v"
|
|
908
|
+
;;
|
|
909
|
+
e2e)
|
|
910
|
+
test_cmd="pytest tests/e2e/ -v"
|
|
911
|
+
;;
|
|
912
|
+
all|*)
|
|
913
|
+
test_cmd="pytest -v"
|
|
914
|
+
;;
|
|
915
|
+
esac
|
|
916
|
+
elif docker exec "$CONTAINER_NAME" test -f /workspace/Cargo.toml; then
|
|
917
|
+
test_cmd="cargo test"
|
|
918
|
+
elif docker exec "$CONTAINER_NAME" test -f /workspace/go.mod; then
|
|
919
|
+
test_cmd="go test ./..."
|
|
920
|
+
fi
|
|
921
|
+
|
|
922
|
+
if [[ -z "$test_cmd" ]] || [[ "$test_cmd" == "null" ]]; then
|
|
923
|
+
log_warn "Could not auto-detect test command"
|
|
924
|
+
log_info "Supported test types: unit, integration, e2e, all"
|
|
925
|
+
log_info "Or run: loki sandbox run 'your-test-command'"
|
|
926
|
+
return 1
|
|
927
|
+
fi
|
|
928
|
+
|
|
929
|
+
log_success "Running: $test_cmd"
|
|
930
|
+
docker exec -it "$CONTAINER_NAME" bash -c "cd /workspace && $test_cmd"
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
# Check SDLC phase and suggest testing
|
|
934
|
+
sandbox_phase() {
|
|
935
|
+
check_docker
|
|
936
|
+
|
|
937
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
938
|
+
log_error "Sandbox is not running"
|
|
939
|
+
return 1
|
|
940
|
+
fi
|
|
941
|
+
|
|
942
|
+
# Get current phase from orchestrator state
|
|
943
|
+
local phase=$(docker exec "$CONTAINER_NAME" bash -c \
|
|
944
|
+
"python3 -c \"import json; print(json.load(open('/workspace/.loki/state/orchestrator.json')).get('currentPhase', 'UNKNOWN'))\" 2>/dev/null" \
|
|
945
|
+
|| echo "UNKNOWN")
|
|
946
|
+
|
|
947
|
+
echo ""
|
|
948
|
+
echo -e "${BOLD}Current SDLC Phase: ${CYAN}$phase${NC}"
|
|
949
|
+
echo ""
|
|
950
|
+
|
|
951
|
+
case "$phase" in
|
|
952
|
+
BOOTSTRAP|DISCOVERY|ARCHITECTURE)
|
|
953
|
+
log_info "Phase $phase - No testing required yet"
|
|
954
|
+
log_info "Testing begins in DEVELOPMENT phase"
|
|
955
|
+
;;
|
|
956
|
+
INFRASTRUCTURE)
|
|
957
|
+
log_info "Phase $phase - Infrastructure testing"
|
|
958
|
+
echo " Recommended:"
|
|
959
|
+
echo " loki sandbox run 'docker-compose up -d'"
|
|
960
|
+
echo " loki sandbox run 'terraform plan'"
|
|
961
|
+
;;
|
|
962
|
+
DEVELOPMENT)
|
|
963
|
+
log_info "Phase $phase - Development testing recommended"
|
|
964
|
+
echo ""
|
|
965
|
+
echo " Run dev server:"
|
|
966
|
+
echo " loki sandbox serve"
|
|
967
|
+
echo ""
|
|
968
|
+
echo " Run unit tests:"
|
|
969
|
+
echo " loki sandbox test unit"
|
|
970
|
+
echo ""
|
|
971
|
+
echo " Manual testing:"
|
|
972
|
+
echo " loki sandbox shell"
|
|
973
|
+
;;
|
|
974
|
+
QA)
|
|
975
|
+
log_info "Phase $phase - Full testing required"
|
|
976
|
+
echo ""
|
|
977
|
+
echo " Run all tests:"
|
|
978
|
+
echo " loki sandbox test all"
|
|
979
|
+
echo ""
|
|
980
|
+
echo " Run specific test suites:"
|
|
981
|
+
echo " loki sandbox test unit"
|
|
982
|
+
echo " loki sandbox test integration"
|
|
983
|
+
echo " loki sandbox test e2e"
|
|
984
|
+
echo ""
|
|
985
|
+
echo " Interactive testing:"
|
|
986
|
+
echo " loki sandbox serve"
|
|
987
|
+
echo " loki sandbox shell"
|
|
988
|
+
echo ""
|
|
989
|
+
echo " To report issues during testing:"
|
|
990
|
+
echo " loki sandbox prompt 'Found bug: describe the bug here'"
|
|
991
|
+
;;
|
|
992
|
+
DEPLOYMENT)
|
|
993
|
+
log_info "Phase $phase - Smoke testing"
|
|
994
|
+
echo ""
|
|
995
|
+
echo " Run smoke tests:"
|
|
996
|
+
echo " loki sandbox run 'npm run test:smoke'"
|
|
997
|
+
echo ""
|
|
998
|
+
echo " Check deployment status:"
|
|
999
|
+
echo " loki sandbox run 'curl -s http://localhost:3000/health'"
|
|
1000
|
+
;;
|
|
1001
|
+
GROWTH|*)
|
|
1002
|
+
log_info "Phase $phase - Continuous testing"
|
|
1003
|
+
echo ""
|
|
1004
|
+
echo " Run regression tests:"
|
|
1005
|
+
echo " loki sandbox test all"
|
|
1006
|
+
echo ""
|
|
1007
|
+
echo " Performance testing:"
|
|
1008
|
+
echo " loki sandbox run 'npx k6 run tests/load.js'"
|
|
1009
|
+
;;
|
|
1010
|
+
esac
|
|
1011
|
+
|
|
1012
|
+
echo ""
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
# Expose additional ports (for development testing)
|
|
1016
|
+
sandbox_expose() {
|
|
1017
|
+
local port="${1:-}"
|
|
1018
|
+
|
|
1019
|
+
if [[ -z "$port" ]]; then
|
|
1020
|
+
log_error "Usage: loki sandbox expose <port>"
|
|
1021
|
+
log_info "Example: loki sandbox expose 8080"
|
|
1022
|
+
return 1
|
|
1023
|
+
fi
|
|
1024
|
+
|
|
1025
|
+
check_docker
|
|
1026
|
+
|
|
1027
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
1028
|
+
log_error "Sandbox is not running"
|
|
1029
|
+
log_info "Note: Ports must be exposed when starting the sandbox"
|
|
1030
|
+
log_info "Set LOKI_EXTRA_PORTS='$port:$port' before running 'loki sandbox start'"
|
|
1031
|
+
return 1
|
|
1032
|
+
fi
|
|
1033
|
+
|
|
1034
|
+
log_warn "Cannot expose ports on running container"
|
|
1035
|
+
log_info ""
|
|
1036
|
+
log_info "To expose additional ports, restart sandbox with:"
|
|
1037
|
+
echo ""
|
|
1038
|
+
echo " LOKI_EXTRA_PORTS='$port:$port' loki sandbox start"
|
|
1039
|
+
echo ""
|
|
1040
|
+
log_info "Or access via sandbox shell:"
|
|
1041
|
+
echo " loki sandbox shell"
|
|
1042
|
+
echo " # Then run your server inside the container"
|
|
1043
|
+
}
|
|
1044
|
+
|
|
289
1045
|
sandbox_status() {
|
|
290
1046
|
check_docker
|
|
291
1047
|
|
|
@@ -338,17 +1094,39 @@ sandbox_build() {
|
|
|
338
1094
|
}
|
|
339
1095
|
|
|
340
1096
|
show_help() {
|
|
341
|
-
echo -e "${BOLD}Loki Mode
|
|
1097
|
+
echo -e "${BOLD}Loki Mode Sandbox${NC}"
|
|
342
1098
|
echo ""
|
|
343
1099
|
echo "Usage: loki sandbox <command> [options]"
|
|
344
1100
|
echo ""
|
|
345
1101
|
echo "Commands:"
|
|
346
|
-
echo " start [PRD]
|
|
347
|
-
echo " stop
|
|
348
|
-
echo " status
|
|
349
|
-
echo " logs [N]
|
|
350
|
-
echo " shell
|
|
351
|
-
echo " build
|
|
1102
|
+
echo " start [PRD] Start sandbox with optional PRD"
|
|
1103
|
+
echo " stop Stop running sandbox"
|
|
1104
|
+
echo " status Check sandbox status"
|
|
1105
|
+
echo " logs [N] View last N log lines (default: 100)"
|
|
1106
|
+
echo " shell Open bash shell in sandbox"
|
|
1107
|
+
echo " build Build/rebuild sandbox image"
|
|
1108
|
+
echo " cleanup Remove orphaned sandbox worktrees/branches"
|
|
1109
|
+
echo ""
|
|
1110
|
+
echo "Interactive Commands (while sandbox is running):"
|
|
1111
|
+
echo " prompt <msg> Send a prompt/directive to Loki in real-time"
|
|
1112
|
+
echo " run <cmd> Run a command inside the sandbox"
|
|
1113
|
+
echo " serve [port] Auto-detect and start dev server (default port: 3000)"
|
|
1114
|
+
echo ""
|
|
1115
|
+
echo "Testing Commands:"
|
|
1116
|
+
echo " test [type] Run tests (type: unit, integration, e2e, all)"
|
|
1117
|
+
echo " phase Check SDLC phase and get testing suggestions"
|
|
1118
|
+
echo " expose <port> Show how to expose additional ports"
|
|
1119
|
+
echo ""
|
|
1120
|
+
echo "Mode Options:"
|
|
1121
|
+
echo " --docker Force Docker sandbox (full isolation)"
|
|
1122
|
+
echo " --worktree Force git worktree sandbox (soft isolation)"
|
|
1123
|
+
echo " --auto Auto-detect best mode (default)"
|
|
1124
|
+
echo ""
|
|
1125
|
+
echo "Sandbox Modes:"
|
|
1126
|
+
echo " Docker (default) - Full isolation with seccomp, dropped capabilities,"
|
|
1127
|
+
echo " resource limits, network control"
|
|
1128
|
+
echo " Worktree - Git worktree isolation (fallback if Docker unavailable)"
|
|
1129
|
+
echo " Warning: No filesystem/network/process isolation"
|
|
352
1130
|
echo ""
|
|
353
1131
|
echo "Environment Variables:"
|
|
354
1132
|
echo " LOKI_SANDBOX_IMAGE Docker image (default: loki-mode:sandbox)"
|
|
@@ -356,19 +1134,29 @@ show_help() {
|
|
|
356
1134
|
echo " LOKI_SANDBOX_CPUS CPU limit (default: 2)"
|
|
357
1135
|
echo " LOKI_SANDBOX_MEMORY Memory limit (default: 4g)"
|
|
358
1136
|
echo " LOKI_SANDBOX_READONLY Mount project read-only (default: false)"
|
|
1137
|
+
echo " LOKI_SANDBOX_CLEANUP Auto-cleanup worktree on stop (default: true)"
|
|
1138
|
+
echo " LOKI_EXTRA_PORTS Expose extra ports (e.g., '3000:3000,8080:8080')"
|
|
1139
|
+
echo " LOKI_PROMPT_INJECTION Enable real-time prompts (default: false)"
|
|
359
1140
|
echo ""
|
|
360
|
-
echo "Security Features:"
|
|
1141
|
+
echo "Security Features (Docker mode):"
|
|
361
1142
|
echo " - Seccomp profile restricts syscalls"
|
|
362
1143
|
echo " - No new privileges flag"
|
|
363
1144
|
echo " - Dropped capabilities"
|
|
364
1145
|
echo " - Resource limits (CPU, memory, PIDs)"
|
|
365
1146
|
echo " - API keys passed as env vars (not mounted)"
|
|
1147
|
+
echo " - Prompt injection DISABLED by default (enterprise security)"
|
|
366
1148
|
echo ""
|
|
367
1149
|
echo "Examples:"
|
|
368
|
-
echo " loki sandbox start
|
|
369
|
-
echo " loki sandbox start ./prd.md #
|
|
370
|
-
echo "
|
|
371
|
-
echo "
|
|
1150
|
+
echo " loki sandbox start # Start (auto-detect mode)"
|
|
1151
|
+
echo " loki sandbox start --docker ./prd.md # Force Docker mode"
|
|
1152
|
+
echo " loki sandbox start --worktree # Force worktree mode"
|
|
1153
|
+
echo " loki sandbox prompt 'start the app and show URL' # Send prompt"
|
|
1154
|
+
echo " loki sandbox serve # Start dev server"
|
|
1155
|
+
echo " loki sandbox test # Run all tests"
|
|
1156
|
+
echo " loki sandbox test unit # Run unit tests only"
|
|
1157
|
+
echo " loki sandbox phase # Check phase, get testing tips"
|
|
1158
|
+
echo " loki sandbox run 'npm test' # Run custom command"
|
|
1159
|
+
echo " loki sandbox cleanup # Remove old worktrees"
|
|
372
1160
|
}
|
|
373
1161
|
|
|
374
1162
|
#===============================================================================
|
|
@@ -379,18 +1167,50 @@ main() {
|
|
|
379
1167
|
local command="${1:-help}"
|
|
380
1168
|
shift || true
|
|
381
1169
|
|
|
1170
|
+
# Parse mode option
|
|
1171
|
+
local mode="auto"
|
|
1172
|
+
local args=()
|
|
1173
|
+
while [[ $# -gt 0 ]]; do
|
|
1174
|
+
case "$1" in
|
|
1175
|
+
--docker) mode="docker"; shift ;;
|
|
1176
|
+
--worktree) mode="worktree"; shift ;;
|
|
1177
|
+
--auto) mode="auto"; shift ;;
|
|
1178
|
+
*) args+=("$1"); shift ;;
|
|
1179
|
+
esac
|
|
1180
|
+
done
|
|
1181
|
+
|
|
1182
|
+
# Detect sandbox mode for commands that need it
|
|
1183
|
+
local sandbox_mode=""
|
|
1184
|
+
case "$command" in
|
|
1185
|
+
start|stop|status|prompt)
|
|
1186
|
+
sandbox_mode=$(detect_sandbox_mode "$mode") || exit 1
|
|
1187
|
+
;;
|
|
1188
|
+
esac
|
|
1189
|
+
|
|
382
1190
|
case "$command" in
|
|
383
1191
|
start)
|
|
384
|
-
|
|
1192
|
+
if [[ "$sandbox_mode" == "docker" ]]; then
|
|
1193
|
+
start_sandbox "${args[@]}"
|
|
1194
|
+
else
|
|
1195
|
+
start_worktree_sandbox "${args[@]}"
|
|
1196
|
+
fi
|
|
385
1197
|
;;
|
|
386
1198
|
stop)
|
|
387
|
-
|
|
1199
|
+
if [[ "$sandbox_mode" == "docker" ]]; then
|
|
1200
|
+
stop_sandbox
|
|
1201
|
+
else
|
|
1202
|
+
stop_worktree_sandbox
|
|
1203
|
+
fi
|
|
388
1204
|
;;
|
|
389
1205
|
status)
|
|
390
|
-
|
|
1206
|
+
if [[ "$sandbox_mode" == "docker" ]]; then
|
|
1207
|
+
sandbox_status
|
|
1208
|
+
else
|
|
1209
|
+
worktree_sandbox_status
|
|
1210
|
+
fi
|
|
391
1211
|
;;
|
|
392
1212
|
logs)
|
|
393
|
-
sandbox_logs "
|
|
1213
|
+
sandbox_logs "${args[@]}"
|
|
394
1214
|
;;
|
|
395
1215
|
shell)
|
|
396
1216
|
sandbox_shell
|
|
@@ -398,6 +1218,31 @@ main() {
|
|
|
398
1218
|
build)
|
|
399
1219
|
sandbox_build
|
|
400
1220
|
;;
|
|
1221
|
+
prompt)
|
|
1222
|
+
if [[ "$sandbox_mode" == "docker" ]]; then
|
|
1223
|
+
sandbox_prompt "${args[@]}"
|
|
1224
|
+
else
|
|
1225
|
+
worktree_sandbox_prompt "${args[@]}"
|
|
1226
|
+
fi
|
|
1227
|
+
;;
|
|
1228
|
+
run)
|
|
1229
|
+
sandbox_run "${args[@]}"
|
|
1230
|
+
;;
|
|
1231
|
+
cleanup)
|
|
1232
|
+
cleanup_worktrees
|
|
1233
|
+
;;
|
|
1234
|
+
serve)
|
|
1235
|
+
sandbox_serve "${args[@]}"
|
|
1236
|
+
;;
|
|
1237
|
+
test)
|
|
1238
|
+
sandbox_test "${args[@]}"
|
|
1239
|
+
;;
|
|
1240
|
+
phase)
|
|
1241
|
+
sandbox_phase
|
|
1242
|
+
;;
|
|
1243
|
+
expose)
|
|
1244
|
+
sandbox_expose "${args[@]}"
|
|
1245
|
+
;;
|
|
401
1246
|
help|--help|-h)
|
|
402
1247
|
show_help
|
|
403
1248
|
;;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "5.6.
|
|
3
|
+
"version": "5.6.1",
|
|
4
4
|
"description": "Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"startup",
|
|
12
12
|
"multi-agent"
|
|
13
13
|
],
|
|
14
|
-
"homepage": "https://github.
|
|
14
|
+
"homepage": "https://asklokesh.github.io/loki-mode",
|
|
15
15
|
"bugs": {
|
|
16
16
|
"url": "https://github.com/asklokesh/loki-mode/issues"
|
|
17
17
|
},
|