fix(#2911): keep the forge token out of argv

`forge_git_url` spliced `core:<token>@` between scheme and authority, and
that URL is a process argument. `/proc/<pid>/cmdline` is mode 0444 —
world-readable — so the core admin token, which provisions every agent's
forge account, was published to any local user for the lifetime of each
git child. Seven call sites built such a URL.

The credential now travels in the environment instead:
`git_command_authed` sets `http.extraHeader` via `GIT_CONFIG_*`, which
git reads exactly like a config file, and `/proc/<pid>/environ` is 0400 —
owner-only. Same credential, materially smaller audience. The remote is a
plain `http://forge/<org>/<repo>.git`, and `forge_git_url` no longer takes
a token, so the old shape cannot be rebuilt by accident.

`knowledge`'s clone was the one place a credentialed URL was stored as a
named remote — git persists the clone URL into `.git/config`, so the
token sat on disk and every later `pull` authenticated from there. That
is the case `forge::repos::push_config` documents as forbidden ("the
tokenised URL ... deliberately never stored as a named remote"). `pull`
now rewrites `origin` to the plain URL first, which also scrubs the
persisted token from existing deployments, and authenticates from the
environment when a token is available. The repo is public, so the pull
still works without one.

Three call sites also stopped spawning `Command::new("git")` directly,
so they honour the `HYPERHIVE_GIT` path the NixOS module bakes in and
the `kill_on_drop` every other git spawn gets.

The two URL-shape tests now assert the *absence* of a credential, and a
new one decodes the header back to `core:<token>` — without that, a
malformed header would leave every forge operation silently anonymous
with the other assertions still green.
This commit is contained in:
atlas 2026-08-02 13:21:42 +02:00
commit 44572d1e1a
7 changed files with 134 additions and 56 deletions

View file

@ -60,8 +60,29 @@ pub fn git_command() -> Command {
cmd
}
pub async fn git(dir: &Path, args: &[&str]) -> Result<()> {
let out = git_command()
/// [`git_command`] carrying HTTP credentials **in its environment**.
///
/// `auth_header` is an `Authorization:` line (see
/// [`crate::forge::core_auth_header`]) handed to git as `http.extraHeader`
/// through `GIT_CONFIG_*`, which git reads exactly like a config file. The
/// alternative — userinfo in the remote URL — puts the secret in `argv`, and
/// `/proc/<pid>/cmdline` is world-readable while `/proc/<pid>/environ` is
/// owner-only. Same credential, materially smaller audience.
///
/// `GIT_CONFIG_COUNT` needs git >= 2.31; `HYPERHIVE_GIT` points at a pinned
/// nixpkgs git well past that.
#[must_use]
pub fn git_command_authed(auth_header: &str) -> Command {
let mut cmd = git_command();
cmd.env("GIT_CONFIG_COUNT", "1")
.env("GIT_CONFIG_KEY_0", "http.extraHeader")
.env("GIT_CONFIG_VALUE_0", auth_header);
cmd
}
/// Run `cmd` as `git <args>` in `dir`, erroring on a non-zero exit.
async fn run(mut cmd: Command, dir: &Path, args: &[&str]) -> Result<()> {
let out = cmd
.current_dir(dir)
.args(args)
.output()
@ -78,6 +99,17 @@ pub async fn git(dir: &Path, args: &[&str]) -> Result<()> {
Ok(())
}
pub async fn git(dir: &Path, args: &[&str]) -> Result<()> {
run(git_command(), dir, args).await
}
/// [`git`] against an authenticated remote — for the paths that talk to the
/// forge. The credential rides the environment, so `args` (and therefore the
/// error above) stay free of it.
pub async fn git_authed(dir: &Path, args: &[&str], auth_header: &str) -> Result<()> {
run(git_command_authed(auth_header), dir, args).await
}
/// Resolve `refname` (a tag, branch, or sha) in `dir` to its full sha.
pub async fn git_rev_parse(dir: &Path, refname: &str) -> Result<String> {
let out = git_command()

View file

@ -7,8 +7,9 @@ mod setup;
mod tests;
pub use git::{
git, git_command, git_delete_ref, git_is_ancestor, git_read_tree_reset, git_rev_parse, git_tag,
git_tag_annotated, git_update_ref, git_update_ref_cas,
git, git_authed, git_command, git_command_authed, git_delete_ref, git_is_ancestor,
git_read_tree_reset, git_rev_parse, git_tag, git_tag_annotated, git_update_ref,
git_update_ref_cas,
};
pub use host_config::write_dropins;
pub use setup::{