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