use crate::{ApiError, AppState, HttpError, api_location, run_blocking};
use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde_json::{Value, json};
use std::sync::Arc;
pub async fn health() -> impl IntoResponse {
Json(json!({"healthy": true}))
}
pub async fn location(State(st): State<Arc<AppState>>) -> impl IntoResponse {
Json(api_location(&st))
}
fn available_provider_ids(payloads: &crate::Payloads) -> Vec<String> {
let connected: Vec<String> = payloads
.provider
.get("connected")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let cfg_providers = payloads.config.get("provider").and_then(|v| v.as_object());
let cat_obj = payloads.catalog.as_object();
connected
.into_iter()
.filter(|pid| {
if pid == "opencode" {
return true;
}
let has_cfg_key = cfg_providers
.and_then(|c| c.get(pid))
.and_then(|v| v.pointer("/options/apiKey"))
.and_then(|v| v.as_str())
.is_some_and(|s| !s.is_empty());
if has_cfg_key {
return true;
}
cat_obj.map(|c| !c.contains_key(pid)).unwrap_or(true)
})
.collect()
}
fn project_model_v2(v1: &Value) -> Value {
let id = v1.get("id").cloned().unwrap_or(Value::Null);
let provider_id = v1.get("providerID").cloned().unwrap_or(Value::Null);
let api = v1.get("api").cloned().unwrap_or(json!({}));
let npm = api
.get("npm")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
let mut v2api = match npm {
Some(pkg) => json!({"id": id, "type": "aisdk", "package": pkg}),
None => json!({"id": id, "type": "native", "settings": {}}),
};
if let Some(url) = api.get("url") {
v2api["url"] = url.clone();
}
let caps = v1.get("capabilities").cloned().unwrap_or(json!({}));
let arr_of = |keys: &[&str]| -> Vec<Value> {
keys.iter()
.filter(|k| caps.get(**k).and_then(|v| v.as_bool()).unwrap_or(false))
.map(|k| json!(k))
.collect()
};
let input = {
let mut v = vec![json!("text")];
if caps
.get("attachment")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
v.push(json!("image"));
}
v
};
let _ = arr_of;
let capabilities = json!({
"tools": caps.get("toolcall").and_then(|v| v.as_bool()).unwrap_or(false),
"input": input,
"output": [json!("text")],
});
let cost = v1.get("cost").cloned().unwrap_or(json!({}));
let cache = cost.get("cache").cloned().unwrap_or(json!({}));
let cost_v2 = json!([{
"input": cost.get("input").cloned().unwrap_or(json!(0)),
"output": cost.get("output").cloned().unwrap_or(json!(0)),
"cache": {
"read": cache.get("read").cloned().unwrap_or(json!(0)),
"write": cache.get("write").cloned().unwrap_or(json!(0)),
}
}]);
let limit = v1.get("limit").cloned().unwrap_or(json!({}));
let variants: Vec<Value> = v1
.get("variants")
.and_then(|v| v.as_object())
.map(|m| {
m.iter()
.map(|(k, vv)| {
let body = vv
.get("body")
.or_else(|| vv.get("settings"))
.cloned()
.unwrap_or(json!({}));
json!({"id": k, "headers": {}, "body": body})
})
.collect()
})
.unwrap_or_default();
let released = v1
.get("release_date")
.and_then(|v| v.as_str())
.and_then(parse_release_ms);
let mut out = json!({
"id": id,
"providerID": provider_id,
"name": v1.get("name").cloned().unwrap_or(Value::Null),
"api": v2api,
"capabilities": capabilities,
"request": {"headers": {}, "body": {}},
"variants": variants,
"time": {"released": released.unwrap_or(0)},
"cost": cost_v2,
"status": v1.get("status").cloned().unwrap_or(json!("active")),
"enabled": v1.get("status").and_then(|s| s.as_str()).map(|s| s != "deprecated").unwrap_or(true),
"limit": {"context": limit.get("context").cloned().unwrap_or(json!(0)), "output": limit.get("output").cloned().unwrap_or(json!(0))},
});
if let Some(fam) = v1.get("family") {
out["family"] = fam.clone();
}
out
}
fn parse_release_ms(date: &str) -> Option<i64> {
let parts: Vec<&str> = date.split('-').collect();
if parts.len() != 3 {
return None;
}
let y: i64 = parts[0].parse().ok()?;
let m: i64 = parts[1].parse().ok()?;
let d: i64 = parts[2].parse().ok()?;
let y2 = if m <= 2 { y - 1 } else { y };
let era = if y2 >= 0 { y2 } else { y2 - 399 } / 400;
let yoe = y2 - era * 400;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let days = era * 146097 + doe - 719468;
Some(days * 86_400_000)
}
pub async fn list_providers(
State(st): State<Arc<AppState>>,
headers: axum::http::HeaderMap,
) -> axum::response::Response {
let ae = headers
.get(axum::http::header::ACCEPT_ENCODING)
.and_then(|v| v.to_str().ok());
let inm = headers
.get(axum::http::header::IF_NONE_MATCH)
.and_then(|v| v.to_str().ok());
crate::wire_json_h(&st, "api_provider", ae, inm).unwrap_or_else(|| {
let payloads = st.payloads.read();
Json(providers_body(&payloads, &api_location(&st))).into_response()
})
}
pub fn providers_body(p: &crate::Payloads, location: &Value) -> Value {
let avail = available_provider_ids(p);
let all = p
.provider
.get("all")
.and_then(|v| v.as_array())
.map(|v| v.as_slice())
.unwrap_or(&[]);
let data: Vec<Value> = all
.iter()
.filter(|p| {
p.get("id")
.and_then(|v| v.as_str())
.is_some_and(|id| avail.iter().any(|a| a == id))
})
.map(project_provider_v2)
.collect();
json!({"location": location, "data": data})
}
pub fn models_body(p: &crate::Payloads, location: &Value) -> Value {
let avail = available_provider_ids(p);
let all = p
.provider
.get("all")
.and_then(|v| v.as_array())
.map(|v| v.as_slice())
.unwrap_or(&[]);
let mut data: Vec<Value> = Vec::new();
for p in all {
let pid = p.get("id").and_then(|v| v.as_str()).unwrap_or("");
if !avail.iter().any(|a| a == pid) {
continue;
}
if let Some(models) = p.get("models").and_then(|m| m.as_object()) {
for m in models.values() {
if m.get("status").and_then(|s| s.as_str()) == Some("deprecated") {
continue;
}
data.push(project_model_v2(m));
}
}
}
data.sort_by(|a, b| {
let ra = a
.pointer("/time/released")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let rb = b
.pointer("/time/released")
.and_then(|v| v.as_i64())
.unwrap_or(0);
rb.cmp(&ra)
});
json!({"location": location, "data": data})
}
pub fn integrations_body(p: &crate::Payloads, location: &Value) -> Value {
let catalog = p.catalog.as_object();
let mut out: Vec<Value> = Vec::new();
if let Some(map) = catalog {
for (id, entry) in map {
let name = entry.get("name").and_then(|v| v.as_str()).unwrap_or(id);
let env = entry
.get("env")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
out.push(integration_entry(id, name, &env));
}
}
if let Some(cps) = p.config.get("provider").and_then(|v| v.as_object()) {
for (id, v) in cps {
if catalog.map(|c| c.contains_key(id)).unwrap_or(false) {
continue;
}
let name = v.get("name").and_then(|x| x.as_str()).unwrap_or(id);
out.push(integration_entry(id, name, &[]));
}
}
json!({"location": location, "data": out})
}
fn project_provider_v2(v1: &Value) -> Value {
let id = v1.get("id").cloned().unwrap_or(Value::Null);
let npm = v1
.pointer("/models")
.and_then(|m| m.as_object())
.and_then(|m| m.values().next())
.and_then(|m| m.pointer("/api/npm"))
.and_then(|v| v.as_str());
let url = v1
.pointer("/options/baseURL")
.and_then(|v| v.as_str())
.or_else(|| {
v1.pointer("/models")
.and_then(|m| m.as_object())
.and_then(|m| m.values().next())
.and_then(|m| m.pointer("/api/url"))
.and_then(|v| v.as_str())
})
.unwrap_or("");
let mut api = json!({"type": if npm.is_some() { "aisdk" } else { "native" }});
if let Some(n) = npm {
api["package"] = json!(n);
}
if !url.is_empty() {
api["url"] = json!(url);
}
api["settings"] = json!({});
let mut body = json!({});
if id == json!("opencode") {
body = json!({"apiKey": "public"});
} else if let Some(k) = v1.pointer("/options/apiKey").and_then(|v| v.as_str())
&& !k.is_empty()
{
body = json!({"apiKey": k});
}
json!({
"id": id,
"name": v1.get("name").cloned().unwrap_or(Value::Null),
"api": api,
"request": {"headers": {}, "body": body},
})
}
pub async fn get_provider(
State(st): State<Arc<AppState>>,
Path(provider_id): Path<String>,
) -> Result<Json<Value>, ApiError> {
let payloads = st.payloads.read();
let all = payloads
.provider
.get("all")
.and_then(|v| v.as_array())
.map(|v| v.as_slice())
.unwrap_or(&[]);
match all
.iter()
.find(|p| p.get("id").and_then(|v| v.as_str()) == Some(&provider_id))
{
Some(p) => Ok(Json(
json!({"location": api_location(&st), "data": project_provider_v2(p)}),
)),
None => Err(ApiError {
status: StatusCode::NOT_FOUND,
name: "NotFoundError",
message: format!("Provider not found: {provider_id}"),
}),
}
}
pub async fn list_models(
State(st): State<Arc<AppState>>,
headers: axum::http::HeaderMap,
) -> axum::response::Response {
let ae = headers
.get(axum::http::header::ACCEPT_ENCODING)
.and_then(|v| v.to_str().ok());
let inm = headers
.get(axum::http::header::IF_NONE_MATCH)
.and_then(|v| v.to_str().ok());
crate::wire_json_h(&st, "api_model", ae, inm).unwrap_or_else(|| {
let payloads = st.payloads.read();
Json(models_body(&payloads, &api_location(&st))).into_response()
})
}
pub async fn list_skills(State(st): State<Arc<AppState>>) -> impl IntoResponse {
let payloads = st.payloads.read();
let data: Vec<Value> = payloads
.command
.iter()
.filter(|c| c.get("source").and_then(|v| v.as_str()) == Some("skill"))
.map(|c| {
json!({
"name": c.get("name").cloned().unwrap_or(Value::Null),
"description": c.get("description").cloned().unwrap_or(json!("")),
"slash": false,
"location": "",
"content": c.get("template").cloned().unwrap_or(json!("")),
})
})
.collect();
Json(json!({"location": api_location(&st), "data": data}))
}
fn integration_entry(id: &str, name: &str, env: &[Value]) -> Value {
let mut methods = vec![json!({"type": "key"})];
if !env.is_empty() {
methods.push(json!({"type": "env", "names": env}));
}
json!({"id": id, "name": name, "methods": methods, "connections": []})
}
pub async fn list_integrations(
State(st): State<Arc<AppState>>,
headers: axum::http::HeaderMap,
) -> axum::response::Response {
let ae = headers
.get(axum::http::header::ACCEPT_ENCODING)
.and_then(|v| v.to_str().ok());
let inm = headers
.get(axum::http::header::IF_NONE_MATCH)
.and_then(|v| v.to_str().ok());
crate::wire_json_h(&st, "api_integration", ae, inm).unwrap_or_else(|| {
let payloads = st.payloads.read();
Json(integrations_body(&payloads, &api_location(&st))).into_response()
})
}
pub async fn get_integration(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<Value>, ApiError> {
let payloads = st.payloads.read();
let items = integrations_body(&payloads, &api_location(&st));
match items["data"]
.as_array()
.and_then(|a| {
a.iter()
.find(|i| i.get("id").and_then(|v| v.as_str()) == Some(&id))
})
.cloned()
{
Some(i) => Ok(Json(json!({"location": api_location(&st), "data": i}))),
None => Ok(Json(json!({"location": api_location(&st), "data": null}))),
}
}
pub async fn list_references(State(st): State<Arc<AppState>>) -> impl IntoResponse {
Json(json!({"location": api_location(&st), "data": []}))
}
pub async fn fs_find(
State(st): State<Arc<AppState>>,
Query(q): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<Value>, crate::HttpError> {
let base = std::path::Path::new(st.paths["directory"].as_str().unwrap_or("/")).to_path_buf();
let query = q
.get("query")
.ok_or_else(|| crate::HttpError::TaggedData {
status: StatusCode::BAD_REQUEST,
tag: "InvalidRequestError",
fields: json!({"message": "Missing key\n at [\"query\"]", "kind": "Query"}),
})?
.to_lowercase();
if query.is_empty() {
return Ok(Json(json!({"location": api_location(&st), "data": []})));
}
let want_type = q.get("type").map(String::as_str).unwrap_or("").to_string();
let limit: usize = q
.get("limit")
.and_then(|l| l.parse().ok())
.unwrap_or(50)
.clamp(1, 200);
let glob_mode = query.contains('*') || query.contains('?');
let walk_base = base.clone();
let out = tokio::task::spawn_blocking(move || {
crate::walk_files(&walk_base, &query, &want_type, limit, glob_mode)
})
.await
.map_err(|e| {
crate::HttpError::Api(ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
name: "InternalServerError",
message: format!("fs_find worker: {e}"),
})
})?
.map_err(|e| {
crate::HttpError::Api(ApiError {
status: StatusCode::BAD_REQUEST,
name: "BadRequest",
message: format!("fs_find: {e:#}"),
})
})?;
let data: Vec<Value> = out
.into_iter()
.map(|p| {
let is_dir = p.ends_with('/')
|| std::fs::metadata(base_join(&base, &p))
.map(|m| m.is_dir())
.unwrap_or(false);
json!({"path": p, "type": if is_dir { "directory" } else { "file" }})
})
.collect();
Ok(Json(json!({"location": api_location(&st), "data": data})))
}
fn base_join(base: &std::path::Path, rel: &str) -> std::path::PathBuf {
base.join(rel.trim_end_matches('/'))
}
pub async fn fs_list(
State(st): State<Arc<AppState>>,
Query(q): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<Value>, ApiError> {
let base = std::path::Path::new(st.paths["directory"].as_str().unwrap_or("/")).to_path_buf();
let rel = q.get("path").cloned().unwrap_or_default();
let dir = resolve_within(&base, &rel).map_err(|_| ApiError {
status: StatusCode::BAD_REQUEST,
name: "BadRequest",
message: "path escapes the project directory".into(),
})?;
let entries = tokio::task::spawn_blocking(move || -> anyhow::Result<Vec<Value>> {
let mut out = Vec::new();
let rd = std::fs::read_dir(&dir)?;
for e in rd.flatten() {
let name = e.file_name().to_string_lossy().to_string();
if name == ".git" || name == "node_modules" {
continue;
}
let is_dir = e.file_type().map(|t| t.is_dir()).unwrap_or(false);
out.push(json!({
"path": if is_dir { format!("{name}/") } else { name },
"type": if is_dir { "directory" } else { "file" },
}));
}
out.sort_by(|a, b| a["path"].as_str().cmp(&b["path"].as_str()));
Ok(out)
})
.await
.map_err(|e| ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
name: "InternalServerError",
message: format!("fs_list worker: {e}"),
})?
.map_err(|e| ApiError {
status: StatusCode::BAD_REQUEST,
name: "BadRequest",
message: format!("fs_list: {e:#}"),
})?;
Ok(Json(
json!({"location": api_location(&st), "data": entries}),
))
}
fn resolve_within(base: &std::path::Path, rel: &str) -> std::io::Result<std::path::PathBuf> {
let base_canon = base.canonicalize()?;
let joined = base_canon.join(rel.trim_start_matches('/'));
match joined.canonicalize() {
Ok(real) => {
if real.starts_with(&base_canon) {
Ok(real)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"path escapes the project directory",
))
}
}
Err(_) => {
let lex = lexically_normalize(&joined);
if lex.starts_with(&base_canon) {
Ok(joined)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"path escapes the project directory",
))
}
}
}
}
fn lexically_normalize(p: &std::path::Path) -> std::path::PathBuf {
use std::path::Component;
let mut out = std::path::PathBuf::new();
for c in p.components() {
match c {
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
pub async fn fs_read(
State(st): State<Arc<AppState>>,
Path(path): Path<String>,
) -> Result<axum::response::Response, ApiError> {
let base = std::path::Path::new(st.paths["directory"].as_str().unwrap_or("/")).to_path_buf();
let full = base.join(path.trim_start_matches('/'));
let canon = full.canonicalize().map_err(|_| ApiError {
status: StatusCode::NOT_FOUND,
name: "NotFoundError",
message: format!("File not found: {path}"),
})?;
if !canon.starts_with(&base) {
return Err(ApiError {
status: StatusCode::BAD_REQUEST,
name: "BadRequest",
message: "path escapes the project directory".into(),
});
}
let bytes = tokio::fs::read(&canon).await.map_err(|e| ApiError {
status: StatusCode::NOT_FOUND,
name: "NotFoundError",
message: format!("File not found: {path} ({e})"),
})?;
Ok((
[(axum::http::header::CONTENT_TYPE, "application/octet-stream")],
bytes,
)
.into_response())
}
pub async fn session_diff(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<Value>, ApiError> {
let sid = id.clone();
let s = run_blocking(&st.db, move |db| ocserve_store::load_session_wire(db, &sid))
.await
.map_err(internal)?;
let files = s
.as_ref()
.and_then(|s| s.get("revert"))
.and_then(|r| r.get("files"))
.cloned()
.unwrap_or(json!([]));
Ok(Json(files))
}
fn internal(e: anyhow::Error) -> ApiError {
ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
name: "InternalError",
message: format!("{e:#}"),
}
}
fn session_not_found(id: &str) -> crate::HttpError {
crate::HttpError::TaggedData {
status: StatusCode::NOT_FOUND,
tag: "SessionNotFoundError",
fields: json!({"sessionID": id, "message": format!("Session not found: {id}")}),
}
}
fn session_v2(s: &Value) -> Value {
let dir = s.get("directory").and_then(|v| v.as_str()).unwrap_or("/");
let mut out = json!({
"id": s.get("id").cloned().unwrap_or(Value::Null),
"projectID": s.get("projectID").cloned().unwrap_or(json!("global")),
"cost": s.get("cost").cloned().unwrap_or(json!(0)),
"tokens": s.get("tokens").cloned().unwrap_or(json!({"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}})),
"time": s.get("time").cloned().unwrap_or(json!({"created":0,"updated":0})),
"title": s.get("title").cloned().unwrap_or(json!("")),
"location": {"directory": dir},
});
if let Some(pid) = s.get("parentID").and_then(|v| v.as_str()) {
out["parentID"] = json!(pid);
}
if let Some(a) = s.get("agent").and_then(|v| v.as_str()) {
out["agent"] = json!(a);
}
if let Some(m) = s.get("model")
&& !m.is_null()
{
out["model"] = m.clone();
}
if let Some(path) = s.get("path").and_then(|v| v.as_str())
&& !path.is_empty()
{
out["subpath"] = json!(path);
}
if let Some(r) = s.get("revert")
&& !r.is_null()
{
out["revert"] = r.clone();
}
out
}
pub async fn list_sessions(
State(st): State<Arc<AppState>>,
Query(q): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<Value>, crate::HttpError> {
let q_err = |message: String| crate::HttpError::TaggedData {
status: StatusCode::BAD_REQUEST,
tag: "InvalidRequestError",
fields: json!({"message": message, "kind": "Query"}),
};
let limit: usize = match q.get("limit") {
None => 100,
Some(raw) => {
let n: f64 = raw
.parse()
.map_err(|_| q_err("Expected an integer, got NaN\n at [\"limit\"]".into()))?;
if !n.is_finite() || n < 0.0 || n.fract() != 0.0 {
return Err(q_err(format!(
"Expected an integer, got {raw}\n at [\"limit\"]"
)));
}
(n as usize).clamp(1, 200)
}
};
if let Some(order) = q.get("order")
&& order != "asc"
&& order != "desc"
{
return Err(q_err(format!(
"Expected \"asc\" | \"desc\", got \"{order}\"\n at [\"order\"]"
)));
}
if let Some(cur) = q.get("cursor")
&& ocserve_store::decode_cursor(cur).is_err()
{
return Err(crate::HttpError::TaggedData {
status: StatusCode::BAD_REQUEST,
tag: "InvalidCursorError",
fields: json!({"message": "Invalid cursor"}),
});
}
let roots = q.get("roots").map(|v| v == "true").unwrap_or(false);
let search = q.get("search").cloned().unwrap_or_default().to_lowercase();
let sid = q.get("sessionID").cloned();
let db = st.db.clone();
let mut sessions = run_blocking(&db, ocserve_store::load_sessions_wire)
.await
.map_err(|e| crate::HttpError::Api(internal(e)))?;
if roots {
sessions.retain(|s| s.get("parentID").map(|p| p.is_null()).unwrap_or(true));
}
if !search.is_empty() {
sessions.retain(|s| {
s.get("title")
.and_then(|v| v.as_str())
.map(|t| t.to_lowercase().contains(&search))
.unwrap_or(false)
});
}
let _ = sid;
let total = sessions.len();
let more = total > limit;
let window: Vec<Value> = sessions
.into_iter()
.take(limit)
.map(|s| session_v2(&s))
.collect();
let _ = more;
Ok(Json(
json!({"data": window, "cursor": {"previous": Value::Null, "next": Value::Null}}),
))
}
pub async fn active_sessions() -> impl IntoResponse {
Json(json!({"data": {}}))
}
pub async fn get_session(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<Value>, crate::HttpError> {
let sid = id.clone();
let s = run_blocking(&st.db, move |db| ocserve_store::load_session_wire(db, &sid))
.await
.map_err(|e| crate::HttpError::Api(internal(e)))?;
match s {
Some(s) => Ok(Json(json!({"data": session_v2(&s)}))),
None => Err(session_not_found(&id)),
}
}
pub async fn list_messages(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
Query(q): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<Value>, crate::HttpError> {
let limit: usize = q
.get("limit")
.and_then(|l| l.parse().ok())
.unwrap_or(50)
.clamp(1, 200);
let s = {
let sid = id.clone();
run_blocking(&st.db, move |db| ocserve_store::load_session_wire(db, &sid))
.await
.map_err(|e| crate::HttpError::Api(internal(e)))?
};
if s.is_none() {
return Err(session_not_found(&id));
}
let sid = id.clone();
let msgs = run_blocking(&st.db, move |db| {
ocserve_store::page_messages(db, &sid, limit as u64, None)
})
.await
.map_err(|e| crate::HttpError::Api(internal(e)))?;
let data: Vec<Value> = msgs
.0
.into_iter()
.filter_map(|(mid, info)| project_message_v2(&mid, &info))
.collect();
let mut cursor = json!({"previous": Value::Null, "next": Value::Null});
if let Some(next) = msgs.2 {
cursor["next"] = json!(next);
}
Ok(Json(json!({"data": data, "cursor": cursor})))
}
fn project_message_v2(id: &str, info: &str) -> Option<Value> {
let v: Value = serde_json::from_str(info).ok()?;
let role = v.get("role").and_then(|r| r.as_str()).unwrap_or("");
let created = v.pointer("/time/created").cloned().unwrap_or(json!(0));
let time = json!({"created": created});
match role {
"user" => {
let text = v
.pointer("/summary/title")
.and_then(|t| t.as_str())
.unwrap_or("")
.to_string();
Some(json!({"id": id, "time": time, "type": "user", "text": text}))
}
"assistant" => Some(json!({
"id": id,
"time": time,
"type": "assistant",
"agent": v.get("agent").cloned().unwrap_or(json!("build")),
"model": {"id": v.get("modelID").cloned().unwrap_or(json!("")), "providerID": v.get("providerID").cloned().unwrap_or(json!(""))},
"content": [],
"cost": v.get("cost").cloned().unwrap_or(json!(0)),
"tokens": normalize_tokens(v.get("tokens")),
})),
_ => None,
}
}
fn normalize_tokens(t: Option<&Value>) -> Value {
let t = t.cloned().unwrap_or(json!({}));
let cache = t.get("cache").cloned().unwrap_or(json!({}));
json!({
"input": t.get("input").cloned().unwrap_or(json!(0)),
"output": t.get("output").cloned().unwrap_or(json!(0)),
"reasoning": t.get("reasoning").cloned().unwrap_or(json!(0)),
"cache": {"read": cache.get("read").cloned().unwrap_or(json!(0)), "write": cache.get("write").cloned().unwrap_or(json!(0))},
})
}
pub async fn get_message(
State(st): State<Arc<AppState>>,
Path((id, mid)): Path<(String, String)>,
) -> Result<Json<Value>, ApiError> {
let sid = id.clone();
let mid2 = mid.clone();
let row = run_blocking(&st.db, move |db| {
ocserve_store::message_by_id(db, &sid, &mid2)
})
.await
.map_err(internal)?;
let info = row
.and_then(|v| v.get("info").map(|i| i.to_string()))
.and_then(|info| project_message_v2(&mid, &info));
match info {
Some(m) => Ok(Json(json!({"data": m}))),
None => Err(ApiError {
status: StatusCode::NOT_FOUND,
name: "NotFoundError",
message: format!("Message not found: {mid} (session {id})"),
}),
}
}
pub async fn session_context(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<Value>, crate::HttpError> {
list_messages(State(st), Path(id), Query(std::collections::HashMap::new())).await
}
pub async fn session_history(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<Value>, ApiError> {
let sid = id.clone();
let events = run_blocking(&st.db, move |db| {
ocserve_store::load_session_events(db, &sid, 100)
})
.await
.map_err(internal)?;
Ok(Json(json!({"data": events, "hasMore": false})))
}
pub async fn permission_saved(State(st): State<Arc<AppState>>) -> impl IntoResponse {
let _ = &st;
Json(json!({"data": []}))
}
pub async fn permission_request(State(st): State<Arc<AppState>>) -> impl IntoResponse {
let data = st.gate.list();
Json(json!({"location": api_location(&st), "data": data}))
}
pub async fn question_request(State(st): State<Arc<AppState>>) -> impl IntoResponse {
let data = st.question_gate.list();
Json(json!({"location": api_location(&st), "data": data}))
}
pub async fn session_permission(
State(st): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> impl IntoResponse {
Json(json!({"data": st.gate.list()}))
}
pub async fn session_question(
State(st): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> impl IntoResponse {
Json(json!({"data": st.question_gate.list()}))
}
pub async fn switch_agent(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
Json(body): Json<Value>,
) -> Result<StatusCode, crate::HttpError> {
let agent = body.get("agent").and_then(|v| v.as_str()).unwrap_or("");
set_session_field(&st, &id, "agent", json!(agent)).await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn switch_model(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
Json(body): Json<Value>,
) -> Result<StatusCode, crate::HttpError> {
set_session_field(
&st,
&id,
"model",
body.get("model").cloned().unwrap_or(json!({})),
)
.await?;
Ok(StatusCode::NO_CONTENT)
}
async fn set_session_field(
st: &AppState,
id: &str,
field: &str,
val: Value,
) -> Result<(), crate::HttpError> {
let col = match field {
"agent" => "agent",
"model" => "model",
_ => {
return Err(crate::HttpError::Api(ApiError {
status: StatusCode::BAD_REQUEST,
name: "BadRequest",
message: format!("unknown field {field}"),
}));
}
};
let db = st.db.clone();
let id_check = id.to_string();
let exists = tokio::task::spawn_blocking(move || ocserve_store::session_exists(&db, &id_check))
.await
.map_err(|e| crate::HttpError::Api(internal(anyhow::anyhow!("exists join: {e}"))))?
.map_err(|e| crate::HttpError::Api(internal(e)))?;
if !exists {
return Err(session_not_found(id));
}
let text = if field == "model" && val.is_object() {
val.to_string()
} else if let Some(s) = val.as_str() {
s.to_string()
} else {
val.to_string()
};
let w = st.writer.clone();
let id_w = id.to_string();
let col_w = col.to_string();
tokio::task::spawn_blocking(move || {
w.write(vec![ocserve_store::WriteOp::Sql {
sql: format!("UPDATE session SET {col_w} = ?2 WHERE id = ?1"),
params: vec![id_w.into(), text.into()],
}])
})
.await
.map_err(|e| crate::HttpError::Api(internal(anyhow::anyhow!("write join: {e}"))))?
.map_err(|e| crate::HttpError::Api(internal(e)))?;
Ok(())
}
pub async fn console_orgs() -> impl IntoResponse {
Json(json!({"orgs": []}))
}
pub async fn workspace_list() -> impl IntoResponse {
Json(json!([]))
}
pub async fn workspace_status() -> impl IntoResponse {
Json(json!([]))
}
pub async fn workspace_adapter() -> impl IntoResponse {
Json(json!([{"type": "worktree", "name": "Worktree", "description": "Create a git worktree"}]))
}
pub async fn worktree_list() -> impl IntoResponse {
Json(json!([]))
}
pub async fn prompt(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
Json(body): Json<Value>,
) -> Result<Json<Value>, HttpError> {
let input = crate::decode_v2(ocserve_core::wire::V2PromptInput::decode(&body))?;
let req = ocserve_core::wire::PromptRequest {
message_id: input.id.clone(), parts: normalize_v2_prompt_parts(&input),
..Default::default()
};
let ctx = crate::build_prompt_context(&st, &req, &id)?;
let release = crate::lock_session(&st, &id).await?;
let Ok(_guard) = release.arc().try_lock() else {
return Err(crate::session_busy(&id).into());
};
let writer = st.writer.clone();
let (info, _parts) = ocserve_core::prompt::run_prompt(&ctx, &writer, &id, &req)
.await
.map_err(crate::prompt_err)?;
drop(_guard);
let msg_id = info.get("id").cloned().unwrap_or(Value::Null);
Ok(Json(json!({
"data": {
"id": msg_id,
"sessionID": id,
"admittedSeq": 0,
"prompt": body.get("prompt").cloned().unwrap_or(json!({"text": ""})),
"delivery": Value::String(input.delivery.unwrap_or_else(|| "steer".into())),
"timeCreated": info.pointer("/time/created").cloned().unwrap_or(json!(0)),
}
})))
}
fn normalize_v2_prompt_parts(input: &ocserve_core::wire::V2PromptInput) -> Vec<Value> {
let mut parts = Vec::new();
parts.push(json!({"type": "text", "text": input.prompt_text}));
for f in &input.prompt_files {
parts.push(json!({"type": "file", "file": f}));
}
for a in &input.prompt_agents {
parts.push(json!({"type": "agent", "agent": a}));
}
parts
}
pub async fn interrupt(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, ApiError> {
if let Some((_, handle)) = st.prompt_tasks.lock().remove(&id) {
handle.abort();
}
Ok(StatusCode::NO_CONTENT)
}
pub async fn compact(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, ApiError> {
let payload = ocserve_core::wire::PromptRequest::default();
let ctx = match crate::build_prompt_context(&st, &payload, &id) {
Ok(c) => c,
Err(e) => {
return Err(ApiError {
status: StatusCode::SERVICE_UNAVAILABLE,
name: "ServiceUnavailableError",
message: e.message,
});
}
};
let release = crate::lock_session(&st, &id).await?;
let Ok(_guard) = release.arc().try_lock() else {
return Err(crate::session_busy(&id));
};
let writer = st.writer.clone();
let _ = crate::run_compact(&st, &ctx, &writer, &id).await;
drop(_guard);
Ok(StatusCode::NO_CONTENT)
}
pub async fn wait(State(_): State<Arc<AppState>>, Path(_id): Path<String>) -> impl IntoResponse {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(
json!({"_tag": "ServiceUnavailableError", "message": "Session wait is not available yet", "service": "session.wait"}),
),
)
}
pub async fn revert_stage(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
Json(body): Json<Value>,
) -> Result<Json<Value>, HttpError> {
let mid = body.get("messageID").and_then(|v| v.as_str()).unwrap_or("");
if mid.is_empty() {
return Err(payload_err("messageID"));
}
let mut marker = json!({"messageID": mid, "diff": "", "files": []});
if let Some(p) = body.get("partID")
&& !p.is_null()
{
marker["partID"] = p.clone();
}
ocserve_store::set_session_revert(&st.writer, &id, Some(&marker)).map_err(internal)?;
Ok(Json(json!({"data": marker})))
}
pub async fn revert_clear(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<StatusCode, ApiError> {
ocserve_store::set_session_revert(&st.writer, &id, None).map_err(internal)?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn revert_commit(
State(_): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Result<StatusCode, ApiError> {
Ok(StatusCode::NO_CONTENT)
}
fn payload_err(key: &str) -> HttpError {
HttpError::TaggedData {
status: StatusCode::BAD_REQUEST,
tag: "InvalidRequestError",
fields: json!({"message": format!("Missing key\n at [\"{key}\"]"), "kind": "Payload"}),
}
}
pub async fn permission_reply(
State(st): State<Arc<AppState>>,
Path((_sid, rid)): Path<(String, String)>,
Json(body): Json<Value>,
) -> Result<Json<Value>, crate::HttpError> {
let reply = crate::validate_permission_reply(&body)?;
if st.gate.reply(&rid, reply) {
Ok(Json(json!({"data": {}})))
} else {
Err(crate::HttpError::TaggedData {
status: StatusCode::NOT_FOUND,
tag: "PermissionNotFoundError",
fields: json!({"requestID": rid, "message": format!("Permission request not found: {rid}")}),
})
}
}
pub async fn question_reply(
State(st): State<Arc<AppState>>,
Path((_sid, rid)): Path<(String, String)>,
Json(body): Json<Value>,
) -> Result<Json<Value>, crate::HttpError> {
crate::validate_request_prefix(&rid, "que")?;
let raw = body
.get("answers")
.and_then(|a| a.as_array())
.ok_or_else(|| {
crate::HttpError::Api(ApiError {
status: StatusCode::BAD_REQUEST,
name: "BadRequest",
message: "body must be {answers: [[String]]}".into(),
})
})?;
let mut answers: Vec<Vec<String>> = Vec::with_capacity(raw.len());
for a in raw {
answers.push(
a.as_array()
.map(|r| {
r.iter()
.map(|l| l.as_str().unwrap_or_default().to_string())
.collect()
})
.unwrap_or_default(),
);
}
if st.question_gate.reply(&rid, answers) {
Ok(Json(json!({"data": {}})))
} else {
Err(crate::question_not_found(&rid))
}
}
pub async fn question_reject(
State(st): State<Arc<AppState>>,
Path((_sid, rid)): Path<(String, String)>,
) -> Result<Json<Value>, crate::HttpError> {
crate::validate_request_prefix(&rid, "que")?;
if st.question_gate.reject(&rid) {
Ok(Json(json!({"data": {}})))
} else {
Err(crate::question_not_found(&rid))
}
}
pub async fn session_permission_one(
State(st): State<Arc<AppState>>,
Path((_sid, rid)): Path<(String, String)>,
) -> Result<Json<Value>, ApiError> {
if !rid.starts_with("per") {
return Err(ApiError {
status: StatusCode::BAD_REQUEST,
name: "InvalidRequestError",
message: format!("Expected a string starting with \"per\", got \"{rid}\""),
});
}
let found = st
.gate
.list()
.into_iter()
.find(|p| p.get("id").and_then(|v| v.as_str()) == Some(rid.as_str()));
match found {
Some(p) => Ok(Json(json!({"data": p}))),
None => Err(ApiError::not_found(format!(
"Permission request not found: {rid}"
))),
}
}
pub async fn permission_saved_delete(
State(_): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> StatusCode {
StatusCode::NO_CONTENT
}
pub async fn pty_list(State(st): State<Arc<AppState>>) -> impl IntoResponse {
Json(json!({"location": api_location(&st), "data": st.pty.list_running()}))
}
pub async fn pty_create(
State(st): State<Arc<AppState>>,
body: Option<Json<Value>>,
) -> impl IntoResponse {
let input = body.map(|b| b.0).unwrap_or_else(|| json!({}));
match st.pty.create(&input) {
Ok(info) => Json(json!({"location": api_location(&st), "data": info})).into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"name": "UnknownError", "data": {"message": e}})),
)
.into_response(),
}
}
pub async fn pty_get(State(st): State<Arc<AppState>>, Path(id): Path<String>) -> impl IntoResponse {
match st.pty.get_running(&id).ok() {
Some(info) => Json(json!({"location": api_location(&st), "data": info})).into_response(),
None => (StatusCode::NOT_FOUND, Json(json!({"name": "NotFoundError", "data": {"message": format!("pty not found: {id}")}}))).into_response(),
}
}
pub async fn pty_update(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
body: Option<Json<Value>>,
) -> impl IntoResponse {
let input = body.map(|b| b.0).unwrap_or_else(|| json!({}));
match st.pty.update(&id, &input) {
Ok(info) => Json(json!({"location": api_location(&st), "data": info})).into_response(),
Err(e) => (
StatusCode::NOT_FOUND,
Json(json!({"name": "NotFoundError", "data": {"message": attach_err_msg(&e, &id)}})),
)
.into_response(),
}
}
fn attach_err_msg(e: &ocserve_pty::AttachError, id: &str) -> String {
match e {
ocserve_pty::AttachError::NotFound => format!("pty not found: {id}"),
ocserve_pty::AttachError::Exited => format!("pty exited: {id}"),
}
}
pub async fn pty_remove(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> impl IntoResponse {
match st.pty.remove(&id) {
Ok(_) => Json(json!({"location": api_location(&st), "data": true})).into_response(),
Err(e) => (
StatusCode::NOT_FOUND,
Json(json!({"name": "NotFoundError", "data": {"message": attach_err_msg(&e, &id)}})),
)
.into_response(),
}
}
pub async fn pty_connect_token(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> impl IntoResponse {
let t = st.pty.issue_ticket(&id);
Json(json!({"location": api_location(&st), "data": t})).into_response()
}
pub async fn console_switch(Json(body): Json<Value>) -> impl IntoResponse {
if body.get("accountID").and_then(|v| v.as_str()).is_none() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"name": "BadRequest", "data": {"message": "Missing key\n at [\"accountID\"]", "kind": "Payload"}})),
)
.into_response();
}
Json(json!({})).into_response()
}
pub async fn workspace_sync_list() -> StatusCode {
StatusCode::NO_CONTENT
}
pub async fn workspace_warp(Json(body): Json<Value>) -> impl IntoResponse {
if body.get("id").and_then(|v| v.as_str()).is_none() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"name": "BadRequest", "data": {"message": "Missing key\n at [\"id\"]", "kind": "Payload"}})),
)
.into_response();
}
Json(json!({})).into_response()
}
pub async fn worktree_create(Json(_body): Json<Value>) -> impl IntoResponse {
(
StatusCode::BAD_REQUEST,
Json(
json!({"name": "WorktreeNotGitError", "data": {"message": "Worktrees are only supported for git projects"}}),
),
)
}
pub async fn worktree_reset(Json(body): Json<Value>) -> impl IntoResponse {
if body.get("directory").and_then(|v| v.as_str()).is_none() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"name": "BadRequest", "data": {"message": "Missing key\n at [\"directory\"]", "kind": "Payload"}})),
)
.into_response();
}
Json(json!({})).into_response()
}
pub async fn control_move_session(Json(body): Json<Value>) -> impl IntoResponse {
if body.get("sessionID").and_then(|v| v.as_str()).is_none() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"name": "BadRequest", "data": {"message": "Missing key\n at [\"sessionID\"]", "kind": "Payload"}})),
)
.into_response();
}
Json(json!({})).into_response()
}
pub async fn project_copy_create(
Path(_id): Path<String>,
Json(body): Json<Value>,
) -> impl IntoResponse {
if body.get("strategy").is_none() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"_tag": "InvalidRequestError", "message": "Missing key\n at [\"strategy\"]", "kind": "Payload"})),
)
.into_response();
}
Json(json!({})).into_response()
}
pub async fn project_copy_generate_name(Path(_id): Path<String>) -> impl IntoResponse {
const A: [&str; 8] = [
"glowing", "silent", "rapid", "amber", "quiet", "brave", "lunar", "vivid",
];
const B: [&str; 8] = [
"pixel", "river", "comet", "forest", "harbor", "ember", "meadow", "falcon",
];
let n = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as usize)
.unwrap_or(0);
Json(json!({"name": format!("{}-{}", A[n % A.len()], B[(n / A.len()) % B.len()])}))
}
pub async fn project_copy_refresh(Path(_id): Path<String>) -> StatusCode {
StatusCode::NO_CONTENT
}
pub async fn project_copy_remove(Path(_id): Path<String>) -> impl IntoResponse {
(
StatusCode::BAD_REQUEST,
Json(
json!({"_tag": "InvalidRequestError", "message": "Expected object, got undefined", "kind": "Payload"}),
),
)
}
pub async fn session_background(Path(_id): Path<String>) -> impl IntoResponse {
Json(json!(false))
}
pub async fn workspace_delete(Path(_id): Path<String>) -> StatusCode {
StatusCode::NO_CONTENT
}
pub async fn session_share(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<Value>, ApiError> {
let sid = id.clone();
run_blocking(&st.db, move |db| ocserve_store::load_session_wire(db, &sid))
.await
.map_err(internal)?
.map(Json)
.ok_or_else(|| ApiError::not_found(format!("Session not found: {id}")))
}
pub async fn session_unshare(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Json<Value>, ApiError> {
let sid = id.clone();
run_blocking(&st.db, move |db| ocserve_store::load_session_wire(db, &sid))
.await
.map_err(internal)?
.map(Json)
.ok_or_else(|| ApiError::not_found(format!("Session not found: {id}")))
}
pub async fn session_event(State(st): State<Arc<AppState>>) -> axum::response::Response {
crate::global_event(State(st)).await
}
pub async fn permission_create(
State(st): State<Arc<AppState>>,
Path(id): Path<String>,
Json(body): Json<Value>,
) -> Result<Json<Value>, HttpError> {
let action = body.get("action").and_then(|v| v.as_str());
let resources = body.get("resources").and_then(|v| v.as_array());
let (Some(action), Some(resources)) = (action, resources) else {
let key = if action.is_none() {
"action"
} else {
"resources"
};
return Err(payload_err(key));
};
if let Some(sv) = body.get("save").filter(|v| !v.is_null() && !v.is_array()) {
return Err(HttpError::TaggedData {
status: StatusCode::BAD_REQUEST,
tag: "InvalidRequestError",
fields: json!({
"message": format!("Expected array, got {sv}\n at [\"save\"]"),
"kind": "Payload",
}),
});
}
let agent = body
.get("agent")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from)
.unwrap_or_default();
let agent = if agent.is_empty() {
session_agent_async(&st, &id).await
} else {
agent
};
let rules = crate::agent_rules(&st, &agent);
let mut effect = "allow";
for r in resources {
let Some(res) = r.as_str() else { continue };
match ocserve_tools::evaluate(action, res, &rules).as_str() {
"deny" => {
effect = "deny";
break;
}
"ask" if effect != "deny" => effect = "ask",
_ => {}
}
}
let pid = format!("per_{}", ocserve_core::ids::ascending_tail());
if effect == "ask" {
let request = json!({
"id": pid,
"sessionID": id,
"action": action,
"resources": resources,
"patterns": resources,
"always": resources,
"metadata": body.get("metadata").cloned().unwrap_or(json!({})),
});
st.gate.register_persistent(&pid, request);
}
Ok(Json(json!({"data": {"id": pid, "effect": effect}})))
}
fn session_agent(st: &Arc<AppState>, session_id: &str) -> String {
if let Ok(conn) = ocserve_store::pragma::open_reader(&st.db)
&& let Ok(Some(raw)) = conn.query_row(
"SELECT agent FROM session WHERE id = ?1",
[session_id],
|r| r.get::<_, Option<String>>(0),
)
&& !raw.is_empty()
{
return raw;
}
st.llm.read().default_agent.clone()
}
async fn session_agent_async(st: &Arc<AppState>, session_id: &str) -> String {
let st2 = st.clone();
let sid = session_id.to_string();
tokio::task::spawn_blocking(move || session_agent(&st2, &sid))
.await
.unwrap_or_else(|_| st.llm.read().default_agent.clone())
}
pub async fn worktree_remove() -> impl IntoResponse {
(
StatusCode::BAD_REQUEST,
Json(
json!({"name": "BadRequest", "data": {"message": "Expected WorktreeRemoveInput, got undefined", "kind": "Payload"}}),
),
)
}
pub async fn patch_project(Path(id): Path<String>) -> impl IntoResponse {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
Json(json!({
"id": id,
"worktree": "/",
"time": {"created": now_ms, "updated": now_ms, "initialized": now_ms},
"sandboxes": [],
}))
}
pub async fn sync_start() -> impl IntoResponse {
Json(json!(true))
}
pub async fn git_init(State(st): State<Arc<AppState>>) -> impl IntoResponse {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
let worktree = st.paths["directory"].as_str().unwrap_or("/").to_string();
Json(json!({
"id": "global",
"worktree": worktree,
"vcs": "git",
"time": {"created": now_ms, "updated": now_ms, "initialized": now_ms},
"sandboxes": [],
}))
}
pub async fn credential_update(Path(_id): Path<String>) -> StatusCode {
StatusCode::NO_CONTENT
}
pub async fn credential_remove(Path(_id): Path<String>) -> StatusCode {
StatusCode::NO_CONTENT
}
pub async fn integration_connect_key(Path(_id): Path<String>) -> StatusCode {
StatusCode::NO_CONTENT
}
pub async fn integration_connect_oauth(Path(_id): Path<String>) -> impl IntoResponse {
(
StatusCode::UNAUTHORIZED,
Json(json!({"_tag": "UnauthorizedError", "message": "No OAuth credentials available"})),
)
}
pub async fn integration_attempt_get(Path(id): Path<String>) -> impl IntoResponse {
(
StatusCode::NOT_FOUND,
Json(json!({"_tag": "InvalidRequestError", "message": format!("Attempt not found: {id}")})),
)
}
pub async fn integration_attempt_delete(Path(id): Path<String>) -> impl IntoResponse {
(
StatusCode::NOT_FOUND,
Json(json!({"_tag": "InvalidRequestError", "message": format!("Attempt not found: {id}")})),
)
}
pub async fn integration_attempt_complete(Path(id): Path<String>) -> impl IntoResponse {
(
StatusCode::NOT_FOUND,
Json(json!({"_tag": "InvalidRequestError", "message": format!("Attempt not found: {id}")})),
)
}
pub async fn global_upgrade(Json(body): Json<Value>) -> impl IntoResponse {
if body.get("target").and_then(|v| v.as_str()).is_none() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"name": "BadRequest", "data": {"message": "Missing key\n at [\"target\"]", "kind": "Payload"}})),
)
.into_response();
}
(
StatusCode::BAD_REQUEST,
Json(json!({"name": "BadRequest", "data": {"message": "Upgrade is not supported", "kind": "Payload"}})),
)
.into_response()
}
pub async fn sync_replay(Json(_body): Json<Value>) -> impl IntoResponse {
Json(json!(true))
}
pub async fn sync_history(Json(_body): Json<Value>) -> impl IntoResponse {
Json(json!({"data": []}))
}
pub async fn sync_steal(Json(_body): Json<Value>) -> impl IntoResponse {
Json(json!(true))
}
pub async fn list_skills_raw(State(st): State<Arc<AppState>>) -> impl IntoResponse {
let payloads = st.payloads.read();
let data: Vec<Value> = payloads
.command
.iter()
.filter(|c| c.get("source").and_then(|v| v.as_str()) == Some("skill"))
.map(|c| {
json!({
"name": c.get("name").cloned().unwrap_or(Value::Null),
"description": c.get("description").cloned().unwrap_or(json!("")),
"location": "",
"content": c.get("template").cloned().unwrap_or(json!("")),
})
})
.collect();
Json(data)
}
pub async fn vcs_apply(Json(body): Json<Value>) -> impl IntoResponse {
if body.get("patch").and_then(|v| v.as_str()).is_none() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"name": "BadRequest", "data": {"message": "Missing key\n at [\"patch\"]", "kind": "Payload"}})),
)
.into_response();
}
(
StatusCode::BAD_REQUEST,
Json(json!({"name": "VcsApplyError", "data": {"message": "Patch can't be applied", "reason": "not-clean"}})),
)
.into_response()
}
pub async fn workspace_create(Json(body): Json<Value>) -> impl IntoResponse {
let Some(wtype) = body.get("type").and_then(|v| v.as_str()) else {
return (
StatusCode::BAD_REQUEST,
Json(json!({"name": "BadRequest", "data": {"message": "Missing key\n at [\"type\"]", "kind": "Payload"}})),
)
.into_response();
};
let known = matches!(wtype, "worktree" | "local" | "directory");
if !known {
return (
StatusCode::BAD_REQUEST,
Json(json!({"name": "WorkspaceCreateError", "data": {"message": format!("Unknown workspace adapter: {wtype}")}})),
)
.into_response();
}
Json(json!({"id": format!("wrk_{}", ocserve_core::ids::ascending_tail()), "type": wtype}))
.into_response()
}
pub async fn create_session(
State(st): State<Arc<AppState>>,
Json(body): Json<Value>,
) -> Result<Json<Value>, ApiError> {
let resolved = crate::resolve_create_dir_path(&st, &body, None).await;
let info = crate::create_session_record(&st, &body, resolved)?;
Ok(Json(json!({"data": info})))
}