missions: fix repo checkout for retries + tokenize git.redclaw.dev clones
- mission_orchestrator: run ensure_checkout BEFORE the team_id short-circuit. Previously, a re-launched or retried mission bailed out at the team_id=already-bound guard and skipped repo checkout entirely, so agents ran against an empty workspace. - mission_workspace: inject GITEA_TOKEN into git.redclaw.dev URLs so clone auth works from the server container. Redact any token echoed back on failure. - refresh buttons on MissionCanvas + MissionsList now spin the icon while loading so clicks are visibly acknowledged. - refresh-spinner keyframe added to motion.css. Requires operator on gw-04: sudo chown 65532:65532 /var/lib/clawmates-missions (applied 2026-07-21 pre-commit).
This commit is contained in:
@@ -13,10 +13,11 @@
|
||||
//! to bring it in sync
|
||||
//! - dir missing → `git clone --depth 1 <url> <path>`
|
||||
//!
|
||||
//! Auth: relies on the ambient git credential setup (SSH agent,
|
||||
//! .netrc, or git-credential helper) in the process environment. We
|
||||
//! deliberately don't embed tokens in URLs — footgun risk outweighs
|
||||
//! the ergonomics, and prod already runs with a helper configured.
|
||||
//! Auth: for `git.redclaw.dev` clones we inject the ambient
|
||||
//! `GITEA_TOKEN` (already provisioned in the server container's env)
|
||||
//! into the clone URL as basic-auth. For any other host we fall back
|
||||
//! to the ambient credential setup (SSH agent, .netrc, git helper) —
|
||||
//! prod hosts run with those configured. Tokens are never logged.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use tokio::process::Command;
|
||||
@@ -64,14 +65,32 @@ pub async fn ensure_checkout(
|
||||
.map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
|
||||
}
|
||||
|
||||
let auth_url = with_ambient_auth(clone_url);
|
||||
if path.join(".git").exists() {
|
||||
fetch_and_reset(&path, default_branch).await?;
|
||||
} else {
|
||||
clone(&path, clone_url).await?;
|
||||
clone(&path, &auth_url).await?;
|
||||
}
|
||||
Ok(Some(path))
|
||||
}
|
||||
|
||||
/// If the URL points at git.redclaw.dev AND GITEA_TOKEN is set in the
|
||||
/// environment, rewrite it to include the token as basic-auth. Returns
|
||||
/// the URL unchanged otherwise. The token is never logged (we only
|
||||
/// pass the rewritten URL into `git clone` via argv).
|
||||
fn with_ambient_auth(url: &str) -> String {
|
||||
let Ok(token) = std::env::var("GITEA_TOKEN") else {
|
||||
return url.to_string();
|
||||
};
|
||||
if token.is_empty() {
|
||||
return url.to_string();
|
||||
}
|
||||
if let Some(rest) = url.strip_prefix("https://git.redclaw.dev/") {
|
||||
return format!("https://oauth2:{token}@git.redclaw.dev/{rest}");
|
||||
}
|
||||
url.to_string()
|
||||
}
|
||||
|
||||
async fn clone(path: &std::path::Path, url: &str) -> Result<(), String> {
|
||||
let out = Command::new("git")
|
||||
.args(["clone", "--depth", "1", url, &path.display().to_string()])
|
||||
@@ -80,9 +99,9 @@ async fn clone(path: &std::path::Path, url: &str) -> Result<(), String> {
|
||||
.map_err(|e| format!("spawn git clone: {e}"))?;
|
||||
if !out.status.success() {
|
||||
return Err(format!(
|
||||
"git clone {url} → exit {}: {}",
|
||||
"git clone → exit {}: {}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
redact_token(&String::from_utf8_lossy(&out.stderr))
|
||||
.chars()
|
||||
.take(400)
|
||||
.collect::<String>()
|
||||
@@ -91,6 +110,23 @@ async fn clone(path: &std::path::Path, url: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn redact_token(s: &str) -> String {
|
||||
// Strip any "oauth2:<token>@" segment that git may echo back on
|
||||
// failures. Belt-and-braces: also nuke any raw token env value.
|
||||
let mut out = s.to_string();
|
||||
if let Some(pos) = out.find("oauth2:") {
|
||||
if let Some(at) = out[pos..].find('@') {
|
||||
out.replace_range(pos..pos + at, "oauth2:***");
|
||||
}
|
||||
}
|
||||
if let Ok(t) = std::env::var("GITEA_TOKEN") {
|
||||
if !t.is_empty() {
|
||||
out = out.replace(&t, "***");
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
async fn fetch_and_reset(path: &std::path::Path, branch: &str) -> Result<(), String> {
|
||||
let fetch = Command::new("git")
|
||||
.args([
|
||||
|
||||
Reference in New Issue
Block a user