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 otherwise | 1 | //! 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 otherwise | 8 | //! - **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 otherwise | 10 | //! - **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 otherwise | 17 | //! |
| 18 | //! Mirrors `packages/contracts/src/integrations.ts`. | |
| 19 | ||
| 20 | use serde::{Deserialize, Serialize}; | |
| 21 | ||
| 22 | use crate::repos::RepoPath; | |
| 23 | use 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")] | |
| 28 | pub 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. | |
| 64 | pub 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 | ||
| 79 | const 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 | ||
| 91 | const 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. | |
| 104 | pub 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 | ||
| 126 | impl 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 otherwise | 171 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 172 | #[serde(rename_all = "snake_case")] | |
| 173 | pub 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 otherwise | 175 | 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 otherwise | 179 | 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")] | |
| 186 | pub 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 otherwise | 220 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 221 | pub model: Option<String>, | |
| 222 | } | |
| 223 | ||
| 224 | fn yes() -> bool { | |
| 225 | true | |
| 226 | } | |
| 227 | ||
| 228 | impl 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")] | |
| 249 | pub 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")] | |
| 274 | pub 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")] | |
| 291 | pub 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")] | |
| 308 | pub 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)] | |
| 325 | pub 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")] | |
| 333 | pub 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 otherwise | 346 | #[serde(default)] |
| 347 | pub signing_secret: Option<String>, | |
| 348 | } | |
| 349 | ||
| 350 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 351 | #[serde(rename_all = "camelCase")] | |
| 352 | pub 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")] | |
| 362 | pub 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)] | |
| 379 | pub struct ConnectionArgs { | |
| 380 | pub actor: User, | |
| 381 | pub workspace: String, | |
| 382 | pub id: String, | |
| 383 | } | |
| 384 | ||
| 385 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 386 | pub 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)] | |
| 394 | pub 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)] | |
| 403 | pub 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)] | |
| 411 | pub 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)] | |
| 420 | pub 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 otherwise | 427 | /// `import`: opens an issue from a ticket. Returns `Outcome<Imported>`. |
| 428 | #[derive(Debug, Serialize, Deserialize)] | |
| 429 | pub struct ImportArgs { | |
| 430 | pub actor: User, | |
| 431 | pub repo: RepoPath, | |
| 432 | pub reference: String, | |
| 433 | } | |
| 434 | ||
| 435 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 436 | pub 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")] | |
| 448 | pub 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. | |
| 456 | pub const ASK_MAX_MESSAGES: usize = 40; | |
| 457 | /// The longest one message may be, in characters. | |
| 458 | pub const ASK_MAX_MESSAGE_CHARS: usize = 16_000; | |
| 459 | /// The longest a whole conversation may be, in characters. | |
| 460 | pub const ASK_MAX_TOTAL_CHARS: usize = 64_000; | |
| 461 | /// The most tokens one answer may run to. | |
| 462 | pub 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)] | |
| 467 | pub 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)] | |
| 481 | pub 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)] | |
| 490 | pub 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")] | |
| 503 | pub enum AskRole { | |
| 504 | User, | |
| 505 | Assistant, | |
| 506 | } | |
| 507 | ||
| 508 | /// One turn of an Ask conversation. | |
| 509 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 510 | pub 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)] | |
| 517 | pub 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)] |
| 540 | pub 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. | |
| 550 | pub const ASK_MAX_TOOL_ROUNDS: usize = 10; | |
| 551 | /// The most tool calls one answer makes, across its rounds. | |
| 552 | pub 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. | |
| 555 | pub const ASK_MAX_TOOL_RESULT_CHARS: usize = 16_000; | |
| 556 | /// The most characters of tool results one answer gives the model in all. | |
| 557 | pub 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. | |
| 560 | pub const ASK_MAX_TOOL_SECONDS: u64 = 120; | |
| 561 | /// The most sources one answer lists. | |
| 562 | pub 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")] | |
| 567 | pub 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)] | |
| 580 | pub 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")] | |
| 592 | pub 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 otherwise | 634 | #[cfg(test)] |
| 635 | mod 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 providers | 658 | } |
| 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 providers | 666 | } |
| 667 | ||
| 668 | #[test] | |
| README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise | 669 | 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 otherwise | 718 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.