pr_01m47d15m3e54sn21z27rpy5n9/crates/contracts/src/integrations.rs

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