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