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