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

Inbox: the events service tells people what needs them as events arrive1//! The inbox: what needs a person, or what they follow, as it happens.
2//!
3//! The events service keeps it, beside the event log: as events arrive it
4//! works out who should hear of each (see `services/events/src/inbox.rs`)
Inbox: threads, reasons, subscriptions and watching5//! and why. Each person has one item per **thread**, the thing it is about
6//! (an issue, a pull request, a workflow on a branch, a deployment): new
7//! activity on a thread brings its item back to the top, unread, and adds
8//! a line to its short history, rather than adding another item. Items are
9//! kept by username, which never changes.
10//!
11//! Who hears of a thread follows its **subscriptions**: whoever opened it,
12//! is assigned to it, was asked to review it, commented on it or was
13//! mentioned in it is subscribed without asking, and anyone can subscribe
14//! or unsubscribe by hand. A person can also **watch** a repository: only
15//! what they take part in (the default), all of its activity, only some
16//! kinds of it, or nothing at all.
17//!
18//! Methods, served at `POST /rpc/<method>` on the events service:
Inbox: the events service tells people what needs them as events arrive19//!
20//! - `inbox_list` takes `ListInboxArgs` and returns `InboxPage`. Items about
21//! a repository the viewer can no longer read are dropped as they are
22//! found.
23//! - `inbox_counts` takes `InboxCountsArgs` and returns `InboxCounts`: the
24//! unread items, by severity. One query, for every page's top bar.
25//! - `inbox_mark` takes `MarkInboxArgs` and returns how many items changed.
Inbox: threads, reasons, subscriptions and watching26//! - `inbox_thread` takes `ThreadArgs` and returns `Option<InboxThread>`:
27//! one item with its history and the person's subscription.
28//! - `inbox_subscription` takes `SubscriptionArgs` and returns
29//! `Option<ThreadSubscription>`; `inbox_subscribe` takes `SubscribeArgs`
30//! and returns the same.
31//! - `inbox_watching` takes `WatchingArgs` and returns `Watching`;
32//! `inbox_watch` takes `WatchArgs` and returns `Watching`;
33//! `inbox_watched` takes `InboxCountsArgs` and returns `Vec<Watching>`.
34//! - `inbox_settings` takes `InboxCountsArgs` and returns `InboxSettings`;
35//! `inbox_update_settings` takes `UpdateInboxSettingsArgs` and returns
36//! `InboxSettings`.
Inbox: the events service tells people what needs them as events arrive37//!
38//! What an event is about (the issue or pull request, its people, the
39//! comment) comes from the work service's `inbox_subject`, which takes
40//! `InboxSubjectArgs` and returns `Option<InboxSubject>`.
41
42use serde::{Deserialize, Serialize};
43
44use crate::Viewer;
45use crate::credentials::Principal;
46
47/// How much an item matters, and how it is shown: a failure, something a
Inbox: threads, reasons, subscriptions and watching48/// person must answer (an agent waiting on them, a review asked of them),
49/// something that went well, or something to know.
Inbox: the events service tells people what needs them as events arrive50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum Severity {
53 Error,
54 Warning,
55 Success,
56 Info,
57}
58
59impl Severity {
60 pub const ALL: [Severity; 4] = [Severity::Error, Severity::Warning, Severity::Success, Severity::Info];
61
62 pub fn as_str(self) -> &'static str {
63 match self {
64 Severity::Error => "error",
65 Severity::Warning => "warning",
66 Severity::Success => "success",
67 Severity::Info => "info",
68 }
69 }
70
71 pub fn parse(value: &str) -> Option<Severity> {
72 Severity::ALL.into_iter().find(|severity| severity.as_str() == value)
73 }
Inbox: threads, reasons, subscriptions and watching74
75 /// Which of two is kept on an unread thread: what needs the person,
76 /// then a failure, then good news, then the rest. Lower comes first.
77 pub fn urgency(self) -> u8 {
78 match self {
79 Severity::Warning => 0,
80 Severity::Error => 1,
81 Severity::Success => 2,
82 Severity::Info => 3,
83 }
84 }
85}
86
87/// Why a person was told: what ties them to the thread, or what it asked
88/// of them.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum Reason {
92 /// An agent is waiting on them: it asked a question, or it stopped
93 /// until a person steps in.
94 Agent,
95 /// Someone asked them to review a pull request.
96 ReviewRequested,
97 /// They were assigned to it.
98 Assign,
99 /// Someone mentioned them by name.
100 Mention,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar101 /// Someone mentioned a team they are in (`@workspace/team`).
102 TeamMention,
Inbox: threads, reasons, subscriptions and watching103 /// A check, workflow or deployment on their work finished.
104 CiActivity,
105 /// A security alert on a repository they look after.
106 SecurityAlert,
107 /// It was closed, reopened or merged.
108 StateChange,
109 /// They opened it, or asked g1t for it.
110 Author,
111 /// They commented on it.
112 Comment,
113 /// They subscribed to it by hand.
114 Manual,
115 /// They watch its repository.
116 Subscribed,
Inbox: the events service tells people what needs them as events arrive117}
118
Inbox: threads, reasons, subscriptions and watching119impl Reason {
120 /// Most specific first: when one person is told of something for more
121 /// than one reason, the first of these is the one shown.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar122 pub const ALL: [Reason; 12] = [
Inbox: threads, reasons, subscriptions and watching123 Reason::Agent,
124 Reason::ReviewRequested,
125 Reason::Assign,
126 Reason::Mention,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar127 Reason::TeamMention,
Inbox: threads, reasons, subscriptions and watching128 Reason::CiActivity,
129 Reason::SecurityAlert,
130 Reason::StateChange,
131 Reason::Author,
132 Reason::Comment,
133 Reason::Manual,
134 Reason::Subscribed,
135 ];
136
137 pub fn as_str(self) -> &'static str {
138 match self {
139 Reason::Agent => "agent",
140 Reason::ReviewRequested => "review_requested",
141 Reason::Assign => "assign",
142 Reason::Mention => "mention",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar143 Reason::TeamMention => "team_mention",
Inbox: threads, reasons, subscriptions and watching144 Reason::CiActivity => "ci_activity",
145 Reason::SecurityAlert => "security_alert",
146 Reason::StateChange => "state_change",
147 Reason::Author => "author",
148 Reason::Comment => "comment",
149 Reason::Manual => "manual",
150 Reason::Subscribed => "subscribed",
151 }
152 }
153
154 pub fn parse(value: &str) -> Option<Reason> {
155 Reason::ALL.into_iter().find(|reason| reason.as_str() == value)
156 }
157
158 /// Lower is more specific.
159 pub fn rank(self) -> usize {
160 Reason::ALL.iter().position(|reason| *reason == self).unwrap_or(Reason::ALL.len())
161 }
162
163 /// Whether the person takes part in the thread themselves, rather than
164 /// following it: everything but a hand subscription and watching.
165 pub fn participating(self) -> bool {
166 !matches!(self, Reason::Manual | Reason::Subscribed)
167 }
168
169 /// What is asked of the person directly: told even when they
170 /// unsubscribed from the thread, though never when they ignore it or
171 /// its repository.
172 pub fn direct(self) -> bool {
173 matches!(
174 self,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar175 Reason::Agent
176 | Reason::ReviewRequested
177 | Reason::Assign
178 | Reason::Mention
179 | Reason::TeamMention
180 | Reason::CiActivity
181 | Reason::SecurityAlert
Inbox: threads, reasons, subscriptions and watching182 )
183 }
184}
185
Inbox: the events service tells people what needs them as events arrive186/// What an item is about.
187#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "lowercase")]
189pub enum SubjectKind {
190 Issue,
191 Pull,
Inbox: threads, reasons, subscriptions and watching192 /// A workflow's runs on one branch.
Inbox: the events service tells people what needs them as events arrive193 Run,
Inbox: threads, reasons, subscriptions and watching194 /// A project's deployments: production, or one pull request's preview.
195 Deploy,
Inbox: the events service tells people what needs them as events arrive196}
197
198impl SubjectKind {
Inbox: threads, reasons, subscriptions and watching199 pub const ALL: [SubjectKind; 4] = [SubjectKind::Issue, SubjectKind::Pull, SubjectKind::Run, SubjectKind::Deploy];
200
Inbox: the events service tells people what needs them as events arrive201 pub fn as_str(self) -> &'static str {
202 match self {
203 SubjectKind::Issue => "issue",
204 SubjectKind::Pull => "pull",
205 SubjectKind::Run => "run",
Inbox: threads, reasons, subscriptions and watching206 SubjectKind::Deploy => "deploy",
Inbox: the events service tells people what needs them as events arrive207 }
208 }
209
210 pub fn parse(value: &str) -> Option<SubjectKind> {
Inbox: threads, reasons, subscriptions and watching211 SubjectKind::ALL.into_iter().find(|kind| kind.as_str() == value)
Inbox: the events service tells people what needs them as events arrive212 }
213}
214
Inbox: threads, reasons, subscriptions and watching215/// One thread in a person's inbox: what it is about, and its latest
216/// activity.
Inbox: the events service tells people what needs them as events arrive217#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct InboxItem {
Inbox: threads, reasons, subscriptions and watching220 /// The thread's id: the same for as long as the person has it.
Inbox: the events service tells people what needs them as events arrive221 pub id: String,
Inbox: threads, reasons, subscriptions and watching222 /// Why they were told of the latest activity.
223 pub reason: Reason,
224 /// While unread, the most urgent of what happened since it was last
225 /// read; once read, the latest's.
Inbox: the events service tells people what needs them as events arrive226 pub severity: Severity,
Inbox: threads, reasons, subscriptions and watching227 /// One line: what happened last, and where.
Inbox: the events service tells people what needs them as events arrive228 pub title: String,
229 /// One line: what it happened to, such as the pull request's title.
230 pub body: String,
Inbox: threads, reasons, subscriptions and watching231 /// The event behind the latest activity, such as `pull.merged`.
232 pub event: Option<String>,
Inbox: the events service tells people what needs them as events arrive233 /// `owner/name`.
234 pub repo: Option<String>,
235 /// The workspace it happened in.
236 pub workspace: Option<String>,
237 pub subject: Option<SubjectKind>,
238 /// The issue or pull request's number.
239 pub number: Option<u32>,
240 /// Where it is on g1t.sh: a path such as `/acme/rocket/pull/12`.
241 pub url: String,
Inbox: threads, reasons, subscriptions and watching242 /// Who did the latest: a username, or `g1t`. Absent when nobody did.
Inbox: the events service tells people what needs them as events arrive243 pub actor: Option<String>,
Inbox: threads, reasons, subscriptions and watching244 /// How many things have happened on the thread.
245 pub count: u32,
246 /// RFC 3339: when the person was first told of the thread.
Inbox: the events service tells people what needs them as events arrive247 pub created_at: String,
Inbox: threads, reasons, subscriptions and watching248 /// RFC 3339: its latest activity.
249 pub updated_at: String,
Inbox: the events service tells people what needs them as events arrive250 pub read_at: Option<String>,
251 pub done_at: Option<String>,
252 pub saved: bool,
253 /// While this is in the future the item is out of the list.
254 pub snoozed_until: Option<String>,
255}
256
Inbox: threads, reasons, subscriptions and watching257/// One thing that happened on a thread, as the person was told of it.
258#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
259#[serde(rename_all = "camelCase")]
260pub struct InboxActivity {
261 pub reason: Reason,
262 pub severity: Severity,
263 pub title: String,
264 pub body: String,
265 pub event: Option<String>,
266 pub actor: Option<String>,
267 pub created_at: String,
268}
269
270/// The most activity kept per thread, newest first.
271pub const MAX_ACTIVITY: u32 = 10;
272
Inbox: the events service tells people what needs them as events arrive273/// Which items a list shows.
274#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
275#[serde(rename_all = "lowercase")]
276pub enum InboxView {
277 /// Everything not done and not snoozed: the inbox itself.
278 #[default]
279 Inbox,
280 /// Saved items, done or not.
281 Saved,
282 /// Items marked done.
283 Done,
284}
285
Inbox: threads, reasons, subscriptions and watching286impl InboxView {
287 pub fn parse(value: &str) -> Option<InboxView> {
288 match value {
289 "inbox" => Some(InboxView::Inbox),
290 "saved" => Some(InboxView::Saved),
291 "done" => Some(InboxView::Done),
292 _ => None,
293 }
294 }
295}
296
297/// `inbox_list`. Latest activity first, except that in the inbox
298/// unfiltered, unread warnings (what is waiting on the person) come first.
299#[derive(Clone, Debug, Default, Serialize, Deserialize)]
Inbox: the events service tells people what needs them as events arrive300#[serde(rename_all = "camelCase")]
301pub struct ListInboxArgs {
302 /// Whose inbox: the person signed in. Their memberships decide which
303 /// repositories they can still read.
304 pub viewer: Viewer,
305 #[serde(default)]
306 pub view: InboxView,
307 /// Only items of this severity.
308 #[serde(default)]
309 pub severity: Option<Severity>,
Inbox: threads, reasons, subscriptions and watching310 /// Only items told for this reason.
311 #[serde(default)]
312 pub reason: Option<Reason>,
313 /// Only items the person takes part in (see [`Reason::participating`]).
Inbox: the events service tells people what needs them as events arrive314 #[serde(default)]
Inbox: threads, reasons, subscriptions and watching315 pub participating: bool,
316 /// Only items about this repository.
317 #[serde(default)]
318 pub repo_id: Option<String>,
319 #[serde(default)]
Inbox: the events service tells people what needs them as events arrive320 pub unread: bool,
Inbox: threads, reasons, subscriptions and watching321 /// RFC 3339: only items with activity at or after it.
322 #[serde(default)]
323 pub since: Option<String>,
324 /// RFC 3339: only items whose latest activity was before it.
325 #[serde(default)]
326 pub updated_before: Option<String>,
Inbox: the events service tells people what needs them as events arrive327 /// The `next` of the page before.
328 #[serde(default)]
329 pub before: Option<String>,
330 #[serde(default)]
331 pub limit: Option<u32>,
332}
333
334pub const DEFAULT_INBOX_PAGE: u32 = 30;
335pub const MAX_INBOX_PAGE: u32 = 100;
336
337#[derive(Clone, Debug, Default, Serialize, Deserialize)]
338#[serde(rename_all = "camelCase")]
339pub struct InboxPage {
340 pub items: Vec<InboxItem>,
341 /// Pass as `before` for the next page; absent on the last.
342 pub next: Option<String>,
343}
344
Inbox: threads, reasons, subscriptions and watching345/// `inbox_counts`, and the other methods that need only whose.
Inbox: the events service tells people what needs them as events arrive346#[derive(Clone, Debug, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub struct InboxCountsArgs {
349 pub username: String,
350}
351
352/// Unread items in the inbox view, by severity.
353#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
354#[serde(rename_all = "camelCase")]
355pub struct InboxCounts {
356 pub unread: u32,
357 pub error: u32,
358 pub warning: u32,
359 pub success: u32,
360 pub info: u32,
361}
362
363/// What `inbox_mark` does to the items it names.
364#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
365#[serde(rename_all = "snake_case")]
366pub enum InboxMark {
367 Read,
368 Unread,
369 /// Out of the inbox, into Done; read with it.
370 Done,
371 /// Back into the inbox.
372 Undone,
373 Save,
374 Unsave,
375 /// Out of the inbox until `until`.
376 Snooze,
Inbox: threads, reasons, subscriptions and watching377 /// Back into the inbox now.
378 Unsnooze,
Inbox: the events service tells people what needs them as events arrive379}
380
381/// `inbox_mark`: changes the person's own items, by id, or every item in
382/// their inbox when `ids` is empty and `all` is set (Mark all read).
383#[derive(Clone, Debug, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct MarkInboxArgs {
386 pub username: String,
387 pub mark: InboxMark,
388 #[serde(default)]
389 pub ids: Vec<String>,
390 #[serde(default)]
391 pub all: bool,
392 /// With `all`: only items of this severity.
393 #[serde(default)]
394 pub severity: Option<Severity>,
Inbox: threads, reasons, subscriptions and watching395 /// With `all`: only items about this repository.
396 #[serde(default)]
397 pub repo_id: Option<String>,
398 /// With `all`: only items whose latest activity was at or before this
399 /// (RFC 3339), so what arrived after the person looked stays unread.
400 #[serde(default)]
401 pub last_read_at: Option<String>,
Inbox: the events service tells people what needs them as events arrive402 /// For `snooze`: RFC 3339.
403 #[serde(default)]
404 pub until: Option<String>,
405}
406
407/// The most ids one `inbox_mark` call changes.
408pub const MAX_MARK: usize = 100;
409
Inbox: threads, reasons, subscriptions and watching410/// `inbox_thread`: one of the viewer's own threads, by id.
411#[derive(Clone, Debug, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct ThreadArgs {
414 pub viewer: Viewer,
415 pub id: String,
416}
417
418/// A thread with its history and the person's subscription to it.
419#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
420#[serde(rename_all = "camelCase")]
421pub struct InboxThread {
422 #[serde(flatten)]
423 pub item: InboxItem,
424 /// Newest first, at most [`MAX_ACTIVITY`].
425 pub activity: Vec<InboxActivity>,
426 /// For an issue or pull request; absent for a run or a deployment,
427 /// which nobody subscribes to.
428 pub subscription: Option<ThreadSubscription>,
429}
430
431/// A person's subscription to an issue or pull request.
432#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
433#[serde(rename_all = "camelCase")]
434pub struct ThreadSubscription {
435 /// Whether they hear of what happens on it.
436 pub subscribed: bool,
437 /// Whether they hear of nothing on it at all, not even a mention.
438 pub ignored: bool,
439 /// Why they are subscribed: they opened it (`author`), are assigned
440 /// (`assign`), were asked to review (`review_requested`), commented
441 /// (`comment`), were mentioned (`mention`) or subscribed by hand
442 /// (`manual`). Absent when they are not.
443 pub reason: Option<Reason>,
444 /// `owner/name`, and the issue or pull request's number.
445 pub repo: Option<String>,
446 pub number: Option<u32>,
447 /// RFC 3339: when they last chose, or null if they never did.
448 pub updated_at: Option<String>,
449}
450
451/// Which issue or pull request: by a thread's id, or by its repository and
452/// number.
453#[derive(Clone, Debug, Default, Serialize, Deserialize)]
454#[serde(rename_all = "camelCase")]
455pub struct SubscriptionArgs {
456 pub viewer: Viewer,
457 #[serde(default)]
458 pub id: Option<String>,
459 #[serde(default)]
460 pub repo_id: Option<String>,
461 #[serde(default)]
462 pub number: Option<u32>,
463}
464
465/// `inbox_subscribe`.
466#[derive(Clone, Debug, Default, Serialize, Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct SubscribeArgs {
469 #[serde(flatten)]
470 pub on: SubscriptionArgs,
471 /// True to subscribe, false to unsubscribe. Absent with `ignored`
472 /// false: back to the default, subscribed only while taking part.
473 #[serde(default)]
474 pub subscribed: Option<bool>,
475 /// True to hear of nothing on it, not even a mention.
476 #[serde(default)]
477 pub ignored: bool,
478}
479
480/// How closely a person follows a repository.
481#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
482#[serde(rename_all = "snake_case")]
483pub enum WatchLevel {
484 /// Only what they take part in or are mentioned in: the default.
485 #[default]
486 Participating,
487 /// Everything: every issue and pull request opened, commented on,
488 /// closed or merged, and every deployment.
489 All,
490 /// Nothing at all, not even a mention.
491 Ignore,
492 /// What they take part in, and the kinds of activity in `events`.
493 Custom,
494}
495
496impl WatchLevel {
497 pub const ALL: [WatchLevel; 4] = [WatchLevel::Participating, WatchLevel::All, WatchLevel::Ignore, WatchLevel::Custom];
498
499 pub fn as_str(self) -> &'static str {
500 match self {
501 WatchLevel::Participating => "participating",
502 WatchLevel::All => "all",
503 WatchLevel::Ignore => "ignore",
504 WatchLevel::Custom => "custom",
505 }
506 }
507
508 pub fn parse(value: &str) -> Option<WatchLevel> {
509 WatchLevel::ALL.into_iter().find(|level| level.as_str() == value)
510 }
511}
512
513/// The kinds of activity a custom watch can follow.
514pub const WATCH_EVENTS: [&str; 4] = ["issues", "pulls", "deployments", "security"];
515
516/// How a person watches one repository.
517#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
518#[serde(rename_all = "camelCase")]
519pub struct Watching {
520 pub repo_id: String,
521 /// `owner/name`, when known.
522 #[serde(default)]
523 pub repo: Option<String>,
524 pub level: WatchLevel,
525 /// With `custom`: some of [`WATCH_EVENTS`].
526 #[serde(default)]
527 pub events: Vec<String>,
528 /// RFC 3339: when they chose, or null if they never did.
529 #[serde(default)]
530 pub updated_at: Option<String>,
531}
532
533/// `inbox_watching`.
534#[derive(Clone, Debug, Serialize, Deserialize)]
535#[serde(rename_all = "camelCase")]
536pub struct WatchingArgs {
537 pub username: String,
538 pub repo_id: String,
539}
540
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97541/// `inbox_watchers`: how many watch a repository: all of its activity,
542/// or some of it (custom). The default (participating) and ignoring are
543/// not counted. Returns `u64`.
544#[derive(Clone, Debug, Serialize, Deserialize)]
545#[serde(rename_all = "camelCase")]
546pub struct WatchersArgs {
547 pub repo_id: String,
548}
549
Inbox: threads, reasons, subscriptions and watching550/// `inbox_watch`. No `level`: back to the default.
551#[derive(Clone, Debug, Serialize, Deserialize)]
552#[serde(rename_all = "camelCase")]
553pub struct WatchArgs {
554 pub username: String,
555 pub repo_id: String,
556 /// `owner/name`, kept to list what the person watches.
557 #[serde(default)]
558 pub repo: Option<String>,
559 #[serde(default)]
560 pub level: Option<WatchLevel>,
561 #[serde(default)]
562 pub events: Vec<String>,
563}
564
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.565/// When security alerts that are not urgent (medium, low, and what is only
566/// to know) reach a person: in a digest once a day or once a week, at once
567/// like the urgent ones, or not at all. Critical and high alerts, a refused
568/// push and a vulnerability report never wait for the digest.
569#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
570#[serde(rename_all = "snake_case")]
571pub enum DigestCadence {
572 #[default]
573 Daily,
574 Weekly,
575 AtOnce,
576 Off,
577}
578
579impl DigestCadence {
580 pub const ALL: [DigestCadence; 4] = [DigestCadence::Daily, DigestCadence::Weekly, DigestCadence::AtOnce, DigestCadence::Off];
581
582 pub fn as_str(self) -> &'static str {
583 match self {
584 DigestCadence::Daily => "daily",
585 DigestCadence::Weekly => "weekly",
586 DigestCadence::AtOnce => "at_once",
587 DigestCadence::Off => "off",
588 }
589 }
590
591 pub fn parse(value: &str) -> Option<DigestCadence> {
592 DigestCadence::ALL.into_iter().find(|cadence| cadence.as_str() == value)
593 }
594}
595
Inbox: threads, reasons, subscriptions and watching596/// A person's choices about being told.
597#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
598#[serde(rename_all = "camelCase")]
599pub struct InboxSettings {
600 /// The reasons they are also emailed for.
601 pub email: Vec<Reason>,
602 /// How they watch a repository they create.
603 pub default_watch: WatchLevel,
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.604 /// Whether critical and high security alerts on repositories they watch
605 /// for security reach their inbox at once. A workspace's owners and
606 /// security managers are told whatever this says: those alerts are
607 /// theirs to act on.
608 #[serde(default = "yes")]
609 pub security_alerts: bool,
610 /// When the rest of the security alerts reach them.
611 #[serde(default)]
612 pub security_digest: DigestCadence,
613}
614
615fn yes() -> bool {
616 true
Inbox: threads, reasons, subscriptions and watching617}
618
619impl Default for InboxSettings {
620 fn default() -> Self {
621 InboxSettings {
622 email: DEFAULT_EMAIL.to_vec(),
623 default_watch: WatchLevel::All,
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.624 security_alerts: true,
625 security_digest: DigestCadence::Daily,
Inbox: threads, reasons, subscriptions and watching626 }
627 }
628}
629
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.630/// What a person is emailed for until they choose: what is waiting on
631/// them, and security alerts, which reach their inbox only when urgent or
632/// as their digest.
633pub const DEFAULT_EMAIL: [Reason; 4] = [Reason::Agent, Reason::ReviewRequested, Reason::Mention, Reason::SecurityAlert];
Inbox: threads, reasons, subscriptions and watching634
635/// `inbox_update_settings`. What is left out is unchanged.
636#[derive(Clone, Debug, Serialize, Deserialize)]
637#[serde(rename_all = "camelCase")]
638pub struct UpdateInboxSettingsArgs {
639 pub username: String,
640 #[serde(default)]
641 pub email: Option<Vec<Reason>>,
642 #[serde(default)]
643 pub default_watch: Option<WatchLevel>,
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.644 #[serde(default)]
645 pub security_alerts: Option<bool>,
646 #[serde(default)]
647 pub security_digest: Option<DigestCadence>,
Inbox: threads, reasons, subscriptions and watching648}
649
Inbox: the events service tells people what needs them as events arrive650/// `inbox_subject` on the work service: what an event names, for the
651/// inbox. A service-to-service read: it checks nobody's access, and what
652/// it returns is only ever shown to the people it names, or to those who
653/// can read the repository.
654#[derive(Clone, Debug, Serialize, Deserialize)]
655#[serde(rename_all = "camelCase")]
656pub struct InboxSubjectArgs {
657 pub repo_id: String,
658 pub number: u32,
659 /// The comment the event is about, if any.
660 #[serde(default)]
661 pub comment_id: Option<String>,
662}
663
664#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
665#[serde(rename_all = "camelCase")]
666pub struct InboxSubject {
667 /// `issue` or `pull`.
668 pub kind: Option<SubjectKind>,
669 pub title: String,
670 pub author: Principal,
671 /// For work g1t did: the person it was done for.
672 #[serde(default)]
673 pub requested_by: Option<Principal>,
674 /// Usernames.
675 #[serde(default)]
676 pub assignees: Vec<String>,
677 /// Usernames, and `g1t`. Pull requests only.
678 #[serde(default)]
679 pub reviewers: Vec<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar680 /// The people its description mentions by name, outside code and
681 /// quotes, for `issue.opened` and `pull.opened`. Never `g1t`.
682 #[serde(default)]
683 pub mentions: Vec<String>,
684 /// The teams its description mentions, with the people each tells.
685 #[serde(default)]
686 pub team_mentions: Vec<TeamMentioned>,
Inbox: the events service tells people what needs them as events arrive687 /// For a pull request: the issue it is for, with that issue's people.
688 #[serde(default)]
689 pub issue: Option<Box<InboxSubject>>,
690 #[serde(default)]
691 pub comment: Option<InboxComment>,
692}
693
694impl InboxSubject {
695 /// Whose it is to answer for: whoever asked g1t for it, or its author.
696 pub fn owner(&self) -> &Principal {
697 self.requested_by.as_ref().unwrap_or(&self.author)
698 }
699}
700
701#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
702#[serde(rename_all = "camelCase")]
703pub struct InboxComment {
704 pub author: Principal,
705 /// The first line or so, as written.
706 pub excerpt: String,
707 /// The people it mentions by name, outside code and quotes. Never `g1t`.
708 #[serde(default)]
709 pub mentions: Vec<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar710 /// The teams it mentions (`@workspace/team`), with the people each
711 /// tells: everyone in the team and its child teams, unless the team
712 /// turned notifications off. A secret team tells nobody unless the
713 /// writer is in it.
714 #[serde(default)]
715 pub team_mentions: Vec<TeamMentioned>,
Inbox: the events service tells people what needs them as events arrive716 /// A review's verdict: `approve` or `request_changes`.
717 #[serde(default)]
718 pub verdict: Option<String>,
719 /// Something that happened (an assignment, a close), not something written.
720 #[serde(default)]
721 pub event: bool,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar722 /// Set when one of the workspace's agents wrote it, as itself: it is
723 /// shown by its name, marked as an agent, never as a person.
724 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub agent: Option<crate::work::AgentRef>,
726 /// Who the agent acted for, set with `agent`.
727 #[serde(default, skip_serializing_if = "Option::is_none")]
728 pub acting_for: Option<Principal>,
729 /// An agent's review: its verdict is advisory.
730 #[serde(default)]
731 pub advisory: bool,
Inbox: the events service tells people what needs them as events arrive732}
733
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar734/// A team a comment or description mentions, and who it tells.
735#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
736#[serde(rename_all = "camelCase")]
737pub struct TeamMentioned {
738 /// `workspace/slug`.
739 pub team: String,
740 /// Usernames.
741 pub members: Vec<String>,
742}
743
Inbox: threads, reasons, subscriptions and watching744/// `notify_by_email` on the identity service: one item, emailed to the
745/// person it is for, if they can still read its repository and their
746/// address is confirmed. Returns whether it was sent.
747#[derive(Clone, Debug, Serialize, Deserialize)]
748#[serde(rename_all = "camelCase")]
749pub struct NotifyByEmailArgs {
750 pub username: String,
751 pub repo_id: String,
752 /// The subject line and the first paragraph.
753 pub subject: String,
754 pub intro: String,
755 /// What was said, and who said it, when it was written by someone.
756 #[serde(default)]
757 pub quote: Option<(String, String)>,
758 /// A path on the site, such as `/acme/rocket/pull/12`.
759 pub path: String,
760 pub reason: Reason,
761}
762
Inbox: the events service tells people what needs them as events arrive763#[cfg(test)]
764mod tests {
765 use super::*;
766
767 #[test]
Inbox: threads, reasons, subscriptions and watching768 fn severities_reasons_and_subjects_read_back() {
Inbox: the events service tells people what needs them as events arrive769 for severity in Severity::ALL {
770 assert_eq!(Severity::parse(severity.as_str()), Some(severity));
771 }
772 assert_eq!(Severity::parse("fatal"), None);
Inbox: threads, reasons, subscriptions and watching773 for reason in Reason::ALL {
774 assert_eq!(Reason::parse(reason.as_str()), Some(reason));
775 assert_eq!(serde_json::to_value(reason).unwrap(), reason.as_str());
776 }
777 for kind in SubjectKind::ALL {
778 assert_eq!(SubjectKind::parse(kind.as_str()), Some(kind));
779 }
780 for level in WatchLevel::ALL {
781 assert_eq!(WatchLevel::parse(level.as_str()), Some(level));
782 }
Inbox: the events service tells people what needs them as events arrive783 assert_eq!(serde_json::to_value(InboxMark::Unsave).unwrap(), "unsave");
784 }
Inbox: threads, reasons, subscriptions and watching785
786 #[test]
787 fn what_is_asked_of_a_person_outranks_what_they_follow() {
788 assert!(Reason::Agent.rank() < Reason::Mention.rank());
789 assert!(Reason::Mention.rank() < Reason::Author.rank());
790 assert!(Reason::Author.rank() < Reason::Subscribed.rank());
791 assert!(Reason::ReviewRequested.direct() && !Reason::Comment.direct());
792 assert!(!Reason::Subscribed.participating() && Reason::Author.participating());
793 assert!(Severity::Warning.urgency() < Severity::Error.urgency());
794 }
795
796 #[test]
797 fn a_thread_carries_its_item_flat() {
798 let thread = InboxThread {
799 item: InboxItem {
800 id: "ntf_1".into(),
801 reason: Reason::Mention,
802 severity: Severity::Info,
803 title: "t".into(),
804 body: "b".into(),
805 event: None,
806 repo: None,
807 workspace: None,
808 subject: None,
809 number: None,
810 url: "/inbox".into(),
811 actor: None,
812 count: 1,
813 created_at: "2026-10-07T12:00:00.000Z".into(),
814 updated_at: "2026-10-07T12:00:00.000Z".into(),
815 read_at: None,
816 done_at: None,
817 saved: false,
818 snoozed_until: None,
819 },
820 activity: Vec::new(),
821 subscription: None,
822 };
823 let value = serde_json::to_value(&thread).unwrap();
824 assert_eq!(value["id"], "ntf_1");
825 assert_eq!(value["reason"], "mention");
826 assert!(value["activity"].is_array());
827 }
Inbox: the events service tells people what needs them as events arrive828}

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