loki-mode 9.55.0 → 9.57.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/proof-check.sh +9 -0
- package/autonomy/lib/proof-generator.py +12 -3
- package/autonomy/lib/workspace_diff.py +18 -7
- package/autonomy/loki +133 -3
- package/autonomy/run.sh +385 -22
- 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/VERIFIED-COMPLETION-PLAN.md +7 -5
- package/docs/dev/architecture-reference.md +99 -0
- package/docs/dev/project-structure.md +134 -0
- package/docs/dev/release-checklist.md +157 -0
- package/docs/dev/research-foundation.md +29 -0
- package/docs/dev/tmp-cleanup.md +157 -0
- package/docs/environment-variables.md +1 -0
- package/docs/v10/BACKLOG.md +33 -0
- package/docs/v10/BOARD.md +97 -27
- package/docs/v10/DECISIONS.md +43 -0
- package/docs/v10/OPERATING-MODEL.md +47 -0
- package/docs/v10/PROGRESS.md +893 -0
- package/loki-ts/dist/loki.js +326 -325
- 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/model_catalog.json +12 -5
- package/providers/models.sh +1 -1
- package/references/core-workflow.md +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/run.sh
CHANGED
|
@@ -5338,6 +5338,62 @@ except Exception:
|
|
|
5338
5338
|
return 0
|
|
5339
5339
|
}
|
|
5340
5340
|
|
|
5341
|
+
#===============================================================================
|
|
5342
|
+
# Rule of Two (moat P9): a GitHub token never reaches an agent session.
|
|
5343
|
+
#
|
|
5344
|
+
# The run reads untrusted issue text and spawns agents that act on it, so a
|
|
5345
|
+
# token in the exported environment is one prompt injection away from a push.
|
|
5346
|
+
# main() un-exports every GitHub token it inherited (the shell keeps the value).
|
|
5347
|
+
# Every provider, reviewer, helper and test the agent writes then starts without
|
|
5348
|
+
# it. Loki's own trusted GitHub calls get it back for the one command:
|
|
5349
|
+
# - direct `gh ...` calls, through the gh() wrapper defined below;
|
|
5350
|
+
# - _loki_with_github_tokens <cmd...>, for the post-session push/PR paths.
|
|
5351
|
+
# LOKI_ALLOW_AGENT_GITHUB_TOKEN=1 (exact value) restores the old inheritance
|
|
5352
|
+
# and prints one stderr warning that the agent holds the token.
|
|
5353
|
+
# This is hygiene against a naive injection, not an isolation boundary: code
|
|
5354
|
+
# running as the same user can still read the parent's environment block
|
|
5355
|
+
# (/proc/<pid>/environ on Linux, sudo on a hosted runner). The boundary is a
|
|
5356
|
+
# CI job that holds no write token while the agent runs (see
|
|
5357
|
+
# .github/workflows/loki-issue-to-pr.yml).
|
|
5358
|
+
#===============================================================================
|
|
5359
|
+
_LOKI_WITHHELD_TOKENS=""
|
|
5360
|
+
|
|
5361
|
+
_loki_with_github_tokens() {
|
|
5362
|
+
local _v _rc=0
|
|
5363
|
+
for _v in $_LOKI_WITHHELD_TOKENS; do export "${_v?}"; done
|
|
5364
|
+
"$@" || _rc=$?
|
|
5365
|
+
for _v in $_LOKI_WITHHELD_TOKENS; do export -n "${_v?}"; done
|
|
5366
|
+
return "$_rc"
|
|
5367
|
+
}
|
|
5368
|
+
|
|
5369
|
+
_loki_withhold_github_tokens() {
|
|
5370
|
+
local _v _held=""
|
|
5371
|
+
# Operator opt-out (exact value 1): keep the earlier behavior, where the
|
|
5372
|
+
# agent inherits the token. That is a Rule of Two exposure, so say so.
|
|
5373
|
+
if [ "${LOKI_ALLOW_AGENT_GITHUB_TOKEN:-}" = "1" ]; then
|
|
5374
|
+
for _v in GH_TOKEN GITHUB_TOKEN GH_ENTERPRISE_TOKEN GITHUB_ENTERPRISE_TOKEN; do
|
|
5375
|
+
[ -n "${!_v:-}" ] && _held="${_held:+$_held }$_v"
|
|
5376
|
+
done
|
|
5377
|
+
[ -z "$_held" ] || printf '%s\n' "WARNING: LOKI_ALLOW_AGENT_GITHUB_TOKEN=1: the agent session holds the GitHub token ($_held); an injected prompt can push with it (Rule of Two exposure)." >&2
|
|
5378
|
+
return 0
|
|
5379
|
+
fi
|
|
5380
|
+
for _v in GH_TOKEN GITHUB_TOKEN GH_ENTERPRISE_TOKEN GITHUB_ENTERPRISE_TOKEN; do
|
|
5381
|
+
[ -n "${!_v:-}" ] || continue
|
|
5382
|
+
export -n "${_v?}"
|
|
5383
|
+
case " $_LOKI_WITHHELD_TOKENS " in
|
|
5384
|
+
*" $_v "*) ;;
|
|
5385
|
+
*) _LOKI_WITHHELD_TOKENS="${_LOKI_WITHHELD_TOKENS:+$_LOKI_WITHHELD_TOKENS }$_v" ;;
|
|
5386
|
+
esac
|
|
5387
|
+
done
|
|
5388
|
+
[ -n "$_LOKI_WITHHELD_TOKENS" ] || return 0
|
|
5389
|
+
# Only when the binary exists, so `command -v gh` keeps meaning "gh is
|
|
5390
|
+
# installed" for every caller that checks it.
|
|
5391
|
+
if command -v gh >/dev/null 2>&1; then
|
|
5392
|
+
gh() { _loki_with_github_tokens command gh "$@"; }
|
|
5393
|
+
fi
|
|
5394
|
+
log_info "Withheld from agent sessions (Rule of Two): $_LOKI_WITHHELD_TOKENS. Loki's own push and PR steps still use it."
|
|
5395
|
+
}
|
|
5396
|
+
|
|
5341
5397
|
#===============================================================================
|
|
5342
5398
|
# on_run_complete (Slice 3: opt-in local git output on success)
|
|
5343
5399
|
#
|
|
@@ -5403,11 +5459,14 @@ on_run_complete() {
|
|
|
5403
5459
|
# timeout is not installed (a local wrapper keeps this set -u safe on bash
|
|
5404
5460
|
# 3.2, where an empty array expansion would error). Keeps every existing
|
|
5405
5461
|
# `|| true` non-fatal behavior.
|
|
5462
|
+
# The push and gh calls below are the trusted post-session step, so they
|
|
5463
|
+
# get the withheld GitHub token back (a plain `timeout 30 gh` would exec
|
|
5464
|
+
# the binary and bypass the gh() wrapper).
|
|
5406
5465
|
_loki_net() {
|
|
5407
5466
|
if command -v timeout >/dev/null 2>&1; then
|
|
5408
|
-
timeout 30 "$@"
|
|
5467
|
+
_loki_with_github_tokens timeout 30 "$@"
|
|
5409
5468
|
else
|
|
5410
|
-
"$@"
|
|
5469
|
+
_loki_with_github_tokens "$@"
|
|
5411
5470
|
fi
|
|
5412
5471
|
}
|
|
5413
5472
|
# Require gh + auth.
|
|
@@ -9159,10 +9218,27 @@ setup_agent_branch() {
|
|
|
9159
9218
|
|
|
9160
9219
|
# Record the user's own untracked and gitignored files so
|
|
9161
9220
|
# commit_session_changes and the receipt leave them alone.
|
|
9162
|
-
local snap_ok=1 leftover=""
|
|
9221
|
+
local snap_ok=1 leftover="" gittool=""
|
|
9163
9222
|
if ! _loki_snapshot_or_fail_closed; then
|
|
9164
9223
|
snap_ok=0
|
|
9165
|
-
rm -f .loki/state/preexisting-untracked.
|
|
9224
|
+
rm -f .loki/state/preexisting-untracked.sha.z 2>/dev/null
|
|
9225
|
+
# BACKLOG 88: the receipt (workspace_diff._preexisting_untracked) reads
|
|
9226
|
+
# preexisting-untracked.z on its own, independent of
|
|
9227
|
+
# _LOKI_SNAPSHOT_THIS_RUN below. Leaving it absent after a failed
|
|
9228
|
+
# snapshot (old code: `rm -f` here too) makes the receipt treat EVERY
|
|
9229
|
+
# currently-untracked file as this run's own work, even though nothing
|
|
9230
|
+
# is actually committed -- a pre-existing file the user never touched
|
|
9231
|
+
# gets named as the run's. `ls-files --others --exclude-standard` needs
|
|
9232
|
+
# no --no-renames or --ignored=matching, so it still works on the git
|
|
9233
|
+
# that just failed the richer `status` call above, giving the receipt a
|
|
9234
|
+
# names-only fallback list. Ignored files are not covered by this
|
|
9235
|
+
# fallback (old-git territory), so it is partial, not a full snapshot;
|
|
9236
|
+
# the commit path is untouched (snap_ok stays 0, _LOKI_SNAPSHOT_THIS_RUN
|
|
9237
|
+
# is never set below), so this still fails closed.
|
|
9238
|
+
gittool="$(_loki_snapshot_git_tool)" && \
|
|
9239
|
+
"$gittool" ls-files --others --exclude-standard -z > .loki/state/preexisting-untracked.z.tmp 2>/dev/null && \
|
|
9240
|
+
mv .loki/state/preexisting-untracked.z.tmp .loki/state/preexisting-untracked.z \
|
|
9241
|
+
|| rm -f .loki/state/preexisting-untracked.z .loki/state/preexisting-untracked.z.tmp 2>/dev/null
|
|
9166
9242
|
log_warn "Could not record pre-existing untracked files; this session will commit nothing (review and commit manually)"
|
|
9167
9243
|
fi
|
|
9168
9244
|
|
|
@@ -9180,6 +9256,12 @@ setup_agent_branch() {
|
|
|
9180
9256
|
log_warn "Left uncommitted from an earlier unfinished session, now treated as your files: $leftover"
|
|
9181
9257
|
fi
|
|
9182
9258
|
rm -f .loki/state/session-created.z 2>/dev/null
|
|
9259
|
+
# BACKLOG 130(c): seal the now-empty session-created.z record so this
|
|
9260
|
+
# fresh mint's OWN in-process memory of "absent" is what every later
|
|
9261
|
+
# record call and commit_session_changes verify against for the rest of
|
|
9262
|
+
# this run -- not the file's on-disk content, which an agent process could
|
|
9263
|
+
# otherwise repopulate with a planted path before this run ever trusts it.
|
|
9264
|
+
_loki_session_created_seal # BACKLOG-130C-MINT-SEAL
|
|
9183
9265
|
# BACKLOG 68 rework: a fresh mint starts with no tracked-since anchor and no
|
|
9184
9266
|
# in-flight marker (never seed the anchor from session-start-sha -- an
|
|
9185
9267
|
# anchor==start would let a later resume's "anchor..HEAD" diff include this
|
|
@@ -9236,6 +9318,40 @@ _loki_snapshot_or_fail_closed() {
|
|
|
9236
9318
|
# ~9165) so that path is never reported as tampering.
|
|
9237
9319
|
_LOKI_SNAPSHOT_SEAL=""
|
|
9238
9320
|
|
|
9321
|
+
# BACKLOG 130(c): .loki/state/session-created.z is NOT one of the two files
|
|
9322
|
+
# _LOKI_SNAPSHOT_SEAL above covers. It records the paths THIS session created
|
|
9323
|
+
# (unioned after every provider turn by _loki_record_session_created), and
|
|
9324
|
+
# _loki_snapshot_preexisting's resume union (line ~9683) trusts its content
|
|
9325
|
+
# directly: any path it lists is EXCLUDED from the new preexisting-untracked.z
|
|
9326
|
+
# snapshot, i.e. treated as "the session's own, never the user's". An agent
|
|
9327
|
+
# that plants a user's file path into session-created.z therefore gets that
|
|
9328
|
+
# exact path permanently exempted from pre-existing-file protection on the
|
|
9329
|
+
# next resume -- the file is swept into `git add -A` and the session commit as
|
|
9330
|
+
# if the session had created it. A separate seal, sibling to
|
|
9331
|
+
# _LOKI_SNAPSHOT_SEAL, closes this WITHIN one process's lifetime: sealed right
|
|
9332
|
+
# after every legitimate write this process makes (a successful record, the
|
|
9333
|
+
# fresh-mint delete, and this process's first look at the file on resume),
|
|
9334
|
+
# verified before any LATER point in the SAME process trusts the content,
|
|
9335
|
+
# fail-closed (delete the file, clear the seal) on a mismatch. Kept SEPARATE
|
|
9336
|
+
# from _LOKI_SNAPSHOT_SEAL rather than folded into one combined digest: this
|
|
9337
|
+
# file legitimately changes every provider turn, so a shared digest would
|
|
9338
|
+
# falsely flag tampering on every normal turn, and the fresh-mint `rm` (line
|
|
9339
|
+
# ~9241) runs after preexisting-untracked.z is sealed, which would falsely
|
|
9340
|
+
# flag every fresh mint too.
|
|
9341
|
+
# ACCEPTED GAP (disclosure, not prevention, same bar as BACKLOG 70's own): a
|
|
9342
|
+
# seal lives only in this process's memory, so it cannot verify content a
|
|
9343
|
+
# PRIOR process wrote before this one starts (setup_agent_branch's resume path
|
|
9344
|
+
# seals its own first read of the file rather than verifying it, since no
|
|
9345
|
+
# prior seal survives to check against) -- tampering already on disk before
|
|
9346
|
+
# this process begins is trusted at that one read, same as
|
|
9347
|
+
# _LOKI_SNAPSHOT_SEAL's own resume-time trust of preexisting-untracked.z's
|
|
9348
|
+
# prior content. What this DOES close: an agent process co-located with THIS
|
|
9349
|
+
# run can no longer plant a path into session-created.z between this run's own
|
|
9350
|
+
# writes and its own later reads (the resume union's exclude use, the next
|
|
9351
|
+
# record's merge, or the commit-time gate) without it being caught before that
|
|
9352
|
+
# content is trusted.
|
|
9353
|
+
_LOKI_SESSION_CREATED_SEAL=""
|
|
9354
|
+
|
|
9239
9355
|
# BACKLOG 129 (follow-up to BACKLOG 70/BACKLOG 70-awk): the DIGEST computation
|
|
9240
9356
|
# above is hardened (fixed-root-path-first, no awk), but until this function
|
|
9241
9357
|
# was added the snapshot WRITER (_loki_untracked_merge) and READER
|
|
@@ -9421,11 +9537,15 @@ _loki_snapshot_hash_tool() {
|
|
|
9421
9537
|
# which a user-writable directory ahead of the system dirs (e.g. Homebrew's
|
|
9422
9538
|
# prefix on macOS) can shadow -- not fully closed, only narrowed to hosts
|
|
9423
9539
|
# lacking the standard tool paths; the python3 fallback below, kept only for
|
|
9424
|
-
# the extremely rare host with neither tool anywhere, is
|
|
9425
|
-
#
|
|
9426
|
-
#
|
|
9427
|
-
#
|
|
9428
|
-
#
|
|
9540
|
+
# the extremely rare host with neither tool anywhere, is resolved via
|
|
9541
|
+
# _loki_snapshot_py_tool and run with -I -S (BACKLOG 131(b) -- see the comment
|
|
9542
|
+
# at that fallback for why -S, not just -I, is required to close the .pth
|
|
9543
|
+
# class), so it carries the same fixed-root-path-first resolution and
|
|
9544
|
+
# site-packages exclusion as the other hardened call sites in this file. It
|
|
9545
|
+
# still inherits _loki_snapshot_py_tool's own accepted gaps (documented at
|
|
9546
|
+
# that function): replacing the interpreter AT ONE OF ITS FIXED PATHS requires
|
|
9547
|
+
# root, and its own PATH-walk fallback still trusts whatever sits first on an
|
|
9548
|
+
# absolute PATH entry on a host lacking those fixed paths.
|
|
9429
9549
|
_loki_snapshot_digest() {
|
|
9430
9550
|
local tool
|
|
9431
9551
|
tool="$(_loki_snapshot_hash_tool)" || tool=""
|
|
@@ -9464,10 +9584,17 @@ _loki_snapshot_digest() {
|
|
|
9464
9584
|
return 0
|
|
9465
9585
|
fi
|
|
9466
9586
|
# Fallback: neither sha256sum nor shasum found anywhere checked above
|
|
9467
|
-
# (extremely rare on macOS/Linux).
|
|
9468
|
-
#
|
|
9469
|
-
#
|
|
9470
|
-
#
|
|
9587
|
+
# (extremely rare on macOS/Linux). Resolved via _loki_snapshot_py_tool
|
|
9588
|
+
# (fixed root-owned path first) and run with -I -S, the same BACKLOG 129
|
|
9589
|
+
# pattern used at _loki_untracked_merge / _loki_covered_paths: -I alone is
|
|
9590
|
+
# NOT enough -- a reviewer reproduced, on a sibling function in this same
|
|
9591
|
+
# tamper-detection system, that a .pth file planted in Homebrew python3's
|
|
9592
|
+
# own (user-writable) site-packages directory still fires under `python3
|
|
9593
|
+
# -I` alone, since -I implies -s (skip user site-packages) but does not
|
|
9594
|
+
# skip the resolved interpreter's OWN site-packages. Only -S (skip ALL
|
|
9595
|
+
# site-packages, including the interpreter's own) closes that. A resolve
|
|
9596
|
+
# failure degrades this fallback to printing nothing, same as any other
|
|
9597
|
+
# failure path here (caller treats empty as "could not seal"). Additional
|
|
9471
9598
|
# accepted gap specific to this fallback (not present on the tool-based
|
|
9472
9599
|
# path above, which distinguishes "-" from "?"): OSError covers BOTH
|
|
9473
9600
|
# "file does not exist" and "file exists but is unreadable" here, so an
|
|
@@ -9478,7 +9605,9 @@ _loki_snapshot_digest() {
|
|
|
9478
9605
|
# for present-but-unreadable) is straightforward but left undone here: this
|
|
9479
9606
|
# fallback only runs on a host with neither sha256sum nor shasum anywhere,
|
|
9480
9607
|
# already the rare case this whole function treats as a residual gap.
|
|
9481
|
-
|
|
9608
|
+
local pytool=""
|
|
9609
|
+
pytool="$(_loki_snapshot_py_tool)" || return 1
|
|
9610
|
+
"$pytool" -I -S -c 'import sys, hashlib
|
|
9482
9611
|
sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
9483
9612
|
out = []
|
|
9484
9613
|
for path in sys.argv[1:]:
|
|
@@ -9491,6 +9620,155 @@ print(" ".join(out))' \
|
|
|
9491
9620
|
".loki/state/preexisting-untracked.z" ".loki/state/preexisting-untracked.sha.z" 2>/dev/null
|
|
9492
9621
|
}
|
|
9493
9622
|
|
|
9623
|
+
# _loki_session_created_digest: sha256 of session-created.z alone ("-" for
|
|
9624
|
+
# absent, "?" for present-but-unreadable/unhashable, same sentinel contract as
|
|
9625
|
+
# _loki_snapshot_digest above). Deliberately a thin sibling, not a parameterized
|
|
9626
|
+
# reuse of _loki_snapshot_digest: that function's exact text (the `for path in
|
|
9627
|
+
# ...` line and the python fallback's trailing argv line) is matched by
|
|
9628
|
+
# tests/test-branch-lifecycle.sh's RED mutation harness, so leaving it
|
|
9629
|
+
# untouched keeps that harness's anchors valid. Resolved via the SAME hardened
|
|
9630
|
+
# tool resolvers (_loki_snapshot_hash_tool, _loki_snapshot_py_tool) as the
|
|
9631
|
+
# sibling above -- no fresh/unhardened python3 or shasum call site.
|
|
9632
|
+
_loki_session_created_digest() {
|
|
9633
|
+
local tool
|
|
9634
|
+
tool="$(_loki_snapshot_hash_tool)" || tool=""
|
|
9635
|
+
local path=".loki/state/session-created.z"
|
|
9636
|
+
# A FIFO or ANY symlink at $path (dangling or not -- [ -L ] rejects both,
|
|
9637
|
+
# deliberately broader than just the dangling case: the only way this
|
|
9638
|
+
# file is ever legitimately written is _loki_untracked_merge's own
|
|
9639
|
+
# write-to-.tmp-then-`mv -f`, which always leaves a plain regular file at
|
|
9640
|
+
# $path, never a symlink, so a symlink here is never this session's own
|
|
9641
|
+
# legitimate output) is neither absent ([ -f ] is false for both) nor a
|
|
9642
|
+
# hashable regular file -- falling through to the absent-file branch below
|
|
9643
|
+
# would print the same "-" sentinel every fresh mint already seals as its
|
|
9644
|
+
# normal starting state, silently trusting an agent-planted FIFO (whose
|
|
9645
|
+
# blocked writer feeds content into the merge's later open() call) or a
|
|
9646
|
+
# dangling symlink (whose target read fails and is treated as "-" too).
|
|
9647
|
+
# Caught here, before either digest branch, with a sentinel ("?") that can
|
|
9648
|
+
# never equal "-", a real hex digest, or empty.
|
|
9649
|
+
if [ -L "$path" ] || { [ -e "$path" ] && [ ! -f "$path" ]; }; then printf '%s\n' "?"; return 0; fi # BACKLOG-130C-NONREG-CHECK
|
|
9650
|
+
if [ -n "$tool" ]; then
|
|
9651
|
+
local h=""
|
|
9652
|
+
if [ -f "$path" ]; then
|
|
9653
|
+
case "${tool##*/}" in
|
|
9654
|
+
shasum) h="$("$tool" -a 256 -- "$path" 2>/dev/null)" || h="" ;;
|
|
9655
|
+
*) h="$("$tool" -- "$path" 2>/dev/null)" || h="" ;;
|
|
9656
|
+
esac
|
|
9657
|
+
h="${h%% *}"
|
|
9658
|
+
if [ "${#h}" -eq 64 ]; then
|
|
9659
|
+
case "$h" in
|
|
9660
|
+
*[!0123456789abcdef]*) h="?" ;;
|
|
9661
|
+
esac
|
|
9662
|
+
else
|
|
9663
|
+
h="?"
|
|
9664
|
+
fi
|
|
9665
|
+
printf '%s\n' "$h"
|
|
9666
|
+
else
|
|
9667
|
+
printf '%s\n' "-"
|
|
9668
|
+
fi
|
|
9669
|
+
return 0
|
|
9670
|
+
fi
|
|
9671
|
+
# Fallback: neither sha256sum nor shasum found anywhere (same rare-host
|
|
9672
|
+
# case _loki_snapshot_digest's own fallback documents). Resolved via
|
|
9673
|
+
# _loki_snapshot_py_tool, run with -I -S (see that function's comment for
|
|
9674
|
+
# why -S, not just -I, is required to close the .pth vector).
|
|
9675
|
+
local pytool=""
|
|
9676
|
+
pytool="$(_loki_snapshot_py_tool)" || return 1
|
|
9677
|
+
"$pytool" -I -S -c 'import sys, hashlib
|
|
9678
|
+
sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
9679
|
+
try:
|
|
9680
|
+
with open(sys.argv[1], "rb") as fh:
|
|
9681
|
+
print(hashlib.sha256(fh.read()).hexdigest())
|
|
9682
|
+
except OSError:
|
|
9683
|
+
print("-")' \
|
|
9684
|
+
"$path" 2>/dev/null
|
|
9685
|
+
}
|
|
9686
|
+
|
|
9687
|
+
# _loki_session_created_seal: record this process's own memory of the current
|
|
9688
|
+
# session-created.z content, right after every point that legitimately changes
|
|
9689
|
+
# it (a successful union in _loki_record_session_created, the fresh-mint
|
|
9690
|
+
# delete, a resume, and the post-commit/no-op delete in
|
|
9691
|
+
# commit_session_changes). Same fail-closed shape as _loki_snapshot_seal: an
|
|
9692
|
+
# empty or "?" digest at seal time is itself suspicious (a legitimate write or
|
|
9693
|
+
# delete should never immediately be unreadable), so it is rejected rather
|
|
9694
|
+
# than sealed, which would let seal and a later verify silently agree on the
|
|
9695
|
+
# sentinel and disarm the guard for the rest of the session.
|
|
9696
|
+
_loki_session_created_seal() {
|
|
9697
|
+
local d
|
|
9698
|
+
d="$(_loki_session_created_digest)"
|
|
9699
|
+
case "$d" in
|
|
9700
|
+
""|*'?'*)
|
|
9701
|
+
# Fail closed, not just "untrusted": an empty seal makes
|
|
9702
|
+
# _loki_session_created_verify's `[ -n "$_LOKI_SESSION_CREATED_SEAL" ]
|
|
9703
|
+
# || return 0` skip every later check for the rest of this
|
|
9704
|
+
# process's life. Leaving the file on disk here would fail OPEN --
|
|
9705
|
+
# unreadable-but-present content would then be silently trusted by
|
|
9706
|
+
# every later read site with no seal ever able to catch it. Delete
|
|
9707
|
+
# it instead, matching _loki_session_created_verify's own
|
|
9708
|
+
# fail-closed action on a real mismatch -- and, like that function's
|
|
9709
|
+
# own re-seal after its delete, set the seal to the digest's real
|
|
9710
|
+
# "absent" sentinel ("-"), not empty: an EMPTY seal here re-arms
|
|
9711
|
+
# nothing (the guard above short-circuits as "never sealed" on
|
|
9712
|
+
# every later check for the rest of this process), so an agent that
|
|
9713
|
+
# recreates the file with a planted path in this window would get
|
|
9714
|
+
# it silently merged in as trusted on the next successful record.
|
|
9715
|
+
# "-" is a real, non-empty seal value that the next verify's live
|
|
9716
|
+
# digest of a freshly-recreated file cannot match, keeping the
|
|
9717
|
+
# guard armed against exactly that recreation.
|
|
9718
|
+
rm -f .loki/state/session-created.z 2>/dev/null
|
|
9719
|
+
_LOKI_SESSION_CREATED_SEAL="-"
|
|
9720
|
+
log_warn "Could not seal the session-created record (hash tool failed or produced no readable digest); discarding it (its files are treated as yours, left uncommitted)"
|
|
9721
|
+
audit_log "SESSION_CREATED_SEAL_FAILED" "_loki_session_created_digest returned empty or an unreadable-file sentinel at seal time"
|
|
9722
|
+
;;
|
|
9723
|
+
*)
|
|
9724
|
+
_LOKI_SESSION_CREATED_SEAL="$d"
|
|
9725
|
+
;;
|
|
9726
|
+
esac
|
|
9727
|
+
}
|
|
9728
|
+
|
|
9729
|
+
# _loki_session_created_verify: compare the live session-created.z digest
|
|
9730
|
+
# against the sealed one. No seal recorded (never sealed this process, or
|
|
9731
|
+
# deliberately cleared) is not tampering: returns success, does nothing --
|
|
9732
|
+
# EVERY caller that trusts session-created.z's content must therefore have
|
|
9733
|
+
# sealed it first (mint, resume, and after each successful record all do). A
|
|
9734
|
+
# seal that no longer matches IS tampering: logs and discloses it (same
|
|
9735
|
+
# DISCLOSURE bar as BACKLOG 70, not prevention against a same-UID process), and
|
|
9736
|
+
# fails closed by deleting the untrusted file outright -- its entries then
|
|
9737
|
+
# become "the user's, not recorded as this session's", the safe direction,
|
|
9738
|
+
# since a false claim of user ownership only leaves a file uncommitted on disk,
|
|
9739
|
+
# never deletes it, while a false claim of session ownership can get a user's
|
|
9740
|
+
# file swept into a commit and removed from disk on a later checkout of the
|
|
9741
|
+
# base. Called BEFORE any later point in the SAME process treats the file's
|
|
9742
|
+
# content as trustworthy (verify-before-trust, not verify-after-use):
|
|
9743
|
+
# _loki_record_session_created verifies at its very top, before its own
|
|
9744
|
+
# self-referential merge (base="$out"), and unconditionally -- not gated on
|
|
9745
|
+
# _LOKI_SNAPSHOT_THIS_RUN, since an interrupt/cleanup call can still fire after
|
|
9746
|
+
# that flag was cleared mid-session; commit_session_changes verifies at its
|
|
9747
|
+
# very top, before every one of its several early returns (the untrack
|
|
9748
|
+
# failure, the git 2.25 unstage failure, a secret-scan abort, a failed `git
|
|
9749
|
+
# commit`) that would otherwise skip its own rm -f cleanup and leave a
|
|
9750
|
+
# tampered file on disk for the next resume. _loki_resume_snapshot does NOT
|
|
9751
|
+
# call this function: it is this process's FIRST read of a file the PREVIOUS
|
|
9752
|
+
# (dead) process wrote, so there is no seal from that process to verify
|
|
9753
|
+
# against here; it instead calls _loki_session_created_seal directly to
|
|
9754
|
+
# establish THIS process's own baseline before anything (including
|
|
9755
|
+
# _loki_snapshot_preexisting's resume-union exclude use, right after) reads
|
|
9756
|
+
# the file. See the ACCEPTED GAP note at _LOKI_SESSION_CREATED_SEAL above.
|
|
9757
|
+
_loki_session_created_verify() {
|
|
9758
|
+
local live="" path=".loki/state/session-created.z"
|
|
9759
|
+
[ -n "$_LOKI_SESSION_CREATED_SEAL" ] || return 0
|
|
9760
|
+
live="$(_loki_session_created_digest)"
|
|
9761
|
+
if [ "$live" = "$_LOKI_SESSION_CREATED_SEAL" ]; then
|
|
9762
|
+
return 0
|
|
9763
|
+
fi
|
|
9764
|
+
log_warn "session-created.z changed after this session recorded it (possible tampering by an agent process); discarding the record (its files are treated as yours, left uncommitted)"
|
|
9765
|
+
audit_log "SESSION_CREATED_TAMPERED" "session-created.z changed after being sealed: live=$live,sealed=$_LOKI_SESSION_CREATED_SEAL"
|
|
9766
|
+
rm -f "$path" 2>/dev/null
|
|
9767
|
+
_LOKI_SESSION_CREATED_SEAL=""
|
|
9768
|
+
_loki_session_created_seal
|
|
9769
|
+
return 1
|
|
9770
|
+
}
|
|
9771
|
+
|
|
9494
9772
|
# _loki_snapshot_seal: record this process's own memory of what it just wrote.
|
|
9495
9773
|
# Called only right after a successful _loki_snapshot_preexisting. Minor
|
|
9496
9774
|
# reviewer note: a legitimate snapshot write should never hit an unreadable-
|
|
@@ -9556,10 +9834,13 @@ _loki_snapshot_verify() {
|
|
|
9556
9834
|
if [ "$live" = "$_LOKI_SNAPSHOT_SEAL" ]; then
|
|
9557
9835
|
return 0
|
|
9558
9836
|
fi
|
|
9837
|
+
log_warn "Pre-existing-file snapshot or its hash file changed after this session recorded it (possible tampering by an agent process); treating this session as unsnapshotted (nothing will be committed)"
|
|
9838
|
+
# Record both digests (before clearing the seal below) so an operator
|
|
9839
|
+
# reading the audit log can tell a genuine content tamper apart from a
|
|
9840
|
+
# transient hash-tool failure that just produced a different digest.
|
|
9841
|
+
audit_log "SNAPSHOT_TAMPERED" "preexisting-untracked.z or .sha.z changed after being sealed: live=$live,sealed=$_LOKI_SNAPSHOT_SEAL"
|
|
9559
9842
|
_LOKI_SNAPSHOT_SEAL=""
|
|
9560
9843
|
_LOKI_SNAPSHOT_THIS_RUN=0
|
|
9561
|
-
log_warn "Pre-existing-file snapshot or its hash file changed after this session recorded it (possible tampering by an agent process); treating this session as unsnapshotted (nothing will be committed)"
|
|
9562
|
-
audit_log "SNAPSHOT_TAMPERED" "preexisting-untracked.z or .sha.z changed after being sealed"
|
|
9563
9844
|
mkdir -p .loki/state 2>/dev/null
|
|
9564
9845
|
: > .loki/state/preexisting-untracked.failed 2>/dev/null
|
|
9565
9846
|
return 1
|
|
@@ -9844,6 +10125,36 @@ _loki_advance_tracked_since_anchor() {
|
|
|
9844
10125
|
# disk, not committed).
|
|
9845
10126
|
_loki_record_session_created() {
|
|
9846
10127
|
local snap=".loki/state/preexisting-untracked.z" out=".loki/state/session-created.z"
|
|
10128
|
+
# BACKLOG 130(c): verify session-created.z BEFORE this run's THIS_RUN gate
|
|
10129
|
+
# or its own merge trust the file -- verify-before-trust, not verify-after-
|
|
10130
|
+
# use. Runs unconditionally (not gated on THIS_RUN=1): an interrupt/cleanup
|
|
10131
|
+
# call can still fire after THIS_RUN was cleared mid-session (e.g. by
|
|
10132
|
+
# _loki_snapshot_verify below on an earlier turn), and that call must not
|
|
10133
|
+
# skip this check just because the gate below is about to return early. On
|
|
10134
|
+
# a mismatch the file is already deleted by the time this returns (fail
|
|
10135
|
+
# closed inside _loki_session_created_verify itself, not via this return
|
|
10136
|
+
# value alone -- every call site of this function ignores its return with
|
|
10137
|
+
# `|| true`, same as _loki_snapshot_verify above); the merge below then
|
|
10138
|
+
# recomputes cleanly from a clean slate (base="$out" is now absent), so a
|
|
10139
|
+
# legitimate record right after a caught tamper still records this turn's
|
|
10140
|
+
# real files instead of hard-failing the whole call.
|
|
10141
|
+
# ACCEPTED GAP (verify-to-use window, disclosed here since ordering is the
|
|
10142
|
+
# whole point of this fix): this verify call and the merge's own read of
|
|
10143
|
+
# "$out" as `base` (inside _loki_untracked_merge, below) are not atomic --
|
|
10144
|
+
# _loki_advance_tracked_since_anchor, a git rev-parse, and a whole-repo
|
|
10145
|
+
# `git status` all run in between. A same-UID write landing in that window
|
|
10146
|
+
# is merged, then sealed as trusted by the seal call after a successful
|
|
10147
|
+
# merge. This is the SAME shape of gap _LOKI_SNAPSHOT_SEAL's own
|
|
10148
|
+
# verify/merge pair already accepts for preexisting-untracked.z (see
|
|
10149
|
+
# _loki_untracked_merge's own BACKLOG 129 comment). Closing it would mean
|
|
10150
|
+
# re-verifying again immediately before _loki_untracked_merge's read, inside
|
|
10151
|
+
# that function -- deliberately not done here: _loki_untracked_merge's exact
|
|
10152
|
+
# text is what tests/test-branch-lifecycle.sh's RED mutation harness matches
|
|
10153
|
+
# by name-anchor, and threading a second, narrower verify through its
|
|
10154
|
+
# shared codepath (also called with mode=exact for the OTHER snapshot) risks
|
|
10155
|
+
# that anchor for a gap this narrow. Disclosure, not full prevention, is the
|
|
10156
|
+
# same bar BACKLOG 70 itself accepts.
|
|
10157
|
+
_loki_session_created_verify || true
|
|
9847
10158
|
[ "${_LOKI_SNAPSHOT_THIS_RUN:-0}" = 1 ] || return 0
|
|
9848
10159
|
# BACKLOG 70: detect tampering with the sealed snapshot as early as
|
|
9849
10160
|
# possible (this runs after every provider turn, not just at commit time).
|
|
@@ -9857,6 +10168,11 @@ _loki_record_session_created() {
|
|
|
9857
10168
|
if [ -f "$snap" ] && _loki_untracked_status "$out.status" \
|
|
9858
10169
|
&& _loki_untracked_merge "$out.status" "$out" "$snap" cover "$out"; then
|
|
9859
10170
|
rm -f "$out.status"
|
|
10171
|
+
# BACKLOG 130(c): seal this process's own memory of the record it just
|
|
10172
|
+
# wrote, so the NEXT call to this function (or commit_session_changes)
|
|
10173
|
+
# verifies against what THIS process actually wrote, not whatever an
|
|
10174
|
+
# agent process may write to the file in between.
|
|
10175
|
+
_loki_session_created_seal
|
|
9860
10176
|
return 0
|
|
9861
10177
|
fi
|
|
9862
10178
|
rm -f "$out.status" "$out.tmp" 2>/dev/null
|
|
@@ -9868,6 +10184,19 @@ _loki_record_session_created() {
|
|
|
9868
10184
|
# from an unfinished previous session (this session's commit includes them).
|
|
9869
10185
|
_loki_resume_snapshot() {
|
|
9870
10186
|
local carried=""
|
|
10187
|
+
# BACKLOG 130(c): seal THIS process's own first look at session-created.z
|
|
10188
|
+
# before anything below reads or trusts it (verify-before-trust). This is
|
|
10189
|
+
# a resuming process picking up a file the PREVIOUS (now-dead) process
|
|
10190
|
+
# wrote; there is no seal from that process to verify against here (a
|
|
10191
|
+
# seal never survives past the process that set it) -- sealing now is what
|
|
10192
|
+
# lets THIS process detect an agent tampering with the file for the rest
|
|
10193
|
+
# of ITS OWN lifetime (every subsequent record call, and the commit-time
|
|
10194
|
+
# check), the same disclosure bar BACKLOG 70 already accepts for the
|
|
10195
|
+
# sibling snapshot: tampering already on disk before this process starts
|
|
10196
|
+
# is an accepted gap, never silently trusted forever after. Unconditional
|
|
10197
|
+
# (not gated on the union below succeeding): the union does not modify
|
|
10198
|
+
# session-created.z, so this process's view of it is stable either way.
|
|
10199
|
+
_loki_session_created_seal
|
|
9871
10200
|
if ! _loki_snapshot_or_fail_closed union; then
|
|
9872
10201
|
log_warn "Could not add your current untracked files to the pre-existing list; this session will commit nothing (review and commit manually)"
|
|
9873
10202
|
return 0
|
|
@@ -10005,7 +10334,9 @@ _loki_untrack_agent_committed_user_files() {
|
|
|
10005
10334
|
fi
|
|
10006
10335
|
rm -f "$rec.added"
|
|
10007
10336
|
if [ ! -s "$rec.new" ]; then
|
|
10008
|
-
|
|
10337
|
+
# BACKLOG 102: zero hits this session -- clear a stale record left by
|
|
10338
|
+
# an earlier session, or it persists forever with nothing to read it.
|
|
10339
|
+
rm -f "$rec.new" "$rec"
|
|
10009
10340
|
return 0
|
|
10010
10341
|
fi
|
|
10011
10342
|
mv -f "$rec.new" "$rec"
|
|
@@ -10028,6 +10359,18 @@ commit_session_changes() {
|
|
|
10028
10359
|
command -v git >/dev/null 2>&1 || return 0
|
|
10029
10360
|
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
|
|
10030
10361
|
|
|
10362
|
+
# BACKLOG 130(c): verify session-created.z BEFORE any of this function's
|
|
10363
|
+
# several early returns below (the untrack failure, the git 2.25 unstage
|
|
10364
|
+
# failure, the secret-scan abort, a failed `git commit`) -- every one of
|
|
10365
|
+
# them skips the two rm -f .loki/state/session-created.z cleanup sites
|
|
10366
|
+
# further down, so a tampered file left on disk by any of those paths
|
|
10367
|
+
# would otherwise survive untouched into the next session's resume union.
|
|
10368
|
+
# Placed at the very top, before even the branch-protection/PARALLEL_MODE/
|
|
10369
|
+
# branch-name checks below, so no early return in this function can skip
|
|
10370
|
+
# it. On a mismatch the file is deleted here already; nothing downstream
|
|
10371
|
+
# needs its own check.
|
|
10372
|
+
_loki_session_created_verify || true # BACKLOG-130C-COMMIT-TOP-VERIFY
|
|
10373
|
+
|
|
10031
10374
|
# Only act when a session feature branch was set up. This preserves the
|
|
10032
10375
|
# LOCK A1 opt-out contract (LOKI_BRANCH_PROTECTION=false -> no agent-branch.txt
|
|
10033
10376
|
# -> we never commit on the user's own branch), and also no-ops the detached
|
|
@@ -13283,14 +13626,26 @@ TREOF
|
|
|
13283
13626
|
# summary the old best-effort parse of the tail still applies (null when it
|
|
13284
13627
|
# finds nothing). Either way a recorded count above zero is a failure: the
|
|
13285
13628
|
# council evidence gate and the Bun gate both read failed_count > 0 as one.
|
|
13629
|
+
# BACKLOG 111: pytest's summary line also reports a distinct "error" outcome
|
|
13630
|
+
# for a fixture/collection/setup error ("1 passed, 1 error in 0.5s"), which
|
|
13631
|
+
# is neither "passed" nor "failed" text, so the [0-9]+ failed match alone
|
|
13632
|
+
# found nothing and printed n=0 even though seen=1 -- a real error gated
|
|
13633
|
+
# PASS. pytest pluralizes it ("2 errors"), so match errors? and ADD it to
|
|
13634
|
+
# the failed count on that line ("2 failed, 3 errors" is 5 distinct broken
|
|
13635
|
+
# tests, not 3 -- failed and error are independent outcomes within one
|
|
13636
|
+
# summary line, unlike the separate cross-line max below, which exists to
|
|
13637
|
+
# stop a later green summary from hiding an earlier failed run).
|
|
13286
13638
|
# LC_ALL=C: bytes, so any runner output parses; [ \t] not [[:space:]] for
|
|
13287
13639
|
# old mawk.
|
|
13288
13640
|
local _tr_passed_n _tr_failed_n _tr_summary_red=false
|
|
13289
13641
|
_tr_failed_n=$(printf '%s\n' "${output:-}" | LC_ALL=C sed "s/$(printf '\033')\[[0-9;]*[A-Za-z]//g" | LC_ALL=C awk '
|
|
13290
13642
|
function upd(n) { if (n > f) f = n; seen = 1 }
|
|
13291
13643
|
/^Tests:[ \t]/ || /^[ \t]*Tests[ \t]+[0-9]/ ||
|
|
13292
|
-
/^=+ .*[0-9]+ (passed|failed)/ || /^[0-9]+ (passed|failed).* in [0-9.]+s/ {
|
|
13293
|
-
n = 0
|
|
13644
|
+
/^=+ .*[0-9]+ (passed|failed|errors?)/ || /^[0-9]+ (passed|failed|errors?).* in [0-9.]+s/ {
|
|
13645
|
+
n = 0
|
|
13646
|
+
if (match($0, /[0-9]+ failed/)) n = substr($0, RSTART, RLENGTH) + 0
|
|
13647
|
+
if (match($0, /[0-9]+ errors?/)) n += substr($0, RSTART, RLENGTH) + 0
|
|
13648
|
+
upd(n) }
|
|
13294
13649
|
/^[^ A-Za-z0-9]+ fail [0-9]+[ \t]*$/ { if (prev ~ /^[^ A-Za-z0-9]+ pass [0-9]+[ \t]*$/) upd($3 + 0) }
|
|
13295
13650
|
/^[ \t]*[0-9]+ passing \(/ { upd(0) }
|
|
13296
13651
|
/^[ \t]*[0-9]+ failing[ \t]*$/ { upd($1 + 0) }
|
|
@@ -13643,8 +13998,8 @@ if not isinstance(d, dict):
|
|
|
13643
13998
|
verdict = d.get('verdict', 'unknown')
|
|
13644
13999
|
diff = d.get('diff', {}) if isinstance(d.get('diff'), dict) else {}
|
|
13645
14000
|
tests = d.get('tests', {}) if isinstance(d.get('tests'), dict) else {}
|
|
13646
|
-
diff_ok = diff.get('ok')
|
|
13647
|
-
tests_ok = tests.get('ok')
|
|
14001
|
+
diff_ok = 'inconclusive' if diff.get('inconclusive') else diff.get('ok')
|
|
14002
|
+
tests_ok = 'inconclusive' if tests.get('inconclusive') else tests.get('ok')
|
|
13648
14003
|
runner = tests.get('runner', 'none')
|
|
13649
14004
|
parts = ['verdict=%s' % verdict]
|
|
13650
14005
|
parts.append('diff_ok=%s' % diff_ok)
|
|
@@ -27428,6 +27783,8 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
27428
27783
|
main() {
|
|
27429
27784
|
_loki_install_signal_traps
|
|
27430
27785
|
SESSION_START_EPOCH=$(date +%s)
|
|
27786
|
+
# Before anything can spawn a provider (capability probes included).
|
|
27787
|
+
_loki_withhold_github_tokens
|
|
27431
27788
|
|
|
27432
27789
|
# First-run disclosure (shown once, before any work; best-effort).
|
|
27433
27790
|
if type loki_show_disclosure_once &>/dev/null; then
|
|
@@ -27596,6 +27953,10 @@ main() {
|
|
|
27596
27953
|
|
|
27597
27954
|
# Handle background mode
|
|
27598
27955
|
if [ "$BACKGROUND_MODE" = "true" ]; then
|
|
27956
|
+
# The relaunched runner withholds the tokens itself; it needs them in
|
|
27957
|
+
# its environment to do so. This process exits right after launching.
|
|
27958
|
+
local _bg_tok
|
|
27959
|
+
for _bg_tok in $_LOKI_WITHHELD_TOKENS; do export "${_bg_tok?}"; done
|
|
27599
27960
|
# Initialize .loki directory first
|
|
27600
27961
|
mkdir -p .loki/logs
|
|
27601
27962
|
|
|
@@ -28231,7 +28592,9 @@ main() {
|
|
|
28231
28592
|
# then advise the user how to open a PR. Both are no-ops when no agent branch
|
|
28232
28593
|
# was set up (LOKI_BRANCH_PROTECTION=false) or nothing changed.
|
|
28233
28594
|
commit_session_changes
|
|
28234
|
-
|
|
28595
|
+
# Trusted post-session step: it gets the GitHub token main() withheld from
|
|
28596
|
+
# agent sessions (its push may authenticate through the env token).
|
|
28597
|
+
_loki_with_github_tokens create_session_pr
|
|
28235
28598
|
audit_agent_action "session_stop" "Session ended" "result=$result,iterations=$ITERATION_COUNT"
|
|
28236
28599
|
|
|
28237
28600
|
# The first terminal summary is written before session changes are committed.
|
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
|
|