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.
Files changed (94) hide show
  1. package/dist/agent/claudeAdapter.js +6 -0
  2. package/dist/agent/piAdapter.js +9 -0
  3. package/dist/agent/port.d.ts +2 -0
  4. package/dist/channels/config.d.ts +11 -0
  5. package/dist/channels/config.js +95 -0
  6. package/dist/channels/configWriter.d.ts +40 -0
  7. package/dist/channels/configWriter.js +148 -0
  8. package/dist/channels/connectionEdits.d.ts +34 -0
  9. package/dist/channels/connectionEdits.js +53 -0
  10. package/dist/channels/errorInfo.d.ts +18 -0
  11. package/dist/channels/errorInfo.js +71 -0
  12. package/dist/channels/errors.d.ts +12 -0
  13. package/dist/channels/errors.js +13 -0
  14. package/dist/channels/health.d.ts +41 -0
  15. package/dist/channels/health.js +99 -0
  16. package/dist/channels/host.d.ts +55 -0
  17. package/dist/channels/host.js +185 -0
  18. package/dist/channels/http.d.ts +31 -0
  19. package/dist/channels/http.js +79 -0
  20. package/dist/channels/index.d.ts +10 -0
  21. package/dist/channels/index.js +9 -0
  22. package/dist/channels/ledger.d.ts +37 -0
  23. package/dist/channels/ledger.js +95 -0
  24. package/dist/channels/providers/agentmail.d.ts +26 -0
  25. package/dist/channels/providers/agentmail.js +81 -0
  26. package/dist/channels/providers/kit.d.ts +40 -0
  27. package/dist/channels/providers/kit.js +78 -0
  28. package/dist/channels/providers/photonImessage.d.ts +45 -0
  29. package/dist/channels/providers/photonImessage.js +152 -0
  30. package/dist/channels/providers/photonWhatsapp.d.ts +49 -0
  31. package/dist/channels/providers/photonWhatsapp.js +146 -0
  32. package/dist/channels/providers/slack.d.ts +25 -0
  33. package/dist/channels/providers/slack.js +101 -0
  34. package/dist/channels/providers/telegram.d.ts +21 -0
  35. package/dist/channels/providers/telegram.js +100 -0
  36. package/dist/channels/providers.d.ts +5 -0
  37. package/dist/channels/providers.js +22 -0
  38. package/dist/channels/registry.d.ts +54 -0
  39. package/dist/channels/registry.js +186 -0
  40. package/dist/channels/secretStore.d.ts +21 -0
  41. package/dist/channels/secretStore.js +75 -0
  42. package/dist/channels/secrets.d.ts +15 -0
  43. package/dist/channels/secrets.js +50 -0
  44. package/dist/channels/service.d.ts +54 -0
  45. package/dist/channels/service.js +307 -0
  46. package/dist/channels/stageChannels.d.ts +45 -0
  47. package/dist/channels/stageChannels.js +110 -0
  48. package/dist/channels/types.d.ts +142 -0
  49. package/dist/channels/types.js +1 -0
  50. package/dist/cli/doctorCommand.js +3 -1
  51. package/dist/config/draftPackage.d.ts +1 -0
  52. package/dist/config/draftPackage.js +4 -0
  53. package/dist/config/hostConfig.js +1 -0
  54. package/dist/config/loadPipeline.js +27 -3
  55. package/dist/config/loadStage.js +15 -0
  56. package/dist/config/mergePipelineIncludes.d.ts +3 -0
  57. package/dist/config/mergePipelineIncludes.js +27 -0
  58. package/dist/config/pipelineStageKeys.js +1 -0
  59. package/dist/config/validateCatalog.d.ts +4 -1
  60. package/dist/config/validateCatalog.js +23 -0
  61. package/dist/errors/codes.d.ts +1 -1
  62. package/dist/errors/codes.js +1 -0
  63. package/dist/mcp/catalogTools.js +1 -0
  64. package/dist/package-meta.d.ts +1 -1
  65. package/dist/package-meta.js +1 -1
  66. package/dist/preflight/channelChecks.d.ts +17 -0
  67. package/dist/preflight/channelChecks.js +106 -0
  68. package/dist/preflight/pipelinePreflight.d.ts +9 -2
  69. package/dist/preflight/pipelinePreflight.js +9 -1
  70. package/dist/runtime/pipelineRunner.js +3 -1
  71. package/dist/runtime/repositoryMaterialize.d.ts +1 -1
  72. package/dist/runtime/runManager.js +4 -1
  73. package/dist/runtime/stageAttemptBootstrap.js +16 -0
  74. package/dist/runtime/stageProcessLauncher.js +31 -1
  75. package/dist/server/bootstrap.d.ts +3 -0
  76. package/dist/server/bootstrap.js +4 -0
  77. package/dist/server/channelRoutes.d.ts +9 -0
  78. package/dist/server/channelRoutes.js +88 -0
  79. package/dist/server/http.js +11 -0
  80. package/dist/tools/readEmail.d.ts +3 -3
  81. package/dist/tools/sendMessage.d.ts +21 -0
  82. package/dist/tools/sendMessage.js +25 -0
  83. package/dist/types/pipeline.d.ts +4 -1
  84. package/dist/types/stage.d.ts +13 -0
  85. package/dist/ui/assets/index-BOhqnPNg.js +260 -0
  86. package/dist/ui/assets/{index-CIPbbBUi.css → index-Clv4sJpU.css} +1 -1
  87. package/dist/ui/index.html +2 -2
  88. package/examples/channels-smoke/README.md +12 -0
  89. package/examples/channels-smoke/channels-smoke.pipeline.yaml +11 -0
  90. package/examples/channels-smoke/channels-smoke.task.yaml +3 -0
  91. package/examples/channels-smoke/send-email.yaml +32 -0
  92. package/examples/channels-smoke/send-text.yaml +31 -0
  93. package/package.json +8 -3
  94. 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
- stages.push(stage);
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
- ...stageOutcome.value,
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)
@@ -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 };
@@ -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;
@@ -24,6 +24,7 @@ export const NETWORK_ERROR_CODES = [
24
24
  "tool_version_mismatch",
25
25
  "unknown_version",
26
26
  "secret_unavailable",
27
+ "channel_check_failed",
27
28
  "not_ready",
28
29
  "internal_error",
29
30
  "retry_in_progress",
@@ -63,6 +63,7 @@ const inlinePipelineSchema = z
63
63
  model: z.unknown().optional(),
64
64
  schemas: z.unknown().optional(),
65
65
  requires: z.unknown().optional(),
66
+ channels: z.unknown().optional(),
66
67
  })
67
68
  .strict();
68
69
  const startRunSchema = z
@@ -1,3 +1,3 @@
1
1
  export declare const PACKAGE_NAME = "stageflow";
2
- export declare const PACKAGE_VERSION = "0.35.0";
2
+ export declare const PACKAGE_VERSION = "0.36.0";
3
3
  export declare const BUILD_SHA: string;
@@ -1,3 +1,3 @@
1
1
  export const PACKAGE_NAME = "stageflow";
2
- export const PACKAGE_VERSION = "0.35.0";
2
+ export const PACKAGE_VERSION = "0.36.0";
3
3
  export const BUILD_SHA = process.env.STAGEFLOW_BUILD_SHA ?? "unknown";
@@ -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
- super(`Pipeline preflight failed: ${code}`);
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