Separate Console image bodies from session snapshots

This commit is contained in:
2026-10-04 12:56:40 +09:00
parent f7b722246d
commit 2eb9418a4e
20 changed files with 1021 additions and 31 deletions
+23
View File
@@ -514,6 +514,13 @@ pub struct WorkerSessionHistoryRequest {
pub limit: Option<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WorkerSessionAttachmentRequest {
pub worker_ref: WorkerRef,
pub session_id: String,
pub attachment_id: String,
}
pub trait WorkerExecutionBackend: Send + Sync + 'static {
fn backend_id(&self) -> &str;
@@ -544,6 +551,14 @@ pub trait WorkerExecutionBackend: Send + Sync + 'static {
}
}
fn worker_session_attachment(
&self,
_request: WorkerSessionAttachmentRequest,
) -> Result<session_store::RetainedSessionAttachment, session_store::RetainedAttachmentReadError>
{
Err(session_store::RetainedAttachmentReadError::RetentionMissing)
}
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult;
/// Read-only restore validation performed before any live controller,
@@ -780,6 +795,14 @@ impl WorkerExecutionBackendRef {
self.backend.worker_session_history(request)
}
pub(crate) fn worker_session_attachment(
&self,
request: WorkerSessionAttachmentRequest,
) -> Result<session_store::RetainedSessionAttachment, session_store::RetainedAttachmentReadError>
{
self.backend.worker_session_attachment(request)
}
pub(crate) fn spawn_worker(
&self,
request: WorkerExecutionSpawnRequest,
+69 -1
View File
@@ -336,6 +336,13 @@ fn runtime_http_router_with_auth_and_ssh_keyscan_program(
.route(
remaining_route(runtime_api::RUNTIME_ROUTE_WORKER_ATTACHMENT, &["DELETE"]),
delete(delete_worker_uploaded_file),
)
.route(
remaining_route(
runtime_api::RUNTIME_ROUTE_WORKER_SESSION_ATTACHMENT,
&["GET"],
),
get(get_worker_session_attachment),
);
#[cfg(feature = "ws-server")]
@@ -1980,6 +1987,60 @@ async fn delete_worker_uploaded_file(
}))
}
async fn get_worker_session_attachment(
State(state): State<RuntimeHttpState>,
auth: Option<Extension<RuntimeAuthContext>>,
Path((worker_id, session_id, attachment_id)): Path<(String, String, String)>,
Query(request): Query<runtime_api::WorkerSessionRequest>,
) -> Result<Response, RuntimeHttpRestError> {
let worker_ref = worker_ref_for(&state.runtime, worker_id)?;
let scope = auth_workspace_scope(&state, auth.as_ref())?.ok_or_else(|| {
RuntimeHttpRestError::new(
StatusCode::FORBIDDEN,
"runtime_worker_session_scope_required",
"Worker Session attachment reads require a Workspace-scoped capability",
)
})?;
if scope.workspace_id != request.workspace_id {
return Err(RuntimeHttpRestError::new(
StatusCode::FORBIDDEN,
"runtime_worker_session_workspace_scope_mismatch",
"Worker Session Workspace scope does not match the authenticated capability",
));
}
let attachment = state
.runtime
.worker_session_attachment_scoped(&scope, &worker_ref, session_id, attachment_id)
.map_err(|error| {
use session_store::RetainedAttachmentReadError as Error;
let (status, code) = match error {
Error::RetentionMissing | Error::ActivePointerMissing => {
(StatusCode::GONE, "session_attachment_expired")
}
Error::SessionMismatch | Error::NotFound => {
(StatusCode::NOT_FOUND, "session_attachment_not_found")
}
Error::ResourceLimit => (
StatusCode::PAYLOAD_TOO_LARGE,
"session_attachment_too_large",
),
Error::MigrationRequired | Error::CorruptLog | Error::StorageUnavailable => (
StatusCode::SERVICE_UNAVAILABLE,
"session_attachment_unavailable",
),
};
RuntimeHttpRestError::new(status, code, error.to_string())
})?;
let content_type = axum::http::HeaderValue::from_str(&attachment.media_type).map_err(|_| {
RuntimeHttpRestError::new(
StatusCode::SERVICE_UNAVAILABLE,
"session_attachment_media_type_invalid",
"Session attachment media type is invalid",
)
})?;
Ok(([(header::CONTENT_TYPE, content_type)], attachment.data).into_response())
}
#[allow(dead_code)]
async fn stop_worker(
State(state): State<RuntimeHttpState>,
@@ -2289,7 +2350,7 @@ fn required_runtime_permission(method: &Method, path: &str) -> Option<&'static s
}
if path.ends_with("/input")
|| path.ends_with("/restore")
|| path.contains("/attachments")
|| (path.contains("/attachments") && *method != Method::GET)
|| path.contains("/workdir-attachments")
{
return Some("workers:input");
@@ -3031,6 +3092,13 @@ mod tests {
),
Some("workers:input")
);
assert_eq!(
required_runtime_permission(
&Method::GET,
"/v1/workers/7/sessions/session-1/attachments/attachment-1"
),
Some("workers:read")
);
assert_eq!(
required_runtime_permission(&Method::POST, "/v1/workers/7/workdir-attachments"),
Some("workers:input")
+33
View File
@@ -1673,6 +1673,39 @@ impl Runtime {
})
}
pub fn worker_session_attachment_scoped(
&self,
scope: &RuntimeWorkspaceScope,
worker_ref: &WorkerRef,
session_id: String,
attachment_id: String,
) -> Result<session_store::RetainedSessionAttachment, session_store::RetainedAttachmentReadError>
{
let operation_lock = self
.worker_operation_lock(worker_ref.worker_id)
.map_err(|_| session_store::RetainedAttachmentReadError::RetentionMissing)?;
let _operation_guard = operation_lock
.lock()
.map_err(|_| session_store::RetainedAttachmentReadError::StorageUnavailable)?;
self.ensure_worker_in_workspace(scope, worker_ref)
.map_err(|_| session_store::RetainedAttachmentReadError::NotFound)?;
let backend = {
let state = self
.lock()
.map_err(|_| session_store::RetainedAttachmentReadError::StorageUnavailable)?;
state
.worker(worker_ref)
.map_err(|_| session_store::RetainedAttachmentReadError::NotFound)?;
state.execution_backend.clone()
}
.ok_or(session_store::RetainedAttachmentReadError::StorageUnavailable)?;
backend.worker_session_attachment(crate::execution::WorkerSessionAttachmentRequest {
worker_ref: worker_ref.clone(),
session_id,
attachment_id,
})
}
/// Attach a live execution through a workspace-scoped Runtime authorization context.
pub fn restore_worker_scoped(
&self,
@@ -186,6 +186,16 @@ pub trait RuntimeWorkerFactory: Send + Sync + 'static {
Err(session_store::RetainedHistoryReadError::RetentionMissing)
}
fn retained_session_attachment(
&self,
_worker_ref: &WorkerRef,
_session_id: &str,
_attachment_id: &str,
) -> Result<session_store::RetainedSessionAttachment, session_store::RetainedAttachmentReadError>
{
Err(session_store::RetainedAttachmentReadError::RetentionMissing)
}
async fn spawn_controller(
&self,
request: WorkerExecutionSpawnRequest,
@@ -1116,6 +1126,27 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
)
}
fn retained_session_attachment(
&self,
worker_ref: &WorkerRef,
session_id: &str,
attachment_id: &str,
) -> Result<session_store::RetainedSessionAttachment, session_store::RetainedAttachmentReadError>
{
let aggregate_dir = self
.worker_aggregate_dir(worker_ref)
.map_err(|_| session_store::RetainedAttachmentReadError::RetentionMissing)?;
let worker_name = Self::runtime_worker_name_for_ref(worker_ref);
session_store::read_retained_session_attachment(
&aggregate_dir,
&worker_name,
session_id,
attachment_id,
session_store::DEFAULT_RETAINED_HISTORY_MAX_SCAN_BYTES,
10 * 1024 * 1024,
)
}
fn observe_workspace_prompt_projection(
&self,
projection: worker::WorkspacePromptProjection,
@@ -2412,6 +2443,18 @@ where
}
}
fn worker_session_attachment(
&self,
request: crate::execution::WorkerSessionAttachmentRequest,
) -> Result<session_store::RetainedSessionAttachment, session_store::RetainedAttachmentReadError>
{
self.factory.retained_session_attachment(
&request.worker_ref,
&request.session_id,
&request.attachment_id,
)
}
fn observe_workspace_prompt_projection(
&self,
projection: worker::WorkspacePromptProjection,