diff --git a/src/client.ts b/src/client.ts index 7a5000f..a05c4dd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -500,6 +500,16 @@ export interface JobDetails { branch_name?: string; commit_login: string; commit_email: string; + /** + * Login for the `Co-authored-by` trailer on agent commits. Distinct from the + * commit author (`commit_login`): by default the agent authors the commit and + * the user is credited as co-author, but under act-as-user the roles swap so + * the user authors and the agent is the co-author. Optional for backward + * compatibility with platforms that do not populate it. + */ + commit_coauthor_login?: string; + /** Email for the `Co-authored-by` trailer. See {@link commit_coauthor_login}. */ + commit_coauthor_email?: string; mcp_proxy_url?: string; /** Model selected by the platform for this run. Present when model selection is enabled. */ selected_model?: string; diff --git a/src/git.ts b/src/git.ts index 30d768c..0c3f709 100644 --- a/src/git.ts +++ b/src/git.ts @@ -16,6 +16,13 @@ import { existsSync } from "fs"; const DEFAULT_CLONE_DIR = "/tmp/workspace"; +// Environment variables carrying the platform-served commit co-author, set by +// the ccav3 app from job details. Used to append a `Co-authored-by` trailer to +// commits this SDK creates (the finalize safety-net commit), mirroring the +// trailer the runtime emits for report_progress commits. +const COAUTHOR_LOGIN_ENV = "GITHUB_COPILOT_COMMIT_COAUTHOR_LOGIN"; +const COAUTHOR_EMAIL_ENV = "GITHUB_COPILOT_COMMIT_COAUTHOR_EMAIL"; + // ============================================================================= // Helpers // ============================================================================= @@ -33,6 +40,29 @@ function branchExistsOnRemote(repoLocation: string, branchName: string): boolean return output.length > 0; } +/** + * Appends a `Co-authored-by` trailer built from the platform-served commit + * co-author (exposed via GITHUB_COPILOT_COMMIT_COAUTHOR_LOGIN/GITHUB_COPILOT_COMMIT_COAUTHOR_EMAIL) + * to the commit message. The served email is used verbatim so it matches the + * trailer the runtime emits and the server-side signing check. Returns the + * message unchanged when the variables are unset or the trailer is already + * present. + */ +export function withCoAuthorTrailer(commitMessage: string): string { + const login = process.env[COAUTHOR_LOGIN_ENV]; + const email = process.env[COAUTHOR_EMAIL_ENV]; + if (!login || !email) { + return commitMessage; + } + const trailer = `Co-authored-by: ${login} <${email}>`; + const normalizedMessage = commitMessage.trimEnd(); + const lastParagraph = normalizedMessage.split(/\r?\n(?:\r?\n)+/).pop() ?? ""; + if (lastParagraph.split(/\r?\n/).includes(trailer)) { + return commitMessage; + } + return `${normalizedMessage}\n\n${trailer}`; +} + // ============================================================================= // Types // ============================================================================= @@ -266,7 +296,7 @@ export function commitAndPush(repoLocation: string, commitMessage: string): Comm if (status) { hadChanges = true; git(["add", "."], repoLocation); - git(["commit", "-m", commitMessage], repoLocation); + git(["commit", "-m", withCoAuthorTrailer(commitMessage)], repoLocation); } pushWithRebaseFallback(repoLocation); diff --git a/src/mcp-server.ts b/src/mcp-server.ts index c5f7ec1..d2967c9 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -25,7 +25,7 @@ import { appendFileSync } from "fs"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; -import { commitAndPush } from "./git.js"; +import { commitAndPush, withCoAuthorTrailer } from "./git.js"; import type { PlatformClient } from "./client.js"; // ============================================================================= @@ -135,7 +135,7 @@ async function executeReportProgress( if (status) { execFileSync("git", ["add", "."], { cwd: config.workingDir }); - execFileSync("git", ["commit", "-m", commitMessage], { cwd: config.workingDir }); + execFileSync("git", ["commit", "-m", withCoAuthorTrailer(commitMessage)], { cwd: config.workingDir }); log("git commit complete (local only)", { message: commitMessage }); results.push(`Committed locally: ${commitMessage}`); } else {