stageflow 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/agent/cursorProvider.d.ts +8 -0
  2. package/dist/agent/cursorProvider.js +63 -13
  3. package/dist/agent/piAdapter.js +21 -0
  4. package/dist/agent/port.d.ts +2 -0
  5. package/dist/agent/providerAuth.d.ts +7 -3
  6. package/dist/agent/providerAuth.js +5 -3
  7. package/dist/cli/doctorCommand.js +7 -0
  8. package/dist/cli/providersCommand.d.ts +1 -1
  9. package/dist/cli/providersCommand.js +4 -4
  10. package/dist/cli.js +1 -1
  11. package/dist/config/loadStage.js +10 -0
  12. package/dist/config/pipelineStageKeys.js +1 -0
  13. package/dist/email/accounts.d.ts +109 -0
  14. package/dist/email/accounts.js +285 -0
  15. package/dist/email/adapter.d.ts +69 -0
  16. package/dist/email/adapter.js +720 -0
  17. package/dist/email/attachments.d.ts +21 -0
  18. package/dist/email/attachments.js +146 -0
  19. package/dist/email/events.d.ts +95 -0
  20. package/dist/email/events.js +292 -0
  21. package/dist/email/host.d.ts +15 -0
  22. package/dist/email/host.js +119 -0
  23. package/dist/email/messages.d.ts +44 -0
  24. package/dist/email/messages.js +182 -0
  25. package/dist/email/port.d.ts +161 -0
  26. package/dist/email/port.js +12 -0
  27. package/dist/email/replies.d.ts +5 -0
  28. package/dist/email/replies.js +56 -0
  29. package/dist/email/submissions.d.ts +31 -0
  30. package/dist/email/submissions.js +123 -0
  31. package/dist/email/triggers.d.ts +140 -0
  32. package/dist/email/triggers.js +563 -0
  33. package/dist/package-meta.d.ts +1 -1
  34. package/dist/package-meta.js +1 -1
  35. package/dist/runstore/diskUsage.d.ts +1 -1
  36. package/dist/runstore/diskUsage.js +2 -7
  37. package/dist/runstore/port.d.ts +4 -0
  38. package/dist/runstore/sqlite/SqliteRunStore.d.ts +1 -0
  39. package/dist/runstore/sqlite/SqliteRunStore.js +24 -5
  40. package/dist/runstore/sqlite/migrations/011-email-dispatch-key.d.ts +7 -0
  41. package/dist/runstore/sqlite/migrations/011-email-dispatch-key.js +12 -0
  42. package/dist/runstore/sqlite/migrations/index.js +2 -0
  43. package/dist/runtime/credentialBinding.d.ts +7 -4
  44. package/dist/runtime/credentialBinding.js +20 -31
  45. package/dist/runtime/pipelineRunner.d.ts +3 -0
  46. package/dist/runtime/pipelineRunner.js +11 -0
  47. package/dist/runtime/pipelineScheduler.js +4 -0
  48. package/dist/runtime/runManager.d.ts +1 -0
  49. package/dist/runtime/runManager.js +36 -5
  50. package/dist/runtime/settingsFile.d.ts +2 -2
  51. package/dist/runtime/settingsFile.js +5 -2
  52. package/dist/runtime/stageAttemptBootstrap.js +14 -0
  53. package/dist/runtime/stageProcessLauncher.d.ts +5 -0
  54. package/dist/runtime/stageProcessLauncher.js +55 -3
  55. package/dist/server/bootstrap.d.ts +7 -0
  56. package/dist/server/bootstrap.js +23 -0
  57. package/dist/server/emailRoutes.d.ts +5 -0
  58. package/dist/server/emailRoutes.js +136 -0
  59. package/dist/server/http.d.ts +2 -0
  60. package/dist/server/http.js +16 -0
  61. package/dist/tools/readEmail.d.ts +77 -0
  62. package/dist/tools/readEmail.js +31 -0
  63. package/dist/tools/replyEmail.d.ts +31 -0
  64. package/dist/tools/replyEmail.js +25 -0
  65. package/dist/tools/sendEmail.d.ts +43 -0
  66. package/dist/tools/sendEmail.js +26 -0
  67. package/dist/types/stage.d.ts +4 -0
  68. package/dist/ui/assets/index-59cjF0OA.js +133 -0
  69. package/dist/ui/index.html +1 -1
  70. package/examples/conditional-fork/decide.yaml +1 -1
  71. package/examples/github-release/README.md +17 -3
  72. package/examples/hello-email/README.md +23 -0
  73. package/examples/hello-email/hello-email-reply.pipeline.yaml +6 -0
  74. package/examples/hello-email/hello-email.pipeline.yaml +6 -0
  75. package/examples/hello-email/hello-email.task.yaml +6 -0
  76. package/examples/hello-email/reply-hello-email.yaml +63 -0
  77. package/examples/hello-email/send-hello-email.yaml +37 -0
  78. package/package.json +10 -5
  79. package/dist/ui/assets/index-CntB4bZR.js +0 -133
@@ -1,7 +1,15 @@
1
1
  import { type StageProviderSupport } from "./providerSupport.js";
2
2
  export declare function cursorExtensionEntryInPackage(packageRoot: string): string | undefined;
3
+ export declare function resolveBundledCursorExtensionPath(): string | undefined;
4
+ /** `$STAGEFLOW_HOME/agent/npm` install (Pi-style); after bundled dependency. */
5
+ export declare function resolveAgentHomeCursorExtensionPath(): string | undefined;
3
6
  export declare function resolveCursorExtensionPath(): string | undefined;
4
7
  export declare function isCursorModelRef(modelRef: string): boolean;
5
8
  export declare function workshopCursorBridgeHint(toolNames: readonly string[]): string;
6
9
  export declare function cursorBridgePrompt(message: string, modelId: string | undefined, toolNames: readonly string[]): string;
7
10
  export declare const cursorProviderSupport: StageProviderSupport;
11
+ export declare function readCursorApiKey(env?: NodeJS.ProcessEnv): string | undefined;
12
+ export declare function getCursorHostStatus(env?: NodeJS.ProcessEnv): {
13
+ cursorSdkReady: boolean;
14
+ cursorApiKeyConfigured: boolean;
15
+ };
@@ -5,10 +5,13 @@
5
5
  * module's prepare()/env sealing path.
6
6
  */
7
7
  import { existsSync, readFileSync } from "node:fs";
8
- import os from "node:os";
8
+ import { createRequire } from "node:module";
9
9
  import path from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { readSecretFromEnvOrFile } from "../config/secretFromEnvOrFile.js";
12
+ import { globalStageflowHome } from "../project/globalHome.js";
11
13
  import { registerProviderSupport, } from "./providerSupport.js";
14
+ import { stageflowAgentAuthPath } from "../runtime/credentialBinding.js";
12
15
  const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
13
16
  /**
14
17
  * Locate the pi-cursor-sdk extension entry without opening full global package
@@ -17,21 +20,18 @@ const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
17
20
  * Resolution order:
18
21
  * 1. STAGEFLOW_CURSOR_EXTENSION (absolute path to the extension .ts/.js)
19
22
  * 2. Path package from the Pi agent settings.json (same source interactive pi uses)
20
- * 3. npm install under <pi-agent>/npm/node_modules/pi-cursor-sdk
23
+ * 3. npm dependency `pi-cursor-sdk` bundled with the Stageflow install (repo or global)
24
+ * 4. npm install under <stageflow-agent>/npm/node_modules/pi-cursor-sdk
21
25
  * (`dist/index.js` for 0.3+, `src/index.ts` for older publishes)
22
- * 4. Sibling checkout at ../pi-cursor-sdk relative to this repo
26
+ * 5. Sibling checkout at ../pi-cursor-sdk relative to this repo
23
27
  *
24
- * A stage worker's HOME is an empty attempt directory, so the Pi agent dir is
25
- * the directory of STAGEFLOW_PI_HOME_AUTH_PATH when the Host set it, not
26
- * os.homedir().
28
+ * A stage worker's HOME is an empty attempt directory; the agent dir comes from
29
+ * STAGEFLOW_AGENT_AUTH_PATH when the Host set it.
27
30
  */
28
31
  const CURSOR_PACKAGE_ENTRIES = ["dist/index.js", "src/index.ts"];
29
- /** Pi agent dir. Stage workers must not use os.homedir(): HOME is the attempt dir. */
32
+ /** Stageflow agent dir ($STAGEFLOW_HOME/agent). Stage workers must not use os.homedir(): HOME is the attempt dir. */
30
33
  function piAgentDir() {
31
- const auth = process.env.STAGEFLOW_PI_HOME_AUTH_PATH?.trim();
32
- if (auth)
33
- return path.dirname(path.resolve(auth));
34
- return path.join(os.homedir(), ".pi", "agent");
34
+ return path.dirname(stageflowAgentAuthPath());
35
35
  }
36
36
  export function cursorExtensionEntryInPackage(packageRoot) {
37
37
  for (const rel of CURSOR_PACKAGE_ENTRIES) {
@@ -42,6 +42,21 @@ export function cursorExtensionEntryInPackage(packageRoot) {
42
42
  }
43
43
  return undefined;
44
44
  }
45
+ export function resolveBundledCursorExtensionPath() {
46
+ const require = createRequire(import.meta.url);
47
+ let packageRoot;
48
+ try {
49
+ packageRoot = path.dirname(require.resolve("pi-cursor-sdk/package.json"));
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ return cursorExtensionEntryInPackage(packageRoot);
55
+ }
56
+ /** `$STAGEFLOW_HOME/agent/npm` install (Pi-style); after bundled dependency. */
57
+ export function resolveAgentHomeCursorExtensionPath() {
58
+ return cursorExtensionEntryInPackage(path.join(piAgentDir(), "npm", "node_modules", "pi-cursor-sdk"));
59
+ }
45
60
  export function resolveCursorExtensionPath() {
46
61
  const fromEnv = process.env.STAGEFLOW_CURSOR_EXTENSION?.trim();
47
62
  if (fromEnv && existsSync(fromEnv)) {
@@ -51,7 +66,11 @@ export function resolveCursorExtensionPath() {
51
66
  if (fromSettings) {
52
67
  return fromSettings;
53
68
  }
54
- const npmEntry = cursorExtensionEntryInPackage(path.join(piAgentDir(), "npm", "node_modules", "pi-cursor-sdk"));
69
+ const bundled = resolveBundledCursorExtensionPath();
70
+ if (bundled) {
71
+ return bundled;
72
+ }
73
+ const npmEntry = resolveAgentHomeCursorExtensionPath();
55
74
  if (npmEntry) {
56
75
  return npmEntry;
57
76
  }
@@ -78,7 +97,7 @@ function sealCursorSettingSources() {
78
97
  function missingExtensionReason(modelRef) {
79
98
  return [
80
99
  `Model "${modelRef}" requires pi-cursor-sdk, but no extension entry was found.`,
81
- "Install with `pi install npm:pi-cursor-sdk`, or set STAGEFLOW_CURSOR_EXTENSION",
100
+ `Install Stageflow with npm (includes pi-cursor-sdk), or set STAGEFLOW_CURSOR_EXTENSION`,
82
101
  "to the absolute path of pi-cursor-sdk/dist/index.js (or src/index.ts).",
83
102
  "Also ensure a Cursor SDK API key is available via Pi /login or CURSOR_API_KEY.",
84
103
  ].join(" ");
@@ -122,6 +141,37 @@ export const cursorProviderSupport = {
122
141
  ].join(" ");
123
142
  },
124
143
  };
144
+ const CURSOR_API_KEY_FILE_NAME = "cursor-api-key";
145
+ export function readCursorApiKey(env = process.env) {
146
+ let fromEnv;
147
+ try {
148
+ fromEnv = readSecretFromEnvOrFile(env, "CURSOR_API_KEY");
149
+ }
150
+ catch {
151
+ return undefined;
152
+ }
153
+ if (fromEnv !== undefined && fromEnv.trim() !== "") {
154
+ return fromEnv.trim();
155
+ }
156
+ const filePath = path.join(globalStageflowHome(), "agent", CURSOR_API_KEY_FILE_NAME);
157
+ if (!existsSync(filePath)) {
158
+ return undefined;
159
+ }
160
+ try {
161
+ const raw = readFileSync(filePath, "utf8").replace(/\r?\n$/, "").trim();
162
+ return raw.length > 0 ? raw : undefined;
163
+ }
164
+ catch {
165
+ return undefined;
166
+ }
167
+ }
168
+ export function getCursorHostStatus(env = process.env) {
169
+ const key = readCursorApiKey(env);
170
+ return {
171
+ cursorSdkReady: resolveCursorExtensionPath() !== undefined,
172
+ cursorApiKeyConfigured: key !== undefined,
173
+ };
174
+ }
125
175
  registerProviderSupport(cursorProviderSupport);
126
176
  function resolveFromPiSettings() {
127
177
  const agentDir = piAgentDir();
@@ -44,6 +44,9 @@ import { attemptContext, noAttemptContext, } from "../runtime/stageAttemptContex
44
44
  import { createEmitStageEnvelopeTool, } from "../tools/emitStageEnvelope.js";
45
45
  import { createAskOperatorTool, } from "../tools/askOperator.js";
46
46
  import { createWriteStageArtifactTool } from "../tools/writeStageArtifact.js";
47
+ import { createSendEmailTool } from "../tools/sendEmail.js";
48
+ import { createReplyEmailTool } from "../tools/replyEmail.js";
49
+ import { createGetEmailTool, createSearchEmailTool, createDownloadEmailAttachmentTool } from "../tools/readEmail.js";
47
50
  import "./cursorProvider.js";
48
51
  import { findProviderSupport } from "./providerSupport.js";
49
52
  import { mapSessionEventToActivity, readActivityVerbose } from "./activity.js";
@@ -479,6 +482,9 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
479
482
  ].join("\n")
480
483
  : `Create factory stage artifacts under ${attemptArtifactsPath} relative to the run folder.`;
481
484
  return [
485
+ ...(input.stage.email?.length ? [
486
+ `Stageflow email permissions: ${JSON.stringify(input.stage.email)}. Use only declared accounts and operations. Use a stable operationKey for each intended message. Provider acceptance does not prove delivery. Never resend an unknown submission automatically.`,
487
+ ] : []),
482
488
  `Task id: ${input.task.id}`,
483
489
  `Stage id: ${runtimeStageId(input)}`,
484
490
  `Goal: ${input.task.goal}`,
@@ -908,6 +914,21 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
908
914
  if (attached.extensionFactories !== undefined) {
909
915
  tools.push(...collectMcpExtensionToolNames(loader));
910
916
  }
917
+ if (input.email) {
918
+ const permitted = new Set(input.stage.email?.flatMap(permission => permission.operations));
919
+ const definitions = [
920
+ permitted.has("send") ? createSendEmailTool(input.email) : undefined,
921
+ permitted.has("reply") ? createReplyEmailTool(input.email) : undefined,
922
+ permitted.has("search") ? createSearchEmailTool(input.email) : undefined,
923
+ permitted.has("getMessage") ? createGetEmailTool(input.email) : undefined,
924
+ permitted.has("downloadAttachment") ? createDownloadEmailAttachmentTool(input.email) : undefined,
925
+ ];
926
+ for (const definition of definitions)
927
+ if (definition) {
928
+ customTools.push(defineTool(definition));
929
+ tools.push(definition.name);
930
+ }
931
+ }
911
932
  return {
912
933
  sessionManager,
913
934
  modelRuntime,
@@ -7,6 +7,7 @@ import type { TaskFile } from "../types/task.js";
7
7
  import type { StageActivityEvent } from "./activity.js";
8
8
  import type { ResolvedMcpServers } from "../config/resolveStageMcpServers.js";
9
9
  import type { StageRoots } from "../runtime/stageRoots.js";
10
+ import type { StageEmail } from "../email/host.js";
10
11
  import type { QaExchange } from "../hitl/qaTrail.js";
11
12
  import type { StageUsage } from "../types/usage.js";
12
13
  /** Opaque to runtime; adapters interpret. */
@@ -63,6 +64,7 @@ export type StageRunInput = {
63
64
  /** Optional raw assistant-text-delta sink, fired alongside onActivity, not instead of it. */
64
65
  onAssistantTextDelta?: (delta: string) => void;
65
66
  skillFilePath?: string;
67
+ email?: StageEmail;
66
68
  /** Bundled browser skill; set by the runtime when the stage has `browser`. */
67
69
  browserSkillFilePath?: string;
68
70
  forkEmitContext?: ForkEmitContext;
@@ -25,12 +25,16 @@ export type ProvidersListResult = {
25
25
  via: "pi";
26
26
  providers: ProviderSummary[];
27
27
  };
28
- export type PiHomeDetectResult = {
29
- piHomeUsable: boolean;
28
+ export type ProvidersDetectResult = {
30
29
  credentialSource?: CredentialSource;
31
30
  provisional: boolean;
32
31
  source: CredentialSource;
32
+ authConfigured: boolean;
33
+ cursorSdkReady: boolean;
34
+ cursorApiKeyConfigured: boolean;
33
35
  };
36
+ /** @deprecated Use ProvidersDetectResult */
37
+ export type PiHomeDetectResult = ProvidersDetectResult;
34
38
  export type ProviderAuthRuntime = {
35
39
  getProviders(): readonly Provider[];
36
40
  getProvider(providerId: string): Provider | undefined;
@@ -84,7 +88,7 @@ export type OauthLoginResult = {
84
88
  };
85
89
  export declare function loginWithOauth(cwd: string, providerId: string, interaction: AuthInteraction, ctx?: ProviderAuthContext): Promise<OauthLoginResult>;
86
90
  export declare function logoutProvider(cwd: string, providerId: string, ctx?: ProviderAuthContext): Promise<ProviderAuthStatus>;
87
- export declare function detectPiHome(cwd: string): PiHomeDetectResult;
91
+ export declare function detectPiHome(cwd: string): ProvidersDetectResult;
88
92
  export declare function getCredentialSourceSettings(cwd: string): {
89
93
  credentialSource?: CredentialSource;
90
94
  binding: {
@@ -1,5 +1,6 @@
1
1
  import { CredentialSynchronizationError, ModelRuntime, } from "@earendil-works/pi-coding-agent";
2
- import { isUsableAuthFile, parseCredentialSource, piHomeAuthPath, resolveCredentialBinding, writeCredentialSourceToContext, } from "../runtime/credentialBinding.js";
2
+ import { parseCredentialSource, resolveCredentialBinding, writeCredentialSourceToContext, isUsableAuthFile, } from "../runtime/credentialBinding.js";
3
+ import { getCursorHostStatus } from "./cursorProvider.js";
3
4
  import { readPersistedCredentialSourceFromContext, } from "../runtime/settingsFile.js";
4
5
  import { resolveProjectContext } from "../project/resolveProjectContext.js";
5
6
  export function makeMutationLock() {
@@ -251,10 +252,11 @@ export function detectPiHome(cwd) {
251
252
  const binding = resolveCredentialBinding(projectCtx);
252
253
  const persisted = readPersistedCredentialSourceFromContext(projectCtx);
253
254
  return {
254
- piHomeUsable: isUsableAuthFile(piHomeAuthPath()),
255
255
  ...(persisted !== undefined ? { credentialSource: persisted } : {}),
256
256
  provisional: binding.provisional,
257
257
  source: binding.source,
258
+ authConfigured: isUsableAuthFile(binding.authPath),
259
+ ...getCursorHostStatus(),
258
260
  };
259
261
  }
260
262
  export function getCredentialSourceSettings(cwd) {
@@ -272,7 +274,7 @@ export function getCredentialSourceSettings(cwd) {
272
274
  export function setCredentialSource(cwd, credentialSource) {
273
275
  const parsed = parseCredentialSource(credentialSource);
274
276
  if (parsed === undefined) {
275
- throw new ProviderAuthError('credentialSource must be "pi_home" or "sf_owned"', 400);
277
+ throw new ProviderAuthError('credentialSource must be "sf_owned"', 400);
276
278
  }
277
279
  const projectCtx = resolveProjectContext(cwd);
278
280
  writeCredentialSourceToContext(projectCtx, parsed);
@@ -215,6 +215,13 @@ async function freeDiskCheck(home, env) {
215
215
  const size = await readFilesystemSize(home);
216
216
  const floor = resolveMinFreeDiskFloor(env.STAGEFLOW_MIN_FREE_DISK_BYTES, size.totalBytes);
217
217
  const freeGiB = (size.freeBytes / (1024 * 1024 * 1024)).toFixed(2);
218
+ if (floor === 0) {
219
+ return {
220
+ id: "free_disk",
221
+ status: "pass",
222
+ message: `${home} has ${freeGiB} GiB free (free-disk admission disabled)`,
223
+ };
224
+ }
218
225
  if (size.freeBytes < floor) {
219
226
  return {
220
227
  id: "free_disk",
@@ -1,6 +1,6 @@
1
1
  import type { AuthInteraction } from "@earendil-works/pi-ai";
2
2
  import { type ProviderAuthContext } from "../agent/providerAuth.js";
3
- export declare const PROVIDERS_USAGE = "Usage:\n sf providers list\n sf providers status [--provider <id>]\n sf providers detect\n sf providers source [get | set <pi_home|sf_owned>]\n sf providers login <providerId> [--type api_key|oauth] [--api-key-env <VAR>]\n sf providers logout <providerId>";
3
+ export declare const PROVIDERS_USAGE = "Usage:\n sf providers list\n sf providers status [--provider <id>]\n sf providers detect\n sf providers source [get | set sf_owned]\n sf providers login <providerId> [--type api_key|oauth] [--api-key-env <VAR>]\n sf providers logout <providerId>";
4
4
  export type ProvidersCommandIo = {
5
5
  log: (line: string) => void;
6
6
  error: (line: string) => void;
@@ -6,7 +6,7 @@ export const PROVIDERS_USAGE = `Usage:
6
6
  sf providers list
7
7
  sf providers status [--provider <id>]
8
8
  sf providers detect
9
- sf providers source [get | set <pi_home|sf_owned>]
9
+ sf providers source [get | set sf_owned]
10
10
  sf providers login <providerId> [--type api_key|oauth] [--api-key-env <VAR>]
11
11
  sf providers logout <providerId>`;
12
12
  const defaultIo = {
@@ -167,7 +167,7 @@ export async function runProvidersCommand(args, cwd, io = {}, ctx = defaultConte
167
167
  ? `credentialSource=${detected.credentialSource}`
168
168
  : "credentialSource=(unset)";
169
169
  out.log([
170
- `piHomeUsable=${detected.piHomeUsable}`,
170
+ `authConfigured=${detected.authConfigured}`,
171
171
  sourceLine,
172
172
  `provisional=${detected.provisional}`,
173
173
  `bindingSource=${detected.source}`,
@@ -188,8 +188,8 @@ export async function runProvidersCommand(args, cwd, io = {}, ctx = defaultConte
188
188
  }
189
189
  if (action === "set") {
190
190
  const value = parsed.positionals[1];
191
- if (value !== "pi_home" && value !== "sf_owned") {
192
- out.error('source set requires pi_home or sf_owned');
191
+ if (value !== "sf_owned") {
192
+ out.error('source set requires sf_owned');
193
193
  out.error(PROVIDERS_USAGE);
194
194
  return 1;
195
195
  }
package/dist/cli.js CHANGED
@@ -69,7 +69,7 @@ const USAGE = `Usage:
69
69
  sf providers list
70
70
  sf providers status [--provider <id>]
71
71
  sf providers detect
72
- sf providers source [get | set <pi_home|sf_owned>]
72
+ sf providers source [get | set sf_owned]
73
73
  sf providers login <providerId> [--type api_key|oauth] [--api-key-env <VAR>]
74
74
  sf providers logout <providerId>
75
75
  sf skills list
@@ -7,6 +7,7 @@ import { parseModelField } from "./modelField.js";
7
7
  import { allowLegacyYamlAuthoring, dialectWarningForDocument, legacyAuthoringRejected, presentLegacyKeys, } from "./legacyYaml.js";
8
8
  import { parsePreEmitChecks } from "./parsePreEmitChecks.js";
9
9
  import { readYamlObject } from "./readYamlObject.js";
10
+ import { z } from "zod";
10
11
  import { parseStageSecrets } from "../runtime/stageSecretDecl.js";
11
12
  import { parseToolRequires } from "./toolRequires.js";
12
13
  import { parseStageBrowser } from "./stageBrowser.js";
@@ -140,6 +141,15 @@ function parseStageFields(raw, label, entryId, deferSchemaRefs, requireIo, compi
140
141
  id: entryId,
141
142
  system_prompt: raw.system_prompt,
142
143
  };
144
+ if (raw.email !== undefined) {
145
+ const permissions = z.array(z.object({ accountId: z.string().min(1).max(200),
146
+ operations: z.array(z.enum(["send", "reply", "getMessage", "search", "downloadAttachment"])).min(1).max(5),
147
+ }).strict()).max(20).safeParse(raw.email);
148
+ if (!permissions.success || new Set(permissions.data.map(permission => permission.accountId)).size !== permissions.data.length) {
149
+ return loadFailure([{ code: "stage.invalid_shape", message: `Invalid stage ${label}: invalid email permissions`, category: "stage", stageId: entryId }]);
150
+ }
151
+ stage.email = permissions.data;
152
+ }
143
153
  if (raw.clone_actions !== undefined) {
144
154
  return loadFailure([
145
155
  {
@@ -28,6 +28,7 @@ export const WIRING_KEYS = new Set([
28
28
  ]);
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
+ "email",
31
32
  "system_prompt",
32
33
  "model",
33
34
  "payload_schema",
@@ -0,0 +1,109 @@
1
+ import { z } from "zod";
2
+ import { type EmailAccountStatus } from "./port.js";
3
+ declare const connectionSchema: z.ZodObject<{
4
+ host: z.ZodString;
5
+ port: z.ZodNumber;
6
+ username: z.ZodString;
7
+ tls: z.ZodDefault<z.ZodEnum<{
8
+ implicit: "implicit";
9
+ none: "none";
10
+ starttls: "starttls";
11
+ }>>;
12
+ auth: z.ZodObject<{
13
+ type: z.ZodEnum<{
14
+ oauth2: "oauth2";
15
+ password: "password";
16
+ }>;
17
+ secretRef: z.ZodString;
18
+ expiresAt: z.ZodOptional<z.ZodISODateTime>;
19
+ }, z.core.$strict>;
20
+ }, z.core.$strict>;
21
+ declare const accountSchema: z.ZodObject<{
22
+ displayName: z.ZodString;
23
+ address: z.ZodEmail;
24
+ enabled: z.ZodDefault<z.ZodBoolean>;
25
+ adapter: z.ZodDefault<z.ZodLiteral<"local">>;
26
+ folders: z.ZodDefault<z.ZodArray<z.ZodString>>;
27
+ senderAliases: z.ZodDefault<z.ZodArray<z.ZodEmail>>;
28
+ imap: z.ZodObject<{
29
+ host: z.ZodString;
30
+ port: z.ZodNumber;
31
+ username: z.ZodString;
32
+ tls: z.ZodDefault<z.ZodEnum<{
33
+ implicit: "implicit";
34
+ none: "none";
35
+ starttls: "starttls";
36
+ }>>;
37
+ auth: z.ZodObject<{
38
+ type: z.ZodEnum<{
39
+ oauth2: "oauth2";
40
+ password: "password";
41
+ }>;
42
+ secretRef: z.ZodString;
43
+ expiresAt: z.ZodOptional<z.ZodISODateTime>;
44
+ }, z.core.$strict>;
45
+ }, z.core.$strict>;
46
+ smtp: z.ZodObject<{
47
+ host: z.ZodString;
48
+ port: z.ZodNumber;
49
+ username: z.ZodString;
50
+ tls: z.ZodDefault<z.ZodEnum<{
51
+ implicit: "implicit";
52
+ none: "none";
53
+ starttls: "starttls";
54
+ }>>;
55
+ auth: z.ZodObject<{
56
+ type: z.ZodEnum<{
57
+ oauth2: "oauth2";
58
+ password: "password";
59
+ }>;
60
+ secretRef: z.ZodString;
61
+ expiresAt: z.ZodOptional<z.ZodISODateTime>;
62
+ }, z.core.$strict>;
63
+ }, z.core.$strict>;
64
+ allowInsecureLocalDevelopment: z.ZodDefault<z.ZodBoolean>;
65
+ connectionTimeoutMs: z.ZodDefault<z.ZodNumber>;
66
+ pollingIntervalMs: z.ZodDefault<z.ZodNumber>;
67
+ reconnectMaxDelayMs: z.ZodDefault<z.ZodNumber>;
68
+ searchWorkLimit: z.ZodDefault<z.ZodNumber>;
69
+ attachmentLimits: z.ZodDefault<z.ZodObject<{
70
+ count: z.ZodDefault<z.ZodNumber>;
71
+ perFileBytes: z.ZodDefault<z.ZodNumber>;
72
+ totalBytes: z.ZodDefault<z.ZodNumber>;
73
+ downloadBytes: z.ZodDefault<z.ZodNumber>;
74
+ }, z.core.$strict>>;
75
+ sentFolder: z.ZodOptional<z.ZodString>;
76
+ sentCopyPolicy: z.ZodDefault<z.ZodEnum<{
77
+ "imap-append": "imap-append";
78
+ "provider-managed": "provider-managed";
79
+ }>>;
80
+ sentCopyMaxBytes: z.ZodDefault<z.ZodNumber>;
81
+ }, z.core.$strict>;
82
+ export type EmailAccountInput = z.input<typeof accountSchema>;
83
+ export type EmailConnection = z.output<typeof connectionSchema>;
84
+ export type EmailAccount = z.output<typeof accountSchema> & {
85
+ accountId: string;
86
+ scope: string;
87
+ };
88
+ /** Host-owned configuration. Reads return copies; mutations replace the file atomically. */
89
+ export declare class EmailAccounts {
90
+ readonly scope: string;
91
+ private readonly file;
92
+ private state;
93
+ private readonly listeners;
94
+ constructor(cwd: string);
95
+ list(): EmailAccount[];
96
+ get(accountId: string, requireEnabled?: boolean): EmailAccount;
97
+ create(input: unknown): EmailAccount;
98
+ update(accountId: string, patch: unknown): EmailAccount;
99
+ remove(accountId: string): void;
100
+ health(accountId: string): EmailAccountStatus | undefined;
101
+ recordHealth(status: EmailAccountStatus): void;
102
+ onChange(listener: (accountId: string) => void): () => void;
103
+ private notify;
104
+ private requireHttpAccount;
105
+ private syncStartupAccounts;
106
+ private persist;
107
+ }
108
+ export declare function resolveEmailSecret(connection: EmailConnection, env?: NodeJS.ProcessEnv): string;
109
+ export {};