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. |
| Cache hits are counted, not only their bytes | 2 | //! |
| 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. | |
| Cache hits are counted, not only their bytes | 9 | //! |
| 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. | |
| Cache hits are counted, not only their bytes | 14 | //! |
| 15 | //! Each `*Args` struct is the argument of the method of the same name, | |
| 16 | //! served at `POST /rpc/<method>`. | |
| 17 | ||
| 18 | use serde::{Deserialize, Serialize}; | |
| 19 | ||
| 20 | use crate::repos::RepoPath; | |
| 21 | use crate::work::{Pull, PullStatus, SessionEntry}; | |
| 22 | use 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. | |
| Cache hits are counted, not only their bytes | 28 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 29 | #[serde(rename_all = "snake_case")] | |
| 30 | pub 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 | ||
| 51 | impl 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")] | |
| 86 | pub enum RunStatus { | |
| 87 | /// Its sandbox is starting. | |
| 88 | Queued, | |
| 89 | Running, | |
| 90 | Succeeded, | |
| 91 | Failed, | |
| 92 | /// A person stopped it. | |
| 93 | Stopped, | |
| 94 | } | |
| 95 | ||
| 96 | impl 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)] | |
| 126 | pub 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). | |
| Cache hits are counted, not only their bytes | 134 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 135 | #[serde(rename_all = "camelCase")] | |
| 136 | pub 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")] | |
| 185 | pub 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")] | |
| 194 | pub 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)] | |
| 228 | pub 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)] | |
| 248 | pub 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)] | |
| 264 | pub 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")] | |
| 273 | pub 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)] | |
| 300 | pub 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)] | |
| 313 | pub 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")] | |
| 321 | pub 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")] | |
| 336 | pub enum MemoryScope { | |
| 337 | /// About one project's codebase. | |
| 338 | Project, | |
| 339 | /// True across the workspace's projects. | |
| 340 | Workspace, | |
| 341 | } | |
| 342 | ||
| 343 | impl 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")] | |
| 354 | pub 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 | ||
| 366 | impl 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")] | |
| 391 | pub 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. | |
| Cache hits are counted, not only their bytes | 405 | #[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. | |
| Cache hits are counted, not only their bytes | 415 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
| 416 | #[serde(rename_all = "snake_case")] | |
| 417 | pub enum MemoryStatus { | |
| 418 | Candidate, | |
| 419 | #[default] | |
| 420 | Kept, | |
| 421 | Dismissed, | |
| 422 | } | |
| 423 | ||
| 424 | impl 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")] | |
| 442 | pub 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. |
| Cache hits are counted, not only their bytes | 462 | #[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")] | |
| 474 | pub 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)] | |
| 483 | pub 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")] | |
| 495 | pub 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)] | |
| 514 | pub 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)] | |
| 528 | pub 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>`. | |
| Cache hits are counted, not only their bytes | 537 | #[derive(Debug, Serialize, Deserialize)] |
| 538 | pub 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. | |
| 548 | pub const MAX_MEMORY_CHARS: usize = 1000; | |
| 549 | ||
| 550 | /// Prefixes of credentials that say what they are. | |
| 551 | const 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. | |
| 575 | const 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`. | |
| 587 | fn 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. | |
| 606 | pub 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)] | |
| 681 | mod 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.