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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -58,6 +58,9 @@ func ValidateParams(params []model.ParameterSchema, values map[string]string) er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case model.ParamTypeFile, model.ParamTypeDirectory:
|
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:
|
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.
|
// 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
|
// Only tokens defined in the schema are replaced. Replacement is done longest-name-first
|
||||||
// to avoid partial matches (e.g., $JOB_NAME before $JOB).
|
// 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 {
|
func RenderScript(template string, params []model.ParameterSchema, values map[string]string) string {
|
||||||
sorted := make([]model.ParameterSchema, len(params))
|
sorted := make([]model.ParameterSchema, len(params))
|
||||||
copy(sorted, 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)
|
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
|
result := template
|
||||||
for _, p := range sorted {
|
for _, p := range sorted {
|
||||||
val, ok := values[p.Name]
|
val, ok := values[p.Name]
|
||||||
@@ -89,8 +97,13 @@ func RenderScript(template string, params []model.ParameterSchema, values map[st
|
|||||||
continue
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,8 +261,61 @@ func (s *TaskService) ProcessTask(ctx context.Context, taskID int64) error {
|
|||||||
return fail(model.TaskStepSubmitting, err.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
|
// 17. Render script
|
||||||
rendered := RenderScript(app.ScriptTemplate, params, values)
|
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
|
// 18. Submit to Slurm
|
||||||
jobResp, err := s.jobSvc.SubmitJob(ctx, &model.SubmitJobRequest{
|
jobResp, err := s.jobSvc.SubmitJob(ctx, &model.SubmitJobRequest{
|
||||||
|
|||||||
Reference in New Issue
Block a user