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
@@ -13,6 +13,8 @@ import { buildStageEnvironment, isAmbientBlockedEnv, isForeverDeniedSecret, } fr
13
13
  import { ensureStageCacheDirs } from "./stageCacheEnv.js";
14
14
  import { SF_STAGE_WORKER, STAGE_WORKER_EXIT, } from "./stageWorkerProtocol.js";
15
15
  import { emailHostFor, emailWorkerEnvironment, stageEmail, validateStageEmailAccounts } from "../email/host.js";
16
+ import { ChannelError } from "../channels/errors.js";
17
+ import { channelFault, channelHostFor, channelsWorkerEnvironment, stageChannels, validateStageChannelConnections } from "../channels/host.js";
16
18
  import { EmailError } from "../email/port.js";
17
19
  import { overlayStageBindingEnv, } from "./stageRoots.js";
18
20
  function defaultStageCliEntry() {
@@ -189,6 +191,15 @@ export class StageProcessLauncher {
189
191
  return { type: "failed", reason: error.code };
190
192
  throw error;
191
193
  }
194
+ try {
195
+ if (input.stage?.channels?.length)
196
+ validateStageChannelConnections(channelHostFor(input.factoryCwd ?? input.operatorCatalog?.cwd ?? input.rootDir), input.stage);
197
+ }
198
+ catch (error) {
199
+ if (error instanceof ChannelError)
200
+ return { type: "failed", reason: error.code };
201
+ throw error;
202
+ }
192
203
  input = { ...input, stage: input.stage ? structuredClone(input.stage) : undefined };
193
204
  const acquired = await this.waitForCapacity(input.runId);
194
205
  if (!acquired) {
@@ -363,7 +374,7 @@ export class StageProcessLauncher {
363
374
  }
364
375
  const child = this.forkFn(this.cliEntry, args, {
365
376
  cwd: input.rootDir,
366
- env: emailWorkerEnvironment({ ...childEnv, [SF_STAGE_WORKER]: "1" }, emailHostFor(factoryCwd).accounts),
377
+ env: channelsWorkerEnvironment(emailWorkerEnvironment({ ...childEnv, [SF_STAGE_WORKER]: "1" }, emailHostFor(factoryCwd).accounts), factoryCwd),
367
378
  stdio: ["pipe", "pipe", "pipe", "ipc"],
368
379
  detached: true,
369
380
  // Explicit: do not inherit Host execArgv; set heap from cgroup budget.
@@ -432,6 +443,25 @@ export class StageProcessLauncher {
432
443
  })();
433
444
  return;
434
445
  }
446
+ if (!settled && request?.type === "channel.send") {
447
+ void (async () => {
448
+ if (typeof request.requestId !== "string" || request.requestId.length > 100)
449
+ return;
450
+ let response;
451
+ try {
452
+ const stage = input.stage;
453
+ if (!stage || settled)
454
+ throw new ChannelError("CHANNEL_UNAUTHORIZED");
455
+ response = { receipt: await stageChannels(channelHostFor(factoryCwd), stage, input.runId).send(request.input) };
456
+ }
457
+ catch (error) {
458
+ response = { error: channelFault(error) };
459
+ }
460
+ if (child.connected && !settled)
461
+ child.send({ type: "channel.response", requestId: request.requestId, ...response });
462
+ })();
463
+ return;
464
+ }
435
465
  if (!isStageWorkerResult(message))
436
466
  return;
437
467
  finish(resultFromWorkerMessage(message));
@@ -9,6 +9,7 @@ import type { StageBrowserSupport } from "../browser/browserHost.js";
9
9
  import { RunManager } from "../runtime/runManager.js";
10
10
  import { emailHostFor } from "../email/host.js";
11
11
  import { EmailTriggers } from "../email/triggers.js";
12
+ import { type ChannelService } from "../channels/service.js";
12
13
  import { type RunChangeBus } from "../runtime/runChangeBus.js";
13
14
  import { type McpHttpHandler } from "../mcp/server.js";
14
15
  import { type HostConfig } from "../config/hostConfig.js";
@@ -59,6 +60,8 @@ export type StageflowHostBootstrap = {
59
60
  stopGithubPollSource: () => void;
60
61
  /** Stops the email-trigger IMAP/IDLE listeners started at boot. */
61
62
  stopEmailSource: () => void;
63
+ /** Connections page read model and background health checks. */
64
+ channels?: ChannelService;
62
65
  email?: ReturnType<typeof emailHostFor> & {
63
66
  triggers: EmailTriggers;
64
67
  stop: () => Promise<void>;
@@ -20,6 +20,7 @@ import { GithubPollSource, githubPollIntervalMsFromEnv, } from "../runtime/githu
20
20
  import { EmailSource } from "../runtime/emailSource.js";
21
21
  import { emailHostFor, releaseEmailHost } from "../email/host.js";
22
22
  import { EmailTriggers } from "../email/triggers.js";
23
+ import { startChannelService } from "../channels/service.js";
23
24
  import { fireTrigger } from "../runtime/triggerRunner.js";
24
25
  import { createRunChangeBus, getRunChangeBusFromWrappedStore, isRunStoreWrapped, wrapRunStoreWithChangeBus, } from "../runtime/runChangeBus.js";
25
26
  import { createMcpHttpHandler, resolveMcpStateless, } from "../mcp/server.js";
@@ -331,9 +332,11 @@ export async function bootstrapStageflowHost(options) {
331
332
  await Promise.all([stopAccountEmail(), emailSource.stop(), scheduleSource.stop(), githubPollSource.stop()]);
332
333
  throw error;
333
334
  }
335
+ const channels = startChannelService(cwd);
334
336
  const stopEmailSource = () => {
335
337
  void emailSource.stop();
336
338
  void stopAccountEmail();
339
+ void channels.stop();
337
340
  };
338
341
  const mcpStateless = resolveMcpStateless({
339
342
  mcpStateless: options.mcpStateless,
@@ -374,6 +377,7 @@ export async function bootstrapStageflowHost(options) {
374
377
  stopGithubPollSource,
375
378
  stopEmailSource,
376
379
  email: { ...emailHost, triggers: emailTriggers, stop: stopAccountEmail },
380
+ channels,
377
381
  storeFilesystem,
378
382
  workspaceCatalog,
379
383
  };
@@ -0,0 +1,9 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { ChannelService } from "../channels/service.js";
3
+ type Json = (res: ServerResponse, status: number, body: unknown) => void;
4
+ /**
5
+ * Listing, live checks and edits. Request bodies carry secret values on the way in only; nothing
6
+ * the API returns contains one, and failures answer with a code, never the request or its values.
7
+ */
8
+ export declare function handleChannelRoutes(req: IncomingMessage, method: string, pathname: string, service: ChannelService, res: ServerResponse, json: Json): Promise<boolean>;
9
+ export {};
@@ -0,0 +1,88 @@
1
+ import { ChannelError } from "../channels/errors.js";
2
+ import { channelErrorStatus } from "../channels/errorInfo.js";
3
+ const MAX_BODY_BYTES = 65536;
4
+ async function readBody(req) {
5
+ const chunks = [];
6
+ let size = 0;
7
+ for await (const chunk of req) {
8
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
9
+ size += buffer.length;
10
+ if (size > MAX_BODY_BYTES)
11
+ throw new ChannelError("CHANNEL_RESOURCE_LIMIT");
12
+ chunks.push(buffer);
13
+ }
14
+ if (chunks.length === 0)
15
+ return {};
16
+ try {
17
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
18
+ }
19
+ catch {
20
+ throw new ChannelError("CHANNEL_INVALID_INPUT");
21
+ }
22
+ }
23
+ /**
24
+ * Listing, live checks and edits. Request bodies carry secret values on the way in only; nothing
25
+ * the API returns contains one, and failures answer with a code, never the request or its values.
26
+ */
27
+ export async function handleChannelRoutes(req, method, pathname, service, res, json) {
28
+ const idMatch = pathname.match(/^\/api\/connections\/([^/]+)(?:\/(check|validate-target))?$/);
29
+ const isCollection = pathname === "/api/connections";
30
+ const isTest = pathname === "/api/connections/test";
31
+ const isMeta = pathname === "/api/connections/meta";
32
+ if (!isCollection && !isTest && !isMeta && !idMatch)
33
+ return false;
34
+ try {
35
+ if (isMeta && method === "GET") {
36
+ json(res, 200, service.meta());
37
+ return true;
38
+ }
39
+ if (isCollection && method === "GET") {
40
+ json(res, 200, service.list());
41
+ return true;
42
+ }
43
+ if (isCollection && method === "POST") {
44
+ json(res, 201, await service.create(await readBody(req)));
45
+ return true;
46
+ }
47
+ if (isTest && method === "POST") {
48
+ json(res, 200, await service.test(await readBody(req)));
49
+ return true;
50
+ }
51
+ if (idMatch) {
52
+ let id;
53
+ try {
54
+ id = decodeURIComponent(idMatch[1]);
55
+ }
56
+ catch {
57
+ throw new ChannelError("CHANNEL_INVALID_INPUT");
58
+ }
59
+ if (idMatch[2] === "check" && method === "POST") {
60
+ const health = await service.checkNow(id);
61
+ if (health)
62
+ json(res, 200, health);
63
+ else
64
+ json(res, 404, { code: "CHANNEL_NOT_FOUND" });
65
+ return true;
66
+ }
67
+ if (idMatch[2] === "validate-target" && method === "POST") {
68
+ json(res, 200, service.validateTarget(id, await readBody(req)));
69
+ return true;
70
+ }
71
+ if (!idMatch[2] && method === "PATCH") {
72
+ json(res, 200, await service.update(id, await readBody(req)));
73
+ return true;
74
+ }
75
+ if (!idMatch[2] && method === "DELETE") {
76
+ await service.remove(id);
77
+ json(res, 200, { removed: true });
78
+ return true;
79
+ }
80
+ }
81
+ }
82
+ catch (error) {
83
+ const code = error instanceof ChannelError ? error.code : "CHANNEL_STORAGE_FAILED";
84
+ json(res, channelErrorStatus(code), { code });
85
+ return true;
86
+ }
87
+ return false;
88
+ }
@@ -51,6 +51,7 @@ import { isTaskFile } from "../runtime/taskInput.js";
51
51
  import { findTokenShapedField, tokenRejectedPayload, } from "../runtime/startPayload.js";
52
52
  import { parseAskOperatorAnswer } from "../tools/askOperator.js";
53
53
  import { handleEmailRoutes } from "./emailRoutes.js";
54
+ import { handleChannelRoutes } from "./channelRoutes.js";
54
55
  import { bootstrapStageflowHost, } from "./bootstrap.js";
55
56
  import { assertAllowedHttpAccess, isTrustedLocalHttpRequest, resolveAllowedHosts, } from "./allowedHosts.js";
56
57
  import { enforceBearerAuth, loadControlTokens, requiredScopeFor, } from "./controlToken.js";
@@ -214,6 +215,16 @@ export function createOperatorRoutes(deps) {
214
215
  try {
215
216
  if (await liveView.handle(req, res))
216
217
  return true;
218
+ if (pathname === "/api/connections" || pathname.startsWith("/api/connections/")) {
219
+ if (!assertAllowedHttpAccess({ entries: [] }, req, res, { requireOrigin: method !== "GET" }))
220
+ return true;
221
+ if (!boot.channels) {
222
+ json(res, 503, { code: "CHANNEL_CONNECTION_FAILED" });
223
+ return true;
224
+ }
225
+ if (await handleChannelRoutes(req, method, pathname, boot.channels, res, json))
226
+ return true;
227
+ }
217
228
  if (pathname.startsWith("/api/email/")) {
218
229
  if (!assertAllowedHttpAccess({ entries: [] }, req, res, { requireOrigin: ["POST", "PATCH", "DELETE"].includes(method) }))
219
230
  return true;
@@ -6,12 +6,12 @@ export declare function createSearchEmailTool(email: StageEmail): {
6
6
  description: string;
7
7
  parameters: Type.TObject<Type.TProperties>;
8
8
  execute(_toolCallId: string, input: unknown): Promise<{
9
- isError?: undefined;
10
9
  content: {
11
10
  type: "text";
12
11
  text: string;
13
12
  }[];
14
13
  details: unknown;
14
+ isError?: undefined;
15
15
  } | {
16
16
  content: {
17
17
  type: "text";
@@ -31,12 +31,12 @@ export declare function createGetEmailTool(email: StageEmail): {
31
31
  description: string;
32
32
  parameters: Type.TObject<Type.TProperties>;
33
33
  execute(_toolCallId: string, input: unknown): Promise<{
34
- isError?: undefined;
35
34
  content: {
36
35
  type: "text";
37
36
  text: string;
38
37
  }[];
39
38
  details: unknown;
39
+ isError?: undefined;
40
40
  } | {
41
41
  content: {
42
42
  type: "text";
@@ -56,12 +56,12 @@ export declare function createDownloadEmailAttachmentTool(email: StageEmail): {
56
56
  description: string;
57
57
  parameters: Type.TObject<Type.TProperties>;
58
58
  execute(_toolCallId: string, input: unknown): Promise<{
59
- isError?: undefined;
60
59
  content: {
61
60
  type: "text";
62
61
  text: string;
63
62
  }[];
64
63
  details: unknown;
64
+ isError?: undefined;
65
65
  } | {
66
66
  content: {
67
67
  type: "text";
@@ -0,0 +1,21 @@
1
+ import { Type } from "typebox";
2
+ import { type StageChannels } from "../channels/host.js";
3
+ export declare function createSendMessageTool(channels: StageChannels): {
4
+ name: string;
5
+ label: string;
6
+ description: string;
7
+ parameters: Type.TObject<{
8
+ channel: Type.TOptional<Type.TString>;
9
+ text: Type.TString;
10
+ subject: Type.TOptional<Type.TString>;
11
+ operationKey: Type.TString;
12
+ }>;
13
+ execute(_toolCallId: string, input: unknown): Promise<{
14
+ content: {
15
+ type: "text";
16
+ text: string;
17
+ }[];
18
+ details: unknown;
19
+ isError?: boolean;
20
+ }>;
21
+ };
@@ -0,0 +1,25 @@
1
+ import { Type } from "typebox";
2
+ import { MAX_MESSAGE_TEXT, channelFault } from "../channels/host.js";
3
+ export function createSendMessageTool(channels) {
4
+ return {
5
+ name: "send_message",
6
+ label: "Send message",
7
+ description: "Send a message on one of this stage's declared communication channels. The recipient is fixed by Stageflow configuration and cannot be chosen. Pass the channel alias when the stage declares more than one. Reuse the same operationKey when retrying the same message. Acceptance means provider submission, not delivery or that anyone read it. Never retry an unknown outcome with a new key. Do not put secrets in a message.",
8
+ parameters: Type.Object({
9
+ channel: Type.Optional(Type.String()),
10
+ text: Type.String({ maxLength: MAX_MESSAGE_TEXT }),
11
+ subject: Type.Optional(Type.String()),
12
+ operationKey: Type.String(),
13
+ }),
14
+ async execute(_toolCallId, input) {
15
+ try {
16
+ const receipt = await channels.send(input);
17
+ return { content: [{ type: "text", text: JSON.stringify(receipt) }], details: receipt };
18
+ }
19
+ catch (error) {
20
+ const details = channelFault(error);
21
+ return { content: [{ type: "text", text: JSON.stringify(details) }], details, isError: true };
22
+ }
23
+ },
24
+ };
25
+ }
@@ -1,4 +1,4 @@
1
- import type { LoadedStageConfig, StageBrowserConfig, StageGateKind, StageIoYaml } from "./stage.js";
1
+ import type { LoadedStageConfig, StageChannel, StageBrowserConfig, StageGateKind, StageIoYaml } from "./stage.js";
2
2
  import type { CompletionContract, RecoveryPolicy } from "./completion.js";
3
3
  import type { StageSecretDecl } from "../runtime/stageSecretDecl.js";
4
4
  import type { ToolRequirement } from "../config/toolRequires.js";
@@ -13,6 +13,8 @@ export type PipelineConfig = {
13
13
  schemas?: Record<string, unknown>;
14
14
  /** Declared toolchain binaries this pipeline needs (Slot 9). */
15
15
  requires?: ToolRequirement[];
16
+ /** Default channels inherited by every stage that does not override them. */
17
+ channels?: StageChannel[];
16
18
  };
17
19
  /** An inline pipeline definition authored directly in a `start_run` call, with no backing file. */
18
20
  export type InlinePipelineDefinition = {
@@ -22,6 +24,7 @@ export type InlinePipelineDefinition = {
22
24
  model?: unknown;
23
25
  schemas?: unknown;
24
26
  requires?: unknown;
27
+ channels?: unknown;
25
28
  };
26
29
  export type PipelineForkConfig = {
27
30
  select: "one" | "subset";
@@ -36,6 +36,17 @@ export type StageBrowserConfig = {
36
36
  logged_out_url?: string | string[];
37
37
  };
38
38
  };
39
+ export type StageChannelOperation = "send";
40
+ /** A pinned communication target for a stage. Targets are validated per provider. */
41
+ export type StageChannel = {
42
+ alias?: string;
43
+ connection: string;
44
+ operations: StageChannelOperation[];
45
+ to: string | string[];
46
+ cc?: string[];
47
+ /** Provider-specific message template, such as a pinned WhatsApp template. Validated by the provider. */
48
+ template?: Record<string, unknown>;
49
+ };
39
50
  export type StageConfig = {
40
51
  id: string;
41
52
  system_prompt: string;
@@ -56,6 +67,8 @@ export type StageConfig = {
56
67
  operations: ("send" | "reply" | "getMessage" | "search" | "downloadAttachment")[];
57
68
  }[];
58
69
  mcp?: string[];
70
+ /** Communication channels this stage may send on. Inherits pipeline `channels` defaults. */
71
+ channels?: StageChannel[];
59
72
  /** Declared Host secret names this stage may receive (Slot 6). */
60
73
  secrets?: StageSecretDecl[];
61
74
  /** Declared toolchain binaries this stage needs (Slot 9). */