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
732 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.

The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.1//! Runs, recorded sessions and memory. Kept by the work service.
Last commits read trees two dozen commits at a time, and the file list waits at most 3 s for them2//!
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.3//! Every sandbox g1t starts for checks, the merge queue or a merge check is
4//! an [`AgentRun`]: what it is doing, on which pull request, step by step,
5//! and how it ended. Runs of the agents g1t once ran itself stay readable.
6//!
7//! A session is what a coding agent recorded on a pull request
8//! (`append_session`, `read_session`), summed up and listed here.
Last commits read trees two dozen commits at a time, and the file list waits at most 3 s for them9//!
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.10//! Memory is what people and their coding agents have written down that
11//! the next one should know: about one project (its codebase), or about
12//! the whole workspace (true across its projects). It is members-only,
13//! read with `recall`, and never holds a secret.
Last commits read trees two dozen commits at a time, and the file list waits at most 3 s for them14//!
15//! Each `*Args` struct is the argument of the method of the same name,
16//! served at `POST /rpc/<method>`.
17
18use serde::{Deserialize, Serialize};
19
20use crate::repos::RepoPath;
21use crate::work::{Pull, PullStatus, SessionEntry};
22use crate::{User, Viewer};
23
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.24/// The work a run does. Only `Checks`, `Queue` and `Mergecheck` are
25/// started now; the others are runs of the agents g1t once ran itself,
26/// kept so the runs and run credentials already stored still parse. None
27/// of them is started or granted any more.
Last commits read trees two dozen commits at a time, and the file list waits at most 3 s for them28#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum RunKind {
31 /// Making the change an issue asks for.
32 Implement,
33 /// Addressing failed checks or a review on its own pull request.
34 Revise,
35 /// Reviewing someone's pull request.
36 Review,
37 /// Answering another agent's question or handoff.
38 Answer,
39 /// Merging in the branch a pull request will land on.
40 Update,
41 /// Turning an outcome into a plan of issues.
42 Plan,
43 /// Running an issue's acceptance checks. Not an agent: a sandbox.
44 Checks,
45 /// Building and checking a state of the merge queue. Not an agent.
46 Queue,
47 /// Finding out whether a pull request merges cleanly. Not an agent.
48 Mergecheck,
49}
50
51impl RunKind {
52 pub const ALL: [RunKind; 9] = [
53 RunKind::Implement,
54 RunKind::Revise,
55 RunKind::Review,
56 RunKind::Answer,
57 RunKind::Update,
58 RunKind::Plan,
59 RunKind::Checks,
60 RunKind::Queue,
61 RunKind::Mergecheck,
62 ];
63
64 pub fn as_str(self) -> &'static str {
65 match self {
66 RunKind::Implement => "implement",
67 RunKind::Revise => "revise",
68 RunKind::Review => "review",
69 RunKind::Answer => "answer",
70 RunKind::Update => "update",
71 RunKind::Plan => "plan",
72 RunKind::Checks => "checks",
73 RunKind::Queue => "queue",
74 RunKind::Mergecheck => "mergecheck",
75 }
76 }
77
78 pub fn parse(value: &str) -> Option<RunKind> {
79 RunKind::ALL.into_iter().find(|kind| kind.as_str() == value)
80 }
81
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum RunStatus {
87 /// Its sandbox is starting.
88 Queued,
89 Running,
90 Succeeded,
91 Failed,
92 /// A person stopped it.
93 Stopped,
94}
95
96impl RunStatus {
97 pub fn as_str(self) -> &'static str {
98 match self {
99 RunStatus::Queued => "queued",
100 RunStatus::Running => "running",
101 RunStatus::Succeeded => "succeeded",
102 RunStatus::Failed => "failed",
103 RunStatus::Stopped => "stopped",
104 }
105 }
106
107 pub fn parse(value: &str) -> Option<RunStatus> {
108 [
109 RunStatus::Queued,
110 RunStatus::Running,
111 RunStatus::Succeeded,
112 RunStatus::Failed,
113 RunStatus::Stopped,
114 ]
115 .into_iter()
116 .find(|status| status.as_str() == value)
117 }
118
119 pub fn is_active(self) -> bool {
120 matches!(self, RunStatus::Queued | RunStatus::Running)
121 }
122}
123
124/// One step of a run, as one short line.
125#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct RunStep {
127 /// RFC 3339.
128 pub at: String,
129 pub text: String,
130}
131
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.132/// One sandbox g1t started: a run of checks, a merge queue build or a
133/// merge check (or, on older rows, one of its former agents at work).
Last commits read trees two dozen commits at a time, and the file list waits at most 3 s for them134#[derive(Clone, Debug, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct AgentRun {
137 pub id: String,
138 pub repo: RepoPath,
139 /// The pull request it works on; for a plan, none.
140 pub number: Option<u32>,
141 /// That pull request's title, or the plan's brief.
142 pub title: Option<String>,
143 pub kind: RunKind,
144 /// Who is working: `g1t` for g1t's own agent and for a sandbox that
145 /// runs commands.
146 pub agent: String,
147 /// The model, by its public name. Members only.
148 pub model: Option<String>,
149 pub status: RunStatus,
150 /// What it is doing now, in one line.
151 pub step: Option<String>,
152 /// Every step so far, oldest first. Empty in lists.
153 #[serde(default)]
154 pub steps: Vec<RunStep>,
155 /// How many steps there are.
156 #[serde(default)]
157 pub step_count: u32,
158 /// Who put it to work, when a person did.
159 pub started_by: Option<String>,
160 /// Why it failed.
161 pub error: Option<String>,
162 /// What it cost, in US dollars, as the harness reported it. Members only.
163 pub cost_usd: Option<f64>,
164 pub turns: Option<u32>,
165 /// The most it may cost, in US dollars, from its guardrails. None: no
166 /// cap. Members only.
167 #[serde(default)]
168 pub budget_usd: Option<f64>,
169 /// The longest it may take, in minutes, from its guardrails.
170 #[serde(default)]
171 pub time_cap_minutes: Option<u32>,
172 /// `budget` or `time` when g1t stopped it for reaching a cap.
173 #[serde(default)]
174 pub halted: Option<String>,
175 /// RFC 3339.
176 pub created_at: String,
177 pub started_at: Option<String>,
178 pub finished_at: Option<String>,
179 pub updated_at: String,
180}
181
182/// What lets a sandbox, and nothing else, report on its run.
183#[derive(Clone, Debug, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct AgentRunTicket {
186 pub run_id: String,
187 pub token: String,
188}
189
190/// `open_run`: records a sandbox the runner is starting. Called by the
191/// runner service only. Returns `Outcome<AgentRunTicket>`.
192#[derive(Debug, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct OpenRunArgs {
195 /// Who the sandbox acts as.
196 pub actor: User,
197 pub repo: RepoPath,
198 #[serde(default)]
199 pub number: Option<u32>,
200 #[serde(default)]
201 pub pull_id: Option<String>,
202 /// For a run with no pull request, such as a plan: what it is about.
203 #[serde(default)]
204 pub title: Option<String>,
205 pub kind: RunKind,
206 #[serde(default)]
207 pub agent: Option<String>,
208 #[serde(default)]
209 pub model: Option<String>,
210 /// The sandbox's name, which stopping it needs.
211 pub sandbox: String,
212 #[serde(default)]
213 pub started_by: Option<String>,
214 /// Its cost cap, from its guardrails, in US dollars.
215 #[serde(default)]
216 pub budget_usd: Option<f64>,
217 /// Its time cap, from its guardrails, in minutes.
218 #[serde(default)]
219 pub time_cap_minutes: Option<u32>,
220}
221
222/// `report_run`: a sandbox telling how its run goes, with the run's token.
223/// Steps are added, the current step replaced, and an outcome ends the run.
224/// A finished run accepts nothing more. Returns `Outcome<RunStatus>`: the
225/// run's status after the report, so a sandbox can tell it was stopped.
226#[derive(Debug, Default, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase", default)]
228pub struct ReportRunArgs {
229 pub run_id: String,
230 pub token: String,
231 pub steps: Vec<String>,
232 pub step: Option<String>,
233 pub cost_usd: Option<f64>,
234 pub turns: Option<u32>,
235 /// `succeeded` or `failed`, to end the run.
236 pub outcome: Option<RunStatus>,
237 pub error: Option<String>,
238 /// `budget` or `time`: the run reached a cap of its guardrails and
239 /// stopped. Ends it as stopped, like a person's stop.
240 pub halt: Option<crate::guardrails::Halt>,
241}
242
243/// `list_runs`: runs in a repository, or with `workspace` instead, in every
244/// repository of a workspace (members only), newest first. Returns
245/// `Outcome<Vec<AgentRun>>`.
246#[derive(Debug, Default, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase", default)]
248pub struct ListRunsArgs {
249 pub viewer: Viewer,
250 pub repo: Option<RepoPath>,
251 pub workspace: Option<String>,
252 /// Only those queued or running.
253 pub active: bool,
254 pub kind: Option<RunKind>,
255 pub status: Option<RunStatus>,
256 /// Only those on this pull request.
257 pub number: Option<u32>,
258 /// At most 200; 50 if not given.
259 pub limit: Option<u32>,
260}
261
262/// `get_run`: one run with all its steps. Returns `Outcome<AgentRun>`.
263#[derive(Debug, Serialize, Deserialize)]
264pub struct GetRunArgs {
265 pub viewer: Viewer,
266 pub repo: RepoPath,
267 pub id: String,
268}
269
270/// A pull request's recorded session, summed up for a list.
271#[derive(Clone, Debug, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct SessionSummary {
274 pub number: u32,
275 pub title: String,
276 pub status: PullStatus,
277 /// The agent label on the pull request, such as `g1t`.
278 pub agent: String,
279 pub entries: u32,
280 /// How many tools it called.
281 pub tools: u32,
282 /// The start of the first prompt.
283 pub prompt: Option<String>,
284 /// The kinds of the runs g1t made for it.
285 pub kinds: Vec<RunKind>,
286 pub runs: u32,
287 /// What its runs cost together. Members only.
288 pub cost_usd: Option<f64>,
289 /// Whether one of its runs is under way.
290 pub active: bool,
291 /// RFC 3339.
292 pub started_at: String,
293 pub last_at: String,
294}
295
296/// `list_sessions`: the pull requests of a repository that have a session,
297/// most recently active first. Returns `Outcome<Vec<SessionSummary>>`.
298#[derive(Debug, Default, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase", default)]
300pub struct ListSessionsArgs {
301 pub viewer: Viewer,
302 pub repo: Option<RepoPath>,
303 /// Only those with a run of this kind.
304 pub kind: Option<RunKind>,
305 /// The pull request's status: `draft`, `open`, `merged` or `closed`.
306 pub outcome: Option<PullStatus>,
307 pub number: Option<u32>,
308}
309
310/// `get_session`: one pull request's session in full, with its runs.
311/// Returns `Outcome<SessionView>`.
312#[derive(Debug, Serialize, Deserialize)]
313pub struct GetSessionArgs {
314 pub viewer: Viewer,
315 pub repo: RepoPath,
316 pub number: u32,
317}
318
319#[derive(Clone, Debug, Serialize, Deserialize)]
320#[serde(rename_all = "camelCase")]
321pub struct SessionView {
322 pub pull: Pull,
323 /// Oldest first; the first 2000.
324 pub entries: Vec<SessionEntry>,
325 /// Newest first, with their steps.
326 pub runs: Vec<AgentRun>,
327 /// Members only.
328 pub cost_usd: Option<f64>,
329}
330
331// --- Memory -----------------------------------------------------------------
332
333/// Whose memory: one project's, or the whole workspace's.
334#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
335#[serde(rename_all = "snake_case")]
336pub enum MemoryScope {
337 /// About one project's codebase.
338 Project,
339 /// True across the workspace's projects.
340 Workspace,
341}
342
343impl MemoryScope {
344 pub fn as_str(self) -> &'static str {
345 match self {
346 MemoryScope::Project => "project",
347 MemoryScope::Workspace => "workspace",
348 }
349 }
350}
351
352#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case")]
354pub enum MemoryKind {
355 /// How something is: "staging lives at staging.example.com".
356 #[default]
357 Fact,
358 /// How things are done here: "we use pnpm everywhere".
359 Convention,
360 /// Something chosen, and why: "we keep Postgres, not MySQL, for jsonb".
361 Decision,
362 /// A trap: "the tests need TZ=UTC or the date tests fail".
363 Gotcha,
364}
365
366impl MemoryKind {
367 pub fn as_str(self) -> &'static str {
368 match self {
369 MemoryKind::Fact => "fact",
370 MemoryKind::Convention => "convention",
371 MemoryKind::Decision => "decision",
372 MemoryKind::Gotcha => "gotcha",
373 }
374 }
375
376 pub fn parse(value: &str) -> Option<MemoryKind> {
377 [
378 MemoryKind::Fact,
379 MemoryKind::Convention,
380 MemoryKind::Decision,
381 MemoryKind::Gotcha,
382 ]
383 .into_iter()
384 .find(|kind| kind.as_str() == value)
385 }
386}
387
388/// Where a memory came from.
389#[derive(Clone, Debug, Default, Serialize, Deserialize)]
390#[serde(rename_all = "camelCase")]
391pub struct MemorySource {
392 /// `person`, `agent` or `run`.
393 pub kind: String,
394 /// The agent run that learned it.
395 #[serde(default)]
396 pub run_id: Option<String>,
397 /// The project and pull request it was learned on.
398 #[serde(default)]
399 pub repo: Option<RepoPath>,
400 #[serde(default)]
401 pub number: Option<u32>,
402 /// What it was captured from: `run:<id>`, `comment:<id>`,
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.403 /// `pull:<repo id>#<n>`, `doc:<repo id>:<path>`, on memories captured
404 /// before automatic capture was retired.
Last commits read trees two dozen commits at a time, and the file list waits at most 3 s for them405 #[serde(default)]
406 pub reference: Option<String>,
407 /// What it was learned from, quoted.
408 #[serde(default)]
409 pub evidence: Option<String>,
410}
411
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.412/// Whether a memory is recalled: `kept` ones are. `candidate` and
413/// `dismissed` are on memories captured automatically before that was
414/// retired, kept so those rows still parse; neither is recalled.
Last commits read trees two dozen commits at a time, and the file list waits at most 3 s for them415#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
416#[serde(rename_all = "snake_case")]
417pub enum MemoryStatus {
418 Candidate,
419 #[default]
420 Kept,
421 Dismissed,
422}
423
424impl MemoryStatus {
425 pub fn as_str(self) -> &'static str {
426 match self {
427 MemoryStatus::Candidate => "candidate",
428 MemoryStatus::Kept => "kept",
429 MemoryStatus::Dismissed => "dismissed",
430 }
431 }
432
433 pub fn parse(value: &str) -> Option<MemoryStatus> {
434 [MemoryStatus::Candidate, MemoryStatus::Kept, MemoryStatus::Dismissed]
435 .into_iter()
436 .find(|status| status.as_str() == value)
437 }
438}
439
440#[derive(Clone, Debug, Serialize, Deserialize)]
441#[serde(rename_all = "camelCase")]
442pub struct Memory {
443 pub id: String,
444 pub scope: MemoryScope,
445 /// The workspace's slug.
446 pub workspace: String,
447 /// For a project's memory, the project's repository.
448 pub repo: Option<RepoPath>,
449 pub text: String,
450 pub kind: MemoryKind,
451 pub source: MemorySource,
452 /// Who wrote it: a username, or `g1t`.
453 pub created_by: String,
454 /// Pinned memories are given to every agent first.
455 pub pinned: bool,
456 /// RFC 3339.
457 pub created_at: String,
458 pub updated_at: String,
459 /// When it was last given to an agent.
460 pub last_used_at: Option<String>,
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.461 /// Only kept memories are recalled.
Last commits read trees two dozen commits at a time, and the file list waits at most 3 s for them462 #[serde(default)]
463 pub status: MemoryStatus,
464 /// 0 to 1: how sure its source was. None for what people wrote.
465 #[serde(default)]
466 pub confidence: Option<f64>,
467 /// How many independent sources said it.
468 #[serde(default)]
469 pub seen: u32,
470}
471
472#[derive(Clone, Debug, Default, Serialize, Deserialize)]
473#[serde(rename_all = "camelCase")]
474pub struct Memories {
475 /// The project's own; empty when no project was named.
476 pub project: Vec<Memory>,
477 pub workspace: Vec<Memory>,
478}
479
480/// `list_memories`: a workspace's memory and, with `repo`, that project's.
481/// Members only. Returns `Outcome<Memories>`.
482#[derive(Debug, Serialize, Deserialize)]
483pub struct ListMemoriesArgs {
484 pub viewer: Viewer,
485 pub workspace: String,
486 #[serde(default)]
487 pub repo: Option<RepoPath>,
488}
489
490/// `add_memory`: a member or an agent adds to memory. A project's memory
491/// needs `repo`. Text that looks like a key or a token is refused.
492/// Returns `Outcome<Memory>`.
493#[derive(Debug, Serialize, Deserialize)]
494#[serde(rename_all = "camelCase")]
495pub struct AddMemoryArgs {
496 pub actor: User,
497 pub workspace: String,
498 #[serde(default)]
499 pub repo: Option<RepoPath>,
500 pub scope: MemoryScope,
501 pub text: String,
502 #[serde(default)]
503 pub kind: MemoryKind,
504 #[serde(default)]
505 pub pinned: bool,
506 /// For an agent: the pull request it is working on.
507 #[serde(default)]
508 pub from_number: Option<u32>,
509}
510
511/// `update_memory`: changes a memory's text, kind or pin. Members only.
512/// Returns `Outcome<Memory>`.
513#[derive(Debug, Serialize, Deserialize)]
514pub struct UpdateMemoryArgs {
515 pub actor: User,
516 pub workspace: String,
517 pub id: String,
518 #[serde(default)]
519 pub text: Option<String>,
520 #[serde(default)]
521 pub kind: Option<MemoryKind>,
522 #[serde(default)]
523 pub pinned: Option<bool>,
524}
525
526/// `delete_memory`: forgets a memory. Members only. Returns `Outcome<bool>`.
527#[derive(Debug, Serialize, Deserialize)]
528pub struct DeleteMemoryArgs {
529 pub actor: User,
530 pub workspace: String,
531 pub id: String,
532}
533
534/// `recall`: what a project and its workspace remember, matching `query`
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.535/// when given (every word, in any order), pinned first. Members only.
536/// Returns `Outcome<Memories>`.
Last commits read trees two dozen commits at a time, and the file list waits at most 3 s for them537#[derive(Debug, Serialize, Deserialize)]
538pub struct RecallArgs {
539 pub viewer: Viewer,
540 pub repo: RepoPath,
541 #[serde(default)]
542 pub query: Option<String>,
543 #[serde(default)]
544 pub limit: Option<u32>,
545}
546
547/// The longest a memory may be.
548pub const MAX_MEMORY_CHARS: usize = 1000;
549
550/// Prefixes of credentials that say what they are.
551const SECRET_PREFIXES: [(&str, &str); 20] = [
552 ("sk-ant-", "an Anthropic key"),
553 ("sk-proj-", "an OpenAI key"),
554 ("sk_live_", "a Stripe key"),
555 ("sk_test_", "a Stripe key"),
556 ("rk_live_", "a Stripe key"),
557 ("whsec_", "a webhook signing secret"),
558 ("ghp_", "a GitHub token"),
559 ("gho_", "a GitHub token"),
560 ("ghs_", "a GitHub token"),
561 ("ghu_", "a GitHub token"),
562 ("github_pat_", "a GitHub token"),
563 ("glpat-", "a GitLab token"),
564 ("xoxb-", "a Slack token"),
565 ("xoxp-", "a Slack token"),
566 ("AKIA", "an AWS access key"),
567 ("ASIA", "an AWS access key"),
568 ("AIza", "a Google API key"),
569 ("g1t_", "a g1t token"),
570 ("npm_", "an npm token"),
571 ("SG.", "a SendGrid key"),
572];
573
574/// Words that, followed by `=` or `:` and a value, set a credential.
575const SECRET_WORDS: [&str; 8] = [
576 "password",
577 "passwd",
578 "secret",
579 "api_key",
580 "apikey",
581 "api-key",
582 "token",
583 "private_key",
584];
585
586/// Bits of entropy per character of `word`.
587fn entropy(word: &str) -> f64 {
588 let mut counts = std::collections::HashMap::new();
589 for c in word.chars() {
590 *counts.entry(c).or_insert(0usize) += 1;
591 }
592 let length = word.chars().count() as f64;
593 counts
594 .values()
595 .map(|&count| {
596 let p = count as f64 / length;
597 -p * p.log2()
598 })
599 .sum()
600}
601
602/// What `text` seems to hold that must never be stored in memory, which
603/// every agent in the workspace reads: a key, a token, a private key or a
604/// password. None when it looks clean. Errs on the side of refusing; a
605/// person can always say where a secret lives instead of what it is.
606pub fn secret_in(text: &str) -> Option<&'static str> {
607 if text.contains("-----BEGIN") && text.contains("PRIVATE KEY") {
608 return Some("a private key");
609 }
610 let lower = text.to_lowercase();
611 for word in SECRET_WORDS {
612 let mut rest = lower.as_str();
613 while let Some(at) = rest.find(word) {
614 let after = rest[at + word.len()..].trim_start_matches(['"', '\'', ' ']);
615 if let Some(value) = after.strip_prefix(['=', ':']) {
616 let value: String = value
617 .trim_start_matches([' ', '"', '\''])
618 .chars()
619 .take_while(|c| !c.is_whitespace() && *c != '"' && *c != '\'' && *c != ',')
620 .collect();
621 // "token: see 1Password" names where it is, which is fine.
622 if value.chars().count() >= 8 && value.chars().any(|c| c.is_ascii_digit()) {
623 return Some("a password or token");
624 }
625 }
626 rest = &rest[at + word.len()..];
627 }
628 }
629 let words = text.split(|c: char| {
630 c.is_whitespace() || matches!(c, '"' | '\'' | '`' | ',' | ';' | '(' | ')' | '<' | '>' | '[' | ']' | '{' | '}')
631 });
632 for word in words {
633 let word = word.trim_end_matches(['.', ':']);
634 // A key=value pair is judged by its value.
635 let word = word.rsplit_once('=').map_or(word, |(_, value)| value);
636 for (prefix, what) in SECRET_PREFIXES {
637 if let Some(rest) = word.strip_prefix(prefix) {
638 let tail = rest.chars().filter(|c| c.is_ascii_alphanumeric()).count();
639 if tail >= 12 {
640 return Some(what);
641 }
642 }
643 }
644 // A URL with a password in it.
645 if let Some((_, rest)) = word.split_once("://")
646 && let Some((credentials, _)) = rest.split_once('@')
647 && credentials.contains(':')
648 {
649 return Some("a URL with a password in it");
650 }
651 let length = word.chars().count();
652 if length < 32 || word.contains("://") || word.contains('/') && !word.contains('+') {
653 continue;
654 }
655 let token_chars = word
656 .chars()
657 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '+' | '/' | '='));
658 if !token_chars {
659 continue;
660 }
661 let hex = word.chars().all(|c| c.is_ascii_hexdigit());
662 // A commit's id is 40 hex characters and fine; longer runs of hex
663 // are keys and digests of keys.
664 if hex {
665 if length >= 48 {
666 return Some("a key or token");
667 }
668 continue;
669 }
670 let upper = word.chars().any(|c| c.is_ascii_uppercase());
671 let lower = word.chars().any(|c| c.is_ascii_lowercase());
672 let digit = word.chars().any(|c| c.is_ascii_digit());
673 if upper && lower && digit && entropy(word) > 4.0 {
674 return Some("a key or token");
675 }
676 }
677 None
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683
684 #[test]
685 fn plain_knowledge_is_kept() {
686 for text in [
687 "We use pnpm everywhere, never npm or yarn.",
688 "Staging lives at https://staging.example.com and deploys from main.",
689 "The tests need TZ=UTC or the date tests fail.",
690 "Commit 9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13 introduced the flaky retry.",
691 "The API token is in 1Password under 'Deploy'.",
692 "token: ask Ana for one",
693 "src/components/ui/select.tsx wraps Radix; import from there, not radix-ui.",
694 "Run cargo test -p g1t-work before pushing changes to services/work.",
695 ] {
696 assert_eq!(secret_in(text), None, "{text}");
697 }
698 }
699
700 #[test]
701 fn credentials_are_refused() {
702 // Key-shaped values are joined at run time, so no whole key sits in
703 // the source, and each is an issuer's documented example or says
704 // EXAMPLE: were one ever found, it would be listed as a likely test
705 // value, never as a leaked key.
706 let join = |a: &str, b: &str| format!("{a}{b}");
707 for text in [
708 join("The key is sk-", "ant-api03-EXAMPLEKEY0EXAMPLEKEY0EXAMPLEKEY0EXAMPLE"),
709 join("use gh", "p_EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMP to clone"),
710 join("AWS: AK", "IAIOSFODNN7EXAMPLE"),
711 join("STRIPE_KEY=sk_l", "ive_EXAMPLE0EXAMPLE0EXAMPLE0"),
712 "password = hunter2hunter2".to_owned(),
713 "db: postgres://admin:s3cret@db.internal:5432/app".to_owned(),
714 "token is 3f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a".to_owned(),
715 "-----BEGIN RSA PRIVATE KEY-----\nMIIE...".to_owned(),
716 "secret: Zq8wN3vR7tY2uI5oP1aS6dF4gH9jK0lX".to_owned(),
717 "it is xK9mQ2vL8nR4tP7wZ3yB6cF1dG5hJ0sA".to_owned(),
718 ] {
719 assert!(secret_in(&text).is_some(), "{text}");
720 }
721 }
722
723 #[test]
724 fn kinds_and_statuses_round_trip() {
725 for kind in RunKind::ALL {
726 assert_eq!(RunKind::parse(kind.as_str()), Some(kind));
727 }
728 assert_eq!(RunStatus::parse("stopped"), Some(RunStatus::Stopped));
729 assert!(RunStatus::Queued.is_active() && !RunStatus::Failed.is_active());
730 assert_eq!(MemoryKind::parse("gotcha"), Some(MemoryKind::Gotcha));
731 }
732}

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