g1t/crates/runner/src/reply.rs
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 1 | //! Answers a question someone asked `@g1t` in a comment, in the |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 2 | //! thread it was asked in, changing nothing. |
| 3 | //! | |
| 4 | //! The agent reads the repository as it is (the default branch for an | |
| 5 | //! issue, the pull request's head for a pull request) and writes its answer | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 6 | //! to a file. This program posts the answer as `g1t`, with the |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 7 | //! agent's own token, which the agent itself never holds. |
| 8 | //! | |
| 9 | //! Configuration comes from the environment: | |
| 10 | //! | |
| 11 | //! - `G1T_API`, `G1T_REPO`, `REPLY_NUMBER`: where the question was asked. | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 12 | //! - `G1T_AGENT_TOKEN`: g1t's token for this run, to post the answer. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 13 | //! - `GIT_REMOTE`, `GIT_REF`: what to read, and at which branch or commit. |
| 14 | //! - `G1T_USER`, `G1T_TOKEN`: to read it, if it is private. | |
| 15 | //! - `PROMPT`: the question and what the agent is told around it. | |
| 16 | ||
| 17 | use std::path::Path; | |
| 18 | ||
| 19 | use anyhow::{Context, Result, bail}; | |
| 20 | ||
| 21 | use crate::report::Reporter; | |
| 22 | use crate::{WORKDIR, auth_option, env, git, harness}; | |
| 23 | ||
| Reviews, plans and replies are written where the guardrail allows: g1t's answer files are inside it | 24 | pub(crate) const ANSWER_FILE: &str = "/work/answer.md"; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 25 | const MAX_ANSWER_CHARS: usize = 20_000; |
| 26 | ||
| 27 | const INSTRUCTIONS: &str = "Write your answer to /work/answer.md as Markdown, addressed to whoever asked: \ | |
| 28 | plain sentences, specific, naming files and functions where that helps, no headings and no emoji. \ | |
| 29 | Read the code before you answer; say so when you are not sure. Do not change any file in the repository, \ | |
| 30 | and do not post the answer with add_comment: it is posted in the thread for you. Then finish."; | |
| 31 | ||
| 32 | fn answer() -> Result<String> { | |
| 33 | let remote = env("GIT_REMOTE")?; | |
| 34 | let auth = auth_option(&env("G1T_USER")?, &env("G1T_TOKEN")?); | |
| 35 | let workdir = Path::new(WORKDIR); | |
| 36 | std::fs::create_dir_all("/work")?; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 37 | crate::clone::clone(Path::new("/work"), &auth, &[], &remote, WORKDIR).context("could not clone the repository")?; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 38 | if let Ok(reference) = env("GIT_REF") |
| 39 | && !reference.is_empty() | |
| 40 | { | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 41 | // A commit the clone may not have fetched by name: fetch it, and |
| 42 | // everything if that is refused. | |
| 43 | if crate::clone::fetch(workdir, &auth, "origin", &reference).is_err() && crate::clone::is_shallow(workdir) { | |
| 44 | let _ = git(workdir, &["-c", &auth, "fetch", "--quiet", "--unshallow", "origin"]); | |
| 45 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 46 | git(workdir, &["checkout", "--quiet", "--detach", &reference]) |
| 47 | .or_else(|_| git(workdir, &["checkout", "--quiet", "--detach", "FETCH_HEAD"])) | |
| 48 | .context("could not check out what the question is about")?; | |
| 49 | } | |
| 50 | let prompt = format!("{}\n\n{INSTRUCTIONS}", env("PROMPT")?); | |
| 51 | // Steps show on the agent run; the answer is what matters here. | |
| 52 | let mut reporter = Reporter::silent(); | |
| 53 | let summary = harness::run_claude(workdir, &prompt, &mut reporter)?; | |
| 54 | let written = std::fs::read_to_string(ANSWER_FILE).unwrap_or_default(); | |
| 55 | let answer = if written.trim().is_empty() { summary } else { written }; | |
| 56 | let answer: String = answer.trim().chars().take(MAX_ANSWER_CHARS).collect(); | |
| 57 | if answer.is_empty() { | |
| 58 | bail!("the agent wrote no answer"); | |
| 59 | } | |
| 60 | Ok(answer) | |
| 61 | } | |
| 62 | ||
| 63 | pub fn main() -> i32 { | |
| 64 | let (Ok(api), Ok(repo), Ok(number), Ok(token)) = ( | |
| 65 | env("G1T_API"), | |
| 66 | env("G1T_REPO"), | |
| 67 | env("REPLY_NUMBER"), | |
| 68 | env("G1T_AGENT_TOKEN"), | |
| 69 | ) else { | |
| 70 | eprintln!("g1t-runner: G1T_API, G1T_REPO, REPLY_NUMBER and G1T_AGENT_TOKEN must be set"); | |
| 71 | return 2; | |
| 72 | }; | |
| 73 | let secrets: Vec<String> = ["G1T_TOKEN", "G1T_AGENT_TOKEN", "ANTHROPIC_API_KEY", "BILLING_TOKEN", "AGENT_RUN_TOKEN"] | |
| 74 | .iter() | |
| 75 | .filter_map(|name| std::env::var(name).ok()) | |
| 76 | .filter(|secret| !secret.is_empty()) | |
| 77 | .collect(); | |
| 78 | let outcome = answer(); | |
| 79 | let body = match &outcome { | |
| 80 | Ok(answer) => answer.clone(), | |
| 81 | Err(error) => { | |
| 82 | eprintln!("g1t-runner: {error:#}"); | |
| 83 | "I could not answer this: the run failed before I had an answer. Its steps are on the Agents page.".to_owned() | |
| 84 | } | |
| 85 | }; | |
| 86 | // Whatever the agent wrote passes through here, so nothing it could | |
| 87 | // have read from its environment leaves in the answer. | |
| 88 | let body = secrets | |
| 89 | .iter() | |
| 90 | .fold(body, |text, secret| text.replace(secret, "[redacted]")); | |
| 91 | let posted = ureq::post(&format!("{api}/repos/{repo}/issues/{number}/comments")) | |
| 92 | .set("Authorization", &format!("Bearer {token}")) | |
| 93 | .send_json(serde_json::json!({ "body": body })); | |
| 94 | if let Err(error) = posted { | |
| 95 | eprintln!("g1t-runner: could not post the answer: {error:#}"); | |
| 96 | return 1; | |
| 97 | } | |
| 98 | i32::from(outcome.is_err()) | |
| 99 | } |