Separate Console image bodies from session snapshots
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user