Adopt uniform error response format across Arc API

All API errors now return a consistent JSON shape:
{"errors": [{"status": "4xx", "title": "...", "detail": "..."}]}

Introduces ApiError type with IntoResponse impl, replaces bare
StatusCode returns and ad-hoc {"error": "..."} responses in all
handlers and auth extractors. Updates OpenAPI spec and regenerates
TypeScript client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-04 21:32:24 -05:00
parent af7a2e52eb
commit 3e68c41f4e
21 changed files with 439 additions and 88 deletions

View file

@ -8,6 +8,7 @@ use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use crate::error::ApiError;
use crate::jwt_auth::AuthenticatedService;
use crate::server::AppState;
@ -126,7 +127,7 @@ pub async fn get_run_status(
}),
)
.into_response(),
None => StatusCode::NOT_FOUND.into_response(),
None => ApiError::not_found("Run not found.").into_response(),
}
}
@ -152,7 +153,7 @@ pub async fn run_events_stub(
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
StatusCode::GONE.into_response()
ApiError::new(StatusCode::GONE, "Event stream closed.").into_response()
}
pub async fn checkpoint_stub(
@ -196,11 +197,7 @@ pub async fn get_run_graph(
{
Ok(child) => child,
Err(_) => {
return (
StatusCode::BAD_GATEWAY,
Json(serde_json::json!({"error": "graphviz dot command not available"})),
)
.into_response();
return ApiError::new(StatusCode::BAD_GATEWAY, "Graphviz dot command not available.").into_response();
}
};
@ -216,11 +213,7 @@ pub async fn get_run_graph(
output.stdout,
)
.into_response(),
_ => (
StatusCode::BAD_GATEWAY,
Json(serde_json::json!({"error": "dot rendering failed"})),
)
.into_response(),
_ => ApiError::new(StatusCode::BAD_GATEWAY, "Dot rendering failed.").into_response(),
}
}
@ -417,7 +410,7 @@ pub async fn get_workflow(
) -> Response {
match workflows::detail(&name) {
Some(detail) => (StatusCode::OK, Json(detail)).into_response(),
None => StatusCode::NOT_FOUND.into_response(),
None => ApiError::not_found("Workflow not found.").into_response(),
}
}
@ -461,7 +454,7 @@ pub async fn get_verification_detail(
) -> Response {
match verifications::detail(&slug) {
Some(detail) => (StatusCode::OK, Json(detail)).into_response(),
None => StatusCode::NOT_FOUND.into_response(),
None => ApiError::not_found("Verification not found.").into_response(),
}
}
@ -501,7 +494,7 @@ pub async fn get_session(
) -> Response {
match sessions::detail(&id) {
Some(detail) => (StatusCode::OK, Json(detail)).into_response(),
None => StatusCode::NOT_FOUND.into_response(),
None => ApiError::not_found("Session not found.").into_response(),
}
}
@ -519,7 +512,7 @@ pub async fn session_events_stub(
Path(_id): Path<String>,
) -> Response {
// Return an empty SSE-like response
StatusCode::GONE.into_response()
ApiError::new(StatusCode::GONE, "Event stream closed.").into_response()
}
// ── Insights ───────────────────────────────────────────────────────────

View file

@ -0,0 +1,67 @@
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;
#[derive(Serialize)]
struct ErrorEntry {
status: String,
title: String,
detail: String,
}
#[derive(Serialize)]
struct ErrorBody {
errors: Vec<ErrorEntry>,
}
/// Uniform API error response.
///
/// Serializes to `{"errors": [{"status": "4xx", "title": "...", "detail": "..."}]}`.
pub struct ApiError {
status: StatusCode,
detail: String,
}
impl ApiError {
pub fn new(status: StatusCode, detail: impl Into<String>) -> Self {
Self {
status,
detail: detail.into(),
}
}
pub fn not_found(detail: impl Into<String>) -> Self {
Self::new(StatusCode::NOT_FOUND, detail)
}
pub fn bad_request(detail: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_REQUEST, detail)
}
pub fn unauthorized() -> Self {
Self::new(StatusCode::UNAUTHORIZED, "Authentication required.")
}
pub fn forbidden() -> Self {
Self::new(StatusCode::FORBIDDEN, "Access denied.")
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let title = self
.status
.canonical_reason()
.unwrap_or("Unknown")
.to_string();
let body = ErrorBody {
errors: vec![ErrorEntry {
status: self.status.as_u16().to_string(),
title,
detail: self.detail,
}],
};
(self.status, Json(body)).into_response()
}
}

View file

@ -2,12 +2,13 @@ use std::sync::Arc;
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum::http::StatusCode;
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
use rustls_pki_types::CertificateDer;
use serde::Deserialize;
use tracing::warn;
use crate::error::ApiError;
/// JWT claims for service-to-service authentication.
#[derive(Debug, Deserialize)]
struct Claims {
@ -148,23 +149,23 @@ fn try_jwt(
key: &DecodingKey,
validation: &Validation,
allowed_usernames: &[String],
) -> Result<(), StatusCode> {
) -> Result<(), ApiError> {
let header = parts
.headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
.ok_or_else(ApiError::unauthorized)?;
let token = header
.strip_prefix("Bearer ")
.ok_or(StatusCode::UNAUTHORIZED)?;
.ok_or_else(ApiError::unauthorized)?;
let token_data = jsonwebtoken::decode::<Claims>(token, key, validation)
.map_err(|_| StatusCode::UNAUTHORIZED)?;
.map_err(|_| ApiError::unauthorized())?;
// Fail closed: if no usernames are allowed, reject all requests
if allowed_usernames.is_empty() {
return Err(StatusCode::FORBIDDEN);
return Err(ApiError::forbidden());
}
// Extract GitHub username from sub claim URL (last path segment)
@ -173,38 +174,38 @@ fn try_jwt(
.sub
.as_deref()
.and_then(|s| s.rsplit('/').next())
.ok_or(StatusCode::FORBIDDEN)?;
.ok_or_else(ApiError::forbidden)?;
if !allowed_usernames.iter().any(|u| u == username) {
return Err(StatusCode::FORBIDDEN);
return Err(ApiError::forbidden());
}
Ok(())
}
/// Try to authenticate via mTLS peer certificates.
fn try_mtls(parts: &Parts) -> Result<(), StatusCode> {
fn try_mtls(parts: &Parts) -> Result<(), ApiError> {
let peer_certs = parts
.extensions
.get::<PeerCertificates>()
.and_then(|pc| pc.0.as_ref())
.ok_or(StatusCode::UNAUTHORIZED)?;
.ok_or_else(ApiError::unauthorized)?;
if peer_certs.is_empty() {
return Err(StatusCode::UNAUTHORIZED);
return Err(ApiError::unauthorized());
}
// Verify we can parse the leaf certificate and extract a CN
let cert = &peer_certs[0];
let (_, parsed) =
x509_parser::parse_x509_certificate(cert).map_err(|_| StatusCode::UNAUTHORIZED)?;
x509_parser::parse_x509_certificate(cert).map_err(|_| ApiError::unauthorized())?;
parsed
.subject()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
.ok_or_else(ApiError::unauthorized)?;
Ok(())
}
@ -217,7 +218,7 @@ fn try_mtls(parts: &Parts) -> Result<(), StatusCode> {
pub struct AuthenticatedService;
impl<S: Send + Sync> FromRequestParts<S> for AuthenticatedService {
type Rejection = StatusCode;
type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let auth_mode = parts
@ -231,10 +232,10 @@ impl<S: Send + Sync> FromRequestParts<S> for AuthenticatedService {
};
if strategies.is_empty() {
return Err(StatusCode::UNAUTHORIZED);
return Err(ApiError::unauthorized());
}
let mut last_err = StatusCode::UNAUTHORIZED;
let mut last_err = ApiError::unauthorized();
for strategy in strategies {
let result = match strategy {
@ -265,7 +266,7 @@ pub struct AuthenticatedUser {
}
impl<S: Send + Sync> FromRequestParts<S> for AuthenticatedUser {
type Rejection = StatusCode;
type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let auth_mode = parts
@ -283,10 +284,10 @@ impl<S: Send + Sync> FromRequestParts<S> for AuthenticatedUser {
};
if strategies.is_empty() {
return Err(StatusCode::UNAUTHORIZED);
return Err(ApiError::unauthorized());
}
let mut last_err = StatusCode::UNAUTHORIZED;
let mut last_err = ApiError::unauthorized();
for strategy in strategies {
match strategy {
@ -300,7 +301,7 @@ impl<S: Send + Sync> FromRequestParts<S> for AuthenticatedUser {
return Ok(AuthenticatedUser { login });
}
}
last_err = StatusCode::UNAUTHORIZED;
last_err = ApiError::unauthorized();
}
AuthStrategy::Mtls => {
if try_mtls(parts).is_ok() {
@ -308,7 +309,7 @@ impl<S: Send + Sync> FromRequestParts<S> for AuthenticatedUser {
return Ok(AuthenticatedUser { login });
}
}
last_err = StatusCode::UNAUTHORIZED;
last_err = ApiError::unauthorized();
}
}
}

View file

@ -1,4 +1,5 @@
mod demo;
pub mod error;
pub mod jwt_auth;
pub mod serve;
pub mod server;

View file

@ -16,6 +16,7 @@ use tracing::{error, info};
use arc_agent::LocalSandbox;
use crate::error::ApiError;
use crate::jwt_auth::{AuthMode, AuthenticatedService, AuthenticatedUser};
use arc_workflows::checkpoint::Checkpoint;
use arc_workflows::context::Context;
@ -191,7 +192,7 @@ pub fn build_router(state: Arc<AppState>, auth_mode: AuthMode) -> Router {
}
async fn not_implemented() -> Response {
StatusCode::NOT_IMPLEMENTED.into_response()
ApiError::new(StatusCode::NOT_IMPLEMENTED, "Not implemented.").into_response()
}
async fn root() -> Response {
@ -268,11 +269,7 @@ async fn start_run(
let graph = match arc_workflows::workflow::prepare_workflow(&req.dot_source) {
Ok(g) => g,
Err(e) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e.to_string()})),
)
.into_response();
return ApiError::bad_request(e.to_string()).into_response();
}
};
@ -416,7 +413,7 @@ async fn get_run_status(
}),
)
.into_response(),
None => StatusCode::NOT_FOUND.into_response(),
None => ApiError::not_found("Run not found.").into_response(),
}
}
@ -449,7 +446,7 @@ async fn get_questions(
.collect();
(StatusCode::OK, Json(questions)).into_response()
}
None => StatusCode::NOT_FOUND.into_response(),
None => ApiError::not_found("Run not found.").into_response(),
}
}
@ -474,11 +471,7 @@ async fn submit_answer(
match option {
Some(opt) => Answer::selected(key.clone(), opt),
None => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "invalid option key"})),
)
.into_response();
return ApiError::bad_request("Invalid option key.").into_response();
}
}
}
@ -487,7 +480,7 @@ async fn submit_answer(
let accepted = managed_run.interviewer.submit_answer(&qid, answer);
(StatusCode::OK, Json(SubmitAnswerResponse { accepted })).into_response()
}
None => StatusCode::NOT_FOUND.into_response(),
None => ApiError::not_found("Run not found.").into_response(),
}
}
@ -501,9 +494,9 @@ async fn get_events(
match runs.get(&id) {
Some(managed_run) => match &managed_run.event_tx {
Some(tx) => tx.subscribe(),
None => return StatusCode::GONE.into_response(),
None => return ApiError::new(StatusCode::GONE, "Event stream closed.").into_response(),
},
None => return StatusCode::NOT_FOUND.into_response(),
None => return ApiError::not_found("Run not found.").into_response(),
}
};
@ -532,7 +525,7 @@ async fn get_checkpoint(
Some(cp) => (StatusCode::OK, Json(cp.clone())).into_response(),
None => (StatusCode::OK, Json(serde_json::json!(null))).into_response(),
},
None => StatusCode::NOT_FOUND.into_response(),
None => ApiError::not_found("Run not found.").into_response(),
}
}
@ -547,7 +540,7 @@ async fn get_context(
Some(ctx) => (StatusCode::OK, Json(ctx.snapshot())).into_response(),
None => (StatusCode::OK, Json(serde_json::json!({}))).into_response(),
},
None => StatusCode::NOT_FOUND.into_response(),
None => ApiError::not_found("Run not found.").into_response(),
}
}
@ -560,11 +553,7 @@ async fn cancel_run(
match runs.get_mut(&id) {
Some(managed_run) => {
if managed_run.status != RunStatus::Running {
return (
StatusCode::CONFLICT,
Json(serde_json::json!({"error": "run is not running"})),
)
.into_response();
return ApiError::new(StatusCode::CONFLICT, "Run is not running.").into_response();
}
managed_run.cancel_token.store(true, Ordering::Relaxed);
if let Some(cancel_tx) = managed_run.cancel_tx.take() {
@ -573,7 +562,7 @@ async fn cancel_run(
managed_run.status = RunStatus::Cancelled;
(StatusCode::OK, Json(serde_json::json!({"cancelled": true}))).into_response()
}
None => StatusCode::NOT_FOUND.into_response(),
None => ApiError::not_found("Run not found.").into_response(),
}
}
@ -586,7 +575,7 @@ async fn get_retro(
let runs = state.runs.lock().expect("runs lock poisoned");
match runs.get(&id) {
Some(managed_run) => managed_run.logs_root.clone(),
None => return StatusCode::NOT_FOUND.into_response(),
None => return ApiError::not_found("Run not found.").into_response(),
}
};
@ -609,7 +598,7 @@ async fn get_graph(
let runs = state.runs.lock().expect("runs lock poisoned");
match runs.get(&id) {
Some(managed_run) => managed_run.dot_source.clone(),
None => return StatusCode::NOT_FOUND.into_response(),
None => return ApiError::not_found("Run not found.").into_response(),
}
};
@ -622,11 +611,7 @@ async fn get_graph(
{
Ok(child) => child,
Err(_) => {
return (
StatusCode::BAD_GATEWAY,
Json(serde_json::json!({"error": "graphviz dot command not available"})),
)
.into_response();
return ApiError::new(StatusCode::BAD_GATEWAY, "Graphviz dot command not available.").into_response();
}
};
@ -645,17 +630,11 @@ async fn get_graph(
.into_response(),
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
(
StatusCode::BAD_GATEWAY,
Json(serde_json::json!({"error": format!("dot failed: {stderr}")})),
)
.into_response()
ApiError::new(StatusCode::BAD_GATEWAY, format!("dot failed: {stderr}")).into_response()
}
Err(e) => {
ApiError::new(StatusCode::BAD_GATEWAY, format!("dot process error: {e}")).into_response()
}
Err(e) => (
StatusCode::BAD_GATEWAY,
Json(serde_json::json!({"error": format!("dot process error: {e}")})),
)
.into_response(),
}
}

View file

@ -91,6 +91,10 @@ paths:
$ref: "#/components/schemas/UserResponse"
"401":
description: Not authenticated
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ── Runs ──────────────────────────────────────────────────────────────
@ -148,6 +152,10 @@ paths:
$ref: "#/components/schemas/RunStatusResponse"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/cancel:
post:
@ -170,6 +178,10 @@ paths:
- cancelled
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"409":
description: Run is not running
content:
@ -193,6 +205,10 @@ paths:
type: string
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"502":
description: Graphviz not available
content:
@ -215,6 +231,10 @@ paths:
schema: {}
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/context:
get:
@ -232,6 +252,10 @@ paths:
type: object
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/events:
get:
@ -249,8 +273,16 @@ paths:
type: string
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"410":
description: Event stream closed
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/questions:
get:
@ -270,6 +302,10 @@ paths:
$ref: "#/components/schemas/ApiQuestion"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/questions/{qid}/answer:
post:
@ -304,6 +340,10 @@ paths:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/retro:
get:
@ -320,6 +360,10 @@ paths:
schema: {}
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/stages:
get:
@ -339,6 +383,10 @@ paths:
$ref: "#/components/schemas/RunStage"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/stages/{stageId}/turns:
get:
@ -363,6 +411,10 @@ paths:
$ref: "#/components/schemas/StageTurn"
"404":
description: Run or stage not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/files:
get:
@ -385,6 +437,10 @@ paths:
$ref: "#/components/schemas/RunFiles"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/usage:
get:
@ -402,6 +458,10 @@ paths:
$ref: "#/components/schemas/RunUsage"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/verifications:
get:
@ -421,6 +481,10 @@ paths:
$ref: "#/components/schemas/RunVerification"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/configuration:
get:
@ -438,6 +502,10 @@ paths:
type: string
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/steer:
post:
@ -466,6 +534,10 @@ paths:
- accepted
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/preview:
post:
@ -489,6 +561,10 @@ paths:
$ref: "#/components/schemas/PreviewUrlResponse"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ── Workflows ─────────────────────────────────────────────────────────
@ -527,6 +603,10 @@ paths:
$ref: "#/components/schemas/WorkflowDetail"
"404":
description: Workflow not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/workflows/{name}/runs:
get:
@ -550,6 +630,10 @@ paths:
$ref: "#/components/schemas/RunListItem"
"404":
description: Workflow not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
post:
operationId: startWorkflowRun
tags: [Workflows]
@ -569,6 +653,10 @@ paths:
$ref: "#/components/schemas/StartRunResponse"
"404":
description: Workflow not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ── Verifications ─────────────────────────────────────────────────────
@ -607,6 +695,10 @@ paths:
$ref: "#/components/schemas/VerificationDetailResponse"
"404":
description: Control not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ── Retros ────────────────────────────────────────────────────────────
@ -679,6 +771,10 @@ paths:
$ref: "#/components/schemas/SessionDetail"
"404":
description: Session not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/sessions/{id}/messages:
post:
@ -711,6 +807,10 @@ paths:
- accepted
"404":
description: Session not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/sessions/{id}/events:
get:
@ -732,6 +832,10 @@ paths:
type: string
"404":
description: Session not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ── Insights ──────────────────────────────────────────────────────────
@ -793,6 +897,10 @@ paths:
$ref: "#/components/schemas/SavedQuery"
"404":
description: Query not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
delete:
operationId: deleteSavedQuery
tags: [Insights]
@ -808,6 +916,10 @@ paths:
description: Query deleted
"404":
description: Query not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/insights/execute:
post:
@ -899,6 +1011,10 @@ paths:
$ref: "#/components/schemas/Branch"
"404":
description: Project not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
components:
parameters:
@ -1000,13 +1116,29 @@ components:
accepted:
type: boolean
ErrorResponseEntry:
type: object
required:
- status
- title
- detail
properties:
status:
type: string
title:
type: string
detail:
type: string
ErrorResponse:
type: object
required:
- error
- errors
properties:
error:
type: string
errors:
type: array
items:
$ref: "#/components/schemas/ErrorResponseEntry"
# ── New Run Schemas ─────────────────────────────────────────────────

View file

@ -91,6 +91,10 @@ paths:
$ref: "#/components/schemas/UserResponse"
"401":
description: Not authenticated
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ── Runs ──────────────────────────────────────────────────────────────
@ -148,6 +152,10 @@ paths:
$ref: "#/components/schemas/RunStatusResponse"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/cancel:
post:
@ -170,6 +178,10 @@ paths:
- cancelled
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"409":
description: Run is not running
content:
@ -193,6 +205,10 @@ paths:
type: string
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"502":
description: Graphviz not available
content:
@ -215,6 +231,10 @@ paths:
schema: {}
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/context:
get:
@ -232,6 +252,10 @@ paths:
type: object
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/events:
get:
@ -249,8 +273,16 @@ paths:
type: string
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"410":
description: Event stream closed
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/questions:
get:
@ -270,6 +302,10 @@ paths:
$ref: "#/components/schemas/ApiQuestion"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/questions/{qid}/answer:
post:
@ -304,6 +340,10 @@ paths:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/retro:
get:
@ -320,6 +360,10 @@ paths:
schema: {}
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/stages:
get:
@ -339,6 +383,10 @@ paths:
$ref: "#/components/schemas/RunStage"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/stages/{stageId}/turns:
get:
@ -363,6 +411,10 @@ paths:
$ref: "#/components/schemas/StageTurn"
"404":
description: Run or stage not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/files:
get:
@ -385,6 +437,10 @@ paths:
$ref: "#/components/schemas/RunFiles"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/usage:
get:
@ -402,6 +458,10 @@ paths:
$ref: "#/components/schemas/RunUsage"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/verifications:
get:
@ -421,6 +481,10 @@ paths:
$ref: "#/components/schemas/RunVerification"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/configuration:
get:
@ -438,6 +502,10 @@ paths:
type: string
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/steer:
post:
@ -466,6 +534,10 @@ paths:
- accepted
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/runs/{id}/preview:
post:
@ -489,6 +561,10 @@ paths:
$ref: "#/components/schemas/PreviewUrlResponse"
"404":
description: Run not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ── Workflows ─────────────────────────────────────────────────────────
@ -527,6 +603,10 @@ paths:
$ref: "#/components/schemas/WorkflowDetail"
"404":
description: Workflow not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/workflows/{name}/runs:
get:
@ -550,6 +630,10 @@ paths:
$ref: "#/components/schemas/RunListItem"
"404":
description: Workflow not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
post:
operationId: startWorkflowRun
tags: [Workflows]
@ -569,6 +653,10 @@ paths:
$ref: "#/components/schemas/StartRunResponse"
"404":
description: Workflow not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ── Verifications ─────────────────────────────────────────────────────
@ -607,6 +695,10 @@ paths:
$ref: "#/components/schemas/VerificationDetailResponse"
"404":
description: Control not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ── Retros ────────────────────────────────────────────────────────────
@ -679,6 +771,10 @@ paths:
$ref: "#/components/schemas/SessionDetail"
"404":
description: Session not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/sessions/{id}/messages:
post:
@ -711,6 +807,10 @@ paths:
- accepted
"404":
description: Session not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/sessions/{id}/events:
get:
@ -732,6 +832,10 @@ paths:
type: string
"404":
description: Session not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ── Insights ──────────────────────────────────────────────────────────
@ -793,6 +897,10 @@ paths:
$ref: "#/components/schemas/SavedQuery"
"404":
description: Query not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
delete:
operationId: deleteSavedQuery
tags: [Insights]
@ -808,6 +916,10 @@ paths:
description: Query deleted
"404":
description: Query not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/insights/execute:
post:
@ -899,6 +1011,10 @@ paths:
$ref: "#/components/schemas/Branch"
"404":
description: Project not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
components:
parameters:
@ -1000,13 +1116,29 @@ components:
accepted:
type: boolean
ErrorResponseEntry:
type: object
required:
- status
- title
- detail
properties:
status:
type: string
title:
type: string
detail:
type: string
ErrorResponse:
type: object
required:
- error
- errors
properties:
error:
type: string
errors:
type: array
items:
$ref: "#/components/schemas/ErrorResponseEntry"
# ── New Run Schemas ─────────────────────────────────────────────────

View file

@ -28,6 +28,7 @@ models/create-session-request.ts
models/create-session-response.ts
models/diff-file.ts
models/diff-stats.ts
models/error-response-entry.ts
models/error-response.ts
models/evaluation-result.ts
models/execute-query-request.ts

View file

@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { HealthResponse } from '../models';
// @ts-ignore
import type { RootResponse } from '../models';

View file

@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { ExecuteQueryRequest } from '../models';
// @ts-ignore
import type { ExecuteQueryResponse } from '../models';
@ -94,6 +96,7 @@ export const InsightsApiAxiosParamCreator = function (configuration?: Configurat
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Accept'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};

View file

@ -24,6 +24,8 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError
// @ts-ignore
import type { Branch } from '../models';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { Project } from '../models';
/**
* ProjectsApi - axios parameter creator

View file

@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { RetroListItem } from '../models';
/**
* RetrosApi - axios parameter creator

View file

@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { RunStage } from '../models';
// @ts-ignore
import type { StageTurn } from '../models';
@ -159,7 +161,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Accept'] = 'text/plain';
localVarHeaderParameter['Accept'] = 'text/plain,application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};

View file

@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { RunFiles } from '../models';
// @ts-ignore
import type { RunUsage } from '../models';

View file

@ -228,7 +228,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Accept'] = 'text/event-stream';
localVarHeaderParameter['Accept'] = 'text/event-stream,application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};

View file

@ -26,6 +26,8 @@ import type { CreateSessionRequest } from '../models';
// @ts-ignore
import type { CreateSessionResponse } from '../models';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { SendMessageRequest } from '../models';
// @ts-ignore
import type { SessionDetail } from '../models';
@ -199,7 +201,7 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Accept'] = 'text/event-stream';
localVarHeaderParameter['Accept'] = 'text/event-stream,application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};

View file

@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { VerificationCategory } from '../models';
// @ts-ignore
import type { VerificationDetailResponse } from '../models';

View file

@ -22,6 +22,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { RunListItem } from '../models';
// @ts-ignore
import type { StartRunResponse } from '../models';

View file

@ -0,0 +1,22 @@
/* tslint:disable */
/* eslint-disable */
/**
* Arc Run API
* HTTP API for managing Arc workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface ErrorResponseEntry {
'status': string;
'title': string;
'detail': string;
}

View file

@ -13,8 +13,11 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { ErrorResponseEntry } from './error-response-entry';
export interface ErrorResponse {
'error': string;
'errors': Array<ErrorResponseEntry>;
}

View file

@ -12,6 +12,7 @@ export * from './create-session-response';
export * from './diff-file';
export * from './diff-stats';
export * from './error-response';
export * from './error-response-entry';
export * from './evaluation-result';
export * from './execute-query-request';
export * from './execute-query-response';