loki-mode 9.54.2 → 9.56.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/.github/actions/issue-to-pr/action.yml +165 -29
- package/.github/workflows/loki-issue-to-pr.yml +137 -24
- package/README.md +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +110 -34
- package/autonomy/crash.sh +6 -6
- package/autonomy/lib/done-recognition.sh +7 -0
- package/autonomy/lib/prd-enrich.sh +7 -0
- package/autonomy/lib/proof-check.sh +9 -0
- package/autonomy/lib/proof-generator.py +12 -3
- package/autonomy/lib/proof-verify.py +93 -21
- package/autonomy/lib/voter-agents.sh +7 -0
- package/autonomy/lib/workspace_diff.py +18 -7
- package/autonomy/loki +537 -122
- package/autonomy/quickstart.sh +7 -0
- package/autonomy/run.sh +956 -37
- package/autonomy/verify.sh +114 -8
- package/bin/loki +12 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_keys.py +12 -5
- package/dashboard/build_supervisor.py +25 -1
- package/dashboard/server.py +46 -24
- package/dashboard/static/index.html +57 -57
- package/docs/INSTALLATION.md +6 -5
- package/docs/VERIFIED-COMPLETION-PLAN.md +7 -5
- package/docs/air-gapped.md +39 -17
- package/docs/enterprise/security.md +10 -13
- package/docs/environment-variables.md +2 -0
- package/docs/exit-codes.md +76 -2
- package/docs/v10/BACKLOG.md +56 -0
- package/docs/v10/BOARD.md +118 -32
- package/docs/v10/CONTROL.md +36 -0
- package/docs/v10/DECISIONS.md +74 -0
- package/docs/v10/METRICS.md +78 -1
- package/docs/v10/PROGRESS.md +928 -0
- package/loki-ts/dist/loki.js +331 -327
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +1 -1
- package/providers/loader.sh +10 -0
- package/providers/model_catalog.json +12 -5
- package/providers/models.sh +1 -1
- package/references/core-workflow.md +1 -1
- package/tools/receipt-attest.py +1 -1
- package/web-app/dist/assets/{AdminPage-CZrBkzRF.js → AdminPage-Oyx-w2Ch.js} +1 -1
- package/web-app/dist/assets/{Avatar-gQIp6FBI.js → Avatar-D60m65JB.js} +1 -1
- package/web-app/dist/assets/{Badge-D2_fBTFY.js → Badge-CqfZNaem.js} +1 -1
- package/web-app/dist/assets/{Button-D5qDd2ef.js → Button-BqjzE4fz.js} +1 -1
- package/web-app/dist/assets/CockpitPage-Czk7o_ou.js +47 -0
- package/web-app/dist/assets/ComparePage-BEfTrTb1.js +1 -0
- package/web-app/dist/assets/{ErrorBoundary-BRABI-BT.js → ErrorBoundary-BrLk5nkO.js} +1 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-9vEpi5ir.js +1 -0
- package/web-app/dist/assets/{GitHubIssuesPanel-BC5GzKUN.js → GitHubIssuesPanel-D6izZx0w.js} +1 -1
- package/web-app/dist/assets/GitHubPRsPanel-BTkG2FgA.js +17 -0
- package/web-app/dist/assets/{HomePage-D_CDMoen.js → HomePage-nsr4kpN1.js} +2 -2
- package/web-app/dist/assets/{LoginPage-BBNA_N9t.js → LoginPage-BVblQrr9.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CnFRZCMx.js → MagicPage-CmnSyLq7.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CPfLMRlh.js → MetricsPage-BVfwU81j.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-lA0XSGTz.js → NotFoundPage-AYQ6uqqI.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CNAPbr9z.js → ProjectPage-DMnbw9X9.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-CMtm8goN.js → ProjectsPage-CNVTAF2N.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-BhIjVMYE.js → SettingsPage-D1_qasFD.js} +3 -3
- package/web-app/dist/assets/{ShowcasePage-BndtGeYo.js → ShowcasePage-2dBCjLXn.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-euXI-KXE.js → SystemSettingsPage-Djo6IE4u.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-ClUKecun.js → TeamsPage-CY8p-yxD.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-BWHWVk0L.js → TemplatesPage-B6iFgH6t.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Cv3N3Szf.js → TerminalOutput-B3yzZe-h.js} +1 -1
- package/web-app/dist/assets/{activity-dEhz-bXR.js → activity-CNfAjtRv.js} +1 -1
- package/web-app/dist/assets/{bell-B3XUMq1t.js → bell-CMETsHPH.js} +1 -1
- package/web-app/dist/assets/{bot-X-ssNlwR.js → bot-C9kXvk1i.js} +1 -1
- package/web-app/dist/assets/{check-C_9Nx7kb.js → check-BRK-zLB5.js} +1 -1
- package/web-app/dist/assets/{chevron-left-tvNswo3A.js → chevron-left-CxI5uD4g.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CSB88nEs.js → circle-alert-CVFRPWpv.js} +1 -1
- package/web-app/dist/assets/{clock-C8p2e4NI.js → clock-CLYV8a_x.js} +1 -1
- package/web-app/dist/assets/{cloud-CKp2ZvhG.js → cloud-CiISiIOa.js} +1 -1
- package/web-app/dist/assets/{code-xml-dY70LCHD.js → code-xml-DHsgqwV-.js} +1 -1
- package/web-app/dist/assets/{database-DObn5fmn.js → database-DbvUqtz3.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-DSu6n3K3.js → dollar-sign-Ct0lRPHw.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DpCnS73z.js → file-code-corner-D1sjFEh7.js} +1 -1
- package/web-app/dist/assets/{file-plus-D3IPKCf1.js → file-plus-DnmX18Fi.js} +1 -1
- package/web-app/dist/assets/{globe-C9QXkgH_.js → globe-bSpDNgoA.js} +1 -1
- package/web-app/dist/assets/{hammer-D_FtWe1y.js → hammer-Bj41Kvlq.js} +1 -1
- package/web-app/dist/assets/{index-CI54rara.css → index-BfQ0f3-S.css} +1 -1
- package/web-app/dist/assets/{index-Cz7q1bvC.js → index-CxajNJVu.js} +3 -3
- package/web-app/dist/assets/{layers-DsW7jnJX.js → layers-BNtBwYER.js} +1 -1
- package/web-app/dist/assets/{loader-circle-B3HspjKR.js → loader-circle-YY_VZofG.js} +1 -1
- package/web-app/dist/assets/{lock-DSI29pct.js → lock-rpjEPVoC.js} +1 -1
- package/web-app/dist/assets/{package-CLNxtA2x.js → package-desXU6XA.js} +1 -1
- package/web-app/dist/assets/{plus-DyI4WC4_.js → plus-7AoOU-I9.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-D2dv0xA0.js → refresh-cw-oNj-Xn4B.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CBvihA-a.js → rotate-ccw-DeiFjfVO.js} +1 -1
- package/web-app/dist/assets/{scroll-text-DvoOvAl_.js → scroll-text-Q9AzWiNV.js} +1 -1
- package/web-app/dist/assets/{server-DenWvSUt.js → server-DKgVwfek.js} +1 -1
- package/web-app/dist/assets/{shield-alert-40cX_FvG.js → shield-alert-G6x-bS-I.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-BrjZ6QuV.js → thumbs-up-Qhk0yvw0.js} +1 -1
- package/web-app/dist/assets/{trash-2-C6evwkEH.js → trash-2-D23gvznl.js} +1 -1
- package/web-app/dist/assets/{trending-down-CfbZis3O.js → trending-down-CSQClPrr.js} +1 -1
- package/web-app/dist/assets/{trending-up-DWuEK3WC.js → trending-up-sjMSd1wc.js} +1 -1
- package/web-app/dist/assets/{upload-B9DFBcPG.js → upload-Dchovhld.js} +1 -1
- package/web-app/dist/assets/{usePolling-BryVC-T1.js → usePolling-DwLceXwF.js} +1 -1
- package/web-app/dist/assets/{user-DMRXwZiw.js → user-BqZM8CmD.js} +1 -1
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +52 -17
- package/web-app/dist/assets/CockpitPage-DVMk2RUK.js +0 -47
- package/web-app/dist/assets/ComparePage-NMLyE1xL.js +0 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-D2LNKqUx.js +0 -1
- package/web-app/dist/assets/GitHubPRsPanel-BSlmoVTS.js +0 -17
package/autonomy/verify.sh
CHANGED
|
@@ -1689,14 +1689,26 @@ verify_gate_runtime() {
|
|
|
1689
1689
|
local boot_log="$artifact_dir/boot.log"
|
|
1690
1690
|
|
|
1691
1691
|
# Boot the app in the background, bounded by the timeout wrapper. The whole
|
|
1692
|
-
# process group is killed on teardown. Env is inherited (no secrets injected)
|
|
1693
|
-
#
|
|
1694
|
-
#
|
|
1695
|
-
# it
|
|
1692
|
+
# process group is killed on teardown. Env is inherited (no secrets injected)
|
|
1693
|
+
# PLUS one new marker: LOKI_VERIFY_RUN_TOKEN, a per-run random value used
|
|
1694
|
+
# ONLY as an ownership proof at teardown (see _verify_runtime_teardown) --
|
|
1695
|
+
# it identifies "a process this exact verify run launched" even after a
|
|
1696
|
+
# setsid()'d daemon detaches into its own session/process group, which
|
|
1697
|
+
# defeats every pgid/ppid-based check (round-4 council finding: v7.109.0's
|
|
1698
|
+
# daemonize-leak fixture uses child_process.spawn({detached:true}), and no
|
|
1699
|
+
# pgid/ppid arm can ever match a setsid'd process). Environment variables
|
|
1700
|
+
# survive setsid() and fork()/spawn() regardless of process-group changes,
|
|
1701
|
+
# so this closes exactly the gap those checks cannot. PORT is exported to
|
|
1702
|
+
# the resolved port so 12-factor apps (node, many python frameworks) listen
|
|
1703
|
+
# where the probe looks; apps that hardcode a port ignore it harmlessly.
|
|
1704
|
+
# This keeps boot-port and probe-port consistent.
|
|
1705
|
+
local run_token=""
|
|
1706
|
+
run_token="verify-$$-${RANDOM}-${RANDOM}-$(date +%s 2>/dev/null || echo 0)"
|
|
1696
1707
|
local app_pid=""
|
|
1697
1708
|
(
|
|
1698
1709
|
cd "$tree" || exit 127
|
|
1699
1710
|
export PORT="$port"
|
|
1711
|
+
export LOKI_VERIFY_RUN_TOKEN="$run_token"
|
|
1700
1712
|
exec "$timeout_bin" "$boot_timeout" sh -c "$method"
|
|
1701
1713
|
) >"$boot_log" 2>&1 &
|
|
1702
1714
|
app_pid=$!
|
|
@@ -1769,7 +1781,7 @@ verify_gate_runtime() {
|
|
|
1769
1781
|
# Reclaim BOTH the detected port and the actually-bound port (scraped from the
|
|
1770
1782
|
# boot log) so a server that daemonized onto a different port than we guessed
|
|
1771
1783
|
# does not leak. scraped_port may be empty (no banner) -- teardown handles it.
|
|
1772
|
-
_verify_runtime_teardown "$app_pid" "$port" "$scraped_port"
|
|
1784
|
+
_verify_runtime_teardown "$app_pid" "$port" "$scraped_port" "$run_token"
|
|
1773
1785
|
|
|
1774
1786
|
# Interpret the result.
|
|
1775
1787
|
if [ "$answered" != "true" ]; then
|
|
@@ -1907,8 +1919,34 @@ SMOKE_SCRIPT
|
|
|
1907
1919
|
|
|
1908
1920
|
# Teardown: terminate the boot launcher and any process still holding the port.
|
|
1909
1921
|
# Bounded and best effort; never blocks the gate.
|
|
1922
|
+
# Does pid's environment carry LOKI_VERIFY_RUN_TOKEN=<tok> exactly? Portable
|
|
1923
|
+
# ownership proof that survives setsid() (see the fourth arm below for why
|
|
1924
|
+
# pgid/ppid checks cannot). Fails CLOSED: an empty token, an unreadable
|
|
1925
|
+
# environment, or any ambiguity returns "no match" -- never a false positive
|
|
1926
|
+
# that would authorize a kill. Checked directly (not assumed) on this
|
|
1927
|
+
# codebase's target platforms: Linux via /proc/<pid>/environ (NUL-separated,
|
|
1928
|
+
# same-uid readable); macOS/BSD via `ps -E -ww -o command=` (the `-E` flag
|
|
1929
|
+
# specifically requests the environment block; `-e` on BSD ps means "all
|
|
1930
|
+
# processes" and shows nothing extra -- do not confuse the two).
|
|
1931
|
+
_verify_runtime_pid_has_token() {
|
|
1932
|
+
local pid="$1" tok="$2"
|
|
1933
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
1934
|
+
[ -n "$tok" ] || return 1
|
|
1935
|
+
if [ -r "/proc/$pid/environ" ]; then
|
|
1936
|
+
tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep -qxF "LOKI_VERIFY_RUN_TOKEN=$tok"
|
|
1937
|
+
return $?
|
|
1938
|
+
fi
|
|
1939
|
+
if command -v ps >/dev/null 2>&1; then
|
|
1940
|
+
case " $(ps -E -ww -o command= -p "$pid" 2>/dev/null) " in
|
|
1941
|
+
*" LOKI_VERIFY_RUN_TOKEN=$tok "*) return 0 ;;
|
|
1942
|
+
*) return 1 ;;
|
|
1943
|
+
esac
|
|
1944
|
+
fi
|
|
1945
|
+
return 1
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1910
1948
|
_verify_runtime_teardown() {
|
|
1911
|
-
local app_pid="$1" port="$2" scraped_port="${3:-}"
|
|
1949
|
+
local app_pid="$1" port="$2" scraped_port="${3:-}" run_token="${4:-}"
|
|
1912
1950
|
if [ -n "$app_pid" ]; then
|
|
1913
1951
|
# Best: kill the launcher's whole process GROUP so a server it spawned in
|
|
1914
1952
|
# a subshell (e.g. `vite &`) dies too. GUARDED against self-suicide: in a
|
|
@@ -1943,6 +1981,22 @@ _verify_runtime_teardown() {
|
|
|
1943
1981
|
# Reclaim BOTH the detected port and the actually-bound (scraped) port from
|
|
1944
1982
|
# any orphan that outlived the launcher -- a daemonized server that bound a
|
|
1945
1983
|
# different port than we guessed would otherwise leak. Bounded, best effort.
|
|
1984
|
+
#
|
|
1985
|
+
# SCOPING (D14/D15/D16 class): `lsof -ti tcp:$port` alone, with no
|
|
1986
|
+
# -sTCP:LISTEN filter, matches EVERY socket referencing that port number,
|
|
1987
|
+
# including a CLIENT with an established connection whose remote port
|
|
1988
|
+
# happens to be $port (e.g. a browser tab open to localhost:$port) -- that
|
|
1989
|
+
# process gets SIGKILL'd even though it never bound anything. And with no
|
|
1990
|
+
# ownership check at all, ANY process (this run's or not) listening on the
|
|
1991
|
+
# reused port gets killed. Fixed to (1) filter to LISTEN sockets only, and
|
|
1992
|
+
# (2) verify each candidate is actually part of THIS launcher's tree before
|
|
1993
|
+
# killing it: its pgid matches $child_pgid (the group this teardown just
|
|
1994
|
+
# tried to signal above), or its direct parent is $app_pid. A holder that
|
|
1995
|
+
# matches neither is left alone -- this run never bound that port, so it
|
|
1996
|
+
# never reclaims it. Trade-off: a server that called setsid() into a fresh
|
|
1997
|
+
# session AND was reparented away from app_pid (neither condition holds)
|
|
1998
|
+
# will leak here; failing closed on an unverified kill is the safer
|
|
1999
|
+
# direction (see docs/v10/DECISIONS.md D14/D15/D16).
|
|
1946
2000
|
if command -v lsof >/dev/null 2>&1; then
|
|
1947
2001
|
# Build a unique, non-empty port list (scraped only if it differs).
|
|
1948
2002
|
local _ports="$port"
|
|
@@ -1951,10 +2005,62 @@ _verify_runtime_teardown() {
|
|
|
1951
2005
|
for _rp in $_ports; do
|
|
1952
2006
|
[ -z "$_rp" ] && continue
|
|
1953
2007
|
local holders
|
|
1954
|
-
holders="$(lsof -ti tcp:"$_rp" 2>/dev/null || true)"
|
|
2008
|
+
holders="$(lsof -ti tcp:"$_rp" -sTCP:LISTEN 2>/dev/null || true)"
|
|
1955
2009
|
if [ -n "$holders" ]; then
|
|
1956
2010
|
printf '%s\n' "$holders" | while IFS= read -r pid; do
|
|
1957
|
-
[ -
|
|
2011
|
+
[ -z "$pid" ] && continue
|
|
2012
|
+
local _h_pgid="" _h_ppid=""
|
|
2013
|
+
_h_pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ' || true)"
|
|
2014
|
+
_h_ppid="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ' || true)"
|
|
2015
|
+
# THIRD ARM (council round 2, REJECT reproduced and fixed):
|
|
2016
|
+
# GNU `timeout` (this codebase's launcher, verify.sh:1699)
|
|
2017
|
+
# makes ITSELF the process-group leader, so the group's
|
|
2018
|
+
# pgid equals timeout's own pid, which equals app_pid (the
|
|
2019
|
+
# subshell execs straight into timeout with no fork). If
|
|
2020
|
+
# the launched command daemonizes -- forks a server and
|
|
2021
|
+
# the parent (timeout, then sh -c) exits -- by teardown
|
|
2022
|
+
# time: child_pgid is EMPTY (timeout already exited, so
|
|
2023
|
+
# `ps -o pgid= -p "$app_pid"` finds nothing); the orphan's
|
|
2024
|
+
# ppid is 1, not app_pid; but its pgid is STILL app_pid,
|
|
2025
|
+
# the value it inherited when timeout created the group.
|
|
2026
|
+
# Neither of the two arms above can ever match this case,
|
|
2027
|
+
# so the classic "daemonized" leak this function's own
|
|
2028
|
+
# comments describe was never reclaimed. Reproduced
|
|
2029
|
+
# directly: own daemon listener LEAKED pre-this-arm,
|
|
2030
|
+
# reclaimed after. Safe as an ownership proof, not a
|
|
2031
|
+
# loosening: a process group's pgid cannot be reassigned
|
|
2032
|
+
# to an unrelated group while any member of the original
|
|
2033
|
+
# group (here, the orphan itself, still alive and
|
|
2034
|
+
# reporting this exact pgid) still exists -- POSIX pgid
|
|
2035
|
+
# reuse is blocked until the whole group is empty.
|
|
2036
|
+
# FOURTH ARM (council round 4, 2 of 4 reviewers REJECTed
|
|
2037
|
+
# with an identical reproduction; verified independently
|
|
2038
|
+
# before applying): a process launched via
|
|
2039
|
+
# child_process.spawn({detached:true}) (Node's setsid()
|
|
2040
|
+
# wrapper) becomes its OWN session and process group
|
|
2041
|
+
# leader -- its pgid equals its OWN pid, not app_pid or
|
|
2042
|
+
# child_pgid, and its ppid is 1. None of the three arms
|
|
2043
|
+
# above can ever match that shape, which is exactly
|
|
2044
|
+
# test-runtime-gate.sh case I's fixture (the v7.109.0
|
|
2045
|
+
# daemonize-leak regression test) and a real pattern many
|
|
2046
|
+
# servers use to daemonize. Environment variables survive
|
|
2047
|
+
# setsid()/fork()/exec() regardless of process-group
|
|
2048
|
+
# changes, so LOKI_VERIFY_RUN_TOKEN (exported into the
|
|
2049
|
+
# launched app's env at boot, see the launch site above)
|
|
2050
|
+
# is checked here as a portability-verified ownership
|
|
2051
|
+
# proof (see _verify_runtime_pid_has_token; confirmed
|
|
2052
|
+
# directly on macOS with `ps -E -ww`, not assumed).
|
|
2053
|
+
# Residual, disclosed gap: a daemon that scrubs its own
|
|
2054
|
+
# environment (env -i, some process supervisors) before
|
|
2055
|
+
# detaching still leaks -- no ownership proof survives
|
|
2056
|
+
# that, and failing closed on an unverified kill remains
|
|
2057
|
+
# the safer direction.
|
|
2058
|
+
if { [ -n "$child_pgid" ] && [ -n "$_h_pgid" ] && [ "$_h_pgid" = "$child_pgid" ]; } \
|
|
2059
|
+
|| { [ -n "$app_pid" ] && [ -n "$_h_ppid" ] && [ "$_h_ppid" = "$app_pid" ]; } \
|
|
2060
|
+
|| { [ -n "$app_pid" ] && [ -n "$_h_pgid" ] && [ "$_h_pgid" = "$app_pid" ]; } \
|
|
2061
|
+
|| _verify_runtime_pid_has_token "$pid" "$run_token"; then
|
|
2062
|
+
kill -9 "$pid" 2>/dev/null || true
|
|
2063
|
+
fi
|
|
1958
2064
|
done
|
|
1959
2065
|
fi
|
|
1960
2066
|
done
|
package/bin/loki
CHANGED
|
@@ -348,6 +348,18 @@ if [ "${1:-}" = "trust" ]; then
|
|
|
348
348
|
done
|
|
349
349
|
fi
|
|
350
350
|
|
|
351
|
+
# `doctor --airgap` (egress audit) lives only in bash (_loki_airgap_audit).
|
|
352
|
+
# The Bun doctor rejected the flag, so the default route had no audit at all.
|
|
353
|
+
# Routed here, flag-anywhere, so the committed dist needs no rebuild and bash
|
|
354
|
+
# stays the single implementation; bash main() fires the one cli_command event.
|
|
355
|
+
if [ "${1:-}" = "doctor" ]; then
|
|
356
|
+
for _doctor_arg in "${@:2}"; do
|
|
357
|
+
if [ "$_doctor_arg" = "--airgap" ]; then
|
|
358
|
+
exec "$BASH_CLI" "$@"
|
|
359
|
+
fi
|
|
360
|
+
done
|
|
361
|
+
fi
|
|
362
|
+
|
|
351
363
|
# CLI consolidation (Phase B): `report kpis` is the canonical form of the
|
|
352
364
|
# Bun-only KPI snapshot. The `report` noun is otherwise bash-owned (every other
|
|
353
365
|
# subcommand -- session/metrics/cost/export/share/dogfood -- routes to bash
|
package/dashboard/__init__.py
CHANGED
package/dashboard/api_keys.py
CHANGED
|
@@ -254,6 +254,13 @@ def list_keys_with_details(include_rotating: bool = True) -> list[dict]:
|
|
|
254
254
|
Returns:
|
|
255
255
|
List of key detail dicts (no hashes or raw tokens).
|
|
256
256
|
"""
|
|
257
|
+
# Purge rotating keys past their grace period before listing. This is the
|
|
258
|
+
# only live read path over the full token store, so it doubles as the
|
|
259
|
+
# sweep for cleanup_expired_rotating_keys, which otherwise has no caller
|
|
260
|
+
# (BACKLOG 33). Lazy, not scheduled: runs on whatever cadence callers
|
|
261
|
+
# already poll this list at.
|
|
262
|
+
cleanup_expired_rotating_keys()
|
|
263
|
+
|
|
257
264
|
tokens = auth._load_tokens()
|
|
258
265
|
result = []
|
|
259
266
|
|
|
@@ -298,15 +305,15 @@ def cleanup_expired_rotating_keys() -> list[str]:
|
|
|
298
305
|
List of deleted key IDs.
|
|
299
306
|
"""
|
|
300
307
|
tokens = auth._load_tokens()
|
|
301
|
-
now = datetime.now(timezone.utc)
|
|
302
308
|
to_delete = []
|
|
303
309
|
|
|
304
310
|
for tid, entry in tokens["tokens"].items():
|
|
305
311
|
rotation_exp = entry.get("rotation_expires_at")
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
312
|
+
# auth._deadline_passed fails closed: an unparseable or naive
|
|
313
|
+
# timestamp counts as expired, matching what validate_token already
|
|
314
|
+
# rejects at validation time (BACKLOG 119).
|
|
315
|
+
if rotation_exp and auth._deadline_passed(rotation_exp):
|
|
316
|
+
to_delete.append(tid)
|
|
310
317
|
|
|
311
318
|
for tid in to_delete:
|
|
312
319
|
del tokens["tokens"][tid]
|
|
@@ -885,7 +885,31 @@ def _host_seatbelt_profile(
|
|
|
885
885
|
lines.append(
|
|
886
886
|
f'(deny network-outbound (remote tcp "*:{port}"))'
|
|
887
887
|
)
|
|
888
|
-
|
|
888
|
+
# network-bind uses "tcp4", not "tcp": confirmed on macOS 27.0 (26A428)
|
|
889
|
+
# that sandbox-exec's (local tcp "*:PORT") filter on network-bind never
|
|
890
|
+
# matches an actual bind (a bind to 127.0.0.1 or 0.0.0.0 on the denied
|
|
891
|
+
# port succeeds), while (local tcp4 "*:PORT") correctly denies it with
|
|
892
|
+
# a clean PermissionError. network-outbound's "tcp" filter above is
|
|
893
|
+
# unaffected (verified separately: an outbound connect to a denied
|
|
894
|
+
# port is still refused), so only this line changes.
|
|
895
|
+
#
|
|
896
|
+
# Known residual gap, same platform bug family, found while fixing
|
|
897
|
+
# the above (not introduced by it -- reproduced identically against
|
|
898
|
+
# the un-patched "tcp" rule too): on this same macOS 27.0 build, EVERY
|
|
899
|
+
# port-qualified network-bind filter for IPv6 ("tcp6 \"*:PORT\"" and
|
|
900
|
+
# "ip6 \"*:PORT\"") also fails to match, so an IPv6 bind to the
|
|
901
|
+
# reserved port (e.g. "::1"/"::") still succeeds even with a "tcp6"
|
|
902
|
+
# deny added. Only a port-unqualified "(deny network-bind (local
|
|
903
|
+
# ip6))" was confirmed to actually block IPv6 binds, which would deny
|
|
904
|
+
# ALL IPv6 binding (not just the reserved ports) -- a materially wider
|
|
905
|
+
# behavior change than this port-scoping fix, and nothing in this
|
|
906
|
+
# codebase binds IPv6 today (grepped: no AF_INET6/IPv6 usage). Left
|
|
907
|
+
# unfixed here to keep this change scoped to the failing test; the
|
|
908
|
+
# reserved ports remain enforced for IPv4 (the only family in active
|
|
909
|
+
# use), and a confined build could still self-expose a reserved port
|
|
910
|
+
# over IPv6 loopback/wildcard. Tracked as a follow-up, not silently
|
|
911
|
+
# closed.
|
|
912
|
+
lines.append(f'(deny network-bind (local tcp4 "*:{port}"))')
|
|
889
913
|
return ("\n".join(lines) + "\n").encode("utf-8")
|
|
890
914
|
|
|
891
915
|
|
package/dashboard/server.py
CHANGED
|
@@ -8614,16 +8614,27 @@ async def get_pricing():
|
|
|
8614
8614
|
# Completion Council API (v5.25.0)
|
|
8615
8615
|
# =============================================================================
|
|
8616
8616
|
|
|
8617
|
+
def council_state(loki_dir: _Path) -> dict:
|
|
8618
|
+
"""Read council state.json, distinguishing "no state yet" from "unreadable".
|
|
8619
|
+
|
|
8620
|
+
A missing state.json means the council genuinely has not recorded a vote:
|
|
8621
|
+
total_votes is really 0. A state.json that exists but fails to parse (torn
|
|
8622
|
+
write, corruption) means we do not know how many votes were recorded --
|
|
8623
|
+
total_votes must be None, not a fabricated 0 that reads as a real count.
|
|
8624
|
+
"""
|
|
8625
|
+
state_file = loki_dir / "council" / "state.json"
|
|
8626
|
+
if not state_file.exists():
|
|
8627
|
+
return {"enabled": False, "total_votes": 0, "verdicts": []}
|
|
8628
|
+
try:
|
|
8629
|
+
return json.loads(state_file.read_text())
|
|
8630
|
+
except Exception:
|
|
8631
|
+
return {"enabled": None, "total_votes": None, "verdicts": None, "error": "unreadable_state"}
|
|
8632
|
+
|
|
8633
|
+
|
|
8617
8634
|
@app.get("/api/council/state", dependencies=[Depends(auth.require_scope("read"))])
|
|
8618
8635
|
async def get_council_state():
|
|
8619
8636
|
"""Get current Completion Council state."""
|
|
8620
|
-
|
|
8621
|
-
if state_file.exists():
|
|
8622
|
-
try:
|
|
8623
|
-
return json.loads(state_file.read_text())
|
|
8624
|
-
except Exception:
|
|
8625
|
-
pass
|
|
8626
|
-
return {"enabled": False, "total_votes": 0, "verdicts": []}
|
|
8637
|
+
return council_state(_get_loki_dir())
|
|
8627
8638
|
|
|
8628
8639
|
|
|
8629
8640
|
@app.get("/api/council/verdicts", dependencies=[Depends(auth.require_scope("read"))])
|
|
@@ -8873,30 +8884,41 @@ async def get_context():
|
|
|
8873
8884
|
# Notification Trigger API (v5.40.0)
|
|
8874
8885
|
# =============================================================================
|
|
8875
8886
|
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8887
|
+
_EMPTY_NOTIFICATION_SUMMARY = {"total": 0, "unacknowledged": 0, "critical": 0, "warning": 0, "info": 0}
|
|
8888
|
+
|
|
8889
|
+
|
|
8890
|
+
def read_active_notifications(loki_dir: _Path) -> dict:
|
|
8891
|
+
"""Read notifications/active.json, distinguishing "none yet" from "unreadable".
|
|
8892
|
+
|
|
8893
|
+
A missing active.json means there genuinely are no active notifications:
|
|
8894
|
+
the zero summary is real. A file that exists but fails to parse (torn
|
|
8895
|
+
write, corruption) means we do not know what is active -- summary must be
|
|
8896
|
+
None, not a fabricated all-zero summary that reads as "nothing active".
|
|
8897
|
+
"""
|
|
8883
8898
|
active_file = loki_dir / "notifications" / "active.json"
|
|
8884
8899
|
|
|
8885
8900
|
if not active_file.exists():
|
|
8886
|
-
return {
|
|
8887
|
-
"notifications": [],
|
|
8888
|
-
"summary": {"total": 0, "unacknowledged": 0, "critical": 0, "warning": 0, "info": 0},
|
|
8889
|
-
}
|
|
8901
|
+
return {"notifications": [], "summary": dict(_EMPTY_NOTIFICATION_SUMMARY)}
|
|
8890
8902
|
|
|
8891
8903
|
try:
|
|
8892
8904
|
data = json.loads(active_file.read_text())
|
|
8893
8905
|
except (json.JSONDecodeError, OSError):
|
|
8894
|
-
return {
|
|
8895
|
-
"notifications": [],
|
|
8896
|
-
"summary": {"total": 0, "unacknowledged": 0, "critical": 0, "warning": 0, "info": 0},
|
|
8897
|
-
}
|
|
8906
|
+
return {"notifications": None, "summary": None, "error": "unreadable_active_notifications"}
|
|
8898
8907
|
|
|
8899
|
-
notifications
|
|
8908
|
+
return {"notifications": data.get("notifications", []), "summary": data.get("summary", {})}
|
|
8909
|
+
|
|
8910
|
+
|
|
8911
|
+
@app.get("/api/notifications", dependencies=[Depends(auth.require_scope("read"))])
|
|
8912
|
+
async def get_notifications(
|
|
8913
|
+
severity: Optional[str] = Query(None, pattern="^(critical|warning|info)$"),
|
|
8914
|
+
unread_only: bool = Query(False),
|
|
8915
|
+
):
|
|
8916
|
+
"""Get notification list from .loki/notifications/active.json."""
|
|
8917
|
+
result = read_active_notifications(_get_loki_dir())
|
|
8918
|
+
notifications = result["notifications"]
|
|
8919
|
+
|
|
8920
|
+
if notifications is None:
|
|
8921
|
+
return result
|
|
8900
8922
|
|
|
8901
8923
|
# Apply filters
|
|
8902
8924
|
if severity:
|
|
@@ -8906,7 +8928,7 @@ async def get_notifications(
|
|
|
8906
8928
|
|
|
8907
8929
|
return {
|
|
8908
8930
|
"notifications": notifications,
|
|
8909
|
-
"summary":
|
|
8931
|
+
"summary": result["summary"],
|
|
8910
8932
|
}
|
|
8911
8933
|
|
|
8912
8934
|
|