loki-mode 7.78.0 → 7.80.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 +273 -8
- package/autonomy/run.sh +230 -195
- package/autonomy/sandbox.sh +98 -0
- package/autonomy/trigger-server.py +419 -43
- package/dashboard/__init__.py +1 -1
- package/dashboard/registry.py +212 -0
- package/dashboard/server.py +299 -7
- package/dashboard/static/index.html +383 -150
- package/docs/CONFIG-FILE-PLAN.md +459 -0
- package/docs/ENTERPRISE-IDENTITY-ROADMAP.md +206 -0
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/lokistore/__init__.py +65 -0
- package/lokistore/base.py +172 -0
- package/lokistore/cloud.py +305 -0
- package/lokistore/factory.py +187 -0
- package/lokistore/local.py +219 -0
- package/mcp/__init__.py +1 -1
- package/memory/retrieval.py +147 -0
- package/memory/tree_index.py +499 -0
- package/memory/tree_search.py +305 -0
- package/package.json +2 -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
|
|
@@ -5335,6 +5253,9 @@ print_ttfv_next_steps() {
|
|
|
5335
5253
|
echo " loki start ./prd.md # build from a full PRD"
|
|
5336
5254
|
echo " loki start \"<one line>\" # fast first pass from a brief"
|
|
5337
5255
|
fi
|
|
5256
|
+
# Frictionless discovery: surface 'loki why' so users learn the
|
|
5257
|
+
# what-happened/what-to-do diagnosis without having to know it exists.
|
|
5258
|
+
echo " loki why # explain this outcome + what to do next"
|
|
5338
5259
|
echo ""
|
|
5339
5260
|
return 0
|
|
5340
5261
|
}
|
|
@@ -6275,6 +6196,28 @@ check_command_allowed() {
|
|
|
6275
6196
|
return 0
|
|
6276
6197
|
}
|
|
6277
6198
|
|
|
6199
|
+
# A5 (ALLOWED_PATHS enforcement) -- HONEST THREAT MODEL
|
|
6200
|
+
# --------------------------------------------------------------------------
|
|
6201
|
+
# The DOMINANT file-write path in an autonomous build is the PROVIDER CLI
|
|
6202
|
+
# (claude / codex / cline / aider) writing files directly. run.sh hands the
|
|
6203
|
+
# work to that CLI and never sees those writes as shell redirections, so run.sh
|
|
6204
|
+
# CANNOT intercept provider-driven writes. ALLOWED_PATHS therefore cannot be a
|
|
6205
|
+
# complete sandbox; the real containment for provider writes is the OS user +
|
|
6206
|
+
# the LOKI_SANDBOX_MODE container (sandbox.sh, cap-drop/seccomp/read-only
|
|
6207
|
+
# mounts), not this shell.
|
|
6208
|
+
#
|
|
6209
|
+
# Where ALLOWED_PATHS is actually enforced: the sandbox custom --mount surface.
|
|
6210
|
+
# That enforcement lives in sandbox.sh, NOT here. When ALLOWED_PATHS is set, a
|
|
6211
|
+
# host path that loki is about to bind-mount into the container is checked by
|
|
6212
|
+
# _sandbox_path_within_allowed (autonomy/sandbox.sh), and an operator-supplied
|
|
6213
|
+
# `loki sandbox run` argv is checked by _sandbox_command_allowed there too.
|
|
6214
|
+
# run.sh itself does not consume ALLOWED_PATHS for any write decision -- it has
|
|
6215
|
+
# no run.sh-controlled host-write surface to gate -- so there is no enforcement
|
|
6216
|
+
# helper here. (A run.sh-local helper existed previously but had zero call
|
|
6217
|
+
# sites; it was removed so the only enforcement path is the one that genuinely
|
|
6218
|
+
# runs. See tests/test-allowed-paths-a5.sh, which exercises the sandbox.sh
|
|
6219
|
+
# functions.)
|
|
6220
|
+
|
|
6278
6221
|
#===============================================================================
|
|
6279
6222
|
# Cross-Project Learnings Database
|
|
6280
6223
|
#===============================================================================
|
|
@@ -9693,6 +9636,18 @@ create_checkpoint() {
|
|
|
9693
9636
|
cp ".loki/$f" "$cp_dir/$f" 2>/dev/null || true
|
|
9694
9637
|
fi
|
|
9695
9638
|
done
|
|
9639
|
+
# A6: under a namespaced session the live state file is at
|
|
9640
|
+
# .loki/sessions/<id>/autonomy-state.json, which the legacy loop above misses.
|
|
9641
|
+
# Snapshot it into the checkpoint as autonomy-state.json so a rollback still
|
|
9642
|
+
# captures the current run's machine state. (Default `loki start` has no
|
|
9643
|
+
# LOKI_SESSION_ID, so this block is skipped and the loop above is unchanged.)
|
|
9644
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
9645
|
+
local _ns_state_file
|
|
9646
|
+
_ns_state_file="$(_loki_state_file)"
|
|
9647
|
+
if [ -f "$_ns_state_file" ]; then
|
|
9648
|
+
cp "$_ns_state_file" "$cp_dir/autonomy-state.json" 2>/dev/null || true
|
|
9649
|
+
fi
|
|
9650
|
+
fi
|
|
9696
9651
|
|
|
9697
9652
|
# R6: capture a real working-tree snapshot so code can be truly undone later.
|
|
9698
9653
|
# Loki does not commit per iteration, so git_sha (HEAD) cannot reconstruct
|
|
@@ -9790,6 +9745,47 @@ print(json.dumps({'id':m['id'],'ts':m['timestamp'],'iter':m['iteration'],'task':
|
|
|
9790
9745
|
# it without parsing stdout (log_info writes to stdout, so command-substitution
|
|
9791
9746
|
# capture would include log lines).
|
|
9792
9747
|
_LAST_CHECKPOINT_ID="$checkpoint_id"
|
|
9748
|
+
|
|
9749
|
+
# A3: best-effort object-store sync of checkpoint state. No-op unless
|
|
9750
|
+
# LOKI_STORAGE_BACKEND is set to a non-local backend. Never blocks/fails the
|
|
9751
|
+
# build on a sync error (the shim swallows errors; we also `|| true`).
|
|
9752
|
+
_loki_object_store_sync_checkpoints || true
|
|
9753
|
+
}
|
|
9754
|
+
|
|
9755
|
+
# A3: push .loki/state/checkpoints/** to the configured object store (best-effort).
|
|
9756
|
+
# Local/unset backend => no-op (zero behavior change for local users). Provider
|
|
9757
|
+
# writes are out of scope; this syncs only the checkpoint state run.sh owns.
|
|
9758
|
+
_loki_object_store_sync_checkpoints() {
|
|
9759
|
+
local backend="${LOKI_STORAGE_BACKEND:-local}"
|
|
9760
|
+
case "$backend" in
|
|
9761
|
+
local|""|file|filesystem) return 0 ;;
|
|
9762
|
+
esac
|
|
9763
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
9764
|
+
local shim="${SCRIPT_DIR}/lib/checkpoint_sync.py"
|
|
9765
|
+
[ -f "$shim" ] || return 0
|
|
9766
|
+
python3 "$shim" sync >/dev/null 2>&1 || log_warn "Object-store checkpoint sync skipped (backend=$backend); build continues."
|
|
9767
|
+
return 0
|
|
9768
|
+
}
|
|
9769
|
+
|
|
9770
|
+
# A3: hydrate .loki/state/checkpoints/** from the object store on a durable
|
|
9771
|
+
# resume when the local volume is empty (best-effort). No-op unless
|
|
9772
|
+
# LOKI_DURABLE_STATE=1 AND a non-local LOKI_STORAGE_BACKEND is set. The shim
|
|
9773
|
+
# itself guards against overwriting a non-empty local volume.
|
|
9774
|
+
_loki_object_store_hydrate_checkpoints() {
|
|
9775
|
+
[ "${LOKI_DURABLE_STATE:-0}" = "1" ] || return 0
|
|
9776
|
+
local backend="${LOKI_STORAGE_BACKEND:-local}"
|
|
9777
|
+
case "$backend" in
|
|
9778
|
+
local|""|file|filesystem) return 0 ;;
|
|
9779
|
+
esac
|
|
9780
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
9781
|
+
local shim="${SCRIPT_DIR}/lib/checkpoint_sync.py"
|
|
9782
|
+
[ -f "$shim" ] || return 0
|
|
9783
|
+
if python3 "$shim" hydrate >/dev/null 2>&1; then
|
|
9784
|
+
log_info "Durable resume: checked object store ($backend) for prior checkpoints."
|
|
9785
|
+
else
|
|
9786
|
+
log_warn "Object-store checkpoint hydrate skipped (backend=$backend); build continues with local state."
|
|
9787
|
+
fi
|
|
9788
|
+
return 0
|
|
9793
9789
|
}
|
|
9794
9790
|
|
|
9795
9791
|
start_dashboard() {
|
|
@@ -11807,16 +11803,39 @@ PYEOF
|
|
|
11807
11803
|
# Save/Load Wrapper State
|
|
11808
11804
|
#===============================================================================
|
|
11809
11805
|
|
|
11806
|
+
# A6 (multi-build-per-pod state isolation): resolve the autonomy-state.json path.
|
|
11807
|
+
# The existing .loki/sessions/<id>/ scoping (v6.4.0) namespaces PID + lock files
|
|
11808
|
+
# so concurrent sessions in the SAME TARGET_DIR do not collide. autonomy-state.json
|
|
11809
|
+
# was NOT covered by that scoping, so two concurrent builds with distinct
|
|
11810
|
+
# LOKI_SESSION_IDs still raced on the one global .loki/autonomy-state.json
|
|
11811
|
+
# (lost-update on retry/iteration counts). When LOKI_SESSION_ID is set we mirror
|
|
11812
|
+
# the session scoping and put the file under .loki/sessions/<id>/. When unset
|
|
11813
|
+
# (the normal `loki start ./prd.md` case) the legacy .loki/autonomy-state.json
|
|
11814
|
+
# path is returned UNCHANGED, so default single-session behavior is byte-identical.
|
|
11815
|
+
_loki_state_file() {
|
|
11816
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
11817
|
+
printf '%s' ".loki/sessions/${LOKI_SESSION_ID}/autonomy-state.json"
|
|
11818
|
+
else
|
|
11819
|
+
printf '%s' ".loki/autonomy-state.json"
|
|
11820
|
+
fi
|
|
11821
|
+
}
|
|
11822
|
+
|
|
11810
11823
|
save_state() {
|
|
11811
11824
|
local retry_count="$1"
|
|
11812
11825
|
local status="$2"
|
|
11813
11826
|
local exit_code="$3"
|
|
11814
11827
|
|
|
11815
|
-
|
|
11816
|
-
|
|
11828
|
+
local state_file
|
|
11829
|
+
state_file="$(_loki_state_file)"
|
|
11830
|
+
|
|
11831
|
+
# BUG-ST-013: Ensure target directory exists (defensive -- may be called from
|
|
11832
|
+
# signal handler). For a namespaced session path this also creates
|
|
11833
|
+
# .loki/sessions/<id>/; for the default path it is .loki/ as before.
|
|
11834
|
+
mkdir -p "$(dirname "$state_file")" 2>/dev/null || true
|
|
11817
11835
|
|
|
11818
|
-
# BUG-XC-004: Atomic write via temp file + mv
|
|
11819
|
-
|
|
11836
|
+
# BUG-XC-004: Atomic write via temp file + mv (temp lives beside the target so
|
|
11837
|
+
# the rename stays on the same filesystem)
|
|
11838
|
+
local state_tmp="${state_file}.tmp.$$"
|
|
11820
11839
|
cat > "$state_tmp" << EOF
|
|
11821
11840
|
{
|
|
11822
11841
|
"retryCount": $retry_count,
|
|
@@ -11830,23 +11849,30 @@ save_state() {
|
|
|
11830
11849
|
"baseWait": $BASE_WAIT
|
|
11831
11850
|
}
|
|
11832
11851
|
EOF
|
|
11833
|
-
mv -f "$state_tmp" "
|
|
11852
|
+
mv -f "$state_tmp" "$state_file"
|
|
11834
11853
|
}
|
|
11835
11854
|
|
|
11836
11855
|
load_state() {
|
|
11856
|
+
local state_file
|
|
11857
|
+
state_file="$(_loki_state_file)"
|
|
11858
|
+
|
|
11837
11859
|
# BUG-EP-015: Clean up orphaned temp files from kill -9 crashes
|
|
11838
11860
|
# These are left behind when the process is killed during atomic writes
|
|
11839
11861
|
find .loki/ -maxdepth 1 -name "*.tmp.*" -mmin +5 -delete 2>/dev/null || true
|
|
11840
11862
|
find .loki/state/ -name "*.tmp.*" -mmin +5 -delete 2>/dev/null || true
|
|
11863
|
+
# A6: also sweep namespaced session state temps when running under a session id
|
|
11864
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
11865
|
+
find ".loki/sessions/${LOKI_SESSION_ID}/" -maxdepth 1 -name "*.tmp.*" -mmin +5 -delete 2>/dev/null || true
|
|
11866
|
+
fi
|
|
11841
11867
|
|
|
11842
|
-
if [ -f "
|
|
11868
|
+
if [ -f "$state_file" ]; then
|
|
11843
11869
|
if command -v python3 &> /dev/null; then
|
|
11844
11870
|
# BUG-ST-006: Validate checkpoint integrity before loading state
|
|
11845
11871
|
local state_valid
|
|
11846
|
-
state_valid=$(python3 -c "
|
|
11847
|
-
import json, sys
|
|
11872
|
+
state_valid=$(LOKI_STATE_FILE="$state_file" python3 -c "
|
|
11873
|
+
import json, os, sys
|
|
11848
11874
|
try:
|
|
11849
|
-
with open(
|
|
11875
|
+
with open(os.environ['LOKI_STATE_FILE']) as f:
|
|
11850
11876
|
d = json.load(f)
|
|
11851
11877
|
# Validate required fields exist and have sane types
|
|
11852
11878
|
rc = d.get('retryCount', 0)
|
|
@@ -11868,16 +11894,16 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
|
11868
11894
|
RETRY_COUNT=0
|
|
11869
11895
|
ITERATION_COUNT=0
|
|
11870
11896
|
# Back up corrupted state file for diagnosis
|
|
11871
|
-
mv "
|
|
11897
|
+
mv "$state_file" "${state_file}.corrupt.$(date +%s)" 2>/dev/null || true
|
|
11872
11898
|
return
|
|
11873
11899
|
fi
|
|
11874
11900
|
|
|
11875
11901
|
# Load retry count, iteration count, and status from previous session
|
|
11876
11902
|
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(
|
|
11903
|
+
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")
|
|
11904
|
+
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
11905
|
# BUG-RUN-003: Restore ITERATION_COUNT from persisted state
|
|
11880
|
-
ITERATION_COUNT=$(python3 -c "import json; print(json.load(open(
|
|
11906
|
+
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
11907
|
|
|
11882
11908
|
# Reset retry count + iteration count if previous session ended in a
|
|
11883
11909
|
# terminal state. A fresh `loki start` after a terminal run is a NEW
|
|
@@ -13979,7 +14005,7 @@ except Exception as exc:
|
|
|
13979
14005
|
|
|
13980
14006
|
# Check max iterations before starting
|
|
13981
14007
|
if check_max_iterations; then
|
|
13982
|
-
log_error "Max iterations already reached. Reset with: rm
|
|
14008
|
+
log_error "Max iterations already reached. Reset with: rm $(_loki_state_file)"
|
|
13983
14009
|
# Delegate-then-notify: terminal state. Mirror the in-loop max-iterations
|
|
13984
14010
|
# site so a detached (--bg) run still writes COMPLETION.txt + fires the
|
|
13985
14011
|
# ping on this pre-loop exit. _LOKI_RUN_START_SHA is already exported
|
|
@@ -16818,6 +16844,14 @@ main() {
|
|
|
16818
16844
|
# durable mount, so a misconfigured volume never silently loses build state.
|
|
16819
16845
|
assert_durable_state_mount
|
|
16820
16846
|
|
|
16847
|
+
# A3: on a durable resume with an object-store backend, hydrate this run's
|
|
16848
|
+
# checkpoints from the store IF the local volume came up empty (a fresh pod /
|
|
16849
|
+
# non-durable volume). Best-effort + idempotent: the shim no-ops when the
|
|
16850
|
+
# backend is local, when the local volume already has checkpoints, or when
|
|
16851
|
+
# the store has nothing for this run. Runs before the main loop so any
|
|
16852
|
+
# resume/rollback sees the restored state. Zero behavior change for local.
|
|
16853
|
+
_loki_object_store_hydrate_checkpoints || true
|
|
16854
|
+
|
|
16821
16855
|
# Setup agent branch protection (isolates agent changes to a feature branch)
|
|
16822
16856
|
setup_agent_branch
|
|
16823
16857
|
|
|
@@ -17035,8 +17069,9 @@ main() {
|
|
|
17035
17069
|
# because the process was SIGKILLed before reaching here). Retryable;
|
|
17036
17070
|
# the restarted Job resumes via the ENT-2 durable-resume path.
|
|
17037
17071
|
if [ "${LOKI_DURABLE_STATE:-0}" = "1" ]; then
|
|
17038
|
-
local _final_status
|
|
17039
|
-
|
|
17072
|
+
local _final_status _final_state_file
|
|
17073
|
+
_final_state_file="$(_loki_state_file)"
|
|
17074
|
+
_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
17075
|
case "$_final_status" in
|
|
17041
17076
|
council_approved|council_force_approved|completion_promise_fulfilled|force_stopped|paused|interrupted|budget_exceeded|stopped)
|
|
17042
17077
|
result=0 ;;
|