@ryanfw/prompt-orchestration-pipeline 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +2 -1
  2. package/src/components/DAGGrid.jsx +157 -47
  3. package/src/components/ui/RestartJobModal.jsx +26 -6
  4. package/src/components/ui/StopJobModal.jsx +183 -0
  5. package/src/core/config.js +7 -3
  6. package/src/core/lifecycle-policy.js +62 -0
  7. package/src/core/pipeline-runner.js +312 -217
  8. package/src/core/status-writer.js +84 -0
  9. package/src/pages/Code.jsx +8 -1
  10. package/src/pages/PipelineDetail.jsx +85 -3
  11. package/src/pages/PromptPipelineDashboard.jsx +10 -11
  12. package/src/ui/client/adapters/job-adapter.js +60 -0
  13. package/src/ui/client/api.js +233 -8
  14. package/src/ui/client/hooks/useJobList.js +14 -1
  15. package/src/ui/dist/app.js +262 -0
  16. package/src/ui/dist/assets/{index-DeDzq-Kk.js → index-B320avRx.js} +4854 -2104
  17. package/src/ui/dist/assets/index-B320avRx.js.map +1 -0
  18. package/src/ui/dist/assets/style-BYCoLBnK.css +62 -0
  19. package/src/ui/dist/favicon.svg +12 -0
  20. package/src/ui/dist/index.html +2 -2
  21. package/src/ui/endpoints/file-endpoints.js +330 -0
  22. package/src/ui/endpoints/job-control-endpoints.js +1001 -0
  23. package/src/ui/endpoints/job-endpoints.js +62 -0
  24. package/src/ui/endpoints/sse-endpoints.js +223 -0
  25. package/src/ui/endpoints/state-endpoint.js +85 -0
  26. package/src/ui/endpoints/upload-endpoints.js +406 -0
  27. package/src/ui/express-app.js +182 -0
  28. package/src/ui/server.js +38 -1880
  29. package/src/ui/sse-broadcast.js +93 -0
  30. package/src/ui/utils/http-utils.js +139 -0
  31. package/src/ui/utils/mime-types.js +196 -0
  32. package/src/ui/vite.config.js +22 -0
  33. package/src/utils/jobs.js +39 -0
  34. package/src/ui/dist/assets/style-aBtD_Yrs.css +0 -62
@@ -0,0 +1,12 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="32"
4
+ height="32"
5
+ viewBox="0 0 1200 1200"
6
+ >
7
+ <path
8
+ fill="#009966"
9
+ d="M406.13 988.31c-17.297 75.047-84.562 131.11-164.86 131.11-93.375 0-169.18-75.797-169.18-169.18s75.797-169.18 169.18-169.18 169.18 75.797 169.18 169.18v1.266h447.74v-167.9H671.63c-14.859 0-29.062-5.906-39.562-16.406s-16.406-24.703-16.406-39.562v-37.312h-317.16c-10.312 0-18.656-8.344-18.656-18.656V355.78h-147.94c-14.859 0-29.062-5.906-39.562-16.406s-16.406-24.75-16.406-39.562v-111.94c0-14.859 5.906-29.109 16.406-39.562 10.5-10.5 24.75-16.406 39.562-16.406h391.78c14.859 0 29.062 5.906 39.562 16.406s16.406 24.75 16.406 39.562v37.312h202.4c9.281-84.609 81.094-150.52 168.14-150.52 93.375 0 169.18 75.797 169.18 169.18s-75.797 169.18-169.18 169.18c-87.047 0-158.86-65.906-168.14-150.52h-202.4v37.312c0 14.859-5.906 29.062-16.406 39.562s-24.75 16.406-39.562 16.406h-206.53v297.24h298.5v-37.312c0-14.859 5.906-29.062 16.406-39.562s24.703-16.406 39.562-16.406h392.63c14.859 0 29.062 5.906 39.562 16.406s16.406 24.703 16.406 39.562v111.94c0 14.859-5.906 29.062-16.406 39.562s-24.75 16.406-39.562 16.406h-168.74v186.56c0 10.312-8.344 18.656-18.656 18.656h-466.4c-1.5 0-2.906-.187-4.312-.516zM225.19 262.45h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H225.19c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm186.56 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H411.75c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm-93.281 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm616.18 0h85.5c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-85.5l29.062-22.594c8.109-6.328 9.609-18.047 3.281-26.156s-18.047-9.609-26.156-3.281l-71.953 55.969a18.61 18.61 0 0 0 0 29.438l71.953 55.969c8.109 6.328 19.875 4.875 26.156-3.281 6.328-8.109 4.875-19.875-3.281-26.203l-29.062-22.594zm-779.95 696.66l50.391 50.391c7.266 7.313 19.078 7.313 26.391 0l100.73-100.73c7.266-7.266 7.266-19.078 0-26.391-7.266-7.266-19.078-7.266-26.391 0l-87.562 87.562-37.172-37.172c-7.266-7.266-19.078-7.266-26.391 0-7.266 7.266-7.266 19.078 0 26.391zm797.21-268.78h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm-186.56 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm93.281 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H858.63c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656z"
10
+ fill-rule="evenodd"
11
+ />
12
+ </svg>
@@ -11,8 +11,8 @@
11
11
  />
12
12
  <title>Prompt Pipeline Dashboard</title>
13
13
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
14
- <script type="module" crossorigin src="/assets/index-DeDzq-Kk.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/style-aBtD_Yrs.css">
14
+ <script type="module" crossorigin src="/assets/index-B320avRx.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/style-BYCoLBnK.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -0,0 +1,330 @@
1
+ /**
2
+ * File endpoint handlers for task file operations
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { sendJson } from "../utils/http-utils.js";
8
+ import { getMimeType, isTextMime } from "../utils/mime-types.js";
9
+ import { getJobDirectoryPath } from "../../config/paths.js";
10
+
11
+ const exists = async (p) =>
12
+ fs.promises
13
+ .access(p)
14
+ .then(() => true)
15
+ .catch(() => false);
16
+
17
+ /**
18
+ * Resolve job lifecycle directory deterministically
19
+ * @param {string} dataDir - Base data directory
20
+ * @param {string} jobId - Job identifier
21
+ * @returns {Promise<string|null>} One of "current", "complete", "rejected", or null if job not found
22
+ */
23
+ async function resolveJobLifecycle(dataDir, jobId) {
24
+ const currentJobDir = getJobDirectoryPath(dataDir, jobId, "current");
25
+ const completeJobDir = getJobDirectoryPath(dataDir, jobId, "complete");
26
+ const rejectedJobDir = getJobDirectoryPath(dataDir, jobId, "rejected");
27
+
28
+ // Check in order of preference: current > complete > rejected
29
+ const currentExists = await exists(currentJobDir);
30
+ const completeExists = await exists(completeJobDir);
31
+ const rejectedExists = await exists(rejectedJobDir);
32
+
33
+ if (currentExists) {
34
+ return "current";
35
+ }
36
+
37
+ if (completeExists) {
38
+ return "complete";
39
+ }
40
+
41
+ if (rejectedExists) {
42
+ return "rejected";
43
+ }
44
+
45
+ // Job not found in any lifecycle
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Consolidated path jail security validation with generic error messages
51
+ * @param {string} filename - Filename to validate
52
+ * @returns {Object|null} Validation result or null if valid
53
+ */
54
+ export function validateFilePath(filename) {
55
+ // Check for path traversal patterns
56
+ if (filename.includes("..")) {
57
+ console.error("Path security: path traversal detected", { filename });
58
+ return {
59
+ allowed: false,
60
+ message: "Path validation failed",
61
+ };
62
+ }
63
+
64
+ // Check for absolute paths (POSIX, Windows, backslashes, ~)
65
+ if (
66
+ path.isAbsolute(filename) ||
67
+ /^[a-zA-Z]:/.test(filename) ||
68
+ filename.includes("\\") ||
69
+ filename.startsWith("~")
70
+ ) {
71
+ console.error("Path security: absolute path detected", { filename });
72
+ return {
73
+ allowed: false,
74
+ message: "Path validation failed",
75
+ };
76
+ }
77
+
78
+ // Check for empty filename
79
+ if (!filename || filename.trim() === "") {
80
+ console.error("Path security: empty filename detected");
81
+ return {
82
+ allowed: false,
83
+ message: "Path validation failed",
84
+ };
85
+ }
86
+
87
+ // Path is valid
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Handle task file list request with validation and security checks
93
+ * @param {http.IncomingMessage} req - HTTP request
94
+ * @param {http.ServerResponse} res - HTTP response
95
+ * @param {Object} params - Request parameters
96
+ * @param {string} params.jobId - Job ID
97
+ * @param {string} params.taskId - Task ID
98
+ * @param {string} params.type - File type (artifacts, logs, tmp)
99
+ * @param {string} params.dataDir - Data directory
100
+ */
101
+ export async function handleTaskFileListRequest(
102
+ req,
103
+ res,
104
+ { jobId, taskId, type, dataDir }
105
+ ) {
106
+ // Resolve job lifecycle deterministically
107
+ const lifecycle = await resolveJobLifecycle(dataDir, jobId);
108
+ if (!lifecycle) {
109
+ // Job not found, return empty list
110
+ sendJson(res, 200, {
111
+ ok: true,
112
+ data: {
113
+ files: [],
114
+ jobId,
115
+ taskId,
116
+ type,
117
+ },
118
+ });
119
+ return;
120
+ }
121
+
122
+ // Use single lifecycle directory
123
+ const jobDir = getJobDirectoryPath(dataDir, jobId, lifecycle);
124
+ const taskDir = path.join(jobDir, "files", type);
125
+
126
+ // Use path.relative for stricter jail enforcement
127
+ const resolvedPath = path.resolve(taskDir);
128
+ const relativePath = path.relative(jobDir, resolvedPath);
129
+
130
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
131
+ console.error("Path security: directory traversal detected", {
132
+ taskDir,
133
+ relativePath,
134
+ });
135
+ sendJson(res, 403, {
136
+ ok: false,
137
+ error: "forbidden",
138
+ message: "Path validation failed",
139
+ });
140
+ return;
141
+ }
142
+
143
+ // Check if directory exists
144
+ if (!(await exists(taskDir))) {
145
+ // Directory doesn't exist, return empty list
146
+ sendJson(res, 200, {
147
+ ok: true,
148
+ data: {
149
+ files: [],
150
+ jobId,
151
+ taskId,
152
+ type,
153
+ },
154
+ });
155
+ return;
156
+ }
157
+
158
+ try {
159
+ // Read directory contents
160
+ const entries = await fs.promises.readdir(taskDir, {
161
+ withFileTypes: true,
162
+ });
163
+
164
+ // Filter and map to file list
165
+ const files = [];
166
+ for (const entry of entries) {
167
+ if (entry.isFile()) {
168
+ // Validate each filename using the consolidated function
169
+ const validation = validateFilePath(entry.name);
170
+ if (validation) {
171
+ console.error("Path security: skipping invalid file", {
172
+ filename: entry.name,
173
+ reason: validation.message,
174
+ });
175
+ continue; // Skip files that fail validation
176
+ }
177
+
178
+ const filePath = path.join(taskDir, entry.name);
179
+ const stats = await fs.promises.stat(filePath);
180
+
181
+ files.push({
182
+ name: entry.name,
183
+ size: stats.size,
184
+ mtime: stats.mtime.toISOString(),
185
+ mime: getMimeType(entry.name),
186
+ });
187
+ }
188
+ }
189
+
190
+ // Sort files by name
191
+ files.sort((a, b) => a.name.localeCompare(b.name));
192
+
193
+ // Send successful response
194
+ sendJson(res, 200, {
195
+ ok: true,
196
+ data: {
197
+ files,
198
+ jobId,
199
+ taskId,
200
+ type,
201
+ },
202
+ });
203
+ } catch (error) {
204
+ console.error("Error listing files:", error);
205
+ sendJson(res, 500, {
206
+ ok: false,
207
+ error: "internal_error",
208
+ message: "Failed to list files",
209
+ });
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Handle task file request with validation, jail checks, and proper encoding
215
+ * @param {http.IncomingMessage} req - HTTP request
216
+ * @param {http.ServerResponse} res - HTTP response
217
+ * @param {Object} params - Request parameters
218
+ * @param {string} params.jobId - Job ID
219
+ * @param {string} params.taskId - Task ID
220
+ * @param {string} params.type - File type (artifacts, logs, tmp)
221
+ * @param {string} params.filename - Filename
222
+ * @param {string} params.dataDir - Data directory
223
+ */
224
+ export async function handleTaskFileRequest(
225
+ req,
226
+ res,
227
+ { jobId, taskId, type, filename, dataDir }
228
+ ) {
229
+ // Unified security validation
230
+ const validation = validateFilePath(filename);
231
+ if (validation) {
232
+ sendJson(res, 403, {
233
+ ok: false,
234
+ error: "forbidden",
235
+ message: validation.message,
236
+ });
237
+ return;
238
+ }
239
+
240
+ // Resolve job lifecycle deterministically
241
+ const lifecycle = await resolveJobLifecycle(dataDir, jobId);
242
+ if (!lifecycle) {
243
+ sendJson(res, 404, {
244
+ ok: false,
245
+ error: "not_found",
246
+ message: "Job not found",
247
+ });
248
+ return;
249
+ }
250
+
251
+ // Use single lifecycle directory
252
+ const jobDir = getJobDirectoryPath(dataDir, jobId, lifecycle);
253
+ const taskDir = path.join(jobDir, "files", type);
254
+ const filePath = path.join(taskDir, filename);
255
+
256
+ // Use path.relative for stricter jail enforcement
257
+ const resolvedPath = path.resolve(filePath);
258
+ const relativePath = path.relative(jobDir, resolvedPath);
259
+
260
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
261
+ sendJson(res, 403, {
262
+ ok: false,
263
+ error: "forbidden",
264
+ message: "Path resolves outside allowed directory",
265
+ });
266
+ return;
267
+ }
268
+
269
+ // Check if file exists
270
+ if (!(await exists(filePath))) {
271
+ sendJson(res, 404, {
272
+ ok: false,
273
+ error: "not_found",
274
+ message: "File not found",
275
+ filePath,
276
+ });
277
+ return;
278
+ }
279
+
280
+ try {
281
+ // Get file stats
282
+ const stats = await fs.promises.stat(filePath);
283
+ if (!stats.isFile()) {
284
+ sendJson(res, 404, {
285
+ ok: false,
286
+ error: "not_found",
287
+ message: "Not a regular file",
288
+ });
289
+ return;
290
+ }
291
+
292
+ // Determine MIME type and encoding
293
+ const mime = getMimeType(filename);
294
+ const isText = isTextMime(mime);
295
+ const encoding = isText ? "utf8" : "base64";
296
+
297
+ // Read file content
298
+ let content;
299
+ if (isText) {
300
+ content = await fs.promises.readFile(filePath, "utf8");
301
+ } else {
302
+ const buffer = await fs.promises.readFile(filePath);
303
+ content = buffer.toString("base64");
304
+ }
305
+
306
+ // Build relative path for response
307
+ const relativePath = path.join("tasks", taskId, type, filename);
308
+
309
+ // Send successful response
310
+ sendJson(res, 200, {
311
+ ok: true,
312
+ jobId,
313
+ taskId,
314
+ type,
315
+ path: relativePath,
316
+ mime,
317
+ size: stats.size,
318
+ mtime: stats.mtime.toISOString(),
319
+ encoding,
320
+ content,
321
+ });
322
+ } catch (error) {
323
+ console.error("Error reading file:", error);
324
+ sendJson(res, 500, {
325
+ ok: false,
326
+ error: "internal_error",
327
+ message: "Failed to read file",
328
+ });
329
+ }
330
+ }