stageflow 0.30.0 → 0.32.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/dist/agent/cursorProvider.d.ts +8 -0
- package/dist/agent/cursorProvider.js +63 -13
- package/dist/agent/piAdapter.js +21 -0
- package/dist/agent/port.d.ts +2 -0
- package/dist/agent/providerAuth.d.ts +7 -3
- package/dist/agent/providerAuth.js +5 -3
- package/dist/cli/doctorCommand.js +7 -0
- package/dist/cli/providersCommand.d.ts +1 -1
- package/dist/cli/providersCommand.js +4 -4
- package/dist/cli.js +1 -1
- package/dist/config/loadStage.js +10 -0
- package/dist/config/pipelineStageKeys.js +1 -0
- package/dist/email/accounts.d.ts +109 -0
- package/dist/email/accounts.js +285 -0
- package/dist/email/adapter.d.ts +69 -0
- package/dist/email/adapter.js +720 -0
- package/dist/email/attachments.d.ts +21 -0
- package/dist/email/attachments.js +146 -0
- package/dist/email/events.d.ts +95 -0
- package/dist/email/events.js +292 -0
- package/dist/email/host.d.ts +15 -0
- package/dist/email/host.js +119 -0
- package/dist/email/messages.d.ts +44 -0
- package/dist/email/messages.js +182 -0
- package/dist/email/port.d.ts +161 -0
- package/dist/email/port.js +12 -0
- package/dist/email/replies.d.ts +5 -0
- package/dist/email/replies.js +56 -0
- package/dist/email/submissions.d.ts +31 -0
- package/dist/email/submissions.js +123 -0
- package/dist/email/triggers.d.ts +140 -0
- package/dist/email/triggers.js +563 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/diskUsage.d.ts +1 -1
- package/dist/runstore/diskUsage.js +2 -7
- package/dist/runstore/port.d.ts +4 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +1 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +24 -5
- package/dist/runstore/sqlite/migrations/011-email-dispatch-key.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/011-email-dispatch-key.js +12 -0
- package/dist/runstore/sqlite/migrations/index.js +2 -0
- package/dist/runtime/credentialBinding.d.ts +7 -4
- package/dist/runtime/credentialBinding.js +20 -31
- package/dist/runtime/pipelineRunner.d.ts +3 -0
- package/dist/runtime/pipelineRunner.js +11 -0
- package/dist/runtime/pipelineScheduler.js +4 -0
- package/dist/runtime/runManager.d.ts +1 -0
- package/dist/runtime/runManager.js +36 -5
- package/dist/runtime/settingsFile.d.ts +2 -2
- package/dist/runtime/settingsFile.js +5 -2
- package/dist/runtime/stageAttemptBootstrap.js +14 -0
- package/dist/runtime/stageProcessLauncher.d.ts +5 -0
- package/dist/runtime/stageProcessLauncher.js +55 -3
- package/dist/server/bootstrap.d.ts +7 -0
- package/dist/server/bootstrap.js +23 -0
- package/dist/server/emailRoutes.d.ts +5 -0
- package/dist/server/emailRoutes.js +136 -0
- package/dist/server/http.d.ts +2 -0
- package/dist/server/http.js +16 -0
- package/dist/tools/readEmail.d.ts +77 -0
- package/dist/tools/readEmail.js +31 -0
- package/dist/tools/replyEmail.d.ts +31 -0
- package/dist/tools/replyEmail.js +25 -0
- package/dist/tools/sendEmail.d.ts +43 -0
- package/dist/tools/sendEmail.js +26 -0
- package/dist/types/stage.d.ts +4 -0
- package/dist/ui/assets/index-59cjF0OA.js +133 -0
- package/dist/ui/index.html +1 -1
- package/examples/conditional-fork/decide.yaml +1 -1
- package/examples/github-release/README.md +17 -3
- package/examples/hello-email/README.md +23 -0
- package/examples/hello-email/hello-email-reply.pipeline.yaml +6 -0
- package/examples/hello-email/hello-email.pipeline.yaml +6 -0
- package/examples/hello-email/hello-email.task.yaml +6 -0
- package/examples/hello-email/reply-hello-email.yaml +63 -0
- package/examples/hello-email/send-hello-email.yaml +37 -0
- package/package.json +10 -5
- package/dist/ui/assets/index-CntB4bZR.js +0 -133
|
@@ -189,9 +189,13 @@ export class SqliteRunStore {
|
|
|
189
189
|
}
|
|
190
190
|
async createRun(input) {
|
|
191
191
|
await this.ready();
|
|
192
|
+
const existing = input.dispatchKey ? await this.findRunByDispatchKey(input.dispatchKey) : undefined;
|
|
193
|
+
if (existing)
|
|
194
|
+
return existing;
|
|
192
195
|
const runId = input.runId ?? newRunId();
|
|
193
196
|
const workspaceDir = this.getWorkspaceDir(runId);
|
|
194
|
-
|
|
197
|
+
if (!input.dispatchKey)
|
|
198
|
+
await mkdir(path.join(workspaceDir, "stages"), { recursive: true });
|
|
195
199
|
const now = new Date().toISOString();
|
|
196
200
|
const pipelinePath = input.pipelinePath
|
|
197
201
|
? normalizeCatalogPath(input.pipelinePath)
|
|
@@ -214,13 +218,15 @@ export class SqliteRunStore {
|
|
|
214
218
|
if (existing)
|
|
215
219
|
throw new RunSubmissionExistsError(existing);
|
|
216
220
|
}
|
|
217
|
-
this.db
|
|
221
|
+
const inserted = this.db
|
|
218
222
|
.prepare(`INSERT INTO runs
|
|
219
|
-
(run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, repository, ref, resolved_sha, run_branch, git_author_name, git_author_email, pipeline_source, pipeline_body, caller_id, run_manifest, skip_gates)
|
|
223
|
+
(run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, repository, ref, resolved_sha, run_branch, git_author_name, git_author_email, pipeline_source, pipeline_body, caller_id, run_manifest, skip_gates, dispatch_key)
|
|
220
224
|
VALUES
|
|
221
|
-
(@run_id, @pipeline_id, @task_id, @task_yaml, @status, @created_at, @updated_at, @checkout_root, @pipeline_dag_json, @git_sha, @ci_pr_url, @ci_job_url, @pipeline_path, @task_path, @project_root, @repository, @ref, @resolved_sha, @run_branch, @git_author_name, @git_author_email, @pipeline_source, @pipeline_body, @caller_id, @run_manifest, @skip_gates)
|
|
225
|
+
(@run_id, @pipeline_id, @task_id, @task_yaml, @status, @created_at, @updated_at, @checkout_root, @pipeline_dag_json, @git_sha, @ci_pr_url, @ci_job_url, @pipeline_path, @task_path, @project_root, @repository, @ref, @resolved_sha, @run_branch, @git_author_name, @git_author_email, @pipeline_source, @pipeline_body, @caller_id, @run_manifest, @skip_gates, @dispatch_key)
|
|
226
|
+
ON CONFLICT(dispatch_key) DO NOTHING`)
|
|
222
227
|
.run({
|
|
223
228
|
run_id: runId,
|
|
229
|
+
dispatch_key: input.dispatchKey ?? null,
|
|
224
230
|
pipeline_id: input.pipelineId,
|
|
225
231
|
task_id: input.taskId ?? null,
|
|
226
232
|
task_yaml: input.taskYaml,
|
|
@@ -251,12 +257,25 @@ export class SqliteRunStore {
|
|
|
251
257
|
: null,
|
|
252
258
|
skip_gates: input.skipGates === undefined ? null : input.skipGates ? 1 : 0,
|
|
253
259
|
});
|
|
260
|
+
if (!inserted.changes)
|
|
261
|
+
return;
|
|
254
262
|
if (input.submission) {
|
|
255
263
|
this.db.prepare("INSERT INTO run_submissions (submission_key, request_hash, run_id) VALUES (?, ?, ?)")
|
|
256
264
|
.run(input.submission.key, input.submission.requestHash, runId);
|
|
257
265
|
}
|
|
258
266
|
})();
|
|
259
|
-
|
|
267
|
+
if (input.dispatchKey) {
|
|
268
|
+
const selected = await this.findRunByDispatchKey(input.dispatchKey);
|
|
269
|
+
if (selected && selected.runId !== runId)
|
|
270
|
+
return selected;
|
|
271
|
+
await mkdir(path.join(workspaceDir, "stages"), { recursive: true });
|
|
272
|
+
}
|
|
273
|
+
return { runId, workspaceDir, ...(input.dispatchKey ? { created: true } : {}) };
|
|
274
|
+
}
|
|
275
|
+
async findRunByDispatchKey(dispatchKey) {
|
|
276
|
+
await this.ready();
|
|
277
|
+
const row = this.db.prepare("SELECT run_id FROM runs WHERE dispatch_key = ?").get(dispatchKey);
|
|
278
|
+
return row ? { runId: row.run_id, workspaceDir: this.getWorkspaceDir(row.run_id), created: false } : undefined;
|
|
260
279
|
}
|
|
261
280
|
readSubmission(key) {
|
|
262
281
|
const row = this.db.prepare("SELECT submission_key AS key, request_hash AS requestHash, run_id AS runId FROM run_submissions WHERE submission_key = ?")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "../../../package-meta.js";
|
|
2
|
+
export const MIGRATION_011 = {
|
|
3
|
+
version: 11,
|
|
4
|
+
name: "011_email_dispatch_key",
|
|
5
|
+
minStageflowVersion: PACKAGE_VERSION,
|
|
6
|
+
up(db) {
|
|
7
|
+
const columns = db.prepare("PRAGMA table_info(runs)").all();
|
|
8
|
+
if (!columns.some(column => column.name === "dispatch_key"))
|
|
9
|
+
db.exec("ALTER TABLE runs ADD COLUMN dispatch_key TEXT");
|
|
10
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_dispatch_key ON runs(dispatch_key)");
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -9,6 +9,7 @@ import { MIGRATION_007 } from "./007-projects-registry.js";
|
|
|
9
9
|
import { MIGRATION_008 } from "./008-triggers-table.js";
|
|
10
10
|
import { MIGRATION_009 } from "./009-trigger-next-run.js";
|
|
11
11
|
import { MIGRATION_010 } from "./010-trigger-adapter-state.js";
|
|
12
|
+
import { MIGRATION_011 } from "./011-email-dispatch-key.js";
|
|
12
13
|
const DEFAULT_MIGRATIONS = [
|
|
13
14
|
MIGRATION_001,
|
|
14
15
|
MIGRATION_002,
|
|
@@ -20,6 +21,7 @@ const DEFAULT_MIGRATIONS = [
|
|
|
20
21
|
MIGRATION_008,
|
|
21
22
|
MIGRATION_009,
|
|
22
23
|
MIGRATION_010,
|
|
24
|
+
MIGRATION_011,
|
|
23
25
|
];
|
|
24
26
|
export const CURRENT_SCHEMA_VERSION = DEFAULT_MIGRATIONS[DEFAULT_MIGRATIONS.length - 1].version;
|
|
25
27
|
function throwStoreSchemaTooNew(db, version, maxKnown) {
|
|
@@ -7,14 +7,17 @@ export type CredentialBinding = {
|
|
|
7
7
|
provisional: boolean;
|
|
8
8
|
};
|
|
9
9
|
export type ResolveCredentialBindingOptions = {
|
|
10
|
-
|
|
10
|
+
agentAuthPath?: string;
|
|
11
11
|
};
|
|
12
12
|
export declare function sfOwnedAgentDir(): string;
|
|
13
13
|
export declare function sfOwnedAuthPath(): string;
|
|
14
|
-
export declare const
|
|
15
|
-
/**
|
|
14
|
+
export declare const STAGEFLOW_AGENT_AUTH_PATH_ENV = "STAGEFLOW_AGENT_AUTH_PATH";
|
|
15
|
+
/** @deprecated Use STAGEFLOW_AGENT_AUTH_PATH_ENV. */
|
|
16
|
+
export declare const PI_HOME_AUTH_PATH_ENV = "STAGEFLOW_AGENT_AUTH_PATH";
|
|
17
|
+
export declare function stageflowAgentAuthPath(override?: string): string;
|
|
18
|
+
/** @deprecated Use stageflowAgentAuthPath */
|
|
16
19
|
export declare function piHomeAuthPath(override?: string): string;
|
|
17
20
|
export declare function isUsableAuthFile(authPath: string): boolean;
|
|
18
21
|
export declare function ensureSfOwnedAuthStore(): string;
|
|
19
|
-
export declare function resolveCredentialBinding(ctx: ProjectContext | string,
|
|
22
|
+
export declare function resolveCredentialBinding(ctx: ProjectContext | string, _options?: ResolveCredentialBindingOptions): CredentialBinding;
|
|
20
23
|
export { readCredentialSourceFromFile, writeCredentialSourceToFile, } from "./settingsFile.js";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { ensureGlobalHome } from "../project/globalHome.js";
|
|
5
4
|
import { resolveProjectContext, } from "../project/resolveProjectContext.js";
|
|
@@ -11,12 +10,23 @@ export function sfOwnedAgentDir() {
|
|
|
11
10
|
export function sfOwnedAuthPath() {
|
|
12
11
|
return path.join(sfOwnedAgentDir(), "auth.json");
|
|
13
12
|
}
|
|
14
|
-
export const
|
|
15
|
-
/**
|
|
13
|
+
export const STAGEFLOW_AGENT_AUTH_PATH_ENV = "STAGEFLOW_AGENT_AUTH_PATH";
|
|
14
|
+
/** @deprecated Use STAGEFLOW_AGENT_AUTH_PATH_ENV. */
|
|
15
|
+
export const PI_HOME_AUTH_PATH_ENV = STAGEFLOW_AGENT_AUTH_PATH_ENV;
|
|
16
|
+
export function stageflowAgentAuthPath(override) {
|
|
17
|
+
if (override !== undefined) {
|
|
18
|
+
return path.resolve(override);
|
|
19
|
+
}
|
|
20
|
+
const fromEnv = process.env[STAGEFLOW_AGENT_AUTH_PATH_ENV]?.trim() ||
|
|
21
|
+
process.env.STAGEFLOW_PI_HOME_AUTH_PATH?.trim();
|
|
22
|
+
if (fromEnv) {
|
|
23
|
+
return path.resolve(fromEnv);
|
|
24
|
+
}
|
|
25
|
+
return sfOwnedAuthPath();
|
|
26
|
+
}
|
|
27
|
+
/** @deprecated Use stageflowAgentAuthPath */
|
|
16
28
|
export function piHomeAuthPath(override) {
|
|
17
|
-
return (override
|
|
18
|
-
(process.env[PI_HOME_AUTH_PATH_ENV] || undefined) ??
|
|
19
|
-
path.join(os.homedir(), ".pi", "agent", "auth.json"));
|
|
29
|
+
return stageflowAgentAuthPath(override);
|
|
20
30
|
}
|
|
21
31
|
export function isUsableAuthFile(authPath) {
|
|
22
32
|
if (!existsSync(authPath))
|
|
@@ -43,35 +53,14 @@ export function ensureSfOwnedAuthStore() {
|
|
|
43
53
|
}
|
|
44
54
|
return authPath;
|
|
45
55
|
}
|
|
46
|
-
export function resolveCredentialBinding(ctx,
|
|
56
|
+
export function resolveCredentialBinding(ctx, _options = {}) {
|
|
47
57
|
const projectCtx = typeof ctx === "string" ? resolveProjectContext(ctx) : ctx;
|
|
48
|
-
const piHome = piHomeAuthPath(options.piHomeAuthPath);
|
|
49
58
|
const persisted = readCredentialSourceFromContext(projectCtx);
|
|
50
|
-
|
|
51
|
-
if (persisted === "sf_owned") {
|
|
52
|
-
return {
|
|
53
|
-
source: "sf_owned",
|
|
54
|
-
authPath: ensureSfOwnedAuthStore(),
|
|
55
|
-
provisional: false,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
source: "pi_home",
|
|
60
|
-
authPath: piHome,
|
|
61
|
-
provisional: false,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
if (isUsableAuthFile(piHome)) {
|
|
65
|
-
return {
|
|
66
|
-
source: "pi_home",
|
|
67
|
-
authPath: piHome,
|
|
68
|
-
provisional: true,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
59
|
+
const authPath = ensureSfOwnedAuthStore();
|
|
71
60
|
return {
|
|
72
61
|
source: "sf_owned",
|
|
73
|
-
authPath
|
|
74
|
-
provisional:
|
|
62
|
+
authPath,
|
|
63
|
+
provisional: persisted === undefined,
|
|
75
64
|
};
|
|
76
65
|
}
|
|
77
66
|
export { readCredentialSourceFromFile, writeCredentialSourceToFile, } from "./settingsFile.js";
|
|
@@ -41,6 +41,7 @@ export type PipelineRunResult = {
|
|
|
41
41
|
findings?: ValidationFinding[];
|
|
42
42
|
};
|
|
43
43
|
export type StartedPipeline = {
|
|
44
|
+
created?: boolean;
|
|
44
45
|
runId: string;
|
|
45
46
|
runDir: string;
|
|
46
47
|
done: Promise<PipelineRunResult>;
|
|
@@ -51,6 +52,7 @@ export type PreparedPipeline = {
|
|
|
51
52
|
run: {
|
|
52
53
|
runId: string;
|
|
53
54
|
workspaceDir: string;
|
|
55
|
+
created?: boolean;
|
|
54
56
|
};
|
|
55
57
|
agent: AgentPort;
|
|
56
58
|
store: RunStore;
|
|
@@ -97,6 +99,7 @@ export declare function runPipeline(options: {
|
|
|
97
99
|
}): Promise<PipelineRunResult>;
|
|
98
100
|
/** Create the run immediately, then execute stages in the returned promise. */
|
|
99
101
|
export declare function startPipeline(options: {
|
|
102
|
+
dispatchKey?: string;
|
|
100
103
|
submission?: RunSubmission;
|
|
101
104
|
agent: AgentPort;
|
|
102
105
|
store: RunStore;
|
|
@@ -197,6 +197,7 @@ async function preparePipeline(options) {
|
|
|
197
197
|
namedSecrets,
|
|
198
198
|
});
|
|
199
199
|
run = await options.store.createRun({
|
|
200
|
+
dispatchKey: options.dispatchKey,
|
|
200
201
|
submission: options.submission,
|
|
201
202
|
runId,
|
|
202
203
|
pipelineId: loaded.pipeline.id,
|
|
@@ -299,6 +300,7 @@ export async function startPipeline(options) {
|
|
|
299
300
|
const cwd = options.cwd ?? process.cwd();
|
|
300
301
|
const projectRoot = options.projectRoot ?? cwd;
|
|
301
302
|
const prepared = await preparePipeline({
|
|
303
|
+
dispatchKey: options.dispatchKey,
|
|
302
304
|
submission: options.submission,
|
|
303
305
|
agent: options.agent,
|
|
304
306
|
store: options.store,
|
|
@@ -327,6 +329,15 @@ export async function startPipeline(options) {
|
|
|
327
329
|
callerId: options.callerId,
|
|
328
330
|
...(options.skills !== undefined ? { skills: options.skills } : {}),
|
|
329
331
|
});
|
|
332
|
+
if (prepared.run.created === false) {
|
|
333
|
+
const meta = await prepared.store.readRunMeta(prepared.run.runId);
|
|
334
|
+
return {
|
|
335
|
+
created: false,
|
|
336
|
+
runId: prepared.run.runId,
|
|
337
|
+
runDir: prepared.run.workspaceDir,
|
|
338
|
+
done: Promise.resolve({ ok: meta.status === "succeeded", outcome: meta.status === "succeeded" ? "succeeded" : "failed", runId: prepared.run.runId, runDir: prepared.run.workspaceDir, reason: "Recovered existing dispatch; execution was not restarted" }),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
330
341
|
const done = executeStages(prepared, {
|
|
331
342
|
maxActiveStagesPerRun: options.maxActiveStagesPerRun,
|
|
332
343
|
executionMode: prepared.executionMode,
|
|
@@ -968,9 +968,13 @@ export async function runPipelineDag(options) {
|
|
|
968
968
|
runId: run.runId,
|
|
969
969
|
stageId,
|
|
970
970
|
rootDir: factoryCwd,
|
|
971
|
+
stage,
|
|
972
|
+
factoryCwd,
|
|
973
|
+
workspaceDir: run.workspaceDir,
|
|
971
974
|
attempt,
|
|
972
975
|
env: stageBinding.env,
|
|
973
976
|
bindingKind: stageBinding.kind,
|
|
977
|
+
model: stage.model,
|
|
974
978
|
grants,
|
|
975
979
|
attemptHome,
|
|
976
980
|
...(browserEnv !== undefined ? { browserEnv } : {}),
|
|
@@ -1153,6 +1153,15 @@ export class RunManager {
|
|
|
1153
1153
|
? (findProjectRoot(path.dirname(path.resolve(hostCwd, input.pipeline))) ?? this.projectRoot)
|
|
1154
1154
|
: this.projectRoot);
|
|
1155
1155
|
const cwd = wireRoot ?? hostCwd;
|
|
1156
|
+
if (input.dispatchKey) {
|
|
1157
|
+
if (!this.options.store.findRunByDispatchKey)
|
|
1158
|
+
throw new Error("Run store does not support durable dispatch");
|
|
1159
|
+
const existing = await this.options.store.findRunByDispatchKey(input.dispatchKey);
|
|
1160
|
+
if (existing) {
|
|
1161
|
+
const meta = await this.options.store.readRunMeta(existing.runId);
|
|
1162
|
+
return { ok: true, runId: existing.runId, done: Promise.resolve({ ok: meta.status === "succeeded", outcome: meta.status === "succeeded" ? "succeeded" : "failed", runId: existing.runId, runDir: existing.workspaceDir, reason: "Recovered existing dispatch; execution was not restarted" }) };
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1156
1165
|
let resolved;
|
|
1157
1166
|
try {
|
|
1158
1167
|
resolved = resolveStartTaskInput(input, cwd);
|
|
@@ -1187,7 +1196,7 @@ export class RunManager {
|
|
|
1187
1196
|
gitSha: input.gitSha,
|
|
1188
1197
|
ciPrUrl: input.ciPrUrl,
|
|
1189
1198
|
ciJobUrl: input.ciJobUrl,
|
|
1190
|
-
}, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission, undefined, input.callerId, input.skills);
|
|
1199
|
+
}, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission, undefined, input.callerId, input.skills, input.dispatchKey);
|
|
1191
1200
|
}
|
|
1192
1201
|
async rerun(runId, options) {
|
|
1193
1202
|
if (!this.acceptingWork) {
|
|
@@ -1972,6 +1981,9 @@ export class RunManager {
|
|
|
1972
1981
|
runId,
|
|
1973
1982
|
stageId,
|
|
1974
1983
|
rootDir: runProjectRoot,
|
|
1984
|
+
factoryCwd: runProjectRoot,
|
|
1985
|
+
stage: resumedStage,
|
|
1986
|
+
workspaceDir,
|
|
1975
1987
|
...(browserEnv !== undefined ? { browserEnv } : {}),
|
|
1976
1988
|
mode: "resume",
|
|
1977
1989
|
resumeAnswer: opaqueAnswer,
|
|
@@ -1979,6 +1991,7 @@ export class RunManager {
|
|
|
1979
1991
|
sessionFilePath,
|
|
1980
1992
|
env: stageBinding.env,
|
|
1981
1993
|
bindingKind: stageBinding.kind,
|
|
1994
|
+
...(resumedStage !== undefined ? { model: resumedStage.model } : {}),
|
|
1982
1995
|
...(this.options.operatorCatalog !== undefined
|
|
1983
1996
|
? { operatorCatalog: this.options.operatorCatalog }
|
|
1984
1997
|
: {}),
|
|
@@ -2134,7 +2147,7 @@ export class RunManager {
|
|
|
2134
2147
|
this.attachedWaiting.delete(`${runId}\0${stageId}`);
|
|
2135
2148
|
}
|
|
2136
2149
|
}
|
|
2137
|
-
async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath, submission, pinned, callerId, skills) {
|
|
2150
|
+
async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath, submission, pinned, callerId, skills, dispatchKey) {
|
|
2138
2151
|
const persistence = pipelinePersistenceForStart(pipeline);
|
|
2139
2152
|
if (!persistence.ok) {
|
|
2140
2153
|
return {
|
|
@@ -2307,6 +2320,7 @@ export class RunManager {
|
|
|
2307
2320
|
namedSecrets,
|
|
2308
2321
|
});
|
|
2309
2322
|
const created = await this.options.store.createRun({
|
|
2323
|
+
dispatchKey,
|
|
2310
2324
|
submission,
|
|
2311
2325
|
runId,
|
|
2312
2326
|
pipelineId,
|
|
@@ -2334,6 +2348,10 @@ export class RunManager {
|
|
|
2334
2348
|
runManifest,
|
|
2335
2349
|
skipGates,
|
|
2336
2350
|
});
|
|
2351
|
+
if (created.created === false) {
|
|
2352
|
+
const existing = await this.options.store.readRunMeta(created.runId);
|
|
2353
|
+
return { ok: true, runId: created.runId, done: Promise.resolve({ ok: existing.status === "succeeded", outcome: existing.status === "succeeded" ? "succeeded" : "failed", runId: created.runId, runDir: created.workspaceDir, reason: "Recovered existing dispatch; execution was not restarted" }) };
|
|
2354
|
+
}
|
|
2337
2355
|
if (hasSkills) {
|
|
2338
2356
|
await materializeRunSkills(created.workspaceDir, validatedSkills);
|
|
2339
2357
|
}
|
|
@@ -2386,6 +2404,7 @@ export class RunManager {
|
|
|
2386
2404
|
rollback = linkRollback;
|
|
2387
2405
|
const schedulingHalt = this.ensureSchedulingHalt(materialized.runId);
|
|
2388
2406
|
const started = await startPipeline({
|
|
2407
|
+
dispatchKey,
|
|
2389
2408
|
submission,
|
|
2390
2409
|
...(this.options.browser !== undefined
|
|
2391
2410
|
? { browser: this.options.browser }
|
|
@@ -2417,7 +2436,10 @@ export class RunManager {
|
|
|
2417
2436
|
callerId,
|
|
2418
2437
|
...(hasSkills ? { skills: validatedSkills } : {}),
|
|
2419
2438
|
});
|
|
2420
|
-
|
|
2439
|
+
if (started.created === false)
|
|
2440
|
+
this.clearReservation(admitted.provisionalId);
|
|
2441
|
+
else
|
|
2442
|
+
this.track(admitted.provisionalId, started.runId, started.done);
|
|
2421
2443
|
return { ok: true, runId: started.runId, done: started.done };
|
|
2422
2444
|
}
|
|
2423
2445
|
catch (err) {
|
|
@@ -2506,17 +2528,26 @@ export class RunManager {
|
|
|
2506
2528
|
};
|
|
2507
2529
|
}
|
|
2508
2530
|
async checkDiskFloorAdmission() {
|
|
2531
|
+
const raw = process.env.STAGEFLOW_MIN_FREE_DISK_BYTES;
|
|
2532
|
+
if (raw === undefined || raw.trim() === "") {
|
|
2533
|
+
return undefined;
|
|
2534
|
+
}
|
|
2509
2535
|
try {
|
|
2510
2536
|
const readFree = this.options.freeSpaceReader ?? readFilesystemSize;
|
|
2511
2537
|
const size = await readFree(globalStageflowHome());
|
|
2512
|
-
const minFreeBytes = resolveMinFreeDiskFloor(
|
|
2538
|
+
const minFreeBytes = resolveMinFreeDiskFloor(raw, size.totalBytes);
|
|
2539
|
+
if (minFreeBytes === 0) {
|
|
2540
|
+
return undefined;
|
|
2541
|
+
}
|
|
2513
2542
|
if (size.freeBytes < minFreeBytes) {
|
|
2514
2543
|
return this.insufficientDiskFailure(size.freeBytes, minFreeBytes);
|
|
2515
2544
|
}
|
|
2516
2545
|
return undefined;
|
|
2517
2546
|
}
|
|
2518
2547
|
catch (err) {
|
|
2519
|
-
|
|
2548
|
+
if (resolveMinFreeDiskFloor(raw, 0) === 0 && !raw.trim().endsWith("%")) {
|
|
2549
|
+
return undefined;
|
|
2550
|
+
}
|
|
2520
2551
|
return {
|
|
2521
2552
|
ok: false,
|
|
2522
2553
|
reason: `Disk free-space check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -2,9 +2,9 @@ import type { ProjectContext } from "../project/resolveProjectContext.js";
|
|
|
2
2
|
import type { StageflowContext } from "../project/resolveStageflowContext.js";
|
|
3
3
|
export declare const SETTINGS_FILE_NAME = "settings.json";
|
|
4
4
|
export declare const INVALID_SLOT_COUNT_MESSAGE = "maxConcurrent must be an integer >= 1";
|
|
5
|
-
export declare const INVALID_CREDENTIAL_SOURCE_MESSAGE = "credentialSource must be \"
|
|
5
|
+
export declare const INVALID_CREDENTIAL_SOURCE_MESSAGE = "credentialSource must be \"sf_owned\"";
|
|
6
6
|
export declare const INVALID_WORKSHOP_MODEL_MESSAGE = "workshopModel must be a non-empty string";
|
|
7
|
-
export type CredentialSource = "
|
|
7
|
+
export type CredentialSource = "sf_owned";
|
|
8
8
|
export type FactorySettings = {
|
|
9
9
|
maxConcurrent?: number;
|
|
10
10
|
credentialSource?: CredentialSource;
|
|
@@ -5,7 +5,7 @@ import { globalStageflowHome } from "../project/globalHome.js";
|
|
|
5
5
|
import { storeRootFor } from "../runstore/paths.js";
|
|
6
6
|
export const SETTINGS_FILE_NAME = "settings.json";
|
|
7
7
|
export const INVALID_SLOT_COUNT_MESSAGE = "maxConcurrent must be an integer >= 1";
|
|
8
|
-
export const INVALID_CREDENTIAL_SOURCE_MESSAGE = 'credentialSource must be "
|
|
8
|
+
export const INVALID_CREDENTIAL_SOURCE_MESSAGE = 'credentialSource must be "sf_owned"';
|
|
9
9
|
export const INVALID_WORKSHOP_MODEL_MESSAGE = "workshopModel must be a non-empty string";
|
|
10
10
|
export function globalSettingsFilePath() {
|
|
11
11
|
return path.join(globalStageflowHome(), SETTINGS_FILE_NAME);
|
|
@@ -29,9 +29,12 @@ export function parseSlotCount(value) {
|
|
|
29
29
|
return value;
|
|
30
30
|
}
|
|
31
31
|
export function parseCredentialSource(value) {
|
|
32
|
-
if (value === "
|
|
32
|
+
if (value === "sf_owned") {
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
|
+
if (value === "pi_home") {
|
|
36
|
+
return "sf_owned";
|
|
37
|
+
}
|
|
35
38
|
return undefined;
|
|
36
39
|
}
|
|
37
40
|
export function parseWorkshopModelSetting(value) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { emailHostFor, stageEmail, workerStageEmail, validateStageEmailAccounts } from "../email/host.js";
|
|
2
|
+
import { EmailError } from "../email/port.js";
|
|
1
3
|
import { bindingKindForOrigin, catalogOrSeededOrigin, decideWorkspaceConfigTrust, } from "../config/configOrigin.js";
|
|
2
4
|
import { loginCheckPromptBlock, readStageLoginCheck, } from "../browser/loginCheck.js";
|
|
3
5
|
import { humanLoginPromptBlock, isHumanLoginStage } from "../browser/humanLogin.js";
|
|
@@ -429,6 +431,16 @@ export async function openStageAttempt(input) {
|
|
|
429
431
|
const meta = await input.store.readRunMeta(input.runId);
|
|
430
432
|
const bindingKind = input.bindingKind ?? bindingKindFromMeta(meta);
|
|
431
433
|
const trustWorkspaceConfig = resolveTrustWorkspaceConfig(input.trustWorkspaceConfig);
|
|
434
|
+
if (process.env.SF_STAGE_WORKER !== "1" && input.stage.email?.length) {
|
|
435
|
+
try {
|
|
436
|
+
validateStageEmailAccounts(emailHostFor(input.factoryCwd ?? process.cwd()).accounts, input.stage);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (error instanceof EmailError)
|
|
440
|
+
return { ok: false, reason: error.code };
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
432
444
|
const opened = await openStageWithOperatorCatalog(input.agent, {
|
|
433
445
|
roots,
|
|
434
446
|
stage: input.stage,
|
|
@@ -442,6 +454,8 @@ export async function openStageAttempt(input) {
|
|
|
442
454
|
? { priorEnvelopesByStage: priorResult.priorEnvelopesByStage }
|
|
443
455
|
: {}),
|
|
444
456
|
resumeToken,
|
|
457
|
+
email: input.stage.email?.length ? (process.env.SF_STAGE_WORKER === "1" ? workerStageEmail()
|
|
458
|
+
: stageEmail(emailHostFor(input.factoryCwd ?? process.cwd()).mailbox, input.stage, input.runId, { workspaceDir: input.workspaceDir, attempt })) : undefined,
|
|
445
459
|
...(input.sessionMode !== undefined ? { sessionMode: input.sessionMode } : {}),
|
|
446
460
|
onActivity: (event) => {
|
|
447
461
|
input.onActivity?.(input.stage.browser !== undefined
|
|
@@ -2,22 +2,27 @@ import * as childProcess from "node:child_process";
|
|
|
2
2
|
import { type Logger } from "../logging/logger.js";
|
|
3
3
|
import { type ResolvedStageGrants } from "./stageEnvironment.js";
|
|
4
4
|
import type { OperatorCatalog } from "./stageAttemptBootstrap.js";
|
|
5
|
+
import type { StageConfig } from "../types/stage.js";
|
|
5
6
|
import { type DerivedBindingKind } from "./stageRoots.js";
|
|
6
7
|
export type StageLaunchInput = {
|
|
7
8
|
runId: string;
|
|
8
9
|
stageId: string;
|
|
9
10
|
rootDir: string;
|
|
11
|
+
workspaceDir?: string;
|
|
10
12
|
mode?: "run" | "resume" | "feedback_resume" | "new_session";
|
|
11
13
|
resumeAnswer?: unknown;
|
|
12
14
|
attempt?: number;
|
|
13
15
|
sessionFilePath?: string;
|
|
14
16
|
operatorCatalog?: OperatorCatalog;
|
|
17
|
+
factoryCwd?: string;
|
|
18
|
+
stage?: StageConfig;
|
|
15
19
|
skipGates?: boolean;
|
|
16
20
|
env?: Record<string, string>;
|
|
17
21
|
bindingKind?: DerivedBindingKind;
|
|
18
22
|
grants?: ResolvedStageGrants;
|
|
19
23
|
attemptHome?: string;
|
|
20
24
|
browserEnv?: Record<string, string>;
|
|
25
|
+
model?: string;
|
|
21
26
|
};
|
|
22
27
|
export type StageLaunchResult = {
|
|
23
28
|
type: "succeeded";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as childProcess from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { logger as rootLogger, resolveLogMaxLineBytes, } from "../logging/logger.js";
|
|
4
|
+
import { isCursorModelRef, readCursorApiKey } from "../agent/cursorProvider.js";
|
|
4
5
|
import { BROWSER_ENV_PREFIX } from "../browser/browserHost.js";
|
|
5
6
|
import { PACKAGE_VERSION } from "../package-meta.js";
|
|
6
|
-
import { PI_HOME_AUTH_PATH_ENV,
|
|
7
|
+
import { PI_HOME_AUTH_PATH_ENV, stageflowAgentAuthPath } from "./credentialBinding.js";
|
|
7
8
|
import { redactString } from "../logging/redact.js";
|
|
8
9
|
import { getNamedSecrets } from "../logging/namedSecrets.js";
|
|
9
10
|
import { readMaxActiveStageProcesses, } from "./stageConcurrency.js";
|
|
@@ -11,6 +12,8 @@ import { getContainerLimits } from "./containerLimits.js";
|
|
|
11
12
|
import { buildStageEnvironment, isAmbientBlockedEnv, isForeverDeniedSecret, } from "./stageEnvironment.js";
|
|
12
13
|
import { ensureStageCacheDirs } from "./stageCacheEnv.js";
|
|
13
14
|
import { SF_STAGE_WORKER, STAGE_WORKER_EXIT, } from "./stageWorkerProtocol.js";
|
|
15
|
+
import { emailHostFor, emailWorkerEnvironment, stageEmail, validateStageEmailAccounts } from "../email/host.js";
|
|
16
|
+
import { EmailError } from "../email/port.js";
|
|
14
17
|
import { overlayStageBindingEnv, } from "./stageRoots.js";
|
|
15
18
|
function defaultStageCliEntry() {
|
|
16
19
|
const source = import.meta.url.split("?")[0]?.endsWith(".ts") === true;
|
|
@@ -177,6 +180,16 @@ export class StageProcessLauncher {
|
|
|
177
180
|
}
|
|
178
181
|
}
|
|
179
182
|
async launch(input) {
|
|
183
|
+
try {
|
|
184
|
+
if (input.stage?.email?.length)
|
|
185
|
+
validateStageEmailAccounts(emailHostFor(input.factoryCwd ?? input.operatorCatalog?.cwd ?? input.rootDir).accounts, input.stage);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (error instanceof EmailError)
|
|
189
|
+
return { type: "failed", reason: error.code };
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
input = { ...input, stage: input.stage ? structuredClone(input.stage) : undefined };
|
|
180
193
|
const acquired = await this.waitForCapacity(input.runId);
|
|
181
194
|
if (!acquired) {
|
|
182
195
|
return { type: "failed", reason: "cancelled" };
|
|
@@ -278,6 +291,7 @@ export class StageProcessLauncher {
|
|
|
278
291
|
}
|
|
279
292
|
}
|
|
280
293
|
spawnAndWait(input) {
|
|
294
|
+
const factoryCwd = input.factoryCwd ?? input.operatorCatalog?.cwd ?? input.rootDir;
|
|
281
295
|
const mode = input.mode ?? (input.resumeAnswer !== undefined ? "resume" : "run");
|
|
282
296
|
const args = [
|
|
283
297
|
"internal",
|
|
@@ -338,11 +352,18 @@ export class StageProcessLauncher {
|
|
|
338
352
|
: overlaid;
|
|
339
353
|
const childEnv = {
|
|
340
354
|
...withBrowser,
|
|
341
|
-
[PI_HOME_AUTH_PATH_ENV]:
|
|
355
|
+
[PI_HOME_AUTH_PATH_ENV]: stageflowAgentAuthPath(),
|
|
342
356
|
};
|
|
357
|
+
const cursorApiKey = readCursorApiKey(hostEnv);
|
|
358
|
+
if (isCursorModelRef(input.model ?? "") &&
|
|
359
|
+
cursorApiKey !== undefined &&
|
|
360
|
+
cursorApiKey !== "" &&
|
|
361
|
+
childEnv.CURSOR_API_KEY === undefined) {
|
|
362
|
+
childEnv.CURSOR_API_KEY = cursorApiKey;
|
|
363
|
+
}
|
|
343
364
|
const child = this.forkFn(this.cliEntry, args, {
|
|
344
365
|
cwd: input.rootDir,
|
|
345
|
-
env: { ...childEnv, [SF_STAGE_WORKER]: "1" },
|
|
366
|
+
env: emailWorkerEnvironment({ ...childEnv, [SF_STAGE_WORKER]: "1" }, emailHostFor(factoryCwd).accounts),
|
|
346
367
|
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
|
347
368
|
detached: true,
|
|
348
369
|
// Explicit: do not inherit Host execArgv; set heap from cgroup budget.
|
|
@@ -380,6 +401,37 @@ export class StageProcessLauncher {
|
|
|
380
401
|
resolve(result);
|
|
381
402
|
};
|
|
382
403
|
child.on("message", (message) => {
|
|
404
|
+
const request = message;
|
|
405
|
+
if (!settled && ["email.send", "email.reply", "email.search", "email.getMessage", "email.downloadAttachment"].includes(request?.type ?? "")) {
|
|
406
|
+
void (async () => {
|
|
407
|
+
if (typeof request.requestId !== "string" || request.requestId.length > 100)
|
|
408
|
+
return;
|
|
409
|
+
let response;
|
|
410
|
+
try {
|
|
411
|
+
const stage = input.stage;
|
|
412
|
+
if (!stage || settled)
|
|
413
|
+
throw new EmailError("EMAIL_UNAUTHORIZED");
|
|
414
|
+
const email = stageEmail(emailHostFor(factoryCwd).mailbox, stage, input.runId, input.workspaceDir ? { workspaceDir: input.workspaceDir, attempt: input.attempt ?? 1 } : undefined);
|
|
415
|
+
if (request.type === "email.send")
|
|
416
|
+
response = { receipt: await email.send(request.input) };
|
|
417
|
+
else if (request.type === "email.reply")
|
|
418
|
+
response = { receipt: await email.reply(request.input) };
|
|
419
|
+
else if (request.type === "email.search")
|
|
420
|
+
response = { result: await email.search(request.input) };
|
|
421
|
+
else if (request.type === "email.getMessage")
|
|
422
|
+
response = { result: await email.getMessage(request.input) };
|
|
423
|
+
else
|
|
424
|
+
response = { result: await email.downloadAttachment(request.input) };
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
const fault = error instanceof EmailError ? error : new EmailError("EMAIL_UNAUTHORIZED");
|
|
428
|
+
response = { error: { code: fault.code, retryable: fault.retryable, ...(fault.unsupportedFields ? { unsupportedFields: fault.unsupportedFields } : {}) } };
|
|
429
|
+
}
|
|
430
|
+
if (child.connected && !settled)
|
|
431
|
+
child.send({ type: "email.response", requestId: request.requestId, ...response });
|
|
432
|
+
})();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
383
435
|
if (!isStageWorkerResult(message))
|
|
384
436
|
return;
|
|
385
437
|
finish(resultFromWorkerMessage(message));
|
|
@@ -5,6 +5,8 @@ import { type RunStoreKind } from "../runstore/createStore.js";
|
|
|
5
5
|
import type { RunStore } from "../runstore/port.js";
|
|
6
6
|
import type { StoreFilesystemClassification } from "../runstore/storeFilesystem.js";
|
|
7
7
|
import { RunManager } from "../runtime/runManager.js";
|
|
8
|
+
import { emailHostFor } from "../email/host.js";
|
|
9
|
+
import { EmailTriggers } from "../email/triggers.js";
|
|
8
10
|
import { type RunChangeBus } from "../runtime/runChangeBus.js";
|
|
9
11
|
import { type McpHttpHandler } from "../mcp/server.js";
|
|
10
12
|
import { type HostConfig } from "../config/hostConfig.js";
|
|
@@ -25,6 +27,7 @@ export type StageflowHostOptions = {
|
|
|
25
27
|
hostConfig?: HostConfig;
|
|
26
28
|
/** Skip HostConfig load (tests that inject store/manager pieces only). */
|
|
27
29
|
skipHostConfig?: boolean;
|
|
30
|
+
emailTriggerQueue?: ConstructorParameters<typeof EmailTriggers>[0]["queue"];
|
|
28
31
|
};
|
|
29
32
|
export type StageflowHostBootstrap = {
|
|
30
33
|
a2a?: A2aHost;
|
|
@@ -51,6 +54,10 @@ export type StageflowHostBootstrap = {
|
|
|
51
54
|
stopGithubPollSource: () => void;
|
|
52
55
|
/** Stops the email-trigger IMAP/IDLE listeners started at boot. */
|
|
53
56
|
stopEmailSource: () => void;
|
|
57
|
+
email?: ReturnType<typeof emailHostFor> & {
|
|
58
|
+
triggers: EmailTriggers;
|
|
59
|
+
stop: () => Promise<void>;
|
|
60
|
+
};
|
|
54
61
|
/** Filesystem classification for `$STAGEFLOW_HOME` (Slot 8). */
|
|
55
62
|
storeFilesystem?: StoreFilesystemClassification;
|
|
56
63
|
/**
|