Skip to content
732 linesCodeBlameRaw
1//! Runs, recorded sessions and memory. Kept by the work service.
2//!
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.
9//!
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.
14//!
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
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.
28#[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
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).
134#[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>`,
403 /// `pull:<repo id>#<n>`, `doc:<repo id>:<path>`, on memories captured
404 /// before automatic capture was retired.
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
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.
415#[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>,
461 /// Only kept memories are recalled.
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")]
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`
535/// when given (every word, in any order), pinned first. Members only.
536/// Returns `Outcome<Memories>`.
537#[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}