@ryanfw/prompt-orchestration-pipeline 1.3.1 → 1.3.2

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 (31) hide show
  1. package/docs/pop-task-guide.md +1 -0
  2. package/package.json +1 -2
  3. package/src/core/__tests__/agent-step.test.ts +322 -3
  4. package/src/core/__tests__/task-runner.test.ts +57 -1
  5. package/src/core/agent-step.ts +231 -14
  6. package/src/core/agent-types.ts +3 -0
  7. package/src/core/file-io.ts +8 -74
  8. package/src/core/pipeline-runner.ts +54 -3
  9. package/src/core/status-writer.ts +44 -26
  10. package/src/core/task-runner.ts +27 -3
  11. package/src/harness/__tests__/discovery.test.ts +60 -8
  12. package/src/harness/discovery.ts +16 -6
  13. package/src/ui/client/hooks/useJobListWithUpdates.ts +16 -1
  14. package/src/ui/dist/assets/{index-CbS3OsW7.js → index--RH3sAt3.js} +14 -1
  15. package/src/ui/dist/assets/{index-CbS3OsW7.js.map → index--RH3sAt3.js.map} +1 -1
  16. package/src/ui/dist/assets/style-CSSKuMOe.css +2 -0
  17. package/src/ui/dist/index.html +2 -2
  18. package/src/ui/embedded-assets.js +6 -6
  19. package/src/ui/server/__tests__/gate-endpoints.test.ts +90 -0
  20. package/src/ui/server/__tests__/job-control-endpoints.test.ts +188 -0
  21. package/src/ui/server/__tests__/path-containment.test.ts +54 -0
  22. package/src/ui/server/__tests__/status-corruption.test.ts +55 -0
  23. package/src/ui/server/config-bridge.ts +1 -0
  24. package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +77 -0
  25. package/src/ui/server/endpoints/gate-endpoints.ts +17 -1
  26. package/src/ui/server/endpoints/job-control-endpoints.ts +36 -2
  27. package/src/ui/server/endpoints/upload-endpoints.ts +13 -3
  28. package/src/ui/server/utils/http-utils.ts +6 -1
  29. package/src/ui/server/utils/path-containment.ts +18 -0
  30. package/src/ui/server/utils/status-corruption.ts +27 -0
  31. package/src/ui/dist/assets/style-BUFg3Sth.css +0 -2
@@ -3,6 +3,9 @@ import path from "node:path";
3
3
 
4
4
  import { parseMultipartFormData, sendJson } from "../utils/http-utils";
5
5
  import { extractSeedZip } from "../zip-utils";
6
+ import { createErrorResponse } from "../config-bridge";
7
+ import { Constants } from "../config-bridge-node";
8
+ import { resolveWithin } from "../utils/path-containment";
6
9
 
7
10
  export interface SeedUploadResult {
8
11
  seedObject: Record<string, unknown>;
@@ -40,10 +43,17 @@ export async function handleSeedUploadDirect(
40
43
  // current/{jobId}/files/artifacts/ during promotion.
41
44
  if (artifacts.length > 0) {
42
45
  const stagingJobDir = path.join(pipelineData, "staging", jobId);
46
+
47
+ for (const artifact of artifacts) {
48
+ if (resolveWithin(stagingJobDir, artifact.filename) === null) {
49
+ return sendJson(400, createErrorResponse(Constants.ERROR_CODES.BAD_REQUEST, `unsafe artifact path: ${artifact.filename}`));
50
+ }
51
+ }
52
+
43
53
  for (const artifact of artifacts) {
44
- const artifactPath = path.join(stagingJobDir, artifact.filename);
45
- await mkdir(path.dirname(artifactPath), { recursive: true });
46
- await Bun.write(artifactPath, artifact.content);
54
+ const resolved = resolveWithin(stagingJobDir, artifact.filename)!;
55
+ await mkdir(path.dirname(resolved), { recursive: true });
56
+ await Bun.write(resolved, artifact.content);
47
57
  }
48
58
  }
49
59
 
@@ -10,6 +10,11 @@ function badRequest(message: string): Error {
10
10
  return new Error(message);
11
11
  }
12
12
 
13
+ function normalizeContentType(contentType: string): string {
14
+ const mediaType = contentType.split(";", 1)[0]?.trim();
15
+ return mediaType || "application/octet-stream";
16
+ }
17
+
13
18
  export function sendJson(statusCode: number, data: unknown): Response {
14
19
  return new Response(JSON.stringify(data), {
15
20
  status: statusCode,
@@ -57,7 +62,7 @@ export async function parseMultipartFormData(
57
62
  } else {
58
63
  files.push({
59
64
  filename: value.name,
60
- contentType: value.type || "application/octet-stream",
65
+ contentType: normalizeContentType(value.type),
61
66
  content: new Uint8Array(await value.arrayBuffer()),
62
67
  });
63
68
  }
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+
3
+ /**
4
+ * Resolve a relative path under rootDir and return null when it escapes or
5
+ * resolves to rootDir itself.
6
+ * POSIX-oriented for Bun on macOS/Linux; revalidate Windows separators and
7
+ * case-insensitive filesystem behavior before reusing this on other boundaries.
8
+ */
9
+ export function resolveWithin(rootDir: string, relativePath: unknown): string | null {
10
+ if (typeof relativePath !== "string" || relativePath.length === 0) return null;
11
+ if (relativePath.includes("\0")) return null;
12
+ if (path.isAbsolute(relativePath)) return null;
13
+ const root = path.resolve(rootDir);
14
+ const resolved = path.resolve(root, relativePath);
15
+ if (resolved === root) return null;
16
+ if (!resolved.startsWith(root + path.sep)) return null;
17
+ return resolved;
18
+ }
@@ -0,0 +1,27 @@
1
+ import path from "node:path";
2
+
3
+ import { createErrorResponse } from "../config-bridge";
4
+ import { Constants } from "../config-bridge-node";
5
+ import { getJobDirectoryPath } from "../../../config/paths";
6
+ import { StatusCorruptError } from "../../../core/status-writer";
7
+ import { sendJson } from "./http-utils";
8
+
9
+ export async function assertStatusParseable(statusPath: string): Promise<void> {
10
+ try {
11
+ JSON.parse(await Bun.file(statusPath).text());
12
+ } catch (err: unknown) {
13
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
14
+ if (err instanceof SyntaxError) throw new StatusCorruptError(statusPath, { cause: err });
15
+ throw err;
16
+ }
17
+ }
18
+
19
+ export async function assertKnownJobStatusesParseable(dataDir: string, jobId: string): Promise<void> {
20
+ for (const location of ["current", "complete"] as const) {
21
+ await assertStatusParseable(path.join(getJobDirectoryPath(dataDir, jobId, location), "tasks-status.json"));
22
+ }
23
+ }
24
+
25
+ export function statusCorruptResponse(err: StatusCorruptError): Response {
26
+ return sendJson(409, createErrorResponse(Constants.ERROR_CODES.STATUS_CORRUPT, err.message, err.statusPath));
27
+ }