stageflow 0.35.0 → 0.39.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 (320) hide show
  1. package/README.md +18 -8
  2. package/dist/agent/claudeAdapter.js +12 -0
  3. package/dist/agent/piAdapter.d.ts +9 -1
  4. package/dist/agent/piAdapter.js +30 -2
  5. package/dist/agent/piIsolatedMcp.js +14 -2
  6. package/dist/agent/piIsolatedMcpProbe.d.ts +5 -1
  7. package/dist/agent/piIsolatedMcpProbe.js +16 -30
  8. package/dist/agent/port.d.ts +7 -0
  9. package/dist/agents/agentId.d.ts +8 -0
  10. package/dist/agents/agentId.js +27 -0
  11. package/dist/agents/approvalBroker.d.ts +16 -0
  12. package/dist/agents/approvalBroker.js +45 -0
  13. package/dist/agents/authorChat.d.ts +19 -0
  14. package/dist/agents/authorChat.js +63 -0
  15. package/dist/agents/chatTurn.d.ts +23 -0
  16. package/dist/agents/chatTurn.js +77 -0
  17. package/dist/agents/communications/agentmail.d.ts +19 -0
  18. package/dist/agents/communications/agentmail.js +46 -0
  19. package/dist/agents/communications/agentmailListener.d.ts +59 -0
  20. package/dist/agents/communications/agentmailListener.js +183 -0
  21. package/dist/agents/communications/allowlist.d.ts +10 -0
  22. package/dist/agents/communications/allowlist.js +36 -0
  23. package/dist/agents/communications/channelConnections.d.ts +3 -0
  24. package/dist/agents/communications/channelConnections.js +51 -0
  25. package/dist/agents/communications/delivery.d.ts +53 -0
  26. package/dist/agents/communications/delivery.js +142 -0
  27. package/dist/agents/communications/email.d.ts +26 -0
  28. package/dist/agents/communications/email.js +20 -0
  29. package/dist/agents/communications/identity.d.ts +13 -0
  30. package/dist/agents/communications/identity.js +18 -0
  31. package/dist/agents/communications/imessageListener.d.ts +41 -0
  32. package/dist/agents/communications/imessageListener.js +92 -0
  33. package/dist/agents/communications/inbound.d.ts +63 -0
  34. package/dist/agents/communications/inbound.js +143 -0
  35. package/dist/agents/communications/listener.d.ts +36 -0
  36. package/dist/agents/communications/listener.js +38 -0
  37. package/dist/agents/communications/photon.d.ts +19 -0
  38. package/dist/agents/communications/photon.js +38 -0
  39. package/dist/agents/communications/port.d.ts +27 -0
  40. package/dist/agents/communications/port.js +6 -0
  41. package/dist/agents/communications/slack.d.ts +28 -0
  42. package/dist/agents/communications/slack.js +65 -0
  43. package/dist/agents/communications/supervisor.d.ts +36 -0
  44. package/dist/agents/communications/supervisor.js +96 -0
  45. package/dist/agents/communications/telegram.d.ts +19 -0
  46. package/dist/agents/communications/telegram.js +52 -0
  47. package/dist/agents/definitionSummary.d.ts +13 -0
  48. package/dist/agents/definitionSummary.js +91 -0
  49. package/dist/agents/delegate.d.ts +3 -0
  50. package/dist/agents/delegate.js +8 -0
  51. package/dist/agents/errors.d.ts +10 -0
  52. package/dist/agents/errors.js +32 -0
  53. package/dist/agents/guardrails.d.ts +24 -0
  54. package/dist/agents/guardrails.js +39 -0
  55. package/dist/agents/historyFit.d.ts +10 -0
  56. package/dist/agents/historyFit.js +26 -0
  57. package/dist/agents/hostContext.d.ts +54 -0
  58. package/dist/agents/hostContext.js +156 -0
  59. package/dist/agents/ledgers.d.ts +29 -0
  60. package/dist/agents/ledgers.js +31 -0
  61. package/dist/agents/lineDiff.d.ts +12 -0
  62. package/dist/agents/lineDiff.js +46 -0
  63. package/dist/agents/ndjson.d.ts +9 -0
  64. package/dist/agents/ndjson.js +47 -0
  65. package/dist/agents/otlpExport.d.ts +9 -0
  66. package/dist/agents/otlpExport.js +58 -0
  67. package/dist/agents/otlpSink.d.ts +7 -0
  68. package/dist/agents/otlpSink.js +23 -0
  69. package/dist/agents/policy.d.ts +31 -0
  70. package/dist/agents/policy.js +84 -0
  71. package/dist/agents/projectRoot.d.ts +5 -0
  72. package/dist/agents/projectRoot.js +15 -0
  73. package/dist/agents/runtime/piRuntime.d.ts +52 -0
  74. package/dist/agents/runtime/piRuntime.js +474 -0
  75. package/dist/agents/runtime/types.d.ts +77 -0
  76. package/dist/agents/runtime/types.js +26 -0
  77. package/dist/agents/sandbox.d.ts +17 -0
  78. package/dist/agents/sandbox.js +10 -0
  79. package/dist/agents/scheduleSource.d.ts +32 -0
  80. package/dist/agents/scheduleSource.js +100 -0
  81. package/dist/agents/service.d.ts +298 -0
  82. package/dist/agents/service.js +1105 -0
  83. package/dist/agents/sessions.d.ts +38 -0
  84. package/dist/agents/sessions.js +321 -0
  85. package/dist/agents/store.d.ts +277 -0
  86. package/dist/agents/store.js +643 -0
  87. package/dist/agents/trace.d.ts +27 -0
  88. package/dist/agents/trace.js +29 -0
  89. package/dist/agents/turnLifecycle.d.ts +72 -0
  90. package/dist/agents/turnLifecycle.js +157 -0
  91. package/dist/agents/webChat/publicChat.d.ts +36 -0
  92. package/dist/agents/webChat/publicChat.js +290 -0
  93. package/dist/agents/webChat/rateLimit.d.ts +15 -0
  94. package/dist/agents/webChat/rateLimit.js +33 -0
  95. package/dist/agents/webChat/slug.d.ts +2 -0
  96. package/dist/agents/webChat/slug.js +9 -0
  97. package/dist/channels/config.d.ts +11 -0
  98. package/dist/channels/config.js +95 -0
  99. package/dist/channels/configWriter.d.ts +40 -0
  100. package/dist/channels/configWriter.js +148 -0
  101. package/dist/channels/connectionEdits.d.ts +34 -0
  102. package/dist/channels/connectionEdits.js +53 -0
  103. package/dist/channels/errorInfo.d.ts +18 -0
  104. package/dist/channels/errorInfo.js +71 -0
  105. package/dist/channels/errors.d.ts +12 -0
  106. package/dist/channels/errors.js +13 -0
  107. package/dist/channels/health.d.ts +41 -0
  108. package/dist/channels/health.js +99 -0
  109. package/dist/channels/host.d.ts +55 -0
  110. package/dist/channels/host.js +185 -0
  111. package/dist/channels/http.d.ts +31 -0
  112. package/dist/channels/http.js +79 -0
  113. package/dist/channels/index.d.ts +10 -0
  114. package/dist/channels/index.js +9 -0
  115. package/dist/channels/ledger.d.ts +37 -0
  116. package/dist/channels/ledger.js +95 -0
  117. package/dist/channels/providers/agentmail.d.ts +26 -0
  118. package/dist/channels/providers/agentmail.js +94 -0
  119. package/dist/channels/providers/kit.d.ts +40 -0
  120. package/dist/channels/providers/kit.js +78 -0
  121. package/dist/channels/providers/photonImessage.d.ts +45 -0
  122. package/dist/channels/providers/photonImessage.js +188 -0
  123. package/dist/channels/providers/photonWhatsapp.d.ts +49 -0
  124. package/dist/channels/providers/photonWhatsapp.js +146 -0
  125. package/dist/channels/providers/slack.d.ts +25 -0
  126. package/dist/channels/providers/slack.js +102 -0
  127. package/dist/channels/providers/telegram.d.ts +21 -0
  128. package/dist/channels/providers/telegram.js +101 -0
  129. package/dist/channels/providers.d.ts +5 -0
  130. package/dist/channels/providers.js +22 -0
  131. package/dist/channels/registry.d.ts +54 -0
  132. package/dist/channels/registry.js +186 -0
  133. package/dist/channels/secretStore.d.ts +21 -0
  134. package/dist/channels/secretStore.js +75 -0
  135. package/dist/channels/secrets.d.ts +15 -0
  136. package/dist/channels/secrets.js +50 -0
  137. package/dist/channels/service.d.ts +54 -0
  138. package/dist/channels/service.js +307 -0
  139. package/dist/channels/stageChannels.d.ts +45 -0
  140. package/dist/channels/stageChannels.js +110 -0
  141. package/dist/channels/types.d.ts +169 -0
  142. package/dist/channels/types.js +1 -0
  143. package/dist/cli/agentsCommand.d.ts +14 -0
  144. package/dist/cli/agentsCommand.js +291 -0
  145. package/dist/cli/doctorCommand.js +3 -1
  146. package/dist/cli.js +9 -0
  147. package/dist/config/browseCatalog.js +4 -11
  148. package/dist/config/draftPackage.d.ts +2 -0
  149. package/dist/config/draftPackage.js +8 -0
  150. package/dist/config/hostConfig.js +8 -0
  151. package/dist/config/loadAgent.d.ts +266 -0
  152. package/dist/config/loadAgent.js +374 -0
  153. package/dist/config/loadPipeline.js +31 -4
  154. package/dist/config/loadStage.js +15 -0
  155. package/dist/config/mcpHostCatalog.d.ts +50 -0
  156. package/dist/config/mcpHostCatalog.js +405 -0
  157. package/dist/config/mcpOAuth.d.ts +37 -0
  158. package/dist/config/mcpOAuth.js +263 -0
  159. package/dist/config/mcpRedaction.d.ts +42 -0
  160. package/dist/config/mcpRedaction.js +314 -0
  161. package/dist/config/mcpToolCache.d.ts +20 -0
  162. package/dist/config/mcpToolCache.js +43 -0
  163. package/dist/config/mergePipelineIncludes.d.ts +5 -0
  164. package/dist/config/mergePipelineIncludes.js +52 -0
  165. package/dist/config/pipelineStageKeys.js +1 -0
  166. package/dist/config/resolveStageMcpServers.d.ts +35 -3
  167. package/dist/config/resolveStageMcpServers.js +124 -9
  168. package/dist/config/validateCatalog.d.ts +4 -1
  169. package/dist/config/validateCatalog.js +45 -1
  170. package/dist/errors/codes.d.ts +1 -1
  171. package/dist/errors/codes.js +1 -0
  172. package/dist/mcp/agentTools.d.ts +8 -0
  173. package/dist/mcp/agentTools.js +132 -0
  174. package/dist/mcp/catalogTools.js +7 -9
  175. package/dist/mcp/deps.d.ts +6 -0
  176. package/dist/mcp/pipelineAuthorSupport.d.ts +19 -0
  177. package/dist/mcp/pipelineAuthorSupport.js +10 -0
  178. package/dist/mcp/pipelineAuthorTools.d.ts +8 -0
  179. package/dist/mcp/pipelineAuthorTools.js +109 -0
  180. package/dist/mcp/server.js +5 -0
  181. package/dist/mcp/tools.js +4 -0
  182. package/dist/mcp/triggerTools.js +32 -0
  183. package/dist/memory/access.d.ts +36 -0
  184. package/dist/memory/access.js +116 -0
  185. package/dist/memory/agentIssues.d.ts +3 -0
  186. package/dist/memory/agentIssues.js +23 -0
  187. package/dist/memory/bashDeny.d.ts +6 -0
  188. package/dist/memory/bashDeny.js +35 -0
  189. package/dist/memory/host.d.ts +51 -0
  190. package/dist/memory/host.js +394 -0
  191. package/dist/memory/ids.d.ts +15 -0
  192. package/dist/memory/ids.js +31 -0
  193. package/dist/memory/worker.d.ts +1 -0
  194. package/dist/memory/worker.js +84 -0
  195. package/dist/models/catalog.d.ts +113 -0
  196. package/dist/models/catalog.js +238 -0
  197. package/dist/models/response.d.ts +29 -0
  198. package/dist/models/response.js +25 -0
  199. package/dist/operatorAgent/agentAuthorContext.d.ts +17 -0
  200. package/dist/operatorAgent/agentAuthorContext.js +32 -0
  201. package/dist/operatorAgent/fakeHost.js +2 -0
  202. package/dist/operatorAgent/host.js +6 -3
  203. package/dist/operatorAgent/index.d.ts +4 -0
  204. package/dist/operatorAgent/index.js +4 -0
  205. package/dist/operatorAgent/piModel.js +2 -0
  206. package/dist/operatorAgent/profiles/agentAuthor.d.ts +11 -0
  207. package/dist/operatorAgent/profiles/agentAuthor.js +192 -0
  208. package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -0
  209. package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +29 -0
  210. package/dist/operatorAgent/profiles/workshopAuthor.js +2 -2
  211. package/dist/operatorAgent/profiles/workshopAuthorTools.js +3 -3
  212. package/dist/operatorAgent/proposals.d.ts +1 -0
  213. package/dist/operatorAgent/proposals.js +23 -2
  214. package/dist/package-meta.d.ts +1 -1
  215. package/dist/package-meta.js +1 -1
  216. package/dist/preflight/channelChecks.d.ts +17 -0
  217. package/dist/preflight/channelChecks.js +106 -0
  218. package/dist/preflight/pipelinePreflight.d.ts +9 -2
  219. package/dist/preflight/pipelinePreflight.js +10 -1
  220. package/dist/runstore/createStore.d.ts +2 -0
  221. package/dist/runstore/createStore.js +8 -0
  222. package/dist/runstore/overviewSummary.d.ts +56 -0
  223. package/dist/runstore/overviewSummary.js +134 -0
  224. package/dist/runstore/port.d.ts +3 -0
  225. package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -0
  226. package/dist/runstore/sqlite/SqliteRunStore.js +18 -0
  227. package/dist/runstore/sqlite/migrations/013-agents.d.ts +12 -0
  228. package/dist/runstore/sqlite/migrations/013-agents.js +156 -0
  229. package/dist/runstore/sqlite/migrations/014-agent-deliveries.d.ts +11 -0
  230. package/dist/runstore/sqlite/migrations/014-agent-deliveries.js +32 -0
  231. package/dist/runstore/sqlite/migrations/index.js +4 -0
  232. package/dist/runtime/pipelineRunner.js +3 -1
  233. package/dist/runtime/repositoryMaterialize.d.ts +1 -1
  234. package/dist/runtime/runManager.js +4 -1
  235. package/dist/runtime/stageAttemptBootstrap.js +31 -1
  236. package/dist/runtime/stageEnvironment.d.ts +2 -0
  237. package/dist/runtime/stageEnvironment.js +6 -1
  238. package/dist/runtime/stageProcessLauncher.js +54 -1
  239. package/dist/runtime/stageSecrets.d.ts +6 -1
  240. package/dist/runtime/stageSecrets.js +34 -1
  241. package/dist/server/agentAuthorRoutes.d.ts +5 -0
  242. package/dist/server/agentAuthorRoutes.js +64 -0
  243. package/dist/server/agentRoutes.d.ts +5 -0
  244. package/dist/server/agentRoutes.js +323 -0
  245. package/dist/server/bootstrap.d.ts +11 -0
  246. package/dist/server/bootstrap.js +40 -10
  247. package/dist/server/channelRoutes.d.ts +9 -0
  248. package/dist/server/channelRoutes.js +88 -0
  249. package/dist/server/controlToken.js +8 -0
  250. package/dist/server/http.d.ts +8 -1
  251. package/dist/server/http.js +183 -20
  252. package/dist/server/mcpHost.js +1 -0
  253. package/dist/server/memoryRoutes.d.ts +5 -0
  254. package/dist/server/memoryRoutes.js +146 -0
  255. package/dist/server/projectMcpRoutes.d.ts +1 -0
  256. package/dist/server/projectMcpRoutes.js +241 -3
  257. package/dist/toolkit/guard.d.ts +27 -0
  258. package/dist/toolkit/guard.js +98 -0
  259. package/dist/toolkit/index.d.ts +48 -0
  260. package/dist/toolkit/index.js +82 -0
  261. package/dist/toolkit/meta.d.ts +36 -0
  262. package/dist/toolkit/meta.js +117 -0
  263. package/dist/toolkit/profiles.d.ts +13 -0
  264. package/dist/toolkit/profiles.js +25 -0
  265. package/dist/toolkit/record.d.ts +15 -0
  266. package/dist/toolkit/record.js +17 -0
  267. package/dist/tools/memoryTools.d.ts +95 -0
  268. package/dist/tools/memoryTools.js +179 -0
  269. package/dist/tools/sendMessage.d.ts +21 -0
  270. package/dist/tools/sendMessage.js +25 -0
  271. package/dist/types/pipeline.d.ts +7 -1
  272. package/dist/types/stage.d.ts +15 -0
  273. package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +9 -0
  274. package/dist/ui/assets/MarkdownText-D7tYaIFO.js +29 -0
  275. package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +6 -0
  276. package/dist/ui/assets/Tooltip-DrpoRRNi.js +1 -0
  277. package/dist/ui/assets/chat-BbLukq9W.js +5 -0
  278. package/dist/ui/assets/chat-zIZU_bP1.css +2 -0
  279. package/dist/ui/assets/jsx-runtime-iVoFAvQ_.js +1 -0
  280. package/dist/ui/assets/{index-CIPbbBUi.css → main-BFo79krC.css} +1 -1
  281. package/dist/ui/assets/main-CijYMeM0.js +259 -0
  282. package/dist/ui/chat.html +25 -0
  283. package/dist/ui/index.html +6 -2
  284. package/dist/workshop/modelSettings.d.ts +2 -2
  285. package/dist/workshop/modelSettings.js +5 -11
  286. package/examples/README.md +1 -0
  287. package/examples/channels-smoke/README.md +23 -0
  288. package/examples/channels-smoke/channels-smoke.pipeline.yaml +11 -0
  289. package/examples/channels-smoke/channels-smoke.task.yaml +3 -0
  290. package/examples/channels-smoke/send-email.yaml +32 -0
  291. package/examples/channels-smoke/send-text.yaml +31 -0
  292. package/examples/github-release/README.md +1 -1
  293. package/examples/memory/README.md +99 -0
  294. package/examples/memory/desk-outage.task.yaml +15 -0
  295. package/examples/memory/desk-refund.task.yaml +15 -0
  296. package/examples/memory/desk-revisit-outage.task.yaml +11 -0
  297. package/examples/memory/desk-revisit.pipeline.yaml +123 -0
  298. package/examples/memory/desk.pipeline.yaml +192 -0
  299. package/examples/memory/file-card.pipeline.yaml +39 -0
  300. package/examples/memory/file-ledger.task.yaml +22 -0
  301. package/examples/memory/file-release.task.yaml +24 -0
  302. package/examples/memory/file-rich.pipeline.yaml +40 -0
  303. package/examples/memory/file-ticket.task.yaml +7 -0
  304. package/examples/memory/file-timeline.task.yaml +28 -0
  305. package/examples/memory/find-card.pipeline.yaml +46 -0
  306. package/examples/memory/find-ticket.task.yaml +7 -0
  307. package/examples/memory/missing.pipeline.yaml +16 -0
  308. package/examples/memory/recall.pipeline.yaml +24 -0
  309. package/examples/memory/recall.task.yaml +5 -0
  310. package/examples/memory/remember-ada.task.yaml +6 -0
  311. package/examples/memory/remember-grace.task.yaml +6 -0
  312. package/examples/memory/remember.pipeline.yaml +58 -0
  313. package/examples/memory/stageflow.yaml +12 -0
  314. package/examples/memory/update-card.pipeline.yaml +43 -0
  315. package/examples/memory/update-ledger.task.yaml +23 -0
  316. package/examples/memory/update-rich.pipeline.yaml +44 -0
  317. package/examples/memory/update-ticket.task.yaml +7 -0
  318. package/examples/stage-mcp/README.md +1 -1
  319. package/package.json +9 -3
  320. package/dist/ui/assets/index-BoFPkKFg.js +0 -256
@@ -0,0 +1,77 @@
1
+ import { AgentConversation } from "./sessions.js";
2
+ /**
3
+ * Serializes turns per session (or per agent while a session is being
4
+ * opened) and keeps a handle on the running turn so it can be stopped.
5
+ */
6
+ export class AgentChatController {
7
+ options;
8
+ turnChains = new Map();
9
+ activeBySession = new Map();
10
+ activeByAgent = new Map();
11
+ /** Sessions whose turn was stopped: their runtime session is released once the turn has wound down. */
12
+ stopped = new Set();
13
+ constructor(options) {
14
+ this.options = options;
15
+ }
16
+ runExclusive(key, work) {
17
+ const previous = this.turnChains.get(key) ?? Promise.resolve();
18
+ const run = previous.then(work, work);
19
+ this.turnChains.set(key, run.then(() => undefined, () => undefined));
20
+ return run;
21
+ }
22
+ send(input) {
23
+ return this.runExclusive(input.sessionId ?? `agent:${input.agentId}`, async () => {
24
+ const chat = new AgentConversation(this.options);
25
+ if (input.sessionId)
26
+ this.activeBySession.set(input.sessionId, chat);
27
+ else
28
+ this.activeByAgent.set(input.agentId, chat);
29
+ let openedSessionId;
30
+ try {
31
+ return await chat.send({
32
+ ...input,
33
+ onSessionOpened: (sessionId) => {
34
+ openedSessionId = sessionId;
35
+ this.activeBySession.set(sessionId, chat);
36
+ input.onSessionOpened?.(sessionId);
37
+ },
38
+ });
39
+ }
40
+ finally {
41
+ const sessionId = input.sessionId ?? openedSessionId;
42
+ if (sessionId && this.stopped.delete(sessionId))
43
+ await this.release(sessionId);
44
+ if (sessionId && this.activeBySession.get(sessionId) === chat)
45
+ this.activeBySession.delete(sessionId);
46
+ if (this.activeByAgent.get(input.agentId) === chat)
47
+ this.activeByAgent.delete(input.agentId);
48
+ }
49
+ });
50
+ }
51
+ /** Stop the running turn on a session. Returns false when nothing was running. */
52
+ stop(sessionId) {
53
+ const chat = this.activeBySession.get(sessionId);
54
+ if (!chat)
55
+ return false;
56
+ this.stopped.add(sessionId);
57
+ chat.stop();
58
+ return true;
59
+ }
60
+ /** Let the runtime drop what it holds for a session (model session, isolated MCP processes). Never throws. */
61
+ async release(sessionId) {
62
+ try {
63
+ await this.options.backend.release?.(sessionId);
64
+ }
65
+ catch (error) {
66
+ this.options.logError?.(`releasing agent session ${sessionId} failed: ${error instanceof Error ? error.message : String(error)}`);
67
+ }
68
+ }
69
+ /** Stop a turn that is still opening its session (client left before the session id arrived). */
70
+ stopOpening(agentId) {
71
+ const chat = this.activeByAgent.get(agentId);
72
+ if (!chat)
73
+ return false;
74
+ chat.stop();
75
+ return true;
76
+ }
77
+ }
@@ -0,0 +1,19 @@
1
+ import type { AgentInboundMessage } from "./port.js";
2
+ export type AgentMailInboundConfig = {
3
+ connectionId: string;
4
+ webhookSecret: string;
5
+ };
6
+ export type AgentMailParseResult = {
7
+ ok: true;
8
+ message: AgentInboundMessage;
9
+ } | {
10
+ ok: false;
11
+ code: "agent.signature_invalid" | "agent.invalid";
12
+ };
13
+ export declare function verifyAgentMailSignature(config: AgentMailInboundConfig, signatureHeader: string | undefined, rawBody: string): {
14
+ ok: true;
15
+ } | {
16
+ ok: false;
17
+ code: "agent.signature_invalid";
18
+ };
19
+ export declare function parseAgentMailInbound(config: AgentMailInboundConfig, payload: unknown): AgentMailParseResult;
@@ -0,0 +1,46 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ export function verifyAgentMailSignature(config, signatureHeader, rawBody) {
3
+ if (signatureHeader === undefined)
4
+ return { ok: false, code: "agent.signature_invalid" };
5
+ const expected = createHmac("sha256", config.webhookSecret).update(rawBody).digest("hex");
6
+ const left = Buffer.from(expected);
7
+ const right = Buffer.from(signatureHeader);
8
+ if (left.length !== right.length || !timingSafeEqual(left, right)) {
9
+ return { ok: false, code: "agent.signature_invalid" };
10
+ }
11
+ return { ok: true };
12
+ }
13
+ export function parseAgentMailInbound(config, payload) {
14
+ if (typeof payload !== "object" || payload === null)
15
+ return { ok: false, code: "agent.invalid" };
16
+ const record = payload;
17
+ const eventId = typeof record.id === "string" ? record.id : undefined;
18
+ const threadId = typeof record.thread_id === "string"
19
+ ? record.thread_id
20
+ : typeof record.inbox_id === "string"
21
+ ? record.inbox_id
22
+ : undefined;
23
+ const text = typeof record.text === "string"
24
+ ? record.text.trim()
25
+ : typeof record.body === "string"
26
+ ? record.body.trim()
27
+ : "";
28
+ if (eventId === undefined || threadId === undefined || text.length === 0) {
29
+ return { ok: false, code: "agent.invalid" };
30
+ }
31
+ const from = typeof record.from === "string" ? record.from : undefined;
32
+ // Replies go to a message id (AgentMail's reply endpoint), never to the sender's address.
33
+ const replyTo = typeof record.message_id === "string" ? record.message_id : eventId;
34
+ return {
35
+ ok: true,
36
+ message: {
37
+ provider: "agentmail",
38
+ connectionId: config.connectionId,
39
+ eventId: `agentmail:${eventId}`,
40
+ threadId,
41
+ text,
42
+ replyTo,
43
+ ...(from !== undefined ? { from } : {}),
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,59 @@
1
+ import type { AgentInboundMessage } from "./port.js";
2
+ import { type ChannelListener, type ListenerDeps } from "./listener.js";
3
+ /** The slice of the AgentMail SDK socket this listener uses; tests supply a fake. */
4
+ export type AgentMailSocket = {
5
+ on(event: "open" | "message" | "close" | "error", callback: (payload?: any) => void): void;
6
+ sendSubscribe(message: {
7
+ type: "subscribe";
8
+ inboxIds: string[];
9
+ eventTypes: string[];
10
+ }): void;
11
+ waitForOpen(): Promise<unknown>;
12
+ close(): void;
13
+ };
14
+ export type AgentMailMessage = {
15
+ inboxId: string;
16
+ threadId: string;
17
+ messageId: string;
18
+ labels?: string[];
19
+ timestamp?: string | Date;
20
+ from: string;
21
+ subject?: string;
22
+ text?: string;
23
+ html?: string;
24
+ extractedText?: string;
25
+ headers?: Record<string, string>;
26
+ };
27
+ export type AgentMailTransport = {
28
+ connect(): Promise<AgentMailSocket>;
29
+ /** Mail that arrived while the Host was not listening. */
30
+ recent(inbox: string, since: Date): Promise<AgentMailMessage[]>;
31
+ };
32
+ export type AgentMailListenerConfig = {
33
+ connectionId: string;
34
+ inbox: string;
35
+ };
36
+ /** The bare address of "Name <a@b.com>", lowercased. */
37
+ export declare function addressOf(from: string): string;
38
+ /**
39
+ * Why an email must not get an answer, or undefined when it should. Keeps the agent out of mail loops:
40
+ * its own mail, bounces, automatic replies and no-reply senders.
41
+ */
42
+ export declare function ignoreReason(message: AgentMailMessage, inbox: string): string | undefined;
43
+ /** What the agent reads: the new text of the email (no quoted history), led by the subject on a first message. */
44
+ export declare function bodyOf(message: AgentMailMessage): string;
45
+ export declare function toInbound(connectionId: string, message: AgentMailMessage): AgentInboundMessage | undefined;
46
+ /** Minutes of missed mail answered after a reconnect or restart. */
47
+ export declare const CATCHUP_MINUTES = 15;
48
+ /**
49
+ * Holds an AgentMail WebSocket open for one inbox. A dropped connection is retried with backoff and, once back,
50
+ * mail that arrived meanwhile is answered too (deduped by message id). Only `message.received` events count:
51
+ * spam, blocked and unauthenticated (spoofed sender) mail uses other event types and never reaches an agent.
52
+ */
53
+ export declare function createAgentMailListener(config: AgentMailListenerConfig, transport: AgentMailTransport, deps: ListenerDeps): ChannelListener;
54
+ /** The real transport over the AgentMail SDK. The SDK loads on first use, so a Host without AgentMail never needs it. */
55
+ export declare function sdkAgentMailTransport(options: {
56
+ apiKey: string;
57
+ region: "us" | "eu";
58
+ inbox: string;
59
+ }): AgentMailTransport;
@@ -0,0 +1,183 @@
1
+ import { abortableSleep, reconnectDelay, StateTracker } from "./listener.js";
2
+ const NOREPLY_LOCALPART = /^(mailer-daemon|postmaster|no-?reply|do-?not-?reply|bounce[sd]?|notifications?)([+._-].*)?$/i;
3
+ /** The bare address of "Name <a@b.com>", lowercased. */
4
+ export function addressOf(from) {
5
+ const angle = /<([^<>]+)>/.exec(from);
6
+ return (angle ? angle[1] : from).trim().toLowerCase();
7
+ }
8
+ function stripHtml(html) {
9
+ return html
10
+ .replace(/<(script|style)[\s\S]*?<\/\1>/gi, "")
11
+ .replace(/<br\s*\/?>|<\/p>|<\/div>/gi, "\n")
12
+ .replace(/<[^>]+>/g, "")
13
+ .replace(/&nbsp;/g, " ")
14
+ .replace(/&amp;/g, "&")
15
+ .replace(/&lt;/g, "<")
16
+ .replace(/&gt;/g, ">")
17
+ .replace(/\n{3,}/g, "\n\n")
18
+ .trim();
19
+ }
20
+ /**
21
+ * Why an email must not get an answer, or undefined when it should. Keeps the agent out of mail loops:
22
+ * its own mail, bounces, automatic replies and no-reply senders.
23
+ */
24
+ export function ignoreReason(message, inbox) {
25
+ const labels = message.labels ?? [];
26
+ if (labels.includes("sent") || (labels.length > 0 && !labels.includes("received")))
27
+ return "not received mail";
28
+ const sender = addressOf(message.from);
29
+ if (sender === inbox.toLowerCase())
30
+ return "from the agent's own inbox";
31
+ const local = sender.split("@")[0] ?? "";
32
+ if (NOREPLY_LOCALPART.test(local))
33
+ return "from a no-reply or system sender";
34
+ const headers = Object.fromEntries(Object.entries(message.headers ?? {}).map(([key, value]) => [key.toLowerCase(), String(value).toLowerCase()]));
35
+ if (headers["auto-submitted"] !== undefined && headers["auto-submitted"] !== "no")
36
+ return "automatic reply";
37
+ if (headers["x-auto-response-suppress"] !== undefined)
38
+ return "automatic reply";
39
+ if (["bulk", "list", "junk"].includes(headers.precedence ?? ""))
40
+ return "bulk mail";
41
+ return undefined;
42
+ }
43
+ /** What the agent reads: the new text of the email (no quoted history), led by the subject on a first message. */
44
+ export function bodyOf(message) {
45
+ const body = (message.extractedText ?? message.text ?? (message.html ? stripHtml(message.html) : "")).trim();
46
+ const subject = message.subject?.trim();
47
+ if (body.length === 0)
48
+ return "";
49
+ return subject && !/^(re|fwd?):/i.test(subject) ? `Subject: ${subject}\n\n${body}` : body;
50
+ }
51
+ export function toInbound(connectionId, message) {
52
+ const text = bodyOf(message);
53
+ if (text.length === 0)
54
+ return undefined;
55
+ return {
56
+ provider: "agentmail",
57
+ connectionId,
58
+ // The message id (not the event id) so the live stream and the catch-up list dedupe against each other.
59
+ eventId: `agentmail:${message.messageId}`,
60
+ threadId: message.threadId,
61
+ text,
62
+ from: message.from,
63
+ replyTo: message.messageId,
64
+ };
65
+ }
66
+ /** Minutes of missed mail answered after a reconnect or restart. */
67
+ export const CATCHUP_MINUTES = 15;
68
+ /**
69
+ * Holds an AgentMail WebSocket open for one inbox. A dropped connection is retried with backoff and, once back,
70
+ * mail that arrived meanwhile is answered too (deduped by message id). Only `message.received` events count:
71
+ * spam, blocked and unauthenticated (spoofed sender) mail uses other event types and never reaches an agent.
72
+ */
73
+ export function createAgentMailListener(config, transport, deps) {
74
+ const now = deps.now ?? Date.now;
75
+ const sleep = deps.sleep ?? abortableSleep;
76
+ const tracker = new StateTracker(now);
77
+ const abort = new AbortController();
78
+ let loop;
79
+ let socket;
80
+ async function handle(message) {
81
+ const reason = ignoreReason(message, config.inbox);
82
+ if (reason) {
83
+ deps.log(`agentmail ${config.connectionId}: ignored a message (${reason})`);
84
+ return;
85
+ }
86
+ const inbound = toInbound(config.connectionId, message);
87
+ if (!inbound)
88
+ return;
89
+ tracker.received();
90
+ await deps.onMessage(inbound).catch((error) => deps.log(`agentmail ${config.connectionId}: delivery failed: ${error instanceof Error ? error.message : String(error)}`));
91
+ }
92
+ async function session() {
93
+ const opened = await transport.connect();
94
+ socket = opened;
95
+ const closed = new Promise((resolve) => {
96
+ opened.on("close", () => resolve(undefined));
97
+ opened.on("error", (error) => resolve(error instanceof Error ? error.message : "connection error"));
98
+ });
99
+ opened.on("message", (payload) => {
100
+ if (payload?.type === "event" && payload.eventType === "message.received" && payload.message && payload.message.inboxId === config.inbox)
101
+ void handle(payload.message);
102
+ });
103
+ await opened.waitForOpen();
104
+ opened.sendSubscribe({ type: "subscribe", inboxIds: [config.inbox], eventTypes: ["message.received"] });
105
+ tracker.set("connected");
106
+ const missed = await transport.recent(config.inbox, new Date(now() - CATCHUP_MINUTES * 60_000)).catch(() => []);
107
+ for (const message of missed)
108
+ await handle(message);
109
+ const reason = await Promise.race([closed, new Promise((resolve) => abort.signal.addEventListener("abort", () => resolve(undefined), { once: true }))]);
110
+ if (!abort.signal.aborted)
111
+ throw new Error(reason ?? "connection closed");
112
+ }
113
+ async function run() {
114
+ let attempt = 0;
115
+ while (!abort.signal.aborted) {
116
+ try {
117
+ await session();
118
+ attempt = 0;
119
+ }
120
+ catch (error) {
121
+ if (abort.signal.aborted)
122
+ break;
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ tracker.set(attempt === 0 ? "reconnecting" : "failed", message);
125
+ deps.log(`agentmail ${config.connectionId}: ${message}; retrying`);
126
+ attempt += 1;
127
+ await sleep(reconnectDelay(attempt - 1), abort.signal);
128
+ }
129
+ finally {
130
+ try {
131
+ socket?.close();
132
+ }
133
+ catch {
134
+ // already closed
135
+ }
136
+ socket = undefined;
137
+ }
138
+ }
139
+ tracker.set("stopped");
140
+ }
141
+ return {
142
+ async start() {
143
+ loop = run();
144
+ await Promise.resolve();
145
+ },
146
+ async stop() {
147
+ abort.abort();
148
+ try {
149
+ socket?.close();
150
+ }
151
+ catch {
152
+ // already closed
153
+ }
154
+ await loop;
155
+ },
156
+ state: () => tracker.get(),
157
+ };
158
+ }
159
+ /** The real transport over the AgentMail SDK. The SDK loads on first use, so a Host without AgentMail never needs it. */
160
+ export function sdkAgentMailTransport(options) {
161
+ let client;
162
+ const load = async () => {
163
+ if (client)
164
+ return client;
165
+ const sdk = await import("agentmail");
166
+ client = new sdk.AgentMailClient({ apiKey: options.apiKey, environment: options.region === "eu" ? sdk.AgentMailEnvironment.EuProd : sdk.AgentMailEnvironment.Prod });
167
+ return client;
168
+ };
169
+ return {
170
+ async connect() {
171
+ const sdkClient = await load();
172
+ const socket = await sdkClient.websockets.connect({ reconnectAttempts: 0 });
173
+ return socket;
174
+ },
175
+ async recent(inbox, since) {
176
+ const sdkClient = await load();
177
+ const page = await sdkClient.inboxes.messages.list(inbox, { after: since, labels: ["received"], limit: 50, ascending: true });
178
+ // The list carries previews only; the agent needs the body.
179
+ const full = await Promise.all((page.messages ?? []).map((item) => sdkClient.inboxes.messages.get(inbox, item.messageId)));
180
+ return full;
181
+ },
182
+ };
183
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Who may message an agent over its channel. Entries are an email address, a phone number
3
+ * (E.164), `@domain` for everyone at a domain, a Slack user id (`U02ABC123`), a Telegram `@username`,
4
+ * or `*` for anyone. An empty list lets nobody in.
5
+ */
6
+ export declare const SENDER_ENTRY: RegExp;
7
+ /** "Jane Doe <Jane@X.com>" -> "jane@x.com"; phone numbers lose spaces, dashes and brackets. */
8
+ export declare function normalizeSender(raw: string): string;
9
+ export declare function normalizeEntry(entry: string): string;
10
+ export declare function senderAllowed(allowFrom: readonly string[], sender: string | undefined): boolean;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Who may message an agent over its channel. Entries are an email address, a phone number
3
+ * (E.164), `@domain` for everyone at a domain, a Slack user id (`U02ABC123`), a Telegram `@username`,
4
+ * or `*` for anyone. An empty list lets nobody in.
5
+ */
6
+ export const SENDER_ENTRY = /^(\*|@[^\s@]+\.[^\s@]+|[^\s@]+@[^\s@]+\.[^\s@]+|\+[1-9]\d{6,14}|[UW][A-Z0-9]{7,}|@[A-Za-z][A-Za-z0-9_]{4,31})$/;
7
+ /** "Jane Doe <Jane@X.com>" -> "jane@x.com"; phone numbers lose spaces, dashes and brackets. */
8
+ export function normalizeSender(raw) {
9
+ const angle = /<([^<>]+)>/.exec(raw);
10
+ const value = (angle ? angle[1] : raw).trim();
11
+ if (/^[+\d][\d\s().-]*$/.test(value))
12
+ return value.replace(/[\s().-]/g, "");
13
+ return value.toLowerCase();
14
+ }
15
+ export function normalizeEntry(entry) {
16
+ return entry === "*" ? entry : normalizeSender(entry);
17
+ }
18
+ export function senderAllowed(allowFrom, sender) {
19
+ if (allowFrom.length === 0)
20
+ return false;
21
+ if (allowFrom.includes("*"))
22
+ return true;
23
+ if (sender === undefined)
24
+ return false;
25
+ const who = normalizeSender(sender);
26
+ const domain = who.includes("@") ? who.slice(who.lastIndexOf("@")) : undefined;
27
+ return allowFrom.some((entry) => {
28
+ const allowed = normalizeEntry(entry);
29
+ if (allowed === who)
30
+ return true;
31
+ if (!allowed.startsWith("@"))
32
+ return false;
33
+ // `@domain` matches everyone at it; a dotless `@name` is a Telegram username (senders arrive without the `@`).
34
+ return allowed.includes(".") ? domain === allowed : who === allowed.slice(1);
35
+ });
36
+ }
@@ -0,0 +1,3 @@
1
+ import type { InboundConnections } from "./inbound.js";
2
+ /** Inbound lookups and replies over the Host's connections (`connections.yaml`). */
3
+ export declare function channelInboundConnections(cwd: string): InboundConnections;
@@ -0,0 +1,51 @@
1
+ import { createHash } from "node:crypto";
2
+ import { channelHostFor } from "../../channels/host.js";
3
+ const hashOf = (parts) => createHash("sha256").update(JSON.stringify(parts)).digest("hex");
4
+ /** Inbound lookups and replies over the Host's connections (`connections.yaml`). */
5
+ export function channelInboundConnections(cwd) {
6
+ return {
7
+ find(connectionId) {
8
+ try {
9
+ const host = channelHostFor(cwd);
10
+ const connection = host.registry.resolve(connectionId);
11
+ return { provider: connection.provider, secret: connection.secrets.inboundSecret };
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ },
17
+ async reply(connectionId, to, text, operationKey) {
18
+ const host = channelHostFor(cwd);
19
+ const provider = host.registry.providerFor(connectionId);
20
+ const connection = host.registry.resolve(connectionId);
21
+ const message = { text };
22
+ // A provider that can answer inside the conversation does; otherwise the reply is a new message to the sender.
23
+ const submit = provider.reply
24
+ ? () => provider.reply(connection, to, message, { operationKey })
25
+ : () => provider.send(connection, host.registry.validateTarget(connectionId, { to }), message, { operationKey });
26
+ await host.sends.send({ connectionId, operationKey, hash: hashOf([connectionId, to, message]) }, submit);
27
+ },
28
+ async contact(connectionId) {
29
+ try {
30
+ const host = channelHostFor(cwd);
31
+ const provider = host.registry.providerFor(connectionId);
32
+ if (!provider.contact)
33
+ return undefined;
34
+ return await provider.contact(host.registry.resolve(connectionId, false));
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ },
40
+ resolve(connectionId) {
41
+ try {
42
+ const host = channelHostFor(cwd);
43
+ const connection = host.registry.resolve(connectionId);
44
+ return { id: connection.id, provider: connection.provider, config: connection.config, secrets: connection.secrets, credential: connection.credential ? { secrets: connection.credential.secrets } : undefined };
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,53 @@
1
+ import type { DeliveryInput } from "../ledgers.js";
2
+ import type { AgentService } from "../service.js";
3
+ import type { AgentDelivery } from "../store.js";
4
+ /** Waits before each automatic retry of a reply that definitely did not go out. */
5
+ export declare const RETRY_BACKOFF_MS: number[];
6
+ export type { DeliveryInput };
7
+ type Reply = (connectionId: string, to: string, text: string, operationKey: string) => Promise<void>;
8
+ type Outcome = {
9
+ kind: "failed" | "unknown";
10
+ retryable: boolean;
11
+ code: string;
12
+ message: string;
13
+ };
14
+ /** Sorts a send failure into "never left" (safe to retry when transient) and "may have arrived" (never resent on its own). */
15
+ export declare function classifySendFailure(error: unknown): Outcome;
16
+ /**
17
+ * Gets an agent's reply to the person on the channel they wrote from. The turn only counts as succeeded once the
18
+ * reply is delivered: a definite failure is retried with backoff when it may be transient; an unknown outcome is never
19
+ * resent automatically (it could arrive twice); anything left over is a failed turn a person can resend or mark delivered.
20
+ */
21
+ export declare class ReplyDelivery {
22
+ private readonly options;
23
+ private readonly timers;
24
+ private sweeping;
25
+ constructor(options: {
26
+ service: Pick<AgentService, "deliveries" | "settleInterruptedDeliveries" | "setReplyDelivered" | "clockMs">;
27
+ reply: Reply;
28
+ log?: (message: string) => void;
29
+ backoffMs?: number[];
30
+ });
31
+ private get backoff();
32
+ /** Record the reply and make the first attempt. Resolves with the delivery's state after that attempt. */
33
+ deliver(input: DeliveryInput): Promise<AgentDelivery>;
34
+ /**
35
+ * One send attempt. The right to send is claimed first, atomically (`pending`/`failed`/`unknown` -> `sending`), and
36
+ * the row is what is trusted, not any list the caller read earlier: an attempt that finds the delivery already
37
+ * claimed, sent or no longer due does nothing and returns it as it is. Overlapping sweeps and timers therefore
38
+ * cannot send the same reply twice.
39
+ */
40
+ private attempt;
41
+ private later;
42
+ /** A timer-driven sweep has no caller to report to; a failure (a store closing mid-sweep) must not escape as an unhandled rejection. */
43
+ private sweepInBackground;
44
+ /** Retry what is due. Also run on a timer, so retries survive a restart. */
45
+ sweep(): Promise<number>;
46
+ /** On start: sends a restart cut off are now unknown; retries that were waiting are picked up by the sweep. */
47
+ start(intervalMs?: number): void;
48
+ stop(): void;
49
+ /** A person resends a reply that failed or whose outcome was unknown. One attempt; no automatic retries after it. */
50
+ resend(turnId: string): Promise<AgentDelivery>;
51
+ /** A person confirms the reply did arrive (for example after an unknown outcome). */
52
+ markDelivered(turnId: string): AgentDelivery;
53
+ }