loki-mode 11.5.1 → 11.7.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/README.md +19 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/backlog.py +11 -2
- package/autonomy/lib/chat.sh +57 -1
- package/autonomy/lib/loki_yaml.py +8 -2
- package/autonomy/lib/workspace.py +267 -11
- package/autonomy/loki +327 -222
- package/autonomy/notification-checker.py +1 -1
- package/autonomy/trigger-server.py +185 -2
- package/autonomy/verify.sh +4 -0
- package/completions/_loki +4 -0
- package/completions/loki.bash +20 -2
- package/dashboard/__init__.py +3 -10
- package/dashboard/control.py +5 -435
- package/docs/ARCHITECTURE.md +1 -2
- package/docs/CLI-CORE-VERBS.md +2 -6
- package/docs/CLI-REFERENCE.md +2 -1
- package/docs/COMPONENTS.md +0 -2
- package/docs/DEPLOY-PLAN.md +17 -8
- package/docs/EVALUATING.md +8 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/WORKSPACES.md +15 -2
- package/docs/authentication.md +19 -1
- package/docs/dev/architecture-reference.md +8 -0
- package/docs/dev/release-checklist.md +9 -8
- package/docs/enterprise/security.md +3 -234
- package/docs/sentry-intake.md +11 -0
- package/docs/v10/ARCHITECT-CUTS.md +304 -0
- package/docs/v10/CLI-MODERN.md +1 -1
- package/docs/v10/DECISIONS.md +5 -5
- package/docs/v10/ENGINE.md +735 -299
- package/docs/v10/GUARDS.md +16 -14
- package/docs/v10/GUIDE.md +5 -7
- package/docs/v10/PROGRESS.md +12 -1
- package/docs/v11/MARKET-SLICES.md +1 -5
- package/integrations/openclaw/README.md +20 -0
- package/integrations/vibe-kanban.md +12 -50
- package/loki-ts/dist/loki.js +1213 -1178
- package/mcp/__init__.py +1 -1
- package/mcp/_sdk_loader.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +1090 -924
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/quality-control.md +1 -116
- package/schemas/loki-yaml.schema.json +5 -2
- package/skills/00-index.md +2 -2
- package/skills/artifacts.md +0 -22
- package/skills/production.md +2 -38
- package/src/policies/engine.js +41 -2
- package/src/policies/types.js +49 -0
- package/web-app/dist/assets/{AdminPage-Cbu3pXL8.js → AdminPage-S3BYeapm.js} +1 -1
- package/web-app/dist/assets/{Avatar-DGqGGcGx.js → Avatar-C1rHwLgn.js} +1 -1
- package/web-app/dist/assets/{Badge-Bhlipt6v.js → Badge-CzlnN5aQ.js} +1 -1
- package/web-app/dist/assets/{Button-CpN2sO61.js → Button-B1w5AeGC.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-BqeDJwdL.js → CockpitPage-Dyeqjfkc.js} +1 -1
- package/web-app/dist/assets/{ComparePage-DUieA2ba.js → ComparePage-C6y2GkGR.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-DTWsYrOm.js → ErrorBoundary-Dg2fcGkR.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-BU_B9X19.js → EvidenceReceiptPanel-CxbyjvTj.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-DEZoLOsP.js → GitHubIssuesPanel-Dpys1xDb.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BEsnrq7Y.js → GitHubPRsPanel-CRVjWoFl.js} +1 -1
- package/web-app/dist/assets/{HomePage-BoUVuq7h.js → HomePage-BKnv-oYw.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CmOED7vb.js → LoginPage-BGksmFRd.js} +1 -1
- package/web-app/dist/assets/{MagicPage-DATZitrZ.js → MagicPage-OCvhp16M.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-DGL1v0I-.js → MetricsPage-CJOJpXWC.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-C2WgOjZd.js → NotFoundPage-C04p3QvN.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-BMi1tepj.js → ProjectPage-BRB8txsY.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-vc3y5YPh.js → ProjectsPage-CoUYeQEh.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-kbq0Ftvi.js → SettingsPage-BkjmJGfM.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-Bn6Q8Kbg.js → ShowcasePage-B4vU9TjQ.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-Basmhs_b.js → SystemSettingsPage-Z2jiVsni.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-BTqOc4TR.js → TeamsPage-Ulm8SISq.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-2P9002VL.js → TemplatesPage-C_7MM6Ey.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CAoSPBrg.js → TerminalOutput-Cumd7Tgs.js} +1 -1
- package/web-app/dist/assets/{activity-BcWVWCqU.js → activity-CZAUkbiY.js} +1 -1
- package/web-app/dist/assets/{bell-CYRqqgZ6.js → bell-BOvSXpEr.js} +1 -1
- package/web-app/dist/assets/{bot-CU7gdn3g.js → bot-DOTZxBYj.js} +1 -1
- package/web-app/dist/assets/{check-DLlCpz9V.js → check-BxpE0B5u.js} +1 -1
- package/web-app/dist/assets/{chevron-left-CE9Cb30C.js → chevron-left-D6KPvBOA.js} +1 -1
- package/web-app/dist/assets/{circle-alert-uou4bjua.js → circle-alert-Bajq86oe.js} +1 -1
- package/web-app/dist/assets/{clock-C_dSJcDX.js → clock-CnlHAVJc.js} +1 -1
- package/web-app/dist/assets/{cloud-DZ22ITF6.js → cloud-DjaymjwM.js} +1 -1
- package/web-app/dist/assets/{code-xml-BrJS0hhS.js → code-xml-BADEw2Ld.js} +1 -1
- package/web-app/dist/assets/{database-D2HOmxtl.js → database-B4aKb5Cq.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CQ1vK_6q.js → dollar-sign-DrldXpja.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-w2vpGm5d.js → file-code-corner-DVebDVR-.js} +1 -1
- package/web-app/dist/assets/{file-plus-DlF6wjkD.js → file-plus-BD-N_-Tl.js} +1 -1
- package/web-app/dist/assets/{globe-Dy3lBJg1.js → globe-C7ieInC-.js} +1 -1
- package/web-app/dist/assets/{hammer-C3xIiRXN.js → hammer-BSugKIji.js} +1 -1
- package/web-app/dist/assets/{index-4t-_IW2y.js → index-nLQgfvOn.js} +3 -3
- package/web-app/dist/assets/{layers-Y8Fs8qXI.js → layers-CHXxU2on.js} +1 -1
- package/web-app/dist/assets/{loader-circle-CxgN1v4m.js → loader-circle-BEEz0LbD.js} +1 -1
- package/web-app/dist/assets/{lock-DFFNbXDs.js → lock-CzQGfcnE.js} +1 -1
- package/web-app/dist/assets/{package-5e6qCTUg.js → package-CcHYQLGd.js} +1 -1
- package/web-app/dist/assets/{plus-C0Go_ytj.js → plus-DdzgFEXn.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-D4i-NW3o.js → refresh-cw-7x0HbH9Y.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-YlKkiUW6.js → rotate-ccw-DBahX1Sp.js} +1 -1
- package/web-app/dist/assets/{scroll-text-4qXOHTap.js → scroll-text-BB8y8a9Z.js} +1 -1
- package/web-app/dist/assets/{server-BB6zXNp9.js → server-CfkbW2MH.js} +1 -1
- package/web-app/dist/assets/{shield-alert-D4tzjB1H.js → shield-alert-DUgD8rBF.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-DZiRwdFj.js → thumbs-up-DGoyY6QC.js} +1 -1
- package/web-app/dist/assets/{trash-2-Dv_LkRIK.js → trash-2-B6xrSUMr.js} +1 -1
- package/web-app/dist/assets/{trending-down-BJYp9PQe.js → trending-down-B6RGKTOx.js} +1 -1
- package/web-app/dist/assets/{trending-up-BTcXLklK.js → trending-up-B44DCrt1.js} +1 -1
- package/web-app/dist/assets/{upload-fcHqwOxr.js → upload-4kWm33ZY.js} +1 -1
- package/web-app/dist/assets/{usePolling-BeJYw_4L.js → usePolling-zkUtRQG6.js} +1 -1
- package/web-app/dist/assets/{user-BcDwUCsO.js → user-04cEVIe2.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/dashboard/run.py +0 -63
- package/docs/openclaw-integration.md +0 -52
- package/docs/v10/HANDOFF-NEXT-WEEK.md +0 -40
- package/docs/v10/MORNING-BRIEF-1003.md +0 -25
- package/docs/v10/MORNING-BRIEF.md +0 -52
- package/docs/v10/TODO.md +0 -35
- package/mcp/resources.py +0 -351
- package/mcp/tools.py +0 -200
|
@@ -265,7 +265,7 @@ def check_quality_gate(trigger, loki_dir, iteration, notifications):
|
|
|
265
265
|
defect as the budget key, in the same file.
|
|
266
266
|
|
|
267
267
|
gate-failures.txt is the canonical failure source used by
|
|
268
|
-
proof-generator.py:413 and loki-ts
|
|
268
|
+
proof-generator.py:413 and loki-ts build_prompt.ts, so this
|
|
269
269
|
reuses a proven contract rather than introducing a second one.
|
|
270
270
|
"""
|
|
271
271
|
failures_file = Path(loki_dir) / "quality" / "gate-failures.txt"
|
|
@@ -26,6 +26,12 @@ GitHub, the bearer token authenticates a human operator -- so holding one must
|
|
|
26
26
|
never grant the other. If no API token is configured the server still starts but
|
|
27
27
|
every /jobs request is rejected with 503 and an audit log line.
|
|
28
28
|
|
|
29
|
+
Self-healing entry point: POST /sentry accepts a Sentry integration webhook
|
|
30
|
+
authenticated by Sentry's own HMAC (sentry-hook-signature) under a THIRD,
|
|
31
|
+
separate secret (LOKI_SENTRY_WEBHOOK_SECRET or SENTRY_CLIENT_SECRET). A valid
|
|
32
|
+
alert enqueues `loki start "<normalized brief>" --pr --detach`: the output is a
|
|
33
|
+
receipt-gated PR only, never a deploy. No secret means the route answers 503.
|
|
34
|
+
|
|
29
35
|
Usage:
|
|
30
36
|
python3 autonomy/trigger-server.py [--port PORT] [--secret SECRET] [--dry-run]
|
|
31
37
|
[--workers N] [--queue-size N]
|
|
@@ -398,6 +404,21 @@ OUTCOME_FAILED = JOB_STATUS_FAILED # launch failed, or exited non-zero
|
|
|
398
404
|
OUTCOME_UNKNOWN = JOB_STATUS_UNKNOWN # detached; exit code never observed
|
|
399
405
|
|
|
400
406
|
|
|
407
|
+
# Secrets this server holds that a spawned `loki start` must never inherit: a
|
|
408
|
+
# prompt-injected run could otherwise forge /sentry, /webhook or /jobs requests.
|
|
409
|
+
CHILD_ENV_DENYLIST = (
|
|
410
|
+
"GITHUB_WEBHOOK_SECRET",
|
|
411
|
+
"LOKI_API_TOKEN",
|
|
412
|
+
"LOKI_API_TOKEN_FILE",
|
|
413
|
+
"LOKI_SENTRY_WEBHOOK_SECRET",
|
|
414
|
+
"SENTRY_CLIENT_SECRET",
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _scoped_child_env():
|
|
419
|
+
return {k: v for k, v in os.environ.items() if k not in CHILD_ENV_DENYLIST}
|
|
420
|
+
|
|
421
|
+
|
|
401
422
|
def run_loki_outcome(args, dry_run=False):
|
|
402
423
|
"""Run a loki command and report WHICH of the three outcomes occurred.
|
|
403
424
|
|
|
@@ -414,6 +435,7 @@ def run_loki_outcome(args, dry_run=False):
|
|
|
414
435
|
cmd,
|
|
415
436
|
stdout=subprocess.PIPE,
|
|
416
437
|
stderr=subprocess.PIPE,
|
|
438
|
+
env=_scoped_child_env(),
|
|
417
439
|
)
|
|
418
440
|
except (FileNotFoundError, OSError) as e:
|
|
419
441
|
logging.error("Failed to launch %s: %s", " ".join(cmd), e)
|
|
@@ -577,6 +599,104 @@ def handle_job_event(payload, dry_run=False):
|
|
|
577
599
|
return summary, status
|
|
578
600
|
|
|
579
601
|
|
|
602
|
+
# Pseudo-event for a Sentry alert. Like JOB_EVENT it is NOT a GitHub event and is
|
|
603
|
+
# refused on /webhook: only /sentry, under its own HMAC secret, may enqueue it.
|
|
604
|
+
SENTRY_EVENT = "sentry_issue"
|
|
605
|
+
INTERNAL_EVENTS = (JOB_EVENT, SENTRY_EVENT)
|
|
606
|
+
|
|
607
|
+
# Sentry actions that mean "a new problem needs a fix". resolved/assigned/etc.
|
|
608
|
+
# are skipped so a state change never starts a build.
|
|
609
|
+
SENTRY_ACTIONS = ("triggered", "created")
|
|
610
|
+
SENTRY_BRIEF_MAX = 1500
|
|
611
|
+
ISSUE_PROVIDERS_SH = Path(__file__).resolve().parent / "issue-providers.sh"
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def validate_sentry_signature(secret, body, signature_header):
|
|
615
|
+
"""Validate Sentry's `sentry-hook-signature` (hex HMAC-SHA256, no prefix).
|
|
616
|
+
|
|
617
|
+
Fails closed: no secret or no header is never valid.
|
|
618
|
+
"""
|
|
619
|
+
if not secret or not signature_header:
|
|
620
|
+
return False
|
|
621
|
+
expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
|
|
622
|
+
return constant_time_equals(expected, signature_header.strip().lower())
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def normalize_sentry_payload(payload):
|
|
626
|
+
"""Run the existing normalize_sentry_json over a Sentry webhook payload.
|
|
627
|
+
|
|
628
|
+
Reuses autonomy/issue-providers.sh (single owner of the Sentry shape) via a
|
|
629
|
+
scoped-env bash call. Returns the normalized dict or None.
|
|
630
|
+
"""
|
|
631
|
+
data = payload.get("data")
|
|
632
|
+
if not isinstance(data, dict):
|
|
633
|
+
return None
|
|
634
|
+
raw = {k: data[k] for k in ("issue", "event") if isinstance(data.get(k), dict)}
|
|
635
|
+
if not raw:
|
|
636
|
+
return None
|
|
637
|
+
env = {"PATH": os.environ.get("PATH", "/usr/bin:/bin")}
|
|
638
|
+
ev = raw.get("event") or {}
|
|
639
|
+
env["_LOKI_SENTRY_URL"] = str(ev.get("web_url") or ev.get("url") or "")
|
|
640
|
+
try:
|
|
641
|
+
proc = subprocess.run(
|
|
642
|
+
["bash", "-c", 'source "$1" && normalize_sentry_json', "_",
|
|
643
|
+
str(ISSUE_PROVIDERS_SH)],
|
|
644
|
+
input=json.dumps(raw).encode("utf-8"), capture_output=True,
|
|
645
|
+
env=env, timeout=20,
|
|
646
|
+
)
|
|
647
|
+
except (OSError, subprocess.SubprocessError):
|
|
648
|
+
return None
|
|
649
|
+
if proc.returncode != 0:
|
|
650
|
+
return None
|
|
651
|
+
try:
|
|
652
|
+
out = json.loads(proc.stdout.decode("utf-8", errors="replace").strip().splitlines()[-1])
|
|
653
|
+
except (ValueError, IndexError):
|
|
654
|
+
return None
|
|
655
|
+
return out if isinstance(out, dict) else None
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def build_sentry_brief(norm):
|
|
659
|
+
"""One-line, control-char-free brief that can never parse as a CLI flag."""
|
|
660
|
+
culprit = ""
|
|
661
|
+
for line in str(norm.get("body", "")).splitlines():
|
|
662
|
+
if line.startswith("- Culprit: "):
|
|
663
|
+
culprit = line[len("- Culprit: "):].strip()
|
|
664
|
+
break
|
|
665
|
+
parts = ["Fix Sentry issue %s: %s" % (norm.get("number", "?"), norm.get("title", ""))]
|
|
666
|
+
if culprit:
|
|
667
|
+
parts.append("(culprit %s)" % culprit)
|
|
668
|
+
if norm.get("url"):
|
|
669
|
+
parts.append("See %s." % norm["url"])
|
|
670
|
+
parts.append("Reproduce with a regression test, then fix the root cause.")
|
|
671
|
+
brief = CONTROL_CHARS_RE.sub(" ", " ".join(parts))
|
|
672
|
+
brief = " ".join(brief.split())
|
|
673
|
+
return brief.encode("utf-8")[:SENTRY_BRIEF_MAX].decode("utf-8", errors="ignore")
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def handle_sentry_event(payload, dry_run=False):
|
|
677
|
+
"""Handle a verified Sentry alert: loki start <brief> --pr --detach.
|
|
678
|
+
|
|
679
|
+
PR only. Never passes a deploy argument: a fix still has to earn a VERIFIED
|
|
680
|
+
receipt, and shipping it stays a separate, gated human step.
|
|
681
|
+
"""
|
|
682
|
+
action = payload.get("action", "")
|
|
683
|
+
if action not in SENTRY_ACTIONS:
|
|
684
|
+
return None, "skipped (action=%s)" % action
|
|
685
|
+
norm = normalize_sentry_payload(payload)
|
|
686
|
+
if norm is None:
|
|
687
|
+
return None, "rejected (unparseable sentry payload)"
|
|
688
|
+
brief = build_sentry_brief(norm)
|
|
689
|
+
if not valid_spec(brief):
|
|
690
|
+
return None, "rejected (invalid brief)"
|
|
691
|
+
args = ["start", brief, "--pr", "--detach"]
|
|
692
|
+
summary = "sentry issue %s" % norm.get("number", "?")
|
|
693
|
+
success = run_loki_command(args, dry_run=dry_run)
|
|
694
|
+
status = "fired" if success else "error"
|
|
695
|
+
if success:
|
|
696
|
+
send_notification("Trigger fired: %s" % summary)
|
|
697
|
+
return summary, status
|
|
698
|
+
|
|
699
|
+
|
|
580
700
|
# Map event name -> handler. Keeps do_POST routing declarative. JOB_EVENT rides
|
|
581
701
|
# the same table (and therefore the same bounded queue and worker pool) but is
|
|
582
702
|
# explicitly refused on the /webhook path.
|
|
@@ -585,6 +705,7 @@ EVENT_HANDLERS = {
|
|
|
585
705
|
"pull_request": handle_pull_request_event,
|
|
586
706
|
"workflow_run": handle_workflow_run_event,
|
|
587
707
|
JOB_EVENT: handle_job_event,
|
|
708
|
+
SENTRY_EVENT: handle_sentry_event,
|
|
588
709
|
}
|
|
589
710
|
|
|
590
711
|
|
|
@@ -875,6 +996,8 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
875
996
|
# Bearer token for the /jobs API. MUST be a different secret from `secret`
|
|
876
997
|
# above: one authenticates GitHub, the other authenticates a human.
|
|
877
998
|
api_token = ""
|
|
999
|
+
# Sentry integration client secret for /sentry. Distinct from both above.
|
|
1000
|
+
sentry_secret = ""
|
|
878
1001
|
dispatcher = None
|
|
879
1002
|
# Ed25519 key that attests receipts, and the retired public keys that keep
|
|
880
1003
|
# pre-rotation receipts verifiable. Both default to unconfigured, which
|
|
@@ -1179,11 +1302,51 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1179
1302
|
log_event(JOB_EVENT, "", job["spec"], "queued")
|
|
1180
1303
|
self._send_json(202, {"id": job_id, "status": "queued"})
|
|
1181
1304
|
|
|
1305
|
+
def _handle_sentry(self):
|
|
1306
|
+
"""POST /sentry -- Sentry alert, HMAC-verified, enqueues a fix run."""
|
|
1307
|
+
body = self._read_body()
|
|
1308
|
+
if body is None:
|
|
1309
|
+
return
|
|
1310
|
+
event_type = SENTRY_EVENT
|
|
1311
|
+
if not self.sentry_secret:
|
|
1312
|
+
logging.warning("Rejecting /sentry from %s: no Sentry secret configured",
|
|
1313
|
+
self.address_string())
|
|
1314
|
+
log_event(event_type, "", "", "rejected (no sentry secret configured)")
|
|
1315
|
+
self._send_json(503, {"error": "sentry webhook secret not configured"})
|
|
1316
|
+
return
|
|
1317
|
+
sig = self.headers.get("sentry-hook-signature", "")
|
|
1318
|
+
if not validate_sentry_signature(self.sentry_secret, body, sig):
|
|
1319
|
+
logging.warning("Invalid Sentry signature from %s", self.address_string())
|
|
1320
|
+
log_event(event_type, "", "", "rejected (invalid signature)")
|
|
1321
|
+
self._send_json(401, {"error": "invalid signature"})
|
|
1322
|
+
return
|
|
1323
|
+
try:
|
|
1324
|
+
payload = json.loads(body)
|
|
1325
|
+
except json.JSONDecodeError:
|
|
1326
|
+
self._send_json(400, {"error": "invalid JSON"})
|
|
1327
|
+
return
|
|
1328
|
+
if not isinstance(payload, dict):
|
|
1329
|
+
self._send_json(400, {"error": "payload must be a JSON object"})
|
|
1330
|
+
return
|
|
1331
|
+
action = payload.get("action", "")
|
|
1332
|
+
if self.dispatcher.seen_delivery(self.headers.get("Request-ID", "")):
|
|
1333
|
+
log_event(event_type, action, "", "duplicate")
|
|
1334
|
+
self._send_json(200, {"event": event_type, "status": "duplicate"})
|
|
1335
|
+
return
|
|
1336
|
+
if not self.dispatcher.submit(event_type, payload):
|
|
1337
|
+
log_event(event_type, action, "", "rejected (queue full)")
|
|
1338
|
+
self._send_json(503, {"error": "server busy, retry later"})
|
|
1339
|
+
return
|
|
1340
|
+
self._send_json(202, {"event": event_type, "action": action, "status": "queued"})
|
|
1341
|
+
|
|
1182
1342
|
def do_POST(self):
|
|
1183
1343
|
path = self.path.split("?", 1)[0]
|
|
1184
1344
|
if path == "/jobs":
|
|
1185
1345
|
self._handle_job_submit()
|
|
1186
1346
|
return
|
|
1347
|
+
if path == "/sentry":
|
|
1348
|
+
self._handle_sentry()
|
|
1349
|
+
return
|
|
1187
1350
|
if path != "/webhook":
|
|
1188
1351
|
self._send_json(404, {"error": "not found"})
|
|
1189
1352
|
return
|
|
@@ -1201,10 +1364,10 @@ class WebhookHandler(http.server.BaseHTTPRequestHandler):
|
|
|
1201
1364
|
# never be reachable with the webhook HMAC. Without this, a holder of
|
|
1202
1365
|
# the webhook secret could POST X-GitHub-Event: loki_job and submit an
|
|
1203
1366
|
# arbitrary spec, defeating the separate-credential requirement.
|
|
1204
|
-
if event_type
|
|
1367
|
+
if event_type in INTERNAL_EVENTS:
|
|
1205
1368
|
logging.warning(
|
|
1206
1369
|
"Rejecting %s on /webhook from %s: submits require the /jobs "
|
|
1207
|
-
"API and its own bearer token",
|
|
1370
|
+
"API and its own bearer token (Sentry uses /sentry)",
|
|
1208
1371
|
JOB_EVENT,
|
|
1209
1372
|
self.address_string(),
|
|
1210
1373
|
)
|
|
@@ -1350,6 +1513,20 @@ def main():
|
|
|
1350
1513
|
)
|
|
1351
1514
|
api_token = ""
|
|
1352
1515
|
|
|
1516
|
+
# Sentry secret: env only (never argv). Must differ from the other two
|
|
1517
|
+
# credentials, since each authenticates a different party.
|
|
1518
|
+
sentry_secret = (os.environ.get("LOKI_SENTRY_WEBHOOK_SECRET")
|
|
1519
|
+
or os.environ.get("SENTRY_CLIENT_SECRET") or "")
|
|
1520
|
+
if sentry_secret and (
|
|
1521
|
+
(secret and constant_time_equals(sentry_secret, secret))
|
|
1522
|
+
or (api_token and constant_time_equals(sentry_secret, api_token))
|
|
1523
|
+
):
|
|
1524
|
+
logging.error(
|
|
1525
|
+
"The Sentry secret is identical to the webhook secret or API "
|
|
1526
|
+
"token. They MUST differ; disabling /sentry."
|
|
1527
|
+
)
|
|
1528
|
+
sentry_secret = ""
|
|
1529
|
+
|
|
1353
1530
|
# Persist resolved values, but never write the secret to disk.
|
|
1354
1531
|
config["port"] = port
|
|
1355
1532
|
config["dry_run"] = dry_run
|
|
@@ -1363,6 +1540,7 @@ def main():
|
|
|
1363
1540
|
WebhookHandler.dry_run = dry_run
|
|
1364
1541
|
WebhookHandler.secret = secret
|
|
1365
1542
|
WebhookHandler.api_token = api_token
|
|
1543
|
+
WebhookHandler.sentry_secret = sentry_secret
|
|
1366
1544
|
WebhookHandler.dispatcher = dispatcher
|
|
1367
1545
|
|
|
1368
1546
|
signing_key, signing_kid = load_signing_key(auto_generate=False)
|
|
@@ -1404,6 +1582,11 @@ def main():
|
|
|
1404
1582
|
"503. Set LOKI_API_TOKEN or LOKI_API_TOKEN_FILE to enable remote "
|
|
1405
1583
|
"submits."
|
|
1406
1584
|
)
|
|
1585
|
+
if not sentry_secret:
|
|
1586
|
+
logging.warning(
|
|
1587
|
+
"No Sentry secret configured: POST /sentry is rejected with 503. "
|
|
1588
|
+
"Set LOKI_SENTRY_WEBHOOK_SECRET or SENTRY_CLIENT_SECRET."
|
|
1589
|
+
)
|
|
1407
1590
|
if not secret:
|
|
1408
1591
|
logging.warning(
|
|
1409
1592
|
"No webhook secret configured: ALL webhooks will be rejected with "
|
package/autonomy/verify.sh
CHANGED
|
@@ -2584,6 +2584,10 @@ OPTIONS:
|
|
|
2584
2584
|
is exactly as without --hosted (fail-open, never a silent
|
|
2585
2585
|
pass). Without --hosted, output is byte-identical to prior
|
|
2586
2586
|
releases.
|
|
2587
|
+
--seal-composite <run-dir>
|
|
2588
|
+
Seal a workspace run into one signed composite receipt
|
|
2589
|
+
(composite.json). `loki verify <composite.json>` then
|
|
2590
|
+
re-checks it offline. Routed to the engine10 verifier.
|
|
2587
2591
|
-h, --help Show this help.
|
|
2588
2592
|
|
|
2589
2593
|
GATES (deterministic, reproducible by construction):
|
package/completions/_loki
CHANGED
|
@@ -313,6 +313,10 @@ function _loki_start {
|
|
|
313
313
|
_arguments \
|
|
314
314
|
'--provider[AI provider]:provider name:(claude cline codex aider opencode)' \
|
|
315
315
|
'--parallel[Parallel mode]' \
|
|
316
|
+
'--workspace[Run a loki.yaml workspace for the brief]:workspace name:' \
|
|
317
|
+
'--deploy[Deploy after a VERIFIED run via loki deploy --execute]' \
|
|
318
|
+
'--health-url[With --deploy: health probe URL]:url:' \
|
|
319
|
+
'--auto-rollback[With --deploy: roll back on a failed health probe]' \
|
|
316
320
|
'--background[Background mode]' \
|
|
317
321
|
'--bg[Background mode]' \
|
|
318
322
|
'--simple[Force simple complexity]' \
|
package/completions/loki.bash
CHANGED
|
@@ -27,7 +27,7 @@ _loki_completion() {
|
|
|
27
27
|
|
|
28
28
|
# If the word starts with a dash, show flags
|
|
29
29
|
if [[ "$cur" == -* ]]; then
|
|
30
|
-
local flags="--provider --max-iterations --parallel --background --bg --simple --complex --github --no-dashboard --sandbox --skip-memory --spec --spec-first --yes --budget --attempts --model --isolation --max-cost --no-pr --explain-route --help"
|
|
30
|
+
local flags="--provider --max-iterations --parallel --background --bg --simple --complex --github --no-dashboard --sandbox --skip-memory --spec --spec-first --yes --budget --attempts --model --isolation --max-cost --no-pr --explain-route --workspace --deploy --health-url --auto-rollback --help"
|
|
31
31
|
COMPREPLY=( $(compgen -W "${flags}" -- "$cur") )
|
|
32
32
|
return 0
|
|
33
33
|
fi
|
|
@@ -57,7 +57,11 @@ _loki_completion() {
|
|
|
57
57
|
;;
|
|
58
58
|
|
|
59
59
|
memory)
|
|
60
|
-
local memory_cmds="list show search stats export clear dedupe index timeline consolidate economics retrieve lessons learn forget episode pattern skill vectors help"
|
|
60
|
+
local memory_cmds="list show search stats export clear dedupe index timeline consolidate economics retrieve lessons learn forget episode pattern skill vectors brief help"
|
|
61
|
+
if [[ "$cur" == -* ]]; then
|
|
62
|
+
COMPREPLY=( $(compgen -W "--task --help" -- "$cur") )
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
61
65
|
COMPREPLY=( $(compgen -W "${memory_cmds}" -- "$cur") )
|
|
62
66
|
;;
|
|
63
67
|
|
|
@@ -136,6 +140,20 @@ _loki_completion() {
|
|
|
136
140
|
fi
|
|
137
141
|
;;
|
|
138
142
|
|
|
143
|
+
deploy)
|
|
144
|
+
if [[ "$cur" == -* ]]; then
|
|
145
|
+
COMPREPLY=( $(compgen -W "--execute --health-url --auto-rollback --help" -- "$cur") )
|
|
146
|
+
return 0
|
|
147
|
+
fi
|
|
148
|
+
;;
|
|
149
|
+
|
|
150
|
+
verify)
|
|
151
|
+
if [[ "$cur" == -* ]]; then
|
|
152
|
+
COMPREPLY=( $(compgen -W "--base --out --block-on --no-llm --json --explain --check-fresh --hosted --seal-composite --help" -- "$cur") )
|
|
153
|
+
return 0
|
|
154
|
+
fi
|
|
155
|
+
;;
|
|
156
|
+
|
|
139
157
|
chat)
|
|
140
158
|
if [[ "$cur" == -* ]]; then
|
|
141
159
|
COMPREPLY=( $(compgen -W "--attach --tui --help" -- "$cur") )
|
package/dashboard/__init__.py
CHANGED
|
@@ -4,16 +4,9 @@ Loki Mode Dashboard Backend
|
|
|
4
4
|
FastAPI-based dashboard for managing Loki Mode projects, tasks, and agents.
|
|
5
5
|
|
|
6
6
|
Modules:
|
|
7
|
-
control: Session control
|
|
7
|
+
control: Session control helpers (status, atomic writes, origin check)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "11.
|
|
10
|
+
__version__ = "11.7.1"
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
try:
|
|
14
|
-
from .control import app as control_app
|
|
15
|
-
except ImportError:
|
|
16
|
-
# FastAPI not installed, control module not available
|
|
17
|
-
control_app = None
|
|
18
|
-
|
|
19
|
-
__all__ = ["control_app", "__version__"]
|
|
12
|
+
__all__ = ["__version__"]
|