loki-mode 9.57.0 → 9.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +79 -15
- package/autonomy/lib/browser-open.sh +51 -0
- package/autonomy/lib/deadline.py +46 -5
- package/autonomy/lib/proof-check.sh +17 -2
- package/autonomy/loki +50 -73
- package/autonomy/run.sh +662 -51
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/dev/release-checklist.md +11 -32
- package/docs/v10/BOARD.md +46 -32
- package/docs/v10/DECISIONS.md +33 -0
- package/docs/v10/GUARDS.md +435 -0
- package/docs/v10/METRICS.md +33 -0
- package/docs/v10/PROGRESS.md +20 -0
- package/docs/v10/SWARM.md +24 -0
- package/learning/suggest.sh +1 -1
- package/loki-ts/dist/loki.js +307 -307
- package/loki-ts/package.json +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/autonomy/run.sh
CHANGED
|
@@ -1228,6 +1228,12 @@ if [ -f "$LOCK_LIB" ]; then
|
|
|
1228
1228
|
source "$LOCK_LIB"
|
|
1229
1229
|
fi
|
|
1230
1230
|
|
|
1231
|
+
# Guarded browser opener (loki_open_url): the only way run.sh opens a browser.
|
|
1232
|
+
if [ -f "$SCRIPT_DIR/lib/browser-open.sh" ]; then
|
|
1233
|
+
# shellcheck source=lib/browser-open.sh
|
|
1234
|
+
source "$SCRIPT_DIR/lib/browser-open.sh"
|
|
1235
|
+
fi
|
|
1236
|
+
|
|
1231
1237
|
# Git PR advisory (shared print-only helper for create_session_pr and loki deploy)
|
|
1232
1238
|
GIT_PR_ADVISORY_LIB="$SCRIPT_DIR/lib/git-pr-advisory.sh"
|
|
1233
1239
|
if [ -f "$GIT_PR_ADVISORY_LIB" ]; then
|
|
@@ -5339,7 +5345,13 @@ except Exception:
|
|
|
5339
5345
|
}
|
|
5340
5346
|
|
|
5341
5347
|
#===============================================================================
|
|
5342
|
-
# Rule of Two (moat P9): a GitHub token never reaches an agent session
|
|
5348
|
+
# Rule of Two (moat P9): a GitHub token never reaches an agent session via the
|
|
5349
|
+
# IMPLICIT resolution paths this module withholds (env vars, gh config store,
|
|
5350
|
+
# git credential.helper, SSH agent/ssh command). Not an absolute claim -- see
|
|
5351
|
+
# the disclosed residuals at the end of this header (an explicit named-account
|
|
5352
|
+
# keyring read, a direct ssh/hosts.yml/keychain read outside git); the actual
|
|
5353
|
+
# security boundary is a CI job holding no write token and no SSH agent while
|
|
5354
|
+
# the agent runs.
|
|
5343
5355
|
#
|
|
5344
5356
|
# The run reads untrusted issue text and spawns agents that act on it, so a
|
|
5345
5357
|
# token in the exported environment is one prompt injection away from a push.
|
|
@@ -5350,26 +5362,581 @@ except Exception:
|
|
|
5350
5362
|
# - _loki_with_github_tokens <cmd...>, for the post-session push/PR paths.
|
|
5351
5363
|
# LOKI_ALLOW_AGENT_GITHUB_TOKEN=1 (exact value) restores the old inheritance
|
|
5352
5364
|
# and prints one stderr warning that the agent holds the token.
|
|
5365
|
+
#
|
|
5366
|
+
# BACKLOG 149 (round 2, REJECT rework): the previous fix scoped GH_CONFIG_DIR
|
|
5367
|
+
# to a fresh empty directory but a reviewer confirmed, live, that this does not
|
|
5368
|
+
# close the vulnerability. Two bypasses, both reproduced empirically against
|
|
5369
|
+
# real gh 2.92 + a real macOS Keychain-backed `gh auth login` on 2026-09-27:
|
|
5370
|
+
#
|
|
5371
|
+
# 1. `GH_CONFIG_DIR=<empty dir> gh auth token` (real $HOME, no env token)
|
|
5372
|
+
# still exits 0: gh's resolution is config-dir file, THEN OS keyring
|
|
5373
|
+
# fallback (Keychain on macOS, libsecret on Linux). An empty config dir
|
|
5374
|
+
# does not stop the keyring lookup.
|
|
5375
|
+
# 2. git's own `credential.helper` (`gh auth setup-git` wires
|
|
5376
|
+
# `credential.https://github.com.helper = !gh auth git-credential`, and a
|
|
5377
|
+
# plain `osxkeychain`/`libsecret`/etc. helper may ALSO be configured,
|
|
5378
|
+
# unscoped, ahead of or behind it) is invoked directly by git on any
|
|
5379
|
+
# `git push`/`git credential fill` over HTTPS. This path is git-invoked,
|
|
5380
|
+
# not GH_CONFIG_DIR-mediated at all -- scoping GH_CONFIG_DIR does nothing
|
|
5381
|
+
# to it, and it resolves through the same keyring fallback OR a
|
|
5382
|
+
# completely separate OS-keychain entry that git's own helper (not gh's)
|
|
5383
|
+
# maintains independently.
|
|
5384
|
+
#
|
|
5385
|
+
# Fix, verified against `gh help environment` and empirically against both
|
|
5386
|
+
# bypasses (see the S-18 rework session transcript for the repro/counter-repro
|
|
5387
|
+
# pairs; no real credential value was ever printed during that verification):
|
|
5388
|
+
#
|
|
5389
|
+
# (a) GH_TOKEN/GITHUB_TOKEN/GH_ENTERPRISE_TOKEN/GITHUB_ENTERPRISE_TOKEN are
|
|
5390
|
+
# no longer merely unset -- each is set to a fresh per-process garbage
|
|
5391
|
+
# value (SENTINEL below). `gh help environment` documents the env token
|
|
5392
|
+
# as taking "precedence over previously stored credentials", and this
|
|
5393
|
+
# was confirmed live: with the sentinel present, `gh auth token` AND
|
|
5394
|
+
# `gh auth git-credential get` both echo back the garbage value, never
|
|
5395
|
+
# falling through to the keyring. `gh auth git-credential get` is a pure
|
|
5396
|
+
# read/print in this path -- it does not write to GH_CONFIG_DIR or touch
|
|
5397
|
+
# the keychain, so planting a sentinel has no destructive side effect;
|
|
5398
|
+
# a `git push` that ends up using it simply gets a 401 from GitHub, the
|
|
5399
|
+
# same as any other wrong password.
|
|
5400
|
+
# (b) The sentinel alone does not close a plain `osxkeychain` (or similar)
|
|
5401
|
+
# helper that is configured UNSCOPED (`credential.helper = osxkeychain`,
|
|
5402
|
+
# no gh involved at all) and holds its own independently-cached
|
|
5403
|
+
# credential -- confirmed present as a THIRD, gh-independent store on
|
|
5404
|
+
# the verification machine (git's own keychain entry, distinct from
|
|
5405
|
+
# gh's). GH_TOKEN only feeds gh's own resolution; it does not touch
|
|
5406
|
+
# git's credential-helper chain. So the session additionally gets
|
|
5407
|
+
# `credential.helper=` (empty string) injected via
|
|
5408
|
+
# GIT_CONFIG_COUNT/GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_n, appended after
|
|
5409
|
+
# any GIT_CONFIG_COUNT the operator already has set. Per gitcredentials(7):
|
|
5410
|
+
# "If credential.helper is configured to the empty string, this resets
|
|
5411
|
+
# the helper list to empty" -- this is why the reset must be UNSCOPED
|
|
5412
|
+
# (plain `credential.helper`, not `credential.<url>.helper`, since a
|
|
5413
|
+
# URL-scoped reset does not necessarily out-order every already
|
|
5414
|
+
# configured helper) and appended LAST (env-var config values override
|
|
5415
|
+
# every config FILE, per `git help config` on GIT_CONFIG_COUNT, and later
|
|
5416
|
+
# GIT_CONFIG_KEY_n entries are read after earlier ones). Confirmed live:
|
|
5417
|
+
# with this override present, `git credential fill` against github.com
|
|
5418
|
+
# fails closed ("could not read Username") instead of returning a
|
|
5419
|
+
# password line, with no fallback to any other helper. This closes the
|
|
5420
|
+
# git-invoked path directly, independent of GH_CONFIG_DIR or any
|
|
5421
|
+
# GH_TOKEN-family value.
|
|
5422
|
+
#
|
|
5423
|
+
# GH_CONFIG_DIR is still scoped to a fresh empty directory (unconditionally,
|
|
5424
|
+
# same as before) -- it remains correct for the plaintext-hosts.yml-read case
|
|
5425
|
+
# and for any gh subcommand that does not consult GH_TOKEN. $HOME itself stays
|
|
5426
|
+
# untouched, so Claude's own OAuth (F49) is unaffected -- nothing here reads
|
|
5427
|
+
# or writes $HOME/.claude or touches any Anthropic credential path.
|
|
5428
|
+
#
|
|
5429
|
+
# BACKLOG 149 (round 3, REJECT rework): round 2's fix is HTTPS-only. A
|
|
5430
|
+
# round-3 reviewer confirmed, live, that none of the above touches
|
|
5431
|
+
# SSH_AUTH_SOCK or GIT_SSH_COMMAND -- the provider session's real, untouched
|
|
5432
|
+
# environment always has a reachable SSH agent (by this design's own stated
|
|
5433
|
+
# intent of keeping the environment live for Claude's own OAuth), so
|
|
5434
|
+
# `git remote set-url origin git@github.com:<owner>/<repo>.git && git push`
|
|
5435
|
+
# authenticates exactly as an unrestricted session. Reproduced with a
|
|
5436
|
+
# synthetic-only ssh-agent and a throwaway key (never a real credential):
|
|
5437
|
+
# `ssh-add -l` against the inherited SSH_AUTH_SOCK lists the identity, and any
|
|
5438
|
+
# ssh-transport git operation would authenticate with it.
|
|
5439
|
+
#
|
|
5440
|
+
# Fix: SSH_AUTH_SOCK is unset (not sentineled) for the withheld session.
|
|
5441
|
+
# Unlike the GH_TOKEN-family vars, a present-but-wrong SSH_AUTH_SOCK does NOT
|
|
5442
|
+
# stop ssh from falling back: with no usable agent, ssh(1) still tries the
|
|
5443
|
+
# default key files (~/.ssh/id_*) and any `IdentityAgent` socket named in
|
|
5444
|
+
# ~/.ssh/config (and on macOS a launchd agent socket is rediscoverable). A
|
|
5445
|
+
# sentinel therefore buys nothing over unset, so unset is the simpler choice
|
|
5446
|
+
# for THIS var. Those fallbacks are why GIT_SSH_COMMAND=false (below) is the
|
|
5447
|
+
# part that actually closes git's ssh transport: git never runs ssh at all.
|
|
5448
|
+
# An agent that explicitly removes it (`env -u GIT_SSH_COMMAND git push
|
|
5449
|
+
# git@...`) gets ssh back with those fallbacks -- same class as `env -u
|
|
5450
|
+
# GH_TOKEN`, disclosed in the residual list at the end of this header. That
|
|
5451
|
+
# does not contradict round 2's reasoning for sentineling
|
|
5452
|
+
# the token vars, it applies that same reasoning to a var with different
|
|
5453
|
+
# fallback semantics. GIT_SSH_COMMAND is set to `false` (a POSIX builtin that
|
|
5454
|
+
# ignores all arguments and always exits 1): git invokes it in place of ssh
|
|
5455
|
+
# for any ssh-transport fetch/push, and git's own docs (`git help git-config`)
|
|
5456
|
+
# confirm the env var "is overridden when the environment variable is set" --
|
|
5457
|
+
# i.e. GIT_SSH_COMMAND (env) unconditionally outranks core.sshCommand
|
|
5458
|
+
# (config), so setting only the env var closes both an operator-configured
|
|
5459
|
+
# core.sshCommand AND an agent-configured one, with no separate
|
|
5460
|
+
# GIT_CONFIG_COUNT-based core.sshCommand reset needed. This means even an
|
|
5461
|
+
# explicit `ssh -i <path>` invocation made THROUGH git (git fetch/push/clone
|
|
5462
|
+
# over an ssh:// or git@ URL) fails; it does NOT stop an agent from invoking
|
|
5463
|
+
# the real `/usr/bin/ssh` binary directly outside of git, which is a
|
|
5464
|
+
# disclosed residual (see below), the same class as the already-disclosed
|
|
5465
|
+
# `gh auth token -u <username>` and on-disk hosts.yml/keychain reads.
|
|
5466
|
+
#
|
|
5467
|
+
# Loki's own trusted calls (gh() below, _loki_with_github_tokens) restore the
|
|
5468
|
+
# REAL env values (or absence) for the 4 token vars, the real GH_CONFIG_DIR
|
|
5469
|
+
# (or absence), the real GIT_CONFIG_COUNT/KEY/VALUE state (or absence), and
|
|
5470
|
+
# the real SSH_AUTH_SOCK/GIT_SSH_COMMAND state (or absence) around the one
|
|
5471
|
+
# trusted command, then re-apply the withhold afterward -- this repo's own
|
|
5472
|
+
# trusted git operations (create_session_pr, on_run_complete's push,
|
|
5473
|
+
# proof-check.sh) push to an https or ssh GitHub origin, so an SSH-origin
|
|
5474
|
+
# user's Loki-initiated push must still work; the SSH sentinel needs the same
|
|
5475
|
+
# re-grant path as the token vars, not a simpler one.
|
|
5476
|
+
#
|
|
5477
|
+
# Restoring credentials is only safe if the credentialed process does not
|
|
5478
|
+
# also load code the agent controls (round 5). Until round 5 it did: the
|
|
5479
|
+
# trusted push was a `git push` inside the agent's working tree, so the
|
|
5480
|
+
# agent's repo config (.git/hooks, core.hooksPath, core.sshCommand, a
|
|
5481
|
+
# repo-local credential.helper, core.fsmonitor, url.*insteadOf, include/
|
|
5482
|
+
# includeIf) ran holding every restored credential. Now the push goes through
|
|
5483
|
+
# _loki_trusted_push (fresh Loki-owned repo, origin URL read as data and
|
|
5484
|
+
# accepted only as a github.com https/ssh URL) and every trusted gh call runs
|
|
5485
|
+
# from / with GH_REPO (_loki_run_neutral), so no credentialed git or gh process
|
|
5486
|
+
# loads the agent's repo config. What the trusted step still trusts:
|
|
5487
|
+
# - the operator's GLOBAL and SYSTEM git config (they carry the real
|
|
5488
|
+
# credential helper). An agent that writes ~/.gitconfig, /etc/gitconfig or
|
|
5489
|
+
# an included file there gets the same code execution back; this is the
|
|
5490
|
+
# same-user filesystem class as editing ~/.ssh or a shell rc file.
|
|
5491
|
+
# - the branch contents, which are the agent's work by design (Loki pushes
|
|
5492
|
+
# them for review; nothing in them executes during the push).
|
|
5493
|
+
# - the origin OWNER/REPO the agent's config names: the agent can point the
|
|
5494
|
+
# push at another github.com repository. GitHub still enforces the
|
|
5495
|
+
# operator token's permissions there.
|
|
5496
|
+
# A non-GitHub origin is no longer pushed at all (LOKI_AUTO_PR=1 used to push
|
|
5497
|
+
# to any origin).
|
|
5498
|
+
#
|
|
5353
5499
|
# This is hygiene against a naive injection, not an isolation boundary: code
|
|
5354
5500
|
# running as the same user can still read the parent's environment block
|
|
5355
|
-
# (/proc/<pid>/environ on Linux, sudo on a hosted runner)
|
|
5356
|
-
#
|
|
5501
|
+
# (/proc/<pid>/environ on Linux, sudo on a hosted runner), the hosts.yml file
|
|
5502
|
+
# directly off disk (F49 keeps $HOME live), or invoke `git -c
|
|
5503
|
+
# credential.helper=...` / `security find-generic-password` /
|
|
5504
|
+
# `env -u GH_TOKEN -u GITHUB_TOKEN gh auth token` / `gh auth token -u
|
|
5505
|
+
# <username>` / `env -u GIT_SSH_COMMAND git push git@...` (ssh then falls back
|
|
5506
|
+
# to ~/.ssh/id_* default keys and any ~/.ssh/config IdentityAgent) / the real
|
|
5507
|
+
# `ssh` binary directly with an explicit `-i` key path or a re-discovered agent
|
|
5508
|
+
# socket (e.g. `launchctl getenv SSH_AUTH_SOCK` on macOS) / git's `ext::`
|
|
5509
|
+
# transport (`git -c protocol.ext.allow=always` with an `ext::<command>` URL,
|
|
5510
|
+
# which runs an arbitrary command as the transport) / GIT_ASKPASS (VS Code's
|
|
5511
|
+
# integrated terminal sets it to a helper that can answer git's credential
|
|
5512
|
+
# prompt over IPC; it is left untouched here) explicitly to route around
|
|
5513
|
+
# this. The boundary is a CI job that holds
|
|
5514
|
+
# no write token and no SSH agent while the agent runs (see
|
|
5357
5515
|
# .github/workflows/loki-issue-to-pr.yml).
|
|
5358
5516
|
#===============================================================================
|
|
5359
5517
|
_LOKI_WITHHELD_TOKENS=""
|
|
5518
|
+
_LOKI_GIT_CONFIG_COUNT_UNSUPPORTED=""
|
|
5519
|
+
_LOKI_GH_TOKEN_REAL_HAD=""
|
|
5520
|
+
_LOKI_GH_TOKEN_REAL_VAL=""
|
|
5521
|
+
_LOKI_GITHUB_TOKEN_REAL_HAD=""
|
|
5522
|
+
_LOKI_GITHUB_TOKEN_REAL_VAL=""
|
|
5523
|
+
_LOKI_GH_ENT_TOKEN_REAL_HAD=""
|
|
5524
|
+
_LOKI_GH_ENT_TOKEN_REAL_VAL=""
|
|
5525
|
+
_LOKI_GITHUB_ENT_TOKEN_REAL_HAD=""
|
|
5526
|
+
_LOKI_GITHUB_ENT_TOKEN_REAL_VAL=""
|
|
5527
|
+
_LOKI_GH_CONFIG_SCOPED=""
|
|
5528
|
+
_LOKI_GH_CONFIG_DIR_HAD=""
|
|
5529
|
+
_LOKI_GH_CONFIG_DIR_OLD=""
|
|
5530
|
+
_LOKI_GIT_CRED_SCOPED=""
|
|
5531
|
+
_LOKI_GIT_CONFIG_COUNT_OLD=""
|
|
5532
|
+
_LOKI_GIT_CONFIG_COUNT_HAD=""
|
|
5533
|
+
_LOKI_GIT_CRED_INDEX=""
|
|
5534
|
+
_LOKI_GH_SENTINEL=""
|
|
5535
|
+
_LOKI_SSH_AUTH_SOCK_HAD=""
|
|
5536
|
+
_LOKI_SSH_AUTH_SOCK_OLD=""
|
|
5537
|
+
_LOKI_GIT_SSH_COMMAND_HAD=""
|
|
5538
|
+
_LOKI_GIT_SSH_COMMAND_OLD=""
|
|
5539
|
+
_LOKI_SSH_WITHHELD=""
|
|
5540
|
+
_LOKI_GH_INHERITED=""
|
|
5541
|
+
|
|
5542
|
+
# Restore the real value (or absence) of one of the 4 token vars, using the
|
|
5543
|
+
# per-var _HAD/_VAL pair captured by _loki_gh_capture. Bash has no portable
|
|
5544
|
+
# array-of-names-by-reference here, so this is spelled out per var rather than
|
|
5545
|
+
# looped over a dynamic name. Also used by the --bg relaunch path to hand the
|
|
5546
|
+
# REAL values to the relaunched child (see call site below).
|
|
5547
|
+
_loki_restore_one_token() {
|
|
5548
|
+
local _var="$1" _had="$2" _val="$3"
|
|
5549
|
+
if [ -n "$_had" ]; then
|
|
5550
|
+
export "$_var=$_val"
|
|
5551
|
+
else
|
|
5552
|
+
unset "$_var"
|
|
5553
|
+
fi
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
# Capture the REAL pre-withhold state exactly once per process: the 4 token
|
|
5557
|
+
# vars, GH_CONFIG_DIR, and GIT_CONFIG_COUNT. Idempotent via _LOKI_WITHHELD_TOKENS
|
|
5558
|
+
# as the guard, so a later _loki_gh_apply (called after a trusted command
|
|
5559
|
+
# restores and re-applies) never re-captures its own sentinel/scoped state as
|
|
5560
|
+
# if it were the original.
|
|
5561
|
+
_loki_gh_capture() {
|
|
5562
|
+
[ -z "$_LOKI_WITHHELD_TOKENS" ] || return 0
|
|
5563
|
+
local _v
|
|
5564
|
+
# GIT_CONFIG_COUNT/GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_n were introduced in
|
|
5565
|
+
# git 2.31.0 (2021-03-15); an older git silently ignores them, leaving the
|
|
5566
|
+
# credential.helper reset a no-op with no error -- the exact
|
|
5567
|
+
# fail-open-with-no-signal this fix must not have. Detected once per
|
|
5568
|
+
# process (not on every apply) from `git --version`; a `git version
|
|
5569
|
+
# X.Y.Z` line below 2.31 sets the flag so _loki_withhold_github_tokens can
|
|
5570
|
+
# warn once. Ubuntu 24.04 (2.43) and Debian bookworm (2.39), the two base
|
|
5571
|
+
# images this project ships (Dockerfile, Dockerfile.sandbox), are both
|
|
5572
|
+
# well above this floor; the warning matters for an operator's own older
|
|
5573
|
+
# git (e.g. RHEL 8 ships 2.18).
|
|
5574
|
+
if command -v git >/dev/null 2>&1; then
|
|
5575
|
+
local _gv _gv_major _gv_minor
|
|
5576
|
+
_gv="$(git --version 2>/dev/null)"
|
|
5577
|
+
case "$_gv" in
|
|
5578
|
+
git\ version\ [0-9]*.[0-9]*.*)
|
|
5579
|
+
_gv="${_gv#git version }"
|
|
5580
|
+
_gv_major="${_gv%%.*}"
|
|
5581
|
+
_gv="${_gv#*.}"
|
|
5582
|
+
_gv_minor="${_gv%%.*}"
|
|
5583
|
+
case "$_gv_major" in *[!0-9]*) _gv_major="" ;; esac
|
|
5584
|
+
case "$_gv_minor" in *[!0-9]*) _gv_minor="" ;; esac
|
|
5585
|
+
if [ -n "$_gv_major" ] && [ -n "$_gv_minor" ]; then
|
|
5586
|
+
if [ "$_gv_major" -lt 2 ] || { [ "$_gv_major" -eq 2 ] && [ "$_gv_minor" -lt 31 ]; }; then
|
|
5587
|
+
_LOKI_GIT_CONFIG_COUNT_UNSUPPORTED=1
|
|
5588
|
+
fi
|
|
5589
|
+
fi
|
|
5590
|
+
;;
|
|
5591
|
+
esac
|
|
5592
|
+
fi
|
|
5593
|
+
# Cross-runtime/nested-`loki start` guard: a bash child spawned by an
|
|
5594
|
+
# ALREADY-withheld process (Bun's delegateToBash forwarding
|
|
5595
|
+
# {...process.env}, or an agent literally invoking `loki start` inside its
|
|
5596
|
+
# own withheld session) inherits the PARENT's sentinel/scoped state as
|
|
5597
|
+
# ordinary env vars. Without this guard this process would capture that
|
|
5598
|
+
# inherited state as if it were the operator's real original and restore
|
|
5599
|
+
# to it for its own trusted calls -- i.e. Loki's own push/PR would use the
|
|
5600
|
+
# inherited garbage sentinel instead of a real credential. Detect the
|
|
5601
|
+
# sentinel shape (see _LOKI_GH_SENTINEL below) and treat it, and the
|
|
5602
|
+
# scoped GH_CONFIG_DIR/credential.helper reset that came with it, as
|
|
5603
|
+
# ABSENT rather than as the real original.
|
|
5604
|
+
local _inherited_sentinel=""
|
|
5605
|
+
case "${GH_TOKEN:-}" in ghp_LOKIWITHHELDsentinel*INVALID) _inherited_sentinel=1 ;; esac
|
|
5606
|
+
# Nested-run rule (BACKLOG 149 round 4): an inherited sentinel means this
|
|
5607
|
+
# run was started from INSIDE an already-withheld session -- an agent
|
|
5608
|
+
# running `loki start` in its own session, or a build started from the
|
|
5609
|
+
# dashboard that start_dashboard spawned after the withhold. Such a run has
|
|
5610
|
+
# no real credentials to restore on ANY channel, and reopening any of them
|
|
5611
|
+
# for its "trusted" push would hand an agent-chosen repo/branch the
|
|
5612
|
+
# operator's credential helper or default SSH keys. So it fails CLOSED on
|
|
5613
|
+
# every channel: _loki_gh_restore becomes a no-op and Loki's own push/PR
|
|
5614
|
+
# from a nested run runs with the withheld state (and fails). Before this
|
|
5615
|
+
# rule the channels disagreed (HTTPS helper chain and default SSH keys
|
|
5616
|
+
# reopened, agent socket closed).
|
|
5617
|
+
[ -z "$_inherited_sentinel" ] || _LOKI_GH_INHERITED=1
|
|
5618
|
+
if [ -n "$_inherited_sentinel" ]; then
|
|
5619
|
+
: # GH_TOKEN/GITHUB_TOKEN/GH_ENTERPRISE_TOKEN/GITHUB_ENTERPRISE_TOKEN
|
|
5620
|
+
# all get the SAME sentinel per _loki_gh_apply, so one check covers
|
|
5621
|
+
# all 4 -- _LOKI_*_REAL_HAD stays unset for each, meaning "restore to
|
|
5622
|
+
# absent", not "restore to this garbage value". The same inherited
|
|
5623
|
+
# process also had SSH_AUTH_SOCK unset and GIT_SSH_COMMAND=false by
|
|
5624
|
+
# this same apply, so _LOKI_SSH_AUTH_SOCK_HAD/_LOKI_GIT_SSH_COMMAND_HAD
|
|
5625
|
+
# stay unset too (restore to absent, not to `false`) -- see the SSH
|
|
5626
|
+
# capture below, gated on this same flag.
|
|
5627
|
+
else
|
|
5628
|
+
if [ -n "${GH_TOKEN+x}" ]; then _LOKI_GH_TOKEN_REAL_HAD=1; _LOKI_GH_TOKEN_REAL_VAL="$GH_TOKEN"; fi
|
|
5629
|
+
if [ -n "${GITHUB_TOKEN+x}" ]; then _LOKI_GITHUB_TOKEN_REAL_HAD=1; _LOKI_GITHUB_TOKEN_REAL_VAL="$GITHUB_TOKEN"; fi
|
|
5630
|
+
if [ -n "${GH_ENTERPRISE_TOKEN+x}" ]; then _LOKI_GH_ENT_TOKEN_REAL_HAD=1; _LOKI_GH_ENT_TOKEN_REAL_VAL="$GH_ENTERPRISE_TOKEN"; fi
|
|
5631
|
+
if [ -n "${GITHUB_ENTERPRISE_TOKEN+x}" ]; then _LOKI_GITHUB_ENT_TOKEN_REAL_HAD=1; _LOKI_GITHUB_ENT_TOKEN_REAL_VAL="$GITHUB_ENTERPRISE_TOKEN"; fi
|
|
5632
|
+
fi
|
|
5633
|
+
# SSH sentinel (BACKLOG 149 round 3): capture the real pre-withhold state
|
|
5634
|
+
# of SSH_AUTH_SOCK and GIT_SSH_COMMAND, same inherited-sentinel guard as
|
|
5635
|
+
# the token vars above (GIT_SSH_COMMAND=false is this fix's own withheld
|
|
5636
|
+
# value, never an operator's real config -- an operator who genuinely
|
|
5637
|
+
# wants `false` as their ssh command is not a real scenario worth
|
|
5638
|
+
# preserving across the guard).
|
|
5639
|
+
if [ -z "$_inherited_sentinel" ]; then
|
|
5640
|
+
if [ -n "${SSH_AUTH_SOCK+x}" ]; then _LOKI_SSH_AUTH_SOCK_HAD=1; _LOKI_SSH_AUTH_SOCK_OLD="$SSH_AUTH_SOCK"; fi
|
|
5641
|
+
if [ -n "${GIT_SSH_COMMAND+x}" ] && [ "$GIT_SSH_COMMAND" != "false" ]; then
|
|
5642
|
+
_LOKI_GIT_SSH_COMMAND_HAD=1
|
|
5643
|
+
_LOKI_GIT_SSH_COMMAND_OLD="$GIT_SSH_COMMAND"
|
|
5644
|
+
fi
|
|
5645
|
+
fi
|
|
5646
|
+
for _v in GH_TOKEN GITHUB_TOKEN GH_ENTERPRISE_TOKEN GITHUB_ENTERPRISE_TOKEN; do
|
|
5647
|
+
_LOKI_WITHHELD_TOKENS="${_LOKI_WITHHELD_TOKENS:+$_LOKI_WITHHELD_TOKENS }$_v"
|
|
5648
|
+
done
|
|
5649
|
+
# An inherited scoped GH_CONFIG_DIR (bash's loki-gh-config.* or the Bun
|
|
5650
|
+
# route's loki-gh-config-*) is likewise not the operator's real original.
|
|
5651
|
+
# TMPDIR is normalized first: on macOS it ends in "/", so bash's own
|
|
5652
|
+
# mktemp path has "//" while Node's tmpdir() (Bun route) strips the slash.
|
|
5653
|
+
# Without accepting both, Bun's scoped dir was mistaken for the operator's.
|
|
5654
|
+
local _tmp_root="${TMPDIR:-/tmp}"
|
|
5655
|
+
_tmp_root="${_tmp_root%/}"
|
|
5656
|
+
case "${GH_CONFIG_DIR:-}" in
|
|
5657
|
+
"$_tmp_root"/loki-gh-config.* | "$_tmp_root"//loki-gh-config.* \
|
|
5658
|
+
| "$_tmp_root"/loki-gh-config-* | "$_tmp_root"//loki-gh-config-*) ;;
|
|
5659
|
+
*)
|
|
5660
|
+
if [ -n "${GH_CONFIG_DIR+x}" ]; then
|
|
5661
|
+
_LOKI_GH_CONFIG_DIR_HAD=1
|
|
5662
|
+
_LOKI_GH_CONFIG_DIR_OLD="$GH_CONFIG_DIR"
|
|
5663
|
+
fi
|
|
5664
|
+
;;
|
|
5665
|
+
esac
|
|
5666
|
+
# An inherited credential.helper reset: if the sentinel was inherited AND
|
|
5667
|
+
# the LAST GIT_CONFIG_KEY/VALUE pair is exactly this fix's reset
|
|
5668
|
+
# (credential.helper=""), treat the count as if that pair were never
|
|
5669
|
+
# added -- i.e. capture the state BENEATH it as the real original, so
|
|
5670
|
+
# restore correctly removes only the inherited pair rather than an
|
|
5671
|
+
# operator-configured one at the same index.
|
|
5672
|
+
if [ -n "${GIT_CONFIG_COUNT:-}" ] && [ "${GIT_CONFIG_COUNT}" -eq "${GIT_CONFIG_COUNT}" ] 2>/dev/null; then
|
|
5673
|
+
local _n=$((GIT_CONFIG_COUNT - 1)) _last_key_var _last_val_var
|
|
5674
|
+
_last_key_var="GIT_CONFIG_KEY_${_n}"
|
|
5675
|
+
_last_val_var="GIT_CONFIG_VALUE_${_n}"
|
|
5676
|
+
if [ -n "$_inherited_sentinel" ] && [ "$_n" -ge 0 ] \
|
|
5677
|
+
&& [ "${!_last_key_var:-}" = "credential.helper" ] && [ -z "${!_last_val_var:-}" ]; then
|
|
5678
|
+
if [ "$_n" -gt 0 ]; then
|
|
5679
|
+
_LOKI_GIT_CONFIG_COUNT_HAD=1
|
|
5680
|
+
_LOKI_GIT_CONFIG_COUNT_OLD="$_n"
|
|
5681
|
+
fi
|
|
5682
|
+
_LOKI_GIT_CRED_INDEX="$_n"
|
|
5683
|
+
else
|
|
5684
|
+
_LOKI_GIT_CONFIG_COUNT_HAD=1
|
|
5685
|
+
_LOKI_GIT_CONFIG_COUNT_OLD="$GIT_CONFIG_COUNT"
|
|
5686
|
+
_LOKI_GIT_CRED_INDEX="$GIT_CONFIG_COUNT"
|
|
5687
|
+
fi
|
|
5688
|
+
else
|
|
5689
|
+
_LOKI_GIT_CRED_INDEX=0
|
|
5690
|
+
fi
|
|
5691
|
+
# Fixed for the process lifetime once captured, so restore-then-reapply
|
|
5692
|
+
# (the trusted-call cycle) always re-exports the SAME sentinel rather than
|
|
5693
|
+
# minting a new one that a caller mid-command could not have seen.
|
|
5694
|
+
# Alphanumeric-only after the ghp_ prefix (no underscores): matches the
|
|
5695
|
+
# real gh token shape and the existing token-redaction regex in
|
|
5696
|
+
# autonomy/lib/proof_redact.py (gh[pousr]_[A-Za-z0-9]{20,}), so if this
|
|
5697
|
+
# sentinel ever leaked into a proof/receipt artifact it would still be
|
|
5698
|
+
# caught by the existing redaction filter rather than passing it by shape.
|
|
5699
|
+
_LOKI_GH_SENTINEL="ghp_LOKIWITHHELDsentinel$$${RANDOM}${RANDOM}INVALID"
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5702
|
+
# Unconditionally (re-)export the withheld/scoped state. Never captures --
|
|
5703
|
+
# _loki_gh_capture must run first. Silent: callers that want the one-time
|
|
5704
|
+
# operator-facing log line print it themselves.
|
|
5705
|
+
_loki_gh_apply() {
|
|
5706
|
+
export GH_TOKEN="$_LOKI_GH_SENTINEL" GITHUB_TOKEN="$_LOKI_GH_SENTINEL" \
|
|
5707
|
+
GH_ENTERPRISE_TOKEN="$_LOKI_GH_SENTINEL" GITHUB_ENTERPRISE_TOKEN="$_LOKI_GH_SENTINEL"
|
|
5708
|
+
if [ -z "$_LOKI_GH_CONFIG_SCOPED" ]; then
|
|
5709
|
+
local _empty_cfg
|
|
5710
|
+
_empty_cfg="$(mktemp -d "${TMPDIR:-/tmp}/loki-gh-config.XXXXXX" 2>/dev/null)" || _empty_cfg=""
|
|
5711
|
+
[ -n "$_empty_cfg" ] && _LOKI_GH_CONFIG_SCOPED="$_empty_cfg"
|
|
5712
|
+
fi
|
|
5713
|
+
[ -n "$_LOKI_GH_CONFIG_SCOPED" ] && export GH_CONFIG_DIR="$_LOKI_GH_CONFIG_SCOPED"
|
|
5714
|
+
export "GIT_CONFIG_KEY_${_LOKI_GIT_CRED_INDEX}=credential.helper"
|
|
5715
|
+
export "GIT_CONFIG_VALUE_${_LOKI_GIT_CRED_INDEX}="
|
|
5716
|
+
export GIT_CONFIG_COUNT=$((_LOKI_GIT_CRED_INDEX + 1))
|
|
5717
|
+
_LOKI_GIT_CRED_SCOPED=1
|
|
5718
|
+
# SSH sentinel (BACKLOG 149 round 3): no reachable SSH agent, and any
|
|
5719
|
+
# ssh-transport git operation fails closed via GIT_SSH_COMMAND=false
|
|
5720
|
+
# (env var, so it overrides any core.sshCommand -- see the header comment
|
|
5721
|
+
# above _LOKI_WITHHELD_TOKENS for the git-docs citation).
|
|
5722
|
+
unset SSH_AUTH_SOCK
|
|
5723
|
+
export GIT_SSH_COMMAND=false
|
|
5724
|
+
_LOKI_SSH_WITHHELD=1
|
|
5725
|
+
if command -v gh >/dev/null 2>&1; then
|
|
5726
|
+
# Repo read as data BEFORE the credentials come back; gh then runs
|
|
5727
|
+
# from / (round 5: gh's own git calls must not load the agent's repo
|
|
5728
|
+
# config while holding the real credentials).
|
|
5729
|
+
gh() {
|
|
5730
|
+
local _gh_repo
|
|
5731
|
+
_gh_repo="$(_loki_trusted_repo)"
|
|
5732
|
+
_loki_with_github_tokens _loki_run_neutral "$_gh_repo" command gh "$@"
|
|
5733
|
+
}
|
|
5734
|
+
fi
|
|
5735
|
+
}
|
|
5360
5736
|
|
|
5737
|
+
# Put back the REAL pre-withhold state around one trusted command. A no-op in
|
|
5738
|
+
# a run that inherited the sentinel: there is no real state to put back, and
|
|
5739
|
+
# the nested-run rule in _loki_gh_capture is to fail closed on every channel.
|
|
5740
|
+
_loki_gh_restore() {
|
|
5741
|
+
[ -z "$_LOKI_GH_INHERITED" ] || return 0
|
|
5742
|
+
_loki_restore_one_token GH_TOKEN "$_LOKI_GH_TOKEN_REAL_HAD" "$_LOKI_GH_TOKEN_REAL_VAL"
|
|
5743
|
+
_loki_restore_one_token GITHUB_TOKEN "$_LOKI_GITHUB_TOKEN_REAL_HAD" "$_LOKI_GITHUB_TOKEN_REAL_VAL"
|
|
5744
|
+
_loki_restore_one_token GH_ENTERPRISE_TOKEN "$_LOKI_GH_ENT_TOKEN_REAL_HAD" "$_LOKI_GH_ENT_TOKEN_REAL_VAL"
|
|
5745
|
+
_loki_restore_one_token GITHUB_ENTERPRISE_TOKEN "$_LOKI_GITHUB_ENT_TOKEN_REAL_HAD" "$_LOKI_GITHUB_ENT_TOKEN_REAL_VAL"
|
|
5746
|
+
if [ -n "$_LOKI_GH_CONFIG_DIR_HAD" ]; then
|
|
5747
|
+
export GH_CONFIG_DIR="$_LOKI_GH_CONFIG_DIR_OLD"
|
|
5748
|
+
else
|
|
5749
|
+
unset GH_CONFIG_DIR
|
|
5750
|
+
fi
|
|
5751
|
+
if [ -n "$_LOKI_GIT_CONFIG_COUNT_HAD" ]; then
|
|
5752
|
+
export GIT_CONFIG_COUNT="$_LOKI_GIT_CONFIG_COUNT_OLD"
|
|
5753
|
+
else
|
|
5754
|
+
unset GIT_CONFIG_COUNT
|
|
5755
|
+
fi
|
|
5756
|
+
unset "GIT_CONFIG_KEY_${_LOKI_GIT_CRED_INDEX}" "GIT_CONFIG_VALUE_${_LOKI_GIT_CRED_INDEX}"
|
|
5757
|
+
# SSH sentinel restore: give the trusted command back the real SSH agent
|
|
5758
|
+
# and ssh command, so Loki's own push/PR still works over an SSH origin.
|
|
5759
|
+
_loki_restore_one_token SSH_AUTH_SOCK "$_LOKI_SSH_AUTH_SOCK_HAD" "$_LOKI_SSH_AUTH_SOCK_OLD"
|
|
5760
|
+
_loki_restore_one_token GIT_SSH_COMMAND "$_LOKI_GIT_SSH_COMMAND_HAD" "$_LOKI_GIT_SSH_COMMAND_OLD"
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5763
|
+
_LOKI_GH_TRUSTED_DEPTH=0
|
|
5764
|
+
|
|
5765
|
+
# Reentrancy: a trusted function (create_session_pr) calls `gh` directly one
|
|
5766
|
+
# or more times; since the gh() wrapper (_loki_gh_apply) IS
|
|
5767
|
+
# _loki_with_github_tokens, each of those calls re-enters this function while
|
|
5768
|
+
# the OUTER call is still in progress. Without depth tracking, the FIRST
|
|
5769
|
+
# nested `gh` call's own apply-on-exit would re-scope the environment before
|
|
5770
|
+
# the outer caller's second `gh` call (or any bare `git` call) runs, breaking
|
|
5771
|
+
# it silently -- caught in this session before landing: create_session_pr
|
|
5772
|
+
# calls `gh pr list` then `gh pr create`, and the nested apply between them
|
|
5773
|
+
# would have handed the second call sentineled/scoped state instead of real
|
|
5774
|
+
# credentials. Only the OUTERMOST call restores on entry and re-applies on
|
|
5775
|
+
# exit; a nested call finds the depth already > 0 and is a pure passthrough
|
|
5776
|
+
# (real credentials are already in place from the outer restore).
|
|
5361
5777
|
_loki_with_github_tokens() {
|
|
5362
|
-
local
|
|
5363
|
-
|
|
5778
|
+
local _rc=0 _outermost=0
|
|
5779
|
+
if [ "$_LOKI_GH_TRUSTED_DEPTH" -eq 0 ]; then
|
|
5780
|
+
_outermost=1
|
|
5781
|
+
if [ -n "$_LOKI_WITHHELD_TOKENS" ]; then
|
|
5782
|
+
_loki_gh_restore
|
|
5783
|
+
fi
|
|
5784
|
+
fi
|
|
5785
|
+
_LOKI_GH_TRUSTED_DEPTH=$((_LOKI_GH_TRUSTED_DEPTH + 1))
|
|
5364
5786
|
"$@" || _rc=$?
|
|
5365
|
-
|
|
5787
|
+
_LOKI_GH_TRUSTED_DEPTH=$((_LOKI_GH_TRUSTED_DEPTH - 1))
|
|
5788
|
+
if [ "$_outermost" -eq 1 ] && [ -n "$_LOKI_WITHHELD_TOKENS" ]; then
|
|
5789
|
+
_loki_gh_apply
|
|
5790
|
+
fi
|
|
5791
|
+
return "$_rc"
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5794
|
+
#-------------------------------------------------------------------------------
|
|
5795
|
+
# BACKLOG 149 round 5: a credentialed git or gh process never loads the agent's
|
|
5796
|
+
# repo config.
|
|
5797
|
+
#
|
|
5798
|
+
# _loki_with_github_tokens hands the real credentials back for one command. If
|
|
5799
|
+
# that command is `git push` run inside the agent's working tree, git loads the
|
|
5800
|
+
# agent-writable repo config first: .git/hooks/pre-push, core.hooksPath,
|
|
5801
|
+
# core.sshCommand, a repo-local credential.helper, core.fsmonitor,
|
|
5802
|
+
# url.insteadOf/pushInsteadOf, include/includeIf. Two reviewers reproduced a
|
|
5803
|
+
# planted pre-push hook reading the real GH_TOKEN and SSH_AUTH_SOCK, and
|
|
5804
|
+
# `git credential fill` returning the operator's password. `-c
|
|
5805
|
+
# core.hooksPath=/dev/null` alone does not stop core.sshCommand, and a list of
|
|
5806
|
+
# per-key overrides would have to stay complete forever.
|
|
5807
|
+
#
|
|
5808
|
+
# So the credentialed step never runs with that config loaded:
|
|
5809
|
+
# - push: read remote.origin.url as DATA (no credentials), accept only a
|
|
5810
|
+
# github.com https/ssh URL, fetch the branch into a fresh Loki-owned repo
|
|
5811
|
+
# (the fetch runs upload-pack in the agent repo WITHOUT credentials), then
|
|
5812
|
+
# push from that repo with the credentials restored.
|
|
5813
|
+
# - gh: run from / with GH_REPO set to the validated origin, so gh's own git
|
|
5814
|
+
# calls find no repository. An explicit --repo/-R still overrides GH_REPO.
|
|
5815
|
+
# The operator's global/system git config still loads (it holds the real
|
|
5816
|
+
# credential helper); an agent that can write ~/.gitconfig is a same-user
|
|
5817
|
+
# filesystem residual, disclosed in the header above.
|
|
5818
|
+
#-------------------------------------------------------------------------------
|
|
5819
|
+
|
|
5820
|
+
# Echo OWNER/REPO for a github.com origin URL, else fail. Accepted forms:
|
|
5821
|
+
# https://github.com/O/R[.git][/], git@github.com:O/R[.git][/],
|
|
5822
|
+
# ssh://git@github.com/O/R[.git][/], host matched case-insensitively. Anything
|
|
5823
|
+
# else (other hosts, SSH host aliases such as github.com-work, embedded
|
|
5824
|
+
# credentials, extra path segments, odd characters) is rejected;
|
|
5825
|
+
# _loki_origin_refusal says why.
|
|
5826
|
+
_loki_github_repo_from_url() {
|
|
5827
|
+
local _u="${1:-}" _lc _pre _p
|
|
5828
|
+
_lc="$(printf '%s' "$_u" | tr '[:upper:]' '[:lower:]')"
|
|
5829
|
+
case "$_lc" in
|
|
5830
|
+
https://github.com/*) _pre="https://github.com/" ;;
|
|
5831
|
+
git@github.com:*) _pre="git@github.com:" ;;
|
|
5832
|
+
ssh://git@github.com/*) _pre="ssh://git@github.com/" ;;
|
|
5833
|
+
*) return 1 ;;
|
|
5834
|
+
esac
|
|
5835
|
+
_p="${_u:${#_pre}}"
|
|
5836
|
+
_p="${_p%/}"
|
|
5837
|
+
_p="${_p%.git}"
|
|
5838
|
+
case "$_p" in
|
|
5839
|
+
*/*/* | /* | */ | */.* | .* | *..* | *[!A-Za-z0-9._/-]*) return 1 ;;
|
|
5840
|
+
?*/?*) printf '%s\n' "$_p" ;;
|
|
5841
|
+
*) return 1 ;;
|
|
5842
|
+
esac
|
|
5843
|
+
}
|
|
5844
|
+
|
|
5845
|
+
# One line saying why an origin is refused. Never echoes the URL itself (it
|
|
5846
|
+
# may carry embedded credentials).
|
|
5847
|
+
_loki_origin_refusal() {
|
|
5848
|
+
local _lc
|
|
5849
|
+
_lc="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
|
|
5850
|
+
case "$_lc" in
|
|
5851
|
+
'') echo "this repository has no origin remote" ;;
|
|
5852
|
+
*@github.com[!:/]*|*://github.com[!/]*|*@github.com[!:/]*:*)
|
|
5853
|
+
echo "origin uses an SSH host alias or look-alike of github.com (for example github.com-work from ~/.ssh/config); Loki pushes only to a literal github.com origin, so the push never depends on agent-editable ssh config" ;;
|
|
5854
|
+
*github.com*)
|
|
5855
|
+
echo "origin names github.com but is not a plain https or ssh OWNER/REPO URL (embedded credentials, extra path segments or unusual characters are refused)" ;;
|
|
5856
|
+
*)
|
|
5857
|
+
echo "origin is not on github.com; Loki currently pushes only to github.com (GitHub Enterprise and GitLab are planned)" ;;
|
|
5858
|
+
esac
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
# Origin pin (round 6, reviewer F1): the agent could otherwise repoint origin
|
|
5862
|
+
# at another github.com repository and have Loki push its branch there with
|
|
5863
|
+
# the operator's credentials. Loki never changes origin itself, so the value
|
|
5864
|
+
# recorded before the first iteration (possibly "no origin") is the only one
|
|
5865
|
+
# a trusted push accepts. Recorded once per process.
|
|
5866
|
+
_LOKI_ORIGIN_PINNED=""
|
|
5867
|
+
_LOKI_PINNED_ORIGIN=""
|
|
5868
|
+
_loki_pin_origin() {
|
|
5869
|
+
[ -z "$_LOKI_ORIGIN_PINNED" ] || return 0
|
|
5870
|
+
_LOKI_PINNED_ORIGIN="$(git -C "${TARGET_DIR:-.}" config --get remote.origin.url 2>/dev/null || true)"
|
|
5871
|
+
_LOKI_ORIGIN_PINNED=1
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5874
|
+
# OWNER/REPO of TARGET_DIR's origin, read as data, or nothing.
|
|
5875
|
+
_loki_trusted_repo() {
|
|
5876
|
+
_loki_github_repo_from_url "$(git -C "${TARGET_DIR:-.}" config --get remote.origin.url 2>/dev/null)" 2>/dev/null || true
|
|
5877
|
+
}
|
|
5878
|
+
|
|
5879
|
+
# _loki_run_neutral <owner/repo|""> <cmd...>: run cmd from / so no repository
|
|
5880
|
+
# (and so no repo config) is discovered, with GH_REPO naming the repo for gh.
|
|
5881
|
+
# A subshell, so functions stay callable; callers pass `command gh` to reach
|
|
5882
|
+
# the binary.
|
|
5883
|
+
_loki_run_neutral() {
|
|
5884
|
+
local _repo="$1"
|
|
5885
|
+
shift
|
|
5886
|
+
(
|
|
5887
|
+
cd / || exit 1
|
|
5888
|
+
unset GIT_DIR GIT_WORK_TREE
|
|
5889
|
+
if [ -n "$_repo" ] && [ -z "${GH_REPO:-}" ]; then
|
|
5890
|
+
export GH_REPO="$_repo"
|
|
5891
|
+
fi
|
|
5892
|
+
"$@"
|
|
5893
|
+
)
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5896
|
+
# _loki_trusted_push <runner> <repo-dir> <branch>: push <branch> of the agent
|
|
5897
|
+
# repo to its github.com origin without loading the agent repo's config in
|
|
5898
|
+
# the credentialed process. <runner> is the credential re-grant wrapper
|
|
5899
|
+
# (_loki_with_github_tokens, or on_run_complete's timed _loki_net). The agent
|
|
5900
|
+
# repo's branch gets no upstream set (it did with `push -u`); nothing reads it.
|
|
5901
|
+
_loki_trusted_push() {
|
|
5902
|
+
local _runner="$1" _dir="$2" _branch="$3" _url _tmp _rc=1
|
|
5903
|
+
_dir="$(cd "$_dir" 2>/dev/null && pwd -P)" || return 1
|
|
5904
|
+
git check-ref-format --branch "$_branch" >/dev/null 2>&1 || return 1
|
|
5905
|
+
_url="$(git -C "$_dir" config --get remote.origin.url 2>/dev/null)" || _url=""
|
|
5906
|
+
if ! _loki_github_repo_from_url "$_url" >/dev/null; then
|
|
5907
|
+
log_warn "Not pushing branch '$_branch': $(_loki_origin_refusal "$_url")."
|
|
5908
|
+
return 2
|
|
5909
|
+
fi
|
|
5910
|
+
if [ -n "$_LOKI_ORIGIN_PINNED" ] && [ "$_url" != "$_LOKI_PINNED_ORIGIN" ]; then
|
|
5911
|
+
log_warn "Not pushing branch '$_branch': origin changed during the run (it was $(_loki_github_repo_from_url "$_LOKI_PINNED_ORIGIN" 2>/dev/null || echo "not a GitHub repository") when the run started). Loki does not push to a destination the agent could have chosen."
|
|
5912
|
+
return 2
|
|
5913
|
+
fi
|
|
5914
|
+
_tmp="$(mktemp -d "${TMPDIR:-/tmp}/loki-push.XXXXXX")" || return 1
|
|
5915
|
+
# --update-shallow: a shallow agent repo's history ends in shallow roots,
|
|
5916
|
+
# which a plain fetch rejects while still exiting 0. The rev-parse makes a
|
|
5917
|
+
# silently empty fetch fail here instead of at the push.
|
|
5918
|
+
if git init -q --template= "$_tmp" >/dev/null 2>&1 \
|
|
5919
|
+
&& git -C "$_tmp" fetch -q --no-tags --update-shallow "$_dir" "+refs/heads/$_branch:refs/heads/$_branch" >/dev/null 2>&1 \
|
|
5920
|
+
&& git -C "$_tmp" rev-parse -q --verify "refs/heads/$_branch" >/dev/null 2>&1; then
|
|
5921
|
+
_rc=0
|
|
5922
|
+
# `-C "$_tmp"`: git starts repository discovery in the fresh repo, so
|
|
5923
|
+
# the agent repo is never found, whatever the caller's cwd.
|
|
5924
|
+
"$_runner" git -C "$_tmp" push -q "$_url" "refs/heads/$_branch:refs/heads/$_branch" || _rc=$?
|
|
5925
|
+
fi
|
|
5926
|
+
rm -rf "$_tmp"
|
|
5366
5927
|
return "$_rc"
|
|
5367
5928
|
}
|
|
5368
5929
|
|
|
5369
5930
|
_loki_withhold_github_tokens() {
|
|
5370
5931
|
local _v _held=""
|
|
5932
|
+
# Before any provider runs, so the pin is the operator's origin, not one
|
|
5933
|
+
# an agent set. Also under the opt-out: it guards the push destination.
|
|
5934
|
+
_loki_pin_origin
|
|
5371
5935
|
# Operator opt-out (exact value 1): keep the earlier behavior, where the
|
|
5372
|
-
# agent inherits the token. That is a Rule of Two
|
|
5936
|
+
# agent inherits the token and the real gh config. That is a Rule of Two
|
|
5937
|
+
# exposure, so say so. No GH_CONFIG_DIR/credential.helper scoping under
|
|
5938
|
+
# the opt-out either -- the point of this knob is the old, fully-inherited
|
|
5939
|
+
# behavior.
|
|
5373
5940
|
if [ "${LOKI_ALLOW_AGENT_GITHUB_TOKEN:-}" = "1" ]; then
|
|
5374
5941
|
for _v in GH_TOKEN GITHUB_TOKEN GH_ENTERPRISE_TOKEN GITHUB_ENTERPRISE_TOKEN; do
|
|
5375
5942
|
[ -n "${!_v:-}" ] && _held="${_held:+$_held }$_v"
|
|
@@ -5377,21 +5944,30 @@ _loki_withhold_github_tokens() {
|
|
|
5377
5944
|
[ -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
5945
|
return 0
|
|
5379
5946
|
fi
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
#
|
|
5390
|
-
#
|
|
5391
|
-
|
|
5392
|
-
|
|
5947
|
+
local _first_time=""
|
|
5948
|
+
[ -n "$_LOKI_WITHHELD_TOKENS" ] || _first_time=1
|
|
5949
|
+
# gh documents (`gh help environment`) that an env token "takes precedence
|
|
5950
|
+
# over previously stored credentials", so a garbage-but-present value
|
|
5951
|
+
# makes gh's own resolution (both `gh auth token` and `gh auth
|
|
5952
|
+
# git-credential`, the latter reachable directly by git's
|
|
5953
|
+
# credential.helper) return the garbage value instead of falling through
|
|
5954
|
+
# to a keyring/hosts.yml lookup. GH_CONFIG_DIR stays scoped to a fresh
|
|
5955
|
+
# empty dir (the plaintext-hosts.yml-read case). credential.helper is
|
|
5956
|
+
# additionally reset to empty (gitcredentials(7)): this is git-invoked,
|
|
5957
|
+
# not gh-mediated, so the sentinel does not reach a plain
|
|
5958
|
+
# osxkeychain/libsecret helper on its own.
|
|
5959
|
+
_loki_gh_capture
|
|
5960
|
+
_loki_gh_apply
|
|
5961
|
+
if [ -n "$_first_time" ]; then
|
|
5962
|
+
if [ -n "$_LOKI_GH_INHERITED" ]; then
|
|
5963
|
+
printf '%s\n' "WARNING: this run was started from inside an already-withheld Loki session (inherited Rule of Two sentinel). It has no real GitHub or SSH credentials to restore, so its own push and PR steps will fail closed. Start builds that need to push from a normal shell." >&2
|
|
5964
|
+
else
|
|
5965
|
+
log_info "Withheld from agent sessions (Rule of Two): $_LOKI_WITHHELD_TOKENS (sentineled, not merely unset), gh config store, git credential.helper, SSH agent (SSH_AUTH_SOCK unset, GIT_SSH_COMMAND=false). Loki's own push and PR steps still use the real credentials."
|
|
5966
|
+
fi
|
|
5967
|
+
if [ -n "$_LOKI_GIT_CONFIG_COUNT_UNSUPPORTED" ]; then
|
|
5968
|
+
printf '%s\n' "WARNING: git < 2.31 detected -- GIT_CONFIG_COUNT/GIT_CONFIG_KEY/GIT_CONFIG_VALUE (used to reset credential.helper) are silently ignored on this git version. The git-invoked credential-helper bypass (BACKLOG 149 round 2) is NOT closed on this host; upgrade git to 2.31+ to close it. The GH_TOKEN-family sentinel, GH_CONFIG_DIR scoping, and the SSH_AUTH_SOCK/GIT_SSH_COMMAND withhold above are unaffected and still apply (GIT_SSH_COMMAND is a plain env var, not a GIT_CONFIG_* mechanism, so it needs no version floor)." >&2
|
|
5969
|
+
fi
|
|
5393
5970
|
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
5971
|
}
|
|
5396
5972
|
|
|
5397
5973
|
#===============================================================================
|
|
@@ -5462,11 +6038,16 @@ on_run_complete() {
|
|
|
5462
6038
|
# The push and gh calls below are the trusted post-session step, so they
|
|
5463
6039
|
# get the withheld GitHub token back (a plain `timeout 30 gh` would exec
|
|
5464
6040
|
# the binary and bypass the gh() wrapper).
|
|
6041
|
+
# Round 5: every call runs from / (_loki_run_neutral) with GH_REPO read as
|
|
6042
|
+
# data first, so neither gh nor git loads the agent's repo config while
|
|
6043
|
+
# holding the credentials; the push goes through _loki_trusted_push.
|
|
5465
6044
|
_loki_net() {
|
|
6045
|
+
local _net_repo
|
|
6046
|
+
_net_repo="$(_loki_trusted_repo)"
|
|
5466
6047
|
if command -v timeout >/dev/null 2>&1; then
|
|
5467
|
-
_loki_with_github_tokens timeout 30 "$@"
|
|
6048
|
+
_loki_with_github_tokens _loki_run_neutral "$_net_repo" timeout 30 "$@"
|
|
5468
6049
|
else
|
|
5469
|
-
_loki_with_github_tokens "$@"
|
|
6050
|
+
_loki_with_github_tokens _loki_run_neutral "$_net_repo" "$@"
|
|
5470
6051
|
fi
|
|
5471
6052
|
}
|
|
5472
6053
|
# Require gh + auth.
|
|
@@ -5492,14 +6073,25 @@ on_run_complete() {
|
|
|
5492
6073
|
log_info "LOKI_DELEGATE_PR=1: opening a local pull request for branch '$branch'..."
|
|
5493
6074
|
# Push, then create. Non-interactive (no tty in --bg). Best-effort, each
|
|
5494
6075
|
# network call bounded by the timeout guard above.
|
|
5495
|
-
|
|
6076
|
+
# A refused or failed push is reported (the refusal names its reason on
|
|
6077
|
+
# stderr) and ends the delegate step: there is nothing to open a PR from.
|
|
6078
|
+
local _push_rc=0
|
|
6079
|
+
_loki_trusted_push _loki_net "${TARGET_DIR:-.}" "$branch" >/dev/null || _push_rc=$?
|
|
6080
|
+
if [ "$_push_rc" -ne 0 ]; then
|
|
6081
|
+
[ "$_push_rc" -eq 2 ] || log_warn "LOKI_DELEGATE_PR=1: pushing branch '$branch' failed; not opening a pull request."
|
|
6082
|
+
return 0
|
|
6083
|
+
fi
|
|
6084
|
+
# Explicit --repo on every gh call (not only GH_REPO): the repo the push
|
|
6085
|
+
# just validated, so no gh subcommand can fall back to cwd detection.
|
|
6086
|
+
local _pr_repo
|
|
6087
|
+
_pr_repo="$(_loki_trusted_repo)"
|
|
5496
6088
|
local pr_title
|
|
5497
6089
|
pr_title="Loki Mode: ${branch}"
|
|
5498
6090
|
local pr_url=""
|
|
5499
6091
|
# ENT-4 (idempotent PR): reuse an existing OPEN PR for this head instead of
|
|
5500
6092
|
# attempting a second create on a platform retry / resume.
|
|
5501
6093
|
local existing_pr
|
|
5502
|
-
existing_pr="$( (cd "${TARGET_DIR:-.}" && _loki_net gh pr list --head "$branch" --state open --json url --jq '.[0].url') 2>/dev/null || true )"
|
|
6094
|
+
existing_pr="$( (cd "${TARGET_DIR:-.}" && _loki_net gh pr list --repo "$_pr_repo" --head "$branch" --state open --json url --jq '.[0].url') 2>/dev/null || true )"
|
|
5503
6095
|
if [ -n "$existing_pr" ]; then
|
|
5504
6096
|
_LOKI_DELEGATE_PR_URL="$existing_pr"
|
|
5505
6097
|
export _LOKI_DELEGATE_PR_URL
|
|
@@ -5530,7 +6122,7 @@ ${_del_receipt}"
|
|
|
5530
6122
|
fi
|
|
5531
6123
|
fi
|
|
5532
6124
|
fi
|
|
5533
|
-
pr_url="$( (cd "${TARGET_DIR:-.}" && _loki_net gh pr create --title "$pr_title" --body "$_del_body" --head "$branch") 2>/dev/null || true )"
|
|
6125
|
+
pr_url="$( (cd "${TARGET_DIR:-.}" && _loki_net gh pr create --repo "$_pr_repo" --title "$pr_title" --body "$_del_body" --head "$branch") 2>/dev/null || true )"
|
|
5534
6126
|
if [ -n "$pr_url" ]; then
|
|
5535
6127
|
# Export so build_completion_summary folds the url into the summary.
|
|
5536
6128
|
_LOKI_DELEGATE_PR_URL="$pr_url"
|
|
@@ -10611,10 +11203,18 @@ create_session_pr() {
|
|
|
10611
11203
|
|
|
10612
11204
|
# OPT-IN (LOKI_AUTO_PR=1): legacy auto push + PR, now with the correct base.
|
|
10613
11205
|
log_info "Pushing agent branch: $branch_name"
|
|
10614
|
-
|
|
10615
|
-
|
|
11206
|
+
# Round 5: never a credentialed `git push` inside the agent's repo (its
|
|
11207
|
+
# config -- hooks, sshCommand, credential.helper -- would run holding the
|
|
11208
|
+
# operator's credentials). See _loki_trusted_push.
|
|
11209
|
+
# stderr is left visible: a refusal names its reason there.
|
|
11210
|
+
local _push_rc=0
|
|
11211
|
+
_loki_trusted_push _loki_with_github_tokens . "$branch_name" >/dev/null || _push_rc=$?
|
|
11212
|
+
if [ "$_push_rc" -ne 0 ]; then
|
|
11213
|
+
[ "$_push_rc" -eq 2 ] || log_warn "Failed to push agent branch: $branch_name"
|
|
10616
11214
|
return 1
|
|
10617
11215
|
fi
|
|
11216
|
+
local _pr_repo
|
|
11217
|
+
_pr_repo="$(_loki_trusted_repo)"
|
|
10618
11218
|
|
|
10619
11219
|
# Create PR if gh CLI is available
|
|
10620
11220
|
if command -v gh &>/dev/null; then
|
|
@@ -10626,7 +11226,7 @@ create_session_pr() {
|
|
|
10626
11226
|
# guarantee explicit and the log honest: if an OPEN PR already exists for
|
|
10627
11227
|
# this head, reuse its URL instead of attempting a second create.
|
|
10628
11228
|
local existing_pr
|
|
10629
|
-
existing_pr=$(gh pr list --head "$branch_name" --state open --json url --jq '.[0].url' 2>/dev/null || true)
|
|
11229
|
+
existing_pr=$(gh pr list --repo "$_pr_repo" --head "$branch_name" --state open --json url --jq '.[0].url' 2>/dev/null || true)
|
|
10630
11230
|
if [ -n "$existing_pr" ]; then
|
|
10631
11231
|
log_info "PR already exists for branch $branch_name: $existing_pr (skipping create)"
|
|
10632
11232
|
audit_log "PR_EXISTS" "branch=$branch_name,url=$existing_pr"
|
|
@@ -10667,6 +11267,7 @@ ${_auto_receipt}"
|
|
|
10667
11267
|
fi
|
|
10668
11268
|
fi
|
|
10669
11269
|
pr_url=$(gh pr create \
|
|
11270
|
+
--repo "$_pr_repo" \
|
|
10670
11271
|
--title "Loki Mode: Agent session changes ($branch_name)" \
|
|
10671
11272
|
--body "$_auto_body" \
|
|
10672
11273
|
--base "$base" \
|
|
@@ -18588,21 +19189,10 @@ start_dashboard() {
|
|
|
18588
19189
|
log_info "Dashboard: ${CYAN}${url_scheme}://127.0.0.1:$DASHBOARD_PORT/${NC}"
|
|
18589
19190
|
|
|
18590
19191
|
# Auto-open the dashboard in the browser, but ONLY for an interactive
|
|
18591
|
-
# foreground session.
|
|
18592
|
-
#
|
|
18593
|
-
|
|
18594
|
-
|
|
18595
|
-
if [ -t 1 ] && [ "${BACKGROUND_MODE:-false}" != "true" ] && [ "${LOKI_NO_AUTO_OPEN:-0}" != "1" ]; then
|
|
18596
|
-
local _dash_url="${url_scheme}://127.0.0.1:$DASHBOARD_PORT/"
|
|
18597
|
-
if command -v open >/dev/null 2>&1; then
|
|
18598
|
-
open "$_dash_url" 2>/dev/null || true
|
|
18599
|
-
elif command -v xdg-open >/dev/null 2>&1; then
|
|
18600
|
-
xdg-open "$_dash_url" 2>/dev/null || true
|
|
18601
|
-
elif command -v cmd.exe >/dev/null 2>&1; then
|
|
18602
|
-
# Windows (Git Bash/WSL): `start` is a cmd builtin, not on PATH,
|
|
18603
|
-
# so invoke it via cmd.exe. The empty "" is start's title arg.
|
|
18604
|
-
cmd.exe /c start "" "$_dash_url" 2>/dev/null || true
|
|
18605
|
-
fi
|
|
19192
|
+
# foreground session. loki_open_url (lib/browser-open.sh) refuses on
|
|
19193
|
+
# no TTY, CI, test runners, LOKI_NO_BROWSER=1 or LOKI_NO_AUTO_OPEN=1.
|
|
19194
|
+
if [ "${BACKGROUND_MODE:-false}" != "true" ]; then
|
|
19195
|
+
loki_open_url "${url_scheme}://127.0.0.1:$DASHBOARD_PORT/" 2>/dev/null || true
|
|
18606
19196
|
fi
|
|
18607
19197
|
return 0
|
|
18608
19198
|
else
|
|
@@ -27498,6 +28088,15 @@ _loki_session_exit_cleanup() {
|
|
|
27498
28088
|
# completion-path reap for the rest of the run.
|
|
27499
28089
|
_loki_remove_pgid_file
|
|
27500
28090
|
_loki_remove_temp_self_copy
|
|
28091
|
+
# BACKLOG 149: remove the per-run empty GH_CONFIG_DIR scoped by
|
|
28092
|
+
# _loki_withhold_github_tokens. Same subshell guard as
|
|
28093
|
+
# _loki_remove_pgid_file -- a subshell's EXIT trap must not delete the
|
|
28094
|
+
# live parent's directory ($$ is stable across subshells, BASHPID is not).
|
|
28095
|
+
if [ -n "${_LOKI_GH_CONFIG_SCOPED:-}" ] && [ "${BASHPID:-$$}" = "$$" ]; then
|
|
28096
|
+
case "$_LOKI_GH_CONFIG_SCOPED" in
|
|
28097
|
+
"${TMPDIR:-/tmp}"/loki-gh-config.*) rm -rf "$_LOKI_GH_CONFIG_SCOPED" 2>/dev/null || true ;;
|
|
28098
|
+
esac
|
|
28099
|
+
fi
|
|
27501
28100
|
return "$exit_code"
|
|
27502
28101
|
}
|
|
27503
28102
|
|
|
@@ -27953,10 +28552,19 @@ main() {
|
|
|
27953
28552
|
|
|
27954
28553
|
# Handle background mode
|
|
27955
28554
|
if [ "$BACKGROUND_MODE" = "true" ]; then
|
|
27956
|
-
# The relaunched runner withholds the tokens itself; it needs
|
|
27957
|
-
#
|
|
27958
|
-
|
|
27959
|
-
|
|
28555
|
+
# The relaunched runner withholds the tokens itself; it needs the REAL
|
|
28556
|
+
# pre-withhold state in its inherited environment to capture correctly,
|
|
28557
|
+
# not this process's own scoped/sentineled state (BACKLOG 149 round 2:
|
|
28558
|
+
# if the child inherited THIS process's scoped GH_CONFIG_DIR/
|
|
28559
|
+
# GIT_CONFIG_COUNT as if they were the operator's originals, its own
|
|
28560
|
+
# trusted gh/git calls would run with credential.helper reset and
|
|
28561
|
+
# GH_CONFIG_DIR pointed at a directory this process's EXIT trap deletes
|
|
28562
|
+
# seconds later). Full restore, not just the 4 token vars, exactly like
|
|
28563
|
+
# the pre-trusted-call restore. This process exits right after
|
|
28564
|
+
# launching, so nothing here needs to re-apply the withhold afterward.
|
|
28565
|
+
if [ -n "$_LOKI_WITHHELD_TOKENS" ]; then
|
|
28566
|
+
_loki_gh_restore
|
|
28567
|
+
fi
|
|
27960
28568
|
# Initialize .loki directory first
|
|
27961
28569
|
mkdir -p .loki/logs
|
|
27962
28570
|
|
|
@@ -28592,9 +29200,12 @@ main() {
|
|
|
28592
29200
|
# then advise the user how to open a PR. Both are no-ops when no agent branch
|
|
28593
29201
|
# was set up (LOKI_BRANCH_PROTECTION=false) or nothing changed.
|
|
28594
29202
|
commit_session_changes
|
|
28595
|
-
# Trusted post-session step
|
|
28596
|
-
#
|
|
28597
|
-
|
|
29203
|
+
# Trusted post-session step. NOT wrapped in _loki_with_github_tokens as a
|
|
29204
|
+
# whole (round 5): that ran every git call inside it (rev-list, merge-base,
|
|
29205
|
+
# push) with the real credentials AND the agent's repo config loaded. Its
|
|
29206
|
+
# push (_loki_trusted_push) and gh calls (gh wrapper) each re-grant the
|
|
29207
|
+
# credentials themselves, outside the agent's repo.
|
|
29208
|
+
create_session_pr
|
|
28598
29209
|
audit_agent_action "session_stop" "Session ended" "result=$result,iterations=$ITERATION_COUNT"
|
|
28599
29210
|
|
|
28600
29211
|
# The first terminal summary is written before session changes are committed.
|