flagon-io/g1t

public

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/crates/contracts/src/integrations.rs

537 lines18,562 bytesCodeBlame
1//! The integrations service: a workspace's connections to systems outside
2//! g1t, and everything that crosses between them.
3//!
4//! - **Models.** A workspace connects as many model providers as it uses
5//! (Anthropic, OpenAI, Gemini, and anything compatible with either API)
6//! and routes each kind of work to one of them, or to g1t's hosted models.
7//! Sandboxes never hold a key: they hold a token for one run, and the
8//! model proxy puts the credentials on each request, translating to
9//! OpenAI's API where the provider speaks it.
10//! - **Alerts.** Sentry, Datadog or any signed webhook opens an issue in a
11//! repository, once per problem however often it fires, and can put an
12//! agent on it.
13//! - **Trackers.** A Jira or Linear key, such as `TECH-1234`, resolves to the
14//! ticket: agents read it, people import it as an issue, and when the work
15//! lands the ticket is told.
16//!
17//! Mirrors `packages/contracts/src/integrations.ts`.
18
19use serde::{Deserialize, Serialize};
20
21use crate::repos::RepoPath;
22use crate::{User, Viewer};
23
24/// Which outside system a connection is to.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum Provider {
28 /// The workspace's own Anthropic API key.
29 Anthropic,
30 /// Any endpoint that speaks Anthropic's Messages API: the workspace's
31 /// own Cloudflare AI Gateway, LiteLLM, a proxy in front of Bedrock or
32 /// Vertex, or a self-hosted model.
33 AnthropicEndpoint,
34 /// The workspace's own OpenAI API key.
35 Openai,
36 /// The workspace's own Google Gemini API key, through Gemini's
37 /// OpenAI-compatible endpoint.
38 Gemini,
39 /// Any endpoint that speaks OpenAI's Chat Completions API: Azure
40 /// OpenAI, OpenRouter, Groq, Together, vLLM, Ollama.
41 OpenaiEndpoint,
42 Sentry,
43 Datadog,
44 /// Anything that can send a signed JSON request.
45 Webhook,
46 Jira,
47 Linear,
48}
49
50impl Provider {
51 pub const ALL: [Provider; 10] = [
52 Provider::Anthropic,
53 Provider::AnthropicEndpoint,
54 Provider::Openai,
55 Provider::Gemini,
56 Provider::OpenaiEndpoint,
57 Provider::Sentry,
58 Provider::Datadog,
59 Provider::Webhook,
60 Provider::Jira,
61 Provider::Linear,
62 ];
63
64 pub fn name(self) -> &'static str {
65 match self {
66 Provider::Anthropic => "anthropic",
67 Provider::AnthropicEndpoint => "anthropic_endpoint",
68 Provider::Openai => "openai",
69 Provider::Gemini => "gemini",
70 Provider::OpenaiEndpoint => "openai_endpoint",
71 Provider::Sentry => "sentry",
72 Provider::Datadog => "datadog",
73 Provider::Webhook => "webhook",
74 Provider::Jira => "jira",
75 Provider::Linear => "linear",
76 }
77 }
78
79 pub fn parse(name: &str) -> Option<Provider> {
80 Provider::ALL.into_iter().find(|provider| provider.name() == name)
81 }
82
83 /// What people call it.
84 pub fn label(self) -> &'static str {
85 match self {
86 Provider::Anthropic => "Anthropic",
87 Provider::AnthropicEndpoint => "Anthropic-compatible endpoint",
88 Provider::Openai => "OpenAI",
89 Provider::Gemini => "Google Gemini",
90 Provider::OpenaiEndpoint => "OpenAI-compatible endpoint",
91 Provider::Sentry => "Sentry",
92 Provider::Datadog => "Datadog",
93 Provider::Webhook => "Webhook",
94 Provider::Jira => "Jira",
95 Provider::Linear => "Linear",
96 }
97 }
98
99 pub fn kind(self) -> ProviderKind {
100 match self {
101 Provider::Anthropic
102 | Provider::AnthropicEndpoint
103 | Provider::Openai
104 | Provider::Gemini
105 | Provider::OpenaiEndpoint => ProviderKind::Models,
106 Provider::Sentry | Provider::Datadog | Provider::Webhook => ProviderKind::Alerts,
107 Provider::Jira | Provider::Linear => ProviderKind::Tracker,
108 }
109 }
110
111 /// Whether it sends g1t requests, at the connection's own address.
112 pub fn receives(self) -> bool {
113 matches!(self, Provider::Sentry | Provider::Datadog | Provider::Webhook)
114 }
115
116 /// For a model provider, the API it speaks: `anthropic` or `openai`.
117 pub fn api(self) -> &'static str {
118 match self {
119 Provider::Anthropic | Provider::AnthropicEndpoint => "anthropic",
120 _ => "openai",
121 }
122 }
123}
124
125/// The kinds of work a model is chosen for, and `default` for the rest.
126pub const MODEL_TASKS: [&str; 5] = ["default", "implement", "review", "plan", "update"];
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "snake_case")]
130pub enum ProviderKind {
131 /// Where agents' model requests go. A workspace can have several and
132 /// routes each kind of work to one.
133 Models,
134 /// Problems that become issues.
135 Alerts,
136 /// Tickets that agents read and people import.
137 Tracker,
138}
139
140/// A connection's settings: everything about it except its secrets. Each
141/// provider uses the fields that apply to it.
142#[derive(Clone, Debug, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct ConnectionConfig {
145 /// For alerts: the repository issues are opened in, `owner/name`. For a
146 /// tracker: where an imported ticket goes when no repository is named.
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub repo: Option<String>,
149 /// For alerts: put a g1t agent on each issue opened.
150 #[serde(default)]
151 pub assign: bool,
152 /// For alerts: the label put on each issue opened. `bug` when unset.
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub label: Option<String>,
155 /// Tell the outside system when the work lands: resolve the Sentry
156 /// issue, comment on the ticket.
157 #[serde(default = "yes")]
158 pub write_back: bool,
159 /// Sentry: the organization's slug.
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub organization: Option<String>,
162 /// The system's address, for Jira (`https://acme.atlassian.net`) or a
163 /// Sentry that is not sentry.io.
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub site: Option<String>,
166 /// Jira: the account the API token belongs to.
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub email: Option<String>,
169 /// Jira project keys or Linear team keys this connection answers for,
170 /// such as `TECH`. Empty answers for every key.
171 #[serde(default, skip_serializing_if = "Vec::is_empty")]
172 pub keys: Vec<String>,
173 /// Your own endpoint: its base URL, without `/v1`.
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub base_url: Option<String>,
176 /// Your own endpoint: send the key as `x-api-key` (the default) or as
177 /// `authorization: Bearer`.
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub auth_header: Option<String>,
180 /// Models: the model used when a route to this connection names none.
181 /// Required for providers that speak OpenAI's API; for Anthropic, g1t's
182 /// choice for the kind of work when unset.
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub model: Option<String>,
185}
186
187fn yes() -> bool {
188 true
189}
190
191impl Default for ConnectionConfig {
192 fn default() -> Self {
193 ConnectionConfig {
194 repo: None,
195 assign: false,
196 label: None,
197 write_back: true,
198 organization: None,
199 site: None,
200 email: None,
201 keys: Vec::new(),
202 base_url: None,
203 auth_header: None,
204 model: None,
205 }
206 }
207}
208
209/// A connection, as anyone in the workspace sees it. Secrets are never
210/// shown after they are saved; `secretHint` is enough to tell keys apart.
211#[derive(Clone, Debug, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct Connection {
214 pub id: String,
215 pub workspace: String,
216 pub provider: Provider,
217 pub kind: ProviderKind,
218 pub name: String,
219 pub config: ConnectionConfig,
220 /// The last four characters of the saved key, such as `…3f9a`.
221 pub secret_hint: Option<String>,
222 /// For a provider that sends g1t requests: where it sends them.
223 pub webhook_url: Option<String>,
224 pub created_by: String,
225 /// RFC 3339.
226 pub created_at: String,
227 pub last_used_at: Option<String>,
228 /// The last thing that went wrong talking to it, until it next works.
229 pub last_error: Option<String>,
230 /// For a model provider: the models it offered when last checked.
231 #[serde(default)]
232 pub models: Vec<String>,
233}
234
235/// Where one kind of work's model requests go in a workspace.
236#[derive(Clone, Debug, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct ModelRoute {
239 /// One of [`MODEL_TASKS`].
240 pub task: String,
241 /// The workspace's own model connection, or `None` for g1t's hosted
242 /// models.
243 pub connection_id: Option<String>,
244 /// The model at that connection; its default model when `None`.
245 pub model: Option<String>,
246}
247
248/// One request an outside system sent, and what g1t did with it.
249#[derive(Clone, Debug, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct Delivery {
252 pub id: String,
253 /// RFC 3339.
254 pub received_at: String,
255 /// What it was about, in the sender's terms: `issue.created`.
256 pub event: String,
257 /// `opened`, `updated`, `reopened`, `ignored` or `refused`.
258 pub outcome: String,
259 pub detail: String,
260 /// The issue it opened or updated, `owner/name#number`.
261 pub issue: Option<String>,
262}
263
264/// Something outside g1t, fetched as it is now. Its text was written outside
265/// g1t, so it is reference material and never instructions.
266#[derive(Clone, Debug, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct ContextItem {
269 pub provider: Provider,
270 /// `TECH-1234`, or the Sentry issue's short id.
271 pub key: String,
272 pub title: String,
273 pub url: String,
274 /// Its status in that system: `In Progress`, `unresolved`.
275 pub status: Option<String>,
276 /// Its description, as plain text, shortened if long.
277 pub body: String,
278 /// RFC 3339: when g1t fetched it.
279 pub fetched_at: String,
280}
281
282/// An issue's tie to something outside g1t.
283#[derive(Clone, Debug, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct Link {
286 pub provider: Provider,
287 pub connection_id: String,
288 pub key: String,
289 pub title: String,
290 pub url: String,
291 /// How many times an alert has fired for it.
292 pub count: u32,
293 /// RFC 3339.
294 pub first_seen: String,
295 pub last_seen: String,
296}
297
298/// Where a workspace's agents' model requests go.
299#[derive(Clone, Debug, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct ModelSession {
302 /// What the sandbox sends instead of a key. Lives as long as one run.
303 pub token: String,
304 /// `g1t` when g1t pays the provider and charges the workspace,
305 /// `workspace` when the workspace's own account does.
306 pub billed_to: String,
307 /// The connection's name, when it is the workspace's own.
308 pub provider_name: Option<String>,
309 /// The model to use instead of g1t's choice, if the connection names one.
310 pub model: Option<String>,
311}
312
313/// What the model proxy needs to forward one run's requests.
314#[derive(Clone, Debug, Serialize, Deserialize)]
315#[serde(rename_all = "camelCase")]
316pub struct ModelUpstream {
317 /// `g1t`, `anthropic` or `endpoint`.
318 pub route: String,
319 /// The API the provider speaks: `anthropic` or `openai`, which the proxy
320 /// translates to.
321 pub api: String,
322 /// The model every request of the run is sent to, when the route names
323 /// one.
324 pub model: Option<String>,
325 /// For `openai`: OpenAI's own API, which shapes requests its own way.
326 pub official: bool,
327 pub workspace: String,
328 pub repo: String,
329 pub number: u32,
330 pub task: String,
331 /// For `endpoint`: where to send requests.
332 pub base_url: Option<String>,
333 /// For `anthropic` and `endpoint`: the workspace's key.
334 pub api_key: Option<String>,
335 /// `x-api-key` or `authorization`.
336 pub auth_header: Option<String>,
337 /// For an endpoint behind an authenticated Cloudflare AI Gateway: the
338 /// gateway's own token, sent as `cf-aig-authorization`.
339 #[serde(default)]
340 pub gateway_token: Option<String>,
341}
342
343// --- Methods -----------------------------------------------------------------
344
345/// `list`. Returns `Outcome<Vec<Connection>>`. Members only.
346#[derive(Debug, Serialize, Deserialize)]
347pub struct ListArgs {
348 pub workspace: String,
349 pub viewer: Viewer,
350}
351
352/// `connect`. Returns `Outcome<Connected>`. Owners only.
353#[derive(Debug, Serialize, Deserialize)]
354#[serde(rename_all = "camelCase")]
355pub struct ConnectArgs {
356 pub actor: User,
357 pub workspace: String,
358 pub provider: Provider,
359 #[serde(default)]
360 pub name: Option<String>,
361 #[serde(default)]
362 pub config: ConnectionConfig,
363 /// The API key or token g1t uses to call it.
364 #[serde(default)]
365 pub secret: Option<String>,
366 /// What it signs its requests to g1t with: Sentry's client secret.
367 /// Made by g1t for Datadog and webhooks, and shown once. For a model
368 /// endpoint behind an authenticated Cloudflare AI Gateway, the gateway's
369 /// token.
370 #[serde(default)]
371 pub signing_secret: Option<String>,
372}
373
374#[derive(Clone, Debug, Serialize, Deserialize)]
375#[serde(rename_all = "camelCase")]
376pub struct Connected {
377 pub connection: Connection,
378 /// A signing secret g1t made, shown this once.
379 pub signing_secret: Option<String>,
380}
381
382/// `update`: only the fields given change. Returns `Outcome<Connection>`.
383/// Owners only.
384#[derive(Debug, Serialize, Deserialize)]
385#[serde(rename_all = "camelCase")]
386pub struct UpdateArgs {
387 pub actor: User,
388 pub workspace: String,
389 pub id: String,
390 #[serde(default)]
391 pub name: Option<String>,
392 #[serde(default)]
393 pub config: Option<ConnectionConfig>,
394 #[serde(default)]
395 pub secret: Option<String>,
396 #[serde(default)]
397 pub signing_secret: Option<String>,
398}
399
400/// `disconnect` and `test`. `disconnect` returns `Outcome<bool>`; `test`
401/// returns `Outcome<Tested>`. Owners only.
402#[derive(Debug, Serialize, Deserialize)]
403pub struct ConnectionArgs {
404 pub actor: User,
405 pub workspace: String,
406 pub id: String,
407}
408
409#[derive(Clone, Debug, Serialize, Deserialize)]
410pub struct Tested {
411 pub ok: bool,
412 pub message: String,
413}
414
415/// `deliveries`: the latest requests a connection received, newest first.
416/// Returns `Outcome<Vec<Delivery>>`. Members only.
417#[derive(Debug, Serialize, Deserialize)]
418pub struct DeliveriesArgs {
419 pub workspace: String,
420 pub viewer: Viewer,
421 pub id: String,
422}
423
424/// `receive`: a request an outside system sent to a connection's address.
425/// Returns `Received`. Anyone can send one; only a signed one is acted on.
426#[derive(Debug, Serialize, Deserialize)]
427pub struct ReceiveArgs {
428 pub id: String,
429 /// Header names in lowercase.
430 pub headers: std::collections::HashMap<String, String>,
431 pub body: String,
432}
433
434#[derive(Clone, Debug, Serialize, Deserialize)]
435pub struct Received {
436 /// The HTTP status to answer with.
437 pub status: u16,
438 pub message: String,
439}
440
441/// `resolve`: fetches one outside reference. Returns `Outcome<ContextItem>`.
442/// Members of the workspace only.
443#[derive(Debug, Serialize, Deserialize)]
444pub struct ResolveArgs {
445 pub workspace: String,
446 pub viewer: Viewer,
447 /// `TECH-1234`, or a Jira, Linear or Sentry address.
448 pub reference: String,
449}
450
451/// `references`: every outside reference in `text` that one of the
452/// workspace's connections answers for, fetched. Returns `Vec<ContextItem>`.
453/// For g1t's own agents, about work in that workspace.
454#[derive(Debug, Serialize, Deserialize)]
455pub struct ReferencesArgs {
456 pub workspace: String,
457 pub text: String,
458 #[serde(default)]
459 pub limit: Option<u32>,
460}
461
462/// `import`: opens an issue from a ticket. Returns `Outcome<Imported>`.
463#[derive(Debug, Serialize, Deserialize)]
464pub struct ImportArgs {
465 pub actor: User,
466 pub repo: RepoPath,
467 pub reference: String,
468 /// Put a g1t agent on it.
469 #[serde(default)]
470 pub assign: bool,
471}
472
473#[derive(Clone, Debug, Serialize, Deserialize)]
474pub struct Imported {
475 pub number: u32,
476 pub item: ContextItem,
477 /// False when the ticket had been imported already, and `number` is
478 /// that issue.
479 pub created: bool,
480}
481
482/// `links`: what an issue is tied to outside g1t. Returns `Vec<Link>`.
483/// Callers must have checked the viewer may see the issue.
484#[derive(Debug, Serialize, Deserialize)]
485#[serde(rename_all = "camelCase")]
486pub struct LinksArgs {
487 pub repo: RepoPath,
488 pub number: u32,
489}
490
491/// `open_model_session`: where one run's model requests go, by the
492/// workspace's routes. Returns `Outcome<ModelSession>`: a failure, with the
493/// reason to show, when the route goes nowhere it can use.
494#[derive(Debug, Serialize, Deserialize)]
495#[serde(rename_all = "camelCase")]
496pub struct OpenModelSessionArgs {
497 pub workspace: String,
498 pub repo: RepoPath,
499 pub number: u32,
500 pub task: String,
501 /// Whether g1t's hosted models are open to the workspace. The runner
502 /// decides that; this service only follows the routes.
503 #[serde(default = "yes")]
504 pub hosted_open: bool,
505}
506
507/// `routes`: a workspace's model routes, one per kind of work that has its
508/// own. Returns `Outcome<Vec<ModelRoute>>`. Members only.
509#[derive(Debug, Serialize, Deserialize)]
510pub struct RoutesArgs {
511 pub workspace: String,
512 pub viewer: Viewer,
513}
514
515/// `set_routes`: replaces a workspace's model routes. A kind of work left
516/// out follows `default`; with no `default`, g1t's hosted models where they
517/// are open. Returns `Outcome<Vec<ModelRoute>>`. Owners only.
518#[derive(Debug, Serialize, Deserialize)]
519pub struct SetRoutesArgs {
520 pub actor: User,
521 pub workspace: String,
522 pub routes: Vec<ModelRoute>,
523}
524
525/// `model_upstream`: what a model session's token stands for, or null when
526/// it is unknown or expired. Returns `Option<ModelUpstream>`.
527#[derive(Debug, Serialize, Deserialize)]
528pub struct ModelUpstreamArgs {
529 pub token: String,
530}
531
532/// `model_provider`: the workspace's own model connection, if it has one.
533/// Returns `Option<Connection>`.
534#[derive(Debug, Serialize, Deserialize)]
535pub struct ModelProviderArgs {
536 pub workspace: String,
537}