stageflow 0.1.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/agent/activity.d.ts +76 -0
- package/dist/agent/activity.js +89 -0
- package/dist/agent/activityObserver.d.ts +17 -0
- package/dist/agent/activityObserver.js +66 -0
- package/dist/agent/cursorExtension.d.ts +12 -0
- package/dist/agent/cursorExtension.js +88 -0
- package/dist/agent/cursorProvider.d.ts +14 -0
- package/dist/agent/cursorProvider.js +139 -0
- package/dist/agent/fakeAgent.d.ts +29 -0
- package/dist/agent/fakeAgent.js +295 -0
- package/dist/agent/oauthSessionManager.d.ts +33 -0
- package/dist/agent/oauthSessionManager.js +214 -0
- package/dist/agent/piAdapter.d.ts +128 -0
- package/dist/agent/piAdapter.js +967 -0
- package/dist/agent/port.d.ts +63 -0
- package/dist/agent/port.js +46 -0
- package/dist/agent/providerAuth.d.ts +101 -0
- package/dist/agent/providerAuth.js +282 -0
- package/dist/agent/providerSupport.d.ts +28 -0
- package/dist/agent/providerSupport.js +19 -0
- package/dist/cli/providersCommand.d.ts +11 -0
- package/dist/cli/providersCommand.js +275 -0
- package/dist/cli/terminalAuthInteraction.d.ts +8 -0
- package/dist/cli/terminalAuthInteraction.js +65 -0
- package/dist/cli/terminalSecret.d.ts +2 -0
- package/dist/cli/terminalSecret.js +67 -0
- package/dist/cli/validateCommand.d.ts +11 -0
- package/dist/cli/validateCommand.js +81 -0
- package/dist/cli/validateOutput.d.ts +8 -0
- package/dist/cli/validateOutput.js +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +273 -0
- package/dist/config/createPipeline.d.ts +31 -0
- package/dist/config/createPipeline.js +255 -0
- package/dist/config/createStage.d.ts +25 -0
- package/dist/config/createStage.js +181 -0
- package/dist/config/listConfig.d.ts +33 -0
- package/dist/config/listConfig.js +195 -0
- package/dist/config/listExtensions.d.ts +26 -0
- package/dist/config/listExtensions.js +50 -0
- package/dist/config/listSkills.d.ts +29 -0
- package/dist/config/listSkills.js +55 -0
- package/dist/config/loadOutcome.d.ts +27 -0
- package/dist/config/loadOutcome.js +6 -0
- package/dist/config/loadPipeline.d.ts +25 -0
- package/dist/config/loadPipeline.js +196 -0
- package/dist/config/loadStage.d.ts +4 -0
- package/dist/config/loadStage.js +127 -0
- package/dist/config/loadTask.d.ts +7 -0
- package/dist/config/loadTask.js +69 -0
- package/dist/config/readYamlObject.d.ts +1 -0
- package/dist/config/readYamlObject.js +5 -0
- package/dist/config/resolvePipelineDag.d.ts +12 -0
- package/dist/config/resolvePipelineDag.js +282 -0
- package/dist/config/validateCatalog.d.ts +36 -0
- package/dist/config/validateCatalog.js +322 -0
- package/dist/envelope/check.d.ts +3 -0
- package/dist/envelope/check.js +52 -0
- package/dist/envelope/payloadSchema.d.ts +10 -0
- package/dist/envelope/payloadSchema.js +91 -0
- package/dist/hitl/qaTrail.d.ts +31 -0
- package/dist/hitl/qaTrail.js +65 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +7 -0
- package/dist/mcp/projectRun.d.ts +37 -0
- package/dist/mcp/projectRun.js +57 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +46 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +26 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +96 -0
- package/dist/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -0
- package/dist/prompt/priorEnvelope.js +5 -0
- package/dist/runstore/catalog.d.ts +8 -0
- package/dist/runstore/catalog.js +16 -0
- package/dist/runstore/createStore.d.ts +15 -0
- package/dist/runstore/createStore.js +23 -0
- package/dist/runstore/disk/DiskRunStore.d.ts +30 -0
- package/dist/runstore/disk/DiskRunStore.js +238 -0
- package/dist/runstore/disk/catalogHelpers.d.ts +11 -0
- package/dist/runstore/disk/catalogHelpers.js +102 -0
- package/dist/runstore/layout.d.ts +22 -0
- package/dist/runstore/layout.js +58 -0
- package/dist/runstore/paths.d.ts +7 -0
- package/dist/runstore/paths.js +60 -0
- package/dist/runstore/pipelineDagSnapshot.d.ts +5 -0
- package/dist/runstore/pipelineDagSnapshot.js +43 -0
- package/dist/runstore/port.d.ts +146 -0
- package/dist/runstore/port.js +28 -0
- package/dist/runstore/runProjection.d.ts +6 -0
- package/dist/runstore/runProjection.js +102 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +48 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +460 -0
- package/dist/runstore/sqlite/migrateFromDisk.d.ts +5 -0
- package/dist/runstore/sqlite/migrateFromDisk.js +108 -0
- package/dist/runstore/sqlite/schema.d.ts +1 -0
- package/dist/runstore/sqlite/schema.js +59 -0
- package/dist/runstore/stageExecution.d.ts +13 -0
- package/dist/runstore/stageExecution.js +22 -0
- package/dist/runstore/stageSnapshot.d.ts +2 -0
- package/dist/runstore/stageSnapshot.js +46 -0
- package/dist/runstore/syntheticStageSnapshot.d.ts +2 -0
- package/dist/runstore/syntheticStageSnapshot.js +10 -0
- package/dist/runstore/trackProjection.d.ts +7 -0
- package/dist/runstore/trackProjection.js +94 -0
- package/dist/runstore/workspaceLayout.d.ts +31 -0
- package/dist/runstore/workspaceLayout.js +172 -0
- package/dist/runtime/answerResume.d.ts +16 -0
- package/dist/runtime/answerResume.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +16 -0
- package/dist/runtime/credentialBinding.js +80 -0
- package/dist/runtime/dagTraversal.d.ts +3 -0
- package/dist/runtime/dagTraversal.js +26 -0
- package/dist/runtime/envelopeRouting.d.ts +20 -0
- package/dist/runtime/envelopeRouting.js +59 -0
- package/dist/runtime/hitlSeams.d.ts +38 -0
- package/dist/runtime/hitlSeams.js +2 -0
- package/dist/runtime/pipelineRunner.d.ts +77 -0
- package/dist/runtime/pipelineRunner.js +141 -0
- package/dist/runtime/pipelineScheduler.d.ts +90 -0
- package/dist/runtime/pipelineScheduler.js +494 -0
- package/dist/runtime/pipelineValidationError.d.ts +6 -0
- package/dist/runtime/pipelineValidationError.js +8 -0
- package/dist/runtime/resumeReconstruct.d.ts +33 -0
- package/dist/runtime/resumeReconstruct.js +144 -0
- package/dist/runtime/runManager.d.ts +129 -0
- package/dist/runtime/runManager.js +818 -0
- package/dist/runtime/runRetryCoordinator.d.ts +85 -0
- package/dist/runtime/runRetryCoordinator.js +372 -0
- package/dist/runtime/settingsFile.d.ts +17 -0
- package/dist/runtime/settingsFile.js +88 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +40 -0
- package/dist/runtime/stageAttemptBootstrap.js +82 -0
- package/dist/runtime/stageAttemptContext.d.ts +11 -0
- package/dist/runtime/stageAttemptContext.js +26 -0
- package/dist/runtime/stageConcurrency.d.ts +10 -0
- package/dist/runtime/stageConcurrency.js +51 -0
- package/dist/runtime/stageHitl.d.ts +95 -0
- package/dist/runtime/stageHitl.js +198 -0
- package/dist/runtime/stageProcessLauncher.d.ts +45 -0
- package/dist/runtime/stageProcessLauncher.js +199 -0
- package/dist/runtime/stageRecovery.d.ts +10 -0
- package/dist/runtime/stageRecovery.js +43 -0
- package/dist/runtime/stageRoots.d.ts +22 -0
- package/dist/runtime/stageRoots.js +93 -0
- package/dist/runtime/stageRunner.d.ts +59 -0
- package/dist/runtime/stageRunner.js +221 -0
- package/dist/runtime/stageWorker.d.ts +6 -0
- package/dist/runtime/stageWorker.js +109 -0
- package/dist/runtime/stageWorkerProtocol.d.ts +27 -0
- package/dist/runtime/stageWorkerProtocol.js +16 -0
- package/dist/runtime/taskInput.d.ts +15 -0
- package/dist/runtime/taskInput.js +38 -0
- package/dist/server/http.d.ts +31 -0
- package/dist/server/http.js +561 -0
- package/dist/server/providerRoutes.d.ts +15 -0
- package/dist/server/providerRoutes.js +186 -0
- package/dist/tools/askOperator.d.ts +185 -0
- package/dist/tools/askOperator.js +409 -0
- package/dist/tools/emitStageEnvelope.d.ts +42 -0
- package/dist/tools/emitStageEnvelope.js +61 -0
- package/dist/tools/writeStageArtifact.d.ts +26 -0
- package/dist/tools/writeStageArtifact.js +103 -0
- package/dist/types/envelope.d.ts +12 -0
- package/dist/types/envelope.js +6 -0
- package/dist/types/pipeline.d.ts +25 -0
- package/dist/types/pipeline.js +1 -0
- package/dist/types/stage.d.ts +12 -0
- package/dist/types/stage.js +6 -0
- package/dist/types/task.d.ts +7 -0
- package/dist/types/task.js +1 -0
- package/dist/ui/assets/index-Cry0Tpfx.js +118 -0
- package/dist/ui/assets/index-DCsDopak.css +1 -0
- package/dist/ui/index.html +21 -0
- package/package.json +72 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { readFile, access } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { localhostHostValidation, localhostOriginValidation, } from "@modelcontextprotocol/node";
|
|
7
|
+
import { getCredentialSourceSettings, setCredentialSource, } from "../agent/providerAuth.js";
|
|
8
|
+
import { handleProviderRoutes, providerAuthErrorBody, } from "./providerRoutes.js";
|
|
9
|
+
import { createPipeline, parseCreatePipelineBody } from "../config/createPipeline.js";
|
|
10
|
+
import { createStage, parseCreateStageBody } from "../config/createStage.js";
|
|
11
|
+
import { listModels, listPipelines, listStages, listTasks, } from "../config/listConfig.js";
|
|
12
|
+
import { listExtensions } from "../config/listExtensions.js";
|
|
13
|
+
import { listSkills } from "../config/listSkills.js";
|
|
14
|
+
import { readRunArtifact } from "../mcp/readArtifact.js";
|
|
15
|
+
import { handleMcpHttpRequest } from "../mcp/server.js";
|
|
16
|
+
import { createRunStore } from "../runstore/createStore.js";
|
|
17
|
+
import { RunManager, } from "../runtime/runManager.js";
|
|
18
|
+
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, } from "../runtime/settingsFile.js";
|
|
19
|
+
import { isTaskFile } from "../runtime/taskInput.js";
|
|
20
|
+
import { parseAskOperatorAnswer } from "../tools/askOperator.js";
|
|
21
|
+
const DEFAULT_PORT = 3847;
|
|
22
|
+
function json(res, status, body) {
|
|
23
|
+
const payload = JSON.stringify(body);
|
|
24
|
+
res.writeHead(status, {
|
|
25
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
26
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
27
|
+
});
|
|
28
|
+
res.end(payload);
|
|
29
|
+
}
|
|
30
|
+
function startFailureBody(result) {
|
|
31
|
+
const { ok: _ok, status: _status, reason, ...rest } = result;
|
|
32
|
+
return { error: reason, ...rest };
|
|
33
|
+
}
|
|
34
|
+
function inferRetryStageErrorCode(reason) {
|
|
35
|
+
if (/retry already in progress/i.test(reason))
|
|
36
|
+
return "retry_in_progress";
|
|
37
|
+
if (/waiting for input/i.test(reason))
|
|
38
|
+
return "hitl_not_retriable";
|
|
39
|
+
if (/already has active orchestration/i.test(reason))
|
|
40
|
+
return "run_not_retryable";
|
|
41
|
+
if (/run is not failed/i.test(reason))
|
|
42
|
+
return "run_not_retryable";
|
|
43
|
+
if (/stage is not failed/i.test(reason))
|
|
44
|
+
return "stage_not_failed";
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function retryStageFailureBody(result) {
|
|
48
|
+
const { ok: _ok, status: _status, reason, ...rest } = result;
|
|
49
|
+
const code = inferRetryStageErrorCode(reason);
|
|
50
|
+
return { error: reason, ...(code ? { code } : {}), ...rest };
|
|
51
|
+
}
|
|
52
|
+
function textPlain(res, status, body) {
|
|
53
|
+
res.writeHead(status, {
|
|
54
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
55
|
+
"Content-Length": Buffer.byteLength(body),
|
|
56
|
+
});
|
|
57
|
+
res.end(body);
|
|
58
|
+
}
|
|
59
|
+
async function readJsonBody(req) {
|
|
60
|
+
const chunks = [];
|
|
61
|
+
for await (const chunk of req) {
|
|
62
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
63
|
+
}
|
|
64
|
+
if (chunks.length === 0)
|
|
65
|
+
return {};
|
|
66
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
67
|
+
}
|
|
68
|
+
function contentTypeFor(filePath) {
|
|
69
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
70
|
+
switch (ext) {
|
|
71
|
+
case ".html":
|
|
72
|
+
return "text/html; charset=utf-8";
|
|
73
|
+
case ".js":
|
|
74
|
+
return "text/javascript; charset=utf-8";
|
|
75
|
+
case ".css":
|
|
76
|
+
return "text/css; charset=utf-8";
|
|
77
|
+
case ".json":
|
|
78
|
+
return "application/json; charset=utf-8";
|
|
79
|
+
case ".svg":
|
|
80
|
+
return "image/svg+xml";
|
|
81
|
+
case ".png":
|
|
82
|
+
return "image/png";
|
|
83
|
+
case ".ico":
|
|
84
|
+
return "image/x-icon";
|
|
85
|
+
default:
|
|
86
|
+
return "application/octet-stream";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function serveStatic(res, uiDistDir, urlPath) {
|
|
90
|
+
const safePath = decodeURIComponent(urlPath.split("?")[0] ?? "/");
|
|
91
|
+
let rel = safePath === "/" ? "index.html" : safePath.replace(/^\//, "");
|
|
92
|
+
if (rel.includes("..")) {
|
|
93
|
+
json(res, 400, { error: "Invalid path" });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
let filePath = path.join(uiDistDir, rel);
|
|
97
|
+
try {
|
|
98
|
+
await access(filePath);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
filePath = path.join(uiDistDir, "index.html");
|
|
102
|
+
try {
|
|
103
|
+
await access(filePath);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const data = await readFile(filePath);
|
|
110
|
+
res.writeHead(200, { "Content-Type": contentTypeFor(filePath) });
|
|
111
|
+
res.end(data);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
function isMutatingApi(method, pathname) {
|
|
115
|
+
if (method !== "POST")
|
|
116
|
+
return false;
|
|
117
|
+
return (pathname === "/api/runs" ||
|
|
118
|
+
pathname === "/api/settings" ||
|
|
119
|
+
pathname === "/api/stages" ||
|
|
120
|
+
pathname === "/api/pipelines" ||
|
|
121
|
+
/^\/api\/runs\/[^/]+\/rerun$/.test(pathname) ||
|
|
122
|
+
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/answer$/.test(pathname) ||
|
|
123
|
+
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/retry$/.test(pathname) ||
|
|
124
|
+
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/abandon$/.test(pathname) ||
|
|
125
|
+
/^\/api\/providers\/[^/]+\/login$/.test(pathname) ||
|
|
126
|
+
/^\/api\/providers\/[^/]+\/login\/[^/]+\/answer$/.test(pathname) ||
|
|
127
|
+
/^\/api\/providers\/[^/]+\/login\/[^/]+\/cancel$/.test(pathname) ||
|
|
128
|
+
/^\/api\/providers\/[^/]+\/logout$/.test(pathname));
|
|
129
|
+
}
|
|
130
|
+
function isCredentialMutatingApi(method, pathname) {
|
|
131
|
+
if (method !== "POST")
|
|
132
|
+
return false;
|
|
133
|
+
return (/^\/api\/providers\/[^/]+\/login$/.test(pathname) ||
|
|
134
|
+
/^\/api\/providers\/[^/]+\/login\/[^/]+\/answer$/.test(pathname) ||
|
|
135
|
+
/^\/api\/providers\/[^/]+\/login\/[^/]+\/cancel$/.test(pathname) ||
|
|
136
|
+
/^\/api\/providers\/[^/]+\/logout$/.test(pathname));
|
|
137
|
+
}
|
|
138
|
+
/** Login/logout require a non-empty loopback Origin (stricter than other mutating APIs). */
|
|
139
|
+
function assertCredentialMutatingOrigin(req, res) {
|
|
140
|
+
const origin = req.headers.origin;
|
|
141
|
+
if (typeof origin !== "string" || origin.length === 0) {
|
|
142
|
+
json(res, 403, { error: "Origin required" });
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const originHost = new URL(origin).hostname;
|
|
147
|
+
if (!isLoopbackHostname(originHost)) {
|
|
148
|
+
json(res, 403, { error: "Forbidden origin" });
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
json(res, 403, { error: "Forbidden origin" });
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
function hostnameFromHostHeader(hostHeader) {
|
|
159
|
+
if (hostHeader.startsWith("[")) {
|
|
160
|
+
const end = hostHeader.indexOf("]");
|
|
161
|
+
return end === -1 ? hostHeader : hostHeader.slice(1, end);
|
|
162
|
+
}
|
|
163
|
+
const colon = hostHeader.lastIndexOf(":");
|
|
164
|
+
if (colon > 0 && /^\d+$/.test(hostHeader.slice(colon + 1))) {
|
|
165
|
+
return hostHeader.slice(0, colon);
|
|
166
|
+
}
|
|
167
|
+
return hostHeader;
|
|
168
|
+
}
|
|
169
|
+
function isLoopbackHostname(hostname) {
|
|
170
|
+
const h = hostname.toLowerCase();
|
|
171
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1";
|
|
172
|
+
}
|
|
173
|
+
/** REST-friendly Host/Origin gate (plain JSON 403). Absent Origin is allowed. */
|
|
174
|
+
function assertLoopbackHttpAccess(req, res) {
|
|
175
|
+
const hostHeader = req.headers.host;
|
|
176
|
+
if (typeof hostHeader !== "string" || hostHeader.length === 0) {
|
|
177
|
+
json(res, 403, { error: "Forbidden host" });
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (!isLoopbackHostname(hostnameFromHostHeader(hostHeader))) {
|
|
181
|
+
json(res, 403, { error: "Forbidden host" });
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const origin = req.headers.origin;
|
|
185
|
+
if (typeof origin === "string" && origin.length > 0) {
|
|
186
|
+
try {
|
|
187
|
+
const originHost = new URL(origin).hostname;
|
|
188
|
+
if (!isLoopbackHostname(originHost)) {
|
|
189
|
+
json(res, 403, { error: "Forbidden origin" });
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
json(res, 403, { error: "Forbidden origin" });
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
export function defaultUiDistDir() {
|
|
201
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
202
|
+
return path.resolve(here, "../ui");
|
|
203
|
+
}
|
|
204
|
+
export async function startUiServer(options) {
|
|
205
|
+
const cwd = options.cwd ?? process.cwd();
|
|
206
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
207
|
+
const rootDir = options.rootDir ?? cwd;
|
|
208
|
+
const host = options.host ?? "127.0.0.1";
|
|
209
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
210
|
+
const providerAuthContext = options.providerAuthContext;
|
|
211
|
+
const uiDistDir = options.uiDistDir ?? defaultUiDistDir();
|
|
212
|
+
const store = options.store ??
|
|
213
|
+
createRunStore({ rootDir, kind: options.storeKind });
|
|
214
|
+
const manager = new RunManager({
|
|
215
|
+
agent: options.agent,
|
|
216
|
+
cwd,
|
|
217
|
+
store,
|
|
218
|
+
maxConcurrent: options.maxConcurrent,
|
|
219
|
+
operatorCatalog: { cwd, agentDir },
|
|
220
|
+
});
|
|
221
|
+
await manager.attachWaitingStages();
|
|
222
|
+
await manager.reconcileOrphanedStages();
|
|
223
|
+
const validateMcpHost = localhostHostValidation();
|
|
224
|
+
const validateMcpOrigin = localhostOriginValidation();
|
|
225
|
+
const server = createServer(async (req, res) => {
|
|
226
|
+
const method = req.method ?? "GET";
|
|
227
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
228
|
+
const pathname = url.pathname;
|
|
229
|
+
if (pathname === "/mcp") {
|
|
230
|
+
if (!validateMcpHost(req, res) || !validateMcpOrigin(req, res)) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
await handleMcpHttpRequest(req, res, { manager, store, cwd });
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
if (!res.headersSent) {
|
|
238
|
+
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
239
|
+
res.end(err instanceof Error ? err.message : String(err));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (isMutatingApi(method, pathname)) {
|
|
245
|
+
if (!assertLoopbackHttpAccess(req, res)) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (isCredentialMutatingApi(method, pathname) &&
|
|
249
|
+
!assertCredentialMutatingOrigin(req, res)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
if (method === "GET" && pathname === "/api/runs") {
|
|
255
|
+
json(res, 200, { runs: await store.listRuns() });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const artifactMatch = pathname.match(/^\/api\/runs\/([^/]+)\/artifact$/);
|
|
259
|
+
if (method === "GET" && artifactMatch) {
|
|
260
|
+
const runId = decodeURIComponent(artifactMatch[1] ?? "");
|
|
261
|
+
const artifactPath = url.searchParams.get("path");
|
|
262
|
+
if (artifactPath === null || artifactPath.trim().length === 0) {
|
|
263
|
+
json(res, 400, { error: "path query parameter is required" });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const content = await readRunArtifact(store, runId, artifactPath);
|
|
268
|
+
textPlain(res, 200, content);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
272
|
+
if (message === "Artifact path denied") {
|
|
273
|
+
json(res, 403, { error: message });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const notFound = message.startsWith("Run not found") ||
|
|
277
|
+
message.startsWith("Artifact not found") ||
|
|
278
|
+
/no such|not found/i.test(message);
|
|
279
|
+
json(res, notFound ? 404 : 400, { error: message });
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (method === "GET" && pathname.startsWith("/api/runs/")) {
|
|
284
|
+
const rest = pathname.slice("/api/runs/".length);
|
|
285
|
+
if (rest && !rest.includes("/")) {
|
|
286
|
+
try {
|
|
287
|
+
json(res, 200, await store.readRun(decodeURIComponent(rest)));
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
json(res, 404, {
|
|
291
|
+
error: err instanceof Error ? err.message : String(err),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (method === "POST" && pathname === "/api/runs") {
|
|
298
|
+
const body = (await readJsonBody(req));
|
|
299
|
+
if (typeof body.pipeline !== "string" ||
|
|
300
|
+
!body.pipeline.trim() ||
|
|
301
|
+
body.task === undefined) {
|
|
302
|
+
json(res, 400, { error: "task and pipeline are required" });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (typeof body.task !== "string" && !isTaskFile(body.task)) {
|
|
306
|
+
json(res, 400, {
|
|
307
|
+
error: "task must be a path string or TaskFile object (id and goal required)",
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const result = await manager.startRun({
|
|
312
|
+
task: body.task,
|
|
313
|
+
pipeline: body.pipeline.trim(),
|
|
314
|
+
});
|
|
315
|
+
if (!result.ok) {
|
|
316
|
+
json(res, result.status ?? 500, startFailureBody(result));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
json(res, 202, { runId: result.runId });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (method === "POST" && pathname.match(/^\/api\/runs\/[^/]+\/rerun$/)) {
|
|
323
|
+
const runId = decodeURIComponent(pathname.split("/")[3] ?? "");
|
|
324
|
+
const result = await manager.rerun(runId);
|
|
325
|
+
if (!result.ok) {
|
|
326
|
+
json(res, result.status ?? 500, startFailureBody(result));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
json(res, 202, { runId: result.runId });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const answerMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/answer$/);
|
|
333
|
+
if (method === "POST" && answerMatch) {
|
|
334
|
+
const runId = decodeURIComponent(answerMatch[1] ?? "");
|
|
335
|
+
const stageId = decodeURIComponent(answerMatch[2] ?? "");
|
|
336
|
+
let answer;
|
|
337
|
+
try {
|
|
338
|
+
answer = parseAskOperatorAnswer(await readJsonBody(req));
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
json(res, 400, {
|
|
342
|
+
error: err instanceof Error ? err.message : String(err),
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const result = await manager.deliverAnswer(runId, stageId, answer);
|
|
347
|
+
if (!result.ok) {
|
|
348
|
+
json(res, result.status, { error: result.reason });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
json(res, 202, { ok: true });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const retryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/retry$/);
|
|
355
|
+
if (method === "POST" && retryMatch) {
|
|
356
|
+
await readJsonBody(req);
|
|
357
|
+
const runId = decodeURIComponent(retryMatch[1] ?? "");
|
|
358
|
+
const stageId = decodeURIComponent(retryMatch[2] ?? "");
|
|
359
|
+
const result = await manager.retryStage(runId, stageId);
|
|
360
|
+
if (!result.ok) {
|
|
361
|
+
json(res, result.status ?? 500, retryStageFailureBody(result));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
json(res, 202, {
|
|
365
|
+
runId: result.runId,
|
|
366
|
+
stageId: result.stageId,
|
|
367
|
+
attemptIndex: result.attemptIndex,
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
|
|
372
|
+
if (method === "POST" && abandonMatch) {
|
|
373
|
+
await readJsonBody(req);
|
|
374
|
+
const runId = decodeURIComponent(abandonMatch[1] ?? "");
|
|
375
|
+
const stageId = decodeURIComponent(abandonMatch[2] ?? "");
|
|
376
|
+
const result = await manager.abandonStage(runId, stageId);
|
|
377
|
+
if (!result.ok) {
|
|
378
|
+
json(res, result.status ?? 500, {
|
|
379
|
+
error: result.reason,
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
json(res, 202, {
|
|
384
|
+
ok: true,
|
|
385
|
+
runId: result.runId,
|
|
386
|
+
stageId: result.stageId,
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (method === "GET" && pathname === "/api/tasks") {
|
|
391
|
+
json(res, 200, { tasks: await listTasks(cwd) });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (method === "GET" && pathname === "/api/pipelines") {
|
|
395
|
+
json(res, 200, { pipelines: await listPipelines(cwd) });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (method === "GET" && pathname === "/api/stages") {
|
|
399
|
+
json(res, 200, { stages: await listStages(cwd) });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (method === "POST" && pathname === "/api/stages") {
|
|
403
|
+
let body;
|
|
404
|
+
try {
|
|
405
|
+
body = await readJsonBody(req);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const parsed = parseCreateStageBody(body);
|
|
412
|
+
if ("ok" in parsed) {
|
|
413
|
+
json(res, parsed.status, { error: parsed.error });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const result = await createStage(cwd, parsed);
|
|
417
|
+
if (!result.ok) {
|
|
418
|
+
json(res, result.status, { error: result.error });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
json(res, 201, result.stage);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (method === "POST" && pathname === "/api/pipelines") {
|
|
425
|
+
let body;
|
|
426
|
+
try {
|
|
427
|
+
body = await readJsonBody(req);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const parsed = parseCreatePipelineBody(body);
|
|
434
|
+
if ("ok" in parsed) {
|
|
435
|
+
json(res, parsed.status, { error: parsed.error });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const result = await createPipeline(cwd, parsed);
|
|
439
|
+
if (!result.ok) {
|
|
440
|
+
json(res, result.status, { error: result.error });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
json(res, 201, result.pipeline);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (method === "GET" && pathname === "/api/models") {
|
|
447
|
+
json(res, 200, { models: await listModels(cwd) });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (await handleProviderRoutes(req, res, {
|
|
451
|
+
cwd,
|
|
452
|
+
readJsonBody,
|
|
453
|
+
json,
|
|
454
|
+
providerAuthContext,
|
|
455
|
+
})) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (method === "GET" && pathname === "/api/skills") {
|
|
459
|
+
json(res, 200, await listSkills({ cwd, agentDir }));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (method === "GET" && pathname === "/api/extensions") {
|
|
463
|
+
json(res, 200, await listExtensions({ cwd, agentDir }));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (method === "GET" && pathname === "/api/health") {
|
|
467
|
+
json(res, 200, manager.getHealth());
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (method === "GET" && pathname === "/api/settings") {
|
|
471
|
+
const health = manager.getHealth();
|
|
472
|
+
const credential = getCredentialSourceSettings(cwd);
|
|
473
|
+
json(res, 200, {
|
|
474
|
+
maxConcurrent: health.maxConcurrent,
|
|
475
|
+
...credential,
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (method === "POST" && pathname === "/api/settings") {
|
|
480
|
+
let body;
|
|
481
|
+
try {
|
|
482
|
+
body = await readJsonBody(req);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const record = body !== null && typeof body === "object" && !Array.isArray(body)
|
|
489
|
+
? body
|
|
490
|
+
: {};
|
|
491
|
+
const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
|
|
492
|
+
const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
|
|
493
|
+
if (!hasMax && !hasCredentialSource) {
|
|
494
|
+
json(res, 400, {
|
|
495
|
+
error: "maxConcurrent or credentialSource is required",
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
let health = manager.getHealth();
|
|
500
|
+
if (hasMax) {
|
|
501
|
+
const n = parseSlotCount(record.maxConcurrent);
|
|
502
|
+
if (n === undefined) {
|
|
503
|
+
json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
health = manager.setMaxConcurrent(n);
|
|
507
|
+
}
|
|
508
|
+
let credential = getCredentialSourceSettings(cwd);
|
|
509
|
+
if (hasCredentialSource) {
|
|
510
|
+
try {
|
|
511
|
+
credential = setCredentialSource(cwd, record.credentialSource);
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
const mapped = providerAuthErrorBody(err);
|
|
515
|
+
json(res, mapped.status, mapped.body);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
json(res, 200, {
|
|
520
|
+
...health,
|
|
521
|
+
...credential,
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (method === "GET") {
|
|
526
|
+
const served = await serveStatic(res, uiDistDir, pathname);
|
|
527
|
+
if (served)
|
|
528
|
+
return;
|
|
529
|
+
json(res, 404, {
|
|
530
|
+
error: "UI not built. Run npm run ui:build, or use Vite dev proxy.",
|
|
531
|
+
});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
json(res, 404, { error: "Not found" });
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
if (!res.headersSent) {
|
|
538
|
+
json(res, 500, {
|
|
539
|
+
error: err instanceof Error ? err.message : String(err),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
await new Promise((resolve, reject) => {
|
|
545
|
+
server.listen(port, host, () => resolve());
|
|
546
|
+
server.on("error", reject);
|
|
547
|
+
});
|
|
548
|
+
const address = server.address();
|
|
549
|
+
const boundPort = address && typeof address !== "string" ? address.port : port;
|
|
550
|
+
const url = `http://${host}:${boundPort}`;
|
|
551
|
+
return {
|
|
552
|
+
server,
|
|
553
|
+
port: boundPort,
|
|
554
|
+
host,
|
|
555
|
+
url,
|
|
556
|
+
mcpUrl: `${url}/mcp`,
|
|
557
|
+
manager,
|
|
558
|
+
store,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
export { DEFAULT_PORT };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { type ProviderAuthContext } from "../agent/providerAuth.js";
|
|
3
|
+
export type ProviderRoutesCtx = {
|
|
4
|
+
cwd: string;
|
|
5
|
+
readJsonBody: (req: IncomingMessage) => Promise<unknown>;
|
|
6
|
+
json: (res: ServerResponse, status: number, body: unknown) => void;
|
|
7
|
+
providerAuthContext?: ProviderAuthContext;
|
|
8
|
+
};
|
|
9
|
+
export declare function providerAuthErrorBody(err: unknown): {
|
|
10
|
+
status: number;
|
|
11
|
+
body: {
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare function handleProviderRoutes(req: IncomingMessage, res: ServerResponse, ctx: ProviderRoutesCtx): Promise<boolean>;
|