Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions app/src/server/server_api/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ use warp_graphql::{
UpdateMerkleTreeVariables,
},
},
queries::task_git_credentials::{
TaskGitCredentials, TaskGitCredentialsInput, TaskGitCredentialsResult,
TaskGitCredentialsVariables,
},
queries::{
codebase_context_config::{
CodebaseContextConfigQuery, CodebaseContextConfigResult, CodebaseContextConfigVariables,
Expand Down Expand Up @@ -515,6 +519,19 @@ pub struct CreateFileArtifactUploadResponse {
pub upload_target: FileArtifactUploadTargetInfo,
}

/// A single git credential entry returned by `taskGitCredentials`.
#[derive(Debug, Clone)]
Comment thread
jasonkeung marked this conversation as resolved.
Outdated
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<String>,
/// The GitHub email. `None` for service-account principals.
pub email: Option<String>,
/// The host (always `"github.com"` in V1).
pub host: String,
}

/// Filter parameters for listing ambient agent tasks.
#[derive(Clone, Debug, Default)]
pub struct TaskListFilter {
Expand Down Expand Up @@ -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<Vec<GitCredential>, anyhow::Error>;

async fn get_task_attachments(
&self,
task_id: String,
Expand Down Expand Up @@ -1780,6 +1803,44 @@ impl AIClient for ServerApi {
Ok(())
}

async fn get_task_git_credentials(
&self,
task_id: String,
workload_token: String,
) -> anyhow::Result<Vec<GitCredential>, 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,
Expand Down
1 change: 1 addition & 0 deletions crates/graphql/src/api/queries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
50 changes: 50 additions & 0 deletions crates/graphql/src/api/queries/task_git_credentials.rs
Original file line number Diff line number Diff line change
@@ -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<TaskGitCredential>,
}

#[derive(cynic::QueryFragment, Debug)]
pub struct TaskGitCredential {
pub token: String,
pub username: Option<String>,
pub email: Option<String>,
pub host: String,
}
1 change: 1 addition & 0 deletions crates/warp_graphql_schema/api/client-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const clientQueries = [
'getIntegrationsUsingEnvironment',
'scheduledAgentHistory',
'task',
'taskGitCredentials',
'taskSecrets',
'listAIConversations',
'suggestCloudEnvironmentImage'
Expand Down
38 changes: 38 additions & 0 deletions crates/warp_graphql_schema/api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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."""
Expand Down
Loading