loki-mode 7.78.0 → 7.79.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/lib/checkpoint_sync.py +189 -0
- package/autonomy/lib/config-map.sh +955 -0
- package/autonomy/loki +143 -8
- package/autonomy/run.sh +227 -195
- package/autonomy/sandbox.sh +98 -0
- package/autonomy/trigger-server.py +419 -43
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +63 -7
- package/docs/CONFIG-FILE-PLAN.md +459 -0
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/autonomy/run.sh
CHANGED
|
@@ -40,12 +40,18 @@
|
|
|
40
40
|
# LOKI_AUDIT_DISABLED - Disable audit logging (default: false)
|
|
41
41
|
# LOKI_MAX_PARALLEL_AGENTS - Limit concurrent agent spawning (default: 10)
|
|
42
42
|
# LOKI_SANDBOX_MODE - Run in sandboxed container (default: false, requires Docker)
|
|
43
|
-
# LOKI_ALLOWED_PATHS -
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
43
|
+
# LOKI_ALLOWED_PATHS - Comma-separated path allowlist. PARTIAL enforcement
|
|
44
|
+
# (honest scope): when set, run.sh rejects a sandbox
|
|
45
|
+
# custom --mount whose host path is outside the
|
|
46
|
+
# allowlist (a write surface run.sh itself controls).
|
|
47
|
+
# It does NOT and CANNOT restrict provider-driven
|
|
48
|
+
# agent file writes: claude/codex/cline/aider write
|
|
49
|
+
# files directly, not through run.sh, so run.sh never
|
|
50
|
+
# sees those writes. The containment for agent writes
|
|
51
|
+
# is the OS user + the LOKI_SANDBOX_MODE container
|
|
52
|
+
# (cap-drop, seccomp, read-only mounts), not this var.
|
|
53
|
+
# Treat ALLOWED_PATHS as defense-in-depth on the
|
|
54
|
+
# mount surface, not a complete write sandbox.
|
|
49
55
|
# LOKI_BLOCKED_COMMANDS - Comma-separated blocked shell commands (default: rm -rf /)
|
|
50
56
|
#
|
|
51
57
|
# OIDC / SSO Authentication (optional, works alongside token auth):
|
|
@@ -230,6 +236,17 @@ fi
|
|
|
230
236
|
# Configuration File Support (v4.1.0)
|
|
231
237
|
# Loads settings from config file, environment variables take precedence
|
|
232
238
|
#===============================================================================
|
|
239
|
+
# #691: source the canonical config mapping + shared per-key export helper. This
|
|
240
|
+
# is the SAME lib the loki CLI pre-pass uses, so the YAML/config-file mapping
|
|
241
|
+
# cannot drift between the CLI and the runner. Side-effect-free on source. The
|
|
242
|
+
# parsers below delegate to loki_config_export_key with override=0, preserving
|
|
243
|
+
# the shipped env-wins auto-discovery contract byte-for-byte.
|
|
244
|
+
_LOKI_CONFIG_MAP_LIB="$SCRIPT_DIR/lib/config-map.sh"
|
|
245
|
+
if [ -f "$_LOKI_CONFIG_MAP_LIB" ]; then
|
|
246
|
+
# shellcheck source=lib/config-map.sh
|
|
247
|
+
source "$_LOKI_CONFIG_MAP_LIB"
|
|
248
|
+
fi
|
|
249
|
+
|
|
233
250
|
load_config_file() {
|
|
234
251
|
local config_file=""
|
|
235
252
|
|
|
@@ -263,95 +280,22 @@ load_config_file() {
|
|
|
263
280
|
parse_yaml_with_yq "$config_file"
|
|
264
281
|
}
|
|
265
282
|
|
|
266
|
-
# Fallback YAML parser for simple key: value format
|
|
283
|
+
# Fallback YAML parser for simple key: value format.
|
|
284
|
+
# #691: iterates the canonical LOKI_CONFIG_MAP (single source of truth) instead
|
|
285
|
+
# of 64 hand-maintained set_from_yaml calls. override=0 -> ambient env still
|
|
286
|
+
# wins (auto-discovery contract unchanged). Falls back to nothing if the lib is
|
|
287
|
+
# absent (defensive; the lib ships alongside run.sh).
|
|
267
288
|
parse_simple_yaml() {
|
|
268
289
|
local file="$1"
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
set_from_yaml "$file" "dashboard.port" "LOKI_DASHBOARD_PORT"
|
|
279
|
-
|
|
280
|
-
# Resources
|
|
281
|
-
set_from_yaml "$file" "resources.check_interval" "LOKI_RESOURCE_CHECK_INTERVAL"
|
|
282
|
-
set_from_yaml "$file" "resources.cpu_threshold" "LOKI_RESOURCE_CPU_THRESHOLD"
|
|
283
|
-
set_from_yaml "$file" "resources.mem_threshold" "LOKI_RESOURCE_MEM_THRESHOLD"
|
|
284
|
-
|
|
285
|
-
# Security
|
|
286
|
-
set_from_yaml "$file" "security.staged_autonomy" "LOKI_STAGED_AUTONOMY"
|
|
287
|
-
set_from_yaml "$file" "security.audit_log" "LOKI_AUDIT_LOG"
|
|
288
|
-
set_from_yaml "$file" "security.max_parallel_agents" "LOKI_MAX_PARALLEL_AGENTS"
|
|
289
|
-
set_from_yaml "$file" "security.sandbox_mode" "LOKI_SANDBOX_MODE"
|
|
290
|
-
set_from_yaml "$file" "security.allowed_paths" "LOKI_ALLOWED_PATHS"
|
|
291
|
-
set_from_yaml "$file" "security.blocked_commands" "LOKI_BLOCKED_COMMANDS"
|
|
292
|
-
|
|
293
|
-
# Phases
|
|
294
|
-
set_from_yaml "$file" "phases.unit_tests" "LOKI_PHASE_UNIT_TESTS"
|
|
295
|
-
set_from_yaml "$file" "phases.api_tests" "LOKI_PHASE_API_TESTS"
|
|
296
|
-
set_from_yaml "$file" "phases.e2e_tests" "LOKI_PHASE_E2E_TESTS"
|
|
297
|
-
set_from_yaml "$file" "phases.security" "LOKI_PHASE_SECURITY"
|
|
298
|
-
set_from_yaml "$file" "phases.integration" "LOKI_PHASE_INTEGRATION"
|
|
299
|
-
set_from_yaml "$file" "phases.code_review" "LOKI_PHASE_CODE_REVIEW"
|
|
300
|
-
set_from_yaml "$file" "phases.web_research" "LOKI_PHASE_WEB_RESEARCH"
|
|
301
|
-
set_from_yaml "$file" "phases.performance" "LOKI_PHASE_PERFORMANCE"
|
|
302
|
-
set_from_yaml "$file" "phases.accessibility" "LOKI_PHASE_ACCESSIBILITY"
|
|
303
|
-
set_from_yaml "$file" "phases.regression" "LOKI_PHASE_REGRESSION"
|
|
304
|
-
set_from_yaml "$file" "phases.uat" "LOKI_PHASE_UAT"
|
|
305
|
-
|
|
306
|
-
# Completion
|
|
307
|
-
set_from_yaml "$file" "completion.promise" "LOKI_COMPLETION_PROMISE"
|
|
308
|
-
set_from_yaml "$file" "completion.max_iterations" "LOKI_MAX_ITERATIONS"
|
|
309
|
-
set_from_yaml "$file" "completion.perpetual_mode" "LOKI_PERPETUAL_MODE"
|
|
310
|
-
set_from_yaml "$file" "completion.council.enabled" "LOKI_COUNCIL_ENABLED"
|
|
311
|
-
set_from_yaml "$file" "completion.council.size" "LOKI_COUNCIL_SIZE"
|
|
312
|
-
set_from_yaml "$file" "completion.council.threshold" "LOKI_COUNCIL_THRESHOLD"
|
|
313
|
-
set_from_yaml "$file" "completion.council.check_interval" "LOKI_COUNCIL_CHECK_INTERVAL"
|
|
314
|
-
set_from_yaml "$file" "completion.council.min_iterations" "LOKI_COUNCIL_MIN_ITERATIONS"
|
|
315
|
-
set_from_yaml "$file" "completion.council.stagnation_limit" "LOKI_COUNCIL_STAGNATION_LIMIT"
|
|
316
|
-
set_from_yaml "$file" "completion.uncertainty.escalation" "LOKI_UNCERTAINTY_ESCALATION"
|
|
317
|
-
set_from_yaml "$file" "completion.uncertainty.rounds" "LOKI_UNCERTAINTY_ROUNDS"
|
|
318
|
-
set_from_yaml "$file" "completion.uncertainty.nochange_min" "LOKI_UNCERTAINTY_NOCHANGE_MIN"
|
|
319
|
-
set_from_yaml "$file" "completion.uncertainty.split_rounds" "LOKI_UNCERTAINTY_SPLIT_ROUNDS"
|
|
320
|
-
|
|
321
|
-
# Model
|
|
322
|
-
set_from_yaml "$file" "model.prompt_repetition" "LOKI_PROMPT_REPETITION"
|
|
323
|
-
set_from_yaml "$file" "model.confidence_routing" "LOKI_CONFIDENCE_ROUTING"
|
|
324
|
-
set_from_yaml "$file" "model.autonomy_mode" "LOKI_AUTONOMY_MODE"
|
|
325
|
-
set_from_yaml "$file" "model.planning" "LOKI_MODEL_PLANNING"
|
|
326
|
-
set_from_yaml "$file" "model.development" "LOKI_MODEL_DEVELOPMENT"
|
|
327
|
-
set_from_yaml "$file" "model.fast" "LOKI_MODEL_FAST"
|
|
328
|
-
|
|
329
|
-
# Parallel
|
|
330
|
-
set_from_yaml "$file" "parallel.enabled" "LOKI_PARALLEL_MODE"
|
|
331
|
-
set_from_yaml "$file" "parallel.max_worktrees" "LOKI_MAX_WORKTREES"
|
|
332
|
-
set_from_yaml "$file" "parallel.max_sessions" "LOKI_MAX_PARALLEL_SESSIONS"
|
|
333
|
-
set_from_yaml "$file" "parallel.testing" "LOKI_PARALLEL_TESTING"
|
|
334
|
-
set_from_yaml "$file" "parallel.docs" "LOKI_PARALLEL_DOCS"
|
|
335
|
-
set_from_yaml "$file" "parallel.blog" "LOKI_PARALLEL_BLOG"
|
|
336
|
-
set_from_yaml "$file" "parallel.auto_merge" "LOKI_AUTO_MERGE"
|
|
337
|
-
|
|
338
|
-
# Complexity
|
|
339
|
-
set_from_yaml "$file" "complexity.tier" "LOKI_COMPLEXITY"
|
|
340
|
-
|
|
341
|
-
# GitHub
|
|
342
|
-
set_from_yaml "$file" "github.import" "LOKI_GITHUB_IMPORT"
|
|
343
|
-
set_from_yaml "$file" "github.pr" "LOKI_GITHUB_PR"
|
|
344
|
-
set_from_yaml "$file" "github.sync" "LOKI_GITHUB_SYNC"
|
|
345
|
-
set_from_yaml "$file" "github.repo" "LOKI_GITHUB_REPO"
|
|
346
|
-
set_from_yaml "$file" "github.labels" "LOKI_GITHUB_LABELS"
|
|
347
|
-
set_from_yaml "$file" "github.milestone" "LOKI_GITHUB_MILESTONE"
|
|
348
|
-
set_from_yaml "$file" "github.assignee" "LOKI_GITHUB_ASSIGNEE"
|
|
349
|
-
set_from_yaml "$file" "github.limit" "LOKI_GITHUB_LIMIT"
|
|
350
|
-
set_from_yaml "$file" "github.pr_label" "LOKI_GITHUB_PR_LABEL"
|
|
351
|
-
|
|
352
|
-
# Notifications
|
|
353
|
-
set_from_yaml "$file" "notifications.enabled" "LOKI_NOTIFICATIONS"
|
|
354
|
-
set_from_yaml "$file" "notifications.sound" "LOKI_NOTIFICATION_SOUND"
|
|
290
|
+
if ! declare -p LOKI_CONFIG_MAP >/dev/null 2>&1; then
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
local mapping yaml_path env_var
|
|
294
|
+
for mapping in "${LOKI_CONFIG_MAP[@]}"; do
|
|
295
|
+
yaml_path="${mapping%%:*}"
|
|
296
|
+
env_var="${mapping##*:}"
|
|
297
|
+
set_from_yaml "$file" "$yaml_path" "$env_var"
|
|
298
|
+
done
|
|
355
299
|
}
|
|
356
300
|
|
|
357
301
|
# Validate YAML value to prevent injection attacks
|
|
@@ -364,11 +308,19 @@ validate_yaml_value() {
|
|
|
364
308
|
return 1
|
|
365
309
|
fi
|
|
366
310
|
|
|
367
|
-
# Reject values with dangerous shell metacharacters
|
|
368
|
-
#
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
311
|
+
# Reject values with dangerous shell metacharacters.
|
|
312
|
+
# SECURITY FIX (#691 wave): the previous [[ "$value" =~ [\$\`...] ]]
|
|
313
|
+
# bracket-regex matched NOTHING -- backslash escapes inside a bash regex
|
|
314
|
+
# bracket class are taken literally, so the guard ACCEPTED $(...), backticks,
|
|
315
|
+
# pipes, etc. (verified behaviorally: it returned 0 for "$(touch /tmp/x)").
|
|
316
|
+
# A `case` glob class has no such ambiguity and actually rejects the metachars.
|
|
317
|
+
# Allow alphanumeric, spaces, dots, dashes, underscores, slashes, colons,
|
|
318
|
+
# commas, @.
|
|
319
|
+
case "$value" in
|
|
320
|
+
*'$'* | *'`'* | *'|'* | *';'* | *'&'* | *'>'* | *'<'* | *'('* | *')'* \
|
|
321
|
+
| *'{'* | *'}'* | *'['* | *']'* | *'\'* )
|
|
322
|
+
return 1 ;;
|
|
323
|
+
esac
|
|
372
324
|
|
|
373
325
|
# Reject values that are too long (DoS protection)
|
|
374
326
|
if [ "${#value}" -gt "$max_length" ]; then
|
|
@@ -396,7 +348,8 @@ set_from_yaml() {
|
|
|
396
348
|
local yaml_path="$2"
|
|
397
349
|
local env_var="$3"
|
|
398
350
|
|
|
399
|
-
# Skip if env var is already set
|
|
351
|
+
# Skip if env var is already set (env-wins guard; the shared helper also
|
|
352
|
+
# enforces this with override=0, but checking here avoids the extraction).
|
|
400
353
|
if [ -n "${!env_var:-}" ]; then
|
|
401
354
|
return 0
|
|
402
355
|
fi
|
|
@@ -414,95 +367,40 @@ set_from_yaml() {
|
|
|
414
367
|
# Use read to avoid xargs command execution risks
|
|
415
368
|
value=$(grep -E "^\s*${escaped_key}:" "$file" 2>/dev/null | head -1 | sed -E 's/.*:\s*//' | sed 's/#.*//' | sed 's/^["\x27]//;s/["\x27]$//' | tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
416
369
|
|
|
417
|
-
#
|
|
418
|
-
|
|
370
|
+
# #691: delegate the env-wins guard + ${VAR}-expand + validate + export to the
|
|
371
|
+
# single shared helper (override=0 -> ambient env wins). Falls back to the
|
|
372
|
+
# original inline export if the lib is unavailable, so behavior is identical
|
|
373
|
+
# either way.
|
|
374
|
+
if declare -f loki_config_export_key >/dev/null 2>&1; then
|
|
375
|
+
loki_config_export_key "$env_var" "$value" 0 || true
|
|
376
|
+
elif [ -n "$value" ] && [ "$value" != "null" ] && validate_yaml_value "$value"; then
|
|
419
377
|
export "$env_var=$value"
|
|
420
378
|
fi
|
|
421
379
|
}
|
|
422
380
|
|
|
423
|
-
# Parse YAML using yq (proper parser)
|
|
381
|
+
# Parse YAML using yq (proper parser).
|
|
382
|
+
# #691: iterates the canonical LOKI_CONFIG_MAP and delegates the env-wins guard +
|
|
383
|
+
# expand + validate + export to the single shared helper (override=0 -> ambient
|
|
384
|
+
# env wins; auto-discovery contract unchanged). The inline 61-entry table that
|
|
385
|
+
# used to drift from parse_simple_yaml's 64 is gone.
|
|
424
386
|
parse_yaml_with_yq() {
|
|
425
387
|
local file="$1"
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
"core.max_wait:LOKI_MAX_WAIT"
|
|
430
|
-
"core.skip_prereqs:LOKI_SKIP_PREREQS"
|
|
431
|
-
"dashboard.enabled:LOKI_DASHBOARD"
|
|
432
|
-
"dashboard.port:LOKI_DASHBOARD_PORT"
|
|
433
|
-
"resources.check_interval:LOKI_RESOURCE_CHECK_INTERVAL"
|
|
434
|
-
"resources.cpu_threshold:LOKI_RESOURCE_CPU_THRESHOLD"
|
|
435
|
-
"resources.mem_threshold:LOKI_RESOURCE_MEM_THRESHOLD"
|
|
436
|
-
"security.staged_autonomy:LOKI_STAGED_AUTONOMY"
|
|
437
|
-
"security.audit_log:LOKI_AUDIT_LOG"
|
|
438
|
-
"security.max_parallel_agents:LOKI_MAX_PARALLEL_AGENTS"
|
|
439
|
-
"security.sandbox_mode:LOKI_SANDBOX_MODE"
|
|
440
|
-
"security.allowed_paths:LOKI_ALLOWED_PATHS"
|
|
441
|
-
"security.blocked_commands:LOKI_BLOCKED_COMMANDS"
|
|
442
|
-
"phases.unit_tests:LOKI_PHASE_UNIT_TESTS"
|
|
443
|
-
"phases.api_tests:LOKI_PHASE_API_TESTS"
|
|
444
|
-
"phases.e2e_tests:LOKI_PHASE_E2E_TESTS"
|
|
445
|
-
"phases.security:LOKI_PHASE_SECURITY"
|
|
446
|
-
"phases.integration:LOKI_PHASE_INTEGRATION"
|
|
447
|
-
"phases.code_review:LOKI_PHASE_CODE_REVIEW"
|
|
448
|
-
"phases.web_research:LOKI_PHASE_WEB_RESEARCH"
|
|
449
|
-
"phases.performance:LOKI_PHASE_PERFORMANCE"
|
|
450
|
-
"phases.accessibility:LOKI_PHASE_ACCESSIBILITY"
|
|
451
|
-
"phases.regression:LOKI_PHASE_REGRESSION"
|
|
452
|
-
"phases.uat:LOKI_PHASE_UAT"
|
|
453
|
-
"completion.promise:LOKI_COMPLETION_PROMISE"
|
|
454
|
-
"completion.max_iterations:LOKI_MAX_ITERATIONS"
|
|
455
|
-
"completion.perpetual_mode:LOKI_PERPETUAL_MODE"
|
|
456
|
-
"completion.council.enabled:LOKI_COUNCIL_ENABLED"
|
|
457
|
-
"completion.council.size:LOKI_COUNCIL_SIZE"
|
|
458
|
-
"completion.council.threshold:LOKI_COUNCIL_THRESHOLD"
|
|
459
|
-
"completion.council.check_interval:LOKI_COUNCIL_CHECK_INTERVAL"
|
|
460
|
-
"completion.council.min_iterations:LOKI_COUNCIL_MIN_ITERATIONS"
|
|
461
|
-
"completion.council.stagnation_limit:LOKI_COUNCIL_STAGNATION_LIMIT"
|
|
462
|
-
"completion.uncertainty.escalation:LOKI_UNCERTAINTY_ESCALATION"
|
|
463
|
-
"completion.uncertainty.rounds:LOKI_UNCERTAINTY_ROUNDS"
|
|
464
|
-
"completion.uncertainty.nochange_min:LOKI_UNCERTAINTY_NOCHANGE_MIN"
|
|
465
|
-
"completion.uncertainty.split_rounds:LOKI_UNCERTAINTY_SPLIT_ROUNDS"
|
|
466
|
-
"model.prompt_repetition:LOKI_PROMPT_REPETITION"
|
|
467
|
-
"model.confidence_routing:LOKI_CONFIDENCE_ROUTING"
|
|
468
|
-
"model.autonomy_mode:LOKI_AUTONOMY_MODE"
|
|
469
|
-
"parallel.enabled:LOKI_PARALLEL_MODE"
|
|
470
|
-
"parallel.max_worktrees:LOKI_MAX_WORKTREES"
|
|
471
|
-
"parallel.max_sessions:LOKI_MAX_PARALLEL_SESSIONS"
|
|
472
|
-
"parallel.testing:LOKI_PARALLEL_TESTING"
|
|
473
|
-
"parallel.docs:LOKI_PARALLEL_DOCS"
|
|
474
|
-
"parallel.blog:LOKI_PARALLEL_BLOG"
|
|
475
|
-
"parallel.auto_merge:LOKI_AUTO_MERGE"
|
|
476
|
-
"complexity.tier:LOKI_COMPLEXITY"
|
|
477
|
-
"github.import:LOKI_GITHUB_IMPORT"
|
|
478
|
-
"github.pr:LOKI_GITHUB_PR"
|
|
479
|
-
"github.sync:LOKI_GITHUB_SYNC"
|
|
480
|
-
"github.repo:LOKI_GITHUB_REPO"
|
|
481
|
-
"github.labels:LOKI_GITHUB_LABELS"
|
|
482
|
-
"github.milestone:LOKI_GITHUB_MILESTONE"
|
|
483
|
-
"github.assignee:LOKI_GITHUB_ASSIGNEE"
|
|
484
|
-
"github.limit:LOKI_GITHUB_LIMIT"
|
|
485
|
-
"github.pr_label:LOKI_GITHUB_PR_LABEL"
|
|
486
|
-
"notifications.enabled:LOKI_NOTIFICATIONS"
|
|
487
|
-
"notifications.sound:LOKI_NOTIFICATION_SOUND"
|
|
488
|
-
)
|
|
388
|
+
if ! declare -p LOKI_CONFIG_MAP >/dev/null 2>&1; then
|
|
389
|
+
return 0
|
|
390
|
+
fi
|
|
489
391
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
392
|
+
local mapping yaml_path env_var value
|
|
393
|
+
for mapping in "${LOKI_CONFIG_MAP[@]}"; do
|
|
394
|
+
yaml_path="${mapping%%:*}"
|
|
395
|
+
env_var="${mapping##*:}"
|
|
493
396
|
|
|
494
|
-
#
|
|
495
|
-
if [ -n "${!env_var:-}" ]; then
|
|
496
|
-
continue
|
|
497
|
-
fi
|
|
498
|
-
|
|
499
|
-
# Extract value using yq
|
|
500
|
-
local value
|
|
397
|
+
# Extract value using yq (env-wins guard is enforced by the shared helper).
|
|
501
398
|
value=$(yq eval ".$yaml_path // \"\"" "$file" 2>/dev/null)
|
|
502
399
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
400
|
+
if declare -f loki_config_export_key >/dev/null 2>&1; then
|
|
401
|
+
loki_config_export_key "$env_var" "$value" 0 || true
|
|
402
|
+
elif [ -n "$value" ] && [ "$value" != "null" ] && [ "$value" != "" ] \
|
|
403
|
+
&& [ -z "${!env_var:-}" ] && validate_yaml_value "$value"; then
|
|
506
404
|
export "$env_var=$value"
|
|
507
405
|
fi
|
|
508
406
|
done
|
|
@@ -593,9 +491,29 @@ STAGED_AUTONOMY=${LOKI_STAGED_AUTONOMY:-false} # Require plan approval
|
|
|
593
491
|
AUDIT_LOG_ENABLED=${LOKI_AUDIT_LOG:-true} # Enable audit logging (on by default)
|
|
594
492
|
MAX_PARALLEL_AGENTS=${LOKI_MAX_PARALLEL_AGENTS:-10} # Limit concurrent agents
|
|
595
493
|
SANDBOX_MODE=${LOKI_SANDBOX_MODE:-false} # Docker sandbox mode (informational; the real dispatch reads LOKI_SANDBOX_MODE at autonomy/loki:1965 and execs sandbox.sh -- this var is not consumed in run.sh)
|
|
596
|
-
ALLOWED_PATHS=${LOKI_ALLOWED_PATHS:-""} #
|
|
494
|
+
ALLOWED_PATHS=${LOKI_ALLOWED_PATHS:-""} # PARTIAL enforcement. When set, a sandbox custom --mount host path outside the allowlist is rejected in sandbox.sh (_sandbox_path_within_allowed), NOT in run.sh. Does NOT restrict provider-driven agent writes (run.sh never sees them); the sandbox container is the containment for those. Default empty = no restriction.
|
|
597
495
|
BLOCKED_COMMANDS=${LOKI_BLOCKED_COMMANDS:-"rm -rf /,dd if=,mkfs,:(){ :|:& };:"}
|
|
598
496
|
|
|
497
|
+
# LOKI_SESSION_ID is woven into many filesystem paths (.loki/sessions/<id>/...,
|
|
498
|
+
# session locks, PID files, per-session state). It is operator/issue-derived
|
|
499
|
+
# (trusted-ish), but a value like "../../tmp/x" would escape the .loki/ tree.
|
|
500
|
+
# Sanitize ONCE here at intake: any char outside [A-Za-z0-9._-] is replaced with
|
|
501
|
+
# "_" and a leading run of dots/dashes is neutralized, so every downstream
|
|
502
|
+
# session path is contained. Unset stays unset (default behavior unchanged).
|
|
503
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
504
|
+
_loki_sid_raw="$LOKI_SESSION_ID"
|
|
505
|
+
_loki_sid_safe="${_loki_sid_raw//[^A-Za-z0-9._-]/_}"
|
|
506
|
+
# Strip leading dots/dashes so "../x" -> "x" cannot reference a parent dir
|
|
507
|
+
# or look like an option; collapse a now-empty result to a safe literal.
|
|
508
|
+
_loki_sid_safe="${_loki_sid_safe#"${_loki_sid_safe%%[!.-]*}"}"
|
|
509
|
+
[ -n "$_loki_sid_safe" ] || _loki_sid_safe="session"
|
|
510
|
+
if [ "$_loki_sid_safe" != "$_loki_sid_raw" ]; then
|
|
511
|
+
printf 'loki: WARNING sanitized unsafe LOKI_SESSION_ID %s -> %s\n' "$_loki_sid_raw" "$_loki_sid_safe" >&2
|
|
512
|
+
fi
|
|
513
|
+
export LOKI_SESSION_ID="$_loki_sid_safe"
|
|
514
|
+
unset _loki_sid_raw _loki_sid_safe
|
|
515
|
+
fi
|
|
516
|
+
|
|
599
517
|
# Process Supervision (opt-in)
|
|
600
518
|
WATCHDOG_ENABLED=${LOKI_WATCHDOG:-"false"} # Enable process health monitoring
|
|
601
519
|
WATCHDOG_INTERVAL=${LOKI_WATCHDOG_INTERVAL:-30} # Check interval in seconds
|
|
@@ -6275,6 +6193,28 @@ check_command_allowed() {
|
|
|
6275
6193
|
return 0
|
|
6276
6194
|
}
|
|
6277
6195
|
|
|
6196
|
+
# A5 (ALLOWED_PATHS enforcement) -- HONEST THREAT MODEL
|
|
6197
|
+
# --------------------------------------------------------------------------
|
|
6198
|
+
# The DOMINANT file-write path in an autonomous build is the PROVIDER CLI
|
|
6199
|
+
# (claude / codex / cline / aider) writing files directly. run.sh hands the
|
|
6200
|
+
# work to that CLI and never sees those writes as shell redirections, so run.sh
|
|
6201
|
+
# CANNOT intercept provider-driven writes. ALLOWED_PATHS therefore cannot be a
|
|
6202
|
+
# complete sandbox; the real containment for provider writes is the OS user +
|
|
6203
|
+
# the LOKI_SANDBOX_MODE container (sandbox.sh, cap-drop/seccomp/read-only
|
|
6204
|
+
# mounts), not this shell.
|
|
6205
|
+
#
|
|
6206
|
+
# Where ALLOWED_PATHS is actually enforced: the sandbox custom --mount surface.
|
|
6207
|
+
# That enforcement lives in sandbox.sh, NOT here. When ALLOWED_PATHS is set, a
|
|
6208
|
+
# host path that loki is about to bind-mount into the container is checked by
|
|
6209
|
+
# _sandbox_path_within_allowed (autonomy/sandbox.sh), and an operator-supplied
|
|
6210
|
+
# `loki sandbox run` argv is checked by _sandbox_command_allowed there too.
|
|
6211
|
+
# run.sh itself does not consume ALLOWED_PATHS for any write decision -- it has
|
|
6212
|
+
# no run.sh-controlled host-write surface to gate -- so there is no enforcement
|
|
6213
|
+
# helper here. (A run.sh-local helper existed previously but had zero call
|
|
6214
|
+
# sites; it was removed so the only enforcement path is the one that genuinely
|
|
6215
|
+
# runs. See tests/test-allowed-paths-a5.sh, which exercises the sandbox.sh
|
|
6216
|
+
# functions.)
|
|
6217
|
+
|
|
6278
6218
|
#===============================================================================
|
|
6279
6219
|
# Cross-Project Learnings Database
|
|
6280
6220
|
#===============================================================================
|
|
@@ -9693,6 +9633,18 @@ create_checkpoint() {
|
|
|
9693
9633
|
cp ".loki/$f" "$cp_dir/$f" 2>/dev/null || true
|
|
9694
9634
|
fi
|
|
9695
9635
|
done
|
|
9636
|
+
# A6: under a namespaced session the live state file is at
|
|
9637
|
+
# .loki/sessions/<id>/autonomy-state.json, which the legacy loop above misses.
|
|
9638
|
+
# Snapshot it into the checkpoint as autonomy-state.json so a rollback still
|
|
9639
|
+
# captures the current run's machine state. (Default `loki start` has no
|
|
9640
|
+
# LOKI_SESSION_ID, so this block is skipped and the loop above is unchanged.)
|
|
9641
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
9642
|
+
local _ns_state_file
|
|
9643
|
+
_ns_state_file="$(_loki_state_file)"
|
|
9644
|
+
if [ -f "$_ns_state_file" ]; then
|
|
9645
|
+
cp "$_ns_state_file" "$cp_dir/autonomy-state.json" 2>/dev/null || true
|
|
9646
|
+
fi
|
|
9647
|
+
fi
|
|
9696
9648
|
|
|
9697
9649
|
# R6: capture a real working-tree snapshot so code can be truly undone later.
|
|
9698
9650
|
# Loki does not commit per iteration, so git_sha (HEAD) cannot reconstruct
|
|
@@ -9790,6 +9742,47 @@ print(json.dumps({'id':m['id'],'ts':m['timestamp'],'iter':m['iteration'],'task':
|
|
|
9790
9742
|
# it without parsing stdout (log_info writes to stdout, so command-substitution
|
|
9791
9743
|
# capture would include log lines).
|
|
9792
9744
|
_LAST_CHECKPOINT_ID="$checkpoint_id"
|
|
9745
|
+
|
|
9746
|
+
# A3: best-effort object-store sync of checkpoint state. No-op unless
|
|
9747
|
+
# LOKI_STORAGE_BACKEND is set to a non-local backend. Never blocks/fails the
|
|
9748
|
+
# build on a sync error (the shim swallows errors; we also `|| true`).
|
|
9749
|
+
_loki_object_store_sync_checkpoints || true
|
|
9750
|
+
}
|
|
9751
|
+
|
|
9752
|
+
# A3: push .loki/state/checkpoints/** to the configured object store (best-effort).
|
|
9753
|
+
# Local/unset backend => no-op (zero behavior change for local users). Provider
|
|
9754
|
+
# writes are out of scope; this syncs only the checkpoint state run.sh owns.
|
|
9755
|
+
_loki_object_store_sync_checkpoints() {
|
|
9756
|
+
local backend="${LOKI_STORAGE_BACKEND:-local}"
|
|
9757
|
+
case "$backend" in
|
|
9758
|
+
local|""|file|filesystem) return 0 ;;
|
|
9759
|
+
esac
|
|
9760
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
9761
|
+
local shim="${SCRIPT_DIR}/lib/checkpoint_sync.py"
|
|
9762
|
+
[ -f "$shim" ] || return 0
|
|
9763
|
+
python3 "$shim" sync >/dev/null 2>&1 || log_warn "Object-store checkpoint sync skipped (backend=$backend); build continues."
|
|
9764
|
+
return 0
|
|
9765
|
+
}
|
|
9766
|
+
|
|
9767
|
+
# A3: hydrate .loki/state/checkpoints/** from the object store on a durable
|
|
9768
|
+
# resume when the local volume is empty (best-effort). No-op unless
|
|
9769
|
+
# LOKI_DURABLE_STATE=1 AND a non-local LOKI_STORAGE_BACKEND is set. The shim
|
|
9770
|
+
# itself guards against overwriting a non-empty local volume.
|
|
9771
|
+
_loki_object_store_hydrate_checkpoints() {
|
|
9772
|
+
[ "${LOKI_DURABLE_STATE:-0}" = "1" ] || return 0
|
|
9773
|
+
local backend="${LOKI_STORAGE_BACKEND:-local}"
|
|
9774
|
+
case "$backend" in
|
|
9775
|
+
local|""|file|filesystem) return 0 ;;
|
|
9776
|
+
esac
|
|
9777
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
9778
|
+
local shim="${SCRIPT_DIR}/lib/checkpoint_sync.py"
|
|
9779
|
+
[ -f "$shim" ] || return 0
|
|
9780
|
+
if python3 "$shim" hydrate >/dev/null 2>&1; then
|
|
9781
|
+
log_info "Durable resume: checked object store ($backend) for prior checkpoints."
|
|
9782
|
+
else
|
|
9783
|
+
log_warn "Object-store checkpoint hydrate skipped (backend=$backend); build continues with local state."
|
|
9784
|
+
fi
|
|
9785
|
+
return 0
|
|
9793
9786
|
}
|
|
9794
9787
|
|
|
9795
9788
|
start_dashboard() {
|
|
@@ -11807,16 +11800,39 @@ PYEOF
|
|
|
11807
11800
|
# Save/Load Wrapper State
|
|
11808
11801
|
#===============================================================================
|
|
11809
11802
|
|
|
11803
|
+
# A6 (multi-build-per-pod state isolation): resolve the autonomy-state.json path.
|
|
11804
|
+
# The existing .loki/sessions/<id>/ scoping (v6.4.0) namespaces PID + lock files
|
|
11805
|
+
# so concurrent sessions in the SAME TARGET_DIR do not collide. autonomy-state.json
|
|
11806
|
+
# was NOT covered by that scoping, so two concurrent builds with distinct
|
|
11807
|
+
# LOKI_SESSION_IDs still raced on the one global .loki/autonomy-state.json
|
|
11808
|
+
# (lost-update on retry/iteration counts). When LOKI_SESSION_ID is set we mirror
|
|
11809
|
+
# the session scoping and put the file under .loki/sessions/<id>/. When unset
|
|
11810
|
+
# (the normal `loki start ./prd.md` case) the legacy .loki/autonomy-state.json
|
|
11811
|
+
# path is returned UNCHANGED, so default single-session behavior is byte-identical.
|
|
11812
|
+
_loki_state_file() {
|
|
11813
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
11814
|
+
printf '%s' ".loki/sessions/${LOKI_SESSION_ID}/autonomy-state.json"
|
|
11815
|
+
else
|
|
11816
|
+
printf '%s' ".loki/autonomy-state.json"
|
|
11817
|
+
fi
|
|
11818
|
+
}
|
|
11819
|
+
|
|
11810
11820
|
save_state() {
|
|
11811
11821
|
local retry_count="$1"
|
|
11812
11822
|
local status="$2"
|
|
11813
11823
|
local exit_code="$3"
|
|
11814
11824
|
|
|
11815
|
-
|
|
11816
|
-
|
|
11825
|
+
local state_file
|
|
11826
|
+
state_file="$(_loki_state_file)"
|
|
11827
|
+
|
|
11828
|
+
# BUG-ST-013: Ensure target directory exists (defensive -- may be called from
|
|
11829
|
+
# signal handler). For a namespaced session path this also creates
|
|
11830
|
+
# .loki/sessions/<id>/; for the default path it is .loki/ as before.
|
|
11831
|
+
mkdir -p "$(dirname "$state_file")" 2>/dev/null || true
|
|
11817
11832
|
|
|
11818
|
-
# BUG-XC-004: Atomic write via temp file + mv
|
|
11819
|
-
|
|
11833
|
+
# BUG-XC-004: Atomic write via temp file + mv (temp lives beside the target so
|
|
11834
|
+
# the rename stays on the same filesystem)
|
|
11835
|
+
local state_tmp="${state_file}.tmp.$$"
|
|
11820
11836
|
cat > "$state_tmp" << EOF
|
|
11821
11837
|
{
|
|
11822
11838
|
"retryCount": $retry_count,
|
|
@@ -11830,23 +11846,30 @@ save_state() {
|
|
|
11830
11846
|
"baseWait": $BASE_WAIT
|
|
11831
11847
|
}
|
|
11832
11848
|
EOF
|
|
11833
|
-
mv -f "$state_tmp" "
|
|
11849
|
+
mv -f "$state_tmp" "$state_file"
|
|
11834
11850
|
}
|
|
11835
11851
|
|
|
11836
11852
|
load_state() {
|
|
11853
|
+
local state_file
|
|
11854
|
+
state_file="$(_loki_state_file)"
|
|
11855
|
+
|
|
11837
11856
|
# BUG-EP-015: Clean up orphaned temp files from kill -9 crashes
|
|
11838
11857
|
# These are left behind when the process is killed during atomic writes
|
|
11839
11858
|
find .loki/ -maxdepth 1 -name "*.tmp.*" -mmin +5 -delete 2>/dev/null || true
|
|
11840
11859
|
find .loki/state/ -name "*.tmp.*" -mmin +5 -delete 2>/dev/null || true
|
|
11860
|
+
# A6: also sweep namespaced session state temps when running under a session id
|
|
11861
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
11862
|
+
find ".loki/sessions/${LOKI_SESSION_ID}/" -maxdepth 1 -name "*.tmp.*" -mmin +5 -delete 2>/dev/null || true
|
|
11863
|
+
fi
|
|
11841
11864
|
|
|
11842
|
-
if [ -f "
|
|
11865
|
+
if [ -f "$state_file" ]; then
|
|
11843
11866
|
if command -v python3 &> /dev/null; then
|
|
11844
11867
|
# BUG-ST-006: Validate checkpoint integrity before loading state
|
|
11845
11868
|
local state_valid
|
|
11846
|
-
state_valid=$(python3 -c "
|
|
11847
|
-
import json, sys
|
|
11869
|
+
state_valid=$(LOKI_STATE_FILE="$state_file" python3 -c "
|
|
11870
|
+
import json, os, sys
|
|
11848
11871
|
try:
|
|
11849
|
-
with open(
|
|
11872
|
+
with open(os.environ['LOKI_STATE_FILE']) as f:
|
|
11850
11873
|
d = json.load(f)
|
|
11851
11874
|
# Validate required fields exist and have sane types
|
|
11852
11875
|
rc = d.get('retryCount', 0)
|
|
@@ -11868,16 +11891,16 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
|
11868
11891
|
RETRY_COUNT=0
|
|
11869
11892
|
ITERATION_COUNT=0
|
|
11870
11893
|
# Back up corrupted state file for diagnosis
|
|
11871
|
-
mv "
|
|
11894
|
+
mv "$state_file" "${state_file}.corrupt.$(date +%s)" 2>/dev/null || true
|
|
11872
11895
|
return
|
|
11873
11896
|
fi
|
|
11874
11897
|
|
|
11875
11898
|
# Load retry count, iteration count, and status from previous session
|
|
11876
11899
|
local prev_status
|
|
11877
|
-
prev_status=$(python3 -c "import json; print(json.load(open(
|
|
11878
|
-
RETRY_COUNT=$(python3 -c "import json; print(json.load(open(
|
|
11900
|
+
prev_status=$(LOKI_STATE_FILE="$state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status', 'unknown'))" 2>/dev/null || echo "unknown")
|
|
11901
|
+
RETRY_COUNT=$(LOKI_STATE_FILE="$state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('retryCount', 0))" 2>/dev/null || echo "0")
|
|
11879
11902
|
# BUG-RUN-003: Restore ITERATION_COUNT from persisted state
|
|
11880
|
-
ITERATION_COUNT=$(python3 -c "import json; print(json.load(open(
|
|
11903
|
+
ITERATION_COUNT=$(LOKI_STATE_FILE="$state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('iterationCount', 0))" 2>/dev/null || echo "0")
|
|
11881
11904
|
|
|
11882
11905
|
# Reset retry count + iteration count if previous session ended in a
|
|
11883
11906
|
# terminal state. A fresh `loki start` after a terminal run is a NEW
|
|
@@ -13979,7 +14002,7 @@ except Exception as exc:
|
|
|
13979
14002
|
|
|
13980
14003
|
# Check max iterations before starting
|
|
13981
14004
|
if check_max_iterations; then
|
|
13982
|
-
log_error "Max iterations already reached. Reset with: rm
|
|
14005
|
+
log_error "Max iterations already reached. Reset with: rm $(_loki_state_file)"
|
|
13983
14006
|
# Delegate-then-notify: terminal state. Mirror the in-loop max-iterations
|
|
13984
14007
|
# site so a detached (--bg) run still writes COMPLETION.txt + fires the
|
|
13985
14008
|
# ping on this pre-loop exit. _LOKI_RUN_START_SHA is already exported
|
|
@@ -16818,6 +16841,14 @@ main() {
|
|
|
16818
16841
|
# durable mount, so a misconfigured volume never silently loses build state.
|
|
16819
16842
|
assert_durable_state_mount
|
|
16820
16843
|
|
|
16844
|
+
# A3: on a durable resume with an object-store backend, hydrate this run's
|
|
16845
|
+
# checkpoints from the store IF the local volume came up empty (a fresh pod /
|
|
16846
|
+
# non-durable volume). Best-effort + idempotent: the shim no-ops when the
|
|
16847
|
+
# backend is local, when the local volume already has checkpoints, or when
|
|
16848
|
+
# the store has nothing for this run. Runs before the main loop so any
|
|
16849
|
+
# resume/rollback sees the restored state. Zero behavior change for local.
|
|
16850
|
+
_loki_object_store_hydrate_checkpoints || true
|
|
16851
|
+
|
|
16821
16852
|
# Setup agent branch protection (isolates agent changes to a feature branch)
|
|
16822
16853
|
setup_agent_branch
|
|
16823
16854
|
|
|
@@ -17035,8 +17066,9 @@ main() {
|
|
|
17035
17066
|
# because the process was SIGKILLed before reaching here). Retryable;
|
|
17036
17067
|
# the restarted Job resumes via the ENT-2 durable-resume path.
|
|
17037
17068
|
if [ "${LOKI_DURABLE_STATE:-0}" = "1" ]; then
|
|
17038
|
-
local _final_status
|
|
17039
|
-
|
|
17069
|
+
local _final_status _final_state_file
|
|
17070
|
+
_final_state_file="$(_loki_state_file)"
|
|
17071
|
+
_final_status=$(LOKI_STATE_FILE="$_final_state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status','unknown'))" 2>/dev/null || echo "unknown")
|
|
17040
17072
|
case "$_final_status" in
|
|
17041
17073
|
council_approved|council_force_approved|completion_promise_fulfilled|force_stopped|paused|interrupted|budget_exceeded|stopped)
|
|
17042
17074
|
result=0 ;;
|