chore: cargo fmt

This commit is contained in:
Nathan Flurry 2026-02-05 00:37:36 -08:00
parent 14f2743b9a
commit 886f93aaca
15 changed files with 240 additions and 181 deletions

View file

@ -3,7 +3,7 @@ resolver = "2"
members = ["server/packages/*"] members = ["server/packages/*"]
[workspace.package] [workspace.package]
version = "0.1.6-rc.1" version = "0.1.6"
edition = "2021" edition = "2021"
authors = [ "Rivet Gaming, LLC <developer@rivet.gg>" ] authors = [ "Rivet Gaming, LLC <developer@rivet.gg>" ]
license = "Apache-2.0" license = "Apache-2.0"
@ -12,12 +12,12 @@ description = "Universal API for automatic coding agents in sandboxes. Supprots
[workspace.dependencies] [workspace.dependencies]
# Internal crates # Internal crates
sandbox-agent = { version = "0.1.6-rc.1", path = "server/packages/sandbox-agent" } sandbox-agent = { version = "0.1.6", path = "server/packages/sandbox-agent" }
sandbox-agent-error = { version = "0.1.6-rc.1", path = "server/packages/error" } sandbox-agent-error = { version = "0.1.6", path = "server/packages/error" }
sandbox-agent-agent-management = { version = "0.1.6-rc.1", path = "server/packages/agent-management" } sandbox-agent-agent-management = { version = "0.1.6", path = "server/packages/agent-management" }
sandbox-agent-agent-credentials = { version = "0.1.6-rc.1", path = "server/packages/agent-credentials" } sandbox-agent-agent-credentials = { version = "0.1.6", path = "server/packages/agent-credentials" }
sandbox-agent-universal-agent-schema = { version = "0.1.6-rc.1", path = "server/packages/universal-agent-schema" } sandbox-agent-universal-agent-schema = { version = "0.1.6", path = "server/packages/universal-agent-schema" }
sandbox-agent-extracted-agent-schemas = { version = "0.1.6-rc.1", path = "server/packages/extracted-agent-schemas" } sandbox-agent-extracted-agent-schemas = { version = "0.1.6", path = "server/packages/extracted-agent-schemas" }
# Serialization # Serialization
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-shared", "name": "@sandbox-agent/cli-shared",
"version": "0.1.6-rc.1", "version": "0.1.6",
"description": "Shared helpers for sandbox-agent CLI and SDK", "description": "Shared helpers for sandbox-agent CLI and SDK",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli", "name": "@sandbox-agent/cli",
"version": "0.1.6-rc.1", "version": "0.1.6",
"description": "CLI for sandbox-agent - run AI coding agents in sandboxes", "description": "CLI for sandbox-agent - run AI coding agents in sandboxes",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-darwin-arm64", "name": "@sandbox-agent/cli-darwin-arm64",
"version": "0.1.6-rc.1", "version": "0.1.6",
"description": "sandbox-agent CLI binary for macOS ARM64", "description": "sandbox-agent CLI binary for macOS ARM64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-darwin-x64", "name": "@sandbox-agent/cli-darwin-x64",
"version": "0.1.6-rc.1", "version": "0.1.6",
"description": "sandbox-agent CLI binary for macOS x64", "description": "sandbox-agent CLI binary for macOS x64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-linux-arm64", "name": "@sandbox-agent/cli-linux-arm64",
"version": "0.1.6-rc.1", "version": "0.1.6",
"description": "sandbox-agent CLI binary for Linux arm64", "description": "sandbox-agent CLI binary for Linux arm64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-linux-x64", "name": "@sandbox-agent/cli-linux-x64",
"version": "0.1.6-rc.1", "version": "0.1.6",
"description": "sandbox-agent CLI binary for Linux x64", "description": "sandbox-agent CLI binary for Linux x64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@sandbox-agent/cli-win32-x64", "name": "@sandbox-agent/cli-win32-x64",
"version": "0.1.6-rc.1", "version": "0.1.6",
"description": "sandbox-agent CLI binary for Windows x64", "description": "sandbox-agent CLI binary for Windows x64",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "sandbox-agent", "name": "sandbox-agent",
"version": "0.1.6-rc.1", "version": "0.1.6",
"description": "Universal API for automatic coding agents in sandboxes. Supprots Claude Code, Codex, OpenCode, and Amp.", "description": "Universal API for automatic coding agents in sandboxes. Supprots Claude Code, Codex, OpenCode, and Amp.",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {

View file

@ -515,7 +515,8 @@ fn run_opencode(cli: &Cli, args: &OpencodeArgs) -> Result<(), CliError> {
let base_url = format!("http://{}:{}", args.host, args.port); let base_url = format!("http://{}:{}", args.host, args.port);
wait_for_health(&mut server_child, &base_url, token.as_deref())?; wait_for_health(&mut server_child, &base_url, token.as_deref())?;
let session_id = create_opencode_session(&base_url, token.as_deref(), args.session_title.as_deref())?; let session_id =
create_opencode_session(&base_url, token.as_deref(), args.session_title.as_deref())?;
write_stdout_line(&format!("OpenCode session: {session_id}"))?; write_stdout_line(&format!("OpenCode session: {session_id}"))?;
let attach_url = format!("{base_url}/opencode"); let attach_url = format!("{base_url}/opencode");
@ -776,7 +777,10 @@ fn create_opencode_session(
}; };
let mut request = client.post(&url).json(&body); let mut request = client.post(&url).json(&body);
if let Ok(directory) = std::env::current_dir() { if let Ok(directory) = std::env::current_dir() {
request = request.header("x-opencode-directory", directory.to_string_lossy().to_string()); request = request.header(
"x-opencode-directory",
directory.to_string_lossy().to_string(),
);
} }
if let Some(token) = token { if let Some(token) = token {
request = request.bearer_auth(token); request = request.bearer_auth(token);

View file

@ -6,9 +6,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::Infallible; use std::convert::Infallible;
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::str::FromStr;
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
@ -24,12 +24,12 @@ use tokio::time::interval;
use utoipa::{IntoParams, OpenApi, ToSchema}; use utoipa::{IntoParams, OpenApi, ToSchema};
use crate::router::{AppState, CreateSessionRequest, PermissionReply}; use crate::router::{AppState, CreateSessionRequest, PermissionReply};
use sandbox_agent_error::SandboxError;
use sandbox_agent_agent_management::agents::AgentId; use sandbox_agent_agent_management::agents::AgentId;
use sandbox_agent_error::SandboxError;
use sandbox_agent_universal_agent_schema::{ use sandbox_agent_universal_agent_schema::{
ContentPart, ItemDeltaData, ItemEventData, ItemKind, ItemRole, UniversalEvent, UniversalEventData, ContentPart, FileAction, ItemDeltaData, ItemEventData, ItemKind, ItemRole, ItemStatus,
UniversalEventType, UniversalItem, PermissionEventData, PermissionStatus, QuestionEventData, PermissionEventData, PermissionStatus, QuestionEventData, QuestionStatus, UniversalEvent,
QuestionStatus, FileAction, ItemStatus, UniversalEventData, UniversalEventType, UniversalItem,
}; };
static SESSION_COUNTER: AtomicU64 = AtomicU64::new(1); static SESSION_COUNTER: AtomicU64 = AtomicU64::new(1);
@ -261,18 +261,12 @@ impl OpenCodeState {
if let Some(value) = query { if let Some(value) = query {
return value.clone(); return value.clone();
} }
if let Some(value) = self if let Some(value) = self.config.fixed_directory.as_ref().cloned().or_else(|| {
.config headers
.fixed_directory .get("x-opencode-directory")
.as_ref() .and_then(|v| v.to_str().ok())
.cloned() .map(|v| v.to_string())
.or_else(|| { }) {
headers
.get("x-opencode-directory")
.and_then(|v| v.to_str().ok())
.map(|v| v.to_string())
})
{
return value; return value;
} }
std::env::current_dir() std::env::current_dir()
@ -602,7 +596,8 @@ fn resolve_agent_from_model(provider_id: &str, model_id: &str) -> Option<AgentId
} }
fn normalize_agent_mode(agent: Option<String>) -> String { fn normalize_agent_mode(agent: Option<String>) -> String {
agent.filter(|value| !value.is_empty()) agent
.filter(|value| !value.is_empty())
.unwrap_or_else(|| default_agent_mode().to_string()) .unwrap_or_else(|| default_agent_mode().to_string())
} }
@ -962,7 +957,6 @@ fn unique_assistant_message_id(
} }
} }
fn extract_text_from_content(parts: &[ContentPart]) -> Option<String> { fn extract_text_from_content(parts: &[ContentPart]) -> Option<String> {
let mut text = String::new(); let mut text = String::new();
for part in parts { for part in parts {
@ -1146,22 +1140,19 @@ fn question_event(event_type: &str, question: &Value) -> Value {
} }
fn message_id_from_info(info: &Value) -> Option<String> { fn message_id_from_info(info: &Value) -> Option<String> {
info.get("id").and_then(|v| v.as_str()).map(|v| v.to_string()) info.get("id")
.and_then(|v| v.as_str())
.map(|v| v.to_string())
} }
async fn upsert_message_info( async fn upsert_message_info(state: &OpenCodeState, session_id: &str, info: Value) -> Vec<Value> {
state: &OpenCodeState,
session_id: &str,
info: Value,
) -> Vec<Value> {
let mut messages = state.messages.lock().await; let mut messages = state.messages.lock().await;
let entry = messages.entry(session_id.to_string()).or_default(); let entry = messages.entry(session_id.to_string()).or_default();
let message_id = message_id_from_info(&info); let message_id = message_id_from_info(&info);
if let Some(message_id) = message_id.clone() { if let Some(message_id) = message_id.clone() {
if let Some(existing) = entry if let Some(existing) = entry.iter_mut().find(|record| {
.iter_mut() message_id_from_info(&record.info).as_deref() == Some(message_id.as_str())
.find(|record| message_id_from_info(&record.info).as_deref() == Some(message_id.as_str())) }) {
{
existing.info = info.clone(); existing.info = info.clone();
} else { } else {
entry.push(OpenCodeMessageRecord { entry.push(OpenCodeMessageRecord {
@ -1385,7 +1376,9 @@ async fn apply_permission_event(
let mut permissions = state.opencode.permissions.lock().await; let mut permissions = state.opencode.permissions.lock().await;
permissions.insert(record.id.clone(), record); permissions.insert(record.id.clone(), record);
drop(permissions); drop(permissions);
state.opencode.emit_event(permission_event("permission.asked", &value)); state
.opencode
.emit_event(permission_event("permission.asked", &value));
} }
PermissionStatus::Approved | PermissionStatus::Denied => { PermissionStatus::Approved | PermissionStatus::Denied => {
let reply = match permission.status { let reply = match permission.status {
@ -1441,7 +1434,9 @@ async fn apply_question_event(
let mut questions = state.opencode.questions.lock().await; let mut questions = state.opencode.questions.lock().await;
questions.insert(record.id.clone(), record); questions.insert(record.id.clone(), record);
drop(questions); drop(questions);
state.opencode.emit_event(question_event("question.asked", &value)); state
.opencode
.emit_event(question_event("question.asked", &value));
} }
QuestionStatus::Answered => { QuestionStatus::Answered => {
let answers = question let answers = question
@ -1525,20 +1520,17 @@ async fn apply_item_event(
} }
if let Some(id) = message_id.clone() { if let Some(id) = message_id.clone() {
if let Some(item_key) = item_id_key.clone() { if let Some(item_key) = item_id_key.clone() {
runtime runtime.message_id_for_item.insert(item_key, id.clone());
.message_id_for_item
.insert(item_key, id.clone());
} }
if let Some(native_key) = native_id_key.clone() { if let Some(native_key) = native_id_key.clone() {
runtime runtime.message_id_for_item.insert(native_key, id.clone());
.message_id_for_item
.insert(native_key, id.clone());
} }
} }
}) })
.await; .await;
let message_id = message_id let message_id = message_id.unwrap_or_else(|| {
.unwrap_or_else(|| unique_assistant_message_id(&runtime, parent_id.as_ref(), event.sequence)); unique_assistant_message_id(&runtime, parent_id.as_ref(), event.sequence)
});
let parent_id = parent_id.or_else(|| runtime.last_user_message_id.clone()); let parent_id = parent_id.or_else(|| runtime.last_user_message_id.clone());
let agent = runtime let agent = runtime
.last_agent .last_agent
@ -1594,7 +1586,9 @@ async fn apply_item_event(
.entry(message_id.clone()) .entry(message_id.clone())
.or_insert_with(|| format!("{}_text", message_id)) .or_insert_with(|| format!("{}_text", message_id))
.clone(); .clone();
runtime.text_by_message.insert(message_id.clone(), text.clone()); runtime
.text_by_message
.insert(message_id.clone(), text.clone());
let part = build_text_part_with_id(&session_id, &message_id, &part_id, &text); let part = build_text_part_with_id(&session_id, &message_id, &part_id, &text);
upsert_message_part(&state.opencode, &session_id, &message_id, part.clone()).await; upsert_message_part(&state.opencode, &session_id, &message_id, part.clone()).await;
state state
@ -1619,8 +1613,13 @@ async fn apply_item_event(
let part_id = next_id("part_", &PART_COUNTER); let part_id = next_id("part_", &PART_COUNTER);
let reasoning_part = let reasoning_part =
build_reasoning_part(&session_id, &message_id, &part_id, text, now); build_reasoning_part(&session_id, &message_id, &part_id, text, now);
upsert_message_part(&state.opencode, &session_id, &message_id, reasoning_part.clone()) upsert_message_part(
.await; &state.opencode,
&session_id,
&message_id,
reasoning_part.clone(),
)
.await;
state state
.opencode .opencode
.emit_event(part_event("message.part.updated", &reasoning_part)); .emit_event(part_event("message.part.updated", &reasoning_part));
@ -1640,8 +1639,14 @@ async fn apply_item_event(
"input": {"arguments": arguments}, "input": {"arguments": arguments},
"raw": arguments, "raw": arguments,
}); });
let tool_part = let tool_part = build_tool_part(
build_tool_part(&session_id, &message_id, &part_id, call_id, name, state_value); &session_id,
&message_id,
&part_id,
call_id,
name,
state_value,
);
upsert_message_part(&state.opencode, &session_id, &message_id, tool_part.clone()) upsert_message_part(&state.opencode, &session_id, &message_id, tool_part.clone())
.await; .await;
state state
@ -1704,8 +1709,13 @@ async fn apply_item_event(
FileAction::Patch => "text/x-diff", FileAction::Patch => "text/x-diff",
_ => "text/plain", _ => "text/plain",
}; };
let part = let part = build_file_part_from_path(
build_file_part_from_path(&session_id, &message_id, path, mime, diff.as_deref()); &session_id,
&message_id,
path,
mime,
diff.as_deref(),
);
upsert_message_part(&state.opencode, &session_id, &message_id, part.clone()).await; upsert_message_part(&state.opencode, &session_id, &message_id, part.clone()).await;
state state
.opencode .opencode
@ -1787,14 +1797,10 @@ async fn apply_tool_item_event(
} }
if let Some(id) = message_id.clone() { if let Some(id) = message_id.clone() {
if let Some(item_key) = item_id_key.clone() { if let Some(item_key) = item_id_key.clone() {
runtime runtime.message_id_for_item.insert(item_key, id.clone());
.message_id_for_item
.insert(item_key, id.clone());
} }
if let Some(native_key) = native_id_key.clone() { if let Some(native_key) = native_id_key.clone() {
runtime runtime.message_id_for_item.insert(native_key, id.clone());
.message_id_for_item
.insert(native_key, id.clone());
} }
runtime runtime
.tool_message_by_call .tool_message_by_call
@ -1803,8 +1809,9 @@ async fn apply_tool_item_event(
}) })
.await; .await;
let message_id = message_id let message_id = message_id.unwrap_or_else(|| {
.unwrap_or_else(|| unique_assistant_message_id(&runtime, parent_id.as_ref(), event.sequence)); unique_assistant_message_id(&runtime, parent_id.as_ref(), event.sequence)
});
let parent_id = parent_id.or_else(|| runtime.last_user_message_id.clone()); let parent_id = parent_id.or_else(|| runtime.last_user_message_id.clone());
let agent = runtime let agent = runtime
.last_agent .last_agent
@ -1967,7 +1974,11 @@ async fn apply_item_delta(
delta: String, delta: String,
) { ) {
let session_id = event.session_id.clone(); let session_id = event.session_id.clone();
let item_id_key = if item_id.is_empty() { None } else { Some(item_id) }; let item_id_key = if item_id.is_empty() {
None
} else {
Some(item_id)
};
let native_id_key = native_item_id; let native_id_key = native_item_id;
let is_user_delta = item_id_key let is_user_delta = item_id_key
.as_ref() .as_ref()
@ -2003,20 +2014,17 @@ async fn apply_item_delta(
} }
if let Some(id) = message_id.clone() { if let Some(id) = message_id.clone() {
if let Some(item_key) = item_id_key.clone() { if let Some(item_key) = item_id_key.clone() {
runtime runtime.message_id_for_item.insert(item_key, id.clone());
.message_id_for_item
.insert(item_key, id.clone());
} }
if let Some(native_key) = native_id_key.clone() { if let Some(native_key) = native_id_key.clone() {
runtime runtime.message_id_for_item.insert(native_key, id.clone());
.message_id_for_item
.insert(native_key, id.clone());
} }
} }
}) })
.await; .await;
let message_id = message_id let message_id = message_id.unwrap_or_else(|| {
.unwrap_or_else(|| unique_assistant_message_id(&runtime, parent_id.as_ref(), event.sequence)); unique_assistant_message_id(&runtime, parent_id.as_ref(), event.sequence)
});
let parent_id = parent_id.or_else(|| runtime.last_user_message_id.clone()); let parent_id = parent_id.or_else(|| runtime.last_user_message_id.clone());
let directory = session_directory(&state.opencode, &session_id).await; let directory = session_directory(&state.opencode, &session_id).await;
let worktree = state.opencode.worktree_for(&directory); let worktree = state.opencode.worktree_for(&directory);
@ -2086,7 +2094,10 @@ pub fn build_opencode_router(state: Arc<OpenCodeAppState>) -> Router {
.route("/event", get(oc_event_subscribe)) .route("/event", get(oc_event_subscribe))
.route("/global/event", get(oc_global_event)) .route("/global/event", get(oc_global_event))
.route("/global/health", get(oc_global_health)) .route("/global/health", get(oc_global_health))
.route("/global/config", get(oc_global_config_get).patch(oc_global_config_patch)) .route(
"/global/config",
get(oc_global_config_get).patch(oc_global_config_patch),
)
.route("/global/dispose", post(oc_global_dispose)) .route("/global/dispose", post(oc_global_dispose))
.route("/instance/dispose", post(oc_instance_dispose)) .route("/instance/dispose", post(oc_instance_dispose))
.route("/log", post(oc_log)) .route("/log", post(oc_log))
@ -2124,7 +2135,10 @@ pub fn build_opencode_router(state: Arc<OpenCodeAppState>) -> Router {
"/session/:sessionID/message/:messageID/part/:partID", "/session/:sessionID/message/:messageID/part/:partID",
patch(oc_message_part_update).delete(oc_message_part_delete), patch(oc_message_part_update).delete(oc_message_part_delete),
) )
.route("/session/:sessionID/prompt_async", post(oc_session_prompt_async)) .route(
"/session/:sessionID/prompt_async",
post(oc_session_prompt_async),
)
.route("/session/:sessionID/command", post(oc_session_command)) .route("/session/:sessionID/command", post(oc_session_command))
.route("/session/:sessionID/shell", post(oc_session_shell)) .route("/session/:sessionID/shell", post(oc_session_shell))
.route("/session/:sessionID/revert", post(oc_session_revert)) .route("/session/:sessionID/revert", post(oc_session_revert))
@ -2133,7 +2147,10 @@ pub fn build_opencode_router(state: Arc<OpenCodeAppState>) -> Router {
"/session/:sessionID/permissions/:permissionID", "/session/:sessionID/permissions/:permissionID",
post(oc_session_permission_reply), post(oc_session_permission_reply),
) )
.route("/session/:sessionID/share", post(oc_session_share).delete(oc_session_unshare)) .route(
"/session/:sessionID/share",
post(oc_session_share).delete(oc_session_unshare),
)
.route("/session/:sessionID/todo", get(oc_session_todo)) .route("/session/:sessionID/todo", get(oc_session_todo))
// Permissions + questions (global) // Permissions + questions (global)
.route("/permission", get(oc_permission_list)) .route("/permission", get(oc_permission_list))
@ -2171,7 +2188,10 @@ pub fn build_opencode_router(state: Arc<OpenCodeAppState>) -> Router {
.route("/find/symbol", get(oc_find_symbols)) .route("/find/symbol", get(oc_find_symbols))
// MCP // MCP
.route("/mcp", get(oc_mcp_list).post(oc_mcp_register)) .route("/mcp", get(oc_mcp_list).post(oc_mcp_register))
.route("/mcp/:name/auth", post(oc_mcp_auth).delete(oc_mcp_auth_remove)) .route(
"/mcp/:name/auth",
post(oc_mcp_auth).delete(oc_mcp_auth_remove),
)
.route("/mcp/:name/auth/callback", post(oc_mcp_auth_callback)) .route("/mcp/:name/auth/callback", post(oc_mcp_auth_callback))
.route("/mcp/:name/auth/authenticate", post(oc_mcp_authenticate)) .route("/mcp/:name/auth/authenticate", post(oc_mcp_authenticate))
.route("/mcp/:name/connect", post(oc_mcp_connect)) .route("/mcp/:name/connect", post(oc_mcp_connect))
@ -2182,7 +2202,9 @@ pub fn build_opencode_router(state: Arc<OpenCodeAppState>) -> Router {
.route("/experimental/resource", get(oc_resource_list)) .route("/experimental/resource", get(oc_resource_list))
.route( .route(
"/experimental/worktree", "/experimental/worktree",
get(oc_worktree_list).post(oc_worktree_create).delete(oc_worktree_delete), get(oc_worktree_list)
.post(oc_worktree_create)
.delete(oc_worktree_delete),
) )
.route("/experimental/worktree/reset", post(oc_worktree_reset)) .route("/experimental/worktree/reset", post(oc_worktree_reset))
// Skills // Skills
@ -2300,7 +2322,9 @@ async fn oc_event_subscribe(
Query(query): Query<DirectoryQuery>, Query(query): Query<DirectoryQuery>,
) -> Sse<impl futures::Stream<Item = Result<Event, Infallible>>> { ) -> Sse<impl futures::Stream<Item = Result<Event, Infallible>>> {
let receiver = state.opencode.subscribe(); let receiver = state.opencode.subscribe();
let directory = state.opencode.directory_for(&headers, query.directory.as_ref()); let directory = state
.opencode
.directory_for(&headers, query.directory.as_ref());
let branch = state.opencode.branch_name(); let branch = state.opencode.branch_name();
state.opencode.emit_event(json!({ state.opencode.emit_event(json!({
"type": "server.connected", "type": "server.connected",
@ -2318,33 +2342,36 @@ async fn oc_event_subscribe(
"type": "server.heartbeat", "type": "server.heartbeat",
"properties": {} "properties": {}
}); });
let stream = stream::unfold((receiver, interval(std::time::Duration::from_secs(30))), move |(mut rx, mut ticker)| { let stream = stream::unfold(
let heartbeat = heartbeat_payload.clone(); (receiver, interval(std::time::Duration::from_secs(30))),
async move { move |(mut rx, mut ticker)| {
tokio::select! { let heartbeat = heartbeat_payload.clone();
_ = ticker.tick() => { async move {
let sse_event = Event::default() tokio::select! {
.json_data(&heartbeat) _ = ticker.tick() => {
.unwrap_or_else(|_| Event::default().data("{}")); let sse_event = Event::default()
Some((Ok(sse_event), (rx, ticker))) .json_data(&heartbeat)
} .unwrap_or_else(|_| Event::default().data("{}"));
event = rx.recv() => { Some((Ok(sse_event), (rx, ticker)))
match event { }
Ok(event) => { event = rx.recv() => {
let sse_event = Event::default() match event {
.json_data(&event) Ok(event) => {
.unwrap_or_else(|_| Event::default().data("{}")); let sse_event = Event::default()
Some((Ok(sse_event), (rx, ticker))) .json_data(&event)
.unwrap_or_else(|_| Event::default().data("{}"));
Some((Ok(sse_event), (rx, ticker)))
}
Err(broadcast::error::RecvError::Lagged(_)) => {
Some((Ok(Event::default().comment("lagged")), (rx, ticker)))
}
Err(broadcast::error::RecvError::Closed) => None,
} }
Err(broadcast::error::RecvError::Lagged(_)) => {
Some((Ok(Event::default().comment("lagged")), (rx, ticker)))
}
Err(broadcast::error::RecvError::Closed) => None,
} }
} }
} }
} },
}); );
Sse::new(stream).keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(15))) Sse::new(stream).keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(15)))
} }
@ -2361,7 +2388,9 @@ async fn oc_global_event(
Query(query): Query<DirectoryQuery>, Query(query): Query<DirectoryQuery>,
) -> Sse<impl futures::Stream<Item = Result<Event, Infallible>>> { ) -> Sse<impl futures::Stream<Item = Result<Event, Infallible>>> {
let receiver = state.opencode.subscribe(); let receiver = state.opencode.subscribe();
let directory = state.opencode.directory_for(&headers, query.directory.as_ref()); let directory = state
.opencode
.directory_for(&headers, query.directory.as_ref());
let branch = state.opencode.branch_name(); let branch = state.opencode.branch_name();
state.opencode.emit_event(json!({ state.opencode.emit_event(json!({
"type": "server.connected", "type": "server.connected",
@ -2381,35 +2410,38 @@ async fn oc_global_event(
"properties": {} "properties": {}
} }
}); });
let stream = stream::unfold((receiver, interval(std::time::Duration::from_secs(30))), move |(mut rx, mut ticker)| { let stream = stream::unfold(
let directory = directory.clone(); (receiver, interval(std::time::Duration::from_secs(30))),
let heartbeat = heartbeat_payload.clone(); move |(mut rx, mut ticker)| {
async move { let directory = directory.clone();
tokio::select! { let heartbeat = heartbeat_payload.clone();
_ = ticker.tick() => { async move {
let sse_event = Event::default() tokio::select! {
.json_data(&heartbeat) _ = ticker.tick() => {
.unwrap_or_else(|_| Event::default().data("{}")); let sse_event = Event::default()
Some((Ok(sse_event), (rx, ticker))) .json_data(&heartbeat)
} .unwrap_or_else(|_| Event::default().data("{}"));
event = rx.recv() => { Some((Ok(sse_event), (rx, ticker)))
match event { }
Ok(event) => { event = rx.recv() => {
let payload = json!({"directory": directory, "payload": event}); match event {
let sse_event = Event::default() Ok(event) => {
.json_data(&payload) let payload = json!({"directory": directory, "payload": event});
.unwrap_or_else(|_| Event::default().data("{}")); let sse_event = Event::default()
Some((Ok(sse_event), (rx, ticker))) .json_data(&payload)
.unwrap_or_else(|_| Event::default().data("{}"));
Some((Ok(sse_event), (rx, ticker)))
}
Err(broadcast::error::RecvError::Lagged(_)) => {
Some((Ok(Event::default().comment("lagged")), (rx, ticker)))
}
Err(broadcast::error::RecvError::Closed) => None,
} }
Err(broadcast::error::RecvError::Lagged(_)) => {
Some((Ok(Event::default().comment("lagged")), (rx, ticker)))
}
Err(broadcast::error::RecvError::Closed) => None,
} }
} }
} }
} },
}); );
Sse::new(stream).keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(15))) Sse::new(stream).keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(15)))
} }
@ -2507,7 +2539,10 @@ async fn oc_formatter_status() -> impl IntoResponse {
responses((status = 200)), responses((status = 200)),
tag = "opencode" tag = "opencode"
)] )]
async fn oc_path(State(state): State<Arc<OpenCodeAppState>>, headers: HeaderMap) -> impl IntoResponse { async fn oc_path(
State(state): State<Arc<OpenCodeAppState>>,
headers: HeaderMap,
) -> impl IntoResponse {
let directory = state.opencode.directory_for(&headers, None); let directory = state.opencode.directory_for(&headers, None);
let worktree = state.opencode.worktree_for(&directory); let worktree = state.opencode.worktree_for(&directory);
( (
@ -2543,7 +2578,10 @@ async fn oc_vcs(State(state): State<Arc<OpenCodeAppState>>) -> impl IntoResponse
responses((status = 200)), responses((status = 200)),
tag = "opencode" tag = "opencode"
)] )]
async fn oc_project_list(State(state): State<Arc<OpenCodeAppState>>, headers: HeaderMap) -> impl IntoResponse { async fn oc_project_list(
State(state): State<Arc<OpenCodeAppState>>,
headers: HeaderMap,
) -> impl IntoResponse {
let directory = state.opencode.directory_for(&headers, None); let directory = state.opencode.directory_for(&headers, None);
let worktree = state.opencode.worktree_for(&directory); let worktree = state.opencode.worktree_for(&directory);
let now = state.opencode.now_ms(); let now = state.opencode.now_ms();
@ -2564,20 +2602,23 @@ async fn oc_project_list(State(state): State<Arc<OpenCodeAppState>>, headers: He
responses((status = 200)), responses((status = 200)),
tag = "opencode" tag = "opencode"
)] )]
async fn oc_project_current(State(state): State<Arc<OpenCodeAppState>>, headers: HeaderMap) -> impl IntoResponse { async fn oc_project_current(
State(state): State<Arc<OpenCodeAppState>>,
headers: HeaderMap,
) -> impl IntoResponse {
let directory = state.opencode.directory_for(&headers, None); let directory = state.opencode.directory_for(&headers, None);
let worktree = state.opencode.worktree_for(&directory); let worktree = state.opencode.worktree_for(&directory);
let now = state.opencode.now_ms(); let now = state.opencode.now_ms();
( (
StatusCode::OK, StatusCode::OK,
Json(json!({ Json(json!({
"id": state.opencode.default_project_id.clone(), "id": state.opencode.default_project_id.clone(),
"worktree": worktree, "worktree": worktree,
"vcs": "git", "vcs": "git",
"name": "sandbox-agent", "name": "sandbox-agent",
"time": {"created": now, "updated": now}, "time": {"created": now, "updated": now},
"sandboxes": [], "sandboxes": [],
})), })),
) )
} }
@ -2615,7 +2656,9 @@ async fn oc_session_create(
parent_id: None, parent_id: None,
permission: None, permission: None,
}); });
let directory = state.opencode.directory_for(&headers, query.directory.as_ref()); let directory = state
.opencode
.directory_for(&headers, query.directory.as_ref());
let now = state.opencode.now_ms(); let now = state.opencode.now_ms();
let id = next_id("ses_", &SESSION_COUNTER); let id = next_id("ses_", &SESSION_COUNTER);
let slug = format!("session-{}", id); let slug = format!("session-{}", id);
@ -2792,7 +2835,9 @@ async fn oc_session_fork(
headers: HeaderMap, headers: HeaderMap,
Query(query): Query<DirectoryQuery>, Query(query): Query<DirectoryQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let directory = state.opencode.directory_for(&headers, query.directory.as_ref()); let directory = state
.opencode
.directory_for(&headers, query.directory.as_ref());
let now = state.opencode.now_ms(); let now = state.opencode.now_ms();
let id = next_id("ses_", &SESSION_COUNTER); let id = next_id("ses_", &SESSION_COUNTER);
let slug = format!("session-{}", id); let slug = format!("session-{}", id);
@ -2841,9 +2886,7 @@ async fn oc_session_diff() -> impl IntoResponse {
responses((status = 200)), responses((status = 200)),
tag = "opencode" tag = "opencode"
)] )]
async fn oc_session_summarize( async fn oc_session_summarize(Json(body): Json<SessionSummarizeRequest>) -> impl IntoResponse {
Json(body): Json<SessionSummarizeRequest>,
) -> impl IntoResponse {
if body.provider_id.is_none() || body.model_id.is_none() { if body.provider_id.is_none() || body.model_id.is_none() {
return bad_request("providerID and modelID are required"); return bad_request("providerID and modelID are required");
} }
@ -2886,9 +2929,15 @@ async fn oc_session_message_create(
Json(body): Json<SessionMessageRequest>, Json(body): Json<SessionMessageRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
if std::env::var("OPENCODE_COMPAT_LOG_BODY").is_ok() { if std::env::var("OPENCODE_COMPAT_LOG_BODY").is_ok() {
tracing::info!(target = "sandbox_agent::opencode", ?body, "opencode prompt body"); tracing::info!(
target = "sandbox_agent::opencode",
?body,
"opencode prompt body"
);
} }
let directory = state.opencode.directory_for(&headers, query.directory.as_ref()); let directory = state
.opencode
.directory_for(&headers, query.directory.as_ref());
let _ = state let _ = state
.opencode .opencode
.ensure_session(&session_id, directory.clone()) .ensure_session(&session_id, directory.clone())
@ -3161,7 +3210,9 @@ async fn oc_session_command(
if body.command.is_none() || body.arguments.is_none() { if body.command.is_none() || body.arguments.is_none() {
return bad_request("command and arguments are required").into_response(); return bad_request("command and arguments are required").into_response();
} }
let directory = state.opencode.directory_for(&headers, query.directory.as_ref()); let directory = state
.opencode
.directory_for(&headers, query.directory.as_ref());
let worktree = state.opencode.worktree_for(&directory); let worktree = state.opencode.worktree_for(&directory);
let now = state.opencode.now_ms(); let now = state.opencode.now_ms();
let assistant_message_id = next_id("msg_", &MESSAGE_COUNTER); let assistant_message_id = next_id("msg_", &MESSAGE_COUNTER);
@ -3206,7 +3257,9 @@ async fn oc_session_shell(
if body.command.is_none() || body.agent.is_none() { if body.command.is_none() || body.agent.is_none() {
return bad_request("agent and command are required").into_response(); return bad_request("agent and command are required").into_response();
} }
let directory = state.opencode.directory_for(&headers, query.directory.as_ref()); let directory = state
.opencode
.directory_for(&headers, query.directory.as_ref());
let worktree = state.opencode.worktree_for(&directory); let worktree = state.opencode.worktree_for(&directory);
let now = state.opencode.now_ms(); let now = state.opencode.now_ms();
let assistant_message_id = next_id("msg_", &MESSAGE_COUNTER); let assistant_message_id = next_id("msg_", &MESSAGE_COUNTER);
@ -3355,7 +3408,11 @@ async fn oc_session_todo() -> impl IntoResponse {
tag = "opencode" tag = "opencode"
)] )]
async fn oc_permission_list(State(state): State<Arc<OpenCodeAppState>>) -> impl IntoResponse { async fn oc_permission_list(State(state): State<Arc<OpenCodeAppState>>) -> impl IntoResponse {
let pending = state.inner.session_manager().list_pending_permissions().await; let pending = state
.inner
.session_manager()
.list_pending_permissions()
.await;
let mut values = Vec::new(); let mut values = Vec::new();
for item in pending { for item in pending {
let record = OpenCodePermissionRecord { let record = OpenCodePermissionRecord {
@ -3561,7 +3618,6 @@ async fn oc_provider_auth() -> impl IntoResponse {
(StatusCode::OK, Json(auth)) (StatusCode::OK, Json(auth))
} }
#[utoipa::path( #[utoipa::path(
post, post,
path = "/provider/{providerID}/oauth/authorize", path = "/provider/{providerID}/oauth/authorize",
@ -3599,7 +3655,10 @@ async fn oc_provider_oauth_callback(Path(_provider_id): Path<String>) -> impl In
responses((status = 200)), responses((status = 200)),
tag = "opencode" tag = "opencode"
)] )]
async fn oc_auth_set(Path(_provider_id): Path<String>, Json(_body): Json<Value>) -> impl IntoResponse { async fn oc_auth_set(
Path(_provider_id): Path<String>,
Json(_body): Json<Value>,
) -> impl IntoResponse {
bool_ok(true) bool_ok(true)
} }
@ -3639,7 +3698,9 @@ async fn oc_pty_create(
Query(query): Query<DirectoryQuery>, Query(query): Query<DirectoryQuery>,
Json(body): Json<PtyCreateRequest>, Json(body): Json<PtyCreateRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let directory = state.opencode.directory_for(&headers, query.directory.as_ref()); let directory = state
.opencode
.directory_for(&headers, query.directory.as_ref());
let id = next_id("pty_", &PTY_COUNTER); let id = next_id("pty_", &PTY_COUNTER);
let record = OpenCodePtyRecord { let record = OpenCodePtyRecord {
id: id.clone(), id: id.clone(),
@ -3854,10 +3915,7 @@ async fn oc_mcp_register() -> impl IntoResponse {
responses((status = 200)), responses((status = 200)),
tag = "opencode" tag = "opencode"
)] )]
async fn oc_mcp_auth( async fn oc_mcp_auth(Path(_name): Path<String>, _body: Option<Json<Value>>) -> impl IntoResponse {
Path(_name): Path<String>,
_body: Option<Json<Value>>,
) -> impl IntoResponse {
(StatusCode::OK, Json(json!({"status": "needs_auth"}))) (StatusCode::OK, Json(json!({"status": "needs_auth"})))
} }
@ -3962,7 +4020,10 @@ async fn oc_resource_list() -> impl IntoResponse {
responses((status = 200)), responses((status = 200)),
tag = "opencode" tag = "opencode"
)] )]
async fn oc_worktree_list(State(state): State<Arc<OpenCodeAppState>>, headers: HeaderMap) -> impl IntoResponse { async fn oc_worktree_list(
State(state): State<Arc<OpenCodeAppState>>,
headers: HeaderMap,
) -> impl IntoResponse {
let directory = state.opencode.directory_for(&headers, None); let directory = state.opencode.directory_for(&headers, None);
let worktree = state.opencode.worktree_for(&directory); let worktree = state.opencode.worktree_for(&directory);
(StatusCode::OK, Json(json!([worktree]))) (StatusCode::OK, Json(json!([worktree])))
@ -3975,7 +4036,10 @@ async fn oc_worktree_list(State(state): State<Arc<OpenCodeAppState>>, headers: H
responses((status = 200)), responses((status = 200)),
tag = "opencode" tag = "opencode"
)] )]
async fn oc_worktree_create(State(state): State<Arc<OpenCodeAppState>>, headers: HeaderMap) -> impl IntoResponse { async fn oc_worktree_create(
State(state): State<Arc<OpenCodeAppState>>,
headers: HeaderMap,
) -> impl IntoResponse {
let directory = state.opencode.directory_for(&headers, None); let directory = state.opencode.directory_for(&headers, None);
let worktree = state.opencode.worktree_for(&directory); let worktree = state.opencode.worktree_for(&directory);
( (

View file

@ -16,6 +16,7 @@ use axum::response::{IntoResponse, Response, Sse};
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::Json; use axum::Json;
use axum::Router; use axum::Router;
use base64::Engine;
use futures::{stream, StreamExt}; use futures::{stream, StreamExt};
use reqwest::Client; use reqwest::Client;
use sandbox_agent_error::{AgentError, ErrorType, ProblemDetails, SandboxError}; use sandbox_agent_error::{AgentError, ErrorType, ProblemDetails, SandboxError};
@ -34,7 +35,6 @@ use tokio::sync::{broadcast, mpsc, oneshot, Mutex};
use tokio::time::sleep; use tokio::time::sleep;
use tokio_stream::wrappers::BroadcastStream; use tokio_stream::wrappers::BroadcastStream;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use base64::Engine;
use tracing::Span; use tracing::Span;
use utoipa::{Modify, OpenApi, ToSchema}; use utoipa::{Modify, OpenApi, ToSchema};

View file

@ -56,9 +56,7 @@ impl ServerLogs {
.last_rotation .last_rotation
.date_naive() .date_naive()
.and_hms_opt(0, 0, 0) .and_hms_opt(0, 0, 0)
.ok_or_else(|| { .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "invalid date"))?
std::io::Error::new(std::io::ErrorKind::Other, "invalid date")
})?
+ Duration::days(1)), + Duration::days(1)),
); );

View file

@ -65,9 +65,7 @@ impl ServerLogs {
.last_rotation .last_rotation
.date_naive() .date_naive()
.and_hms_opt(0, 0, 0) .and_hms_opt(0, 0, 0)
.ok_or_else(|| { .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "invalid date"))?
std::io::Error::new(std::io::ErrorKind::Other, "invalid date")
})?
+ Duration::days(1)), + Duration::days(1)),
); );

View file

@ -34,8 +34,9 @@ fn official_spec_path() -> PathBuf {
#[test] #[test]
fn opencode_openapi_matches_official_paths() { fn opencode_openapi_matches_official_paths() {
let official_path = official_spec_path(); let official_path = official_spec_path();
let official_json = fs::read_to_string(&official_path) let official_json = fs::read_to_string(&official_path).unwrap_or_else(|err| {
.unwrap_or_else(|err| panic!("failed to read official OpenCode spec at {official_path:?}: {err}")); panic!("failed to read official OpenCode spec at {official_path:?}: {err}")
});
let official: Value = let official: Value =
serde_json::from_str(&official_json).expect("official OpenCode spec is not valid JSON"); serde_json::from_str(&official_json).expect("official OpenCode spec is not valid JSON");
@ -45,14 +46,8 @@ fn opencode_openapi_matches_official_paths() {
let official_methods = collect_path_methods(&official); let official_methods = collect_path_methods(&official);
let our_methods = collect_path_methods(&ours_value); let our_methods = collect_path_methods(&ours_value);
let missing: Vec<_> = official_methods let missing: Vec<_> = official_methods.difference(&our_methods).cloned().collect();
.difference(&our_methods) let extra: Vec<_> = our_methods.difference(&official_methods).cloned().collect();
.cloned()
.collect();
let extra: Vec<_> = our_methods
.difference(&official_methods)
.cloned()
.collect();
if !missing.is_empty() || !extra.is_empty() { if !missing.is_empty() || !extra.is_empty() {
let mut message = String::new(); let mut message = String::new();