codetether-agent 4.5.7

A2A-native AI coding agent for the CodeTether ecosystem
Documentation
use crate::provenance::ExecutionProvenance;
use anyhow::{Context, Result};
use std::path::Path;

use super::identity::{git_author_email, git_author_name};
use super::{enrich_from_repo, sign_provenance};

pub fn commit_message_with_provenance(
    commit_msg: &str,
    provenance: Option<&ExecutionProvenance>,
) -> String {
    let Some(provenance) = provenance else {
        return commit_msg.to_string();
    };
    let trailers = build_trailers(commit_msg, provenance);
    if trailers.is_empty() {
        return commit_msg.to_string();
    }
    format!("{}\n\n{}", commit_msg.trim_end(), trailers.join("\n"))
}

pub async fn git_commit_with_provenance(
    repo_path: &Path,
    commit_msg: &str,
    provenance: Option<&ExecutionProvenance>,
) -> Result<std::process::Output> {
    let enriched = enrich_repo_provenance(provenance, repo_path);
    let message = commit_message_with_provenance(commit_msg, enriched.as_ref());
    let mut command = tokio::process::Command::new("git");
    command
        .args(["commit", "-m", &message])
        .current_dir(repo_path);
    apply_git_identity_env_tokio(&mut command, enriched.as_ref());
    command
        .output()
        .await
        .context("Failed to execute git commit")
}

pub fn git_commit_with_provenance_blocking(
    repo_path: &Path,
    commit_msg: &str,
    provenance: Option<&ExecutionProvenance>,
) -> Result<std::process::Output> {
    let enriched = enrich_repo_provenance(provenance, repo_path);
    let message = commit_message_with_provenance(commit_msg, enriched.as_ref());
    let mut command = std::process::Command::new("git");
    command
        .args(["commit", "-m", &message])
        .current_dir(repo_path);
    apply_git_identity_env_blocking(&mut command, enriched.as_ref());
    command.output().context("Failed to execute git commit")
}

fn enrich_repo_provenance(
    provenance: Option<&ExecutionProvenance>,
    repo_path: &Path,
) -> Option<ExecutionProvenance> {
    provenance.map(|value| enrich_from_repo(value, repo_path))
}

fn build_trailers(commit_msg: &str, provenance: &ExecutionProvenance) -> Vec<String> {
    trailer_pairs(provenance)
        .into_iter()
        .filter(|(label, _)| !commit_msg.contains(label))
        .map(|(label, value)| format!("{label}: {value}"))
        .collect()
}

fn trailer_pairs(provenance: &ExecutionProvenance) -> Vec<(&'static str, String)> {
    let mut trailers = vec![
        ("CodeTether-Provenance-ID", provenance.provenance_id.clone()),
        (
            "CodeTether-Origin",
            provenance.identity.origin.as_str().to_string(),
        ),
        (
            "CodeTether-Agent-Name",
            provenance.identity.agent_name.clone(),
        ),
    ];
    push_opt(
        &mut trailers,
        "CodeTether-Agent-Identity",
        provenance.identity.agent_identity_id.clone(),
    );
    push_opt(
        &mut trailers,
        "CodeTether-Tenant-ID",
        provenance.identity.tenant_id.clone(),
    );
    push_opt(
        &mut trailers,
        "CodeTether-Worker-ID",
        provenance.identity.worker_id.clone(),
    );
    push_opt(
        &mut trailers,
        "CodeTether-Session-ID",
        provenance.session_id.clone(),
    );
    push_opt(
        &mut trailers,
        "CodeTether-Task-ID",
        provenance.task_id.clone(),
    );
    push_opt(
        &mut trailers,
        "CodeTether-Run-ID",
        provenance.run_id.clone(),
    );
    push_opt(
        &mut trailers,
        "CodeTether-Attempt-ID",
        provenance.attempt_id.clone(),
    );
    push_opt(
        &mut trailers,
        "CodeTether-Key-ID",
        provenance.identity.key_id.clone(),
    );
    push_opt(
        &mut trailers,
        "CodeTether-GitHub-Installation-ID",
        provenance.identity.github_installation_id.clone(),
    );
    push_opt(
        &mut trailers,
        "CodeTether-GitHub-App-ID",
        provenance.identity.github_app_id.clone(),
    );
    push_opt(
        &mut trailers,
        "CodeTether-Signature",
        sign_provenance(provenance),
    );
    trailers
}

fn push_opt(
    trailers: &mut Vec<(&'static str, String)>,
    label: &'static str,
    value: Option<String>,
) {
    if let Some(value) = value {
        trailers.push((label, value));
    }
}

fn git_identity_env_vars(provenance: Option<&ExecutionProvenance>) -> Vec<(&'static str, String)> {
    let Some(provenance) = provenance else {
        return Vec::new();
    };
    let name = git_author_name(provenance.identity.agent_identity_id.as_deref());
    let email = git_author_email(
        provenance.identity.agent_identity_id.as_deref(),
        provenance.identity.worker_id.as_deref(),
        Some(provenance.provenance_id.as_str()),
    );
    vec![
        ("GIT_AUTHOR_NAME", name.clone()),
        ("GIT_COMMITTER_NAME", name),
        ("GIT_AUTHOR_EMAIL", email.clone()),
        ("GIT_COMMITTER_EMAIL", email),
    ]
}

fn apply_git_identity_env_tokio(
    command: &mut tokio::process::Command,
    provenance: Option<&ExecutionProvenance>,
) {
    for (key, value) in git_identity_env_vars(provenance) {
        command.env(key, value);
    }
}

fn apply_git_identity_env_blocking(
    command: &mut std::process::Command,
    provenance: Option<&ExecutionProvenance>,
) {
    for (key, value) in git_identity_env_vars(provenance) {
        command.env(key, value);
    }
}

#[cfg(test)]
mod tests {
    use super::{commit_message_with_provenance, git_identity_env_vars};
    use crate::provenance::{ExecutionOrigin, ExecutionProvenance};

    #[test]
    fn appends_codetether_trailers() {
        let mut provenance = ExecutionProvenance::for_operation("ralph", ExecutionOrigin::Ralph);
        provenance.session_id = Some("session-1".to_string());
        provenance.apply_worker_task("worker-1", "task-1");
        provenance.set_run_id("run-1");
        provenance.attempt_id = Some("run-1:attempt:2".to_string());
        let message = commit_message_with_provenance("feat: test", Some(&provenance));
        assert!(message.contains("CodeTether-Provenance-ID:"));
        assert!(message.contains("CodeTether-Session-ID: session-1"));
        assert!(message.contains("CodeTether-Task-ID: task-1"));
        assert!(message.contains("CodeTether-Run-ID: run-1"));
        assert!(message.contains("CodeTether-Attempt-ID: run-1:attempt:2"));
    }

    #[test]
    fn derives_real_git_identity_env() {
        let mut provenance = ExecutionProvenance::for_operation("worker", ExecutionOrigin::Worker);
        provenance.identity.agent_identity_id = Some("user:abc-123".to_string());
        provenance.identity.worker_id = Some("wrk-1".to_string());
        let env = git_identity_env_vars(Some(&provenance));
        assert!(
            env.iter()
                .any(|(k, v)| *k == "GIT_AUTHOR_NAME" && v.contains("user-abc-123"))
        );
        assert!(
            env.iter()
                .any(|(k, v)| *k == "GIT_AUTHOR_EMAIL" && v == "agent+user-abc-123@codetether.run")
        );
    }
}