Skip to main content

systemprompt_api/routes/agent/
artifacts.rs

1//! Artifact retrieval routes for the agent surface.
2//!
3//! Handlers list artifacts by context, task, or user, fetch a single artifact,
4//! and render an artifact as MCP App UI. Every accessor enforces ownership
5//! against the authenticated [`RequestContext`] before returning data.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use axum::extract::{Path, Query, State};
11use axum::http::{StatusCode, header};
12use axum::response::{IntoResponse, Response};
13use axum::{Extension, Json};
14use serde::Deserialize;
15
16use systemprompt_identifiers::{ArtifactId, ContextId, TaskId, UserId};
17use systemprompt_mcp::services::ui_renderer::MCP_APP_MIME_TYPE;
18use systemprompt_mcp::services::ui_renderer::registry::{
19    create_default_registry, resolve_artifact_type,
20};
21use systemprompt_models::RequestContext;
22use systemprompt_runtime::AppContext;
23
24use crate::error::ApiHttpError;
25
26#[derive(Debug, Clone, Copy, Deserialize)]
27pub struct ArtifactQueryParams {
28    pub limit: Option<u32>,
29}
30
31pub async fn list_artifacts_by_context(
32    Extension(req_ctx): Extension<RequestContext>,
33    State(app_context): State<AppContext>,
34    Path(context_id): Path<String>,
35) -> Result<impl IntoResponse, ApiHttpError> {
36    tracing::debug!(context_id = %context_id, "Listing artifacts by context");
37
38    let context_id_typed = ContextId::try_new(&context_id)
39        .map_err(|e| ApiHttpError::bad_request(format!("invalid context id: {e}")))?;
40
41    let context_repo = app_context.a2a_repositories().contexts.clone();
42    context_repo
43        .validate_context_ownership(&context_id_typed, req_ctx.user_id())
44        .await?;
45
46    let artifact_repo = app_context.a2a_repositories().artifacts.clone();
47    let artifacts = artifact_repo
48        .get_artifacts_by_context(&context_id_typed)
49        .await?;
50
51    tracing::debug!(
52        context_id = %context_id,
53        count = artifacts.len(),
54        "Artifacts listed"
55    );
56    Ok((StatusCode::OK, Json(artifacts)))
57}
58
59pub async fn list_artifacts_by_task(
60    Extension(req_ctx): Extension<RequestContext>,
61    State(app_context): State<AppContext>,
62    Path(task_id): Path<String>,
63) -> Result<impl IntoResponse, ApiHttpError> {
64    tracing::debug!(task_id = %task_id, "Listing artifacts by task");
65
66    let task_id_typed = TaskId::new(&task_id);
67
68    let task_repo = app_context.a2a_repositories().tasks.clone();
69    task_repo
70        .validate_task_ownership(&task_id_typed, req_ctx.user_id())
71        .await?;
72
73    let artifact_repo = app_context.a2a_repositories().artifacts.clone();
74    let artifacts = artifact_repo.get_artifacts_by_task(&task_id_typed).await?;
75
76    tracing::debug!(
77        task_id = %task_id,
78        count = artifacts.len(),
79        "Artifacts listed"
80    );
81    Ok((StatusCode::OK, Json(artifacts)))
82}
83
84pub async fn get_artifact(
85    Extension(req_ctx): Extension<RequestContext>,
86    State(app_context): State<AppContext>,
87    Path(artifact_id): Path<String>,
88) -> Result<impl IntoResponse, ApiHttpError> {
89    tracing::debug!(artifact_id = %artifact_id, "Retrieving artifact");
90
91    let artifact_repo = app_context.a2a_repositories().artifacts.clone();
92
93    let artifact_id_typed = ArtifactId::new(&artifact_id);
94    artifact_repo
95        .validate_artifact_ownership(&artifact_id_typed, req_ctx.user_id())
96        .await?;
97
98    let artifact = artifact_repo
99        .get_artifact_by_id(&artifact_id_typed)
100        .await?
101        .ok_or_else(|| ApiHttpError::not_found(format!("Artifact '{artifact_id}' not found")))?;
102
103    tracing::debug!("Artifact retrieved successfully");
104    Ok((StatusCode::OK, Json(artifact)))
105}
106
107pub async fn list_artifacts_by_user(
108    Extension(req_ctx): Extension<RequestContext>,
109    State(app_context): State<AppContext>,
110    Query(params): Query<ArtifactQueryParams>,
111) -> Result<impl IntoResponse, ApiHttpError> {
112    let user_id = req_ctx.auth.actor.user_id.as_str();
113
114    tracing::debug!(user_id = %user_id, "Listing artifacts by user");
115
116    let artifact_repo = app_context.a2a_repositories().artifacts.clone();
117
118    let user_id_typed = UserId::new(user_id);
119    let artifacts = artifact_repo
120        .get_artifacts_by_user_id(&user_id_typed, params.limit.map(|l| l as i32))
121        .await?;
122
123    tracing::debug!(
124        user_id = %user_id,
125        count = artifacts.len(),
126        "Artifacts listed"
127    );
128    Ok((StatusCode::OK, Json(artifacts)))
129}
130
131pub async fn get_artifact_ui(
132    Extension(req_ctx): Extension<RequestContext>,
133    State(app_context): State<AppContext>,
134    Path(artifact_id): Path<String>,
135) -> Result<Response, ApiHttpError> {
136    tracing::debug!(artifact_id = %artifact_id, "Rendering artifact as MCP App UI");
137
138    let artifact_repo = app_context.a2a_repositories().artifacts.clone();
139    let artifact_id_typed = ArtifactId::new(&artifact_id);
140
141    artifact_repo
142        .validate_artifact_ownership(&artifact_id_typed, req_ctx.user_id())
143        .await?;
144
145    let artifact = artifact_repo
146        .get_artifact_by_id(&artifact_id_typed)
147        .await?
148        .ok_or_else(|| ApiHttpError::not_found(format!("Artifact '{artifact_id}' not found")))?;
149
150    let registry = create_default_registry();
151    let artifact_type = resolve_artifact_type(&artifact);
152
153    if !registry.supports(artifact_type) {
154        tracing::warn!(artifact_type = %artifact_type, "No UI renderer for artifact type");
155        return Err(ApiHttpError::bad_request(format!(
156            "No UI renderer available for artifact type '{artifact_type}'"
157        )));
158    }
159
160    let ui_resource: systemprompt_mcp::services::ui_renderer::UiResource = registry
161        .render(&artifact)
162        .await
163        .map_err(|e| ApiHttpError::internal_error(format!("Failed to render artifact UI: {e}")))?;
164
165    tracing::debug!(artifact_id = %artifact_id, "Artifact UI rendered successfully");
166
167    Response::builder()
168        .status(StatusCode::OK)
169        .header(header::CONTENT_TYPE, MCP_APP_MIME_TYPE)
170        .header(
171            header::CONTENT_SECURITY_POLICY,
172            ui_resource.csp.to_header_value(),
173        )
174        .header(header::X_FRAME_OPTIONS, "SAMEORIGIN")
175        .body(axum::body::Body::from(ui_resource.html))
176        .map_err(|e| ApiHttpError::internal_error(format!("Failed to build response: {e}")))
177}