fix: pass mission_id to file uploads for mission-specific context directories
Files uploaded to ./context/ were going to the workspace context directory
instead of the mission-specific context directory at /root/context/{mission_id}/
- Add mission_id parameter to PathQuery, FinalizeUploadRequest, DownloadUrlRequest
- Update resolve_path_for_workspace to resolve context paths to mission-specific dirs
- Update dashboard uploadFile, uploadFileChunked, finalizeChunkedUpload, downloadFromUrl
- Pass mission.id from control-client.tsx when uploading files
This commit is contained in:
@@ -2243,9 +2243,10 @@ export default function ControlClient() {
|
||||
// Upload into the workspace-local ./context (symlinked to mission context inside the container).
|
||||
const contextPath = "./context/";
|
||||
|
||||
// Get workspace_id from current or viewing mission
|
||||
// Get workspace_id and mission_id from current or viewing mission
|
||||
const mission = viewingMission ?? currentMission;
|
||||
const workspaceId = mission?.workspace_id;
|
||||
const missionId = mission?.id;
|
||||
|
||||
// Use chunked upload for files > 10MB, regular for smaller
|
||||
const useChunked = fileToUpload.size > 10 * 1024 * 1024;
|
||||
@@ -2253,10 +2254,10 @@ export default function ControlClient() {
|
||||
const result = useChunked
|
||||
? await uploadFileChunked(fileToUpload, contextPath, (progress) => {
|
||||
setUploadProgress({ fileName: displayName, progress });
|
||||
}, workspaceId)
|
||||
}, workspaceId, missionId)
|
||||
: await uploadFile(fileToUpload, contextPath, (progress) => {
|
||||
setUploadProgress({ fileName: displayName, progress });
|
||||
}, workspaceId);
|
||||
}, workspaceId, missionId);
|
||||
|
||||
toast.success(`Uploaded ${result.name}`);
|
||||
|
||||
@@ -2282,11 +2283,12 @@ export default function ControlClient() {
|
||||
try {
|
||||
const contextPath = "./context/";
|
||||
|
||||
// Get workspace_id from current or viewing mission
|
||||
// Get workspace_id and mission_id from current or viewing mission
|
||||
const mission = viewingMission ?? currentMission;
|
||||
const workspaceId = mission?.workspace_id;
|
||||
const missionId = mission?.id;
|
||||
|
||||
const result = await downloadFromUrl(urlInput.trim(), contextPath, undefined, workspaceId);
|
||||
const result = await downloadFromUrl(urlInput.trim(), contextPath, undefined, workspaceId, missionId);
|
||||
toast.success(`Downloaded ${result.name}`);
|
||||
|
||||
// Add a message about the download at the beginning (consistent with uploads)
|
||||
|
||||
@@ -1036,7 +1036,8 @@ export function uploadFile(
|
||||
file: File,
|
||||
remotePath: string = "./context/",
|
||||
onProgress?: (progress: UploadProgress) => void,
|
||||
workspaceId?: string
|
||||
workspaceId?: string,
|
||||
missionId?: string
|
||||
): Promise<UploadResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
@@ -1044,6 +1045,9 @@ export function uploadFile(
|
||||
if (workspaceId) {
|
||||
params.append("workspace_id", workspaceId);
|
||||
}
|
||||
if (missionId) {
|
||||
params.append("mission_id", missionId);
|
||||
}
|
||||
const url = apiUrl(`/api/fs/upload?${params}`);
|
||||
|
||||
// Track upload progress
|
||||
@@ -1103,7 +1107,8 @@ export async function uploadFileChunked(
|
||||
file: File,
|
||||
remotePath: string = "./context/",
|
||||
onProgress?: (progress: ChunkedUploadProgress) => void,
|
||||
workspaceId?: string
|
||||
workspaceId?: string,
|
||||
missionId?: string
|
||||
): Promise<UploadResult> {
|
||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||
const uploadId = `${file.name}-${file.size}-${Date.now()}`;
|
||||
@@ -1114,7 +1119,7 @@ export async function uploadFileChunked(
|
||||
...p,
|
||||
chunkIndex: 0,
|
||||
totalChunks: 1,
|
||||
}) : undefined, workspaceId);
|
||||
}) : undefined, workspaceId, missionId);
|
||||
}
|
||||
|
||||
let uploadedBytes = 0;
|
||||
@@ -1152,7 +1157,7 @@ export async function uploadFileChunked(
|
||||
}
|
||||
|
||||
// Finalize the upload
|
||||
return finalizeChunkedUpload(remotePath, uploadId, file.name, totalChunks, workspaceId);
|
||||
return finalizeChunkedUpload(remotePath, uploadId, file.name, totalChunks, workspaceId, missionId);
|
||||
}
|
||||
|
||||
async function uploadChunk(
|
||||
@@ -1192,7 +1197,8 @@ async function finalizeChunkedUpload(
|
||||
uploadId: string,
|
||||
fileName: string,
|
||||
totalChunks: number,
|
||||
workspaceId?: string
|
||||
workspaceId?: string,
|
||||
missionId?: string
|
||||
): Promise<UploadResult> {
|
||||
const body: Record<string, unknown> = {
|
||||
path: remotePath,
|
||||
@@ -1203,6 +1209,9 @@ async function finalizeChunkedUpload(
|
||||
if (workspaceId) {
|
||||
body.workspace_id = workspaceId;
|
||||
}
|
||||
if (missionId) {
|
||||
body.mission_id = missionId;
|
||||
}
|
||||
|
||||
const res = await apiFetch("/api/fs/upload-finalize", {
|
||||
method: "POST",
|
||||
@@ -1222,7 +1231,8 @@ export async function downloadFromUrl(
|
||||
url: string,
|
||||
remotePath: string = "./context/",
|
||||
fileName?: string,
|
||||
workspaceId?: string
|
||||
workspaceId?: string,
|
||||
missionId?: string
|
||||
): Promise<UploadResult> {
|
||||
const body: Record<string, unknown> = {
|
||||
url,
|
||||
@@ -1232,6 +1242,9 @@ export async function downloadFromUrl(
|
||||
if (workspaceId) {
|
||||
body.workspace_id = workspaceId;
|
||||
}
|
||||
if (missionId) {
|
||||
body.mission_id = missionId;
|
||||
}
|
||||
|
||||
const res = await apiFetch("/api/fs/download-url", {
|
||||
method: "POST",
|
||||
|
||||
@@ -162,10 +162,12 @@ fn content_type_for_path(path: &Path) -> &'static str {
|
||||
}
|
||||
|
||||
/// Resolve a path relative to a specific workspace.
|
||||
/// If mission_id is provided and path is a context path, resolves to mission-specific context.
|
||||
async fn resolve_path_for_workspace(
|
||||
state: &Arc<AppState>,
|
||||
workspace_id: uuid::Uuid,
|
||||
path: &str,
|
||||
mission_id: Option<uuid::Uuid>,
|
||||
) -> Result<PathBuf, (StatusCode, String)> {
|
||||
let workspace = state
|
||||
.workspaces
|
||||
@@ -191,12 +193,23 @@ async fn resolve_path_for_workspace(
|
||||
let resolved = if input.is_absolute() {
|
||||
input.to_path_buf()
|
||||
} else if path.starts_with("./context") || path.starts_with("context") {
|
||||
// For "context" paths, use the workspace's context directory
|
||||
// For "context" paths, use the mission-specific context directory if mission_id provided
|
||||
let suffix = path
|
||||
.trim_start_matches("./")
|
||||
.trim_start_matches("context/")
|
||||
.trim_start_matches("context");
|
||||
let context_path = workspace_root.join("context");
|
||||
|
||||
// If mission_id is provided, use mission-specific context directory
|
||||
// This ensures uploaded files go to the right place for the agent to find them
|
||||
let context_path = if let Some(mid) = mission_id {
|
||||
// Mission context is at /root/context/{mission_id} (or workspace equivalent)
|
||||
// For host workspaces, the global context root is typically at working_dir/context
|
||||
let context_root = state.config.working_dir.join("context");
|
||||
context_root.join(mid.to_string())
|
||||
} else {
|
||||
workspace_root.join("context")
|
||||
};
|
||||
|
||||
if suffix.is_empty() {
|
||||
context_path
|
||||
} else {
|
||||
@@ -244,14 +257,18 @@ async fn resolve_path_for_workspace(
|
||||
}
|
||||
};
|
||||
|
||||
// Validate that the resolved path is within the workspace
|
||||
if !canonical.starts_with(&workspace_root) {
|
||||
// Validate that the resolved path is within an allowed location
|
||||
// This can be either the workspace root or the global context directory for missions
|
||||
let context_root = state.config.working_dir.join("context");
|
||||
let in_workspace = canonical.starts_with(&workspace_root);
|
||||
let in_context = mission_id.is_some() && canonical.starts_with(&context_root);
|
||||
|
||||
if !in_workspace && !in_context {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
format!(
|
||||
"Path traversal attempt: {} is outside workspace {}",
|
||||
"Path traversal attempt: {} is outside allowed directories",
|
||||
canonical.display(),
|
||||
workspace_root.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -421,6 +438,8 @@ pub struct PathQuery {
|
||||
pub path: String,
|
||||
/// Optional workspace ID to resolve relative paths against
|
||||
pub workspace_id: Option<uuid::Uuid>,
|
||||
/// Optional mission ID for mission-specific context directories
|
||||
pub mission_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -556,8 +575,9 @@ pub async fn upload(
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
// If workspace_id is provided, resolve path relative to that workspace
|
||||
// If mission_id is also provided, context paths resolve to mission-specific directory
|
||||
let base = if let Some(workspace_id) = q.workspace_id {
|
||||
resolve_path_for_workspace(&state, workspace_id, &q.path).await?
|
||||
resolve_path_for_workspace(&state, workspace_id, &q.path, q.mission_id).await?
|
||||
} else {
|
||||
resolve_upload_base(&q.path)?
|
||||
};
|
||||
@@ -709,6 +729,8 @@ pub struct FinalizeUploadRequest {
|
||||
pub total_chunks: u32,
|
||||
/// Optional workspace ID to resolve relative paths against
|
||||
pub workspace_id: Option<uuid::Uuid>,
|
||||
/// Optional mission ID for mission-specific context directories
|
||||
pub mission_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
// Finalize chunked upload by assembling chunks
|
||||
@@ -717,8 +739,9 @@ pub async fn upload_finalize(
|
||||
Json(req): Json<FinalizeUploadRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
// If workspace_id is provided, resolve path relative to that workspace
|
||||
// If mission_id is also provided, context paths resolve to mission-specific directory
|
||||
let base = if let Some(workspace_id) = req.workspace_id {
|
||||
resolve_path_for_workspace(&state, workspace_id, &req.path).await?
|
||||
resolve_path_for_workspace(&state, workspace_id, &req.path, req.mission_id).await?
|
||||
} else {
|
||||
resolve_upload_base(&req.path)?
|
||||
};
|
||||
@@ -812,6 +835,8 @@ pub struct DownloadUrlRequest {
|
||||
pub file_name: Option<String>,
|
||||
/// Optional workspace ID to resolve relative paths against
|
||||
pub workspace_id: Option<uuid::Uuid>,
|
||||
/// Optional mission ID for mission-specific context directories
|
||||
pub mission_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
// Download file from URL to server filesystem
|
||||
@@ -931,8 +956,9 @@ pub async fn download_from_url(
|
||||
drop(f);
|
||||
|
||||
// Move to destination
|
||||
// If mission_id is provided, context paths resolve to mission-specific directory
|
||||
let base = if let Some(workspace_id) = req.workspace_id {
|
||||
resolve_path_for_workspace(&state, workspace_id, &req.path).await?
|
||||
resolve_path_for_workspace(&state, workspace_id, &req.path, req.mission_id).await?
|
||||
} else {
|
||||
resolve_upload_base(&req.path)?
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user