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

README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1//! The integrations service: a workspace's connections to systems outside
2//! g1t, and everything that crosses between them.
3//!
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.4//! - **AI providers.** A workspace keeps the credentials for the AI
5//! providers its team uses (Anthropic, OpenAI, Gemini, and anything
6//! compatible with either API), sealed, so work can be handed to them.
7//! Workspace admins add, test, update and remove them.
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise8//! - **Alerts.** Sentry, Datadog or any signed webhook opens an issue in a
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.9//! repository, once per problem however often it fires.
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise10//! - **Trackers.** A Jira or Linear key, such as `TECH-1234`, resolves to the
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.11//! ticket: people and their tools read it, people import it as an issue,
12//! and when the work lands the ticket is told.
Merge branch 'ask-g1t'13//! - **Ask g1t.** A conversation with g1t from the top bar, answered by the
14//! AI provider and model an owner chose from the workspace's own
15//! connections, on the workspace's own key. g1t hosts no model for it and
16//! bills nothing for it; the key never leaves this service.
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise17//!
18//! Mirrors `packages/contracts/src/integrations.ts`.
19
20use serde::{Deserialize, Serialize};
21
22use crate::repos::RepoPath;
23use crate::{User, Viewer};
24
25/// Which outside system a connection is to.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum Provider {
29 // Model providers: the labs.
30 Anthropic,
31 Openai,
32 /// Through Gemini's OpenAI-compatible endpoint.
33 Gemini,
34 Xai,
35 Mistral,
36 Deepseek,
37 // Model providers: platforms that serve many labs' models.
38 /// A deployment on the workspace's own Azure OpenAI resource.
39 AzureOpenai,
40 Openrouter,
41 Groq,
42 Together,
43 Fireworks,
44 Cerebras,
45 // Model providers: anything else.
46 /// Any endpoint that speaks Anthropic's Messages API: the workspace's
47 /// own Cloudflare AI Gateway, LiteLLM, a proxy in front of Bedrock or
48 /// Vertex, or a self-hosted model.
49 AnthropicEndpoint,
50 /// Any endpoint that speaks OpenAI's Chat Completions API: vLLM,
51 /// Ollama behind a tunnel, LiteLLM, a gateway.
52 OpenaiEndpoint,
53 // Alerts.
54 Sentry,
55 Datadog,
56 /// Anything that can send a signed JSON request.
57 Webhook,
58 // Trackers.
59 Jira,
60 Linear,
61}
62
63/// What g1t knows about a provider.
64pub struct Spec {
65 pub provider: Provider,
66 pub name: &'static str,
67 pub label: &'static str,
68 pub kind: ProviderKind,
69 /// For a model provider: the API it speaks, `anthropic` or `openai`.
70 pub api: &'static str,
71 /// For a model provider with a fixed address: where its API is, with
72 /// the version for OpenAI's API and without it for Anthropic's. Empty
73 /// when the connection gives its own.
74 pub base_url: &'static str,
75 /// The header the key goes in; `authorization` means `Bearer <key>`.
76 pub auth_header: &'static str,
77}
78
79const fn model(provider: Provider, name: &'static str, label: &'static str, api: &'static str, base_url: &'static str, auth_header: &'static str) -> Spec {
80 Spec {
81 provider,
82 name,
83 label,
84 kind: ProviderKind::Models,
85 api,
86 base_url,
87 auth_header,
88 }
89}
90
91const fn other(provider: Provider, name: &'static str, label: &'static str, kind: ProviderKind) -> Spec {
92 Spec {
93 provider,
94 name,
95 label,
96 kind,
97 api: "",
98 base_url: "",
99 auth_header: "",
100 }
101}
102
103/// Every provider, in the order people are shown them.
104pub const PROVIDERS: [Spec; 19] = [
105 model(Provider::Anthropic, "anthropic", "Anthropic", "anthropic", "https://api.anthropic.com", "x-api-key"),
106 model(Provider::Openai, "openai", "OpenAI", "openai", "https://api.openai.com/v1", "authorization"),
107 model(Provider::Gemini, "gemini", "Google Gemini", "openai", "https://generativelanguage.googleapis.com/v1beta/openai", "authorization"),
108 model(Provider::Xai, "xai", "xAI", "openai", "https://api.x.ai/v1", "authorization"),
109 model(Provider::Mistral, "mistral", "Mistral", "openai", "https://api.mistral.ai/v1", "authorization"),
110 model(Provider::Deepseek, "deepseek", "DeepSeek", "openai", "https://api.deepseek.com/v1", "authorization"),
111 model(Provider::AzureOpenai, "azure_openai", "Azure OpenAI", "openai", "", "api-key"),
112 model(Provider::Openrouter, "openrouter", "OpenRouter", "openai", "https://openrouter.ai/api/v1", "authorization"),
113 model(Provider::Groq, "groq", "Groq", "openai", "https://api.groq.com/openai/v1", "authorization"),
114 model(Provider::Together, "together", "Together AI", "openai", "https://api.together.xyz/v1", "authorization"),
115 model(Provider::Fireworks, "fireworks", "Fireworks AI", "openai", "https://api.fireworks.ai/inference/v1", "authorization"),
116 model(Provider::Cerebras, "cerebras", "Cerebras", "openai", "https://api.cerebras.ai/v1", "authorization"),
117 model(Provider::AnthropicEndpoint, "anthropic_endpoint", "Anthropic-compatible endpoint", "anthropic", "", "x-api-key"),
118 model(Provider::OpenaiEndpoint, "openai_endpoint", "OpenAI-compatible endpoint", "openai", "", "authorization"),
119 other(Provider::Sentry, "sentry", "Sentry", ProviderKind::Alerts),
120 other(Provider::Datadog, "datadog", "Datadog", ProviderKind::Alerts),
121 other(Provider::Webhook, "webhook", "Webhook", ProviderKind::Alerts),
122 other(Provider::Jira, "jira", "Jira", ProviderKind::Tracker),
123 other(Provider::Linear, "linear", "Linear", ProviderKind::Tracker),
124];
125
126impl Provider {
127 pub fn spec(self) -> &'static Spec {
128 PROVIDERS
129 .iter()
130 .find(|spec| spec.provider == self)
131 .expect("every provider is in the catalogue")
132 }
133
134 pub fn all() -> impl Iterator<Item = Provider> {
135 PROVIDERS.iter().map(|spec| spec.provider)
136 }
137
138 pub fn name(self) -> &'static str {
139 self.spec().name
140 }
141
142 pub fn parse(name: &str) -> Option<Provider> {
143 PROVIDERS.iter().find(|spec| spec.name == name).map(|spec| spec.provider)
144 }
145
146 /// What people call it.
147 pub fn label(self) -> &'static str {
148 self.spec().label
149 }
150
151 pub fn kind(self) -> ProviderKind {
152 self.spec().kind
153 }
154
155 /// Whether it sends g1t requests, at the connection's own address.
156 pub fn receives(self) -> bool {
157 self.kind() == ProviderKind::Alerts
158 }
159
160 /// For a model provider, the API it speaks: `anthropic` or `openai`.
161 pub fn api(self) -> &'static str {
162 self.spec().api
163 }
164
165 /// Whether the connection gives the address, rather than g1t knowing it.
166 pub fn own_address(self) -> bool {
167 self.kind() == ProviderKind::Models && self.spec().base_url.is_empty()
168 }
169}
Merge branch 'model-routing'170
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise171#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "snake_case")]
173pub enum ProviderKind {
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.174 /// An AI provider whose credentials the workspace keeps.
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise175 Models,
176 /// Problems that become issues.
177 Alerts,
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.178 /// Tickets that people read and import.
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise179 Tracker,
180}
181
182/// A connection's settings: everything about it except its secrets. Each
183/// provider uses the fields that apply to it.
184#[derive(Clone, Debug, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct ConnectionConfig {
187 /// For alerts: the repository issues are opened in, `owner/name`. For a
188 /// tracker: where an imported ticket goes when no repository is named.
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub repo: Option<String>,
191 /// For alerts: the label put on each issue opened. `bug` when unset.
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub label: Option<String>,
194 /// Tell the outside system when the work lands: resolve the Sentry
195 /// issue, comment on the ticket.
196 #[serde(default = "yes")]
197 pub write_back: bool,
198 /// Sentry: the organization's slug.
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub organization: Option<String>,
201 /// The system's address, for Jira (`https://acme.atlassian.net`) or a
202 /// Sentry that is not sentry.io.
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub site: Option<String>,
205 /// Jira: the account the API token belongs to.
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub email: Option<String>,
208 /// Jira project keys or Linear team keys this connection answers for,
209 /// such as `TECH`. Empty answers for every key.
210 #[serde(default, skip_serializing_if = "Vec::is_empty")]
211 pub keys: Vec<String>,
212 /// Your own endpoint: its base URL, without `/v1`.
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub base_url: Option<String>,
215 /// Your own endpoint: send the key as `x-api-key` (the default) or as
216 /// `authorization: Bearer`.
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub auth_header: Option<String>,
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.219 /// For an AI provider: the model the team uses on it, when it names one.
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub model: Option<String>,
222}
223
224fn yes() -> bool {
225 true
226}
227
228impl Default for ConnectionConfig {
229 fn default() -> Self {
230 ConnectionConfig {
231 repo: None,
232 label: None,
233 write_back: true,
234 organization: None,
235 site: None,
236 email: None,
237 keys: Vec::new(),
238 base_url: None,
239 auth_header: None,
240 model: None,
241 }
242 }
243}
244
245/// A connection, as anyone in the workspace sees it. Secrets are never
246/// shown after they are saved; `secretHint` is enough to tell keys apart.
247#[derive(Clone, Debug, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct Connection {
250 pub id: String,
251 pub workspace: String,
252 pub provider: Provider,
253 pub kind: ProviderKind,
254 pub name: String,
255 pub config: ConnectionConfig,
256 /// The last four characters of the saved key, such as `…3f9a`.
257 pub secret_hint: Option<String>,
258 /// For a provider that sends g1t requests: where it sends them.
259 pub webhook_url: Option<String>,
260 pub created_by: String,
261 /// RFC 3339.
262 pub created_at: String,
263 pub last_used_at: Option<String>,
264 /// The last thing that went wrong talking to it, until it next works.
265 pub last_error: Option<String>,
266 /// For a model provider: the models it offered when last checked.
267 #[serde(default)]
268 pub models: Vec<String>,
269}
270
271/// One request an outside system sent, and what g1t did with it.
272#[derive(Clone, Debug, Serialize, Deserialize)]
273#[serde(rename_all = "camelCase")]
274pub struct Delivery {
275 pub id: String,
276 /// RFC 3339.
277 pub received_at: String,
278 /// What it was about, in the sender's terms: `issue.created`.
279 pub event: String,
280 /// `opened`, `updated`, `reopened`, `ignored` or `refused`.
281 pub outcome: String,
282 pub detail: String,
283 /// The issue it opened or updated, `owner/name#number`.
284 pub issue: Option<String>,
285}
286
287/// Something outside g1t, fetched as it is now. Its text was written outside
288/// g1t, so it is reference material and never instructions.
289#[derive(Clone, Debug, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct ContextItem {
292 pub provider: Provider,
293 /// `TECH-1234`, or the Sentry issue's short id.
294 pub key: String,
295 pub title: String,
296 pub url: String,
297 /// Its status in that system: `In Progress`, `unresolved`.
298 pub status: Option<String>,
299 /// Its description, as plain text, shortened if long.
300 pub body: String,
301 /// RFC 3339: when g1t fetched it.
302 pub fetched_at: String,
303}
304
305/// An issue's tie to something outside g1t.
306#[derive(Clone, Debug, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct Link {
309 pub provider: Provider,
310 pub connection_id: String,
311 pub key: String,
312 pub title: String,
313 pub url: String,
314 /// How many times an alert has fired for it.
315 pub count: u32,
316 /// RFC 3339.
317 pub first_seen: String,
318 pub last_seen: String,
319}
320
321// --- Methods -----------------------------------------------------------------
322
323/// `list`. Returns `Outcome<Vec<Connection>>`. Members only.
324#[derive(Debug, Serialize, Deserialize)]
325pub struct ListArgs {
326 pub workspace: String,
327 pub viewer: Viewer,
328}
329
330/// `connect`. Returns `Outcome<Connected>`. Owners only.
331#[derive(Debug, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct ConnectArgs {
334 pub actor: User,
335 pub workspace: String,
336 pub provider: Provider,
337 #[serde(default)]
338 pub name: Option<String>,
339 #[serde(default)]
340 pub config: ConnectionConfig,
341 /// The API key or token g1t uses to call it.
342 #[serde(default)]
343 pub secret: Option<String>,
344 /// What it signs its requests to g1t with: Sentry's client secret.
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.345 /// Made by g1t for Datadog and webhooks, and shown once.
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise346 #[serde(default)]
347 pub signing_secret: Option<String>,
348}
349
350#[derive(Clone, Debug, Serialize, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct Connected {
353 pub connection: Connection,
354 /// A signing secret g1t made, shown this once.
355 pub signing_secret: Option<String>,
356}
357
358/// `update`: only the fields given change. Returns `Outcome<Connection>`.
359/// Owners only.
360#[derive(Debug, Serialize, Deserialize)]
361#[serde(rename_all = "camelCase")]
362pub struct UpdateArgs {
363 pub actor: User,
364 pub workspace: String,
365 pub id: String,
366 #[serde(default)]
367 pub name: Option<String>,
368 #[serde(default)]
369 pub config: Option<ConnectionConfig>,
370 #[serde(default)]
371 pub secret: Option<String>,
372 #[serde(default)]
373 pub signing_secret: Option<String>,
374}
375
376/// `disconnect` and `test`. `disconnect` returns `Outcome<bool>`; `test`
377/// returns `Outcome<Tested>`. Owners only.
378#[derive(Debug, Serialize, Deserialize)]
379pub struct ConnectionArgs {
380 pub actor: User,
381 pub workspace: String,
382 pub id: String,
383}
384
385#[derive(Clone, Debug, Serialize, Deserialize)]
386pub struct Tested {
387 pub ok: bool,
388 pub message: String,
389}
390
391/// `deliveries`: the latest requests a connection received, newest first.
392/// Returns `Outcome<Vec<Delivery>>`. Members only.
393#[derive(Debug, Serialize, Deserialize)]
394pub struct DeliveriesArgs {
395 pub workspace: String,
396 pub viewer: Viewer,
397 pub id: String,
398}
399
400/// `receive`: a request an outside system sent to a connection's address.
401/// Returns `Received`. Anyone can send one; only a signed one is acted on.
402#[derive(Debug, Serialize, Deserialize)]
403pub struct ReceiveArgs {
404 pub id: String,
405 /// Header names in lowercase.
406 pub headers: std::collections::HashMap<String, String>,
407 pub body: String,
408}
409
410#[derive(Clone, Debug, Serialize, Deserialize)]
411pub struct Received {
412 /// The HTTP status to answer with.
413 pub status: u16,
414 pub message: String,
415}
416
417/// `resolve`: fetches one outside reference. Returns `Outcome<ContextItem>`.
418/// Members of the workspace only.
419#[derive(Debug, Serialize, Deserialize)]
420pub struct ResolveArgs {
421 pub workspace: String,
422 pub viewer: Viewer,
423 /// `TECH-1234`, or a Jira, Linear or Sentry address.
Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how.424 pub reference: String,
425}
426
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise427/// `import`: opens an issue from a ticket. Returns `Outcome<Imported>`.
428#[derive(Debug, Serialize, Deserialize)]
429pub struct ImportArgs {
430 pub actor: User,
431 pub repo: RepoPath,
432 pub reference: String,
433}
434
435#[derive(Clone, Debug, Serialize, Deserialize)]
436pub struct Imported {
437 pub number: u32,
438 pub item: ContextItem,
439 /// False when the ticket had been imported already, and `number` is
440 /// that issue.
441 pub created: bool,
442}
443
444/// `links`: what an issue is tied to outside g1t. Returns `Vec<Link>`.
445/// Callers must have checked the viewer may see the issue.
446#[derive(Debug, Serialize, Deserialize)]
447#[serde(rename_all = "camelCase")]
448pub struct LinksArgs {
449 pub repo: RepoPath,
450 pub number: u32,
451}
452
Merge branch 'ask-g1t'453// --- Ask g1t ---------------------------------------------------------------
454
455/// The most messages one ask sends: the conversation's latest turns.
456pub const ASK_MAX_MESSAGES: usize = 40;
457/// The longest one message may be, in characters.
458pub const ASK_MAX_MESSAGE_CHARS: usize = 16_000;
459/// The longest a whole conversation may be, in characters.
460pub const ASK_MAX_TOTAL_CHARS: usize = 64_000;
461/// The most tokens one answer may run to.
462pub const ASK_MAX_OUTPUT_TOKENS: u32 = 4_096;
463
464/// What Ask g1t answers with in a workspace: the connection and model an
465/// owner chose, or why it cannot answer yet.
466#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
467pub struct AskSetup {
468 pub connection_id: Option<String>,
469 /// The connection's name, as its owner named it.
470 pub connection_name: Option<String>,
471 pub provider: Option<Provider>,
472 pub model: Option<String>,
473 /// Why Ask cannot answer, in words for the person asking. `None` when
474 /// it can.
475 pub problem: Option<String>,
476}
477
478/// `ask_settings`: what Ask answers with. Returns `Outcome<AskSetup>`.
479/// Members only.
480#[derive(Debug, Serialize, Deserialize)]
481pub struct AskSettingsArgs {
482 pub workspace: String,
483 pub viewer: Viewer,
484}
485
486/// `set_ask_settings`: chooses the connection and model Ask answers with;
487/// no `connection_id` turns Ask off. Returns `Outcome<AskSetup>`. Owners
488/// only.
489#[derive(Debug, Serialize, Deserialize)]
490pub struct SetAskSettingsArgs {
491 pub actor: User,
492 pub workspace: String,
493 #[serde(default)]
494 pub connection_id: Option<String>,
495 /// One of the models the connection offers; its own model (an Azure
496 /// deployment) when not given.
497 #[serde(default)]
498 pub model: Option<String>,
499}
500
501#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
502#[serde(rename_all = "snake_case")]
503pub enum AskRole {
504 User,
505 Assistant,
506}
507
508/// One turn of an Ask conversation.
509#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
510pub struct AskMessage {
511 pub role: AskRole,
512 pub content: String,
513}
514
515/// Where the person asking is in g1t, so the answer is about their work.
516#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
517pub struct AskPage {
518 /// The page's path, such as `/acme/web/issues/12`.
519 #[serde(default)]
520 pub path: Option<String>,
521 #[serde(default)]
522 pub repo: Option<RepoPath>,
523 #[serde(default)]
524 pub issue: Option<u32>,
525 #[serde(default)]
526 pub pull: Option<u32>,
527}
528
529/// `ask`: one answer, streamed as newline-delimited JSON [`AskEvent`]s:
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.530/// `meta` first, then `text` as it comes and a `tool` line as each step
531/// starts and ends, then `sources` and `done`, or `error` at any point.
532/// Members only. The conversation alternates, starting and ending with the
533/// person; its turns are text only, since the steps of an answer live in
534/// that answer alone.
535///
536/// The provider reads g1t with the MCP server's own tools, as the person
537/// asking, on a read-only credential identity makes for the answer
538/// (`create_ask_token`), so it sees what they may see and changes nothing.
Merge branch 'ask-g1t'539#[derive(Debug, Serialize, Deserialize)]
540pub struct AskArgs {
541 pub actor: User,
542 pub workspace: String,
543 pub messages: Vec<AskMessage>,
544 #[serde(default)]
545 pub page: AskPage,
546}
547
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.548/// The most rounds of tool calls one answer takes before it must answer
549/// with what it has.
550pub const ASK_MAX_TOOL_ROUNDS: usize = 10;
551/// The most tool calls one answer makes, across its rounds.
552pub const ASK_MAX_TOOL_CALLS: usize = 30;
553/// The most characters of one tool's result the model is given; more is
554/// trimmed, long texts and lists first, with a note saying so.
555pub const ASK_MAX_TOOL_RESULT_CHARS: usize = 16_000;
556/// The most characters of tool results one answer gives the model in all.
557pub const ASK_MAX_TOOL_TOTAL_CHARS: usize = 120_000;
558/// How long one answer may spend calling tools, in seconds. After it, the
559/// model answers with what it has.
560pub const ASK_MAX_TOOL_SECONDS: u64 = 120;
561/// The most sources one answer lists.
562pub const ASK_MAX_SOURCES: usize = 12;
563
564/// Where a step of an answer is.
565#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
566#[serde(rename_all = "snake_case")]
567pub enum AskToolStatus {
568 /// Started; its result has not come.
569 Running,
570 Done,
571 /// The tool answered with a refusal or an error: `summary` says what.
572 Failed,
573 /// Ask did not run it: it is not something Ask does (it only reads, and
574 /// hands work off), or the answer had used its tool calls.
575 Refused,
576}
577
578/// A page on g1t an answer drew on.
579#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
580pub struct AskSource {
581 pub title: String,
582 /// An absolute address on the site, such as
583 /// `https://g1t.sh/acme/web/pull/7`.
584 pub url: String,
585 /// Whether the answer links to it, rather than only having read it.
586 pub cited: bool,
587}
588
Merge branch 'ask-g1t'589/// One line of an `ask` answer.
590#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
591#[serde(tag = "type", rename_all = "snake_case")]
592pub enum AskEvent {
593 /// Who is answering.
594 Meta { provider: Provider, provider_label: String, model: String },
595 /// The next piece of the answer.
596 Text { text: String },
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.597 /// A step taken to answer: one of g1t's tools, run as the person
598 /// asking. Sent when it starts, `running`, and again with the same `id`
599 /// when it ends, with what it found in `summary` and the page it was
600 /// about in `link`.
601 Tool {
602 id: String,
603 /// The tool, such as `pull_request`, and its `action`, such as `list`.
604 name: String,
605 action: Option<String>,
606 /// What it does, for a person: "Read pull requests in acme/web".
607 label: String,
608 status: AskToolStatus,
609 summary: Option<String>,
610 link: Option<String>,
611 },
612 /// What the answer drew on, cited first. Sent once, before `done`.
613 Sources { sources: Vec<AskSource> },
614 /// Work Ask proposes to hand to a session in the person's own harness:
615 /// on an issue, or on a new one with `title` and `body`. Nothing has
616 /// started; the person starts it, or copies the command, from the card
617 /// the answer shows. `mode` is `plan`, `implement` or `review`.
618 Proposal {
619 id: String,
620 repo: String,
621 issue: Option<u64>,
622 title: Option<String>,
623 body: Option<String>,
624 mode: String,
625 instruction: Option<String>,
626 },
Merge branch 'ask-g1t'627 /// The answer is complete. `stop_reason` as the provider said it, such
628 /// as `end_turn`, `stop` or `max_tokens`.
629 Done { stop_reason: Option<String> },
630 /// The answer stopped: what went wrong, for the person asking.
631 Error { message: String },
632}
633
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise634#[cfg(test)]
635mod tests {
636 use super::*;
637
638 #[test]
639 fn every_provider_is_named_once_and_found_again() {
640 let mut names: Vec<&str> = PROVIDERS.iter().map(|spec| spec.name).collect();
641 for provider in Provider::all() {
642 assert_eq!(Provider::parse(provider.name()), Some(provider));
643 }
644 names.sort();
645 names.dedup();
646 assert_eq!(names.len(), PROVIDERS.len());
647 }
648
649 #[test]
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.650 fn ai_providers_are_connections_like_any_other() {
651 assert_eq!(Provider::parse("anthropic"), Some(Provider::Anthropic));
652 let stored: Provider = serde_json::from_str("\"openai_endpoint\"").unwrap();
653 assert_eq!(stored.kind(), ProviderKind::Models);
654 assert!(stored.own_address());
655 assert!(!Provider::Anthropic.own_address());
656 assert!(Provider::all().any(|provider| provider == Provider::Groq));
657 assert!(Provider::all().any(|provider| provider == Provider::Sentry));
AI Gateway: OpenAI's format, open models, and your own providers658 }
659
660 #[test]
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.661 fn stored_configs_with_old_fields_parse() {
662 let config: ConnectionConfig =
663 serde_json::from_str(r#"{"repo":"acme/web","assign":true,"gatewayModels":["*"],"writeBack":false}"#).unwrap();
664 assert_eq!(config.repo.as_deref(), Some("acme/web"));
665 assert!(!config.write_back);
AI Gateway: OpenAI's format, open models, and your own providers666 }
667
668 #[test]
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise669 fn model_providers_say_how_to_reach_them() {
670 for spec in PROVIDERS.iter().filter(|spec| spec.kind == ProviderKind::Models) {
671 assert!(spec.api == "anthropic" || spec.api == "openai", "{}", spec.name);
672 assert!(!spec.auth_header.is_empty(), "{}", spec.name);
673 assert!(spec.base_url.is_empty() || spec.base_url.starts_with("https://"), "{}", spec.name);
674 }
675 }
Merge branch 'ask-g1t'676
677 #[test]
678 fn ask_events_are_tagged_lines() {
679 let line = serde_json::to_string(&AskEvent::Text { text: "Hi".into() }).unwrap();
680 assert_eq!(line, r#"{"type":"text","text":"Hi"}"#);
681 let meta: AskEvent =
682 serde_json::from_str(r#"{"type":"meta","provider":"anthropic","provider_label":"Anthropic","model":"claude"}"#).unwrap();
683 assert_eq!(
684 meta,
685 AskEvent::Meta { provider: Provider::Anthropic, provider_label: "Anthropic".into(), model: "claude".into() }
686 );
687 let page: AskPage = serde_json::from_str("{}").unwrap();
688 assert_eq!(page, AskPage::default());
689 }
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.690
691 #[test]
692 fn ask_steps_and_sources_are_snake_case_lines() {
693 let step = AskEvent::Tool {
694 id: "t1".into(),
695 name: "pull_request".into(),
696 action: Some("list".into()),
697 label: "Read pull requests in acme/web".into(),
698 status: AskToolStatus::Done,
699 summary: Some("12 results".into()),
700 link: Some("https://g1t.sh/acme/web/pulls".into()),
701 };
702 assert_eq!(
703 serde_json::to_value(&step).unwrap(),
704 serde_json::json!({
705 "type": "tool", "id": "t1", "name": "pull_request", "action": "list",
706 "label": "Read pull requests in acme/web", "status": "done",
707 "summary": "12 results", "link": "https://g1t.sh/acme/web/pulls",
708 })
709 );
710 let sources = AskEvent::Sources {
711 sources: vec![AskSource { title: "Fix login".into(), url: "https://g1t.sh/acme/web/pull/7".into(), cited: true }],
712 };
713 let line = serde_json::to_string(&sources).unwrap();
714 assert_eq!(line, r#"{"type":"sources","sources":[{"title":"Fix login","url":"https://g1t.sh/acme/web/pull/7","cited":true}]}"#);
715 assert_eq!(serde_json::from_str::<AskEvent>(&line).unwrap(), sources);
716 assert_eq!(serde_json::to_string(&AskToolStatus::Refused).unwrap(), r#""refused""#);
717 }
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise718}

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