stageflow 0.35.0 → 0.36.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/claudeAdapter.js +6 -0
- package/dist/agent/piAdapter.js +9 -0
- package/dist/agent/port.d.ts +2 -0
- package/dist/channels/config.d.ts +11 -0
- package/dist/channels/config.js +95 -0
- package/dist/channels/configWriter.d.ts +40 -0
- package/dist/channels/configWriter.js +148 -0
- package/dist/channels/connectionEdits.d.ts +34 -0
- package/dist/channels/connectionEdits.js +53 -0
- package/dist/channels/errorInfo.d.ts +18 -0
- package/dist/channels/errorInfo.js +71 -0
- package/dist/channels/errors.d.ts +12 -0
- package/dist/channels/errors.js +13 -0
- package/dist/channels/health.d.ts +41 -0
- package/dist/channels/health.js +99 -0
- package/dist/channels/host.d.ts +55 -0
- package/dist/channels/host.js +185 -0
- package/dist/channels/http.d.ts +31 -0
- package/dist/channels/http.js +79 -0
- package/dist/channels/index.d.ts +10 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/ledger.d.ts +37 -0
- package/dist/channels/ledger.js +95 -0
- package/dist/channels/providers/agentmail.d.ts +26 -0
- package/dist/channels/providers/agentmail.js +81 -0
- package/dist/channels/providers/kit.d.ts +40 -0
- package/dist/channels/providers/kit.js +78 -0
- package/dist/channels/providers/photonImessage.d.ts +45 -0
- package/dist/channels/providers/photonImessage.js +152 -0
- package/dist/channels/providers/photonWhatsapp.d.ts +49 -0
- package/dist/channels/providers/photonWhatsapp.js +146 -0
- package/dist/channels/providers/slack.d.ts +25 -0
- package/dist/channels/providers/slack.js +101 -0
- package/dist/channels/providers/telegram.d.ts +21 -0
- package/dist/channels/providers/telegram.js +100 -0
- package/dist/channels/providers.d.ts +5 -0
- package/dist/channels/providers.js +22 -0
- package/dist/channels/registry.d.ts +54 -0
- package/dist/channels/registry.js +186 -0
- package/dist/channels/secretStore.d.ts +21 -0
- package/dist/channels/secretStore.js +75 -0
- package/dist/channels/secrets.d.ts +15 -0
- package/dist/channels/secrets.js +50 -0
- package/dist/channels/service.d.ts +54 -0
- package/dist/channels/service.js +307 -0
- package/dist/channels/stageChannels.d.ts +45 -0
- package/dist/channels/stageChannels.js +110 -0
- package/dist/channels/types.d.ts +142 -0
- package/dist/channels/types.js +1 -0
- package/dist/cli/doctorCommand.js +3 -1
- package/dist/config/draftPackage.d.ts +1 -0
- package/dist/config/draftPackage.js +4 -0
- package/dist/config/hostConfig.js +1 -0
- package/dist/config/loadPipeline.js +27 -3
- package/dist/config/loadStage.js +15 -0
- package/dist/config/mergePipelineIncludes.d.ts +3 -0
- package/dist/config/mergePipelineIncludes.js +27 -0
- package/dist/config/pipelineStageKeys.js +1 -0
- package/dist/config/validateCatalog.d.ts +4 -1
- package/dist/config/validateCatalog.js +23 -0
- package/dist/errors/codes.d.ts +1 -1
- package/dist/errors/codes.js +1 -0
- package/dist/mcp/catalogTools.js +1 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/preflight/channelChecks.d.ts +17 -0
- package/dist/preflight/channelChecks.js +106 -0
- package/dist/preflight/pipelinePreflight.d.ts +9 -2
- package/dist/preflight/pipelinePreflight.js +9 -1
- package/dist/runtime/pipelineRunner.js +3 -1
- package/dist/runtime/repositoryMaterialize.d.ts +1 -1
- package/dist/runtime/runManager.js +4 -1
- package/dist/runtime/stageAttemptBootstrap.js +16 -0
- package/dist/runtime/stageProcessLauncher.js +31 -1
- package/dist/server/bootstrap.d.ts +3 -0
- package/dist/server/bootstrap.js +4 -0
- package/dist/server/channelRoutes.d.ts +9 -0
- package/dist/server/channelRoutes.js +88 -0
- package/dist/server/http.js +11 -0
- package/dist/tools/readEmail.d.ts +3 -3
- package/dist/tools/sendMessage.d.ts +21 -0
- package/dist/tools/sendMessage.js +25 -0
- package/dist/types/pipeline.d.ts +4 -1
- package/dist/types/stage.d.ts +13 -0
- package/dist/ui/assets/index-BOhqnPNg.js +260 -0
- package/dist/ui/assets/{index-CIPbbBUi.css → index-Clv4sJpU.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/examples/channels-smoke/README.md +12 -0
- package/examples/channels-smoke/channels-smoke.pipeline.yaml +11 -0
- package/examples/channels-smoke/channels-smoke.task.yaml +3 -0
- package/examples/channels-smoke/send-email.yaml +32 -0
- package/examples/channels-smoke/send-text.yaml +31 -0
- package/package.json +8 -3
- package/dist/ui/assets/index-BoFPkKFg.js +0 -256
|
@@ -77,6 +77,8 @@ function pipelineDocument(pipeline) {
|
|
|
77
77
|
doc.schemas = pipeline.schemas;
|
|
78
78
|
if (pipeline.requires !== undefined)
|
|
79
79
|
doc.requires = pipeline.requires;
|
|
80
|
+
if (pipeline.channels !== undefined)
|
|
81
|
+
doc.channels = pipeline.channels;
|
|
80
82
|
return doc;
|
|
81
83
|
}
|
|
82
84
|
function resolveDirectory(projectRoot, directory) {
|
|
@@ -164,6 +166,7 @@ export async function validateDraftPackage(draft, options = {}) {
|
|
|
164
166
|
...(draft.pipeline.requires !== undefined
|
|
165
167
|
? { requires: draft.pipeline.requires }
|
|
166
168
|
: {}),
|
|
169
|
+
...(draft.pipeline.channels !== undefined ? { channels: draft.pipeline.channels } : {}),
|
|
167
170
|
};
|
|
168
171
|
const result = await loadPipelineValidated(inline, {
|
|
169
172
|
cwd,
|
|
@@ -716,6 +719,7 @@ export async function loadDraftPackage(projectRoot, pipelineRelPath, options = {
|
|
|
716
719
|
...(raw.model !== undefined ? { model: raw.model } : {}),
|
|
717
720
|
...(raw.schemas !== undefined ? { schemas: raw.schemas } : {}),
|
|
718
721
|
...(raw.requires !== undefined ? { requires: raw.requires } : {}),
|
|
722
|
+
...(raw.channels !== undefined ? { channels: raw.channels } : {}),
|
|
719
723
|
},
|
|
720
724
|
...(stageArtifacts.length > 0 ? { stages: stageArtifacts } : {}),
|
|
721
725
|
};
|
|
@@ -18,6 +18,7 @@ export class HostConfigError extends Error {
|
|
|
18
18
|
export const KNOWN_STAGEFLOW_ENV_KEYS = new Set([
|
|
19
19
|
"STAGEFLOW_HOME",
|
|
20
20
|
"STAGEFLOW_CREDENTIAL_HOME",
|
|
21
|
+
"STAGEFLOW_CONNECTIONS_FILE",
|
|
21
22
|
"STAGEFLOW_BIND",
|
|
22
23
|
"STAGEFLOW_ALLOW_UNAUTHENTICATED_BIND",
|
|
23
24
|
"STAGEFLOW_ALLOWED_HOSTS",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MAX_STAGE_CHANNELS, channelLimitMessage, mergeChannelDefaults, stageChannelLimit } from "../channels/stageChannels.js";
|
|
1
2
|
import { access } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { compilePayloadSchema, expandPayloadSchemaRefs, isPayloadSchemaSubset, UnresolvedSchemaRefError, } from "../envelope/payloadSchema.js";
|
|
@@ -128,9 +129,25 @@ export async function loadPipelineFromObject(raw, options = {}) {
|
|
|
128
129
|
}
|
|
129
130
|
return outcome.value;
|
|
130
131
|
}
|
|
132
|
+
function applyPipelineChannels(stage, defaults, pipelineId) {
|
|
133
|
+
const channels = mergeChannelDefaults(defaults, stage.channels);
|
|
134
|
+
if (channels === undefined)
|
|
135
|
+
return loadSuccess(stage);
|
|
136
|
+
if (channels.length > Math.min(MAX_STAGE_CHANNELS, stageChannelLimit())) {
|
|
137
|
+
return loadFailure([
|
|
138
|
+
{
|
|
139
|
+
code: "stage.invalid_channels",
|
|
140
|
+
message: `Pipeline ${pipelineId}: stage "${stage.id}" ends up with ${channels.length} channels after merging pipeline defaults, but ${channelLimitMessage()}. Use \`channels: []\` on the stage to drop the pipeline default.`,
|
|
141
|
+
category: "stage",
|
|
142
|
+
stageId: stage.id,
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
return loadSuccess({ ...stage, channels });
|
|
147
|
+
}
|
|
131
148
|
async function buildLoadedPipelineFromMerge(merge, options) {
|
|
132
149
|
const { projectRoot, requireIo, pipelinePath } = options;
|
|
133
|
-
const { entries: rawEntries, pipelineId, agent: pipelineAgent, model: pipelineModel, schemas: pipelineSchemas, requires: pipelineRequires, warnings: mergeWarnings, } = merge;
|
|
150
|
+
const { entries: rawEntries, pipelineId, agent: pipelineAgent, model: pipelineModel, schemas: pipelineSchemas, requires: pipelineRequires, channels: pipelineChannels, warnings: mergeWarnings, } = merge;
|
|
134
151
|
const ctx = { pipelineId, path: pipelinePath };
|
|
135
152
|
const warnings = [...mergeWarnings];
|
|
136
153
|
const normalizeOutcome = normalizePipelineStageEntries(rawEntries, ctx);
|
|
@@ -199,7 +216,10 @@ async function buildLoadedPipelineFromMerge(merge, options) {
|
|
|
199
216
|
if (inlineRequires.value !== undefined) {
|
|
200
217
|
stage = { ...stage, requires: inlineRequires.value };
|
|
201
218
|
}
|
|
202
|
-
|
|
219
|
+
const inlineChannels = applyPipelineChannels(stage, pipelineChannels, pipelineId);
|
|
220
|
+
if (!inlineChannels.ok)
|
|
221
|
+
return loadFailure(inlineChannels.issues);
|
|
222
|
+
stages.push(inlineChannels.value);
|
|
203
223
|
stageSources[stageId] = { kind: "inline" };
|
|
204
224
|
continue;
|
|
205
225
|
}
|
|
@@ -259,8 +279,11 @@ async function buildLoadedPipelineFromMerge(merge, options) {
|
|
|
259
279
|
if (!fileRequires.ok)
|
|
260
280
|
return loadFailure(fileRequires.issues);
|
|
261
281
|
stageRequires = fileRequires.value;
|
|
282
|
+
const withChannels = applyPipelineChannels(stageOutcome.value, pipelineChannels, pipelineId);
|
|
283
|
+
if (!withChannels.ok)
|
|
284
|
+
return loadFailure(withChannels.issues);
|
|
262
285
|
const stage = {
|
|
263
|
-
...
|
|
286
|
+
...withChannels.value,
|
|
264
287
|
...(browser !== undefined ? { browser } : {}),
|
|
265
288
|
...(entry.mcp !== undefined ? { mcp: entry.mcp } : {}),
|
|
266
289
|
...(entry.secrets !== undefined ? { secrets: entry.secrets } : {}),
|
|
@@ -315,6 +338,7 @@ async function buildLoadedPipelineFromMerge(merge, options) {
|
|
|
315
338
|
...(pipelineModel !== undefined ? { model: pipelineModel } : {}),
|
|
316
339
|
...(pipelineSchemas !== undefined ? { schemas: pipelineSchemas } : {}),
|
|
317
340
|
...(pipelineRequires !== undefined ? { requires: pipelineRequires } : {}),
|
|
341
|
+
...(pipelineChannels !== undefined ? { channels: pipelineChannels } : {}),
|
|
318
342
|
};
|
|
319
343
|
const effectiveRequires = mergeToolRequires([pipelineRequires, ...loadedStages.map((s) => s.requires)], { pipelineId });
|
|
320
344
|
if (!effectiveRequires.ok)
|
package/dist/config/loadStage.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseChannels } from "../channels/stageChannels.js";
|
|
1
2
|
import { parseAgentField } from "../agent/agentBackend.js";
|
|
2
3
|
import { STAGEFLOW_MCP_SERVER_NAME } from "../agent/claudeTools.js";
|
|
3
4
|
import { STAGE_GATE_KINDS, } from "../types/stage.js";
|
|
@@ -362,6 +363,20 @@ function parseStageFields(raw, label, entryId, deferSchemaRefs, requireIo, compi
|
|
|
362
363
|
if (mcpOutcome.value !== undefined) {
|
|
363
364
|
stage.mcp = mcpOutcome.value;
|
|
364
365
|
}
|
|
366
|
+
if (raw.channels !== undefined) {
|
|
367
|
+
const channels = parseChannels(raw.channels);
|
|
368
|
+
if (!channels.ok) {
|
|
369
|
+
return loadFailure([
|
|
370
|
+
{
|
|
371
|
+
code: "stage.invalid_channels",
|
|
372
|
+
message: `Invalid stage ${label}: ${channels.message}`,
|
|
373
|
+
category: "stage",
|
|
374
|
+
stageId: entryId,
|
|
375
|
+
},
|
|
376
|
+
]);
|
|
377
|
+
}
|
|
378
|
+
stage.channels = channels.value;
|
|
379
|
+
}
|
|
365
380
|
const secretsOutcome = parseStageSecrets(raw.secrets, label, entryId);
|
|
366
381
|
if (!secretsOutcome.ok)
|
|
367
382
|
return secretsOutcome;
|
|
@@ -2,6 +2,7 @@ import type { PayloadSchemaMap } from "../envelope/payloadSchema.js";
|
|
|
2
2
|
import type { InlinePipelineDefinition } from "../types/pipeline.js";
|
|
3
3
|
import { type LoadIssue, type LoadOutcome } from "./loadOutcome.js";
|
|
4
4
|
import { type ToolRequirement } from "./toolRequires.js";
|
|
5
|
+
import type { StageChannel } from "../types/stage.js";
|
|
5
6
|
export type RawMergedEntry = {
|
|
6
7
|
raw: unknown;
|
|
7
8
|
declaringPath: string;
|
|
@@ -13,6 +14,7 @@ export declare function mergePipelineStages(rootPath: string): Promise<LoadOutco
|
|
|
13
14
|
model?: string;
|
|
14
15
|
schemas?: PayloadSchemaMap;
|
|
15
16
|
requires?: ToolRequirement[];
|
|
17
|
+
channels?: StageChannel[];
|
|
16
18
|
warnings: LoadIssue[];
|
|
17
19
|
}>>;
|
|
18
20
|
/** Synthetic `declaringPath` for stage entries with no backing file. */
|
|
@@ -24,5 +26,6 @@ export declare function mergeInlinePipelineStages(raw: InlinePipelineDefinition)
|
|
|
24
26
|
model?: string;
|
|
25
27
|
schemas?: PayloadSchemaMap;
|
|
26
28
|
requires?: ToolRequirement[];
|
|
29
|
+
channels?: StageChannel[];
|
|
27
30
|
warnings: LoadIssue[];
|
|
28
31
|
}>>;
|
|
@@ -6,6 +6,7 @@ import { parseModelField } from "./modelField.js";
|
|
|
6
6
|
import { readYamlObject } from "./readYamlObject.js";
|
|
7
7
|
import { yamlParsePosition } from "./yamlParsePosition.js";
|
|
8
8
|
import { parseToolRequires } from "./toolRequires.js";
|
|
9
|
+
import { parseChannels } from "../channels/stageChannels.js";
|
|
9
10
|
import { classifyYamlDocument, collectDocumentKeys, mixedDialectIssue, } from "./yamlDialect.js";
|
|
10
11
|
function isPlainObject(value) {
|
|
11
12
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -189,6 +190,10 @@ export async function mergePipelineStages(rootPath) {
|
|
|
189
190
|
if (!requiresOutcome.ok)
|
|
190
191
|
return requiresOutcome;
|
|
191
192
|
const requires = requiresOutcome.value;
|
|
193
|
+
const channelsOutcome = parsePipelineChannels(raw.channels, `pipeline ${absRoot}`, pipelineId);
|
|
194
|
+
if (!channelsOutcome.ok)
|
|
195
|
+
return channelsOutcome;
|
|
196
|
+
const channels = channelsOutcome.value;
|
|
192
197
|
const idLocations = new Map();
|
|
193
198
|
const entries = [];
|
|
194
199
|
const warnings = [];
|
|
@@ -211,6 +216,7 @@ export async function mergePipelineStages(rootPath) {
|
|
|
211
216
|
...(model !== undefined ? { model } : {}),
|
|
212
217
|
...(schemas !== undefined ? { schemas } : {}),
|
|
213
218
|
...(requires !== undefined ? { requires } : {}),
|
|
219
|
+
...(channels !== undefined ? { channels } : {}),
|
|
214
220
|
warnings,
|
|
215
221
|
});
|
|
216
222
|
}
|
|
@@ -273,6 +279,10 @@ export async function mergeInlinePipelineStages(raw) {
|
|
|
273
279
|
if (!requiresOutcome.ok)
|
|
274
280
|
return requiresOutcome;
|
|
275
281
|
const requires = requiresOutcome.value;
|
|
282
|
+
const channelsOutcome = parsePipelineChannels(raw.channels, `inline pipeline ${pipelineId}`, pipelineId);
|
|
283
|
+
if (!channelsOutcome.ok)
|
|
284
|
+
return channelsOutcome;
|
|
285
|
+
const channels = channelsOutcome.value;
|
|
276
286
|
const entries = [];
|
|
277
287
|
for (let index = 0; index < raw.stages.length; index++) {
|
|
278
288
|
const entry = raw.stages[index];
|
|
@@ -295,9 +305,26 @@ export async function mergeInlinePipelineStages(raw) {
|
|
|
295
305
|
...(model !== undefined ? { model } : {}),
|
|
296
306
|
...(schemas !== undefined ? { schemas } : {}),
|
|
297
307
|
...(requires !== undefined ? { requires } : {}),
|
|
308
|
+
...(channels !== undefined ? { channels } : {}),
|
|
298
309
|
warnings: [],
|
|
299
310
|
});
|
|
300
311
|
}
|
|
312
|
+
function parsePipelineChannels(raw, label, pipelineId) {
|
|
313
|
+
if (raw === undefined)
|
|
314
|
+
return loadSuccess(undefined);
|
|
315
|
+
const parsed = parseChannels(raw);
|
|
316
|
+
if (!parsed.ok) {
|
|
317
|
+
return loadFailure([
|
|
318
|
+
{
|
|
319
|
+
code: "pipeline.invalid_channels",
|
|
320
|
+
message: `Invalid ${label}: ${parsed.message}`,
|
|
321
|
+
category: "pipeline",
|
|
322
|
+
pipelineId,
|
|
323
|
+
},
|
|
324
|
+
]);
|
|
325
|
+
}
|
|
326
|
+
return loadSuccess(parsed.value);
|
|
327
|
+
}
|
|
301
328
|
function parsePipelineSchemas(raw, absPath, pipelineId) {
|
|
302
329
|
if (raw === undefined)
|
|
303
330
|
return loadSuccess(undefined);
|
|
@@ -29,6 +29,7 @@ export const WIRING_KEYS = new Set([
|
|
|
29
29
|
/** Stage body keys. Target: `io` / `verify`. IR/legacy: `payload_schema`, `pre_emit_checks`, `clone_input_schema`. */
|
|
30
30
|
export const BODY_KEYS = new Set([
|
|
31
31
|
"email",
|
|
32
|
+
"channels",
|
|
32
33
|
"system_prompt",
|
|
33
34
|
"model",
|
|
34
35
|
"payload_schema",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { InlinePipelineDefinition, LoadedPipeline } from "../types/pipeline.js";
|
|
2
2
|
import type { TriggerFile } from "../types/trigger.js";
|
|
3
3
|
import type { LoadIssue } from "./loadOutcome.js";
|
|
4
|
+
import type { AnyChannelProvider } from "../channels/types.js";
|
|
4
5
|
export type ValidationSeverity = "error" | "warning";
|
|
5
6
|
export type ValidationCategory = "pipeline" | "stage" | "catalog" | "task" | "trigger";
|
|
6
|
-
export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.stage_wrapper_skill_rejected" | "pipeline.invalid_completion" | "catalog.path_case_mismatch" | "pipeline.invalid_recovery" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "pipeline.invalid_agent" | "pipeline.invalid_model" | "pipeline.invalid_verify" | "pipeline.io_incompatible" | "pipeline.model_applies" | "pipeline.route_if_invalid" | "pipeline.route_all_gated" | "pipeline.invalid_requires" | "pipeline.requires_conflict" | "stage.invalid_shape" | "stage.invalid_model" | "stage.missing_model" | "stage.invalid_payload_schema" | "stage.invalid_clone_input_schema" | "stage.unresolved_schema_ref" | "stage.invalid_clone_actions" | "stage.invalid_gate_kinds" | "stage.invalid_pre_emit_checks" | "stage.invalid_timeout_ms" | "stage.invalid_skill" | "stage.invalid_mcp" | "stage.invalid_browser" | "stage.invalid_secrets" | "stage.unknown_secret" | "stage.denied_secret" | "stage.invalid_requires" | "stage.invalid_agent" | "stage.invalid_io" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "task.entry_input_unmet" | "task.binding_conflict" | "task.repository_ref_required" | "task.ref_without_repository" | "task.repository_invalid" | "trigger.invalid_shape" | "trigger.load_error" | "trigger.unknown_pipeline" | "trigger.unknown_task" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error" | "catalog.invalid_mcp" | "catalog.mcp_command_missing" | "catalog.mixed_yaml_dialect" | "catalog.legacy_yaml" | "catalog.stageflow_home_absolute_path";
|
|
7
|
+
export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.stage_wrapper_skill_rejected" | "pipeline.invalid_completion" | "catalog.path_case_mismatch" | "pipeline.invalid_recovery" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "pipeline.invalid_agent" | "pipeline.invalid_model" | "pipeline.invalid_verify" | "pipeline.io_incompatible" | "pipeline.model_applies" | "pipeline.route_if_invalid" | "pipeline.route_all_gated" | "pipeline.invalid_requires" | "pipeline.requires_conflict" | "pipeline.invalid_channels" | "stage.invalid_shape" | "stage.invalid_model" | "stage.missing_model" | "stage.invalid_payload_schema" | "stage.invalid_clone_input_schema" | "stage.unresolved_schema_ref" | "stage.invalid_clone_actions" | "stage.invalid_gate_kinds" | "stage.invalid_pre_emit_checks" | "stage.invalid_timeout_ms" | "stage.invalid_skill" | "stage.invalid_mcp" | "stage.invalid_channels" | "stage.channel_unavailable" | "stage.invalid_browser" | "stage.invalid_secrets" | "stage.unknown_secret" | "stage.denied_secret" | "stage.invalid_requires" | "stage.invalid_agent" | "stage.invalid_io" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "task.entry_input_unmet" | "task.binding_conflict" | "task.repository_ref_required" | "task.ref_without_repository" | "task.repository_invalid" | "trigger.invalid_shape" | "trigger.load_error" | "trigger.unknown_pipeline" | "trigger.unknown_task" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error" | "catalog.invalid_mcp" | "catalog.mcp_command_missing" | "catalog.mixed_yaml_dialect" | "catalog.legacy_yaml" | "catalog.stageflow_home_absolute_path";
|
|
7
8
|
export type ValidationFinding = {
|
|
8
9
|
severity: ValidationSeverity;
|
|
9
10
|
code: ValidationFindingCode | string;
|
|
@@ -50,6 +51,7 @@ export type ValidatePipelineOptions = {
|
|
|
50
51
|
projectRoot?: string;
|
|
51
52
|
validateStages?: boolean;
|
|
52
53
|
strict?: boolean;
|
|
54
|
+
channelProviders?: readonly AnyChannelProvider[];
|
|
53
55
|
};
|
|
54
56
|
export declare function validatePipeline(nameOrPath: string, options?: ValidatePipelineOptions): Promise<ValidationResult>;
|
|
55
57
|
export type LoadPipelineValidatedResult = {
|
|
@@ -64,6 +66,7 @@ export declare function loadPipelineValidated(nameOrPath: string | InlinePipelin
|
|
|
64
66
|
cwd?: string;
|
|
65
67
|
projectRoot?: string;
|
|
66
68
|
validateStages?: boolean;
|
|
69
|
+
channelProviders?: readonly AnyChannelProvider[];
|
|
67
70
|
}): Promise<LoadPipelineValidatedResult>;
|
|
68
71
|
export declare function collectPipelineIdsFromPaths(pipelinePaths: string[]): Promise<Map<string, string[]>>;
|
|
69
72
|
export declare function collectTaskIdsFromPaths(taskPaths: string[]): Promise<Set<string>>;
|
|
@@ -14,6 +14,9 @@ import { MCP_CATALOG_FILENAME, StageMcpError, assertMcpAllowlistKnown, loadMcpCa
|
|
|
14
14
|
import { loadSecretRegistry } from "../runtime/stageSecrets.js";
|
|
15
15
|
import { isForeverDeniedSecret } from "../runtime/stageEnvironment.js";
|
|
16
16
|
import { findMissingMcpCommands } from "../preflight/mcpCommands.js";
|
|
17
|
+
import { ChannelError } from "../channels/errors.js";
|
|
18
|
+
import { ChannelRegistry } from "../channels/registry.js";
|
|
19
|
+
import { validateStageChannels } from "../channels/stageChannels.js";
|
|
17
20
|
export function relPath(cwd, absPath) {
|
|
18
21
|
return path.relative(cwd, absPath).replace(/\\/g, "/");
|
|
19
22
|
}
|
|
@@ -256,6 +259,23 @@ function collectMcpAllowlistNames(loaded) {
|
|
|
256
259
|
}
|
|
257
260
|
return [...names];
|
|
258
261
|
}
|
|
262
|
+
function findingsForStageChannels(cwd, pipelinePath, loaded, projectRoot, providers) {
|
|
263
|
+
const users = loaded.stages.filter((stage) => (stage.channels?.length ?? 0) > 0);
|
|
264
|
+
if (users.length === 0)
|
|
265
|
+
return [];
|
|
266
|
+
let registry;
|
|
267
|
+
try {
|
|
268
|
+
registry = new ChannelRegistry({ cwd: projectRoot, providers });
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
const code = error instanceof ChannelError ? error.code : "CHANNEL_INVALID_INPUT";
|
|
272
|
+
const field = error instanceof ChannelError && error.detail?.connectionId
|
|
273
|
+
? ` (connection "${error.detail.connectionId}"${error.detail.field ? `, field "${error.detail.field}"` : ""})`
|
|
274
|
+
: "";
|
|
275
|
+
return users.map((stage) => findingStageError(cwd, pipelinePath, `connections.yaml could not be loaded: ${code}${field}`, "stage.channel_unavailable", stage.id));
|
|
276
|
+
}
|
|
277
|
+
return users.flatMap((stage) => validateStageChannels(registry, stage).map((issue) => findingStageError(cwd, pipelinePath, issue.message, "stage.channel_unavailable", issue.stageId)));
|
|
278
|
+
}
|
|
259
279
|
function findingsForStageSecrets(cwd, pipelinePath, loaded) {
|
|
260
280
|
const registry = loadSecretRegistry(process.env);
|
|
261
281
|
const findings = [];
|
|
@@ -438,6 +458,7 @@ async function runPipelineValidation(nameOrPath, options) {
|
|
|
438
458
|
}
|
|
439
459
|
findings.push(...(await findingsForStageMcpCatalog(cwd, outcome.value)));
|
|
440
460
|
findings.push(...findingsForStageSecrets(cwd, pipelinePath, outcome.value));
|
|
461
|
+
findings.push(...findingsForStageChannels(cwd, pipelinePath, outcome.value, projectRoot, options.channelProviders));
|
|
441
462
|
findings.push(...findingsForStageflowHomeAbsolutePaths(cwd, outcome.value));
|
|
442
463
|
if (validateStages && outcome.value.stageSources) {
|
|
443
464
|
for (const source of Object.values(outcome.value.stageSources)) {
|
|
@@ -461,6 +482,7 @@ export async function validatePipeline(nameOrPath, options = {}) {
|
|
|
461
482
|
cwd,
|
|
462
483
|
projectRoot,
|
|
463
484
|
validateStages,
|
|
485
|
+
channelProviders: options.channelProviders,
|
|
464
486
|
});
|
|
465
487
|
return buildValidationResult("pipeline", core.findings, strict);
|
|
466
488
|
}
|
|
@@ -472,6 +494,7 @@ export async function loadPipelineValidated(nameOrPath, options = {}) {
|
|
|
472
494
|
cwd,
|
|
473
495
|
projectRoot,
|
|
474
496
|
validateStages,
|
|
497
|
+
channelProviders: options.channelProviders,
|
|
475
498
|
});
|
|
476
499
|
if (core.ok) {
|
|
477
500
|
return { ok: true, loaded: core.loaded, findings: core.findings };
|
package/dist/errors/codes.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const BUSY_CHECKOUT: "busy_checkout";
|
|
|
4
4
|
/** Per-caller concurrency quota: queued when under global capacity; reject when queue full. */
|
|
5
5
|
export declare const BUSY_CALLER_QUOTA: "busy_caller_quota";
|
|
6
6
|
export declare const ABORTED: "aborted";
|
|
7
|
-
export declare const NETWORK_ERROR_CODES: readonly ["busy_capacity", "busy_checkout", "busy_caller_quota", "aborted", "unknown_project_root", "absolute_path_not_allowed", "path_outside_project_root", "catalog_root_unreadable", "ensure_project_not_allowed", "config_invalid", "config_unknown_key", "provider_not_configured", "a2a_configuration_error", "untrusted_config_origin", "command_not_on_path", "missing_tool", "tool_version_mismatch", "unknown_version", "secret_unavailable", "not_ready", "internal_error", "retry_in_progress", "hitl_not_retriable", "run_not_retryable", "stage_not_failed", "manual_recovery_required", "run_not_found", "stage_not_found", "artifact_not_found", "artifact_path_denied", "envelope_not_found", "insufficient_disk", "disk_check_failed", "shutting_down", "start.token_rejected", "inline_pipeline_too_large", "skills_invalid_name", "skills_invalid_path", "skills_missing_skill_md", "skills_payload_too_large", "pinned_sha_unavailable", "store_integrity_failed", "store_unsupported_filesystem", "tmpdir_unusable", "backup_insufficient_disk"];
|
|
7
|
+
export declare const NETWORK_ERROR_CODES: readonly ["busy_capacity", "busy_checkout", "busy_caller_quota", "aborted", "unknown_project_root", "absolute_path_not_allowed", "path_outside_project_root", "catalog_root_unreadable", "ensure_project_not_allowed", "config_invalid", "config_unknown_key", "provider_not_configured", "a2a_configuration_error", "untrusted_config_origin", "command_not_on_path", "missing_tool", "tool_version_mismatch", "unknown_version", "secret_unavailable", "channel_check_failed", "not_ready", "internal_error", "retry_in_progress", "hitl_not_retriable", "run_not_retryable", "stage_not_failed", "manual_recovery_required", "run_not_found", "stage_not_found", "artifact_not_found", "artifact_path_denied", "envelope_not_found", "insufficient_disk", "disk_check_failed", "shutting_down", "start.token_rejected", "inline_pipeline_too_large", "skills_invalid_name", "skills_invalid_path", "skills_missing_skill_md", "skills_payload_too_large", "pinned_sha_unavailable", "store_integrity_failed", "store_unsupported_filesystem", "tmpdir_unusable", "backup_insufficient_disk"];
|
|
8
8
|
export type NetworkErrorCode = (typeof NETWORK_ERROR_CODES)[number] | string;
|
|
9
9
|
export type NetworkErrorBody = {
|
|
10
10
|
code: NetworkErrorCode;
|
package/dist/errors/codes.js
CHANGED
package/dist/mcp/catalogTools.js
CHANGED
package/dist/package-meta.d.ts
CHANGED
package/dist/package-meta.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AnyChannelProvider } from "../channels/types.js";
|
|
2
|
+
import type { LoadedPipeline } from "../types/pipeline.js";
|
|
3
|
+
import type { PreflightItem } from "./pipelinePreflight.js";
|
|
4
|
+
export type ChannelCheckOptions = {
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
hostEnv?: NodeJS.ProcessEnv;
|
|
7
|
+
providers?: readonly AnyChannelProvider[];
|
|
8
|
+
/** Waits between attempts. Each entry allows one more retry. */
|
|
9
|
+
retryDelaysMs?: readonly number[];
|
|
10
|
+
attemptTimeoutMs?: number;
|
|
11
|
+
};
|
|
12
|
+
export declare const DEFAULT_CHANNEL_RETRY_DELAYS_MS: readonly [500, 1500];
|
|
13
|
+
export declare const DEFAULT_CHANNEL_ATTEMPT_TIMEOUT_MS = 8000;
|
|
14
|
+
/** Live check of every connection a pipeline's stages declare. Never sends a message. */
|
|
15
|
+
export declare function checkStageChannels(loaded: LoadedPipeline, options: ChannelCheckOptions): Promise<PreflightItem[]>;
|
|
16
|
+
/** One line naming each failed connection and why. Codes and plain reasons only, never a secret. */
|
|
17
|
+
export declare function channelFailureSummary(checks: readonly PreflightItem[]): string | undefined;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ChannelError } from "../channels/errors.js";
|
|
2
|
+
import { ChannelRegistry } from "../channels/registry.js";
|
|
3
|
+
import { channelErrorReason } from "../channels/errorInfo.js";
|
|
4
|
+
import { channelTarget } from "../channels/stageChannels.js";
|
|
5
|
+
export const DEFAULT_CHANNEL_RETRY_DELAYS_MS = [500, 1500];
|
|
6
|
+
export const DEFAULT_CHANNEL_ATTEMPT_TIMEOUT_MS = 8000;
|
|
7
|
+
function failed(code, retryable) {
|
|
8
|
+
return { state: "failed", checkedAt: new Date().toISOString(), error: { code, retryable } };
|
|
9
|
+
}
|
|
10
|
+
async function attempt(run, timeoutMs) {
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
let timer;
|
|
13
|
+
const timeout = new Promise(resolve => {
|
|
14
|
+
timer = setTimeout(() => { controller.abort(); resolve(failed("CHANNEL_TIMEOUT", true)); }, timeoutMs);
|
|
15
|
+
});
|
|
16
|
+
try {
|
|
17
|
+
return await Promise.race([run(controller.signal), timeout]);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return error instanceof ChannelError
|
|
21
|
+
? failed(error.code, error.retryable)
|
|
22
|
+
: failed("CHANNEL_CONNECTION_FAILED", false);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Retries only failures the provider marked transient. Authentication, configuration,
|
|
30
|
+
* permission and target problems return at once so a bad setup is never slowed or hidden.
|
|
31
|
+
*/
|
|
32
|
+
async function withRetry(run, options) {
|
|
33
|
+
let result = await attempt(run, options.timeoutMs);
|
|
34
|
+
for (const delay of options.delays) {
|
|
35
|
+
if (result.state === "ok" || result.error?.retryable !== true)
|
|
36
|
+
break;
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
38
|
+
result = await attempt(run, options.timeoutMs);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function item(connection, health) {
|
|
43
|
+
if (health.state === "ok")
|
|
44
|
+
return { kind: "channel", status: "ok", connection };
|
|
45
|
+
const code = health.error?.code ?? "CHANNEL_CONNECTION_FAILED";
|
|
46
|
+
return {
|
|
47
|
+
kind: "channel", status: "channel_check_failed", connection, channelCode: code,
|
|
48
|
+
retryable: health.error?.retryable ?? false, message: `connection "${connection}": ${code}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Live check of every connection a pipeline's stages declare. Never sends a message. */
|
|
52
|
+
export async function checkStageChannels(loaded, options) {
|
|
53
|
+
const targetsByConnection = new Map();
|
|
54
|
+
for (const stage of loaded.stages) {
|
|
55
|
+
for (const channel of stage.channels ?? []) {
|
|
56
|
+
const targets = targetsByConnection.get(channel.connection) ?? new Map();
|
|
57
|
+
targets.set(JSON.stringify(channelTarget(channel)), channelTarget(channel));
|
|
58
|
+
targetsByConnection.set(channel.connection, targets);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (targetsByConnection.size === 0)
|
|
62
|
+
return [];
|
|
63
|
+
let registry;
|
|
64
|
+
try {
|
|
65
|
+
registry = new ChannelRegistry({ cwd: options.projectRoot, env: options.hostEnv, providers: options.providers });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const code = error instanceof ChannelError ? error.code : "CHANNEL_INVALID_INPUT";
|
|
69
|
+
return [...targetsByConnection.keys()].map(connection => item(connection, failed(code, false)));
|
|
70
|
+
}
|
|
71
|
+
const retry = {
|
|
72
|
+
delays: options.retryDelaysMs ?? DEFAULT_CHANNEL_RETRY_DELAYS_MS,
|
|
73
|
+
timeoutMs: options.attemptTimeoutMs ?? DEFAULT_CHANNEL_ATTEMPT_TIMEOUT_MS,
|
|
74
|
+
};
|
|
75
|
+
return Promise.all([...targetsByConnection].map(async ([id, targets]) => {
|
|
76
|
+
let resolved;
|
|
77
|
+
let provider;
|
|
78
|
+
try {
|
|
79
|
+
provider = registry.providerFor(id);
|
|
80
|
+
resolved = registry.resolve(id);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return item(id, error instanceof ChannelError ? failed(error.code, false) : failed("CHANNEL_CONNECTION_FAILED", false));
|
|
84
|
+
}
|
|
85
|
+
const health = await withRetry(signal => provider.check(resolved, signal), retry);
|
|
86
|
+
if (health.state !== "ok" || !provider.checkTargets)
|
|
87
|
+
return item(id, health);
|
|
88
|
+
const validated = [...targets.values()].map(target => provider.validateTarget(target));
|
|
89
|
+
if (validated.some(result => !result.ok))
|
|
90
|
+
return item(id, failed("CHANNEL_TARGET_INVALID", false));
|
|
91
|
+
const values = validated.flatMap(result => (result.ok ? [result.value] : []));
|
|
92
|
+
return item(id, await withRetry(signal => provider.checkTargets(resolved, values, signal), retry));
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
/** One line naming each failed connection and why. Codes and plain reasons only, never a secret. */
|
|
96
|
+
export function channelFailureSummary(checks) {
|
|
97
|
+
const failed = checks.filter(check => check.kind === "channel" && check.status === "channel_check_failed");
|
|
98
|
+
if (failed.length === 0)
|
|
99
|
+
return undefined;
|
|
100
|
+
return failed
|
|
101
|
+
.map(check => {
|
|
102
|
+
const code = check.channelCode ?? "CHANNEL_CONNECTION_FAILED";
|
|
103
|
+
return `${check.connection}: ${channelErrorReason(code)} (${code})`;
|
|
104
|
+
})
|
|
105
|
+
.join("; ");
|
|
106
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { LoadedPipeline } from "../types/pipeline.js";
|
|
2
2
|
import { type ToolRequirement } from "../config/toolRequires.js";
|
|
3
|
+
import { type ChannelCheckOptions } from "./channelChecks.js";
|
|
3
4
|
import { type CheckToolchainOptions, type ToolchainCheckResult } from "./toolchain.js";
|
|
4
|
-
export type PreflightCheckKind = "toolchain" | "secret" | "mcp";
|
|
5
|
+
export type PreflightCheckKind = "toolchain" | "secret" | "mcp" | "channel";
|
|
5
6
|
export type PreflightItem = {
|
|
6
7
|
kind: PreflightCheckKind;
|
|
7
|
-
status: "ok" | "missing_tool" | "tool_version_mismatch" | "unknown_version" | "secret_unavailable" | "stage.unknown_secret" | "stage.denied_secret" | "unresolved_var" | "missing_catalog" | "unknown_server" | "invalid_config" | "untrusted_config_origin";
|
|
8
|
+
status: "ok" | "missing_tool" | "tool_version_mismatch" | "unknown_version" | "secret_unavailable" | "stage.unknown_secret" | "stage.denied_secret" | "unresolved_var" | "missing_catalog" | "unknown_server" | "invalid_config" | "untrusted_config_origin" | "channel_check_failed";
|
|
8
9
|
tool?: string;
|
|
9
10
|
required?: string;
|
|
10
11
|
found?: string;
|
|
@@ -12,6 +13,11 @@ export type PreflightItem = {
|
|
|
12
13
|
secret?: string;
|
|
13
14
|
stageId?: string;
|
|
14
15
|
server?: string;
|
|
16
|
+
/** Connection id for `kind: "channel"`. */
|
|
17
|
+
connection?: string;
|
|
18
|
+
/** Provider-neutral channel error code, such as CHANNEL_AUTH_FAILED. */
|
|
19
|
+
channelCode?: string;
|
|
20
|
+
retryable?: boolean;
|
|
15
21
|
message?: string;
|
|
16
22
|
};
|
|
17
23
|
export type PipelinePreflightResult = {
|
|
@@ -33,6 +39,7 @@ export declare function runPipelinePreflight(loaded: LoadedPipeline, options: {
|
|
|
33
39
|
strict?: boolean;
|
|
34
40
|
/** start_run: unknown_version passes; missing/mismatch fail. */
|
|
35
41
|
forStart?: boolean;
|
|
42
|
+
channels?: Pick<ChannelCheckOptions, "providers" | "retryDelaysMs" | "attemptTimeoutMs">;
|
|
36
43
|
}): Promise<PipelinePreflightResult>;
|
|
37
44
|
export declare function preflightFailureCode(result: PipelinePreflightResult, options?: {
|
|
38
45
|
strict?: boolean;
|
|
@@ -2,6 +2,7 @@ import { collectEffectiveRequires, } from "../config/toolRequires.js";
|
|
|
2
2
|
import { resolveStageMcpServers, StageMcpError, } from "../config/resolveStageMcpServers.js";
|
|
3
3
|
import { assertSecretPresent, loadSecretRegistry, SecretUnavailableError, } from "../runtime/stageSecrets.js";
|
|
4
4
|
import { isForeverDeniedSecret } from "../runtime/stageEnvironment.js";
|
|
5
|
+
import { checkStageChannels } from "./channelChecks.js";
|
|
5
6
|
import { checkToolchainRequirements, curatedStageEnv, } from "./toolchain.js";
|
|
6
7
|
function toolchainItems(checks) {
|
|
7
8
|
return checks.map((c) => ({
|
|
@@ -165,14 +166,21 @@ export async function runPipelinePreflight(loaded, options) {
|
|
|
165
166
|
projectRoot: options.projectRoot,
|
|
166
167
|
hostEnv,
|
|
167
168
|
});
|
|
169
|
+
const channelChecks = await checkStageChannels(loaded, {
|
|
170
|
+
projectRoot: options.projectRoot,
|
|
171
|
+
hostEnv,
|
|
172
|
+
...options.channels,
|
|
173
|
+
});
|
|
168
174
|
const checks = [
|
|
169
175
|
...toolchainItems(toolchain.checks),
|
|
170
176
|
...secretChecks,
|
|
171
177
|
...mcpChecks,
|
|
178
|
+
...channelChecks,
|
|
172
179
|
];
|
|
173
180
|
const ok = toolchain.ok &&
|
|
174
181
|
!secretChecks.some((c) => c.status !== "ok") &&
|
|
175
|
-
!mcpChecks.some((c) => c.status !== "ok")
|
|
182
|
+
!mcpChecks.some((c) => c.status !== "ok") &&
|
|
183
|
+
!channelChecks.some((c) => c.status !== "ok");
|
|
176
184
|
return { ok, checks, requirements, toolchain };
|
|
177
185
|
}
|
|
178
186
|
export function preflightFailureCode(result, options = {}) {
|
|
@@ -13,6 +13,7 @@ import { checkTaskEntryInput } from "./taskInput.js";
|
|
|
13
13
|
import { pipelinePersistenceForStart } from "./startPayload.js";
|
|
14
14
|
import { materializeRunSkills, } from "./runSkills.js";
|
|
15
15
|
import { preflightFailureCode, runPipelinePreflight, } from "../preflight/pipelinePreflight.js";
|
|
16
|
+
import { channelFailureSummary } from "../preflight/channelChecks.js";
|
|
16
17
|
import { networkError } from "../errors/codes.js";
|
|
17
18
|
import { newRunId } from "../runstore/paths.js";
|
|
18
19
|
import { bindingFromFields, buildInitialRunManifest, collectDeclaredSecretValues, finaliseStoredRunManifest, } from "../runstore/runManifest.js";
|
|
@@ -24,7 +25,8 @@ export class PipelinePreflightError extends Error {
|
|
|
24
25
|
code;
|
|
25
26
|
preflight;
|
|
26
27
|
constructor(code, preflight) {
|
|
27
|
-
|
|
28
|
+
const detail = code === "channel_check_failed" ? channelFailureSummary(preflight.checks) : undefined;
|
|
29
|
+
super(`Pipeline preflight failed: ${code}${detail ? `: ${detail}` : ""}`);
|
|
28
30
|
this.name = "PipelinePreflightError";
|
|
29
31
|
this.code = code;
|
|
30
32
|
this.preflight = preflight;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RunMeta, RunStore } from "../runstore/port.js";
|
|
2
2
|
import type { TaskFile } from "../types/task.js";
|
|
3
3
|
import type { WorkspaceBinding } from "./workspaceBinding.js";
|
|
4
|
-
export type StartFailureCode = "busy_capacity" | "busy_checkout" | "busy_caller_quota" | "repository_auth_failed" | "repository_not_found" | "ref_not_found" | "repository_fetch_failed" | "worktree_create_failed" | "git_missing" | "insufficient_disk" | "disk_check_failed" | "pinned_sha_unavailable" | "invalid_run_branch" | "task.binding_conflict" | "task.repository_ref_required" | "task.ref_without_repository" | "task.repository_invalid" | "task.invalid_shape" | "task.load_error" | "trigger.task_override_not_allowed" | "trigger.task_required" | "start.token_rejected" | "inline_pipeline_too_large" | "skills_invalid_name" | "skills_invalid_path" | "skills_missing_skill_md" | "skills_payload_too_large" | "missing_tool" | "tool_version_mismatch" | "unknown_version" | "secret_unavailable" | "unresolved_var" | "shutting_down" | "autostart_disabled" | "ensure_project_not_allowed" | "claude_as_root";
|
|
4
|
+
export type StartFailureCode = "busy_capacity" | "busy_checkout" | "busy_caller_quota" | "repository_auth_failed" | "repository_not_found" | "ref_not_found" | "repository_fetch_failed" | "worktree_create_failed" | "git_missing" | "insufficient_disk" | "disk_check_failed" | "pinned_sha_unavailable" | "invalid_run_branch" | "task.binding_conflict" | "task.repository_ref_required" | "task.ref_without_repository" | "task.repository_invalid" | "task.invalid_shape" | "task.load_error" | "trigger.task_override_not_allowed" | "trigger.task_required" | "start.token_rejected" | "inline_pipeline_too_large" | "skills_invalid_name" | "skills_invalid_path" | "skills_missing_skill_md" | "skills_payload_too_large" | "missing_tool" | "tool_version_mismatch" | "unknown_version" | "secret_unavailable" | "channel_check_failed" | "unresolved_var" | "shutting_down" | "autostart_disabled" | "ensure_project_not_allowed" | "claude_as_root";
|
|
5
5
|
export type MaterializedBinding = {
|
|
6
6
|
runId: string;
|
|
7
7
|
checkoutRoot?: string;
|
|
@@ -41,6 +41,7 @@ import { getContainerLimits } from "./containerLimits.js";
|
|
|
41
41
|
import { stageflowCacheRoot } from "./stageCacheEnv.js";
|
|
42
42
|
import { assertClaudeNotRoot, ClaudeRootError } from "../preflight/claudeRoot.js";
|
|
43
43
|
import { preflightFailureCode, runPipelinePreflight, } from "../preflight/pipelinePreflight.js";
|
|
44
|
+
import { channelFailureSummary } from "../preflight/channelChecks.js";
|
|
44
45
|
import { asAgentBackendId } from "../agent/agentBackend.js";
|
|
45
46
|
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, readMaxConcurrentFromGlobal, writeMaxConcurrentToGlobal, } from "./settingsFile.js";
|
|
46
47
|
import { StageHitlController, waitKey, } from "./stageHitl.js";
|
|
@@ -2331,7 +2332,9 @@ export class RunManager {
|
|
|
2331
2332
|
"missing_tool";
|
|
2332
2333
|
return {
|
|
2333
2334
|
ok: false,
|
|
2334
|
-
reason: `Pipeline preflight failed: ${code}
|
|
2335
|
+
reason: `Pipeline preflight failed: ${code}${code === "channel_check_failed" && channelFailureSummary(preflight.checks)
|
|
2336
|
+
? `: ${channelFailureSummary(preflight.checks)}`
|
|
2337
|
+
: ""}`,
|
|
2335
2338
|
status: 400,
|
|
2336
2339
|
code,
|
|
2337
2340
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { emailHostFor, stageEmail, workerStageEmail, validateStageEmailAccounts } from "../email/host.js";
|
|
2
|
+
import { ChannelError } from "../channels/errors.js";
|
|
3
|
+
import { channelHostFor, stageChannels, validateStageChannelConnections, workerStageChannels } from "../channels/host.js";
|
|
2
4
|
import { EmailError } from "../email/port.js";
|
|
3
5
|
import { bindingKindForOrigin, catalogOrSeededOrigin, decideWorkspaceConfigTrust, } from "../config/configOrigin.js";
|
|
4
6
|
import { loginCheckPromptBlock, readStageLoginCheck, } from "../browser/loginCheck.js";
|
|
@@ -450,6 +452,16 @@ export async function openStageAttempt(input) {
|
|
|
450
452
|
throw error;
|
|
451
453
|
}
|
|
452
454
|
}
|
|
455
|
+
if (process.env.SF_STAGE_WORKER !== "1" && input.stage.channels?.length) {
|
|
456
|
+
try {
|
|
457
|
+
validateStageChannelConnections(channelHostFor(input.factoryCwd ?? process.cwd()), input.stage);
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
if (error instanceof ChannelError)
|
|
461
|
+
return { ok: false, reason: error.code };
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
453
465
|
const opened = await openStageWithOperatorCatalog(input.agent, {
|
|
454
466
|
roots,
|
|
455
467
|
stage: input.stage,
|
|
@@ -465,6 +477,10 @@ export async function openStageAttempt(input) {
|
|
|
465
477
|
resumeToken,
|
|
466
478
|
email: input.stage.email?.length ? (process.env.SF_STAGE_WORKER === "1" ? workerStageEmail()
|
|
467
479
|
: stageEmail(emailHostFor(input.factoryCwd ?? process.cwd()).mailbox, input.stage, input.runId, { workspaceDir: input.workspaceDir, attempt })) : undefined,
|
|
480
|
+
...(input.stage.channels?.length ? {
|
|
481
|
+
channels: process.env.SF_STAGE_WORKER === "1" ? workerStageChannels()
|
|
482
|
+
: stageChannels(channelHostFor(input.factoryCwd ?? process.cwd()), input.stage, input.runId),
|
|
483
|
+
} : {}),
|
|
468
484
|
...(input.sessionMode !== undefined ? { sessionMode: input.sessionMode } : {}),
|
|
469
485
|
onActivity: (event) => {
|
|
470
486
|
input.onActivity?.(input.stage.browser !== undefined
|