g1t/crates/contracts/src/integrations.rs

531 lines18,260 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}
338
339// --- Methods -----------------------------------------------------------------
340
341/// `list`. Returns `Outcome<Vec<Connection>>`. Members only.
342#[derive(Debug, Serialize, Deserialize)]
343pub struct ListArgs {
344 pub workspace: String,
345 pub viewer: Viewer,
346}
347
348/// `connect`. Returns `Outcome<Connected>`. Owners only.
349#[derive(Debug, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct ConnectArgs {
352 pub actor: User,
353 pub workspace: String,
354 pub provider: Provider,
355 #[serde(default)]
356 pub name: Option<String>,
357 #[serde(default)]
358 pub config: ConnectionConfig,
359 /// The API key or token g1t uses to call it.
360 #[serde(default)]
361 pub secret: Option<String>,
362 /// What it signs its requests to g1t with: Sentry's client secret.
363 /// Made by g1t for Datadog and webhooks, and shown once.
364 #[serde(default)]
365 pub signing_secret: Option<String>,
366}
367
368#[derive(Clone, Debug, Serialize, Deserialize)]
369#[serde(rename_all = "camelCase")]
370pub struct Connected {
371 pub connection: Connection,
372 /// A signing secret g1t made, shown this once.
373 pub signing_secret: Option<String>,
374}
375
376/// `update`: only the fields given change. Returns `Outcome<Connection>`.
377/// Owners only.
378#[derive(Debug, Serialize, Deserialize)]
379#[serde(rename_all = "camelCase")]
380pub struct UpdateArgs {
381 pub actor: User,
382 pub workspace: String,
383 pub id: String,
384 #[serde(default)]
385 pub name: Option<String>,
386 #[serde(default)]
387 pub config: Option<ConnectionConfig>,
388 #[serde(default)]
389 pub secret: Option<String>,
390 #[serde(default)]
391 pub signing_secret: Option<String>,
392}
393
394/// `disconnect` and `test`. `disconnect` returns `Outcome<bool>`; `test`
395/// returns `Outcome<Tested>`. Owners only.
396#[derive(Debug, Serialize, Deserialize)]
397pub struct ConnectionArgs {
398 pub actor: User,
399 pub workspace: String,
400 pub id: String,
401}
402
403#[derive(Clone, Debug, Serialize, Deserialize)]
404pub struct Tested {
405 pub ok: bool,
406 pub message: String,
407}
408
409/// `deliveries`: the latest requests a connection received, newest first.
410/// Returns `Outcome<Vec<Delivery>>`. Members only.
411#[derive(Debug, Serialize, Deserialize)]
412pub struct DeliveriesArgs {
413 pub workspace: String,
414 pub viewer: Viewer,
415 pub id: String,
416}
417
418/// `receive`: a request an outside system sent to a connection's address.
419/// Returns `Received`. Anyone can send one; only a signed one is acted on.
420#[derive(Debug, Serialize, Deserialize)]
421pub struct ReceiveArgs {
422 pub id: String,
423 /// Header names in lowercase.
424 pub headers: std::collections::HashMap<String, String>,
425 pub body: String,
426}
427
428#[derive(Clone, Debug, Serialize, Deserialize)]
429pub struct Received {
430 /// The HTTP status to answer with.
431 pub status: u16,
432 pub message: String,
433}
434
435/// `resolve`: fetches one outside reference. Returns `Outcome<ContextItem>`.
436/// Members of the workspace only.
437#[derive(Debug, Serialize, Deserialize)]
438pub struct ResolveArgs {
439 pub workspace: String,
440 pub viewer: Viewer,
441 /// `TECH-1234`, or a Jira, Linear or Sentry address.
442 pub reference: String,
443}
444
445/// `references`: every outside reference in `text` that one of the
446/// workspace's connections answers for, fetched. Returns `Vec<ContextItem>`.
447/// For g1t's own agents, about work in that workspace.
448#[derive(Debug, Serialize, Deserialize)]
449pub struct ReferencesArgs {
450 pub workspace: String,
451 pub text: String,
452 #[serde(default)]
453 pub limit: Option<u32>,
454}
455
456/// `import`: opens an issue from a ticket. Returns `Outcome<Imported>`.
457#[derive(Debug, Serialize, Deserialize)]
458pub struct ImportArgs {
459 pub actor: User,
460 pub repo: RepoPath,
461 pub reference: String,
462 /// Put a g1t agent on it.
463 #[serde(default)]
464 pub assign: bool,
465}
466
467#[derive(Clone, Debug, Serialize, Deserialize)]
468pub struct Imported {
469 pub number: u32,
470 pub item: ContextItem,
471 /// False when the ticket had been imported already, and `number` is
472 /// that issue.
473 pub created: bool,
474}
475
476/// `links`: what an issue is tied to outside g1t. Returns `Vec<Link>`.
477/// Callers must have checked the viewer may see the issue.
478#[derive(Debug, Serialize, Deserialize)]
479#[serde(rename_all = "camelCase")]
480pub struct LinksArgs {
481 pub repo: RepoPath,
482 pub number: u32,
483}
484
485/// `open_model_session`: where one run's model requests go, by the
486/// workspace's routes. Returns `Outcome<ModelSession>`: a failure, with the
487/// reason to show, when the route goes nowhere it can use.
488#[derive(Debug, Serialize, Deserialize)]
489#[serde(rename_all = "camelCase")]
490pub struct OpenModelSessionArgs {
491 pub workspace: String,
492 pub repo: RepoPath,
493 pub number: u32,
494 pub task: String,
495 /// Whether g1t's hosted models are open to the workspace. The runner
496 /// decides that; this service only follows the routes.
497 #[serde(default = "yes")]
498 pub hosted_open: bool,
499}
500
501/// `routes`: a workspace's model routes, one per kind of work that has its
502/// own. Returns `Outcome<Vec<ModelRoute>>`. Members only.
503#[derive(Debug, Serialize, Deserialize)]
504pub struct RoutesArgs {
505 pub workspace: String,
506 pub viewer: Viewer,
507}
508
509/// `set_routes`: replaces a workspace's model routes. A kind of work left
510/// out follows `default`; with no `default`, g1t's hosted models where they
511/// are open. Returns `Outcome<Vec<ModelRoute>>`. Owners only.
512#[derive(Debug, Serialize, Deserialize)]
513pub struct SetRoutesArgs {
514 pub actor: User,
515 pub workspace: String,
516 pub routes: Vec<ModelRoute>,
517}
518
519/// `model_upstream`: what a model session's token stands for, or null when
520/// it is unknown or expired. Returns `Option<ModelUpstream>`.
521#[derive(Debug, Serialize, Deserialize)]
522pub struct ModelUpstreamArgs {
523 pub token: String,
524}
525
526/// `model_provider`: the workspace's own model connection, if it has one.
527/// Returns `Option<Connection>`.
528#[derive(Debug, Serialize, Deserialize)]
529pub struct ModelProviderArgs {
530 pub workspace: String,
531}