@ryanfw/prompt-orchestration-pipeline 0.10.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.
- package/package.json +3 -1
- package/src/api/index.js +38 -1
- package/src/components/DAGGrid.jsx +180 -53
- package/src/components/JobDetail.jsx +11 -0
- package/src/components/TaskDetailSidebar.jsx +27 -3
- package/src/components/UploadSeed.jsx +2 -2
- package/src/components/ui/RestartJobModal.jsx +26 -6
- package/src/components/ui/StopJobModal.jsx +183 -0
- package/src/core/config.js +7 -3
- package/src/core/lifecycle-policy.js +62 -0
- package/src/core/orchestrator.js +32 -0
- package/src/core/pipeline-runner.js +312 -217
- package/src/core/status-initializer.js +155 -0
- package/src/core/status-writer.js +235 -13
- package/src/pages/Code.jsx +8 -1
- package/src/pages/PipelineDetail.jsx +85 -3
- package/src/pages/PromptPipelineDashboard.jsx +10 -11
- package/src/ui/client/adapters/job-adapter.js +81 -2
- package/src/ui/client/api.js +233 -8
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +92 -0
- package/src/ui/client/hooks/useJobList.js +14 -1
- package/src/ui/dist/app.js +262 -0
- package/src/ui/dist/assets/{index-DqkbzXZ1.js → index-B320avRx.js} +5051 -2186
- package/src/ui/dist/assets/index-B320avRx.js.map +1 -0
- package/src/ui/dist/assets/style-BYCoLBnK.css +62 -0
- package/src/ui/dist/favicon.svg +12 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/endpoints/file-endpoints.js +330 -0
- package/src/ui/endpoints/job-control-endpoints.js +1001 -0
- package/src/ui/endpoints/job-endpoints.js +62 -0
- package/src/ui/endpoints/sse-endpoints.js +223 -0
- package/src/ui/endpoints/state-endpoint.js +85 -0
- package/src/ui/endpoints/upload-endpoints.js +406 -0
- package/src/ui/express-app.js +182 -0
- package/src/ui/server.js +38 -1788
- package/src/ui/sse-broadcast.js +93 -0
- package/src/ui/utils/http-utils.js +139 -0
- package/src/ui/utils/mime-types.js +196 -0
- package/src/ui/vite.config.js +22 -0
- package/src/ui/zip-utils.js +103 -0
- package/src/utils/jobs.js +39 -0
- package/src/ui/dist/assets/style-DBF9NQGk.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>
|
package/src/ui/dist/index.html
CHANGED
|
@@ -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-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/style-
|
|
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
|
+
}
|