loki-mode 9.22.12 → 9.23.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 +3 -3
- package/VERSION +1 -1
- package/autonomy/lib/claude-flags.sh +69 -5
- package/autonomy/lib/contract-scaffold/scaffold.sh +1 -1
- package/autonomy/loki +324 -74
- package/autonomy/run.sh +120 -39
- package/autonomy/verify.sh +16 -3
- package/bin/loki +25 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/control.py +88 -4
- package/dashboard/server.py +50 -48
- package/docs/INSTALLATION.md +2 -2
- package/docs/RARV-C-100X-PLAN.md +9 -1
- package/docs/enterprise/performance.md +51 -9
- package/docs/exit-codes.md +4 -0
- package/loki-ts/dist/cockpit.js +1 -1
- package/loki-ts/dist/loki.js +634 -549
- package/loki-ts/package.json +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +6 -5
- package/plugins/loki-mode/.claude-plugin/plugin.json +2 -3
- package/providers/loader.sh +2 -2
- package/providers/model_catalog.json +26 -8
- package/providers/models.sh +1 -1
- package/providers/opencode.sh +1 -1
- package/skills/providers.md +42 -14
- package/tools/normalize-package-permissions.mjs +34 -0
- package/web-app/dist/assets/{AdminPage-DLhWX6si.js → AdminPage-PyyVXg4b.js} +1 -1
- package/web-app/dist/assets/{Avatar-CqyDcUxl.js → Avatar-CTUlr_Xf.js} +1 -1
- package/web-app/dist/assets/{Badge-BKGSt1n2.js → Badge-BQeXvmg8.js} +1 -1
- package/web-app/dist/assets/{Button-DuuKHXre.js → Button-Dc6-P6pz.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DdClYERC.js → CockpitPage-BspOJ3Fv.js} +1 -1
- package/web-app/dist/assets/{ComparePage-CUWlqBxv.js → ComparePage-CFXIM--Z.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-RwfGKyRJ.js → ErrorBoundary-C7vQYDW9.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-CzIgk9sg.js → EvidenceReceiptPanel-CqjsjTOs.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-CsXVKuyJ.js → GitHubIssuesPanel-DyuNDMgD.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CYRp1eKS.js → GitHubPRsPanel-BtAq20xF.js} +1 -1
- package/web-app/dist/assets/{HomePage-CVJcI5pQ.js → HomePage-BuVFOrO8.js} +1 -1
- package/web-app/dist/assets/{LoginPage-BBJNV9Qb.js → LoginPage-CuhhrxGb.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CIi9P8jK.js → MagicPage-D2KZPX3F.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-Bm34ipiy.js → MetricsPage-DFEYJl9z.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-DpTbcC2P.js → NotFoundPage-CTnnaWlL.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CY2SMGx9.js → ProjectPage-uJFaoM8-.js} +55 -55
- package/web-app/dist/assets/{ProjectsPage-DSrXUUKJ.js → ProjectsPage-8lKAOBIW.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-jvDyOqYi.js → SettingsPage-D1BBd5qy.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-C8tFWKau.js → ShowcasePage-CmQ0n8iO.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-DH4FTH3K.js → SystemSettingsPage-CSHaVyuD.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-C1fQYauF.js → TeamsPage-BS0TtVUa.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-CSTFt-4P.js → TemplatesPage-C2zPnnf2.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Cu4NsyN6.js → TerminalOutput-Dle1klqq.js} +1 -1
- package/web-app/dist/assets/{activity-CtjsDzmh.js → activity-DQp5U9f9.js} +1 -1
- package/web-app/dist/assets/{bell-C5es1xAs.js → bell-BqkTmgL7.js} +1 -1
- package/web-app/dist/assets/{bot-DE04U5Lc.js → bot-DILhaIGH.js} +1 -1
- package/web-app/dist/assets/{check-DSKKA9EU.js → check-BC9x7oPm.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DJ13W5AR.js → chevron-left--yqvAfjx.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CS0BfqAd.js → circle-alert-DOH6PcgZ.js} +1 -1
- package/web-app/dist/assets/{clock-D1Y5nruk.js → clock-DKSDFsE6.js} +1 -1
- package/web-app/dist/assets/{cloud-nU6LBoZL.js → cloud-DRBU3JYs.js} +1 -1
- package/web-app/dist/assets/{code-xml-BZfvIdFo.js → code-xml-D2LqszR9.js} +1 -1
- package/web-app/dist/assets/{copy-CK_2O1Xv.js → copy-BKW4U4Qr.js} +1 -1
- package/web-app/dist/assets/{database-DJiwznAm.js → database-D7ceedlh.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-pO20A3QY.js → dollar-sign-D0A47egY.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Besy6vek.js → file-code-corner-CH_Qbtjo.js} +1 -1
- package/web-app/dist/assets/{file-plus-DeaswxYY.js → file-plus-CUNMjHk9.js} +1 -1
- package/web-app/dist/assets/{folder-open-cr1UvQLI.js → folder-open-CQ_SVq94.js} +1 -1
- package/web-app/dist/assets/{git-commit-horizontal-BLxnfbSn.js → git-commit-horizontal-DQUBe0oD.js} +1 -1
- package/web-app/dist/assets/{globe-DrhG1NfX.js → globe-gZnaGg8N.js} +1 -1
- package/web-app/dist/assets/{hammer-CeZtPBfE.js → hammer-BhJC5DMK.js} +1 -1
- package/web-app/dist/assets/{index-CLvjO22Z.js → index-BMmYYws0.js} +2 -2
- package/web-app/dist/assets/{layers-fldADzyE.js → layers-glXeoUbJ.js} +1 -1
- package/web-app/dist/assets/{lightbulb-CznOfsPb.js → lightbulb-gXn8H0Lf.js} +1 -1
- package/web-app/dist/assets/{loader-circle---lPB1Fs.js → loader-circle-Do2ke8xm.js} +1 -1
- package/web-app/dist/assets/{lock-D5jVF-tO.js → lock-Bgplb1_n.js} +1 -1
- package/web-app/dist/assets/{mail-D7HkLnlW.js → mail-j4Lo8hfB.js} +1 -1
- package/web-app/dist/assets/{minus-C-dT19Ct.js → minus-dhrzKSK5.js} +1 -1
- package/web-app/dist/assets/{package-Cm9B-cKE.js → package-CInWQQ97.js} +1 -1
- package/web-app/dist/assets/{plus-CQwOffLD.js → plus-BpYvoLlZ.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CvWJZLbN.js → refresh-cw-F6koqEgF.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-C2Q1LYgM.js → rotate-ccw-CQ2W5S8f.js} +1 -1
- package/web-app/dist/assets/{save-BP0MvpoQ.js → save-DMCYGm06.js} +1 -1
- package/web-app/dist/assets/{server-4i4VZXix.js → server-Bj8EX4Z1.js} +1 -1
- package/web-app/dist/assets/{shield-alert-D2_bgb7j.js → shield-alert-CiYuulCU.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-vACbzeXj.js → thumbs-up-BqEh1puP.js} +1 -1
- package/web-app/dist/assets/{trash-2-EYjdESLs.js → trash-2-CCwouZJJ.js} +1 -1
- package/web-app/dist/assets/{trending-down-BWBdcvE3.js → trending-down-Cx1vhE4o.js} +1 -1
- package/web-app/dist/assets/{trending-up-Bix6V_-Y.js → trending-up-BUhVYJ2T.js} +1 -1
- package/web-app/dist/assets/{upload-B7hh6ERF.js → upload-D5IsZeFM.js} +1 -1
- package/web-app/dist/assets/{usePolling-b_e-k-IN.js → usePolling-CJbfhFcH.js} +1 -1
- package/web-app/dist/assets/{user-pmjZKWEg.js → user-BRLhezsD.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/web-app/requirements.txt +1 -0
- package/web-app/server.py +160 -31
package/autonomy/run.sh
CHANGED
|
@@ -967,6 +967,36 @@ _loki_provider_pipeline_exit_code() {
|
|
|
967
967
|
fi
|
|
968
968
|
}
|
|
969
969
|
|
|
970
|
+
# Invoke through a provider's argv-builder seam while preserving the main
|
|
971
|
+
# loop's deadline, fan-out logging, and provider-stage exit status.
|
|
972
|
+
_loki_invoke_argv_provider() {
|
|
973
|
+
local tier="${1:-development}"
|
|
974
|
+
local prompt="${2:-}"
|
|
975
|
+
local log_file="${3:-}"
|
|
976
|
+
local agent_log="${4:-}"
|
|
977
|
+
local iter_output="${5:-}"
|
|
978
|
+
local -a _loki_argv_pipe_status=()
|
|
979
|
+
|
|
980
|
+
if ! type provider_invoke_argv >/dev/null 2>&1; then
|
|
981
|
+
log_error "Provider ${PROVIDER_NAME:-unknown} does not implement provider_invoke_argv"
|
|
982
|
+
return 125
|
|
983
|
+
fi
|
|
984
|
+
if ! provider_invoke_argv "$tier" "$prompt" \
|
|
985
|
+
|| [ "${#_LOKI_INVOKE_ARGV[@]}" -eq 0 ]; then
|
|
986
|
+
log_error "Provider ${PROVIDER_NAME:-unknown} failed to build invocation argv"
|
|
987
|
+
return 125
|
|
988
|
+
fi
|
|
989
|
+
|
|
990
|
+
LOKI_DEADLINE_IDLE_TIMEOUT="${LOKI_PROVIDER_IDLE_TIMEOUT:-0}" \
|
|
991
|
+
_loki_with_deadline "${LOKI_PROVIDER_CALL_TIMEOUT:-0}" \
|
|
992
|
+
"${_LOKI_INVOKE_ARGV[@]}" 2>&1 \
|
|
993
|
+
| tee -a "$log_file" "$agent_log" "$iter_output"
|
|
994
|
+
_loki_argv_pipe_status=("${PIPESTATUS[@]}")
|
|
995
|
+
return "$(_loki_provider_pipeline_exit_code \
|
|
996
|
+
"${_loki_argv_pipe_status[0]:-125}" \
|
|
997
|
+
"${_loki_argv_pipe_status[1]:-125}" 0)"
|
|
998
|
+
}
|
|
999
|
+
|
|
970
1000
|
_LOKI_DEPENDENCY_SETUP_LIB="${SCRIPT_DIR}/lib/dependency-setup.sh"
|
|
971
1001
|
if [ -r "$_LOKI_DEPENDENCY_SETUP_LIB" ]; then
|
|
972
1002
|
# shellcheck source=lib/dependency-setup.sh
|
|
@@ -1519,20 +1549,20 @@ PYREG
|
|
|
1519
1549
|
# if any other project is still running (KEEP) it is left up. NEVER uses a
|
|
1520
1550
|
# blanket pkill and NEVER touches another folder's pids. Best-effort and
|
|
1521
1551
|
# failure-swallowed: teardown bookkeeping must never block a clean exit.
|
|
1522
|
-
# Is this pid
|
|
1523
|
-
#
|
|
1524
|
-
#
|
|
1525
|
-
#
|
|
1552
|
+
# Is this pid positively identified as our dashboard, rather than a recycled
|
|
1553
|
+
# number now naming something else? Ownership-sensitive cleanup must fail closed:
|
|
1554
|
+
# an absent `ps` result is not evidence, and a generic python/loki substring is
|
|
1555
|
+
# not specific enough to authorize a signal.
|
|
1526
1556
|
_loki_pid_looks_like_dashboard() {
|
|
1527
1557
|
local _p="$1" _cmd
|
|
1528
1558
|
case "$_p" in ''|*[!0-9]*) return 1 ;; esac
|
|
1529
1559
|
[ "$_p" -gt 1 ] 2>/dev/null || return 1 # never signal pid 0/1
|
|
1530
1560
|
_cmd="$(ps -o command= -p "$_p" 2>/dev/null)"
|
|
1531
|
-
[ -n "$_cmd" ] || return
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1561
|
+
[ -n "$_cmd" ] || return 1
|
|
1562
|
+
# run.sh and `loki dashboard start` both launch this exact module form.
|
|
1563
|
+
# Anchor the interpreter and module tokens so an unrelated command merely
|
|
1564
|
+
# mentioning "dashboard.server" in an argument cannot forge identity.
|
|
1565
|
+
[[ "$_cmd" =~ ^[[:space:]]*([^[:space:]]*/)?[Pp]ython([0-9]+([.][0-9]+)*)?[[:space:]]+-m[[:space:]]+dashboard[.]server([[:space:]]|$) ]]
|
|
1536
1566
|
}
|
|
1537
1567
|
|
|
1538
1568
|
loki_mark_project_stopped_and_maybe_kill_shared_dashboard() {
|
|
@@ -1591,9 +1621,10 @@ PYCHECK
|
|
|
1591
1621
|
#
|
|
1592
1622
|
# kill -0 is NOT sufficient: a recycled pid is alive by definition,
|
|
1593
1623
|
# which is exactly the insufficiency the loki.pgid self-check had.
|
|
1594
|
-
#
|
|
1595
|
-
#
|
|
1596
|
-
#
|
|
1624
|
+
# This guard fails CLOSED: if `ps` says nothing, or says something
|
|
1625
|
+
# that is not the dashboard's own command, we do not signal. The
|
|
1626
|
+
# cost of failing closed is a leaked dashboard the next explicit
|
|
1627
|
+
# stop reaps; the cost of failing open was signalling a stranger.
|
|
1597
1628
|
if [ -n "$_shared_pid" ] && _loki_pid_looks_like_dashboard "$_shared_pid"; then
|
|
1598
1629
|
kill "$_shared_pid" 2>/dev/null || true
|
|
1599
1630
|
sleep 0.5
|
|
@@ -1601,27 +1632,11 @@ PYCHECK
|
|
|
1601
1632
|
fi
|
|
1602
1633
|
rm -f "$_shared_pidfile" 2>/dev/null || true
|
|
1603
1634
|
fi
|
|
1604
|
-
#
|
|
1605
|
-
#
|
|
1606
|
-
#
|
|
1607
|
-
#
|
|
1608
|
-
#
|
|
1609
|
-
# A healthy server is reusable by every project, so probe /api/status first
|
|
1610
|
-
# and only reclaim the port when nothing is answering (a genuinely stale
|
|
1611
|
-
# listener). Opt out of the probe with LOKI_DASHBOARD_FORCE_RECLAIM=1.
|
|
1612
|
-
if command -v lsof >/dev/null 2>&1; then
|
|
1613
|
-
local _dash_port="${DASHBOARD_PORT:-57374}"
|
|
1614
|
-
local _dash_alive=""
|
|
1615
|
-
if [ "${LOKI_DASHBOARD_FORCE_RECLAIM:-}" != "1" ] && command -v curl >/dev/null 2>&1; then
|
|
1616
|
-
_dash_alive=$(curl -s -o /dev/null -w '%{http_code}' --max-time 1 \
|
|
1617
|
-
"http://127.0.0.1:${_dash_port}/api/status" 2>/dev/null || true)
|
|
1618
|
-
fi
|
|
1619
|
-
if [ "$_dash_alive" = "200" ]; then
|
|
1620
|
-
log_info "Reusing the healthy dashboard already serving on port ${_dash_port} (not reclaiming)."
|
|
1621
|
-
else
|
|
1622
|
-
lsof -ti:"${_dash_port}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
|
|
1623
|
-
fi
|
|
1624
|
-
fi
|
|
1635
|
+
# A port number and an HTTP response are not ownership evidence. In
|
|
1636
|
+
# particular, a 404 can be an unrelated healthy service. The shared PID
|
|
1637
|
+
# file plus positive command identity above is the only authority to
|
|
1638
|
+
# signal a process; absent or forged ownership leaves every port holder
|
|
1639
|
+
# untouched.
|
|
1625
1640
|
fi
|
|
1626
1641
|
}
|
|
1627
1642
|
# Register as running now. We deliberately do NOT install an EXIT trap to
|
|
@@ -3207,7 +3222,7 @@ validate_api_keys() {
|
|
|
3207
3222
|
# "loggedin" or "unknown" -> proceed (fail open on uncertainty).
|
|
3208
3223
|
fi
|
|
3209
3224
|
|
|
3210
|
-
# CLI tools (claude, codex, cline, aider) use their own login sessions.
|
|
3225
|
+
# CLI tools (claude, codex, cline, aider, opencode) use their own login sessions.
|
|
3211
3226
|
# Only require API keys inside Docker/K8s where CLI login isn't available.
|
|
3212
3227
|
if [[ ! -f "/.dockerenv" ]] && [[ -z "${KUBERNETES_SERVICE_HOST:-}" ]]; then
|
|
3213
3228
|
return 0
|
|
@@ -13425,6 +13440,43 @@ _loki_requirements_contract_emit() {
|
|
|
13425
13440
|
"${LOKI_REVIEW_REQUIREMENTS_IDENTITY:-}"
|
|
13426
13441
|
}
|
|
13427
13442
|
|
|
13443
|
+
# A reviewer dispatch can die BEFORE the provider is ever invoked -- the
|
|
13444
|
+
# prompt rematerialization or the shard-identity lookup below both exit 125
|
|
13445
|
+
# while LOKI_REVIEW_STDERR_FILE is still unset and no timing sidecar exists
|
|
13446
|
+
# yet. That left a lost shard structurally invisible: the logical aggregation
|
|
13447
|
+
# (REQUIREMENTS_LOGICAL_TIMING) substituted an anonymous placeholder and the
|
|
13448
|
+
# only symptom was a bare call count that could not name WHICH shard vanished.
|
|
13449
|
+
# Emit the same sidecar the completed path emits, plus one bounded stderr
|
|
13450
|
+
# line, so the existing aggregation carries a shard-identifying cause.
|
|
13451
|
+
# ponytail: fixed-format single line, no truncation helper needed.
|
|
13452
|
+
_dispatch_reviewer_prestub_failure() {
|
|
13453
|
+
local review_output="$1" stderr_output="$2" stage="$3" shard_index="${4:-0}"
|
|
13454
|
+
printf 'loki-review-prestub-failure stage=%s shard_index=%s reviewer=%s\n' \
|
|
13455
|
+
"$stage" "$shard_index" "${5:-unknown}" >> "$stderr_output" 2>/dev/null || true
|
|
13456
|
+
chmod 600 "$stderr_output" 2>/dev/null || true
|
|
13457
|
+
_LOKI_RDT_PATH="${review_output%.txt}-timing.json" \
|
|
13458
|
+
_LOKI_RDT_STAGE="$stage" _LOKI_RDT_SHARD="$shard_index" \
|
|
13459
|
+
_LOKI_RDT_STDERR="$stderr_output" \
|
|
13460
|
+
_LOKI_RDT_BUDGET="${LOKI_REVIEW_CALL_TIMEOUT:-0}" python3 - <<'REVIEW_PRESTUB_TIMING' 2>/dev/null || true
|
|
13461
|
+
import json
|
|
13462
|
+
import os
|
|
13463
|
+
|
|
13464
|
+
record = {
|
|
13465
|
+
"schema": "loki-review-dispatch/v1",
|
|
13466
|
+
"budget_seconds": int(os.environ.get("_LOKI_RDT_BUDGET", "0") or 0),
|
|
13467
|
+
"elapsed_ms": 0,
|
|
13468
|
+
"exit_code": 125,
|
|
13469
|
+
"outcome": "prestub_error",
|
|
13470
|
+
"deadline_scope": "pre_dispatch",
|
|
13471
|
+
"stage": os.environ.get("_LOKI_RDT_STAGE", "unknown"),
|
|
13472
|
+
"shard_index": int(os.environ.get("_LOKI_RDT_SHARD", "0") or 0),
|
|
13473
|
+
"stderr_bytes": os.path.getsize(os.environ["_LOKI_RDT_STDERR"]),
|
|
13474
|
+
}
|
|
13475
|
+
with open(os.environ["_LOKI_RDT_PATH"], "w", encoding="utf-8") as handle:
|
|
13476
|
+
json.dump(record, handle, sort_keys=True)
|
|
13477
|
+
REVIEW_PRESTUB_TIMING
|
|
13478
|
+
}
|
|
13479
|
+
|
|
13428
13480
|
# Persist one compact record per provider review. The sidecar is written by the
|
|
13429
13481
|
# same process that owns the dispatch, so parallel reviewer timings do not get
|
|
13430
13482
|
# distorted by the parent's ordered wait loop.
|
|
@@ -15373,7 +15425,13 @@ BUILD_PROMPT
|
|
|
15373
15425
|
prompt_text=$(python3 "$requirements_helper" read-bound \
|
|
15374
15426
|
"$review_prompt_file" "$prompt_sha" \
|
|
15375
15427
|
"$_review_max_prompt_bytes" "$prompt_identity" 2>/dev/null) \
|
|
15376
|
-
||
|
|
15428
|
+
|| {
|
|
15429
|
+
_dispatch_reviewer_prestub_failure \
|
|
15430
|
+
"$review_stage_file" \
|
|
15431
|
+
"$review_dir/$review_id/${reviewer_name}-stderr.log" \
|
|
15432
|
+
prompt_read_bound "$reviewer_shard_index" "$reviewer_name"
|
|
15433
|
+
exit 125
|
|
15434
|
+
}
|
|
15377
15435
|
if [ "$reviewer_logical_name" = "requirements-verifier" ]; then
|
|
15378
15436
|
local shard_label shard_identity
|
|
15379
15437
|
printf -v shard_label '%03d' "$reviewer_shard_index"
|
|
@@ -15381,7 +15439,13 @@ BUILD_PROMPT
|
|
|
15381
15439
|
_LOKI_REVIEW_SHARD_INDEX="$reviewer_shard_index" python3 -c '
|
|
15382
15440
|
import json, os
|
|
15383
15441
|
print(json.loads(os.environ["_LOKI_REVIEW_SHARD_IDENTITIES"])[int(os.environ["_LOKI_REVIEW_SHARD_INDEX"]) - 1])
|
|
15384
|
-
') ||
|
|
15442
|
+
') || {
|
|
15443
|
+
_dispatch_reviewer_prestub_failure \
|
|
15444
|
+
"$review_stage_file" \
|
|
15445
|
+
"$review_dir/$review_id/${reviewer_name}-stderr.log" \
|
|
15446
|
+
shard_identity "$reviewer_shard_index" "$reviewer_name"
|
|
15447
|
+
exit 125
|
|
15448
|
+
}
|
|
15385
15449
|
LOKI_REVIEW_REQUIREMENTS_HELPER="$requirements_helper" \
|
|
15386
15450
|
LOKI_REVIEW_REQUIREMENTS_SOURCE="$requirements_source" \
|
|
15387
15451
|
LOKI_REVIEW_REQUIREMENTS_SNAPSHOT="$requirements_file" \
|
|
@@ -16557,9 +16621,16 @@ start_dashboard() {
|
|
|
16557
16621
|
# Check if it's our own dashboard
|
|
16558
16622
|
local existing_pid=$(lsof -ti :$DASHBOARD_PORT 2>/dev/null | head -1)
|
|
16559
16623
|
if [ -n "$existing_pid" ]; then
|
|
16560
|
-
# Only kill
|
|
16624
|
+
# Only kill a process positively identified as OUR dashboard.
|
|
16625
|
+
# `-o comm=` yields just the executable name ("python3"), so the old
|
|
16626
|
+
# *python*/*uvicorn* test matched ANY python process that happened to
|
|
16627
|
+
# hold this port -- an unrelated http.server answering 404 was killed
|
|
16628
|
+
# as a "stuck dashboard". Reuse the same full-command-line guard the
|
|
16629
|
+
# teardown path uses: it fails closed on an unreadable or foreign
|
|
16630
|
+
# command, so a stranger on this port is stepped over (port++) rather
|
|
16631
|
+
# than signalled.
|
|
16561
16632
|
local proc_cmd=$(ps -p "$existing_pid" -o comm= 2>/dev/null || true)
|
|
16562
|
-
if
|
|
16633
|
+
if _loki_pid_looks_like_dashboard "$existing_pid"; then
|
|
16563
16634
|
# Never kill a HEALTHY dashboard already serving here: it is almost
|
|
16564
16635
|
# always the user's own live dashboard (open in their browser), and
|
|
16565
16636
|
# killing it on `loki start` drops their session mid-use. A healthy
|
|
@@ -23266,6 +23337,16 @@ if __name__ == "__main__":
|
|
|
23266
23337
|
} && exit_code=0 || exit_code=$?
|
|
23267
23338
|
;;
|
|
23268
23339
|
|
|
23340
|
+
opencode)
|
|
23341
|
+
# Keep argv construction provider-owned: its prompt is
|
|
23342
|
+
# positional, --auto is mandatory, and the model is provider
|
|
23343
|
+
# scoped. The wrapper preserves the main-loop deadline/tee
|
|
23344
|
+
# contract and returns the provider stage's nonzero status.
|
|
23345
|
+
_loki_invoke_argv_provider "$CURRENT_TIER" "$prompt" \
|
|
23346
|
+
"$log_file" "$agent_log" "$iter_output"
|
|
23347
|
+
exit_code=$?
|
|
23348
|
+
;;
|
|
23349
|
+
|
|
23269
23350
|
*)
|
|
23270
23351
|
log_error "Unknown provider: ${PROVIDER_NAME:-unknown}"
|
|
23271
23352
|
local exit_code=1
|
|
@@ -25745,7 +25826,7 @@ main() {
|
|
|
25745
25826
|
fi
|
|
25746
25827
|
shift 2
|
|
25747
25828
|
else
|
|
25748
|
-
log_error "--provider requires a value (claude, codex, cline, aider)"
|
|
25829
|
+
log_error "--provider requires a value (claude, codex, cline, aider, opencode)"
|
|
25749
25830
|
exit 1
|
|
25750
25831
|
fi
|
|
25751
25832
|
;;
|
package/autonomy/verify.sh
CHANGED
|
@@ -368,15 +368,28 @@ _verify_zero_tests_executed() {
|
|
|
368
368
|
[ -n "$_zt_f" ] || continue
|
|
369
369
|
_zt_bases="${_zt_bases}${_zt_f}"$'\n'"$(basename "$_zt_f")"$'\n'
|
|
370
370
|
done
|
|
371
|
-
local _zt_real=0 _zt_line _zt_label
|
|
371
|
+
local _zt_real=0 _zt_line _zt_label _zt_matches
|
|
372
|
+
_zt_matches=$(printf '%s\n' "$_zt_out" | grep -E '^(ok|not ok) [0-9]+ - ' 2>/dev/null || true)
|
|
372
373
|
while IFS= read -r _zt_line; do
|
|
374
|
+
[ -n "$_zt_line" ] || continue
|
|
373
375
|
_zt_label="${_zt_line#* - }"
|
|
374
376
|
case "$_zt_bases" in
|
|
375
377
|
*$'\n'"$_zt_label"$'\n'*) continue ;;
|
|
376
378
|
esac
|
|
377
379
|
_zt_real=$((_zt_real + 1))
|
|
378
|
-
done
|
|
379
|
-
|
|
380
|
+
done << ZT_MATCHES_EOF
|
|
381
|
+
$_zt_matches
|
|
382
|
+
ZT_MATCHES_EOF
|
|
383
|
+
[ "$_zt_real" -gt 0 ] && return 1
|
|
384
|
+
|
|
385
|
+
# Node 26 defaults to the human-readable spec reporter under npm
|
|
386
|
+
# scripts. It has no TAP ok-lines, so an absent match is not proof
|
|
387
|
+
# that zero tests ran. Only classify zero tests when the output is
|
|
388
|
+
# positively identifiable as TAP; unknown/custom reporter shapes
|
|
389
|
+
# remain a passing run instead of becoming a false CONCERN.
|
|
390
|
+
case "$_zt_out" in
|
|
391
|
+
*"TAP version"*|*$'\n# tests '*|"# tests "*) return 0 ;;
|
|
392
|
+
esac
|
|
380
393
|
return 1
|
|
381
394
|
;;
|
|
382
395
|
jest)
|
package/bin/loki
CHANGED
|
@@ -188,7 +188,7 @@ fi
|
|
|
188
188
|
# error regardless of which subcommand they invoke.
|
|
189
189
|
if [ "${LOKI_PROVIDER:-}" = "gemini" ]; then
|
|
190
190
|
echo "Error: Provider 'gemini' is deprecated as of v7.5.18 and has been removed." >&2
|
|
191
|
-
echo "Active providers: claude, codex, cline, aider" >&2
|
|
191
|
+
echo "Active providers: claude, codex, cline, aider, opencode" >&2
|
|
192
192
|
echo "Unset LOKI_PROVIDER or use: LOKI_PROVIDER=claude" >&2
|
|
193
193
|
exit 1
|
|
194
194
|
fi
|
|
@@ -273,8 +273,19 @@ _loki_start_needs_bash() {
|
|
|
273
273
|
# $@ = the start args (already know $1 = start). Return 0 (divert to bash) if
|
|
274
274
|
# any bash-only orchestration flag or an issue-ref spec is present.
|
|
275
275
|
local a
|
|
276
|
+
local explicit_provider=""
|
|
277
|
+
local provider_value_next=0
|
|
276
278
|
for a in "$@"; do
|
|
279
|
+
if [ "$provider_value_next" = "1" ]; then
|
|
280
|
+
explicit_provider="$a"
|
|
281
|
+
provider_value_next=0
|
|
282
|
+
continue
|
|
283
|
+
fi
|
|
277
284
|
case "$a" in
|
|
285
|
+
--provider)
|
|
286
|
+
provider_value_next=1 ;;
|
|
287
|
+
--provider=*)
|
|
288
|
+
explicit_provider="${a#--provider=}" ;;
|
|
278
289
|
--parallel|--bg|--background|--github|--api|--sandbox \
|
|
279
290
|
|--fast|--repo-fast|--full|--repo-full \
|
|
280
291
|
|--bmad-project|--openspec \
|
|
@@ -296,6 +307,19 @@ _loki_start_needs_bash() {
|
|
|
296
307
|
esac
|
|
297
308
|
done
|
|
298
309
|
unset _loki_iso_next
|
|
310
|
+
|
|
311
|
+
# The Bun RARV loop has not ported the opencode invoker yet. Keep the
|
|
312
|
+
# released, autonomous shell adapter reachable when SDK mode is enabled
|
|
313
|
+
# instead of accepting the selection and then failing after intake. CLI
|
|
314
|
+
# selection wins over env, which wins over the project-level saved choice.
|
|
315
|
+
local effective_provider="$explicit_provider"
|
|
316
|
+
if [ -z "$effective_provider" ] && [ -n "${LOKI_PROVIDER:-}" ]; then
|
|
317
|
+
effective_provider="$LOKI_PROVIDER"
|
|
318
|
+
fi
|
|
319
|
+
if [ -z "$effective_provider" ] && [ -f "${PWD}/.loki/state/provider" ]; then
|
|
320
|
+
effective_provider="$(sed -n '1p' "${PWD}/.loki/state/provider" 2>/dev/null || true)"
|
|
321
|
+
fi
|
|
322
|
+
[ "$effective_provider" = "opencode" ] && return 0
|
|
299
323
|
return 1
|
|
300
324
|
}
|
|
301
325
|
if [ "${1:-}" = "start" ] && { [ "${LOKI_SDK_LOOP:-}" = "1" ] || [ "${LOKI_SDK_LOOP:-}" = "true" ]; }; then
|
package/dashboard/__init__.py
CHANGED
package/dashboard/control.py
CHANGED
|
@@ -20,10 +20,11 @@ import tempfile
|
|
|
20
20
|
from datetime import datetime, timezone
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
from typing import Optional
|
|
23
|
+
from urllib.parse import urlsplit
|
|
23
24
|
|
|
24
|
-
from fastapi import Depends, FastAPI, HTTPException
|
|
25
|
+
from fastapi import Depends, FastAPI, HTTPException, Request
|
|
25
26
|
from fastapi.middleware.cors import CORSMiddleware
|
|
26
|
-
from fastapi.responses import StreamingResponse
|
|
27
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
27
28
|
from pydantic import BaseModel
|
|
28
29
|
|
|
29
30
|
# Auth gating for the standalone control app.
|
|
@@ -56,6 +57,63 @@ except Exception: # pragma: no cover - defensive fallback for non-package runs
|
|
|
56
57
|
|
|
57
58
|
_CONTROL_DEP = _require_control_scope()
|
|
58
59
|
|
|
60
|
+
|
|
61
|
+
def _normalized_http_origin(value: str) -> tuple[str, str, int] | None:
|
|
62
|
+
"""Return a comparable browser origin, or ``None`` when malformed.
|
|
63
|
+
|
|
64
|
+
Origin is scheme + host + effective port. Paths, credentials, queries and
|
|
65
|
+
fragments are never valid in an Origin header. Normalising default ports
|
|
66
|
+
makes ``https://host`` and ``https://host:443`` equivalent without doing
|
|
67
|
+
loose prefix/suffix matching on attacker-controlled text.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
parsed = urlsplit(value.strip())
|
|
71
|
+
if (
|
|
72
|
+
parsed.scheme.lower() not in ("http", "https")
|
|
73
|
+
or not parsed.hostname
|
|
74
|
+
or parsed.username is not None
|
|
75
|
+
or parsed.password is not None
|
|
76
|
+
or parsed.path not in ("", "/")
|
|
77
|
+
or parsed.query
|
|
78
|
+
or parsed.fragment
|
|
79
|
+
):
|
|
80
|
+
return None
|
|
81
|
+
port = parsed.port
|
|
82
|
+
except (TypeError, ValueError):
|
|
83
|
+
return None
|
|
84
|
+
if port is None:
|
|
85
|
+
port = 443 if parsed.scheme.lower() == "https" else 80
|
|
86
|
+
return parsed.scheme.lower(), parsed.hostname.lower(), port
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def browser_mutation_origin_allowed(
|
|
90
|
+
request: Request, allowed_origins: list[str]
|
|
91
|
+
) -> bool:
|
|
92
|
+
"""Allow non-browser clients, same-origin pages and explicit CORS origins.
|
|
93
|
+
|
|
94
|
+
CORS controls whether a browser may *read* a response; it does not stop a
|
|
95
|
+
simple cross-origin POST from being sent. State-changing routes therefore
|
|
96
|
+
need their own check. Browsers always attach Origin to cross-origin fetches,
|
|
97
|
+
while CLI/SDK/local health clients commonly omit it, so absence preserves
|
|
98
|
+
the existing local-first API. A present malformed or unrelated origin is
|
|
99
|
+
refused before route code can touch disk or processes.
|
|
100
|
+
"""
|
|
101
|
+
supplied = request.headers.get("origin")
|
|
102
|
+
if supplied is None:
|
|
103
|
+
return True
|
|
104
|
+
if "*" in allowed_origins:
|
|
105
|
+
return True
|
|
106
|
+
supplied_origin = _normalized_http_origin(supplied)
|
|
107
|
+
if supplied_origin is None:
|
|
108
|
+
return False
|
|
109
|
+
request_origin = _normalized_http_origin(str(request.base_url))
|
|
110
|
+
if supplied_origin == request_origin:
|
|
111
|
+
return True
|
|
112
|
+
return any(
|
|
113
|
+
supplied_origin == _normalized_http_origin(candidate)
|
|
114
|
+
for candidate in allowed_origins
|
|
115
|
+
)
|
|
116
|
+
|
|
59
117
|
# Configuration
|
|
60
118
|
LOKI_DIR = Path(os.environ.get("LOKI_DIR", ".loki"))
|
|
61
119
|
STATE_DIR = LOKI_DIR / "state"
|
|
@@ -202,16 +260,42 @@ app = FastAPI(
|
|
|
202
260
|
# CORS middleware for dashboard frontend - restricted to localhost by default.
|
|
203
261
|
# Set LOKI_DASHBOARD_CORS to override (comma-separated origins).
|
|
204
262
|
_cors_default = "http://localhost:57374,http://127.0.0.1:57374"
|
|
205
|
-
|
|
263
|
+
_cors_raw = os.environ.get("LOKI_DASHBOARD_CORS", _cors_default)
|
|
264
|
+
_cors_origins = [o.strip() for o in _cors_raw.split(",") if o.strip()]
|
|
265
|
+
if "*" in _cors_origins or _cors_raw.strip() == "*":
|
|
266
|
+
if os.environ.get("LOKI_ENV") == "production":
|
|
267
|
+
raise RuntimeError(
|
|
268
|
+
"Wildcard CORS ('*') is not allowed in production. "
|
|
269
|
+
"Set LOKI_DASHBOARD_CORS to a specific origin list, "
|
|
270
|
+
"or set LOKI_ENV != production."
|
|
271
|
+
)
|
|
272
|
+
import logging
|
|
273
|
+
|
|
274
|
+
logging.getLogger(__name__).warning(
|
|
275
|
+
"LOKI_DASHBOARD_CORS is set to '*' -- all origins are allowed. "
|
|
276
|
+
"This is insecure for production deployments."
|
|
277
|
+
)
|
|
206
278
|
app.add_middleware(
|
|
207
279
|
CORSMiddleware,
|
|
208
|
-
allow_origins=
|
|
280
|
+
allow_origins=_cors_origins,
|
|
209
281
|
allow_credentials=True,
|
|
210
282
|
allow_methods=["*"],
|
|
211
283
|
allow_headers=["*"],
|
|
212
284
|
)
|
|
213
285
|
|
|
214
286
|
|
|
287
|
+
@app.middleware("http")
|
|
288
|
+
async def browser_mutation_boundary(request: Request, call_next):
|
|
289
|
+
"""Refuse cross-origin browser mutations before control side effects."""
|
|
290
|
+
if request.method in ("POST", "PUT", "PATCH", "DELETE"):
|
|
291
|
+
if not browser_mutation_origin_allowed(request, _cors_origins):
|
|
292
|
+
return JSONResponse(
|
|
293
|
+
status_code=403,
|
|
294
|
+
content={"detail": "cross-origin mutation refused"},
|
|
295
|
+
)
|
|
296
|
+
return await call_next(request)
|
|
297
|
+
|
|
298
|
+
|
|
215
299
|
# Request/Response models
|
|
216
300
|
class StartRequest(BaseModel):
|
|
217
301
|
"""Request body for starting a Loki Mode session."""
|
package/dashboard/server.py
CHANGED
|
@@ -58,7 +58,12 @@ from . import audit
|
|
|
58
58
|
from . import app_secrets as secrets_mod
|
|
59
59
|
from . import telemetry as _telemetry
|
|
60
60
|
from . import build_supervisor as _build_execution
|
|
61
|
-
from .control import
|
|
61
|
+
from .control import (
|
|
62
|
+
atomic_write_json,
|
|
63
|
+
browser_mutation_origin_allowed,
|
|
64
|
+
find_skill_dir,
|
|
65
|
+
is_process_running,
|
|
66
|
+
)
|
|
62
67
|
from .activity_logger import get_activity_logger
|
|
63
68
|
from .api_v2 import (
|
|
64
69
|
TenantContext,
|
|
@@ -187,53 +192,46 @@ def _rate_key(base: str, request: Optional[Request]) -> str:
|
|
|
187
192
|
logger = logging.getLogger(__name__)
|
|
188
193
|
|
|
189
194
|
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
195
|
+
# Public API metadata needed before a caller can authenticate or before the UI
|
|
196
|
+
# can choose a provider. Everything else under an API namespace is
|
|
197
|
+
# state-bearing unless it is explicitly admitted here.
|
|
193
198
|
#
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
199
|
+
# This is deliberately an ALLOWLIST, not the old list of sensitive prefixes.
|
|
200
|
+
# That list silently started every new read family public. Measured from a
|
|
201
|
+
# routable remote address with auth off, routes added after the list --
|
|
202
|
+
# /api/context, /api/notifications, /api/agents, /api/usage,
|
|
203
|
+
# /api/prd-observations and every /api/v2/* read -- all reached their handlers
|
|
204
|
+
# and returned project, tenant or audit state.
|
|
197
205
|
#
|
|
198
|
-
# /health
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"/api/
|
|
204
|
-
"/api/
|
|
205
|
-
"/api/
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"
|
|
218
|
-
"/
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"/api/failures",
|
|
228
|
-
"/api/prompt",
|
|
229
|
-
"/api/quality",
|
|
230
|
-
"/api/migration",
|
|
231
|
-
"/api/managed",
|
|
232
|
-
"/api/app-runner",
|
|
233
|
-
"/api/playwright",
|
|
234
|
-
"/api/checklist",
|
|
235
|
-
"/api/control",
|
|
236
|
-
)
|
|
206
|
+
# /health, /metrics, /.well-known/*, docs and static/UI routes are outside the
|
|
207
|
+
# API namespaces and remain public. The three entries below contain only
|
|
208
|
+
# capability/auth bootstrap metadata and are already pinned as public by the
|
|
209
|
+
# dashboard auth inventory tests.
|
|
210
|
+
_PUBLIC_API_GET_PATHS = frozenset({
|
|
211
|
+
"/api/auth/info",
|
|
212
|
+
"/api/enterprise/status",
|
|
213
|
+
"/api/providers/models",
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _is_state_bearing_get(path: str) -> bool:
|
|
218
|
+
"""Classify dashboard reads at the namespace boundary.
|
|
219
|
+
|
|
220
|
+
A future GET under /api or the mounted Purple Lab's /lab/api namespace is
|
|
221
|
+
private by default. Exact public metadata is admitted above; probes,
|
|
222
|
+
discovery documents and UI/static files live outside these namespaces.
|
|
223
|
+
Normalize one trailing slash so FastAPI's redirect spelling cannot turn a
|
|
224
|
+
public metadata request into a remote-only failure.
|
|
225
|
+
"""
|
|
226
|
+
normalized = path.rstrip("/") or "/"
|
|
227
|
+
if normalized in _PUBLIC_API_GET_PATHS:
|
|
228
|
+
return False
|
|
229
|
+
return (
|
|
230
|
+
normalized == "/api"
|
|
231
|
+
or normalized.startswith("/api/")
|
|
232
|
+
or normalized == "/lab/api"
|
|
233
|
+
or normalized.startswith("/lab/api/")
|
|
234
|
+
)
|
|
237
235
|
|
|
238
236
|
|
|
239
237
|
def _trusted_proxies() -> frozenset:
|
|
@@ -1263,9 +1261,13 @@ async def dashboard_control_boundary(request: Request, call_next):
|
|
|
1263
1261
|
# /health and /metrics are intentionally NOT in the sensitive list, so a
|
|
1264
1262
|
# container health probe and a Prometheus scrape keep working unconfigured.
|
|
1265
1263
|
gated = request.method in ("POST", "PUT", "PATCH", "DELETE")
|
|
1264
|
+
if gated and not browser_mutation_origin_allowed(request, _cors_origins):
|
|
1265
|
+
return JSONResponse(
|
|
1266
|
+
status_code=403,
|
|
1267
|
+
content={"detail": "cross-origin mutation refused"},
|
|
1268
|
+
)
|
|
1266
1269
|
if not gated:
|
|
1267
|
-
|
|
1268
|
-
gated = any(path.startswith(pfx) for pfx in _SENSITIVE_READ_PREFIXES)
|
|
1270
|
+
gated = _is_state_bearing_get(request.url.path)
|
|
1269
1271
|
if gated:
|
|
1270
1272
|
try:
|
|
1271
1273
|
require_local_or_authenticated(request)
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v9.
|
|
5
|
+
**Version:** v9.23.1
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -343,7 +343,7 @@ The `HUMAN_INPUT.md` file has security controls:
|
|
|
343
343
|
|
|
344
344
|
## Multi-Provider Support
|
|
345
345
|
|
|
346
|
-
Loki Mode supports
|
|
346
|
+
Loki Mode supports five active providers across three capability modes, plus historical/upcoming entries. Pick by capability + cost.
|
|
347
347
|
|
|
348
348
|
### Supported Providers
|
|
349
349
|
|
package/docs/RARV-C-100X-PLAN.md
CHANGED
|
@@ -220,7 +220,15 @@ Every commit proven on BOTH routes (`bin/loki` + `LOKI_LEGACY_BASH=1 bin/loki`).
|
|
|
220
220
|
- **Post-release smoke from SHIPPED artifacts on BOTH routes**: `npm pack loki-mode@8.0.0` + `bun run bin/loki
|
|
221
221
|
version` and `LOKI_LEGACY_BASH=1 ... version`; `docker run --rm asklokesh/loki-mode:8.0.0 doctor --json`;
|
|
222
222
|
WebFetch brew formula version+sha.
|
|
223
|
-
- **Cleanup after every local-ci/validation**:
|
|
223
|
+
- **Cleanup after every local-ci/validation**: create one exact run-owned directory with `mktemp -d`
|
|
224
|
+
under the canonical `${TMPDIR:-/tmp}` root and keep all validation artifacts and recorded child
|
|
225
|
+
PIDs inside it. Use the executable operator-side sequence in
|
|
226
|
+
[`enterprise/performance.md`](enterprise/performance.md#cleanup); it does not depend on a shipped
|
|
227
|
+
cleanup helper. Stop only the exact verified child PIDs. Before removing the exact saved path,
|
|
228
|
+
require its `.loki-run-owned` marker to contain that path, require the target and marker to belong
|
|
229
|
+
to the current UID, require the target to be a canonical direct child of the temp root, refuse
|
|
230
|
+
symlinks, and refuse any target with `.git` worktree metadata. A failed guard leaves the path in
|
|
231
|
+
place for inspection.
|
|
224
232
|
|
|
225
233
|
---
|
|
226
234
|
|