Add sandbox
ci/woodpecker/push/tests Pipeline was canceled

This commit is contained in:
2026-09-17 13:16:15 +00:00
parent 536c55f27b
commit 99d1c2feef
16 changed files with 1537 additions and 12 deletions
+75 -6
View File
@@ -1,22 +1,33 @@
use futures_util::stream::TryStreamExt;
use openrouter_rs::types::Tool;
use tokio::io::AsyncReadExt;
use tokio_util::io::StreamReader;
use tracing::instrument;
use tracing::{info, instrument, warn};
use crate::{
bot::ReviewResult,
consts::{BOT_PROCESS_MSG, MAX_DIFF_SIZE, REVIEW_PROMPT},
consts::{BOT_PROCESS_MSG, MAX_DIFF_SIZE, REVIEW_PROMPT, SANDBOX_SYSTEM_PROMPT},
gitea::{GiteaAPI, ReviewPayload},
metrics,
open_router::OpenRouterClient,
sandbox::{Sandbox, SandboxConfig, agent},
};
#[instrument(skip(gitea_api, open_router_client, http_client, review_payload))]
#[instrument(skip(
gitea_api,
open_router_client,
http_client,
sandbox_config,
tools,
review_payload
))]
pub async fn exec_review(
gitea_api: &GiteaAPI,
open_router_client: &OpenRouterClient,
http_client: &reqwest::Client,
model: &str,
sandbox_config: &SandboxConfig,
tools: Vec<Tool>,
review_payload: ReviewPayload,
) -> anyhow::Result<()> {
tracing::info!(
@@ -45,10 +56,24 @@ pub async fn exec_review(
.replace("{comment}", &review_payload.comment.body)
.replace("{diff}", &diff_for_llm);
let chat_result = open_router_client.chat(&bot_request).await?;
let mut review_result = serde_json::from_str::<ReviewResult>(&chat_result.message)?;
let (message, cost) = if sandbox_config.enabled {
run_sandboxed_review(
gitea_api,
open_router_client,
sandbox_config,
tools,
&review_payload,
&bot_request,
)
.await?
} else {
let chat_result = open_router_client.chat(&bot_request).await?;
(chat_result.message, chat_result.cost)
};
review_result.cost = chat_result.cost;
let mut review_result = serde_json::from_str::<ReviewResult>(&message)?;
review_result.cost = cost;
if let Some(cost) = review_result.cost {
metrics::openrouter_cost_usd(cost);
}
@@ -86,6 +111,50 @@ pub async fn exec_review(
}
}
/// Runs the review inside a sandbox container, letting the model explore the
/// repository with tools before answering.
async fn run_sandboxed_review(
gitea_api: &GiteaAPI,
open_router_client: &OpenRouterClient,
sandbox_config: &SandboxConfig,
tools: Vec<Tool>,
review_payload: &ReviewPayload,
bot_request: &str,
) -> anyhow::Result<(String, Option<f64>)> {
let repo_url = gitea_api.repo_clone_url(&review_payload.repository.full_name);
let sandbox = Sandbox::create(
&sandbox_config.runtime,
&repo_url,
gitea_api.token(),
review_payload.pull_request.number,
)
.await?;
let result = agent::run(
open_router_client,
&sandbox,
tools,
SANDBOX_SYSTEM_PROMPT,
bot_request,
sandbox_config.max_iterations,
)
.await;
if let Err(err) = sandbox.cleanup().await {
warn!(%err, "Failed to clean up sandbox container");
}
let result = result?;
info!(
iterations = result.iterations,
cost = ?result.cost,
"Sandboxed review finished"
);
Ok((result.message, result.cost))
}
fn review_result_to_markdown(review_result: &ReviewResult) -> String {
if review_result.reviews.is_empty() {
return String::from("No issues found. ✅");