diff --git a/hive-c0re/src/forge/mod.rs b/hive-c0re/src/forge/mod.rs index 167e631f..05c7d356 100644 --- a/hive-c0re/src/forge/mod.rs +++ b/hive-c0re/src/forge/mod.rs @@ -31,34 +31,7 @@ use users::{ }; const FORGE_CONTAINER: &str = "hive-forge"; - -/// Base HTTP URL for the local Forgejo instance. Reads `HIVE_FORGE_URL` -/// from the environment (set unconditionally by `hive-c0re.nix` to -/// `http://`) so the forge port is never hardcoded. -/// Falls back to `http://localhost:3000` for bare runs outside the -/// NixOS module (tests, manual invocation). -pub(crate) fn forge_http_base() -> &'static str { - static BASE: OnceLock = OnceLock::new(); - BASE.get_or_init(|| { - std::env::var("HIVE_FORGE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string()) - }) -} - -/// Token-in-URL git remote for `repo` (e.g. `"core/meta"`). Inserts -/// `core:` credentials between the scheme and authority of -/// [`forge_http_base()`] — the form git accepts for inline auth. -pub(crate) fn forge_git_url(token: &str, repo: &str) -> String { - let base = forge_http_base(); - // Split on "://" to isolate scheme + authority. The base URL always - // contains "://" (validated fallback + `HIVE_FORGE_URL` is - // operator-set and expected to be well-formed). - if let Some((scheme, host)) = base.split_once("://") { - format!("{scheme}://core:{token}@{host}/{repo}.git") - } else { - format!("http://core:{token}@localhost:3000/{repo}.git") - } -} - +pub(crate) const FORGE_HTTP: &str = "http://localhost:3000"; /// Forgejo org grouping every agent's config repo. Core is a site admin /// and reads + writes every repo here. As of the agent-config-PR flow each /// agent is a **write collaborator on its own** `agent-configs/` repo — @@ -77,7 +50,7 @@ const CONFIG_ORG: &str = "agent-configs"; /// (i.e. `core` user) can push. const SHARED_ORG: &str = "internal"; /// The shared docs repo inside `SHARED_ORG`. Cloneable by every agent -/// at `{forge_http_base()}/internal/docs.git`. +/// at `{FORGE_HTTP}/internal/docs.git`. const SHARED_DOCS_REPO: &str = "docs"; /// The hive-wide knowledge repo inside `SHARED_ORG`. Public — agents /// can fork it and open PRs without explicit collaborator grants. @@ -135,7 +108,7 @@ async fn forge_admin(args: &[&str]) -> Result { Ok(stdout) } -/// Typed Forgejo API client for the local forge ([`forge_http_base()`]), +/// Typed Forgejo API client for the local forge ([`FORGE_HTTP`]), /// authenticated as `token`. All Forgejo API calls that don't shell /// out to `forgejo admin` go through clients built here — one place /// for the base URL and auth. Tokens differ per call site (core admin @@ -146,7 +119,7 @@ async fn forge_admin(args: &[&str]) -> Result { pub(crate) fn api(token: &str) -> Result { static URL: OnceLock = OnceLock::new(); let url = URL - .get_or_init(|| Url::parse(forge_http_base()).expect("forge_http_base() is a valid URL")) + .get_or_init(|| Url::parse(FORGE_HTTP).expect("FORGE_HTTP is a valid URL")) .clone(); Forgejo::new(Auth::Token(token), url).context("build forgejo api client") } diff --git a/hive-c0re/src/forge/pr_merge.rs b/hive-c0re/src/forge/pr_merge.rs index 74398dec..c82e83f6 100644 --- a/hive-c0re/src/forge/pr_merge.rs +++ b/hive-c0re/src/forge/pr_merge.rs @@ -9,7 +9,7 @@ use forgejo_api::structs::{MergePullRequestOption, MergePullRequestOptionDo}; use crate::coordinator::Coordinator; -use super::{CONFIG_ORG, api, core_token, forge_git_url}; +use super::{CONFIG_ORG, api, core_token}; // --------------------------------------------------------------------------- // PR-based config-flow merge primitives (part of the @@ -77,6 +77,13 @@ fn repo_agent_name(repo: &str) -> &str { repo.rsplit('/').next().unwrap_or(repo) } +/// Token-in-URL clone/push URL for a forge repo (`owner/name`). Mirrors +/// `push_config`'s pattern; the token is passed straight to git and never +/// stored as a named remote. +fn tokenised_repo_url(repo: &str, token: &str) -> String { + format!("http://core:{token}@localhost:3000/{repo}.git") +} + /// Resolve a PR's head sha via `git ls-remote refs/pull//head` /// (Forgejo exposes PR heads there). Pure read, no mutation — the handler's /// primary drift gate (compare against the approved sha), and `mark_pr_merged` @@ -87,7 +94,7 @@ fn repo_agent_name(repo: &str) -> &str { pub async fn pr_head_sha(repo: &str, pr: u64) -> Result { let token = core_token() .ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?; - let url = forge_git_url(&token, repo); + let url = tokenised_repo_url(repo, &token); let refspec = format!("refs/pull/{pr}/head"); let out = crate::lifecycle::git_command() .args(["ls-remote", &url, &refspec]) @@ -132,7 +139,7 @@ pub fn config_repo(agent: &str) -> String { pub async fn fetch_pr_head_into_applied(repo: &str, pr: u64) -> Result<(), ForgeMergeError> { let token = core_token() .ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?; - let url = forge_git_url(&token, repo); + let url = tokenised_repo_url(repo, &token); let applied = Coordinator::agent_applied_dir(repo_agent_name(repo)); let refspec = format!("refs/pull/{pr}/head"); let out = crate::lifecycle::git_command() @@ -164,7 +171,7 @@ pub async fn fetch_pr_head_into_applied(repo: &str, pr: u64) -> Result<(), Forge pub async fn ff_push_to_main(repo: &str, sha: &str) -> Result<(), ForgeMergeError> { let token = core_token() .ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?; - let url = forge_git_url(&token, repo); + let url = tokenised_repo_url(repo, &token); let applied = Coordinator::agent_applied_dir(repo_agent_name(repo)); // Current `main` on the forge repo. @@ -293,8 +300,7 @@ pub async fn mark_pr_merged(repo: &str, pr: u64, sha: &str) -> Result<(), ForgeM #[cfg(test)] mod tests { - use super::repo_agent_name; - use crate::forge::forge_git_url; + use super::{repo_agent_name, tokenised_repo_url}; #[test] fn repo_agent_name_takes_trailing_segment() { @@ -304,14 +310,10 @@ mod tests { } #[test] - fn forge_git_url_shape() { - // Credentials are inserted between scheme and authority; fallback - // base is `http://localhost:3000` when HIVE_FORGE_URL is unset. - let url = forge_git_url("tok", "agent-configs/iris"); - assert!(url.contains("core:tok@"), "must embed credentials: {url}"); - assert!( - url.ends_with("/agent-configs/iris.git"), - "must end with repo path: {url}" + fn tokenised_repo_url_shape() { + assert_eq!( + tokenised_repo_url("agent-configs/iris", "tok"), + "http://core:tok@localhost:3000/agent-configs/iris.git" ); } } diff --git a/hive-c0re/src/forge/repos.rs b/hive-c0re/src/forge/repos.rs index 2991715f..9eacedeb 100644 --- a/hive-c0re/src/forge/repos.rs +++ b/hive-c0re/src/forge/repos.rs @@ -19,8 +19,8 @@ use tokio::process::Command; use crate::coordinator::Coordinator; use super::{ - AGENTS_ORG, CONFIG_ORG, KNOWLEDGE_REPO, OPERATORS_TEAM, SHARED_DOCS_REPO, SHARED_ORG, api, - core_token, forge_git_url, forge_http_base, is_present, + AGENTS_ORG, CONFIG_ORG, FORGE_HTTP, KNOWLEDGE_REPO, OPERATORS_TEAM, SHARED_DOCS_REPO, + SHARED_ORG, api, core_token, is_present, }; /// Creation options for an empty repo defaulting to `main`. @@ -197,7 +197,7 @@ pub async fn push_meta(dir: &Path) -> Result<()> { // Token-in-URL push. Forgejo accepts `oauth2:` or just // any-username:; using `core` matches the owner so the // remote name is self-describing. - let url = forge_git_url(&token, "core/meta"); + let url = format!("http://core:{token}@localhost:3000/core/meta.git"); let out = Command::new("git") .current_dir(dir) .args(["push", "--force", &url, "HEAD:main"]) @@ -298,8 +298,8 @@ pub async fn meta_read_access(name: &str, core_token: &str) -> Result<()> { Ok(()) } -/// Add the forge `core/meta.git` URL as the `meta` remote in the -/// agent's proposed config repo so the agent (and the manager) can +/// Add `http://localhost:3000/core/meta.git` as the `meta` remote in +/// the agent's proposed config repo so the agent (and the manager) can /// fetch the meta flake from the forge. Idempotent: no-op when the /// remote already points at the right URL, or when the proposed repo /// does not exist yet. No-op when the forge is not running. @@ -311,7 +311,7 @@ pub async fn ensure_meta_remote(name: &str) -> Result<()> { if !proposed_dir.join(".git").exists() { return Ok(()); } - let want = format!("{}/core/meta.git", forge_http_base()); + let want = format!("{FORGE_HTTP}/core/meta.git"); let existing = crate::lifecycle::git_command() .current_dir(&proposed_dir) .args(["remote", "get-url", "meta"]) @@ -363,7 +363,7 @@ pub async fn push_config(name: &str) -> Result<()> { if !dir.join(".git").exists() { return Ok(()); } - let url = forge_git_url(&token, &format!("{CONFIG_ORG}/{name}")); + let url = format!("http://core:{token}@localhost:3000/{CONFIG_ORG}/{name}.git"); let out = crate::lifecycle::git_command() .current_dir(&dir) .args([ diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index faf94249..641b3729 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -393,7 +393,7 @@ async fn handle_create_repo(agent: &str, repo: &str) -> hive_sh4re::Response { }; match crate::forge::create_agent_repo(agent, repo, &core_token).await { Ok(full_name) => hive_sh4re::Response::RepoCreated { - clone_url: format!("{}/{full_name}.git", crate::forge::forge_http_base()), + clone_url: format!("{}/{full_name}.git", crate::forge::FORGE_HTTP), full_name, }, Err(e) => hive_sh4re::Response::Err { diff --git a/hive-c0re/src/workers/knowledge.rs b/hive-c0re/src/workers/knowledge.rs index c3647b42..8eaba76e 100644 --- a/hive-c0re/src/workers/knowledge.rs +++ b/hive-c0re/src/workers/knowledge.rs @@ -16,8 +16,6 @@ use std::collections::BTreeMap; use anyhow::{Context, Result}; use forgejo_api::structs::{CreateHookOption, CreateHookOptionConfig, CreateHookOptionType}; -use crate::forge::forge_git_url; - pub const ORG: &str = "internal"; pub const REPO: &str = "knowledge"; @@ -70,7 +68,8 @@ pub async fn ensure_local_clone(core_token: &str) -> Result<()> { return Ok(()); } std::fs::create_dir_all(LOCAL_DIR).context("create knowledge local dir")?; - let url = forge_git_url(core_token, &format!("{ORG}/{REPO}")); + // Embed credentials in the URL — safe for localhost-only forge. + let url = format!("http://core:{core_token}@localhost:3000/{ORG}/{REPO}.git"); let out = tokio::process::Command::new("git") .args(["clone", &url, LOCAL_DIR]) .output() @@ -124,7 +123,7 @@ async fn seed_readme(core_token: &str) -> Result<()> { anyhow::bail!("git {args:?} failed: {stderr}"); } } - let url = forge_git_url(core_token, &format!("{ORG}/{REPO}")); + let url = format!("http://core:{core_token}@localhost:3000/{ORG}/{REPO}.git"); let out = tokio::process::Command::new("git") .args(["-C", LOCAL_DIR, "push", &url, "HEAD:main"]) .output() diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 184c63a8..bea4c230 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -984,11 +984,9 @@ in ) // { # In-cluster forge URL — the gateway vhost (`forge.`), which - # nginx proxies to forgejo. Used both for internal API calls in - # hive-c0re (forge/mod.rs `forge_http_base()`) and forwarded to - # agents via meta.rs for their forge-notify client. The forge is - # mandatory, so this is unconditional (the whole env block is already - # gated on hyperhive being enabled). See `docs/gateway.md::HIVE_FORGE_URL`. + # nginx proxies to forgejo. The forge is mandatory, so this is + # unconditional (the whole env block is already gated on hyperhive + # being enabled). See `docs/gateway.md::HIVE_FORGE_URL`. HIVE_FORGE_URL = "http://${config.services.hyperhive.forge.domain}"; } // lib.optionalAttrs config.services.hyperhive.matrix.enable {