Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d89ee71
docs: scrub internal cluster names from master-delivery design doc (#…
clubanderson Aug 18, 2026
1cbcd03
πŸ› fix(hub): leaked background commit-order resolves reached the real …
clubanderson Aug 18, 2026
e15b828
feat(deploy): openshift-netadmin SCC overlay + NET_ADMIN pre-flight d…
clubanderson Aug 18, 2026
c2e910e
πŸ› fix(relay): the CLI-liveness probe could never see a dead CLI, so t…
Danathar Aug 18, 2026
8dcade4
✨ feature(governor,config,dashboard): scale default mode thresholds b…
Danathar Aug 18, 2026
e379c7a
Merge pull request #4039 from Danathar/fix/relay-cli-liveness
kubestellar-prow[bot] Aug 18, 2026
a209867
πŸ› fix(contribute): pin the CLI's working directory β€” a tmux server wi…
Danathar Aug 18, 2026
43067dd
πŸ› fix(contribute): pin the container entrypoint's tmux cwd too
clubanderson Aug 18, 2026
2c575fc
✨ feat(config): project.issue_filter β€” label-gate which issues agents…
clubanderson Aug 18, 2026
52b06ab
πŸ› fix(gh-wrapper): label injection must never fail the operation; edi…
clubanderson Aug 18, 2026
d811360
Merge pull request #4046 from Danathar/fix/relay-pane-cwd
kubestellar-prow[bot] Aug 18, 2026
e0c30a5
πŸ”’ fix(agent-launch): scrub backend-re-exported GitHub tokens from age…
clubanderson Aug 18, 2026
bd5efab
πŸ› fix(gh-wrapper): restore author-gated listing for staff agents β€” tr…
clubanderson Aug 18, 2026
ce60ad5
πŸ› fix(config): de-materialize stale login_patterns so #3959 defaults …
clubanderson Aug 18, 2026
b5c3da9
πŸ”„ sync: top up v5 with latest v4
clubanderson Aug 18, 2026
34e181d
✨ feat(pause): surface pause provenance β€” WHO paused WHAT WHEN, every…
clubanderson Aug 18, 2026
5b435b6
πŸ”„ sync: fold in v4 #4055 pause provenance
clubanderson Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/v2-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ jobs:
working-directory: .
run: bash bin/test_gh_auth_native_no_cat.sh

# #4045: agent CLI backends (observed live: Copilot CLI) re-export their
# own live GitHub credential as GITHUB_TOKEN into every tool shell they
# spawn β€” after all launch-path scrubbing β€” letting a wrapper-denied
# agent write to repos via raw curl, outside every gh-wrapper control.
# This EXECUTES shells the way a backend spawns them (token in the spawn
# env) and asserts the BASH_ENV scrub leaves them token-less, with
# positive controls that the probe can see a leak and that the wrapper
# still authenticates from the per-agent cache.
- name: agent shell token scrub (#4045)
working-directory: .
run: bash bin/test_agent_env_scrub.sh

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
Expand Down
32 changes: 29 additions & 3 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -489,10 +489,36 @@ contribute-hive backend="" mode="docker": check-version
fi
fi

# Create tmux session with the CLI
# Create tmux session with the CLI.
#
# The launch command CDs into the repo first. That is not belt-and-braces:
# a long-lived tmux server keeps its own working directory, and when that
# directory is deleted (here: a nested clone's v2/pkg/agent, orphaned when
# the repo renamed v2/ -> src/) every pane the server forks inherits the
# dead cwd. The pane's shell says so β€” "shell-init: error retrieving
# current directory" β€” and a backend that requires a resolvable cwd then
# dies seconds after its first task. agy exits 2 that way; claude, codex
# and goose happen to tolerate it, which is why this hid for so long.
#
# -c is NOT sufficient on its own: on a server whose own cwd is gone,
# `tmux new-session -c <valid path>` still forks the pane into the deleted
# directory (verified against tmux on Fedora 44). It is passed anyway
# because it IS correct on a healthy server; the cd is what carries the
# fix.
tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$TMUX_SESSION" -x 200 -y 50
tmux send-keys -t "$TMUX_SESSION" "${LITELLM_ENV:+$LITELLM_ENV }$CMD $PERM_FLAG" Enter
tmux new-session -d -s "$TMUX_SESSION" -x 200 -y 50 -c "$PWD"
tmux send-keys -t "$TMUX_SESSION" "cd $(printf %q "$PWD") && ${LITELLM_ENV:+$LITELLM_ENV }$CMD $PERM_FLAG" Enter

# Surface a poisoned tmux server rather than letting the backend die a
# silent, unexplained death 30 seconds into its first task.
PANE_PATH="$(tmux display-message -p -t "$TMUX_SESSION" '#{pane_current_path}' 2>/dev/null || echo '')"
case "$PANE_PATH" in
*"(deleted)"|"")
echo "WARNING: this tmux server's working directory no longer exists (pane reports: ${PANE_PATH:-unknown})." >&2
echo " The cd above works around it, but panes you open by hand will start in a dead directory." >&2
echo " Fix it for good with: tmux kill-server (ends all tmux sessions, then rerun this recipe)" >&2
;;
esac

# Start the relay
export HIVE_AGENT_SESSION="$TMUX_SESSION"
Expand Down
1 change: 1 addition & 0 deletions bin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Most production scripts are installed under `/usr/local/bin` by `bin/hive-deploy
| `gh-wrapper.sh` | Enforcement | `gh` wrapper that injects App tokens and enforces global/per-agent restriction rules from `/etc/hive/restrictions/<agent-id>.json`. |
| `hive-open-pr.sh` | Enforcement | Agent-side wrapper for PR creation requests. It writes a request file for the Hive watcher so PRs are opened by the GitHub App bot and pass the same ACMM authorization checks. |
| `setup-proxy-iptables.sh` | Enforcement | Installs iptables rules in the container to force GitHub HTTPS traffic through the ACMM proxy even if an agent unsets proxy variables. |
| `agent-env-scrub.sh` | Enforcement | Sourced (never executed) at the start of every shell in an agent's process tree, via `BASH_ENV`/`ENV` from `agent-launch.sh` and an `/etc/bash.bashrc` guard, to unset the live GitHub credentials backend CLIs re-export into agent tool shells (#4045). |
| `hive-config.sh` | Config | Shared shell config reader that exposes project, repo, agent, dashboard, health, and policy values parsed from `hive-project.yaml`. |

## Deployment and local operation
Expand Down
48 changes: 48 additions & 0 deletions bin/agent-env-scrub.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# agent-env-scrub.sh β€” shell-boundary credential scrub for agent tool shells.
#
# SECURITY (#4045): agent CLI backends re-export their own live GitHub
# credentials into the shells they spawn for tool calls. Observed live on a
# Copilot-backed fleet: the CLI authenticates from its persistent credential
# store (/data/copilot-user-token) and sets GITHUB_TOKEN in the environment of
# every tool shell β€” AFTER all of the hive's launch-path scrubbing (#3931) has
# run. A wrapper-denied agent then fell back to
# curl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/...
# and succeeded at a repo write, bypassing every gh-wrapper control (allowlist
# #3854, mode/ACMM gates, merge eligibility, authorship routing, provenance).
#
# This file is SOURCED β€” never executed β€” at the start of every shell in the
# agent's process tree, via BASH_ENV/ENV exported by agent-launch.sh (tool
# shells, which are non-interactive) and via the /etc/bash.bashrc guard the
# Dockerfile installs (interactive shells). Because the unset runs at CHILD
# shell startup, it neutralizes tokens however they arrived: inherited from the
# session env, or set explicitly in the spawn env by the backend CLI itself β€”
# the #4045 mechanism, which no amount of parent-side scrubbing can reach.
#
# What this deliberately does NOT break:
# - The backend CLI process keeps its own auth. The CLI (node/binary) never
# sources this file; COPILOT_GITHUB_TOKEN / GITHUB_TOKEN stay in ITS env
# for Copilot API auth and the built-in GitHub MCP server (the sanctioned,
# hive-mediated write path under github.app_authored_prs).
# - gh-wrapper.sh still authenticates: it sources this at startup (losing
# only inherited token env, which it must never trust anyway β€” audit H3)
# and then exports GH_TOKEN itself from HIVE_AGENT_TOKEN_CACHE, which is a
# PATH, not a credential, and is deliberately not scrubbed.
# - git-credential-hive.sh reads the per-agent cache file directly; hive-merge
# and hive-open-pr likewise never rely on inherited token env.
#
# Residual (documented, closed elsewhere): a same-uid agent can still read a
# backend CLI's /proc/<pid>/environ deliberately. That extraction lane β€” and
# any smuggled credential β€” is closed at the transport by the MITM proxy's
# Authorization strip/inject (#1861, PR #4032): the proxy, not the agent env,
# decides what credential GitHub ever sees.
#
# POSIX sh compatible (dash-safe): no bashisms, and `unset` of an absent
# variable is not an error. Keep the variable list in sync with
# bin/test_agent_env_scrub.sh, which source-asserts every name below.
unset GITHUB_TOKEN
unset GH_TOKEN
unset GH_ENTERPRISE_TOKEN
unset GITHUB_ENTERPRISE_TOKEN
unset COPILOT_GITHUB_TOKEN
unset GITHUB_COPILOT_TOKEN
unset HIVE_GITHUB_TOKEN
30 changes: 30 additions & 0 deletions bin/agent-launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,36 @@ if [[ "$BACKEND" == "copilot" ]]; then
fi
fi

# SECURITY (#4045): the backend CLI re-exports live GitHub credentials into
# the tool shells it spawns. The Copilot CLI authenticates from its own
# persistent store (or COPILOT_GITHUB_TOKEN above) and sets GITHUB_TOKEN in
# every shell it runs for the agent β€” after all launch-path scrubbing (#3931)
# has already happened, so unsetting here cannot reach it. Observed live: a
# wrapper-denied agent fell back to raw
# curl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/...
# and performed a repo write outside every gh-wrapper control.
#
# BASH_ENV is sourced by every NON-INTERACTIVE bash at startup β€” i.e. by each
# tool shell the CLI spawns, INCLUDING ones the CLI hands an explicit
# GITHUB_TOKEN in the spawn env β€” so the scrub runs inside the child, at the
# only boundary that sees the backend's re-export. ENV covers interactive
# POSIX-mode shells the same way. The CLI process itself is not a shell and
# never sources this file, so its own auth (COPILOT_GITHUB_TOKEN, the
# app_authored_prs GITHUB_TOKEN for the built-in GitHub MCP server) is
# untouched: this changes what the AGENT'S SHELLS see, not what the backend
# can do. gh-wrapper.sh and git-credential-hive.sh keep working β€” both
# authenticate from HIVE_AGENT_TOKEN_CACHE (a path, deliberately not
# scrubbed), never from inherited token env. Interactive shells get the same
# scrub from the /etc/bash.bashrc guard installed by the Dockerfile.
AGENT_ENV_SCRUB="${SCRIPT_DIR}/agent-env-scrub.sh"
[[ -f "$AGENT_ENV_SCRUB" ]] || AGENT_ENV_SCRUB="/usr/local/bin/agent-env-scrub.sh"
if [[ -f "$AGENT_ENV_SCRUB" ]]; then
export BASH_ENV="$AGENT_ENV_SCRUB"
export ENV="$AGENT_ENV_SCRUB"
else
echo "[agent-launch] WARN: agent-env-scrub.sh not found β€” backend-exported GitHub tokens will be visible in agent tool shells (#4045)" >&2
fi

# Scrub GitHub token patterns and JWTs from stderr before writing to disk.
scrub_tokens() {
sed -u -E \
Expand Down
22 changes: 21 additions & 1 deletion bin/contributor-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,21 @@ if [[ "$CONTRIBUTOR_MODE" == "interactive" ]]; then
tmux new-session -d -s "$TMUX_SESSION" -c "$HIVE_WORKSPACE_DIR" -x 200 -y 50
fi

# kubestellar/hive#4046: a long-lived tmux SERVER (this container can run one
# for its whole lifetime) keeps its own working directory, and every pane it
# forks afterward inherits it β€” even a pane started with an explicit, valid
# `-c`, if the server's own cwd is already gone. That is exactly the shape of
# directory HIVE_WORKSPACE_DIR is: agents clone into subdirectories of it
# (contribute_ws.go's assignment prompt), so pinning the launch's `cd` at
# HIVE_WORKSPACE_DIR itself would re-create the trap the moment that directory
# is ever removed and recreated out from under a still-running server. `cd`
# into $HOME instead β€” a directory nothing in the task lifecycle deletes or
# recreates β€” so the CLI always starts somewhere resolvable regardless of what
# happens to the workspace subtree underneath it; the CLI's own per-task `cd`
# into $HIVE_WORKSPACE_DIR/<repo> (per the assignment prompt) is unchanged.
HIVE_AGENT_CWD="${HOME}"
mkdir -p "$HIVE_AGENT_CWD"

# Start the relay in the background
echo "Starting ClankeR relay connection to hub..."
node "${SCRIPT_DIR}/contributor-relay.sh" &
Expand Down Expand Up @@ -528,7 +543,12 @@ fi
# relay launches a one-shot CLI per task, and one-shot invocations do not draw
# the trust/theme/onboarding dialogs this loop dismisses.
if [[ "$CONTRIBUTOR_MODE" == "interactive" ]]; then
tmux send-keys -t "$TMUX_SESSION" "$CMD $PERM_FLAG $MODEL_FLAG $REASONING_FLAG" Enter
# kubestellar/hive#4046: `-c` on new-session above is NOT sufficient once the
# server's own cwd is already gone β€” verified: new-session with a valid `-c`
# still forks the pane into the dead directory. The `cd` in the literal
# launch line is what actually carries the fix; -c is defense-in-depth for a
# healthy server.
tmux send-keys -t "$TMUX_SESSION" "cd $(printf %q "$HIVE_AGENT_CWD") && $CMD $PERM_FLAG $MODEL_FLAG $REASONING_FLAG" Enter

# Auto-dismiss startup prompts (workspace trust, theme picker, etc.)
AUTO_DISMISS_ATTEMPTS=10
Expand Down
Loading
Loading