Start cloud threads with the local GitHub project (#60007)

## What changed

Pass the local project's GitHub origin to cloud prewarming so new cloud threads can provision a repository checkout. Preserve the attached local project when switching backends and resolve its origin for each thread start.

Normalize supported GitHub origins to credential-free HTTPS URLs and select a repository-specific cloud working directory. Unsupported origins retain the blank environment behavior. Keep project metadata outside the app-server wire protocol, and fix HTTP Git URL parsing so query strings and fragments cannot be mistaken for authority text.

## Testing

Add coverage for project routing, origin normalization and rejection, managed environment encoding, local project retention, and Git URL credential stripping.

GitOrigin-RevId: 5859769965faf060461ccc1cbb3fd48f8e2f2155
This commit is contained in:
Benjamin Carlsson
2026-11-06 23:00:38 +00:00
committed by copyberry
parent df769889c0
commit 7de8a0afc0
21 changed files with 491 additions and 15 deletions
@@ -180,7 +180,21 @@ async fn run_worker(
let _ = command.response.send(Ok(Ok(Value::Null)));
return;
}
CloudOperation::Request(request) => Ok((*request, cloud::SubmissionMode::StartIfIdle)),
CloudOperation::Request(request) => {
Ok((*request, cloud::SubmissionMode::StartIfIdle, None))
}
CloudOperation::StartProjectThread {
request_id,
params,
project,
} => Ok((
protocol::ClientRequest::ThreadStart {
request_id,
params: *params,
},
cloud::SubmissionMode::StartIfIdle,
Some(project),
)),
CloudOperation::SubmitUserMessage(params) => Ok((
protocol::ClientRequest::TurnStart {
// Native admission is keyed by the caller's stable message ID.
@@ -188,6 +202,7 @@ async fn run_worker(
params: *params,
},
cloud::SubmissionMode::Steer,
None,
)),
CloudOperation::Notify(_) => {
Err(eyre!("Cloud client notifications are not supported yet."))
@@ -197,9 +212,9 @@ async fn run_worker(
)),
};
let result = match request {
Ok((request, mode)) => tokio::select! {
Ok((request, mode, project)) => tokio::select! {
_ = output.closed() => return,
result = handle_request(request, mode, requests, replies, output) => result,
result = handle_request(request, mode, project, requests, replies, output) => result,
},
Err(error) => Err(error),
};
@@ -231,6 +246,7 @@ async fn run_worker(
async fn handle_request(
request: protocol::ClientRequest,
mode: cloud::SubmissionMode,
project: Option<cloud::ProjectEnvironment>,
requests: &mut requests::Requests,
replies: &mut Replies,
output: &mpsc::UnboundedSender<AppServerEvent>,
@@ -256,7 +272,7 @@ async fn handle_request(
replies.admit_turn(&params.thread_id, output)?;
}
match replies
.while_listening(requests.handle(request, mode, output), output)
.while_listening(requests.handle(request, mode, project, output), output)
.await?
{
requests::Response::Value(value) => Ok(value),
@@ -54,6 +54,7 @@ impl Requests {
&mut self,
request: protocol::ClientRequest,
mode: cloud::SubmissionMode,
project: Option<cloud::ProjectEnvironment>,
output: &mpsc::UnboundedSender<crate::AppServerEvent>,
) -> Result<Response> {
use protocol::ClientRequest as Request;
@@ -79,7 +80,7 @@ impl Requests {
"Cloud conversations start with server defaults. Custom settings are not supported yet."
);
}
let settings = self.client.prewarm_default_thread(model, provider).await?;
let settings = self.client.prewarm_thread(model, provider, project).await?;
return attach_response(settings);
}
Request::ThreadResume { params, .. } => {