loki-mode 5.40.0 → 5.41.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 +3 -3
- package/VERSION +1 -1
- package/autonomy/hooks/validate-bash.sh +12 -0
- package/autonomy/loki +186 -4
- package/autonomy/run.sh +163 -14
- package/dashboard/__init__.py +1 -1
- package/dashboard/auth.py +44 -11
- package/dashboard/control.py +101 -2
- package/dashboard/registry.py +3 -2
- package/dashboard/server.py +191 -6
- package/dashboard/static/index.html +8 -8
- package/dashboard/telemetry.py +4 -1
- package/docs/INSTALLATION.md +1 -1
- package/events/emit.sh +22 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/skills/github-integration.md +43 -11
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with zero human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v5.
|
|
6
|
+
# Loki Mode v5.41.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -258,8 +258,8 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
258
258
|
|---------|--------|-------|
|
|
259
259
|
| PRE-ACT goal drift detection | Planned | Agent-level attention check before each action; no automated enforcement yet |
|
|
260
260
|
| CONTINUITY.md working memory | Implemented (v5.35.0) | Auto-managed by run.sh, updated each iteration |
|
|
261
|
-
| GitHub
|
|
261
|
+
| GitHub integration | Implemented (v5.41.0) | Import, sync-back, PR creation, export. CLI: `loki github`, API: `/api/github/*` |
|
|
262
262
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
263
263
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
264
264
|
|
|
265
|
-
**v5.
|
|
265
|
+
**v5.41.0 | feat: GitHub sync-back, PR creation, export (fully wired) | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.41.0
|
|
@@ -10,6 +10,9 @@ CWD=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).ge
|
|
|
10
10
|
|
|
11
11
|
# Dangerous command patterns (matched anywhere in the command string)
|
|
12
12
|
# Safe paths like /tmp/ and relative paths (./) are excluded below
|
|
13
|
+
# NOTE: This is defense-in-depth, not a security boundary. Motivated attackers
|
|
14
|
+
# can bypass with advanced techniques (heredocs, printf, arbitrary string building).
|
|
15
|
+
# This hook catches common mistakes and simple bypass attempts.
|
|
13
16
|
BLOCKED_PATTERNS=(
|
|
14
17
|
"rm -rf /"
|
|
15
18
|
"rm -rf ~"
|
|
@@ -18,6 +21,15 @@ BLOCKED_PATTERNS=(
|
|
|
18
21
|
"mkfs[. ]"
|
|
19
22
|
"dd if=/dev/zero"
|
|
20
23
|
"chmod -R 777 /"
|
|
24
|
+
"eval.*base64"
|
|
25
|
+
"base64.*\|.*sh"
|
|
26
|
+
"base64.*\|.*bash"
|
|
27
|
+
"\$\(base64"
|
|
28
|
+
"eval.*\\\$\("
|
|
29
|
+
"curl.*\|.*sh"
|
|
30
|
+
"wget.*\|.*sh"
|
|
31
|
+
"curl.*\|.*bash"
|
|
32
|
+
"wget.*\|.*bash"
|
|
21
33
|
)
|
|
22
34
|
|
|
23
35
|
# Safe path patterns that override rm -rf / matches
|
package/autonomy/loki
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# loki status - Show current status
|
|
15
15
|
# loki dashboard - Open dashboard in browser
|
|
16
16
|
# loki import - Import GitHub issues
|
|
17
|
+
# loki github [cmd] - GitHub integration (sync|export|pr|status)
|
|
17
18
|
# loki help - Show this help
|
|
18
19
|
#===============================================================================
|
|
19
20
|
|
|
@@ -323,6 +324,7 @@ show_help() {
|
|
|
323
324
|
echo " notify [cmd] Send notifications (test|slack|discord|webhook|status)"
|
|
324
325
|
echo " voice [cmd] Voice input for PRD creation (status|listen|dictate|speak|start)"
|
|
325
326
|
echo " import Import GitHub issues as tasks"
|
|
327
|
+
echo " github [cmd] GitHub integration (sync|export|pr|status)"
|
|
326
328
|
echo " config [cmd] Manage configuration (show|init|edit|path)"
|
|
327
329
|
echo " completions [bash|zsh] Output shell completion scripts"
|
|
328
330
|
echo " memory [cmd] Cross-project learnings (list|show|search|stats)"
|
|
@@ -515,7 +517,7 @@ cmd_start() {
|
|
|
515
517
|
if [ -n "$prd_file" ]; then
|
|
516
518
|
args+=("$prd_file")
|
|
517
519
|
else
|
|
518
|
-
# No PRD file specified -- warn and confirm before
|
|
520
|
+
# No PRD file specified -- warn and confirm before starting
|
|
519
521
|
# Auto-confirm in CI environments or when LOKI_AUTO_CONFIRM is set
|
|
520
522
|
# LOKI_AUTO_CONFIRM takes precedence when explicitly set;
|
|
521
523
|
# fall back to CI env var only when LOKI_AUTO_CONFIRM is unset
|
|
@@ -524,10 +526,10 @@ cmd_start() {
|
|
|
524
526
|
echo -e "${YELLOW}Warning: No PRD file specified. Auto-confirming (CI mode).${NC}"
|
|
525
527
|
else
|
|
526
528
|
echo -e "${YELLOW}Warning: No PRD file specified.${NC}"
|
|
527
|
-
echo "Loki Mode will
|
|
528
|
-
echo "
|
|
529
|
+
echo "Loki Mode will analyze the existing codebase and generate"
|
|
530
|
+
echo "a PRD automatically. No requirements document needed."
|
|
529
531
|
echo ""
|
|
530
|
-
echo -e "
|
|
532
|
+
echo -e "Continue? [y/N] \c"
|
|
531
533
|
read -r confirm
|
|
532
534
|
if [[ ! "$confirm" =~ ^[Yy] ]]; then
|
|
533
535
|
echo "Aborted. Usage: loki start <path-to-prd.md>"
|
|
@@ -1832,6 +1834,183 @@ cmd_import() {
|
|
|
1832
1834
|
fi
|
|
1833
1835
|
}
|
|
1834
1836
|
|
|
1837
|
+
# GitHub integration management (v5.41.0)
|
|
1838
|
+
cmd_github() {
|
|
1839
|
+
local subcmd="${1:-help}"
|
|
1840
|
+
shift 2>/dev/null || true
|
|
1841
|
+
|
|
1842
|
+
case "$subcmd" in
|
|
1843
|
+
sync)
|
|
1844
|
+
# Sync completed tasks back to GitHub issues
|
|
1845
|
+
if [ ! -d "$LOKI_DIR" ]; then
|
|
1846
|
+
echo -e "${RED}No active Loki session found${NC}"
|
|
1847
|
+
exit 1
|
|
1848
|
+
fi
|
|
1849
|
+
|
|
1850
|
+
if ! command -v gh &>/dev/null; then
|
|
1851
|
+
echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}"
|
|
1852
|
+
exit 1
|
|
1853
|
+
fi
|
|
1854
|
+
|
|
1855
|
+
if ! gh auth status &>/dev/null; then
|
|
1856
|
+
echo -e "${RED}Error: gh CLI not authenticated. Run: gh auth login${NC}"
|
|
1857
|
+
exit 1
|
|
1858
|
+
fi
|
|
1859
|
+
|
|
1860
|
+
export LOKI_GITHUB_SYNC=true
|
|
1861
|
+
source "$RUN_SH" 2>/dev/null || true
|
|
1862
|
+
|
|
1863
|
+
echo -e "${GREEN}Syncing completed tasks to GitHub...${NC}"
|
|
1864
|
+
if type sync_github_completed_tasks &>/dev/null; then
|
|
1865
|
+
sync_github_completed_tasks
|
|
1866
|
+
echo -e "${GREEN}Sync complete.${NC}"
|
|
1867
|
+
|
|
1868
|
+
# Show what was synced
|
|
1869
|
+
if [ -f "$LOKI_DIR/github/synced.log" ]; then
|
|
1870
|
+
local count
|
|
1871
|
+
count=$(wc -l < "$LOKI_DIR/github/synced.log" | tr -d ' ')
|
|
1872
|
+
echo -e "${DIM}Total synced status updates: $count${NC}"
|
|
1873
|
+
fi
|
|
1874
|
+
else
|
|
1875
|
+
echo -e "${YELLOW}Sync function not available.${NC}"
|
|
1876
|
+
fi
|
|
1877
|
+
;;
|
|
1878
|
+
|
|
1879
|
+
export)
|
|
1880
|
+
# Export local tasks as GitHub issues
|
|
1881
|
+
if [ ! -d "$LOKI_DIR" ]; then
|
|
1882
|
+
echo -e "${RED}No active Loki session found${NC}"
|
|
1883
|
+
exit 1
|
|
1884
|
+
fi
|
|
1885
|
+
|
|
1886
|
+
if ! command -v gh &>/dev/null; then
|
|
1887
|
+
echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}"
|
|
1888
|
+
exit 1
|
|
1889
|
+
fi
|
|
1890
|
+
|
|
1891
|
+
echo -e "${GREEN}Exporting local tasks to GitHub issues...${NC}"
|
|
1892
|
+
source "$RUN_SH" 2>/dev/null || true
|
|
1893
|
+
if type export_tasks_to_github &>/dev/null; then
|
|
1894
|
+
export_tasks_to_github
|
|
1895
|
+
echo -e "${GREEN}Export complete.${NC}"
|
|
1896
|
+
else
|
|
1897
|
+
echo -e "${YELLOW}Export function not available.${NC}"
|
|
1898
|
+
fi
|
|
1899
|
+
;;
|
|
1900
|
+
|
|
1901
|
+
pr)
|
|
1902
|
+
# Create PR from completed work
|
|
1903
|
+
if ! command -v gh &>/dev/null; then
|
|
1904
|
+
echo -e "${RED}Error: gh CLI not found. Install with: brew install gh${NC}"
|
|
1905
|
+
exit 1
|
|
1906
|
+
fi
|
|
1907
|
+
|
|
1908
|
+
local feature_name="${1:-Loki Mode changes}"
|
|
1909
|
+
export LOKI_GITHUB_PR=true
|
|
1910
|
+
source "$RUN_SH" 2>/dev/null || true
|
|
1911
|
+
|
|
1912
|
+
echo -e "${GREEN}Creating pull request: $feature_name${NC}"
|
|
1913
|
+
if type create_github_pr &>/dev/null; then
|
|
1914
|
+
create_github_pr "$feature_name"
|
|
1915
|
+
else
|
|
1916
|
+
echo -e "${YELLOW}PR function not available.${NC}"
|
|
1917
|
+
fi
|
|
1918
|
+
;;
|
|
1919
|
+
|
|
1920
|
+
status)
|
|
1921
|
+
# Show GitHub integration status
|
|
1922
|
+
echo -e "${BOLD}GitHub Integration Status${NC}"
|
|
1923
|
+
echo ""
|
|
1924
|
+
|
|
1925
|
+
# gh CLI
|
|
1926
|
+
if command -v gh &>/dev/null; then
|
|
1927
|
+
echo -e " gh CLI: ${GREEN}installed$(gh --version 2>/dev/null | head -1 | sed 's/gh version /v/')${NC}"
|
|
1928
|
+
if gh auth status &>/dev/null 2>&1; then
|
|
1929
|
+
echo -e " Auth: ${GREEN}authenticated${NC}"
|
|
1930
|
+
else
|
|
1931
|
+
echo -e " Auth: ${RED}not authenticated${NC}"
|
|
1932
|
+
fi
|
|
1933
|
+
else
|
|
1934
|
+
echo -e " gh CLI: ${RED}not installed${NC}"
|
|
1935
|
+
fi
|
|
1936
|
+
|
|
1937
|
+
# Repo detection
|
|
1938
|
+
local repo=""
|
|
1939
|
+
repo=$(git remote get-url origin 2>/dev/null | sed 's|.*github.com[:/]||;s|\.git$||' || echo "")
|
|
1940
|
+
if [ -n "$repo" ]; then
|
|
1941
|
+
echo -e " Repository: ${GREEN}$repo${NC}"
|
|
1942
|
+
else
|
|
1943
|
+
echo -e " Repository: ${YELLOW}not detected${NC}"
|
|
1944
|
+
fi
|
|
1945
|
+
|
|
1946
|
+
# Config flags
|
|
1947
|
+
echo ""
|
|
1948
|
+
echo -e "${BOLD}Configuration${NC}"
|
|
1949
|
+
echo -e " LOKI_GITHUB_IMPORT: ${LOKI_GITHUB_IMPORT:-false}"
|
|
1950
|
+
echo -e " LOKI_GITHUB_SYNC: ${LOKI_GITHUB_SYNC:-false}"
|
|
1951
|
+
echo -e " LOKI_GITHUB_PR: ${LOKI_GITHUB_PR:-false}"
|
|
1952
|
+
echo -e " LOKI_GITHUB_LABELS: ${LOKI_GITHUB_LABELS:-(all)}"
|
|
1953
|
+
echo -e " LOKI_GITHUB_LIMIT: ${LOKI_GITHUB_LIMIT:-100}"
|
|
1954
|
+
|
|
1955
|
+
# Sync log
|
|
1956
|
+
if [ -f "$LOKI_DIR/github/synced.log" ]; then
|
|
1957
|
+
echo ""
|
|
1958
|
+
echo -e "${BOLD}Sync History${NC}"
|
|
1959
|
+
local total
|
|
1960
|
+
total=$(wc -l < "$LOKI_DIR/github/synced.log" | tr -d ' ')
|
|
1961
|
+
echo -e " Total synced updates: $total"
|
|
1962
|
+
echo -e " Recent:"
|
|
1963
|
+
tail -5 "$LOKI_DIR/github/synced.log" | sed 's/^/ /'
|
|
1964
|
+
fi
|
|
1965
|
+
|
|
1966
|
+
# Imported tasks
|
|
1967
|
+
if [ -f "$LOKI_DIR/queue/pending.json" ]; then
|
|
1968
|
+
local gh_tasks
|
|
1969
|
+
gh_tasks=$(python3 -c "
|
|
1970
|
+
import json
|
|
1971
|
+
try:
|
|
1972
|
+
with open('$LOKI_DIR/queue/pending.json') as f:
|
|
1973
|
+
data = json.load(f)
|
|
1974
|
+
tasks = data.get('tasks', data) if isinstance(data, dict) else data
|
|
1975
|
+
gh = [t for t in tasks if t.get('source') == 'github']
|
|
1976
|
+
print(len(gh))
|
|
1977
|
+
except: print(0)
|
|
1978
|
+
" 2>/dev/null || echo "0")
|
|
1979
|
+
echo ""
|
|
1980
|
+
echo -e "${BOLD}Imported Issues${NC}"
|
|
1981
|
+
echo -e " GitHub tasks in queue: $gh_tasks"
|
|
1982
|
+
fi
|
|
1983
|
+
;;
|
|
1984
|
+
|
|
1985
|
+
help|*)
|
|
1986
|
+
echo -e "${BOLD}loki github${NC} - GitHub integration management"
|
|
1987
|
+
echo ""
|
|
1988
|
+
echo "Commands:"
|
|
1989
|
+
echo " status Show GitHub integration status"
|
|
1990
|
+
echo " sync Sync completed task status back to GitHub issues"
|
|
1991
|
+
echo " export Export local tasks as new GitHub issues"
|
|
1992
|
+
echo " pr [name] Create pull request from completed work"
|
|
1993
|
+
echo ""
|
|
1994
|
+
echo "Environment Variables:"
|
|
1995
|
+
echo " LOKI_GITHUB_IMPORT=true Import open issues as tasks on start"
|
|
1996
|
+
echo " LOKI_GITHUB_SYNC=true Sync status back to issues during session"
|
|
1997
|
+
echo " LOKI_GITHUB_PR=true Create PR when session completes successfully"
|
|
1998
|
+
echo " LOKI_GITHUB_LABELS=bug Filter issues by label (comma-separated)"
|
|
1999
|
+
echo " LOKI_GITHUB_MILESTONE=v2 Filter by milestone"
|
|
2000
|
+
echo " LOKI_GITHUB_ASSIGNEE=me Filter by assignee"
|
|
2001
|
+
echo " LOKI_GITHUB_LIMIT=50 Max issues to import (default: 100)"
|
|
2002
|
+
echo " LOKI_GITHUB_PR_LABEL=loki Label to add to created PRs"
|
|
2003
|
+
echo ""
|
|
2004
|
+
echo "Examples:"
|
|
2005
|
+
echo " loki github status"
|
|
2006
|
+
echo " loki github sync"
|
|
2007
|
+
echo " loki github export"
|
|
2008
|
+
echo " loki github pr \"Add user authentication\""
|
|
2009
|
+
echo " LOKI_GITHUB_SYNC=true loki start --github ./prd.md"
|
|
2010
|
+
;;
|
|
2011
|
+
esac
|
|
2012
|
+
}
|
|
2013
|
+
|
|
1835
2014
|
# Parse GitHub issue using issue-parser.sh
|
|
1836
2015
|
cmd_issue_parse() {
|
|
1837
2016
|
local issue_ref=""
|
|
@@ -4278,6 +4457,9 @@ main() {
|
|
|
4278
4457
|
import)
|
|
4279
4458
|
cmd_import
|
|
4280
4459
|
;;
|
|
4460
|
+
github)
|
|
4461
|
+
cmd_github "$@"
|
|
4462
|
+
;;
|
|
4281
4463
|
issue)
|
|
4282
4464
|
cmd_issue "$@"
|
|
4283
4465
|
;;
|
package/autonomy/run.sh
CHANGED
|
@@ -1273,10 +1273,12 @@ create_github_pr() {
|
|
|
1273
1273
|
local pr_body=".loki/reports/pr-body.md"
|
|
1274
1274
|
mkdir -p "$(dirname "$pr_body")"
|
|
1275
1275
|
|
|
1276
|
+
local version
|
|
1277
|
+
version=$(cat "${SCRIPT_DIR%/*}/VERSION" 2>/dev/null || echo "unknown")
|
|
1276
1278
|
cat > "$pr_body" << EOF
|
|
1277
1279
|
## Summary
|
|
1278
1280
|
|
|
1279
|
-
Automated implementation by Loki Mode
|
|
1281
|
+
Automated implementation by Loki Mode v$version ($ITERATION_COUNT iterations, provider: ${PROVIDER_NAME:-claude})
|
|
1280
1282
|
|
|
1281
1283
|
### Feature: $feature_name
|
|
1282
1284
|
|
|
@@ -1349,24 +1351,105 @@ sync_github_status() {
|
|
|
1349
1351
|
return 1
|
|
1350
1352
|
fi
|
|
1351
1353
|
|
|
1354
|
+
# Track synced issues to avoid duplicate comments
|
|
1355
|
+
mkdir -p .loki/github
|
|
1356
|
+
local sync_log=".loki/github/synced.log"
|
|
1357
|
+
local sync_key="${issue_number}:${status}"
|
|
1358
|
+
if [ -f "$sync_log" ] && grep -qF "$sync_key" "$sync_log" 2>/dev/null; then
|
|
1359
|
+
return 0 # Already synced this status
|
|
1360
|
+
fi
|
|
1361
|
+
|
|
1352
1362
|
case "$status" in
|
|
1353
1363
|
"in_progress")
|
|
1354
1364
|
gh issue comment "$issue_number" --repo "$repo" \
|
|
1355
|
-
--body "Loki Mode
|
|
1365
|
+
--body "**Loki Mode** -- Working on this issue (iteration $ITERATION_COUNT)" \
|
|
1356
1366
|
2>/dev/null || true
|
|
1357
1367
|
;;
|
|
1358
1368
|
"completed")
|
|
1369
|
+
local branch
|
|
1370
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
|
|
1371
|
+
local commit
|
|
1372
|
+
commit=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
1359
1373
|
gh issue comment "$issue_number" --repo "$repo" \
|
|
1360
|
-
--body "Loki Mode
|
|
1374
|
+
--body "**Loki Mode** -- Implementation complete on \`$branch\` ($commit). ${message:-}" \
|
|
1361
1375
|
2>/dev/null || true
|
|
1362
1376
|
;;
|
|
1363
1377
|
"closed")
|
|
1364
1378
|
gh issue close "$issue_number" --repo "$repo" \
|
|
1365
1379
|
--reason "completed" \
|
|
1366
|
-
--comment "Loki Mode
|
|
1380
|
+
--comment "**Loki Mode** -- Resolved. ${message:-}" \
|
|
1367
1381
|
2>/dev/null || true
|
|
1368
1382
|
;;
|
|
1369
1383
|
esac
|
|
1384
|
+
|
|
1385
|
+
# Record sync to avoid duplicates
|
|
1386
|
+
echo "$sync_key" >> "$sync_log"
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
# Sync all completed GitHub-sourced tasks back to their issues
|
|
1390
|
+
# Called after each iteration and at session end
|
|
1391
|
+
sync_github_completed_tasks() {
|
|
1392
|
+
if [ "$GITHUB_SYNC" != "true" ]; then
|
|
1393
|
+
return 0
|
|
1394
|
+
fi
|
|
1395
|
+
|
|
1396
|
+
if ! check_github_cli; then
|
|
1397
|
+
return 0
|
|
1398
|
+
fi
|
|
1399
|
+
|
|
1400
|
+
local completed_file=".loki/queue/completed.json"
|
|
1401
|
+
if [ ! -f "$completed_file" ]; then
|
|
1402
|
+
return 0
|
|
1403
|
+
fi
|
|
1404
|
+
|
|
1405
|
+
# Find GitHub-sourced tasks in completed queue that haven't been synced
|
|
1406
|
+
python3 -c "
|
|
1407
|
+
import json, sys
|
|
1408
|
+
try:
|
|
1409
|
+
with open('$completed_file') as f:
|
|
1410
|
+
tasks = json.load(f)
|
|
1411
|
+
for t in tasks:
|
|
1412
|
+
tid = t.get('id', '')
|
|
1413
|
+
if tid.startswith('github-'):
|
|
1414
|
+
print(tid)
|
|
1415
|
+
except Exception:
|
|
1416
|
+
pass
|
|
1417
|
+
" 2>/dev/null | while read -r task_id; do
|
|
1418
|
+
sync_github_status "$task_id" "completed"
|
|
1419
|
+
done
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
# Sync GitHub-sourced tasks currently in-progress
|
|
1423
|
+
sync_github_in_progress_tasks() {
|
|
1424
|
+
if [ "$GITHUB_SYNC" != "true" ]; then
|
|
1425
|
+
return 0
|
|
1426
|
+
fi
|
|
1427
|
+
|
|
1428
|
+
if ! check_github_cli; then
|
|
1429
|
+
return 0
|
|
1430
|
+
fi
|
|
1431
|
+
|
|
1432
|
+
local pending_file=".loki/queue/pending.json"
|
|
1433
|
+
if [ ! -f "$pending_file" ]; then
|
|
1434
|
+
return 0
|
|
1435
|
+
fi
|
|
1436
|
+
|
|
1437
|
+
# Find GitHub-sourced tasks in pending queue (about to be worked on)
|
|
1438
|
+
python3 -c "
|
|
1439
|
+
import json
|
|
1440
|
+
try:
|
|
1441
|
+
with open('$pending_file') as f:
|
|
1442
|
+
data = json.load(f)
|
|
1443
|
+
tasks = data.get('tasks', data) if isinstance(data, dict) else data
|
|
1444
|
+
for t in tasks:
|
|
1445
|
+
tid = t.get('id', '')
|
|
1446
|
+
if tid.startswith('github-'):
|
|
1447
|
+
print(tid)
|
|
1448
|
+
except Exception:
|
|
1449
|
+
pass
|
|
1450
|
+
" 2>/dev/null | while read -r task_id; do
|
|
1451
|
+
sync_github_status "$task_id" "in_progress"
|
|
1452
|
+
done
|
|
1370
1453
|
}
|
|
1371
1454
|
|
|
1372
1455
|
# Export tasks to GitHub issues (reverse sync)
|
|
@@ -2147,13 +2230,32 @@ init_loki_dir() {
|
|
|
2147
2230
|
|
|
2148
2231
|
# Clean up stale control files ONLY if no other session is running
|
|
2149
2232
|
# Deleting these while another session is active would destroy its signals
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2233
|
+
# Use flock if available to avoid TOCTOU race
|
|
2234
|
+
local lock_file=".loki/session.lock"
|
|
2235
|
+
local can_cleanup=false
|
|
2236
|
+
|
|
2237
|
+
if command -v flock >/dev/null 2>&1 && [ -f "$lock_file" ]; then
|
|
2238
|
+
# Try non-blocking lock - if we get it, no other session is running
|
|
2239
|
+
{
|
|
2240
|
+
if flock -n 201 2>/dev/null; then
|
|
2241
|
+
can_cleanup=true
|
|
2242
|
+
fi
|
|
2243
|
+
} 201>"$lock_file"
|
|
2244
|
+
else
|
|
2245
|
+
# Fallback: check PID file
|
|
2246
|
+
local existing_pid=""
|
|
2247
|
+
if [ -f ".loki/loki.pid" ]; then
|
|
2248
|
+
existing_pid=$(cat ".loki/loki.pid" 2>/dev/null)
|
|
2249
|
+
fi
|
|
2250
|
+
if [ -z "$existing_pid" ] || ! kill -0 "$existing_pid" 2>/dev/null; then
|
|
2251
|
+
can_cleanup=true
|
|
2252
|
+
fi
|
|
2153
2253
|
fi
|
|
2154
|
-
|
|
2254
|
+
|
|
2255
|
+
if [ "$can_cleanup" = "true" ]; then
|
|
2155
2256
|
rm -f .loki/PAUSE .loki/STOP .loki/HUMAN_INPUT.md 2>/dev/null
|
|
2156
2257
|
rm -f .loki/loki.pid 2>/dev/null
|
|
2258
|
+
rm -f .loki/session.lock 2>/dev/null
|
|
2157
2259
|
fi
|
|
2158
2260
|
|
|
2159
2261
|
mkdir -p .loki/{state,queue,messages,logs,config,prompts,artifacts,scripts}
|
|
@@ -2785,6 +2887,9 @@ EFF_EOF
|
|
|
2785
2887
|
# Check notification triggers (v5.40.0)
|
|
2786
2888
|
check_notification_triggers "$iteration"
|
|
2787
2889
|
|
|
2890
|
+
# Sync completed GitHub tasks back to issues (v5.41.0)
|
|
2891
|
+
sync_github_completed_tasks
|
|
2892
|
+
|
|
2788
2893
|
# Get task from in-progress
|
|
2789
2894
|
local in_progress_file=".loki/queue/in-progress.json"
|
|
2790
2895
|
local completed_file=".loki/queue/completed.json"
|
|
@@ -6945,16 +7050,50 @@ main() {
|
|
|
6945
7050
|
update_continuity
|
|
6946
7051
|
|
|
6947
7052
|
# Session lock: prevent concurrent sessions on same repo
|
|
7053
|
+
# Use flock for atomic locking to prevent TOCTOU race conditions
|
|
6948
7054
|
local pid_file=".loki/loki.pid"
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
7055
|
+
local lock_file=".loki/session.lock"
|
|
7056
|
+
|
|
7057
|
+
# Try to acquire exclusive lock with flock (if available)
|
|
7058
|
+
if command -v flock >/dev/null 2>&1; then
|
|
7059
|
+
# Create lock file
|
|
7060
|
+
touch "$lock_file"
|
|
7061
|
+
|
|
7062
|
+
# Open FD 200 at process scope so flock persists for entire session lifetime
|
|
7063
|
+
# (block-scoped redirection would release the lock when the block exits)
|
|
7064
|
+
exec 200>"$lock_file"
|
|
7065
|
+
|
|
7066
|
+
# Try to acquire exclusive lock (non-blocking)
|
|
7067
|
+
if ! flock -n 200 2>/dev/null; then
|
|
7068
|
+
log_error "Another Loki session is already running (locked)"
|
|
6955
7069
|
log_error "Stop it first with: loki stop"
|
|
6956
7070
|
exit 1
|
|
6957
7071
|
fi
|
|
7072
|
+
|
|
7073
|
+
# Check PID file after acquiring lock
|
|
7074
|
+
if [ -f "$pid_file" ]; then
|
|
7075
|
+
local existing_pid
|
|
7076
|
+
existing_pid=$(cat "$pid_file" 2>/dev/null)
|
|
7077
|
+
# Skip if it's our own PID or parent PID (background mode writes PID before child starts)
|
|
7078
|
+
if [ -n "$existing_pid" ] && [ "$existing_pid" != "$$" ] && [ "$existing_pid" != "$PPID" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
7079
|
+
log_error "Another Loki session is already running (PID: $existing_pid)"
|
|
7080
|
+
log_error "Stop it first with: loki stop"
|
|
7081
|
+
exit 1
|
|
7082
|
+
fi
|
|
7083
|
+
fi
|
|
7084
|
+
else
|
|
7085
|
+
# Fallback to original behavior if flock not available
|
|
7086
|
+
log_warn "flock not available - using non-atomic PID check (race condition possible)"
|
|
7087
|
+
if [ -f "$pid_file" ]; then
|
|
7088
|
+
local existing_pid
|
|
7089
|
+
existing_pid=$(cat "$pid_file" 2>/dev/null)
|
|
7090
|
+
# Skip if it's our own PID or parent PID (background mode writes PID before child starts)
|
|
7091
|
+
if [ -n "$existing_pid" ] && [ "$existing_pid" != "$$" ] && [ "$existing_pid" != "$PPID" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
7092
|
+
log_error "Another Loki session is already running (PID: $existing_pid)"
|
|
7093
|
+
log_error "Stop it first with: loki stop"
|
|
7094
|
+
exit 1
|
|
7095
|
+
fi
|
|
7096
|
+
fi
|
|
6958
7097
|
fi
|
|
6959
7098
|
|
|
6960
7099
|
# Write PID file for ALL modes (foreground + background)
|
|
@@ -6967,6 +7106,8 @@ main() {
|
|
|
6967
7106
|
# Import GitHub issues if enabled (v4.1.0)
|
|
6968
7107
|
if [ "$GITHUB_IMPORT" = "true" ]; then
|
|
6969
7108
|
import_github_issues
|
|
7109
|
+
# Notify GitHub that imported issues are being worked on (v5.41.0)
|
|
7110
|
+
sync_github_in_progress_tasks
|
|
6970
7111
|
fi
|
|
6971
7112
|
|
|
6972
7113
|
# Start web dashboard (if enabled)
|
|
@@ -7056,6 +7197,14 @@ main() {
|
|
|
7056
7197
|
run_autonomous "$PRD_PATH" || result=$?
|
|
7057
7198
|
fi
|
|
7058
7199
|
|
|
7200
|
+
# Final GitHub sync: sync all completed tasks and create PR (v5.41.0)
|
|
7201
|
+
sync_github_completed_tasks
|
|
7202
|
+
if [ "$GITHUB_PR" = "true" ] && [ "$result" = "0" ]; then
|
|
7203
|
+
local feature_name="${PRD_PATH:-Codebase improvements}"
|
|
7204
|
+
feature_name=$(basename "$feature_name" .md 2>/dev/null || echo "$feature_name")
|
|
7205
|
+
create_github_pr "$feature_name"
|
|
7206
|
+
fi
|
|
7207
|
+
|
|
7059
7208
|
# Extract and save learnings from this session
|
|
7060
7209
|
extract_learnings_from_session
|
|
7061
7210
|
|
package/dashboard/__init__.py
CHANGED
package/dashboard/auth.py
CHANGED
|
@@ -33,6 +33,7 @@ TOKEN_FILE = TOKEN_DIR / "tokens.json"
|
|
|
33
33
|
OIDC_ISSUER = os.environ.get("LOKI_OIDC_ISSUER", "") # e.g., https://accounts.google.com
|
|
34
34
|
OIDC_CLIENT_ID = os.environ.get("LOKI_OIDC_CLIENT_ID", "")
|
|
35
35
|
OIDC_AUDIENCE = os.environ.get("LOKI_OIDC_AUDIENCE", "") # Usually same as client_id
|
|
36
|
+
OIDC_SKIP_SIGNATURE_VERIFY = os.environ.get("LOKI_OIDC_SKIP_SIGNATURE_VERIFY", "").lower() in ("true", "1", "yes")
|
|
36
37
|
OIDC_ENABLED = bool(OIDC_ISSUER and OIDC_CLIENT_ID)
|
|
37
38
|
|
|
38
39
|
# Role-to-scope mapping (predefined roles)
|
|
@@ -54,11 +55,19 @@ _SCOPE_HIERARCHY = {
|
|
|
54
55
|
|
|
55
56
|
if OIDC_ENABLED:
|
|
56
57
|
import logging as _logging
|
|
57
|
-
_logging.getLogger("loki.auth")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
_logger = _logging.getLogger("loki.auth")
|
|
59
|
+
if OIDC_SKIP_SIGNATURE_VERIFY:
|
|
60
|
+
_logger.critical(
|
|
61
|
+
"OIDC/SSO signature verification DISABLED (LOKI_OIDC_SKIP_SIGNATURE_VERIFY=true). "
|
|
62
|
+
"This is INSECURE and allows forged JWTs. Only use for local testing. "
|
|
63
|
+
"For production, install PyJWT + cryptography and remove this env var."
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
_logger.warning(
|
|
67
|
+
"OIDC/SSO enabled (EXPERIMENTAL). Claims-based validation only -- "
|
|
68
|
+
"JWT signatures are NOT cryptographically verified. Install PyJWT + "
|
|
69
|
+
"cryptography for production signature verification."
|
|
70
|
+
)
|
|
62
71
|
|
|
63
72
|
# OIDC JWKS cache (issuer URL -> (keys_dict, fetch_timestamp))
|
|
64
73
|
_oidc_jwks_cache = {} # type: dict[str, tuple[dict, float]]
|
|
@@ -452,15 +461,18 @@ def validate_oidc_token(token_str: str) -> Optional[dict]:
|
|
|
452
461
|
|
|
453
462
|
This is a claims-based validation that checks:
|
|
454
463
|
- Token structure (3 base64url-encoded parts)
|
|
464
|
+
- Signature part is not empty (basic sanity check)
|
|
455
465
|
- Issuer matches OIDC_ISSUER
|
|
456
466
|
- Audience matches OIDC_AUDIENCE or OIDC_CLIENT_ID
|
|
457
467
|
- Token is not expired
|
|
458
468
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
469
|
+
SECURITY WARNING: Cryptographic signature verification is NOT performed
|
|
470
|
+
unless PyJWT is installed. This implementation only validates claims.
|
|
471
|
+
An attacker can forge JWTs unless LOKI_OIDC_SKIP_SIGNATURE_VERIFY is
|
|
472
|
+
explicitly set to 'true' (which is INSECURE and only for local testing).
|
|
473
|
+
|
|
474
|
+
For production: Install PyJWT + cryptography for proper RSA/HMAC
|
|
475
|
+
signature verification.
|
|
464
476
|
"""
|
|
465
477
|
if not OIDC_ENABLED:
|
|
466
478
|
return None
|
|
@@ -470,8 +482,29 @@ def validate_oidc_token(token_str: str) -> Optional[dict]:
|
|
|
470
482
|
if len(parts) != 3:
|
|
471
483
|
return None
|
|
472
484
|
|
|
485
|
+
# Basic sanity check: signature part must not be empty
|
|
486
|
+
header_b64, payload_b64, signature_b64 = parts
|
|
487
|
+
if not signature_b64 or len(signature_b64) < 10:
|
|
488
|
+
import logging as _logging
|
|
489
|
+
_logging.getLogger("loki.auth").error(
|
|
490
|
+
"OIDC token rejected: signature part is missing or too short"
|
|
491
|
+
)
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
# CRITICAL: Check if signature verification is explicitly skipped
|
|
495
|
+
if not OIDC_SKIP_SIGNATURE_VERIFY:
|
|
496
|
+
import logging as _logging
|
|
497
|
+
_logging.getLogger("loki.auth").critical(
|
|
498
|
+
"OIDC token received but signature verification is NOT implemented. "
|
|
499
|
+
"Set LOKI_OIDC_SKIP_SIGNATURE_VERIFY=true to explicitly allow "
|
|
500
|
+
"unverified tokens (INSECURE - local testing only), or install "
|
|
501
|
+
"PyJWT + cryptography for production signature verification. "
|
|
502
|
+
"Rejecting token for security."
|
|
503
|
+
)
|
|
504
|
+
return None
|
|
505
|
+
|
|
473
506
|
# Decode payload (claims)
|
|
474
|
-
claims = json.loads(_base64url_decode(
|
|
507
|
+
claims = json.loads(_base64url_decode(payload_b64))
|
|
475
508
|
|
|
476
509
|
# Validate issuer
|
|
477
510
|
if claims.get("iss") != OIDC_ISSUER:
|