Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
889 linesCodeBlameRaw

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.

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1//! Agents at work and what they remember. Kept by the work service.
2//!
3//! Every sandbox g1t starts for an agent is an [`AgentRun`]: what it is
4//! doing, on which pull request or issue, step by step, what it cost, and
5//! how it ended. People watch runs live, stop them and message them.
6//!
7//! Memory is what agents and people have learned that the next agent
8//! should know: about one project (its codebase), or about the whole
9//! workspace (true across its projects). It is members-only, is given to
10//! every g1t agent run, and never holds a secret.
11//!
12//! Each `*Args` struct is the argument of the method of the same name,
13//! served at `POST /rpc/<method>`.
14
15use serde::{Deserialize, Serialize};
16
17use crate::repos::RepoPath;
18use crate::work::{Pull, PullStatus, SessionEntry};
19use crate::{User, Viewer};
20
21/// The work an agent run does.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum RunKind {
25 /// Making the change an issue asks for.
26 Implement,
27 /// Addressing failed checks or a review on its own pull request.
28 Revise,
29 /// Reviewing someone's pull request.
30 Review,
31 /// Answering another agent's question or handoff.
32 Answer,
33 /// Merging in the branch a pull request will land on.
34 Update,
35 /// Turning an outcome into a plan of issues.
36 Plan,
37 /// Running an issue's acceptance checks. Not an agent: a sandbox.
38 Checks,
39 /// Building and checking a state of the merge queue. Not an agent.
40 Queue,
41 /// Finding out whether a pull request merges cleanly. Not an agent.
42 Mergecheck,
43}
44
45impl RunKind {
46 pub const ALL: [RunKind; 9] = [
47 RunKind::Implement,
48 RunKind::Revise,
49 RunKind::Review,
50 RunKind::Answer,
51 RunKind::Update,
52 RunKind::Plan,
53 RunKind::Checks,
54 RunKind::Queue,
55 RunKind::Mergecheck,
56 ];
57
58 pub fn as_str(self) -> &'static str {
59 match self {
60 RunKind::Implement => "implement",
61 RunKind::Revise => "revise",
62 RunKind::Review => "review",
63 RunKind::Answer => "answer",
64 RunKind::Update => "update",
65 RunKind::Plan => "plan",
66 RunKind::Checks => "checks",
67 RunKind::Queue => "queue",
68 RunKind::Mergecheck => "mergecheck",
69 }
70 }
71
72 pub fn parse(value: &str) -> Option<RunKind> {
73 RunKind::ALL.into_iter().find(|kind| kind.as_str() == value)
74 }
75
76 /// Whether a model does the work, rather than commands g1t runs.
77 pub fn is_agent(self) -> bool {
78 !matches!(self, RunKind::Checks | RunKind::Queue | RunKind::Mergecheck)
79 }
80
81 /// Whether a message reaches the agent while it runs: the harness asks
82 /// for messages after each tool call in these. Others read nothing new
83 /// until the next run on the same pull request.
84 pub fn takes_messages(self) -> bool {
85 matches!(self, RunKind::Implement | RunKind::Revise | RunKind::Answer)
86 }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum RunStatus {
92 /// Its sandbox is starting.
93 Queued,
94 Running,
95 Succeeded,
96 Failed,
97 /// A person stopped it.
98 Stopped,
99}
100
101impl RunStatus {
102 pub fn as_str(self) -> &'static str {
103 match self {
104 RunStatus::Queued => "queued",
105 RunStatus::Running => "running",
106 RunStatus::Succeeded => "succeeded",
107 RunStatus::Failed => "failed",
108 RunStatus::Stopped => "stopped",
109 }
110 }
111
112 pub fn parse(value: &str) -> Option<RunStatus> {
113 [
114 RunStatus::Queued,
115 RunStatus::Running,
116 RunStatus::Succeeded,
117 RunStatus::Failed,
118 RunStatus::Stopped,
119 ]
120 .into_iter()
121 .find(|status| status.as_str() == value)
122 }
123
124 pub fn is_active(self) -> bool {
125 matches!(self, RunStatus::Queued | RunStatus::Running)
126 }
127}
128
129/// One step of a run, as one short line.
130#[derive(Clone, Debug, Serialize, Deserialize)]
131pub struct RunStep {
132 /// RFC 3339.
133 pub at: String,
134 pub text: String,
135}
136
137/// One sandbox g1t started: an agent at work, or a run of checks.
138#[derive(Clone, Debug, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct AgentRun {
141 pub id: String,
142 pub repo: RepoPath,
143 /// The pull request it works on; for a plan, none.
144 pub number: Option<u32>,
145 /// That pull request's title, or the plan's brief.
146 pub title: Option<String>,
147 pub kind: RunKind,
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent148 /// Who is working: `g1t` for g1t's own agent and for a sandbox that
149 /// runs commands.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily150 pub agent: String,
151 /// The model, by its public name. Members only.
152 pub model: Option<String>,
153 pub status: RunStatus,
154 /// What it is doing now, in one line.
155 pub step: Option<String>,
156 /// Every step so far, oldest first. Empty in lists.
157 #[serde(default)]
158 pub steps: Vec<RunStep>,
159 /// How many steps there are.
160 #[serde(default)]
161 pub step_count: u32,
162 /// Who put it to work, when a person did.
163 pub started_by: Option<String>,
164 /// Why it failed.
165 pub error: Option<String>,
166 /// What it cost, in US dollars, as the harness reported it. Members only.
167 pub cost_usd: Option<f64>,
168 pub turns: Option<u32>,
169 /// The most it may cost, in US dollars, from its guardrails. None: no
170 /// cap. Members only.
171 #[serde(default)]
172 pub budget_usd: Option<f64>,
173 /// The longest it may take, in minutes, from its guardrails.
174 #[serde(default)]
175 pub time_cap_minutes: Option<u32>,
176 /// `budget` or `time` when g1t stopped it for reaching a cap.
177 #[serde(default)]
178 pub halted: Option<String>,
179 /// How sure g1t was of the change as this run left it, for a run that
180 /// made or revised one.
181 #[serde(default)]
182 pub confidence: Option<crate::work::Confidence>,
183 /// RFC 3339.
184 pub created_at: String,
185 pub started_at: Option<String>,
186 pub finished_at: Option<String>,
187 pub updated_at: String,
188}
189
190/// What lets a sandbox, and nothing else, report on its run.
191#[derive(Clone, Debug, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct AgentRunTicket {
194 pub run_id: String,
195 pub token: String,
196}
197
198/// `open_run`: records a sandbox the runner is starting. Called by the
199/// runner service only. Returns `Outcome<AgentRunTicket>`.
200#[derive(Debug, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct OpenRunArgs {
203 /// Who the sandbox acts as.
204 pub actor: User,
205 pub repo: RepoPath,
206 #[serde(default)]
207 pub number: Option<u32>,
208 #[serde(default)]
209 pub pull_id: Option<String>,
210 /// For a run with no pull request, such as a plan: what it is about.
211 #[serde(default)]
212 pub title: Option<String>,
213 pub kind: RunKind,
214 #[serde(default)]
215 pub agent: Option<String>,
216 #[serde(default)]
217 pub model: Option<String>,
218 /// The sandbox's name, which stopping it needs.
219 pub sandbox: String,
220 #[serde(default)]
221 pub started_by: Option<String>,
222 /// Its cost cap, from its guardrails, in US dollars.
223 #[serde(default)]
224 pub budget_usd: Option<f64>,
225 /// Its time cap, from its guardrails, in minutes.
226 #[serde(default)]
227 pub time_cap_minutes: Option<u32>,
228}
229
230/// `report_run`: a sandbox telling how its run goes, with the run's token.
231/// Steps are added, the current step replaced, and an outcome ends the run.
232/// A finished run accepts nothing more. Returns `Outcome<RunStatus>`: the
233/// run's status after the report, so a sandbox can tell it was stopped.
234#[derive(Debug, Default, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase", default)]
236pub struct ReportRunArgs {
237 pub run_id: String,
238 pub token: String,
239 pub steps: Vec<String>,
240 pub step: Option<String>,
241 pub cost_usd: Option<f64>,
242 pub turns: Option<u32>,
243 /// `succeeded` or `failed`, to end the run.
244 pub outcome: Option<RunStatus>,
245 pub error: Option<String>,
246 /// `budget` or `time`: the run reached a cap of its guardrails and
247 /// stopped. Ends it as stopped, like a person's stop.
248 pub halt: Option<crate::guardrails::Halt>,
249}
250
251/// `stop_run`: a member stops a run. Marks it stopped and returns where its
252/// sandbox is, for the runner to destroy. A pull request it worked on waits
253/// for a person. Returns `Outcome<StoppedRun>`.
254#[derive(Debug, Serialize, Deserialize)]
255pub struct StopRunArgs {
256 pub actor: User,
257 pub repo: RepoPath,
258 pub id: String,
259}
260
261#[derive(Debug, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub struct StoppedRun {
264 pub run: AgentRun,
265 pub sandbox: String,
266}
267
268/// `list_runs`: runs in a repository, or with `workspace` instead, in every
269/// repository of a workspace (members only), newest first. Returns
270/// `Outcome<Vec<AgentRun>>`.
271#[derive(Debug, Default, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase", default)]
273pub struct ListRunsArgs {
274 pub viewer: Viewer,
275 pub repo: Option<RepoPath>,
276 pub workspace: Option<String>,
277 /// Only those queued or running.
278 pub active: bool,
279 pub kind: Option<RunKind>,
280 pub status: Option<RunStatus>,
281 /// Only those on this pull request.
282 pub number: Option<u32>,
283 /// At most 200; 50 if not given.
284 pub limit: Option<u32>,
285}
286
287/// `get_run`: one run with all its steps. Returns `Outcome<AgentRun>`.
288#[derive(Debug, Serialize, Deserialize)]
289pub struct GetRunArgs {
290 pub viewer: Viewer,
291 pub repo: RepoPath,
292 pub id: String,
293}
294
295/// A pull request's recorded session, summed up for a list.
296#[derive(Clone, Debug, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct SessionSummary {
299 pub number: u32,
300 pub title: String,
301 pub status: PullStatus,
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent302 /// The agent label on the pull request, such as `g1t`.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily303 pub agent: String,
304 pub entries: u32,
305 /// How many tools it called.
306 pub tools: u32,
307 /// The start of the first prompt.
308 pub prompt: Option<String>,
309 /// The kinds of the runs g1t made for it.
310 pub kinds: Vec<RunKind>,
311 pub runs: u32,
312 /// What its runs cost together. Members only.
313 pub cost_usd: Option<f64>,
314 /// Whether one of its runs is under way.
315 pub active: bool,
316 /// RFC 3339.
317 pub started_at: String,
318 pub last_at: String,
319}
320
321/// `list_sessions`: the pull requests of a repository that have a session,
322/// most recently active first. Returns `Outcome<Vec<SessionSummary>>`.
323#[derive(Debug, Default, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase", default)]
325pub struct ListSessionsArgs {
326 pub viewer: Viewer,
327 pub repo: Option<RepoPath>,
328 /// Only those with a run of this kind.
329 pub kind: Option<RunKind>,
330 /// The pull request's status: `draft`, `open`, `merged` or `closed`.
331 pub outcome: Option<PullStatus>,
332 pub number: Option<u32>,
333}
334
335/// `get_session`: one pull request's session in full, with its runs.
336/// Returns `Outcome<SessionView>`.
337#[derive(Debug, Serialize, Deserialize)]
338pub struct GetSessionArgs {
339 pub viewer: Viewer,
340 pub repo: RepoPath,
341 pub number: u32,
342}
343
344#[derive(Clone, Debug, Serialize, Deserialize)]
345#[serde(rename_all = "camelCase")]
346pub struct SessionView {
347 pub pull: Pull,
348 /// Oldest first; the first 2000.
349 pub entries: Vec<SessionEntry>,
350 /// Newest first, with their steps.
351 pub runs: Vec<AgentRun>,
352 /// Members only.
353 pub cost_usd: Option<f64>,
354}
355
356// --- Memory -----------------------------------------------------------------
357
358/// Whose memory: one project's, or the whole workspace's.
359#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
360#[serde(rename_all = "snake_case")]
361pub enum MemoryScope {
362 /// About one project's codebase.
363 Project,
364 /// True across the workspace's projects.
365 Workspace,
366}
367
368impl MemoryScope {
369 pub fn as_str(self) -> &'static str {
370 match self {
371 MemoryScope::Project => "project",
372 MemoryScope::Workspace => "workspace",
373 }
374 }
375}
376
377#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
378#[serde(rename_all = "snake_case")]
379pub enum MemoryKind {
380 /// How something is: "staging lives at staging.example.com".
381 #[default]
382 Fact,
383 /// How things are done here: "we use pnpm everywhere".
384 Convention,
385 /// Something chosen, and why: "we keep Postgres, not MySQL, for jsonb".
386 Decision,
387 /// A trap: "the tests need TZ=UTC or the date tests fail".
388 Gotcha,
389}
390
391impl MemoryKind {
392 pub fn as_str(self) -> &'static str {
393 match self {
394 MemoryKind::Fact => "fact",
395 MemoryKind::Convention => "convention",
396 MemoryKind::Decision => "decision",
397 MemoryKind::Gotcha => "gotcha",
398 }
399 }
400
401 pub fn parse(value: &str) -> Option<MemoryKind> {
402 [
403 MemoryKind::Fact,
404 MemoryKind::Convention,
405 MemoryKind::Decision,
406 MemoryKind::Gotcha,
407 ]
408 .into_iter()
409 .find(|kind| kind.as_str() == value)
410 }
411}
412
413/// Where a memory came from.
414#[derive(Clone, Debug, Default, Serialize, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct MemorySource {
417 /// `person`, `agent` or `run`.
418 pub kind: String,
419 /// The agent run that learned it.
420 #[serde(default)]
421 pub run_id: Option<String>,
422 /// The project and pull request it was learned on.
423 #[serde(default)]
424 pub repo: Option<RepoPath>,
425 #[serde(default)]
426 pub number: Option<u32>,
427 /// What it was captured from: `run:<id>`, `comment:<id>`,
428 /// `pull:<repo id>#<n>`, `doc:<repo id>:<path>`. See `crate::capture`.
429 #[serde(default)]
430 pub reference: Option<String>,
431 /// What it was learned from, quoted.
432 #[serde(default)]
433 pub evidence: Option<String>,
434}
435
436/// Whether agents are given a memory: `kept` ones are; a `candidate` waits
437/// for review (see `crate::capture`); a `dismissed` one is never suggested
438/// again from the same wording.
439#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
440#[serde(rename_all = "snake_case")]
441pub enum MemoryStatus {
442 Candidate,
443 #[default]
444 Kept,
445 Dismissed,
446}
447
448impl MemoryStatus {
449 pub fn as_str(self) -> &'static str {
450 match self {
451 MemoryStatus::Candidate => "candidate",
452 MemoryStatus::Kept => "kept",
453 MemoryStatus::Dismissed => "dismissed",
454 }
455 }
456
457 pub fn parse(value: &str) -> Option<MemoryStatus> {
458 [MemoryStatus::Candidate, MemoryStatus::Kept, MemoryStatus::Dismissed]
459 .into_iter()
460 .find(|status| status.as_str() == value)
461 }
462}
463
464#[derive(Clone, Debug, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct Memory {
467 pub id: String,
468 pub scope: MemoryScope,
469 /// The workspace's slug.
470 pub workspace: String,
471 /// For a project's memory, the project's repository.
472 pub repo: Option<RepoPath>,
473 pub text: String,
474 pub kind: MemoryKind,
475 pub source: MemorySource,
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent476 /// Who wrote it: a username, or `g1t`.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily477 pub created_by: String,
478 /// Pinned memories are given to every agent first.
479 pub pinned: bool,
480 /// RFC 3339.
481 pub created_at: String,
482 pub updated_at: String,
483 /// When it was last given to an agent.
484 pub last_used_at: Option<String>,
485 /// Kept memories are given to agents; candidates wait for review.
486 #[serde(default)]
487 pub status: MemoryStatus,
488 /// 0 to 1: how sure its source was. None for what people wrote.
489 #[serde(default)]
490 pub confidence: Option<f64>,
491 /// How many independent sources said it.
492 #[serde(default)]
493 pub seen: u32,
494}
495
496#[derive(Clone, Debug, Default, Serialize, Deserialize)]
497#[serde(rename_all = "camelCase")]
498pub struct Memories {
499 /// The project's own; empty when no project was named.
500 pub project: Vec<Memory>,
501 pub workspace: Vec<Memory>,
502}
503
504/// `list_memories`: a workspace's memory and, with `repo`, that project's.
505/// Members only. Returns `Outcome<Memories>`.
506#[derive(Debug, Serialize, Deserialize)]
507pub struct ListMemoriesArgs {
508 pub viewer: Viewer,
509 pub workspace: String,
510 #[serde(default)]
511 pub repo: Option<RepoPath>,
512}
513
514/// `add_memory`: a member or an agent adds to memory. A project's memory
515/// needs `repo`. Text that looks like a key or a token is refused.
516/// Returns `Outcome<Memory>`.
517#[derive(Debug, Serialize, Deserialize)]
518#[serde(rename_all = "camelCase")]
519pub struct AddMemoryArgs {
520 pub actor: User,
521 pub workspace: String,
522 #[serde(default)]
523 pub repo: Option<RepoPath>,
524 pub scope: MemoryScope,
525 pub text: String,
526 #[serde(default)]
527 pub kind: MemoryKind,
528 #[serde(default)]
529 pub pinned: bool,
530 /// For an agent: the pull request it is working on.
531 #[serde(default)]
532 pub from_number: Option<u32>,
533}
534
535/// `update_memory`: changes a memory's text, kind or pin. Members only.
536/// Returns `Outcome<Memory>`.
537#[derive(Debug, Serialize, Deserialize)]
538pub struct UpdateMemoryArgs {
539 pub actor: User,
540 pub workspace: String,
541 pub id: String,
542 #[serde(default)]
543 pub text: Option<String>,
544 #[serde(default)]
545 pub kind: Option<MemoryKind>,
546 #[serde(default)]
547 pub pinned: Option<bool>,
548}
549
550/// `delete_memory`: forgets a memory. Members only. Returns `Outcome<bool>`.
551#[derive(Debug, Serialize, Deserialize)]
552pub struct DeleteMemoryArgs {
553 pub actor: User,
554 pub workspace: String,
555 pub id: String,
556}
557
558/// `recall`: what a project and its workspace remember, matching `query`
559/// when given (every word, in any order), pinned first. Members and g1t's
560/// agents. Returns `Outcome<Memories>`.
561#[derive(Debug, Serialize, Deserialize)]
562pub struct RecallArgs {
563 pub viewer: Viewer,
564 pub repo: RepoPath,
565 #[serde(default)]
566 pub query: Option<String>,
567 #[serde(default)]
568 pub limit: Option<u32>,
569}
570
571/// `memory_context`: what to tell an agent starting work in `repo`, within
572/// `budget` characters: pinned first, then the most recently used, the
573/// workspace's and the project's labelled apart. Marks what it gives as
574/// used. Called by the runner service only. Returns `MemoryContext`.
575///
576/// `requester` is the person the run acts for. The workspace's memory is
577/// its members': for a requester who is not one (an outside collaborator),
578/// only the project's memory is given. None, as from a runner that does not
579/// say, is the workspace's own step and gets both.
580#[derive(Debug, Serialize, Deserialize)]
581pub struct MemoryContextArgs {
582 pub repo: RepoPath,
583 #[serde(default)]
584 pub budget: Option<u32>,
585 #[serde(default)]
586 pub requester: Option<User>,
587}
588
589#[derive(Clone, Debug, Default, Serialize, Deserialize)]
590pub struct MemoryContext {
591 /// None when there is nothing to tell.
592 pub text: Option<String>,
593 pub count: u32,
594}
595
596/// The longest a memory may be.
597pub const MAX_MEMORY_CHARS: usize = 1000;
598
599/// Prefixes of credentials that say what they are.
600const SECRET_PREFIXES: [(&str, &str); 20] = [
601 ("sk-ant-", "an Anthropic key"),
602 ("sk-proj-", "an OpenAI key"),
603 ("sk_live_", "a Stripe key"),
604 ("sk_test_", "a Stripe key"),
605 ("rk_live_", "a Stripe key"),
606 ("whsec_", "a webhook signing secret"),
607 ("ghp_", "a GitHub token"),
608 ("gho_", "a GitHub token"),
609 ("ghs_", "a GitHub token"),
610 ("ghu_", "a GitHub token"),
611 ("github_pat_", "a GitHub token"),
612 ("glpat-", "a GitLab token"),
613 ("xoxb-", "a Slack token"),
614 ("xoxp-", "a Slack token"),
615 ("AKIA", "an AWS access key"),
616 ("ASIA", "an AWS access key"),
617 ("AIza", "a Google API key"),
618 ("g1t_", "a g1t token"),
619 ("npm_", "an npm token"),
620 ("SG.", "a SendGrid key"),
621];
622
623/// Words that, followed by `=` or `:` and a value, set a credential.
624const SECRET_WORDS: [&str; 8] = [
625 "password",
626 "passwd",
627 "secret",
628 "api_key",
629 "apikey",
630 "api-key",
631 "token",
632 "private_key",
633];
634
635/// Bits of entropy per character of `word`.
636fn entropy(word: &str) -> f64 {
637 let mut counts = std::collections::HashMap::new();
638 for c in word.chars() {
639 *counts.entry(c).or_insert(0usize) += 1;
640 }
641 let length = word.chars().count() as f64;
642 counts
643 .values()
644 .map(|&count| {
645 let p = count as f64 / length;
646 -p * p.log2()
647 })
648 .sum()
649}
650
651/// What `text` seems to hold that must never be stored in memory, which
652/// every agent in the workspace reads: a key, a token, a private key or a
653/// password. None when it looks clean. Errs on the side of refusing; a
654/// person can always say where a secret lives instead of what it is.
655pub fn secret_in(text: &str) -> Option<&'static str> {
656 if text.contains("-----BEGIN") && text.contains("PRIVATE KEY") {
657 return Some("a private key");
658 }
659 let lower = text.to_lowercase();
660 for word in SECRET_WORDS {
661 let mut rest = lower.as_str();
662 while let Some(at) = rest.find(word) {
663 let after = rest[at + word.len()..].trim_start_matches(['"', '\'', ' ']);
664 if let Some(value) = after.strip_prefix(['=', ':']) {
665 let value: String = value
666 .trim_start_matches([' ', '"', '\''])
667 .chars()
668 .take_while(|c| !c.is_whitespace() && *c != '"' && *c != '\'' && *c != ',')
669 .collect();
670 // "token: see 1Password" names where it is, which is fine.
671 if value.chars().count() >= 8 && value.chars().any(|c| c.is_ascii_digit()) {
672 return Some("a password or token");
673 }
674 }
675 rest = &rest[at + word.len()..];
676 }
677 }
678 let words = text.split(|c: char| {
679 c.is_whitespace() || matches!(c, '"' | '\'' | '`' | ',' | ';' | '(' | ')' | '<' | '>' | '[' | ']' | '{' | '}')
680 });
681 for word in words {
682 let word = word.trim_end_matches(['.', ':']);
683 // A key=value pair is judged by its value.
684 let word = word.rsplit_once('=').map_or(word, |(_, value)| value);
685 for (prefix, what) in SECRET_PREFIXES {
686 if let Some(rest) = word.strip_prefix(prefix) {
687 let tail = rest.chars().filter(|c| c.is_ascii_alphanumeric()).count();
688 if tail >= 12 {
689 return Some(what);
690 }
691 }
692 }
693 // A URL with a password in it.
694 if let Some((_, rest)) = word.split_once("://")
695 && let Some((credentials, _)) = rest.split_once('@')
696 && credentials.contains(':')
697 {
698 return Some("a URL with a password in it");
699 }
700 let length = word.chars().count();
701 if length < 32 || word.contains("://") || word.contains('/') && !word.contains('+') {
702 continue;
703 }
704 let token_chars = word
705 .chars()
706 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '+' | '/' | '='));
707 if !token_chars {
708 continue;
709 }
710 let hex = word.chars().all(|c| c.is_ascii_hexdigit());
711 // A commit's id is 40 hex characters and fine; longer runs of hex
712 // are keys and digests of keys.
713 if hex {
714 if length >= 48 {
715 return Some("a key or token");
716 }
717 continue;
718 }
719 let upper = word.chars().any(|c| c.is_ascii_uppercase());
720 let lower = word.chars().any(|c| c.is_ascii_lowercase());
721 let digit = word.chars().any(|c| c.is_ascii_digit());
722 if upper && lower && digit && entropy(word) > 4.0 {
723 return Some("a key or token");
724 }
725 }
726 None
727}
728
729// --- Compute gating ---------------------------------------------------------
730//
731// What the runner needs from the work service to apply a workspace's plan
732// caps: how many agents it has at work, what an issue's agents have spent,
733// and somewhere for runs to wait for a free slot.
734
735/// The kinds of run that count against a workspace's agents-at-once cap.
736pub const AGENT_KINDS: [RunKind; 6] = [
737 RunKind::Implement,
738 RunKind::Revise,
739 RunKind::Review,
740 RunKind::Answer,
741 RunKind::Update,
742 RunKind::Plan,
743];
744
745/// `active_agents`: how many agent runs a workspace has queued or running
746/// now. Called by the runner. Returns `u32`.
747#[derive(Debug, Serialize, Deserialize)]
748pub struct ActiveAgentsArgs {
749 pub workspace: String,
750}
751
752/// `issue_spend`: what the agents on an issue have cost so far, in all its
753/// pull requests' runs and the runs on the issue itself. `number` may name
754/// the issue or one of its pull requests. Called by the runner. Returns
755/// `Outcome<IssueSpend>`.
756#[derive(Debug, Serialize, Deserialize)]
757pub struct IssueSpendArgs {
758 pub repo: RepoPath,
759 pub number: u32,
760}
761
762#[derive(Clone, Debug, Serialize, Deserialize)]
763#[serde(rename_all = "camelCase")]
764pub struct IssueSpend {
765 /// The issue counted: the one `number` names, or its pull request's.
766 /// A pull request with no issue counts as its own.
767 pub issue: u32,
768 /// What the model providers charged for its runs, in millionths of a
769 /// dollar.
770 pub spent_micros: i64,
771}
772
773/// `wait_for_slot`: gives back a lifecycle step the runner claimed but
774/// could not start because the workspace's agents were all busy, so the
775/// next sweep takes it again, and says so on the pull request. Returns
776/// `bool`.
777#[derive(Debug, Serialize, Deserialize)]
778#[serde(rename_all = "camelCase")]
779pub struct WaitForSlotArgs {
780 pub pull_id: String,
781 pub reason: String,
782}
783
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent784/// `agent_comment`: a comment from g1t on an issue or pull request,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily785/// for what it could not do there. Called by the runner. Returns `bool`.
786#[derive(Debug, Serialize, Deserialize)]
787pub struct AgentCommentArgs {
788 pub repo: RepoPath,
789 pub number: u32,
790 pub body: String,
791}
792
793/// `add_wait`: a run a person asked for, waiting for one of the
794/// workspace's agent slots. `payload` is what the runner needs to start it
795/// later; the work service only keeps it. Returns `bool`.
796#[derive(Debug, Serialize, Deserialize)]
797pub struct AddWaitArgs {
798 pub workspace: String,
799 pub kind: String,
800 pub payload: serde_json::Value,
801}
802
803/// `waiting_workspaces`: the workspaces with runs waiting, oldest first.
804/// Returns `Vec<String>`.
805#[derive(Debug, Default, Serialize, Deserialize)]
806pub struct WaitingWorkspacesArgs {}
807
808/// `take_wait`: the oldest run waiting in a workspace, taken out of the
809/// queue in one statement. Returns `Option<AgentWait>`.
810#[derive(Debug, Serialize, Deserialize)]
811pub struct TakeWaitArgs {
812 pub workspace: String,
813}
814
815#[derive(Clone, Debug, Serialize, Deserialize)]
816#[serde(rename_all = "camelCase")]
817pub struct AgentWait {
818 pub id: String,
819 pub workspace: String,
820 pub kind: String,
821 pub payload: serde_json::Value,
822 pub created_at: String,
823}
824
825/// `run_cost`: what a run's model cost, as its sandbox reported it, for
826/// the runner settling the run's reservation. Needs the run's token.
827/// Returns `Option<f64>`, in US dollars.
828#[derive(Debug, Serialize, Deserialize)]
829#[serde(rename_all = "camelCase")]
830pub struct RunCostArgs {
831 pub run_id: String,
832 pub token: String,
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838
839 #[test]
840 fn plain_knowledge_is_kept() {
841 for text in [
842 "We use pnpm everywhere, never npm or yarn.",
843 "Staging lives at https://staging.example.com and deploys from main.",
844 "The tests need TZ=UTC or the date tests fail.",
845 "Commit 9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13 introduced the flaky retry.",
846 "The API token is in 1Password under 'Deploy'.",
847 "token: ask Ana for one",
848 "src/components/ui/select.tsx wraps Radix; import from there, not radix-ui.",
849 "Run cargo test -p g1t-work before pushing changes to services/work.",
850 ] {
851 assert_eq!(secret_in(text), None, "{text}");
852 }
853 }
854
855 #[test]
856 fn credentials_are_refused() {
857 // Key-shaped values are joined at run time, so no whole key sits in
858 // the source, and each is an issuer's documented example or says
859 // EXAMPLE: were one ever found, it would be listed as a likely test
860 // value, never as a leaked key.
861 let join = |a: &str, b: &str| format!("{a}{b}");
862 for text in [
863 join("The key is sk-", "ant-api03-EXAMPLEKEY0EXAMPLEKEY0EXAMPLEKEY0EXAMPLE"),
864 join("use gh", "p_EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMP to clone"),
865 join("AWS: AK", "IAIOSFODNN7EXAMPLE"),
866 join("STRIPE_KEY=sk_l", "ive_EXAMPLE0EXAMPLE0EXAMPLE0"),
867 "password = hunter2hunter2".to_owned(),
868 "db: postgres://admin:s3cret@db.internal:5432/app".to_owned(),
869 "token is 3f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a".to_owned(),
870 "-----BEGIN RSA PRIVATE KEY-----\nMIIE...".to_owned(),
871 "secret: Zq8wN3vR7tY2uI5oP1aS6dF4gH9jK0lX".to_owned(),
872 "it is xK9mQ2vL8nR4tP7wZ3yB6cF1dG5hJ0sA".to_owned(),
873 ] {
874 assert!(secret_in(&text).is_some(), "{text}");
875 }
876 }
877
878 #[test]
879 fn kinds_and_statuses_round_trip() {
880 for kind in RunKind::ALL {
881 assert_eq!(RunKind::parse(kind.as_str()), Some(kind));
882 }
883 assert!(RunKind::Implement.takes_messages() && !RunKind::Review.takes_messages());
884 assert!(!RunKind::Checks.is_agent());
885 assert_eq!(RunStatus::parse("stopped"), Some(RunStatus::Stopped));
886 assert!(RunStatus::Queued.is_active() && !RunStatus::Failed.is_active());
887 assert_eq!(MemoryKind::parse("gotcha"), Some(MemoryKind::Gotcha));
888 }
889}

This file's history is long; its oldest lines are credited to the oldest commit read.