From 07ae8ad6cdc4c816643aeaaea0ad37445a616ceb Mon Sep 17 00:00:00 2001 From: dailz Date: Thu, 16 Apr 2026 17:56:28 +0800 Subject: [PATCH] feat(service): auto-inject WORK_DIR and resolve file-type params in task script rendering - ProcessTask injects $WORK_DIR only when script template uses it - File/directory type params: resolves file_id to filename before rendering - ValidateParams validates file/directory params as valid int64 file IDs - RenderScript no longer shell-escapes file/directory type values - Log rendered script before submitting to Slurm for debugging Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- internal/service/script_utils.go | 19 ++++++++++-- internal/service/task_service.go | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/internal/service/script_utils.go b/internal/service/script_utils.go index 71df455..97b1b4e 100644 --- a/internal/service/script_utils.go +++ b/internal/service/script_utils.go @@ -58,6 +58,9 @@ func ValidateParams(params []model.ParameterSchema, values map[string]string) er } } case model.ParamTypeFile, model.ParamTypeDirectory: + if _, err := strconv.ParseInt(val, 10, 64); err != nil { + errs = append(errs, fmt.Sprintf("parameter %q must be a valid file ID (integer), got %q", p.Name, val)) + } case model.ParamTypeString: } } @@ -71,7 +74,9 @@ func ValidateParams(params []model.ParameterSchema, values map[string]string) er // RenderScript replaces $PARAM tokens in the template with user-provided values. // Only tokens defined in the schema are replaced. Replacement is done longest-name-first // to avoid partial matches (e.g., $JOB_NAME before $JOB). -// All values are shell-escaped using single-quote wrapping. +// String, integer, boolean, and enum values are shell-escaped using single-quote wrapping. +// File and directory values (including WORK_DIR) are inserted raw (no escaping) because +// they are paths used in SBATCH directives and command arguments. func RenderScript(template string, params []model.ParameterSchema, values map[string]string) string { sorted := make([]model.ParameterSchema, len(params)) copy(sorted, params) @@ -79,6 +84,9 @@ func RenderScript(template string, params []model.ParameterSchema, values map[st return len(sorted[i].Name) > len(sorted[j].Name) }) + // Add virtual "WORK_DIR" entry so $WORK_DIR is replaced without escaping. + sorted = append(sorted, model.ParameterSchema{Name: "WORK_DIR", Type: model.ParamTypeFile}) + result := template for _, p := range sorted { val, ok := values[p.Name] @@ -89,8 +97,13 @@ func RenderScript(template string, params []model.ParameterSchema, values map[st continue } } - escaped := "'" + strings.ReplaceAll(val, "'", "'\\''") + "'" - result = strings.ReplaceAll(result, "$"+p.Name, escaped) + + if p.Type == model.ParamTypeFile || p.Type == model.ParamTypeDirectory { + result = strings.ReplaceAll(result, "$"+p.Name, val) + } else { + escaped := "'" + strings.ReplaceAll(val, "'", "'\\''") + "'" + result = strings.ReplaceAll(result, "$"+p.Name, escaped) + } } return result } diff --git a/internal/service/task_service.go b/internal/service/task_service.go index 7af492c..707a85f 100644 --- a/internal/service/task_service.go +++ b/internal/service/task_service.go @@ -261,8 +261,61 @@ func (s *TaskService) ProcessTask(ctx context.Context, taskID int64) error { return fail(model.TaskStepSubmitting, err.Error()) } + if strings.Contains(app.ScriptTemplate, "$WORK_DIR") { + values["WORK_DIR"] = workDir + } + + // Resolve file-type parameters: user sends file_id, we replace with filename. + // Only query the database if there are file/directory-type parameters with values. + var fileLookupIDs []int64 + for _, p := range params { + if p.Type != model.ParamTypeFile && p.Type != model.ParamTypeDirectory { + continue + } + val, ok := values[p.Name] + if !ok || val == "" { + continue + } + fileID, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return fail(model.TaskStepSubmitting, fmt.Sprintf("parameter %q: invalid file_id %q, expected numeric file ID", p.Name, val)) + } + fileLookupIDs = append(fileLookupIDs, fileID) + } + + if len(fileLookupIDs) > 0 && s.fileStore != nil { + fetchedFiles, err := s.fileStore.GetByIDs(ctx, fileLookupIDs) + if err != nil { + return fail(model.TaskStepSubmitting, fmt.Sprintf("fetch file names for parameter resolution: %v", err)) + } + fileMap := make(map[int64]string, len(fetchedFiles)) + for _, f := range fetchedFiles { + fileMap[f.ID] = f.Name + } + for _, p := range params { + if p.Type != model.ParamTypeFile && p.Type != model.ParamTypeDirectory { + continue + } + val, ok := values[p.Name] + if !ok || val == "" { + continue + } + fileID, _ := strconv.ParseInt(val, 10, 64) + filename, found := fileMap[fileID] + if !found { + return fail(model.TaskStepSubmitting, fmt.Sprintf("parameter %q: file_id %d not found", p.Name, fileID)) + } + values[p.Name] = filename + } + } + // 17. Render script rendered := RenderScript(app.ScriptTemplate, params, values) + s.logger.Info("rendered script", + zap.Int64("task_id", taskID), + zap.String("work_dir", workDir), + zap.String("script", rendered), + ) // 18. Submit to Slurm jobResp, err := s.jobSvc.SubmitJob(ctx, &model.SubmitJobRequest{