From e18f271051627e93e3129f72e5faed0503b2c252 Mon Sep 17 00:00:00 2001 From: Jason Keung Date: Tue, 5 May 2026 11:56:59 -0400 Subject: [PATCH 1/2] [REMOTE-1370] Phase 2a: taskGitCredentials schema, query, and AIClient - Add taskGitCredentials types to schema.graphql (verified against staging) and add to clientQueries allowlist in client-schema.ts - Add task_git_credentials.rs GraphQL query file (cynic) following the task_secrets pattern - Add GitCredential struct and get_task_git_credentials to AIClient trait with a ServerApi implementation Co-Authored-By: Oz --- app/src/server/server_api/ai.rs | 61 +++++++++++++++++++ crates/graphql/src/api/queries/mod.rs | 1 + .../src/api/queries/task_git_credentials.rs | 50 +++++++++++++++ .../warp_graphql_schema/api/client-schema.ts | 1 + crates/warp_graphql_schema/api/schema.graphql | 38 ++++++++++++ 5 files changed, 151 insertions(+) create mode 100644 crates/graphql/src/api/queries/task_git_credentials.rs diff --git a/app/src/server/server_api/ai.rs b/app/src/server/server_api/ai.rs index 0b3fe2c28..7f1f3e546 100644 --- a/app/src/server/server_api/ai.rs +++ b/app/src/server/server_api/ai.rs @@ -120,6 +120,10 @@ use warp_graphql::{ UpdateMerkleTreeVariables, }, }, + queries::task_git_credentials::{ + TaskGitCredentials, TaskGitCredentialsInput, TaskGitCredentialsResult, + TaskGitCredentialsVariables, + }, queries::{ codebase_context_config::{ CodebaseContextConfigQuery, CodebaseContextConfigResult, CodebaseContextConfigVariables, @@ -515,6 +519,19 @@ pub struct CreateFileArtifactUploadResponse { pub upload_target: FileArtifactUploadTargetInfo, } +/// A single git credential entry returned by `taskGitCredentials`. +#[derive(Debug, Clone)] +pub struct GitCredential { + /// The GitHub token (OAuth user token or App installation token). + pub token: String, + /// The GitHub username. `None` for service-account (installation token) principals. + pub username: Option, + /// The GitHub email. `None` for service-account principals. + pub email: Option, + /// The host (always `"github.com"` in V1). + pub host: String, +} + /// Filter parameters for listing ambient agent tasks. #[derive(Clone, Debug, Default)] pub struct TaskListFilter { @@ -901,6 +918,12 @@ pub trait AIClient: 'static + Send + Sync { task_id: &AmbientAgentTaskId, ) -> anyhow::Result<(), anyhow::Error>; + async fn get_task_git_credentials( + &self, + task_id: String, + workload_token: String, + ) -> anyhow::Result, anyhow::Error>; + async fn get_task_attachments( &self, task_id: String, @@ -1780,6 +1803,44 @@ impl AIClient for ServerApi { Ok(()) } + async fn get_task_git_credentials( + &self, + task_id: String, + workload_token: String, + ) -> anyhow::Result, anyhow::Error> { + let variables = TaskGitCredentialsVariables { + input: TaskGitCredentialsInput { + task_id: cynic::Id::new(task_id), + workload_token, + }, + request_context: get_request_context(), + }; + let operation = TaskGitCredentials::build(variables); + let response = self.send_graphql_request(operation, None).await?; + + match response.task_git_credentials { + TaskGitCredentialsResult::TaskGitCredentialsOutput(output) => { + let credentials = output + .credentials + .into_iter() + .map(|c| GitCredential { + token: c.token, + username: c.username, + email: c.email, + host: c.host, + }) + .collect(); + Ok(credentials) + } + TaskGitCredentialsResult::UserFacingError(error) => { + Err(anyhow!(get_user_facing_error_message(error))) + } + TaskGitCredentialsResult::Unknown => { + Err(anyhow!("Failed to fetch task git credentials")) + } + } + } + async fn get_task_attachments( &self, task_id: String, diff --git a/crates/graphql/src/api/queries/mod.rs b/crates/graphql/src/api/queries/mod.rs index 9ab0851f5..a6da98d86 100644 --- a/crates/graphql/src/api/queries/mod.rs +++ b/crates/graphql/src/api/queries/mod.rs @@ -28,6 +28,7 @@ pub mod rerank_fragments; pub mod suggest_cloud_environment_image; pub mod sync_merkle_tree; pub mod task_attachments; +pub mod task_git_credentials; pub mod task_secrets; pub mod user_github_info; pub mod user_repo_auth_status; diff --git a/crates/graphql/src/api/queries/task_git_credentials.rs b/crates/graphql/src/api/queries/task_git_credentials.rs new file mode 100644 index 000000000..c80ff5f54 --- /dev/null +++ b/crates/graphql/src/api/queries/task_git_credentials.rs @@ -0,0 +1,50 @@ +use crate::{error::UserFacingError, request_context::RequestContext, schema}; + +/// A GraphQL query to fetch git credentials for a specific task. +/// +/// This query is used by Agent Mode tasks to retrieve a fresh GitHub token that the +/// driver uses to configure git and the gh CLI, and to refresh those credentials +/// periodically so long-running agents retain GitHub access for their full duration. +#[derive(cynic::QueryFragment, Debug)] +#[cynic(graphql_type = "RootQuery", variables = "TaskGitCredentialsVariables")] +pub struct TaskGitCredentials { + #[arguments(input: $input, requestContext: $request_context)] + pub task_git_credentials: TaskGitCredentialsResult, +} + +crate::client::define_operation! { + task_git_credentials(TaskGitCredentialsVariables) -> TaskGitCredentials; +} + +#[derive(cynic::QueryVariables, Debug)] +pub struct TaskGitCredentialsVariables { + pub input: TaskGitCredentialsInput, + pub request_context: RequestContext, +} + +#[derive(cynic::InputObject, Debug)] +pub struct TaskGitCredentialsInput { + pub task_id: cynic::Id, + pub workload_token: String, +} + +#[derive(cynic::InlineFragments, Debug)] +pub enum TaskGitCredentialsResult { + TaskGitCredentialsOutput(TaskGitCredentialsOutput), + UserFacingError(UserFacingError), + #[cynic(fallback)] + Unknown, +} + +#[derive(cynic::QueryFragment, Debug)] +pub struct TaskGitCredentialsOutput { + pub credentials: Vec, +} + +#[derive(cynic::QueryFragment, Debug)] +pub struct TaskGitCredential { + pub token: String, + pub username: Option, + pub email: Option, + pub host: String, +} diff --git a/crates/warp_graphql_schema/api/client-schema.ts b/crates/warp_graphql_schema/api/client-schema.ts index 510983082..2b19aa73b 100644 --- a/crates/warp_graphql_schema/api/client-schema.ts +++ b/crates/warp_graphql_schema/api/client-schema.ts @@ -97,6 +97,7 @@ const clientQueries = [ 'getIntegrationsUsingEnvironment', 'scheduledAgentHistory', 'task', + 'taskGitCredentials', 'taskSecrets', 'listAIConversations', 'suggestCloudEnvironmentImage' diff --git a/crates/warp_graphql_schema/api/schema.graphql b/crates/warp_graphql_schema/api/schema.graphql index 2d371a657..a992c46af 100644 --- a/crates/warp_graphql_schema/api/schema.graphql +++ b/crates/warp_graphql_schema/api/schema.graphql @@ -2769,6 +2769,7 @@ type RootQuery { In the future, this may be merged with taskSecrets into a single task query. """ task(input: TaskInput!, requestContext: RequestContext!): TaskResult! + taskGitCredentials(input: TaskGitCredentialsInput!, requestContext: RequestContext!): TaskGitCredentialsResult! taskSecrets(input: TaskSecretsInput!, requestContext: RequestContext!): TaskSecretsResult! updatedCloudObjects(input: UpdatedCloudObjectsInput!, requestContext: RequestContext!): UpdatedCloudObjectsResult! user(requestContext: RequestContext!): UserResult! @@ -3183,6 +3184,43 @@ type TaskSecretEntry { value: ManagedSecretValue! } +"""A set of git credentials for a single hosting provider.""" +type TaskGitCredential { + """ + The email address associated with the token, if available. + Null for service-account (installation) tokens. + """ + email: String + + """The git hosting provider (e.g. \"github.com\").""" + host: String! + + """The OAuth or installation access token.""" + token: String! + + """ + The GitHub username associated with the token, if available. + Null for service-account (installation) tokens. + """ + username: String +} + +"""Input for the taskGitCredentials query.""" +input TaskGitCredentialsInput { + """The ID of the task.""" + taskId: ID! + + """A short-lived token authorizing credential retrieval for the workload.""" + workloadToken: String! +} + +type TaskGitCredentialsOutput implements Response { + credentials: [TaskGitCredential!]! + responseContext: ResponseContext! +} + +union TaskGitCredentialsResult = TaskGitCredentialsOutput | UserFacingError + """Input for the taskSecrets query.""" input TaskSecretsInput { """The ID of the task.""" From c1f3b8d20bac789ebe47aa532be8eec943cb68b5 Mon Sep 17 00:00:00 2001 From: Jason Keung Date: Tue, 5 May 2026 13:27:58 -0400 Subject: [PATCH 2/2] Update app/src/server/server_api/ai.rs Co-authored-by: oz-for-oss[bot] <277970191+oz-for-oss[bot]@users.noreply.github.com> --- app/src/server/server_api/ai.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/server/server_api/ai.rs b/app/src/server/server_api/ai.rs index 7f1f3e546..67843ea08 100644 --- a/app/src/server/server_api/ai.rs +++ b/app/src/server/server_api/ai.rs @@ -520,7 +520,7 @@ pub struct CreateFileArtifactUploadResponse { } /// A single git credential entry returned by `taskGitCredentials`. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct GitCredential { /// The GitHub token (OAuth user token or App installation token). pub token: String,